@yanzc/claude-pro 0.0.0-stage → 0.4.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/.env.example +41 -0
- package/LICENSE +21 -0
- package/README.md +314 -3
- package/bin/claude-pro.js +32 -0
- package/package.json +40 -6
- package/src/agent.js +752 -0
- package/src/agents.js +186 -0
- package/src/api.js +488 -0
- package/src/cache.js +150 -0
- package/src/checkpoints.js +184 -0
- package/src/claude-code.js +60 -0
- package/src/codex/client.js +300 -0
- package/src/codex/translate-request.js +239 -0
- package/src/codex/translate-stream.js +328 -0
- package/src/commands.js +803 -0
- package/src/config.js +453 -0
- package/src/context.js +109 -0
- package/src/filestate.js +57 -0
- package/src/gateway.js +110 -0
- package/src/hooks.js +81 -0
- package/src/main.js +193 -0
- package/src/mcp.js +290 -0
- package/src/modes.js +154 -0
- package/src/oauth.js +287 -0
- package/src/peers.js +156 -0
- package/src/print.js +106 -0
- package/src/prompts.js +179 -0
- package/src/ratelimit.js +244 -0
- package/src/restart.js +115 -0
- package/src/runtime.js +162 -0
- package/src/selfcheck.js +71 -0
- package/src/session.js +139 -0
- package/src/supervisor.js +110 -0
- package/src/tasks.js +76 -0
- package/src/tools/bash.js +227 -0
- package/src/tools/effort.js +41 -0
- package/src/tools/files.js +352 -0
- package/src/tools/index.js +52 -0
- package/src/tools/interaction.js +157 -0
- package/src/tools/notebook.js +91 -0
- package/src/tools/peers.js +45 -0
- package/src/tools/restart.js +50 -0
- package/src/tools/search.js +458 -0
- package/src/tools/skill.js +32 -0
- package/src/tools/task.js +139 -0
- package/src/tools/web.js +283 -0
- package/src/ui/ansi.js +228 -0
- package/src/ui/dialogs.js +122 -0
- package/src/ui/editor.js +389 -0
- package/src/ui/markdown.js +249 -0
- package/src/ui/render.js +155 -0
- package/src/ui/repl.js +1450 -0
- package/src/ui/screen.js +259 -0
- package/src/updater.js +93 -0
- package/src/utils/clipboard.js +197 -0
- package/src/utils/diff.js +95 -0
- package/src/utils/paths.js +37 -0
- package/src/utils/shell.js +251 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const isWin = process.platform === 'win32';
|
|
7
|
+
let shellCache;
|
|
8
|
+
|
|
9
|
+
// Locate the shell used by the Bash tool. On Windows we need Git Bash, like Claude Code.
|
|
10
|
+
export function findShell(config) {
|
|
11
|
+
if (shellCache) return shellCache;
|
|
12
|
+
const candidates = [];
|
|
13
|
+
if (isWin) {
|
|
14
|
+
if (config?.env.CLAUDE_CODE_GIT_BASH_PATH) candidates.push(config.env.CLAUDE_CODE_GIT_BASH_PATH);
|
|
15
|
+
try {
|
|
16
|
+
const where = spawnSync('where', ['git'], { encoding: 'utf8', windowsHide: true });
|
|
17
|
+
for (const line of (where.stdout || '').split(/\r?\n/).filter(Boolean)) {
|
|
18
|
+
// ...\Git\cmd\git.exe or ...\Git\bin\git.exe or ...\Git\mingw64\bin\git.exe
|
|
19
|
+
let dir = path.dirname(line.trim());
|
|
20
|
+
for (let i = 0; i < 3; i++) {
|
|
21
|
+
candidates.push(path.join(dir, 'bin', 'bash.exe'));
|
|
22
|
+
dir = path.dirname(dir);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch { /* ignore */ }
|
|
26
|
+
candidates.push('C:\\Program Files\\Git\\bin\\bash.exe', 'C:\\Program Files (x86)\\Git\\bin\\bash.exe');
|
|
27
|
+
for (const c of candidates) {
|
|
28
|
+
if (c && fs.existsSync(c)) return (shellCache = { path: c, kind: 'bash', name: 'bash (Git Bash)' });
|
|
29
|
+
}
|
|
30
|
+
return (shellCache = { path: process.env.ComSpec || 'cmd.exe', kind: 'cmd', name: 'cmd.exe' });
|
|
31
|
+
}
|
|
32
|
+
const envShell = process.env.SHELL;
|
|
33
|
+
if (envShell && /(bash|zsh)$/.test(envShell) && fs.existsSync(envShell)) {
|
|
34
|
+
return (shellCache = { path: envShell, kind: 'bash', name: path.basename(envShell) });
|
|
35
|
+
}
|
|
36
|
+
for (const c of ['/bin/bash', '/usr/bin/bash', '/bin/zsh', '/bin/sh']) {
|
|
37
|
+
if (fs.existsSync(c)) return (shellCache = { path: c, kind: 'bash', name: path.basename(c) });
|
|
38
|
+
}
|
|
39
|
+
return (shellCache = { path: 'sh', kind: 'bash', name: 'sh' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---- Process trees on Windows
|
|
43
|
+
//
|
|
44
|
+
// Git for Windows' bin\bash.exe is only a launcher: the real bash is its child, and what a command
|
|
45
|
+
// starts is created through MSYS's fork emulation, which leaves Windows' own parent links broken (a
|
|
46
|
+
// `sleep.exe` is not a descendant of the launcher as far as Windows can tell). `taskkill /T` on the
|
|
47
|
+
// launcher therefore kills bash and native children such as node, but leaves those processes running.
|
|
48
|
+
// MSYS keeps the true parent/child links (`ps -W` prints them), so before killing, the shell's own
|
|
49
|
+
// process id (written to a file by the script, see spawnShell) is used to ask MSYS for everything
|
|
50
|
+
// under it, and all of it is handed to taskkill together with the launcher.
|
|
51
|
+
|
|
52
|
+
// Rows of `ps -W`: PID PPID PGID WINPID ... where the first three are MSYS ids and WINPID the
|
|
53
|
+
// Windows process id. Header and unparsable lines are skipped.
|
|
54
|
+
export function parsePsRows(text) {
|
|
55
|
+
return String(text).split(/\r?\n/)
|
|
56
|
+
.map((line) => /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s/.exec(line))
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.map((m) => ({ pid: +m[1], ppid: +m[2], pgid: +m[3], winpid: +m[4] }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Windows process ids of the MSYS process `rootPid`, everything below it by parent links, and every
|
|
62
|
+
// member of its process group (a child whose parent already exited is no longer linked to it).
|
|
63
|
+
export function msysTree(rows, rootPid) {
|
|
64
|
+
const root = rows.find((r) => r.pid === rootPid);
|
|
65
|
+
if (!root) return [];
|
|
66
|
+
const children = new Map();
|
|
67
|
+
for (const r of rows) {
|
|
68
|
+
if (!children.has(r.ppid)) children.set(r.ppid, []);
|
|
69
|
+
children.get(r.ppid).push(r);
|
|
70
|
+
}
|
|
71
|
+
const seen = new Set([root.pid]);
|
|
72
|
+
const winpids = new Set([root.winpid]);
|
|
73
|
+
const queue = [root.pid];
|
|
74
|
+
while (queue.length) {
|
|
75
|
+
for (const r of children.get(queue.pop()) || []) {
|
|
76
|
+
if (seen.has(r.pid)) continue;
|
|
77
|
+
seen.add(r.pid);
|
|
78
|
+
winpids.add(r.winpid);
|
|
79
|
+
queue.push(r.pid);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (root.pgid > 1) for (const r of rows) if (r.pgid === root.pgid) winpids.add(r.winpid);
|
|
83
|
+
return [...winpids];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let psCache;
|
|
87
|
+
function findPs() {
|
|
88
|
+
if (psCache !== undefined) return psCache;
|
|
89
|
+
psCache = null;
|
|
90
|
+
const shell = findShell(null);
|
|
91
|
+
if (shell.kind === 'bash') {
|
|
92
|
+
const bin = path.dirname(shell.path);
|
|
93
|
+
for (const candidate of [path.join(bin, '..', 'usr', 'bin', 'ps.exe'), path.join(bin, 'ps.exe')]) {
|
|
94
|
+
if (fs.existsSync(candidate)) { psCache = path.resolve(candidate); break; }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return psCache;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Windows ids of the processes MSYS says are under the shell started by spawnShell (empty when unknown).
|
|
101
|
+
function msysDescendants(child) {
|
|
102
|
+
try {
|
|
103
|
+
const root = parseInt(fs.readFileSync(child.claudeProPidFile, 'utf8'), 10);
|
|
104
|
+
const ps = findPs();
|
|
105
|
+
if (!Number.isFinite(root) || !ps) return [];
|
|
106
|
+
const r = spawnSync(ps, ['-W'], { encoding: 'utf8', windowsHide: true, timeout: 5000 });
|
|
107
|
+
return r.status === 0 ? msysTree(parsePsRows(r.stdout), root) : [];
|
|
108
|
+
} catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Kill a process and all its children.
|
|
114
|
+
export function killTree(child) {
|
|
115
|
+
if (!child || child.exitCode !== null || child.killed) return;
|
|
116
|
+
try {
|
|
117
|
+
if (isWin) {
|
|
118
|
+
// The tree has to be looked up before anything is killed: once its parents are gone the links are too.
|
|
119
|
+
const pids = new Set([child.pid, ...(child.claudeProPidFile ? msysDescendants(child) : [])]);
|
|
120
|
+
const args = ['/F', '/T'];
|
|
121
|
+
for (const pid of pids) args.push('/PID', String(pid));
|
|
122
|
+
spawnSync('taskkill', args, { windowsHide: true, stdio: 'ignore' });
|
|
123
|
+
if (child.claudeProPidFile) fs.rmSync(child.claudeProPidFile, { force: true });
|
|
124
|
+
} else {
|
|
125
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
try { child.kill('SIGKILL'); } catch { /* ignore */ }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Run a quick command and return stdout (used for git info etc.). Never throws.
|
|
133
|
+
export function run(cmd, args, { cwd, timeout = 5000 } = {}) {
|
|
134
|
+
try {
|
|
135
|
+
const r = spawnSync(cmd, args, { cwd, encoding: 'utf8', timeout, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
136
|
+
if (r.status !== 0) return null;
|
|
137
|
+
return r.stdout.trim();
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const rgWorks = new Map();
|
|
144
|
+
// The ripgrep binary to use: CLAUDE_PRO_RIPGREP_PATH if it runs, else `rg` on PATH, else null
|
|
145
|
+
// (Grep then uses its JS scanner). CLAUDE_PRO_RIPGREP_PATH=off skips ripgrep altogether.
|
|
146
|
+
export function ripgrepPath(config) {
|
|
147
|
+
const configured = String(config?.env?.CLAUDE_PRO_RIPGREP_PATH ?? process.env.CLAUDE_PRO_RIPGREP_PATH ?? '').trim();
|
|
148
|
+
if (/^(off|none|false|0)$/i.test(configured)) return null;
|
|
149
|
+
for (const c of [configured, 'rg'].filter(Boolean)) {
|
|
150
|
+
if (!rgWorks.has(c)) rgWorks.set(c, run(c, ['--version']) !== null);
|
|
151
|
+
if (rgWorks.get(c)) return c;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
export function hasRipgrep(config) { return ripgrepPath(config) !== null; }
|
|
156
|
+
|
|
157
|
+
// `stdin`: text to feed the command on standard input (hooks get their event JSON this way).
|
|
158
|
+
// Without it the command's stdin is closed, so nothing can hang waiting for input.
|
|
159
|
+
export function spawnShell(shell, command, { cwd, env, cwdFile, stdin }) {
|
|
160
|
+
const withStdin = typeof stdin === 'string';
|
|
161
|
+
const stdio = [withStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'];
|
|
162
|
+
const feed = (child) => {
|
|
163
|
+
if (withStdin) { child.stdin.on('error', () => {}); child.stdin.end(stdin); }
|
|
164
|
+
return child;
|
|
165
|
+
};
|
|
166
|
+
if (shell.kind === 'cmd') {
|
|
167
|
+
return feed(spawn(shell.path, ['/d', '/s', '/c', `"${command}"`], {
|
|
168
|
+
cwd, env, windowsHide: true, windowsVerbatimArguments: true, stdio,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
// The command is evaluated via an env var so no quoting is needed; afterwards we
|
|
172
|
+
// record the final working directory so `cd` persists between calls. On Windows the script first
|
|
173
|
+
// writes its own (MSYS) process id to a file, which is how killTree finds everything under it.
|
|
174
|
+
const pidFile = isWin ? path.join(os.tmpdir(), `claude-pro-pid-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`) : null;
|
|
175
|
+
const script = [
|
|
176
|
+
...(pidFile ? ['if [ -n "$CLAUDE_PRO_PID_FILE" ]; then echo $$ > "$CLAUDE_PRO_PID_FILE"; fi'] : []),
|
|
177
|
+
`eval "$CLAUDE_PRO_COMMAND"${withStdin ? '' : ' < /dev/null'}`,
|
|
178
|
+
'__ccc_status=$?',
|
|
179
|
+
'if [ -n "$CLAUDE_PRO_CWD_FILE" ]; then { pwd -W 2>/dev/null || pwd -P; } > "$CLAUDE_PRO_CWD_FILE" 2>/dev/null; fi',
|
|
180
|
+
'exit $__ccc_status',
|
|
181
|
+
].join('\n');
|
|
182
|
+
const child = spawn(shell.path, ['-c', script], {
|
|
183
|
+
cwd,
|
|
184
|
+
env: { ...env, CLAUDE_PRO_COMMAND: command, CLAUDE_PRO_CWD_FILE: cwdFile || '', CLAUDE_PRO_PID_FILE: pidFile || '' },
|
|
185
|
+
windowsHide: true,
|
|
186
|
+
detached: !isWin,
|
|
187
|
+
stdio,
|
|
188
|
+
});
|
|
189
|
+
if (pidFile) {
|
|
190
|
+
child.claudeProPidFile = pidFile;
|
|
191
|
+
child.once('close', () => { try { fs.rmSync(pidFile, { force: true }); } catch { /* best effort */ } });
|
|
192
|
+
}
|
|
193
|
+
return feed(child);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Collect the output of a shell started with spawnShell and report when it is finished:
|
|
197
|
+
// onOutput(text, 'stdout' | 'stderr') for each piece (decoded as a stream, so a multi-byte character
|
|
198
|
+
// split between two chunks survives) and done(code, error) exactly once.
|
|
199
|
+
//
|
|
200
|
+
// A command that leaves a process running (`server &`) keeps the shell's output pipes open after the
|
|
201
|
+
// shell itself has exited, and 'close' would not fire until that process ends. Waiting for it would hold
|
|
202
|
+
// the caller until its timeout, which then kills the very process the command meant to start. So once the
|
|
203
|
+
// shell is gone, done is called as soon as the output has been quiet for idleMs, or after graceMs at the
|
|
204
|
+
// latest. The pipes are drained rather than closed, so the process is not killed by a broken pipe.
|
|
205
|
+
export function watchShell(child, { onOutput, done, idleMs = 500, graceMs = 3000 }) {
|
|
206
|
+
let finished = false;
|
|
207
|
+
let exited = false;
|
|
208
|
+
let idleTimer = null;
|
|
209
|
+
let graceTimer = null;
|
|
210
|
+
const finish = (code, err) => {
|
|
211
|
+
if (finished) return;
|
|
212
|
+
finished = true;
|
|
213
|
+
clearTimeout(idleTimer);
|
|
214
|
+
clearTimeout(graceTimer);
|
|
215
|
+
done(code, err);
|
|
216
|
+
};
|
|
217
|
+
const settleAfterExit = () => finish(child.exitCode);
|
|
218
|
+
const armIdle = () => { clearTimeout(idleTimer); idleTimer = setTimeout(settleAfterExit, idleMs); };
|
|
219
|
+
for (const stream of ['stdout', 'stderr']) {
|
|
220
|
+
child[stream].setEncoding('utf8');
|
|
221
|
+
child[stream].on('data', (text) => {
|
|
222
|
+
if (finished) return; // late output of a process the command left running is drained, not kept
|
|
223
|
+
onOutput(text, stream);
|
|
224
|
+
if (exited) armIdle();
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
child.on('error', (e) => finish(null, e));
|
|
228
|
+
child.on('exit', () => {
|
|
229
|
+
exited = true;
|
|
230
|
+
graceTimer = setTimeout(settleAfterExit, graceMs);
|
|
231
|
+
armIdle();
|
|
232
|
+
});
|
|
233
|
+
child.on('close', (code) => finish(code));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Delete the small files shells leave in the temp directory (pid files, working directory files)
|
|
237
|
+
// when the program was killed before it could remove them. Best effort, run at startup.
|
|
238
|
+
export function pruneStaleShellFiles(dir = os.tmpdir(), maxAgeMs = 7 * 24 * 3600_000) {
|
|
239
|
+
let names = [];
|
|
240
|
+
try { names = fs.readdirSync(dir); } catch { return 0; }
|
|
241
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
242
|
+
let removed = 0;
|
|
243
|
+
for (const name of names) {
|
|
244
|
+
if (!/^claude-pro-(?:pid|cwd)-/.test(name)) continue;
|
|
245
|
+
const file = path.join(dir, name);
|
|
246
|
+
try {
|
|
247
|
+
if (fs.statSync(file).mtimeMs < cutoff) { fs.rmSync(file, { force: true }); removed++; }
|
|
248
|
+
} catch { /* ignore */ }
|
|
249
|
+
}
|
|
250
|
+
return removed;
|
|
251
|
+
}
|