claws-code 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/.claude/commands/claws-auto.md +90 -0
  2. package/.claude/commands/claws-bin.md +28 -0
  3. package/.claude/commands/claws-cleanup.md +28 -0
  4. package/.claude/commands/claws-do.md +82 -0
  5. package/.claude/commands/claws-fix.md +40 -0
  6. package/.claude/commands/claws-goal.md +111 -0
  7. package/.claude/commands/claws-help.md +54 -0
  8. package/.claude/commands/claws-plan.md +103 -0
  9. package/.claude/commands/claws-report.md +29 -0
  10. package/.claude/commands/claws-status.md +37 -0
  11. package/.claude/commands/claws-update.md +32 -0
  12. package/.claude/commands/claws.md +64 -0
  13. package/.claude/rules/claws-default-behavior.md +76 -0
  14. package/.claude/settings.json +112 -0
  15. package/.claude/settings.local.json +19 -0
  16. package/.claude/skills/claws-auto-engine/SKILL.md +97 -0
  17. package/.claude/skills/claws-goal-tracker/SKILL.md +106 -0
  18. package/.claude/skills/claws-prompt-templates/SKILL.md +203 -0
  19. package/.claude/skills/claws-wave-lead/SKILL.md +126 -0
  20. package/.claude/skills/claws-wave-subworker/SKILL.md +60 -0
  21. package/CHANGELOG.md +1949 -0
  22. package/LICENSE +21 -0
  23. package/README.md +420 -0
  24. package/bin/cli.js +84 -0
  25. package/cli.js +223 -0
  26. package/docs/ARCHITECTURE.md +511 -0
  27. package/docs/event-protocol.md +588 -0
  28. package/docs/features.md +562 -0
  29. package/docs/guide.md +891 -0
  30. package/docs/index.html +716 -0
  31. package/docs/protocol.md +323 -0
  32. package/extension/.vscodeignore +15 -0
  33. package/extension/CHANGELOG.md +1906 -0
  34. package/extension/LICENSE +21 -0
  35. package/extension/README.md +137 -0
  36. package/extension/docs/features.md +424 -0
  37. package/extension/docs/protocol.md +197 -0
  38. package/extension/esbuild.mjs +25 -0
  39. package/extension/icon.png +0 -0
  40. package/extension/native/.metadata.json +10 -0
  41. package/extension/native/node-pty/LICENSE +69 -0
  42. package/extension/native/node-pty/README.md +165 -0
  43. package/extension/native/node-pty/lib/conpty_console_list_agent.js +16 -0
  44. package/extension/native/node-pty/lib/conpty_console_list_agent.js.map +1 -0
  45. package/extension/native/node-pty/lib/eventEmitter2.js +47 -0
  46. package/extension/native/node-pty/lib/eventEmitter2.js.map +1 -0
  47. package/extension/native/node-pty/lib/index.js +52 -0
  48. package/extension/native/node-pty/lib/index.js.map +1 -0
  49. package/extension/native/node-pty/lib/interfaces.js +7 -0
  50. package/extension/native/node-pty/lib/interfaces.js.map +1 -0
  51. package/extension/native/node-pty/lib/shared/conout.js +11 -0
  52. package/extension/native/node-pty/lib/shared/conout.js.map +1 -0
  53. package/extension/native/node-pty/lib/terminal.js +190 -0
  54. package/extension/native/node-pty/lib/terminal.js.map +1 -0
  55. package/extension/native/node-pty/lib/types.js +7 -0
  56. package/extension/native/node-pty/lib/types.js.map +1 -0
  57. package/extension/native/node-pty/lib/unixTerminal.js +346 -0
  58. package/extension/native/node-pty/lib/unixTerminal.js.map +1 -0
  59. package/extension/native/node-pty/lib/utils.js +39 -0
  60. package/extension/native/node-pty/lib/utils.js.map +1 -0
  61. package/extension/native/node-pty/lib/windowsConoutConnection.js +125 -0
  62. package/extension/native/node-pty/lib/windowsConoutConnection.js.map +1 -0
  63. package/extension/native/node-pty/lib/windowsPtyAgent.js +320 -0
  64. package/extension/native/node-pty/lib/windowsPtyAgent.js.map +1 -0
  65. package/extension/native/node-pty/lib/windowsTerminal.js +199 -0
  66. package/extension/native/node-pty/lib/windowsTerminal.js.map +1 -0
  67. package/extension/native/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  68. package/extension/native/node-pty/lib/worker/conoutSocketWorker.js.map +1 -0
  69. package/extension/native/node-pty/package.json +64 -0
  70. package/extension/native/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  71. package/extension/native/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  72. package/extension/native/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  73. package/extension/native/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  74. package/extension/native/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  75. package/extension/native/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  76. package/extension/native/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  77. package/extension/native/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  78. package/extension/native/node-pty/prebuilds/win32-arm64/pty.node +0 -0
  79. package/extension/native/node-pty/prebuilds/win32-arm64/winpty-agent.exe +0 -0
  80. package/extension/native/node-pty/prebuilds/win32-arm64/winpty.dll +0 -0
  81. package/extension/native/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  82. package/extension/native/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  83. package/extension/native/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  84. package/extension/native/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  85. package/extension/native/node-pty/prebuilds/win32-x64/pty.node +0 -0
  86. package/extension/native/node-pty/prebuilds/win32-x64/winpty-agent.exe +0 -0
  87. package/extension/native/node-pty/prebuilds/win32-x64/winpty.dll +0 -0
  88. package/extension/package-lock.json +605 -0
  89. package/extension/package.json +343 -0
  90. package/extension/scripts/bundle-native.mjs +104 -0
  91. package/extension/scripts/deploy-dev.mjs +60 -0
  92. package/extension/src/ansi-strip.ts +52 -0
  93. package/extension/src/backends/vscode/claws-pty.ts +483 -0
  94. package/extension/src/backends/vscode/status-bar.ts +99 -0
  95. package/extension/src/backends/vscode/vscode-backend.ts +282 -0
  96. package/extension/src/capture-store.ts +125 -0
  97. package/extension/src/event-log.ts +629 -0
  98. package/extension/src/event-schemas.ts +478 -0
  99. package/extension/src/extension.js +492 -0
  100. package/extension/src/extension.ts +873 -0
  101. package/extension/src/lifecycle-engine.ts +60 -0
  102. package/extension/src/lifecycle-rules.ts +171 -0
  103. package/extension/src/lifecycle-store.ts +506 -0
  104. package/extension/src/peer-registry.ts +176 -0
  105. package/extension/src/pipeline-registry.ts +82 -0
  106. package/extension/src/platform.ts +64 -0
  107. package/extension/src/protocol.ts +532 -0
  108. package/extension/src/server-config.ts +98 -0
  109. package/extension/src/server.ts +2210 -0
  110. package/extension/src/task-registry.ts +51 -0
  111. package/extension/src/terminal-backend.ts +211 -0
  112. package/extension/src/terminal-manager.ts +395 -0
  113. package/extension/src/topic-registry.ts +70 -0
  114. package/extension/src/topic-utils.ts +46 -0
  115. package/extension/src/transport.ts +45 -0
  116. package/extension/src/uninstall-cleanup.ts +232 -0
  117. package/extension/src/wave-registry.ts +314 -0
  118. package/extension/src/websocket-transport.ts +153 -0
  119. package/extension/tsconfig.json +23 -0
  120. package/lib/capabilities.js +145 -0
  121. package/lib/dry-run.js +43 -0
  122. package/lib/install.js +1018 -0
  123. package/lib/mcp-setup.js +92 -0
  124. package/lib/platform.js +240 -0
  125. package/lib/preflight.js +152 -0
  126. package/lib/shell-hook.js +343 -0
  127. package/lib/uninstall.js +162 -0
  128. package/lib/verify.js +166 -0
  129. package/mcp_server.js +3529 -0
  130. package/package.json +48 -0
  131. package/rules/claws-default-behavior.md +72 -0
  132. package/scripts/_helpers/atomic-file.mjs +137 -0
  133. package/scripts/_helpers/fix-repair.js +64 -0
  134. package/scripts/_helpers/json-safe.mjs +218 -0
  135. package/scripts/bump-version.sh +84 -0
  136. package/scripts/codegen/gen-docs.mjs +61 -0
  137. package/scripts/codegen/gen-json-schema.mjs +62 -0
  138. package/scripts/codegen/gen-mcp-tools.mjs +358 -0
  139. package/scripts/codegen/gen-types.mjs +172 -0
  140. package/scripts/codegen/index.mjs +42 -0
  141. package/scripts/dev-hooks/check-extension-dirs.js +77 -0
  142. package/scripts/dev-hooks/check-open-claws-terminals.js +70 -0
  143. package/scripts/dev-hooks/check-stale-main.js +55 -0
  144. package/scripts/dev-hooks/check-tag-pushed.js +51 -0
  145. package/scripts/dev-hooks/check-tag-vs-main.js +56 -0
  146. package/scripts/dev-vsix-install.sh +60 -0
  147. package/scripts/fix.sh +702 -0
  148. package/scripts/gen-client-types.mjs +81 -0
  149. package/scripts/git-hooks/pre-commit +31 -0
  150. package/scripts/hooks/lifecycle-state.js +61 -0
  151. package/scripts/hooks/package.json +4 -0
  152. package/scripts/hooks/post-tool-use-claws.js +292 -0
  153. package/scripts/hooks/pre-bash-no-verify-block.js +72 -0
  154. package/scripts/hooks/pre-tool-use-claws.js +206 -0
  155. package/scripts/hooks/session-start-claws.js +97 -0
  156. package/scripts/hooks/stop-claws.js +88 -0
  157. package/scripts/inject-claude-md.js +205 -0
  158. package/scripts/inject-dev-hooks.js +96 -0
  159. package/scripts/inject-global-claude-md.js +140 -0
  160. package/scripts/inject-settings-hooks.js +370 -0
  161. package/scripts/install.ps1 +146 -0
  162. package/scripts/install.sh +1729 -0
  163. package/scripts/monitor-arm-watch.js +155 -0
  164. package/scripts/rebuild-node-pty.sh +245 -0
  165. package/scripts/report.sh +232 -0
  166. package/scripts/shell-hook.fish +164 -0
  167. package/scripts/shell-hook.ps1 +33 -0
  168. package/scripts/shell-hook.sh +232 -0
  169. package/scripts/stream-events.js +399 -0
  170. package/scripts/terminal-wrapper.sh +36 -0
  171. package/scripts/test-enforcement.sh +132 -0
  172. package/scripts/test-install.sh +174 -0
  173. package/scripts/test-installer-parity.sh +135 -0
  174. package/scripts/test-template-enforcement.sh +76 -0
  175. package/scripts/uninstall.sh +143 -0
  176. package/scripts/update.sh +337 -0
  177. package/scripts/verify-release.sh +323 -0
  178. package/scripts/verify-wrapped.sh +194 -0
  179. package/templates/CLAUDE.global.md +135 -0
  180. package/templates/CLAUDE.project.md +37 -0
@@ -0,0 +1,483 @@
1
+ import * as vscode from 'vscode';
2
+ import { spawn, spawnSync, ChildProcessWithoutNullStreams } from 'child_process';
3
+ import * as fs from 'fs';
4
+ import * as os from 'os';
5
+ import * as path from 'path';
6
+ import { CaptureStore } from '../../capture-store';
7
+
8
+ interface NodePtyModule {
9
+ spawn(
10
+ shell: string,
11
+ args: string[],
12
+ opts: {
13
+ name?: string;
14
+ cols?: number;
15
+ rows?: number;
16
+ cwd?: string;
17
+ env?: NodeJS.ProcessEnv;
18
+ },
19
+ ): NodePtyProcess;
20
+ }
21
+
22
+ interface NodePtyProcess {
23
+ pid: number;
24
+ onData(cb: (data: string) => void): void;
25
+ onExit(cb: (e: { exitCode: number; signal?: number }) => void): void;
26
+ write(data: string): void;
27
+ resize(cols: number, rows: number): void;
28
+ kill(signal?: string): void;
29
+ }
30
+
31
+ interface LoadAttempt {
32
+ path: string;
33
+ message: string;
34
+ code?: string;
35
+ }
36
+
37
+ let nodePtyCache: NodePtyModule | null = null;
38
+ let loadedFromPath: string | null = null;
39
+ let lastLoadError: { message: string; code?: string; stack?: string; attempts: LoadAttempt[] } | null = null;
40
+
41
+ // Resolution order for node-pty. We always prefer the bundled copy at
42
+ // <extension>/native/node-pty because it ships with the VSIX — it works even
43
+ // when node_modules/ is stripped (which is what .vscodeignore does). Standard
44
+ // resolution is kept as a fallback so `npm link`'d dev installs still work.
45
+ function resolveCandidates(): string[] {
46
+ // __dirname is <extension>/dist at runtime (esbuild output) or
47
+ // <extension>/out in ts-node/dev. Either way, ../native/node-pty lands on
48
+ // the bundled copy.
49
+ const bundled = path.join(__dirname, '..', 'native', 'node-pty');
50
+ return [bundled, 'node-pty'];
51
+ }
52
+
53
+ // Load node-pty. We cache ONLY successful loads — failures are retried on
54
+ // the next spawn so that if node-pty appears on disk mid-session (e.g. after
55
+ // /claws-update compiles it), new terminals pick it up without a VS Code
56
+ // reload. The full error from EACH failed require() is captured for the
57
+ // diagnostic surface (exposed via loadNodePtyStatus() for the Health Check
58
+ // command).
59
+ function loadNodePty(logger?: (msg: string) => void): NodePtyModule | null {
60
+ if (nodePtyCache) return nodePtyCache;
61
+
62
+ const attempts: LoadAttempt[] = [];
63
+ for (const candidate of resolveCandidates()) {
64
+ try {
65
+ logger?.(`[node-pty] trying ${candidate}`);
66
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
67
+ const mod = require(candidate) as NodePtyModule;
68
+ nodePtyCache = mod;
69
+ loadedFromPath = candidate;
70
+ lastLoadError = null;
71
+ logger?.(`[node-pty] loaded successfully from ${candidate}`);
72
+ return nodePtyCache;
73
+ } catch (err: unknown) {
74
+ const e = err as NodeJS.ErrnoException;
75
+ const attempt: LoadAttempt = {
76
+ path: candidate,
77
+ message: e.message || String(err),
78
+ code: e.code,
79
+ };
80
+ attempts.push(attempt);
81
+ logger?.(`[node-pty] FAILED: ${attempt.message}${attempt.code ? ` (code=${attempt.code})` : ''}`);
82
+ }
83
+ }
84
+
85
+ const primary = attempts[0] ?? { path: '(none)', message: 'no candidates' };
86
+ lastLoadError = {
87
+ message: primary.message,
88
+ code: primary.code,
89
+ attempts,
90
+ stack: attempts.map((a) => ` ${a.path}: ${a.message}`).join('\n'),
91
+ };
92
+ if (logger) {
93
+ logger(`[node-pty] load FAILED — tried ${attempts.length} candidate(s):`);
94
+ for (const a of attempts) {
95
+ logger(`[node-pty] ${a.path}: ${a.message}${a.code ? ` (${a.code})` : ''}`);
96
+ }
97
+ logger(`[node-pty] this causes wrapped terminals to fall back to pipe-mode.`);
98
+ logger(`[node-pty] fix: run 'Claws: Rebuild Native PTY' from the command palette`);
99
+ }
100
+ return null;
101
+ }
102
+
103
+ export function loadNodePtyStatus(): {
104
+ loaded: boolean;
105
+ loadedFrom?: string;
106
+ error?: { message: string; code?: string; attempts: LoadAttempt[] };
107
+ } {
108
+ if (nodePtyCache) return { loaded: true, loadedFrom: loadedFromPath ?? undefined };
109
+ if (lastLoadError) {
110
+ return {
111
+ loaded: false,
112
+ error: {
113
+ message: lastLoadError.message,
114
+ code: lastLoadError.code,
115
+ attempts: lastLoadError.attempts,
116
+ },
117
+ };
118
+ }
119
+ return { loaded: false };
120
+ }
121
+
122
+ export interface ClawsPtyOptions {
123
+ terminalId: string;
124
+ shellPath?: string;
125
+ shellArgs?: string[];
126
+ cwd?: string;
127
+ env?: NodeJS.ProcessEnv;
128
+ captureStore: CaptureStore;
129
+ logger: (msg: string) => void;
130
+ /** Called synchronously when VS Code invokes Pseudoterminal.open(). */
131
+ onOpenHook?: () => void;
132
+ /** Called on the first byte of output from the spawned process. */
133
+ onFirstOutputHook?: () => void;
134
+ /** AF-AC Phase 1: called as the first action in handleExit, before closeEmitter.fire.
135
+ * Fires from IPty.onExit (node-pty) or child_process 'exit' — the OS-authoritative signal. */
136
+ onProcessExitHook?: (exitCode: number) => void;
137
+ /** AC-1: when present, set as CLAWS_TERMINAL_CORR_ID env var on the spawned pty process.
138
+ * Uses cross-platform {env: {...}} option on node-pty spawn — identical on darwin/linux/win32. */
139
+ correlationId?: string;
140
+ }
141
+
142
+ export class ClawsPty implements vscode.Pseudoterminal {
143
+ private readonly writeEmitter = new vscode.EventEmitter<string>();
144
+ private readonly closeEmitter = new vscode.EventEmitter<number | void>();
145
+
146
+ readonly onDidWrite: vscode.Event<string> = this.writeEmitter.event;
147
+ readonly onDidClose: vscode.Event<number | void> = this.closeEmitter.event;
148
+
149
+ private ptyProc: NodePtyProcess | null = null;
150
+ private childProc: ChildProcessWithoutNullStreams | null = null;
151
+ private isOpen = false;
152
+ private openedAt: number | null = null;
153
+ private readonly createdAt = Date.now();
154
+ private firstOutputFired = false;
155
+
156
+ constructor(private readonly opts: ClawsPtyOptions) {}
157
+
158
+ get pid(): number | null {
159
+ return this.ptyProc?.pid ?? this.childProc?.pid ?? null;
160
+ }
161
+
162
+ get mode(): 'pty' | 'pipe' | 'none' {
163
+ if (this.ptyProc) return 'pty';
164
+ if (this.childProc) return 'pipe';
165
+ return 'none';
166
+ }
167
+
168
+ /** True once VS Code has invoked our `open()` hook. */
169
+ hasOpened(): boolean {
170
+ return this.openedAt != null;
171
+ }
172
+
173
+ /** Wall-clock ms since this ClawsPty was constructed. */
174
+ ageMs(): number {
175
+ return Date.now() - this.createdAt;
176
+ }
177
+
178
+ /**
179
+ * Returns the foreground process running under the shell managed by this
180
+ * PTY. Uses pgrep to find the most recent child of the shell PID, then ps
181
+ * to get its basename. Falls back to the shell PID itself if no child is
182
+ * found. Returns { pid: null, basename: null } if the PTY has no spawned
183
+ * process yet.
184
+ */
185
+ getForegroundProcess(): { pid: number | null; basename: string | null } {
186
+ const shellPid = this.ptyProc?.pid ?? this.childProc?.pid;
187
+ if (!shellPid) return { pid: null, basename: null };
188
+ if (process.platform === 'win32') {
189
+ // ConPTY stub: pgrep/ps do not exist on Windows. Return the shell PID and
190
+ // the basename of the configured shell path as the best approximation.
191
+ // The safety gate uses basename for TUI detection; 'unknown' triggers a
192
+ // warning (not a hard block), so this is safe for v0.8.
193
+ const shell = this.opts.shellPath || '';
194
+ const basename = shell ? (shell.split(/[\\/]/).pop() ?? null) : null;
195
+ return { pid: shellPid, basename };
196
+ }
197
+ try {
198
+ const pgrepResult = spawnSync('pgrep', ['-P', String(shellPid)], { encoding: 'utf8', timeout: 500 });
199
+ const childOutput = (pgrepResult.stdout ?? '').trim();
200
+ let targetPid: number = shellPid;
201
+ if (childOutput) {
202
+ const childPids = childOutput.split('\n').filter(Boolean);
203
+ const candidatePid = parseInt(childPids[childPids.length - 1], 10);
204
+ if (!isNaN(candidatePid)) targetPid = candidatePid;
205
+ }
206
+ const psResult = spawnSync('ps', ['-p', String(targetPid), '-o', 'comm='], { encoding: 'utf8', timeout: 500 });
207
+ let basename = (psResult.stdout ?? '').trim() || null;
208
+ // If the child process has already exited, fall back to the shell itself.
209
+ if (!basename && targetPid !== shellPid) {
210
+ const fallback = spawnSync('ps', ['-p', String(shellPid), '-o', 'comm='], { encoding: 'utf8', timeout: 500 });
211
+ basename = (fallback.stdout ?? '').trim() || null;
212
+ targetPid = shellPid;
213
+ }
214
+ return { pid: targetPid, basename };
215
+ } catch {
216
+ return { pid: shellPid, basename: null };
217
+ }
218
+ }
219
+
220
+ open(initialDimensions: vscode.TerminalDimensions | undefined): void {
221
+ this.isOpen = true;
222
+ this.openedAt = Date.now();
223
+ this.opts.onOpenHook?.();
224
+ const shell = this.opts.shellPath || defaultShell();
225
+ const args = this.opts.shellArgs ?? defaultShellArgs(shell);
226
+ const cwd = this.opts.cwd || os.homedir();
227
+ // CLAWS_WRAPPED=1 is set ONLY when ptyProc is real, so the shell hook
228
+ // (scripts/shell-hook.sh) and any user-side check can distinguish a real
229
+ // wrapped pty from the pipe-mode degraded fallback. Set it here as the
230
+ // base env; we re-stamp it below once we know which path won.
231
+ const env = sanitizeEnv(process.env, { ...(this.opts.env || {}), TERM: 'xterm-256color' });
232
+ const cols = initialDimensions?.columns ?? 80;
233
+ const rows = initialDimensions?.rows ?? 24;
234
+
235
+ const nodePty = loadNodePty(this.opts.logger);
236
+ if (nodePty) {
237
+ // Warn if spawn-helper is not executable (darwin/linux only — win32 has no spawn-helper).
238
+ // node-pty uses posix_spawnp to exec spawn-helper; a non-executable file causes
239
+ // posix_spawnp to fail, degrading every wrapped terminal to pipe-mode silently.
240
+ // This check does NOT auto-fix the mode — only warns, so the upstream source bug is visible.
241
+ if (process.platform !== 'win32') {
242
+ const platKey = `${process.platform}-${process.arch}`;
243
+ const spawnHelperPath = path.join(__dirname, '..', 'native', 'node-pty', 'prebuilds', platKey, 'spawn-helper');
244
+ if (fs.existsSync(spawnHelperPath)) {
245
+ try {
246
+ const mode = fs.statSync(spawnHelperPath).mode;
247
+ if ((mode & 0o111) === 0) {
248
+ this.opts.logger(
249
+ `[claws-pty] spawn-helper at ${spawnHelperPath} is not executable (mode ${(mode & 0o777).toString(8)}). ` +
250
+ `node-pty.spawn will fail with posix_spawnp; falling back to pipe-mode. ` +
251
+ `Fix: chmod +x ${spawnHelperPath} (or rebuild VSIX after AC-1.1).`,
252
+ );
253
+ }
254
+ } catch { /* stat failed — proceed; spawn will surface the error */ }
255
+ }
256
+ }
257
+ try {
258
+ const ptyEnv: NodeJS.ProcessEnv = { ...env, CLAWS_WRAPPED: '1', CLAWS_TERMINAL_ID: this.opts.terminalId };
259
+ if (this.opts.correlationId) ptyEnv.CLAWS_TERMINAL_CORR_ID = this.opts.correlationId;
260
+ this.ptyProc = nodePty.spawn(shell, args, { cols, rows, cwd, env: ptyEnv, name: 'xterm-256color' });
261
+ this.ptyProc.onData((data) => this.handleOutput(data));
262
+ this.ptyProc.onExit(({ exitCode }) => this.handleExit(exitCode));
263
+ this.opts.logger(`[claws-pty ${this.opts.terminalId}] node-pty spawned ${shell} pid=${this.ptyProc.pid} (real pty)`);
264
+ return;
265
+ } catch (err) {
266
+ this.opts.logger(`[claws-pty ${this.opts.terminalId}] node-pty spawn failed: ${(err as Error).message}. Falling back to child_process pipe-mode.`);
267
+ this.ptyProc = null;
268
+ }
269
+ }
270
+
271
+ // Pipe-mode fallback. Log loudly to the Output channel AND emit the
272
+ // yellow banner into the terminal so the user sees it both ways.
273
+ try {
274
+ // Mark pipe-mode explicitly so the shell hook can warn.
275
+ const pipeEnv: NodeJS.ProcessEnv = { ...env, CLAWS_PIPE_MODE: '1', CLAWS_TERMINAL_ID: this.opts.terminalId };
276
+ if (this.opts.correlationId) pipeEnv.CLAWS_TERMINAL_CORR_ID = this.opts.correlationId;
277
+ this.childProc = spawn(shell, args, { cwd, env: pipeEnv, stdio: ['pipe', 'pipe', 'pipe'] });
278
+ this.childProc.stdout.on('data', (d: Buffer) => this.handleOutput(d.toString('utf8')));
279
+ this.childProc.stderr.on('data', (d: Buffer) => this.handleOutput(d.toString('utf8')));
280
+ this.childProc.on('exit', (code) => this.handleExit(code ?? 0));
281
+ const loadErr = lastLoadError?.message || 'unknown reason';
282
+ this.opts.logger(`[claws-pty ${this.opts.terminalId}] PIPE-MODE active (node-pty unavailable): ${loadErr}`);
283
+ this.opts.logger(`[claws-pty ${this.opts.terminalId}] TUIs will not render correctly. Run 'Claws: Health Check' for diagnostics.`);
284
+ this.opts.logger(`[claws-pty ${this.opts.terminalId}] child_process fallback ${shell} pid=${this.childProc.pid}`);
285
+ this.writeEmitter.fire('\x1b[33m[claws] running in pipe-mode (node-pty unavailable); TUIs may render poorly\x1b[0m\r\n');
286
+ this.writeEmitter.fire('\x1b[2m[claws] run "Claws: Health Check" in the command palette for why\x1b[0m\r\n');
287
+ } catch (err) {
288
+ this.opts.logger(`[claws-pty ${this.opts.terminalId}] SPAWN FAILED: ${(err as Error).message}`);
289
+ this.writeEmitter.fire(`\x1b[31m[claws] failed to spawn shell: ${(err as Error).message}\x1b[0m\r\n`);
290
+ this.closeEmitter.fire(1);
291
+ }
292
+ }
293
+
294
+ close(): void {
295
+ if (!this.isOpen) return;
296
+ this.isOpen = false;
297
+ if (this.ptyProc) {
298
+ try { this.ptyProc.kill(); } catch { /* ignore */ }
299
+ this.ptyProc = null;
300
+ }
301
+ if (this.childProc) {
302
+ try { this.childProc.kill(); } catch { /* ignore */ }
303
+ this.childProc = null;
304
+ }
305
+ }
306
+
307
+ handleInput(data: string): void {
308
+ if (!this.isOpen) return;
309
+ if (this.ptyProc) {
310
+ this.ptyProc.write(data);
311
+ } else if (this.childProc?.stdin.writable) {
312
+ this.childProc.stdin.write(data);
313
+ }
314
+ }
315
+
316
+ setDimensions(dimensions: vscode.TerminalDimensions): void {
317
+ if (this.ptyProc) {
318
+ try { this.ptyProc.resize(dimensions.columns, dimensions.rows); } catch { /* ignore */ }
319
+ }
320
+ }
321
+
322
+ writeInjected(text: string, withNewline: boolean, bracketedPaste: boolean): void {
323
+ if (!this.isOpen) return;
324
+ const body = bracketedPaste ? `\x1b[200~${text}\x1b[201~` : text;
325
+ this.writeRaw(body);
326
+ if (!withNewline) return;
327
+ // For bracketed paste, the trailing CR must arrive in a separate write
328
+ // after a short delay so the TUI's paste-detection window closes first.
329
+ // Otherwise Ink-based TUIs (Claude Code) bundle the CR into the paste
330
+ // burst and it never registers as a discrete Enter keypress.
331
+ if (bracketedPaste) {
332
+ setTimeout(() => this.writeRaw('\r'), 30);
333
+ } else {
334
+ this.writeRaw('\r');
335
+ }
336
+ }
337
+
338
+ private writeRaw(data: string): void {
339
+ if (!this.isOpen) return;
340
+ if (this.ptyProc) {
341
+ this.ptyProc.write(data);
342
+ } else if (this.childProc?.stdin.writable) {
343
+ this.childProc.stdin.write(data);
344
+ }
345
+ }
346
+
347
+ private handleOutput(data: string): void {
348
+ this.writeEmitter.fire(data);
349
+ this.opts.captureStore.append(this.opts.terminalId, data);
350
+ if (!this.firstOutputFired) {
351
+ this.firstOutputFired = true;
352
+ this.opts.onFirstOutputHook?.();
353
+ }
354
+ }
355
+
356
+ private handleExit(code: number): void {
357
+ this.opts.onProcessExitHook?.(code);
358
+ if (this.isOpen) {
359
+ this.isOpen = false;
360
+ this.closeEmitter.fire(code);
361
+ }
362
+ }
363
+ }
364
+
365
+ // ─── Shell resolution ─────────────────────────────────────────────────────
366
+
367
+ /**
368
+ * Pick the shell to spawn a wrapped terminal under.
369
+ *
370
+ * Order:
371
+ * 1. $SHELL (user's configured login shell — respect their choice)
372
+ * 2. /bin/bash (more common default on Linux)
373
+ * 3. /bin/zsh (default on macOS Catalina+)
374
+ * 4. /bin/sh (POSIX bottom floor — always present)
375
+ *
376
+ * We deliberately don't hardcode zsh: on headless Linux boxes, zsh often
377
+ * isn't installed at all and falling back to it produces ENOENT at spawn.
378
+ */
379
+ export function defaultShell(): string {
380
+ if (process.platform === 'win32') {
381
+ return process.env.COMSPEC || 'powershell.exe';
382
+ }
383
+ if (process.env.SHELL) return process.env.SHELL;
384
+ const candidates = ['/bin/bash', '/bin/zsh', '/bin/sh'];
385
+ for (const c of candidates) {
386
+ try {
387
+ if (fs.existsSync(c)) return c;
388
+ } catch { /* ignore */ }
389
+ }
390
+ return '/bin/sh';
391
+ }
392
+
393
+ /**
394
+ * Pick default argv for the chosen shell.
395
+ *
396
+ * We always pass `-i` (interactive) so the shell reads its per-user rc file
397
+ * (`.zshrc`, `.bashrc`, `fish.config`) — this is what makes aliases, PATH
398
+ * adjustments, and prompt customisation show up.
399
+ *
400
+ * We add `-l` (login) ONLY when the user has a login-shell profile file on
401
+ * disk (`.zprofile`, `.bash_profile`, `.profile`). Adding `-l` unconditionally
402
+ * is a footgun: many users have slow `.profile` scripts (nvm init, asdf init,
403
+ * cargo env…) that would then run on EVERY wrapped-terminal creation.
404
+ * Defaulting to `-i` alone is the fast path — explicit login-profile files
405
+ * signal the user actually wants login-shell semantics.
406
+ */
407
+ export function defaultShellArgs(shell: string): string[] {
408
+ if (process.platform === 'win32') return [];
409
+ const base = shell.split('/').pop() || shell;
410
+ if (base === 'zsh' || base === 'bash' || base === 'fish' || base === 'sh') {
411
+ const home = process.env.HOME || os.homedir();
412
+ const loginFiles = ['.zprofile', '.bash_profile', '.profile'];
413
+ let hasLoginProfile = false;
414
+ for (const f of loginFiles) {
415
+ try {
416
+ if (fs.existsSync(path.join(home, f))) { hasLoginProfile = true; break; }
417
+ } catch { /* ignore */ }
418
+ }
419
+ return hasLoginProfile ? ['-i', '-l'] : ['-i'];
420
+ }
421
+ return [];
422
+ }
423
+
424
+ // ─── Env sanitization ─────────────────────────────────────────────────────
425
+
426
+ /**
427
+ * Strip VS Code/Electron/npm-lifecycle environment vars before forwarding to
428
+ * the user's shell. Leaves standard user env (PATH, HOME, USER, LANG, LC_*,
429
+ * SHELL, EDITOR, VISUAL, TERM, DISPLAY, etc.) intact.
430
+ *
431
+ * Why: VS Code's process environment is polluted with internal variables
432
+ * like VSCODE_IPC_HOOK, VSCODE_PID, ELECTRON_RUN_AS_NODE, plus npm_* vars
433
+ * from however it was started. Propagating these to the user's shell
434
+ * confuses `claude` (which inspects ELECTRON_*) and produces bogus
435
+ * "running inside Electron" warnings.
436
+ *
437
+ * `overrides` wins over `baseEnv`, and any `undefined` in `overrides`
438
+ * explicitly deletes the key from the result.
439
+ */
440
+ export function sanitizeEnv(
441
+ baseEnv: NodeJS.ProcessEnv,
442
+ overrides: NodeJS.ProcessEnv = {},
443
+ ): NodeJS.ProcessEnv {
444
+ const DROP_PREFIXES = ['VSCODE_', 'ELECTRON_', 'CHROME_', 'GOOGLE_API_', 'npm_'];
445
+ const DROP_EXACT = new Set([
446
+ 'INIT_CWD',
447
+ 'VSCODE_PID',
448
+ 'VSCODE_CWD',
449
+ 'VSCODE_IPC_HOOK',
450
+ 'VSCODE_IPC_HOOK_CLI',
451
+ 'VSCODE_NLS_CONFIG',
452
+ 'VSCODE_CODE_CACHE_PATH',
453
+ 'VSCODE_CRASH_REPORTER_PROCESS_TYPE',
454
+ 'VSCODE_HANDLES_UNCAUGHT_ERRORS',
455
+ 'VSCODE_INJECTION',
456
+ 'VSCODE_L10N_BUNDLE_LOCATION',
457
+ 'NODE_OPTIONS', // Often set to --inspect by debug; let user shell pick its own.
458
+ ]);
459
+
460
+ const shouldDrop = (key: string): boolean => {
461
+ const upper = key.toUpperCase();
462
+ if (DROP_EXACT.has(key) || DROP_EXACT.has(upper)) return true;
463
+ for (const p of DROP_PREFIXES) {
464
+ if (upper.startsWith(p.toUpperCase())) return true;
465
+ }
466
+ return false;
467
+ };
468
+
469
+ const out: NodeJS.ProcessEnv = {};
470
+ for (const [k, v] of Object.entries(baseEnv)) {
471
+ if (v === undefined) continue;
472
+ if (shouldDrop(k)) continue;
473
+ out[k] = v;
474
+ }
475
+ for (const [k, v] of Object.entries(overrides)) {
476
+ if (v === undefined) {
477
+ delete out[k];
478
+ } else {
479
+ out[k] = v;
480
+ }
481
+ }
482
+ return out;
483
+ }
@@ -0,0 +1,99 @@
1
+ // Claws status bar item. Right-aligned, priority 100, click → Health Check.
2
+ // Color shifts to warning / error based on server + node-pty state.
3
+
4
+ import * as vscode from 'vscode';
5
+ import * as path from 'path';
6
+ import { loadNodePtyStatus } from './claws-pty';
7
+ import { ClawsServer } from '../../server';
8
+
9
+ export interface StatusBarOptions {
10
+ activated: boolean;
11
+ version: string;
12
+ getServers: () => Map<string, ClawsServer>;
13
+ getTerminalCount: () => number;
14
+ }
15
+
16
+ export interface StatusBarHandle {
17
+ item: vscode.StatusBarItem;
18
+ timer: NodeJS.Timeout;
19
+ update: () => void;
20
+ dispose: () => void;
21
+ }
22
+
23
+ /**
24
+ * Create the status bar item and start a 30-second refresh timer. Caller
25
+ * owns the lifecycle — call `dispose()` on deactivate to clear the timer
26
+ * and dispose the item.
27
+ */
28
+ export function createStatusBar(
29
+ context: vscode.ExtensionContext,
30
+ opts: StatusBarOptions,
31
+ ): StatusBarHandle {
32
+ const item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
33
+ item.name = 'Claws';
34
+ item.text = '$(terminal) Claws';
35
+ item.command = 'claws.healthCheck';
36
+ item.show();
37
+ context.subscriptions.push(item);
38
+
39
+ const update = (): void => renderStatusBar(item, opts);
40
+
41
+ // Refresh every 30s so pid counts, node-pty status, socket liveness stay
42
+ // current even without terminal-lifecycle events firing.
43
+ const timer = setInterval(update, 30_000);
44
+ if (typeof timer.unref === 'function') timer.unref();
45
+
46
+ update();
47
+
48
+ const dispose = (): void => {
49
+ clearInterval(timer);
50
+ try { item.dispose(); } catch { /* ignore */ }
51
+ };
52
+
53
+ return { item, timer, update, dispose };
54
+ }
55
+
56
+ function renderStatusBar(item: vscode.StatusBarItem, opts: StatusBarOptions): void {
57
+ const servers = opts.getServers();
58
+ const npty = loadNodePtyStatus();
59
+
60
+ let state: 'ok' | 'warn' | 'err' = 'ok';
61
+ let hint = '';
62
+
63
+ if (!opts.activated) {
64
+ state = 'err';
65
+ hint = 'bridge disabled (no workspace open)';
66
+ } else if (servers.size === 0) {
67
+ state = 'err';
68
+ hint = 'no socket servers running';
69
+ } else if (npty.loaded === false && npty.error) {
70
+ state = 'warn';
71
+ hint = 'pipe-mode (node-pty not loaded)';
72
+ }
73
+
74
+ const termCount = opts.getTerminalCount();
75
+ const iconPrefix = state === 'err' ? '$(error)' : state === 'warn' ? '$(warning)' : '$(terminal)';
76
+ item.text = `${iconPrefix} Claws${termCount > 0 ? ` (${termCount})` : ''}`;
77
+ item.color = state === 'err'
78
+ ? new vscode.ThemeColor('statusBarItem.errorForeground')
79
+ : state === 'warn'
80
+ ? new vscode.ThemeColor('statusBarItem.warningForeground')
81
+ : undefined;
82
+
83
+ const tooltip = new vscode.MarkdownString(undefined, true);
84
+ tooltip.isTrusted = false;
85
+ tooltip.appendMarkdown(`**Claws** · v${opts.version}\n\n`);
86
+ if (hint) tooltip.appendMarkdown(`_${hint}_\n\n`);
87
+ tooltip.appendMarkdown(`**Sockets** (${servers.size}):\n`);
88
+ if (servers.size === 0) {
89
+ tooltip.appendMarkdown(`· _none_\n`);
90
+ } else {
91
+ for (const [root, srv] of servers.entries()) {
92
+ tooltip.appendMarkdown(`· \`${path.basename(root)}\` → \`${srv.getSocketPath() ?? '(pending)'}\`\n`);
93
+ }
94
+ }
95
+ tooltip.appendMarkdown(`\n**Terminals**: ${termCount}\n\n`);
96
+ tooltip.appendMarkdown(`**node-pty**: ${npty.loaded ? 'loaded' : npty.error ? 'not loaded (pipe-mode)' : 'not attempted yet'}\n\n`);
97
+ tooltip.appendMarkdown(`_Click for Health Check_`);
98
+ item.tooltip = tooltip;
99
+ }