@modelworks/quack 0.0.0-stage → 0.2.8

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,8 @@
1
+ {
2
+ "version": "0.2.8",
3
+ "build": "20261025131502",
4
+ "commit": "afaf6a2",
5
+ "commitLong": "afaf6a2cd8edc9c225df99eb55daa81944de4761",
6
+ "dirty": false,
7
+ "builtAt": "2026-10-25T13:15:02.360Z"
8
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Circuit URL parsing, version resolution, adaptor discovery, and SSE
3
+ * adaptor lifecycle (find/create/delete).
4
+ *
5
+ * @module
6
+ */
7
+ import type { CircuitAdaptorPublic, CircuitPublic, CircuitVersionPublic, ParsedCircuitRef } from "./types.js";
8
+ /**
9
+ * HTTP error with status code, thrown by `parseJson` on non-OK responses.
10
+ * Lets callers distinguish 403 (access denied) from other failures.
11
+ */
12
+ export declare class HttpError extends Error {
13
+ readonly status: number;
14
+ readonly body: string;
15
+ constructor(status: number, statusText: string, body: string);
16
+ }
17
+ /** Convert a circuit-definition adapter type to the API enum format. */
18
+ export declare function toApiAdapterType(defType: string): string;
19
+ /** Convert an API enum adapter type back to circuit-definition format. */
20
+ export declare function fromApiAdapterType(apiType: string): string;
21
+ /**
22
+ * Parse a circuit reference: a full URL, a bare circuit UID (`c-xxx`),
23
+ * or a bare version UID (`cv-xxx`).
24
+ *
25
+ * Accepts:
26
+ * - `https://modelworks.ai/circuits/<circuit_uid>`
27
+ * - `https://modelworks.ai/circuits/<circuit_uid>/versions/<version_uid>`
28
+ * - `<circuit_uid>` or `c-xxx`
29
+ * - `<version_uid>` or `cv-xxx`
30
+ */
31
+ export declare function parseCircuitRef(input: string): ParsedCircuitRef;
32
+ /** Fetch a circuit by UID (`GET /circuits/<uid>`). */
33
+ export declare function getCircuit(apiBase: string, circuitUid: string): Promise<CircuitPublic>;
34
+ /** Fetch a user's username by UID (`GET /users/<uid>`). */
35
+ export declare function getUsername(apiBase: string, userUid: string): Promise<string>;
36
+ /**
37
+ * Resolve a circuit reference to a RUNNING version.
38
+ *
39
+ * If `version_uid` is given, fetch that version directly (must be RUNNING
40
+ * for non-owners; owners can also use STAGING). Otherwise list the circuit's
41
+ * versions and pick the latest RUNNING one.
42
+ */
43
+ export declare function resolveRunningVersion(apiBase: string, ref: ParsedCircuitRef): Promise<{
44
+ circuit: CircuitPublic;
45
+ version: CircuitVersionPublic;
46
+ }>;
47
+ /** List existing adaptors for a circuit version (`GET /circuit-adaptors/by-circuit-version/<uid>`). */
48
+ export declare function listAdaptors(apiBase: string, versionUid: string): Promise<CircuitAdaptorPublic[]>;
49
+ /**
50
+ * Discover the adapter types a circuit version supports.
51
+ *
52
+ * Primary path: parse the `json_blob` of the circuit version and read
53
+ * its `adapters` array (visible for public circuits and owners).
54
+ *
55
+ * Fallback (protected, non-owner — no `json_blob`): list existing
56
+ * adaptors and read their `adapter_type` values.
57
+ *
58
+ * Returns `{ adapters, determined }` where `determined` is false when
59
+ * neither source yielded adapter types (no `json_blob` and no existing
60
+ * adaptors). In that case the caller should assume SSE-compatible and
61
+ * let runtime validation be the safety net.
62
+ */
63
+ export declare function discoverAdapters(version: CircuitVersionPublic, apiBase: string): Promise<{
64
+ adapters: string[];
65
+ determined: boolean;
66
+ }>;
67
+ /** Is `OPEN_AI_SSE` in the list of supported adapters? */
68
+ export declare function isSseCompatible(adapters: string[]): boolean;
69
+ /**
70
+ * Find the **public** (system-created) `OPEN_AI_SSE` adaptor for a circuit version.
71
+ *
72
+ * When a circuit version is uploaded, the system creates public adaptors from
73
+ * the circuit file's `adapters` array. These have `user_uid: null` and require
74
+ * Bearer auth (the proxy injects it). We use these instead of creating temporal
75
+ * adaptors — no per-user adaptor lifecycle needed.
76
+ *
77
+ * Returns the adaptor UID (`ca-xxx`), or `null` if no public SSE adaptor exists.
78
+ */
79
+ export declare function findPublicSseAdaptor(apiBase: string, versionUid: string): Promise<string | null>;
80
+ /** Result of a `quack add` — used to print a summary. */
81
+ export interface AddResult {
82
+ circuit_uid: string;
83
+ version_uid: string;
84
+ name: string;
85
+ adapters: string[];
86
+ sse_adaptor_uid: string | null;
87
+ sse_compatible: boolean;
88
+ added: boolean;
89
+ }
90
+ /**
91
+ * `quack add <circuit-url>` — resolve, discover, create SSE adaptor, snapshot.
92
+ *
93
+ * Reads auth + api base from the stored credentials.
94
+ */
95
+ export declare function addCircuit(input: string, apiBase: string): Promise<AddResult>;
96
+ /**
97
+ * `quack circuit remove <circuit-uid|url>` — remove from config + state.
98
+ *
99
+ * Public adaptors are NOT deleted — they belong to the circuit (system-created),
100
+ * not to the quack user. Other users of the same circuit rely on them.
101
+ */
102
+ export declare function removeCircuit(input: string): Promise<{
103
+ circuit_uid: string;
104
+ name: string;
105
+ }>;
106
+ /** `quack refresh [circuit-uid]` — re-resolve all (or one) circuit. */
107
+ export declare function refreshCircuits(apiBase: string, circuitUid?: string): Promise<AddResult[]>;
108
+ /**
109
+ * Auto-sync: for circuits in config but missing from state (cloned repo
110
+ * case), run the same resolve + discover + create flow as `quack add`,
111
+ * writing to state only (not config).
112
+ *
113
+ * Called by `quack opencode` before building the injected config.
114
+ */
115
+ export declare function ensureState(apiBase: string): Promise<void>;
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Circuit URL parsing, version resolution, adaptor discovery, and SSE
3
+ * adaptor lifecycle (find/create/delete).
4
+ *
5
+ * @module
6
+ */
7
+ import { getValidAccessToken, refreshToken, } from "./auth.js";
8
+ import { addCircuitToConfig, readConfig, readState, removeCircuitFromConfig, removeCircuitState, upsertCircuitState, } from "./project.js";
9
+ /**
10
+ * HTTP error with status code, thrown by `parseJson` on non-OK responses.
11
+ * Lets callers distinguish 403 (access denied) from other failures.
12
+ */
13
+ export class HttpError extends Error {
14
+ status;
15
+ body;
16
+ constructor(status, statusText, body) {
17
+ super(`HTTP ${status} ${statusText}${body ? ` — ${body}` : ""}`);
18
+ this.name = "HttpError";
19
+ this.status = status;
20
+ this.body = body;
21
+ }
22
+ }
23
+ /** The adapter type string for OpenAI-compatible SSE adaptors. */
24
+ const OPEN_AI_SSE = "OPEN_AI_SSE";
25
+ /**
26
+ * Map adapter type from circuit definition format (SCREAMING_SNAKE_CASE,
27
+ * as found in `json_blob`'s `adapters` array) to API enum format
28
+ * (PascalCase, as required by `POST /circuit-adaptors`).
29
+ *
30
+ * Example: `"OPEN_AI_SSE"` → `"OpenAiSse"`.
31
+ */
32
+ const ADAPTER_TYPE_MAP = {
33
+ OPEN_AI_SSE: "OpenAiSse",
34
+ MODELWORKS_SSE: "ModelworksSse",
35
+ MODELWORKS_HTTP: "ModelworksHTTP",
36
+ MCP: "MCP",
37
+ XMCP: "XMCP",
38
+ NONE: "None",
39
+ };
40
+ /** Convert a circuit-definition adapter type to the API enum format. */
41
+ export function toApiAdapterType(defType) {
42
+ return ADAPTER_TYPE_MAP[defType] ?? defType;
43
+ }
44
+ /** Convert an API enum adapter type back to circuit-definition format. */
45
+ export function fromApiAdapterType(apiType) {
46
+ for (const [def, api] of Object.entries(ADAPTER_TYPE_MAP)) {
47
+ if (api === apiType)
48
+ return def;
49
+ }
50
+ return apiType;
51
+ }
52
+ // ---------------------------------------------------------------------------
53
+ // URL / UID parsing
54
+ // ---------------------------------------------------------------------------
55
+ /**
56
+ * Parse a circuit reference: a full URL, a bare circuit UID (`c-xxx`),
57
+ * or a bare version UID (`cv-xxx`).
58
+ *
59
+ * Accepts:
60
+ * - `https://modelworks.ai/circuits/<circuit_uid>`
61
+ * - `https://modelworks.ai/circuits/<circuit_uid>/versions/<version_uid>`
62
+ * - `<circuit_uid>` or `c-xxx`
63
+ * - `<version_uid>` or `cv-xxx`
64
+ */
65
+ export function parseCircuitRef(input) {
66
+ // Bare UIDs first.
67
+ if (input.startsWith("cv-")) {
68
+ return { circuit_uid: "", version_uid: input };
69
+ }
70
+ if (input.startsWith("c-")) {
71
+ return { circuit_uid: input, version_uid: null };
72
+ }
73
+ // URLs.
74
+ try {
75
+ const url = new URL(input);
76
+ const parts = url.pathname.split("/").filter(Boolean);
77
+ // Expected: ["circuits", "<circuit_uid>"] or ["circuits", "<circuit_uid>", "versions", "<version_uid>"]
78
+ const circuitsIdx = parts.indexOf("circuits");
79
+ if (circuitsIdx === -1 || circuitsIdx + 1 >= parts.length) {
80
+ throw new Error(`Could not find circuit UID in URL: ${input}`);
81
+ }
82
+ const circuit_uid = parts[circuitsIdx + 1];
83
+ let version_uid = null;
84
+ const versionsIdx = parts.indexOf("versions", circuitsIdx);
85
+ if (versionsIdx !== -1 && versionsIdx + 1 < parts.length) {
86
+ version_uid = parts[versionsIdx + 1];
87
+ }
88
+ return { circuit_uid, version_uid };
89
+ }
90
+ catch (err) {
91
+ throw new Error(`Invalid circuit reference: ${input}. Use a circuit URL, c-xxx, or cv-xxx. (${err.message})`);
92
+ }
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // HTTP helpers
96
+ // ---------------------------------------------------------------------------
97
+ /** Authenticated fetch — injects Bearer token, refreshes on 401. */
98
+ async function authedFetch(apiBase, path, init) {
99
+ const token = await getValidAccessToken();
100
+ if (!token) {
101
+ throw new Error("Not logged in. Run `quack login` first.");
102
+ }
103
+ const headers = new Headers(init?.headers);
104
+ headers.set("authorization", `Bearer ${token}`);
105
+ const res = await fetch(`${apiBase}${path}`, { ...init, headers });
106
+ // One refresh + retry on 401.
107
+ if (res.status === 401) {
108
+ const ok = await refreshToken();
109
+ if (ok) {
110
+ const newToken = await getValidAccessToken();
111
+ if (newToken) {
112
+ headers.set("authorization", `Bearer ${newToken}`);
113
+ return fetch(`${apiBase}${path}`, { ...init, headers });
114
+ }
115
+ }
116
+ }
117
+ return res;
118
+ }
119
+ /** Parse a JSON API response, throwing a friendly error on non-OK. */
120
+ async function parseJson(res) {
121
+ if (!res.ok) {
122
+ const text = await res.text().catch(() => "");
123
+ throw new HttpError(res.status, res.statusText, text);
124
+ }
125
+ const body = (await res.json());
126
+ return body;
127
+ }
128
+ /**
129
+ * Unwrap the ModelWorks API envelope.
130
+ *
131
+ * The API wraps all responses in `{ status, status_code, payload }`.
132
+ * This extracts the `payload` field (the actual data) and throws if
133
+ * the envelope reports an error.
134
+ */
135
+ function unwrap(body) {
136
+ if (body && typeof body === "object" && "payload" in body) {
137
+ const envelope = body;
138
+ if (envelope.status === "error") {
139
+ throw new Error(`API error: ${JSON.stringify(envelope)}`);
140
+ }
141
+ return envelope.payload;
142
+ }
143
+ return body;
144
+ }
145
+ // ---------------------------------------------------------------------------
146
+ // Version resolution
147
+ // ---------------------------------------------------------------------------
148
+ /** Fetch a circuit by UID (`GET /circuits/<uid>`). */
149
+ export async function getCircuit(apiBase, circuitUid) {
150
+ const res = await authedFetch(apiBase, `/api/circuits/${circuitUid}`);
151
+ return unwrap(await parseJson(res));
152
+ }
153
+ /** Fetch a user's username by UID (`GET /users/<uid>`). */
154
+ export async function getUsername(apiBase, userUid) {
155
+ const res = await authedFetch(apiBase, `/api/users/${userUid}`);
156
+ const payload = await parseJson(res);
157
+ return unwrap(payload).username ?? "unknown";
158
+ }
159
+ /**
160
+ * Resolve a circuit reference to a RUNNING version.
161
+ *
162
+ * If `version_uid` is given, fetch that version directly (must be RUNNING
163
+ * for non-owners; owners can also use STAGING). Otherwise list the circuit's
164
+ * versions and pick the latest RUNNING one.
165
+ */
166
+ export async function resolveRunningVersion(apiBase, ref) {
167
+ let circuitUid = ref.circuit_uid;
168
+ // If only a version UID was given, fetch the version first to discover
169
+ // its parent circuit UID.
170
+ if (!circuitUid && ref.version_uid) {
171
+ const vRes = await authedFetch(apiBase, `/api/circuit-versions/${ref.version_uid}`);
172
+ const version = unwrap(await parseJson(vRes));
173
+ circuitUid = version.circuit_uid;
174
+ const circuit = await getCircuit(apiBase, circuitUid);
175
+ return { circuit, version };
176
+ }
177
+ if (!circuitUid) {
178
+ throw new Error("Could not resolve circuit UID from the given reference.");
179
+ }
180
+ const circuit = await getCircuit(apiBase, circuitUid);
181
+ if (ref.version_uid) {
182
+ const vRes = await authedFetch(apiBase, `/api/circuit-versions/${ref.version_uid}`);
183
+ const version = unwrap(await parseJson(vRes));
184
+ return { circuit, version };
185
+ }
186
+ // No version specified — get the primary (RUNNING) version directly.
187
+ const res = await authedFetch(apiBase, `/api/circuits/${circuitUid}/primary`);
188
+ const version = unwrap(await parseJson(res));
189
+ if (!version) {
190
+ throw new Error(`Circuit ${circuitUid} has no primary (RUNNING) version. Publish or promote a version first.`);
191
+ }
192
+ return { circuit, version };
193
+ }
194
+ // ---------------------------------------------------------------------------
195
+ // Adaptor discovery
196
+ // ---------------------------------------------------------------------------
197
+ /** List existing adaptors for a circuit version (`GET /circuit-adaptors/by-circuit-version/<uid>`). */
198
+ export async function listAdaptors(apiBase, versionUid) {
199
+ const res = await authedFetch(apiBase, `/api/circuit-adaptors/by-circuit-version/${versionUid}`);
200
+ return unwrap(await parseJson(res));
201
+ }
202
+ /**
203
+ * Discover the adapter types a circuit version supports.
204
+ *
205
+ * Primary path: parse the `json_blob` of the circuit version and read
206
+ * its `adapters` array (visible for public circuits and owners).
207
+ *
208
+ * Fallback (protected, non-owner — no `json_blob`): list existing
209
+ * adaptors and read their `adapter_type` values.
210
+ *
211
+ * Returns `{ adapters, determined }` where `determined` is false when
212
+ * neither source yielded adapter types (no `json_blob` and no existing
213
+ * adaptors). In that case the caller should assume SSE-compatible and
214
+ * let runtime validation be the safety net.
215
+ */
216
+ export async function discoverAdapters(version, apiBase) {
217
+ // Primary: json_blob's `adapters` array.
218
+ if (version.json_blob) {
219
+ try {
220
+ const def = JSON.parse(version.json_blob);
221
+ if (Array.isArray(def.adapters)) {
222
+ return { adapters: def.adapters.map((a) => a.type), determined: true };
223
+ }
224
+ }
225
+ catch {
226
+ // Fall through to the list-based fallback.
227
+ }
228
+ }
229
+ // Fallback: list existing adaptors (API returns PascalCase enum values;
230
+ // normalize to circuit-definition format for consistency).
231
+ const adaptors = await listAdaptors(apiBase, version.uid);
232
+ const adapters = adaptors.map((a) => fromApiAdapterType(a.adapter_type));
233
+ return { adapters, determined: adapters.length > 0 };
234
+ }
235
+ /** Is `OPEN_AI_SSE` in the list of supported adapters? */
236
+ export function isSseCompatible(adapters) {
237
+ return adapters.includes(OPEN_AI_SSE);
238
+ }
239
+ // ---------------------------------------------------------------------------
240
+ // SSE adaptor lifecycle
241
+ // ---------------------------------------------------------------------------
242
+ /**
243
+ * Find the **public** (system-created) `OPEN_AI_SSE` adaptor for a circuit version.
244
+ *
245
+ * When a circuit version is uploaded, the system creates public adaptors from
246
+ * the circuit file's `adapters` array. These have `user_uid: null` and require
247
+ * Bearer auth (the proxy injects it). We use these instead of creating temporal
248
+ * adaptors — no per-user adaptor lifecycle needed.
249
+ *
250
+ * Returns the adaptor UID (`ca-xxx`), or `null` if no public SSE adaptor exists.
251
+ */
252
+ export async function findPublicSseAdaptor(apiBase, versionUid) {
253
+ const existing = await listAdaptors(apiBase, versionUid);
254
+ const found = existing.find((a) => fromApiAdapterType(a.adapter_type) === OPEN_AI_SSE && a.user_uid === null);
255
+ return found?.uid ?? null;
256
+ }
257
+ /**
258
+ * `quack add <circuit-url>` — resolve, discover, create SSE adaptor, snapshot.
259
+ *
260
+ * Reads auth + api base from the stored credentials.
261
+ */
262
+ export async function addCircuit(input, apiBase) {
263
+ const ref = parseCircuitRef(input);
264
+ const { circuit, version } = await resolveRunningVersion(apiBase, ref);
265
+ const { adapters, determined } = await discoverAdapters(version, apiBase);
266
+ // List existing adaptors (public, system-created — user_uid: null).
267
+ const existingAdaptors = await listAdaptors(apiBase, version.uid);
268
+ const publicAdaptors = existingAdaptors.filter((a) => a.user_uid === null);
269
+ // Build adaptor_uids map from public adaptors only.
270
+ const adaptor_uids = {};
271
+ for (const a of publicAdaptors) {
272
+ adaptor_uids[fromApiAdapterType(a.adapter_type)] = a.uid;
273
+ }
274
+ // Find the public OPEN_AI_SSE adaptor (no temporal creation).
275
+ const sse_adaptor_uid = await findPublicSseAdaptor(apiBase, version.uid);
276
+ // If we found an SSE adaptor despite not being able to determine
277
+ // adapters, mark SSE-compatible in state (unverified).
278
+ const effectiveAdapters = determined
279
+ ? adapters
280
+ : (sse_adaptor_uid ? [OPEN_AI_SSE, ...adapters] : adapters);
281
+ // Fetch the circuit owner's username for display (`<username>/<circuit_name>`).
282
+ const owner_username = await getUsername(apiBase, circuit.user_uid);
283
+ // Config entry (committable).
284
+ const now = new Date().toISOString();
285
+ const configEntry = {
286
+ url: input,
287
+ circuit_uid: circuit.uid,
288
+ name: circuit.name,
289
+ added_at: now,
290
+ };
291
+ // State entry (gitignored).
292
+ const stateEntry = {
293
+ circuit_uid: circuit.uid,
294
+ version_uid: version.uid,
295
+ circuit_name: circuit.name,
296
+ owner_username,
297
+ adapters: effectiveAdapters,
298
+ sse_adaptor_uid,
299
+ adaptor_uids,
300
+ resolved_at: now,
301
+ };
302
+ // Check if already in config.
303
+ const existingConfig = readConfig();
304
+ const already = existingConfig.circuits.some((c) => c.circuit_uid === circuit.uid);
305
+ if (already) {
306
+ // Still update state (the circuit may have a new version).
307
+ upsertCircuitState(stateEntry);
308
+ }
309
+ else {
310
+ // Add to config + state.
311
+ addCircuitToConfig(configEntry);
312
+ upsertCircuitState(stateEntry);
313
+ }
314
+ return {
315
+ circuit_uid: circuit.uid,
316
+ version_uid: version.uid,
317
+ name: circuit.name,
318
+ adapters: effectiveAdapters,
319
+ sse_adaptor_uid,
320
+ sse_compatible: sse_adaptor_uid !== null,
321
+ added: !already,
322
+ };
323
+ }
324
+ /**
325
+ * `quack circuit remove <circuit-uid|url>` — remove from config + state.
326
+ *
327
+ * Public adaptors are NOT deleted — they belong to the circuit (system-created),
328
+ * not to the quack user. Other users of the same circuit rely on them.
329
+ */
330
+ export async function removeCircuit(input) {
331
+ const ref = parseCircuitRef(input);
332
+ const circuitUid = ref.circuit_uid;
333
+ // Find the circuit name from config.
334
+ const config = readConfig();
335
+ const entry = config.circuits.find((c) => c.circuit_uid === circuitUid);
336
+ if (!entry) {
337
+ throw new Error(`Circuit ${circuitUid} is not in this project.`);
338
+ }
339
+ removeCircuitFromConfig(circuitUid);
340
+ removeCircuitState(circuitUid);
341
+ return { circuit_uid: circuitUid, name: entry.name };
342
+ }
343
+ /** `quack refresh [circuit-uid]` — re-resolve all (or one) circuit. */
344
+ export async function refreshCircuits(apiBase, circuitUid) {
345
+ const config = readConfig();
346
+ const targets = circuitUid
347
+ ? config.circuits.filter((c) => c.circuit_uid === circuitUid)
348
+ : config.circuits;
349
+ if (circuitUid && targets.length === 0) {
350
+ throw new Error(`Circuit ${circuitUid} is not in this project.`);
351
+ }
352
+ const results = [];
353
+ for (const entry of targets) {
354
+ try {
355
+ // Re-add (re-resolve + re-discover + re-create). `addCircuit` upserts state.
356
+ const result = await addCircuit(entry.url, apiBase);
357
+ result.added = false; // It was already in config; we just refreshed.
358
+ results.push(result);
359
+ }
360
+ catch (err) {
361
+ if (err instanceof HttpError && err.status === 403) {
362
+ process.stderr.write(`⚠ skipping ${entry.circuit_uid} (no access — not authorized)\n`);
363
+ continue;
364
+ }
365
+ throw err;
366
+ }
367
+ }
368
+ return results;
369
+ }
370
+ /**
371
+ * Auto-sync: for circuits in config but missing from state (cloned repo
372
+ * case), run the same resolve + discover + create flow as `quack add`,
373
+ * writing to state only (not config).
374
+ *
375
+ * Called by `quack opencode` before building the injected config.
376
+ */
377
+ export async function ensureState(apiBase) {
378
+ const config = readConfig();
379
+ const state = readState();
380
+ const missing = config.circuits.filter((c) => !(c.circuit_uid in state.circuits));
381
+ for (const entry of missing) {
382
+ try {
383
+ await addCircuit(entry.url, apiBase);
384
+ }
385
+ catch (err) {
386
+ if (err instanceof HttpError && err.status === 403) {
387
+ process.stderr.write(`⚠ skipping ${entry.circuit_uid} (no access — not authorized)\n`);
388
+ continue;
389
+ }
390
+ throw err;
391
+ }
392
+ }
393
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Run history — save and list circuit run records locally.
3
+ *
4
+ * After each circuit request through the proxy, the run UID (from the
5
+ * `X-Circuit-Run-Uid` response header) is used to fetch the full run
6
+ * record from the API and save it as a JSON file under `.quack/runs/`.
7
+ * These files are committed to the repo (team-shared).
8
+ *
9
+ * @module
10
+ */
11
+ import type { Credentials, RunRecord, CircuitStateEntry } from "./types.js";
12
+ /** Path to `.quack/runs/` directory. */
13
+ export declare function runsDir(): string;
14
+ /** Path to a single run file. */
15
+ export declare function runFilePath(runUid: string): string;
16
+ /**
17
+ * Fetch a circuit run record from the API and map it to a `RunRecord`.
18
+ *
19
+ * Retries up to 3 times with a 500ms delay if the run hasn't reached a
20
+ * terminal state (COMPLETED/EXCEPTION/FAILED) — the engine might not
21
+ * have finalized the DB record the instant the stream ends.
22
+ */
23
+ export declare function fetchRunFromApi(runUid: string, credentials: Credentials, stateCircuits: Record<string, CircuitStateEntry>, callerUsername: string): Promise<RunRecord | null>;
24
+ /** Save a run record to `.quack/runs/cr-xxx.json`. */
25
+ export declare function saveRunRecord(record: RunRecord): void;
26
+ /**
27
+ * List all run records, sorted by `created_at` descending.
28
+ *
29
+ * Reads every `*.json` file in `.quack/runs/` and parses them.
30
+ * Returns an empty array if the directory doesn't exist.
31
+ */
32
+ export declare function listRunRecords(): RunRecord[];
33
+ /** Get a single run record by UID, or `null` if not found. */
34
+ export declare function getRunRecord(runUid: string): RunRecord | null;