@titan-design/agent-surface 0.0.0 → 0.2.1
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/README.md +6 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +176 -92
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,12 @@ const launcher: Launcher = {
|
|
|
22
22
|
const handle = await surfaceFor("iterm-pane", launcher, { anchor: process.env.ITERM_SESSION_ID }).launch(plan);
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
Placement is the host's decision, passed as options. `split: 'right' | 'below'` picks the
|
|
26
|
+
side of the anchor a pane's stack starts on. `maxInTab` turns a pane into a tab in the
|
|
27
|
+
anchor's window once the tab holds that many sessions. `tabWindow` (an iTerm2 window id)
|
|
28
|
+
opens a tab in that window with no anchor. An anchored launch reports `inTab`, the session
|
|
29
|
+
count of the anchor's tab, on its handle. `iterm-window` ignores all of them.
|
|
30
|
+
|
|
25
31
|
`AppleScriptRunner`, `SpawnFn` and `ProcessProbe` are injectable, so the suites run on Linux.
|
|
26
32
|
|
|
27
33
|
## Command line
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,11 @@ interface LaunchHandle {
|
|
|
29
29
|
pid?: number;
|
|
30
30
|
/** iTerm session UUID. */
|
|
31
31
|
paneRef?: string;
|
|
32
|
+
/**
|
|
33
|
+
* How many sessions the anchor's tab held when this launch looked, before it
|
|
34
|
+
* opened anything. Only for a launch placed by its anchor.
|
|
35
|
+
*/
|
|
36
|
+
inTab?: number;
|
|
32
37
|
/**
|
|
33
38
|
* This launch OPENED the surface, so its owner may also close it.
|
|
34
39
|
*
|
|
@@ -191,6 +196,23 @@ interface SurfaceOptions {
|
|
|
191
196
|
* halved once per spawn.
|
|
192
197
|
*/
|
|
193
198
|
columnAfter?: string;
|
|
199
|
+
/**
|
|
200
|
+
* Which side of the anchor a pane's stack starts on: `right` (the default)
|
|
201
|
+
* keeps the anchor's height, `below` keeps its width. A live `columnAfter`
|
|
202
|
+
* pane is split downwards either way.
|
|
203
|
+
*/
|
|
204
|
+
split?: "right" | "below";
|
|
205
|
+
/**
|
|
206
|
+
* The most sessions a tab may hold. A pane asked for in a tab already this
|
|
207
|
+
* full opens as a tab in the anchor's window instead, with a notice.
|
|
208
|
+
*/
|
|
209
|
+
maxInTab?: number;
|
|
210
|
+
/**
|
|
211
|
+
* An iTerm2 window id. A tab opens in this window with no anchor needed, and
|
|
212
|
+
* a pane whose anchor is absent or gone lands there as a tab before it falls
|
|
213
|
+
* back to a window of its own.
|
|
214
|
+
*/
|
|
215
|
+
tabWindow?: number | string;
|
|
194
216
|
/**
|
|
195
217
|
* Put the agent IN the anchor session rather than beside it.
|
|
196
218
|
*
|
|
@@ -245,6 +267,7 @@ declare function headlessSurface(launcher: Launcher, options?: SurfaceOptions):
|
|
|
245
267
|
* job overwrites it); the launcher emits an OSC 0 escape instead.
|
|
246
268
|
*/
|
|
247
269
|
type ItermSurfaceName = Extract<SurfaceName, `iterm-${string}`>;
|
|
270
|
+
|
|
248
271
|
/**
|
|
249
272
|
* Whether iTerm2 still holds `uuid`, for callers outside a teardown.
|
|
250
273
|
*
|
package/dist/index.js
CHANGED
|
@@ -91,91 +91,27 @@ var relaunchScript = (launcher, agentId) => [
|
|
|
91
91
|
""
|
|
92
92
|
].join("\n");
|
|
93
93
|
|
|
94
|
-
// src/surfaces/
|
|
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
|
|
94
|
+
// src/surfaces/iterm-scripts.ts
|
|
157
95
|
var NO_ANCHOR = "@@no-anchor@@";
|
|
96
|
+
var NO_WINDOW = "@@no-window@@";
|
|
97
|
+
var OVERFLOWED = "overflow";
|
|
158
98
|
var CLOSED = "@@closed@@";
|
|
159
99
|
var PRESENT = "@@present@@";
|
|
160
100
|
var GONE = "@@gone@@";
|
|
161
101
|
var CTRL_U = 21;
|
|
162
|
-
var execFileAsync2 = promisify2(execFile2);
|
|
163
|
-
var osascript = async (script) => (await execFileAsync2("osascript", ["-e", script], { encoding: "utf8" })).stdout.trim();
|
|
164
102
|
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
103
|
var findSessions = (uuid, columnUuid) => `
|
|
170
104
|
set anchorSession to missing value
|
|
171
105
|
set anchorWindow to missing value
|
|
172
106
|
set columnSession to missing value
|
|
107
|
+
set inTab to 0
|
|
173
108
|
repeat with w in windows
|
|
174
109
|
repeat with t in tabs of w
|
|
175
110
|
repeat with s in sessions of t
|
|
176
111
|
if (unique ID of s) is ${asString(uuid)} then
|
|
177
112
|
set anchorSession to s
|
|
178
113
|
set anchorWindow to w
|
|
114
|
+
set inTab to (count of sessions of t)
|
|
179
115
|
end if
|
|
180
116
|
if (unique ID of s) is ${asString(columnUuid)} then
|
|
181
117
|
set columnSession to s
|
|
@@ -193,24 +129,46 @@ var restoreFocus = ` if priorWindow is not missing value then
|
|
|
193
129
|
select priorWindow
|
|
194
130
|
end try
|
|
195
131
|
end if`;
|
|
196
|
-
var beside = (surface, uuid, command, columnUuid
|
|
132
|
+
var beside = ({ surface, uuid, command, columnUuid, split, cap }) => {
|
|
197
133
|
const withCommand = `with default profile command ${asString(command)}`;
|
|
198
|
-
const
|
|
134
|
+
const tab = `tell anchorWindow to set spawned to (current session of (create tab ${withCommand}))`;
|
|
135
|
+
const pane = `if columnSession is not missing value then
|
|
199
136
|
tell columnSession to set spawned to (split horizontally ${withCommand})
|
|
200
137
|
else
|
|
201
|
-
tell anchorSession to set spawned to (split vertically ${withCommand})
|
|
202
|
-
end if
|
|
203
|
-
|
|
138
|
+
tell anchorSession to set spawned to (split ${split === "below" ? "horizontally" : "vertically"} ${withCommand})
|
|
139
|
+
end if`;
|
|
140
|
+
const capped = `if inTab >= ${cap} then
|
|
141
|
+
${tab}
|
|
142
|
+
set overflow to ${asString(OVERFLOWED)}
|
|
143
|
+
else
|
|
144
|
+
${pane.replaceAll("\n", "\n ")}
|
|
145
|
+
end if`;
|
|
146
|
+
const open = surface !== "iterm-pane" ? tab : cap === void 0 ? pane : capped;
|
|
147
|
+
return `tell application "iTerm2"${findSessions(uuid, columnUuid ?? "")}
|
|
204
148
|
${rememberFocus}
|
|
205
149
|
set anchorTab to current tab of anchorWindow
|
|
206
150
|
set anchorWindowSession to current session of anchorWindow
|
|
207
|
-
|
|
151
|
+
set overflow to ""
|
|
152
|
+
${open}
|
|
208
153
|
select anchorTab
|
|
209
154
|
select anchorWindowSession
|
|
210
155
|
${restoreFocus}
|
|
211
|
-
return unique ID of spawned
|
|
156
|
+
return (unique ID of spawned) & "|" & inTab & "|" & overflow
|
|
212
157
|
end tell`;
|
|
213
158
|
};
|
|
159
|
+
var tabIn = (windowId, command) => `tell application "iTerm2"
|
|
160
|
+
set homeWindow to missing value
|
|
161
|
+
repeat with w in windows
|
|
162
|
+
if ((id of w) as text) is ${asString(windowId)} then set homeWindow to w
|
|
163
|
+
end repeat
|
|
164
|
+
if homeWindow is missing value then return ${asString(NO_WINDOW)}
|
|
165
|
+
${rememberFocus}
|
|
166
|
+
set homeTab to current tab of homeWindow
|
|
167
|
+
tell homeWindow to set spawned to (current session of (create tab with default profile command ${asString(command)}))
|
|
168
|
+
select homeTab
|
|
169
|
+
${restoreFocus}
|
|
170
|
+
return unique ID of spawned
|
|
171
|
+
end tell`;
|
|
214
172
|
var inPlace = (uuid, command) => `tell application "iTerm2"${findSessions(uuid, "")}
|
|
215
173
|
tell anchorSession to write text ((character id ${CTRL_U}) & ${asString(command)})
|
|
216
174
|
return unique ID of anchorSession
|
|
@@ -235,6 +193,96 @@ var sessionPresent = (uuid) => `tell application "iTerm2"
|
|
|
235
193
|
end repeat
|
|
236
194
|
return ${asString(GONE)}
|
|
237
195
|
end tell`;
|
|
196
|
+
var sessionField = (uuid, field) => `tell application "iTerm2"
|
|
197
|
+
repeat with w in windows
|
|
198
|
+
repeat with t in tabs of w
|
|
199
|
+
repeat with s in sessions of t
|
|
200
|
+
if (unique ID of s) is ${asString(uuid)} then return ${field} of s
|
|
201
|
+
end repeat
|
|
202
|
+
end repeat
|
|
203
|
+
end repeat
|
|
204
|
+
return ${asString(GONE)}
|
|
205
|
+
end tell`;
|
|
206
|
+
|
|
207
|
+
// src/surfaces/launch-check.ts
|
|
208
|
+
import { execFile } from "child_process";
|
|
209
|
+
import { promisify } from "util";
|
|
210
|
+
var LAUNCH_CHECK_TIMING = { deadlineMs: 5e3, pollMs: 500 };
|
|
211
|
+
var TAIL_LINES = 5;
|
|
212
|
+
var execFileAsync = promisify(execFile);
|
|
213
|
+
var PS_TIMEOUT_MS = 5e3;
|
|
214
|
+
var psProbe = async (tty) => {
|
|
215
|
+
try {
|
|
216
|
+
const { stdout } = await execFileAsync("ps", ["-ww", "-t", tty, "-o", "command="], {
|
|
217
|
+
encoding: "utf8",
|
|
218
|
+
timeout: PS_TIMEOUT_MS,
|
|
219
|
+
killSignal: "SIGKILL"
|
|
220
|
+
});
|
|
221
|
+
return stdout.split("\n").filter((line) => line.trim() !== "");
|
|
222
|
+
} catch (err) {
|
|
223
|
+
const failed = err;
|
|
224
|
+
if (failed.code === 1 && failed.stdout === "") return [];
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var runsLauncher = (line, [first, second]) => {
|
|
229
|
+
const words = line.trim().split(/\s+/);
|
|
230
|
+
return words.some((word, i) => word === first && words[i + 1] === second);
|
|
231
|
+
};
|
|
232
|
+
var TTY = /^\/dev\/tty\w+$/;
|
|
233
|
+
async function lookOnce(pane, probe, marker) {
|
|
234
|
+
const tty = await pane.tty();
|
|
235
|
+
if (tty === void 0) return "unknown";
|
|
236
|
+
if (tty === "") return "absent";
|
|
237
|
+
if (!TTY.test(tty)) return "unknown";
|
|
238
|
+
const lines = await probe(tty.slice("/dev/".length));
|
|
239
|
+
return lines.some((line) => runsLauncher(line, marker)) ? "running" : "absent";
|
|
240
|
+
}
|
|
241
|
+
var pause = (ms) => new Promise((resolve) => {
|
|
242
|
+
const timer = setTimeout(resolve, ms);
|
|
243
|
+
timer.unref?.();
|
|
244
|
+
});
|
|
245
|
+
var tail = (contents) => contents.split("\n").map((line) => line.trimEnd()).filter((line) => line !== "").slice(-TAIL_LINES).join("\n");
|
|
246
|
+
async function failureReason(pane, marker, deadlineMs) {
|
|
247
|
+
const said = await pane.contents().then(tail, () => "");
|
|
248
|
+
const within = `${marker.join(" ")} was not running in its pane ${Math.round(deadlineMs / 1e3)}s after launch`;
|
|
249
|
+
return said === "" ? `${within}, and the pane showed nothing.` : `${within}. The pane's last lines:
|
|
250
|
+
${said}`;
|
|
251
|
+
}
|
|
252
|
+
async function check(pane, probe, marker, timing) {
|
|
253
|
+
const attempts = Math.max(1, Math.ceil(timing.deadlineMs / timing.pollMs));
|
|
254
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
255
|
+
await pause(timing.pollMs);
|
|
256
|
+
const look = await lookOnce(pane, probe, marker).catch(() => "unknown");
|
|
257
|
+
if (look !== "absent") return void 0;
|
|
258
|
+
}
|
|
259
|
+
return failureReason(pane, marker, timing.deadlineMs);
|
|
260
|
+
}
|
|
261
|
+
function watchLaunch(pane, probe, marker, timing = LAUNCH_CHECK_TIMING) {
|
|
262
|
+
return new Promise((resolve) => {
|
|
263
|
+
void check(pane, probe, marker, timing).then((reason) => {
|
|
264
|
+
if (reason !== void 0) resolve(reason);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/surfaces/options.ts
|
|
270
|
+
var SurfaceRefused = class extends Error {
|
|
271
|
+
name = "SurfaceRefused";
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/surfaces/iterm.ts
|
|
275
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
276
|
+
var OSASCRIPT_TIMEOUT_MS = 1e4;
|
|
277
|
+
var osascript = async (script) => (await execFileAsync2("osascript", ["-e", script], {
|
|
278
|
+
encoding: "utf8",
|
|
279
|
+
timeout: OSASCRIPT_TIMEOUT_MS,
|
|
280
|
+
killSignal: "SIGKILL"
|
|
281
|
+
})).stdout.trim();
|
|
282
|
+
var anchorUuid = (anchor) => {
|
|
283
|
+
const uuid = anchor?.slice(anchor.lastIndexOf(":") + 1).trim();
|
|
284
|
+
return uuid === void 0 || uuid === "" ? void 0 : uuid;
|
|
285
|
+
};
|
|
238
286
|
async function itermSessionPresent(uuid, options = {}) {
|
|
239
287
|
const run = options.runAppleScript ?? osascript;
|
|
240
288
|
try {
|
|
@@ -265,16 +313,6 @@ async function requireIterm(options, run) {
|
|
|
265
313
|
throw new SurfaceRefused(`iTerm2 surfaces need macOS (this is ${platform}); use surface 'headless'`);
|
|
266
314
|
if (!await itermRunning(run)) throw new SurfaceRefused("iTerm2 is not running; use surface 'headless'");
|
|
267
315
|
}
|
|
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
316
|
var paneReader = (run, uuid) => ({
|
|
279
317
|
tty: async () => {
|
|
280
318
|
const tty = await run(sessionField(uuid, "tty"));
|
|
@@ -305,21 +343,67 @@ async function reuse(launch, uuid) {
|
|
|
305
343
|
options.onNotice?.(`pane ${uuid} is gone; opening an iTerm window rather than reusing it`);
|
|
306
344
|
return void 0;
|
|
307
345
|
}
|
|
346
|
+
var tabCap = (cap) => cap !== void 0 && Number.isInteger(cap) && cap > 0 ? cap : void 0;
|
|
347
|
+
var parsePlacement = (answer) => {
|
|
348
|
+
const [paneRef = "", count = "", flag] = answer.split("|");
|
|
349
|
+
const inTab = Number.parseInt(count, 10);
|
|
350
|
+
return { paneRef, inTab: Number.isNaN(inTab) ? void 0 : inTab, overflowed: flag === OVERFLOWED };
|
|
351
|
+
};
|
|
352
|
+
async function besideAnchor(launch, uuid, command) {
|
|
353
|
+
const { surface, options, run } = launch;
|
|
354
|
+
const cap = tabCap(options.maxInTab);
|
|
355
|
+
const { columnAfter: columnUuid, split } = options;
|
|
356
|
+
const answer = await run(beside({ surface, uuid, command, columnUuid, split, cap }));
|
|
357
|
+
if (answer === NO_ANCHOR) return `anchor session ${uuid}`;
|
|
358
|
+
const { paneRef, inTab, overflowed } = parsePlacement(answer);
|
|
359
|
+
if (overflowed)
|
|
360
|
+
options.onNotice?.(`the anchor's tab holds ${inTab} sessions (cap ${cap}); opening an iTerm tab instead of ${surface}`);
|
|
361
|
+
const at = overflowed ? "iterm-tab" : surface;
|
|
362
|
+
return watched({ surface: at, paneRef, ownsSurface: true, ...inTab === void 0 ? {} : { inTab } }, launch);
|
|
363
|
+
}
|
|
364
|
+
async function tabInWindow(launch, windowId, command) {
|
|
365
|
+
const paneRef = await launch.run(tabIn(windowId, command));
|
|
366
|
+
if (paneRef === NO_WINDOW) return `window ${windowId}`;
|
|
367
|
+
return watched({ surface: "iterm-tab", paneRef, ownsSurface: true }, launch);
|
|
368
|
+
}
|
|
369
|
+
var rungs = (launch, uuid, command) => {
|
|
370
|
+
const { surface, options } = launch;
|
|
371
|
+
const windowId = options.tabWindow === void 0 ? void 0 : String(options.tabWindow);
|
|
372
|
+
const viaAnchor = uuid === void 0 ? [] : [() => besideAnchor(launch, uuid, command)];
|
|
373
|
+
const viaWindow = windowId === void 0 ? [] : [() => tabInWindow(launch, windowId, command)];
|
|
374
|
+
return surface === "iterm-tab" && viaWindow.length > 0 ? viaWindow : [...viaAnchor, ...viaWindow];
|
|
375
|
+
};
|
|
376
|
+
async function place(launch, uuid, command) {
|
|
377
|
+
const { surface, options } = launch;
|
|
378
|
+
const gone = [];
|
|
379
|
+
let handle;
|
|
380
|
+
for (const rung of rungs(launch, uuid, command)) {
|
|
381
|
+
const result = await rung();
|
|
382
|
+
if (typeof result !== "string") {
|
|
383
|
+
handle = result;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
gone.push(result);
|
|
387
|
+
}
|
|
388
|
+
if (gone.length > 0) {
|
|
389
|
+
const where = handle === void 0 ? "an iTerm window" : `an iTerm tab in window ${options.tabWindow}`;
|
|
390
|
+
options.onNotice?.(`${gone.join(" and ")} ${gone.length > 1 ? "are" : "is"} gone; opening ${where} instead of ${surface}`);
|
|
391
|
+
}
|
|
392
|
+
return handle;
|
|
393
|
+
}
|
|
308
394
|
async function launchIterm(launch) {
|
|
309
395
|
const { surface, plan, launcher, options, run } = launch;
|
|
310
396
|
await requireIterm(options, run);
|
|
311
397
|
const uuid = anchorUuid(options.anchor);
|
|
312
398
|
const command = paneCommand(launcher, plan.agentId);
|
|
313
|
-
const opened = (at, paneRef) => watched({ surface: at, paneRef, ownsSurface: true }, launch);
|
|
314
399
|
if (options.reuseAnchor && uuid !== void 0) {
|
|
315
400
|
const reused = await reuse(launch, uuid);
|
|
316
401
|
if (reused !== void 0) return reused;
|
|
317
|
-
} else if (surface !== "iterm-window"
|
|
318
|
-
const
|
|
319
|
-
if (
|
|
320
|
-
options.onNotice?.(`anchor session ${uuid} is gone; opening an iTerm window instead of ${surface}`);
|
|
402
|
+
} else if (surface !== "iterm-window") {
|
|
403
|
+
const placed = await place(launch, uuid, command);
|
|
404
|
+
if (placed !== void 0) return placed;
|
|
321
405
|
}
|
|
322
|
-
return
|
|
406
|
+
return watched({ surface: "iterm-window", paneRef: await run(newWindow(command)), ownsSurface: true }, launch);
|
|
323
407
|
}
|
|
324
408
|
async function closeIterm(handle, options) {
|
|
325
409
|
if (handle.ownsSurface !== true)
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/surfaces/headless.ts","../src/surfaces/iterm.ts","../src/surfaces/command.ts","../src/surfaces/iterm-scripts.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 * How many sessions the anchor's tab held when this launch looked, before it\n * opened anything. Only for a launch placed by its anchor.\n */\n inTab?: number;\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 } from \"../types.js\";\nimport { paneCommand, relaunchCommand, type Launcher } from \"./command.js\";\nimport {\n CLOSED,\n GONE,\n NO_ANCHOR,\n NO_WINDOW,\n OVERFLOWED,\n PRESENT,\n beside,\n closeSession,\n inPlace,\n newWindow,\n sessionField,\n sessionPresent,\n tabIn,\n type ItermSurfaceName,\n} from \"./iterm-scripts.js\";\nimport { psProbe, watchLaunch, type LaunchMarker, type PaneReader } from \"./launch-check.js\";\nimport { SurfaceRefused, type AppleScriptRunner, type SurfaceOptions } from \"./options.js\";\n\nexport type { ItermSurfaceName };\n\nconst execFileAsync = promisify(execFile);\n\n/** Past this, an Automation prompt or a wedged iTerm2 is not going to answer. */\nexport const OSASCRIPT_TIMEOUT_MS = 10_000;\n\nconst osascript: AppleScriptRunner = async script =>\n (\n await execFileAsync(\"osascript\", [\"-e\", script], {\n encoding: \"utf8\",\n timeout: OSASCRIPT_TIMEOUT_MS,\n killSignal: \"SIGKILL\",\n })\n ).stdout.trim();\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 * 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\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/** A cap that is not a positive whole number is no cap, rather than a script that cannot compile. */\nconst tabCap = (cap: number | undefined): number | undefined =>\n cap !== undefined && Number.isInteger(cap) && cap > 0 ? cap : undefined;\n\n/** `uuid|inTab|overflow`; a bare uuid is still an answer, with nothing known about the tab. */\nconst parsePlacement = (answer: string): { paneRef: string; inTab: number | undefined; overflowed: boolean } => {\n const [paneRef = \"\", count = \"\", flag] = answer.split(\"|\");\n const inTab = Number.parseInt(count, 10);\n return { paneRef, inTab: Number.isNaN(inTab) ? undefined : inTab, overflowed: flag === OVERFLOWED };\n};\n\n/** A rung of the ladder: the handle it opened, or the name of the target that is gone. */\ntype Rung = () => Promise<LaunchHandle | string>;\n\nasync function besideAnchor(launch: ItermLaunch, uuid: string, command: string): Promise<LaunchHandle | string> {\n const { surface, options, run } = launch;\n const cap = tabCap(options.maxInTab);\n const { columnAfter: columnUuid, split } = options;\n const answer = await run(beside({ surface, uuid, command, columnUuid, split, cap }));\n if (answer === NO_ANCHOR) return `anchor session ${uuid}`;\n const { paneRef, inTab, overflowed } = parsePlacement(answer);\n if (overflowed)\n options.onNotice?.(`the anchor's tab holds ${inTab} sessions (cap ${cap}); opening an iTerm tab instead of ${surface}`);\n const at = overflowed ? \"iterm-tab\" : surface;\n return watched({ surface: at, paneRef, ownsSurface: true, ...(inTab === undefined ? {} : { inTab }) }, launch);\n}\n\nasync function tabInWindow(launch: ItermLaunch, windowId: string, command: string): Promise<LaunchHandle | string> {\n const paneRef = await launch.run(tabIn(windowId, command));\n if (paneRef === NO_WINDOW) return `window ${windowId}`;\n return watched({ surface: \"iterm-tab\", paneRef, ownsSurface: true }, launch);\n}\n\n/** A tab with a named window goes there and nowhere else; a pane tries its anchor first. */\nconst rungs = (launch: ItermLaunch, uuid: string | undefined, command: string): Rung[] => {\n const { surface, options } = launch;\n const windowId = options.tabWindow === undefined ? undefined : String(options.tabWindow);\n const viaAnchor = uuid === undefined ? [] : [() => besideAnchor(launch, uuid, command)];\n const viaWindow = windowId === undefined ? [] : [() => tabInWindow(launch, windowId, command)];\n return surface === \"iterm-tab\" && viaWindow.length > 0 ? viaWindow : [...viaAnchor, ...viaWindow];\n};\n\n/** Walks the rungs, and says so once when the agent lands somewhere other than where it was asked. */\nasync function place(launch: ItermLaunch, uuid: string | undefined, command: string): Promise<LaunchHandle | undefined> {\n const { surface, options } = launch;\n const gone: string[] = [];\n let handle: LaunchHandle | undefined;\n for (const rung of rungs(launch, uuid, command)) {\n const result = await rung();\n if (typeof result !== \"string\") {\n handle = result;\n break;\n }\n gone.push(result);\n }\n if (gone.length > 0) {\n const where = handle === undefined ? \"an iTerm window\" : `an iTerm tab in window ${options.tabWindow}`;\n options.onNotice?.(`${gone.join(\" and \")} ${gone.length > 1 ? \"are\" : \"is\"} gone; opening ${where} instead of ${surface}`);\n }\n return handle;\n}\n\n/**\n * The ladder, in order: the anchor or the named window -> there; neither given ->\n * a window; given but gone -> a window plus a notice; no iTerm2 at all ->\n * 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\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\") {\n const placed = await place(launch, uuid, command);\n if (placed !== undefined) return placed;\n }\n return watched({ surface: \"iterm-window\", paneRef: await run(newWindow(command)), ownsSurface: true }, launch);\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 type { SurfaceName } from \"../types.js\";\nimport 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. */\nexport const NO_ANCHOR = \"@@no-anchor@@\";\n\n/** Returned by the tab-in-window script when no window has the id it was given. */\nexport const NO_WINDOW = \"@@no-window@@\";\n\n/** The third field of a placement answer when a full tab turned a pane into a tab. */\nexport const OVERFLOWED = \"overflow\";\n\n/** Returned by the teardown script when it found the session and closed it. */\nexport const CLOSED = \"@@closed@@\";\n\n/** Returned by the existence probe. Two sentinels, so a throw can never read as \"gone\". */\nexport const PRESENT = \"@@present@@\";\nexport const GONE = \"@@gone@@\";\n\n/** Kill-line in zsh and bash, for a prompt that already holds stray keys. */\nconst CTRL_U = 21;\n\nconst asString = (value: string): string => `\"${value.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll('\"', '\\\\\"')}\"`;\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 *\n * `inTab` is counted here, while the anchor's tab is in hand, because a second\n * osascript round trip per spawn is what the load data argues against.\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 set inTab to 0\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 set inTab to (count of sessions of t)\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 * `split: 'below'` starts the stack under the anchor rather than beside it; a\n * live column is extended downwards either way. A tab at `cap` sessions takes\n * no more splits: the agent opens as a tab in the anchor's window instead.\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 */\ninterface Beside {\n surface: ItermSurfaceName;\n uuid: string;\n command: string;\n columnUuid: string | undefined;\n split: SurfaceOptions[\"split\"];\n cap: number | undefined;\n}\n\nexport const beside = ({ surface, uuid, command, columnUuid, split, cap }: Beside): string => {\n const withCommand = `with default profile command ${asString(command)}`;\n const tab = `tell anchorWindow to set spawned to (current session of (create tab ${withCommand}))`;\n const pane = `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 ${split === \"below\" ? \"horizontally\" : \"vertically\"} ${withCommand})\n end if`;\n const capped = `if inTab >= ${cap} then\n ${tab}\n set overflow to ${asString(OVERFLOWED)}\n else\n ${pane.replaceAll(\"\\n\", \"\\n \")}\n end if`;\n const open = surface !== \"iterm-pane\" ? tab : cap === undefined ? pane : capped;\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 set overflow to \"\"\n ${open}\n select anchorTab\n select anchorWindowSession\n${restoreFocus}\n return (unique ID of spawned) & \"|\" & inTab & \"|\" & overflow\nend tell`;\n};\n\n/**\n * A tab in a window named by id, for a requester with no session to anchor on.\n * The window's own tab is re-selected because a new tab selects itself.\n */\nexport const tabIn = (windowId: string, command: string): string => `tell application \"iTerm2\"\n set homeWindow to missing value\n repeat with w in windows\n if ((id of w) as text) is ${asString(windowId)} then set homeWindow to w\n end repeat\n if homeWindow is missing value then return ${asString(NO_WINDOW)}\n${rememberFocus}\n set homeTab to current tab of homeWindow\n tell homeWindow to set spawned to (current session of (create tab with default profile command ${asString(command)}))\n select homeTab\n${restoreFocus}\n return unique ID of spawned\nend tell`;\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 */\nexport const 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. */\nexport const 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 */\nexport const 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 */\nexport const 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/** One field of one session, or `@@gone@@` when iTerm2 no longer lists it. */\nexport const 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","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/** ps on one tty answers at once; this is generous for a loaded machine. */\nexport const PS_TIMEOUT_MS = 5_000;\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=\"], {\n encoding: \"utf8\",\n timeout: PS_TIMEOUT_MS,\n killSignal: \"SIGKILL\",\n });\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 * Which side of the anchor a pane's stack starts on: `right` (the default)\n * keeps the anchor's height, `below` keeps its width. A live `columnAfter`\n * pane is split downwards either way.\n */\n split?: \"right\" | \"below\";\n /**\n * The most sessions a tab may hold. A pane asked for in a tab already this\n * full opens as a tab in the anchor's window instead, with a notice.\n */\n maxInTab?: number;\n /**\n * An iTerm2 window id. A tab opens in this window with no anchor needed, and\n * a pane whose anchor is absent or gone lands there as a tab before it falls\n * back to a window of its own.\n */\n tabWindow?: number | 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,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@titan-design/agent-surface",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Where a spawned agent is presented (headless or an iTerm2 pane, tab or window), and the launcher that execs its plan",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|