@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.
- package/dist/claude/executor.d.ts +17 -0
- package/dist/claude/executor.js +28 -0
- package/dist/claude/models.d.ts +10 -0
- package/dist/claude/models.js +33 -0
- package/dist/claude/native-bridge.d.ts +133 -0
- package/dist/claude/native-bridge.js +299 -0
- package/dist/claude/native-hook-main.d.ts +2 -0
- package/dist/claude/native-hook-main.js +74 -0
- package/dist/claude/native-hooks.d.ts +41 -0
- package/dist/claude/native-hooks.js +73 -0
- package/dist/claude/native-integration.d.ts +213 -0
- package/dist/claude/native-integration.js +665 -0
- package/dist/claude/native-message-display-main.d.ts +2 -0
- package/dist/claude/native-message-display-main.js +51 -0
- package/dist/claude/native-status-main.d.ts +2 -0
- package/dist/claude/native-status-main.js +105 -0
- package/dist/claude/status.d.ts +23 -0
- package/dist/claude/status.js +118 -0
- package/dist/claude/transcript.d.ts +79 -0
- package/dist/claude/transcript.js +272 -0
- package/dist/claude/trust.d.ts +6 -0
- package/dist/claude/trust.js +85 -0
- package/dist/codex/rollout-synth.d.ts +37 -0
- package/dist/codex/rollout-synth.js +212 -0
- package/dist/codex-app-server/client.d.ts +138 -0
- package/dist/codex-app-server/client.js +341 -0
- package/dist/codex-app-server/forwarder.d.ts +92 -0
- package/dist/codex-app-server/forwarder.js +188 -0
- package/dist/codex-app-server/mapping.d.ts +19 -0
- package/dist/codex-app-server/mapping.js +189 -0
- package/dist/codex-app-server/protocol.d.ts +472 -0
- package/dist/codex-app-server/protocol.js +12 -0
- package/dist/codex-app-server/transport.d.ts +139 -0
- package/dist/codex-app-server/transport.js +422 -0
- package/dist/codex-app-server/ws-channel.d.ts +72 -0
- package/dist/codex-app-server/ws-channel.js +233 -0
- package/dist/codex-child-env.d.ts +1 -0
- package/dist/codex-child-env.js +27 -0
- package/dist/codex-home.d.ts +47 -0
- package/dist/codex-home.js +135 -0
- package/dist/codex-session-store.d.ts +42 -0
- package/dist/codex-session-store.js +126 -0
- package/dist/host.d.ts +324 -0
- package/dist/host.js +1323 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +17 -0
- package/dist/models-catalog.d.ts +18 -0
- package/dist/models-catalog.js +27 -0
- package/dist/runner/child.d.ts +58 -0
- package/dist/runner/child.js +268 -0
- package/dist/runner/manager.d.ts +175 -0
- package/dist/runner/manager.js +458 -0
- package/dist/runner/protocol.d.ts +195 -0
- package/dist/runner/protocol.js +41 -0
- package/dist/runner/transport.d.ts +36 -0
- package/dist/runner/transport.js +72 -0
- package/dist/runner-main.d.ts +2 -0
- package/dist/runner-main.js +61 -0
- package/dist/runtime-status.d.ts +16 -0
- package/dist/runtime-status.js +80 -0
- package/dist/terminal/claude-tui.d.ts +27 -0
- package/dist/terminal/claude-tui.js +13 -0
- package/dist/terminal/codex-tui.d.ts +54 -0
- package/dist/terminal/codex-tui.js +26 -0
- package/dist/terminal/registry.d.ts +42 -0
- package/dist/terminal/registry.js +70 -0
- package/dist/terminal/tmux.d.ts +150 -0
- package/dist/terminal/tmux.js +364 -0
- 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>;
|