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,399 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claws stream-events sidecar.
|
|
3
|
+
//
|
|
4
|
+
// Modes:
|
|
5
|
+
// Default (no --wait): holds ONE persistent connection, registers as a claws/2 peer,
|
|
6
|
+
// subscribes to a topic pattern, and prints every server-push frame to stdout as
|
|
7
|
+
// a single line of JSON. Designed to be spawned via Bash run_in_background and
|
|
8
|
+
// consumed by the Monitor tool — each push frame becomes one notification.
|
|
9
|
+
// --wait <uuid>: connects, sends hello, subscribes to system.worker.completed and
|
|
10
|
+
// system.terminal.closed with fromCursor='0000:0' (atomic historical replay + live),
|
|
11
|
+
// exits 0 when a push frame with matching correlation_id arrives, 2 on socket close
|
|
12
|
+
// before match, 3 on timeout. No awk/grep — server-side topic filter only.
|
|
13
|
+
//
|
|
14
|
+
// --wait flags:
|
|
15
|
+
// --keep-alive-on <termId> activates in-process rearm. When the inner timer fires,
|
|
16
|
+
// run a 3-check decision: (1) system.worker.completed in
|
|
17
|
+
// events.log? → exit 0. (2) terminal closed/terminated in
|
|
18
|
+
// events.log? → exit 0. (3) eventsSeen(termId, staleMs)?
|
|
19
|
+
// → rearm. Otherwise → exit 2 (truly stuck).
|
|
20
|
+
// --stale-threshold <ms> liveness window for eventsSeen check (default 120000).
|
|
21
|
+
// --rearm-cycle <ms> interval between rearm checks (default = --timeout-ms).
|
|
22
|
+
//
|
|
23
|
+
// Env:
|
|
24
|
+
// CLAWS_SOCKET override socket path (both modes; used by tests)
|
|
25
|
+
// CLAWS_TOPIC subscribe pattern (default '**') [default mode only]
|
|
26
|
+
// CLAWS_PEER_NAME peer label (default 'orchestrator-stream') [default mode only]
|
|
27
|
+
// CLAWS_ROLE 'orchestrator' | 'worker' | 'observer' (default 'observer') [default mode only]
|
|
28
|
+
// CLAWS_DEBUG '1' or 'true' → emit one structured JSON line to stderr per decision
|
|
29
|
+
// point in the rearm loop (Check 1, Check 2, Check 3, rearm, exit).
|
|
30
|
+
// Additive only — does NOT change exit codes or default stdout output.
|
|
31
|
+
//
|
|
32
|
+
// Default-mode output lines (stdout, one JSON per line):
|
|
33
|
+
// {"type":"sidecar.connected", "socket":"...", "ts":"..."}
|
|
34
|
+
// {"type":"sidecar.hello.ack", "peerId":"p7", ...}
|
|
35
|
+
// {"type":"sidecar.subscribed", "topic":"**", "subscriptionId":"s3", ...}
|
|
36
|
+
// {"type":"event", "push":"message", "topic":"...", "from":"...", "payload":{...}}
|
|
37
|
+
// {"type":"sidecar.error", "error":"..."}
|
|
38
|
+
// {"type":"sidecar.closed", "ts":"..."}
|
|
39
|
+
//
|
|
40
|
+
// Wait-mode output on match (stdout, exactly one JSON line):
|
|
41
|
+
// {"topic":"system.terminal.closed","payload":{...}}
|
|
42
|
+
|
|
43
|
+
'use strict';
|
|
44
|
+
const net = require('net');
|
|
45
|
+
const fs = require('fs');
|
|
46
|
+
const path = require('path');
|
|
47
|
+
|
|
48
|
+
// Walk up from startDir looking for a .claws/ directory (win32 workspace detection).
|
|
49
|
+
function _findWorkspaceRootWin(startDir) {
|
|
50
|
+
let dir = startDir;
|
|
51
|
+
while (dir) {
|
|
52
|
+
try { if (fs.statSync(path.join(dir, '.claws')).isDirectory()) return dir; } catch {}
|
|
53
|
+
const parent = path.dirname(dir);
|
|
54
|
+
if (parent === dir) break;
|
|
55
|
+
dir = parent;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function findSocket() {
|
|
61
|
+
if (process.env.CLAWS_SOCKET) return process.env.CLAWS_SOCKET;
|
|
62
|
+
|
|
63
|
+
if (process.platform === 'win32') {
|
|
64
|
+
// Named pipes are kernel objects — derive name from workspace root hash.
|
|
65
|
+
// Same algorithm as extension/src/transport.ts getServerEndpoint().
|
|
66
|
+
const workspaceRoot = process.env.CLAWS_WORKSPACE_ROOT
|
|
67
|
+
|| _findWorkspaceRootWin(process.cwd())
|
|
68
|
+
|| _findWorkspaceRootWin(path.dirname(__dirname))
|
|
69
|
+
|| process.cwd();
|
|
70
|
+
const hash = require('crypto')
|
|
71
|
+
.createHash('sha256')
|
|
72
|
+
.update(workspaceRoot.toLowerCase())
|
|
73
|
+
.digest('hex')
|
|
74
|
+
.slice(0, 8);
|
|
75
|
+
return `\\\\.\\pipe\\claws-${hash}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Mac / Linux: walk up the directory tree looking for .claws/claws.sock.
|
|
79
|
+
for (const start of [process.cwd(), __dirname]) {
|
|
80
|
+
let dir = start;
|
|
81
|
+
while (dir && dir !== '/' && dir !== path.dirname(dir)) {
|
|
82
|
+
const c = path.join(dir, '.claws', 'claws.sock');
|
|
83
|
+
try { if (fs.statSync(c).isSocket()) return c; } catch { /* */ }
|
|
84
|
+
dir = path.dirname(dir);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function emit(obj) { process.stdout.write(JSON.stringify(obj) + '\n'); }
|
|
91
|
+
|
|
92
|
+
// ── Arg parsing ───────────────────────────────────────────────────────────────
|
|
93
|
+
const _argv = process.argv.slice(2);
|
|
94
|
+
let _waitCorrId = null;
|
|
95
|
+
let _waitFlagSeen = false;
|
|
96
|
+
let _waitTimeoutMs = 600000;
|
|
97
|
+
let _hasAutoSidecar = false;
|
|
98
|
+
let _keepAliveTermId = null;
|
|
99
|
+
let _staleThresholdMs = 120000;
|
|
100
|
+
let _rearmCycleMs = null;
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < _argv.length; i++) {
|
|
103
|
+
if (_argv[i] === '--auto-sidecar') {
|
|
104
|
+
_hasAutoSidecar = true;
|
|
105
|
+
} else if (_argv[i] === '--wait') {
|
|
106
|
+
_waitFlagSeen = true;
|
|
107
|
+
i++;
|
|
108
|
+
_waitCorrId = (_argv[i] !== undefined) ? _argv[i] : null;
|
|
109
|
+
} else if (_argv[i] === '--timeout-ms') {
|
|
110
|
+
i++;
|
|
111
|
+
_waitTimeoutMs = (_argv[i] !== undefined) ? Number(_argv[i]) : NaN;
|
|
112
|
+
} else if (_argv[i] === '--keep-alive-on') {
|
|
113
|
+
i++;
|
|
114
|
+
_keepAliveTermId = (_argv[i] !== undefined) ? String(_argv[i]) : null;
|
|
115
|
+
} else if (_argv[i] === '--stale-threshold') {
|
|
116
|
+
i++;
|
|
117
|
+
_staleThresholdMs = (_argv[i] !== undefined) ? Number(_argv[i]) : NaN;
|
|
118
|
+
} else if (_argv[i] === '--rearm-cycle') {
|
|
119
|
+
i++;
|
|
120
|
+
_rearmCycleMs = (_argv[i] !== undefined) ? Number(_argv[i]) : NaN;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (_rearmCycleMs === null) _rearmCycleMs = _waitTimeoutMs;
|
|
125
|
+
|
|
126
|
+
if (_hasAutoSidecar && _waitFlagSeen) {
|
|
127
|
+
process.stderr.write('stream-events.js: --wait and --auto-sidecar are mutually exclusive\n');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Wait mode ─────────────────────────────────────────────────────────────────
|
|
132
|
+
if (_waitFlagSeen) {
|
|
133
|
+
if (_waitCorrId === null || !/^[0-9a-f-]{36}$/.test(_waitCorrId)) {
|
|
134
|
+
process.stderr.write('stream-events.js: --wait requires a valid UUID (36 hex/dash chars)\n');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
if (!Number.isInteger(_waitTimeoutMs) || _waitTimeoutMs <= 0) {
|
|
138
|
+
process.stderr.write('stream-events.js: --timeout-ms must be a positive integer\n');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const _wSockPath = findSocket();
|
|
143
|
+
if (!_wSockPath) {
|
|
144
|
+
process.stderr.write('stream-events.js --wait: cannot connect to claws.sock\n');
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let _wBuf = '';
|
|
149
|
+
let _wMatched = false;
|
|
150
|
+
const _wCorrId = _waitCorrId;
|
|
151
|
+
|
|
152
|
+
const _debug = process.env.CLAWS_DEBUG === '1' || process.env.CLAWS_DEBUG === 'true';
|
|
153
|
+
function dbg(obj) { if (_debug) process.stderr.write(JSON.stringify(obj) + '\n'); }
|
|
154
|
+
|
|
155
|
+
// ── Helper: scan events.log for a matching completed/terminated event ──────────
|
|
156
|
+
// Returns true if events.log contains a line matching topic + (corrId or terminal_id).
|
|
157
|
+
function eventsLogContains({ topic, corrId, terminal_id }) {
|
|
158
|
+
try {
|
|
159
|
+
const sockDir = path.dirname(_wSockPath);
|
|
160
|
+
const evLog = path.join(sockDir, 'events.log');
|
|
161
|
+
if (!fs.existsSync(evLog)) return false;
|
|
162
|
+
const stat = fs.statSync(evLog);
|
|
163
|
+
const readSz = Math.min(stat.size, 512 * 1024);
|
|
164
|
+
const fd = fs.openSync(evLog, 'r');
|
|
165
|
+
const buf = Buffer.alloc(readSz);
|
|
166
|
+
fs.readSync(fd, buf, 0, readSz, Math.max(0, stat.size - readSz));
|
|
167
|
+
fs.closeSync(fd);
|
|
168
|
+
const topicStr = `"topic":"${topic}"`;
|
|
169
|
+
const corrStr = corrId ? `"correlation_id":"${corrId}"` : null;
|
|
170
|
+
const termStr = terminal_id ? `"terminal_id":"${terminal_id}"` : null;
|
|
171
|
+
for (const line of buf.toString('utf8').split('\n').reverse()) {
|
|
172
|
+
if (!line.includes(topicStr)) continue;
|
|
173
|
+
if (corrStr && line.includes(corrStr)) return true;
|
|
174
|
+
if (termStr && line.includes(termStr)) return true;
|
|
175
|
+
}
|
|
176
|
+
} catch { /* swallow */ }
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Helper: scan events.log for any recent event belonging to termId ──────────
|
|
181
|
+
// Checks both snake_case ("terminal_id") and camelCase ("terminalId") fields
|
|
182
|
+
// so it catches system.worker.* (snake_case) and vehicle.* (camelCase) events.
|
|
183
|
+
// Returns true if any matching line has sentAt (Unix ms) within withinMs of now.
|
|
184
|
+
function eventsSeen(termId, withinMs) {
|
|
185
|
+
try {
|
|
186
|
+
const sockDir = path.dirname(_wSockPath);
|
|
187
|
+
const evLog = path.join(sockDir, 'events.log');
|
|
188
|
+
if (!fs.existsSync(evLog)) return false;
|
|
189
|
+
const stat = fs.statSync(evLog);
|
|
190
|
+
const readSz = Math.min(stat.size, 512 * 1024);
|
|
191
|
+
const fd = fs.openSync(evLog, 'r');
|
|
192
|
+
const buf = Buffer.alloc(readSz);
|
|
193
|
+
fs.readSync(fd, buf, 0, readSz, Math.max(0, stat.size - readSz));
|
|
194
|
+
fs.closeSync(fd);
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
const matchA = `"terminal_id":"${termId}"`;
|
|
197
|
+
const matchB = `"terminalId":"${termId}"`;
|
|
198
|
+
for (const line of buf.toString('utf8').split('\n').reverse()) {
|
|
199
|
+
if (!line.includes(matchA) && !line.includes(matchB)) continue;
|
|
200
|
+
try {
|
|
201
|
+
const ev = JSON.parse(line);
|
|
202
|
+
if (typeof ev.sentAt === 'number' && (now - ev.sentAt) < withinMs) return true;
|
|
203
|
+
} catch { /* malformed line — skip */ }
|
|
204
|
+
}
|
|
205
|
+
} catch { /* swallow */ }
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Rearm decision loop (fires when inner timer expires) ──────────────────────
|
|
210
|
+
function rearmDecisionLoop() {
|
|
211
|
+
const _now = Date.now();
|
|
212
|
+
|
|
213
|
+
// Check 1: completion event already persisted in events.log? (race: Monitor armed after done)
|
|
214
|
+
const _c1Matched = eventsLogContains({ topic: 'system.worker.completed', corrId: _wCorrId });
|
|
215
|
+
dbg({ check: 1, event: 'completion-scan', corrId: _wCorrId, matched: _c1Matched, matchedAt: _c1Matched ? _now : null, now: _now });
|
|
216
|
+
if (_c1Matched) {
|
|
217
|
+
process.stderr.write(`stream-events.js --wait: matched (raced) — system.worker.completed in events.log\n`);
|
|
218
|
+
dbg({ event: 'exit', code: 0, reason: 'check1-completed', corrId: _wCorrId, now: _now });
|
|
219
|
+
clearTimeout(_wTimer);
|
|
220
|
+
try { _wSock.destroy(); } catch {}
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check 2: termination — corrId-only matching for both system.terminal.closed and
|
|
225
|
+
// system.worker.terminated. terminal_id is session-local: VS Code resets the
|
|
226
|
+
// counter on extension reload and recycles integers as terminals open/close.
|
|
227
|
+
// events.log is globally append-only, so any prior-session terminated event
|
|
228
|
+
// with the same numeric terminal_id would false-positive here. correlation_id
|
|
229
|
+
// is a UUID — globally unique, collision-free across sessions and reloads.
|
|
230
|
+
if (_keepAliveTermId) {
|
|
231
|
+
const closedByCorrId = eventsLogContains({ topic: 'system.terminal.closed', corrId: _wCorrId });
|
|
232
|
+
const terminatedByCorrId = eventsLogContains({ topic: 'system.worker.terminated', corrId: _wCorrId });
|
|
233
|
+
dbg({ check: 2, event: 'termination-scan', corrId: _wCorrId, closedByCorrId, terminatedByCorrId, now: _now });
|
|
234
|
+
if (closedByCorrId || terminatedByCorrId) {
|
|
235
|
+
const which = closedByCorrId ? 'system.terminal.closed(corrId)' : 'system.worker.terminated(corrId)';
|
|
236
|
+
process.stderr.write(`stream-events.js --wait: matched (raced) — ${which} in events.log\n`);
|
|
237
|
+
dbg({ event: 'exit', code: 0, reason: 'check2-termination', corrId: _wCorrId, now: _now });
|
|
238
|
+
clearTimeout(_wTimer);
|
|
239
|
+
try { _wSock.destroy(); } catch {}
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check 3: events.log recency scan — is the terminal still producing bus events?
|
|
245
|
+
if (_keepAliveTermId) {
|
|
246
|
+
const alive = eventsSeen(_keepAliveTermId, _staleThresholdMs);
|
|
247
|
+
dbg({ check: 3, event: 'liveness-scan', termId: _keepAliveTermId, alive, staleThresholdMs: _staleThresholdMs, now: _now });
|
|
248
|
+
if (alive) {
|
|
249
|
+
dbg({ event: 'rearm', reason: 'alive', now: _now });
|
|
250
|
+
process.stderr.write(`stream-events.js --wait: rearming — terminal ${_keepAliveTermId} active in events.log within ${_staleThresholdMs}ms\n`);
|
|
251
|
+
_wTimer = setTimeout(rearmDecisionLoop, _rearmCycleMs);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
dbg({ event: 'exit', code: 2, reason: 'no-events-for-terminal', corrId: _wCorrId, now: _now });
|
|
255
|
+
process.stderr.write(`stream-events.js --wait: exit stuck — no events for terminal ${_keepAliveTermId} within ${_staleThresholdMs}ms\n`);
|
|
256
|
+
try { _wSock.destroy(); } catch {}
|
|
257
|
+
process.exit(2);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// No --keep-alive-on provided: original timeout behavior (backwards compat)
|
|
261
|
+
dbg({ event: 'rearm', reason: 'no-keep-alive', now: _now });
|
|
262
|
+
process.stderr.write(`stream-events.js --wait: timeout waiting for close event (correlation_id=${_wCorrId})\n`);
|
|
263
|
+
process.exit(3);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let _wTimer = setTimeout(rearmDecisionLoop, _keepAliveTermId ? _rearmCycleMs : _waitTimeoutMs);
|
|
267
|
+
|
|
268
|
+
const _wSock = net.createConnection(_wSockPath);
|
|
269
|
+
|
|
270
|
+
_wSock.on('error', (e) => {
|
|
271
|
+
clearTimeout(_wTimer);
|
|
272
|
+
if (e.code === 'ENOENT' || e.code === 'ECONNREFUSED') {
|
|
273
|
+
process.stderr.write('stream-events.js --wait: cannot connect to claws.sock\n');
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
process.stderr.write('stream-events.js --wait: socket closed before close event\n');
|
|
277
|
+
process.exit(2);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
_wSock.on('connect', () => {
|
|
281
|
+
// Bug-13 observability: log hello-send time so arm-race investigations can compare
|
|
282
|
+
// this timestamp against the L2 30s window start (spawn time in mcp_server.js).
|
|
283
|
+
process.stderr.write(`stream-events.js --wait: hello sent | corrId=${_wCorrId} | t=${new Date().toISOString()}\n`);
|
|
284
|
+
_wSock.write(JSON.stringify({ id: 1, cmd: 'hello', protocol: 'claws/2', role: 'observer', peerName: 'wait-mode', monitorCorrelationId: _wCorrId }) + '\n');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
_wSock.on('data', (d) => {
|
|
288
|
+
_wBuf += d.toString('utf8');
|
|
289
|
+
let nl;
|
|
290
|
+
while ((nl = _wBuf.indexOf('\n')) !== -1) {
|
|
291
|
+
const line = _wBuf.slice(0, nl);
|
|
292
|
+
_wBuf = _wBuf.slice(nl + 1);
|
|
293
|
+
if (!line.trim()) continue;
|
|
294
|
+
let msg;
|
|
295
|
+
try { msg = JSON.parse(line); }
|
|
296
|
+
catch (_) {
|
|
297
|
+
process.stderr.write(`stream-events.js --wait: malformed event: ${line.slice(0, 200)}\n`);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const rid = msg.rid != null ? msg.rid : msg.id;
|
|
302
|
+
|
|
303
|
+
if (rid === 1 && msg.peerId) {
|
|
304
|
+
// Hello ack — subscribe to both terminal-state topics with full historical replay.
|
|
305
|
+
// fromCursor:'0000:0' makes the server replay matching events from the event log
|
|
306
|
+
// before delivering live pushes, closing the subscribe-before-drain race gap.
|
|
307
|
+
_wSock.write(JSON.stringify({ id: 2, cmd: 'subscribe', topic: 'system.worker.completed', fromCursor: '0000:0' }) + '\n');
|
|
308
|
+
_wSock.write(JSON.stringify({ id: 3, cmd: 'subscribe', topic: 'system.terminal.closed', fromCursor: '0000:0' }) + '\n');
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (rid === 2 || rid === 3) continue; // subscribe acks — no action needed
|
|
312
|
+
|
|
313
|
+
// Push frames (both replayed historical and live events arrive the same way)
|
|
314
|
+
if (msg.push === 'message' &&
|
|
315
|
+
(msg.topic === 'system.worker.completed' || msg.topic === 'system.terminal.closed') &&
|
|
316
|
+
msg.payload != null && msg.payload.correlation_id === _wCorrId) {
|
|
317
|
+
_wMatched = true;
|
|
318
|
+
process.stdout.write(JSON.stringify({ topic: msg.topic, payload: msg.payload }) + '\n');
|
|
319
|
+
clearTimeout(_wTimer);
|
|
320
|
+
_wSock.destroy();
|
|
321
|
+
process.exit(0);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
_wSock.on('close', () => {
|
|
327
|
+
if (!_wMatched) {
|
|
328
|
+
clearTimeout(_wTimer);
|
|
329
|
+
process.stderr.write('stream-events.js --wait: socket closed before close event\n');
|
|
330
|
+
process.exit(2);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
process.on('SIGTERM', () => { clearTimeout(_wTimer); try { _wSock.destroy(); } catch {} process.exit(143); });
|
|
335
|
+
process.on('SIGINT', () => { clearTimeout(_wTimer); try { _wSock.destroy(); } catch {} process.exit(130); });
|
|
336
|
+
process.stdout.on('error', (e) => { if (e.code === 'EPIPE') { clearTimeout(_wTimer); process.exit(141); } });
|
|
337
|
+
|
|
338
|
+
} else {
|
|
339
|
+
// ── Default mode (unchanged) ──────────────────────────────────────────────────
|
|
340
|
+
const SOCK = findSocket();
|
|
341
|
+
const TOPIC = process.env.CLAWS_TOPIC || '**';
|
|
342
|
+
const PEER_NAME = process.env.CLAWS_PEER_NAME || 'orchestrator-stream';
|
|
343
|
+
const ROLE = process.env.CLAWS_ROLE || 'observer';
|
|
344
|
+
|
|
345
|
+
if (!SOCK) {
|
|
346
|
+
emit({ type: 'sidecar.error', error: 'no .claws/claws.sock found' });
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let buf = '';
|
|
351
|
+
const sock = net.createConnection(SOCK);
|
|
352
|
+
|
|
353
|
+
sock.on('connect', () => {
|
|
354
|
+
emit({ type: 'sidecar.connected', socket: SOCK, role: ROLE, ts: new Date().toISOString() });
|
|
355
|
+
sock.write(JSON.stringify({ id: 1, cmd: 'hello', protocol: 'claws/2', role: ROLE, peerName: PEER_NAME }) + '\n');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
sock.on('data', (d) => {
|
|
359
|
+
buf += d.toString('utf8');
|
|
360
|
+
let nl;
|
|
361
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
362
|
+
const line = buf.slice(0, nl);
|
|
363
|
+
buf = buf.slice(nl + 1);
|
|
364
|
+
if (!line.trim()) continue;
|
|
365
|
+
let msg;
|
|
366
|
+
try { msg = JSON.parse(line); }
|
|
367
|
+
catch (e) { emit({ type: 'sidecar.parse-error', line, error: e.message }); continue; }
|
|
368
|
+
|
|
369
|
+
if (msg.rid === 1 && msg.peerId) {
|
|
370
|
+
emit({ type: 'sidecar.hello.ack', peerId: msg.peerId, ts: new Date().toISOString() });
|
|
371
|
+
sock.write(JSON.stringify({ id: 2, cmd: 'subscribe', topic: TOPIC }) + '\n');
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (msg.rid === 2) {
|
|
375
|
+
emit({ type: 'sidecar.subscribed', topic: TOPIC, subscriptionId: msg.subscriptionId, ok: msg.ok, ts: new Date().toISOString() });
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (msg.push) {
|
|
379
|
+
emit({ type: 'event', ...msg, recvTs: new Date().toISOString() });
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
emit({ type: 'sidecar.unknown', msg });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
sock.on('error', (e) => {
|
|
387
|
+
emit({ type: 'sidecar.error', error: e.message });
|
|
388
|
+
process.exit(1);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
sock.on('close', () => {
|
|
392
|
+
emit({ type: 'sidecar.closed', ts: new Date().toISOString() });
|
|
393
|
+
process.exit(0);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const shutdown = () => { try { sock.end(); } catch {} process.exit(0); };
|
|
397
|
+
process.on('SIGTERM', shutdown);
|
|
398
|
+
process.on('SIGINT', shutdown);
|
|
399
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Claws terminal wrapper.
|
|
3
|
+
# Exec-replaces itself with script(1) so every byte that flows through the
|
|
4
|
+
# pty is logged to CLAWS_TERM_LOG. The Claws extension (or any orchestrator)
|
|
5
|
+
# tails that log to read what happened in this terminal — including TUI
|
|
6
|
+
# sessions like claude, vim, less, top that are opaque to shell integration.
|
|
7
|
+
#
|
|
8
|
+
# Set CLAWS_TERM_LOG in the extension's createTerminal env. Fallback
|
|
9
|
+
# derives a path from the PID so the wrapper never fails silently.
|
|
10
|
+
|
|
11
|
+
set -e
|
|
12
|
+
|
|
13
|
+
if [ -z "${CLAWS_TERM_LOG:-}" ]; then
|
|
14
|
+
CLAWS_TERM_LOG="${PWD}/.claws/terminals/claws-$$.log"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
mkdir -p "$(dirname "$CLAWS_TERM_LOG")"
|
|
18
|
+
: > "$CLAWS_TERM_LOG"
|
|
19
|
+
|
|
20
|
+
# Mark ourselves as a wrapped session so any process inside can detect it.
|
|
21
|
+
export CLAWS_WRAPPED=1
|
|
22
|
+
|
|
23
|
+
SHELL_BIN="${SHELL:-/bin/zsh}"
|
|
24
|
+
|
|
25
|
+
# Do NOT use script's -F flag. -F flushes after every write, which splits
|
|
26
|
+
# Ink-based TUI renderers (Claude Code, etc.) into corrupted partial frames.
|
|
27
|
+
# Default buffering produces a clean terminal at the cost of ~1-2s log delay.
|
|
28
|
+
|
|
29
|
+
# Detect BSD (macOS) vs GNU/Linux script(1) — different argument order.
|
|
30
|
+
if script --version 2>&1 | grep -qi "util-linux"; then
|
|
31
|
+
# Linux: script -q -c "command" outfile
|
|
32
|
+
exec script -q -c "$SHELL_BIN -il" "$CLAWS_TERM_LOG"
|
|
33
|
+
else
|
|
34
|
+
# macOS/BSD: script -q outfile command
|
|
35
|
+
exec script -q "$CLAWS_TERM_LOG" "$SHELL_BIN" -il
|
|
36
|
+
fi
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Integration test for Claws behavioral injection enforcement pipeline.
|
|
3
|
+
# Tests all four stages: project CLAUDE.md, global CLAUDE.md, hooks, session-start.
|
|
4
|
+
# Usage: bash scripts/test-enforcement.sh
|
|
5
|
+
# Exit 0 = all tests pass. Exit 1 = one or more tests failed.
|
|
6
|
+
|
|
7
|
+
set -eo pipefail
|
|
8
|
+
INSTALL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
9
|
+
PASS=0; FAIL=0
|
|
10
|
+
|
|
11
|
+
pass() { printf " \033[32m✓\033[0m %s\n" "$*"; PASS=$((PASS+1)); }
|
|
12
|
+
fail() { printf " \033[31m✗\033[0m %s\n" "$*"; FAIL=$((FAIL+1)); }
|
|
13
|
+
header() { printf "\n\033[1m── %s ──\033[0m\n" "$*"; }
|
|
14
|
+
|
|
15
|
+
TMPDIR_TEST="$(mktemp -d)"
|
|
16
|
+
trap 'rm -rf "$TMPDIR_TEST"' EXIT
|
|
17
|
+
|
|
18
|
+
# ── Test 1: inject-claude-md.js writes imperative content ─────────────────
|
|
19
|
+
header "Test 1: inject-claude-md.js"
|
|
20
|
+
TEST_PROJECT="$TMPDIR_TEST/test-project"
|
|
21
|
+
mkdir -p "$TEST_PROJECT"
|
|
22
|
+
node "$INSTALL_DIR/scripts/inject-claude-md.js" "$TEST_PROJECT" >/dev/null 2>&1
|
|
23
|
+
|
|
24
|
+
if [ -f "$TEST_PROJECT/CLAUDE.md" ]; then pass "CLAUDE.md created"; else fail "CLAUDE.md not created"; fi
|
|
25
|
+
|
|
26
|
+
if grep -q "MUST\|MANDATORY\|ALWAYS\|NEVER" "$TEST_PROJECT/CLAUDE.md" 2>/dev/null; then
|
|
27
|
+
pass "CLAUDE.md contains imperative language (MUST/ALWAYS/NEVER)"
|
|
28
|
+
else
|
|
29
|
+
fail "CLAUDE.md missing imperative language — advisory content only"
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if grep -q "CLAWS:BEGIN" "$TEST_PROJECT/CLAUDE.md" 2>/dev/null; then
|
|
33
|
+
pass "CLAUDE.md has CLAWS:BEGIN sentinel"
|
|
34
|
+
else
|
|
35
|
+
fail "CLAUDE.md missing CLAWS:BEGIN sentinel"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
if grep -q "claws_create\|claws_send" "$TEST_PROJECT/CLAUDE.md" 2>/dev/null; then
|
|
39
|
+
pass "CLAUDE.md lists MCP tools"
|
|
40
|
+
else
|
|
41
|
+
fail "CLAUDE.md missing MCP tool list"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Idempotency: run again and verify no duplicate sentinel
|
|
45
|
+
node "$INSTALL_DIR/scripts/inject-claude-md.js" "$TEST_PROJECT" >/dev/null 2>&1
|
|
46
|
+
SENTINEL_COUNT=$(grep -c "CLAWS:BEGIN" "$TEST_PROJECT/CLAUDE.md" 2>/dev/null || echo 0)
|
|
47
|
+
if [ "$SENTINEL_COUNT" -eq 1 ]; then
|
|
48
|
+
pass "inject-claude-md.js is idempotent (sentinel count=1 after 2 runs)"
|
|
49
|
+
else
|
|
50
|
+
fail "inject-claude-md.js not idempotent (sentinel count=$SENTINEL_COUNT after 2 runs)"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# ── Test 2: inject-global-claude-md.js dry-run ────────────────────────────
|
|
54
|
+
header "Test 2: inject-global-claude-md.js"
|
|
55
|
+
DRY_OUT=$(node "$INSTALL_DIR/scripts/inject-global-claude-md.js" --dry-run 2>&1)
|
|
56
|
+
|
|
57
|
+
if echo "$DRY_OUT" | grep -q "CLAWS-GLOBAL:BEGIN v1"; then
|
|
58
|
+
pass "inject-global-claude-md.js dry-run emits CLAWS-GLOBAL:BEGIN v1"
|
|
59
|
+
else
|
|
60
|
+
fail "inject-global-claude-md.js dry-run missing CLAWS-GLOBAL:BEGIN v1"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
if echo "$DRY_OUT" | grep -q "MUST\|ALWAYS\|NEVER"; then
|
|
64
|
+
pass "inject-global-claude-md.js dry-run contains imperative language"
|
|
65
|
+
else
|
|
66
|
+
fail "inject-global-claude-md.js dry-run missing imperative language"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# ── Test 3: inject-settings-hooks.js dry-run ──────────────────────────────
|
|
70
|
+
header "Test 3: inject-settings-hooks.js"
|
|
71
|
+
HOOKS_DRY=$(node "$INSTALL_DIR/scripts/inject-settings-hooks.js" "$INSTALL_DIR/.claws-bin" --dry-run 2>&1)
|
|
72
|
+
|
|
73
|
+
if echo "$HOOKS_DRY" | grep -q "SessionStart"; then
|
|
74
|
+
pass "inject-settings-hooks.js dry-run includes SessionStart hook"
|
|
75
|
+
else
|
|
76
|
+
fail "inject-settings-hooks.js dry-run missing SessionStart hook"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
if echo "$HOOKS_DRY" | grep -q "PreToolUse\|Bash"; then
|
|
80
|
+
pass "inject-settings-hooks.js dry-run includes PreToolUse hook"
|
|
81
|
+
else
|
|
82
|
+
fail "inject-settings-hooks.js dry-run missing PreToolUse hook"
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
if echo "$HOOKS_DRY" | grep -q '"_source".*claws\|claws.*_source'; then
|
|
86
|
+
pass "inject-settings-hooks.js tags hooks with _source:claws"
|
|
87
|
+
else
|
|
88
|
+
fail "inject-settings-hooks.js missing _source:claws tag"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# ── Test 4: session-start-claws.js emits lifecycle reminder ───────────────
|
|
92
|
+
header "Test 4: session-start-claws.js"
|
|
93
|
+
FAKE_PROJECT="$TMPDIR_TEST/fake-claws-project"
|
|
94
|
+
mkdir -p "$FAKE_PROJECT/.claws"
|
|
95
|
+
touch "$FAKE_PROJECT/.claws/claws.sock"
|
|
96
|
+
|
|
97
|
+
HOOK_OUT=$(echo "{\"cwd\":\"$FAKE_PROJECT\"}" | node "$INSTALL_DIR/.claws-bin/hooks/session-start-claws.js" 2>&1)
|
|
98
|
+
|
|
99
|
+
if echo "$HOOK_OUT" | grep -q "MANDATORY"; then
|
|
100
|
+
pass "session-start-claws.js emits MANDATORY reminder when socket present"
|
|
101
|
+
else
|
|
102
|
+
fail "session-start-claws.js missing MANDATORY reminder"
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
if echo "$HOOK_OUT" | grep -q "claws_create\|boot sequence"; then
|
|
106
|
+
pass "session-start-claws.js includes boot sequence or claws_create"
|
|
107
|
+
else
|
|
108
|
+
fail "session-start-claws.js missing boot sequence reference"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
NO_SOCK_OUT=$(echo "{\"cwd\":\"$TMPDIR_TEST\"}" | node "$INSTALL_DIR/.claws-bin/hooks/session-start-claws.js" 2>&1)
|
|
112
|
+
if [ -z "$NO_SOCK_OUT" ]; then
|
|
113
|
+
pass "session-start-claws.js silent when no socket present"
|
|
114
|
+
else
|
|
115
|
+
fail "session-start-claws.js emitted output when no socket (should be silent)"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# ── Test 5: hook scripts have correct exit codes ───────────────────────────
|
|
119
|
+
header "Test 5: hook exit codes"
|
|
120
|
+
echo '{}' | node "$INSTALL_DIR/.claws-bin/hooks/pre-tool-use-claws.js" >/dev/null 2>&1 && pass "pre-tool-use-claws.js exits 0 on empty input" || fail "pre-tool-use-claws.js non-zero exit on empty input"
|
|
121
|
+
echo '{}' | node "$INSTALL_DIR/.claws-bin/hooks/stop-claws.js" >/dev/null 2>&1 && pass "stop-claws.js exits 0 on empty input" || fail "stop-claws.js non-zero exit on empty input"
|
|
122
|
+
|
|
123
|
+
# ── Summary ────────────────────────────────────────────────────────────────
|
|
124
|
+
printf "\n\033[1m── Results ──\033[0m\n"
|
|
125
|
+
printf " Passed: \033[32m%d\033[0m Failed: \033[31m%d\033[0m Total: %d\n" "$PASS" "$FAIL" "$((PASS+FAIL))"
|
|
126
|
+
|
|
127
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
128
|
+
printf "\n\033[31mSome tests failed.\033[0m\n"
|
|
129
|
+
exit 1
|
|
130
|
+
else
|
|
131
|
+
printf "\n\033[32mAll tests passed.\033[0m\n"
|
|
132
|
+
fi
|