@hypercarrier/rarebit 0.1.0-alpha.5 → 0.1.0-alpha.6

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.
@@ -13,17 +13,19 @@ const npm = (args, cwd = root) => run("npm", args, cwd);
13
13
  const required = [
14
14
  "package/README.md", "package/RUNBOOK.md", "package/VISUAL-LANGUAGE.md",
15
15
  "package/AGENTS.md", "package/CHANGELOG.md", "package/LICENSE",
16
- "package/SECURITY.md", "package/bin/rarebit.mjs",
17
- "package/src/index.mjs", "package/src/types.d.ts",
16
+ "package/SECURITY.md", "package/bin/rarebit.mjs", "package/bin/piq.mjs",
17
+ "package/src/index.mjs", "package/src/rarebit-fork-lineage.mjs", "package/src/rarebit-fork.mjs", "package/src/types.d.ts",
18
18
  "package/schemas/rarebit.schema.json", "package/test/rarebit-cli.test.mjs",
19
19
  "package/scripts/e2e-recall-pi083.mjs", "package/scripts/e2e-recall-pi0842.mjs", "package/scripts/verify-package.mjs",
20
20
  ];
21
21
  const forbiddenPath = /(^|\/)(\.git|node_modules|\.github|hc-rarebit\.mjs)(\/|$)|HyperCarrier|timeline|pi-team/i;
22
- const allowedBare = new Set(["@earendil-works/pi-ai"]);
22
+ const allowedBare = new Set(["@earendil-works/pi-ai", "@earendil-works/pi-coding-agent"]);
23
23
  const packageFor = (specifier) => specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
24
24
  const packageJson = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
25
- assert.equal(packageJson.version, "0.1.0-alpha.5", "release version mismatch");
25
+ assert.equal(packageJson.version, "0.1.0-alpha.6", "release version mismatch");
26
26
  assert.equal(packageJson.peerDependencies?.["@earendil-works/pi-ai"], ">=0.83.0", "Pi AI peer range mismatch");
27
+ for (const peer of ["@earendil-works/pi-ai", "@earendil-works/pi-coding-agent"])
28
+ assert.equal(packageJson.peerDependenciesMeta?.[peer]?.optional, true, `${peer} peer must remain optional`);
27
29
 
28
30
  const packed = JSON.parse(npm(["pack", "--json", "--ignore-scripts"]))[0];
29
31
  const tarball = join(root, packed.filename);
@@ -35,7 +37,7 @@ assert(!entries.includes("package/bin/hc-rarebit.mjs"), "legacy CLI is packed");
35
37
  const sourceFiles = entries.filter((path) => path.endsWith(".mjs") && !path.startsWith("package/test/"));
36
38
  for (const entry of sourceFiles) {
37
39
  const text = run("tar", ["-xOf", tarball, entry]);
38
- for (const match of text.matchAll(/(?:from\s*|import\s*\()\s*["']([^"']+)["']/g)) {
40
+ for (const match of text.matchAll(/(?:\bfrom\s+|import\s*\()\s*["']([^"']+)["']/g)) {
39
41
  const specifier = match[1];
40
42
  if (!specifier.startsWith(".") && !specifier.startsWith("node:") && !allowedBare.has(packageFor(specifier))) {
41
43
  throw new Error(`undeclared runtime import ${specifier} in ${entry}`);
@@ -52,10 +54,10 @@ try {
52
54
  npm(["init", "-y"], temp);
53
55
  npm(["install", "--ignore-scripts", "--omit=dev", tarball], temp);
54
56
  const installed = "@hypercarrier/rarebit";
55
- for (const subpath of ["", "/core", "/session", "/service", "/artifact-state", "/visual-language", "/extension"]) {
57
+ for (const subpath of ["", "/core", "/session", "/service", "/artifact-state", "/fork", "/fork-lineage", "/visual-language", "/extension"]) {
56
58
  run(process.execPath, ["--input-type=module", "-e", `await import(${JSON.stringify(installed + subpath)})`], temp);
57
59
  }
58
- const imports = ["", "/core", "/session", "/service", "/artifact-state", "/visual-language", "/extension"]
60
+ const imports = ["", "/core", "/session", "/service", "/artifact-state", "/fork", "/fork-lineage", "/visual-language", "/extension"]
59
61
  .map((subpath) => `import ${JSON.stringify(installed + subpath)};`).join("\n");
60
62
  await writeFile(join(temp, "imports.mts"), imports);
61
63
  run(join(root, "node_modules/.bin/tsc"), ["--noEmit", "--module", "NodeNext", "--moduleResolution", "NodeNext", "--target", "ES2022", "--skipLibCheck", "imports.mts"], temp);
package/src/extension.mjs CHANGED
@@ -1,8 +1,6 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { readFile, rm } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
- import { complete } from "@earendil-works/pi-ai/compat";
5
-
6
4
  export * from "./rarebit-core.mjs";
7
5
  export * from "./rarebit-model.mjs";
8
6
  export * from "./automatic-summary-policy.mjs";
@@ -26,6 +24,12 @@ import {
26
24
  parseRarebitCommand,
27
25
  rarebitCommandDescription,
28
26
  } from "./rarebit-command.mjs";
27
+ import {
28
+ buildRarebitForkPlan,
29
+ createRarebitForkEntries,
30
+ sessionFilenameFor,
31
+ writeRarebitForkFile,
32
+ } from "./rarebit-fork.mjs";
29
33
  import {
30
34
  RAREBIT_CONVERSATION_SCHEMA_VERSION,
31
35
  RAREBIT_RECALL_SCHEMA_VERSION,
@@ -49,6 +53,17 @@ async function readSettings(path) {
49
53
  }
50
54
  }
51
55
 
56
+ function resolveReserveTokens(globalSettings, projectSettings, model) {
57
+ const globalCompaction = globalSettings?.compaction ?? {};
58
+ const projectCompaction = projectSettings?.compaction ?? {};
59
+ const compaction = { ...globalCompaction, ...projectCompaction };
60
+ const modelOverrides = { ...(globalCompaction.modelOverrides ?? {}), ...(projectCompaction.modelOverrides ?? {}) };
61
+ const key = model?.provider && model?.id ? `${model.provider}/${model.id}` : null;
62
+ const override = key ? modelOverrides[key] : undefined;
63
+ const value = override?.reserveTokens ?? (Object.hasOwn(projectCompaction, "reserveTokens") ? projectCompaction.reserveTokens : compaction.reserveTokens) ?? 16_384;
64
+ return Number.isSafeInteger(value) && value >= 0 ? value : 16_384;
65
+ }
66
+
52
67
  function configuredAgentDir(env = process.env) {
53
68
  return env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
54
69
  }
@@ -56,6 +71,7 @@ function configuredAgentDir(env = process.env) {
56
71
  export async function readConfiguredRarebitSettings({
57
72
  cwd,
58
73
  projectTrusted,
74
+ model,
59
75
  agentDir = configuredAgentDir(),
60
76
  } = {}) {
61
77
  const global = await readSettings(join(agentDir, "settings.json"));
@@ -88,6 +104,7 @@ export async function readConfiguredRarebitSettings({
88
104
  },
89
105
  summaryPolicy: resolved.summaryPolicy,
90
106
  autoTitle: resolved.autoTitle,
107
+ reserveTokens: resolveReserveTokens(global.value, project.value, model),
91
108
  };
92
109
  }
93
110
 
@@ -104,10 +121,13 @@ export default function registerPiRarebit(pi, config = {}) {
104
121
  });
105
122
  const activityReporter =
106
123
  explicit.activityReporter ?? createHerdrActivityReporter();
107
- const projectActivity = (ctx) =>
108
- projectRarebitSessionActivity(
109
- selectRarebits(ctx?.sessionManager?.getBranch?.() ?? []),
110
- );
124
+ const projectActivity = (ctx) => {
125
+ const entries = ctx?.sessionManager?.getBranch?.() ?? [];
126
+ return projectRarebitSessionActivity({
127
+ ...selectRarebits(entries),
128
+ entries,
129
+ });
130
+ };
111
131
 
112
132
  const notify = (ctx, text, level = "info") => {
113
133
  if (!ctx?.hasUI || typeof ctx?.ui?.notify !== "function") return;
@@ -179,6 +199,7 @@ export default function registerPiRarebit(pi, config = {}) {
179
199
  injected = await settingsLoader({
180
200
  cwd: ctx?.cwd,
181
201
  projectTrusted: ctx?.isProjectTrusted?.() === true,
202
+ model: ctx?.model,
182
203
  });
183
204
  } catch (error) {
184
205
  injected = {
@@ -200,7 +221,9 @@ export default function registerPiRarebit(pi, config = {}) {
200
221
  ...injected,
201
222
  ...explicit,
202
223
  summaryPolicy,
203
- piAi: explicit.piAi ?? { complete },
224
+ piAi: explicit.piAi ?? {
225
+ complete: async (...args) => (await import("@earendil-works/pi-ai/compat")).complete(...args),
226
+ },
204
227
  };
205
228
  };
206
229
 
@@ -434,6 +457,72 @@ ${fence(requestText)}`;
434
457
  return;
435
458
  }
436
459
  const { subcommand, arguments: rest } = command;
460
+ if (subcommand === "fork") {
461
+ if (typeof ctx?.switchSession !== "function") {
462
+ notify(ctx, "Rarebit fork is unavailable in this Pi runtime", "error");
463
+ return;
464
+ }
465
+ let destination;
466
+ let switchStarted = false;
467
+ try {
468
+ await ctx.waitForIdle?.();
469
+ const sourceFile = ctx.sessionManager?.getSessionFile?.();
470
+ const branch = ctx.sessionManager?.getBranch?.();
471
+ const sourceHeader = ctx.sessionManager?.getHeader?.();
472
+ if (typeof sourceFile !== "string" || !Array.isArray(branch))
473
+ throw new Error("the current Pi Session must be persisted before forking");
474
+ const maxTokenLength = Number(rest[1] ?? 64_000);
475
+ const model = ctx.model ?? null;
476
+ if (!model || typeof model.provider !== "string" || !model.provider || typeof model.id !== "string" || !model.id || !Number.isFinite(model.contextWindow) || model.contextWindow <= 0)
477
+ throw new Error("destination model/context window is unavailable; select a target Pi model before forking");
478
+ const effective = await loadEffective(ctx);
479
+ const promptOverheadTokens = Math.ceil(String(ctx.getSystemPrompt?.() ?? "").length / 4);
480
+ const activeToolNames = new Set(pi.getActiveTools?.() ?? []);
481
+ const activeToolSchema = (pi.getAllTools?.() ?? [])
482
+ .filter((tool) => activeToolNames.has(tool.name))
483
+ .map(({ name, description, parameters }) => ({ name, description, parameters }));
484
+ const toolOverheadTokens = Math.ceil(JSON.stringify(activeToolSchema).length / 4);
485
+ const plan = buildRarebitForkPlan({
486
+ header: sourceHeader, sessionFile: sourceFile, cwd: ctx.cwd, branch,
487
+ targetCwd: ctx.cwd, maxTokenLength, targetModel: model,
488
+ reserveTokens: effective.reserveTokens ?? 16_384,
489
+ promptOverheadTokens,
490
+ toolOverheadTokens,
491
+ });
492
+ const currentLeaf = ctx.sessionManager.getLeafId?.() ?? branch.at(-1)?.id ?? null;
493
+ if (currentLeaf !== (plan.branchLeafId ?? null))
494
+ throw new Error("the active source branch changed during fork preparation; retry without switching branches");
495
+ const session = createRarebitForkEntries(plan);
496
+ const sessionDir = ctx.sessionManager.getSessionDir?.();
497
+ if (typeof sessionDir !== "string" || !sessionDir)
498
+ throw new Error("the current Pi Session store is unavailable");
499
+ destination = sessionFilenameFor({ sessionDir, forkCreatedAt: plan.forkCreatedAt, sessionId: session.sessionId });
500
+ await writeRarebitForkFile(destination, session);
501
+ const currentSourceFile = ctx.sessionManager.getSessionFile?.();
502
+ const currentSourceId = ctx.sessionManager.getHeader?.()?.id;
503
+ const currentSourceLeaf = ctx.sessionManager.getLeafId?.() ?? ctx.sessionManager.getBranch?.()?.at(-1)?.id ?? null;
504
+ if (resolve(currentSourceFile ?? "") !== resolve(sourceFile) || currentSourceId !== sourceHeader?.id || currentSourceLeaf !== (plan.branchLeafId ?? null))
505
+ throw new Error("the active source Session or branch changed during fork preparation; retry without switching branches");
506
+ switchStarted = true;
507
+ const switched = await ctx.switchSession(destination, {
508
+ withSession: async (next) => {
509
+ notify(next, `Rarebit fork opened (${plan.importedTokens} imported tokens; ${plan.omittedOccurrences} omitted occurrences)`, "info");
510
+ },
511
+ });
512
+ if (switched?.cancelled) {
513
+ await rm(destination, { force: true });
514
+ destination = undefined;
515
+ notify(ctx, "Rarebit fork cancelled before switching", "warning");
516
+ }
517
+ } catch (error) {
518
+ if (destination && !switchStarted) await rm(destination, { force: true }).catch(() => {});
519
+ const retained = destination && switchStarted
520
+ ? ` Destination retained at ${destination}; resume it if the replacement is active.`
521
+ : "";
522
+ notify(ctx, `Rarebit fork failed: ${error?.message ?? error}.${retained}`, "error");
523
+ }
524
+ return;
525
+ }
437
526
  if (subcommand === "recall") {
438
527
  const prompt = rest[0];
439
528
  if (typeof pi.sendUserMessage !== "function") {
package/src/index.mjs CHANGED
@@ -8,6 +8,7 @@ export * from "./rarebit-service.mjs";
8
8
  export * from "./rarebit-session.mjs";
9
9
  export * from "./rarebit-store.mjs";
10
10
  export * from "./rarebit-artifact-state.mjs";
11
+ export * from "./rarebit-fork-lineage.mjs";
11
12
  export * from "./rarebit-visual-language.mjs";
12
13
  export * from "./rarebit-settings.mjs";
13
14
  export { default } from "./extension.mjs";
@@ -0,0 +1,237 @@
1
+ import { open, readFile, readdir, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve, sep } from "node:path";
4
+
5
+ export const DEFAULT_PI_SESSION_ROOT = join(
6
+ homedir(),
7
+ ".pi",
8
+ "agent",
9
+ "sessions",
10
+ );
11
+
12
+ function queryError(message) {
13
+ const error = new Error(message);
14
+ error.name = "PiSessionQueryError";
15
+ return error;
16
+ }
17
+
18
+ async function isRegularFile(path) {
19
+ try {
20
+ return (await stat(path)).isFile();
21
+ } catch (error) {
22
+ if (error?.code === "ENOENT") return false;
23
+ throw error;
24
+ }
25
+ }
26
+
27
+ async function* jsonlFiles(root) {
28
+ let entries;
29
+ try {
30
+ entries = await readdir(root, { withFileTypes: true });
31
+ } catch (error) {
32
+ if (error?.code === "ENOENT") return;
33
+ throw error;
34
+ }
35
+ for (const entry of entries) {
36
+ const path = join(root, entry.name);
37
+ if (entry.isDirectory()) yield* jsonlFiles(path);
38
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) yield path;
39
+ }
40
+ }
41
+
42
+ async function readHeader(path) {
43
+ const handle = await open(path, "r");
44
+ try {
45
+ const buffer = Buffer.alloc(16384);
46
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
47
+ const firstLine = buffer
48
+ .subarray(0, bytesRead)
49
+ .toString("utf8")
50
+ .split("\n", 1)[0]
51
+ ?.trim();
52
+ if (!firstLine) return null;
53
+ try {
54
+ const header = JSON.parse(firstLine);
55
+ return header?.type === "session" ? header : null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ } finally {
60
+ await handle.close();
61
+ }
62
+ }
63
+
64
+ function looksLikePath(reference) {
65
+ return (
66
+ reference.includes(sep) ||
67
+ reference.startsWith(".") ||
68
+ reference.endsWith(".jsonl")
69
+ );
70
+ }
71
+
72
+ /** Resolve one exact path or one unambiguous persisted Session ID. */
73
+ export async function resolvePiSessionFile(
74
+ reference,
75
+ { sessionRoot = DEFAULT_PI_SESSION_ROOT } = {},
76
+ ) {
77
+ if (typeof reference !== "string" || !reference.trim())
78
+ throw queryError("--session requires an exact Pi Session path or ID");
79
+ const input = reference.trim();
80
+ const path = resolve(input);
81
+ if (await isRegularFile(path)) return path;
82
+ if (looksLikePath(input))
83
+ throw queryError(`Pi Session file not found: ${input}`);
84
+ const matches = [];
85
+ for await (const candidate of jsonlFiles(resolve(sessionRoot))) {
86
+ const header = await readHeader(candidate);
87
+ if (header?.id === input) matches.push(candidate);
88
+ }
89
+ if (matches.length === 0)
90
+ throw queryError(`No persisted Pi Session has exact ID ${input}`);
91
+ if (matches.length > 1)
92
+ throw queryError(
93
+ `Pi Session ID ${input} is ambiguous across ${matches.length} files; pass an exact path`,
94
+ );
95
+ return resolve(matches[0]);
96
+ }
97
+
98
+ /** Parse native JSONL without normalizing away unknown evidence fields. */
99
+ export function parseNativeSession(content, source = "Pi Session") {
100
+ const records = [];
101
+ for (const [index, line] of String(content).split("\n").entries()) {
102
+ if (!line.trim()) continue;
103
+ try {
104
+ records.push(JSON.parse(line));
105
+ } catch {
106
+ throw queryError(`${source} contains malformed JSON at line ${index + 1}`);
107
+ }
108
+ }
109
+ const header = records[0];
110
+ if (
111
+ !header ||
112
+ header.type !== "session" ||
113
+ typeof header.id !== "string" ||
114
+ !header.id
115
+ )
116
+ throw queryError(`${source} does not begin with a valid Pi Session header`);
117
+ const entries = records.slice(1);
118
+ const version = header.version ?? 1;
119
+ if (typeof version !== "number" || !Number.isInteger(version) || version < 1)
120
+ throw queryError(`${source} has an invalid Session version`);
121
+ const byId = new Map();
122
+ const linear = version < 2;
123
+ if (!linear) {
124
+ for (const entry of entries) {
125
+ if (typeof entry?.id !== "string" || !entry.id)
126
+ throw queryError(`${source} contains a Session entry without an ID`);
127
+ if (byId.has(entry.id))
128
+ throw queryError(
129
+ `${source} contains duplicate Session entry ID ${entry.id}`,
130
+ );
131
+ byId.set(entry.id, entry);
132
+ }
133
+ }
134
+ return { header, entries, byId, linear };
135
+ }
136
+
137
+ function ancestryFromLeaf(parsed, leaf, source) {
138
+ const reversed = [];
139
+ const visited = new Set();
140
+ let current = leaf;
141
+ while (current) {
142
+ if (visited.has(current.id))
143
+ throw queryError(`${source} contains a cycle at Session entry ${current.id}`);
144
+ visited.add(current.id);
145
+ reversed.push(current);
146
+ if (current.parentId === null || current.parentId === undefined) break;
147
+ if (typeof current.parentId !== "string" || !current.parentId)
148
+ throw queryError(`${source} contains an invalid parent ID at ${current.id}`);
149
+ current = parsed.byId.get(current.parentId);
150
+ if (!current)
151
+ throw queryError(
152
+ `${source} is missing parent Session entry ${reversed.at(-1).parentId}`,
153
+ );
154
+ }
155
+ return reversed.reverse();
156
+ }
157
+
158
+ /** Resolve the persisted active branch. The final entry is Pi's default leaf. */
159
+ export function resolveActiveBranch(parsed, source = "Pi Session") {
160
+ if (parsed.linear) return parsed.entries.slice();
161
+ const leaf = parsed.entries.at(-1);
162
+ return leaf ? ancestryFromLeaf(parsed, leaf, source) : [];
163
+ }
164
+
165
+ /** Resolve one explicit leaf, or the native default leaf when omitted. */
166
+ export function resolveSessionBranch(
167
+ parsed,
168
+ { leafId = null } = {},
169
+ source = "Pi Session",
170
+ ) {
171
+ if (parsed.linear) {
172
+ if (leafId !== null && leafId !== undefined)
173
+ throw queryError(
174
+ `${source} uses a linear format and has no explicit branch leaf`,
175
+ );
176
+ return parsed.entries.slice();
177
+ }
178
+ const leaf =
179
+ leafId === null || leafId === undefined
180
+ ? parsed.entries.at(-1)
181
+ : parsed.byId.get(leafId);
182
+ if (!leaf) {
183
+ if (leafId === null || leafId === undefined) return [];
184
+ throw queryError(`${source} has no Session entry with leaf ID ${leafId}`);
185
+ }
186
+ return ancestryFromLeaf(parsed, leaf, source);
187
+ }
188
+
189
+ function childIds(parsed) {
190
+ const children = new Set();
191
+ if (parsed.linear) return children;
192
+ for (const entry of parsed.entries) {
193
+ if (entry.parentId !== null && entry.parentId !== undefined) {
194
+ if (!parsed.byId.has(entry.parentId)) continue;
195
+ children.add(entry.parentId);
196
+ }
197
+ }
198
+ return children;
199
+ }
200
+
201
+ /** Return every persisted leaf in file order; no timestamp-based inference. */
202
+ export function listSessionBranches(parsed, source = "Pi Session") {
203
+ if (parsed.linear) {
204
+ return parsed.entries.length
205
+ ? [{ leafId: null, entryCount: parsed.entries.length }]
206
+ : [];
207
+ }
208
+ // Validate every connected component before looking for leaves. A cycle has
209
+ // no leaf and must remain an explicit error instead of an empty branch list.
210
+ for (const entry of parsed.entries) ancestryFromLeaf(parsed, entry, source);
211
+ const children = childIds(parsed);
212
+ return parsed.entries
213
+ .filter((entry) => !children.has(entry.id))
214
+ .map((entry) => ({
215
+ leafId: entry.id,
216
+ entryCount: ancestryFromLeaf(parsed, entry, source).length,
217
+ }));
218
+ }
219
+
220
+ export async function readPiSession(reference, options = {}) {
221
+ const sessionFile = await resolvePiSessionFile(reference, options);
222
+ const parsed = parseNativeSession(
223
+ await readFile(sessionFile, "utf8"),
224
+ sessionFile,
225
+ );
226
+ const branch = resolveSessionBranch(
227
+ parsed,
228
+ { leafId: options.leafId ?? null },
229
+ sessionFile,
230
+ );
231
+ return {
232
+ sessionFile,
233
+ parsed,
234
+ branch,
235
+ leafId: parsed.linear ? null : (branch.at(-1)?.id ?? null),
236
+ };
237
+ }
@@ -0,0 +1,96 @@
1
+ import { queryPiQ } from "./piq.mjs";
2
+
3
+ const FROM_FLAG = "--from";
4
+
5
+ export const PIQ_CLI_USAGE = `Usage:
6
+ piq <entries|branches|tools|tool-calls|tool-results|compactions> --session <exact-path-or-id> [options]
7
+
8
+ Options:
9
+ --leaf <entry-id> Read one explicit branch leaf (default: native active leaf)
10
+ --entry-id <entry-id> Restrict entries or tools to one native entry ID
11
+ --role <role> Restrict entries to a native message role
12
+ --from <ISO-8601> Include evidence at or after this timestamp
13
+ --to <ISO-8601> Include evidence at or before this timestamp
14
+ --through-compaction <entry-id> Stop the branch at this compaction boundary (or latest)
15
+
16
+ PiQ writes one JSON object per result line. It never writes the source Session.`;
17
+
18
+ function value(argv, index, flag) {
19
+ const next = argv[index + 1];
20
+ if (!next || next.startsWith("--")) throw new Error(`${flag} requires a value`);
21
+ return next;
22
+ }
23
+
24
+ export function parsePiQCliArgs(argv) {
25
+ if (argv.includes("--help") || argv.includes("-h")) return { help: true };
26
+ const [first, ...rest] = argv;
27
+ const commands = new Set([
28
+ "entries",
29
+ "branches",
30
+ "tools",
31
+ "tool-calls",
32
+ "tool-results",
33
+ "compactions",
34
+ ]);
35
+ const kind = commands.has(first) ? first : "entries";
36
+ const args = commands.has(first) ? rest : argv;
37
+ const options = { kind };
38
+ for (let index = 0; index < args.length; index += 1) {
39
+ const argument = args[index];
40
+ switch (argument) {
41
+ case "--session":
42
+ options.session = value(args, index, argument);
43
+ index += 1;
44
+ break;
45
+ case "--leaf":
46
+ options.leafId = value(args, index, argument);
47
+ index += 1;
48
+ break;
49
+ case "--entry-id":
50
+ options.entryId = value(args, index, argument);
51
+ index += 1;
52
+ break;
53
+ case "--role":
54
+ options.role = value(args, index, argument);
55
+ index += 1;
56
+ break;
57
+ case FROM_FLAG:
58
+ options.from = value(args, index, argument);
59
+ index += 1;
60
+ break;
61
+ case "--to":
62
+ options.to = value(args, index, argument);
63
+ index += 1;
64
+ break;
65
+ case "--through-compaction":
66
+ options.throughCompaction = value(args, index, argument);
67
+ index += 1;
68
+ break;
69
+ case "--session-root":
70
+ options.sessionRoot = value(args, index, argument);
71
+ index += 1;
72
+ break;
73
+ case "--jsonl":
74
+ case "--json":
75
+ break;
76
+ default:
77
+ throw new Error(`Unknown argument: ${argument}`);
78
+ }
79
+ }
80
+ if (!options.session) throw new Error("--session is required");
81
+ return options;
82
+ }
83
+
84
+ export async function runPiQCli(options, dependencies = {}) {
85
+ const query = dependencies.query ?? queryPiQ;
86
+ return query(options.session, {
87
+ kind: options.kind,
88
+ leafId: options.leafId,
89
+ sessionRoot: options.sessionRoot,
90
+ entryId: options.entryId,
91
+ role: options.role,
92
+ from: options.from,
93
+ to: options.to,
94
+ throughCompaction: options.throughCompaction,
95
+ });
96
+ }