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,492 @@
|
|
|
1
|
+
// Claws — Terminal Control Bridge for VS Code.
|
|
2
|
+
//
|
|
3
|
+
// Protocol: newline-delimited JSON over ${workspace}/.claws/claws.sock
|
|
4
|
+
// Every request: { id, cmd, ...args }
|
|
5
|
+
// Every response: { id, ok, ...fields } or { id, ok:false, error }
|
|
6
|
+
//
|
|
7
|
+
// Commands:
|
|
8
|
+
// list -> { terminals: [{id,name,pid,hasShellIntegration,active,logPath}] }
|
|
9
|
+
// create {name?, cwd?, show?, wrapped?} -> { id, logPath? }
|
|
10
|
+
// show {id, preserveFocus?} -> {}
|
|
11
|
+
// send {id, text, newline?} -> {}
|
|
12
|
+
// exec {id, command, timeoutMs?} -> { commandLine, output, exitCode }
|
|
13
|
+
// read {id?, since?, limit?} -> { events: [...] }
|
|
14
|
+
// poll {since?} -> { events: [...], cursor }
|
|
15
|
+
// readLog {id, offset?, limit?, strip?} -> { bytes, truncated, logPath }
|
|
16
|
+
// close {id} -> {}
|
|
17
|
+
//
|
|
18
|
+
// Events are {seq, terminalId, terminalName, commandLine, output, exitCode, startedAt, endedAt}.
|
|
19
|
+
|
|
20
|
+
const vscode = require('vscode');
|
|
21
|
+
const net = require('net');
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
// Defaults — overridden by contributes.configuration in package.json.
|
|
26
|
+
const DEFAULT_SOCKET_REL = '.claws/claws.sock';
|
|
27
|
+
const DEFAULT_LOG_DIR_REL = '.claws/terminals';
|
|
28
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
|
|
29
|
+
const DEFAULT_MAX_HISTORY = 500;
|
|
30
|
+
const MAX_READLOG_BYTES = 512 * 1024;
|
|
31
|
+
|
|
32
|
+
// Strip ANSI / control sequences for clean text from a pty log.
|
|
33
|
+
const ANSI_PATTERN = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-ntqry=><]/g;
|
|
34
|
+
const CTRL_PATTERN = /[\u0000-\u0008\u000b-\u001a\u001c-\u001f\u007f]/g;
|
|
35
|
+
|
|
36
|
+
function getConfig(key, fallback) {
|
|
37
|
+
return vscode.workspace.getConfiguration('claws').get(key, fallback);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let server = null;
|
|
41
|
+
let socketPath = null;
|
|
42
|
+
const outputChannel = vscode.window.createOutputChannel('Claws');
|
|
43
|
+
|
|
44
|
+
const terminalIds = new WeakMap();
|
|
45
|
+
let nextTerminalId = 1;
|
|
46
|
+
|
|
47
|
+
const terminalLogPaths = new Map();
|
|
48
|
+
|
|
49
|
+
const pendingWrappedProfiles = [];
|
|
50
|
+
|
|
51
|
+
const runningExec = new WeakMap();
|
|
52
|
+
|
|
53
|
+
const history = [];
|
|
54
|
+
let nextSeq = 1;
|
|
55
|
+
|
|
56
|
+
const execWaiters = new WeakMap();
|
|
57
|
+
|
|
58
|
+
function idFor(terminal) {
|
|
59
|
+
let id = terminalIds.get(terminal);
|
|
60
|
+
if (id == null) {
|
|
61
|
+
id = String(nextTerminalId++);
|
|
62
|
+
terminalIds.set(terminal, id);
|
|
63
|
+
}
|
|
64
|
+
return id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function terminalById(id) {
|
|
68
|
+
for (const t of vscode.window.terminals) {
|
|
69
|
+
if (idFor(t) === String(id)) return t;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function describeTerminal(t) {
|
|
75
|
+
let pid = null;
|
|
76
|
+
try { pid = await t.processId; } catch {}
|
|
77
|
+
const id = idFor(t);
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
name: t.name,
|
|
81
|
+
pid,
|
|
82
|
+
hasShellIntegration: !!t.shellIntegration,
|
|
83
|
+
active: vscode.window.activeTerminal === t,
|
|
84
|
+
logPath: terminalLogPaths.get(id) || null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function maxOutputBytes() {
|
|
89
|
+
return getConfig('maxOutputBytes', DEFAULT_MAX_OUTPUT_BYTES);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function maxHistory() {
|
|
93
|
+
return getConfig('maxHistory', DEFAULT_MAX_HISTORY);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function pushEvent(terminal, commandLine, output, exitCode, startedAt, endedAt) {
|
|
97
|
+
const cap = maxOutputBytes();
|
|
98
|
+
const ev = {
|
|
99
|
+
seq: nextSeq++,
|
|
100
|
+
terminalId: idFor(terminal),
|
|
101
|
+
terminalName: terminal.name,
|
|
102
|
+
commandLine,
|
|
103
|
+
output: output.length > cap
|
|
104
|
+
? output.slice(0, cap) + `\n[...truncated ${output.length - cap} bytes]`
|
|
105
|
+
: output,
|
|
106
|
+
exitCode: exitCode ?? null,
|
|
107
|
+
startedAt,
|
|
108
|
+
endedAt,
|
|
109
|
+
};
|
|
110
|
+
history.push(ev);
|
|
111
|
+
const cap2 = maxHistory();
|
|
112
|
+
while (history.length > cap2) history.shift();
|
|
113
|
+
outputChannel.appendLine(
|
|
114
|
+
`[seq ${ev.seq}] ${ev.terminalName}#${ev.terminalId} exit=${ev.exitCode} ` +
|
|
115
|
+
`cmd=${JSON.stringify((commandLine || '').slice(0, 80))}`
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const waiters = execWaiters.get(terminal);
|
|
119
|
+
if (waiters && waiters.length) {
|
|
120
|
+
const w = waiters.shift();
|
|
121
|
+
w(ev);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function attachShellIntegrationListeners(context) {
|
|
126
|
+
if (typeof vscode.window.onDidStartTerminalShellExecution === 'function') {
|
|
127
|
+
context.subscriptions.push(
|
|
128
|
+
vscode.window.onDidStartTerminalShellExecution(async (e) => {
|
|
129
|
+
const terminal = e.terminal;
|
|
130
|
+
const state = {
|
|
131
|
+
commandLine: (e.execution.commandLine && e.execution.commandLine.value) || '',
|
|
132
|
+
output: '',
|
|
133
|
+
startedAt: Date.now(),
|
|
134
|
+
};
|
|
135
|
+
runningExec.set(terminal, state);
|
|
136
|
+
try {
|
|
137
|
+
const stream = e.execution.read();
|
|
138
|
+
for await (const chunk of stream) {
|
|
139
|
+
state.output += chunk;
|
|
140
|
+
const cap = maxOutputBytes();
|
|
141
|
+
if (state.output.length > cap * 2) {
|
|
142
|
+
state.output = state.output.slice(-cap * 2);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
outputChannel.appendLine(`[read error] ${err}`);
|
|
147
|
+
}
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof vscode.window.onDidEndTerminalShellExecution === 'function') {
|
|
153
|
+
context.subscriptions.push(
|
|
154
|
+
vscode.window.onDidEndTerminalShellExecution((e) => {
|
|
155
|
+
const terminal = e.terminal;
|
|
156
|
+
const state = runningExec.get(terminal);
|
|
157
|
+
runningExec.delete(terminal);
|
|
158
|
+
const commandLine = state ? state.commandLine
|
|
159
|
+
: ((e.execution.commandLine && e.execution.commandLine.value) || '');
|
|
160
|
+
const output = state ? state.output : '';
|
|
161
|
+
pushEvent(terminal, commandLine, output, e.exitCode, state?.startedAt ?? Date.now(), Date.now());
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Resolve the terminal-wrapper.sh script path.
|
|
168
|
+
// Priority: workspace-local scripts/terminal-wrapper.sh > extension-bundled.
|
|
169
|
+
function resolveWrapperScript(wsRoot, extensionPath) {
|
|
170
|
+
const local = path.join(wsRoot, 'scripts', 'terminal-wrapper.sh');
|
|
171
|
+
if (fs.existsSync(local)) return local;
|
|
172
|
+
const bundled = path.join(extensionPath, '..', 'scripts', 'terminal-wrapper.sh');
|
|
173
|
+
if (fs.existsSync(bundled)) return bundled;
|
|
174
|
+
// Fallback: look in extension src directory
|
|
175
|
+
const srcBundled = path.join(extensionPath, 'scripts', 'terminal-wrapper.sh');
|
|
176
|
+
if (fs.existsSync(srcBundled)) return srcBundled;
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function handle(req, extensionPath) {
|
|
181
|
+
const { cmd } = req || {};
|
|
182
|
+
if (cmd === 'list') {
|
|
183
|
+
const out = [];
|
|
184
|
+
for (const t of vscode.window.terminals) {
|
|
185
|
+
out.push(await describeTerminal(t));
|
|
186
|
+
}
|
|
187
|
+
return { ok: true, terminals: out };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (cmd === 'create') {
|
|
191
|
+
const wsRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
192
|
+
const wantWrapped = req.wrapped === true && !!wsRoot;
|
|
193
|
+
|
|
194
|
+
const reservedId = String(nextTerminalId++);
|
|
195
|
+
let logPath = null;
|
|
196
|
+
const options = {
|
|
197
|
+
name: req.name || 'claws',
|
|
198
|
+
cwd: req.cwd,
|
|
199
|
+
shellPath: req.shellPath,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (wantWrapped) {
|
|
203
|
+
const logDir = getConfig('logDirectory', DEFAULT_LOG_DIR_REL);
|
|
204
|
+
logPath = path.join(wsRoot, logDir, `claws-${reservedId}.log`);
|
|
205
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
206
|
+
const wrapperPath = resolveWrapperScript(wsRoot, extensionPath);
|
|
207
|
+
if (!wrapperPath) {
|
|
208
|
+
return { ok: false, error: 'terminal-wrapper.sh not found. Place it in workspace scripts/ or extension scripts/' };
|
|
209
|
+
}
|
|
210
|
+
options.shellPath = wrapperPath;
|
|
211
|
+
options.env = {
|
|
212
|
+
...(req.env || {}),
|
|
213
|
+
CLAWS_TERM_LOG: logPath,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const t = vscode.window.createTerminal(options);
|
|
218
|
+
terminalIds.set(t, reservedId);
|
|
219
|
+
if (logPath) terminalLogPaths.set(reservedId, logPath);
|
|
220
|
+
if (req.show !== false) t.show(req.preserveFocus !== false);
|
|
221
|
+
return { ok: true, id: reservedId, logPath };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (cmd === 'show') {
|
|
225
|
+
const t = terminalById(req.id);
|
|
226
|
+
if (!t) return { ok: false, error: `unknown terminal id ${req.id}` };
|
|
227
|
+
t.show(req.preserveFocus !== false);
|
|
228
|
+
return { ok: true };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (cmd === 'send') {
|
|
232
|
+
const t = terminalById(req.id);
|
|
233
|
+
if (!t) return { ok: false, error: `unknown terminal id ${req.id}` };
|
|
234
|
+
if (req.show !== false) t.show(true);
|
|
235
|
+
t.sendText(req.text ?? '', req.newline !== false);
|
|
236
|
+
return { ok: true };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (cmd === 'exec') {
|
|
240
|
+
const t = terminalById(req.id);
|
|
241
|
+
if (!t) return { ok: false, error: `unknown terminal id ${req.id}` };
|
|
242
|
+
if (req.show !== false) t.show(true);
|
|
243
|
+
if (!t.shellIntegration) {
|
|
244
|
+
t.sendText(req.command, true);
|
|
245
|
+
return {
|
|
246
|
+
ok: true,
|
|
247
|
+
degraded: true,
|
|
248
|
+
note: 'no shell integration active; output not captured. Re-run after shell integration activates.',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const timeoutMs = req.timeoutMs || 180000;
|
|
252
|
+
const result = await new Promise((resolve, reject) => {
|
|
253
|
+
const timer = setTimeout(() => {
|
|
254
|
+
const list = execWaiters.get(t) || [];
|
|
255
|
+
const idx = list.indexOf(resolver);
|
|
256
|
+
if (idx >= 0) list.splice(idx, 1);
|
|
257
|
+
reject(new Error(`exec timeout after ${timeoutMs}ms`));
|
|
258
|
+
}, timeoutMs);
|
|
259
|
+
const resolver = (ev) => { clearTimeout(timer); resolve(ev); };
|
|
260
|
+
const list = execWaiters.get(t) || [];
|
|
261
|
+
list.push(resolver);
|
|
262
|
+
execWaiters.set(t, list);
|
|
263
|
+
try {
|
|
264
|
+
t.shellIntegration.executeCommand(req.command);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
clearTimeout(timer);
|
|
267
|
+
const lst = execWaiters.get(t) || [];
|
|
268
|
+
const idx = lst.indexOf(resolver);
|
|
269
|
+
if (idx >= 0) lst.splice(idx, 1);
|
|
270
|
+
reject(err);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return { ok: true, event: result };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (cmd === 'read') {
|
|
277
|
+
const sinceSeq = req.since ?? 0;
|
|
278
|
+
const limit = req.limit ?? 50;
|
|
279
|
+
const filtered = history.filter((ev) => {
|
|
280
|
+
if (ev.seq <= sinceSeq) return false;
|
|
281
|
+
if (req.id != null && ev.terminalId !== String(req.id)) return false;
|
|
282
|
+
return true;
|
|
283
|
+
});
|
|
284
|
+
const slice = filtered.slice(-limit);
|
|
285
|
+
return {
|
|
286
|
+
ok: true,
|
|
287
|
+
events: slice,
|
|
288
|
+
cursor: slice.length ? slice[slice.length - 1].seq : sinceSeq,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (cmd === 'poll') {
|
|
293
|
+
const sinceSeq = req.since ?? 0;
|
|
294
|
+
const events = history.filter((ev) => ev.seq > sinceSeq);
|
|
295
|
+
return {
|
|
296
|
+
ok: true,
|
|
297
|
+
events,
|
|
298
|
+
cursor: events.length ? events[events.length - 1].seq : sinceSeq,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (cmd === 'close') {
|
|
303
|
+
const t = terminalById(req.id);
|
|
304
|
+
if (!t) return { ok: false, error: `unknown terminal id ${req.id}` };
|
|
305
|
+
t.dispose();
|
|
306
|
+
terminalLogPaths.delete(String(req.id));
|
|
307
|
+
return { ok: true };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (cmd === 'readLog') {
|
|
311
|
+
const id = String(req.id);
|
|
312
|
+
const logPath = terminalLogPaths.get(id);
|
|
313
|
+
if (!logPath) return { ok: false, error: `terminal ${id} is not wrapped (no log path)` };
|
|
314
|
+
if (!fs.existsSync(logPath)) return { ok: true, bytes: '', truncated: false, logPath };
|
|
315
|
+
try {
|
|
316
|
+
const stat = fs.statSync(logPath);
|
|
317
|
+
const totalSize = stat.size;
|
|
318
|
+
const limit = Math.min(req.limit || MAX_READLOG_BYTES, MAX_READLOG_BYTES);
|
|
319
|
+
let offset = req.offset;
|
|
320
|
+
if (offset == null) offset = Math.max(0, totalSize - limit);
|
|
321
|
+
const fd = fs.openSync(logPath, 'r');
|
|
322
|
+
try {
|
|
323
|
+
const buf = Buffer.alloc(Math.min(limit, totalSize - offset));
|
|
324
|
+
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
325
|
+
let text = buf.toString('utf8');
|
|
326
|
+
if (req.strip !== false) {
|
|
327
|
+
text = text.replace(ANSI_PATTERN, '').replace(CTRL_PATTERN, '');
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
ok: true,
|
|
331
|
+
bytes: text,
|
|
332
|
+
offset,
|
|
333
|
+
nextOffset: offset + buf.length,
|
|
334
|
+
totalSize,
|
|
335
|
+
truncated: totalSize > offset + buf.length,
|
|
336
|
+
logPath,
|
|
337
|
+
};
|
|
338
|
+
} finally {
|
|
339
|
+
fs.closeSync(fd);
|
|
340
|
+
}
|
|
341
|
+
} catch (err) {
|
|
342
|
+
return { ok: false, error: `read failed: ${err.message || err}` };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return { ok: false, error: `unknown cmd: ${cmd}` };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function startServer(context) {
|
|
350
|
+
const wsRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
351
|
+
if (!wsRoot) {
|
|
352
|
+
outputChannel.appendLine('[claws] no workspace folder; bridge disabled');
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const socketRel = getConfig('socketPath', DEFAULT_SOCKET_REL);
|
|
356
|
+
socketPath = path.join(wsRoot, socketRel);
|
|
357
|
+
fs.mkdirSync(path.dirname(socketPath), { recursive: true });
|
|
358
|
+
try { fs.unlinkSync(socketPath); } catch {}
|
|
359
|
+
|
|
360
|
+
const extPath = context.extensionPath;
|
|
361
|
+
|
|
362
|
+
server = net.createServer((socket) => {
|
|
363
|
+
let buf = '';
|
|
364
|
+
socket.on('data', (data) => {
|
|
365
|
+
buf += data.toString('utf8');
|
|
366
|
+
let idx;
|
|
367
|
+
while ((idx = buf.indexOf('\n')) !== -1) {
|
|
368
|
+
const line = buf.slice(0, idx);
|
|
369
|
+
buf = buf.slice(idx + 1);
|
|
370
|
+
if (!line.trim()) continue;
|
|
371
|
+
let req;
|
|
372
|
+
try {
|
|
373
|
+
req = JSON.parse(line);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
socket.write(JSON.stringify({ ok: false, error: 'bad json' }) + '\n');
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
handle(req, extPath).then((resp) => {
|
|
379
|
+
socket.write(JSON.stringify({ id: req.id, ...resp }) + '\n');
|
|
380
|
+
}).catch((err) => {
|
|
381
|
+
socket.write(JSON.stringify({ id: req.id, ok: false, error: String(err && err.message || err) }) + '\n');
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
socket.on('error', (err) => {
|
|
386
|
+
outputChannel.appendLine(`[socket error] ${err}`);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
server.listen(socketPath, () => {
|
|
391
|
+
try { fs.chmodSync(socketPath, 0o600); } catch {}
|
|
392
|
+
outputChannel.appendLine(`[claws] listening on ${socketPath}`);
|
|
393
|
+
});
|
|
394
|
+
server.on('error', (err) => {
|
|
395
|
+
outputChannel.appendLine(`[server error] ${err}`);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function activate(context) {
|
|
400
|
+
outputChannel.appendLine('[claws] activating');
|
|
401
|
+
|
|
402
|
+
attachShellIntegrationListeners(context);
|
|
403
|
+
startServer(context);
|
|
404
|
+
|
|
405
|
+
for (const t of vscode.window.terminals) idFor(t);
|
|
406
|
+
|
|
407
|
+
context.subscriptions.push(
|
|
408
|
+
vscode.window.onDidOpenTerminal((t) => {
|
|
409
|
+
const match = /^Claws Wrapped (\d+)$/.exec(t.name || '');
|
|
410
|
+
if (match) {
|
|
411
|
+
const reservedId = match[1];
|
|
412
|
+
const idx = pendingWrappedProfiles.findIndex((p) => p.reservedId === reservedId);
|
|
413
|
+
if (idx >= 0) {
|
|
414
|
+
const pending = pendingWrappedProfiles[idx];
|
|
415
|
+
pendingWrappedProfiles.splice(idx, 1);
|
|
416
|
+
terminalIds.set(t, reservedId);
|
|
417
|
+
terminalLogPaths.set(reservedId, pending.logPath);
|
|
418
|
+
outputChannel.appendLine(
|
|
419
|
+
`[profile] adopted ${t.name} -> id=${reservedId} log=${pending.logPath}`,
|
|
420
|
+
);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
idFor(t);
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const wsRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
429
|
+
if (wsRoot) {
|
|
430
|
+
context.subscriptions.push(
|
|
431
|
+
vscode.window.registerTerminalProfileProvider('claws.wrappedTerminal', {
|
|
432
|
+
provideTerminalProfile() {
|
|
433
|
+
const reservedId = String(nextTerminalId++);
|
|
434
|
+
const logDir = getConfig('logDirectory', DEFAULT_LOG_DIR_REL);
|
|
435
|
+
const logPath = path.join(wsRoot, logDir, `claws-${reservedId}.log`);
|
|
436
|
+
try { fs.mkdirSync(path.dirname(logPath), { recursive: true }); } catch {}
|
|
437
|
+
pendingWrappedProfiles.push({ reservedId, logPath });
|
|
438
|
+
outputChannel.appendLine(
|
|
439
|
+
`[profile] provisioning wrapped terminal id=${reservedId} log=${logPath}`,
|
|
440
|
+
);
|
|
441
|
+
const wrapperPath = resolveWrapperScript(wsRoot, context.extensionPath);
|
|
442
|
+
if (!wrapperPath) {
|
|
443
|
+
outputChannel.appendLine('[profile] WARNING: terminal-wrapper.sh not found');
|
|
444
|
+
}
|
|
445
|
+
return new vscode.TerminalProfile({
|
|
446
|
+
name: `Claws Wrapped ${reservedId}`,
|
|
447
|
+
shellPath: wrapperPath || process.env.SHELL || '/bin/zsh',
|
|
448
|
+
cwd: wsRoot,
|
|
449
|
+
env: { CLAWS_TERM_LOG: logPath },
|
|
450
|
+
});
|
|
451
|
+
},
|
|
452
|
+
}),
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
context.subscriptions.push(
|
|
457
|
+
vscode.commands.registerCommand('claws.status', () => {
|
|
458
|
+
outputChannel.show(true);
|
|
459
|
+
outputChannel.appendLine(
|
|
460
|
+
`status: socket=${socketPath} terminals=${vscode.window.terminals.length} history=${history.length} seq=${nextSeq}`,
|
|
461
|
+
);
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
context.subscriptions.push(
|
|
466
|
+
vscode.commands.registerCommand('claws.listTerminals', async () => {
|
|
467
|
+
const items = [];
|
|
468
|
+
for (const t of vscode.window.terminals) {
|
|
469
|
+
const desc = await describeTerminal(t);
|
|
470
|
+
const wrap = desc.logPath ? 'wrapped' : 'unwrapped';
|
|
471
|
+
items.push(`${desc.id} ${desc.name} pid=${desc.pid} [${wrap}]`);
|
|
472
|
+
}
|
|
473
|
+
outputChannel.show(true);
|
|
474
|
+
outputChannel.appendLine('--- terminals ---');
|
|
475
|
+
items.forEach((i) => outputChannel.appendLine(i));
|
|
476
|
+
}),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
context.subscriptions.push({
|
|
480
|
+
dispose: () => {
|
|
481
|
+
try { server && server.close(); } catch {}
|
|
482
|
+
try { socketPath && fs.unlinkSync(socketPath); } catch {}
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function deactivate() {
|
|
488
|
+
try { server && server.close(); } catch {}
|
|
489
|
+
try { socketPath && fs.unlinkSync(socketPath); } catch {}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
module.exports = { activate, deactivate };
|