stupid-comments 0.1.5
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/.claude-plugin/marketplace.json +22 -0
- package/LICENSE +674 -0
- package/README.md +408 -0
- package/package.json +70 -0
- package/plugins/stupid-comments/.claude-plugin/plugin.json +9 -0
- package/plugins/stupid-comments/commands/check.md +9 -0
- package/plugins/stupid-comments/commands/fix.md +14 -0
- package/plugins/stupid-comments/commands/off.md +7 -0
- package/plugins/stupid-comments/commands/policy.md +6 -0
- package/plugins/stupid-comments/dsh/cordis.patch.yml +5 -0
- package/plugins/stupid-comments/dsh/index.js +297 -0
- package/plugins/stupid-comments/hooks/hooks.json +49 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// DSH (DeepSeek Harness) adapter. The Rust binary holds every rule; this turns
|
|
2
|
+
// harness seams into the hook payload it already speaks, and its exit code back
|
|
3
|
+
// into a DSH decision, so both harnesses run identical logic.
|
|
4
|
+
//
|
|
5
|
+
// Every failure path is silent: a missing binary never blocks a write.
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'node:child_process'
|
|
8
|
+
import { randomUUID } from 'node:crypto'
|
|
9
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
|
|
12
|
+
export const name = 'stupid-comments'
|
|
13
|
+
|
|
14
|
+
const DISARM_ENV = 'STUPID_COMMENTS'
|
|
15
|
+
const DEFAULT_BINARY = 'stupid-comments'
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 15_000
|
|
17
|
+
const BLOCK_EXIT_CODE = 2
|
|
18
|
+
const SOURCE = { kind: 'plugin', plugin: name }
|
|
19
|
+
|
|
20
|
+
/** Tools whose arguments carry file content the policy applies to. */
|
|
21
|
+
const WATCHED_TOOLS = new Set(['write', 'edit', 'multiedit', 'multi_edit', 'str_replace_editor'])
|
|
22
|
+
|
|
23
|
+
const COMMANDS_DIR = new URL('../commands/', import.meta.url)
|
|
24
|
+
const COMMAND_PREFIX = 'stupid-comments-'
|
|
25
|
+
|
|
26
|
+
export function apply(ctx, config = {}) {
|
|
27
|
+
if (process.env[DISARM_ENV] === '0') return
|
|
28
|
+
|
|
29
|
+
const binary = config.binary ?? DEFAULT_BINARY
|
|
30
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
31
|
+
const run = createRunner(ctx, binary, timeoutMs)
|
|
32
|
+
|
|
33
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
34
|
+
if (!WATCHED_TOOLS.has(exec.name.toLowerCase())) return next()
|
|
35
|
+
const call = normalize(exec)
|
|
36
|
+
if (!call) return next()
|
|
37
|
+
const outcome = await run(exec.agent, preToolPayload(exec, call), exec.signal)
|
|
38
|
+
if (outcome.block) return { kind: 'deny', reason: outcome.message }
|
|
39
|
+
if (outcome.message) inject(exec.agent, outcome.message)
|
|
40
|
+
return next()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// A blocking Stop steers the agent instead of letting it settle, which is
|
|
44
|
+
// how Claude Code's Stop hook forces the model to fix what it just wrote.
|
|
45
|
+
ctx.on('agent/turn-stopping', async ({ agent, signal }) => {
|
|
46
|
+
const outcome = await run(agent, stopPayload(agent), signal)
|
|
47
|
+
if (outcome.block) steer(agent, outcome.message)
|
|
48
|
+
else if (outcome.message) inject(agent, outcome.message)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// A child is retained from its start edge: by the time it ends, the registry
|
|
52
|
+
// may already have dropped it, and without the handle there is no workspace
|
|
53
|
+
// to run the check in.
|
|
54
|
+
const children = new Map()
|
|
55
|
+
ctx.on('subagent/start', (info) => {
|
|
56
|
+
const child = ctx.get('agents')?.get(info.id)
|
|
57
|
+
if (child) children.set(info.runId ?? info.id, child)
|
|
58
|
+
})
|
|
59
|
+
ctx.on('subagent/end', (info) => {
|
|
60
|
+
const key = info.runId ?? info.id
|
|
61
|
+
const child = children.get(key) ?? ctx.get('agents')?.get(info.id)
|
|
62
|
+
children.delete(key)
|
|
63
|
+
if (!child) return
|
|
64
|
+
void run(child, subagentStopPayload(child, info)).then((outcome) => {
|
|
65
|
+
if (outcome.message) inject(child, outcome.message)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
ctx.inject(['commands'], (commandCtx) => {
|
|
70
|
+
for (const command of loadCommands(ctx)) {
|
|
71
|
+
commandCtx.commands.register({
|
|
72
|
+
name: COMMAND_PREFIX + command.slug,
|
|
73
|
+
description: command.description,
|
|
74
|
+
...command.hint ? { input: { hint: command.hint } } : {},
|
|
75
|
+
handler: (invocation) => {
|
|
76
|
+
steer(invocation.agent, expand(command.body, invocation.rawInput))
|
|
77
|
+
return { kind: 'success', text: `Running ${command.slug} against the comment policy.` }
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* One spawn of `stupid-comments hook dsh` with the payload on stdin. The first
|
|
86
|
+
* ENOENT disables the runner for the rest of the process: a user who installed
|
|
87
|
+
* the plugin but not the binary gets one warning, not one per tool call.
|
|
88
|
+
*/
|
|
89
|
+
function createRunner(ctx, binary, timeoutMs) {
|
|
90
|
+
const quiet = { block: false, message: '' }
|
|
91
|
+
let missing = false
|
|
92
|
+
|
|
93
|
+
return async function run(agent, payload, signal) {
|
|
94
|
+
if (missing || process.env[DISARM_ENV] === '0') return quiet
|
|
95
|
+
const cwd = workspaceOf(agent)
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const result = await execute(binary, payload, { cwd, timeoutMs, signal })
|
|
99
|
+
const message = result.stderr.trim()
|
|
100
|
+
if (!message) return quiet
|
|
101
|
+
return { block: result.code === BLOCK_EXIT_CODE, message }
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error?.code === 'ENOENT') {
|
|
104
|
+
missing = true
|
|
105
|
+
ctx.logger?.warn(
|
|
106
|
+
`${name}: "${binary}" is not on PATH, so nothing is being enforced. `
|
|
107
|
+
+ 'Install it with: cargo install --root ~/.local --git https://github.com/nmindz/stupid-comments stupid-comments',
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
return quiet
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function execute(binary, payload, { cwd, timeoutMs, signal }) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const child = spawn(binary, ['hook', 'dsh'], {
|
|
118
|
+
...cwd ? { cwd } : {},
|
|
119
|
+
stdio: ['pipe', 'ignore', 'pipe'],
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
let stderr = ''
|
|
123
|
+
let settled = false
|
|
124
|
+
const finish = (fn, value) => {
|
|
125
|
+
if (settled) return
|
|
126
|
+
settled = true
|
|
127
|
+
clearTimeout(timer)
|
|
128
|
+
signal?.removeEventListener('abort', abort)
|
|
129
|
+
fn(value)
|
|
130
|
+
}
|
|
131
|
+
const abort = () => {
|
|
132
|
+
child.kill('SIGKILL')
|
|
133
|
+
finish(resolve, { code: 0, stderr: '' })
|
|
134
|
+
}
|
|
135
|
+
const timer = setTimeout(abort, timeoutMs)
|
|
136
|
+
|
|
137
|
+
child.stderr.setEncoding('utf8')
|
|
138
|
+
child.stderr.on('data', (chunk) => { stderr += chunk })
|
|
139
|
+
child.on('error', (error) => { finish(reject, error) })
|
|
140
|
+
child.on('close', (code) => { finish(resolve, { code: code ?? 0, stderr }) })
|
|
141
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
142
|
+
|
|
143
|
+
child.stdin.on('error', () => {})
|
|
144
|
+
child.stdin.end(JSON.stringify(payload))
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Payloads. Field names are the Claude Code hook input schema, because the
|
|
149
|
+
// binary parses one dialect and both harnesses feed it. ---
|
|
150
|
+
|
|
151
|
+
function base(agent, event) {
|
|
152
|
+
return {
|
|
153
|
+
session_id: agent?.session?.header?.id ?? agent?.session?.id ?? '',
|
|
154
|
+
transcript_path: '',
|
|
155
|
+
cwd: workspaceOf(agent) ?? process.cwd(),
|
|
156
|
+
hook_event_name: event,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function preToolPayload(exec, call) {
|
|
161
|
+
return {
|
|
162
|
+
...base(exec.agent, 'PreToolUse'),
|
|
163
|
+
tool_name: call.tool,
|
|
164
|
+
tool_input: call.input,
|
|
165
|
+
tool_use_id: exec.callId,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Restate a tool call in the two operations the engine understands. Anthropic's
|
|
171
|
+
* text-editor tool names the same file, content, and anchors differently, and
|
|
172
|
+
* translating here keeps that dialect out of the engine. An `insert` command
|
|
173
|
+
* carries no anchor to reconstruct from, so it falls to the stop gate.
|
|
174
|
+
*/
|
|
175
|
+
function normalize(exec) {
|
|
176
|
+
const args = exec.arguments ?? {}
|
|
177
|
+
if (exec.name.toLowerCase() !== 'str_replace_editor') {
|
|
178
|
+
return { tool: exec.name, input: args }
|
|
179
|
+
}
|
|
180
|
+
if (args.command === 'create') {
|
|
181
|
+
return { tool: 'write', input: { file_path: args.path, content: args.file_text } }
|
|
182
|
+
}
|
|
183
|
+
if (args.command === 'str_replace') {
|
|
184
|
+
return { tool: 'edit', input: { file_path: args.path, old_string: args.old_str, new_string: args.new_str } }
|
|
185
|
+
}
|
|
186
|
+
return undefined
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function stopPayload(agent) {
|
|
190
|
+
return { ...base(agent, 'Stop'), stop_hook_active: false }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function subagentStopPayload(agent, info) {
|
|
194
|
+
return { ...base(agent, 'SubagentStop'), agent_id: info.id, stop_hook_active: false }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function workspaceOf(agent) {
|
|
198
|
+
return agent?.session?.header?.cwd ?? undefined
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- Model-facing messages. Built inline so the plugin stays dependency-free
|
|
202
|
+
// and installable into any profile. ---
|
|
203
|
+
|
|
204
|
+
function userMessage(text) {
|
|
205
|
+
return deepFreeze({
|
|
206
|
+
id: randomUUID(),
|
|
207
|
+
role: 'user',
|
|
208
|
+
content: [{ type: 'text', text }],
|
|
209
|
+
source: SOURCE,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function deepFreeze(value) {
|
|
214
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
215
|
+
const child = value[key]
|
|
216
|
+
if (child && typeof child === 'object') deepFreeze(child)
|
|
217
|
+
}
|
|
218
|
+
return Object.freeze(value)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function steer(agent, text) {
|
|
222
|
+
try {
|
|
223
|
+
agent?.steer(userMessage(text))
|
|
224
|
+
} catch {}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function inject(agent, text) {
|
|
228
|
+
try {
|
|
229
|
+
agent?.inject(userMessage(text))
|
|
230
|
+
} catch {}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- Commands. The prompt bodies are the same markdown files Claude Code
|
|
234
|
+
// loads, so neither harness owns a private copy of the wording. ---
|
|
235
|
+
|
|
236
|
+
function loadCommands(ctx) {
|
|
237
|
+
let entries
|
|
238
|
+
try {
|
|
239
|
+
entries = readdirSync(fileURLToPath(COMMANDS_DIR))
|
|
240
|
+
} catch (error) {
|
|
241
|
+
ctx.logger?.warn(`${name}: could not read command definitions: ${String(error)}`)
|
|
242
|
+
return []
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const commands = []
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
if (!entry.endsWith('.md')) continue
|
|
248
|
+
const parsed = parseCommandFile(new URL(entry, COMMANDS_DIR))
|
|
249
|
+
if (parsed) commands.push({ slug: entry.slice(0, -3), ...parsed })
|
|
250
|
+
}
|
|
251
|
+
return commands
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function parseCommandFile(url) {
|
|
255
|
+
let raw
|
|
256
|
+
try {
|
|
257
|
+
raw = readFileSync(fileURLToPath(url), 'utf8')
|
|
258
|
+
} catch {
|
|
259
|
+
return undefined
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const { frontmatter, body } = splitFrontmatter(raw)
|
|
263
|
+
const description = frontmatter.description ?? 'Comment policy command.'
|
|
264
|
+
if (!body.trim()) return undefined
|
|
265
|
+
return {
|
|
266
|
+
description,
|
|
267
|
+
...frontmatter['argument-hint'] ? { hint: frontmatter['argument-hint'] } : {},
|
|
268
|
+
body: body.trim(),
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function splitFrontmatter(raw) {
|
|
273
|
+
const text = raw.replace(/^\uFEFF/, '')
|
|
274
|
+
if (!text.startsWith('---')) return { frontmatter: {}, body: text }
|
|
275
|
+
const end = text.indexOf('\n---', 3)
|
|
276
|
+
if (end === -1) return { frontmatter: {}, body: text }
|
|
277
|
+
|
|
278
|
+
const frontmatter = {}
|
|
279
|
+
for (const line of text.slice(3, end).split('\n')) {
|
|
280
|
+
const at = line.indexOf(':')
|
|
281
|
+
if (at === -1) continue
|
|
282
|
+
frontmatter[line.slice(0, at).trim()] = line.slice(at + 1).trim()
|
|
283
|
+
}
|
|
284
|
+
const bodyStart = text.indexOf('\n', end + 1)
|
|
285
|
+
return { frontmatter, body: bodyStart === -1 ? '' : text.slice(bodyStart + 1) }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** The `$1`, `${1:-default}`, and `$ARGUMENTS` placeholders Claude Code expands. */
|
|
289
|
+
function expand(body, rawInput) {
|
|
290
|
+
const input = rawInput.trim()
|
|
291
|
+
const args = input ? input.split(/\s+/) : []
|
|
292
|
+
return body
|
|
293
|
+
.replace(/\$\{(\d+):-([^}]*)\}/g, (_, index, fallback) => args[Number(index) - 1] ?? fallback)
|
|
294
|
+
.replace(/\$\{(\d+)\}/g, (_, index) => args[Number(index) - 1] ?? '')
|
|
295
|
+
.replace(/\$ARGUMENTS\b/g, input)
|
|
296
|
+
.replace(/\$(\d+)/g, (_, index) => args[Number(index) - 1] ?? '')
|
|
297
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "sh -c 'command -v stupid-comments >/dev/null 2>&1 && exit 0; grep -qiE \"^#{1,6}[[:space:]]*comments?[[:space:]]+polic(y|ies)[[:space:]]*$\" \"$HOME/.claude/CLAUDE.md\" \"$HOME/.dsh/AGENTS.md\" ./CLAUDE.md ./AGENTS.md 2>/dev/null || exit 0; echo \"stupid-comments: a comment policy was found but the binary is not installed, so nothing is being enforced.\"; echo \" install: cargo install --root ~/.local --git https://github.com/nmindz/stupid-comments stupid-comments\"; echo \" disable: /plugin uninstall stupid-comments@stupid-comments\"'",
|
|
9
|
+
"timeout": 15
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PreToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "sh -c 'command -v stupid-comments >/dev/null 2>&1 && exec stupid-comments hook claude || exit 0'",
|
|
21
|
+
"timeout": 15
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"Stop": [
|
|
27
|
+
{
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "sh -c 'command -v stupid-comments >/dev/null 2>&1 && exec stupid-comments hook claude || exit 0'",
|
|
32
|
+
"timeout": 15
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"SubagentStop": [
|
|
38
|
+
{
|
|
39
|
+
"hooks": [
|
|
40
|
+
{
|
|
41
|
+
"type": "command",
|
|
42
|
+
"command": "sh -c 'command -v stupid-comments >/dev/null 2>&1 && exec stupid-comments hook claude || exit 0'",
|
|
43
|
+
"timeout": 15
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
}
|