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,343 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+ const { dryRunLog, getDefaultShellRcFile } = require('./platform.js');
8
+
9
+ // Canonical marker — matches install.sh's HOOK_MARKER exactly.
10
+ const MARKER = '# CLAWS terminal hook';
11
+
12
+ // Legacy format from old Node installer. Stripped as a one-time migration during inject.
13
+ const LEGACY_BEGIN = '# >>> claws-code shell hook >>>';
14
+ const LEGACY_END = '# <<< claws-code shell hook <<<';
15
+
16
+ /**
17
+ * Returns the list of standard shell rc files to inject into.
18
+ * macOS: ~/.zshrc, ~/.bashrc, ~/.bash_profile
19
+ * Linux: ~/.zshrc, ~/.bashrc
20
+ * win32: [] (no-op on Windows)
21
+ * Matches install.sh's multi-file injection (lines 1419–1433).
22
+ * @param {object} [opts]
23
+ * @returns {string[]}
24
+ */
25
+ function _getStandardRcFiles(opts = {}) {
26
+ const platform = opts.platform !== undefined ? opts.platform : process.platform;
27
+ const home = opts.home !== undefined ? opts.home : os.homedir();
28
+
29
+ if (platform === 'win32') return [];
30
+
31
+ const files = [
32
+ path.join(home, '.zshrc'),
33
+ path.join(home, '.bashrc'),
34
+ ];
35
+ if (platform === 'darwin') {
36
+ files.push(path.join(home, '.bash_profile'));
37
+ }
38
+ return files;
39
+ }
40
+
41
+ /**
42
+ * Inject (or replace) the claws shell hook block in all standard rc files.
43
+ * On macOS: ~/.zshrc, ~/.bashrc, ~/.bash_profile.
44
+ * On Linux: ~/.zshrc, ~/.bashrc.
45
+ * Also handles fish (conf.d/claws.fish) and nushell (env.nu) when present.
46
+ * Idempotent per file.
47
+ * @param {string} installDir - path to the claws repo (contains scripts/shell-hook.sh)
48
+ * @param {boolean} [dryRun]
49
+ */
50
+ function injectShellHook(installDir, dryRun = false) {
51
+ if (process.platform === 'win32') {
52
+ _injectPowershellHook(installDir, dryRun);
53
+ return;
54
+ }
55
+ const rcFiles = _getStandardRcFiles();
56
+ for (const rcFile of rcFiles) {
57
+ _injectIntoFile(rcFile, installDir, dryRun);
58
+ }
59
+ _injectFishHook(installDir, dryRun);
60
+ _injectNushellHook(installDir, dryRun);
61
+ }
62
+
63
+ /**
64
+ * Write (or overwrite) ~/.config/fish/conf.d/claws.fish when the fish
65
+ * config directory exists. Fish sources conf.d/ automatically on startup.
66
+ * Matches install.sh lines 1435-1448.
67
+ * @param {string} installDir
68
+ * @param {boolean} [dryRun]
69
+ */
70
+ function _injectFishHook(installDir, dryRun) {
71
+ const home = os.homedir();
72
+ if (!fs.existsSync(path.join(home, '.config', 'fish'))) return;
73
+
74
+ const confD = path.join(home, '.config', 'fish', 'conf.d');
75
+ const fishFile = path.join(confD, 'claws.fish');
76
+ const hookFish = path.join(installDir, 'scripts', 'shell-hook.fish');
77
+
78
+ const content = [
79
+ '# CLAWS terminal hook (auto-generated — do not edit)',
80
+ `set -gx CLAWS_DIR '${installDir}'`,
81
+ `set -gx CLAWS_SOCKET '.claws/claws.sock'`,
82
+ `if test -f '${hookFish}'`,
83
+ ` source '${hookFish}'`,
84
+ 'end',
85
+ '',
86
+ ].join('\n');
87
+
88
+ if (dryRun) {
89
+ dryRunLog(`write fish hook to ${fishFile}`);
90
+ return;
91
+ }
92
+
93
+ fs.mkdirSync(confD, { recursive: true });
94
+ const tmp = fishFile + '.claws-tmp.' + process.pid;
95
+ fs.writeFileSync(tmp, content, 'utf8');
96
+ fs.renameSync(tmp, fishFile);
97
+ process.stdout.write(` \x1b[32m✓\x1b[0m fish hook written to ${fishFile}\n`);
98
+ }
99
+
100
+ /**
101
+ * Append the CLAWS_DIR assignment to nushell env.nu (or config.nu) when
102
+ * either file exists. Idempotent: skips if CLAWS_DIR already present.
103
+ * Matches install.sh lines 1450-1466.
104
+ * @param {string} installDir
105
+ * @param {boolean} [dryRun]
106
+ */
107
+ function _injectNushellHook(installDir, dryRun) {
108
+ const home = os.homedir();
109
+ const nuEnv = path.join(home, '.config', 'nushell', 'env.nu');
110
+ const nuConfig = path.join(home, '.config', 'nushell', 'config.nu');
111
+
112
+ let target = null;
113
+ if (fs.existsSync(nuEnv)) target = nuEnv;
114
+ else if (fs.existsSync(nuConfig)) target = nuConfig;
115
+ if (!target) return;
116
+
117
+ if (dryRun) {
118
+ dryRunLog(`append CLAWS_DIR to nushell ${path.basename(target)}`);
119
+ return;
120
+ }
121
+
122
+ const existing = fs.readFileSync(target, 'utf8');
123
+ if (existing.includes('CLAWS_DIR')) {
124
+ process.stdout.write(` \x1b[2m${path.basename(target)} already has CLAWS_DIR — skipped\x1b[0m\n`);
125
+ return;
126
+ }
127
+
128
+ const append = `\n${MARKER}\n$env.CLAWS_DIR = "${installDir}"\n$env.CLAWS_SOCKET = ".claws/claws.sock"\n`;
129
+ fs.appendFileSync(target, append, 'utf8');
130
+ process.stdout.write(` \x1b[32m✓\x1b[0m nushell env written to ${target}\n`);
131
+ }
132
+
133
+ /**
134
+ * Remove the claws shell hook block from rcFile.
135
+ * Handles both canonical install.sh format and legacy >>>...<<< format.
136
+ * @param {string} rcFile
137
+ * @param {boolean} [dryRun]
138
+ */
139
+ function removeShellHook(rcFile, dryRun = false) {
140
+ if (!fs.existsSync(rcFile)) return;
141
+
142
+ if (dryRun) {
143
+ dryRunLog(`remove shell hook from ${rcFile}`);
144
+ return;
145
+ }
146
+
147
+ const original = fs.readFileSync(rcFile, 'utf8');
148
+ const cleaned = _removePriorBlock(original);
149
+ if (cleaned === original) return;
150
+
151
+ const tmp = rcFile + '.claws-tmp.' + process.pid;
152
+ fs.writeFileSync(tmp, cleaned, 'utf8');
153
+ fs.renameSync(tmp, rcFile);
154
+ }
155
+
156
+ /**
157
+ * Inject the Claws shell hook into the PowerShell profile on win32.
158
+ * Copies shell-hook.ps1 to a stable location ($HOME/.claude/claws/) so that
159
+ * $PROFILE never references the install-time temp dir (which is deleted post-install).
160
+ * Uses the same `# CLAWS terminal hook` marker as the bash installer.
161
+ * @param {string} installDir - path to the claws repo (contains scripts/shell-hook.ps1)
162
+ * @param {boolean} [dryRun]
163
+ * @param {object} [opts] - internal overrides for testing
164
+ * @param {string} [opts.home] - override os.homedir()
165
+ * @param {Function}[opts.execFn] - override execSync for powershell profile lookup
166
+ */
167
+ function _injectPowershellHook(installDir, dryRun, opts = {}) {
168
+ const home = opts.home !== undefined ? opts.home : os.homedir();
169
+ const profilePath = getDefaultShellRcFile({ platform: 'win32', home, execFn: opts.execFn });
170
+ const hookPs1 = path.join(installDir, 'scripts', 'shell-hook.ps1');
171
+
172
+ // Stable location: $HOME/.claude/claws/shell-hook.ps1 — one level above the lifecycle
173
+ // hooks dir (~/.claude/claws/hooks/). $PROFILE sources this stable copy so it survives
174
+ // the install temp-dir deletion (root cause of W7-4B).
175
+ const stableDir = path.join(home, '.claude', 'claws');
176
+ const stableHookPs1 = path.join(stableDir, 'shell-hook.ps1');
177
+
178
+ // PowerShell dot-source syntax: . "absolute\path\to\shell-hook.ps1"
179
+ const block = `\n${MARKER}\n. "${stableHookPs1}"\n`;
180
+
181
+ if (dryRun) {
182
+ dryRunLog(`copy shell-hook.ps1 to ${stableHookPs1}`);
183
+ dryRunLog(`inject PS shell hook into ${profilePath}`);
184
+ return;
185
+ }
186
+
187
+ // Copy hook to stable dir before writing $PROFILE entry.
188
+ fs.mkdirSync(stableDir, { recursive: true });
189
+ if (fs.existsSync(hookPs1)) {
190
+ fs.copyFileSync(hookPs1, stableHookPs1);
191
+ process.stdout.write(` \x1b[32m✓\x1b[0m shell-hook.ps1 copied to ${stableHookPs1}\n`);
192
+ }
193
+
194
+ fs.mkdirSync(path.dirname(profilePath), { recursive: true });
195
+ if (!fs.existsSync(profilePath)) {
196
+ fs.writeFileSync(profilePath, '', 'utf8');
197
+ }
198
+
199
+ // Backup before modification — mirrors _injectIntoFile's backup pattern.
200
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
201
+ const backup = profilePath + '.claws-bak.' + ts;
202
+ try {
203
+ fs.copyFileSync(profilePath, backup);
204
+ } catch (e) {
205
+ process.stderr.write(` ! Warning: could not backup ${profilePath}: ${e.message}\n`);
206
+ }
207
+
208
+ const original = fs.readFileSync(profilePath, 'utf8');
209
+ const cleaned = _removePriorBlock(original);
210
+ const updated = cleaned + block;
211
+
212
+ const tmp = profilePath + '.claws-tmp.' + process.pid;
213
+ fs.writeFileSync(tmp, updated, 'utf8');
214
+ fs.renameSync(tmp, profilePath);
215
+ process.stdout.write(` \x1b[32m✓\x1b[0m PS profile updated: ${profilePath}\n`);
216
+ }
217
+
218
+ /**
219
+ * Return the shell binary to use for syntax-checking rcFile, or null if
220
+ * no check is applicable. Mirrors install.sh lines 1420-1432 (use zsh for
221
+ * .zshrc to avoid false positives with zsh-only syntax like setopt/autoload).
222
+ * @param {string} rcFile
223
+ * @returns {string|null}
224
+ */
225
+ function _getValidatorShell(rcFile) {
226
+ const base = path.basename(rcFile);
227
+ if (base === '.zshrc') return 'zsh';
228
+ if (base === '.bashrc' || base === '.bash_profile') return 'bash';
229
+ return null;
230
+ }
231
+
232
+ function _injectIntoFile(rcFile, installDir, dryRun) {
233
+ const hookSh = path.join(installDir, 'scripts', 'shell-hook.sh');
234
+ // Canonical format matches install.sh: marker line + source line (no closing marker).
235
+ const block = `\n${MARKER}\nsource "${hookSh}"\n`;
236
+
237
+ if (dryRun) {
238
+ dryRunLog(`backup ${rcFile}`);
239
+ dryRunLog(`inject shell hook into ${rcFile}`);
240
+ return;
241
+ }
242
+
243
+ if (!fs.existsSync(rcFile)) {
244
+ fs.mkdirSync(path.dirname(rcFile), { recursive: true });
245
+ fs.writeFileSync(rcFile, '', 'utf8');
246
+ }
247
+
248
+ // Backup before modification — matches install.sh lines 1360-1369.
249
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
250
+ const backup = rcFile + '.claws-bak.' + ts;
251
+ try {
252
+ fs.copyFileSync(rcFile, backup);
253
+ } catch (e) {
254
+ process.stderr.write(` ! Warning: could not backup ${rcFile}: ${e.message}\n`);
255
+ }
256
+
257
+ const original = fs.readFileSync(rcFile, 'utf8');
258
+ const cleaned = _removePriorBlock(original);
259
+ const updated = cleaned + block;
260
+
261
+ const tmp = rcFile + '.claws-tmp.' + process.pid;
262
+ fs.writeFileSync(tmp, updated, 'utf8');
263
+ fs.renameSync(tmp, rcFile);
264
+
265
+ // Syntax validation — matches install.sh lines 1423-1432.
266
+ const shellBin = _getValidatorShell(rcFile);
267
+ if (shellBin) {
268
+ const check = spawnSync(shellBin, ['-n', rcFile], { encoding: 'utf8', stdio: 'pipe' });
269
+ if (check.status !== 0) {
270
+ process.stderr.write(
271
+ ` ! Warning: ${path.basename(rcFile)} failed ${shellBin} -n check — backup at ${backup}\n`
272
+ );
273
+ }
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Remove any prior Claws shell hook block from content.
279
+ * Handles both formats:
280
+ * - Canonical (install.sh): "# CLAWS terminal hook" marker + following source line
281
+ * - Legacy (old Node installer): "# >>> claws-code shell hook >>>" ... "# <<< ... <<<"
282
+ * Mirrors the awk cleanup pattern in install.sh's inject_hook():
283
+ * /# CLAWS terminal hook/ { skip = 1; next }
284
+ * skip && /source.*shell-hook\.sh/ { skip = 0; next }
285
+ * skip { skip = 0; print }
286
+ * { print }
287
+ * @param {string} content
288
+ * @returns {string}
289
+ */
290
+ function _removePriorBlock(content) {
291
+ // Step 1: one-time migration — strip legacy >>>...<<< block if present.
292
+ content = _removeLegacyBlock(content);
293
+
294
+ // Step 2: strip canonical format — marker line + following source line.
295
+ const lines = content.split('\n');
296
+ const out = [];
297
+ let skip = false;
298
+ for (const line of lines) {
299
+ if (/# CLAWS terminal hook/.test(line)) {
300
+ skip = true;
301
+ continue;
302
+ }
303
+ if (skip && /shell-hook\.(sh|ps1)/.test(line)) {
304
+ skip = false;
305
+ continue;
306
+ }
307
+ if (skip) {
308
+ // Marker found but next line is not a source line — keep it, stop skipping.
309
+ skip = false;
310
+ out.push(line);
311
+ continue;
312
+ }
313
+ out.push(line);
314
+ }
315
+ return out.join('\n').replace(/\n+$/, '');
316
+ }
317
+
318
+ /**
319
+ * Strip the legacy Node-installer >>>...<<< block from content.
320
+ * @param {string} content
321
+ * @returns {string}
322
+ */
323
+ function _removeLegacyBlock(content) {
324
+ const beginRe = /^# >>> claws-code shell hook >>>$/m;
325
+ const endRe = /^# <<< claws-code shell hook <<<$/m;
326
+
327
+ const beginIdx = content.search(beginRe);
328
+ if (beginIdx === -1) return content;
329
+
330
+ const afterBegin = content.slice(beginIdx);
331
+ const endMatch = afterBegin.match(endRe);
332
+ if (!endMatch) return content;
333
+
334
+ const endIdx = beginIdx + afterBegin.indexOf(endMatch[0]) + endMatch[0].length;
335
+ return content.slice(0, beginIdx).replace(/\n+$/, '') + content.slice(endIdx);
336
+ }
337
+
338
+ module.exports = {
339
+ injectShellHook, removeShellHook,
340
+ _getStandardRcFiles, _getValidatorShell,
341
+ _injectFishHook, _injectNushellHook, _injectPowershellHook,
342
+ _injectIntoFile, _removePriorBlock, _removeLegacyBlock,
343
+ };
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ const { findCodeCli, getDefaultShellRcFile, dryRunLog } = require('./platform.js');
9
+ const { sweepCommands, sweepSkills } = require('./capabilities.js');
10
+ const { removeShellHook } = require('./shell-hook.js');
11
+ const { removeMcpEntry } = require('./mcp-setup.js');
12
+
13
+ const HOME = os.homedir();
14
+ const REPO_ROOT = path.resolve(__dirname, '..');
15
+
16
+ function _step(label) { process.stdout.write(`\n\x1b[1m${label}\x1b[0m\n`); }
17
+ function _ok(msg) { process.stdout.write(` \x1b[32m✓\x1b[0m ${msg}\n`); }
18
+ function _warn(msg) { process.stdout.write(` \x1b[33m!\x1b[0m ${msg}\n`); }
19
+
20
+ /**
21
+ * Reverse of install: remove all Claws artifacts from the current project and ~/.claude/.
22
+ * @param {object} [opts]
23
+ * @param {boolean} [opts.dryRun]
24
+ */
25
+ function run(opts = {}) {
26
+ const { dryRun = false } = opts;
27
+ const projectRoot = process.cwd();
28
+ const claudeDir = path.join(HOME, '.claude');
29
+
30
+ process.stdout.write('\nUninstalling Claws...\n');
31
+
32
+ // 1. VS Code extension
33
+ _step('Uninstall VS Code extension');
34
+ _uninstallExtension(dryRun);
35
+
36
+ // 2. .claws-bin/
37
+ _step('Remove .claws-bin/');
38
+ const clawsBin = path.join(projectRoot, '.claws-bin');
39
+ if (dryRun) {
40
+ dryRunLog(`rm -rf ${clawsBin}`);
41
+ } else if (fs.existsSync(clawsBin)) {
42
+ fs.rmSync(clawsBin, { recursive: true });
43
+ _ok('.claws-bin/ removed');
44
+ }
45
+
46
+ // 3. .mcp.json claws entry
47
+ _step('Remove claws entry from .mcp.json');
48
+ removeMcpEntry(projectRoot, dryRun);
49
+ if (!dryRun) _ok('.mcp.json updated');
50
+
51
+ // 4. CLAUDE.md CLAWS:BEGIN block (project)
52
+ _step('Remove CLAWS:BEGIN block from CLAUDE.md');
53
+ _removeClawsBlock(
54
+ path.join(projectRoot, 'CLAUDE.md'),
55
+ /<!-- CLAWS:BEGIN(?:[^>]*)-->/,
56
+ /<!-- CLAWS:END(?:[^>]*)-->/,
57
+ dryRun
58
+ );
59
+
60
+ // 5. ~/.claude/CLAUDE.md CLAWS-GLOBAL:BEGIN block
61
+ _step('Remove CLAWS-GLOBAL:BEGIN block from ~/.claude/CLAUDE.md');
62
+ _removeClawsBlock(
63
+ path.join(HOME, '.claude', 'CLAUDE.md'),
64
+ /<!-- CLAWS-GLOBAL:BEGIN(?:[^>]*)-->/,
65
+ /<!-- CLAWS-GLOBAL:END(?:[^>]*)-->/,
66
+ dryRun
67
+ );
68
+
69
+ // 6. Hooks from settings.json
70
+ _step('Remove hooks from ~/.claude/settings.json');
71
+ _removeHooks(dryRun);
72
+
73
+ // 7. claws-default-behavior.md rule
74
+ _step('Remove claws-default-behavior.md');
75
+ const rulePath = path.join(claudeDir, 'rules', 'claws-default-behavior.md');
76
+ if (dryRun) {
77
+ dryRunLog(`rm ${rulePath}`);
78
+ } else if (fs.existsSync(rulePath)) {
79
+ fs.rmSync(rulePath);
80
+ _ok('claws-default-behavior.md removed');
81
+ }
82
+
83
+ // 8. Commands + skills (global)
84
+ _step('Remove commands and skills from ~/.claude/');
85
+ sweepCommands(path.join(claudeDir, 'commands'), dryRun);
86
+ sweepSkills(path.join(claudeDir, 'skills'), dryRun);
87
+ if (!dryRun) _ok('Commands and skills removed');
88
+
89
+ // 9. Shell hook
90
+ _step('Remove shell hook');
91
+ const rcFile = getDefaultShellRcFile();
92
+ removeShellHook(rcFile, dryRun);
93
+ if (!dryRun) _ok(`Shell hook removed from ${rcFile}`);
94
+
95
+ process.stdout.write('\n \x1b[32m✓ Claws uninstalled\x1b[0m\n\n');
96
+ }
97
+
98
+ function _uninstallExtension(dryRun) {
99
+ const codeCli = findCodeCli();
100
+ if (!codeCli) {
101
+ _warn('VS Code CLI not found — extension may still be installed; remove manually');
102
+ return;
103
+ }
104
+
105
+ if (dryRun) { dryRunLog(`${codeCli} --uninstall-extension neunaha.claws`); return; }
106
+
107
+ const r = spawnSync(codeCli, ['--uninstall-extension', 'neunaha.claws'], {
108
+ stdio: 'inherit', encoding: 'utf8',
109
+ });
110
+ if (r.status === 0) { _ok('VS Code extension uninstalled'); return; }
111
+
112
+ // CLI failed — remove extension directory directly
113
+ const extDirs = [
114
+ path.join(HOME, '.vscode', 'extensions'),
115
+ path.join(HOME, '.cursor', 'extensions'),
116
+ ];
117
+ let removed = false;
118
+ for (const dir of extDirs) {
119
+ if (!fs.existsSync(dir)) continue;
120
+ for (const entry of fs.readdirSync(dir).filter(f => f.startsWith('neunaha.claws-'))) {
121
+ fs.rmSync(path.join(dir, entry), { recursive: true });
122
+ removed = true;
123
+ }
124
+ }
125
+ if (removed) { _ok('Extension directory removed'); } else { _warn('Extension not found in extensions dirs'); }
126
+ }
127
+
128
+ function _removeClawsBlock(filePath, beginRe, endRe, dryRun) {
129
+ if (!fs.existsSync(filePath)) return;
130
+
131
+ if (dryRun) { dryRunLog(`remove claws block from ${filePath}`); return; }
132
+
133
+ let content = fs.readFileSync(filePath, 'utf8');
134
+ const beginMatch = content.match(beginRe);
135
+ if (!beginMatch) return;
136
+
137
+ const beginIdx = content.indexOf(beginMatch[0]);
138
+ const endMatch = content.match(endRe);
139
+ if (!endMatch) return;
140
+
141
+ const endIdx = content.lastIndexOf(endMatch[0]) + endMatch[0].length;
142
+ content = (content.slice(0, beginIdx) + content.slice(endIdx)).replace(/\n{3,}/g, '\n\n');
143
+
144
+ const tmp = filePath + '.claws-tmp.' + process.pid;
145
+ fs.writeFileSync(tmp, content, 'utf8');
146
+ fs.renameSync(tmp, filePath);
147
+ _ok(`CLAWS block removed from ${path.basename(filePath)}`);
148
+ }
149
+
150
+ function _removeHooks(dryRun) {
151
+ const script = path.join(REPO_ROOT, 'scripts', 'inject-settings-hooks.js');
152
+ const hooksDir = path.join(REPO_ROOT, 'scripts');
153
+ const extraArgs = dryRun ? [hooksDir, '--dry-run', '--remove'] : [hooksDir, '--remove'];
154
+
155
+ if (!fs.existsSync(script)) { _warn('inject-settings-hooks.js not found — hooks not removed'); return; }
156
+
157
+ spawnSync(process.execPath, [script, ...extraArgs], {
158
+ cwd: REPO_ROOT, stdio: 'inherit', encoding: 'utf8',
159
+ });
160
+ }
161
+
162
+ module.exports = { run };
package/lib/verify.js ADDED
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { spawnSync, spawn } = require('child_process');
7
+
8
+ /**
9
+ * Post-install verification. Returns array of failure strings (empty = OK).
10
+ * All checks use project-local paths under projectRoot/.claude/ — matching
11
+ * where lib/install.js (W7-6) places skills and rules.
12
+ * @param {string} projectRoot
13
+ * @returns {string[]}
14
+ */
15
+ function verify(projectRoot) {
16
+ const failures = [];
17
+
18
+ if (!fs.existsSync(path.join(projectRoot, '.claws-bin', 'mcp_server.js'))) {
19
+ failures.push('.claws-bin/mcp_server.js missing');
20
+ }
21
+ if (!fs.existsSync(path.join(projectRoot, '.mcp.json'))) {
22
+ failures.push('.mcp.json missing');
23
+ }
24
+ if (!fs.existsSync(path.join(projectRoot, '.claude', 'commands', 'claws.md'))) {
25
+ failures.push('claws commands missing from .claude/commands/');
26
+ }
27
+ if (!fs.existsSync(path.join(projectRoot, '.claude', 'skills', 'claws-prompt-templates'))) {
28
+ failures.push('claws skills missing from .claude/skills/');
29
+ }
30
+ if (!fs.existsSync(path.join(projectRoot, '.claude', 'rules', 'claws-default-behavior.md'))) {
31
+ failures.push('claws rule missing from .claude/rules/');
32
+ }
33
+
34
+ // W7h-28: Live MCP server handshake test (matches install.sh:1540-1557).
35
+ // Spawns mcp_server.js, sends an initialize JSON-RPC request, expects
36
+ // a response containing "claws" within 3s.
37
+ const mcpServer = path.join(projectRoot, '.claws-bin', 'mcp_server.js');
38
+ if (fs.existsSync(mcpServer)) {
39
+ const handshakeResult = _mcpHandshake(mcpServer);
40
+ if (!handshakeResult) {
41
+ failures.push(`MCP server failed to respond — run: node ${mcpServer}`);
42
+ }
43
+ }
44
+
45
+ // W7h-29: Hook registration check (matches install.sh:1508-1528).
46
+ // Parse ~/.claude/settings.json and assert PreToolUse + PostToolUse hooks
47
+ // are registered with Claws _source.
48
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
49
+ if (fs.existsSync(settingsPath)) {
50
+ const hookFailures = _checkHooksRegistered(settingsPath);
51
+ for (const f of hookFailures) failures.push(f);
52
+ }
53
+
54
+ return failures;
55
+ }
56
+
57
+ /**
58
+ * Spawn mcp_server.js, send initialize JSON-RPC, expect response with "claws" in 3s.
59
+ * Returns true if handshake succeeded, false otherwise.
60
+ * @param {string} mcpServerPath
61
+ * @returns {boolean}
62
+ */
63
+ function _mcpHandshake(mcpServerPath) {
64
+ try {
65
+ const req = JSON.stringify({
66
+ jsonrpc: '2.0', id: 1, method: 'initialize',
67
+ params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'claws-verify', version: '1' } },
68
+ }) + '\n';
69
+
70
+ // Run the handshake in a synchronous subprocess via a self-contained node -e script.
71
+ const script = `
72
+ const {spawn} = require('child_process');
73
+ const mcp = spawn('node', [process.argv[1]], {stdio:['pipe','pipe','ignore']});
74
+ let buf = '';
75
+ const done = (ok) => { try{mcp.kill()}catch{} process.exit(ok ? 0 : 1); };
76
+ const timer = setTimeout(() => done(false), 3000);
77
+ mcp.stdout.on('data', d => {
78
+ buf += d.toString();
79
+ if (buf.includes('claws')) { clearTimeout(timer); done(true); }
80
+ });
81
+ mcp.on('error', () => { clearTimeout(timer); done(false); });
82
+ mcp.stdin.write(${JSON.stringify(req)});
83
+ `;
84
+ const r = spawnSync(process.execPath, ['-e', script, mcpServerPath],
85
+ { encoding: 'utf8', timeout: 5000 });
86
+ return r.status === 0;
87
+ } catch (_) {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Parse ~/.claude/settings.json and check that PreToolUse + PostToolUse
94
+ * spawn-class hooks are registered (W7h-29). Matches install.sh:1508-1528.
95
+ * @param {string} settingsPath
96
+ * @returns {string[]} array of failure strings
97
+ */
98
+ function _checkHooksRegistered(settingsPath) {
99
+ const failures = [];
100
+ let settings;
101
+ try {
102
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
103
+ } catch (_) {
104
+ return [];
105
+ }
106
+
107
+ const hooks = settings.hooks || {};
108
+ const preHooks = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
109
+ const postHooks = Array.isArray(hooks.PostToolUse) ? hooks.PostToolUse : [];
110
+
111
+ const hasClawsPre = preHooks.some(h => h.matcher && h.matcher.includes('mcp__claws__claws_worker'));
112
+ const hasClawsPost = postHooks.some(h => h.matcher && h.matcher.includes('mcp__claws__claws_worker'));
113
+
114
+ if (!hasClawsPre) {
115
+ failures.push('MCP spawn-class PreToolUse hooks missing — re-run: node scripts/inject-settings-hooks.js');
116
+ }
117
+ if (!hasClawsPost) {
118
+ failures.push('PostToolUse spawn-class hooks missing — re-run: node scripts/inject-settings-hooks.js');
119
+ }
120
+ return failures;
121
+ }
122
+
123
+ /**
124
+ * Print a human-readable status dashboard for the current project.
125
+ * Sets process.exitCode = 1 when any check fails.
126
+ * All path checks are project-local (cwd-relative), matching install.sh behavior.
127
+ */
128
+ function status() {
129
+ const cwd = process.cwd();
130
+ process.stdout.write('\nClaws installation status\n\n');
131
+
132
+ const checks = [
133
+ ['Node.js ≥ 18', _nodeOk()],
134
+ ['git in PATH', _gitOk()],
135
+ ['.claws-bin/ present', fs.existsSync(path.join(cwd, '.claws-bin'))],
136
+ ['.mcp.json present', fs.existsSync(path.join(cwd, '.mcp.json'))],
137
+ ['mcp_server.js in .claws-bin', fs.existsSync(path.join(cwd, '.claws-bin', 'mcp_server.js'))],
138
+ ['commands present', fs.existsSync(path.join(cwd, '.claude', 'commands', 'claws.md'))],
139
+ ['skills present', fs.existsSync(path.join(cwd, '.claude', 'skills', 'claws-prompt-templates'))],
140
+ ['behavior rule present', fs.existsSync(path.join(cwd, '.claude', 'rules', 'claws-default-behavior.md'))],
141
+ ];
142
+
143
+ let passing = 0;
144
+ for (const [label, pass] of checks) {
145
+ const icon = pass ? '✓' : '✗';
146
+ const color = pass ? '\x1b[32m' : '\x1b[31m';
147
+ process.stdout.write(` ${color}${icon}\x1b[0m ${label}\n`);
148
+ if (pass) passing++;
149
+ }
150
+
151
+ process.stdout.write(`\n ${passing}/${checks.length} checks passing\n\n`);
152
+ if (passing < checks.length) {
153
+ process.stdout.write(' Run: claws-code install\n\n');
154
+ process.exitCode = 1;
155
+ }
156
+ }
157
+
158
+ function _nodeOk() {
159
+ return Number(process.versions.node.split('.')[0]) >= 18;
160
+ }
161
+
162
+ function _gitOk() {
163
+ return spawnSync('git', ['--version'], { stdio: 'pipe' }).status === 0;
164
+ }
165
+
166
+ module.exports = { verify, status };