@chendpoc/pi-memory 0.1.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 (177) hide show
  1. package/README.md +180 -0
  2. package/dist/adapters/piComplete.d.ts +17 -0
  3. package/dist/adapters/piComplete.d.ts.map +1 -0
  4. package/dist/adapters/piComplete.js +169 -0
  5. package/dist/adapters/piComplete.js.map +1 -0
  6. package/dist/bundle/install.d.ts +34 -0
  7. package/dist/bundle/install.d.ts.map +1 -0
  8. package/dist/bundle/install.js +183 -0
  9. package/dist/bundle/install.js.map +1 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +245 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/config.d.ts +27 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +49 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/errclass.d.ts +7 -0
  19. package/dist/errclass.d.ts.map +1 -0
  20. package/dist/errclass.js +32 -0
  21. package/dist/errclass.js.map +1 -0
  22. package/dist/extension.d.ts +24 -0
  23. package/dist/extension.d.ts.map +1 -0
  24. package/dist/extension.js +7 -0
  25. package/dist/extension.js.map +1 -0
  26. package/dist/fallback/index.d.ts +11 -0
  27. package/dist/fallback/index.d.ts.map +1 -0
  28. package/dist/fallback/index.js +16 -0
  29. package/dist/fallback/index.js.map +1 -0
  30. package/dist/fallback/llmRerank.d.ts +19 -0
  31. package/dist/fallback/llmRerank.d.ts.map +1 -0
  32. package/dist/fallback/llmRerank.js +60 -0
  33. package/dist/fallback/llmRerank.js.map +1 -0
  34. package/dist/fallback/memoryMd.d.ts +6 -0
  35. package/dist/fallback/memoryMd.d.ts.map +1 -0
  36. package/dist/fallback/memoryMd.js +35 -0
  37. package/dist/fallback/memoryMd.js.map +1 -0
  38. package/dist/fallback/sessionIndex.d.ts +35 -0
  39. package/dist/fallback/sessionIndex.d.ts.map +1 -0
  40. package/dist/fallback/sessionIndex.js +222 -0
  41. package/dist/fallback/sessionIndex.js.map +1 -0
  42. package/dist/fallback/sessionSearch.d.ts +18 -0
  43. package/dist/fallback/sessionSearch.d.ts.map +1 -0
  44. package/dist/fallback/sessionSearch.js +161 -0
  45. package/dist/fallback/sessionSearch.js.map +1 -0
  46. package/dist/index.d.ts +25 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +24 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/paths.d.ts +7 -0
  51. package/dist/paths.d.ts.map +1 -0
  52. package/dist/paths.js +26 -0
  53. package/dist/paths.js.map +1 -0
  54. package/dist/pi-extension.d.ts +6 -0
  55. package/dist/pi-extension.d.ts.map +1 -0
  56. package/dist/pi-extension.js +224 -0
  57. package/dist/pi-extension.js.map +1 -0
  58. package/dist/preflight/detectIntents.d.ts +102 -0
  59. package/dist/preflight/detectIntents.d.ts.map +1 -0
  60. package/dist/preflight/detectIntents.js +624 -0
  61. package/dist/preflight/detectIntents.js.map +1 -0
  62. package/dist/preflight/hook.d.ts +58 -0
  63. package/dist/preflight/hook.d.ts.map +1 -0
  64. package/dist/preflight/hook.js +77 -0
  65. package/dist/preflight/hook.js.map +1 -0
  66. package/dist/preflight/render.d.ts +21 -0
  67. package/dist/preflight/render.d.ts.map +1 -0
  68. package/dist/preflight/render.js +132 -0
  69. package/dist/preflight/render.js.map +1 -0
  70. package/dist/preflight/strip.d.ts +11 -0
  71. package/dist/preflight/strip.d.ts.map +1 -0
  72. package/dist/preflight/strip.js +46 -0
  73. package/dist/preflight/strip.js.map +1 -0
  74. package/dist/service.d.ts +56 -0
  75. package/dist/service.d.ts.map +1 -0
  76. package/dist/service.js +158 -0
  77. package/dist/service.js.map +1 -0
  78. package/dist/sidecar/bundle.d.ts +19 -0
  79. package/dist/sidecar/bundle.d.ts.map +1 -0
  80. package/dist/sidecar/bundle.js +39 -0
  81. package/dist/sidecar/bundle.js.map +1 -0
  82. package/dist/sidecar/client.d.ts +17 -0
  83. package/dist/sidecar/client.d.ts.map +1 -0
  84. package/dist/sidecar/client.js +107 -0
  85. package/dist/sidecar/client.js.map +1 -0
  86. package/dist/sidecar/process.d.ts +14 -0
  87. package/dist/sidecar/process.d.ts.map +1 -0
  88. package/dist/sidecar/process.js +126 -0
  89. package/dist/sidecar/process.js.map +1 -0
  90. package/dist/tools/memoryAppend.d.ts +37 -0
  91. package/dist/tools/memoryAppend.d.ts.map +1 -0
  92. package/dist/tools/memoryAppend.js +99 -0
  93. package/dist/tools/memoryAppend.js.map +1 -0
  94. package/dist/tools/memoryRecall.d.ts +113 -0
  95. package/dist/tools/memoryRecall.d.ts.map +1 -0
  96. package/dist/tools/memoryRecall.js +325 -0
  97. package/dist/tools/memoryRecall.js.map +1 -0
  98. package/dist/trainer/bundleBuilder.d.ts +30 -0
  99. package/dist/trainer/bundleBuilder.d.ts.map +1 -0
  100. package/dist/trainer/bundleBuilder.js +106 -0
  101. package/dist/trainer/bundleBuilder.js.map +1 -0
  102. package/dist/trainer/bundleLoader.d.ts +12 -0
  103. package/dist/trainer/bundleLoader.d.ts.map +1 -0
  104. package/dist/trainer/bundleLoader.js +59 -0
  105. package/dist/trainer/bundleLoader.js.map +1 -0
  106. package/dist/trainer/deltaMerge.d.ts +38 -0
  107. package/dist/trainer/deltaMerge.d.ts.map +1 -0
  108. package/dist/trainer/deltaMerge.js +183 -0
  109. package/dist/trainer/deltaMerge.js.map +1 -0
  110. package/dist/trainer/entityResolver.d.ts +27 -0
  111. package/dist/trainer/entityResolver.d.ts.map +1 -0
  112. package/dist/trainer/entityResolver.js +92 -0
  113. package/dist/trainer/entityResolver.js.map +1 -0
  114. package/dist/trainer/extractFacts.d.ts +67 -0
  115. package/dist/trainer/extractFacts.d.ts.map +1 -0
  116. package/dist/trainer/extractFacts.js +213 -0
  117. package/dist/trainer/extractFacts.js.map +1 -0
  118. package/dist/trainer/index.d.ts +54 -0
  119. package/dist/trainer/index.d.ts.map +1 -0
  120. package/dist/trainer/index.js +82 -0
  121. package/dist/trainer/index.js.map +1 -0
  122. package/dist/trainer/llmExtractor.d.ts +16 -0
  123. package/dist/trainer/llmExtractor.d.ts.map +1 -0
  124. package/dist/trainer/llmExtractor.js +146 -0
  125. package/dist/trainer/llmExtractor.js.map +1 -0
  126. package/dist/trainer/marker.d.ts +10 -0
  127. package/dist/trainer/marker.d.ts.map +1 -0
  128. package/dist/trainer/marker.js +28 -0
  129. package/dist/trainer/marker.js.map +1 -0
  130. package/dist/trainer/scheduler.d.ts +31 -0
  131. package/dist/trainer/scheduler.d.ts.map +1 -0
  132. package/dist/trainer/scheduler.js +72 -0
  133. package/dist/trainer/scheduler.js.map +1 -0
  134. package/dist/trainer/sessionLoader.d.ts +23 -0
  135. package/dist/trainer/sessionLoader.d.ts.map +1 -0
  136. package/dist/trainer/sessionLoader.js +106 -0
  137. package/dist/trainer/sessionLoader.js.map +1 -0
  138. package/dist/types.d.ts +135 -0
  139. package/dist/types.d.ts.map +1 -0
  140. package/dist/types.js +8 -0
  141. package/dist/types.js.map +1 -0
  142. package/package.json +78 -0
  143. package/src/adapters/piComplete.ts +233 -0
  144. package/src/bundle/install.ts +206 -0
  145. package/src/cli.ts +254 -0
  146. package/src/config.ts +92 -0
  147. package/src/errclass.ts +37 -0
  148. package/src/extension.ts +23 -0
  149. package/src/fallback/index.ts +24 -0
  150. package/src/fallback/llmRerank.ts +90 -0
  151. package/src/fallback/memoryMd.ts +36 -0
  152. package/src/fallback/sessionIndex.ts +289 -0
  153. package/src/fallback/sessionSearch.ts +181 -0
  154. package/src/index.ts +213 -0
  155. package/src/paths.ts +28 -0
  156. package/src/pi-extension.ts +276 -0
  157. package/src/preflight/detectIntents.ts +654 -0
  158. package/src/preflight/hook.ts +136 -0
  159. package/src/preflight/render.ts +185 -0
  160. package/src/preflight/strip.ts +50 -0
  161. package/src/service.ts +202 -0
  162. package/src/sidecar/bundle.ts +52 -0
  163. package/src/sidecar/client.ts +166 -0
  164. package/src/sidecar/process.ts +145 -0
  165. package/src/tools/memoryAppend.ts +113 -0
  166. package/src/tools/memoryRecall.ts +364 -0
  167. package/src/trainer/bundleBuilder.ts +192 -0
  168. package/src/trainer/bundleLoader.ts +105 -0
  169. package/src/trainer/deltaMerge.ts +221 -0
  170. package/src/trainer/entityResolver.ts +140 -0
  171. package/src/trainer/extractFacts.ts +312 -0
  172. package/src/trainer/index.ts +147 -0
  173. package/src/trainer/llmExtractor.ts +206 -0
  174. package/src/trainer/marker.ts +30 -0
  175. package/src/trainer/scheduler.ts +104 -0
  176. package/src/trainer/sessionLoader.ts +139 -0
  177. package/src/types.ts +168 -0
package/src/cli.ts ADDED
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { createStandaloneLLMClient } from "./adapters/piComplete.js";
6
+ import { installBundle } from "./bundle/install.js";
7
+ import { defaultMemoryConfig } from "./config.js";
8
+ import { openSessionIndex } from "./fallback/sessionIndex.js";
9
+ import { SidecarClient } from "./sidecar/client.js";
10
+ import { MemoryService } from "./service.js";
11
+ import { trainBundle } from "./trainer/index.js";
12
+ import { createLLMFactExtractor } from "./trainer/llmExtractor.js";
13
+ import { createTrainScheduler } from "./trainer/scheduler.js";
14
+ import type { QueryIntent } from "./types.js";
15
+
16
+ async function tryReloadSidecar(cfg: ReturnType<typeof defaultMemoryConfig>): Promise<void> {
17
+ try {
18
+ fs.accessSync(cfg.socketPath, fs.constants.F_OK);
19
+ } catch {
20
+ return;
21
+ }
22
+ const client = new SidecarClient(cfg.socketPath, cfg.clientRequestTimeoutMs);
23
+ try {
24
+ await client.reload();
25
+ } catch {
26
+ /* sidecar not running — install still succeeded */
27
+ }
28
+ }
29
+
30
+ async function main(): Promise<void> {
31
+ const [cmd, ...rest] = process.argv.slice(2);
32
+ if (!cmd || cmd === "help" || cmd === "--help") {
33
+ printHelp();
34
+ process.exit(0);
35
+ }
36
+
37
+ const cfg = defaultMemoryConfig();
38
+ const service = new MemoryService(cfg);
39
+
40
+ if (cmd === "health") {
41
+ await service.start();
42
+ const h = await service.health();
43
+ console.log(JSON.stringify({ status: service.getStatus(), health: h }, null, 2));
44
+ await service.stop();
45
+ return;
46
+ }
47
+
48
+ if (cmd === "query") {
49
+ const json = rest.join(" ").trim();
50
+ if (!json) {
51
+ console.error('Usage: pi-memory query \'{"anchor_mentions":["Alice"],"mode":"direct_relation"}\'');
52
+ process.exit(1);
53
+ }
54
+ let intent: QueryIntent;
55
+ try {
56
+ intent = JSON.parse(json) as QueryIntent;
57
+ } catch (e) {
58
+ console.error("Invalid JSON intent:", e);
59
+ process.exit(1);
60
+ }
61
+ await service.start();
62
+ const result = await service.query(intent);
63
+ console.log(JSON.stringify(result, null, 2));
64
+ await service.stop();
65
+ return;
66
+ }
67
+
68
+ if (cmd === "status") {
69
+ await service.start();
70
+ console.log(JSON.stringify(service.getStatus(), null, 2));
71
+ await service.stop();
72
+ return;
73
+ }
74
+
75
+ if (cmd === "install-bundle") {
76
+ const bundlePath = rest[0];
77
+ if (!bundlePath) {
78
+ console.error("Usage: pi-memory install-bundle <path-to-bundle-dir>");
79
+ process.exit(1);
80
+ }
81
+ const result = await installBundle({
82
+ bundleRoot: cfg.bundleRoot,
83
+ sourceDir: bundlePath,
84
+ });
85
+ await tryReloadSidecar(cfg);
86
+ console.log(JSON.stringify(result, null, 2));
87
+ return;
88
+ }
89
+
90
+ if (cmd === "train") {
91
+ const flags = parseTrainFlags(rest);
92
+ let extractOpts;
93
+ if (flags.extractor === "llm") {
94
+ try {
95
+ const client = createStandaloneLLMClient(flags.model);
96
+ extractOpts = {
97
+ llmExtractor: createLLMFactExtractor({
98
+ client,
99
+ batchSize: cfg.trainer.llm_batch_size,
100
+ }),
101
+ };
102
+ } catch (err) {
103
+ const message = err instanceof Error ? err.message : String(err);
104
+ console.error(`LLM extractor unavailable (${message}) — falling back to regex.`);
105
+ extractOpts = undefined;
106
+ }
107
+ }
108
+
109
+ const result = await trainBundle({
110
+ sessionsDir: flags.sessionsDir ?? cfg.sessionsDir,
111
+ bundleRoot: cfg.bundleRoot,
112
+ full: flags.full,
113
+ dryRun: flags.dryRun,
114
+ noMerge: flags.noMerge,
115
+ extractOpts,
116
+ });
117
+ if (result.sessionsProcessed === 0) {
118
+ console.log("No new sessions to process.");
119
+ } else {
120
+ const output: Record<string, unknown> = {
121
+ sessions_processed: result.sessionsProcessed,
122
+ entities: result.entityCount,
123
+ relations: result.relationCount,
124
+ events: result.eventCount,
125
+ dry_run: result.dryRun,
126
+ bundle_dir: result.bundleResult?.bundleDir ?? null,
127
+ installed: result.installResult?.installed_dir ?? null,
128
+ };
129
+ if (result.delta) {
130
+ output.delta = {
131
+ added: result.delta.added,
132
+ updated: result.delta.updated,
133
+ deleted: result.delta.deleted,
134
+ skipped: result.delta.skipped,
135
+ };
136
+ }
137
+ console.log(JSON.stringify(output, null, 2));
138
+ if (!result.dryRun) {
139
+ await tryReloadSidecar(cfg);
140
+ }
141
+ }
142
+
143
+ if (flags.watch) {
144
+ const interval = cfg.trainer.auto_interval ?? "1h";
145
+ console.log(`Watching for new sessions (interval: ${interval})...`);
146
+ createTrainScheduler(
147
+ { interval, trainConfig: { sessionsDir: flags.sessionsDir ?? cfg.sessionsDir, bundleRoot: cfg.bundleRoot } },
148
+ (log) => {
149
+ console.log(JSON.stringify({ type: "scheduled_run", ...log }));
150
+ },
151
+ );
152
+ await new Promise(() => {}); // block forever
153
+ }
154
+ return;
155
+ }
156
+
157
+ if (cmd === "index") {
158
+ const sessionsDir = rest.find((a, i) => rest[i - 1] === "--sessions-dir") ?? cfg.sessionsDir;
159
+ const dbPath = path.join(cfg.bundleRoot, "sessions.db");
160
+ const idx = openSessionIndex(dbPath);
161
+ if (!idx) {
162
+ console.error("Failed to open SQLite — is better-sqlite3 installed?");
163
+ process.exit(1);
164
+ }
165
+ console.log(`Rebuilding FTS5 index at ${dbPath}...`);
166
+ const { indexed } = await idx.rebuildIndex(sessionsDir);
167
+ idx.close();
168
+ console.log(JSON.stringify({ indexed, db_path: dbPath }));
169
+ return;
170
+ }
171
+
172
+ console.error(`Unknown command: ${cmd}`);
173
+ printHelp();
174
+ process.exit(1);
175
+ }
176
+
177
+ function parseTrainFlags(args: string[]): {
178
+ sessionsDir?: string;
179
+ full: boolean;
180
+ dryRun: boolean;
181
+ noMerge: boolean;
182
+ extractor: "regex" | "llm";
183
+ watch: boolean;
184
+ model?: string;
185
+ } {
186
+ let sessionsDir: string | undefined;
187
+ let full = false;
188
+ let dryRun = false;
189
+ let noMerge = false;
190
+ let extractor: "regex" | "llm" = "regex";
191
+ let watch = false;
192
+ let model: string | undefined;
193
+ for (let i = 0; i < args.length; i++) {
194
+ const arg = args[i]!;
195
+ if (arg === "--full") { full = true; continue; }
196
+ if (arg === "--dry-run") { dryRun = true; continue; }
197
+ if (arg === "--no-merge") { noMerge = true; continue; }
198
+ if (arg === "--watch") { watch = true; continue; }
199
+ if (arg === "--model" && i + 1 < args.length) {
200
+ model = args[++i];
201
+ continue;
202
+ }
203
+ if (arg === "--sessions-dir" && i + 1 < args.length) {
204
+ sessionsDir = args[++i];
205
+ continue;
206
+ }
207
+ if (arg === "--extractor" && i + 1 < args.length) {
208
+ const val = args[++i]!;
209
+ if (val === "llm" || val === "regex") extractor = val;
210
+ continue;
211
+ }
212
+ }
213
+ return { sessionsDir, full, dryRun, noMerge, extractor, watch, model };
214
+ }
215
+
216
+ function printHelp(): void {
217
+ console.log(`pi-memory — local TLM episodic memory (mode B)
218
+
219
+ Commands:
220
+ health Start sidecar (if bundle present) and print /health
221
+ status Print MemoryService status snapshot
222
+ query POST /query with JSON QueryIntent
223
+ install-bundle Copy a local bundle dir into ~/.pi/memory/current
224
+ train Build a bundle from session history (delta merge by default)
225
+ --sessions-dir Override sessions directory (default from config)
226
+ --full Ignore marker, rebuild from all sessions
227
+ --dry-run Show extraction stats without writing bundle
228
+ --no-merge Skip delta merge, full rebuild (ignore existing bundle)
229
+ --extractor Extractor type: "regex" (default) or "llm"
230
+ --model LLM model for --extractor llm (default: deepseek/deepseek-v4-flash)
231
+ --watch Run once then schedule periodic re-training
232
+ index Rebuild SQLite FTS5 session search index
233
+ --sessions-dir Override sessions directory (default from config)
234
+
235
+ Environment:
236
+ Place bundle at ~/.pi/memory/current/ (symlink to bundles/<ts>/)
237
+ Session fallback searches ~/.pi/sessions/*.json
238
+ Install tlm on PATH or set memory.tlmPath in Pi extension config
239
+ LLM extractor uses provider env vars (e.g. DEEPSEEK_API_KEY for deepseek/deepseek-v4-flash)
240
+
241
+ Example:
242
+ pi-memory query '{"mode":"direct_relation","anchor_mentions":["Alice"]}'
243
+ pi-memory install-bundle ./my-bundle-2026-06-01T00-00-00Z
244
+ pi-memory train --full
245
+ pi-memory train --extractor llm
246
+ pi-memory train --watch
247
+ pi-memory index
248
+ `);
249
+ }
250
+
251
+ main().catch((err) => {
252
+ console.error(err);
253
+ process.exit(1);
254
+ });
package/src/config.ts ADDED
@@ -0,0 +1,92 @@
1
+ import path from "node:path";
2
+
3
+ import {
4
+ defaultBundleRoot,
5
+ defaultPiHome,
6
+ defaultSessionsDir,
7
+ defaultSocketPath,
8
+ expandPath,
9
+ } from "./paths.js";
10
+
11
+ export type MemoryProvider = "disabled" | "local" | "cloud";
12
+
13
+ export type ExtractorType = "regex" | "llm";
14
+
15
+ export interface TrainerConfig {
16
+ /** Which extractor to use (default "regex"). */
17
+ extractor: ExtractorType;
18
+ /** How many turns per LLM call when extractor is "llm" (default 10). */
19
+ llm_batch_size: number;
20
+ /** Auto-train interval: "1h"|"6h"|"12h"|"24h"|null (default null — disabled). */
21
+ auto_interval: string | null;
22
+ }
23
+
24
+ export interface MemoryConfig {
25
+ provider: MemoryProvider;
26
+ tlmPath: string;
27
+ socketPath: string;
28
+ bundleRoot: string;
29
+ sidecarReadyTimeoutMs: number;
30
+ queryTimeoutMs: number;
31
+ clientRequestTimeoutMs: number;
32
+ sessionsDir: string;
33
+ memoryMdPaths: string[];
34
+ trainer: TrainerConfig;
35
+ }
36
+
37
+ export const defaultTrainerConfig: TrainerConfig = {
38
+ extractor: "regex",
39
+ llm_batch_size: 10,
40
+ auto_interval: null,
41
+ };
42
+
43
+ export function defaultMemoryConfig(
44
+ overrides: Partial<MemoryConfig> = {},
45
+ ): MemoryConfig {
46
+ const { trainer: trainerOverrides, ...rest } = overrides;
47
+ return {
48
+ provider: "local",
49
+ tlmPath: "tlm",
50
+ socketPath: defaultSocketPath(),
51
+ bundleRoot: defaultBundleRoot(),
52
+ sidecarReadyTimeoutMs: 15_000,
53
+ queryTimeoutMs: 2_000,
54
+ clientRequestTimeoutMs: 5_000,
55
+ sessionsDir: defaultSessionsDir(),
56
+ memoryMdPaths: [path.join(defaultPiHome(), "MEMORY.md")],
57
+ trainer: { ...defaultTrainerConfig, ...trainerOverrides },
58
+ ...rest,
59
+ };
60
+ }
61
+
62
+ /** Normalize user-supplied paths after JSON/env load. */
63
+ export function normalizeMemoryConfig(
64
+ raw: Partial<MemoryConfig> & Record<string, unknown>,
65
+ ): MemoryConfig {
66
+ const base = defaultMemoryConfig();
67
+ const rawTrainer = (raw.trainer ?? {}) as Partial<TrainerConfig>;
68
+ return {
69
+ provider: (raw.provider as MemoryProvider) ?? base.provider,
70
+ tlmPath: expandPath(String(raw.tlmPath ?? base.tlmPath)),
71
+ socketPath: expandPath(String(raw.socketPath ?? base.socketPath)),
72
+ bundleRoot: expandPath(String(raw.bundleRoot ?? base.bundleRoot)),
73
+ sidecarReadyTimeoutMs: Number(
74
+ raw.sidecarReadyTimeoutMs ?? base.sidecarReadyTimeoutMs,
75
+ ),
76
+ queryTimeoutMs: Number(raw.queryTimeoutMs ?? base.queryTimeoutMs),
77
+ clientRequestTimeoutMs: Number(
78
+ raw.clientRequestTimeoutMs ?? base.clientRequestTimeoutMs,
79
+ ),
80
+ sessionsDir: expandPath(String(raw.sessionsDir ?? base.sessionsDir)),
81
+ memoryMdPaths: Array.isArray(raw.memoryMdPaths)
82
+ ? raw.memoryMdPaths.map((p) => expandPath(String(p)))
83
+ : base.memoryMdPaths,
84
+ trainer: {
85
+ extractor: (rawTrainer.extractor as ExtractorType) ?? base.trainer.extractor,
86
+ llm_batch_size: Number(rawTrainer.llm_batch_size ?? base.trainer.llm_batch_size),
87
+ auto_interval: rawTrainer.auto_interval !== undefined
88
+ ? rawTrainer.auto_interval
89
+ : base.trainer.auto_interval,
90
+ },
91
+ };
92
+ }
@@ -0,0 +1,37 @@
1
+ import { errorSubCode, type ResponseEnvelope } from "./types.js";
2
+
3
+ export type ErrorClass = "ok" | "retryable" | "permanent" | "unavailable";
4
+
5
+ export const ErrTransport = new Error("memory: transport failure");
6
+
7
+ export function classifyTransportError(err: unknown): ErrorClass {
8
+ return err == null ? "ok" : "unavailable";
9
+ }
10
+
11
+ /** Maps HTTP status + envelope → error class (Kocoro memory/errclass.go). */
12
+ export function classifyHTTP(
13
+ status: number,
14
+ env: ResponseEnvelope | null,
15
+ ): ErrorClass {
16
+ if (status >= 200 && status < 300) return "ok";
17
+ const sub = env?.error ? errorSubCode(env.error) : "";
18
+ const code = env?.error?.code ?? "";
19
+ switch (status) {
20
+ case 400:
21
+ return "permanent";
22
+ case 401:
23
+ case 403:
24
+ return "permanent";
25
+ case 409:
26
+ return "retryable";
27
+ case 422:
28
+ return "permanent";
29
+ case 503:
30
+ if (code === "not_ready" || sub === "not_ready") return "unavailable";
31
+ return "permanent";
32
+ case 500:
33
+ return "retryable";
34
+ default:
35
+ return "retryable";
36
+ }
37
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @deprecated Use `@chendpoc/pi-memory/extension` (pi-extension.ts) with real ExtensionAPI.
3
+ * Re-exports the Pi extension entry for backward compatibility.
4
+ */
5
+ export { default, default as piMemoryExtension, getSharedMemoryService } from "./pi-extension.js";
6
+ export type { BeforeTurnHook, MemoryHelperLLM } from "./preflight/hook.js";
7
+ export { createBeforeTurnHook } from "./preflight/hook.js";
8
+
9
+ /** @deprecated Stub types kept for programmatic callers migrating off the old API. */
10
+ export interface PiExtensionAPI {
11
+ config?: Record<string, unknown>;
12
+ registerTool(tool: PiAgentTool): void;
13
+ onBeforeTurn?(hook: import("./preflight/hook.js").BeforeTurnHook): void;
14
+ onUnload?(fn: () => void | Promise<void>): void;
15
+ }
16
+
17
+ /** @deprecated */
18
+ export interface PiAgentTool {
19
+ name: string;
20
+ description: string;
21
+ parameters: Record<string, unknown>;
22
+ execute(args: Record<string, unknown>, ctx?: { signal?: AbortSignal }): Promise<string>;
23
+ }
@@ -0,0 +1,24 @@
1
+ import type { FallbackQuery } from "../types.js";
2
+ import { memoryMdSnippet } from "./memoryMd.js";
3
+ import { sessionKeywordSearch } from "./sessionSearch.js";
4
+
5
+ export type { SessionSearchHit } from "./sessionSearch.js";
6
+ export { sessionKeywordSearch } from "./sessionSearch.js";
7
+ export { memoryMdSnippet } from "./memoryMd.js";
8
+
9
+ export interface FallbackOptions {
10
+ sessionsDir: string;
11
+ memoryMdPaths: string[];
12
+ }
13
+
14
+ /** Real fallback: session JSON keyword search + MEMORY.md grep. */
15
+ export function createFallbackQuery(opts: FallbackOptions): FallbackQuery {
16
+ return {
17
+ async sessionKeyword(query: string, limit: number) {
18
+ return sessionKeywordSearch(opts.sessionsDir, query, limit);
19
+ },
20
+ async memoryFileSnippet(query: string) {
21
+ return memoryMdSnippet(opts.memoryMdPaths, query);
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,90 @@
1
+ import type { LLMClient } from "../trainer/llmExtractor.js";
2
+ import type { SessionSearchHit } from "./sessionSearch.js";
3
+
4
+ export interface RerankOptions {
5
+ client: LLMClient;
6
+ maxCandidates?: number;
7
+ maxTokens?: number;
8
+ }
9
+
10
+ export interface RankedResult {
11
+ index: number;
12
+ score: number;
13
+ summary: string;
14
+ }
15
+
16
+ const DEFAULT_MAX_CANDIDATES = 10;
17
+
18
+ function buildRerankPrompt(query: string, hits: SessionSearchHit[]): string {
19
+ const numbered = hits
20
+ .map((h, i) => `#${i}: [${h.session_title || "untitled"}] ${h.snippet}`)
21
+ .join("\n");
22
+
23
+ return `You are a relevance judge. Given a user query and numbered search results from past sessions, rate each result 0-10 for relevance to the query and write a one-sentence summary of the relevant content.
24
+
25
+ Query: ${query}
26
+
27
+ Results:
28
+ ${numbered}
29
+
30
+ Respond with ONLY a JSON array (no markdown fences, no explanation):
31
+ [{ "index": 0, "score": 8, "summary": "..." }, ...]`;
32
+ }
33
+
34
+ interface RawRankedItem {
35
+ index?: unknown;
36
+ score?: unknown;
37
+ summary?: unknown;
38
+ }
39
+
40
+ function parseRerankResponse(raw: string, hitCount: number): RankedResult[] | null {
41
+ const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```\s*$/m, "").trim();
42
+
43
+ let parsed: unknown;
44
+ try {
45
+ parsed = JSON.parse(cleaned);
46
+ } catch {
47
+ return null;
48
+ }
49
+
50
+ if (!Array.isArray(parsed)) return null;
51
+
52
+ const results: RankedResult[] = [];
53
+ for (const item of parsed as RawRankedItem[]) {
54
+ const index = typeof item.index === "number" ? item.index : -1;
55
+ const score = typeof item.score === "number" ? item.score : 0;
56
+ const summary = typeof item.summary === "string" ? item.summary.trim() : "";
57
+ if (index < 0 || index >= hitCount || !summary) continue;
58
+ results.push({ index, score: Math.max(0, Math.min(10, score)), summary });
59
+ }
60
+
61
+ if (results.length === 0) return null;
62
+
63
+ results.sort((a, b) => b.score - a.score);
64
+ return results;
65
+ }
66
+
67
+ /**
68
+ * Rerank FTS5 search hits using an LLM. Returns scored + summarized results
69
+ * sorted by relevance. On any LLM failure, returns null (caller should use
70
+ * original hits as fallback).
71
+ */
72
+ export async function rerankWithLLM(
73
+ query: string,
74
+ hits: SessionSearchHit[],
75
+ opts: RerankOptions,
76
+ ): Promise<RankedResult[] | null> {
77
+ if (hits.length === 0) return null;
78
+
79
+ const maxCandidates = opts.maxCandidates ?? DEFAULT_MAX_CANDIDATES;
80
+ const truncated = hits.slice(0, maxCandidates);
81
+
82
+ const prompt = buildRerankPrompt(query, truncated);
83
+
84
+ try {
85
+ const response = await opts.client.complete(prompt);
86
+ return parseRerankResponse(response, truncated.length);
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
@@ -0,0 +1,36 @@
1
+ import fs from "node:fs/promises";
2
+
3
+ const SNIPPET_CAP = 4096;
4
+
5
+ /**
6
+ * Best-effort grep of MEMORY.md for query terms (Kocoro memory_fallback.go).
7
+ * Returns joined matching lines capped at 4KB. Empty string if absent/no match.
8
+ */
9
+ export async function memoryMdSnippet(
10
+ paths: string[],
11
+ query: string,
12
+ ): Promise<string> {
13
+ const q = query.trim().toLowerCase();
14
+ if (!q) return "";
15
+
16
+ for (const p of paths) {
17
+ let text: string;
18
+ try {
19
+ text = await fs.readFile(p, "utf8");
20
+ } catch {
21
+ continue;
22
+ }
23
+ const matches: string[] = [];
24
+ let total = 0;
25
+ for (const line of text.split("\n")) {
26
+ if (!line.toLowerCase().includes(q)) continue;
27
+ matches.push(line);
28
+ total += line.length + 1;
29
+ if (total > SNIPPET_CAP) break;
30
+ }
31
+ if (matches.length > 0) {
32
+ return matches.join("\n");
33
+ }
34
+ }
35
+ return "";
36
+ }