@osdlc/agent-seat 0.0.0-stage → 0.0.1-stage

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.
@@ -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
+ };