@titan-design/agent-surface 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +39 -2
- package/dist/bin.js +27 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-5FGP7YKX.js +213 -0
- package/dist/chunk-5FGP7YKX.js.map +1 -0
- package/dist/index.d.ts +375 -0
- package/dist/index.js +400 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Henry Jewkes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @titan-design/agent-surface
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Where a spawned agent is presented, and the launcher that starts it there. A surface is
|
|
4
|
+
headless (a detached child with discarded streams) or an iTerm2 pane, tab or window placed
|
|
5
|
+
beside an anchor session by UUID, never by focus. Every surface launches one fixed command
|
|
6
|
+
supplied by the host; that command reads a launch plan from disk, so a model-authored brief
|
|
7
|
+
never reaches a command line or an AppleScript string.
|
|
8
|
+
|
|
9
|
+
Tier 1 of the titan-platform DAG, with no dependencies. Extracted from agent-chat's
|
|
10
|
+
`agents/surfaces/`, `run-agent.ts`, `launch-output.ts` and `pane-identity.ts` (TP-639).
|
|
11
|
+
|
|
12
|
+
## Library
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { surfaceFor, type Launcher } from "@titan-design/agent-surface";
|
|
16
|
+
|
|
17
|
+
const launcher: Launcher = {
|
|
18
|
+
argv: id => [process.execPath, "/path/to/titan-agent-launch", `/state/agents/${id}/plan.json`],
|
|
19
|
+
env: { STATE_HOME: "/state" },
|
|
20
|
+
relaunchPath: id => `/state/agents/${id}/relaunch`,
|
|
21
|
+
};
|
|
22
|
+
const handle = await surfaceFor("iterm-pane", launcher, { anchor: process.env.ITERM_SESSION_ID }).launch(plan);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`AppleScriptRunner`, `SpawnFn` and `ProcessProbe` are injectable, so the suites run on Linux.
|
|
26
|
+
|
|
27
|
+
## Command line
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
titan-agent-launch [--launcher-pid-env NAME] <plan.json>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Writes the pane title (and, in iTerm, the tab colour and badge), then execs `plan.bin` with
|
|
34
|
+
`plan.args`, `plan.env` over its own environment, `plan.unsetEnv` deleted, and its own pid in
|
|
35
|
+
`NAME` (default `TITAN_AGENT_LAUNCHER_PID`). A headless plan's `stdin` is the brief. The
|
|
36
|
+
stderr tail goes to `stderr-tail.txt` beside the plan. It exits the way the agent did; 64 is
|
|
37
|
+
a usage error and 66 an unreadable plan.
|
|
38
|
+
|
|
39
|
+
The bin execs `plan.bin` as written and inherits its environment unscrubbed. A host that
|
|
40
|
+
resolves `claude` or scrubs secrets calls `runAgent(plan, { resolveBin, baseEnv })` instead.
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
parseLaunchArgs,
|
|
4
|
+
readLaunchPlan,
|
|
5
|
+
runAgent
|
|
6
|
+
} from "./chunk-5FGP7YKX.js";
|
|
7
|
+
|
|
8
|
+
// src/bin.ts
|
|
9
|
+
import path from "path";
|
|
10
|
+
var parsed = parseLaunchArgs(process.argv.slice(2));
|
|
11
|
+
if ("error" in parsed) {
|
|
12
|
+
process.stderr.write(`titan-agent-launch: ${parsed.error}
|
|
13
|
+
`);
|
|
14
|
+
process.exit(64);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const plan = readLaunchPlan(parsed.planFile);
|
|
18
|
+
runAgent(plan, {
|
|
19
|
+
agentDir: path.dirname(parsed.planFile),
|
|
20
|
+
...parsed.launcherPidEnv === void 0 ? {} : { launcherPidEnv: parsed.launcherPidEnv }
|
|
21
|
+
});
|
|
22
|
+
} catch (err) {
|
|
23
|
+
process.stderr.write(`titan-agent-launch: ${err.message}
|
|
24
|
+
`);
|
|
25
|
+
process.exit(66);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\nimport path from \"node:path\";\nimport { parseLaunchArgs } from \"./launch-args.js\";\nimport { readLaunchPlan, runAgent } from \"./run-agent.js\";\n\nconst parsed = parseLaunchArgs(process.argv.slice(2));\nif (\"error\" in parsed) {\n process.stderr.write(`titan-agent-launch: ${parsed.error}\\n`);\n process.exit(64);\n}\n\ntry {\n const plan = readLaunchPlan(parsed.planFile);\n runAgent(plan, {\n agentDir: path.dirname(parsed.planFile),\n ...(parsed.launcherPidEnv === undefined ? {} : { launcherPidEnv: parsed.launcherPidEnv }),\n });\n} catch (err) {\n process.stderr.write(`titan-agent-launch: ${(err as Error).message}\\n`);\n process.exit(66);\n}\n"],"mappings":";;;;;;;;AACA,OAAO,UAAU;AAIjB,IAAM,SAAS,gBAAgB,QAAQ,KAAK,MAAM,CAAC,CAAC;AACpD,IAAI,WAAW,QAAQ;AACrB,UAAQ,OAAO,MAAM,uBAAuB,OAAO,KAAK;AAAA,CAAI;AAC5D,UAAQ,KAAK,EAAE;AACjB;AAEA,IAAI;AACF,QAAM,OAAO,eAAe,OAAO,QAAQ;AAC3C,WAAS,MAAM;AAAA,IACb,UAAU,KAAK,QAAQ,OAAO,QAAQ;AAAA,IACtC,GAAI,OAAO,mBAAmB,SAAY,CAAC,IAAI,EAAE,gBAAgB,OAAO,eAAe;AAAA,EACzF,CAAC;AACH,SAAS,KAAK;AACZ,UAAQ,OAAO,MAAM,uBAAwB,IAAc,OAAO;AAAA,CAAI;AACtE,UAAQ,KAAK,EAAE;AACjB;","names":[]}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// src/launch-args.ts
|
|
2
|
+
var LAUNCH_USAGE = "usage: titan-agent-launch [--launcher-pid-env NAME] <plan.json>";
|
|
3
|
+
var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4
|
+
function parseLaunchArgs(argv) {
|
|
5
|
+
const rest = [...argv];
|
|
6
|
+
let launcherPidEnv;
|
|
7
|
+
if (rest[0] === "--launcher-pid-env") {
|
|
8
|
+
launcherPidEnv = rest[1];
|
|
9
|
+
if (launcherPidEnv === void 0 || !ENV_NAME.test(launcherPidEnv))
|
|
10
|
+
return { error: `--launcher-pid-env needs a variable name
|
|
11
|
+
${LAUNCH_USAGE}` };
|
|
12
|
+
rest.splice(0, 2);
|
|
13
|
+
}
|
|
14
|
+
const [planFile, ...extra] = rest;
|
|
15
|
+
if (planFile === void 0 || planFile.startsWith("-") || extra.length > 0) return { error: LAUNCH_USAGE };
|
|
16
|
+
return launcherPidEnv === void 0 ? { planFile } : { planFile, launcherPidEnv };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/launch-output.ts
|
|
20
|
+
import fs from "fs";
|
|
21
|
+
import path from "path";
|
|
22
|
+
var OUTPUT_TAIL_BYTES = 4096;
|
|
23
|
+
var outputTailPath = (agentDir) => path.join(agentDir, "stderr-tail.txt");
|
|
24
|
+
function tailKeeper(limit = OUTPUT_TAIL_BYTES) {
|
|
25
|
+
let kept = "";
|
|
26
|
+
return {
|
|
27
|
+
append: (chunk) => {
|
|
28
|
+
kept = (kept + chunk).slice(-limit);
|
|
29
|
+
},
|
|
30
|
+
text: () => kept
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function writeOutputTail(agentDir, text) {
|
|
34
|
+
if (text === "") return;
|
|
35
|
+
fs.mkdirSync(agentDir, { recursive: true, mode: 448 });
|
|
36
|
+
fs.writeFileSync(outputTailPath(agentDir), text, { mode: 384 });
|
|
37
|
+
}
|
|
38
|
+
function clearOutputTail(agentDir) {
|
|
39
|
+
fs.rmSync(outputTailPath(agentDir), { force: true });
|
|
40
|
+
}
|
|
41
|
+
function readOutputTail(agentDir) {
|
|
42
|
+
try {
|
|
43
|
+
return fs.readFileSync(outputTailPath(agentDir), "utf8");
|
|
44
|
+
} catch {
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var NOT_LOGGED_IN = /not logged in|please run \/login/i;
|
|
49
|
+
function loginGap(output, configDir) {
|
|
50
|
+
if (output === void 0 || !NOT_LOGGED_IN.test(output)) return void 0;
|
|
51
|
+
const env = configDir === void 0 ? "CLAUDE_CONFIG_DIR unset" : `CLAUDE_CONFIG_DIR=${configDir}`;
|
|
52
|
+
return `Claude Code reported it is not logged in. Run \`claude /login\` with ${env} so the login lands in the config dir this agent uses.`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/pane-identity.ts
|
|
56
|
+
var PANE_PALETTE = [
|
|
57
|
+
"#d9534f",
|
|
58
|
+
"#e8833a",
|
|
59
|
+
"#d4b200",
|
|
60
|
+
"#6aa84f",
|
|
61
|
+
"#2e9e8f",
|
|
62
|
+
"#3d85c6",
|
|
63
|
+
"#674ea7",
|
|
64
|
+
"#c2478f",
|
|
65
|
+
"#8e6c3a",
|
|
66
|
+
"#5b8c2a",
|
|
67
|
+
"#1f6f8b",
|
|
68
|
+
"#a64d79"
|
|
69
|
+
];
|
|
70
|
+
var HEX = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;
|
|
71
|
+
var isHexColour = (value) => typeof value === "string" && HEX.test(value);
|
|
72
|
+
function parseHex(hex) {
|
|
73
|
+
const m = HEX.exec(hex);
|
|
74
|
+
if (m === null) return void 0;
|
|
75
|
+
return { r: parseInt(m[1] ?? "", 16), g: parseInt(m[2] ?? "", 16), b: parseInt(m[3] ?? "", 16) };
|
|
76
|
+
}
|
|
77
|
+
function fnv1a(text) {
|
|
78
|
+
let hash = 2166136261;
|
|
79
|
+
for (const byte of Buffer.from(text, "utf8")) hash = Math.imul(hash ^ byte, 16777619) >>> 0;
|
|
80
|
+
return hash;
|
|
81
|
+
}
|
|
82
|
+
var hashedColour = (key) => PANE_PALETTE[fnv1a(key) % PANE_PALETTE.length] ?? "#3d85c6";
|
|
83
|
+
function coordinatorOf(name, seats) {
|
|
84
|
+
const seat = seats.find((s) => s.name === name) ?? seats.find((s) => name.startsWith(`${s.prefix}-`));
|
|
85
|
+
return seat?.name ?? name.split("-")[0] ?? name;
|
|
86
|
+
}
|
|
87
|
+
function paneColour(name, profile, seats, config) {
|
|
88
|
+
const coordinator = coordinatorOf(name, seats);
|
|
89
|
+
return config.seats[coordinator] ?? (profile === void 0 ? void 0 : config.profiles[profile]) ?? hashedColour(coordinator);
|
|
90
|
+
}
|
|
91
|
+
var OSC = "\x1B]";
|
|
92
|
+
var BEL = "\x07";
|
|
93
|
+
var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
|
|
94
|
+
var oscTitle = (title) => `${OSC}0;${title.replace(CONTROL_CHARS, "")}${BEL}`;
|
|
95
|
+
function itermIdentity(name, colour) {
|
|
96
|
+
const channel = (which, value) => `${OSC}6;1;bg;${which};brightness;${value}${BEL}`;
|
|
97
|
+
const badge = Buffer.from(name, "utf8").toString("base64");
|
|
98
|
+
return channel("red", colour.r) + channel("green", colour.g) + channel("blue", colour.b) + `${OSC}1337;SetBadgeFormat=${badge}${BEL}`;
|
|
99
|
+
}
|
|
100
|
+
var isITerm = (env) => env.TERM_PROGRAM === "iTerm.app";
|
|
101
|
+
var NO_PANE_SOURCES = { seats: () => [], colours: () => ({ seats: {}, profiles: {} }) };
|
|
102
|
+
function paneEscapes(plan, env, sources = NO_PANE_SOURCES) {
|
|
103
|
+
if (plan.surface === "headless") return "";
|
|
104
|
+
if (!isITerm(env)) return oscTitle(plan.title);
|
|
105
|
+
const colour = paneColour(plan.title, sources.profile?.(plan), sources.seats(), sources.colours());
|
|
106
|
+
const rgb = parseHex(colour);
|
|
107
|
+
return oscTitle(plan.title) + (rgb === void 0 ? "" : itermIdentity(plan.title, rgb));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/run-agent.ts
|
|
111
|
+
import { spawn } from "child_process";
|
|
112
|
+
import fs2 from "fs";
|
|
113
|
+
var LAUNCHER_PID_ENV = "TITAN_AGENT_LAUNCHER_PID";
|
|
114
|
+
var launchEnv = (planEnv, base, launcherPid = process.pid, unset = [], launcherPidEnv = LAUNCHER_PID_ENV) => {
|
|
115
|
+
const env = { ...base, ...planEnv };
|
|
116
|
+
for (const name of unset) delete env[name];
|
|
117
|
+
return { ...env, [launcherPidEnv]: String(launcherPid) };
|
|
118
|
+
};
|
|
119
|
+
var LaunchBinUnresolved = class extends Error {
|
|
120
|
+
name = "LaunchBinUnresolved";
|
|
121
|
+
};
|
|
122
|
+
var definedEnv = (env) => Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== void 0));
|
|
123
|
+
function readLaunchPlan(file) {
|
|
124
|
+
if (!fs2.existsSync(file)) throw new Error(`no launch plan at ${file}`);
|
|
125
|
+
const plan = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
126
|
+
const valid = typeof plan.agentId === "string" && typeof plan.bin === "string" && Array.isArray(plan.args) && typeof plan.cwd === "string";
|
|
127
|
+
if (!valid) throw new Error(`launch plan at ${file} lacks agentId, bin, args or cwd`);
|
|
128
|
+
return { env: {}, title: plan.agentId ?? "", surface: "headless", ...plan };
|
|
129
|
+
}
|
|
130
|
+
function runAgent(plan, options) {
|
|
131
|
+
process.stdout.write(paneEscapes(plan, process.env, options.paneSources ?? NO_PANE_SOURCES));
|
|
132
|
+
exec(plan, options);
|
|
133
|
+
}
|
|
134
|
+
function resolvedBin(bin, options, label) {
|
|
135
|
+
try {
|
|
136
|
+
return options.resolveBin?.(bin) ?? bin;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (!(err instanceof LaunchBinUnresolved)) throw err;
|
|
139
|
+
process.stderr.write(`${label}: ${err.message}
|
|
140
|
+
`);
|
|
141
|
+
process.exit(127);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
var STDERR_FLUSH_MS = 250;
|
|
145
|
+
function exec(plan, options) {
|
|
146
|
+
const label = options.label ?? "titan-agent-launch";
|
|
147
|
+
clearOutputTail(options.agentDir);
|
|
148
|
+
const bin = resolvedBin(plan.bin, options, label);
|
|
149
|
+
const base = options.baseEnv ?? definedEnv(process.env);
|
|
150
|
+
const child = spawn(bin, plan.args, {
|
|
151
|
+
cwd: plan.cwd,
|
|
152
|
+
// `plan.env` wins over the base deliberately: it is what the SPAWNER chose for this agent.
|
|
153
|
+
env: launchEnv(plan.env, base, process.pid, plan.unsetEnv, options.launcherPidEnv),
|
|
154
|
+
// The brief goes in on stdin for headless; an interactive surface hands the terminal straight through.
|
|
155
|
+
stdio: plan.stdin === void 0 ? "inherit" : ["pipe", "inherit", "pipe"]
|
|
156
|
+
});
|
|
157
|
+
const stderrTail = tailKeeper();
|
|
158
|
+
child.stderr?.on("data", (chunk) => {
|
|
159
|
+
stderrTail.append(chunk.toString("utf8"));
|
|
160
|
+
process.stderr.write(chunk);
|
|
161
|
+
});
|
|
162
|
+
if (plan.stdin !== void 0) writeBrief(child.stdin, plan.stdin, bin, label);
|
|
163
|
+
child.on("error", (err) => {
|
|
164
|
+
process.stderr.write(`${label}: could not start ${bin}: ${err.message}
|
|
165
|
+
`);
|
|
166
|
+
process.exit(127);
|
|
167
|
+
});
|
|
168
|
+
child.on("exit", (code, signal) => {
|
|
169
|
+
const finish = () => {
|
|
170
|
+
writeOutputTail(options.agentDir, stderrTail.text());
|
|
171
|
+
process.exit(signal !== null ? 128 : code ?? 0);
|
|
172
|
+
};
|
|
173
|
+
if (child.stderr === null || child.stderr.readableEnded) return finish();
|
|
174
|
+
child.stderr.once("end", finish);
|
|
175
|
+
setTimeout(finish, STDERR_FLUSH_MS);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function writeBrief(stdin, brief, bin, label) {
|
|
179
|
+
stdin?.on("error", (err) => {
|
|
180
|
+
if (err.code !== "EPIPE") process.stderr.write(`${label}: could not write the brief to ${bin}: ${err.message}
|
|
181
|
+
`);
|
|
182
|
+
});
|
|
183
|
+
stdin?.end(brief);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export {
|
|
187
|
+
LAUNCH_USAGE,
|
|
188
|
+
parseLaunchArgs,
|
|
189
|
+
OUTPUT_TAIL_BYTES,
|
|
190
|
+
outputTailPath,
|
|
191
|
+
tailKeeper,
|
|
192
|
+
writeOutputTail,
|
|
193
|
+
clearOutputTail,
|
|
194
|
+
readOutputTail,
|
|
195
|
+
loginGap,
|
|
196
|
+
PANE_PALETTE,
|
|
197
|
+
isHexColour,
|
|
198
|
+
parseHex,
|
|
199
|
+
hashedColour,
|
|
200
|
+
coordinatorOf,
|
|
201
|
+
paneColour,
|
|
202
|
+
oscTitle,
|
|
203
|
+
itermIdentity,
|
|
204
|
+
isITerm,
|
|
205
|
+
NO_PANE_SOURCES,
|
|
206
|
+
paneEscapes,
|
|
207
|
+
LAUNCHER_PID_ENV,
|
|
208
|
+
launchEnv,
|
|
209
|
+
LaunchBinUnresolved,
|
|
210
|
+
readLaunchPlan,
|
|
211
|
+
runAgent
|
|
212
|
+
};
|
|
213
|
+
//# sourceMappingURL=chunk-5FGP7YKX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/launch-args.ts","../src/launch-output.ts","../src/pane-identity.ts","../src/run-agent.ts"],"sourcesContent":["export const LAUNCH_USAGE = \"usage: titan-agent-launch [--launcher-pid-env NAME] <plan.json>\";\n\nexport interface LaunchArgs {\n planFile: string;\n launcherPidEnv?: string;\n}\n\nconst ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/** The bin's whole command line: one plan file, and optionally the variable that carries the launcher pid. */\nexport function parseLaunchArgs(argv: readonly string[]): LaunchArgs | { error: string } {\n const rest = [...argv];\n let launcherPidEnv: string | undefined;\n if (rest[0] === \"--launcher-pid-env\") {\n launcherPidEnv = rest[1];\n if (launcherPidEnv === undefined || !ENV_NAME.test(launcherPidEnv))\n return { error: `--launcher-pid-env needs a variable name\\n${LAUNCH_USAGE}` };\n rest.splice(0, 2);\n }\n const [planFile, ...extra] = rest;\n if (planFile === undefined || planFile.startsWith(\"-\") || extra.length > 0) return { error: LAUNCH_USAGE };\n return launcherPidEnv === undefined ? { planFile } : { planFile, launcherPidEnv };\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * The tail of a headless claude's stderr, kept where the host can read it.\n *\n * The host cannot hold the pipe (see surfaces/headless.ts), so the launcher that\n * outlives it does: it drains claude's stderr, keeps a bounded tail, and leaves it\n * in the agent's directory when claude exits. The host reads the file only to\n * explain a failed launch.\n */\nexport const OUTPUT_TAIL_BYTES = 4096;\n\nexport const outputTailPath = (agentDir: string): string => path.join(agentDir, \"stderr-tail.txt\");\n\n/** Keeps the last `limit` characters however much is appended. */\nexport function tailKeeper(limit: number = OUTPUT_TAIL_BYTES): {\n append(chunk: string): void;\n text(): string;\n} {\n let kept = \"\";\n return {\n append: chunk => {\n kept = (kept + chunk).slice(-limit);\n },\n text: () => kept,\n };\n}\n\nexport function writeOutputTail(agentDir: string, text: string): void {\n if (text === \"\") return;\n fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 });\n fs.writeFileSync(outputTailPath(agentDir), text, { mode: 0o600 });\n}\n\n/** A resume reuses the agent dir, so a tail from an earlier launch must not outlive it. */\nexport function clearOutputTail(agentDir: string): void {\n fs.rmSync(outputTailPath(agentDir), { force: true });\n}\n\nexport function readOutputTail(agentDir: string): string | undefined {\n try {\n return fs.readFileSync(outputTailPath(agentDir), \"utf8\");\n } catch {\n return undefined;\n }\n}\n\n/** What claude prints when no login exists under its config dir. */\nconst NOT_LOGGED_IN = /not logged in|please run \\/login/i;\n\n/** The login diagnosis when `output` carries claude's not-logged-in signature, else undefined. */\nexport function loginGap(output: string | undefined, configDir: string | undefined): string | undefined {\n if (output === undefined || !NOT_LOGGED_IN.test(output)) return undefined;\n const env = configDir === undefined ? \"CLAUDE_CONFIG_DIR unset\" : `CLAUDE_CONFIG_DIR=${configDir}`;\n return (\n `Claude Code reported it is not logged in. Run \\`claude /login\\` with ${env} ` +\n \"so the login lands in the config dir this agent uses.\"\n );\n}\n","import type { LaunchPlan } from \"./types.js\";\n\n/**\n * What makes an interactive agent's pane recognisable at a glance. The name is\n * the title and the badge; the tab colour says which coordinator the agent\n * belongs to, so every agent of one seat shares the seat's colour.\n *\n * The coordinator is found from the agent's NAME, not its recorded spawner: the\n * launcher reads only the launch plan, and a seat keys its agents by name prefix.\n */\n\n/** A seat's name and the prefix its agents' names start with. */\nexport interface SeatPrefix {\n name: string;\n prefix: string;\n}\n\n/** A `#rrggbb` per seat and per profile, both optional. */\nexport interface PaneColourConfig {\n seats: Record<string, string>;\n profiles: Record<string, string>;\n}\n\nexport interface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\n/** Mid-brightness hues that stay apart from each other on a dark or a light tab bar. */\nexport const PANE_PALETTE = [\n \"#d9534f\",\n \"#e8833a\",\n \"#d4b200\",\n \"#6aa84f\",\n \"#2e9e8f\",\n \"#3d85c6\",\n \"#674ea7\",\n \"#c2478f\",\n \"#8e6c3a\",\n \"#5b8c2a\",\n \"#1f6f8b\",\n \"#a64d79\",\n] as const;\n\nconst HEX = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;\n\nexport const isHexColour = (value: unknown): value is string => typeof value === \"string\" && HEX.test(value);\n\nexport function parseHex(hex: string): Rgb | undefined {\n const m = HEX.exec(hex);\n if (m === null) return undefined;\n return { r: parseInt(m[1] ?? \"\", 16), g: parseInt(m[2] ?? \"\", 16), b: parseInt(m[3] ?? \"\", 16) };\n}\n\n/** FNV-1a, so a key keeps its colour across processes, machines and Node versions. */\nfunction fnv1a(text: string): number {\n let hash = 0x811c9dc5;\n for (const byte of Buffer.from(text, \"utf8\")) hash = Math.imul(hash ^ byte, 0x01000193) >>> 0;\n return hash;\n}\n\nexport const hashedColour = (key: string): string => PANE_PALETTE[fnv1a(key) % PANE_PALETTE.length] ?? \"#3d85c6\";\n\n/**\n * The coordinator an agent belongs to: the seat it is, or whose prefix its name\n * carries. With no seat to match, the name's first segment stands in for one.\n */\nexport function coordinatorOf(name: string, seats: readonly SeatPrefix[]): string {\n const seat = seats.find(s => s.name === name) ?? seats.find(s => name.startsWith(`${s.prefix}-`));\n return seat?.name ?? name.split(\"-\")[0] ?? name;\n}\n\n/** A seat's colour beats a profile's: one coordinator's agents share a colour whatever their profile. */\nexport function paneColour(\n name: string,\n profile: string | undefined,\n seats: readonly SeatPrefix[],\n config: PaneColourConfig,\n): string {\n const coordinator = coordinatorOf(name, seats);\n return (\n config.seats[coordinator] ??\n (profile === undefined ? undefined : config.profiles[profile]) ??\n hashedColour(coordinator)\n );\n}\n\nconst OSC = \"\\x1b]\";\nconst BEL = \"\\x07\";\n\n// eslint-disable-next-line no-control-regex -- matching control characters is the point\nconst CONTROL_CHARS = /[\\x00-\\x1f\\x7f-\\x9f]/g;\n\n/** Control characters are stripped so a name can never end the title early and start another sequence. */\nexport const oscTitle = (title: string): string => `${OSC}0;${title.replace(CONTROL_CHARS, \"\")}${BEL}`;\n\n/** iTerm's proprietary tab colour (OSC 6) and badge (OSC 1337 SetBadgeFormat, base64 of the text). */\nexport function itermIdentity(name: string, colour: Rgb): string {\n const channel = (which: string, value: number): string => `${OSC}6;1;bg;${which};brightness;${value}${BEL}`;\n const badge = Buffer.from(name, \"utf8\").toString(\"base64\");\n return (\n channel(\"red\", colour.r) +\n channel(\"green\", colour.g) +\n channel(\"blue\", colour.b) +\n `${OSC}1337;SetBadgeFormat=${badge}${BEL}`\n );\n}\n\nexport const isITerm = (env: NodeJS.ProcessEnv): boolean => env.TERM_PROGRAM === \"iTerm.app\";\n\n/** Where the pane's colour comes from: host inputs, so this package reads no charter or config. */\nexport interface PaneSources {\n seats: () => SeatPrefix[];\n colours: () => PaneColourConfig;\n /** The profile a plan was launched under, for the profile colour; absent means none. */\n profile?: (plan: LaunchPlan) => string | undefined;\n}\n\nexport const NO_PANE_SOURCES: PaneSources = { seats: () => [], colours: () => ({ seats: {}, profiles: {} }) };\n\n/**\n * Title via OSC 0 rather than iTerm's `set name`, which does not stick: iTerm\n * overwrites it with the running job. In iTerm the tab colour and badge follow;\n * any other terminal gets the title alone, and a headless agent nothing.\n */\nexport function paneEscapes(plan: LaunchPlan, env: NodeJS.ProcessEnv, sources: PaneSources = NO_PANE_SOURCES): string {\n if (plan.surface === \"headless\") return \"\";\n if (!isITerm(env)) return oscTitle(plan.title);\n const colour = paneColour(plan.title, sources.profile?.(plan), sources.seats(), sources.colours());\n const rgb = parseHex(colour);\n return oscTitle(plan.title) + (rgb === undefined ? \"\" : itermIdentity(plan.title, rgb));\n}\n","import { spawn } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport { clearOutputTail, tailKeeper, writeOutputTail } from \"./launch-output.js\";\nimport { NO_PANE_SOURCES, paneEscapes, type PaneSources } from \"./pane-identity.js\";\nimport type { LaunchPlan } from \"./types.js\";\n\n/**\n * The fixed command every surface launches. It reads the plan, sets the terminal\n * title, and runs the binary with an argv ARRAY. No shell is involved at any\n * point, which is what makes it safe for a model-authored brief to be in the\n * plan at all.\n */\n\n/** The default name of the variable that carries the launcher's own pid to the process it launched. */\nexport const LAUNCHER_PID_ENV = \"TITAN_AGENT_LAUNCHER_PID\";\n\n/** The launcher pid comes last so no plan can forge it. */\nexport const launchEnv = (\n planEnv: Record<string, string>,\n base: Record<string, string>,\n launcherPid: number = process.pid,\n unset: readonly string[] = [],\n launcherPidEnv: string = LAUNCHER_PID_ENV,\n): Record<string, string> => {\n const env: Record<string, string> = { ...base, ...planEnv };\n for (const name of unset) delete env[name];\n return { ...env, [launcherPidEnv]: String(launcherPid) };\n};\n\n/** A `bin` the launcher could not turn into something to exec; the launcher exits 127 with this message. */\nexport class LaunchBinUnresolved extends Error {\n override readonly name = \"LaunchBinUnresolved\";\n}\n\nexport interface RunAgentOptions {\n /** The agent's own directory, where the stderr tail is left for the host. */\n agentDir: string;\n /** The env the plan's env is layered over. Defaults to this process's env; a host passes its scrubbed env. */\n baseEnv?: Record<string, string>;\n /** Turns `plan.bin` into what is exec'd, in the process that execs. Throw `LaunchBinUnresolved` to exit 127. */\n resolveBin?: (bin: string) => string;\n launcherPidEnv?: string;\n paneSources?: PaneSources;\n /** Prefix for the launcher's own messages on stderr. */\n label?: string;\n}\n\nconst definedEnv = (env: NodeJS.ProcessEnv): Record<string, string> =>\n Object.fromEntries(Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined));\n\n/** Reads a plan the host wrote; the host owns its shape, so only the fields the launcher execs are checked. */\nexport function readLaunchPlan(file: string): LaunchPlan {\n if (!fs.existsSync(file)) throw new Error(`no launch plan at ${file}`);\n const plan = JSON.parse(fs.readFileSync(file, \"utf8\")) as Partial<LaunchPlan>;\n const valid =\n typeof plan.agentId === \"string\" &&\n typeof plan.bin === \"string\" &&\n Array.isArray(plan.args) &&\n typeof plan.cwd === \"string\";\n if (!valid) throw new Error(`launch plan at ${file} lacks agentId, bin, args or cwd`);\n return { env: {}, title: plan.agentId ?? \"\", surface: \"headless\", ...plan } as LaunchPlan;\n}\n\n/** Writes the pane escapes and execs the plan; this process exits the way the launched one did. */\nexport function runAgent(plan: LaunchPlan, options: RunAgentOptions): void {\n process.stdout.write(paneEscapes(plan, process.env, options.paneSources ?? NO_PANE_SOURCES));\n exec(plan, options);\n}\n\nfunction resolvedBin(bin: string, options: RunAgentOptions, label: string): string {\n try {\n return options.resolveBin?.(bin) ?? bin;\n } catch (err) {\n if (!(err instanceof LaunchBinUnresolved)) throw err;\n process.stderr.write(`${label}: ${err.message}\\n`);\n process.exit(127);\n }\n}\n\n/** Longest the launcher waits for stderr to drain after the child exits; a grandchild holding fd 2 must not stall it. */\nconst STDERR_FLUSH_MS = 250;\n\nfunction exec(plan: LaunchPlan, options: RunAgentOptions): void {\n const label = options.label ?? \"titan-agent-launch\";\n clearOutputTail(options.agentDir);\n const bin = resolvedBin(plan.bin, options, label);\n const base = options.baseEnv ?? definedEnv(process.env);\n const child = spawn(bin, plan.args, {\n cwd: plan.cwd,\n // `plan.env` wins over the base deliberately: it is what the SPAWNER chose for this agent.\n env: launchEnv(plan.env, base, process.pid, plan.unsetEnv, options.launcherPidEnv),\n // The brief goes in on stdin for headless; an interactive surface hands the terminal straight through.\n stdio: plan.stdin === undefined ? \"inherit\" : [\"pipe\", \"inherit\", \"pipe\"],\n });\n const stderrTail = tailKeeper();\n // Headless only. Drained and passed on so the pipe never fills; the tail is what the host reads.\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n stderrTail.append(chunk.toString(\"utf8\"));\n process.stderr.write(chunk);\n });\n if (plan.stdin !== undefined) writeBrief(child.stdin, plan.stdin, bin, label);\n\n child.on(\"error\", err => {\n process.stderr.write(`${label}: could not start ${bin}: ${err.message}\\n`);\n process.exit(127);\n });\n // Exit the way the child did, so whatever watches the surface sees the agent's outcome, not the launcher's.\n child.on(\"exit\", (code, signal) => {\n const finish = (): never => {\n writeOutputTail(options.agentDir, stderrTail.text());\n process.exit(signal !== null ? 128 : (code ?? 0));\n };\n if (child.stderr === null || child.stderr.readableEnded) return finish();\n child.stderr.once(\"end\", finish);\n setTimeout(finish, STDERR_FLUSH_MS);\n });\n}\n\nfunction writeBrief(stdin: NodeJS.WritableStream | null, brief: string, bin: string, label: string): void {\n // A child that exits before reading its brief closes the pipe; its exit code is what matters, not the failed write.\n stdin?.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code !== \"EPIPE\") process.stderr.write(`${label}: could not write the brief to ${bin}: ${err.message}\\n`);\n });\n stdin?.end(brief);\n}\n"],"mappings":"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","names":["fs","fs"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { SpawnOptions } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
declare const SURFACE_NAMES: readonly ["headless", "iterm-pane", "iterm-tab", "iterm-window"];
|
|
4
|
+
type SurfaceName = (typeof SURFACE_NAMES)[number];
|
|
5
|
+
/** The surfaces that put the agent in front of a human who can answer a prompt. */
|
|
6
|
+
declare const isInteractiveSurface: (surface: SurfaceName) => boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Everything the launcher needs to start one agent, written to disk by the spawner.
|
|
9
|
+
*
|
|
10
|
+
* A surface never sees `bin` or `args`: it launches the fixed launcher command,
|
|
11
|
+
* which reads this plan, so a model-authored brief never reaches a command line.
|
|
12
|
+
*/
|
|
13
|
+
interface LaunchPlan {
|
|
14
|
+
agentId: string;
|
|
15
|
+
bin: string;
|
|
16
|
+
args: string[];
|
|
17
|
+
cwd: string;
|
|
18
|
+
env: Record<string, string>;
|
|
19
|
+
/** Variables the launcher deletes from the inherited env, since an empty string is not the same as unset. */
|
|
20
|
+
unsetEnv?: string[];
|
|
21
|
+
/** Headless only: the prompt goes on stdin, never in argv. */
|
|
22
|
+
stdin?: string;
|
|
23
|
+
title: string;
|
|
24
|
+
surface: SurfaceName;
|
|
25
|
+
}
|
|
26
|
+
interface LaunchHandle {
|
|
27
|
+
surface: SurfaceName;
|
|
28
|
+
/** Headless only. */
|
|
29
|
+
pid?: number;
|
|
30
|
+
/** iTerm session UUID. */
|
|
31
|
+
paneRef?: string;
|
|
32
|
+
/**
|
|
33
|
+
* This launch OPENED the surface, so its owner may also close it.
|
|
34
|
+
*
|
|
35
|
+
* Absent for a pane the launch only wrote INTO: an anchor belongs to whoever
|
|
36
|
+
* was already sitting in it. Only a split, tab or window this launch created
|
|
37
|
+
* sets it.
|
|
38
|
+
*/
|
|
39
|
+
ownsSurface?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Resolves when the process ends. Headless ONLY: the host does not own an
|
|
42
|
+
* iTerm pane's process, so a visible agent's exit has to be inferred from
|
|
43
|
+
* presence instead, and can never carry an exit code.
|
|
44
|
+
*/
|
|
45
|
+
exited?: Promise<{
|
|
46
|
+
code: number | null;
|
|
47
|
+
signal: string | null;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Visible only, for a pane the launch opened or reused: resolves with a reason
|
|
51
|
+
* once the surface has seen the launch fail before the agent could start, and
|
|
52
|
+
* never resolves otherwise. Like `exited`, it cannot be persisted.
|
|
53
|
+
*/
|
|
54
|
+
launchFailed?: Promise<string>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The outcome of a teardown. `closed` means the surface went back and looked,
|
|
58
|
+
* since "the close script ran" and "the pane is gone" have diverged in practice;
|
|
59
|
+
* `reason` is what it saw when the answer is no.
|
|
60
|
+
*/
|
|
61
|
+
interface CloseOutcome {
|
|
62
|
+
closed: boolean;
|
|
63
|
+
reason?: string;
|
|
64
|
+
}
|
|
65
|
+
interface Surface {
|
|
66
|
+
readonly name: SurfaceName;
|
|
67
|
+
readonly interactive: boolean;
|
|
68
|
+
launch(plan: LaunchPlan): Promise<LaunchHandle>;
|
|
69
|
+
/**
|
|
70
|
+
* Tear down the surface this handle was launched on, and report whether it is
|
|
71
|
+
* actually gone. A no-op unless `ownsSurface` is set; the check lives here, in
|
|
72
|
+
* the only layer that knows what a pane is, so no caller can skip it.
|
|
73
|
+
*/
|
|
74
|
+
close(handle: LaunchHandle): Promise<CloseOutcome>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The one command every surface launches, supplied by the host as a `Launcher`.
|
|
79
|
+
*
|
|
80
|
+
* A surface never sees `plan.bin` or `plan.args`. The plan is already on disk and
|
|
81
|
+
* the launcher is what reads it, which keeps a model-authored brief out of every
|
|
82
|
+
* command line: AppleScript only ever carries a fixed string with an agent id in it.
|
|
83
|
+
*/
|
|
84
|
+
interface Launcher {
|
|
85
|
+
/** The full argv that starts one agent; `argv[0]` is the executable. */
|
|
86
|
+
argv(agentId: string): string[];
|
|
87
|
+
/**
|
|
88
|
+
* Variables a pane's shell must be handed explicitly. A pane opens in a fresh
|
|
89
|
+
* login shell with the user's environment, not the host's, so a relocated
|
|
90
|
+
* state directory is invisible to it unless carried here.
|
|
91
|
+
*/
|
|
92
|
+
env?: Record<string, string>;
|
|
93
|
+
/** Where the in-place relaunch script for this agent lives; only a reused pane types it. */
|
|
94
|
+
relaunchPath(agentId: string): string;
|
|
95
|
+
}
|
|
96
|
+
/** Single-quoted for /bin/sh: paths here are ours, but they can still hold spaces. */
|
|
97
|
+
declare const shellQuote: (word: string) => string;
|
|
98
|
+
/** For surfaces that hand a shell a line to type, rather than spawning argv directly. */
|
|
99
|
+
declare const launchCommand: (launcher: Launcher, agentId: string) => string;
|
|
100
|
+
/**
|
|
101
|
+
* For a pane the host opens: the command is given to iTerm at creation, so it is
|
|
102
|
+
* never typed into a shell whose input the human can reach.
|
|
103
|
+
*
|
|
104
|
+
* `zsh -lic` for the environment a typed command used to get: `-l` for PATH from
|
|
105
|
+
* .zprofile, `-i` for whatever .zshrc exports. `exec /bin/zsh -l` afterwards
|
|
106
|
+
* keeps the pane, and whatever the launcher printed, open after it exits or crashes.
|
|
107
|
+
*/
|
|
108
|
+
declare const paneCommand: (launcher: Launcher, agentId: string) => string;
|
|
109
|
+
/**
|
|
110
|
+
* The only thing still typed into a shell: a reused pane has no creation
|
|
111
|
+
* `command` to take, so it gets this short fixed path instead of the full
|
|
112
|
+
* command line. Keys that join it in front make it a different, failing command;
|
|
113
|
+
* keys typed between it and the newline become arguments, which a correct
|
|
114
|
+
* invocation never has.
|
|
115
|
+
*/
|
|
116
|
+
declare const relaunchCommand: (launcher: Launcher, agentId: string) => string;
|
|
117
|
+
/** The script `relaunchCommand` runs; the host writes it to `launcher.relaunchPath(agentId)`. */
|
|
118
|
+
declare const relaunchScript: (launcher: Launcher, agentId: string) => string;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Did the launcher actually start in the pane just opened?
|
|
122
|
+
*
|
|
123
|
+
* The host does not own a pane's process, so without this a launch that died at
|
|
124
|
+
* the shell (`zsh: command not found: s`, exit 127, after keystrokes joined the
|
|
125
|
+
* typed command) read as "still starting" until the attach window ran out. A
|
|
126
|
+
* pane's tty is the one handle that reaches its processes, and `ps -t` answers
|
|
127
|
+
* from it.
|
|
128
|
+
*
|
|
129
|
+
* Only a positive answer of absence fails a launch. A tty that never appears, a
|
|
130
|
+
* `ps` that errors, or a pane that has closed all mean "cannot say", and the
|
|
131
|
+
* host's ordinary attach window keeps deciding those.
|
|
132
|
+
*/
|
|
133
|
+
/** Command lines of the processes on a tty. Rejects when it cannot tell. */
|
|
134
|
+
type ProcessProbe = (tty: string) => Promise<string[]>;
|
|
135
|
+
/** What a surface can read back off the pane it opened. */
|
|
136
|
+
interface PaneReader {
|
|
137
|
+
/** The pane's tty, `''` while it has none yet, undefined once the pane is gone. */
|
|
138
|
+
tty: () => Promise<string | undefined>;
|
|
139
|
+
contents: () => Promise<string>;
|
|
140
|
+
}
|
|
141
|
+
interface LaunchCheckTiming {
|
|
142
|
+
deadlineMs: number;
|
|
143
|
+
pollMs: number;
|
|
144
|
+
}
|
|
145
|
+
declare const LAUNCH_CHECK_TIMING: LaunchCheckTiming;
|
|
146
|
+
/** `-ww` because ps otherwise cuts the line at 80 columns, before the agent id. */
|
|
147
|
+
declare const psProbe: ProcessProbe;
|
|
148
|
+
/**
|
|
149
|
+
* The last two words of the launcher's argv, e.g. a verb and the agent id. The
|
|
150
|
+
* launcher process carries them as two bare words; a shell wrapper's -c string
|
|
151
|
+
* quotes them, so it never matches.
|
|
152
|
+
*/
|
|
153
|
+
type LaunchMarker = readonly [string, string];
|
|
154
|
+
/**
|
|
155
|
+
* Resolves with a failure reason when the launch is known to have failed, and
|
|
156
|
+
* never resolves otherwise, which is the shape a host's attach wait races.
|
|
157
|
+
*/
|
|
158
|
+
declare function watchLaunch(pane: PaneReader, probe: ProcessProbe, marker: LaunchMarker, timing?: LaunchCheckTiming): Promise<string>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Runs one AppleScript and resolves with its trimmed output. Injected so tests
|
|
162
|
+
* need no macOS.
|
|
163
|
+
*
|
|
164
|
+
* Async because a host serving many sessions is one event loop. A synchronous
|
|
165
|
+
* `osascript` stopped it dead for as long as iTerm2 took to answer, measured at
|
|
166
|
+
* five seconds during a burst of spawns.
|
|
167
|
+
*/
|
|
168
|
+
type AppleScriptRunner = (script: string) => Promise<string>;
|
|
169
|
+
/**
|
|
170
|
+
* The slice of `child_process.spawn` a surface uses. Injected for the same reason.
|
|
171
|
+
*
|
|
172
|
+
* `once` is here because a host infers a headless agent's exit from the child
|
|
173
|
+
* itself; a fake that omitted it would let a test pass while the real exit path
|
|
174
|
+
* was never wired.
|
|
175
|
+
*/
|
|
176
|
+
interface SpawnedChild {
|
|
177
|
+
pid?: number | undefined;
|
|
178
|
+
unref: () => void;
|
|
179
|
+
once: (event: string, listener: (...args: any[]) => void) => unknown;
|
|
180
|
+
}
|
|
181
|
+
type SpawnFn = (bin: string, args: string[], options: SpawnOptions) => SpawnedChild;
|
|
182
|
+
interface SurfaceOptions {
|
|
183
|
+
/**
|
|
184
|
+
* The requester's `ITERM_SESSION_ID`. Absent means "no anchor", which is a
|
|
185
|
+
* normal case, not an error: the window surface needs none.
|
|
186
|
+
*/
|
|
187
|
+
anchor?: string;
|
|
188
|
+
/**
|
|
189
|
+
* The pane of the last agent already stacked beside this anchor, if any. A new
|
|
190
|
+
* agent splits THAT rather than the anchor, so the requester's pane is not
|
|
191
|
+
* halved once per spawn.
|
|
192
|
+
*/
|
|
193
|
+
columnAfter?: string;
|
|
194
|
+
/**
|
|
195
|
+
* Put the agent IN the anchor session rather than beside it.
|
|
196
|
+
*
|
|
197
|
+
* Only for a successor taking over a predecessor's own pane, which it has just
|
|
198
|
+
* vacated. Never set for a spawn: an agent must not be able to type into a pane
|
|
199
|
+
* somebody else is working in, and the anchor of a spawn is the requester's
|
|
200
|
+
* live pane.
|
|
201
|
+
*/
|
|
202
|
+
reuseAnchor?: boolean;
|
|
203
|
+
/** Told when a surface silently downgrades, e.g. the anchor pane has closed. */
|
|
204
|
+
onNotice?: (message: string) => void;
|
|
205
|
+
runAppleScript?: AppleScriptRunner;
|
|
206
|
+
/** Lists what runs on a pane's tty, for the launch check. Injected so tests need no `ps`. */
|
|
207
|
+
probeProcesses?: ProcessProbe;
|
|
208
|
+
/** How long, and how often, the launch check looks for the launcher. Shortened in tests. */
|
|
209
|
+
launchCheck?: LaunchCheckTiming;
|
|
210
|
+
spawn?: SpawnFn;
|
|
211
|
+
platform?: NodeJS.Platform;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The surface cannot present the agent at all, as opposed to presenting it
|
|
215
|
+
* somewhere less specific. Distinct from a plain Error so a host can record a
|
|
216
|
+
* refusal rather than treating it as a crash.
|
|
217
|
+
*/
|
|
218
|
+
declare class SurfaceRefused extends Error {
|
|
219
|
+
readonly name = "SurfaceRefused";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
declare function surfaceFor(name: SurfaceName, launcher: Launcher, options?: SurfaceOptions): Surface;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Exported so the regression test can spawn a real, loud process through the very
|
|
226
|
+
* same stdio arrangement. Asserting the literal in a unit test would only restate
|
|
227
|
+
* it; running a megabyte through it is what proves nothing blocks.
|
|
228
|
+
*/
|
|
229
|
+
declare const HEADLESS_STDIO: readonly ["ignore", "ignore", "ignore"];
|
|
230
|
+
/**
|
|
231
|
+
* No terminal at all: a detached child with nowhere for its output to go.
|
|
232
|
+
* Detached is the load-bearing part: the host is what survives every session,
|
|
233
|
+
* and an agent it spawned must not die with the request.
|
|
234
|
+
*/
|
|
235
|
+
declare function headlessSurface(launcher: Launcher, options?: SurfaceOptions): Surface;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The iTerm2 surfaces, and two lessons that must survive:
|
|
239
|
+
*
|
|
240
|
+
* 1. Target the anchor session by the UUID inside `ITERM_SESSION_ID`, iterating
|
|
241
|
+
* windows/tabs/sessions to find it. NEVER `current window`: that follows user
|
|
242
|
+
* focus, so panes land in whichever window is frontmost when the script runs,
|
|
243
|
+
* which is rarely the one the human was looking at when they asked.
|
|
244
|
+
* 2. Do not title anything here. iTerm's `set name` does not stick (the running
|
|
245
|
+
* job overwrites it); the launcher emits an OSC 0 escape instead.
|
|
246
|
+
*/
|
|
247
|
+
type ItermSurfaceName = Extract<SurfaceName, `iterm-${string}`>;
|
|
248
|
+
/**
|
|
249
|
+
* Whether iTerm2 still holds `uuid`, for callers outside a teardown.
|
|
250
|
+
*
|
|
251
|
+
* Unknown (undefined) is a third answer and not a synonym for either: iTerm2 not
|
|
252
|
+
* running, a non-Mac, or osascript failing all mean this cannot say.
|
|
253
|
+
*/
|
|
254
|
+
declare function itermSessionPresent(uuid: string, options?: SurfaceOptions): Promise<boolean | undefined>;
|
|
255
|
+
declare function itermSurface(name: ItermSurfaceName, launcher: Launcher, options?: SurfaceOptions): Surface;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* What makes an interactive agent's pane recognisable at a glance. The name is
|
|
259
|
+
* the title and the badge; the tab colour says which coordinator the agent
|
|
260
|
+
* belongs to, so every agent of one seat shares the seat's colour.
|
|
261
|
+
*
|
|
262
|
+
* The coordinator is found from the agent's NAME, not its recorded spawner: the
|
|
263
|
+
* launcher reads only the launch plan, and a seat keys its agents by name prefix.
|
|
264
|
+
*/
|
|
265
|
+
/** A seat's name and the prefix its agents' names start with. */
|
|
266
|
+
interface SeatPrefix {
|
|
267
|
+
name: string;
|
|
268
|
+
prefix: string;
|
|
269
|
+
}
|
|
270
|
+
/** A `#rrggbb` per seat and per profile, both optional. */
|
|
271
|
+
interface PaneColourConfig {
|
|
272
|
+
seats: Record<string, string>;
|
|
273
|
+
profiles: Record<string, string>;
|
|
274
|
+
}
|
|
275
|
+
interface Rgb {
|
|
276
|
+
r: number;
|
|
277
|
+
g: number;
|
|
278
|
+
b: number;
|
|
279
|
+
}
|
|
280
|
+
/** Mid-brightness hues that stay apart from each other on a dark or a light tab bar. */
|
|
281
|
+
declare const PANE_PALETTE: readonly ["#d9534f", "#e8833a", "#d4b200", "#6aa84f", "#2e9e8f", "#3d85c6", "#674ea7", "#c2478f", "#8e6c3a", "#5b8c2a", "#1f6f8b", "#a64d79"];
|
|
282
|
+
declare const isHexColour: (value: unknown) => value is string;
|
|
283
|
+
declare function parseHex(hex: string): Rgb | undefined;
|
|
284
|
+
declare const hashedColour: (key: string) => string;
|
|
285
|
+
/**
|
|
286
|
+
* The coordinator an agent belongs to: the seat it is, or whose prefix its name
|
|
287
|
+
* carries. With no seat to match, the name's first segment stands in for one.
|
|
288
|
+
*/
|
|
289
|
+
declare function coordinatorOf(name: string, seats: readonly SeatPrefix[]): string;
|
|
290
|
+
/** A seat's colour beats a profile's: one coordinator's agents share a colour whatever their profile. */
|
|
291
|
+
declare function paneColour(name: string, profile: string | undefined, seats: readonly SeatPrefix[], config: PaneColourConfig): string;
|
|
292
|
+
/** Control characters are stripped so a name can never end the title early and start another sequence. */
|
|
293
|
+
declare const oscTitle: (title: string) => string;
|
|
294
|
+
/** iTerm's proprietary tab colour (OSC 6) and badge (OSC 1337 SetBadgeFormat, base64 of the text). */
|
|
295
|
+
declare function itermIdentity(name: string, colour: Rgb): string;
|
|
296
|
+
declare const isITerm: (env: NodeJS.ProcessEnv) => boolean;
|
|
297
|
+
/** Where the pane's colour comes from: host inputs, so this package reads no charter or config. */
|
|
298
|
+
interface PaneSources {
|
|
299
|
+
seats: () => SeatPrefix[];
|
|
300
|
+
colours: () => PaneColourConfig;
|
|
301
|
+
/** The profile a plan was launched under, for the profile colour; absent means none. */
|
|
302
|
+
profile?: (plan: LaunchPlan) => string | undefined;
|
|
303
|
+
}
|
|
304
|
+
declare const NO_PANE_SOURCES: PaneSources;
|
|
305
|
+
/**
|
|
306
|
+
* Title via OSC 0 rather than iTerm's `set name`, which does not stick: iTerm
|
|
307
|
+
* overwrites it with the running job. In iTerm the tab colour and badge follow;
|
|
308
|
+
* any other terminal gets the title alone, and a headless agent nothing.
|
|
309
|
+
*/
|
|
310
|
+
declare function paneEscapes(plan: LaunchPlan, env: NodeJS.ProcessEnv, sources?: PaneSources): string;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The fixed command every surface launches. It reads the plan, sets the terminal
|
|
314
|
+
* title, and runs the binary with an argv ARRAY. No shell is involved at any
|
|
315
|
+
* point, which is what makes it safe for a model-authored brief to be in the
|
|
316
|
+
* plan at all.
|
|
317
|
+
*/
|
|
318
|
+
/** The default name of the variable that carries the launcher's own pid to the process it launched. */
|
|
319
|
+
declare const LAUNCHER_PID_ENV = "TITAN_AGENT_LAUNCHER_PID";
|
|
320
|
+
/** The launcher pid comes last so no plan can forge it. */
|
|
321
|
+
declare const launchEnv: (planEnv: Record<string, string>, base: Record<string, string>, launcherPid?: number, unset?: readonly string[], launcherPidEnv?: string) => Record<string, string>;
|
|
322
|
+
/** A `bin` the launcher could not turn into something to exec; the launcher exits 127 with this message. */
|
|
323
|
+
declare class LaunchBinUnresolved extends Error {
|
|
324
|
+
readonly name = "LaunchBinUnresolved";
|
|
325
|
+
}
|
|
326
|
+
interface RunAgentOptions {
|
|
327
|
+
/** The agent's own directory, where the stderr tail is left for the host. */
|
|
328
|
+
agentDir: string;
|
|
329
|
+
/** The env the plan's env is layered over. Defaults to this process's env; a host passes its scrubbed env. */
|
|
330
|
+
baseEnv?: Record<string, string>;
|
|
331
|
+
/** Turns `plan.bin` into what is exec'd, in the process that execs. Throw `LaunchBinUnresolved` to exit 127. */
|
|
332
|
+
resolveBin?: (bin: string) => string;
|
|
333
|
+
launcherPidEnv?: string;
|
|
334
|
+
paneSources?: PaneSources;
|
|
335
|
+
/** Prefix for the launcher's own messages on stderr. */
|
|
336
|
+
label?: string;
|
|
337
|
+
}
|
|
338
|
+
/** Reads a plan the host wrote; the host owns its shape, so only the fields the launcher execs are checked. */
|
|
339
|
+
declare function readLaunchPlan(file: string): LaunchPlan;
|
|
340
|
+
/** Writes the pane escapes and execs the plan; this process exits the way the launched one did. */
|
|
341
|
+
declare function runAgent(plan: LaunchPlan, options: RunAgentOptions): void;
|
|
342
|
+
|
|
343
|
+
declare const LAUNCH_USAGE = "usage: titan-agent-launch [--launcher-pid-env NAME] <plan.json>";
|
|
344
|
+
interface LaunchArgs {
|
|
345
|
+
planFile: string;
|
|
346
|
+
launcherPidEnv?: string;
|
|
347
|
+
}
|
|
348
|
+
/** The bin's whole command line: one plan file, and optionally the variable that carries the launcher pid. */
|
|
349
|
+
declare function parseLaunchArgs(argv: readonly string[]): LaunchArgs | {
|
|
350
|
+
error: string;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* The tail of a headless claude's stderr, kept where the host can read it.
|
|
355
|
+
*
|
|
356
|
+
* The host cannot hold the pipe (see surfaces/headless.ts), so the launcher that
|
|
357
|
+
* outlives it does: it drains claude's stderr, keeps a bounded tail, and leaves it
|
|
358
|
+
* in the agent's directory when claude exits. The host reads the file only to
|
|
359
|
+
* explain a failed launch.
|
|
360
|
+
*/
|
|
361
|
+
declare const OUTPUT_TAIL_BYTES = 4096;
|
|
362
|
+
declare const outputTailPath: (agentDir: string) => string;
|
|
363
|
+
/** Keeps the last `limit` characters however much is appended. */
|
|
364
|
+
declare function tailKeeper(limit?: number): {
|
|
365
|
+
append(chunk: string): void;
|
|
366
|
+
text(): string;
|
|
367
|
+
};
|
|
368
|
+
declare function writeOutputTail(agentDir: string, text: string): void;
|
|
369
|
+
/** A resume reuses the agent dir, so a tail from an earlier launch must not outlive it. */
|
|
370
|
+
declare function clearOutputTail(agentDir: string): void;
|
|
371
|
+
declare function readOutputTail(agentDir: string): string | undefined;
|
|
372
|
+
/** The login diagnosis when `output` carries claude's not-logged-in signature, else undefined. */
|
|
373
|
+
declare function loginGap(output: string | undefined, configDir: string | undefined): string | undefined;
|
|
374
|
+
|
|
375
|
+
export { type AppleScriptRunner, type CloseOutcome, HEADLESS_STDIO, type ItermSurfaceName, LAUNCHER_PID_ENV, LAUNCH_CHECK_TIMING, LAUNCH_USAGE, type LaunchArgs, LaunchBinUnresolved, type LaunchCheckTiming, type LaunchHandle, type LaunchMarker, type LaunchPlan, type Launcher, NO_PANE_SOURCES, OUTPUT_TAIL_BYTES, PANE_PALETTE, type PaneColourConfig, type PaneReader, type PaneSources, type ProcessProbe, type Rgb, type RunAgentOptions, SURFACE_NAMES, type SeatPrefix, type SpawnFn, type SpawnedChild, type Surface, type SurfaceName, type SurfaceOptions, SurfaceRefused, clearOutputTail, coordinatorOf, hashedColour, headlessSurface, isHexColour, isITerm, isInteractiveSurface, itermIdentity, itermSessionPresent, itermSurface, launchCommand, launchEnv, loginGap, oscTitle, outputTailPath, paneColour, paneCommand, paneEscapes, parseHex, parseLaunchArgs, psProbe, readLaunchPlan, readOutputTail, relaunchCommand, relaunchScript, runAgent, shellQuote, surfaceFor, tailKeeper, watchLaunch, writeOutputTail };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LAUNCHER_PID_ENV,
|
|
3
|
+
LAUNCH_USAGE,
|
|
4
|
+
LaunchBinUnresolved,
|
|
5
|
+
NO_PANE_SOURCES,
|
|
6
|
+
OUTPUT_TAIL_BYTES,
|
|
7
|
+
PANE_PALETTE,
|
|
8
|
+
clearOutputTail,
|
|
9
|
+
coordinatorOf,
|
|
10
|
+
hashedColour,
|
|
11
|
+
isHexColour,
|
|
12
|
+
isITerm,
|
|
13
|
+
itermIdentity,
|
|
14
|
+
launchEnv,
|
|
15
|
+
loginGap,
|
|
16
|
+
oscTitle,
|
|
17
|
+
outputTailPath,
|
|
18
|
+
paneColour,
|
|
19
|
+
paneEscapes,
|
|
20
|
+
parseHex,
|
|
21
|
+
parseLaunchArgs,
|
|
22
|
+
readLaunchPlan,
|
|
23
|
+
readOutputTail,
|
|
24
|
+
runAgent,
|
|
25
|
+
tailKeeper,
|
|
26
|
+
writeOutputTail
|
|
27
|
+
} from "./chunk-5FGP7YKX.js";
|
|
28
|
+
|
|
29
|
+
// src/types.ts
|
|
30
|
+
var SURFACE_NAMES = ["headless", "iterm-pane", "iterm-tab", "iterm-window"];
|
|
31
|
+
var isInteractiveSurface = (surface) => surface !== "headless";
|
|
32
|
+
|
|
33
|
+
// src/surfaces/headless.ts
|
|
34
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
35
|
+
var HEADLESS_STDIO = ["ignore", "ignore", "ignore"];
|
|
36
|
+
function headlessSurface(launcher, options = {}) {
|
|
37
|
+
return {
|
|
38
|
+
name: "headless",
|
|
39
|
+
interactive: false,
|
|
40
|
+
launch: async (plan) => {
|
|
41
|
+
const spawn = options.spawn ?? nodeSpawn;
|
|
42
|
+
const [bin = process.execPath, ...args] = launcher.argv(plan.agentId);
|
|
43
|
+
const child = spawn(bin, args, {
|
|
44
|
+
detached: true,
|
|
45
|
+
// DISCARDED, and this must stay discarded. Piped streams with no reader let a
|
|
46
|
+
// chatty agent fill the ~64KB kernel buffer, block on write, and wedge forever
|
|
47
|
+
// while still looking healthy. A reader in the host cannot fix it: the child
|
|
48
|
+
// outlives the host by design, so only /dev/null drains it safely. The agent's
|
|
49
|
+
// own transcript is the record.
|
|
50
|
+
stdio: [...HEADLESS_STDIO]
|
|
51
|
+
});
|
|
52
|
+
child.unref();
|
|
53
|
+
const exited = new Promise((resolve) => {
|
|
54
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
55
|
+
child.once("error", () => resolve({ code: null, signal: null }));
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
surface: "headless",
|
|
59
|
+
...child.pid === void 0 ? {} : { pid: child.pid },
|
|
60
|
+
exited
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
/** Nothing to close: having no surface is what headless means. */
|
|
64
|
+
close: async () => ({ closed: false, reason: "headless: no surface to close" })
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/surfaces/iterm.ts
|
|
69
|
+
import { execFile as execFile2 } from "child_process";
|
|
70
|
+
import { promisify as promisify2 } from "util";
|
|
71
|
+
|
|
72
|
+
// src/surfaces/command.ts
|
|
73
|
+
var shellQuote = (word) => `'${word.replaceAll("'", `'\\''`)}'`;
|
|
74
|
+
var envWords = (launcher) => Object.entries(launcher.env ?? {}).map(([name, value]) => `${name}=${shellQuote(value)}`);
|
|
75
|
+
var launchCommand = (launcher, agentId) => [...envWords(launcher), ...launcher.argv(agentId).map(shellQuote)].join(" ");
|
|
76
|
+
var itermWord = (word) => {
|
|
77
|
+
if (/["\\]/.test(word))
|
|
78
|
+
throw new Error(`cannot hand iTerm2 a command containing a double quote or backslash: ${word}`);
|
|
79
|
+
return `"${word}"`;
|
|
80
|
+
};
|
|
81
|
+
var paneCommand = (launcher, agentId) => ["/bin/zsh", "-lic", `${launchCommand(launcher, agentId)}; exec /bin/zsh -l`].map(itermWord).join(" ");
|
|
82
|
+
var relaunchCommand = (launcher, agentId) => shellQuote(launcher.relaunchPath(agentId));
|
|
83
|
+
var relaunchScript = (launcher, agentId) => [
|
|
84
|
+
"#!/bin/sh",
|
|
85
|
+
'if [ "$#" -ne 0 ]; then',
|
|
86
|
+
` echo "not relaunching ${agentId}: typed keys joined the command (extra arguments: $*)" >&2`,
|
|
87
|
+
" exit 64",
|
|
88
|
+
"fi",
|
|
89
|
+
...envWords(launcher).map((word) => `export ${word}`),
|
|
90
|
+
`exec ${launcher.argv(agentId).map(shellQuote).join(" ")}`,
|
|
91
|
+
""
|
|
92
|
+
].join("\n");
|
|
93
|
+
|
|
94
|
+
// src/surfaces/launch-check.ts
|
|
95
|
+
import { execFile } from "child_process";
|
|
96
|
+
import { promisify } from "util";
|
|
97
|
+
var LAUNCH_CHECK_TIMING = { deadlineMs: 5e3, pollMs: 500 };
|
|
98
|
+
var TAIL_LINES = 5;
|
|
99
|
+
var execFileAsync = promisify(execFile);
|
|
100
|
+
var psProbe = async (tty) => {
|
|
101
|
+
try {
|
|
102
|
+
const { stdout } = await execFileAsync("ps", ["-ww", "-t", tty, "-o", "command="], { encoding: "utf8" });
|
|
103
|
+
return stdout.split("\n").filter((line) => line.trim() !== "");
|
|
104
|
+
} catch (err) {
|
|
105
|
+
const failed = err;
|
|
106
|
+
if (failed.code === 1 && failed.stdout === "") return [];
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var runsLauncher = (line, [first, second]) => {
|
|
111
|
+
const words = line.trim().split(/\s+/);
|
|
112
|
+
return words.some((word, i) => word === first && words[i + 1] === second);
|
|
113
|
+
};
|
|
114
|
+
var TTY = /^\/dev\/tty\w+$/;
|
|
115
|
+
async function lookOnce(pane, probe, marker) {
|
|
116
|
+
const tty = await pane.tty();
|
|
117
|
+
if (tty === void 0) return "unknown";
|
|
118
|
+
if (tty === "") return "absent";
|
|
119
|
+
if (!TTY.test(tty)) return "unknown";
|
|
120
|
+
const lines = await probe(tty.slice("/dev/".length));
|
|
121
|
+
return lines.some((line) => runsLauncher(line, marker)) ? "running" : "absent";
|
|
122
|
+
}
|
|
123
|
+
var pause = (ms) => new Promise((resolve) => {
|
|
124
|
+
const timer = setTimeout(resolve, ms);
|
|
125
|
+
timer.unref?.();
|
|
126
|
+
});
|
|
127
|
+
var tail = (contents) => contents.split("\n").map((line) => line.trimEnd()).filter((line) => line !== "").slice(-TAIL_LINES).join("\n");
|
|
128
|
+
async function failureReason(pane, marker, deadlineMs) {
|
|
129
|
+
const said = await pane.contents().then(tail, () => "");
|
|
130
|
+
const within = `${marker.join(" ")} was not running in its pane ${Math.round(deadlineMs / 1e3)}s after launch`;
|
|
131
|
+
return said === "" ? `${within}, and the pane showed nothing.` : `${within}. The pane's last lines:
|
|
132
|
+
${said}`;
|
|
133
|
+
}
|
|
134
|
+
async function check(pane, probe, marker, timing) {
|
|
135
|
+
const attempts = Math.max(1, Math.ceil(timing.deadlineMs / timing.pollMs));
|
|
136
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
137
|
+
await pause(timing.pollMs);
|
|
138
|
+
const look = await lookOnce(pane, probe, marker).catch(() => "unknown");
|
|
139
|
+
if (look !== "absent") return void 0;
|
|
140
|
+
}
|
|
141
|
+
return failureReason(pane, marker, timing.deadlineMs);
|
|
142
|
+
}
|
|
143
|
+
function watchLaunch(pane, probe, marker, timing = LAUNCH_CHECK_TIMING) {
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
void check(pane, probe, marker, timing).then((reason) => {
|
|
146
|
+
if (reason !== void 0) resolve(reason);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/surfaces/options.ts
|
|
152
|
+
var SurfaceRefused = class extends Error {
|
|
153
|
+
name = "SurfaceRefused";
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// src/surfaces/iterm.ts
|
|
157
|
+
var NO_ANCHOR = "@@no-anchor@@";
|
|
158
|
+
var CLOSED = "@@closed@@";
|
|
159
|
+
var PRESENT = "@@present@@";
|
|
160
|
+
var GONE = "@@gone@@";
|
|
161
|
+
var CTRL_U = 21;
|
|
162
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
163
|
+
var osascript = async (script) => (await execFileAsync2("osascript", ["-e", script], { encoding: "utf8" })).stdout.trim();
|
|
164
|
+
var asString = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
165
|
+
var anchorUuid = (anchor) => {
|
|
166
|
+
const uuid = anchor?.slice(anchor.lastIndexOf(":") + 1).trim();
|
|
167
|
+
return uuid === void 0 || uuid === "" ? void 0 : uuid;
|
|
168
|
+
};
|
|
169
|
+
var findSessions = (uuid, columnUuid) => `
|
|
170
|
+
set anchorSession to missing value
|
|
171
|
+
set anchorWindow to missing value
|
|
172
|
+
set columnSession to missing value
|
|
173
|
+
repeat with w in windows
|
|
174
|
+
repeat with t in tabs of w
|
|
175
|
+
repeat with s in sessions of t
|
|
176
|
+
if (unique ID of s) is ${asString(uuid)} then
|
|
177
|
+
set anchorSession to s
|
|
178
|
+
set anchorWindow to w
|
|
179
|
+
end if
|
|
180
|
+
if (unique ID of s) is ${asString(columnUuid)} then
|
|
181
|
+
set columnSession to s
|
|
182
|
+
end if
|
|
183
|
+
end repeat
|
|
184
|
+
end repeat
|
|
185
|
+
end repeat
|
|
186
|
+
if anchorSession is missing value then return ${asString(NO_ANCHOR)}`;
|
|
187
|
+
var rememberFocus = ` set priorWindow to missing value
|
|
188
|
+
try
|
|
189
|
+
set priorWindow to current window
|
|
190
|
+
end try`;
|
|
191
|
+
var restoreFocus = ` if priorWindow is not missing value then
|
|
192
|
+
try
|
|
193
|
+
select priorWindow
|
|
194
|
+
end try
|
|
195
|
+
end if`;
|
|
196
|
+
var beside = (surface, uuid, command, columnUuid = "") => {
|
|
197
|
+
const withCommand = `with default profile command ${asString(command)}`;
|
|
198
|
+
const open = surface === "iterm-pane" ? ` if columnSession is not missing value then
|
|
199
|
+
tell columnSession to set spawned to (split horizontally ${withCommand})
|
|
200
|
+
else
|
|
201
|
+
tell anchorSession to set spawned to (split vertically ${withCommand})
|
|
202
|
+
end if` : ` tell anchorWindow to set spawned to (current session of (create tab ${withCommand}))`;
|
|
203
|
+
return `tell application "iTerm2"${findSessions(uuid, columnUuid)}
|
|
204
|
+
${rememberFocus}
|
|
205
|
+
set anchorTab to current tab of anchorWindow
|
|
206
|
+
set anchorWindowSession to current session of anchorWindow
|
|
207
|
+
${open}
|
|
208
|
+
select anchorTab
|
|
209
|
+
select anchorWindowSession
|
|
210
|
+
${restoreFocus}
|
|
211
|
+
return unique ID of spawned
|
|
212
|
+
end tell`;
|
|
213
|
+
};
|
|
214
|
+
var inPlace = (uuid, command) => `tell application "iTerm2"${findSessions(uuid, "")}
|
|
215
|
+
tell anchorSession to write text ((character id ${CTRL_U}) & ${asString(command)})
|
|
216
|
+
return unique ID of anchorSession
|
|
217
|
+
end tell`;
|
|
218
|
+
var newWindow = (command) => `tell application "iTerm2"
|
|
219
|
+
${rememberFocus}
|
|
220
|
+
set spawned to (current session of (create window with default profile command ${asString(command)}))
|
|
221
|
+
${restoreFocus}
|
|
222
|
+
return unique ID of spawned
|
|
223
|
+
end tell`;
|
|
224
|
+
var closeSession = (uuid) => `tell application "iTerm2"${findSessions(uuid, "")}
|
|
225
|
+
tell anchorSession to close
|
|
226
|
+
return ${asString(CLOSED)}
|
|
227
|
+
end tell`;
|
|
228
|
+
var sessionPresent = (uuid) => `tell application "iTerm2"
|
|
229
|
+
repeat with w in windows
|
|
230
|
+
repeat with t in tabs of w
|
|
231
|
+
repeat with s in sessions of t
|
|
232
|
+
if (unique ID of s) is ${asString(uuid)} then return ${asString(PRESENT)}
|
|
233
|
+
end repeat
|
|
234
|
+
end repeat
|
|
235
|
+
end repeat
|
|
236
|
+
return ${asString(GONE)}
|
|
237
|
+
end tell`;
|
|
238
|
+
async function itermSessionPresent(uuid, options = {}) {
|
|
239
|
+
const run = options.runAppleScript ?? osascript;
|
|
240
|
+
try {
|
|
241
|
+
await requireIterm(options, run);
|
|
242
|
+
return await probeSession(run, uuid);
|
|
243
|
+
} catch {
|
|
244
|
+
return void 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function probeSession(run, uuid) {
|
|
248
|
+
try {
|
|
249
|
+
const answer = await run(sessionPresent(uuid));
|
|
250
|
+
return answer === PRESENT ? true : answer === GONE ? false : void 0;
|
|
251
|
+
} catch {
|
|
252
|
+
return void 0;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
var itermRunning = async (run) => {
|
|
256
|
+
try {
|
|
257
|
+
return await run('application "iTerm2" is running') === "true";
|
|
258
|
+
} catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
async function requireIterm(options, run) {
|
|
263
|
+
const platform = options.platform ?? process.platform;
|
|
264
|
+
if (platform !== "darwin")
|
|
265
|
+
throw new SurfaceRefused(`iTerm2 surfaces need macOS (this is ${platform}); use surface 'headless'`);
|
|
266
|
+
if (!await itermRunning(run)) throw new SurfaceRefused("iTerm2 is not running; use surface 'headless'");
|
|
267
|
+
}
|
|
268
|
+
var sessionField = (uuid, field) => `tell application "iTerm2"
|
|
269
|
+
repeat with w in windows
|
|
270
|
+
repeat with t in tabs of w
|
|
271
|
+
repeat with s in sessions of t
|
|
272
|
+
if (unique ID of s) is ${asString(uuid)} then return ${field} of s
|
|
273
|
+
end repeat
|
|
274
|
+
end repeat
|
|
275
|
+
end repeat
|
|
276
|
+
return ${asString(GONE)}
|
|
277
|
+
end tell`;
|
|
278
|
+
var paneReader = (run, uuid) => ({
|
|
279
|
+
tty: async () => {
|
|
280
|
+
const tty = await run(sessionField(uuid, "tty"));
|
|
281
|
+
return tty === GONE ? void 0 : tty;
|
|
282
|
+
},
|
|
283
|
+
contents: async () => {
|
|
284
|
+
const contents = await run(sessionField(uuid, "contents"));
|
|
285
|
+
return contents === GONE ? "" : contents;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
var launchMarker = (launcher, agentId) => {
|
|
289
|
+
const argv = launcher.argv(agentId);
|
|
290
|
+
return [argv.at(-2) ?? "", argv.at(-1) ?? ""];
|
|
291
|
+
};
|
|
292
|
+
var watched = (handle, launch) => ({
|
|
293
|
+
...handle,
|
|
294
|
+
launchFailed: watchLaunch(
|
|
295
|
+
paneReader(launch.run, handle.paneRef),
|
|
296
|
+
launch.options.probeProcesses ?? psProbe,
|
|
297
|
+
launchMarker(launch.launcher, launch.plan.agentId),
|
|
298
|
+
launch.options.launchCheck
|
|
299
|
+
)
|
|
300
|
+
});
|
|
301
|
+
async function reuse(launch, uuid) {
|
|
302
|
+
const { surface, plan, launcher, options, run } = launch;
|
|
303
|
+
const paneRef = await run(inPlace(uuid, relaunchCommand(launcher, plan.agentId)));
|
|
304
|
+
if (paneRef !== NO_ANCHOR) return watched({ surface, paneRef }, launch);
|
|
305
|
+
options.onNotice?.(`pane ${uuid} is gone; opening an iTerm window rather than reusing it`);
|
|
306
|
+
return void 0;
|
|
307
|
+
}
|
|
308
|
+
async function launchIterm(launch) {
|
|
309
|
+
const { surface, plan, launcher, options, run } = launch;
|
|
310
|
+
await requireIterm(options, run);
|
|
311
|
+
const uuid = anchorUuid(options.anchor);
|
|
312
|
+
const command = paneCommand(launcher, plan.agentId);
|
|
313
|
+
const opened = (at, paneRef) => watched({ surface: at, paneRef, ownsSurface: true }, launch);
|
|
314
|
+
if (options.reuseAnchor && uuid !== void 0) {
|
|
315
|
+
const reused = await reuse(launch, uuid);
|
|
316
|
+
if (reused !== void 0) return reused;
|
|
317
|
+
} else if (surface !== "iterm-window" && uuid !== void 0) {
|
|
318
|
+
const paneRef = await run(beside(surface, uuid, command, options.columnAfter));
|
|
319
|
+
if (paneRef !== NO_ANCHOR) return opened(surface, paneRef);
|
|
320
|
+
options.onNotice?.(`anchor session ${uuid} is gone; opening an iTerm window instead of ${surface}`);
|
|
321
|
+
}
|
|
322
|
+
return opened("iterm-window", await run(newWindow(command)));
|
|
323
|
+
}
|
|
324
|
+
async function closeIterm(handle, options) {
|
|
325
|
+
if (handle.ownsSurface !== true)
|
|
326
|
+
return { closed: false, reason: "the host did not open this surface, so it is not ours to close" };
|
|
327
|
+
if (handle.paneRef === void 0) return { closed: false, reason: "no pane was recorded for this agent" };
|
|
328
|
+
const run = options.runAppleScript ?? osascript;
|
|
329
|
+
const paneRef = handle.paneRef;
|
|
330
|
+
try {
|
|
331
|
+
await requireIterm(options, run);
|
|
332
|
+
const answer = await run(closeSession(paneRef));
|
|
333
|
+
if (answer !== CLOSED) return { closed: false, reason: `iTerm2 no longer lists session ${paneRef}` };
|
|
334
|
+
const present = await probeSession(run, paneRef);
|
|
335
|
+
if (present === true) return { closed: false, reason: `iTerm2 still lists session ${paneRef} after closing it` };
|
|
336
|
+
if (present === void 0)
|
|
337
|
+
return { closed: false, reason: `could not re-read iTerm2 to confirm session ${paneRef} is gone` };
|
|
338
|
+
return { closed: true };
|
|
339
|
+
} catch (err) {
|
|
340
|
+
return { closed: false, reason: `iTerm2 could not be reached: ${err.message}` };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function itermSurface(name, launcher, options = {}) {
|
|
344
|
+
return {
|
|
345
|
+
name,
|
|
346
|
+
interactive: true,
|
|
347
|
+
launch: (plan) => launchIterm({ surface: name, plan, launcher, options, run: options.runAppleScript ?? osascript }),
|
|
348
|
+
close: (handle) => closeIterm(handle, options)
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/surfaces/index.ts
|
|
353
|
+
var isIterm = (name) => name !== "headless";
|
|
354
|
+
function surfaceFor(name, launcher, options = {}) {
|
|
355
|
+
return isIterm(name) ? itermSurface(name, launcher, options) : headlessSurface(launcher, options);
|
|
356
|
+
}
|
|
357
|
+
export {
|
|
358
|
+
HEADLESS_STDIO,
|
|
359
|
+
LAUNCHER_PID_ENV,
|
|
360
|
+
LAUNCH_CHECK_TIMING,
|
|
361
|
+
LAUNCH_USAGE,
|
|
362
|
+
LaunchBinUnresolved,
|
|
363
|
+
NO_PANE_SOURCES,
|
|
364
|
+
OUTPUT_TAIL_BYTES,
|
|
365
|
+
PANE_PALETTE,
|
|
366
|
+
SURFACE_NAMES,
|
|
367
|
+
SurfaceRefused,
|
|
368
|
+
clearOutputTail,
|
|
369
|
+
coordinatorOf,
|
|
370
|
+
hashedColour,
|
|
371
|
+
headlessSurface,
|
|
372
|
+
isHexColour,
|
|
373
|
+
isITerm,
|
|
374
|
+
isInteractiveSurface,
|
|
375
|
+
itermIdentity,
|
|
376
|
+
itermSessionPresent,
|
|
377
|
+
itermSurface,
|
|
378
|
+
launchCommand,
|
|
379
|
+
launchEnv,
|
|
380
|
+
loginGap,
|
|
381
|
+
oscTitle,
|
|
382
|
+
outputTailPath,
|
|
383
|
+
paneColour,
|
|
384
|
+
paneCommand,
|
|
385
|
+
paneEscapes,
|
|
386
|
+
parseHex,
|
|
387
|
+
parseLaunchArgs,
|
|
388
|
+
psProbe,
|
|
389
|
+
readLaunchPlan,
|
|
390
|
+
readOutputTail,
|
|
391
|
+
relaunchCommand,
|
|
392
|
+
relaunchScript,
|
|
393
|
+
runAgent,
|
|
394
|
+
shellQuote,
|
|
395
|
+
surfaceFor,
|
|
396
|
+
tailKeeper,
|
|
397
|
+
watchLaunch,
|
|
398
|
+
writeOutputTail
|
|
399
|
+
};
|
|
400
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/surfaces/headless.ts","../src/surfaces/iterm.ts","../src/surfaces/command.ts","../src/surfaces/launch-check.ts","../src/surfaces/options.ts","../src/surfaces/index.ts"],"sourcesContent":["export const SURFACE_NAMES = [\"headless\", \"iterm-pane\", \"iterm-tab\", \"iterm-window\"] as const;\n\nexport type SurfaceName = (typeof SURFACE_NAMES)[number];\n\n/** The surfaces that put the agent in front of a human who can answer a prompt. */\nexport const isInteractiveSurface = (surface: SurfaceName): boolean => surface !== \"headless\";\n\n/**\n * Everything the launcher needs to start one agent, written to disk by the spawner.\n *\n * A surface never sees `bin` or `args`: it launches the fixed launcher command,\n * which reads this plan, so a model-authored brief never reaches a command line.\n */\nexport interface LaunchPlan {\n agentId: string;\n bin: string;\n args: string[];\n cwd: string;\n env: Record<string, string>;\n /** Variables the launcher deletes from the inherited env, since an empty string is not the same as unset. */\n unsetEnv?: string[];\n /** Headless only: the prompt goes on stdin, never in argv. */\n stdin?: string;\n title: string;\n surface: SurfaceName;\n}\n\nexport interface LaunchHandle {\n surface: SurfaceName;\n /** Headless only. */\n pid?: number;\n /** iTerm session UUID. */\n paneRef?: string;\n /**\n * This launch OPENED the surface, so its owner may also close it.\n *\n * Absent for a pane the launch only wrote INTO: an anchor belongs to whoever\n * was already sitting in it. Only a split, tab or window this launch created\n * sets it.\n */\n ownsSurface?: boolean;\n /**\n * Resolves when the process ends. Headless ONLY: the host does not own an\n * iTerm pane's process, so a visible agent's exit has to be inferred from\n * presence instead, and can never carry an exit code.\n */\n exited?: Promise<{ code: number | null; signal: string | null }>;\n /**\n * Visible only, for a pane the launch opened or reused: resolves with a reason\n * once the surface has seen the launch fail before the agent could start, and\n * never resolves otherwise. Like `exited`, it cannot be persisted.\n */\n launchFailed?: Promise<string>;\n}\n\n/**\n * The outcome of a teardown. `closed` means the surface went back and looked,\n * since \"the close script ran\" and \"the pane is gone\" have diverged in practice;\n * `reason` is what it saw when the answer is no.\n */\nexport interface CloseOutcome {\n closed: boolean;\n reason?: string;\n}\n\nexport interface Surface {\n readonly name: SurfaceName;\n readonly interactive: boolean;\n launch(plan: LaunchPlan): Promise<LaunchHandle>;\n /**\n * Tear down the surface this handle was launched on, and report whether it is\n * actually gone. A no-op unless `ownsSurface` is set; the check lives here, in\n * the only layer that knows what a pane is, so no caller can skip it.\n */\n close(handle: LaunchHandle): Promise<CloseOutcome>;\n}\n","import { spawn as nodeSpawn } from \"node:child_process\";\nimport type { CloseOutcome, LaunchHandle, LaunchPlan, Surface } from \"../types.js\";\nimport type { Launcher } from \"./command.js\";\nimport type { SurfaceOptions } from \"./options.js\";\n\n/**\n * Exported so the regression test can spawn a real, loud process through the very\n * same stdio arrangement. Asserting the literal in a unit test would only restate\n * it; running a megabyte through it is what proves nothing blocks.\n */\nexport const HEADLESS_STDIO = [\"ignore\", \"ignore\", \"ignore\"] as const;\n\n/**\n * No terminal at all: a detached child with nowhere for its output to go.\n * Detached is the load-bearing part: the host is what survives every session,\n * and an agent it spawned must not die with the request.\n */\nexport function headlessSurface(launcher: Launcher, options: SurfaceOptions = {}): Surface {\n return {\n name: \"headless\",\n interactive: false,\n launch: async (plan: LaunchPlan): Promise<LaunchHandle> => {\n const spawn = options.spawn ?? nodeSpawn;\n const [bin = process.execPath, ...args] = launcher.argv(plan.agentId);\n const child = spawn(bin, args, {\n detached: true,\n // DISCARDED, and this must stay discarded. Piped streams with no reader let a\n // chatty agent fill the ~64KB kernel buffer, block on write, and wedge forever\n // while still looking healthy. A reader in the host cannot fix it: the child\n // outlives the host by design, so only /dev/null drains it safely. The agent's\n // own transcript is the record.\n stdio: [...HEADLESS_STDIO],\n });\n child.unref();\n\n // Settled rather than pending, so nothing here keeps the host's event loop alive.\n const exited = new Promise<{ code: number | null; signal: string | null }>(resolve => {\n child.once(\"exit\", (code: number | null, signal: string | null) => resolve({ code, signal }));\n child.once(\"error\", () => resolve({ code: null, signal: null }));\n });\n\n return {\n surface: \"headless\",\n ...(child.pid === undefined ? {} : { pid: child.pid }),\n exited,\n };\n },\n /** Nothing to close: having no surface is what headless means. */\n close: async (): Promise<CloseOutcome> => ({ closed: false, reason: \"headless: no surface to close\" }),\n };\n}\n","import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport type { CloseOutcome, LaunchHandle, LaunchPlan, Surface, SurfaceName } from \"../types.js\";\nimport { paneCommand, relaunchCommand, type Launcher } from \"./command.js\";\nimport { psProbe, watchLaunch, type LaunchMarker, type PaneReader } from \"./launch-check.js\";\nimport { SurfaceRefused, type AppleScriptRunner, type SurfaceOptions } from \"./options.js\";\n\n/**\n * The iTerm2 surfaces, and two lessons that must survive:\n *\n * 1. Target the anchor session by the UUID inside `ITERM_SESSION_ID`, iterating\n * windows/tabs/sessions to find it. NEVER `current window`: that follows user\n * focus, so panes land in whichever window is frontmost when the script runs,\n * which is rarely the one the human was looking at when they asked.\n * 2. Do not title anything here. iTerm's `set name` does not stick (the running\n * job overwrites it); the launcher emits an OSC 0 escape instead.\n */\n\nexport type ItermSurfaceName = Extract<SurfaceName, `iterm-${string}`>;\n\n/** Returned by the search scripts when the anchor session no longer exists. */\nconst NO_ANCHOR = \"@@no-anchor@@\";\n\n/** Returned by the teardown script when it found the session and closed it. */\nconst CLOSED = \"@@closed@@\";\n\n/** Returned by the existence probe. Two sentinels, so a throw can never read as \"gone\". */\nconst PRESENT = \"@@present@@\";\nconst GONE = \"@@gone@@\";\n\n/** Kill-line in zsh and bash, for a prompt that already holds stray keys. */\nconst CTRL_U = 21;\n\nconst execFileAsync = promisify(execFile);\n\nconst osascript: AppleScriptRunner = async script =>\n (await execFileAsync(\"osascript\", [\"-e\", script], { encoding: \"utf8\" })).stdout.trim();\n\nconst asString = (value: string): string => `\"${value.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll('\"', '\\\\\"')}\"`;\n\n/** `ITERM_SESSION_ID` is `w0t1p2:UUID`; only the UUID identifies a session. */\nconst anchorUuid = (anchor: string | undefined): string | undefined => {\n const uuid = anchor?.slice(anchor.lastIndexOf(\":\") + 1).trim();\n return uuid === undefined || uuid === \"\" ? undefined : uuid;\n};\n\n/**\n * One pass for both sessions we might target. `columnUuid` is empty when there is\n * no column yet, and no session's unique ID is ever the empty string, so the\n * lookup simply finds nothing, which is also the correct answer when the column\n * pane existed and has since been closed.\n */\nconst findSessions = (uuid: string, columnUuid: string): string => `\n set anchorSession to missing value\n set anchorWindow to missing value\n set columnSession to missing value\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if (unique ID of s) is ${asString(uuid)} then\n set anchorSession to s\n set anchorWindow to w\n end if\n if (unique ID of s) is ${asString(columnUuid)} then\n set columnSession to s\n end if\n end repeat\n end repeat\n end repeat\n if anchorSession is missing value then return ${asString(NO_ANCHOR)}`;\n\n/**\n * Where the human's keystrokes were going, read before a surface opens and put\n * back after it. Read only, never used to place anything.\n *\n * `create tab` makes its window key and `create window` activates iTerm2, so\n * without this the next keystroke lands in the agent's pane. `select` on a\n * window is `makeKeyAndOrderFront`, which does not activate iTerm2 when some\n * other app is in front.\n */\nconst rememberFocus = ` set priorWindow to missing value\n try\n set priorWindow to current window\n end try`;\n\nconst restoreFocus = ` if priorWindow is not missing value then\n try\n select priorWindow\n end try\n end if`;\n\n/**\n * Agents stack in a column beside the anchor, rather than each one splitting the\n * anchor again, which halved the requester's pane on every spawn. The first agent\n * splits the anchor VERTICALLY; each later agent splits the previous AGENT pane\n * HORIZONTALLY, so the column subdivides and the anchor keeps its width.\n *\n * Falling back to the vertical split when the column session is gone is what\n * makes a closed agent pane self-healing: the next spawn starts a fresh column.\n * There is no depth parameter: the anchor is always the requester's own pane, so\n * an agent's own spawns land one column further right for free.\n *\n * The anchor window's tab and session are re-selected afterwards because a\n * split selects the anchor's tab and a new tab selects itself.\n */\nconst beside = (surface: ItermSurfaceName, uuid: string, command: string, columnUuid: string = \"\"): string => {\n const withCommand = `with default profile command ${asString(command)}`;\n const open =\n surface === \"iterm-pane\"\n ? ` if columnSession is not missing value then\n tell columnSession to set spawned to (split horizontally ${withCommand})\n else\n tell anchorSession to set spawned to (split vertically ${withCommand})\n end if`\n : ` tell anchorWindow to set spawned to (current session of (create tab ${withCommand}))`;\n return `tell application \"iTerm2\"${findSessions(uuid, columnUuid)}\n${rememberFocus}\n set anchorTab to current tab of anchorWindow\n set anchorWindowSession to current session of anchorWindow\n${open}\n select anchorTab\n select anchorWindowSession\n${restoreFocus}\n return unique ID of spawned\nend tell`;\n};\n\n/**\n * Run the command IN the anchor session, rather than opening anything.\n *\n * Only for a successor taking a predecessor's own pane: the predecessor has\n * already exited, so its shell is back at a prompt, and the successor lands\n * exactly where the session it continues was sitting.\n *\n * An existing shell cannot be given a command at creation, so this one is still\n * typed. It is one write, Ctrl-U and then the short relaunch path, so no gap is\n * left between clearing the line and filling it; see `relaunchCommand` for what\n * catches keys that still land in front of it or after it.\n */\nconst inPlace = (uuid: string, command: string): string => `tell application \"iTerm2\"${findSessions(uuid, \"\")}\n tell anchorSession to write text ((character id ${CTRL_U}) & ${asString(command)})\n return unique ID of anchorSession\nend tell`;\n\n/** The fallback everything lands on: needs no anchor, so it cannot fail to find one. */\nconst newWindow = (command: string): string => `tell application \"iTerm2\"\n${rememberFocus}\n set spawned to (current session of (create window with default profile command ${asString(command)}))\n${restoreFocus}\n return unique ID of spawned\nend tell`;\n\n/**\n * Close one session, found the same way everything else here finds one.\n *\n * A session is the unit for all three surfaces: closing the last session of a\n * tab closes the tab, and the last tab of a window closes the window. Finding\n * nothing is the ordinary case of a human who already closed it by hand.\n */\nconst closeSession = (uuid: string): string => `tell application \"iTerm2\"${findSessions(uuid, \"\")}\n tell anchorSession to close\n return ${asString(CLOSED)}\nend tell`;\n\n/**\n * Does iTerm2 still list this session? Re-read rather than inferred:\n * `closeSession` returning `@@closed@@` only says the script found the session\n * and issued `close`, not that the close took.\n */\nconst sessionPresent = (uuid: string): string => `tell application \"iTerm2\"\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if (unique ID of s) is ${asString(uuid)} then return ${asString(PRESENT)}\n end repeat\n end repeat\n end repeat\n return ${asString(GONE)}\nend tell`;\n\n/**\n * Whether iTerm2 still holds `uuid`, for callers outside a teardown.\n *\n * Unknown (undefined) is a third answer and not a synonym for either: iTerm2 not\n * running, a non-Mac, or osascript failing all mean this cannot say.\n */\nexport async function itermSessionPresent(\n uuid: string,\n options: SurfaceOptions = {},\n): Promise<boolean | undefined> {\n const run = options.runAppleScript ?? osascript;\n try {\n await requireIterm(options, run);\n return await probeSession(run, uuid);\n } catch {\n return undefined;\n }\n}\n\n/** The probe without the `requireIterm` round trip, for a caller that just made one. */\nasync function probeSession(run: AppleScriptRunner, uuid: string): Promise<boolean | undefined> {\n try {\n const answer = await run(sessionPresent(uuid));\n return answer === PRESENT ? true : answer === GONE ? false : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Asked without launching it: `is running` is false for an app that is not up,\n * where a `tell` would start iTerm2 and drop a window on an unsuspecting desktop.\n */\nconst itermRunning = async (run: AppleScriptRunner): Promise<boolean> => {\n try {\n return (await run('application \"iTerm2\" is running')) === \"true\";\n } catch {\n return false;\n }\n};\n\nasync function requireIterm(options: SurfaceOptions, run: AppleScriptRunner): Promise<void> {\n const platform = options.platform ?? process.platform;\n if (platform !== \"darwin\")\n throw new SurfaceRefused(`iTerm2 surfaces need macOS (this is ${platform}); use surface 'headless'`);\n if (!(await itermRunning(run))) throw new SurfaceRefused(\"iTerm2 is not running; use surface 'headless'\");\n}\n\n/** One field of one session, or `@@gone@@` when iTerm2 no longer lists it. */\nconst sessionField = (uuid: string, field: \"tty\" | \"contents\"): string => `tell application \"iTerm2\"\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if (unique ID of s) is ${asString(uuid)} then return ${field} of s\n end repeat\n end repeat\n end repeat\n return ${asString(GONE)}\nend tell`;\n\nconst paneReader = (run: AppleScriptRunner, uuid: string): PaneReader => ({\n tty: async () => {\n const tty = await run(sessionField(uuid, \"tty\"));\n return tty === GONE ? undefined : tty;\n },\n contents: async () => {\n const contents = await run(sessionField(uuid, \"contents\"));\n return contents === GONE ? \"\" : contents;\n },\n});\n\nconst launchMarker = (launcher: Launcher, agentId: string): LaunchMarker => {\n const argv = launcher.argv(agentId);\n return [argv.at(-2) ?? \"\", argv.at(-1) ?? \"\"];\n};\n\ninterface ItermLaunch {\n surface: ItermSurfaceName;\n plan: LaunchPlan;\n launcher: Launcher;\n options: SurfaceOptions;\n run: AppleScriptRunner;\n}\n\n/** A pane with the check that the launcher started in it: one this launch opened, or a reused one. */\nconst watched = (handle: LaunchHandle & { paneRef: string }, launch: ItermLaunch): LaunchHandle => ({\n ...handle,\n launchFailed: watchLaunch(\n paneReader(launch.run, handle.paneRef),\n launch.options.probeProcesses ?? psProbe,\n launchMarker(launch.launcher, launch.plan.agentId),\n launch.options.launchCheck,\n ),\n});\n\n/** Reuse falls through to the ordinary ladder when the pane has closed, so the agent still lands somewhere findable. */\nasync function reuse(launch: ItermLaunch, uuid: string): Promise<LaunchHandle | undefined> {\n const { surface, plan, launcher, options, run } = launch;\n // No `ownsSurface`: this pane was already open and this launch only wrote into it.\n const paneRef = await run(inPlace(uuid, relaunchCommand(launcher, plan.agentId)));\n if (paneRef !== NO_ANCHOR) return watched({ surface, paneRef }, launch);\n options.onNotice?.(`pane ${uuid} is gone; opening an iTerm window rather than reusing it`);\n return undefined;\n}\n\n/**\n * The ladder, in order: no anchor -> a window; anchor recorded but gone -> a\n * window plus a notice; no iTerm2 at all -> refuse, naming headless.\n */\nasync function launchIterm(launch: ItermLaunch): Promise<LaunchHandle> {\n const { surface, plan, launcher, options, run } = launch;\n await requireIterm(options, run);\n const uuid = anchorUuid(options.anchor);\n const command = paneCommand(launcher, plan.agentId);\n const opened = (at: ItermSurfaceName, paneRef: string): LaunchHandle =>\n watched({ surface: at, paneRef, ownsSurface: true }, launch);\n\n if (options.reuseAnchor && uuid !== undefined) {\n const reused = await reuse(launch, uuid);\n if (reused !== undefined) return reused;\n } else if (surface !== \"iterm-window\" && uuid !== undefined) {\n const paneRef = await run(beside(surface, uuid, command, options.columnAfter));\n if (paneRef !== NO_ANCHOR) return opened(surface, paneRef);\n options.onNotice?.(`anchor session ${uuid} is gone; opening an iTerm window instead of ${surface}`);\n }\n return opened(\"iterm-window\", await run(newWindow(command)));\n}\n\n/**\n * Close what this launch opened, and nothing else, then go and check.\n *\n * Every failure here is benign and none of them should fail a shutdown: iTerm2\n * has quit, the human closed the pane themselves, osascript is unavailable. What\n * is NOT acceptable is reporting it closed when it is not, so each of those\n * outcomes arrives with the sentence that explains it.\n */\nasync function closeIterm(handle: LaunchHandle, options: SurfaceOptions): Promise<CloseOutcome> {\n if (handle.ownsSurface !== true)\n return { closed: false, reason: \"the host did not open this surface, so it is not ours to close\" };\n if (handle.paneRef === undefined) return { closed: false, reason: \"no pane was recorded for this agent\" };\n\n const run = options.runAppleScript ?? osascript;\n const paneRef = handle.paneRef;\n try {\n await requireIterm(options, run);\n const answer = await run(closeSession(paneRef));\n if (answer !== CLOSED) return { closed: false, reason: `iTerm2 no longer lists session ${paneRef}` };\n // `@@closed@@` says the script ran, not that the pane went; only the second look decides.\n const present = await probeSession(run, paneRef);\n if (present === true) return { closed: false, reason: `iTerm2 still lists session ${paneRef} after closing it` };\n if (present === undefined)\n return { closed: false, reason: `could not re-read iTerm2 to confirm session ${paneRef} is gone` };\n return { closed: true };\n } catch (err) {\n return { closed: false, reason: `iTerm2 could not be reached: ${(err as Error).message}` };\n }\n}\n\nexport function itermSurface(name: ItermSurfaceName, launcher: Launcher, options: SurfaceOptions = {}): Surface {\n return {\n name,\n interactive: true,\n launch: (plan: LaunchPlan): Promise<LaunchHandle> =>\n launchIterm({ surface: name, plan, launcher, options, run: options.runAppleScript ?? osascript }),\n close: (handle: LaunchHandle): Promise<CloseOutcome> => closeIterm(handle, options),\n };\n}\n","/**\n * The one command every surface launches, supplied by the host as a `Launcher`.\n *\n * A surface never sees `plan.bin` or `plan.args`. The plan is already on disk and\n * the launcher is what reads it, which keeps a model-authored brief out of every\n * command line: AppleScript only ever carries a fixed string with an agent id in it.\n */\nexport interface Launcher {\n /** The full argv that starts one agent; `argv[0]` is the executable. */\n argv(agentId: string): string[];\n /**\n * Variables a pane's shell must be handed explicitly. A pane opens in a fresh\n * login shell with the user's environment, not the host's, so a relocated\n * state directory is invisible to it unless carried here.\n */\n env?: Record<string, string>;\n /** Where the in-place relaunch script for this agent lives; only a reused pane types it. */\n relaunchPath(agentId: string): string;\n}\n\n/** Single-quoted for /bin/sh: paths here are ours, but they can still hold spaces. */\nexport const shellQuote = (word: string): string => `'${word.replaceAll(\"'\", `'\\\\''`)}'`;\n\nconst envWords = (launcher: Launcher): string[] =>\n Object.entries(launcher.env ?? {}).map(([name, value]) => `${name}=${shellQuote(value)}`);\n\n/** For surfaces that hand a shell a line to type, rather than spawning argv directly. */\nexport const launchCommand = (launcher: Launcher, agentId: string): string =>\n [...envWords(launcher), ...launcher.argv(agentId).map(shellQuote)].join(\" \");\n\n/**\n * One word of iTerm2's `command` parameter, which iTerm splits into argv itself\n * (`componentsInShellCommand`), with no shell and its own escape rules: `\\n`,\n * `\\t`, `\\a` and `\\r` become control characters even inside single quotes, and\n * the string is first evaluated as an interpolated \"swifty\" string where `\\(`\n * starts an expression. So a word is double-quoted and may hold no backslash\n * and no double quote at all, rather than trusting an escape to survive both.\n */\nconst itermWord = (word: string): string => {\n if (/[\"\\\\]/.test(word))\n throw new Error(`cannot hand iTerm2 a command containing a double quote or backslash: ${word}`);\n return `\"${word}\"`;\n};\n\n/**\n * For a pane the host opens: the command is given to iTerm at creation, so it is\n * never typed into a shell whose input the human can reach.\n *\n * `zsh -lic` for the environment a typed command used to get: `-l` for PATH from\n * .zprofile, `-i` for whatever .zshrc exports. `exec /bin/zsh -l` afterwards\n * keeps the pane, and whatever the launcher printed, open after it exits or crashes.\n */\nexport const paneCommand = (launcher: Launcher, agentId: string): string =>\n [\"/bin/zsh\", \"-lic\", `${launchCommand(launcher, agentId)}; exec /bin/zsh -l`].map(itermWord).join(\" \");\n\n/**\n * The only thing still typed into a shell: a reused pane has no creation\n * `command` to take, so it gets this short fixed path instead of the full\n * command line. Keys that join it in front make it a different, failing command;\n * keys typed between it and the newline become arguments, which a correct\n * invocation never has.\n */\nexport const relaunchCommand = (launcher: Launcher, agentId: string): string =>\n shellQuote(launcher.relaunchPath(agentId));\n\n/** The script `relaunchCommand` runs; the host writes it to `launcher.relaunchPath(agentId)`. */\nexport const relaunchScript = (launcher: Launcher, agentId: string): string =>\n [\n \"#!/bin/sh\",\n 'if [ \"$#\" -ne 0 ]; then',\n ` echo \"not relaunching ${agentId}: typed keys joined the command (extra arguments: $*)\" >&2`,\n \" exit 64\",\n \"fi\",\n ...envWords(launcher).map(word => `export ${word}`),\n `exec ${launcher.argv(agentId).map(shellQuote).join(\" \")}`,\n \"\",\n ].join(\"\\n\");\n","import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\n/**\n * Did the launcher actually start in the pane just opened?\n *\n * The host does not own a pane's process, so without this a launch that died at\n * the shell (`zsh: command not found: s`, exit 127, after keystrokes joined the\n * typed command) read as \"still starting\" until the attach window ran out. A\n * pane's tty is the one handle that reaches its processes, and `ps -t` answers\n * from it.\n *\n * Only a positive answer of absence fails a launch. A tty that never appears, a\n * `ps` that errors, or a pane that has closed all mean \"cannot say\", and the\n * host's ordinary attach window keeps deciding those.\n */\n\n/** Command lines of the processes on a tty. Rejects when it cannot tell. */\nexport type ProcessProbe = (tty: string) => Promise<string[]>;\n\n/** What a surface can read back off the pane it opened. */\nexport interface PaneReader {\n /** The pane's tty, `''` while it has none yet, undefined once the pane is gone. */\n tty: () => Promise<string | undefined>;\n contents: () => Promise<string>;\n}\n\nexport interface LaunchCheckTiming {\n deadlineMs: number;\n pollMs: number;\n}\n\nexport const LAUNCH_CHECK_TIMING: LaunchCheckTiming = { deadlineMs: 5_000, pollMs: 500 };\n\n/** How much of the pane a failure reason quotes. */\nconst TAIL_LINES = 5;\n\nconst execFileAsync = promisify(execFile);\n\n/** `-ww` because ps otherwise cuts the line at 80 columns, before the agent id. */\nexport const psProbe: ProcessProbe = async tty => {\n try {\n const { stdout } = await execFileAsync(\"ps\", [\"-ww\", \"-t\", tty, \"-o\", \"command=\"], { encoding: \"utf8\" });\n return stdout.split(\"\\n\").filter(line => line.trim() !== \"\");\n } catch (err) {\n // ps exits 1 with no output when nothing is on the tty, which is an answer.\n const failed = err as { code?: unknown; stdout?: unknown };\n if (failed.code === 1 && failed.stdout === \"\") return [];\n throw err;\n }\n};\n\n/**\n * The last two words of the launcher's argv, e.g. a verb and the agent id. The\n * launcher process carries them as two bare words; a shell wrapper's -c string\n * quotes them, so it never matches.\n */\nexport type LaunchMarker = readonly [string, string];\n\nconst runsLauncher = (line: string, [first, second]: LaunchMarker): boolean => {\n const words = line.trim().split(/\\s+/);\n return words.some((word, i) => word === first && words[i + 1] === second);\n};\n\nconst TTY = /^\\/dev\\/tty\\w+$/;\n\ntype Look = \"running\" | \"absent\" | \"unknown\";\n\nasync function lookOnce(pane: PaneReader, probe: ProcessProbe, marker: LaunchMarker): Promise<Look> {\n const tty = await pane.tty();\n if (tty === undefined) return \"unknown\";\n if (tty === \"\") return \"absent\";\n if (!TTY.test(tty)) return \"unknown\";\n const lines = await probe(tty.slice(\"/dev/\".length));\n return lines.some(line => runsLauncher(line, marker)) ? \"running\" : \"absent\";\n}\n\nconst pause = (ms: number): Promise<void> =>\n new Promise(resolve => {\n const timer = setTimeout(resolve, ms);\n timer.unref?.();\n });\n\nconst tail = (contents: string): string =>\n contents\n .split(\"\\n\")\n .map(line => line.trimEnd())\n .filter(line => line !== \"\")\n .slice(-TAIL_LINES)\n .join(\"\\n\");\n\nasync function failureReason(pane: PaneReader, marker: LaunchMarker, deadlineMs: number): Promise<string> {\n const said = await pane.contents().then(tail, () => \"\");\n const within = `${marker.join(\" \")} was not running in its pane ${Math.round(deadlineMs / 1000)}s after launch`;\n return said === \"\" ? `${within}, and the pane showed nothing.` : `${within}. The pane's last lines:\\n${said}`;\n}\n\nasync function check(\n pane: PaneReader,\n probe: ProcessProbe,\n marker: LaunchMarker,\n timing: LaunchCheckTiming,\n): Promise<string | undefined> {\n const attempts = Math.max(1, Math.ceil(timing.deadlineMs / timing.pollMs));\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n await pause(timing.pollMs);\n const look = await lookOnce(pane, probe, marker).catch((): Look => \"unknown\");\n if (look !== \"absent\") return undefined;\n }\n return failureReason(pane, marker, timing.deadlineMs);\n}\n\n/**\n * Resolves with a failure reason when the launch is known to have failed, and\n * never resolves otherwise, which is the shape a host's attach wait races.\n */\nexport function watchLaunch(\n pane: PaneReader,\n probe: ProcessProbe,\n marker: LaunchMarker,\n timing: LaunchCheckTiming = LAUNCH_CHECK_TIMING,\n): Promise<string> {\n return new Promise(resolve => {\n void check(pane, probe, marker, timing).then(reason => {\n if (reason !== undefined) resolve(reason);\n });\n });\n}\n","import type { SpawnOptions } from \"node:child_process\";\nimport type { LaunchCheckTiming, ProcessProbe } from \"./launch-check.js\";\n\n/**\n * Runs one AppleScript and resolves with its trimmed output. Injected so tests\n * need no macOS.\n *\n * Async because a host serving many sessions is one event loop. A synchronous\n * `osascript` stopped it dead for as long as iTerm2 took to answer, measured at\n * five seconds during a burst of spawns.\n */\nexport type AppleScriptRunner = (script: string) => Promise<string>;\n\n/**\n * The slice of `child_process.spawn` a surface uses. Injected for the same reason.\n *\n * `once` is here because a host infers a headless agent's exit from the child\n * itself; a fake that omitted it would let a test pass while the real exit path\n * was never wired.\n */\nexport interface SpawnedChild {\n pid?: number | undefined;\n unref: () => void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirrors node's overloaded listener signatures\n once: (event: string, listener: (...args: any[]) => void) => unknown;\n}\n\nexport type SpawnFn = (bin: string, args: string[], options: SpawnOptions) => SpawnedChild;\n\nexport interface SurfaceOptions {\n /**\n * The requester's `ITERM_SESSION_ID`. Absent means \"no anchor\", which is a\n * normal case, not an error: the window surface needs none.\n */\n anchor?: string;\n /**\n * The pane of the last agent already stacked beside this anchor, if any. A new\n * agent splits THAT rather than the anchor, so the requester's pane is not\n * halved once per spawn.\n */\n columnAfter?: string;\n /**\n * Put the agent IN the anchor session rather than beside it.\n *\n * Only for a successor taking over a predecessor's own pane, which it has just\n * vacated. Never set for a spawn: an agent must not be able to type into a pane\n * somebody else is working in, and the anchor of a spawn is the requester's\n * live pane.\n */\n reuseAnchor?: boolean;\n /** Told when a surface silently downgrades, e.g. the anchor pane has closed. */\n onNotice?: (message: string) => void;\n runAppleScript?: AppleScriptRunner;\n /** Lists what runs on a pane's tty, for the launch check. Injected so tests need no `ps`. */\n probeProcesses?: ProcessProbe;\n /** How long, and how often, the launch check looks for the launcher. Shortened in tests. */\n launchCheck?: LaunchCheckTiming;\n spawn?: SpawnFn;\n platform?: NodeJS.Platform;\n}\n\n/**\n * The surface cannot present the agent at all, as opposed to presenting it\n * somewhere less specific. Distinct from a plain Error so a host can record a\n * refusal rather than treating it as a crash.\n */\nexport class SurfaceRefused extends Error {\n override readonly name = \"SurfaceRefused\";\n}\n","import type { Surface, SurfaceName } from \"../types.js\";\nimport type { Launcher } from \"./command.js\";\nimport { headlessSurface } from \"./headless.js\";\nimport { itermSurface, type ItermSurfaceName } from \"./iterm.js\";\nimport type { SurfaceOptions } from \"./options.js\";\n\n/**\n * Where a spawned agent is presented, and the only place that knows what a pane\n * is. A `tmux-pane` surface would be a drop-in here and invisible everywhere else.\n *\n * Surfaces are built per launch rather than kept as singletons because the anchor\n * belongs to the requesting connection: reusing one surface across requests would\n * land one requester's spawn in another's window.\n */\n\nconst isIterm = (name: SurfaceName): name is ItermSurfaceName => name !== \"headless\";\n\nexport function surfaceFor(name: SurfaceName, launcher: Launcher, options: SurfaceOptions = {}): Surface {\n return isIterm(name) ? itermSurface(name, launcher, options) : headlessSurface(launcher, options);\n}\n"],"mappings":"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","names":["execFile","promisify","execFileAsync","promisify","execFile"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@titan-design/agent-surface",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Where a spawned agent is presented (headless or an iTerm2 pane, tab or window), and the launcher that execs its plan",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"titan-agent-launch": "./dist/bin.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"author": "Henry Jewkes",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"homepage": "https://github.com/HJewkes/titan-platform#readme",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/HJewkes/titan-platform.git",
|
|
27
|
+
"directory": "packages/agent-surface"
|
|
28
|
+
},
|
|
29
|
+
"bugs": "https://github.com/HJewkes/titan-platform/issues",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"lint": "eslint src"
|
|
40
|
+
}
|
|
6
41
|
}
|