@rynx-ai/runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/claude/executor.d.ts +17 -0
  2. package/dist/claude/executor.js +28 -0
  3. package/dist/claude/models.d.ts +10 -0
  4. package/dist/claude/models.js +33 -0
  5. package/dist/claude/native-bridge.d.ts +133 -0
  6. package/dist/claude/native-bridge.js +299 -0
  7. package/dist/claude/native-hook-main.d.ts +2 -0
  8. package/dist/claude/native-hook-main.js +74 -0
  9. package/dist/claude/native-hooks.d.ts +41 -0
  10. package/dist/claude/native-hooks.js +73 -0
  11. package/dist/claude/native-integration.d.ts +213 -0
  12. package/dist/claude/native-integration.js +665 -0
  13. package/dist/claude/native-message-display-main.d.ts +2 -0
  14. package/dist/claude/native-message-display-main.js +51 -0
  15. package/dist/claude/native-status-main.d.ts +2 -0
  16. package/dist/claude/native-status-main.js +105 -0
  17. package/dist/claude/status.d.ts +23 -0
  18. package/dist/claude/status.js +118 -0
  19. package/dist/claude/transcript.d.ts +79 -0
  20. package/dist/claude/transcript.js +272 -0
  21. package/dist/claude/trust.d.ts +6 -0
  22. package/dist/claude/trust.js +85 -0
  23. package/dist/codex/rollout-synth.d.ts +37 -0
  24. package/dist/codex/rollout-synth.js +212 -0
  25. package/dist/codex-app-server/client.d.ts +138 -0
  26. package/dist/codex-app-server/client.js +341 -0
  27. package/dist/codex-app-server/forwarder.d.ts +92 -0
  28. package/dist/codex-app-server/forwarder.js +188 -0
  29. package/dist/codex-app-server/mapping.d.ts +19 -0
  30. package/dist/codex-app-server/mapping.js +189 -0
  31. package/dist/codex-app-server/protocol.d.ts +472 -0
  32. package/dist/codex-app-server/protocol.js +12 -0
  33. package/dist/codex-app-server/transport.d.ts +139 -0
  34. package/dist/codex-app-server/transport.js +422 -0
  35. package/dist/codex-app-server/ws-channel.d.ts +72 -0
  36. package/dist/codex-app-server/ws-channel.js +233 -0
  37. package/dist/codex-child-env.d.ts +1 -0
  38. package/dist/codex-child-env.js +27 -0
  39. package/dist/codex-home.d.ts +47 -0
  40. package/dist/codex-home.js +135 -0
  41. package/dist/codex-session-store.d.ts +42 -0
  42. package/dist/codex-session-store.js +126 -0
  43. package/dist/host.d.ts +324 -0
  44. package/dist/host.js +1323 -0
  45. package/dist/index.d.ts +18 -0
  46. package/dist/index.js +17 -0
  47. package/dist/models-catalog.d.ts +18 -0
  48. package/dist/models-catalog.js +27 -0
  49. package/dist/runner/child.d.ts +58 -0
  50. package/dist/runner/child.js +268 -0
  51. package/dist/runner/manager.d.ts +175 -0
  52. package/dist/runner/manager.js +458 -0
  53. package/dist/runner/protocol.d.ts +195 -0
  54. package/dist/runner/protocol.js +41 -0
  55. package/dist/runner/transport.d.ts +36 -0
  56. package/dist/runner/transport.js +72 -0
  57. package/dist/runner-main.d.ts +2 -0
  58. package/dist/runner-main.js +61 -0
  59. package/dist/runtime-status.d.ts +16 -0
  60. package/dist/runtime-status.js +80 -0
  61. package/dist/terminal/claude-tui.d.ts +27 -0
  62. package/dist/terminal/claude-tui.js +13 -0
  63. package/dist/terminal/codex-tui.d.ts +54 -0
  64. package/dist/terminal/codex-tui.js +26 -0
  65. package/dist/terminal/registry.d.ts +42 -0
  66. package/dist/terminal/registry.js +70 -0
  67. package/dist/terminal/tmux.d.ts +150 -0
  68. package/dist/terminal/tmux.js +364 -0
  69. package/package.json +32 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Build the Claude Code `--settings` JSON that registers rynx's claude-native
3
+ * hooks. Each hook is a `command` that runs a standalone node entrypoint
4
+ * ({@link ./native-hook-main.ts}) against the session's bridge dir; the
5
+ * subprocess appends the hook payload to `hooks.jsonl` (observer) or POSTs to
6
+ * the daemon and relays the verdict (PermissionRequest).
7
+ *
8
+ * Registered here (the core turn-framing + approval subset; mirrors omnigent's
9
+ * `build_hook_settings`):
10
+ * - `SessionStart` → discovery (transcript_path + claude session id)
11
+ * - `Stop` / `StopFailure` → turn close (idle / failed)
12
+ * - `PermissionRequest` → approvals (only when a serverUrl is provided)
13
+ *
14
+ * Turn OPEN is transcript-driven (the `role:user` record), so `UserPromptSubmit`
15
+ * is intentionally NOT registered. `MessageDisplay` (streaming) and `statusLine`
16
+ * (context/cost) are opt-in; the task/todo hooks are added by their later phase.
17
+ */
18
+ import { fileURLToPath } from "node:url";
19
+ /** Claude waits this long (seconds) for the PermissionRequest hook's verdict — a
20
+ * full day is effectively wait-forever for an interactive approval, matching
21
+ * omnigent (its default ~60s command-hook timeout would kill the long-poll). */
22
+ const PERMISSION_HOOK_TIMEOUT_S = 86_400;
23
+ /** Absolute path to the built standalone hook entrypoint (sibling `.js`). */
24
+ export function resolveHookEntryPath() {
25
+ return fileURLToPath(new URL("./native-hook-main.js", import.meta.url));
26
+ }
27
+ /** Absolute path to the built MessageDisplay hook entrypoint (sibling `.js`). */
28
+ export function resolveMessageDisplayEntryPath() {
29
+ return fileURLToPath(new URL("./native-message-display-main.js", import.meta.url));
30
+ }
31
+ /** Absolute path to the built statusLine entrypoint (sibling `.js`). */
32
+ export function resolveStatusEntryPath() {
33
+ return fileURLToPath(new URL("./native-status-main.js", import.meta.url));
34
+ }
35
+ /** Quote one argv token for a POSIX shell command string (single-quote wrap). */
36
+ function shQuote(token) {
37
+ return `'${token.replace(/'/g, `'\\''`)}'`;
38
+ }
39
+ function shJoin(parts) {
40
+ return parts.map(shQuote).join(" ");
41
+ }
42
+ export function buildClaudeHookSettings(options) {
43
+ const node = options.nodePath ?? process.execPath;
44
+ const entry = options.hookEntryPath ?? resolveHookEntryPath();
45
+ const observer = shJoin([node, entry, "--bridge-dir", options.bridgeDir]);
46
+ const observerHook = { type: "command", command: observer };
47
+ const hooks = {
48
+ SessionStart: [{ hooks: [observerHook] }],
49
+ Stop: [{ hooks: [observerHook] }],
50
+ StopFailure: [{ hooks: [observerHook] }],
51
+ };
52
+ if (options.permission) {
53
+ const permission = shJoin([node, entry, "permission-request", "--bridge-dir", options.bridgeDir]);
54
+ hooks.PermissionRequest = [
55
+ { hooks: [{ type: "command", command: permission, timeout: PERMISSION_HOOK_TIMEOUT_S }] },
56
+ ];
57
+ }
58
+ if (options.messageDisplay) {
59
+ const md = options.messageDisplayEntryPath ?? resolveMessageDisplayEntryPath();
60
+ hooks.MessageDisplay = [
61
+ { hooks: [{ type: "command", command: shJoin([node, md, "--bridge-dir", options.bridgeDir]) }] },
62
+ ];
63
+ }
64
+ const settings = { hooks };
65
+ if (options.statusLine) {
66
+ const status = options.statusEntryPath ?? resolveStatusEntryPath();
67
+ settings.statusLine = {
68
+ type: "command",
69
+ command: shJoin([node, status, "--bridge-dir", options.bridgeDir]),
70
+ };
71
+ }
72
+ return settings;
73
+ }
@@ -0,0 +1,213 @@
1
+ import type { AgentEvent, TerminalCommandData, TodoItem } from "@rynx-ai/core";
2
+ /** Mirror sink — the same shape as `CodexForwarderSink` so the host reuses one
3
+ * per-turn normalizer wiring for both runtimes. */
4
+ export interface ClaudeForwarderSink {
5
+ /** A turn began; `turnId` (the user record uuid) derives a stable responseId. */
6
+ onTurnStart(turnId?: string): void;
7
+ /** The user's prompt text (a `role:user` conversation message — not identity). */
8
+ onUserMessage(text: string): void;
9
+ /** A local shell command the user ran in the TUI (claude `!` bash mode), framed
10
+ * as its own mini-turn (an `onTurnStart`/`onTurnEnd` pair brackets this call). */
11
+ onTerminalCommand(cmd: TerminalCommandData): void;
12
+ /** The agent's task list changed (claude `TaskCreate`/`TaskUpdate`) — the WHOLE
13
+ * current list (a session-level snapshot, not tied to a turn). */
14
+ onTodos(todos: TodoItem[]): void;
15
+ /** One mapped event within the current turn. */
16
+ onEvent(event: AgentEvent): void;
17
+ /** The current turn finished (a new user prompt, or the inactivity backstop).
18
+ * `usage` carries the latest statusLine context/cost snapshot, when captured. */
19
+ onTurnEnd(usage?: Record<string, unknown>): void;
20
+ /** The runtime went idle (Stop hook) — surface idle status WITHOUT finalizing
21
+ * the turn. claude fires Stop around the same time it flushes the final
22
+ * assistant record and the two orderings race; finalizing here would split a
23
+ * late assistant record into its own turn. The turn is finalized by the next
24
+ * user prompt or the inactivity backstop, so a late record still joins it. */
25
+ onIdle(): void;
26
+ /** The current turn failed on the runtime (StopFailure hook). */
27
+ onTurnError(error: Error): void;
28
+ /** Fired once SessionStart reveals claude's session id + transcript path, so
29
+ * the host can persist the id and release its readiness gate. */
30
+ onSessionDiscovered?(claudeSessionId: string, transcriptPath: string): void;
31
+ /** The claude session rotated (`/clear` → fresh, `/fork` → derived): a new
32
+ * SessionStart reported a new session id + transcript. The forwarder has
33
+ * already re-pointed to the new transcript and reset its per-session state; the
34
+ * host mints a fresh rynx session and re-targets the mirror to it. */
35
+ onSessionRotated?(kind: "clear" | "fork", claudeSessionId: string, transcriptPath: string): void;
36
+ }
37
+ export interface ClaudeLiveSessionOptions {
38
+ /** The session's bridge dir (`hooks.jsonl` lives here). */
39
+ bridgeDir: string;
40
+ sink: ClaudeForwarderSink;
41
+ /** Resume: a known transcript path to tail immediately (else await SessionStart). */
42
+ transcriptPath?: string;
43
+ /** Resume: the claude session id for that transcript — used to match the
44
+ * persisted tail cursor so a re-launch resumes instead of re-mirroring. */
45
+ claudeSessionId?: string;
46
+ /** Poll interval (ms). The files are append-only, so polling is simplest. */
47
+ pollMs?: number;
48
+ /** Inactivity (ms) FALLBACK close for a turn whose Stop hook never fired. */
49
+ idleCloseMs?: number;
50
+ /** Grace (ms) after the Stop hook before closing — lets a late assistant record
51
+ * flush still join the turn (Stop and the flush race). */
52
+ stopGraceMs?: number;
53
+ now?: () => number;
54
+ }
55
+ export declare class ClaudeLiveSession {
56
+ private readonly bridgeDir;
57
+ private readonly sink;
58
+ private readonly pollMs;
59
+ private readonly idleCloseMs;
60
+ private readonly stopGraceMs;
61
+ private readonly now;
62
+ private started;
63
+ private stopped;
64
+ private hooksOffset;
65
+ private transcriptOffset;
66
+ /** Secondary dedup: source ids (record uuids) already forwarded, so a re-read
67
+ * (fingerprint reset / mid-poll death) doesn't re-emit. Persisted in the
68
+ * forwarder state; bounded there. */
69
+ private readonly seenSourceIds;
70
+ private transcriptPath?;
71
+ private discovered;
72
+ /** claude's current session uuid (changes on `/clear`·`/fork`·resume). */
73
+ private currentClaudeSessionId?;
74
+ /** Every claude session uuid seen — a resume into an UNSEEN id + a forkedFrom
75
+ * marker signals a `/fork` (vs. resuming a known branch). */
76
+ private readonly seenClaudeSessionIds;
77
+ private turnOpen;
78
+ private currentTurnId?;
79
+ private lastActivityAt;
80
+ /** When the Stop hook fired (null = not yet) — drives the grace-period close. */
81
+ private stopPendingAt;
82
+ /** Open tool-call ids (start seen, end not yet) — suppresses the idle backstop. */
83
+ private readonly openToolIds;
84
+ /** Sub-agent (Task) ids already forwarded — a Task tool_result is processed once. */
85
+ private readonly seenSubagents;
86
+ /** The agent's task list, keyed by task id in creation order (claude
87
+ * `TaskCreate`/`TaskUpdate`). Emitted whole as a snapshot on any change. */
88
+ private readonly todos;
89
+ /** Latest statusLine context/cost snapshot (context_window + cost); attached to
90
+ * a turn's usage on close. undefined until the statusLine hook first fires. */
91
+ private latestStatus?;
92
+ private deltasOffset;
93
+ /** MessageDisplay message ids in first-seen order, FIFO-mapped onto the
94
+ * transcript's assistant-text records so a streamed message and its final
95
+ * item share an itemId (message_id is absent from the transcript). */
96
+ private readonly messageIdQueue;
97
+ private readonly seenMessageIds;
98
+ constructor(opts: ClaudeLiveSessionOptions);
99
+ /**
100
+ * Restore the durable forwarder cursor for `transcriptPath` (omnigent's
101
+ * `_validated_transcript_state`). A cursor for a DIFFERENT file is ignored (a new
102
+ * session starts at 0). A matching cursor whose fingerprint still validates
103
+ * resumes at its `byteOffset`; a MISMATCH (the file was truncated/replaced) skips
104
+ * to the current EOF — never seek into a stale offset — while preserving
105
+ * `seenSourceIds` so nothing re-emits.
106
+ */
107
+ private restoreForwardState;
108
+ /** Persist the durable forwarder cursor (byte offset + seen ids + fingerprint). */
109
+ private persistForwardState;
110
+ /** True once SessionStart bound the transcript (or a resume path was given). */
111
+ isReady(): boolean;
112
+ start(): void;
113
+ stop(): void;
114
+ /** One poll cycle (hooks → transcript → idle backstop). Exposed for tests to
115
+ * drive deterministically; the async {@link loop} just calls it on an interval. */
116
+ tick(): void;
117
+ private loop;
118
+ private pollHooks;
119
+ private handleHook;
120
+ /** SessionStart drives discovery (first) and rotation (a later one with a NEW
121
+ * session id + transcript). `/clear` → source="clear"; `/fork` → source="resume"
122
+ * into an unseen id with a forkedFrom marker. Any other new-transcript
123
+ * SessionStart (compact/resume/startup) is followed WITHOUT rotating. */
124
+ private handleSessionStart;
125
+ /** Switch the tailed transcript and reset per-session accumulators (the bridge
126
+ * files — hooks/deltas/status — are shared by the same claude process across a
127
+ * rotation, so their cursors are NOT reset). */
128
+ private repointTranscript;
129
+ private pollTranscript;
130
+ private handleRecord;
131
+ /** Fold a `TaskCreate` (new pending task) or `TaskUpdate` (status/subject/delete)
132
+ * record into the task list; on any change emit the whole list as a snapshot. */
133
+ private maybeUpdateTodos;
134
+ /** On a `Task` tool_result, replay the sub-agent's own transcript
135
+ * (`subagents/agent-<agentId>.jsonl`) as events tagged with the parent Task
136
+ * tool-use id, so the canonical layer nests them under that call. Fired once
137
+ * per sub-agent (at tool_result time the file is complete — no live race). */
138
+ private maybeForwardSubagent;
139
+ /** Tail streamed assistant-text chunks (MessageDisplay) into live `token`
140
+ * events. Guarded on an open turn — deltas belong to the turn the user record
141
+ * opened; the offset advances only once processed. */
142
+ private pollDeltas;
143
+ /** Refresh the latest statusLine context/cost snapshot (a last-writer-wins file
144
+ * the statusLine hook overwrites on every TUI render). */
145
+ private pollStatus;
146
+ /** A usage record from the latest statusLine snapshot (snake_case, the keys the
147
+ * normalizer's `turn_completed` + the web `readUsageTokens` read), or undefined
148
+ * if the statusLine hook has not fired yet. */
149
+ private statusUsage;
150
+ /** Remap an assistant-text `message_completed` onto the FIFO-matched
151
+ * MessageDisplay message_id, so its streamed deltas and this final item share
152
+ * an itemId. No queued delta (MessageDisplay didn't fire) → keep the transcript id. */
153
+ private remapMessageItem;
154
+ private trackTool;
155
+ private ensureTurn;
156
+ /** Mirror a local `!` command as its own mini-turn: close any open turn, then
157
+ * open→emit→close so it groups as one response with a stable id (the record
158
+ * uuid) and never lingers "running" (input+output are complete in one record). */
159
+ private emitTerminalCommand;
160
+ /** The web Stop button interrupted this session (host sent Escape). claude
161
+ * records the interrupt in its own transcript but may not fire a Stop hook when
162
+ * the response was cancelled before it produced output — leaving the turn open
163
+ * and the UI spinner stuck until the long inactivity backstop. Treat the
164
+ * interrupt like a Stop: surface idle now (clears the spinner) and schedule the
165
+ * turn's close via the same short grace. The Escape has stopped claude, so this
166
+ * does not race a still-running response. */
167
+ noteInterrupted(): void;
168
+ /** Whether a claude turn is currently open (a response is running / pending).
169
+ * The web Stop button gates its interrupt on this so an idle pane is never
170
+ * sent an Escape — an Escape into idle claude submits an empty turn, which
171
+ * claude answers with a stray "No response requested." bubble. */
172
+ isTurnOpen(): boolean;
173
+ private closeTurn;
174
+ private closeTurnError;
175
+ private maybeIdleClose;
176
+ }
177
+ /** The tmux-pane operations claude-native injection needs (a subset of
178
+ * {@link import("../terminal/tmux.js").TmuxTerminal}). The runner-child hands
179
+ * this to the host after launching the pane, since the host doesn't own tmux. */
180
+ export interface TerminalInjector {
181
+ capturePane(): string;
182
+ clearInputLine(): void;
183
+ paste(text: string): void;
184
+ sendEnter(): void;
185
+ /** Interrupt the running turn (Escape) — the web Stop button (omnigent). */
186
+ interrupt(): void;
187
+ }
188
+ export interface InjectViaTerminalOptions {
189
+ promptGlyph?: string;
190
+ promptTimeoutMs?: number;
191
+ pasteCommitMs?: number;
192
+ settleMs?: number;
193
+ submitVerifyMs?: number;
194
+ submitRetryMs?: number;
195
+ pollMs?: number;
196
+ now?: () => number;
197
+ sleep?: (ms: number) => Promise<void>;
198
+ /** Abort the paste/submit sequence (the web Stop button). On abort the input
199
+ * line is cleared and injection returns false — the message is NOT submitted. */
200
+ signal?: AbortSignal;
201
+ }
202
+ /**
203
+ * Deliver `text` into a claude TUI pane, the omnigent recipe:
204
+ * ready-gate (poll for `❯`) → clear leftover → bracketed paste (+ trailing
205
+ * newline) → wait for the draft to land → settle → submit Enter → verify the
206
+ * draft left the box (re-send Enter while it hasn't).
207
+ *
208
+ * THROWS if the prompt never appears within the ready-gate window (omnigent
209
+ * `_wait_for_claude_prompt_ready` RAISE) — a not-ready pane is a hard error the
210
+ * caller reports, NOT a signal to fall through to a second output path. Returns
211
+ * `true` once the message is submitted (best-effort even if submit-verify times out).
212
+ */
213
+ export declare function injectViaTerminal(injector: TerminalInjector, text: string, opts?: InjectViaTerminalOptions): Promise<boolean>;