@osdlc/agent-seat 0.0.0-stage → 0.7.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/CHANGELOG.md +133 -0
- package/CONTRIBUTING.md +44 -0
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +181 -2
- package/SECURITY.md +41 -0
- package/dist/contract.d.ts +297 -0
- package/dist/contract.js +102 -0
- package/dist/harness.d.ts +305 -0
- package/dist/harness.js +610 -0
- package/dist/hosts/cmux.d.ts +67 -0
- package/dist/hosts/cmux.js +252 -0
- package/dist/hosts/herdr.d.ts +78 -0
- package/dist/hosts/herdr.js +324 -0
- package/dist/hosts/jetbrains.d.ts +98 -0
- package/dist/hosts/jetbrains.js +353 -0
- package/dist/hosts/orca.d.ts +67 -0
- package/dist/hosts/orca.js +292 -0
- package/dist/hosts/otty.d.ts +70 -0
- package/dist/hosts/otty.js +233 -0
- package/dist/hosts/registry.d.ts +50 -0
- package/dist/hosts/registry.js +100 -0
- package/dist/hosts/session-panes.d.ts +29 -0
- package/dist/hosts/session-panes.js +49 -0
- package/dist/hosts/terminal.d.ts +23 -0
- package/dist/hosts/terminal.js +47 -0
- package/dist/hosts/tmux.d.ts +145 -0
- package/dist/hosts/tmux.js +409 -0
- package/dist/hosts/vscode-client.d.ts +81 -0
- package/dist/hosts/vscode-client.js +124 -0
- package/dist/hosts/vscode.d.ts +35 -0
- package/dist/hosts/vscode.js +127 -0
- package/dist/hosts/wezterm.d.ts +78 -0
- package/dist/hosts/wezterm.js +405 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +61 -0
- package/dist/open-workspace.d.ts +9 -0
- package/dist/open-workspace.js +12 -0
- package/dist/util.d.ts +50 -0
- package/dist/util.js +108 -0
- package/package.json +76 -3
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness axis — pure argv-building + naming for whichever coding-agent
|
|
3
|
+
* CLI a host opens a session in.
|
|
4
|
+
*
|
|
5
|
+
* `Harness` is independent of `HostName` (`contract.ts`): every host composes
|
|
6
|
+
* `harnessArgv` to run whichever harness the caller asked for.
|
|
7
|
+
*
|
|
8
|
+
* Not `@osdlc/agent-call`'s planner: that plans HEADLESS calls (`claude -p`,
|
|
9
|
+
* `codex exec`) whose output a program reads. A session is the opposite — a
|
|
10
|
+
* person sits in it — so the argv is the bare CLI with the first message,
|
|
11
|
+
* plus the Claude-only flags in {@link claudeSessionArgv}.
|
|
12
|
+
*
|
|
13
|
+
* This module also carries harness-specific machinery: codex's own on-disk
|
|
14
|
+
* session-index lookup (`codexSessionsDir` et al. — harness-specific, not
|
|
15
|
+
* host-specific, so it belongs on this axis), `wrapperSession`, the one body
|
|
16
|
+
* every wrapper host's `session()` shares ("build argv, run it, best-effort
|
|
17
|
+
* resolve a session id"), and `harnessNativeSend`, a per-harness
|
|
18
|
+
* message-injection mechanism independent of any host (`contract.ts`'s
|
|
19
|
+
* `Host.send` is the host-level counterpart).
|
|
20
|
+
*/
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import { type ModelChoice } from '@osdlc/agent-call';
|
|
23
|
+
import { type CommandRunner, type EnvOverrides } from '@osdlc/command-seam';
|
|
24
|
+
import { type CommandRequest, type HostName, type SendReceipt, type SessionRequest } from './contract.ts';
|
|
25
|
+
/**
|
|
26
|
+
* The coding-agent CLI a session runs, by its tool id: a built-in one
|
|
27
|
+
* (`BUILTIN_HARNESSES` from `@osdlc/detect-harness`, e.g. `claude`) or one a caller declares. This file
|
|
28
|
+
* builds the interactive argv ({@link INTERACTIVE_ARGV}).
|
|
29
|
+
*/
|
|
30
|
+
export type Harness = string;
|
|
31
|
+
/**
|
|
32
|
+
* The schema a harness field validates against. A tool id, not a closed list,
|
|
33
|
+
* since a caller may run a tool of its own.
|
|
34
|
+
*/
|
|
35
|
+
export declare const HarnessSchema: z.ZodString;
|
|
36
|
+
/**
|
|
37
|
+
* `value` (what a host reports running in a pane) as a built-in harness id, or null when
|
|
38
|
+
* it names none. An alias or binary name maps to its id (`cursor-agent` → `cursor`), by
|
|
39
|
+
* `harnessId` from `@osdlc/detect-harness`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function harnessOrNull(value: unknown): Harness | null;
|
|
42
|
+
/** Whether an interactive argv can be built for `tool`. */
|
|
43
|
+
export declare function hasInteractiveArgv(tool: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The argv that opens `harness`, run as `binary` (a name on PATH or an
|
|
46
|
+
* absolute path), interactively with `prompt` as its first message
|
|
47
|
+
* ({@link INTERACTIVE_ARGV}). A resolved `choice` adds its launch mode,
|
|
48
|
+
* model and effort flags (`modelArgs`, `@osdlc/agent-call`) before the `--` guard; a
|
|
49
|
+
* tool with no model or effort flags wired throws on a choice rather than
|
|
50
|
+
* dropping it. Throws for a tool with no interactive launch.
|
|
51
|
+
*
|
|
52
|
+
* `injected` is the argv a per-launch observation adds (pi's
|
|
53
|
+
* `--session-id` and `-e <extension>`); it sits before the prompt.
|
|
54
|
+
*/
|
|
55
|
+
export declare function harnessArgv(harness: Harness, binary: string, prompt: string, claude?: ClaudeLaunchFlags, choice?: ModelChoice, injected?: readonly string[]): string[];
|
|
56
|
+
/** Session flags only `claude` takes; a caller can store them to resume with the same flags. */
|
|
57
|
+
export declare const ClaudeSessionFlagsSchema: z.ZodObject<{
|
|
58
|
+
appendSystemPrompt: z.ZodOptional<z.ZodString>;
|
|
59
|
+
plugins: z.ZodOptional<z.ZodObject<{
|
|
60
|
+
dirs: z.ZodArray<z.ZodString>;
|
|
61
|
+
disable: z.ZodArray<z.ZodString>;
|
|
62
|
+
}, z.core.$strip>>;
|
|
63
|
+
hookCommand: z.ZodOptional<z.ZodString>;
|
|
64
|
+
}, z.core.$strip>;
|
|
65
|
+
/**
|
|
66
|
+
* The parsed {@link ClaudeSessionFlagsSchema}: the session flags only `claude` takes.
|
|
67
|
+
*/
|
|
68
|
+
export type ClaudeSessionFlags = z.infer<typeof ClaudeSessionFlagsSchema>;
|
|
69
|
+
/** {@link ClaudeSessionFlags} plus what only a fresh launch passes: the session id to preset. */
|
|
70
|
+
export interface ClaudeLaunchFlags extends ClaudeSessionFlags {
|
|
71
|
+
/**
|
|
72
|
+
* The harness's own session id to preset (`claude --session-id`), so its id and transcript
|
|
73
|
+
* are known at launch. Never with a resume. Not the caller's id: that is
|
|
74
|
+
* `SessionRequest.sessionId`.
|
|
75
|
+
*/
|
|
76
|
+
harnessSessionId?: string | undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The Claude Code hook events the caller listens to. Everything is async except
|
|
80
|
+
* `SessionEnd`, which has to finish its write before Claude exits (its budget
|
|
81
|
+
* is the hook's own `timeout`). `PreToolUse` is matched to `AskUserQuestion`
|
|
82
|
+
* alone: the question is what puts the agent in `waiting_for_input` there, and
|
|
83
|
+
* a hook on every tool call would cost a process each. `PermissionRequest` and
|
|
84
|
+
* `StopFailure` mark a tool-permission prompt and a turn that ended on an API error.
|
|
85
|
+
*/
|
|
86
|
+
export declare const CLAUDE_HOOK_EVENTS: {
|
|
87
|
+
readonly SessionStart: {
|
|
88
|
+
readonly async: true;
|
|
89
|
+
};
|
|
90
|
+
readonly UserPromptSubmit: {
|
|
91
|
+
readonly async: true;
|
|
92
|
+
};
|
|
93
|
+
readonly PreToolUse: {
|
|
94
|
+
readonly async: true;
|
|
95
|
+
readonly matcher: "AskUserQuestion";
|
|
96
|
+
};
|
|
97
|
+
readonly PermissionRequest: {
|
|
98
|
+
readonly async: true;
|
|
99
|
+
};
|
|
100
|
+
readonly PostToolUse: {
|
|
101
|
+
readonly async: true;
|
|
102
|
+
};
|
|
103
|
+
readonly PostToolUseFailure: {
|
|
104
|
+
readonly async: true;
|
|
105
|
+
};
|
|
106
|
+
readonly Stop: {
|
|
107
|
+
readonly async: true;
|
|
108
|
+
};
|
|
109
|
+
readonly StopFailure: {
|
|
110
|
+
readonly async: true;
|
|
111
|
+
};
|
|
112
|
+
readonly SubagentStart: {
|
|
113
|
+
readonly async: true;
|
|
114
|
+
};
|
|
115
|
+
readonly SubagentStop: {
|
|
116
|
+
readonly async: true;
|
|
117
|
+
};
|
|
118
|
+
readonly Notification: {
|
|
119
|
+
readonly async: true;
|
|
120
|
+
};
|
|
121
|
+
readonly PreCompact: {
|
|
122
|
+
readonly async: true;
|
|
123
|
+
};
|
|
124
|
+
readonly PostModelSwitch: {
|
|
125
|
+
readonly async: true;
|
|
126
|
+
};
|
|
127
|
+
readonly SessionEnd: {
|
|
128
|
+
readonly async: false;
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
/** The `hooks` settings object that runs `command` for every {@link CLAUDE_HOOK_EVENTS} event. */
|
|
132
|
+
export declare function claudeHookSettings(command: string): {
|
|
133
|
+
hooks: Record<string, unknown[]>;
|
|
134
|
+
};
|
|
135
|
+
/** The argv for {@link ClaudeLaunchFlags}, shared by the launches and {@link claudeResumeArgv}. */
|
|
136
|
+
export declare function claudeSessionArgv(flags: ClaudeLaunchFlags): string[];
|
|
137
|
+
/**
|
|
138
|
+
* `claude --resume=<id>` plus the launch's session flags, so a resumed session
|
|
139
|
+
* keeps its plugins and prompt pin. Does not validate `id`; `claudeResumeFlag`
|
|
140
|
+
* (`@osdlc/agent-call`) binds it with `=`.
|
|
141
|
+
*/
|
|
142
|
+
export declare function claudeResumeArgv(id: string, flags?: ClaudeSessionFlags): string[];
|
|
143
|
+
/**
|
|
144
|
+
* Whether `harness`'s CLI, `binary` (a name on PATH or an absolute path), is
|
|
145
|
+
* on PATH. A synchronous lookup that spawns nothing. Only a PATH hit counts: an
|
|
146
|
+
* installed app bundle (Cursor.app) gives a session nothing to run.
|
|
147
|
+
*/
|
|
148
|
+
export declare function harnessAvailable(_harness: Harness, binary: string): boolean;
|
|
149
|
+
/** {@link SendReceipt}'s shape, reused here under its own name: both answer "did the message get in", one via a host, one via the harness's own process/daemon directly. */
|
|
150
|
+
export type NativeSendResult = SendReceipt;
|
|
151
|
+
/** `NATIVE_SEND[harness]`, called if present; `null` — not a thrown error — when the harness has no known native-send mechanism at all (see the map's own doc for which). */
|
|
152
|
+
export declare function harnessNativeSend(harness: Harness, sessionId: string, text: string, opts?: {
|
|
153
|
+
runner?: CommandRunner | undefined;
|
|
154
|
+
}): NativeSendResult | null;
|
|
155
|
+
/** `~/.codex/sessions` (or under an injected `codexHome`, for tests). */
|
|
156
|
+
export declare function codexSessionsDir(codexHome?: string): string;
|
|
157
|
+
/**
|
|
158
|
+
* Recursively finds every `rollout-*.jsonl` file under `dir`. Missing/
|
|
159
|
+
* unreadable dirs yield `[]`. Exported so a caller that needs the transcript
|
|
160
|
+
* path can reuse this walk; {@link findCodexSessionId} returns only the id.
|
|
161
|
+
*/
|
|
162
|
+
export declare function walkRolloutFiles(dir: string): string[];
|
|
163
|
+
/** The `cwd`/session-id pair recorded in a rollout file's first (`session_meta`) line, or null if unparsable. */
|
|
164
|
+
export declare function parseRolloutMeta(firstLine: string): {
|
|
165
|
+
cwd: string;
|
|
166
|
+
sessionId: string;
|
|
167
|
+
} | null;
|
|
168
|
+
/**
|
|
169
|
+
* How far before the launch instant a rollout's creation may fall and still
|
|
170
|
+
* count as this launch's: the file name has one-second resolution, and file
|
|
171
|
+
* timestamps come from a coarser clock than `Date.now()`, so a file created
|
|
172
|
+
* just after the launch can read a little before it.
|
|
173
|
+
*/
|
|
174
|
+
export declare const ROLLOUT_CREATED_MARGIN_MS = 2000;
|
|
175
|
+
/**
|
|
176
|
+
* The instant in a rollout file name, `rollout-<YYYY-MM-DDTHH-MM-SS>-<uuid>.jsonl`,
|
|
177
|
+
* or null when the name has none. Read as LOCAL time: codex stamps the name
|
|
178
|
+
* (and the `yyyy/mm/dd` buckets) from its local clock, so this is exact only
|
|
179
|
+
* when codex ran in the same time zone as this process.
|
|
180
|
+
*/
|
|
181
|
+
export declare function rolloutNameTimeMs(path: string): number | null;
|
|
182
|
+
/**
|
|
183
|
+
* When a rollout file was created: its birthtime when the file system records
|
|
184
|
+
* one (a birthtime of 0 means it does not), else the time in its name, else its
|
|
185
|
+
* mtime. The name is read in this process's time zone ({@link rolloutNameTimeMs}),
|
|
186
|
+
* so it is off by the zone difference when codex ran elsewhere; a name time
|
|
187
|
+
* later than the file's last write (beyond the margin) cannot be right and
|
|
188
|
+
* falls through to the mtime.
|
|
189
|
+
*/
|
|
190
|
+
export declare function rolloutCreatedMs(path: string, stats: {
|
|
191
|
+
birthtimeMs: number;
|
|
192
|
+
mtimeMs: number;
|
|
193
|
+
}): number;
|
|
194
|
+
/**
|
|
195
|
+
* The session id of the newest candidate (by creation instant) created no more
|
|
196
|
+
* than {@link ROLLOUT_CREATED_MARGIN_MS} before `sinceMs` whose rollout `cwd`
|
|
197
|
+
* names the same directory as `opts.cwd` (compared as real paths, so a cwd
|
|
198
|
+
* reached through a symlink matches), or null when none qualifies. Takes
|
|
199
|
+
* canned candidates, so a test needs no rollout files.
|
|
200
|
+
*/
|
|
201
|
+
export declare function newestRolloutSessionIdSince(candidates: {
|
|
202
|
+
firstLine: string;
|
|
203
|
+
createdMs: number;
|
|
204
|
+
}[], opts: {
|
|
205
|
+
sinceMs: number;
|
|
206
|
+
cwd: string;
|
|
207
|
+
}): string | null;
|
|
208
|
+
/** How long, and with which clock, a session-id lookup keeps looking for a file not written yet. */
|
|
209
|
+
export interface SessionIdWaitOptions {
|
|
210
|
+
/** How long to keep looking, ms; 0 (the default) looks once. */
|
|
211
|
+
waitMs?: number | undefined;
|
|
212
|
+
/** Pause between looks, ms; default 250. */
|
|
213
|
+
pollMs?: number | undefined;
|
|
214
|
+
/** Blocks for the given ms; default `sleepSync`. */
|
|
215
|
+
sleep?: ((ms: number) => void) | undefined;
|
|
216
|
+
/** The clock, ms; default `Date.now`. */
|
|
217
|
+
now?: (() => number) | undefined;
|
|
218
|
+
/** Codex's home directory; default `~/.codex`. */
|
|
219
|
+
codexHome?: string | undefined;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The `yyyy/mm/dd` folders under `sessionsDir` a rollout created between
|
|
223
|
+
* `fromMs` and `toMs` can sit in. codex names a folder by its own local date,
|
|
224
|
+
* which can differ from this process's by up to {@link ROLLOUT_NAME_ZONE_SLACK_MS},
|
|
225
|
+
* so the range is widened by that much on each side (one or two days).
|
|
226
|
+
*/
|
|
227
|
+
export declare function rolloutDayDirs(sessionsDir: string, fromMs: number, toMs: number): string[];
|
|
228
|
+
/**
|
|
229
|
+
* The codex session started in `cwd` at or after `sinceMs`
|
|
230
|
+
* ({@link newestRolloutSessionIdSince}), or null.
|
|
231
|
+
*
|
|
232
|
+
* Reads codex's own on-disk session index. `~/.codex/session_index.jsonl` is
|
|
233
|
+
* NOT used: it only records threads that were given a name. Instead this scans
|
|
234
|
+
* the rollout files, `~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-*.jsonl`, in
|
|
235
|
+
* the date folders from the launch through now ({@link rolloutDayDirs}), for
|
|
236
|
+
* the session created at or after the launch whose first line, a
|
|
237
|
+
* `session_meta` record
|
|
238
|
+
* (`{"type":"session_meta","payload":{"session_id":"<uuid>","cwd":"<path>",…}}`),
|
|
239
|
+
* names `cwd`. The cwds are compared as real paths: codex records its
|
|
240
|
+
* process's directory with symlinks resolved.
|
|
241
|
+
*
|
|
242
|
+
* Selection is by the rollout's creation instant ({@link rolloutCreatedMs}),
|
|
243
|
+
* not its mtime: another codex session in the same cwd keeps appending to its
|
|
244
|
+
* own rollout, so its mtime would pass a launch-time cutoff. This is still
|
|
245
|
+
* best-effort: two codex sessions launched from the same cwd within a couple of
|
|
246
|
+
* seconds of each other (or one launched in that cwd while a waiting host holds
|
|
247
|
+
* ours) can be mistaken for each other. No local signal tells them apart.
|
|
248
|
+
*
|
|
249
|
+
* With `waitMs`, keeps looking that long for a rollout file codex has not
|
|
250
|
+
* written yet; a file ruled out once (created too early, or another cwd) is not
|
|
251
|
+
* read again. `listFiles` and `codexHome` are injectable so a test needs no
|
|
252
|
+
* real `~/.codex/`.
|
|
253
|
+
*/
|
|
254
|
+
export declare function findCodexSessionId(sinceMs: number, cwd: string, opts?: {
|
|
255
|
+
listFiles?: (() => string[]) | undefined;
|
|
256
|
+
} & SessionIdWaitOptions): string | null;
|
|
257
|
+
/** How long {@link wrapperSession} waits for the agent to record its session after a host returned early. */
|
|
258
|
+
export declare const SESSION_ID_WAIT_MS = 3000;
|
|
259
|
+
/**
|
|
260
|
+
* `argv` carrying `env`, so the vars reach the process whatever terminal runs it.
|
|
261
|
+
* POSIX: prefixed with `env [-u K ...] [K=V ...]`. win32 has no `env`, so the
|
|
262
|
+
* argv becomes a `powershell -Command` that sets (`$env:K='V'`) or clears
|
|
263
|
+
* (`Remove-Item Env:K`) each variable and then runs the original command.
|
|
264
|
+
*
|
|
265
|
+
* Windows limits (not run on Windows): `powershell` is Windows PowerShell 5.1,
|
|
266
|
+
* which passes an argument containing `"` to a native program without escaping
|
|
267
|
+
* it (the program sees it split, or with the quotes gone) and drops an empty
|
|
268
|
+
* argument; and `-Command` turns the program's exit code into 0 or 1. Every
|
|
269
|
+
* word of the command is single-quoted, even one that needs no quoting: Windows
|
|
270
|
+
* PowerShell 5.1 drops a bare `--` (the guard `harnessArgv` puts before the
|
|
271
|
+
* prompt) and splits a bare `-name.value`, which a quoted word avoids.
|
|
272
|
+
*/
|
|
273
|
+
export declare function withEnv(argv: string[], env: EnvOverrides | undefined, platform?: NodeJS.Platform): string[];
|
|
274
|
+
/**
|
|
275
|
+
* The shared body of every wrapper host's `session()`: build `harnessArgv`
|
|
276
|
+
* under `req.env`, run it, best-effort resolve a session id.
|
|
277
|
+
*
|
|
278
|
+
* Most hosts return as soon as the pane exists, before the agent has recorded
|
|
279
|
+
* its session, so after a clean launch the lookup keeps trying for
|
|
280
|
+
* `opts.waitMs` (default {@link SESSION_ID_WAIT_MS}). With `opts.hostWaited`
|
|
281
|
+
* (the host held the launch until the agent exited) it looks once.
|
|
282
|
+
*
|
|
283
|
+
* @param terminal The calling host's own `terminal()`, which opens its
|
|
284
|
+
* terminal or workspace.
|
|
285
|
+
*/
|
|
286
|
+
export declare function wrapperSession(terminal: (req: CommandRequest) => number, req: SessionRequest, opts?: SessionIdWaitOptions & {
|
|
287
|
+
hostWaited?: boolean | undefined;
|
|
288
|
+
}): {
|
|
289
|
+
exitCode: number;
|
|
290
|
+
harnessSessionId: string | null;
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* The `session` part of a host whose `terminal` part opens the harness: assign
|
|
294
|
+
* it as `session: sessionViaTerminal`. `this` is the host the part is called on,
|
|
295
|
+
* so the host's own `terminal` runs; a host that has none throws rather than
|
|
296
|
+
* start nothing. A host honouring `req.wait` returns only once the agent
|
|
297
|
+
* exited, so the session-id lookup does not wait.
|
|
298
|
+
*/
|
|
299
|
+
export declare function sessionViaTerminal(this: {
|
|
300
|
+
name?: HostName;
|
|
301
|
+
terminal?: (req: CommandRequest) => number;
|
|
302
|
+
}, req: SessionRequest): {
|
|
303
|
+
exitCode: number;
|
|
304
|
+
harnessSessionId: string | null;
|
|
305
|
+
};
|