@jmylchreest/aide-plugin 0.0.22 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/config.ts +1 -1
- package/src/cli/index.ts +6 -0
- package/src/cli/install.ts +43 -2
- package/src/cli/mcp.ts +63 -0
- package/src/core/comment-checker.ts +354 -0
- package/src/core/persistence-logic.ts +50 -13
- package/src/core/todo-checker.ts +138 -0
- package/src/core/write-guard.ts +105 -0
- package/src/opencode/hooks.ts +53 -2
- package/src/opencode/index.ts +1 -1
package/package.json
CHANGED
package/src/cli/config.ts
CHANGED
|
@@ -95,7 +95,7 @@ export function addAideToConfig(
|
|
|
95
95
|
if (!mcp[MCP_SERVER_NAME]) {
|
|
96
96
|
mcp[MCP_SERVER_NAME] = {
|
|
97
97
|
type: "local",
|
|
98
|
-
command: ["npx", "-y",
|
|
98
|
+
command: ["npx", "-y", PLUGIN_NAME, "mcp"],
|
|
99
99
|
environment: {
|
|
100
100
|
AIDE_CODE_WATCH: "1",
|
|
101
101
|
AIDE_CODE_WATCH_DELAY: "30s",
|
package/src/cli/index.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* bunx @jmylchreest/aide-plugin install # Install globally for OpenCode
|
|
7
7
|
* bunx @jmylchreest/aide-plugin uninstall # Remove from OpenCode config
|
|
8
8
|
* bunx @jmylchreest/aide-plugin status # Show current installation status
|
|
9
|
+
* bunx @jmylchreest/aide-plugin mcp # Start MCP server (used by OpenCode)
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { install } from "./install.js";
|
|
12
13
|
import { uninstall } from "./uninstall.js";
|
|
13
14
|
import { status } from "./status.js";
|
|
15
|
+
import { mcp } from "./mcp.js";
|
|
14
16
|
|
|
15
17
|
const args = process.argv.slice(2);
|
|
16
18
|
const command = args[0];
|
|
@@ -22,6 +24,7 @@ Usage:
|
|
|
22
24
|
aide-plugin install Install aide plugin globally for OpenCode
|
|
23
25
|
aide-plugin uninstall Remove aide plugin from OpenCode config
|
|
24
26
|
aide-plugin status Show current installation status
|
|
27
|
+
aide-plugin mcp Start MCP server (delegates to aide-wrapper)
|
|
25
28
|
aide-plugin --help Show this help message
|
|
26
29
|
|
|
27
30
|
Options:
|
|
@@ -49,6 +52,9 @@ async function main(): Promise<void> {
|
|
|
49
52
|
case "status":
|
|
50
53
|
await status();
|
|
51
54
|
break;
|
|
55
|
+
case "mcp":
|
|
56
|
+
await mcp(args.slice(1));
|
|
57
|
+
break;
|
|
52
58
|
case "--help":
|
|
53
59
|
case "-h":
|
|
54
60
|
case "help":
|
package/src/cli/install.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Install command — registers aide plugin and MCP server in OpenCode config.
|
|
3
|
+
*
|
|
4
|
+
* On reinstall, detects and upgrades stale MCP command configurations
|
|
5
|
+
* (e.g. old `aide-wrapper` commands) to the current format.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
import {
|
|
@@ -10,6 +13,7 @@ import {
|
|
|
10
13
|
readConfig,
|
|
11
14
|
writeConfig,
|
|
12
15
|
PLUGIN_NAME,
|
|
16
|
+
MCP_SERVER_NAME,
|
|
13
17
|
} from "./config.js";
|
|
14
18
|
|
|
15
19
|
export interface InstallFlags {
|
|
@@ -17,6 +21,32 @@ export interface InstallFlags {
|
|
|
17
21
|
noMcp?: boolean;
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Check if an existing MCP config has the current expected command format.
|
|
26
|
+
* Returns false if the command is missing, empty, or uses an outdated format.
|
|
27
|
+
*/
|
|
28
|
+
function isMcpCommandCurrent(config: ReturnType<typeof readConfig>): boolean {
|
|
29
|
+
const mcpConfig = config.mcp?.[MCP_SERVER_NAME];
|
|
30
|
+
if (!mcpConfig?.command || mcpConfig.command.length === 0) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cmd = mcpConfig.command;
|
|
35
|
+
|
|
36
|
+
// Current format: ["npx", "-y", "@jmylchreest/aide-plugin", "mcp"]
|
|
37
|
+
if (
|
|
38
|
+
cmd.length === 4 &&
|
|
39
|
+
cmd[0] === "npx" &&
|
|
40
|
+
cmd[1] === "-y" &&
|
|
41
|
+
cmd[2] === PLUGIN_NAME &&
|
|
42
|
+
cmd[3] === "mcp"
|
|
43
|
+
) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
20
50
|
export async function install(flags: InstallFlags): Promise<void> {
|
|
21
51
|
const configPath = flags.project
|
|
22
52
|
? getProjectConfigPath()
|
|
@@ -29,7 +59,11 @@ export async function install(flags: InstallFlags): Promise<void> {
|
|
|
29
59
|
const existing = readConfig(configPath);
|
|
30
60
|
const before = isAideConfigured(existing);
|
|
31
61
|
|
|
32
|
-
if (
|
|
62
|
+
// Check if MCP command needs updating (stale format from older versions)
|
|
63
|
+
const mcpNeedsUpdate =
|
|
64
|
+
!flags.noMcp && before.mcp && !isMcpCommandCurrent(existing);
|
|
65
|
+
|
|
66
|
+
if (before.plugin && before.mcp && !mcpNeedsUpdate) {
|
|
33
67
|
console.log(`aide is already configured in ${configPath}`);
|
|
34
68
|
console.log(" plugin: registered");
|
|
35
69
|
console.log(" mcp: registered");
|
|
@@ -37,6 +71,11 @@ export async function install(flags: InstallFlags): Promise<void> {
|
|
|
37
71
|
return;
|
|
38
72
|
}
|
|
39
73
|
|
|
74
|
+
// If MCP config is stale, remove it so addAideToConfig will write the current version
|
|
75
|
+
if (mcpNeedsUpdate && existing.mcp) {
|
|
76
|
+
delete existing.mcp[MCP_SERVER_NAME];
|
|
77
|
+
}
|
|
78
|
+
|
|
40
79
|
// Apply changes
|
|
41
80
|
const updated = addAideToConfig(existing, { noMcp: flags.noMcp });
|
|
42
81
|
writeConfig(configPath, updated);
|
|
@@ -52,7 +91,9 @@ export async function install(flags: InstallFlags): Promise<void> {
|
|
|
52
91
|
}
|
|
53
92
|
|
|
54
93
|
if (!flags.noMcp) {
|
|
55
|
-
if (
|
|
94
|
+
if (mcpNeedsUpdate) {
|
|
95
|
+
console.log(` ~ Updated "aide" MCP server command (was outdated)`);
|
|
96
|
+
} else if (!before.mcp && after.mcp) {
|
|
56
97
|
console.log(` + Added "aide" MCP server`);
|
|
57
98
|
} else if (before.mcp) {
|
|
58
99
|
console.log(` = MCP server already registered`);
|
package/src/cli/mcp.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP subcommand — delegates to aide-wrapper.sh to start the MCP server.
|
|
3
|
+
*
|
|
4
|
+
* This is the entry point used by OpenCode's MCP config:
|
|
5
|
+
* "command": ["npx", "-y", "@jmylchreest/aide-plugin", "mcp"]
|
|
6
|
+
*
|
|
7
|
+
* The wrapper handles binary discovery/download, then exec's `aide mcp`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFileSync } from "child_process";
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
import { dirname, join, resolve } from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Find the aide-wrapper.sh script relative to this CLI module.
|
|
17
|
+
*
|
|
18
|
+
* Resolution: this file lives at <plugin-root>/src/cli/mcp.ts,
|
|
19
|
+
* so the wrapper is at <plugin-root>/bin/aide-wrapper.sh.
|
|
20
|
+
*/
|
|
21
|
+
function findWrapper(): string {
|
|
22
|
+
// __dirname equivalent for ESM
|
|
23
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
// src/cli -> src -> plugin-root
|
|
25
|
+
const pluginRoot = resolve(thisDir, "..", "..");
|
|
26
|
+
const wrapper = join(pluginRoot, "bin", "aide-wrapper.sh");
|
|
27
|
+
|
|
28
|
+
if (!existsSync(wrapper)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`aide-wrapper.sh not found at ${wrapper}\n` +
|
|
31
|
+
`Expected plugin root: ${pluginRoot}\n` +
|
|
32
|
+
`Ensure the package is installed correctly.`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return wrapper;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Start the MCP server by exec'ing aide-wrapper.sh with "mcp" + any extra args.
|
|
41
|
+
* This replaces the current process so stdio is inherited directly.
|
|
42
|
+
*/
|
|
43
|
+
export async function mcp(extraArgs: string[]): Promise<void> {
|
|
44
|
+
const wrapper = findWrapper();
|
|
45
|
+
const args = ["mcp", ...extraArgs];
|
|
46
|
+
|
|
47
|
+
// Use execFileSync with stdio inherit so the MCP JSON-RPC protocol
|
|
48
|
+
// flows directly between OpenCode and the aide binary.
|
|
49
|
+
// The wrapper will exec() into the aide binary, replacing itself.
|
|
50
|
+
try {
|
|
51
|
+
execFileSync(wrapper, args, {
|
|
52
|
+
stdio: "inherit",
|
|
53
|
+
env: process.env,
|
|
54
|
+
});
|
|
55
|
+
} catch (err: unknown) {
|
|
56
|
+
// execFileSync throws on non-zero exit. If the process was killed
|
|
57
|
+
// by a signal (e.g. OpenCode shutting down), exit cleanly.
|
|
58
|
+
if (err && typeof err === "object" && "status" in err) {
|
|
59
|
+
process.exit((err as { status: number }).status ?? 1);
|
|
60
|
+
}
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment checker logic — platform-agnostic.
|
|
3
|
+
*
|
|
4
|
+
* Detects excessive or obvious comments in code written by AI agents.
|
|
5
|
+
* Injected as a warning after Write/Edit tool calls to nudge the agent
|
|
6
|
+
* toward cleaner, human-quality code output.
|
|
7
|
+
*
|
|
8
|
+
* Used by both Claude Code hooks (PostToolUse) and OpenCode plugin (tool.execute.after).
|
|
9
|
+
*
|
|
10
|
+
* Philosophy: LLMs over-comment because training data rewards explanation.
|
|
11
|
+
* The single most visible "AI slop" tell is unnecessary comments like
|
|
12
|
+
* "// Initialize the variable" above `let x = 0`. This checker catches
|
|
13
|
+
* those and nudges the agent to remove them without blocking the tool call.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { debug } from "../lib/logger.js";
|
|
17
|
+
|
|
18
|
+
const SOURCE = "comment-checker";
|
|
19
|
+
|
|
20
|
+
/** File extensions we know how to check for comments */
|
|
21
|
+
const CHECKABLE_EXTENSIONS = new Set([
|
|
22
|
+
".ts",
|
|
23
|
+
".tsx",
|
|
24
|
+
".js",
|
|
25
|
+
".jsx",
|
|
26
|
+
".go",
|
|
27
|
+
".py",
|
|
28
|
+
".rs",
|
|
29
|
+
".java",
|
|
30
|
+
".c",
|
|
31
|
+
".cpp",
|
|
32
|
+
".h",
|
|
33
|
+
".hpp",
|
|
34
|
+
".cs",
|
|
35
|
+
".rb",
|
|
36
|
+
".swift",
|
|
37
|
+
".kt",
|
|
38
|
+
".scala",
|
|
39
|
+
".php",
|
|
40
|
+
".vue",
|
|
41
|
+
".svelte",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/** Extensions that use # for comments */
|
|
45
|
+
const HASH_COMMENT_EXTENSIONS = new Set([".py", ".rb", ".sh", ".yaml", ".yml"]);
|
|
46
|
+
|
|
47
|
+
/** Comment patterns to SKIP (not flag) — these are legitimate */
|
|
48
|
+
const LEGITIMATE_PATTERNS = [
|
|
49
|
+
// Directives and pragmas
|
|
50
|
+
/^\s*\/\/\s*(eslint|prettier|ts-ignore|ts-expect-error|ts-nocheck|@ts-|TODO|FIXME|HACK|BUG|XXX|NOTE|IMPORTANT|WARNING)/i,
|
|
51
|
+
/^\s*\/\/\s*noinspection/i,
|
|
52
|
+
/^\s*\/\*\s*(eslint|prettier|global|jshint)/i,
|
|
53
|
+
/^\s*#\s*(type:\s*ignore|noqa|pragma|pylint|mypy)/i,
|
|
54
|
+
/^\s*\/\/\s*region\b/i,
|
|
55
|
+
/^\s*\/\/\s*endregion\b/i,
|
|
56
|
+
// Shebangs
|
|
57
|
+
/^\s*#!/,
|
|
58
|
+
// License/copyright headers
|
|
59
|
+
/^\s*\/\/\s*(copyright|license|SPDX)/i,
|
|
60
|
+
/^\s*#\s*(copyright|license|SPDX)/i,
|
|
61
|
+
// Encoding declarations
|
|
62
|
+
/^\s*#.*coding[:=]/,
|
|
63
|
+
// BDD/test descriptions (describe, it, test blocks in comments)
|
|
64
|
+
/^\s*\/\/\s*(describe|it|test|expect|should|given|when|then)\b/i,
|
|
65
|
+
// JSDoc/docstring openings (/** and """)
|
|
66
|
+
/^\s*\/\*\*/,
|
|
67
|
+
/^\s*"""/,
|
|
68
|
+
/^\s*'''/,
|
|
69
|
+
// Go build tags
|
|
70
|
+
/^\s*\/\/go:build/,
|
|
71
|
+
/^\s*\/\/\+build/,
|
|
72
|
+
// Rust attributes in comments
|
|
73
|
+
/^\s*\/\/!/,
|
|
74
|
+
/^\s*\/\/\s*#\[/,
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/** Patterns that indicate an OBVIOUS/UNNECESSARY comment */
|
|
78
|
+
const OBVIOUS_PATTERNS = [
|
|
79
|
+
// Restating the code in English
|
|
80
|
+
/^\s*\/\/\s*(initialize|initialise|set|create|declare|define|assign|update|increment|decrement|return|import|export|get|fetch|call|invoke|add|remove|delete|check|validate)\s+(the\s+)?\w+/i,
|
|
81
|
+
/^\s*#\s*(initialize|initialise|set|create|declare|define|assign|update|increment|decrement|return|import|export|get|fetch|call|invoke|add|remove|delete|check|validate)\s+(the\s+)?\w+/i,
|
|
82
|
+
// Section dividers that add no info
|
|
83
|
+
/^\s*\/\/\s*[-=*]{3,}\s*$/,
|
|
84
|
+
/^\s*#\s*[-=*]{3,}\s*$/,
|
|
85
|
+
// Empty comments
|
|
86
|
+
/^\s*\/\/\s*$/,
|
|
87
|
+
/^\s*#\s*$/,
|
|
88
|
+
// Repeating the function/variable name
|
|
89
|
+
/^\s*\/\/\s*(function|method|class|interface|type|const|let|var|def|func)\s+/i,
|
|
90
|
+
/^\s*#\s*(function|method|class|interface|type|const|let|var|def|func)\s+/i,
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
export interface CommentCheckResult {
|
|
94
|
+
/** Whether excessive comments were detected */
|
|
95
|
+
hasExcessiveComments: boolean;
|
|
96
|
+
/** Warning message to inject (empty if no issues) */
|
|
97
|
+
warning: string;
|
|
98
|
+
/** Number of suspicious comments found */
|
|
99
|
+
suspiciousCount: number;
|
|
100
|
+
/** Total comments found */
|
|
101
|
+
totalComments: number;
|
|
102
|
+
/** The suspicious comment lines */
|
|
103
|
+
examples: string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Extract the file extension from a path
|
|
108
|
+
*/
|
|
109
|
+
function getExtension(filePath: string): string {
|
|
110
|
+
const lastDot = filePath.lastIndexOf(".");
|
|
111
|
+
if (lastDot === -1) return "";
|
|
112
|
+
return filePath.slice(lastDot).toLowerCase();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if a line is a legitimate comment (should be kept)
|
|
117
|
+
*/
|
|
118
|
+
function isLegitimateComment(line: string): boolean {
|
|
119
|
+
return LEGITIMATE_PATTERNS.some((p) => p.test(line));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if a line is an obvious/unnecessary comment
|
|
124
|
+
*/
|
|
125
|
+
function isObviousComment(line: string): boolean {
|
|
126
|
+
return OBVIOUS_PATTERNS.some((p) => p.test(line));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract comment lines from code content.
|
|
131
|
+
* Returns only single-line comments (// or #), not block comments or docstrings.
|
|
132
|
+
*/
|
|
133
|
+
function extractCommentLines(
|
|
134
|
+
content: string,
|
|
135
|
+
ext: string,
|
|
136
|
+
): { line: string; lineNumber: number }[] {
|
|
137
|
+
const lines = content.split("\n");
|
|
138
|
+
const comments: { line: string; lineNumber: number }[] = [];
|
|
139
|
+
const usesHash = HASH_COMMENT_EXTENSIONS.has(ext);
|
|
140
|
+
let inBlockComment = false;
|
|
141
|
+
let inDocstring = false;
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < lines.length; i++) {
|
|
144
|
+
const trimmed = lines[i].trim();
|
|
145
|
+
|
|
146
|
+
// Track block comments (/* ... */)
|
|
147
|
+
if (!inDocstring) {
|
|
148
|
+
if (trimmed.startsWith("/*")) {
|
|
149
|
+
inBlockComment = true;
|
|
150
|
+
// Single-line block comments
|
|
151
|
+
if (trimmed.includes("*/")) {
|
|
152
|
+
inBlockComment = false;
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (inBlockComment) {
|
|
157
|
+
if (trimmed.includes("*/")) {
|
|
158
|
+
inBlockComment = false;
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Track Python/Ruby docstrings
|
|
165
|
+
if (ext === ".py" || ext === ".rb") {
|
|
166
|
+
const tripleQuoteCount = (trimmed.match(/"""|'''/g) || []).length;
|
|
167
|
+
if (tripleQuoteCount === 1) {
|
|
168
|
+
inDocstring = !inDocstring;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (inDocstring) continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Single-line comments
|
|
175
|
+
if (trimmed.startsWith("//")) {
|
|
176
|
+
comments.push({ line: lines[i], lineNumber: i + 1 });
|
|
177
|
+
} else if (
|
|
178
|
+
usesHash &&
|
|
179
|
+
trimmed.startsWith("#") &&
|
|
180
|
+
!trimmed.startsWith("#!")
|
|
181
|
+
) {
|
|
182
|
+
comments.push({ line: lines[i], lineNumber: i + 1 });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return comments;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check code content for excessive or obvious comments.
|
|
191
|
+
*
|
|
192
|
+
* @param filePath - Path to the file being written/edited
|
|
193
|
+
* @param content - The full file content after the write/edit, OR the new content being written
|
|
194
|
+
* @param isNewContent - If true, content is only the new/changed portion (Edit newString)
|
|
195
|
+
*/
|
|
196
|
+
export function checkComments(
|
|
197
|
+
filePath: string,
|
|
198
|
+
content: string,
|
|
199
|
+
isNewContent = false,
|
|
200
|
+
): CommentCheckResult {
|
|
201
|
+
const ext = getExtension(filePath);
|
|
202
|
+
|
|
203
|
+
// Skip non-checkable files
|
|
204
|
+
if (!CHECKABLE_EXTENSIONS.has(ext)) {
|
|
205
|
+
return {
|
|
206
|
+
hasExcessiveComments: false,
|
|
207
|
+
warning: "",
|
|
208
|
+
suspiciousCount: 0,
|
|
209
|
+
totalComments: 0,
|
|
210
|
+
examples: [],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const comments = extractCommentLines(content, ext);
|
|
215
|
+
const suspicious: { line: string; lineNumber: number }[] = [];
|
|
216
|
+
|
|
217
|
+
for (const comment of comments) {
|
|
218
|
+
// Skip legitimate comments
|
|
219
|
+
if (isLegitimateComment(comment.line)) continue;
|
|
220
|
+
|
|
221
|
+
// Flag obvious/unnecessary comments
|
|
222
|
+
if (isObviousComment(comment.line)) {
|
|
223
|
+
suspicious.push(comment);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// For full file content: also check comment density
|
|
228
|
+
// (>30% comment lines in non-test files is suspicious)
|
|
229
|
+
const totalLines = content
|
|
230
|
+
.split("\n")
|
|
231
|
+
.filter((l) => l.trim().length > 0).length;
|
|
232
|
+
const isTestFile =
|
|
233
|
+
filePath.includes(".test.") ||
|
|
234
|
+
filePath.includes(".spec.") ||
|
|
235
|
+
filePath.includes("__tests__") ||
|
|
236
|
+
filePath.includes("_test.");
|
|
237
|
+
const commentRatio = totalLines > 0 ? comments.length / totalLines : 0;
|
|
238
|
+
const highDensity =
|
|
239
|
+
!isNewContent && !isTestFile && commentRatio > 0.3 && comments.length > 5;
|
|
240
|
+
|
|
241
|
+
const hasExcessiveComments = suspicious.length >= 2 || highDensity;
|
|
242
|
+
|
|
243
|
+
if (!hasExcessiveComments) {
|
|
244
|
+
return {
|
|
245
|
+
hasExcessiveComments: false,
|
|
246
|
+
warning: "",
|
|
247
|
+
suspiciousCount: suspicious.length,
|
|
248
|
+
totalComments: comments.length,
|
|
249
|
+
examples: [],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Build warning message
|
|
254
|
+
const examples = suspicious.slice(0, 5).map((s) => s.line.trim());
|
|
255
|
+
const parts: string[] = [
|
|
256
|
+
`[aide:comment-checker] Detected ${suspicious.length} potentially unnecessary comment${suspicious.length === 1 ? "" : "s"} in ${filePath}.`,
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
if (highDensity) {
|
|
260
|
+
parts.push(
|
|
261
|
+
`Comment density is ${Math.round(commentRatio * 100)}% (${comments.length}/${totalLines} non-empty lines).`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (examples.length > 0) {
|
|
266
|
+
parts.push("Examples:");
|
|
267
|
+
for (const ex of examples) {
|
|
268
|
+
parts.push(` ${ex}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
parts.push("");
|
|
273
|
+
parts.push(
|
|
274
|
+
"Clean code should be self-documenting. Remove comments that merely restate the code. " +
|
|
275
|
+
"Keep only: TODOs, complex logic explanations, non-obvious workarounds, API docs, and regulatory/compliance notes.",
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
hasExcessiveComments: true,
|
|
280
|
+
warning: parts.join("\n"),
|
|
281
|
+
suspiciousCount: suspicious.length,
|
|
282
|
+
totalComments: comments.length,
|
|
283
|
+
examples,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Determine if a tool call is a write/edit that should be checked.
|
|
289
|
+
* Returns the file path if checkable, null otherwise.
|
|
290
|
+
*/
|
|
291
|
+
export function getCheckableFilePath(
|
|
292
|
+
toolName: string,
|
|
293
|
+
toolInput: Record<string, unknown>,
|
|
294
|
+
): string | null {
|
|
295
|
+
const toolLower = toolName.toLowerCase();
|
|
296
|
+
if (
|
|
297
|
+
toolLower !== "write" &&
|
|
298
|
+
toolLower !== "edit" &&
|
|
299
|
+
toolLower !== "multiedit" &&
|
|
300
|
+
toolLower !== "notebookedit"
|
|
301
|
+
) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const filePath =
|
|
306
|
+
(toolInput.filePath as string) ||
|
|
307
|
+
(toolInput.file_path as string) ||
|
|
308
|
+
(toolInput.path as string);
|
|
309
|
+
|
|
310
|
+
if (!filePath) return null;
|
|
311
|
+
|
|
312
|
+
const ext = getExtension(filePath);
|
|
313
|
+
if (!CHECKABLE_EXTENSIONS.has(ext)) return null;
|
|
314
|
+
|
|
315
|
+
return filePath;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get the content to check from a tool's input.
|
|
320
|
+
* For Write: the full content. For Edit: the newString.
|
|
321
|
+
* Returns [content, isNewContent] tuple.
|
|
322
|
+
*/
|
|
323
|
+
export function getContentToCheck(
|
|
324
|
+
toolName: string,
|
|
325
|
+
toolInput: Record<string, unknown>,
|
|
326
|
+
): [string, boolean] | null {
|
|
327
|
+
const toolLower = toolName.toLowerCase();
|
|
328
|
+
|
|
329
|
+
if (toolLower === "write") {
|
|
330
|
+
const content = toolInput.content as string;
|
|
331
|
+
return content ? [content, false] : null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (toolLower === "edit") {
|
|
335
|
+
const newString = (toolInput.newString || toolInput.new_string) as string;
|
|
336
|
+
return newString ? [newString, true] : null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (toolLower === "multiedit") {
|
|
340
|
+
// Concatenate all new strings for checking
|
|
341
|
+
const edits = toolInput.edits as
|
|
342
|
+
| Array<{ new_string?: string; newString?: string }>
|
|
343
|
+
| undefined;
|
|
344
|
+
if (!edits || edits.length === 0) return null;
|
|
345
|
+
const combined = edits
|
|
346
|
+
.map((e) => e.new_string || e.newString || "")
|
|
347
|
+
.join("\n");
|
|
348
|
+
return combined ? [combined, true] : null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
debug(SOURCE, "Comment checker core loaded");
|
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
MAX_PERSISTENCE_ITERATIONS,
|
|
12
12
|
type PersistenceMode,
|
|
13
13
|
} from "./types.js";
|
|
14
|
+
import { fetchTodosFromAide, checkTodos } from "./todo-checker.js";
|
|
15
|
+
import { debug } from "../lib/logger.js";
|
|
16
|
+
|
|
17
|
+
const SOURCE = "persistence-logic";
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Check if a persistence mode is active
|
|
@@ -27,32 +31,49 @@ export function getActiveMode(
|
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
|
-
* Build a reinforcement message for the persistence mode
|
|
34
|
+
* Build a reinforcement message for the persistence mode.
|
|
35
|
+
* If a todo summary is provided, it's appended to give the agent
|
|
36
|
+
* precise visibility into which tasks are still incomplete.
|
|
31
37
|
*/
|
|
32
|
-
export function buildReinforcement(
|
|
38
|
+
export function buildReinforcement(
|
|
39
|
+
mode: string,
|
|
40
|
+
iteration: number,
|
|
41
|
+
todoSummary?: string,
|
|
42
|
+
): string {
|
|
33
43
|
if (iteration > MAX_PERSISTENCE_ITERATIONS) {
|
|
34
44
|
return `Maximum reinforcements (${MAX_PERSISTENCE_ITERATIONS}) reached. Releasing ${mode} mode.`;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
const parts: string[] = [
|
|
48
|
+
`**${mode.toUpperCase()} MODE ACTIVE** (iteration ${iteration}/${MAX_PERSISTENCE_ITERATIONS})`,
|
|
49
|
+
"",
|
|
50
|
+
"You attempted to stop but work may be incomplete.",
|
|
51
|
+
];
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
if (todoSummary) {
|
|
54
|
+
parts.push("");
|
|
55
|
+
parts.push(todoSummary);
|
|
56
|
+
} else {
|
|
57
|
+
parts.push("");
|
|
58
|
+
parts.push("Before stopping, verify:");
|
|
59
|
+
parts.push("- All tasks in your todo list are marked complete");
|
|
60
|
+
parts.push("- All requested functionality is implemented");
|
|
61
|
+
parts.push("- Tests pass (if applicable)");
|
|
62
|
+
parts.push("- No errors remain unaddressed");
|
|
63
|
+
}
|
|
46
64
|
|
|
47
|
-
|
|
65
|
+
parts.push("");
|
|
66
|
+
parts.push("If ANY item is incomplete, CONTINUE WORKING.");
|
|
48
67
|
|
|
49
|
-
|
|
68
|
+
return parts.join("\n");
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
/**
|
|
53
72
|
* Check persistence and return block decision.
|
|
54
73
|
*
|
|
55
74
|
* Returns null if stop is allowed, or { reason } if stop should be blocked.
|
|
75
|
+
* When a persistence mode is active and todos exist, the reinforcement
|
|
76
|
+
* message includes the specific incomplete tasks.
|
|
56
77
|
*/
|
|
57
78
|
export function checkPersistence(
|
|
58
79
|
binary: string,
|
|
@@ -73,5 +94,21 @@ export function checkPersistence(
|
|
|
73
94
|
return null;
|
|
74
95
|
}
|
|
75
96
|
|
|
76
|
-
|
|
97
|
+
// Fetch todos and build a specific continuation message if incomplete tasks exist
|
|
98
|
+
let todoSummary: string | undefined;
|
|
99
|
+
try {
|
|
100
|
+
const todos = fetchTodosFromAide(binary, cwd);
|
|
101
|
+
const todoResult = checkTodos(todos);
|
|
102
|
+
if (todoResult.hasIncomplete) {
|
|
103
|
+
todoSummary = todoResult.message;
|
|
104
|
+
debug(
|
|
105
|
+
SOURCE,
|
|
106
|
+
`Found ${todoResult.incompleteCount} incomplete todos for persistence reinforcement`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
debug(SOURCE, `Failed to fetch todos for persistence (non-fatal): ${err}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { reason: buildReinforcement(mode, iteration, todoSummary) };
|
|
77
114
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Todo continuation checker — platform-agnostic.
|
|
3
|
+
*
|
|
4
|
+
* Reads the agent's todo list and checks for incomplete items.
|
|
5
|
+
* Used to enhance persistence-logic.ts with precise todo-aware blocking:
|
|
6
|
+
* instead of a generic "verify your work is complete", we list the
|
|
7
|
+
* specific incomplete todos.
|
|
8
|
+
*
|
|
9
|
+
* For Claude Code: reads todos from the transcript (TodoWrite tool outputs)
|
|
10
|
+
* or from aide task list.
|
|
11
|
+
* For OpenCode: reads todos via client.session.todo() API.
|
|
12
|
+
*
|
|
13
|
+
* This module provides the platform-agnostic core. Platform hooks
|
|
14
|
+
* call it with however they obtained the todo list.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { runAide } from "./aide-client.js";
|
|
18
|
+
import { debug } from "../lib/logger.js";
|
|
19
|
+
|
|
20
|
+
const SOURCE = "todo-checker";
|
|
21
|
+
|
|
22
|
+
export interface TodoItem {
|
|
23
|
+
id: string;
|
|
24
|
+
content: string;
|
|
25
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
26
|
+
priority?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TodoCheckResult {
|
|
30
|
+
/** Whether there are incomplete todos */
|
|
31
|
+
hasIncomplete: boolean;
|
|
32
|
+
/** Number of incomplete items */
|
|
33
|
+
incompleteCount: number;
|
|
34
|
+
/** Total items */
|
|
35
|
+
totalCount: number;
|
|
36
|
+
/** The incomplete items */
|
|
37
|
+
incompleteItems: TodoItem[];
|
|
38
|
+
/** Formatted message for injection */
|
|
39
|
+
message: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check a list of todos for incomplete items and build a continuation message.
|
|
44
|
+
*/
|
|
45
|
+
export function checkTodos(todos: TodoItem[]): TodoCheckResult {
|
|
46
|
+
if (!todos || todos.length === 0) {
|
|
47
|
+
return {
|
|
48
|
+
hasIncomplete: false,
|
|
49
|
+
incompleteCount: 0,
|
|
50
|
+
totalCount: 0,
|
|
51
|
+
incompleteItems: [],
|
|
52
|
+
message: "",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const incomplete = todos.filter(
|
|
57
|
+
(t) => t.status !== "completed" && t.status !== "cancelled",
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (incomplete.length === 0) {
|
|
61
|
+
return {
|
|
62
|
+
hasIncomplete: false,
|
|
63
|
+
incompleteCount: 0,
|
|
64
|
+
totalCount: todos.length,
|
|
65
|
+
incompleteItems: [],
|
|
66
|
+
message: "",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const completedCount = todos.length - incomplete.length;
|
|
71
|
+
const lines: string[] = [
|
|
72
|
+
`**TODO CONTINUATION** — ${incomplete.length} of ${todos.length} tasks incomplete (${completedCount} done)`,
|
|
73
|
+
"",
|
|
74
|
+
"Remaining tasks:",
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
for (const item of incomplete) {
|
|
78
|
+
const statusIcon = item.status === "in_progress" ? ">" : " ";
|
|
79
|
+
lines.push(` [${statusIcon}] ${item.content}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
lines.push("");
|
|
83
|
+
lines.push(
|
|
84
|
+
"You stopped but have unfinished tasks. Continue working on the next incomplete item.",
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
hasIncomplete: true,
|
|
89
|
+
incompleteCount: incomplete.length,
|
|
90
|
+
totalCount: todos.length,
|
|
91
|
+
incompleteItems: incomplete,
|
|
92
|
+
message: lines.join("\n"),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse todo items from aide task list output.
|
|
98
|
+
*
|
|
99
|
+
* Output format from `aide task list`:
|
|
100
|
+
* [status] id: content
|
|
101
|
+
* e.g.: [pending] abc123: Implement feature X
|
|
102
|
+
*/
|
|
103
|
+
export function parseTodosFromAide(output: string): TodoItem[] {
|
|
104
|
+
const todos: TodoItem[] = [];
|
|
105
|
+
const lines = output.split("\n");
|
|
106
|
+
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
const match = line.match(
|
|
109
|
+
/\[(pending|in_progress|completed|cancelled)\]\s+(\S+):\s+(.+)/,
|
|
110
|
+
);
|
|
111
|
+
if (match) {
|
|
112
|
+
todos.push({
|
|
113
|
+
status: match[1] as TodoItem["status"],
|
|
114
|
+
id: match[2],
|
|
115
|
+
content: match[3].trim(),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return todos;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Fetch todos from aide binary task list.
|
|
125
|
+
* Returns empty array if binary unavailable or no tasks.
|
|
126
|
+
*/
|
|
127
|
+
export function fetchTodosFromAide(binary: string, cwd: string): TodoItem[] {
|
|
128
|
+
try {
|
|
129
|
+
const output = runAide(binary, cwd, ["task", "list"], { timeout: 5000 });
|
|
130
|
+
if (!output) return [];
|
|
131
|
+
return parseTodosFromAide(output);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
debug(SOURCE, `Failed to fetch todos from aide: ${err}`);
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
debug(SOURCE, "Todo checker core loaded");
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-existing-file guard logic — platform-agnostic.
|
|
3
|
+
*
|
|
4
|
+
* Prevents the Write tool from being used on files that already exist.
|
|
5
|
+
* When a file exists, the agent should use Edit instead, which is surgical
|
|
6
|
+
* and preserves content the agent may have forgotten about.
|
|
7
|
+
*
|
|
8
|
+
* Using Write on existing files is one of the most common destructive
|
|
9
|
+
* failure modes: the agent rewrites the entire file from memory and
|
|
10
|
+
* drops functions, imports, or sections it forgot about.
|
|
11
|
+
*
|
|
12
|
+
* Used by both Claude Code hooks (PreToolUse) and OpenCode plugin (tool.execute.before).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync } from "fs";
|
|
16
|
+
import { resolve, isAbsolute, normalize } from "path";
|
|
17
|
+
import { debug } from "../lib/logger.js";
|
|
18
|
+
|
|
19
|
+
const SOURCE = "write-guard";
|
|
20
|
+
|
|
21
|
+
/** Paths that are allowed to be overwritten via Write even if they exist */
|
|
22
|
+
const WRITE_ALLOWED_PATTERNS = [
|
|
23
|
+
// aide internal state files
|
|
24
|
+
/\/.aide\//,
|
|
25
|
+
// dotfiles that are typically fully rewritten
|
|
26
|
+
/\/\.[^/]+rc$/,
|
|
27
|
+
/\/\.env/,
|
|
28
|
+
/\/\.gitignore$/,
|
|
29
|
+
// Lock files (often fully generated)
|
|
30
|
+
/\/package-lock\.json$/,
|
|
31
|
+
/\/bun\.lock$/,
|
|
32
|
+
/\/yarn\.lock$/,
|
|
33
|
+
/\/pnpm-lock\.yaml$/,
|
|
34
|
+
/\/go\.sum$/,
|
|
35
|
+
/\/Cargo\.lock$/,
|
|
36
|
+
// Generated config that's typically overwritten whole
|
|
37
|
+
/\/tsconfig\.json$/,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export interface WriteGuardResult {
|
|
41
|
+
/** Whether the Write should be allowed */
|
|
42
|
+
allowed: boolean;
|
|
43
|
+
/** Message explaining why the Write was blocked */
|
|
44
|
+
message?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check whether a Write tool call should be allowed.
|
|
49
|
+
*
|
|
50
|
+
* @param toolName - The tool being called (only "Write" is checked)
|
|
51
|
+
* @param toolInput - The tool's input arguments
|
|
52
|
+
* @param cwd - Working directory to resolve relative paths
|
|
53
|
+
* @returns Whether the write is allowed and an explanatory message if blocked
|
|
54
|
+
*/
|
|
55
|
+
export function checkWriteGuard(
|
|
56
|
+
toolName: string,
|
|
57
|
+
toolInput: Record<string, unknown>,
|
|
58
|
+
cwd: string,
|
|
59
|
+
): WriteGuardResult {
|
|
60
|
+
// Only guard the Write tool
|
|
61
|
+
if (toolName.toLowerCase() !== "write") {
|
|
62
|
+
return { allowed: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const filePath =
|
|
66
|
+
(toolInput.filePath as string) ||
|
|
67
|
+
(toolInput.file_path as string) ||
|
|
68
|
+
(toolInput.path as string);
|
|
69
|
+
|
|
70
|
+
if (!filePath) {
|
|
71
|
+
return { allowed: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Resolve the full path
|
|
75
|
+
const resolvedPath = normalize(
|
|
76
|
+
isAbsolute(filePath) ? filePath : resolve(cwd, filePath),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Check if file exists
|
|
80
|
+
if (!existsSync(resolvedPath)) {
|
|
81
|
+
// File doesn't exist — Write is the correct tool for new files
|
|
82
|
+
return { allowed: true };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if this path is in the allowed-overwrite list
|
|
86
|
+
for (const pattern of WRITE_ALLOWED_PATTERNS) {
|
|
87
|
+
if (pattern.test(resolvedPath)) {
|
|
88
|
+
debug(
|
|
89
|
+
SOURCE,
|
|
90
|
+
`Allowing Write to existing file (matches allow pattern): ${filePath}`,
|
|
91
|
+
);
|
|
92
|
+
return { allowed: true };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// File exists and isn't in the allowed list — block with guidance
|
|
97
|
+
debug(SOURCE, `Blocking Write to existing file: ${filePath}`);
|
|
98
|
+
return {
|
|
99
|
+
allowed: false,
|
|
100
|
+
message:
|
|
101
|
+
`File "${filePath}" already exists. Use the Edit tool instead of Write to make changes to existing files. ` +
|
|
102
|
+
`The Edit tool makes surgical replacements and preserves content you haven't explicitly changed. ` +
|
|
103
|
+
`Write overwrites the entire file, which risks losing code you forgot to include.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
package/src/opencode/hooks.ts
CHANGED
|
@@ -45,6 +45,12 @@ import {
|
|
|
45
45
|
import { trackToolUse, updateToolStats } from "../core/tool-tracking.js";
|
|
46
46
|
import { evaluateToolUse, isToolDenied } from "../core/tool-enforcement.js";
|
|
47
47
|
import { checkPersistence, getActiveMode } from "../core/persistence-logic.js";
|
|
48
|
+
import { checkWriteGuard } from "../core/write-guard.js";
|
|
49
|
+
import {
|
|
50
|
+
checkComments,
|
|
51
|
+
getCheckableFilePath,
|
|
52
|
+
getContentToCheck,
|
|
53
|
+
} from "../core/comment-checker.js";
|
|
48
54
|
import { getState, setState } from "../core/aide-client.js";
|
|
49
55
|
import { saveStateSnapshot } from "../core/pre-compact-logic.js";
|
|
50
56
|
import { cleanupSession } from "../core/cleanup.js";
|
|
@@ -395,6 +401,28 @@ function createToolBeforeHandler(
|
|
|
395
401
|
output: { args: Record<string, unknown> },
|
|
396
402
|
) => Promise<void> {
|
|
397
403
|
return async (input, _output) => {
|
|
404
|
+
// Write guard: block Write tool on existing files
|
|
405
|
+
try {
|
|
406
|
+
const guardResult = checkWriteGuard(
|
|
407
|
+
input.tool,
|
|
408
|
+
_output.args || {},
|
|
409
|
+
state.cwd,
|
|
410
|
+
);
|
|
411
|
+
if (!guardResult.allowed) {
|
|
412
|
+
debug(SOURCE, `Write guard blocked: ${guardResult.message}`);
|
|
413
|
+
throw new Error(guardResult.message);
|
|
414
|
+
}
|
|
415
|
+
} catch (err) {
|
|
416
|
+
// Re-throw write guard errors (they're intentional blocks)
|
|
417
|
+
if (
|
|
418
|
+
err instanceof Error &&
|
|
419
|
+
err.message?.includes("already exists. Use the Edit tool")
|
|
420
|
+
) {
|
|
421
|
+
throw err;
|
|
422
|
+
}
|
|
423
|
+
debug(SOURCE, `Write guard check failed (non-fatal): ${err}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
398
426
|
// Tool enforcement: check agent restrictions
|
|
399
427
|
// OpenCode doesn't have named agent types like Claude Code, but
|
|
400
428
|
// we still evaluate in case agent_name is passed via tool args or state
|
|
@@ -410,8 +438,6 @@ function createToolBeforeHandler(
|
|
|
410
438
|
SOURCE,
|
|
411
439
|
`Tool ${input.tool} denied for agent ${agentName}: ${enforcement.denyMessage}`,
|
|
412
440
|
);
|
|
413
|
-
// OpenCode doesn't support blocking tool execution from hooks yet,
|
|
414
|
-
// but we log the violation for visibility
|
|
415
441
|
}
|
|
416
442
|
}
|
|
417
443
|
} catch (err) {
|
|
@@ -443,6 +469,31 @@ function createToolAfterHandler(
|
|
|
443
469
|
if (!state.binary) return;
|
|
444
470
|
|
|
445
471
|
updateToolStats(state.binary, state.cwd, input.tool, input.sessionID);
|
|
472
|
+
|
|
473
|
+
// Comment checker: detect excessive comments in Write/Edit output
|
|
474
|
+
try {
|
|
475
|
+
const toolArgs = (_output.metadata?.args || {}) as Record<
|
|
476
|
+
string,
|
|
477
|
+
unknown
|
|
478
|
+
>;
|
|
479
|
+
const filePath = getCheckableFilePath(input.tool, toolArgs);
|
|
480
|
+
if (filePath) {
|
|
481
|
+
const contentResult = getContentToCheck(input.tool, toolArgs);
|
|
482
|
+
if (contentResult) {
|
|
483
|
+
const [content, isNewContent] = contentResult;
|
|
484
|
+
const result = checkComments(filePath, content, isNewContent);
|
|
485
|
+
if (result.hasExcessiveComments) {
|
|
486
|
+
debug(
|
|
487
|
+
SOURCE,
|
|
488
|
+
`Comment checker: ${result.suspiciousCount} suspicious comments in ${filePath}`,
|
|
489
|
+
);
|
|
490
|
+
_output.output += "\n\n" + result.warning;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} catch (err) {
|
|
495
|
+
debug(SOURCE, `Comment checker failed (non-fatal): ${err}`);
|
|
496
|
+
}
|
|
446
497
|
};
|
|
447
498
|
}
|
|
448
499
|
|
package/src/opencode/index.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* "mcp": {
|
|
17
17
|
* "aide": {
|
|
18
18
|
* "type": "local",
|
|
19
|
-
* "command": ["npx", "-y", "
|
|
19
|
+
* "command": ["npx", "-y", "@jmylchreest/aide-plugin", "mcp"],
|
|
20
20
|
* "environment": { "AIDE_CODE_WATCH": "1", "AIDE_CODE_WATCH_DELAY": "30s" },
|
|
21
21
|
* "enabled": true
|
|
22
22
|
* }
|