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.
@@ -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
+ }