@hypercarrier/rarebit 0.1.0-alpha.1
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/AGENTS.md +14 -0
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/RUNBOOK.md +36 -0
- package/SECURITY.md +11 -0
- package/VISUAL-LANGUAGE.md +89 -0
- package/bin/rarebit.mjs +20 -0
- package/package.json +96 -0
- package/schemas/rarebit.schema.json +618 -0
- package/scripts/e2e-recall-pi083.mjs +199 -0
- package/scripts/e2e-summary-cache.mjs +257 -0
- package/scripts/verify-package.mjs +81 -0
- package/src/automatic-summary-policy.mjs +210 -0
- package/src/extension.mjs +516 -0
- package/src/herdr-activity.mjs +111 -0
- package/src/index.mjs +13 -0
- package/src/lifecycle.mjs +322 -0
- package/src/rarebit-activity.mjs +48 -0
- package/src/rarebit-artifact-state.mjs +839 -0
- package/src/rarebit-cli.mjs +305 -0
- package/src/rarebit-command.mjs +281 -0
- package/src/rarebit-core.mjs +525 -0
- package/src/rarebit-model.mjs +226 -0
- package/src/rarebit-recall.mjs +182 -0
- package/src/rarebit-service.mjs +579 -0
- package/src/rarebit-session.mjs +227 -0
- package/src/rarebit-settings.mjs +79 -0
- package/src/rarebit-store.mjs +771 -0
- package/src/rarebit-visual-language.mjs +110 -0
- package/src/types.d.ts +601 -0
- package/test/automatic-summary-policy.test.mjs +476 -0
- package/test/extension-activity.test.mjs +73 -0
- package/test/fixtures/branch-mixed.json +117 -0
- package/test/fixtures/conformance-corpus.v1.json +306 -0
- package/test/fixtures/truncated-mixed.jsonl +2 -0
- package/test/herdr-activity.test.mjs +93 -0
- package/test/lifecycle-contract-independent.test.mjs +627 -0
- package/test/rarebit-activity.test.mjs +82 -0
- package/test/rarebit-artifact-state.test.mjs +440 -0
- package/test/rarebit-cli.test.mjs +222 -0
- package/test/rarebit-command.test.mjs +127 -0
- package/test/rarebit-core.test.mjs +461 -0
- package/test/rarebit-cross-consumer-conformance.test.mjs +150 -0
- package/test/rarebit-independent-contract.test.mjs +317 -0
- package/test/rarebit-recall.test.mjs +434 -0
- package/test/rarebit-sidecar-protocol.test.mjs +538 -0
- package/test/rarebit-visual-language.test.mjs +75 -0
- package/test/summary-attention-independent.test.mjs +172 -0
- package/test/types-history.ts +13 -0
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
import {
|
|
5
|
+
RAREBIT_SELECTOR_VERSION,
|
|
6
|
+
measureRarebits,
|
|
7
|
+
selectRarebits,
|
|
8
|
+
} from "./rarebit-core.mjs";
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_PI_SESSION_ROOT = join(
|
|
11
|
+
homedir(),
|
|
12
|
+
".pi",
|
|
13
|
+
"agent",
|
|
14
|
+
"sessions",
|
|
15
|
+
);
|
|
16
|
+
export const RAREBIT_QUERY_SCHEMA_VERSION = 1;
|
|
17
|
+
|
|
18
|
+
function queryError(message) {
|
|
19
|
+
const error = new Error(message);
|
|
20
|
+
error.name = "RarebitQueryError";
|
|
21
|
+
return error;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function isRegularFile(path) {
|
|
25
|
+
try {
|
|
26
|
+
return (await stat(path)).isFile();
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error?.code === "ENOENT") return false;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function* jsonlFiles(root) {
|
|
34
|
+
let entries;
|
|
35
|
+
try {
|
|
36
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error?.code === "ENOENT") return;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const path = join(root, entry.name);
|
|
43
|
+
if (entry.isDirectory()) yield* jsonlFiles(path);
|
|
44
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) yield path;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readHeader(path) {
|
|
49
|
+
const handle = await open(path, "r");
|
|
50
|
+
try {
|
|
51
|
+
const buffer = Buffer.alloc(16384);
|
|
52
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
53
|
+
const firstLine = buffer
|
|
54
|
+
.subarray(0, bytesRead)
|
|
55
|
+
.toString("utf8")
|
|
56
|
+
.split("\n", 1)[0]
|
|
57
|
+
?.trim();
|
|
58
|
+
if (!firstLine) return null;
|
|
59
|
+
try {
|
|
60
|
+
const header = JSON.parse(firstLine);
|
|
61
|
+
return header?.type === "session" ? header : null;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
await handle.close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function looksLikePath(reference) {
|
|
71
|
+
return (
|
|
72
|
+
reference.includes(sep) ||
|
|
73
|
+
reference.startsWith(".") ||
|
|
74
|
+
reference.endsWith(".jsonl")
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Resolve either an existing JSONL path or one exact Pi Session ID. */
|
|
79
|
+
export async function resolveRarebitSessionFile(
|
|
80
|
+
reference,
|
|
81
|
+
{ sessionRoot = DEFAULT_PI_SESSION_ROOT } = {},
|
|
82
|
+
) {
|
|
83
|
+
if (typeof reference !== "string" || !reference.trim())
|
|
84
|
+
throw queryError("--session requires an exact Pi Session path or ID");
|
|
85
|
+
const input = reference.trim();
|
|
86
|
+
const path = resolve(input);
|
|
87
|
+
if (await isRegularFile(path)) return path;
|
|
88
|
+
if (looksLikePath(input))
|
|
89
|
+
throw queryError(`Pi Session file not found: ${input}`);
|
|
90
|
+
const matches = [];
|
|
91
|
+
for await (const candidate of jsonlFiles(resolve(sessionRoot))) {
|
|
92
|
+
const header = await readHeader(candidate);
|
|
93
|
+
if (header?.id === input) matches.push(candidate);
|
|
94
|
+
}
|
|
95
|
+
if (matches.length === 0)
|
|
96
|
+
throw queryError(`No persisted Pi Session has exact ID ${input}`);
|
|
97
|
+
if (matches.length > 1)
|
|
98
|
+
throw queryError(
|
|
99
|
+
`Pi Session ID ${input} is ambiguous across ${matches.length} files; pass an exact path`,
|
|
100
|
+
);
|
|
101
|
+
return resolve(matches[0]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function parseNativeSession(content, source = "Pi Session") {
|
|
105
|
+
const records = [];
|
|
106
|
+
for (const [index, line] of String(content).split("\n").entries()) {
|
|
107
|
+
if (!line.trim()) continue;
|
|
108
|
+
try {
|
|
109
|
+
records.push(JSON.parse(line));
|
|
110
|
+
} catch {
|
|
111
|
+
throw queryError(
|
|
112
|
+
`${source} contains malformed JSON at line ${index + 1}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const header = records[0];
|
|
117
|
+
if (header?.type !== "session" || typeof header.id !== "string" || !header.id)
|
|
118
|
+
throw queryError(`${source} does not begin with a valid Pi Session header`);
|
|
119
|
+
const entries = records.slice(1);
|
|
120
|
+
const byId = new Map();
|
|
121
|
+
if ((header.version ?? 1) < 2) return { header, entries, byId, linear: true };
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
if (typeof entry?.id !== "string" || !entry.id)
|
|
124
|
+
throw queryError(`${source} contains a Session entry without an ID`);
|
|
125
|
+
if (byId.has(entry.id))
|
|
126
|
+
throw queryError(
|
|
127
|
+
`${source} contains duplicate Session entry ID ${entry.id}`,
|
|
128
|
+
);
|
|
129
|
+
byId.set(entry.id, entry);
|
|
130
|
+
}
|
|
131
|
+
return { header, entries, byId, linear: false };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Match Pi's persisted active branch: the final entry is the active leaf. */
|
|
135
|
+
export function resolveActiveBranch(
|
|
136
|
+
{ entries, byId, linear },
|
|
137
|
+
source = "Pi Session",
|
|
138
|
+
) {
|
|
139
|
+
if (linear) return entries.slice();
|
|
140
|
+
const leaf = entries.at(-1);
|
|
141
|
+
if (!leaf) return [];
|
|
142
|
+
const reversed = [];
|
|
143
|
+
const visited = new Set();
|
|
144
|
+
let current = leaf;
|
|
145
|
+
while (current) {
|
|
146
|
+
if (visited.has(current.id))
|
|
147
|
+
throw queryError(
|
|
148
|
+
`${source} contains a cycle at Session entry ${current.id}`,
|
|
149
|
+
);
|
|
150
|
+
visited.add(current.id);
|
|
151
|
+
reversed.push(current);
|
|
152
|
+
if (current.parentId === null || current.parentId === undefined) break;
|
|
153
|
+
current = byId.get(current.parentId);
|
|
154
|
+
if (!current)
|
|
155
|
+
throw queryError(
|
|
156
|
+
`${source} is missing parent Session entry ${reversed.at(-1).parentId}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return reversed.reverse();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sessionProjection(parsed, branch) {
|
|
163
|
+
const selection = selectRarebits(branch);
|
|
164
|
+
const measurement = measureRarebits(branch, selection);
|
|
165
|
+
return {
|
|
166
|
+
schemaVersion: RAREBIT_QUERY_SCHEMA_VERSION,
|
|
167
|
+
selectorVersion: RAREBIT_SELECTOR_VERSION,
|
|
168
|
+
session: {
|
|
169
|
+
id: parsed.header.id,
|
|
170
|
+
activeLeafId: parsed.linear ? null : (branch.at(-1)?.id ?? null),
|
|
171
|
+
// The original persisted header timestamp is provenance for a title
|
|
172
|
+
// date choice; source paths and cwd intentionally remain private.
|
|
173
|
+
startedAt:
|
|
174
|
+
typeof parsed.header.timestamp === "string"
|
|
175
|
+
? parsed.header.timestamp
|
|
176
|
+
: null,
|
|
177
|
+
},
|
|
178
|
+
selection,
|
|
179
|
+
measurement,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function readRarebitSession(reference, options = {}) {
|
|
184
|
+
const sessionFile = await resolveRarebitSessionFile(reference, options);
|
|
185
|
+
const parsed = parseNativeSession(
|
|
186
|
+
await readFile(sessionFile, "utf8"),
|
|
187
|
+
sessionFile,
|
|
188
|
+
);
|
|
189
|
+
const branch = resolveActiveBranch(parsed, sessionFile);
|
|
190
|
+
return { sessionFile, parsed, branch, ...sessionProjection(parsed, branch) };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Metadata-only projection, safe for a dashboard or programmatic inspection. */
|
|
194
|
+
export async function queryRarebits(reference, options = {}) {
|
|
195
|
+
const result = await readRarebitSession(reference, options);
|
|
196
|
+
return {
|
|
197
|
+
schemaVersion: result.schemaVersion,
|
|
198
|
+
selectorVersion: result.selectorVersion,
|
|
199
|
+
session: result.session,
|
|
200
|
+
rarebitCount: result.selection.occurrences.length,
|
|
201
|
+
measurement: result.measurement,
|
|
202
|
+
rarebits: result.selection.occurrences.map(
|
|
203
|
+
({ text, ...occurrence }) => occurrence,
|
|
204
|
+
),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Raw selected prose remains inspectable on demand but is never a sidecar authority. */
|
|
209
|
+
export async function extractRarebits(reference, options = {}) {
|
|
210
|
+
const result = await readRarebitSession(reference, options);
|
|
211
|
+
return {
|
|
212
|
+
schemaVersion: result.schemaVersion,
|
|
213
|
+
selectorVersion: result.selectorVersion,
|
|
214
|
+
session: result.session,
|
|
215
|
+
rarebitCount: result.selection.occurrences.length,
|
|
216
|
+
measurement: result.measurement,
|
|
217
|
+
rarebits: result.selection.occurrences.map(
|
|
218
|
+
({ sourceEntryId, timestamp, role, outcome, text }) => ({
|
|
219
|
+
sourceEntryId,
|
|
220
|
+
timestamp,
|
|
221
|
+
role,
|
|
222
|
+
outcome,
|
|
223
|
+
text,
|
|
224
|
+
}),
|
|
225
|
+
),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { DEFAULT_RAREBIT_SUMMARY_POLICY } from "./rarebit-core.mjs";
|
|
2
|
+
|
|
3
|
+
function nonEmptyString(value) {
|
|
4
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Merge just the dedicated Rarebit namespace. Pi's broader settings merge is
|
|
9
|
+
* deliberately not reimplemented here: this boundary has only one nested
|
|
10
|
+
* object whose inheritable fields must survive a Project override.
|
|
11
|
+
*/
|
|
12
|
+
export function mergeRarebitSettings(
|
|
13
|
+
globalSettings = {},
|
|
14
|
+
projectSettings = {},
|
|
15
|
+
) {
|
|
16
|
+
const globalRarebit =
|
|
17
|
+
globalSettings?.rarebit && typeof globalSettings.rarebit === "object"
|
|
18
|
+
? globalSettings.rarebit
|
|
19
|
+
: {};
|
|
20
|
+
const projectRarebit =
|
|
21
|
+
projectSettings?.rarebit && typeof projectSettings.rarebit === "object"
|
|
22
|
+
? projectSettings.rarebit
|
|
23
|
+
: {};
|
|
24
|
+
return { ...globalRarebit, ...projectRarebit };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function modelFromRarebitSettings(rarebit) {
|
|
28
|
+
const candidate = rarebit?.model;
|
|
29
|
+
if (candidate && typeof candidate === "object") {
|
|
30
|
+
const provider = nonEmptyString(candidate.provider);
|
|
31
|
+
const id = nonEmptyString(candidate.id);
|
|
32
|
+
if (provider && id) return { model: { provider, id } };
|
|
33
|
+
}
|
|
34
|
+
const spec = nonEmptyString(candidate);
|
|
35
|
+
if (spec) {
|
|
36
|
+
const separator = spec.indexOf("/");
|
|
37
|
+
if (separator > 0 && separator < spec.length - 1)
|
|
38
|
+
return {
|
|
39
|
+
model: {
|
|
40
|
+
provider: spec.slice(0, separator),
|
|
41
|
+
id: spec.slice(separator + 1),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (candidate === undefined)
|
|
46
|
+
return { error: "Rarebit setting rarebit.model is missing" };
|
|
47
|
+
return {
|
|
48
|
+
error:
|
|
49
|
+
"Rarebit setting rarebit.model must be provider/model or {provider,id}",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pure configuration interpretation used identically by the interactive Pi
|
|
55
|
+
* extension and the external CLI. A caller supplies already-read global and
|
|
56
|
+
* trusted-Project JSON objects; this function never reads files or falls back
|
|
57
|
+
* to Pi's interactive default model.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveRarebitSettings(
|
|
60
|
+
globalSettings = {},
|
|
61
|
+
projectSettings = {},
|
|
62
|
+
) {
|
|
63
|
+
const rarebit = mergeRarebitSettings(globalSettings, projectSettings);
|
|
64
|
+
const resolved = modelFromRarebitSettings(rarebit);
|
|
65
|
+
return {
|
|
66
|
+
...(resolved.model ? { model: resolved.model } : {}),
|
|
67
|
+
...(resolved.error ? { modelConfigurationError: resolved.error } : {}),
|
|
68
|
+
summaryPolicy: {
|
|
69
|
+
...DEFAULT_RAREBIT_SUMMARY_POLICY,
|
|
70
|
+
...(rarebit.min_total_length === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: { minTotalLength: Number(rarebit.min_total_length) }),
|
|
73
|
+
...(rarebit.max_rarebit_ratio === undefined
|
|
74
|
+
? {}
|
|
75
|
+
: { maxRarebitRatio: Number(rarebit.max_rarebit_ratio) }),
|
|
76
|
+
},
|
|
77
|
+
autoTitle: rarebit.auto_title !== false,
|
|
78
|
+
};
|
|
79
|
+
}
|