Compare commits

...

3 Commits

Author SHA1 Message Date
0xtoshii d2d74bde4a changeset 2025-04-29 00:17:50 -07:00
0xtoshii f4d67ff587 words 2025-04-29 00:16:25 -07:00
0xtoshii 258739c7ef base 2025-04-29 00:03:49 -07:00
8 changed files with 172 additions and 14 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
add newrule slash command
+1
View File
@@ -25,6 +25,7 @@ export const toolUseNames = [
"attempt_completion",
"new_task",
"condense",
"new_rule",
] as const
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)
@@ -55,7 +55,10 @@ export function parseAssistantMessage(assistantMessage: string) {
// special case for write_to_file where file contents could contain the closing tag, in which case the param would have closed and we end up with the rest of the file contents here. To work around this, we get the string between the starting content tag and the LAST content tag.
const contentParamName: ToolParamName = "content"
if (currentToolUse.name === "write_to_file" && accumulator.endsWith(`</${contentParamName}>`)) {
if (
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
accumulator.endsWith(`</${contentParamName}>`)
) {
const toolContent = accumulator.slice(currentToolUseStartIndex)
const contentStartTag = `<${contentParamName}>`
const contentEndTag = `</${contentParamName}>`
@@ -7,6 +7,45 @@ import { ClineRulesToggles } from "@shared/cline-rules"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
import * as vscode from "vscode"
/**
* Converts .clinerules file to directory and places old .clinerule file inside directory, renaming it
* Doesn't do anything if .clinerules dir already exists or doesn't exist
* Returns whether there are any uncaught errors
*/
export async function ensureLocalClinerulesDirExists(cwd: string): Promise<boolean> {
const clinerulePath = path.resolve(cwd, GlobalFileNames.clineRules)
const defaultRuleFilename = "default-rules.md"
try {
const exists = await fileExistsAtPath(clinerulePath)
if (exists && !(await isDirectory(clinerulePath))) {
// logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
const content = await fs.readFile(clinerulePath, "utf8")
const tempPath = clinerulePath + ".bak"
await fs.rename(clinerulePath, tempPath) // create backup
try {
await fs.mkdir(clinerulePath, { recursive: true })
await fs.writeFile(path.join(clinerulePath, defaultRuleFilename), content, "utf8")
await fs.unlink(tempPath).catch(() => {}) // delete backup
return false // conversion successful with no errors
} catch (conversionError) {
// attempt to restore backup on conversion failure
try {
await fs.rm(clinerulePath, { recursive: true, force: true }).catch(() => {})
await fs.rename(tempPath, clinerulePath) // restore backup
} catch (restoreError) {}
return true // in either case here we consider this an error
}
}
// exists and is a dir or doesn't exist, either of these cases we dont need to handle here
return false
} catch (error) {
return true
}
}
export const getGlobalClineRules = async (globalClineRulesFilePath: string, toggles: ClineRulesToggles) => {
if (await fileExistsAtPath(globalClineRulesFilePath)) {
if (await isDirectory(globalClineRulesFilePath)) {
+58
View File
@@ -87,3 +87,61 @@ Example:
</explicit_instructions>\n
`
export const newRuleToolResponse = () =>
`<explicit_instructions type="new_rule">
The user has explicitly asked you to help them create a new Cline rule file inside the .clinerules top-level directory based on the conversation up to this point in time. The user may have provided instructions or additional information for you to consider when creating the new Cline rule.
When creating a new Cline rule file, you should NOT overwrite or alter an existing Cline rule file. To create the Cline rule file you MUST use the new_rule tool. The new_rule tool can be used in either of the PLAN or ACT modes.
The new_rule tool is defined below:
Description:
Your task is to create a new Cline rule file which includes guidelines on how to approach developing code in tandem with the user, which can be either project specific or cover more global rules. This includes but is not limited to: desired conversational style, favorite project dependencies, coding styles, naming conventions, architectural choices, ui/ux preferences, etc.
The Cline rule file must be formatted as markdown and be a '.md' file. The name of the file you generate must be as succinct as possible and be encompassing the main overarching concept of the rules you added to the file (e.g., 'memory-bank.md' or 'project-overview.md').
Parameters:
- Path: (required) The path of the file to write to (relative to the current working directory). This will be the Cline rule file you create, and it must be placed inside the .clinerules top-level directory (create this if it doesn't exist). The filename created CANNOT be "default-clineignore.md". For filenames, use hyphens ("-") instead of underscores ("_") to separate words.
- Content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. The content for the Cline rule file MUST be created according to the following instructions:
1. Format the Cline rule file to have distinct guideline sections, each with their own markdown heading, starting with "## Brief overview". Under each of these headings, include bullet points fully fleshing out the details, with examples and/or trigger cases ONLY when applicable.
2. These guidelines can be specific to the task(s) or project worked on thus far, or cover more high-level concepts. Guidelines can include coding conventions, general design patterns, preferred tech stack including favorite libraries and language, communication style with Cline (verbose vs concise), prompting strategies, naming conventions, testing strategies, comment verbosity, time spent on architecting prior to development, and other preferences.
3. When creating guidelines, you should not invent preferences or make assumptions based on what you think a typical user might want. These should be specific to the conversation you had with the user. Your guidelines / rules should not be overly verbose.
4. Your guidelines should NOT be a recollection of the conversation up to this point in time, meaning you should NOT be including arbitrary details of the conversation.
Usage:
<new_rule>
<path>.clinerules/{file name}.md</path>
<content>Cline rule file content here</content>
</new_rule>
Example:
<new_rule>
<path>.clinerules/project-preferences.md</path>
<content>
## Brief overview
[Brief description of the rules, including if this set of guidelines is project-specific or global]
## Communication style
- [Description, rule, preference, instruction]
- [...]
## Development workflow
- [Description, rule, preference, instruction]
- [...]
## Coding best practices
- [Description, rule, preference, instruction]
- [...]
## Project context
- [Description, rule, preference, instruction]
- [...]
## Other guidelines
- [Description, rule, preference, instruction]
- [...]
</content>
</new_rule>
Below is the user's input when they indicated that they wanted to create a new Cline rule file.
</explicit_instructions>\n
`
+6 -5
View File
@@ -1,16 +1,17 @@
import { newTaskToolResponse, condenseToolResponse } from "../prompts/commands"
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse } from "../prompts/commands"
/**
* Processes text for slash commands and transforms them with appropriate instructions
* This is called after parseMentions() to process any slash commands in the user's message
*/
export function parseSlashCommands(text: string): string {
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact"]
export function parseSlashCommands(text: string): { processedText: string; needsClinerulesFileCheck: boolean } {
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule"]
const commandReplacements: Record<string, string> = {
newtask: newTaskToolResponse(),
smol: condenseToolResponse(),
compact: condenseToolResponse(),
newrule: newRuleToolResponse(),
}
// this currently allows matching prepended whitespace prior to /slash-command
@@ -47,11 +48,11 @@ export function parseSlashCommands(text: string): string {
const textWithoutSlashCommand = text.substring(0, slashCommandStartIndex) + text.substring(slashCommandEndIndex)
const processedText = commandReplacements[commandName] + textWithoutSlashCommand
return processedText
return { processedText: processedText, needsClinerulesFileCheck: commandName === "newrule" ? true : false }
}
}
}
// if no supported commands are found, return the original text
return text
return { processedText: text, needsClinerulesFileCheck: false }
}
+55 -8
View File
@@ -86,6 +86,7 @@ import {
getGlobalClineRules,
getLocalClineRules,
refreshClineRulesToggles,
ensureLocalClinerulesDirExists,
} from "@core/context/instructions/user-instructions/cline-rules"
import { getGlobalState } from "@core/storage/state"
import { parseSlashCommands } from "@core/slash-commands"
@@ -1361,6 +1362,7 @@ export class Task {
this.autoApprovalSettings.actions.readFiles,
this.autoApprovalSettings.actions.readFilesExternally ?? false,
]
case "new_rule":
case "write_to_file":
case "replace_in_file":
return [
@@ -1686,6 +1688,8 @@ export class Task {
return `[${block.name} for creating a new task]`
case "condense":
return `[${block.name}]`
case "new_rule":
return `[${block.name} for '${block.params.path}']`
}
}
@@ -1825,6 +1829,7 @@ export class Task {
}
switch (block.name) {
case "new_rule":
case "write_to_file":
case "replace_in_file": {
const relPath: string | undefined = block.params.path
@@ -1970,6 +1975,13 @@ export class Task {
break
}
if (block.name === "new_rule" && !content) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("new_rule", "content"))
await this.diffViewProvider.reset()
break
}
this.consecutiveMistakeCount = 0
@@ -3482,7 +3494,16 @@ export class Task {
}
}
const [parsedUserContent, environmentDetails] = await this.loadContext(userContent, includeFileDetails)
const [parsedUserContent, environmentDetails, clinerulesError] = await this.loadContext(userContent, includeFileDetails)
// error handling if the user uses the /newrule command & their .clinerules is a file, for file read operations didnt work properly
if (clinerulesError === true) {
await this.say(
"error",
"Issue with processing the /newrule command. Double check that, if '.clinerules' already exists, it's a directory and not a file. Otherwise there was an issue referencing this file/directory.",
)
}
userContent = parsedUserContent
// add environment details as its own text block, separate from tool results
userContent.push({ type: "text", text: environmentDetails })
@@ -3767,11 +3788,14 @@ export class Task {
}
}
async loadContext(userContent: UserContent, includeFileDetails: boolean = false) {
return await Promise.all([
async loadContext(userContent: UserContent, includeFileDetails: boolean = false): Promise<[UserContent, string, boolean]> {
// Track if we need to check clinerulesFile
let needsClinerulesFileCheck = false
const processUserContent = async () => {
// This is a temporary solution to dynamically load context mentions from tool results. It checks for the presence of tags that indicate that the tool was rejected and feedback was provided (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions). However if we allow multiple tools responses in the future, we will need to parse mentions specifically within the user content tags.
// (Note: this caused the @/ import alias bug where file contents were being parsed as well, since v2 converted tool results to text blocks)
Promise.all(
return await Promise.all(
userContent.map(async (block) => {
if (block.type === "text") {
// We need to ensure any user generated content is wrapped in one of these tags so that we know to parse mentions
@@ -3782,22 +3806,45 @@ export class Task {
block.text.includes("<task>") ||
block.text.includes("<user_message>")
) {
let parsedText = await parseMentions(block.text, cwd, this.urlContentFetcher, this.fileContextTracker)
const parsedText = await parseMentions(
block.text,
cwd,
this.urlContentFetcher,
this.fileContextTracker,
)
// when parsing slash commands, we still want to allow the user to provide their desired context
parsedText = parseSlashCommands(parsedText)
const { processedText, needsClinerulesFileCheck: needsCheck } = parseSlashCommands(parsedText)
if (needsCheck) {
needsClinerulesFileCheck = true
}
return {
...block,
text: parsedText,
text: processedText,
}
}
}
return block
}),
),
)
}
// Run initial promises in parallel
const [processedUserContent, environmentDetails] = await Promise.all([
processUserContent(),
this.getEnvironmentDetails(includeFileDetails),
])
// After processing content, check clinerulesData if needed
let clinerulesError = false
if (needsClinerulesFileCheck) {
clinerulesError = await ensureLocalClinerulesDirExists(cwd)
}
// Return all results
return [processedUserContent, environmentDetails, clinerulesError]
}
async getEnvironmentDetails(includeFileDetails: boolean = false) {
+4
View File
@@ -12,6 +12,10 @@ export const SUPPORTED_SLASH_COMMANDS: SlashCommand[] = [
name: "smol",
description: "Condenses your current context window",
},
{
name: "newrule",
description: "Create a new Cline rule based on your conversation",
},
]
// Regex for detecting slash commands in text