peon-mem 1.0.0

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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +301 -0
  3. package/bin/peon-mem.mjs +273 -0
  4. package/dist/brain.d.ts +72 -0
  5. package/dist/brain.js +224 -0
  6. package/dist/compression.d.ts +9 -0
  7. package/dist/compression.js +37 -0
  8. package/dist/config.d.ts +22 -0
  9. package/dist/config.js +99 -0
  10. package/dist/daemon-cli.d.ts +2 -0
  11. package/dist/daemon-cli.js +54 -0
  12. package/dist/daemon.d.ts +23 -0
  13. package/dist/daemon.js +1078 -0
  14. package/dist/embedding-store.d.ts +43 -0
  15. package/dist/embedding-store.js +169 -0
  16. package/dist/embeddings.d.ts +93 -0
  17. package/dist/embeddings.js +345 -0
  18. package/dist/entities.d.ts +61 -0
  19. package/dist/entities.js +191 -0
  20. package/dist/entity-extraction.d.ts +33 -0
  21. package/dist/entity-extraction.js +75 -0
  22. package/dist/eval-metrics.d.ts +27 -0
  23. package/dist/eval-metrics.js +50 -0
  24. package/dist/evaluation.d.ts +58 -0
  25. package/dist/evaluation.js +244 -0
  26. package/dist/global-extraction.d.ts +15 -0
  27. package/dist/global-extraction.js +61 -0
  28. package/dist/global-memory.d.ts +43 -0
  29. package/dist/global-memory.js +306 -0
  30. package/dist/global-promotion.d.ts +25 -0
  31. package/dist/global-promotion.js +29 -0
  32. package/dist/hyde.d.ts +31 -0
  33. package/dist/hyde.js +46 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.js +246 -0
  36. package/dist/injection.d.ts +38 -0
  37. package/dist/injection.js +133 -0
  38. package/dist/logger.d.ts +17 -0
  39. package/dist/logger.js +63 -0
  40. package/dist/memory-mutations.d.ts +24 -0
  41. package/dist/memory-mutations.js +57 -0
  42. package/dist/memory-store.d.ts +194 -0
  43. package/dist/memory-store.js +1205 -0
  44. package/dist/monitor.d.ts +13 -0
  45. package/dist/monitor.js +977 -0
  46. package/dist/overview.d.ts +73 -0
  47. package/dist/overview.js +104 -0
  48. package/dist/processor.d.ts +90 -0
  49. package/dist/processor.js +450 -0
  50. package/dist/quality.d.ts +86 -0
  51. package/dist/quality.js +338 -0
  52. package/dist/recuration.d.ts +13 -0
  53. package/dist/recuration.js +65 -0
  54. package/dist/reranker.d.ts +34 -0
  55. package/dist/reranker.js +89 -0
  56. package/dist/retrieval.d.ts +106 -0
  57. package/dist/retrieval.js +392 -0
  58. package/dist/session-index.d.ts +34 -0
  59. package/dist/session-index.js +87 -0
  60. package/dist/temporal.d.ts +20 -0
  61. package/dist/temporal.js +62 -0
  62. package/dist/token-ab-monitor.d.ts +1 -0
  63. package/dist/token-ab-monitor.js +7 -0
  64. package/dist/tools.d.ts +232 -0
  65. package/dist/tools.js +546 -0
  66. package/dist/types.d.ts +169 -0
  67. package/dist/types.js +1 -0
  68. package/docs/assets/neural-universe.png +0 -0
  69. package/package.json +57 -0
  70. package/scripts/claude-peon-hook.mjs +522 -0
  71. package/scripts/codex-peon-hook.mjs +4 -0
  72. package/scripts/eval-retrieval-labeled.mjs +135 -0
  73. package/scripts/eval-retrieval.mjs +96 -0
  74. package/scripts/evaluate-peon.mjs +47 -0
  75. package/scripts/install-peon-stl.mjs +82 -0
  76. package/scripts/install-peon.mjs +318 -0
  77. package/scripts/lib/eval-ledger.mjs +104 -0
  78. package/scripts/lib/stl-classify.mjs +44 -0
  79. package/scripts/longmemeval-eval.mjs +144 -0
  80. package/scripts/peon-report.mjs +155 -0
  81. package/scripts/peon-stl.mjs +506 -0
  82. package/scripts/token-ab-monitor.html +235 -0
package/dist/tools.js ADDED
@@ -0,0 +1,546 @@
1
+ import { basename } from "node:path";
2
+ import { loadPeonConfig } from "./config.js";
3
+ import { createEmbeddingClient } from "./embeddings.js";
4
+ import { PeonMemoryStore } from "./memory-store.js";
5
+ import { PeonMemoryProcessor } from "./processor.js";
6
+ import { selectMemoryRecordsForContext } from "./retrieval.js";
7
+ import { createQualityReport } from "./quality.js";
8
+ import { PeonGlobalMemoryStore } from "./global-memory.js";
9
+ import { redactSecrets } from "./injection.js";
10
+ import { selectGloballyPromotable } from "./global-promotion.js";
11
+ import { createClusterSummarizer } from "./compression.js";
12
+ import { createGlobalExtractor } from "./global-extraction.js";
13
+ import { createRecurator } from "./recuration.js";
14
+ import { evaluatePeonProject } from "./evaluation.js";
15
+ import { buildContextInjection } from "./injection.js";
16
+ import { SessionIndex } from "./session-index.js";
17
+ export function createPeonTools(options = {}) {
18
+ if (options.daemonUrl) {
19
+ return createDaemonBackedTools(options.daemonUrl);
20
+ }
21
+ const storesByProject = new Map();
22
+ const sessionIndex = new SessionIndex(options.sessionIndexPath);
23
+ let globalStorePromise;
24
+ async function storeFor(projectPath) {
25
+ const existing = storesByProject.get(projectPath);
26
+ if (existing)
27
+ return existing;
28
+ const store = await PeonMemoryStore.open({ projectPath });
29
+ storesByProject.set(projectPath, store);
30
+ return store;
31
+ }
32
+ async function storeForSession(sessionId) {
33
+ const record = await sessionIndex.get(sessionId);
34
+ if (!record)
35
+ throw new Error(`Unknown Peon session: ${sessionId}`);
36
+ const store = await storeFor(record.projectPath);
37
+ // Rehydrate the session so record/end operations work after a daemon restart.
38
+ store.ensureSession({
39
+ id: record.sessionId,
40
+ projectPath: record.projectPath,
41
+ client: record.client,
42
+ cwd: record.cwd,
43
+ startedAt: record.startedAt
44
+ });
45
+ return store;
46
+ }
47
+ async function globalStore() {
48
+ globalStorePromise ??= PeonGlobalMemoryStore.open({ globalDir: options.globalMemoryDir });
49
+ return globalStorePromise;
50
+ }
51
+ return {
52
+ async startSession(input) {
53
+ const store = await storeFor(input.projectPath);
54
+ const cwd = input.cwd ?? input.projectPath;
55
+ const session = await store.startSession({ client: input.client, cwd });
56
+ await sessionIndex.set({
57
+ sessionId: session.id,
58
+ projectPath: input.projectPath,
59
+ client: input.client,
60
+ cwd,
61
+ startedAt: session.startedAt
62
+ });
63
+ return { sessionId: session.id, projectPath: input.projectPath };
64
+ },
65
+ async recordMessage(input) {
66
+ const store = await storeForSession(input.sessionId);
67
+ return store.recordMessage(input);
68
+ },
69
+ async recordEvent(input) {
70
+ const store = await storeForSession(input.sessionId);
71
+ return store.recordEvent(input);
72
+ },
73
+ async endSession(input) {
74
+ const store = await storeForSession(input.sessionId);
75
+ const result = await store.endSession(input);
76
+ await sessionIndex.remove(input.sessionId);
77
+ return result;
78
+ },
79
+ async getContext(input) {
80
+ const store = await storeFor(input.projectPath);
81
+ const context = await store.getContext({ query: input.query, maxChars: input.maxChars });
82
+ // HIERARCHY: the global brain is the PARENT of every project brain — recall inherits from
83
+ // it. Append the top query-relevant global beliefs as their own section (small budget,
84
+ // redacted like everything else). Failures degrade silently — the project context stands.
85
+ try {
86
+ const g = (await (await globalStore()).search(input.query ?? "")).slice(0, 4);
87
+ if (g.length > 0) {
88
+ context.global = redactSecrets(g.map((r) => "- [" + r.type + "] " + r.content).join("\n").slice(0, 1200));
89
+ }
90
+ }
91
+ catch {
92
+ // global brain unavailable — project-only context is still valid
93
+ }
94
+ return context;
95
+ },
96
+ async inspectBrain(input) {
97
+ const store = await storeFor(input.projectPath);
98
+ return store.inspectBrain({ query: input.query, maxChars: input.maxChars });
99
+ },
100
+ async searchMemory(input) {
101
+ const store = await storeFor(input.projectPath);
102
+ const ranked = await store.rankRecords(input.query, { limit: input.limit ?? 50 });
103
+ const selected = selectMemoryRecordsForContext(ranked, {
104
+ maxChars: input.maxChars ?? 4000,
105
+ recordFormatter: formatRankedMemoryRecord
106
+ });
107
+ return {
108
+ projectPath: input.projectPath,
109
+ query: input.query,
110
+ records: ranked,
111
+ selected,
112
+ injectionPreview: formatSearchInjectionPreview(selected.records)
113
+ };
114
+ },
115
+ async qualityReport(input) {
116
+ const store = await storeFor(input.projectPath);
117
+ return createQualityReport(await store.listMemoryRecords(), {
118
+ staleAfterDays: input.staleAfterDays
119
+ });
120
+ },
121
+ async rememberGlobal(input) {
122
+ return (await globalStore()).upsert({ ...input.memory, scope: "global" }, input.source);
123
+ },
124
+ async searchGlobalMemory(input) {
125
+ return (await globalStore()).list(input);
126
+ },
127
+ async importGlobalMemory(input) {
128
+ const store = await storeFor(input.projectPath);
129
+ return (await globalStore()).importGlobalRecords(await store.listMemoryRecords(), {
130
+ reason: `project-import:${input.projectPath}`
131
+ });
132
+ },
133
+ async promoteToGlobal(input) {
134
+ const store = await storeFor(input.projectPath);
135
+ const promotable = selectGloballyPromotable(await store.listMemoryRecords());
136
+ const global = await globalStore();
137
+ const promoted = [];
138
+ for (const record of promotable) {
139
+ promoted.push(await global.upsert({
140
+ type: record.type,
141
+ content: record.content,
142
+ scope: "global",
143
+ importance: record.score.importance,
144
+ confidence: record.score.confidence,
145
+ entities: record.entities,
146
+ status: record.status
147
+ }, { kind: "ai_processing", reason: `auto-promote:${input.projectPath}` }));
148
+ }
149
+ return { projectPath: input.projectPath, promoted };
150
+ },
151
+ async extractGlobal(input) {
152
+ const extractor = createGlobalExtractor(loadPeonConfig());
153
+ if (!extractor)
154
+ return { promoted: [] };
155
+ const store = await storeFor(input.projectPath);
156
+ const facts = await extractor(await store.listMemoryRecords());
157
+ const global = await globalStore();
158
+ const promoted = [];
159
+ for (const content of facts) {
160
+ promoted.push(await global.upsert({ type: "fact", content, scope: "global", importance: 0.8, confidence: 0.8 }, { kind: "ai_processing", reason: `global-extract:${input.projectPath}` }));
161
+ }
162
+ return { promoted };
163
+ },
164
+ async recurateProject(input) {
165
+ const recurator = createRecurator(loadPeonConfig());
166
+ const store = await storeFor(input.projectPath);
167
+ const records = await store.listMemoryRecords();
168
+ const considered = records.filter((r) => r.status === "active" && !r.pinned).length;
169
+ if (!recurator || considered === 0)
170
+ return { archived: 0, considered };
171
+ const dropIds = await recurator(records);
172
+ // SAFETY CAP: trimming should remove a small minority. If the model wants to
173
+ // drop more than 25%, it's misjudging the batch — refuse entirely rather than
174
+ // gut the memory. (A real over-aggression incident is why this exists.)
175
+ const MAX_FRACTION = 0.25;
176
+ if (dropIds.length > Math.max(5, Math.floor(considered * MAX_FRACTION))) {
177
+ return { archived: 0, considered, capped: true };
178
+ }
179
+ const archived = await store.archiveRecords(dropIds, "recurated under sharpened prompt");
180
+ return { archived, considered };
181
+ },
182
+ async brainPass(input) {
183
+ const store = await storeFor(input.projectPath);
184
+ // Compression is the only LLM step — built in-process and only when asked
185
+ // (the daemon enables it on the cost-gated consolidation path).
186
+ const summarize = input.compress ? createClusterSummarizer(loadPeonConfig()) ?? undefined : undefined;
187
+ return { actions: await store.runBrainPass({ recalledIds: input.recalledIds, summarize }) };
188
+ },
189
+ async globalBrainPass(input) {
190
+ const summarize = input.compress ? createClusterSummarizer(loadPeonConfig()) ?? undefined : undefined;
191
+ return { actions: await (await globalStore()).runBrainPass({ summarize }) };
192
+ },
193
+ async brainActivity(input) {
194
+ const limit = input.limit ?? 30;
195
+ const items = [];
196
+ for (const projectPath of input.projectPaths) {
197
+ const store = await storeFor(projectPath);
198
+ for (const entry of await store.readBrainActions(10)) {
199
+ for (const action of entry.actions) {
200
+ items.push({ at: entry.at, scope: "project", projectName: basename(projectPath), type: action.type, detail: action.detail });
201
+ }
202
+ }
203
+ }
204
+ for (const entry of await (await globalStore()).readBrainActions(10)) {
205
+ for (const action of entry.actions) {
206
+ items.push({ at: entry.at, scope: "global", projectName: "global", type: action.type, detail: action.detail });
207
+ }
208
+ }
209
+ return items.sort((a, b) => b.at.localeCompare(a.at)).slice(0, limit);
210
+ },
211
+ async globalDashboard() {
212
+ const store = await globalStore();
213
+ const records = await store.list({ status: "active" });
214
+ const byType = {};
215
+ const entityCounts = new Map();
216
+ for (const record of records) {
217
+ byType[record.type] = (byType[record.type] ?? 0) + 1;
218
+ for (const entity of record.entities)
219
+ entityCounts.set(entity, (entityCounts.get(entity) ?? 0) + 1);
220
+ }
221
+ const topEntities = Array.from(entityCounts.entries())
222
+ .map(([entity, count]) => ({ entity, count }))
223
+ .sort((a, b) => b.count - a.count)
224
+ .slice(0, 12);
225
+ const recentActions = (await store.readBrainActions(10)).flatMap((entry) => entry.actions.map((action) => ({ at: entry.at, scope: "global", projectName: "global", type: action.type, detail: action.detail })));
226
+ return {
227
+ totalBeliefs: records.length,
228
+ byType,
229
+ topEntities,
230
+ recentActions,
231
+ records: records.slice(0, 50).map((r) => ({ id: r.id, type: r.type, content: r.content, entities: r.entities }))
232
+ };
233
+ },
234
+ async brainActions(input) {
235
+ const store = await storeFor(input.projectPath);
236
+ return store.readBrainActions(input.limit);
237
+ },
238
+ async restoreBackup(input) {
239
+ const store = await storeFor(input.projectPath);
240
+ return { restored: await store.restoreLatestBackup() };
241
+ },
242
+ async updateMemory(input) {
243
+ const store = await storeFor(input.projectPath);
244
+ return store.updateMemoryRecord(input.id, {
245
+ content: input.content,
246
+ importance: input.importance,
247
+ confidence: input.confidence,
248
+ status: input.status,
249
+ pinned: input.pinned
250
+ });
251
+ },
252
+ async deleteMemory(input) {
253
+ const store = await storeFor(input.projectPath);
254
+ return { deleted: await store.deleteMemoryRecord(input.id) };
255
+ },
256
+ async mergeMemory(input) {
257
+ const store = await storeFor(input.projectPath);
258
+ return store.mergeMemoryRecords(input.keepId, input.dropId);
259
+ },
260
+ async evaluateProject(input) {
261
+ return evaluatePeonProject(input);
262
+ },
263
+ async buildInjection(input) {
264
+ const store = await storeFor(input.projectPath);
265
+ const ranked = await store.rankRecords(input.query, { limit: 50 });
266
+ const globalRecords = await (await globalStore()).list({ query: input.query });
267
+ return buildContextInjection({
268
+ projectResults: ranked,
269
+ globalRecords,
270
+ query: input.query,
271
+ maxChars: input.maxChars ?? 6000,
272
+ includeInactive: input.includeInactive
273
+ });
274
+ },
275
+ async crossProjectSearch(input) {
276
+ const targets = (input.projectPaths ?? []).filter((path) => path && path !== input.excludeProjectPath);
277
+ const perProjectLimit = input.perProjectLimit ?? 6;
278
+ const maxProjects = Math.max(1, input.maxProjects ?? 25);
279
+ const terms = queryTerms(input.query);
280
+ // Phase 1 — cheap lexical pre-filter: read each project's records (no embeddings)
281
+ // and keep only those that mention the query at all, ranked by hit count. This
282
+ // avoids doing the expensive semantic pass over dozens of irrelevant projects.
283
+ const candidates = [];
284
+ for (const projectPath of targets) {
285
+ try {
286
+ const store = await storeFor(projectPath);
287
+ const active = (await store.listMemoryRecords()).filter((r) => r.status === "active");
288
+ const lex = lexicalProjectScore(active, terms);
289
+ // With a real query, require at least one lexical hit; with an empty query, keep all.
290
+ if (terms.length === 0 || lex > 0)
291
+ candidates.push({ projectPath, store, lex });
292
+ }
293
+ catch {
294
+ // skip a project we cannot read
295
+ }
296
+ }
297
+ candidates.sort((left, right) => right.lex - left.lex);
298
+ const shortlist = candidates.slice(0, maxProjects);
299
+ // Embed the query ONCE and reuse it across the shortlisted projects.
300
+ let queryVector;
301
+ try {
302
+ const client = createEmbeddingClient({ config: loadPeonConfig() });
303
+ if (client)
304
+ [queryVector] = await client.embed([input.query]);
305
+ }
306
+ catch {
307
+ // lexical-only if embeddings are unavailable
308
+ }
309
+ // Phase 2 — full (semantic) rank only on the shortlist.
310
+ const hits = [];
311
+ for (const { projectPath, store } of shortlist) {
312
+ try {
313
+ const ranked = await store.rankRecordsReadonly(input.query, { limit: perProjectLimit, queryVector });
314
+ for (const item of ranked) {
315
+ if (item.record.status !== "active")
316
+ continue; // current beliefs only
317
+ hits.push({
318
+ projectPath,
319
+ projectName: basename(projectPath),
320
+ record: item.record,
321
+ score: item.score,
322
+ explanation: item.explanation
323
+ });
324
+ }
325
+ }
326
+ catch {
327
+ // skip a project that fails mid-rank; never fail the whole search
328
+ }
329
+ }
330
+ hits.sort((left, right) => right.score - left.score);
331
+ return {
332
+ query: input.query,
333
+ projectsSearched: shortlist.map((c) => c.projectPath),
334
+ results: hits.slice(0, input.limit ?? 12)
335
+ };
336
+ },
337
+ async processMemory(input) {
338
+ const processor = new PeonMemoryProcessor();
339
+ return processor.processMemory(input);
340
+ },
341
+ async maybeProcessMemory(input) {
342
+ const processor = new PeonMemoryProcessor();
343
+ return processor.maybeProcessMemory(input);
344
+ }
345
+ };
346
+ }
347
+ function createDaemonBackedTools(daemonUrl) {
348
+ const baseUrl = daemonUrl.replace(/\/$/, "");
349
+ return {
350
+ async startSession(input) {
351
+ return postJson(`${baseUrl}/sessions`, input);
352
+ },
353
+ async recordMessage(input) {
354
+ return postJson(`${baseUrl}/messages`, input);
355
+ },
356
+ async recordEvent(input) {
357
+ return postJson(`${baseUrl}/events`, input);
358
+ },
359
+ async endSession(input) {
360
+ return postJson(`${baseUrl}/sessions/${encodeURIComponent(input.sessionId)}/end`, {});
361
+ },
362
+ async getContext(input) {
363
+ const url = new URL(`${baseUrl}/context`);
364
+ url.searchParams.set("projectPath", input.projectPath);
365
+ if (input.query)
366
+ url.searchParams.set("query", input.query);
367
+ if (input.maxChars)
368
+ url.searchParams.set("maxChars", String(input.maxChars));
369
+ const response = await fetch(url);
370
+ return readJsonResponse(response);
371
+ },
372
+ async inspectBrain(input) {
373
+ const url = new URL(`${baseUrl}/brain`);
374
+ url.searchParams.set("projectPath", input.projectPath);
375
+ if (input.query)
376
+ url.searchParams.set("query", input.query);
377
+ if (input.maxChars)
378
+ url.searchParams.set("maxChars", String(input.maxChars));
379
+ const response = await fetch(url);
380
+ return readJsonResponse(response);
381
+ },
382
+ async searchMemory(input) {
383
+ const url = new URL(`${baseUrl}/search`);
384
+ url.searchParams.set("projectPath", input.projectPath);
385
+ url.searchParams.set("query", input.query);
386
+ if (input.limit)
387
+ url.searchParams.set("limit", String(input.limit));
388
+ if (input.maxChars)
389
+ url.searchParams.set("maxChars", String(input.maxChars));
390
+ const response = await fetch(url);
391
+ return readJsonResponse(response);
392
+ },
393
+ async qualityReport(input) {
394
+ const url = new URL(`${baseUrl}/quality`);
395
+ url.searchParams.set("projectPath", input.projectPath);
396
+ if (input.staleAfterDays)
397
+ url.searchParams.set("staleAfterDays", String(input.staleAfterDays));
398
+ const response = await fetch(url);
399
+ return readJsonResponse(response);
400
+ },
401
+ async rememberGlobal(input) {
402
+ return postJson(`${baseUrl}/global/memories`, input);
403
+ },
404
+ async searchGlobalMemory(input) {
405
+ const url = new URL(`${baseUrl}/global/memories`);
406
+ if (input.query)
407
+ url.searchParams.set("query", input.query);
408
+ if (input.type)
409
+ url.searchParams.set("type", input.type);
410
+ if (input.status)
411
+ url.searchParams.set("status", input.status);
412
+ const response = await fetch(url);
413
+ return readJsonResponse(response);
414
+ },
415
+ async importGlobalMemory(input) {
416
+ return postJson(`${baseUrl}/global/import-project`, input);
417
+ },
418
+ async promoteToGlobal(input) {
419
+ return postJson(`${baseUrl}/global/promote`, input);
420
+ },
421
+ async extractGlobal(input) {
422
+ return postJson(`${baseUrl}/global/extract`, input);
423
+ },
424
+ async recurateProject(input) {
425
+ return postJson(`${baseUrl}/recurate`, input);
426
+ },
427
+ async brainPass(input) {
428
+ return postJson(`${baseUrl}/brain/pass`, input);
429
+ },
430
+ async globalBrainPass(input) {
431
+ return postJson(`${baseUrl}/global/brain-pass`, input);
432
+ },
433
+ async brainActivity(input) {
434
+ const url = new URL(`${baseUrl}/brain/activity`);
435
+ if (input.limit)
436
+ url.searchParams.set("limit", String(input.limit));
437
+ return readJsonResponse(await fetch(url));
438
+ },
439
+ async globalDashboard() {
440
+ return readJsonResponse(await fetch(`${baseUrl}/global/dashboard`));
441
+ },
442
+ async brainActions(input) {
443
+ const url = new URL(`${baseUrl}/brain/actions`);
444
+ url.searchParams.set("projectPath", input.projectPath);
445
+ if (input.limit)
446
+ url.searchParams.set("limit", String(input.limit));
447
+ return readJsonResponse(await fetch(url));
448
+ },
449
+ async restoreBackup(input) {
450
+ return postJson(`${baseUrl}/brain/restore`, input);
451
+ },
452
+ async updateMemory(input) {
453
+ return postJson(`${baseUrl}/memory/update`, input);
454
+ },
455
+ async deleteMemory(input) {
456
+ return postJson(`${baseUrl}/memory/delete`, input);
457
+ },
458
+ async mergeMemory(input) {
459
+ return postJson(`${baseUrl}/memory/merge`, input);
460
+ },
461
+ async evaluateProject(input) {
462
+ return postJson(`${baseUrl}/evaluate`, input);
463
+ },
464
+ async buildInjection(input) {
465
+ const url = new URL(`${baseUrl}/injection`);
466
+ url.searchParams.set("projectPath", input.projectPath);
467
+ if (input.query)
468
+ url.searchParams.set("query", input.query);
469
+ if (input.maxChars)
470
+ url.searchParams.set("maxChars", String(input.maxChars));
471
+ if (input.includeInactive)
472
+ url.searchParams.set("includeInactive", "true");
473
+ const response = await fetch(url);
474
+ return readJsonResponse(response);
475
+ },
476
+ async crossProjectSearch(input) {
477
+ const url = new URL(`${baseUrl}/cross-context`);
478
+ url.searchParams.set("query", input.query);
479
+ if (input.excludeProjectPath)
480
+ url.searchParams.set("exclude", input.excludeProjectPath);
481
+ // A single explicit target maps to ?projectPath=; otherwise the daemon searches all known projects.
482
+ if (input.projectPaths && input.projectPaths.length === 1) {
483
+ url.searchParams.set("projectPath", input.projectPaths[0]);
484
+ }
485
+ if (input.limit)
486
+ url.searchParams.set("limit", String(input.limit));
487
+ if (input.maxProjects)
488
+ url.searchParams.set("maxProjects", String(input.maxProjects));
489
+ const response = await fetch(url);
490
+ return readJsonResponse(response);
491
+ },
492
+ async processMemory(input) {
493
+ return postJson(`${baseUrl}/process`, input);
494
+ },
495
+ async maybeProcessMemory(input) {
496
+ return postJson(`${baseUrl}/process/auto`, input);
497
+ }
498
+ };
499
+ }
500
+ function formatRankedMemoryRecord(item) {
501
+ return `- [${item.record.type}] ${item.record.content}\n why: ${item.explanation}\n`;
502
+ }
503
+ const CROSS_STOP_WORDS = new Set(["a", "an", "and", "are", "as", "for", "in", "is", "of", "on", "or", "the", "to", "use", "with", "what", "did", "we", "our", "about", "from"]);
504
+ function queryTerms(query) {
505
+ return [
506
+ ...new Set((query ?? "")
507
+ .toLowerCase()
508
+ .split(/[^a-z0-9_.\/-]+/)
509
+ .map((t) => t.trim())
510
+ .filter((t) => t.length > 1 && !CROSS_STOP_WORDS.has(t)))
511
+ ];
512
+ }
513
+ /** Cheap lexical relevance for the cross-project pre-filter: how many active records mention a query term. */
514
+ function lexicalProjectScore(records, terms) {
515
+ if (terms.length === 0)
516
+ return records.length;
517
+ let score = 0;
518
+ for (const record of records) {
519
+ const haystack = `${record.content} ${record.normalized} ${record.entities.join(" ")}`.toLowerCase();
520
+ if (terms.some((term) => haystack.includes(term)))
521
+ score += 1;
522
+ }
523
+ return score;
524
+ }
525
+ function formatSearchInjectionPreview(records) {
526
+ if (records.length === 0)
527
+ return "";
528
+ return ["Peon Search Results", ...records.map(formatRankedMemoryRecord)].join("\n");
529
+ }
530
+ async function postJson(url, body) {
531
+ const response = await fetch(url, {
532
+ method: "POST",
533
+ headers: { "content-type": "application/json" },
534
+ body: JSON.stringify(body)
535
+ });
536
+ return readJsonResponse(response);
537
+ }
538
+ async function readJsonResponse(response) {
539
+ const text = await response.text();
540
+ const body = text ? JSON.parse(text) : {};
541
+ if (!response.ok) {
542
+ const message = typeof body.error === "string" ? body.error : `Peon daemon request failed: ${response.status}`;
543
+ throw new Error(message);
544
+ }
545
+ return body;
546
+ }