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,282 @@
1
+ // extension/src/backends/vscode/vscode-backend.ts
2
+ // Thin adapter that wraps TerminalManager + CaptureStore behind the
3
+ // TerminalBackend interface. Owns the execWaiters WeakMap (moved from
4
+ // ServerOptions in Commit 3). All VS Code API calls are confined to this file.
5
+
6
+ import * as vscode from 'vscode';
7
+ import * as fs from 'fs';
8
+ import { EventEmitter } from 'events';
9
+ import { CaptureStore } from '../../capture-store';
10
+ import { TerminalManager } from '../../terminal-manager';
11
+ import { stripAnsi } from '../../ansi-strip';
12
+ import { HistoryEvent } from '../../protocol';
13
+ import { VehicleStateName, TerminalCloseOrigin } from '../../event-schemas';
14
+ import type {
15
+ TerminalBackend,
16
+ BackendCreateOptions,
17
+ BackendTerminalInfo,
18
+ BackendLogSlice,
19
+ BackendSendOptions,
20
+ TerminalCreatedEvent,
21
+ TerminalClosedEvent,
22
+ TerminalDataEvent,
23
+ TerminalReadyEvent,
24
+ TerminalProcessExitedEvent,
25
+ ForegroundProcessInfo,
26
+ } from '../../terminal-backend';
27
+
28
+ type StateChangeCallback = (id: string, from: VehicleStateName | null, to: VehicleStateName) => void;
29
+ type ContentChangeCallback = (id: string, pid: number | null, basename: string | null) => void;
30
+
31
+ const MAX_READLOG_BYTES = 512 * 1024;
32
+
33
+ export interface VsCodeBackendOptions {
34
+ captureStore: CaptureStore;
35
+ terminalManager: TerminalManager;
36
+ logger: (msg: string) => void;
37
+ }
38
+
39
+ export class VsCodeBackend extends EventEmitter implements TerminalBackend {
40
+ private readonly captureStore: CaptureStore;
41
+ private readonly tm: TerminalManager;
42
+
43
+ /** Moved from ServerOptions — exec command waiters keyed by vscode.Terminal. */
44
+ readonly execWaiters = new WeakMap<vscode.Terminal, Array<(ev: HistoryEvent) => void>>();
45
+
46
+ constructor(opts: VsCodeBackendOptions) {
47
+ super();
48
+ this.captureStore = opts.captureStore;
49
+ this.tm = opts.terminalManager;
50
+ }
51
+
52
+ async start(): Promise<void> {
53
+ // Wire TerminalManager close callback → emit 'terminal:closed' event.
54
+ this.tm.setTerminalCloseCallback((id: string, _wrapped: boolean, origin: TerminalCloseOrigin) => {
55
+ const ev: TerminalClosedEvent = {
56
+ id,
57
+ origin: origin as TerminalClosedEvent['origin'],
58
+ };
59
+ this.emit('terminal:closed', ev);
60
+ });
61
+ // start() resolves immediately — VS Code APIs are already ready at this point.
62
+ }
63
+
64
+ dispose(): void {
65
+ this.tm.dispose();
66
+ this.removeAllListeners();
67
+ }
68
+
69
+ // ── Optional: VS Code-specific state/content callbacks (not on interface) ──
70
+
71
+ /** Delegates to TerminalManager. Called by ClawsServer to wire pub/sub vehicle events. */
72
+ setStateChangeCallback(cb: StateChangeCallback): void {
73
+ this.tm.setStateChangeCallback(cb);
74
+ }
75
+
76
+ /** Delegates to TerminalManager. Called by ClawsServer to wire content change events. */
77
+ setContentChangeCallback(cb: ContentChangeCallback): void {
78
+ this.tm.setContentChangeCallback(cb);
79
+ }
80
+
81
+ /** Snapshot of currently-tracked terminal IDs (delegates to TerminalManager). */
82
+ liveTerminalIds(): Set<string> {
83
+ return this.tm.liveTerminalIds();
84
+ }
85
+
86
+ // ── TerminalBackend interface ──────────────────────────────────────────────
87
+
88
+ async createTerminal(opts: BackendCreateOptions): Promise<{ id: string; logPath: string | null }> {
89
+ if (opts.wrapped) {
90
+ const corrId = opts.correlationId;
91
+ const { id } = this.tm.createWrapped({
92
+ name: opts.name,
93
+ cwd: opts.cwd,
94
+ shellPath: opts.shellPath,
95
+ env: opts.env,
96
+ correlationId: corrId,
97
+ // AC-1: emit terminal:ready on first pty byte, exactly once, only when corrId is set.
98
+ onFirstOutput: corrId
99
+ ? (termId: string) => {
100
+ const ev: TerminalReadyEvent = { id: termId, correlationId: corrId };
101
+ this.emit('terminal:ready', ev);
102
+ }
103
+ : undefined,
104
+ // AF-AC Phase 1: emit terminal:process_exited before VS Code UI teardown.
105
+ onProcessExit: corrId
106
+ ? (termId: string, code: number) => {
107
+ const ev: TerminalProcessExitedEvent = { id: termId, correlationId: corrId, exitCode: code };
108
+ this.emit('terminal:process_exited', ev);
109
+ }
110
+ : undefined,
111
+ });
112
+ const ev: TerminalCreatedEvent = { id, name: opts.name ?? `Claws ${id}`, wrapped: true, logPath: null };
113
+ this.emit('terminal:created', ev);
114
+ return { id, logPath: null };
115
+ }
116
+ const { id } = this.tm.createStandard({
117
+ name: opts.name,
118
+ cwd: opts.cwd,
119
+ shellPath: opts.shellPath,
120
+ env: opts.env,
121
+ });
122
+ const ev: TerminalCreatedEvent = { id, name: opts.name ?? `Claws ${id}`, wrapped: false, logPath: null };
123
+ this.emit('terminal:created', ev);
124
+ return { id, logPath: null };
125
+ }
126
+
127
+ async listTerminals(): Promise<BackendTerminalInfo[]> {
128
+ const descriptors = await this.tm.describeAll();
129
+ return descriptors.map((d) => ({
130
+ id: d.id,
131
+ name: d.name,
132
+ shellPid: d.ptyPid ?? d.pid ?? null,
133
+ wrapped: d.wrapped,
134
+ logPath: d.logPath ?? null,
135
+ status: (d.status === 'adopted' ? 'alive' : d.status === 'unknown' ? 'unknown' : 'closed') as BackendTerminalInfo['status'],
136
+ vehicleState: d.vehicleState,
137
+ pid: d.pid ?? null,
138
+ hasShellIntegration: d.hasShellIntegration,
139
+ ptyMode: d.ptyMode,
140
+ }));
141
+ }
142
+
143
+ async sendText(id: string, text: string, opts?: BackendSendOptions): Promise<void> {
144
+ const rec = this.tm.recordById(id);
145
+ if (!rec) return;
146
+ const newline = opts?.newline !== false;
147
+ const paste = opts?.paste === true;
148
+ if (rec.pty) {
149
+ rec.pty.writeInjected(text, newline, paste);
150
+ } else {
151
+ rec.terminal.sendText(text, newline);
152
+ }
153
+ }
154
+
155
+ async closeTerminal(id: string, _origin?: TerminalClosedEvent['origin']): Promise<void> {
156
+ const origin = (_origin ?? 'orchestrator') as TerminalCloseOrigin;
157
+ const rec = this.tm.recordById(id);
158
+ if (rec?.pty) {
159
+ const fgPid = rec.pty.getForegroundProcess().pid ?? rec.pty.pid;
160
+ if (fgPid != null) {
161
+ try { process.kill(fgPid, 'SIGTERM'); } catch { /* already gone */ }
162
+ const killTimer = setTimeout(() => {
163
+ try {
164
+ process.kill(fgPid, 0);
165
+ process.kill(fgPid, 'SIGKILL');
166
+ } catch { /* already gone */ }
167
+ }, 5000);
168
+ if (typeof killTimer.unref === 'function') killTimer.unref();
169
+ }
170
+ }
171
+ this.tm.close(id, origin);
172
+ }
173
+
174
+ async readLog(id: string, offset: number | undefined, limit: number, strip: boolean): Promise<BackendLogSlice> {
175
+ const rec = this.tm.recordById(id);
176
+ if (!rec) throw new Error(`VsCodeBackend: unknown terminal id ${id}`);
177
+ const effectiveLimit = Math.min(limit, MAX_READLOG_BYTES);
178
+
179
+ if (rec.wrapped && rec.pty) {
180
+ const slice = this.captureStore.read(id, offset, effectiveLimit, strip);
181
+ return {
182
+ bytes: slice.bytes,
183
+ offset: slice.offset,
184
+ nextOffset: slice.nextOffset,
185
+ totalSize: slice.totalSize,
186
+ truncated: slice.truncated,
187
+ };
188
+ }
189
+
190
+ if (rec.logPath && fs.existsSync(rec.logPath)) {
191
+ const stat = fs.statSync(rec.logPath);
192
+ const totalSize = stat.size;
193
+ const start = offset !== undefined ? offset : Math.max(0, totalSize - effectiveLimit);
194
+ const fd = fs.openSync(rec.logPath, 'r');
195
+ try {
196
+ const buf = Buffer.alloc(Math.min(effectiveLimit, totalSize - start));
197
+ fs.readSync(fd, buf, 0, buf.length, start);
198
+ let text = buf.toString('utf8');
199
+ if (strip) text = stripAnsi(text);
200
+ return {
201
+ bytes: text,
202
+ offset: start,
203
+ nextOffset: start + buf.length,
204
+ totalSize,
205
+ truncated: buf.length < effectiveLimit,
206
+ };
207
+ } finally {
208
+ fs.closeSync(fd);
209
+ }
210
+ }
211
+
212
+ throw new Error(`VsCodeBackend: terminal ${id} is not wrapped and has no logPath`);
213
+ }
214
+
215
+ async getForegroundProcess(id: string): Promise<ForegroundProcessInfo> {
216
+ const rec = this.tm.recordById(id);
217
+ if (!rec?.pty) return { pid: null, basename: null };
218
+ return rec.pty.getForegroundProcess();
219
+ }
220
+
221
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
222
+ on(event: 'terminal:created', listener: (ev: TerminalCreatedEvent) => void): this;
223
+ on(event: 'terminal:closed', listener: (ev: TerminalClosedEvent) => void): this;
224
+ on(event: 'terminal:data', listener: (ev: TerminalDataEvent) => void): this;
225
+ /** AC-1: first pty byte for a wrapped terminal created with correlationId. */
226
+ on(event: 'terminal:ready', listener: (ev: TerminalReadyEvent) => void): this;
227
+ /** AF-AC Phase 1: pty process exited, fires before VS Code UI teardown. */
228
+ on(event: 'terminal:process_exited', listener: (ev: TerminalProcessExitedEvent) => void): this;
229
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
230
+ on(event: string, listener: (...args: any[]) => void): this {
231
+ return super.on(event, listener);
232
+ }
233
+
234
+ off(event: string, listener: (...args: unknown[]) => void): this {
235
+ return super.off(event, listener);
236
+ }
237
+
238
+ async focusTerminal(id: string): Promise<void> {
239
+ const t = this.tm.terminalById(id);
240
+ if (t) t.show(true);
241
+ }
242
+
243
+ async execCommand(id: string, command: string, timeoutMs: number): Promise<{ output: string; exitCode: number | null }> {
244
+ const rec = this.tm.recordById(id);
245
+ if (!rec) throw new Error(`VsCodeBackend: unknown terminal id ${id}`);
246
+
247
+ if (!rec.terminal.shellIntegration) {
248
+ // Fallback: send the command, no output capture.
249
+ if (rec.pty) {
250
+ rec.pty.writeInjected(command, true, false);
251
+ } else {
252
+ rec.terminal.sendText(command, true);
253
+ }
254
+ return { output: '', exitCode: null };
255
+ }
256
+
257
+ const event = await new Promise<HistoryEvent>((resolve, reject) => {
258
+ const list = this.execWaiters.get(rec.terminal) ?? [];
259
+ const resolver = (ev: HistoryEvent) => { clearTimeout(timer); resolve(ev); };
260
+ const timer = setTimeout(() => {
261
+ const i = list.indexOf(resolver);
262
+ if (i >= 0) list.splice(i, 1);
263
+ reject(new Error(`exec timeout after ${timeoutMs}ms`));
264
+ }, timeoutMs);
265
+ list.push(resolver);
266
+ this.execWaiters.set(rec.terminal, list);
267
+ try {
268
+ rec.terminal.shellIntegration!.executeCommand(command);
269
+ } catch (err) {
270
+ clearTimeout(timer);
271
+ const i = list.indexOf(resolver);
272
+ if (i >= 0) list.splice(i, 1);
273
+ reject(err);
274
+ }
275
+ });
276
+
277
+ return {
278
+ output: '',
279
+ exitCode: (event as unknown as { exitCode?: number }).exitCode ?? null,
280
+ };
281
+ }
282
+ }
@@ -0,0 +1,125 @@
1
+ import { stripAnsi } from './ansi-strip';
2
+
3
+ export interface CaptureSlice {
4
+ bytes: string;
5
+ offset: number;
6
+ nextOffset: number;
7
+ totalSize: number;
8
+ truncated: boolean;
9
+ }
10
+
11
+ // Per-terminal bookkeeping. `buf` is a single growable Buffer whose first
12
+ // `length` bytes are live; anything past `length` is uninitialised capacity.
13
+ // `droppedBefore` counts bytes that have been trimmed off the front of the
14
+ // stream but still contribute to the absolute byte offset exposed to callers.
15
+ interface Entry {
16
+ buf: Buffer;
17
+ length: number;
18
+ droppedBefore: number;
19
+ }
20
+
21
+ const MIN_INITIAL_CAPACITY = 8 * 1024; // 8 KB — tiny writes stay cheap.
22
+
23
+ export type CaptureAppendCallback = (id: string, chunkBytes: number) => void;
24
+
25
+ export class CaptureStore {
26
+ private entries = new Map<string, Entry>();
27
+ private maxBytesPerTerminal: number;
28
+ // LH-9: optional sink that fires on every append. Used by ClawsServer to
29
+ // refresh per-worker activity timestamps for the TTL watchdog. Single
30
+ // callback (no list) — server is the only legitimate consumer.
31
+ private onAppend: CaptureAppendCallback | null = null;
32
+
33
+ constructor(maxBytesPerTerminal: number) {
34
+ this.maxBytesPerTerminal = maxBytesPerTerminal;
35
+ }
36
+
37
+ /** LH-9: Wire an activity sink. Cleared by passing null. */
38
+ setOnAppend(cb: CaptureAppendCallback | null): void {
39
+ this.onAppend = cb;
40
+ }
41
+
42
+ append(id: string, chunk: string | Buffer): void {
43
+ const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, 'utf8');
44
+ // Fire activity sink BEFORE the buffer mutation — cheap and cannot throw
45
+ // (callback-side errors are swallowed) so we never lose activity signal
46
+ // due to a mid-append exception. Zero-byte writes still count as a tick.
47
+ try { this.onAppend?.(id, data.length); } catch { /* sink errors must not break capture */ }
48
+ let entry = this.entries.get(id);
49
+ if (!entry) {
50
+ const initialCap = Math.max(MIN_INITIAL_CAPACITY, Math.min(data.length * 2, this.maxBytesPerTerminal));
51
+ entry = { buf: Buffer.allocUnsafe(Math.max(initialCap, data.length)), length: 0, droppedBefore: 0 };
52
+ this.entries.set(id, entry);
53
+ }
54
+ const needed = entry.length + data.length;
55
+ if (needed > entry.buf.length) {
56
+ // Grow to at least `needed`, capped at (maxBytes * 2) — the "+1 chunk"
57
+ // slack lets one pre-trim append happen without rebuilding twice. We
58
+ // allocate slightly oversized so common-case small writes don't churn.
59
+ const cap = Math.max(
60
+ needed,
61
+ Math.min(entry.buf.length * 2, this.maxBytesPerTerminal * 2),
62
+ );
63
+ const bigger = Buffer.allocUnsafe(cap);
64
+ entry.buf.copy(bigger, 0, 0, entry.length);
65
+ entry.buf = bigger;
66
+ }
67
+ data.copy(entry.buf, entry.length);
68
+ entry.length += data.length;
69
+ this.trim(id);
70
+ }
71
+
72
+ private trim(id: string): void {
73
+ const entry = this.entries.get(id);
74
+ if (!entry) return;
75
+ if (entry.length <= this.maxBytesPerTerminal) return;
76
+ const overflow = entry.length - this.maxBytesPerTerminal;
77
+ // Shift live window left by `overflow` bytes. Keep capacity unchanged —
78
+ // no allocations, no GC pressure.
79
+ entry.buf.copy(entry.buf, 0, overflow, entry.length);
80
+ entry.length -= overflow;
81
+ entry.droppedBefore += overflow;
82
+ }
83
+
84
+ read(id: string, offset: number | undefined, limit: number, strip: boolean): CaptureSlice {
85
+ const entry = this.entries.get(id);
86
+ if (!entry) {
87
+ return { bytes: '', offset: 0, nextOffset: 0, totalSize: 0, truncated: false };
88
+ }
89
+ const totalSize = entry.droppedBefore + entry.length;
90
+ const effectiveOffset = offset == null
91
+ ? Math.max(entry.droppedBefore, totalSize - limit)
92
+ : Math.max(offset, entry.droppedBefore);
93
+ const startInPresent = effectiveOffset - entry.droppedBefore;
94
+ const endInPresent = Math.min(entry.length, startInPresent + limit);
95
+ const sliceLength = Math.max(0, endInPresent - startInPresent);
96
+ // subarray returns a view into the same underlying ArrayBuffer — zero-copy
97
+ // until we call toString(), which only allocates for the actual slice.
98
+ const view = entry.buf.subarray(startInPresent, startInPresent + sliceLength);
99
+ const text = strip ? stripAnsi(view.toString('utf8')) : view.toString('utf8');
100
+ return {
101
+ bytes: text,
102
+ offset: effectiveOffset,
103
+ nextOffset: effectiveOffset + sliceLength,
104
+ totalSize,
105
+ truncated: totalSize > effectiveOffset + sliceLength,
106
+ };
107
+ }
108
+
109
+ clear(id: string): void {
110
+ this.entries.delete(id);
111
+ }
112
+
113
+ has(id: string): boolean {
114
+ return this.entries.has(id);
115
+ }
116
+
117
+ setMaxBytesPerTerminal(bytes: number): void {
118
+ this.maxBytesPerTerminal = bytes;
119
+ for (const id of this.entries.keys()) this.trim(id);
120
+ }
121
+
122
+ getMaxBytesPerTerminal(): number {
123
+ return this.maxBytesPerTerminal;
124
+ }
125
+ }