@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.
Files changed (50) hide show
  1. package/AGENTS.md +14 -0
  2. package/CHANGELOG.md +10 -0
  3. package/LICENSE +21 -0
  4. package/README.md +142 -0
  5. package/RUNBOOK.md +36 -0
  6. package/SECURITY.md +11 -0
  7. package/VISUAL-LANGUAGE.md +89 -0
  8. package/bin/rarebit.mjs +20 -0
  9. package/package.json +96 -0
  10. package/schemas/rarebit.schema.json +618 -0
  11. package/scripts/e2e-recall-pi083.mjs +199 -0
  12. package/scripts/e2e-summary-cache.mjs +257 -0
  13. package/scripts/verify-package.mjs +81 -0
  14. package/src/automatic-summary-policy.mjs +210 -0
  15. package/src/extension.mjs +516 -0
  16. package/src/herdr-activity.mjs +111 -0
  17. package/src/index.mjs +13 -0
  18. package/src/lifecycle.mjs +322 -0
  19. package/src/rarebit-activity.mjs +48 -0
  20. package/src/rarebit-artifact-state.mjs +839 -0
  21. package/src/rarebit-cli.mjs +305 -0
  22. package/src/rarebit-command.mjs +281 -0
  23. package/src/rarebit-core.mjs +525 -0
  24. package/src/rarebit-model.mjs +226 -0
  25. package/src/rarebit-recall.mjs +182 -0
  26. package/src/rarebit-service.mjs +579 -0
  27. package/src/rarebit-session.mjs +227 -0
  28. package/src/rarebit-settings.mjs +79 -0
  29. package/src/rarebit-store.mjs +771 -0
  30. package/src/rarebit-visual-language.mjs +110 -0
  31. package/src/types.d.ts +601 -0
  32. package/test/automatic-summary-policy.test.mjs +476 -0
  33. package/test/extension-activity.test.mjs +73 -0
  34. package/test/fixtures/branch-mixed.json +117 -0
  35. package/test/fixtures/conformance-corpus.v1.json +306 -0
  36. package/test/fixtures/truncated-mixed.jsonl +2 -0
  37. package/test/herdr-activity.test.mjs +93 -0
  38. package/test/lifecycle-contract-independent.test.mjs +627 -0
  39. package/test/rarebit-activity.test.mjs +82 -0
  40. package/test/rarebit-artifact-state.test.mjs +440 -0
  41. package/test/rarebit-cli.test.mjs +222 -0
  42. package/test/rarebit-command.test.mjs +127 -0
  43. package/test/rarebit-core.test.mjs +461 -0
  44. package/test/rarebit-cross-consumer-conformance.test.mjs +150 -0
  45. package/test/rarebit-independent-contract.test.mjs +317 -0
  46. package/test/rarebit-recall.test.mjs +434 -0
  47. package/test/rarebit-sidecar-protocol.test.mjs +538 -0
  48. package/test/rarebit-visual-language.test.mjs +75 -0
  49. package/test/summary-attention-independent.test.mjs +172 -0
  50. package/test/types-history.ts +13 -0
@@ -0,0 +1,516 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { complete } from "@earendil-works/pi-ai/compat";
5
+
6
+ export * from "./rarebit-core.mjs";
7
+ export * from "./rarebit-model.mjs";
8
+ export * from "./automatic-summary-policy.mjs";
9
+ export * from "./rarebit-service.mjs";
10
+ export * from "./rarebit-store.mjs";
11
+
12
+ import {
13
+ processRarebitSummary,
14
+ processRarebitTitle,
15
+ } from "./rarebit-service.mjs";
16
+ import {
17
+ DEFAULT_RAREBIT_SUMMARY_POLICY,
18
+ selectRarebits,
19
+ sha256,
20
+ } from "./rarebit-core.mjs";
21
+ import { projectRarebitSessionActivity } from "./rarebit-activity.mjs";
22
+ import { createHerdrActivityReporter } from "./herdr-activity.mjs";
23
+ import { resolveRarebitSettings } from "./rarebit-settings.mjs";
24
+ import {
25
+ getRarebitArgumentCompletions,
26
+ parseRarebitCommand,
27
+ rarebitCommandDescription,
28
+ } from "./rarebit-command.mjs";
29
+ import {
30
+ RAREBIT_CONVERSATION_SCHEMA_VERSION,
31
+ RAREBIT_RECALL_SCHEMA_VERSION,
32
+ materializeRarebitRecall,
33
+ } from "./rarebit-recall.mjs";
34
+ import {
35
+ createDetachedMaterializer,
36
+ registerRarebitLifecycle,
37
+ } from "./lifecycle.mjs";
38
+ import {
39
+ automaticSummaryInhibitionIdentity,
40
+ queryAutomaticSummaryPolicy,
41
+ } from "./automatic-summary-policy.mjs";
42
+
43
+ async function readSettings(path) {
44
+ try {
45
+ return { value: JSON.parse(await readFile(path, "utf8")), path };
46
+ } catch (error) {
47
+ if (error?.code === "ENOENT") return { value: {}, path };
48
+ return { error, path };
49
+ }
50
+ }
51
+
52
+ function configuredAgentDir(env = process.env) {
53
+ return env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
54
+ }
55
+
56
+ export async function readConfiguredRarebitSettings({
57
+ cwd,
58
+ projectTrusted,
59
+ agentDir = configuredAgentDir(),
60
+ } = {}) {
61
+ const global = await readSettings(join(agentDir, "settings.json"));
62
+ const project = projectTrusted
63
+ ? await readSettings(join(cwd ?? process.cwd(), ".pi", "settings.json"))
64
+ : { value: {}, path: null };
65
+ const failed = [global, project].find((entry) => entry.error);
66
+ const rawRefs = [global.path, project.path].filter(Boolean);
67
+ if (failed)
68
+ return {
69
+ modelConfigurationError: `Cannot read configured Pi settings files: ${failed.error.message}`,
70
+ modelProvenance: {
71
+ source: "pi_settings_files",
72
+ settingsKey: "rarebit.model",
73
+ status: "invalid",
74
+ rawRefs,
75
+ },
76
+ };
77
+ const resolved = resolveRarebitSettings(global.value, project.value);
78
+ return {
79
+ ...(resolved.model ? { model: resolved.model } : {}),
80
+ ...(resolved.modelConfigurationError
81
+ ? { modelConfigurationError: resolved.modelConfigurationError }
82
+ : {}),
83
+ modelProvenance: {
84
+ source: "rarebit_settings_files",
85
+ settingsKey: "rarebit.model",
86
+ status: resolved.model ? "resolved" : "invalid",
87
+ rawRefs,
88
+ },
89
+ summaryPolicy: resolved.summaryPolicy,
90
+ autoTitle: resolved.autoTitle,
91
+ };
92
+ }
93
+
94
+ export default function registerPiRarebit(pi, config = {}) {
95
+ const {
96
+ settingsLoader = readConfiguredRarebitSettings,
97
+ recallMaterializer = materializeRarebitRecall,
98
+ ...explicit
99
+ } = config;
100
+ if (!pi?.on) throw new TypeError("A Pi ExtensionAPI with .on is required");
101
+ const eventPolicyQuery = (session) =>
102
+ queryAutomaticSummaryPolicy(pi.events, session, {
103
+ timeoutMs: explicit.automaticSummaryPolicyTimeoutMs,
104
+ });
105
+ const activityReporter =
106
+ explicit.activityReporter ?? createHerdrActivityReporter();
107
+ const projectActivity = (ctx) =>
108
+ projectRarebitSessionActivity(
109
+ selectRarebits(ctx?.sessionManager?.getBranch?.() ?? []),
110
+ );
111
+
112
+ const notify = (ctx, text, level = "info") => {
113
+ if (!ctx?.hasUI || typeof ctx?.ui?.notify !== "function") return;
114
+ try {
115
+ ctx.ui.notify(text, level);
116
+ } catch {
117
+ // TUI feedback is best-effort and must not change the materialization
118
+ // outcome or the agent-visible Session.
119
+ }
120
+ };
121
+
122
+ const modelLabel = ({ provider, id } = {}) =>
123
+ [provider, id].filter(Boolean).join("/") || "unavailable";
124
+
125
+ const resultModelLabel = (result) => {
126
+ const receipt = result?.record?.synthesis;
127
+ const requested = receipt?.requestedModel ?? result?.record?.model;
128
+ const provider = receipt?.provider?.responseProvider ?? requested?.provider;
129
+ const id = receipt?.provider?.responseModel ?? requested?.id;
130
+ return modelLabel({ provider, id });
131
+ };
132
+
133
+ const tokenLabel = (name, value) =>
134
+ typeof value === "number" && Number.isFinite(value)
135
+ ? `${name} tokens: ${value}`
136
+ : `${name} tokens: unavailable`;
137
+
138
+ const notifyOutcome = (ctx, result, triggered) => {
139
+ if (!triggered || result?.duplicate || result?.inFlight) return;
140
+ switch (result?.record?.status) {
141
+ case "ok":
142
+ notify(
143
+ ctx,
144
+ `Rarebit Summary updated (${tokenLabel("input", result.record.synthesis?.usage?.inputTokens)} · ${tokenLabel("output", result.record.synthesis?.usage?.outputTokens)} · model ${resultModelLabel(result)})`,
145
+ "info",
146
+ );
147
+ break;
148
+ case "unavailable_overflow":
149
+ notify(
150
+ ctx,
151
+ "Rarebit Summary unavailable: input is too large",
152
+ "warning",
153
+ );
154
+ break;
155
+ case "failure":
156
+ notify(
157
+ ctx,
158
+ "Rarebit Summary failed; inspect its private materialization",
159
+ "error",
160
+ );
161
+ break;
162
+ case "conflict":
163
+ notify(
164
+ ctx,
165
+ "Rarebit Summary conflict; inspect its private materialization",
166
+ "warning",
167
+ );
168
+ break;
169
+ default:
170
+ break;
171
+ }
172
+ };
173
+
174
+ let policyOverrides = {};
175
+ const loadEffective = async (ctx) => {
176
+ let injected = {};
177
+ if (!explicit.model) {
178
+ try {
179
+ injected = await settingsLoader({
180
+ cwd: ctx?.cwd,
181
+ projectTrusted: ctx?.isProjectTrusted?.() === true,
182
+ });
183
+ } catch (error) {
184
+ injected = {
185
+ modelConfigurationError: `Cannot resolve configured Pi settings files: ${error?.message ?? error}`,
186
+ modelProvenance: {
187
+ source: "pi_settings_files",
188
+ settingsKey: "rarebit.model",
189
+ status: "invalid",
190
+ },
191
+ };
192
+ }
193
+ }
194
+ const summaryPolicy = {
195
+ ...(injected.summaryPolicy ?? DEFAULT_RAREBIT_SUMMARY_POLICY),
196
+ ...(explicit.summaryPolicy ?? {}),
197
+ ...policyOverrides,
198
+ };
199
+ return {
200
+ ...injected,
201
+ ...explicit,
202
+ summaryPolicy,
203
+ piAi: explicit.piAi ?? { complete },
204
+ };
205
+ };
206
+
207
+ const materialize = async (ctx, { force = false } = {}) => {
208
+ let synthesisTriggered = false;
209
+ const effective = await loadEffective(ctx);
210
+ const result = await processRarebitSummary(ctx, {
211
+ ...effective,
212
+ forceSynthesis: force,
213
+ lifecycleBoundary: force ? "manual" : ctx?.lifecycleBoundary,
214
+ queryAutomaticSummaryPolicy:
215
+ explicit.queryAutomaticSummaryPolicy ?? eventPolicyQuery,
216
+ onSynthesisTriggered: (detail) => {
217
+ synthesisTriggered = true;
218
+ const count = detail.rarebitCount ?? detail.rarebitCount;
219
+ notify(
220
+ ctx,
221
+ `Rarebit Summary triggered (${count} Rarebits · input tokens: ~${detail.estimatedInputTokens} estimated (chars/4) · model ${modelLabel(detail.model)})`,
222
+ "info",
223
+ );
224
+ },
225
+ });
226
+ notifyOutcome(ctx, result, synthesisTriggered);
227
+ return result;
228
+ };
229
+ const schedule = createDetachedMaterializer(materialize, (_error, ctx) => {
230
+ notify(
231
+ ctx,
232
+ "Rarebit Summary failed; inspect its private materialization",
233
+ "error",
234
+ );
235
+ });
236
+ const scheduleManualSummary = createDetachedMaterializer(
237
+ (ctx) => materialize(ctx, { force: true }),
238
+ (_error, ctx) =>
239
+ notify(
240
+ ctx,
241
+ "Rarebit Summary failed; inspect its private materialization",
242
+ "error",
243
+ ),
244
+ );
245
+
246
+ const identityFrom = (ctx) => ({
247
+ sessionId: String(
248
+ ctx?.sessionManager?.getHeader?.()?.id ?? ctx?.sessionId ?? "",
249
+ ),
250
+ sessionFile: ctx?.sessionManager?.getSessionFile?.(),
251
+ });
252
+ let activeSession;
253
+ let autoTitleOverride;
254
+ const ownerTitleEvidence = new Map();
255
+ const discardRecall = async (recall) => {
256
+ try {
257
+ await recall?.discard?.();
258
+ } catch {
259
+ // OS-temp cleanup is best-effort and must not attach stale context,
260
+ // block an unrelated prompt, or change the command's delivery outcome.
261
+ }
262
+ };
263
+
264
+ const sameActiveSession = ({ sessionId, sessionFile }) =>
265
+ Boolean(
266
+ activeSession &&
267
+ activeSession.sessionId === sessionId &&
268
+ activeSession.sessionFile === sessionFile,
269
+ );
270
+ const localIsoDate = (date = new Date()) =>
271
+ `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
272
+ const generateTitle = async (ctx, { manual = false, ownerMessage } = {}) => {
273
+ const session = identityFrom(ctx);
274
+ if (!session.sessionId || !session.sessionFile)
275
+ return { status: "skipped_unpersisted_session" };
276
+ if (!sameActiveSession(session))
277
+ return { status: "skipped_session_changed" };
278
+ const priorTitle = pi.getSessionName?.();
279
+ if (!manual && priorTitle) return { status: "skipped_existing_title" };
280
+ const evidence = ownerMessage ?? ownerTitleEvidence.get(session.sessionId);
281
+ const sourceEntryId = evidence?.sourceEntryId ?? null;
282
+ if (!manual && !sourceEntryId && !evidence?.text) {
283
+ return { status: "skipped_owner_evidence_not_found" };
284
+ }
285
+ const effective = await loadEffective(ctx);
286
+ const result = await processRarebitTitle(ctx, {
287
+ ...effective,
288
+ sourceEntryId: sourceEntryId ?? undefined,
289
+ sourceText: !sourceEntryId && evidence?.text ? evidence.text : undefined,
290
+ allowFirstUserFallback: manual && !sourceEntryId,
291
+ evidenceProvenance: evidence
292
+ ? sourceEntryId
293
+ ? "captured_interactive_rpc"
294
+ : "captured_interactive_rpc_text_match"
295
+ : "branch_user_fallback",
296
+ titleDate: localIsoDate(),
297
+ priorTitle: priorTitle ?? null,
298
+ requestIdentity: manual ? `manual:${sha256(priorTitle ?? "")}` : "auto",
299
+ applyTitle: ({ title }) => {
300
+ if (!sameActiveSession(session))
301
+ return { status: "skipped_session_changed" };
302
+ if (manual && pi.getSessionName?.() !== priorTitle)
303
+ return { status: "skipped_title_changed" };
304
+ if (!manual && pi.getSessionName?.())
305
+ return { status: "skipped_existing_title" };
306
+ pi.setSessionName(title);
307
+ return { status: "applied" };
308
+ },
309
+ });
310
+ if (result.record?.status === "applied")
311
+ notify(ctx, `Rarebit titled Session: ${result.record.title}`, "info");
312
+ else if (
313
+ manual &&
314
+ result.record?.status === "skipped_owner_evidence_not_found"
315
+ )
316
+ notify(
317
+ ctx,
318
+ "Rarebit cannot generate a title: persisted user-message evidence is unavailable",
319
+ "warning",
320
+ );
321
+ return result;
322
+ };
323
+ const scheduleTitle = createDetachedMaterializer(
324
+ (ctx) => generateTitle(ctx, { ownerMessage: ctx?.rarebitOwnerMessage }),
325
+ (_error, ctx) =>
326
+ notify(
327
+ ctx,
328
+ "Rarebit auto-title failed; use /rarebit title to retry",
329
+ "error",
330
+ ),
331
+ );
332
+
333
+ registerRarebitLifecycle(pi, schedule, {
334
+ onSessionStart: (ctx) => {
335
+ activeSession = identityFrom(ctx);
336
+ activityReporter.start(projectActivity(ctx));
337
+ },
338
+ onSessionTree: (ctx) => {
339
+ activityReporter.update(projectActivity(ctx));
340
+ },
341
+ onSelectedUserPersisted: (ctx) => {
342
+ activityReporter.update(projectActivity(ctx));
343
+ },
344
+ onAgentSettled: (ctx) => {
345
+ activityReporter.update(projectActivity(ctx));
346
+ },
347
+ onSessionShutdown: () => {
348
+ activeSession = undefined;
349
+ activityReporter.stop();
350
+ },
351
+ onFirstPersistedOwnerMessage: (ctx, ownerMessage) => {
352
+ const sessionId = identityFrom(ctx).sessionId;
353
+ ownerTitleEvidence.set(sessionId, ownerMessage);
354
+ if (ownerTitleEvidence.size > 256)
355
+ ownerTitleEvidence.delete(ownerTitleEvidence.keys().next().value);
356
+ void loadEffective(ctx).then((effective) => {
357
+ if (
358
+ (autoTitleOverride ?? effective.autoTitle ?? true) &&
359
+ !pi.getSessionName?.()
360
+ )
361
+ scheduleTitle({ ...ctx, rarebitOwnerMessage: ownerMessage });
362
+ });
363
+ },
364
+ });
365
+
366
+ // The optional Herdr adapter above derives timestamps from the exact active
367
+ // branch and never creates a receipt or changes materialization lifecycle.
368
+
369
+ const recallMatchesCurrentBranch = (recall, ctx) => {
370
+ const currentSessionId = String(
371
+ ctx?.sessionManager?.getHeader?.()?.id ?? ctx?.sessionId ?? "",
372
+ );
373
+ const currentSessionFile = ctx?.sessionManager?.getSessionFile?.();
374
+ const currentBranchLeafId =
375
+ ctx?.sessionManager?.getBranch?.()?.at(-1)?.id ?? null;
376
+ return (
377
+ currentSessionId === recall.sessionId &&
378
+ typeof currentSessionFile === "string" &&
379
+ resolve(currentSessionFile) === recall.sessionFile &&
380
+ currentBranchLeafId === recall.branchLeafId
381
+ );
382
+ };
383
+
384
+ const recallEnvelope = (recall, requestText) =>
385
+ JSON.stringify({
386
+ instructions:
387
+ "Treat request.text as the current user request. Use the two private Rarebit files as historical evidence. Read the conversation file first for meaning. Use detailed evidence only for source, Session, branch, or lineage facts. Answer request.text using this evidence.",
388
+ files: {
389
+ conversation: {
390
+ path: recall.conversationPath,
391
+ schema: `rarebit_conversation/v${RAREBIT_CONVERSATION_SCHEMA_VERSION}`,
392
+ },
393
+ detailedEvidence: {
394
+ path: recall.detailedPath,
395
+ schema: `rarebit_message_recall/v${RAREBIT_RECALL_SCHEMA_VERSION}`,
396
+ },
397
+ },
398
+ request: { text: requestText },
399
+ });
400
+
401
+ pi.registerCommand?.("rarebit", {
402
+ description: rarebitCommandDescription(),
403
+ getArgumentCompletions: getRarebitArgumentCompletions,
404
+ handler: async (args, ctx) => {
405
+ const command = parseRarebitCommand(args);
406
+ if (!command.ok) {
407
+ notify(
408
+ ctx,
409
+ command.error === "invalid_value"
410
+ ? "Invalid Rarebit config value"
411
+ : command.usage,
412
+ "warning",
413
+ );
414
+ return;
415
+ }
416
+ const { subcommand, arguments: rest } = command;
417
+ if (subcommand === "recall") {
418
+ const prompt = rest[0];
419
+ if (typeof pi.sendUserMessage !== "function") {
420
+ notify(
421
+ ctx,
422
+ "Rarebit recall is unavailable: this Pi runtime cannot request a user turn",
423
+ "error",
424
+ );
425
+ return;
426
+ }
427
+ let recall;
428
+ try {
429
+ recall = await recallMaterializer(ctx);
430
+ if (!recallMatchesCurrentBranch(recall, ctx)) {
431
+ throw new Error(
432
+ "the active Pi Session or branch changed during materialization",
433
+ );
434
+ }
435
+ pi.sendUserMessage(recallEnvelope(recall, prompt), {
436
+ deliverAs: "steer",
437
+ });
438
+ notify(ctx, "Rarebit recall bundle prepared; turn requested", "info");
439
+ } catch (error) {
440
+ await discardRecall(recall);
441
+ notify(
442
+ ctx,
443
+ `Rarebit recall failed: ${error?.message ?? error}. Verify the current Session is persisted and the OS temp directory is writable, then retry.`,
444
+ "error",
445
+ );
446
+ }
447
+ return;
448
+ }
449
+ const effective = await loadEffective(ctx);
450
+ const policy = effective.summaryPolicy ?? DEFAULT_RAREBIT_SUMMARY_POLICY;
451
+ if (
452
+ subcommand === "status" ||
453
+ (subcommand === "config" && rest.length === 0)
454
+ ) {
455
+ const session = identityFrom(ctx);
456
+ const automaticPolicy =
457
+ session.sessionId && session.sessionFile
458
+ ? await (explicit.queryAutomaticSummaryPolicy ?? eventPolicyQuery)({
459
+ sessionId: session.sessionId,
460
+ durableAssociation: session.sessionFile,
461
+ })
462
+ : { decision: "abstain", queryStatus: "unpersisted_session" };
463
+ const policyStatus = automaticSummaryInhibitionIdentity(automaticPolicy)
464
+ ? `; automatic summary inhibited by team-management policy (${automaticPolicy.provider}/${automaticPolicy.reason})`
465
+ : "";
466
+ const ratioSource = Object.hasOwn(policyOverrides, "maxRarebitRatio")
467
+ ? "process_override"
468
+ : "settings_or_default";
469
+ const lengthSource = Object.hasOwn(policyOverrides, "minTotalLength")
470
+ ? "process_override"
471
+ : "settings_or_default";
472
+ notify(
473
+ ctx,
474
+ `Rarebit ${subcommand}: auto-title=${autoTitleOverride ?? effective.autoTitle ?? true}; max_rarebit_ratio=${policy.maxRarebitRatio} (${ratioSource}); min_total_length=${policy.minTotalLength} estimated tokens via ceil(chars/4) (${lengthSource}); measurement=${policy.measurementVersion}; model=${modelLabel(effective.model)} from ${effective.modelProvenance?.settingsKey ?? "explicit config"}${policyStatus}`,
475
+ "info",
476
+ );
477
+ return;
478
+ }
479
+ if (subcommand === "config") {
480
+ const [name, rawValue] = rest;
481
+ const value = Number(rawValue);
482
+ if (name === "max_rarebit_ratio" && value >= 0 && value <= 1)
483
+ policyOverrides = { ...policyOverrides, maxRarebitRatio: value };
484
+ else if (name === "min_total_length" && value >= 0)
485
+ policyOverrides = { ...policyOverrides, minTotalLength: value };
486
+ else {
487
+ notify(ctx, "Invalid Rarebit config value", "warning");
488
+ return;
489
+ }
490
+ notify(ctx, `Rarebit process override set: ${name}=${value}`, "info");
491
+ return;
492
+ }
493
+ if (subcommand === "auto-title") {
494
+ const value = rest[0];
495
+ if (value === "on") autoTitleOverride = true;
496
+ else if (value === "off") autoTitleOverride = false;
497
+ notify(
498
+ ctx,
499
+ `Rarebit auto-title is ${(autoTitleOverride ?? effective.autoTitle ?? true) ? "on" : "off"}`,
500
+ "info",
501
+ );
502
+ return;
503
+ }
504
+ if (subcommand === "title") {
505
+ void generateTitle(ctx, { manual: true }).catch(() =>
506
+ notify(ctx, "Rarebit title generation failed", "error"),
507
+ );
508
+ return;
509
+ }
510
+ if (subcommand === "summarize") {
511
+ scheduleManualSummary(ctx);
512
+ return;
513
+ }
514
+ },
515
+ });
516
+ }
@@ -0,0 +1,111 @@
1
+ import net from "node:net";
2
+
3
+ const SOURCE = "rarebit:activity";
4
+ const APPLIES_TO_SOURCE = "herdr:pi";
5
+ const REFRESH_MS = 30_000;
6
+ const TTL_MS = 90_000;
7
+
8
+ function socketEndpoint(env) {
9
+ const path = env.HERDR_SOCKET_PATH;
10
+ if (!path) return null;
11
+ return process.platform === "win32" ? `\\\\.\\pipe\\${path}` : path;
12
+ }
13
+
14
+ function durationLabel(timestamp, now) {
15
+ if (typeof timestamp !== "string") return null;
16
+ const elapsed = now - Date.parse(timestamp);
17
+ if (!Number.isFinite(elapsed) || elapsed < 0) return null;
18
+ const totalMinutes = Math.floor(elapsed / 60_000);
19
+ if (totalMinutes < 1) return "now";
20
+ const hours = Math.floor(totalMinutes / 60);
21
+ const minutes = totalMinutes % 60;
22
+ if (hours >= 24) {
23
+ const days = Math.floor(hours / 24);
24
+ return `${days}d ${hours % 24}h`;
25
+ }
26
+ return hours > 0
27
+ ? `${hours}h ${String(minutes).padStart(2, "0")}m`
28
+ : `${minutes}m`;
29
+ }
30
+
31
+ export function herdrActivityTokens(activity, now = Date.now()) {
32
+ return {
33
+ rarebit_user_age: durationLabel(activity?.latestUser?.timestamp, now),
34
+ rarebit_stop_age: durationLabel(activity?.latestAgentStop?.timestamp, now),
35
+ };
36
+ }
37
+
38
+ function sendHerdrRequest(endpoint, request, timeoutMs = 500) {
39
+ return new Promise((resolve) => {
40
+ let settled = false;
41
+ let timer;
42
+ const finish = () => {
43
+ if (settled) return;
44
+ settled = true;
45
+ clearTimeout(timer);
46
+ socket.destroy();
47
+ resolve();
48
+ };
49
+ const socket = net.createConnection(endpoint);
50
+ socket.on("error", finish);
51
+ socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
52
+ socket.on("data", finish);
53
+ socket.on("end", finish);
54
+ timer = setTimeout(finish, timeoutMs);
55
+ timer.unref?.();
56
+ });
57
+ }
58
+
59
+ /** Optional Herdr adapter; no connection or timer is created outside Herdr. */
60
+ export function createHerdrActivityReporter({
61
+ env = process.env,
62
+ now = () => Date.now(),
63
+ send = sendHerdrRequest,
64
+ setIntervalFn = setInterval,
65
+ clearIntervalFn = clearInterval,
66
+ } = {}) {
67
+ const endpoint = env.HERDR_ENV === "1" ? socketEndpoint(env) : null;
68
+ const paneId = env.HERDR_PANE_ID;
69
+ let sequence = Date.now() * 1000;
70
+ let activity = null;
71
+ let timer = null;
72
+
73
+ const report = () => {
74
+ if (!endpoint || !paneId) return;
75
+ sequence += 1;
76
+ void send(endpoint, {
77
+ id: `${SOURCE}:${sequence}`,
78
+ method: "pane.report_metadata",
79
+ params: {
80
+ pane_id: paneId,
81
+ source: SOURCE,
82
+ applies_to_source: APPLIES_TO_SOURCE,
83
+ seq: sequence,
84
+ ttl_ms: TTL_MS,
85
+ tokens: herdrActivityTokens(activity, now()),
86
+ },
87
+ });
88
+ };
89
+
90
+ return {
91
+ start(nextActivity) {
92
+ activity = nextActivity;
93
+ if (timer) clearIntervalFn(timer);
94
+ report();
95
+ if (endpoint && paneId) {
96
+ timer = setIntervalFn(report, REFRESH_MS);
97
+ timer.unref?.();
98
+ }
99
+ },
100
+ update(nextActivity) {
101
+ activity = nextActivity;
102
+ report();
103
+ },
104
+ stop() {
105
+ if (timer) clearIntervalFn(timer);
106
+ timer = null;
107
+ activity = null;
108
+ report();
109
+ },
110
+ };
111
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,13 @@
1
+ // Public Rarebit surface. Historical Rarebit selection/materialization is
2
+ // intentionally not re-exported: it was superseded in the Alpha clean-cut.
3
+ export * from "./rarebit-core.mjs";
4
+ export * from "./rarebit-activity.mjs";
5
+ export * from "./rarebit-model.mjs";
6
+ export * from "./automatic-summary-policy.mjs";
7
+ export * from "./rarebit-service.mjs";
8
+ export * from "./rarebit-session.mjs";
9
+ export * from "./rarebit-store.mjs";
10
+ export * from "./rarebit-artifact-state.mjs";
11
+ export * from "./rarebit-visual-language.mjs";
12
+ export * from "./rarebit-settings.mjs";
13
+ export { default } from "./extension.mjs";