@hypercarrier/rarebit 0.1.0-alpha.4 → 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.
- package/CHANGELOG.md +23 -1
- package/README.md +45 -18
- package/RUNBOOK.md +24 -7
- package/bin/piq.mjs +14 -0
- package/package.json +25 -4
- package/scripts/e2e-fork-pi.mjs +824 -0
- package/scripts/e2e-recall-pi083.mjs +49 -247
- package/scripts/e2e-recall-pi0842.mjs +259 -0
- package/scripts/verify-package.mjs +11 -8
- package/src/extension.mjs +97 -8
- package/src/index.mjs +1 -0
- package/src/pi-session.mjs +237 -0
- package/src/piq-cli.mjs +96 -0
- package/src/piq.mjs +265 -0
- package/src/rarebit-activity.mjs +8 -2
- package/src/rarebit-cli.mjs +101 -5
- package/src/rarebit-command.mjs +8 -0
- package/src/rarebit-fork-lineage.mjs +69 -0
- package/src/rarebit-fork.mjs +241 -0
- package/src/rarebit-model.mjs +12 -4
- package/src/rarebit-session.mjs +12 -160
- package/src/types.d.ts +31 -0
- package/test/piq.test.mjs +212 -0
- package/test/rarebit-command.test.mjs +5 -2
- package/test/rarebit-core.test.mjs +35 -1
- package/test/rarebit-fork.test.mjs +124 -0
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
|
-
|
|
109
|
-
|
|
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 ?? {
|
|
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
|
+
}
|
package/src/piq-cli.mjs
ADDED
|
@@ -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
|
+
}
|