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,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
|
+
}
|