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,873 @@
|
|
|
1
|
+
import * as vscode from 'vscode';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { CaptureStore } from './capture-store';
|
|
6
|
+
import { TerminalManager } from './terminal-manager';
|
|
7
|
+
import { ClawsPty, loadNodePtyStatus } from './backends/vscode/claws-pty';
|
|
8
|
+
import { ClawsServer, IntrospectSnapshot } from './server';
|
|
9
|
+
import { VsCodeBackend } from './backends/vscode/vscode-backend';
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_EXEC_TIMEOUT_MS,
|
|
12
|
+
DEFAULT_POLL_LIMIT,
|
|
13
|
+
DEFAULT_STRICT_EVENT_VALIDATION,
|
|
14
|
+
DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
15
|
+
DEFAULT_EVENT_LOG_RETENTION_DAYS,
|
|
16
|
+
DEFAULT_EVENT_LOG_COMPACT,
|
|
17
|
+
DEFAULT_MAX_PUBLISH_RATE_HZ,
|
|
18
|
+
DEFAULT_MAX_QUEUE_DEPTH,
|
|
19
|
+
ServerConfig,
|
|
20
|
+
} from './server-config';
|
|
21
|
+
import { HistoryEvent } from './protocol';
|
|
22
|
+
import { createStatusBar, StatusBarHandle } from './backends/vscode/status-bar';
|
|
23
|
+
import { registerUninstallCleanupCommand } from './uninstall-cleanup';
|
|
24
|
+
|
|
25
|
+
interface PendingProfile {
|
|
26
|
+
/** Stable id reserved from the TerminalManager. */
|
|
27
|
+
id: string;
|
|
28
|
+
/** UUID-suffixed terminal name used for match-on-open. */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Full unique token embedded in the name (not just the short id). */
|
|
31
|
+
token: string;
|
|
32
|
+
pty: ClawsPty;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_SOCKET_REL = '.claws/claws.sock';
|
|
36
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
|
|
37
|
+
const DEFAULT_MAX_HISTORY = 500;
|
|
38
|
+
const DEFAULT_MAX_CAPTURE_BYTES = 1024 * 1024;
|
|
39
|
+
/** Sentinel prefix embedded in wrapped-terminal names for UUID matching. */
|
|
40
|
+
const CLAWS_PROFILE_NAME_PREFIX = 'Claws Wrapped';
|
|
41
|
+
|
|
42
|
+
function cfg<T>(key: string, fallback: T): T {
|
|
43
|
+
return vscode.workspace.getConfiguration('claws').get<T>(key, fallback);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Map of workspaceFolder fsPath → ClawsServer. Multi-root workspaces get one
|
|
47
|
+
// server per folder. For backwards compatibility, the legacy single-server
|
|
48
|
+
// `server` handle points at the first entry in `servers` (or null).
|
|
49
|
+
const servers = new Map<string, ClawsServer>();
|
|
50
|
+
let server: ClawsServer | null = null;
|
|
51
|
+
let outputChannel: vscode.OutputChannel | null = null;
|
|
52
|
+
let statusBar: StatusBarHandle | null = null;
|
|
53
|
+
/** Registered deactivate-time disposers beyond vscode.Disposable. */
|
|
54
|
+
const deactivateHooks: Array<() => Promise<void> | void> = [];
|
|
55
|
+
|
|
56
|
+
function updateStatusBar(): void {
|
|
57
|
+
statusBar?.update();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function activate(context: vscode.ExtensionContext): void {
|
|
61
|
+
// Create the Output channel FIRST so every subsequent log — including
|
|
62
|
+
// errors from the activation body — has a destination. If creating the
|
|
63
|
+
// channel itself fails we can't meaningfully recover; let the exception
|
|
64
|
+
// propagate.
|
|
65
|
+
outputChannel = vscode.window.createOutputChannel('Claws');
|
|
66
|
+
const logger = (msg: string) => outputChannel!.appendLine(msg);
|
|
67
|
+
const version = context.extension?.packageJSON?.version || '0.5.x';
|
|
68
|
+
logger(`[claws] activating — version ${version} (typescript)`);
|
|
69
|
+
logger(`[claws] extension path: ${context.extensionPath}`);
|
|
70
|
+
logger(`[claws] node: ${process.version} (abi ${process.versions.modules})`);
|
|
71
|
+
logger(`[claws] platform: ${process.platform} ${process.arch}`);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
activateInner(context, logger);
|
|
75
|
+
logger('[claws] activation complete');
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const message = (err as Error).message || String(err);
|
|
78
|
+
const stack = (err as Error).stack;
|
|
79
|
+
logger(`[claws] ACTIVATION FAILED: ${message}`);
|
|
80
|
+
if (stack) logger(`[claws] stack: ${stack}`);
|
|
81
|
+
vscode.window.showErrorMessage(
|
|
82
|
+
`Claws failed to activate: ${message}. Open View → Output → Claws for details.`,
|
|
83
|
+
'Open Log',
|
|
84
|
+
).then((choice) => {
|
|
85
|
+
if (choice === 'Open Log') outputChannel?.show(true);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function activateInner(context: vscode.ExtensionContext, logger: (msg: string) => void): void {
|
|
91
|
+
const folders = vscode.workspace.workspaceFolders ?? [];
|
|
92
|
+
const version = (context.extension?.packageJSON?.version as string) || '0.5.x';
|
|
93
|
+
|
|
94
|
+
if (folders.length === 0) {
|
|
95
|
+
logger('[claws] no workspace folder; bridge disabled (open a folder to activate)');
|
|
96
|
+
registerDiagnosticCommandsNoWorkspace(context, logger);
|
|
97
|
+
statusBar = createStatusBar(context, {
|
|
98
|
+
activated: false,
|
|
99
|
+
version,
|
|
100
|
+
getServers: () => servers,
|
|
101
|
+
getTerminalCount: () => 0,
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// First folder is used as the "primary" for wrapped-terminal cwd and other
|
|
107
|
+
// single-folder defaults. Each folder still gets its own socket server.
|
|
108
|
+
const wsRoot = folders[0].uri.fsPath;
|
|
109
|
+
|
|
110
|
+
const captureStore = new CaptureStore(cfg('maxCaptureBytes', DEFAULT_MAX_CAPTURE_BYTES));
|
|
111
|
+
const terminalManager = new TerminalManager(captureStore, logger);
|
|
112
|
+
terminalManager.adoptExisting(vscode.window.terminals);
|
|
113
|
+
|
|
114
|
+
const vscodeBackend = new VsCodeBackend({ captureStore, terminalManager, logger });
|
|
115
|
+
void vscodeBackend.start();
|
|
116
|
+
|
|
117
|
+
const history: HistoryEvent[] = [];
|
|
118
|
+
let nextSeq = 1;
|
|
119
|
+
const runningExec = new WeakMap<vscode.Terminal, {
|
|
120
|
+
commandLine: string;
|
|
121
|
+
output: string;
|
|
122
|
+
startedAt: number;
|
|
123
|
+
}>();
|
|
124
|
+
|
|
125
|
+
const pushEvent = (
|
|
126
|
+
terminal: vscode.Terminal,
|
|
127
|
+
commandLine: string,
|
|
128
|
+
output: string,
|
|
129
|
+
exitCode: number | null,
|
|
130
|
+
startedAt: number,
|
|
131
|
+
endedAt: number,
|
|
132
|
+
): void => {
|
|
133
|
+
const cap = cfg('maxOutputBytes', DEFAULT_MAX_OUTPUT_BYTES);
|
|
134
|
+
const id = terminalManager.idFor(terminal);
|
|
135
|
+
const ev: HistoryEvent = {
|
|
136
|
+
seq: nextSeq++,
|
|
137
|
+
terminalId: id,
|
|
138
|
+
terminalName: terminal.name,
|
|
139
|
+
commandLine,
|
|
140
|
+
output: output.length > cap
|
|
141
|
+
? output.slice(0, cap) + `\n[...truncated ${output.length - cap} bytes]`
|
|
142
|
+
: output,
|
|
143
|
+
exitCode,
|
|
144
|
+
startedAt,
|
|
145
|
+
endedAt,
|
|
146
|
+
};
|
|
147
|
+
history.push(ev);
|
|
148
|
+
const maxHist = cfg('maxHistory', DEFAULT_MAX_HISTORY);
|
|
149
|
+
while (history.length > maxHist) history.shift();
|
|
150
|
+
logger(
|
|
151
|
+
`[seq ${ev.seq}] ${ev.terminalName}#${ev.terminalId} exit=${ev.exitCode} ` +
|
|
152
|
+
`cmd=${JSON.stringify((commandLine || '').slice(0, 80))}`,
|
|
153
|
+
);
|
|
154
|
+
const waiters = vscodeBackend.execWaiters.get(terminal);
|
|
155
|
+
if (waiters && waiters.length) {
|
|
156
|
+
const w = waiters.shift()!;
|
|
157
|
+
w(ev);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (typeof vscode.window.onDidStartTerminalShellExecution === 'function') {
|
|
162
|
+
context.subscriptions.push(
|
|
163
|
+
vscode.window.onDidStartTerminalShellExecution(async (e) => {
|
|
164
|
+
const terminal = e.terminal;
|
|
165
|
+
const state = {
|
|
166
|
+
commandLine: e.execution.commandLine?.value || '',
|
|
167
|
+
output: '',
|
|
168
|
+
startedAt: Date.now(),
|
|
169
|
+
};
|
|
170
|
+
runningExec.set(terminal, state);
|
|
171
|
+
try {
|
|
172
|
+
const stream = e.execution.read();
|
|
173
|
+
for await (const chunk of stream) {
|
|
174
|
+
state.output += chunk;
|
|
175
|
+
const cap = cfg('maxOutputBytes', DEFAULT_MAX_OUTPUT_BYTES);
|
|
176
|
+
if (state.output.length > cap * 2) {
|
|
177
|
+
state.output = state.output.slice(-cap * 2);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
logger(`[read error] ${err}`);
|
|
182
|
+
}
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (typeof vscode.window.onDidEndTerminalShellExecution === 'function') {
|
|
188
|
+
context.subscriptions.push(
|
|
189
|
+
vscode.window.onDidEndTerminalShellExecution((e) => {
|
|
190
|
+
const terminal = e.terminal;
|
|
191
|
+
const state = runningExec.get(terminal);
|
|
192
|
+
runningExec.delete(terminal);
|
|
193
|
+
const commandLine = state ? state.commandLine : (e.execution.commandLine?.value || '');
|
|
194
|
+
const output = state ? state.output : '';
|
|
195
|
+
pushEvent(
|
|
196
|
+
terminal,
|
|
197
|
+
commandLine,
|
|
198
|
+
output,
|
|
199
|
+
e.exitCode ?? null,
|
|
200
|
+
state?.startedAt ?? Date.now(),
|
|
201
|
+
Date.now(),
|
|
202
|
+
);
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const pendingProfiles: PendingProfile[] = [];
|
|
208
|
+
// Timeouts keyed by pending profile id — cleared on successful adoption,
|
|
209
|
+
// fire after 30s to dispose the orphan ClawsPty.
|
|
210
|
+
const pendingTimers = new Map<string, NodeJS.Timeout>();
|
|
211
|
+
const PENDING_TIMEOUT_MS = 30_000;
|
|
212
|
+
|
|
213
|
+
const clearPending = (id: string): void => {
|
|
214
|
+
const t = pendingTimers.get(id);
|
|
215
|
+
if (t) {
|
|
216
|
+
clearTimeout(t);
|
|
217
|
+
pendingTimers.delete(id);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
context.subscriptions.push(
|
|
222
|
+
vscode.window.onDidOpenTerminal((t) => {
|
|
223
|
+
// #6: UUID-based matching. We embed the pending profile's UUID token
|
|
224
|
+
// in the terminal name; here we match by that token rather than by a
|
|
225
|
+
// collision-prone "Claws Wrapped <id>" label. If two provisions raced,
|
|
226
|
+
// each has its own UUID and we can never mis-link them.
|
|
227
|
+
const idx = pendingProfiles.findIndex((p) => t.name.includes(p.token));
|
|
228
|
+
if (idx >= 0) {
|
|
229
|
+
const pending = pendingProfiles[idx];
|
|
230
|
+
pendingProfiles.splice(idx, 1);
|
|
231
|
+
clearPending(pending.id);
|
|
232
|
+
terminalManager.linkProfileTerminal(pending.id, t, pending.pty);
|
|
233
|
+
logger(`[profile] adopted ${t.name} -> id=${pending.id}`);
|
|
234
|
+
updateStatusBar();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
terminalManager.idFor(t);
|
|
238
|
+
updateStatusBar();
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
context.subscriptions.push(
|
|
243
|
+
vscode.window.onDidCloseTerminal((t) => {
|
|
244
|
+
terminalManager.onTerminalClosed(t);
|
|
245
|
+
updateStatusBar();
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
context.subscriptions.push(
|
|
250
|
+
vscode.window.registerTerminalProfileProvider('claws.wrappedTerminal', {
|
|
251
|
+
provideTerminalProfile(): vscode.TerminalProfile {
|
|
252
|
+
const id = terminalManager.reserveNextId();
|
|
253
|
+
const token = randomUUID();
|
|
254
|
+
// Short suffix keeps the name readable while the full UUID lives in
|
|
255
|
+
// the tail for match-on-open — VS Code renders the full string in
|
|
256
|
+
// the tab label but the user sees "Claws Wrapped 3 · a4f7…".
|
|
257
|
+
const short = token.slice(0, 8);
|
|
258
|
+
const name = `${CLAWS_PROFILE_NAME_PREFIX} ${id} · ${short} [${token}]`;
|
|
259
|
+
const pty = new ClawsPty({
|
|
260
|
+
terminalId: id,
|
|
261
|
+
cwd: wsRoot,
|
|
262
|
+
captureStore,
|
|
263
|
+
logger,
|
|
264
|
+
});
|
|
265
|
+
pendingProfiles.push({ id, name, token, pty });
|
|
266
|
+
logger(`[profile] provisioning wrapped terminal id=${id} token=${token.slice(0, 8)}`);
|
|
267
|
+
|
|
268
|
+
// If VS Code never opens a terminal for this profile (user cancelled
|
|
269
|
+
// or some internal error), reclaim the pending slot + dispose the pty.
|
|
270
|
+
const timer = setTimeout(() => {
|
|
271
|
+
const idx = pendingProfiles.findIndex((p) => p.id === id);
|
|
272
|
+
if (idx < 0) return; // already adopted
|
|
273
|
+
const pending = pendingProfiles[idx];
|
|
274
|
+
pendingProfiles.splice(idx, 1);
|
|
275
|
+
pendingTimers.delete(id);
|
|
276
|
+
try { pending.pty.close(); } catch { /* ignore */ }
|
|
277
|
+
logger(`[profile] expired pending id=${id} — disposed orphan pty`);
|
|
278
|
+
}, PENDING_TIMEOUT_MS);
|
|
279
|
+
pendingTimers.set(id, timer);
|
|
280
|
+
|
|
281
|
+
return new vscode.TerminalProfile({ name, pty });
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Live ServerConfig accessor wired from vscode settings — the server
|
|
287
|
+
// receives this and re-reads on every request, so settings.json edits
|
|
288
|
+
// take effect without a VS Code reload.
|
|
289
|
+
const getConfig = (): ServerConfig => ({
|
|
290
|
+
execTimeoutMs: cfg('execTimeoutMs', DEFAULT_EXEC_TIMEOUT_MS),
|
|
291
|
+
pollLimit: cfg('pollLimit', DEFAULT_POLL_LIMIT),
|
|
292
|
+
strictEventValidation: cfg('strictEventValidation', DEFAULT_STRICT_EVENT_VALIDATION),
|
|
293
|
+
heartbeatIntervalMs: cfg('heartbeatIntervalMs', DEFAULT_HEARTBEAT_INTERVAL_MS),
|
|
294
|
+
maxPublishRateHz: cfg('maxPublishRateHz', DEFAULT_MAX_PUBLISH_RATE_HZ),
|
|
295
|
+
maxQueueDepth: cfg('maxQueueDepth', DEFAULT_MAX_QUEUE_DEPTH),
|
|
296
|
+
eventLog: {
|
|
297
|
+
retentionDays: cfg('eventLog.retentionDays', DEFAULT_EVENT_LOG_RETENTION_DAYS),
|
|
298
|
+
compact: cfg('eventLog.compact', DEFAULT_EVENT_LOG_COMPACT),
|
|
299
|
+
},
|
|
300
|
+
auth: {
|
|
301
|
+
enabled: cfg('auth.enabled', false),
|
|
302
|
+
tokenPath: cfg('auth.tokenPath', '.claws/auth.token'),
|
|
303
|
+
},
|
|
304
|
+
webSocket: {
|
|
305
|
+
enabled: cfg('webSocket.enabled', false),
|
|
306
|
+
port: cfg('webSocket.port', 5678),
|
|
307
|
+
certPath: cfg('webSocket.certPath', ''),
|
|
308
|
+
keyPath: cfg('webSocket.keyPath', ''),
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const buildIntrospectSnapshot = (): IntrospectSnapshot => {
|
|
313
|
+
const npty = loadNodePtyStatus();
|
|
314
|
+
return {
|
|
315
|
+
extensionVersion: version,
|
|
316
|
+
nodePty: {
|
|
317
|
+
loaded: npty.loaded,
|
|
318
|
+
loadedFrom: npty.loadedFrom ?? null,
|
|
319
|
+
error: npty.error?.message,
|
|
320
|
+
},
|
|
321
|
+
servers: Array.from(servers.entries()).map(([workspace, srv]) => ({
|
|
322
|
+
workspace,
|
|
323
|
+
socket: srv.getSocketPath(),
|
|
324
|
+
})),
|
|
325
|
+
terminals: vscode.window.terminals.length,
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const startServerFor = (folder: vscode.WorkspaceFolder): void => {
|
|
330
|
+
const root = folder.uri.fsPath;
|
|
331
|
+
if (servers.has(root)) return;
|
|
332
|
+
const srv = new ClawsServer({
|
|
333
|
+
workspaceRoot: root,
|
|
334
|
+
socketRel: cfg('socketPath', DEFAULT_SOCKET_REL),
|
|
335
|
+
captureStore,
|
|
336
|
+
backend: vscodeBackend,
|
|
337
|
+
logger,
|
|
338
|
+
history,
|
|
339
|
+
getConfig,
|
|
340
|
+
getIntrospect: buildIntrospectSnapshot,
|
|
341
|
+
reloadWindow: () => { void vscode.commands.executeCommand('workbench.action.reloadWindow'); },
|
|
342
|
+
});
|
|
343
|
+
srv.start();
|
|
344
|
+
servers.set(root, srv);
|
|
345
|
+
// Keep legacy module-level handle pointing at the first-available server
|
|
346
|
+
// so existing command paths (status, healthCheck) still resolve something.
|
|
347
|
+
if (!server) server = srv;
|
|
348
|
+
logger(`[claws] server started for folder: ${root}`);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const stopServerFor = (root: string): void => {
|
|
352
|
+
const srv = servers.get(root);
|
|
353
|
+
if (!srv) return;
|
|
354
|
+
srv.stop();
|
|
355
|
+
servers.delete(root);
|
|
356
|
+
if (server === srv) server = servers.values().next().value ?? null;
|
|
357
|
+
logger(`[claws] server stopped for folder: ${root}`);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
for (const folder of folders) startServerFor(folder);
|
|
361
|
+
|
|
362
|
+
if (typeof vscode.workspace.onDidChangeWorkspaceFolders === 'function') {
|
|
363
|
+
context.subscriptions.push(
|
|
364
|
+
vscode.workspace.onDidChangeWorkspaceFolders((e) => {
|
|
365
|
+
for (const added of e.added) startServerFor(added);
|
|
366
|
+
for (const removed of e.removed) stopServerFor(removed.uri.fsPath);
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Config hot-reload — cfg() is already live for most call sites, but
|
|
372
|
+
// construct-once state (CaptureStore cap, socket path) must be refreshed.
|
|
373
|
+
if (typeof vscode.workspace.onDidChangeConfiguration === 'function') {
|
|
374
|
+
context.subscriptions.push(
|
|
375
|
+
vscode.workspace.onDidChangeConfiguration((e) => {
|
|
376
|
+
if (e.affectsConfiguration('claws.maxCaptureBytes')) {
|
|
377
|
+
const newCap = cfg('maxCaptureBytes', DEFAULT_MAX_CAPTURE_BYTES);
|
|
378
|
+
captureStore.setMaxBytesPerTerminal(newCap);
|
|
379
|
+
logger(`[config] maxCaptureBytes updated: ${newCap}`);
|
|
380
|
+
}
|
|
381
|
+
if (e.affectsConfiguration('claws.socketPath')) {
|
|
382
|
+
logger('[config] socketPath change detected — reload VS Code to activate new path');
|
|
383
|
+
vscode.window.showInformationMessage?.(
|
|
384
|
+
'Claws: socket path changed. Reload VS Code to use the new path.',
|
|
385
|
+
'Reload Now',
|
|
386
|
+
)?.then?.((c) => {
|
|
387
|
+
if (c === 'Reload Now') vscode.commands.executeCommand('workbench.action.reloadWindow');
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Rich status output — markdown-style with section headers for readability.
|
|
395
|
+
context.subscriptions.push(
|
|
396
|
+
vscode.commands.registerCommand('claws.status', () => {
|
|
397
|
+
renderStatus(version, history, nextSeq, () => vscode.window.terminals.length);
|
|
398
|
+
}),
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// #39 — QuickPick replacing the plain-text listTerminals dump.
|
|
402
|
+
context.subscriptions.push(
|
|
403
|
+
vscode.commands.registerCommand('claws.listTerminals', async () => {
|
|
404
|
+
const rows = await terminalManager.describeAll();
|
|
405
|
+
const liveTerminals = vscode.window.terminals;
|
|
406
|
+
if (rows.length === 0) {
|
|
407
|
+
vscode.window.showInformationMessage('Claws: no terminals open.');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
interface ClawsQuickPickItem extends vscode.QuickPickItem {
|
|
411
|
+
terminalId: string;
|
|
412
|
+
}
|
|
413
|
+
const items: ClawsQuickPickItem[] = rows.map((d) => {
|
|
414
|
+
const kind = d.wrapped ? 'wrapped(pty)' : d.logPath ? 'wrapped(log)' : 'unwrapped';
|
|
415
|
+
return {
|
|
416
|
+
label: `$(terminal) ${d.id} · ${d.name}`,
|
|
417
|
+
description: `${kind} · pid=${d.pid ?? '?'}`,
|
|
418
|
+
detail: d.status === 'unknown' ? 'not yet adopted by Claws' : undefined,
|
|
419
|
+
terminalId: d.id,
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
const picked = await vscode.window.showQuickPick(items, {
|
|
423
|
+
placeHolder: 'Select a Claws terminal to focus',
|
|
424
|
+
matchOnDescription: true,
|
|
425
|
+
matchOnDetail: true,
|
|
426
|
+
});
|
|
427
|
+
if (!picked) return;
|
|
428
|
+
// Prefer a direct lookup via TerminalManager (stable id), fall back to
|
|
429
|
+
// name match if the terminal record is somehow missing.
|
|
430
|
+
const target = terminalManager.terminalById(picked.terminalId)
|
|
431
|
+
?? liveTerminals.find((t) => t.name === picked.label.replace(/^\$\(terminal\) \d+ · /, ''));
|
|
432
|
+
if (target) target.show(false);
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
// #37 — Status bar item with rich tooltip + click-to-healthcheck.
|
|
437
|
+
statusBar = createStatusBar(context, {
|
|
438
|
+
activated: true,
|
|
439
|
+
version,
|
|
440
|
+
getServers: () => servers,
|
|
441
|
+
getTerminalCount: () => vscode.window.terminals.length,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
registerDiagnosticCommands(context, {
|
|
445
|
+
extensionPath: context.extensionPath,
|
|
446
|
+
version,
|
|
447
|
+
wsRoot,
|
|
448
|
+
getServer: () => server,
|
|
449
|
+
getServers: () => servers,
|
|
450
|
+
getTerminalCount: () => vscode.window.terminals.length,
|
|
451
|
+
getHistoryCount: () => history.length,
|
|
452
|
+
getIntrospect: buildIntrospectSnapshot,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
registerUninstallCleanupCommand(context, logger, () => outputChannel?.show(true));
|
|
456
|
+
|
|
457
|
+
context.subscriptions.push({
|
|
458
|
+
dispose: () => {
|
|
459
|
+
for (const s of servers.values()) {
|
|
460
|
+
try { s.stop(); } catch { /* ignore */ }
|
|
461
|
+
}
|
|
462
|
+
servers.clear();
|
|
463
|
+
server = null;
|
|
464
|
+
for (const timer of pendingTimers.values()) clearTimeout(timer);
|
|
465
|
+
pendingTimers.clear();
|
|
466
|
+
for (const p of pendingProfiles) {
|
|
467
|
+
try { p.pty.close(); } catch { /* ignore */ }
|
|
468
|
+
}
|
|
469
|
+
pendingProfiles.length = 0;
|
|
470
|
+
terminalManager.dispose();
|
|
471
|
+
statusBar?.dispose();
|
|
472
|
+
statusBar = null;
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Async teardown hook — invoked from deactivate(). Keeps the sync dispose
|
|
477
|
+
// above (subscriptions runs sync) while still giving us a place to add
|
|
478
|
+
// awaitable cleanup if future features need it.
|
|
479
|
+
deactivateHooks.push(async () => {
|
|
480
|
+
terminalManager.dispose();
|
|
481
|
+
statusBar?.dispose();
|
|
482
|
+
statusBar = null;
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ─── Diagnostic commands ──────────────────────────────────────────────────
|
|
487
|
+
// These are available whether or not we activated fully (no workspace, etc.)
|
|
488
|
+
// so users can always self-diagnose from inside VS Code.
|
|
489
|
+
|
|
490
|
+
interface DiagContext {
|
|
491
|
+
extensionPath: string;
|
|
492
|
+
version: string;
|
|
493
|
+
wsRoot: string;
|
|
494
|
+
getServer: () => ClawsServer | null;
|
|
495
|
+
getServers: () => Map<string, ClawsServer>;
|
|
496
|
+
getTerminalCount: () => number;
|
|
497
|
+
getHistoryCount: () => number;
|
|
498
|
+
getIntrospect: () => IntrospectSnapshot;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function registerDiagnosticCommands(context: vscode.ExtensionContext, diag: DiagContext): void {
|
|
502
|
+
context.subscriptions.push(
|
|
503
|
+
vscode.commands.registerCommand('claws.healthCheck', () => runHealthCheck(diag)),
|
|
504
|
+
);
|
|
505
|
+
context.subscriptions.push(
|
|
506
|
+
vscode.commands.registerCommand('claws.showLog', () => outputChannel?.show(true)),
|
|
507
|
+
);
|
|
508
|
+
context.subscriptions.push(
|
|
509
|
+
vscode.commands.registerCommand('claws.rebuildPty', () => runRebuildPty(diag.extensionPath)),
|
|
510
|
+
);
|
|
511
|
+
context.subscriptions.push(
|
|
512
|
+
vscode.commands.registerCommand('claws.statusBar', () => {
|
|
513
|
+
// Manually invokable "refresh status bar" command — also re-renders it.
|
|
514
|
+
statusBar?.item.show();
|
|
515
|
+
statusBar?.update();
|
|
516
|
+
}),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function registerDiagnosticCommandsNoWorkspace(
|
|
521
|
+
context: vscode.ExtensionContext,
|
|
522
|
+
logger: (msg: string) => void,
|
|
523
|
+
): void {
|
|
524
|
+
// Even without a workspace, surface the health-check so users can inspect
|
|
525
|
+
// why Claws says the bridge is disabled.
|
|
526
|
+
context.subscriptions.push(
|
|
527
|
+
vscode.commands.registerCommand('claws.healthCheck', () => {
|
|
528
|
+
outputChannel!.show(true);
|
|
529
|
+
logger('── Claws Health Check ──');
|
|
530
|
+
logger('status: BRIDGE DISABLED (no workspace folder open)');
|
|
531
|
+
logger('fix: open a folder via File → Open Folder…');
|
|
532
|
+
logger(`extension path: ${context.extensionPath}`);
|
|
533
|
+
logger(`node: ${process.version} (abi ${process.versions.modules})`);
|
|
534
|
+
const npty = loadNodePtyStatus();
|
|
535
|
+
logger(`node-pty loaded: ${npty.loaded}`);
|
|
536
|
+
if (npty.error) logger(`node-pty error: ${npty.error.message}`);
|
|
537
|
+
}),
|
|
538
|
+
);
|
|
539
|
+
context.subscriptions.push(
|
|
540
|
+
vscode.commands.registerCommand('claws.showLog', () => outputChannel?.show(true)),
|
|
541
|
+
);
|
|
542
|
+
context.subscriptions.push(
|
|
543
|
+
vscode.commands.registerCommand('claws.rebuildPty', () => runRebuildPty(context.extensionPath)),
|
|
544
|
+
);
|
|
545
|
+
context.subscriptions.push(
|
|
546
|
+
vscode.commands.registerCommand('claws.statusBar', () => {
|
|
547
|
+
statusBar?.item.show();
|
|
548
|
+
statusBar?.update();
|
|
549
|
+
}),
|
|
550
|
+
);
|
|
551
|
+
// Register uninstall cleanup even without a workspace — user can open a
|
|
552
|
+
// folder and try again, or the command can early-out cleanly.
|
|
553
|
+
registerUninstallCleanupCommand(context, logger, () => outputChannel?.show(true));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ─── Rich status renderer ─────────────────────────────────────────────────
|
|
557
|
+
|
|
558
|
+
function renderStatus(
|
|
559
|
+
version: string,
|
|
560
|
+
history: HistoryEvent[],
|
|
561
|
+
nextSeq: number,
|
|
562
|
+
getTerminalCount: () => number,
|
|
563
|
+
): void {
|
|
564
|
+
outputChannel!.show(true);
|
|
565
|
+
const write = (m: string) => outputChannel!.appendLine(m);
|
|
566
|
+
const npty = loadNodePtyStatus();
|
|
567
|
+
write('');
|
|
568
|
+
write('# Claws Status');
|
|
569
|
+
write('');
|
|
570
|
+
write(`**Version**: ${version}`);
|
|
571
|
+
write(`**Terminals**: ${getTerminalCount()}`);
|
|
572
|
+
write(`**History events**: ${history.length} (next seq: ${nextSeq})`);
|
|
573
|
+
write('');
|
|
574
|
+
write('## Sockets');
|
|
575
|
+
if (servers.size === 0) {
|
|
576
|
+
write('_none active_');
|
|
577
|
+
} else {
|
|
578
|
+
for (const [root, srv] of servers.entries()) {
|
|
579
|
+
write(`- \`${root}\` → \`${srv.getSocketPath() ?? '(pending)'}\``);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
write('');
|
|
583
|
+
write('## Runtime');
|
|
584
|
+
write(`- node: ${process.version} (ABI ${process.versions.modules})`);
|
|
585
|
+
write(`- platform: ${process.platform} ${process.arch}`);
|
|
586
|
+
write(`- node-pty: ${npty.loaded ? 'loaded' : npty.error ? 'not loaded (pipe-mode)' : 'not attempted'}`);
|
|
587
|
+
if (npty.loadedFrom) write(` - source: ${npty.loadedFrom}`);
|
|
588
|
+
write('');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function runHealthCheck(diag: DiagContext): void {
|
|
592
|
+
outputChannel!.show(true);
|
|
593
|
+
const logger = (msg: string) => outputChannel!.appendLine(msg);
|
|
594
|
+
const snap = diag.getIntrospect();
|
|
595
|
+
|
|
596
|
+
logger('');
|
|
597
|
+
logger('──────────── Claws Health Check ────────────');
|
|
598
|
+
logger(`claws version: ${diag.version}`);
|
|
599
|
+
logger(`workspace: ${diag.wsRoot}`);
|
|
600
|
+
const srvMap = diag.getServers();
|
|
601
|
+
if (srvMap.size === 0) {
|
|
602
|
+
logger('sockets: (none started)');
|
|
603
|
+
} else {
|
|
604
|
+
logger(`sockets: ${srvMap.size} active`);
|
|
605
|
+
for (const [root, srv] of srvMap.entries()) {
|
|
606
|
+
logger(` ${root} → ${srv.getSocketPath() ?? '(not listening)'}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
logger(`terminals: ${diag.getTerminalCount()}`);
|
|
610
|
+
logger(`history events: ${diag.getHistoryCount()}`);
|
|
611
|
+
logger('');
|
|
612
|
+
logger(`node: ${process.version} (ABI ${process.versions.modules})`);
|
|
613
|
+
logger(`platform: ${process.platform} ${process.arch}`);
|
|
614
|
+
logger(`extension: ${diag.extensionPath}`);
|
|
615
|
+
logger('');
|
|
616
|
+
|
|
617
|
+
// #49 — MCP server version detection from workspace-local .claws-bin/.
|
|
618
|
+
const mcpInfo = detectMcpServerVersion(diag.wsRoot);
|
|
619
|
+
if (mcpInfo.present) {
|
|
620
|
+
logger(`mcp server: ${mcpInfo.path}`);
|
|
621
|
+
if (mcpInfo.version) {
|
|
622
|
+
const drift = mcpInfo.version !== diag.version;
|
|
623
|
+
logger(` version: ${mcpInfo.version}${drift ? ' (drift vs extension — consider /claws-update)' : ' (matches extension)'}`);
|
|
624
|
+
} else {
|
|
625
|
+
logger(` version: (unknown — no version string detected in file)`);
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
logger('mcp server: (not installed in this workspace — run /claws-setup)');
|
|
629
|
+
}
|
|
630
|
+
logger('');
|
|
631
|
+
|
|
632
|
+
const npty = snap.nodePty;
|
|
633
|
+
if (npty.loaded) {
|
|
634
|
+
logger('node-pty: ✓ LOADED — wrapped terminals will use real pty (clean TUI rendering)');
|
|
635
|
+
if (npty.loadedFrom) logger(` source: ${npty.loadedFrom}`);
|
|
636
|
+
} else if (npty.error) {
|
|
637
|
+
logger('node-pty: ✗ NOT LOADED — wrapped terminals will use pipe-mode (degraded TUIs)');
|
|
638
|
+
logger(` error: ${npty.error}`);
|
|
639
|
+
logger(' fix: run "Claws: Rebuild Native PTY" from the command palette');
|
|
640
|
+
} else {
|
|
641
|
+
logger('node-pty: · not attempted yet (no wrapped terminal spawned this session)');
|
|
642
|
+
logger(' open a "Claws Wrapped Terminal" from the + dropdown to trigger load');
|
|
643
|
+
}
|
|
644
|
+
logger('');
|
|
645
|
+
|
|
646
|
+
// Check that node-pty binary is on disk at expected paths.
|
|
647
|
+
// Post-Wave-6X: prebuilds/<platform>-<arch>/pty.node is the primary location.
|
|
648
|
+
// build/Release/pty.node and node_modules/ are kept for dev-loop compatibility.
|
|
649
|
+
const prebuildsPath = path.join(diag.extensionPath, 'native', 'node-pty', 'prebuilds', `${process.platform}-${process.arch}`, 'pty.node');
|
|
650
|
+
const bundledPath = path.join(diag.extensionPath, 'native', 'node-pty', 'build', 'Release', 'pty.node');
|
|
651
|
+
const nodeModulesPath = path.join(diag.extensionPath, 'node_modules', 'node-pty', 'build', 'Release', 'pty.node');
|
|
652
|
+
logger('pty.node binary search:');
|
|
653
|
+
let found = false;
|
|
654
|
+
if (fs.existsSync(prebuildsPath)) {
|
|
655
|
+
const stat = fs.statSync(prebuildsPath);
|
|
656
|
+
logger(` ✓ ${prebuildsPath} (${stat.size} bytes) [prebuilds: PRIMARY]`);
|
|
657
|
+
found = true;
|
|
658
|
+
} else {
|
|
659
|
+
logger(` · ${prebuildsPath} (not found) [prebuilds: missing]`);
|
|
660
|
+
}
|
|
661
|
+
const bundledExists = fs.existsSync(bundledPath);
|
|
662
|
+
if (bundledExists) {
|
|
663
|
+
const stat = fs.statSync(bundledPath);
|
|
664
|
+
logger(` ✓ ${bundledPath} (${stat.size} bytes) [bundled]`);
|
|
665
|
+
found = true;
|
|
666
|
+
} else {
|
|
667
|
+
logger(` · ${bundledPath} (not found)`);
|
|
668
|
+
}
|
|
669
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
670
|
+
const stat = fs.statSync(nodeModulesPath);
|
|
671
|
+
logger(` ✓ ${nodeModulesPath} (${stat.size} bytes) [dev-only]`);
|
|
672
|
+
found = true;
|
|
673
|
+
} else {
|
|
674
|
+
logger(` · ${nodeModulesPath} (not found)`);
|
|
675
|
+
}
|
|
676
|
+
if (!found) logger(' ✗ no pty.node on disk — prebuilds path is primary (Wave 6X); run "Claws: Rebuild Native PTY" if missing');
|
|
677
|
+
|
|
678
|
+
// Surface the metadata file written by scripts/bundle-native.mjs so we can
|
|
679
|
+
// see which Electron ABI this binary was built for.
|
|
680
|
+
const metadataPath = path.join(diag.extensionPath, 'native', '.metadata.json');
|
|
681
|
+
if (fs.existsSync(metadataPath)) {
|
|
682
|
+
try {
|
|
683
|
+
const raw = fs.readFileSync(metadataPath, 'utf8');
|
|
684
|
+
const meta = JSON.parse(raw) as {
|
|
685
|
+
electronVersion?: string;
|
|
686
|
+
nodePtyVersion?: string;
|
|
687
|
+
platform?: string;
|
|
688
|
+
arch?: string;
|
|
689
|
+
bundledAt?: string;
|
|
690
|
+
};
|
|
691
|
+
logger('');
|
|
692
|
+
logger('native bundle metadata:');
|
|
693
|
+
if (meta.electronVersion) logger(` electron: ${meta.electronVersion}`);
|
|
694
|
+
if (meta.nodePtyVersion) logger(` node-pty: ${meta.nodePtyVersion}`);
|
|
695
|
+
if (meta.platform || meta.arch) logger(` platform: ${meta.platform ?? '?'}-${meta.arch ?? '?'}`);
|
|
696
|
+
if (meta.bundledAt) logger(` bundled at: ${meta.bundledAt}`);
|
|
697
|
+
} catch (err) {
|
|
698
|
+
logger(` metadata read failed: ${(err as Error).message}`);
|
|
699
|
+
}
|
|
700
|
+
} else {
|
|
701
|
+
logger('');
|
|
702
|
+
logger('native bundle metadata: (none — run `npm run build` to generate)');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
logger('');
|
|
706
|
+
logger('─────────────────────────────────────────────');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Look up the project-local MCP server and try to read its version.
|
|
711
|
+
*
|
|
712
|
+
* Strategy:
|
|
713
|
+
* 1. Stat `<workspace>/.claws-bin/mcp_server.js` — if missing, MCP is not installed.
|
|
714
|
+
* 2. Prefer a sibling `package.json` if present.
|
|
715
|
+
* 3. Fall back to grepping a `version: 'x.y.z'` literal out of the JS source.
|
|
716
|
+
*/
|
|
717
|
+
interface McpVersionInfo {
|
|
718
|
+
present: boolean;
|
|
719
|
+
path: string | null;
|
|
720
|
+
version: string | null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function detectMcpServerVersion(wsRoot: string): McpVersionInfo {
|
|
724
|
+
const mcpPath = path.join(wsRoot, '.claws-bin', 'mcp_server.js');
|
|
725
|
+
if (!fs.existsSync(mcpPath)) return { present: false, path: null, version: null };
|
|
726
|
+
const pkgJsonPath = path.join(wsRoot, '.claws-bin', 'package.json');
|
|
727
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
728
|
+
try {
|
|
729
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')) as { version?: string };
|
|
730
|
+
if (pkg.version) return { present: true, path: mcpPath, version: pkg.version };
|
|
731
|
+
} catch { /* fall through to source-scan */ }
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
const src = fs.readFileSync(mcpPath, 'utf8');
|
|
735
|
+
const m = /version:\s*['"]([\d.]+(?:-[a-z0-9.]+)?)['"]/i.exec(src);
|
|
736
|
+
if (m) return { present: true, path: mcpPath, version: m[1] };
|
|
737
|
+
} catch { /* ignore */ }
|
|
738
|
+
return { present: true, path: mcpPath, version: null };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async function runRebuildPty(extensionPath: string): Promise<void> {
|
|
742
|
+
outputChannel!.show(true);
|
|
743
|
+
const logger = (msg: string) => outputChannel!.appendLine(msg);
|
|
744
|
+
logger('');
|
|
745
|
+
logger('── Claws: Rebuild Native PTY ──');
|
|
746
|
+
|
|
747
|
+
// Detect Electron version so @electron/rebuild can target the right ABI.
|
|
748
|
+
// On macOS we read it from the app bundle's Info.plist. On other platforms
|
|
749
|
+
// we fall back to a default — user can override via env var.
|
|
750
|
+
//
|
|
751
|
+
// F6/M-42: 4 candidates × 3s timeout = 12s worst-case synchronous block on
|
|
752
|
+
// a network-mounted /Applications (NFS/SMB hung filesystem). Acceptable for
|
|
753
|
+
// an explicit user-triggered command (Claws: Rebuild Native PTY); all 4 are
|
|
754
|
+
// attempted in order and the loop breaks on the first hit.
|
|
755
|
+
let electronVersion = process.env.CLAWS_ELECTRON_VERSION || '';
|
|
756
|
+
if (!electronVersion && process.platform === 'darwin') {
|
|
757
|
+
const plistPaths = [
|
|
758
|
+
'/Applications/Visual Studio Code.app/Contents/Frameworks/Electron Framework.framework/Resources/Info.plist',
|
|
759
|
+
'/Applications/Visual Studio Code - Insiders.app/Contents/Frameworks/Electron Framework.framework/Resources/Info.plist',
|
|
760
|
+
'/Applications/Cursor.app/Contents/Frameworks/Electron Framework.framework/Resources/Info.plist',
|
|
761
|
+
'/Applications/Windsurf.app/Contents/Frameworks/Electron Framework.framework/Resources/Info.plist',
|
|
762
|
+
];
|
|
763
|
+
for (const p of plistPaths) {
|
|
764
|
+
if (!fs.existsSync(p)) continue;
|
|
765
|
+
try {
|
|
766
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
767
|
+
const { execFileSync } = require('child_process') as typeof import('child_process');
|
|
768
|
+
// M-42: 3s timeout prevents sync call from blocking extension host on
|
|
769
|
+
// network-mounted /Applications (enterprise NFS/SMB with hung filesystem).
|
|
770
|
+
const v = execFileSync('plutil', ['-extract', 'CFBundleVersion', 'raw', p], { encoding: 'utf8', timeout: 3000 }).trim();
|
|
771
|
+
if (v) { electronVersion = v; logger(`detected Electron ${v} from ${p}`); break; }
|
|
772
|
+
} catch { /* try next */ }
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (!electronVersion) electronVersion = '39.8.5';
|
|
776
|
+
logger(`targeting Electron ${electronVersion}`);
|
|
777
|
+
|
|
778
|
+
// Run @electron/rebuild via npx against the extension dir (which has the
|
|
779
|
+
// node_modules/ tree with node-pty in it).
|
|
780
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
781
|
+
const { spawn } = require('child_process') as typeof import('child_process');
|
|
782
|
+
const proc = spawn(
|
|
783
|
+
'npx',
|
|
784
|
+
['--yes', '@electron/rebuild', '--version', electronVersion, '--only', 'node-pty', '--force'],
|
|
785
|
+
{ cwd: extensionPath, env: process.env },
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
// M-41: 5-minute ceiling — SIGTERM first, SIGKILL after 5s grace. Prevents
|
|
789
|
+
// hung @electron/rebuild from freezing VS Code indefinitely. Timer cleared
|
|
790
|
+
// on normal exit so it doesn't fire after a successful rebuild.
|
|
791
|
+
// F4: SIGTERM gives the process a chance to clean up before escalating.
|
|
792
|
+
const killTimer = setTimeout(() => {
|
|
793
|
+
proc.kill('SIGTERM');
|
|
794
|
+
logger('✗ rebuild timed out after 5 minutes — sending SIGTERM, SIGKILL in 5s if still running');
|
|
795
|
+
setTimeout(() => { try { proc.kill('SIGKILL'); } catch { /* already exited */ } }, 5000);
|
|
796
|
+
vscode.window.showErrorMessage('Claws: node-pty rebuild timed out — see Claws Output log.');
|
|
797
|
+
}, 5 * 60 * 1000);
|
|
798
|
+
|
|
799
|
+
proc.stdout?.on('data', (d: Buffer) => logger(`[rebuild] ${d.toString('utf8').trimEnd()}`));
|
|
800
|
+
proc.stderr?.on('data', (d: Buffer) => logger(`[rebuild] ${d.toString('utf8').trimEnd()}`));
|
|
801
|
+
proc.on('exit', (code: number | null) => {
|
|
802
|
+
clearTimeout(killTimer);
|
|
803
|
+
if (code === 0) {
|
|
804
|
+
logger('✓ rebuild complete — reload VS Code (Cmd+Shift+P → Developer: Reload Window)');
|
|
805
|
+
vscode.window.showInformationMessage(
|
|
806
|
+
'Claws: node-pty rebuilt. Reload VS Code to activate.',
|
|
807
|
+
'Reload Now',
|
|
808
|
+
).then((choice) => {
|
|
809
|
+
if (choice === 'Reload Now') {
|
|
810
|
+
vscode.commands.executeCommand('workbench.action.reloadWindow');
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
} else {
|
|
814
|
+
logger(`✗ rebuild failed (exit ${code})`);
|
|
815
|
+
vscode.window.showErrorMessage('Claws: node-pty rebuild failed — see Claws Output log.');
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
proc.on('error', (err: Error) => {
|
|
819
|
+
clearTimeout(killTimer);
|
|
820
|
+
logger(`✗ rebuild spawn failed: ${err.message}`);
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ─── Deactivate ───────────────────────────────────────────────────────────
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* #48 Part A — deactivate() hardening.
|
|
828
|
+
*
|
|
829
|
+
* The extension host gives us a limited window during VS Code shutdown /
|
|
830
|
+
* uninstall. Run every teardown step in a bounded Promise.race so a single
|
|
831
|
+
* slow dispose can't hang the shutdown. We resolve within 3s worst-case.
|
|
832
|
+
*/
|
|
833
|
+
export async function deactivate(): Promise<void> {
|
|
834
|
+
const log = (msg: string) => {
|
|
835
|
+
try { outputChannel?.appendLine(msg); } catch { /* ignore */ }
|
|
836
|
+
};
|
|
837
|
+
const finish = (): void => {
|
|
838
|
+
try { outputChannel?.dispose(); } catch { /* ignore */ }
|
|
839
|
+
outputChannel = null;
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const work = async (): Promise<void> => {
|
|
843
|
+
const socketCount = servers.size;
|
|
844
|
+
let stopped = 0;
|
|
845
|
+
for (const s of servers.values()) {
|
|
846
|
+
try { s.stop(); stopped += 1; } catch { /* ignore */ }
|
|
847
|
+
}
|
|
848
|
+
servers.clear();
|
|
849
|
+
server = null;
|
|
850
|
+
|
|
851
|
+
for (const hook of deactivateHooks) {
|
|
852
|
+
try { await hook(); } catch { /* ignore */ }
|
|
853
|
+
}
|
|
854
|
+
deactivateHooks.length = 0;
|
|
855
|
+
|
|
856
|
+
if (statusBar) {
|
|
857
|
+
try { statusBar.dispose(); } catch { /* ignore */ }
|
|
858
|
+
statusBar = null;
|
|
859
|
+
}
|
|
860
|
+
log(`[claws] deactivated — ${stopped}/${socketCount} sockets closed`);
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
try {
|
|
864
|
+
await Promise.race([
|
|
865
|
+
work(),
|
|
866
|
+
new Promise<void>((resolve) => setTimeout(resolve, 3_000)),
|
|
867
|
+
]);
|
|
868
|
+
} catch (err) {
|
|
869
|
+
log(`[claws] deactivate error: ${(err as Error).message}`);
|
|
870
|
+
} finally {
|
|
871
|
+
finish();
|
|
872
|
+
}
|
|
873
|
+
}
|