@chendpoc/pi-memory 0.1.0 → 0.1.11
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/README.md +156 -111
- package/dist/adapters/ollamaClient.d.ts +11 -0
- package/dist/adapters/ollamaClient.d.ts.map +1 -0
- package/dist/adapters/ollamaClient.js +122 -0
- package/dist/adapters/ollamaClient.js.map +1 -0
- package/dist/adapters/openaiCompatClient.d.ts +11 -0
- package/dist/adapters/openaiCompatClient.d.ts.map +1 -0
- package/dist/adapters/openaiCompatClient.js +118 -0
- package/dist/adapters/openaiCompatClient.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/fallback/sessionIndex.d.ts.map +1 -1
- package/dist/fallback/sessionIndex.js +90 -25
- package/dist/fallback/sessionIndex.js.map +1 -1
- package/dist/fallback/sessionSearch.d.ts +1 -1
- package/dist/fallback/sessionSearch.d.ts.map +1 -1
- package/dist/fallback/sessionSearch.js +101 -28
- package/dist/fallback/sessionSearch.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/local/graphQuery.d.ts +21 -0
- package/dist/local/graphQuery.d.ts.map +1 -0
- package/dist/local/graphQuery.js +170 -0
- package/dist/local/graphQuery.js.map +1 -0
- package/dist/paths.js +1 -1
- package/dist/paths.js.map +1 -1
- package/dist/pi-extension.d.ts.map +1 -1
- package/dist/pi-extension.js +57 -17
- package/dist/pi-extension.js.map +1 -1
- package/dist/service.d.ts +10 -10
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +72 -30
- package/dist/service.js.map +1 -1
- package/dist/settings.d.ts +38 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +68 -0
- package/dist/settings.js.map +1 -0
- package/dist/sidecar/process.d.ts.map +1 -1
- package/dist/sidecar/process.js +16 -4
- package/dist/sidecar/process.js.map +1 -1
- package/dist/trainer/sessionLoader.d.ts +2 -2
- package/dist/trainer/sessionLoader.d.ts.map +1 -1
- package/dist/trainer/sessionLoader.js +115 -39
- package/dist/trainer/sessionLoader.js.map +1 -1
- package/package.json +5 -4
- package/src/adapters/ollamaClient.ts +179 -0
- package/src/adapters/openaiCompatClient.ts +155 -0
- package/src/cli.ts +4 -3
- package/src/fallback/sessionIndex.ts +78 -40
- package/src/fallback/sessionSearch.ts +107 -27
- package/src/index.ts +26 -0
- package/src/local/graphQuery.ts +228 -0
- package/src/paths.ts +1 -1
- package/src/pi-extension.ts +79 -17
- package/src/service.ts +78 -31
- package/src/settings.ts +126 -0
- package/src/sidecar/process.ts +19 -4
- package/src/trainer/sessionLoader.ts +128 -42
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
|
|
4
|
+
import type { LLMClient } from "../trainer/llmExtractor.js";
|
|
5
|
+
import type { MemoryHelperLLM } from "../preflight/detectIntents.js";
|
|
6
|
+
import type { CompileMemoryIntentsResult } from "../preflight/detectIntents.js";
|
|
7
|
+
import {
|
|
8
|
+
COMPILE_MEMORY_INTENTS_PARAMETERS,
|
|
9
|
+
MEMORY_HELPER_TOOL_NAME,
|
|
10
|
+
} from "../preflight/detectIntents.js";
|
|
11
|
+
|
|
12
|
+
export interface OpenAICompatConfig {
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
model: string;
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ChatMessage {
|
|
19
|
+
role: "system" | "user" | "assistant";
|
|
20
|
+
content: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ChatCompletionRequest {
|
|
24
|
+
model: string;
|
|
25
|
+
messages: ChatMessage[];
|
|
26
|
+
max_tokens?: number;
|
|
27
|
+
tools?: Array<{
|
|
28
|
+
type: "function";
|
|
29
|
+
function: { name: string; description: string; parameters: Record<string, unknown> };
|
|
30
|
+
}>;
|
|
31
|
+
tool_choice?: { type: "function"; function: { name: string } };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ChatCompletionResponse {
|
|
35
|
+
choices?: Array<{
|
|
36
|
+
message?: {
|
|
37
|
+
content?: string | null;
|
|
38
|
+
tool_calls?: Array<{
|
|
39
|
+
function?: { name?: string; arguments?: string };
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
42
|
+
}>;
|
|
43
|
+
error?: { message?: string };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function postJSON<T>(url: URL, body: unknown, apiKey?: string): Promise<T> {
|
|
47
|
+
const mod = url.protocol === "https:" ? https : http;
|
|
48
|
+
const payload = JSON.stringify(body);
|
|
49
|
+
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const headers: Record<string, string> = {
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
"Content-Length": String(Buffer.byteLength(payload)),
|
|
54
|
+
};
|
|
55
|
+
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
56
|
+
|
|
57
|
+
const req = mod.request(url, { method: "POST", headers, timeout: 120_000 }, (res) => {
|
|
58
|
+
const chunks: Buffer[] = [];
|
|
59
|
+
res.on("data", (c) => chunks.push(c));
|
|
60
|
+
res.on("end", () => {
|
|
61
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
62
|
+
try { resolve(JSON.parse(text) as T); }
|
|
63
|
+
catch { reject(new Error(`OpenAI-compat: invalid JSON: ${text.slice(0, 200)}`)); }
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
req.on("error", (err) => reject(new Error(`OpenAI-compat: ${err.message}`)));
|
|
67
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("OpenAI-compat: timeout")); });
|
|
68
|
+
req.write(payload);
|
|
69
|
+
req.end();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function openaiCompatHealthCheck(baseUrl: string): Promise<boolean> {
|
|
74
|
+
try {
|
|
75
|
+
const url = new URL("/v1/models", baseUrl);
|
|
76
|
+
const mod = url.protocol === "https:" ? https : http;
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const req = mod.get(url, { timeout: 3_000 }, (res) => {
|
|
79
|
+
res.resume();
|
|
80
|
+
resolve(res.statusCode === 200);
|
|
81
|
+
});
|
|
82
|
+
req.on("error", () => resolve(false));
|
|
83
|
+
req.on("timeout", () => { req.destroy(); resolve(false); });
|
|
84
|
+
});
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createOpenAICompatLLMClient(cfg: OpenAICompatConfig): LLMClient {
|
|
91
|
+
return {
|
|
92
|
+
async complete(prompt: string): Promise<string> {
|
|
93
|
+
const url = new URL("/v1/chat/completions", cfg.baseUrl);
|
|
94
|
+
const body: ChatCompletionRequest = {
|
|
95
|
+
model: cfg.model,
|
|
96
|
+
messages: [{ role: "user", content: prompt }],
|
|
97
|
+
max_tokens: 8192,
|
|
98
|
+
};
|
|
99
|
+
const resp = await postJSON<ChatCompletionResponse>(url, body, cfg.apiKey);
|
|
100
|
+
if (resp.error?.message) throw new Error(`OpenAI-compat: ${resp.error.message}`);
|
|
101
|
+
const text = resp.choices?.[0]?.message?.content?.trim();
|
|
102
|
+
if (!text) throw new Error("OpenAI-compat: empty response");
|
|
103
|
+
return text;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function createOpenAICompatMemoryHelper(cfg: OpenAICompatConfig): MemoryHelperLLM {
|
|
109
|
+
return {
|
|
110
|
+
async compileIntents(text: string): Promise<CompileMemoryIntentsResult> {
|
|
111
|
+
const url = new URL("/v1/chat/completions", cfg.baseUrl);
|
|
112
|
+
const body: ChatCompletionRequest = {
|
|
113
|
+
model: cfg.model,
|
|
114
|
+
messages: [
|
|
115
|
+
{
|
|
116
|
+
role: "user",
|
|
117
|
+
content: `Analyze whether the user message requires recalling private episodic memory.\n\n<message>\n${text}\n</message>`,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
max_tokens: 2048,
|
|
121
|
+
tools: [
|
|
122
|
+
{
|
|
123
|
+
type: "function",
|
|
124
|
+
function: {
|
|
125
|
+
name: MEMORY_HELPER_TOOL_NAME,
|
|
126
|
+
description: "Decide whether to recall private episodic memory and compile structured query intents.",
|
|
127
|
+
parameters: COMPILE_MEMORY_INTENTS_PARAMETERS as unknown as Record<string, unknown>,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
tool_choice: { type: "function", function: { name: MEMORY_HELPER_TOOL_NAME } },
|
|
132
|
+
};
|
|
133
|
+
const resp = await postJSON<ChatCompletionResponse>(url, body, cfg.apiKey);
|
|
134
|
+
if (resp.error?.message) throw new Error(`OpenAI-compat: ${resp.error.message}`);
|
|
135
|
+
|
|
136
|
+
const msg = resp.choices?.[0]?.message;
|
|
137
|
+
const toolCall = msg?.tool_calls?.[0]?.function;
|
|
138
|
+
if (toolCall?.name === MEMORY_HELPER_TOOL_NAME && toolCall.arguments) {
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(toolCall.arguments) as CompileMemoryIntentsResult;
|
|
141
|
+
} catch { /* fall through */ }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const raw = msg?.content?.trim();
|
|
145
|
+
if (!raw) return { should_recall: false, intents: [] };
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```\s*$/m, "").trim();
|
|
149
|
+
return JSON.parse(cleaned) as CompileMemoryIntentsResult;
|
|
150
|
+
} catch {
|
|
151
|
+
return { should_recall: false, intents: [] };
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -4,7 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import { createStandaloneLLMClient } from "./adapters/piComplete.js";
|
|
6
6
|
import { installBundle } from "./bundle/install.js";
|
|
7
|
-
import {
|
|
7
|
+
import type { MemoryConfig } from "./config.js";
|
|
8
|
+
import { loadMemoryConfig } from "./settings.js";
|
|
8
9
|
import { openSessionIndex } from "./fallback/sessionIndex.js";
|
|
9
10
|
import { SidecarClient } from "./sidecar/client.js";
|
|
10
11
|
import { MemoryService } from "./service.js";
|
|
@@ -13,7 +14,7 @@ import { createLLMFactExtractor } from "./trainer/llmExtractor.js";
|
|
|
13
14
|
import { createTrainScheduler } from "./trainer/scheduler.js";
|
|
14
15
|
import type { QueryIntent } from "./types.js";
|
|
15
16
|
|
|
16
|
-
async function tryReloadSidecar(cfg:
|
|
17
|
+
async function tryReloadSidecar(cfg: MemoryConfig): Promise<void> {
|
|
17
18
|
try {
|
|
18
19
|
fs.accessSync(cfg.socketPath, fs.constants.F_OK);
|
|
19
20
|
} catch {
|
|
@@ -34,7 +35,7 @@ async function main(): Promise<void> {
|
|
|
34
35
|
process.exit(0);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
const cfg =
|
|
38
|
+
const cfg = loadMemoryConfig();
|
|
38
39
|
const service = new MemoryService(cfg);
|
|
39
40
|
|
|
40
41
|
if (cmd === "health") {
|
|
@@ -146,62 +146,100 @@ export function openSessionIndex(dbPath: string, injectedDb?: SqliteDatabase): S
|
|
|
146
146
|
return count;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
async function collectFiles(dir: string): Promise<string[]> {
|
|
150
|
+
let names: string[];
|
|
151
|
+
try {
|
|
152
|
+
names = await fs.readdir(dir);
|
|
153
|
+
} catch {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
const files: string[] = [];
|
|
157
|
+
for (const name of names) {
|
|
158
|
+
const full = path.join(dir, name);
|
|
159
|
+
let st;
|
|
160
|
+
try { st = await fs.stat(full); } catch { continue; }
|
|
161
|
+
if (st.isDirectory()) {
|
|
162
|
+
files.push(...await collectFiles(full));
|
|
163
|
+
} else if (st.isFile() && (name.endsWith(".json") || name.endsWith(".jsonl"))) {
|
|
164
|
+
files.push(full);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return files;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseJsonlMessages(raw: string, filePath: string): {
|
|
171
|
+
id: string; title: string; createdAt: string;
|
|
172
|
+
messages: Array<{ role: string; content: string; index: number }>;
|
|
173
|
+
} | null {
|
|
174
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
175
|
+
if (lines.length === 0) return null;
|
|
176
|
+
let id = path.basename(filePath, path.extname(filePath));
|
|
177
|
+
let title = "";
|
|
178
|
+
let createdAt = "";
|
|
179
|
+
const messages: Array<{ role: string; content: string; index: number }> = [];
|
|
180
|
+
let idx = 0;
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
let obj: Record<string, unknown>;
|
|
183
|
+
try { obj = JSON.parse(line) as Record<string, unknown>; } catch { continue; }
|
|
184
|
+
if (obj.type === "session") {
|
|
185
|
+
id = (obj.id as string) ?? id;
|
|
186
|
+
title = (obj.title as string) ?? "";
|
|
187
|
+
createdAt = (obj.timestamp as string) ?? "";
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (obj.type === "message") {
|
|
191
|
+
const msg = (obj as { message?: PiSessionMessage }).message;
|
|
192
|
+
if (!msg?.role || !msg.content) continue;
|
|
193
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
194
|
+
const text = messageText(msg.content);
|
|
195
|
+
if (text.trim()) {
|
|
196
|
+
messages.push({ role: msg.role, content: text, index: idx++ });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (messages.length === 0) return null;
|
|
201
|
+
return { id, title, createdAt, messages };
|
|
202
|
+
}
|
|
203
|
+
|
|
149
204
|
async function loadSessionFiles(sessionsDir: string, modifiedAfter?: Date | null): Promise<Array<{
|
|
150
205
|
id: string;
|
|
151
206
|
title: string;
|
|
152
207
|
createdAt: string;
|
|
153
208
|
messages: Array<{ role: string; content: string; index: number }>;
|
|
154
209
|
}>> {
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
entries = await fs.readdir(sessionsDir);
|
|
158
|
-
} catch {
|
|
159
|
-
return [];
|
|
160
|
-
}
|
|
161
|
-
|
|
210
|
+
const filePaths = await collectFiles(sessionsDir);
|
|
162
211
|
const results: Array<{
|
|
163
|
-
id: string;
|
|
164
|
-
title: string;
|
|
165
|
-
createdAt: string;
|
|
212
|
+
id: string; title: string; createdAt: string;
|
|
166
213
|
messages: Array<{ role: string; content: string; index: number }>;
|
|
167
214
|
}> = [];
|
|
168
215
|
|
|
169
|
-
for (const
|
|
170
|
-
if (!name.endsWith(".json")) continue;
|
|
171
|
-
const filePath = path.join(sessionsDir, name);
|
|
216
|
+
for (const filePath of filePaths) {
|
|
172
217
|
let st;
|
|
173
|
-
try {
|
|
174
|
-
st = await fs.stat(filePath);
|
|
175
|
-
} catch {
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
218
|
+
try { st = await fs.stat(filePath); } catch { continue; }
|
|
178
219
|
if (!st.isFile()) continue;
|
|
179
220
|
if (modifiedAfter && st.mtime <= modifiedAfter) continue;
|
|
180
221
|
|
|
181
|
-
let
|
|
182
|
-
try {
|
|
183
|
-
const raw = await fs.readFile(filePath, "utf8");
|
|
184
|
-
session = JSON.parse(raw) as PiSessionFile;
|
|
185
|
-
} catch {
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
222
|
+
let raw: string;
|
|
223
|
+
try { raw = await fs.readFile(filePath, "utf8"); } catch { continue; }
|
|
188
224
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
225
|
+
if (filePath.endsWith(".jsonl")) {
|
|
226
|
+
const parsed = parseJsonlMessages(raw, filePath);
|
|
227
|
+
if (parsed) results.push(parsed);
|
|
228
|
+
} else {
|
|
229
|
+
let session: PiSessionFile;
|
|
230
|
+
try { session = JSON.parse(raw) as PiSessionFile; } catch { continue; }
|
|
231
|
+
const sessionId = session.id ?? path.basename(filePath, ".json");
|
|
232
|
+
const messages: Array<{ role: string; content: string; index: number }> = [];
|
|
233
|
+
for (let i = 0; i < (session.messages?.length ?? 0); i++) {
|
|
234
|
+
const msg = session.messages![i]!;
|
|
235
|
+
const text = messageText(msg.content);
|
|
236
|
+
if (text.trim()) {
|
|
237
|
+
messages.push({ role: msg.role ?? "unknown", content: text, index: i });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (messages.length > 0) {
|
|
241
|
+
results.push({ id: sessionId, title: session.title ?? "", createdAt: session.created_at ?? "", messages });
|
|
196
242
|
}
|
|
197
|
-
}
|
|
198
|
-
if (messages.length > 0) {
|
|
199
|
-
results.push({
|
|
200
|
-
id: sessionId,
|
|
201
|
-
title: session.title ?? "",
|
|
202
|
-
createdAt: session.created_at ?? "",
|
|
203
|
-
messages,
|
|
204
|
-
});
|
|
205
243
|
}
|
|
206
244
|
}
|
|
207
245
|
return results;
|
|
@@ -16,10 +16,6 @@ export interface SessionSearchHit {
|
|
|
16
16
|
let cachedIndex: SessionIndex | null = null;
|
|
17
17
|
let cachedDbPath: string | null = null;
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
* Get or open the FTS5 session index. Returns null if better-sqlite3
|
|
21
|
-
* is unavailable or the DB file doesn't exist.
|
|
22
|
-
*/
|
|
23
19
|
function getSessionIndex(dbPath: string): SessionIndex | null {
|
|
24
20
|
if (cachedIndex && cachedDbPath === dbPath) return cachedIndex;
|
|
25
21
|
if (!fsSync.existsSync(dbPath)) return null;
|
|
@@ -47,8 +43,29 @@ interface PiSessionFile {
|
|
|
47
43
|
|
|
48
44
|
const SNIPPET_MAX = 240;
|
|
49
45
|
|
|
46
|
+
async function collectFiles(dir: string): Promise<string[]> {
|
|
47
|
+
let names: string[];
|
|
48
|
+
try {
|
|
49
|
+
names = await fs.readdir(dir);
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const files: string[] = [];
|
|
54
|
+
for (const name of names) {
|
|
55
|
+
const full = path.join(dir, name);
|
|
56
|
+
let st;
|
|
57
|
+
try { st = await fs.stat(full); } catch { continue; }
|
|
58
|
+
if (st.isDirectory()) {
|
|
59
|
+
files.push(...await collectFiles(full));
|
|
60
|
+
} else if (st.isFile() && (name.endsWith(".json") || name.endsWith(".jsonl"))) {
|
|
61
|
+
files.push(full);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
/**
|
|
51
|
-
* Keyword search over Pi-style session
|
|
68
|
+
* Keyword search over Pi-style session files (JSON + JSONL, recursive subdirectories).
|
|
52
69
|
* Uses FTS5 index when available, falls back to file scan.
|
|
53
70
|
* All whitespace-separated terms must match (case-insensitive AND).
|
|
54
71
|
*/
|
|
@@ -72,17 +89,10 @@ export async function sessionKeywordSearch(
|
|
|
72
89
|
const terms = splitTerms(q);
|
|
73
90
|
if (terms.length === 0) return [];
|
|
74
91
|
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
entries = await fs.readdir(sessionsDir);
|
|
78
|
-
} catch {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
|
|
92
|
+
const filePaths = await collectFiles(sessionsDir);
|
|
82
93
|
const hits: SessionSearchHit[] = [];
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const filePath = path.join(sessionsDir, name);
|
|
94
|
+
|
|
95
|
+
for (const filePath of filePaths) {
|
|
86
96
|
let st;
|
|
87
97
|
try {
|
|
88
98
|
st = await fs.stat(filePath);
|
|
@@ -91,34 +101,104 @@ export async function sessionKeywordSearch(
|
|
|
91
101
|
}
|
|
92
102
|
if (!st.isFile()) continue;
|
|
93
103
|
|
|
94
|
-
let
|
|
104
|
+
let raw: string;
|
|
105
|
+
try {
|
|
106
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
107
|
+
} catch {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (filePath.endsWith(".jsonl")) {
|
|
112
|
+
scanJsonlFile(raw, filePath, terms, hits, limit);
|
|
113
|
+
} else {
|
|
114
|
+
scanJsonFile(raw, filePath, terms, hits, limit);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (hits.length >= limit) return hits;
|
|
118
|
+
}
|
|
119
|
+
return hits;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function scanJsonFile(
|
|
123
|
+
raw: string,
|
|
124
|
+
filePath: string,
|
|
125
|
+
terms: string[],
|
|
126
|
+
hits: SessionSearchHit[],
|
|
127
|
+
limit: number,
|
|
128
|
+
): void {
|
|
129
|
+
let session: PiSessionFile;
|
|
130
|
+
try {
|
|
131
|
+
session = JSON.parse(raw) as PiSessionFile;
|
|
132
|
+
} catch {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const sessionId = session.id ?? path.basename(filePath, ".json");
|
|
137
|
+
const title = session.title ?? "";
|
|
138
|
+
const createdAt = session.created_at ?? "";
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < (session.messages?.length ?? 0); i++) {
|
|
141
|
+
const msg = session.messages![i]!;
|
|
142
|
+
const text = messageText(msg.content);
|
|
143
|
+
if (!text || !allTermsMatch(text, terms)) continue;
|
|
144
|
+
hits.push({
|
|
145
|
+
session_id: sessionId,
|
|
146
|
+
session_title: title,
|
|
147
|
+
role: msg.role ?? "unknown",
|
|
148
|
+
snippet: makeSnippet(text, terms[0]!),
|
|
149
|
+
msg_index: i,
|
|
150
|
+
created_at: createdAt,
|
|
151
|
+
});
|
|
152
|
+
if (hits.length >= limit) return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function scanJsonlFile(
|
|
157
|
+
raw: string,
|
|
158
|
+
filePath: string,
|
|
159
|
+
terms: string[],
|
|
160
|
+
hits: SessionSearchHit[],
|
|
161
|
+
limit: number,
|
|
162
|
+
): void {
|
|
163
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
164
|
+
let sessionId = path.basename(filePath, ".jsonl");
|
|
165
|
+
let title = "";
|
|
166
|
+
let createdAt = "";
|
|
167
|
+
let msgIndex = 0;
|
|
168
|
+
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
let obj: Record<string, unknown>;
|
|
95
171
|
try {
|
|
96
|
-
|
|
97
|
-
session = JSON.parse(raw) as PiSessionFile;
|
|
172
|
+
obj = JSON.parse(line) as Record<string, unknown>;
|
|
98
173
|
} catch {
|
|
99
174
|
continue;
|
|
100
175
|
}
|
|
101
176
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
177
|
+
if (obj.type === "session") {
|
|
178
|
+
sessionId = (obj.id as string) ?? sessionId;
|
|
179
|
+
title = (obj.title as string) ?? "";
|
|
180
|
+
createdAt = (obj.timestamp as string) ?? "";
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
105
183
|
|
|
106
|
-
|
|
107
|
-
const msg =
|
|
184
|
+
if (obj.type === "message") {
|
|
185
|
+
const msg = (obj as { message?: PiSessionMessage }).message;
|
|
186
|
+
if (!msg?.role || !msg.content) continue;
|
|
187
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
108
188
|
const text = messageText(msg.content);
|
|
109
189
|
if (!text || !allTermsMatch(text, terms)) continue;
|
|
110
190
|
hits.push({
|
|
111
191
|
session_id: sessionId,
|
|
112
192
|
session_title: title,
|
|
113
|
-
role: msg.role
|
|
193
|
+
role: msg.role,
|
|
114
194
|
snippet: makeSnippet(text, terms[0]!),
|
|
115
|
-
msg_index:
|
|
195
|
+
msg_index: msgIndex,
|
|
116
196
|
created_at: createdAt,
|
|
117
197
|
});
|
|
118
|
-
|
|
198
|
+
msgIndex++;
|
|
199
|
+
if (hits.length >= limit) return;
|
|
119
200
|
}
|
|
120
201
|
}
|
|
121
|
-
return hits;
|
|
122
202
|
}
|
|
123
203
|
|
|
124
204
|
function splitTerms(query: string): string[] {
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,15 @@ export {
|
|
|
25
25
|
type MemoryProvider,
|
|
26
26
|
} from "./config.js";
|
|
27
27
|
|
|
28
|
+
export {
|
|
29
|
+
defaultMemoryConfigPath,
|
|
30
|
+
loadMemoryConfig,
|
|
31
|
+
loadMemorySettings,
|
|
32
|
+
resolveHelperModelSpec,
|
|
33
|
+
type LoadedMemorySettings,
|
|
34
|
+
type MemorySettingsFile,
|
|
35
|
+
} from "./settings.js";
|
|
36
|
+
|
|
28
37
|
export {
|
|
29
38
|
defaultBundleRoot,
|
|
30
39
|
defaultPiHome,
|
|
@@ -206,6 +215,23 @@ export {
|
|
|
206
215
|
|
|
207
216
|
export { defaultSessionDbPath } from "./fallback/sessionSearch.js";
|
|
208
217
|
|
|
218
|
+
export { LocalGraphQuerier } from "./local/graphQuery.js";
|
|
219
|
+
|
|
220
|
+
export {
|
|
221
|
+
createOllamaLLMClient,
|
|
222
|
+
createOllamaMemoryHelper,
|
|
223
|
+
ollamaHealthCheck,
|
|
224
|
+
DEFAULT_OLLAMA_CONFIG,
|
|
225
|
+
type OllamaConfig,
|
|
226
|
+
} from "./adapters/ollamaClient.js";
|
|
227
|
+
|
|
228
|
+
export {
|
|
229
|
+
createOpenAICompatLLMClient,
|
|
230
|
+
createOpenAICompatMemoryHelper,
|
|
231
|
+
openaiCompatHealthCheck,
|
|
232
|
+
type OpenAICompatConfig,
|
|
233
|
+
} from "./adapters/openaiCompatClient.js";
|
|
234
|
+
|
|
209
235
|
export {
|
|
210
236
|
rerankWithLLM,
|
|
211
237
|
type RerankOptions,
|