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.
- package/.claude/commands/claws-auto.md +90 -0
- package/.claude/commands/claws-bin.md +28 -0
- package/.claude/commands/claws-cleanup.md +28 -0
- package/.claude/commands/claws-do.md +82 -0
- package/.claude/commands/claws-fix.md +40 -0
- package/.claude/commands/claws-goal.md +111 -0
- package/.claude/commands/claws-help.md +54 -0
- package/.claude/commands/claws-plan.md +103 -0
- package/.claude/commands/claws-report.md +29 -0
- package/.claude/commands/claws-status.md +37 -0
- package/.claude/commands/claws-update.md +32 -0
- package/.claude/commands/claws.md +64 -0
- package/.claude/rules/claws-default-behavior.md +76 -0
- package/.claude/settings.json +112 -0
- package/.claude/settings.local.json +19 -0
- package/.claude/skills/claws-auto-engine/SKILL.md +97 -0
- package/.claude/skills/claws-goal-tracker/SKILL.md +106 -0
- package/.claude/skills/claws-prompt-templates/SKILL.md +203 -0
- package/.claude/skills/claws-wave-lead/SKILL.md +126 -0
- package/.claude/skills/claws-wave-subworker/SKILL.md +60 -0
- package/CHANGELOG.md +1949 -0
- package/LICENSE +21 -0
- package/README.md +420 -0
- package/bin/cli.js +84 -0
- package/cli.js +223 -0
- package/docs/ARCHITECTURE.md +511 -0
- package/docs/event-protocol.md +588 -0
- package/docs/features.md +562 -0
- package/docs/guide.md +891 -0
- package/docs/index.html +716 -0
- package/docs/protocol.md +323 -0
- package/extension/.vscodeignore +15 -0
- package/extension/CHANGELOG.md +1906 -0
- package/extension/LICENSE +21 -0
- package/extension/README.md +137 -0
- package/extension/docs/features.md +424 -0
- package/extension/docs/protocol.md +197 -0
- package/extension/esbuild.mjs +25 -0
- package/extension/icon.png +0 -0
- package/extension/native/.metadata.json +10 -0
- package/extension/native/node-pty/LICENSE +69 -0
- package/extension/native/node-pty/README.md +165 -0
- package/extension/native/node-pty/lib/conpty_console_list_agent.js +16 -0
- package/extension/native/node-pty/lib/conpty_console_list_agent.js.map +1 -0
- package/extension/native/node-pty/lib/eventEmitter2.js +47 -0
- package/extension/native/node-pty/lib/eventEmitter2.js.map +1 -0
- package/extension/native/node-pty/lib/index.js +52 -0
- package/extension/native/node-pty/lib/index.js.map +1 -0
- package/extension/native/node-pty/lib/interfaces.js +7 -0
- package/extension/native/node-pty/lib/interfaces.js.map +1 -0
- package/extension/native/node-pty/lib/shared/conout.js +11 -0
- package/extension/native/node-pty/lib/shared/conout.js.map +1 -0
- package/extension/native/node-pty/lib/terminal.js +190 -0
- package/extension/native/node-pty/lib/terminal.js.map +1 -0
- package/extension/native/node-pty/lib/types.js +7 -0
- package/extension/native/node-pty/lib/types.js.map +1 -0
- package/extension/native/node-pty/lib/unixTerminal.js +346 -0
- package/extension/native/node-pty/lib/unixTerminal.js.map +1 -0
- package/extension/native/node-pty/lib/utils.js +39 -0
- package/extension/native/node-pty/lib/utils.js.map +1 -0
- package/extension/native/node-pty/lib/windowsConoutConnection.js +125 -0
- package/extension/native/node-pty/lib/windowsConoutConnection.js.map +1 -0
- package/extension/native/node-pty/lib/windowsPtyAgent.js +320 -0
- package/extension/native/node-pty/lib/windowsPtyAgent.js.map +1 -0
- package/extension/native/node-pty/lib/windowsTerminal.js +199 -0
- package/extension/native/node-pty/lib/windowsTerminal.js.map +1 -0
- package/extension/native/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/extension/native/node-pty/lib/worker/conoutSocketWorker.js.map +1 -0
- package/extension/native/node-pty/package.json +64 -0
- package/extension/native/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/extension/native/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/winpty-agent.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/winpty.dll +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/winpty-agent.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/winpty.dll +0 -0
- package/extension/package-lock.json +605 -0
- package/extension/package.json +343 -0
- package/extension/scripts/bundle-native.mjs +104 -0
- package/extension/scripts/deploy-dev.mjs +60 -0
- package/extension/src/ansi-strip.ts +52 -0
- package/extension/src/backends/vscode/claws-pty.ts +483 -0
- package/extension/src/backends/vscode/status-bar.ts +99 -0
- package/extension/src/backends/vscode/vscode-backend.ts +282 -0
- package/extension/src/capture-store.ts +125 -0
- package/extension/src/event-log.ts +629 -0
- package/extension/src/event-schemas.ts +478 -0
- package/extension/src/extension.js +492 -0
- package/extension/src/extension.ts +873 -0
- package/extension/src/lifecycle-engine.ts +60 -0
- package/extension/src/lifecycle-rules.ts +171 -0
- package/extension/src/lifecycle-store.ts +506 -0
- package/extension/src/peer-registry.ts +176 -0
- package/extension/src/pipeline-registry.ts +82 -0
- package/extension/src/platform.ts +64 -0
- package/extension/src/protocol.ts +532 -0
- package/extension/src/server-config.ts +98 -0
- package/extension/src/server.ts +2210 -0
- package/extension/src/task-registry.ts +51 -0
- package/extension/src/terminal-backend.ts +211 -0
- package/extension/src/terminal-manager.ts +395 -0
- package/extension/src/topic-registry.ts +70 -0
- package/extension/src/topic-utils.ts +46 -0
- package/extension/src/transport.ts +45 -0
- package/extension/src/uninstall-cleanup.ts +232 -0
- package/extension/src/wave-registry.ts +314 -0
- package/extension/src/websocket-transport.ts +153 -0
- package/extension/tsconfig.json +23 -0
- package/lib/capabilities.js +145 -0
- package/lib/dry-run.js +43 -0
- package/lib/install.js +1018 -0
- package/lib/mcp-setup.js +92 -0
- package/lib/platform.js +240 -0
- package/lib/preflight.js +152 -0
- package/lib/shell-hook.js +343 -0
- package/lib/uninstall.js +162 -0
- package/lib/verify.js +166 -0
- package/mcp_server.js +3529 -0
- package/package.json +48 -0
- package/rules/claws-default-behavior.md +72 -0
- package/scripts/_helpers/atomic-file.mjs +137 -0
- package/scripts/_helpers/fix-repair.js +64 -0
- package/scripts/_helpers/json-safe.mjs +218 -0
- package/scripts/bump-version.sh +84 -0
- package/scripts/codegen/gen-docs.mjs +61 -0
- package/scripts/codegen/gen-json-schema.mjs +62 -0
- package/scripts/codegen/gen-mcp-tools.mjs +358 -0
- package/scripts/codegen/gen-types.mjs +172 -0
- package/scripts/codegen/index.mjs +42 -0
- package/scripts/dev-hooks/check-extension-dirs.js +77 -0
- package/scripts/dev-hooks/check-open-claws-terminals.js +70 -0
- package/scripts/dev-hooks/check-stale-main.js +55 -0
- package/scripts/dev-hooks/check-tag-pushed.js +51 -0
- package/scripts/dev-hooks/check-tag-vs-main.js +56 -0
- package/scripts/dev-vsix-install.sh +60 -0
- package/scripts/fix.sh +702 -0
- package/scripts/gen-client-types.mjs +81 -0
- package/scripts/git-hooks/pre-commit +31 -0
- package/scripts/hooks/lifecycle-state.js +61 -0
- package/scripts/hooks/package.json +4 -0
- package/scripts/hooks/post-tool-use-claws.js +292 -0
- package/scripts/hooks/pre-bash-no-verify-block.js +72 -0
- package/scripts/hooks/pre-tool-use-claws.js +206 -0
- package/scripts/hooks/session-start-claws.js +97 -0
- package/scripts/hooks/stop-claws.js +88 -0
- package/scripts/inject-claude-md.js +205 -0
- package/scripts/inject-dev-hooks.js +96 -0
- package/scripts/inject-global-claude-md.js +140 -0
- package/scripts/inject-settings-hooks.js +370 -0
- package/scripts/install.ps1 +146 -0
- package/scripts/install.sh +1729 -0
- package/scripts/monitor-arm-watch.js +155 -0
- package/scripts/rebuild-node-pty.sh +245 -0
- package/scripts/report.sh +232 -0
- package/scripts/shell-hook.fish +164 -0
- package/scripts/shell-hook.ps1 +33 -0
- package/scripts/shell-hook.sh +232 -0
- package/scripts/stream-events.js +399 -0
- package/scripts/terminal-wrapper.sh +36 -0
- package/scripts/test-enforcement.sh +132 -0
- package/scripts/test-install.sh +174 -0
- package/scripts/test-installer-parity.sh +135 -0
- package/scripts/test-template-enforcement.sh +76 -0
- package/scripts/uninstall.sh +143 -0
- package/scripts/update.sh +337 -0
- package/scripts/verify-release.sh +323 -0
- package/scripts/verify-wrapped.sh +194 -0
- package/templates/CLAUDE.global.md +135 -0
- 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
|
+
});
|