@volter/supercode-terminal 0.2.30
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/CAPABILITIES.md +57 -0
- package/LICENSE-SUBSTRATE +201 -0
- package/NOTICE-SUBSTRATE +14 -0
- package/README.md +190 -0
- package/client.d.ts +17 -0
- package/client.mjs +32 -0
- package/index.d.ts +195 -0
- package/index.mjs +981 -0
- package/package.json +156 -0
- package/react/stream.d.ts +3 -0
- package/react/stream.mjs +446 -0
- package/react/ui.d.ts +136 -0
- package/react/ui.mjs +892 -0
- package/stream.d.ts +3 -0
- package/stream.mjs +446 -0
- package/styles.css +160 -0
- package/substrate/attach.d.ts +29 -0
- package/substrate/attach.js +147 -0
- package/substrate/client.d.ts +62 -0
- package/substrate/client.js +138 -0
- package/substrate/docker-exec.d.ts +41 -0
- package/substrate/docker-exec.js +101 -0
- package/substrate/internal/exec.d.ts +15 -0
- package/substrate/internal/exec.js +192 -0
- package/substrate/internal/process-tree.d.ts +1 -0
- package/substrate/internal/process-tree.js +41 -0
- package/substrate/local-terminal.d.ts +13 -0
- package/substrate/local-terminal.js +41 -0
- package/substrate/native-host.d.ts +57 -0
- package/substrate/native-host.js +529 -0
- package/substrate/session-lifecycle.d.ts +162 -0
- package/substrate/session-lifecycle.js +1344 -0
- package/substrate/session-roll.d.ts +14 -0
- package/substrate/session-roll.js +407 -0
- package/substrate/tmux-stream.d.ts +32 -0
- package/substrate/tmux-stream.js +212 -0
- package/substrate/tmux.d.ts +144 -0
- package/substrate/tmux.js +899 -0
- package/ui.d.ts +136 -0
- package/ui.mjs +892 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
function runEnvironment(options) {
|
|
3
|
+
if (options.inheritEnv === false) {
|
|
4
|
+
return options.env ?? {};
|
|
5
|
+
}
|
|
6
|
+
return options.env ? { ...process.env, ...options.env } : process.env;
|
|
7
|
+
}
|
|
8
|
+
// Memoized by command name: PATH is finalized once at process startup
|
|
9
|
+
// (ensureSystemToolsOnPath, called before any subprocess runs) and never changes
|
|
10
|
+
// for the rest of the process's life in this codebase's usage, so re-spawning
|
|
11
|
+
// `command -v <name>` on every call is pure repeated cost. This matters because
|
|
12
|
+
// requireCommand/assertTmux-style guards run on hot per-request paths (e.g.
|
|
13
|
+
// captureTerminal calling assertTmux on every capture) — a synchronous spawnSync
|
|
14
|
+
// there would otherwise reintroduce a small blocking cost on every single call,
|
|
15
|
+
// undermining the whole point of an async capture path (see CLAUDE.md's
|
|
16
|
+
// event-loop invariant).
|
|
17
|
+
const commandExistsCache = new Map();
|
|
18
|
+
export function commandExists(command) {
|
|
19
|
+
const cached = commandExistsCache.get(command);
|
|
20
|
+
if (cached !== undefined) {
|
|
21
|
+
return cached;
|
|
22
|
+
}
|
|
23
|
+
const result = spawnSync("command", ["-v", command], {
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
shell: true
|
|
26
|
+
});
|
|
27
|
+
const exists = result.status === 0;
|
|
28
|
+
commandExistsCache.set(command, exists);
|
|
29
|
+
return exists;
|
|
30
|
+
}
|
|
31
|
+
export function requireCommand(command, installHint) {
|
|
32
|
+
if (!commandExists(command)) {
|
|
33
|
+
const hint = installHint ? ` ${installHint}` : "";
|
|
34
|
+
throw new Error(`${command} is required but was not found on PATH.${hint}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// The standard system tool directories. Appended (never reordered) to PATH at
|
|
38
|
+
// process startup so `run`/`execFile` of ps, tmux, and coreutils resolve even when
|
|
39
|
+
// the process was launched from a stripped-PATH context — cron, launchd, a GUI
|
|
40
|
+
// app, or a nested agent shell whose PATH was degraded. A provider booted that way
|
|
41
|
+
// otherwise silently loses its lifecycle observation (`spawn ps ENOENT`) while
|
|
42
|
+
// healthz still answers. Non-existent dirs (e.g. /opt/homebrew on Intel/Linux) are
|
|
43
|
+
// harmless — the OS skips them during lookup.
|
|
44
|
+
const systemToolDirs = ["/usr/bin", "/bin", "/usr/sbin", "/sbin", "/usr/local/bin", "/opt/homebrew/bin"];
|
|
45
|
+
// Guarantee the system tool dirs are on PATH without disturbing the caller's own
|
|
46
|
+
// order or entries: only the missing ones are appended. Call once at entrypoint
|
|
47
|
+
// startup, before any subprocess runs. Idempotent.
|
|
48
|
+
export function ensureSystemToolsOnPath() {
|
|
49
|
+
const current = (process.env.PATH ?? "").split(":").filter(Boolean);
|
|
50
|
+
const have = new Set(current);
|
|
51
|
+
const missing = systemToolDirs.filter((dir) => !have.has(dir));
|
|
52
|
+
if (missing.length > 0) {
|
|
53
|
+
process.env.PATH = [...current, ...missing].join(":");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function run(command, args, options = {}) {
|
|
57
|
+
const result = spawnSync(command, args, {
|
|
58
|
+
cwd: options.cwd,
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
env: runEnvironment(options),
|
|
61
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
62
|
+
timeout: options.timeoutMs
|
|
63
|
+
});
|
|
64
|
+
if (result.error) {
|
|
65
|
+
const message = result.error.name === "Error" && "code" in result.error && result.error.code === "ETIMEDOUT"
|
|
66
|
+
? `${command} ${args.join(" ")} timed out after ${options.timeoutMs}ms.`
|
|
67
|
+
: result.error.message;
|
|
68
|
+
const detail = runOutputDetail(result.stdout, result.stderr);
|
|
69
|
+
if (detail) {
|
|
70
|
+
throw new Error(`${message}\n${detail}`);
|
|
71
|
+
}
|
|
72
|
+
throw result.error;
|
|
73
|
+
}
|
|
74
|
+
if (result.status !== 0) {
|
|
75
|
+
const output = runOutputDetail(result.stdout, result.stderr);
|
|
76
|
+
const detail = output ? `\n${output}` : "";
|
|
77
|
+
throw new Error(`${command} ${args.join(" ")} failed with exit ${result.status}.${detail}`);
|
|
78
|
+
}
|
|
79
|
+
return result.stdout;
|
|
80
|
+
}
|
|
81
|
+
function runOutputDetail(stdout, stderr) {
|
|
82
|
+
const stderrText = outputText(stderr);
|
|
83
|
+
const stdoutText = outputText(stdout);
|
|
84
|
+
if (stderrText && stdoutText) {
|
|
85
|
+
return `stderr:\n${stderrText}\nstdout:\n${stdoutText}`;
|
|
86
|
+
}
|
|
87
|
+
if (stderrText) {
|
|
88
|
+
return `stderr:\n${stderrText}`;
|
|
89
|
+
}
|
|
90
|
+
if (stdoutText) {
|
|
91
|
+
return `stdout:\n${stdoutText}`;
|
|
92
|
+
}
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
function outputText(value) {
|
|
96
|
+
if (Buffer.isBuffer(value)) {
|
|
97
|
+
return value.toString("utf8").trim();
|
|
98
|
+
}
|
|
99
|
+
return String(value ?? "").trim();
|
|
100
|
+
}
|
|
101
|
+
export function runBuffer(command, args, options = {}) {
|
|
102
|
+
const result = spawnSync(command, args, {
|
|
103
|
+
cwd: options.cwd,
|
|
104
|
+
env: runEnvironment(options),
|
|
105
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
106
|
+
});
|
|
107
|
+
if (result.error) {
|
|
108
|
+
throw result.error;
|
|
109
|
+
}
|
|
110
|
+
if (result.status !== 0) {
|
|
111
|
+
const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString("utf8").trim() : String(result.stderr ?? "").trim();
|
|
112
|
+
const detail = stderr ? `\n${stderr}` : "";
|
|
113
|
+
throw new Error(`${command} ${args.join(" ")} failed with exit ${result.status}.${detail}`);
|
|
114
|
+
}
|
|
115
|
+
return Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? "");
|
|
116
|
+
}
|
|
117
|
+
export function runWithInput(command, args, input, options = {}) {
|
|
118
|
+
const result = spawnSync(command, args, {
|
|
119
|
+
cwd: options.cwd,
|
|
120
|
+
env: runEnvironment(options),
|
|
121
|
+
input,
|
|
122
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
123
|
+
});
|
|
124
|
+
if (result.error) {
|
|
125
|
+
throw result.error;
|
|
126
|
+
}
|
|
127
|
+
if (result.status !== 0) {
|
|
128
|
+
const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString("utf8").trim() : String(result.stderr ?? "").trim();
|
|
129
|
+
const detail = stderr ? `\n${stderr}` : "";
|
|
130
|
+
throw new Error(`${command} ${args.join(" ")} failed with exit ${result.status}.${detail}`);
|
|
131
|
+
}
|
|
132
|
+
return Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? "");
|
|
133
|
+
}
|
|
134
|
+
export async function runAsync(command, args, options = {}) {
|
|
135
|
+
return await spawnAsync(command, args, options);
|
|
136
|
+
}
|
|
137
|
+
export async function runWithInputAsync(command, args, input, options = {}) {
|
|
138
|
+
return await spawnAsync(command, args, options, input);
|
|
139
|
+
}
|
|
140
|
+
async function spawnAsync(command, args, options, input) {
|
|
141
|
+
return await new Promise((resolve, reject) => {
|
|
142
|
+
const child = spawn(command, args, {
|
|
143
|
+
cwd: options.cwd,
|
|
144
|
+
env: runEnvironment(options),
|
|
145
|
+
stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"]
|
|
146
|
+
});
|
|
147
|
+
const stdout = [];
|
|
148
|
+
const stderr = [];
|
|
149
|
+
// Honor timeoutMs without blocking the loop (the sync `run` relied on
|
|
150
|
+
// spawnSync's timeout): SIGKILL the child and reject if it overruns.
|
|
151
|
+
const timer = options.timeoutMs === undefined
|
|
152
|
+
? undefined
|
|
153
|
+
: setTimeout(() => {
|
|
154
|
+
child.kill("SIGKILL");
|
|
155
|
+
reject(new Error(`${command} ${args.join(" ")} timed out after ${options.timeoutMs}ms.`));
|
|
156
|
+
}, options.timeoutMs);
|
|
157
|
+
timer?.unref?.();
|
|
158
|
+
const clear = () => {
|
|
159
|
+
if (timer)
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
};
|
|
162
|
+
child.stdout?.on("data", (chunk) => stdout.push(chunk));
|
|
163
|
+
child.stderr?.on("data", (chunk) => stderr.push(chunk));
|
|
164
|
+
child.on("error", (error) => {
|
|
165
|
+
clear();
|
|
166
|
+
reject(error);
|
|
167
|
+
});
|
|
168
|
+
child.on("close", (code) => {
|
|
169
|
+
clear();
|
|
170
|
+
if (code !== 0) {
|
|
171
|
+
const detail = Buffer.concat(stderr).toString("utf8").trim();
|
|
172
|
+
reject(new Error(`${command} ${args.join(" ")} failed with exit ${code}.${detail ? `\n${detail}` : ""}`));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
resolve(Buffer.concat(stdout).toString("utf8"));
|
|
176
|
+
});
|
|
177
|
+
if (input !== undefined && child.stdin) {
|
|
178
|
+
child.stdin.on("error", reject);
|
|
179
|
+
child.stdin.end(input);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
export function spawnInherited(command, args) {
|
|
184
|
+
const child = spawn(command, args, { stdio: "inherit" });
|
|
185
|
+
child.on("exit", (code, signal) => {
|
|
186
|
+
if (signal) {
|
|
187
|
+
process.kill(process.pid, signal);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
process.exit(code ?? 1);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function snapshotDescendantPids(rootPids: number[]): number[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
const PROCESS_SNAPSHOT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
3
|
+
// Return only each requested root and its descendants. This is the safe input
|
|
4
|
+
// to the tmux session cleanup sweep: the tmux server is a parent and therefore
|
|
5
|
+
// can never be selected, while helpers detached below a pane remain reachable.
|
|
6
|
+
export function snapshotDescendantPids(rootPids) {
|
|
7
|
+
if (rootPids.length === 0)
|
|
8
|
+
return [];
|
|
9
|
+
const stdout = execFileSync("ps", ["-axo", "pid=,ppid="], {
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
maxBuffer: PROCESS_SNAPSHOT_MAX_BUFFER,
|
|
12
|
+
timeout: 5000
|
|
13
|
+
});
|
|
14
|
+
const children = new Map();
|
|
15
|
+
const processIds = new Set();
|
|
16
|
+
for (const line of stdout.split("\n")) {
|
|
17
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)$/);
|
|
18
|
+
if (!match)
|
|
19
|
+
continue;
|
|
20
|
+
const pid = Number(match[1]);
|
|
21
|
+
const parent = Number(match[2]);
|
|
22
|
+
if (!Number.isInteger(pid) || pid < 1 || !Number.isInteger(parent) || parent < 0)
|
|
23
|
+
continue;
|
|
24
|
+
processIds.add(pid);
|
|
25
|
+
const siblings = children.get(parent) ?? [];
|
|
26
|
+
siblings.push(pid);
|
|
27
|
+
children.set(parent, siblings);
|
|
28
|
+
}
|
|
29
|
+
const result = [];
|
|
30
|
+
const pending = [...new Set(rootPids.filter((pid) => processIds.has(pid)))];
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
while (pending.length > 0) {
|
|
33
|
+
const pid = pending.pop();
|
|
34
|
+
if (pid === undefined || seen.has(pid))
|
|
35
|
+
continue;
|
|
36
|
+
seen.add(pid);
|
|
37
|
+
result.push(pid);
|
|
38
|
+
pending.push(...(children.get(pid) ?? []));
|
|
39
|
+
}
|
|
40
|
+
return result.sort((a, b) => a - b);
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface LocalTerminalCommand {
|
|
2
|
+
program: string;
|
|
3
|
+
arguments?: readonly string[];
|
|
4
|
+
cwd?: string;
|
|
5
|
+
env?: Readonly<Record<string, string>>;
|
|
6
|
+
}
|
|
7
|
+
export type LocalTerminalRunner = (program: string, arguments_: readonly string[]) => Promise<void>;
|
|
8
|
+
export interface OpenLocalTerminalOptions {
|
|
9
|
+
platform?: NodeJS.Platform;
|
|
10
|
+
runner?: LocalTerminalRunner;
|
|
11
|
+
}
|
|
12
|
+
export declare function terminalShellCommand(command: LocalTerminalCommand): string;
|
|
13
|
+
export declare function openLocalTerminal(command: LocalTerminalCommand, options?: OpenLocalTerminalOptions): Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { runAsync } from "./internal/exec.js";
|
|
2
|
+
const APPLE_SCRIPT = [
|
|
3
|
+
"on run argv",
|
|
4
|
+
"ignoring application responses",
|
|
5
|
+
'tell application "Terminal"',
|
|
6
|
+
"activate",
|
|
7
|
+
"do script (item 1 of argv)",
|
|
8
|
+
"end tell",
|
|
9
|
+
"end ignoring",
|
|
10
|
+
"end run",
|
|
11
|
+
].join("\n");
|
|
12
|
+
export function terminalShellCommand(command) {
|
|
13
|
+
if (!command.program)
|
|
14
|
+
throw new Error("A terminal command program is required.");
|
|
15
|
+
const environment = Object.entries(command.env ?? {}).map(([name, value]) => {
|
|
16
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
17
|
+
throw new Error(`Invalid terminal environment variable name: ${JSON.stringify(name)}.`);
|
|
18
|
+
}
|
|
19
|
+
return `${name}=${value}`;
|
|
20
|
+
});
|
|
21
|
+
const launch = [
|
|
22
|
+
...(environment.length ? ["env", ...environment] : []),
|
|
23
|
+
command.program,
|
|
24
|
+
...(command.arguments ?? []),
|
|
25
|
+
].map(shellQuote).join(" ");
|
|
26
|
+
return command.cwd ? `cd ${shellQuote(command.cwd)} && exec ${launch}` : `exec ${launch}`;
|
|
27
|
+
}
|
|
28
|
+
export async function openLocalTerminal(command, options = {}) {
|
|
29
|
+
const platform = options.platform ?? process.platform;
|
|
30
|
+
if (platform !== "darwin") {
|
|
31
|
+
throw new Error(`Opening a local terminal is not supported on ${platform}.`);
|
|
32
|
+
}
|
|
33
|
+
const runner = options.runner ?? defaultRunner;
|
|
34
|
+
await runner("osascript", ["-e", APPLE_SCRIPT, terminalShellCommand(command)]);
|
|
35
|
+
}
|
|
36
|
+
async function defaultRunner(program, arguments_) {
|
|
37
|
+
await runAsync(program, [...arguments_], { timeoutMs: 5_000 });
|
|
38
|
+
}
|
|
39
|
+
function shellQuote(value) {
|
|
40
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type LocalTerminalCommand } from "./local-terminal.js";
|
|
2
|
+
import { type SessionLifecycleState } from "./session-lifecycle.js";
|
|
3
|
+
export type NativeTerminalBackend = "iterm" | "native-terminal-app";
|
|
4
|
+
export type NativeTerminalSession = Readonly<{
|
|
5
|
+
agentSessionId: string;
|
|
6
|
+
backend: NativeTerminalBackend;
|
|
7
|
+
state: SessionLifecycleState;
|
|
8
|
+
}>;
|
|
9
|
+
export type NativeSessionIdleProof = Readonly<{
|
|
10
|
+
observedAtMs: number;
|
|
11
|
+
source: string;
|
|
12
|
+
turn: "idle";
|
|
13
|
+
}>;
|
|
14
|
+
export type NativeSessionResolution = Readonly<{
|
|
15
|
+
proof: NativeSessionIdleProof;
|
|
16
|
+
session: NativeTerminalSession;
|
|
17
|
+
}>;
|
|
18
|
+
export type NativeTerminalApplication = "iterm" | "terminal";
|
|
19
|
+
/**
|
|
20
|
+
* Host-only local terminal capability for embedders.
|
|
21
|
+
*
|
|
22
|
+
* The public surface contains durable agent session ids and semantic state only.
|
|
23
|
+
* Native window ids, tty paths, process ids, AppleScript references, and tmux
|
|
24
|
+
* handles remain private to this process.
|
|
25
|
+
*/
|
|
26
|
+
export declare class NativeTerminalSessionHost {
|
|
27
|
+
private readonly lifecycleStore;
|
|
28
|
+
private readonly rollTracker;
|
|
29
|
+
private observationPending;
|
|
30
|
+
private readonly claudeConfigDir;
|
|
31
|
+
private readonly resolvedSessionIds;
|
|
32
|
+
private sessions;
|
|
33
|
+
constructor(options?: {
|
|
34
|
+
home?: string;
|
|
35
|
+
claudeConfigDir?: string;
|
|
36
|
+
});
|
|
37
|
+
listSessions(): Promise<NativeTerminalSession[]>;
|
|
38
|
+
private observeSessions;
|
|
39
|
+
captureSession(agentSessionId: string, options?: {
|
|
40
|
+
lines?: number;
|
|
41
|
+
}): Promise<string>;
|
|
42
|
+
openSession(command: LocalTerminalCommand, options: {
|
|
43
|
+
application: NativeTerminalApplication;
|
|
44
|
+
}): Promise<void>;
|
|
45
|
+
hasSessionCandidate(agentSessionId: string, options: {
|
|
46
|
+
cwd: string;
|
|
47
|
+
}): boolean;
|
|
48
|
+
resolveSession(agentSessionId: string, options: {
|
|
49
|
+
cwd: string;
|
|
50
|
+
}): Promise<NativeSessionResolution | null>;
|
|
51
|
+
relinquishSession(agentSessionId: string, proof: NativeSessionIdleProof): Promise<{
|
|
52
|
+
agentSessionId: string;
|
|
53
|
+
released: true;
|
|
54
|
+
}>;
|
|
55
|
+
private requireSession;
|
|
56
|
+
private observeSurfaces;
|
|
57
|
+
}
|