@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,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-accept Claude Code's first-run TUI gates so a daemon-spawned `claude`
|
|
3
|
+
* never blocks on a prompt nobody is at the terminal to answer.
|
|
4
|
+
*
|
|
5
|
+
* On a fresh context Claude blocks on prompts that do NOT fire any hook (so we
|
|
6
|
+
* can't auto-answer them in-band):
|
|
7
|
+
* - global onboarding (theme/login) → top-level `hasCompletedOnboarding`
|
|
8
|
+
* - per-dir "trust this folder?" → `projects[<abs cwd>].hasTrustDialogAccepted`
|
|
9
|
+
* - "Allow external CLAUDE.md imports?" (the one that bit us — the global
|
|
10
|
+
* `~/.claude/CLAUDE.md` imports files outside the cwd) →
|
|
11
|
+
* `projects[<abs cwd>].hasClaudeMdExternalIncludesApproved`
|
|
12
|
+
*
|
|
13
|
+
* We seed just these keys idempotently and preserve all other `~/.claude.json`
|
|
14
|
+
* state (the user's onboarding choices, project history, MCP config, OAuth
|
|
15
|
+
* account). This does NOT skip per-tool permission prompts — those are hookable
|
|
16
|
+
* and handled elsewhere; only the unhookable startup gates are pre-accepted.
|
|
17
|
+
*
|
|
18
|
+
* Concurrency: a read-modify-write of a file Claude also rewrites. We run it
|
|
19
|
+
* once before spawning (Claude isn't writing for this session yet) and replace
|
|
20
|
+
* atomically; a last-writer-wins race only re-shows a prompt, which a relaunch
|
|
21
|
+
* clears.
|
|
22
|
+
*/
|
|
23
|
+
import { chmodSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
26
|
+
const PROJECT_TRUST_KEYS = {
|
|
27
|
+
hasTrustDialogAccepted: true,
|
|
28
|
+
hasClaudeMdExternalIncludesApproved: true,
|
|
29
|
+
hasClaudeMdExternalIncludesWarningShown: true,
|
|
30
|
+
};
|
|
31
|
+
function claudeConfigJsonPath() {
|
|
32
|
+
const dir = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
33
|
+
return dir ? join(dir, ".claude.json") : join(homedir(), ".claude.json");
|
|
34
|
+
}
|
|
35
|
+
function isPlainObject(value) {
|
|
36
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Ensure `cwd` is pre-trusted in `~/.claude.json`. Idempotent; throws (rather
|
|
40
|
+
* than clobbering) if an existing config isn't shaped as expected. Opt out with
|
|
41
|
+
* `RYNX_CLAUDE_PRESEED_TRUST=0`.
|
|
42
|
+
*/
|
|
43
|
+
export function ensureProjectTrusted(cwd, configPath = claudeConfigJsonPath()) {
|
|
44
|
+
const flag = process.env.RYNX_CLAUDE_PRESEED_TRUST;
|
|
45
|
+
if (flag === "0" || flag === "false")
|
|
46
|
+
return;
|
|
47
|
+
let data = {};
|
|
48
|
+
if (existsSync(configPath)) {
|
|
49
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
50
|
+
if (!isPlainObject(parsed)) {
|
|
51
|
+
throw new Error(`${configPath} is not a JSON object; refusing to overwrite.`);
|
|
52
|
+
}
|
|
53
|
+
data = parsed;
|
|
54
|
+
}
|
|
55
|
+
let changed = false;
|
|
56
|
+
if (data.hasCompletedOnboarding !== true) {
|
|
57
|
+
data.hasCompletedOnboarding = true;
|
|
58
|
+
changed = true;
|
|
59
|
+
}
|
|
60
|
+
if (data.projects === undefined)
|
|
61
|
+
data.projects = {};
|
|
62
|
+
if (!isPlainObject(data.projects)) {
|
|
63
|
+
throw new Error(`${configPath} 'projects' is not a JSON object; refusing to overwrite.`);
|
|
64
|
+
}
|
|
65
|
+
const projects = data.projects;
|
|
66
|
+
const key = resolve(cwd);
|
|
67
|
+
if (projects[key] === undefined)
|
|
68
|
+
projects[key] = {};
|
|
69
|
+
if (!isPlainObject(projects[key])) {
|
|
70
|
+
throw new Error(`${configPath} projects[${key}] is not a JSON object; refusing to overwrite.`);
|
|
71
|
+
}
|
|
72
|
+
const entry = projects[key];
|
|
73
|
+
for (const [k, v] of Object.entries(PROJECT_TRUST_KEYS)) {
|
|
74
|
+
if (entry[k] !== v) {
|
|
75
|
+
entry[k] = v;
|
|
76
|
+
changed = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!changed)
|
|
80
|
+
return;
|
|
81
|
+
const tmp = join(dirname(configPath), `.${basename(configPath)}.${process.pid}.tmp`);
|
|
82
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
83
|
+
chmodSync(tmp, 0o600); // pin owner-only even under a permissive umask
|
|
84
|
+
renameSync(tmp, configPath);
|
|
85
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SessionItem } from "@rynx-ai/core";
|
|
2
|
+
export interface SynthesizeRolloutOptions {
|
|
3
|
+
threadId: string;
|
|
4
|
+
cwd: string;
|
|
5
|
+
items: SessionItem[];
|
|
6
|
+
/** The rynx session (localThreadId) — resolves the per-session private CODEX_HOME
|
|
7
|
+
* (`codexHomePath(sessionId)`). Required unless `codexHome` is passed directly. */
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
/** Override the private CODEX_HOME (tests, or a pre-resolved home). */
|
|
10
|
+
codexHome?: string;
|
|
11
|
+
/** codex CLI version for `session_meta` (informational for ≥0.133; presence
|
|
12
|
+
* matters). */
|
|
13
|
+
cliVersion?: string;
|
|
14
|
+
/** `session_meta.model_provider` — an empty/unresolvable value silently drops the
|
|
15
|
+
* carried history on resume. Defaults to `openai`. */
|
|
16
|
+
modelProvider?: string;
|
|
17
|
+
/** Clock injection (tests). */
|
|
18
|
+
now?: () => number;
|
|
19
|
+
}
|
|
20
|
+
interface RolloutRecord {
|
|
21
|
+
timestamp: string;
|
|
22
|
+
type: "session_meta" | "turn_context" | "response_item" | "event_msg";
|
|
23
|
+
payload: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
/** Locate an existing rollout for `threadId` under `<codexHome>/sessions/**`. */
|
|
26
|
+
export declare function findCodexRollout(codexHome: string, threadId: string): string | null;
|
|
27
|
+
/** Build the ordered rollout records (session_meta first, then per-turn
|
|
28
|
+
* turn_context + per-item response_item + per-message event_msg). */
|
|
29
|
+
export declare function buildRolloutRecords(opts: SynthesizeRolloutOptions): RolloutRecord[];
|
|
30
|
+
/**
|
|
31
|
+
* Ensure a resumable codex rollout exists for `threadId`. No-op if one is already
|
|
32
|
+
* present (codex owns its own runtime rollouts). Otherwise synthesize one from the
|
|
33
|
+
* session items and write it atomically. Returns `"exists"`, `"written"`, or
|
|
34
|
+
* `"skipped"` (invalid thread id / nothing to carry). Best-effort: never throws.
|
|
35
|
+
*/
|
|
36
|
+
export declare function ensureCodexResumeRollout(opts: SynthesizeRolloutOptions): "exists" | "written" | "skipped";
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthesize a codex rollout file from rynx's canonical session log, so
|
|
3
|
+
* `codex --remote resume <threadId>` works when the local rollout is missing
|
|
4
|
+
* (fork / worktree / cross-machine). Ports omnigent's
|
|
5
|
+
* `_ensure_local_codex_resume_rollout`, adapted to rynx: the daemon (control-api)
|
|
6
|
+
* has BOTH the session items (`SessionLogStore`) and — since the private CODEX_HOME
|
|
7
|
+
* is a deterministic uid-scoped path — the app-server's rollout dir, so this runs
|
|
8
|
+
* entirely daemon-side with no runner round-trip.
|
|
9
|
+
*
|
|
10
|
+
* The record shapes match codex 0.142.5 (empirically captured). The minimal set
|
|
11
|
+
* codex needs to resume with VISIBLE turns is: a `session_meta` first line, then
|
|
12
|
+
* per item a `response_item`, and — critically — an `event_msg` mirror per message
|
|
13
|
+
* (codex ≥0.136 renders an empty thread without them). `turn_context` groups items
|
|
14
|
+
* into turns.
|
|
15
|
+
*/
|
|
16
|
+
import { copyFileSync, mkdirSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { join, relative } from "node:path";
|
|
18
|
+
import { codexHomePath, legacyCodexHomePath } from "../codex-home.js";
|
|
19
|
+
/** codex validates the thread id straight into a filename + resume arg. */
|
|
20
|
+
const THREAD_ID_RE = /^[0-9a-fA-F-]+$/;
|
|
21
|
+
/** Locate an existing rollout for `threadId` under `<codexHome>/sessions/**`. */
|
|
22
|
+
export function findCodexRollout(codexHome, threadId) {
|
|
23
|
+
const root = join(codexHome, "sessions");
|
|
24
|
+
const suffix = `-${threadId}.jsonl`;
|
|
25
|
+
const stack = [root];
|
|
26
|
+
while (stack.length) {
|
|
27
|
+
const dir = stack.pop();
|
|
28
|
+
let entries;
|
|
29
|
+
try {
|
|
30
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
continue; // missing dir — fine
|
|
34
|
+
}
|
|
35
|
+
for (const e of entries) {
|
|
36
|
+
const full = join(dir, e.name);
|
|
37
|
+
if (e.isDirectory())
|
|
38
|
+
stack.push(full);
|
|
39
|
+
else if (e.name.startsWith("rollout-") && e.name.endsWith(suffix))
|
|
40
|
+
return full;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Back-compat: before per-session homes, rollouts lived under one shared
|
|
47
|
+
* {@link legacyCodexHomePath}. If a session's rollout is only there, copy it forward
|
|
48
|
+
* into this session's home (preserving the `sessions/YYYY/MM/DD/` layout codex
|
|
49
|
+
* expects) so the per-session app-server can resume it. Best-effort; gated by
|
|
50
|
+
* `RYNX_CODEX_HOME_LEGACY_FALLBACK` (default on; set `0`/`false` to disable).
|
|
51
|
+
*/
|
|
52
|
+
function adoptLegacyRollout(codexHome, threadId) {
|
|
53
|
+
const flag = process.env.RYNX_CODEX_HOME_LEGACY_FALLBACK?.trim().toLowerCase();
|
|
54
|
+
if (flag === "0" || flag === "false")
|
|
55
|
+
return false;
|
|
56
|
+
const legacy = legacyCodexHomePath();
|
|
57
|
+
if (legacy === codexHome)
|
|
58
|
+
return false;
|
|
59
|
+
const src = findCodexRollout(legacy, threadId);
|
|
60
|
+
if (!src)
|
|
61
|
+
return false;
|
|
62
|
+
const rel = relative(join(legacy, "sessions"), src);
|
|
63
|
+
const dst = join(codexHome, "sessions", rel);
|
|
64
|
+
try {
|
|
65
|
+
mkdirSync(join(dst, ".."), { recursive: true, mode: 0o700 });
|
|
66
|
+
copyFileSync(src, dst);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function iso(ms) {
|
|
74
|
+
return new Date(ms).toISOString();
|
|
75
|
+
}
|
|
76
|
+
/** `resp_codex_<turnId>` → `<turnId>` (rynx derives the responseId from the codex
|
|
77
|
+
* turn id); anything else is used verbatim as a stable turn key. */
|
|
78
|
+
function turnIdOf(responseId) {
|
|
79
|
+
return responseId.startsWith("resp_codex_") ? responseId.slice("resp_codex_".length) : responseId;
|
|
80
|
+
}
|
|
81
|
+
function textOf(item) {
|
|
82
|
+
return item.data.content.map((p) => p.text).join("");
|
|
83
|
+
}
|
|
84
|
+
/** Convert one canonical {@link SessionItem} to its codex `response_item` payload,
|
|
85
|
+
* or null for types codex doesn't carry (reasoning/terminal_command/error). */
|
|
86
|
+
function responseItemPayload(item) {
|
|
87
|
+
switch (item.type) {
|
|
88
|
+
case "message": {
|
|
89
|
+
const apiType = item.data.role === "assistant" ? "output_text" : "input_text";
|
|
90
|
+
const content = item.data.content
|
|
91
|
+
.filter((p) => p.text)
|
|
92
|
+
.map((p) => ({ type: apiType, text: p.text }));
|
|
93
|
+
if (content.length === 0)
|
|
94
|
+
return null;
|
|
95
|
+
return { type: "message", role: item.data.role, content };
|
|
96
|
+
}
|
|
97
|
+
case "function_call":
|
|
98
|
+
return {
|
|
99
|
+
type: "function_call",
|
|
100
|
+
name: item.data.name,
|
|
101
|
+
call_id: item.data.callId,
|
|
102
|
+
arguments: item.data.arguments,
|
|
103
|
+
};
|
|
104
|
+
case "function_call_output":
|
|
105
|
+
return { type: "function_call_output", call_id: item.data.callId, output: item.data.output };
|
|
106
|
+
default:
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** The `event_msg` mirror for a message (required for a VISIBLE turn on codex ≥0.136). */
|
|
111
|
+
function eventMsgPayload(item) {
|
|
112
|
+
const message = textOf(item).trim();
|
|
113
|
+
if (!message)
|
|
114
|
+
return null;
|
|
115
|
+
if (item.data.role === "user") {
|
|
116
|
+
return { type: "user_message", message, images: [], local_images: [], text_elements: [] };
|
|
117
|
+
}
|
|
118
|
+
if (item.data.role === "assistant") {
|
|
119
|
+
return { type: "agent_message", message, phase: "final_answer", memory_citation: null };
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/** Build the ordered rollout records (session_meta first, then per-turn
|
|
124
|
+
* turn_context + per-item response_item + per-message event_msg). */
|
|
125
|
+
export function buildRolloutRecords(opts) {
|
|
126
|
+
const now = opts.now ?? (() => Date.now());
|
|
127
|
+
const modelProvider = opts.modelProvider ?? "openai";
|
|
128
|
+
const cliVersion = opts.cliVersion ?? "0.0.0";
|
|
129
|
+
const metaTs = iso(opts.items[0]?.createdAt ?? now());
|
|
130
|
+
const records = [
|
|
131
|
+
{
|
|
132
|
+
timestamp: metaTs,
|
|
133
|
+
type: "session_meta",
|
|
134
|
+
payload: {
|
|
135
|
+
id: opts.threadId,
|
|
136
|
+
session_id: opts.threadId,
|
|
137
|
+
timestamp: metaTs,
|
|
138
|
+
cwd: opts.cwd,
|
|
139
|
+
originator: "rynx",
|
|
140
|
+
cli_version: cliVersion,
|
|
141
|
+
source: "rynx",
|
|
142
|
+
thread_source: "user",
|
|
143
|
+
model_provider: modelProvider,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
const seenTurns = new Set();
|
|
148
|
+
for (const item of opts.items) {
|
|
149
|
+
const payload = responseItemPayload(item);
|
|
150
|
+
if (!payload)
|
|
151
|
+
continue;
|
|
152
|
+
const ts = iso(item.createdAt);
|
|
153
|
+
const turnId = turnIdOf(item.responseId);
|
|
154
|
+
if (!seenTurns.has(turnId)) {
|
|
155
|
+
seenTurns.add(turnId);
|
|
156
|
+
records.push({
|
|
157
|
+
timestamp: ts,
|
|
158
|
+
type: "turn_context",
|
|
159
|
+
payload: { turn_id: turnId, cwd: opts.cwd, approval_policy: "on-request" },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
records.push({ timestamp: ts, type: "response_item", payload });
|
|
163
|
+
if (item.type === "message") {
|
|
164
|
+
const evt = eventMsgPayload(item);
|
|
165
|
+
if (evt)
|
|
166
|
+
records.push({ timestamp: ts, type: "event_msg", payload: evt });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return records;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Ensure a resumable codex rollout exists for `threadId`. No-op if one is already
|
|
173
|
+
* present (codex owns its own runtime rollouts). Otherwise synthesize one from the
|
|
174
|
+
* session items and write it atomically. Returns `"exists"`, `"written"`, or
|
|
175
|
+
* `"skipped"` (invalid thread id / nothing to carry). Best-effort: never throws.
|
|
176
|
+
*/
|
|
177
|
+
export function ensureCodexResumeRollout(opts) {
|
|
178
|
+
if (!THREAD_ID_RE.test(opts.threadId))
|
|
179
|
+
return "skipped";
|
|
180
|
+
const codexHome = opts.codexHome ?? (opts.sessionId ? codexHomePath(opts.sessionId) : undefined);
|
|
181
|
+
if (!codexHome)
|
|
182
|
+
return "skipped"; // no session context → can't locate the home
|
|
183
|
+
if (findCodexRollout(codexHome, opts.threadId))
|
|
184
|
+
return "exists";
|
|
185
|
+
// Back-compat: a pre-per-session rollout may live under the OLD shared home. Copy
|
|
186
|
+
// it forward into this session's home so the per-session app-server can resume it.
|
|
187
|
+
if (adoptLegacyRollout(codexHome, opts.threadId))
|
|
188
|
+
return "exists";
|
|
189
|
+
const records = buildRolloutRecords(opts);
|
|
190
|
+
// Nothing but the session_meta header → no history to carry; skip (a fresh
|
|
191
|
+
// thread resumes fine without a rollout once it has run a turn).
|
|
192
|
+
if (records.length <= 1)
|
|
193
|
+
return "skipped";
|
|
194
|
+
const now = opts.now ?? (() => Date.now());
|
|
195
|
+
const d = new Date(now());
|
|
196
|
+
const yyyy = String(d.getUTCFullYear());
|
|
197
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
198
|
+
const dd = String(d.getUTCDate()).padStart(2, "0");
|
|
199
|
+
const stamp = iso(d.getTime()).slice(0, 19).replace(/:/g, "-"); // YYYY-MM-DDTHH-MM-SS
|
|
200
|
+
const dir = join(codexHome, "sessions", yyyy, mm, dd);
|
|
201
|
+
const target = join(dir, `rollout-${stamp}-${opts.threadId}.jsonl`);
|
|
202
|
+
const tmp = `${target}.tmp`;
|
|
203
|
+
try {
|
|
204
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
205
|
+
writeFileSync(tmp, records.map((r) => JSON.stringify(r)).join("\n") + "\n");
|
|
206
|
+
renameSync(tmp, target);
|
|
207
|
+
return "written";
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return "skipped"; // best-effort: a failed synth just leaves resume to fail as before
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { AskForApproval, ClientInfo, CollaborationModeListResponse, GetAuthStatusParams, GetAuthStatusResponse, InitializeResponse, ModelListParams, ModelListResponse, ReviewStartParams, ReviewStartResponse, SandboxMode, ThreadForkParams, ThreadGoalClearParams, ThreadGoalGetParams, ThreadGoalGetResponse, ThreadGoalSetParams, ThreadListParams, ThreadListResponse, ResumedThread, ThreadResumeParams, ThreadSettingsUpdateParams, ThreadStartParams, TurnInterruptParams, TurnStartParams, TurnSteerParams, UserInput } from "./protocol.js";
|
|
2
|
+
import { CodexAppServerTransport, type CodexAppServerProcessSpawner, type RpcChannel, type TransportLogger } from "./transport.js";
|
|
3
|
+
export type ApprovalDecisionPolicy = "auto-approve-session" | "auto-decline" | "auto-cancel";
|
|
4
|
+
export type CodexNotificationListener = (method: string, params: unknown) => void;
|
|
5
|
+
export interface CodexAppServerClientOptions {
|
|
6
|
+
/** Default stdio transport. Mutually exclusive with {@link channel}. */
|
|
7
|
+
spawner?: CodexAppServerProcessSpawner;
|
|
8
|
+
/** Alternate I/O channel (e.g. a {@link WsRpcChannel} for co-drive). */
|
|
9
|
+
channel?: RpcChannel;
|
|
10
|
+
logger?: TransportLogger;
|
|
11
|
+
clientInfo?: ClientInfo;
|
|
12
|
+
/**
|
|
13
|
+
* What to do when the server asks for a command / file approval. Defaults
|
|
14
|
+
* to `auto-approve-session`, which matches the existing bridge behaviour
|
|
15
|
+
* of running everything inside `--sandbox workspace-write` without ever
|
|
16
|
+
* prompting a human.
|
|
17
|
+
*/
|
|
18
|
+
approvalDecisionPolicy?: ApprovalDecisionPolicy;
|
|
19
|
+
/**
|
|
20
|
+
* Surface approvals to a user instead of auto-deciding. When true and an
|
|
21
|
+
* approval listener is set, each request blocks until {@link
|
|
22
|
+
* CodexAppServerClient.resolveApproval} is called (or a timeout falls back to
|
|
23
|
+
* {@link approvalDecisionPolicy}).
|
|
24
|
+
*/
|
|
25
|
+
interactiveApprovals?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** A codex approval decision (superset of exec + patch decision enums). */
|
|
28
|
+
export type ApprovalDecision = "acceptForSession" | "accept" | "decline" | "cancel";
|
|
29
|
+
/** A pending approval surfaced to the user, awaiting {@link CodexAppServerClient.resolveApproval}. */
|
|
30
|
+
export interface ApprovalRequest {
|
|
31
|
+
approvalId: string;
|
|
32
|
+
kind: "exec" | "patch";
|
|
33
|
+
command?: string;
|
|
34
|
+
cwd?: string;
|
|
35
|
+
diff?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare class CodexAppServerClient {
|
|
38
|
+
readonly transport: CodexAppServerTransport;
|
|
39
|
+
private readonly logger;
|
|
40
|
+
private readonly clientInfo;
|
|
41
|
+
private readonly approvalDecisionPolicy;
|
|
42
|
+
private readonly interactiveApprovals;
|
|
43
|
+
private readonly channel;
|
|
44
|
+
private readonly notificationSubscribers;
|
|
45
|
+
private approvalListener;
|
|
46
|
+
private readonly pendingApprovals;
|
|
47
|
+
private initializeResponse;
|
|
48
|
+
constructor({ spawner, channel, logger, clientInfo, approvalDecisionPolicy, interactiveApprovals, }: CodexAppServerClientOptions);
|
|
49
|
+
/**
|
|
50
|
+
* The multi-client endpoint a `codex --remote` TUI can attach to, when this
|
|
51
|
+
* client runs over a {@link WsRpcChannel}. `undefined` for the default stdio
|
|
52
|
+
* transport (single client — not co-drivable). Available after the transport
|
|
53
|
+
* has started (post `ensureInitialized`).
|
|
54
|
+
*/
|
|
55
|
+
terminalRemoteUrl(): string | undefined;
|
|
56
|
+
ensureInitialized(): Promise<InitializeResponse>;
|
|
57
|
+
getAuthStatus(params?: GetAuthStatusParams): Promise<GetAuthStatusResponse>;
|
|
58
|
+
threadStart(params: ThreadStartParams): Promise<{
|
|
59
|
+
threadId: string;
|
|
60
|
+
}>;
|
|
61
|
+
threadResume(params: ThreadResumeParams): Promise<{
|
|
62
|
+
threadId: string;
|
|
63
|
+
thread: ResumedThread;
|
|
64
|
+
}>;
|
|
65
|
+
turnStart(params: TurnStartParams): Promise<{
|
|
66
|
+
turnId: string;
|
|
67
|
+
}>;
|
|
68
|
+
turnInterrupt(params: TurnInterruptParams): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Inject additional user input into the currently running turn. Returns the
|
|
71
|
+
* active turn id (may differ from `expectedTurnId`). Fails if no matching
|
|
72
|
+
* active turn exists, so callers should treat errors as best-effort.
|
|
73
|
+
*/
|
|
74
|
+
turnSteer(params: TurnSteerParams): Promise<{
|
|
75
|
+
turnId: string;
|
|
76
|
+
}>;
|
|
77
|
+
/** List local conversation/thread history (newest first by default). */
|
|
78
|
+
threadList(params?: ThreadListParams): Promise<ThreadListResponse>;
|
|
79
|
+
/** Fork an existing thread into a new one, optionally overriding settings. */
|
|
80
|
+
threadFork(params: ThreadForkParams): Promise<{
|
|
81
|
+
thread: {
|
|
82
|
+
id: string;
|
|
83
|
+
};
|
|
84
|
+
} & Record<string, unknown>>;
|
|
85
|
+
/** Update per-thread settings (model / mode / personality / sandbox …). */
|
|
86
|
+
threadSettingsUpdate(params: ThreadSettingsUpdateParams): Promise<Record<string, unknown>>;
|
|
87
|
+
/** List the models the local Codex install exposes. */
|
|
88
|
+
modelList(params?: ModelListParams): Promise<ModelListResponse>;
|
|
89
|
+
/** List the available collaboration-mode presets (experimental). */
|
|
90
|
+
collaborationModeList(): Promise<CollaborationModeListResponse>;
|
|
91
|
+
/** Set (or update) the thread's tracked goal. */
|
|
92
|
+
threadGoalSet(params: ThreadGoalSetParams): Promise<Record<string, unknown>>;
|
|
93
|
+
/** Read the thread's current goal. */
|
|
94
|
+
threadGoalGet(params: ThreadGoalGetParams): Promise<ThreadGoalGetResponse>;
|
|
95
|
+
/** Clear the thread's tracked goal. */
|
|
96
|
+
threadGoalClear(params: ThreadGoalClearParams): Promise<Record<string, unknown>>;
|
|
97
|
+
/** Start a non-interactive code review on the thread. */
|
|
98
|
+
reviewStart(params: ReviewStartParams): Promise<ReviewStartResponse>;
|
|
99
|
+
onNotification(listener: CodexNotificationListener): () => void;
|
|
100
|
+
stop(): Promise<void>;
|
|
101
|
+
private dispatchNotification;
|
|
102
|
+
private handleServerRequest;
|
|
103
|
+
/**
|
|
104
|
+
* Register the listener that surfaces interactive approval requests. Set by
|
|
105
|
+
* the executor for the duration of a run so requests reach its event stream
|
|
106
|
+
* (and thus the web / Lark approval card).
|
|
107
|
+
*/
|
|
108
|
+
setApprovalRequestListener(listener: ((request: ApprovalRequest) => void) | null): void;
|
|
109
|
+
/**
|
|
110
|
+
* Deliver a user's decision for a pending interactive approval. Returns false
|
|
111
|
+
* if the approval id is unknown (already resolved, timed out, or auto-decided).
|
|
112
|
+
*/
|
|
113
|
+
resolveApproval(approvalId: string, decision: ApprovalDecision): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Interactive approval path: surface the request and block the codex
|
|
116
|
+
* server-request until the user decides (or the timeout falls back to the
|
|
117
|
+
* auto policy). When interactive approvals are off, decide immediately.
|
|
118
|
+
*/
|
|
119
|
+
private handleApproval;
|
|
120
|
+
private autoApprovalDecision;
|
|
121
|
+
private respondCommandApproval;
|
|
122
|
+
private respondFileChangeApproval;
|
|
123
|
+
private declineDecisionForApprovals;
|
|
124
|
+
private respondNotSupported;
|
|
125
|
+
}
|
|
126
|
+
export interface BuildSandboxPolicyOptions {
|
|
127
|
+
mode: SandboxMode;
|
|
128
|
+
cwd: string;
|
|
129
|
+
allowedRoots: string[];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Construct a SandboxPolicy compatible with the App Server turn/start params
|
|
133
|
+
* from the env-configured mode and the allowlist of writable roots. The
|
|
134
|
+
* shape mirrors `SandboxPolicy` in `codex-app-server` generated types.
|
|
135
|
+
*/
|
|
136
|
+
export declare function buildSandboxPolicy(options: BuildSandboxPolicyOptions): Record<string, unknown>;
|
|
137
|
+
export declare function buildTextUserInput(message: string): UserInput[];
|
|
138
|
+
export type { AskForApproval };
|