claws-code 0.8.0

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.
Files changed (180) hide show
  1. package/.claude/commands/claws-auto.md +90 -0
  2. package/.claude/commands/claws-bin.md +28 -0
  3. package/.claude/commands/claws-cleanup.md +28 -0
  4. package/.claude/commands/claws-do.md +82 -0
  5. package/.claude/commands/claws-fix.md +40 -0
  6. package/.claude/commands/claws-goal.md +111 -0
  7. package/.claude/commands/claws-help.md +54 -0
  8. package/.claude/commands/claws-plan.md +103 -0
  9. package/.claude/commands/claws-report.md +29 -0
  10. package/.claude/commands/claws-status.md +37 -0
  11. package/.claude/commands/claws-update.md +32 -0
  12. package/.claude/commands/claws.md +64 -0
  13. package/.claude/rules/claws-default-behavior.md +76 -0
  14. package/.claude/settings.json +112 -0
  15. package/.claude/settings.local.json +19 -0
  16. package/.claude/skills/claws-auto-engine/SKILL.md +97 -0
  17. package/.claude/skills/claws-goal-tracker/SKILL.md +106 -0
  18. package/.claude/skills/claws-prompt-templates/SKILL.md +203 -0
  19. package/.claude/skills/claws-wave-lead/SKILL.md +126 -0
  20. package/.claude/skills/claws-wave-subworker/SKILL.md +60 -0
  21. package/CHANGELOG.md +1949 -0
  22. package/LICENSE +21 -0
  23. package/README.md +420 -0
  24. package/bin/cli.js +84 -0
  25. package/cli.js +223 -0
  26. package/docs/ARCHITECTURE.md +511 -0
  27. package/docs/event-protocol.md +588 -0
  28. package/docs/features.md +562 -0
  29. package/docs/guide.md +891 -0
  30. package/docs/index.html +716 -0
  31. package/docs/protocol.md +323 -0
  32. package/extension/.vscodeignore +15 -0
  33. package/extension/CHANGELOG.md +1906 -0
  34. package/extension/LICENSE +21 -0
  35. package/extension/README.md +137 -0
  36. package/extension/docs/features.md +424 -0
  37. package/extension/docs/protocol.md +197 -0
  38. package/extension/esbuild.mjs +25 -0
  39. package/extension/icon.png +0 -0
  40. package/extension/native/.metadata.json +10 -0
  41. package/extension/native/node-pty/LICENSE +69 -0
  42. package/extension/native/node-pty/README.md +165 -0
  43. package/extension/native/node-pty/lib/conpty_console_list_agent.js +16 -0
  44. package/extension/native/node-pty/lib/conpty_console_list_agent.js.map +1 -0
  45. package/extension/native/node-pty/lib/eventEmitter2.js +47 -0
  46. package/extension/native/node-pty/lib/eventEmitter2.js.map +1 -0
  47. package/extension/native/node-pty/lib/index.js +52 -0
  48. package/extension/native/node-pty/lib/index.js.map +1 -0
  49. package/extension/native/node-pty/lib/interfaces.js +7 -0
  50. package/extension/native/node-pty/lib/interfaces.js.map +1 -0
  51. package/extension/native/node-pty/lib/shared/conout.js +11 -0
  52. package/extension/native/node-pty/lib/shared/conout.js.map +1 -0
  53. package/extension/native/node-pty/lib/terminal.js +190 -0
  54. package/extension/native/node-pty/lib/terminal.js.map +1 -0
  55. package/extension/native/node-pty/lib/types.js +7 -0
  56. package/extension/native/node-pty/lib/types.js.map +1 -0
  57. package/extension/native/node-pty/lib/unixTerminal.js +346 -0
  58. package/extension/native/node-pty/lib/unixTerminal.js.map +1 -0
  59. package/extension/native/node-pty/lib/utils.js +39 -0
  60. package/extension/native/node-pty/lib/utils.js.map +1 -0
  61. package/extension/native/node-pty/lib/windowsConoutConnection.js +125 -0
  62. package/extension/native/node-pty/lib/windowsConoutConnection.js.map +1 -0
  63. package/extension/native/node-pty/lib/windowsPtyAgent.js +320 -0
  64. package/extension/native/node-pty/lib/windowsPtyAgent.js.map +1 -0
  65. package/extension/native/node-pty/lib/windowsTerminal.js +199 -0
  66. package/extension/native/node-pty/lib/windowsTerminal.js.map +1 -0
  67. package/extension/native/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  68. package/extension/native/node-pty/lib/worker/conoutSocketWorker.js.map +1 -0
  69. package/extension/native/node-pty/package.json +64 -0
  70. package/extension/native/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  71. package/extension/native/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  72. package/extension/native/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  73. package/extension/native/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  74. package/extension/native/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  75. package/extension/native/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  76. package/extension/native/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  77. package/extension/native/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  78. package/extension/native/node-pty/prebuilds/win32-arm64/pty.node +0 -0
  79. package/extension/native/node-pty/prebuilds/win32-arm64/winpty-agent.exe +0 -0
  80. package/extension/native/node-pty/prebuilds/win32-arm64/winpty.dll +0 -0
  81. package/extension/native/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  82. package/extension/native/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  83. package/extension/native/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  84. package/extension/native/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  85. package/extension/native/node-pty/prebuilds/win32-x64/pty.node +0 -0
  86. package/extension/native/node-pty/prebuilds/win32-x64/winpty-agent.exe +0 -0
  87. package/extension/native/node-pty/prebuilds/win32-x64/winpty.dll +0 -0
  88. package/extension/package-lock.json +605 -0
  89. package/extension/package.json +343 -0
  90. package/extension/scripts/bundle-native.mjs +104 -0
  91. package/extension/scripts/deploy-dev.mjs +60 -0
  92. package/extension/src/ansi-strip.ts +52 -0
  93. package/extension/src/backends/vscode/claws-pty.ts +483 -0
  94. package/extension/src/backends/vscode/status-bar.ts +99 -0
  95. package/extension/src/backends/vscode/vscode-backend.ts +282 -0
  96. package/extension/src/capture-store.ts +125 -0
  97. package/extension/src/event-log.ts +629 -0
  98. package/extension/src/event-schemas.ts +478 -0
  99. package/extension/src/extension.js +492 -0
  100. package/extension/src/extension.ts +873 -0
  101. package/extension/src/lifecycle-engine.ts +60 -0
  102. package/extension/src/lifecycle-rules.ts +171 -0
  103. package/extension/src/lifecycle-store.ts +506 -0
  104. package/extension/src/peer-registry.ts +176 -0
  105. package/extension/src/pipeline-registry.ts +82 -0
  106. package/extension/src/platform.ts +64 -0
  107. package/extension/src/protocol.ts +532 -0
  108. package/extension/src/server-config.ts +98 -0
  109. package/extension/src/server.ts +2210 -0
  110. package/extension/src/task-registry.ts +51 -0
  111. package/extension/src/terminal-backend.ts +211 -0
  112. package/extension/src/terminal-manager.ts +395 -0
  113. package/extension/src/topic-registry.ts +70 -0
  114. package/extension/src/topic-utils.ts +46 -0
  115. package/extension/src/transport.ts +45 -0
  116. package/extension/src/uninstall-cleanup.ts +232 -0
  117. package/extension/src/wave-registry.ts +314 -0
  118. package/extension/src/websocket-transport.ts +153 -0
  119. package/extension/tsconfig.json +23 -0
  120. package/lib/capabilities.js +145 -0
  121. package/lib/dry-run.js +43 -0
  122. package/lib/install.js +1018 -0
  123. package/lib/mcp-setup.js +92 -0
  124. package/lib/platform.js +240 -0
  125. package/lib/preflight.js +152 -0
  126. package/lib/shell-hook.js +343 -0
  127. package/lib/uninstall.js +162 -0
  128. package/lib/verify.js +166 -0
  129. package/mcp_server.js +3529 -0
  130. package/package.json +48 -0
  131. package/rules/claws-default-behavior.md +72 -0
  132. package/scripts/_helpers/atomic-file.mjs +137 -0
  133. package/scripts/_helpers/fix-repair.js +64 -0
  134. package/scripts/_helpers/json-safe.mjs +218 -0
  135. package/scripts/bump-version.sh +84 -0
  136. package/scripts/codegen/gen-docs.mjs +61 -0
  137. package/scripts/codegen/gen-json-schema.mjs +62 -0
  138. package/scripts/codegen/gen-mcp-tools.mjs +358 -0
  139. package/scripts/codegen/gen-types.mjs +172 -0
  140. package/scripts/codegen/index.mjs +42 -0
  141. package/scripts/dev-hooks/check-extension-dirs.js +77 -0
  142. package/scripts/dev-hooks/check-open-claws-terminals.js +70 -0
  143. package/scripts/dev-hooks/check-stale-main.js +55 -0
  144. package/scripts/dev-hooks/check-tag-pushed.js +51 -0
  145. package/scripts/dev-hooks/check-tag-vs-main.js +56 -0
  146. package/scripts/dev-vsix-install.sh +60 -0
  147. package/scripts/fix.sh +702 -0
  148. package/scripts/gen-client-types.mjs +81 -0
  149. package/scripts/git-hooks/pre-commit +31 -0
  150. package/scripts/hooks/lifecycle-state.js +61 -0
  151. package/scripts/hooks/package.json +4 -0
  152. package/scripts/hooks/post-tool-use-claws.js +292 -0
  153. package/scripts/hooks/pre-bash-no-verify-block.js +72 -0
  154. package/scripts/hooks/pre-tool-use-claws.js +206 -0
  155. package/scripts/hooks/session-start-claws.js +97 -0
  156. package/scripts/hooks/stop-claws.js +88 -0
  157. package/scripts/inject-claude-md.js +205 -0
  158. package/scripts/inject-dev-hooks.js +96 -0
  159. package/scripts/inject-global-claude-md.js +140 -0
  160. package/scripts/inject-settings-hooks.js +370 -0
  161. package/scripts/install.ps1 +146 -0
  162. package/scripts/install.sh +1729 -0
  163. package/scripts/monitor-arm-watch.js +155 -0
  164. package/scripts/rebuild-node-pty.sh +245 -0
  165. package/scripts/report.sh +232 -0
  166. package/scripts/shell-hook.fish +164 -0
  167. package/scripts/shell-hook.ps1 +33 -0
  168. package/scripts/shell-hook.sh +232 -0
  169. package/scripts/stream-events.js +399 -0
  170. package/scripts/terminal-wrapper.sh +36 -0
  171. package/scripts/test-enforcement.sh +132 -0
  172. package/scripts/test-install.sh +174 -0
  173. package/scripts/test-installer-parity.sh +135 -0
  174. package/scripts/test-template-enforcement.sh +76 -0
  175. package/scripts/uninstall.sh +143 -0
  176. package/scripts/update.sh +337 -0
  177. package/scripts/verify-release.sh +323 -0
  178. package/scripts/verify-wrapped.sh +194 -0
  179. package/templates/CLAUDE.global.md +135 -0
  180. package/templates/CLAUDE.project.md +37 -0
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ // inject-dev-hooks.js — Register Claws dev-discipline hooks into a project's
3
+ // .claude/settings.json. Idempotent: detects existing hooks by _source tag
4
+ // and updates in place. Safe-merge: uses json-safe.mjs mergeIntoFile —
5
+ // atomic write, JSONC-tolerant, abort-on-malformed (FINDING-B-3).
6
+ //
7
+ // Claude Code settings.json hooks format:
8
+ // { hooks: { SessionStart: [{matcher, hooks:[{type,command}], _source}], ... } }
9
+ //
10
+ // Usage: node scripts/inject-dev-hooks.js [project-root]
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { pathToFileURL } = require('url');
16
+
17
+ const SOURCE_TAG = 'claws-dev-hooks';
18
+
19
+ const HELPERS_MJS = pathToFileURL(
20
+ path.resolve(__dirname, '_helpers', 'json-safe.mjs')
21
+ ).href;
22
+
23
+ // Five dev-hook definitions — event → script file in .claws-bin/dev-hooks/
24
+ const DEV_HOOK_DEFS = [
25
+ { event: 'SessionStart', matcher: '*', script: 'check-stale-main.js' },
26
+ { event: 'PostToolUse', matcher: 'Bash', script: 'check-tag-pushed.js' },
27
+ { event: 'PostToolUse', matcher: 'Bash', script: 'check-tag-vs-main.js' },
28
+ { event: 'Stop', matcher: '*', script: 'check-open-claws-terminals.js' },
29
+ { event: 'SessionStart', matcher: '*', script: 'check-extension-dirs.js' },
30
+ ];
31
+
32
+ function buildEntry(def, binDir) {
33
+ return {
34
+ _source: SOURCE_TAG,
35
+ matcher: def.matcher,
36
+ hooks: [{ type: 'command', command: `node "${path.join(binDir, def.script)}"` }],
37
+ };
38
+ }
39
+
40
+ async function inject(projectRoot) {
41
+ const { mergeIntoFile } = await import(HELPERS_MJS);
42
+ const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
43
+ const binDir = path.join(projectRoot, '.claws-bin', 'dev-hooks');
44
+
45
+ let totalRegistered = 0;
46
+ const result = await mergeIntoFile(settingsPath, (settings) => {
47
+ // Migrate legacy array format (same guard as inject-settings-hooks.js)
48
+ if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) {
49
+ settings.hooks = {};
50
+ }
51
+
52
+ for (const def of DEV_HOOK_DEFS) {
53
+ if (!Array.isArray(settings.hooks[def.event])) {
54
+ settings.hooks[def.event] = [];
55
+ }
56
+ const arr = settings.hooks[def.event];
57
+ const newCmd = `node "${path.join(binDir, def.script)}"`;
58
+ const exactIdx = arr.findIndex(
59
+ (e) => e._source === SOURCE_TAG && e.hooks && e.hooks[0] && e.hooks[0].command === newCmd
60
+ );
61
+ if (exactIdx === -1) {
62
+ // Also remove any old entry for this script path (from a different binDir)
63
+ const oldIdx = arr.findIndex(
64
+ (e) => e._source === SOURCE_TAG && e.hooks && e.hooks[0] &&
65
+ e.hooks[0].command.includes(def.script)
66
+ );
67
+ if (oldIdx !== -1) arr[oldIdx] = buildEntry(def, binDir);
68
+ else arr.push(buildEntry(def, binDir));
69
+ }
70
+ totalRegistered++;
71
+ }
72
+ }, { allowJsonc: true });
73
+
74
+ if (!result.ok) {
75
+ const e = result.error;
76
+ process.stderr.write(`inject-dev-hooks: settings merge failed: ${e.message}\n`);
77
+ if (e.backupSavedAt) process.stderr.write(` original backed up to: ${e.backupSavedAt}\n`);
78
+ process.exit(1);
79
+ }
80
+ return totalRegistered;
81
+ }
82
+
83
+ const projectRoot = process.argv[2] || process.cwd();
84
+
85
+ if (!fs.existsSync(projectRoot)) {
86
+ console.error(`inject-dev-hooks: project root not found: ${projectRoot}`);
87
+ process.exit(1);
88
+ }
89
+
90
+ (async () => {
91
+ const count = await inject(projectRoot);
92
+ console.log(` inject-dev-hooks: ${count} hooks ready (_source: "${SOURCE_TAG}")`);
93
+ })().catch((e) => {
94
+ console.error(`inject-dev-hooks failed: ${e.message}`);
95
+ process.exit(1);
96
+ });
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ // Inject the Claws machine-wide policy block into ~/.claude/CLAUDE.md.
3
+ // Usage: node inject-global-claude-md.js [--dry-run]
4
+ //
5
+ // Behavior:
6
+ // 1. Reads templates/CLAUDE.global.md (relative to this script).
7
+ // 2. Substitutes {VERSION} and {LIFECYCLE_PHASES} from code (package.json,
8
+ // extension/src/lifecycle-store.ts).
9
+ // 3. Inserts or replaces the fenced block:
10
+ // <!-- CLAWS-GLOBAL:BEGIN [v<X.Y.Z>] --> ... <!-- CLAWS-GLOBAL:END [v<X.Y.Z>] -->
11
+ // Sentinel match is regex-based so prior-version blocks upgrade cleanly.
12
+ // 4. Creates ~/.claude/CLAUDE.md with a stub if it doesn't exist.
13
+ // 5. Preserves all non-Claws content byte-for-byte.
14
+ // 6. Idempotent — safe to run on every install.
15
+
16
+ 'use strict';
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
20
+
21
+ // M-28: atomic write helper.
22
+ function writeAtomic(filePath, content) {
23
+ const tmp = filePath + '.claws-tmp.' + process.pid + '-' + (++writeAtomic._nonce);
24
+ let _fd;
25
+ try {
26
+ _fd = fs.openSync(tmp, 'w', 0o644);
27
+ fs.writeSync(_fd, content);
28
+ fs.fsyncSync(_fd);
29
+ fs.closeSync(_fd);
30
+ _fd = null;
31
+ fs.renameSync(tmp, filePath);
32
+ } catch (err) {
33
+ if (_fd != null) { try { fs.closeSync(_fd); } catch { /* ignore */ } }
34
+ try { fs.unlinkSync(tmp); } catch { /* ignore */ }
35
+ throw err;
36
+ }
37
+ }
38
+ writeAtomic._nonce = 0;
39
+
40
+ const REPO_ROOT = path.join(__dirname, '..');
41
+ const DRY_RUN = process.argv.includes('--dry-run');
42
+
43
+ const GLOBAL_CLAUDE_MD = path.join(os.homedir(), '.claude', 'CLAUDE.md');
44
+ const TEMPLATE_PATH = path.join(REPO_ROOT, 'templates', 'CLAUDE.global.md');
45
+
46
+ const BEGIN_RE = /<!-- CLAWS-GLOBAL:BEGIN(?: v[\d.]+(?:-[\w.]+)?| v1)? -->/;
47
+ const END_RE = /<!-- CLAWS-GLOBAL:END(?: v[\d.]+(?:-[\w.]+)?| v1)? -->/;
48
+
49
+ // ── Source-of-truth readers ────────────────────────────────────────────────
50
+ function readPhases() {
51
+ const ltPath = path.join(REPO_ROOT, 'extension', 'src', 'lifecycle-store.ts');
52
+ try {
53
+ const src = fs.readFileSync(ltPath, 'utf8');
54
+ const m = src.match(/export type Phase\s*=([^;]+);/);
55
+ if (!m) return [];
56
+ return Array.from(m[1].matchAll(/'([A-Z][A-Z0-9-]+)'/g)).map((x) => x[1]);
57
+ } catch (err) {
58
+ return [];
59
+ }
60
+ }
61
+
62
+ function readVersion() {
63
+ try {
64
+ const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8'));
65
+ return pkg.version || 'unknown';
66
+ } catch (err) {
67
+ return 'unknown';
68
+ }
69
+ }
70
+
71
+ const PHASES = readPhases();
72
+ const VERSION = readVersion();
73
+
74
+ const BEGIN_LITERAL = `<!-- CLAWS-GLOBAL:BEGIN v${VERSION} -->`;
75
+ const END_LITERAL = `<!-- CLAWS-GLOBAL:END v${VERSION} -->`;
76
+
77
+ // Read template
78
+ let template;
79
+ try {
80
+ template = fs.readFileSync(TEMPLATE_PATH, 'utf8').trim();
81
+ } catch (e) {
82
+ console.error(`inject-global-claude-md: cannot read template at ${TEMPLATE_PATH}: ${e.message}`);
83
+ process.exit(1);
84
+ }
85
+
86
+ // Substitute parameters in the template body, then re-stamp sentinels.
87
+ const phaseChain = PHASES.length ? PHASES.join(' → ') : '(unavailable)';
88
+ template = template
89
+ .replace(/\{VERSION\}/g, VERSION)
90
+ .replace(/\{LIFECYCLE_PHASES\}/g, phaseChain)
91
+ .replace(/<!-- CLAWS-GLOBAL:BEGIN(?: v[\d.]+(?:-[\w.]+)?| v1)? -->/g, BEGIN_LITERAL)
92
+ .replace(/<!-- CLAWS-GLOBAL:END(?: v[\d.]+(?:-[\w.]+)?| v1)? -->/g, END_LITERAL);
93
+
94
+ // Ensure template is wrapped in sentinels (it should already be, but guard)
95
+ if (!template.includes(BEGIN_LITERAL)) {
96
+ template = BEGIN_LITERAL + '\n' + template + '\n' + END_LITERAL;
97
+ }
98
+
99
+ // Read existing global CLAUDE.md (or start fresh)
100
+ let existing = '';
101
+ let existed = false;
102
+ try {
103
+ existing = fs.readFileSync(GLOBAL_CLAUDE_MD, 'utf8');
104
+ existed = true;
105
+ } catch { /* file doesn't exist yet */ }
106
+
107
+ // Insert or replace the fenced block (regex match — accepts any prior version).
108
+ const beginMatch = existing.match(BEGIN_RE);
109
+ const endMatch = existing.match(END_RE);
110
+
111
+ let next;
112
+ if (beginMatch && endMatch && endMatch.index > beginMatch.index) {
113
+ next = existing.slice(0, beginMatch.index) + template + existing.slice(endMatch.index + endMatch[0].length);
114
+ } else if (existed) {
115
+ const sep = existing.endsWith('\n\n') ? '' : existing.endsWith('\n') ? '\n' : '\n\n';
116
+ next = existing + sep + template + '\n';
117
+ } else {
118
+ next = '# Claude Code — Machine-Wide Configuration\n\n' +
119
+ '<!-- Add your personal Claude Code context above this line -->\n\n' +
120
+ template + '\n';
121
+ }
122
+
123
+ if (DRY_RUN) {
124
+ console.log('[dry-run] would write to:', GLOBAL_CLAUDE_MD);
125
+ console.log(next);
126
+ process.exit(0);
127
+ }
128
+
129
+ try { fs.mkdirSync(path.dirname(GLOBAL_CLAUDE_MD), { recursive: true }); } catch { /* ignore */ }
130
+
131
+ let orig = '';
132
+ try { orig = fs.readFileSync(GLOBAL_CLAUDE_MD, 'utf8'); } catch { /* ignore */ }
133
+
134
+ if (next !== orig) {
135
+ writeAtomic(GLOBAL_CLAUDE_MD, next);
136
+ const action = beginMatch ? 'Claws global block updated' : 'Claws global block inserted';
137
+ console.log(`~/.claude/CLAUDE.md ${existed ? action : 'created with Claws global block'} (v${VERSION}, ${PHASES.length} phases)`);
138
+ } else {
139
+ console.log(`~/.claude/CLAUDE.md already has the current Claws global block (v${VERSION})`);
140
+ }
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env node
2
+ // Register Claws lifecycle hooks into ~/.claude/settings.json.
3
+ // M-03/M-38: uses json-safe.mjs mergeIntoFile — atomic write, JSONC-tolerant,
4
+ // abort-on-malformed (NEVER silently reset to {}).
5
+ //
6
+ // Usage: node inject-settings-hooks.js [claws-bin-dir] [--dry-run] [--remove]
7
+ //
8
+ // Adds hooks (all tagged _source:"claws" for clean uninstall):
9
+ // SessionStart — emits lifecycle reminder when .claws/claws.sock detected
10
+ // PreToolUse — long-running Bash guard + Edit/Write mcp_server.js guard
11
+ // PreToolUse (MCP spawn-class) — BUG-28: explicit matchers for claws_create /
12
+ // claws_worker / claws_fleet / claws_dispatch_subworker so Monitor arm gate
13
+ // fires even if Claude Code does not propagate the '*' hook to MCP tools.
14
+ // PostToolUse (MCP spawn-class) — Wave C: fail-close spawn → monitor race window.
15
+ // After any spawn-class tool succeeds, waits up to 5s for lifecycle.monitors
16
+ // to register the terminal. If missing, publishes wave.violation + auto-closes.
17
+ // Explicit per-tool matchers (belt-and-suspenders, matching PreToolUse pattern).
18
+ // Stop — kills sidecar, kills tail -F, reminds model to close terminals
19
+ //
20
+ // Idempotent: running twice produces the same result.
21
+ // --remove: strips all _source:"claws" hooks without touching others.
22
+
23
+ 'use strict';
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const os = require('os');
27
+ const { pathToFileURL } = require('url');
28
+
29
+ const DEFAULT_CLAWS_BIN = __dirname;
30
+ const CLAWS_BIN = path.resolve(
31
+ (process.argv[2] && !process.argv[2].startsWith('--'))
32
+ ? process.argv[2]
33
+ : DEFAULT_CLAWS_BIN
34
+ );
35
+
36
+ const DRY_RUN = process.argv.includes('--dry-run');
37
+ const REMOVE = process.argv.includes('--remove');
38
+ // M-18: atomic remove+add in one read-modify-write cycle — eliminates the
39
+ // kill-window where settings.json has zero Claws hooks between --remove and re-add.
40
+ const UPDATE = process.argv.includes('--update');
41
+ const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
42
+ const SOURCE_TAG = 'claws';
43
+
44
+ // W7-2b: conservative orphan-hook detection. Only entries that match the Claws
45
+ // hook script filename pattern OR reference a Windows temp-dir install path are
46
+ // considered orphans. Entries with _source === 'claws' are intentionally excluded
47
+ // here — they are handled by the existing stale-dedup logic below.
48
+ const CLAWS_HOOK_SCRIPT_RE = /(?:session-start|pre-tool-use|pre-bash-no-verify-block|post-tool-use|stop)-claws\.js/;
49
+
50
+ function isOrphanHookEntry(entry) {
51
+ if (entry._source) return false; // any _source means it belongs to a known tool
52
+ const cmd = (entry.hooks && entry.hooks[0] && entry.hooks[0].command) || '';
53
+ return cmd.includes('claws-install-') || CLAWS_HOOK_SCRIPT_RE.test(cmd);
54
+ }
55
+
56
+ function removeOrphanHooks(cfg) {
57
+ if (!cfg.hooks) return 0;
58
+ let count = 0;
59
+ for (const [event, arr] of Object.entries(cfg.hooks)) {
60
+ if (!Array.isArray(arr)) continue;
61
+ cfg.hooks[event] = arr.filter(entry => {
62
+ if (!isOrphanHookEntry(entry)) return true;
63
+ const cmd = entry.hooks[0].command;
64
+ const truncated = cmd.length > 80 ? cmd.slice(0, 77) + '...' : cmd;
65
+ console.log(` removed orphan ${event}: ${truncated}`);
66
+ count++;
67
+ return false;
68
+ });
69
+ }
70
+ return count;
71
+ }
72
+
73
+ // F5: advisory exclusive lock — prevents concurrent inject-settings-hooks
74
+ // invocations (e.g. install.sh + update.sh race) from producing a torn write.
75
+ // 15 attempts × 100ms backoff = 1.5s max wait; handles bursts of concurrent writers.
76
+ const LOCK_PATH = `${SETTINGS_PATH}.lock`;
77
+ async function withLock(fn) {
78
+ let fd;
79
+ for (let attempt = 0; attempt < 15; attempt++) {
80
+ try {
81
+ fd = fs.openSync(LOCK_PATH, 'wx');
82
+ break;
83
+ } catch (e) {
84
+ if (e.code === 'ENOENT') {
85
+ // Parent directory doesn't exist yet — create it (fresh install).
86
+ fs.mkdirSync(path.dirname(LOCK_PATH), { recursive: true });
87
+ try { fd = fs.openSync(LOCK_PATH, 'wx'); break; } catch (e2) {
88
+ if (e2.code !== 'EEXIST') throw e2; // unexpected error
89
+ // else: another process grabbed it after mkdir — fall through to sleep
90
+ }
91
+ } else if (e.code !== 'EEXIST') {
92
+ throw e; // unexpected error (not a lock contention)
93
+ }
94
+ await new Promise(r => setTimeout(r, 100));
95
+ }
96
+ }
97
+ if (fd === undefined) {
98
+ throw new Error(`settings.json lock busy after ${15 * 100}ms — another process may be stuck`);
99
+ }
100
+ try {
101
+ return await fn();
102
+ } finally {
103
+ try { fs.closeSync(fd); } catch { /* ignore */ }
104
+ try { fs.unlinkSync(LOCK_PATH); } catch { /* ignore */ }
105
+ }
106
+ }
107
+
108
+ const HELPERS_URL = pathToFileURL(path.resolve(__dirname, '_helpers', 'json-safe.mjs')).href;
109
+
110
+ // M-15 + F1: canonical install = CLAWS_BIN/hooks/ directory exists AND the
111
+ // specific script file is present. Both checks are required: if only the dir
112
+ // is present but the script is missing (empty hooks/ dir, custom CLAWS_BIN,
113
+ // or post-install deletion), a bare `node` invocation exits non-zero with
114
+ // MODULE_NOT_FOUND — breaking the SAFETY CONTRACT (hooks must never exit
115
+ // non-zero except intentional deny). Fall through to wrapped form instead.
116
+ function isCanonicalInstall(scriptName) {
117
+ try {
118
+ const hooksDir = path.join(CLAWS_BIN, 'hooks');
119
+ return fs.statSync(hooksDir).isDirectory() &&
120
+ fs.existsSync(path.join(hooksDir, scriptName));
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ function hookCmd(scriptName) {
127
+ const scriptPath = path.join(CLAWS_BIN, 'hooks', scriptName);
128
+ if (isCanonicalInstall(scriptName)) {
129
+ // Direct node invocation: hooks/ dir and script both confirmed present.
130
+ // Skips the sh -c wrapper to reduce fork overhead per hook invocation.
131
+ return `node ${JSON.stringify(scriptPath)}`;
132
+ }
133
+ // W7h-30A: Windows has no sh in PATH — emit direct node invocation.
134
+ // The fault-tolerance wrapper (misfire log) is Unix-only; on Windows, simpler
135
+ // direct node is correct and avoids "sh not found" errors breaking hooks silently.
136
+ if (process.platform === 'win32') {
137
+ return `node ${JSON.stringify(scriptPath)}`;
138
+ }
139
+ // Non-canonical or partially-installed: wrap with file-exists guard + misfire logging.
140
+ // Missing path: writes forensic entry to /tmp/claws-hook-misfire.log (2>/dev/null
141
+ // suppresses errors when /tmp is unwritable) AND echoes to stderr so the event is
142
+ // always visible even on read-only filesystems. exit 0 preserves the SAFETY CONTRACT.
143
+ // Path is passed as $0 to avoid shell-escape pitfalls.
144
+ return (
145
+ `sh -c 'if [ -f "$0" ]; then exec node "$0"; ` +
146
+ `else msg="[claws-hook-misfire] $(date -u +%Y-%m-%dT%H:%M:%SZ) missing path: $0"; ` +
147
+ `printf "%s\\n" "$msg" >> /tmp/claws-hook-misfire.log 2>/dev/null; ` +
148
+ `printf "%s\\n" "$msg" >&2; ` +
149
+ `exit 0; fi' ${JSON.stringify(scriptPath)}`
150
+ );
151
+ }
152
+
153
+ function makeHookEntry(matcher, scriptName) {
154
+ return {
155
+ matcher,
156
+ _source: SOURCE_TAG,
157
+ hooks: [{ type: 'command', command: hookCmd(scriptName) }],
158
+ };
159
+ }
160
+
161
+ (async () => {
162
+ const { mergeIntoFile, parseJsonSafe } = await import(HELPERS_URL);
163
+
164
+ const HOOKS_TO_ADD = [
165
+ // SessionStart: emits lifecycle reminder + spawns stream-events.js sidecar (idempotent)
166
+ { event: 'SessionStart', scriptName: 'session-start-claws.js', entry: makeHookEntry('*', 'session-start-claws.js') },
167
+ // PreToolUse '*': Bash long-running guard + Edit/Write mcp_server.js guard
168
+ { event: 'PreToolUse', scriptName: 'pre-tool-use-claws.js', entry: makeHookEntry('*', 'pre-tool-use-claws.js') },
169
+ // F32 (v0.7.13-hook-p1): block git commit --no-verify and similar bypass flags.
170
+ // advisory-mechanism-audit Finding F32: highest-ROI hook fix. Workers that try
171
+ // to skip pre-commit hooks (tests, lint, type checks) get a hard rejection.
172
+ { event: 'PreToolUse', scriptName: 'pre-bash-no-verify-block.js', entry: makeHookEntry('Bash', 'pre-bash-no-verify-block.js') },
173
+ // W7h-30C / BUG-28: explicit MCP spawn-class PreToolUse matchers — belt-and-suspenders
174
+ // for Claude Code builds where '*' is not propagated to MCP tool calls. The server-side
175
+ // gate in mcp_server.js (T4) remains the primary enforcer; these hooks fire even when
176
+ // the '*' matcher misses, ensuring Monitor-arm enforcement on Windows and older builds.
177
+ { event: 'PreToolUse', scriptName: 'pre-tool-use-claws.js', entry: makeHookEntry('mcp__claws__claws_create', 'pre-tool-use-claws.js') },
178
+ { event: 'PreToolUse', scriptName: 'pre-tool-use-claws.js', entry: makeHookEntry('mcp__claws__claws_worker', 'pre-tool-use-claws.js') },
179
+ { event: 'PreToolUse', scriptName: 'pre-tool-use-claws.js', entry: makeHookEntry('mcp__claws__claws_fleet', 'pre-tool-use-claws.js') },
180
+ { event: 'PreToolUse', scriptName: 'pre-tool-use-claws.js', entry: makeHookEntry('mcp__claws__claws_dispatch_subworker', 'pre-tool-use-claws.js') },
181
+ // Wave C: PostToolUse spawn-class matchers — fail-close the spawn → monitor race window.
182
+ // Explicit per-tool matchers (belt-and-suspenders, same rationale as PreToolUse above).
183
+ // After each successful spawn, waits up to ~5s for lifecycle.monitors to register the
184
+ // terminal. If missing, publishes wave.violation and auto-closes the orphaned terminal.
185
+ { event: 'PostToolUse', scriptName: 'post-tool-use-claws.js', entry: makeHookEntry('mcp__claws__claws_create', 'post-tool-use-claws.js') },
186
+ { event: 'PostToolUse', scriptName: 'post-tool-use-claws.js', entry: makeHookEntry('mcp__claws__claws_worker', 'post-tool-use-claws.js') },
187
+ { event: 'PostToolUse', scriptName: 'post-tool-use-claws.js', entry: makeHookEntry('mcp__claws__claws_fleet', 'post-tool-use-claws.js') },
188
+ { event: 'PostToolUse', scriptName: 'post-tool-use-claws.js', entry: makeHookEntry('mcp__claws__claws_dispatch_subworker', 'post-tool-use-claws.js') },
189
+ // Stop: kills stream-events.js sidecar + reminds model to close terminals / complete REFLECT
190
+ { event: 'Stop', scriptName: 'stop-claws.js', entry: makeHookEntry('*', 'stop-claws.js') },
191
+ ];
192
+
193
+ if (DRY_RUN) {
194
+ let raw = '{}';
195
+ try { raw = fs.readFileSync(SETTINGS_PATH, 'utf8'); } catch (e) { if (e.code !== 'ENOENT') throw e; }
196
+ const parsed = parseJsonSafe(raw, { allowJsonc: true });
197
+ if (!parsed.ok) {
198
+ console.error('[claws] settings.json is malformed — dry-run aborted (file unchanged).');
199
+ console.error(' Error:', parsed.error.message);
200
+ process.exit(1);
201
+ }
202
+ const cfg = parsed.data;
203
+ if (!cfg.hooks) cfg.hooks = {};
204
+ removeOrphanHooks(cfg);
205
+ for (const { event, scriptName, entry } of HOOKS_TO_ADD) {
206
+ if (!cfg.hooks[event]) cfg.hooks[event] = [];
207
+ const arr = cfg.hooks[event];
208
+ // Dry-run: use same exact-match dedup as live path (M-14)
209
+ const exactDryIdx = arr.findIndex(e =>
210
+ e._source === SOURCE_TAG && e.matcher === entry.matcher &&
211
+ e.hooks && e.hooks[0] && e.hooks[0].command === entry.hooks[0].command
212
+ );
213
+ if (exactDryIdx === -1) {
214
+ const staleDryIdx = arr.findIndex(e =>
215
+ e._source === SOURCE_TAG && e.matcher === entry.matcher && e.hooks && e.hooks[0]
216
+ );
217
+ if (staleDryIdx !== -1) arr[staleDryIdx] = entry;
218
+ else arr.push(entry);
219
+ }
220
+ }
221
+ console.log('[dry-run] would write to:', SETTINGS_PATH);
222
+ console.log(JSON.stringify(cfg, null, 2));
223
+ return;
224
+ }
225
+
226
+ if (REMOVE) {
227
+ let removed = 0;
228
+ const result = await withLock(() => mergeIntoFile(SETTINGS_PATH, (cfg) => {
229
+ if (!cfg.hooks) return;
230
+ removed += removeOrphanHooks(cfg);
231
+ for (const [event, arr] of Object.entries(cfg.hooks)) {
232
+ if (!Array.isArray(arr)) continue;
233
+ const filtered = arr.filter(e => e._source !== SOURCE_TAG);
234
+ removed += arr.length - filtered.length;
235
+ cfg.hooks[event] = filtered;
236
+ }
237
+ }));
238
+ if (!result.ok) {
239
+ console.error('[claws] Failed to update settings.json:', result.error.message);
240
+ if (result.error.backupSavedAt) {
241
+ console.error(' Original backed up to:', result.error.backupSavedAt);
242
+ console.error(' Aborting — your settings.json is unchanged.');
243
+ }
244
+ process.exit(1);
245
+ }
246
+ console.log(`Removed ${removed} Claws hook(s) from ${SETTINGS_PATH}`);
247
+ return;
248
+ }
249
+
250
+ // M-18: --update does remove-then-add atomically in one mergeIntoFile call.
251
+ // No kill-window where settings.json has zero Claws hooks.
252
+ if (UPDATE) {
253
+ let removed = 0;
254
+ let changed = 0;
255
+ const result = await withLock(() => mergeIntoFile(SETTINGS_PATH, (cfg) => {
256
+ // FINDING-B-1: migrate legacy flat-array hooks format → object-keyed format.
257
+ // Early Claude Code versions stored hooks as [{event:"SessionStart",...}, ...].
258
+ // JSON.stringify of a named-property-on-array drops the new hooks silently.
259
+ if (Array.isArray(cfg.hooks)) {
260
+ const legacyArr = cfg.hooks;
261
+ cfg.hooks = {};
262
+ for (const entry of legacyArr) {
263
+ if (!entry.event) continue;
264
+ if (entry._source === SOURCE_TAG) { removed++; continue; } // drop old claws hooks
265
+ if (!cfg.hooks[entry.event]) cfg.hooks[entry.event] = [];
266
+ const { event: _ev, ...rest } = entry;
267
+ cfg.hooks[entry.event].push(rest);
268
+ }
269
+ }
270
+ if (!cfg.hooks) cfg.hooks = {};
271
+ // W7-2b: clean orphan hooks before the remove+add cycle
272
+ removed += removeOrphanHooks(cfg);
273
+ // Step 1: remove all existing Claws hooks
274
+ for (const [event, arr] of Object.entries(cfg.hooks)) {
275
+ if (!Array.isArray(arr)) continue;
276
+ const filtered = arr.filter(e => e._source !== SOURCE_TAG);
277
+ removed += arr.length - filtered.length;
278
+ cfg.hooks[event] = filtered;
279
+ }
280
+ // Step 2: add current Claws hooks
281
+ for (const { event, entry } of HOOKS_TO_ADD) {
282
+ if (!cfg.hooks[event]) cfg.hooks[event] = [];
283
+ cfg.hooks[event].push(entry);
284
+ changed++;
285
+ }
286
+ }));
287
+ if (!result.ok) {
288
+ console.error('[claws] Failed to update settings.json:', result.error.message);
289
+ if (result.error.backupSavedAt) {
290
+ console.error(' Original backed up to:', result.error.backupSavedAt);
291
+ console.error(' Aborting — your settings.json is unchanged.');
292
+ }
293
+ process.exit(1);
294
+ }
295
+ console.log(`Updated Claws hooks in ${SETTINGS_PATH} (removed ${removed}, added ${changed})`);
296
+ return;
297
+ }
298
+
299
+ // Add/update hooks
300
+ let changed = 0;
301
+ const result = await withLock(() => mergeIntoFile(SETTINGS_PATH, (cfg) => {
302
+ // FINDING-B-1: migrate legacy flat-array format before add/dedup logic.
303
+ if (Array.isArray(cfg.hooks)) {
304
+ const legacyArr = cfg.hooks;
305
+ cfg.hooks = {};
306
+ for (const entry of legacyArr) {
307
+ if (!entry.event) continue;
308
+ if (!cfg.hooks[entry.event]) cfg.hooks[entry.event] = [];
309
+ const { event: _ev, ...rest } = entry;
310
+ cfg.hooks[entry.event].push(rest);
311
+ }
312
+ }
313
+ if (!cfg.hooks) cfg.hooks = {};
314
+ // W7-2b: remove orphan entries from pre-fix installs before dedup logic
315
+ removeOrphanHooks(cfg);
316
+ for (const { event, scriptName, entry } of HOOKS_TO_ADD) {
317
+ if (!cfg.hooks[event]) cfg.hooks[event] = [];
318
+ const arr = cfg.hooks[event];
319
+
320
+ // M-14: exact-command equality for dedup. The _source === 'claws' guard
321
+ // was already present before M-14 and prevented non-Claws hooks from
322
+ // matching (findIndex short-circuits on _source). M-14's actual improvement:
323
+ // replace substring command.includes(scriptName) with strict equality,
324
+ // making "already current" (no-op) vs "stale/old-format Claws entry"
325
+ // (upgrade in-place via staleIdx) unambiguous and cleaner.
326
+ const exactIdx = arr.findIndex(e =>
327
+ e._source === SOURCE_TAG &&
328
+ e.matcher === entry.matcher &&
329
+ e.hooks && e.hooks[0] &&
330
+ e.hooks[0].command === entry.hooks[0].command
331
+ );
332
+
333
+ if (exactIdx !== -1) {
334
+ // Already current — no-op
335
+ } else {
336
+ // Check for a stale Claws entry to upgrade (different command, same source+matcher)
337
+ const staleIdx = arr.findIndex(e =>
338
+ e._source === SOURCE_TAG &&
339
+ e.matcher === entry.matcher &&
340
+ e.hooks && e.hooks[0]
341
+ );
342
+ if (staleIdx !== -1) {
343
+ arr[staleIdx] = entry;
344
+ changed++;
345
+ } else {
346
+ arr.push(entry);
347
+ changed++;
348
+ }
349
+ }
350
+ }
351
+ }));
352
+
353
+ if (!result.ok) {
354
+ console.error('[claws] Failed to update settings.json:', result.error.message);
355
+ if (result.error.backupSavedAt) {
356
+ console.error(' Original backed up to:', result.error.backupSavedAt);
357
+ console.error(' Aborting — your settings.json is unchanged.');
358
+ }
359
+ process.exit(1);
360
+ }
361
+
362
+ if (changed > 0) {
363
+ console.log(`Added ${changed} Claws hook(s) to ${SETTINGS_PATH}`);
364
+ } else {
365
+ console.log('Claws hooks already present in settings.json');
366
+ }
367
+ })().catch(e => {
368
+ console.error('[claws] inject-settings-hooks unexpected error:', e.message);
369
+ process.exit(1);
370
+ });