@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.
Files changed (60) hide show
  1. package/README.md +156 -111
  2. package/dist/adapters/ollamaClient.d.ts +11 -0
  3. package/dist/adapters/ollamaClient.d.ts.map +1 -0
  4. package/dist/adapters/ollamaClient.js +122 -0
  5. package/dist/adapters/ollamaClient.js.map +1 -0
  6. package/dist/adapters/openaiCompatClient.d.ts +11 -0
  7. package/dist/adapters/openaiCompatClient.d.ts.map +1 -0
  8. package/dist/adapters/openaiCompatClient.js +118 -0
  9. package/dist/adapters/openaiCompatClient.js.map +1 -0
  10. package/dist/cli.js +2 -2
  11. package/dist/cli.js.map +1 -1
  12. package/dist/fallback/sessionIndex.d.ts.map +1 -1
  13. package/dist/fallback/sessionIndex.js +90 -25
  14. package/dist/fallback/sessionIndex.js.map +1 -1
  15. package/dist/fallback/sessionSearch.d.ts +1 -1
  16. package/dist/fallback/sessionSearch.d.ts.map +1 -1
  17. package/dist/fallback/sessionSearch.js +101 -28
  18. package/dist/fallback/sessionSearch.js.map +1 -1
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/local/graphQuery.d.ts +21 -0
  24. package/dist/local/graphQuery.d.ts.map +1 -0
  25. package/dist/local/graphQuery.js +170 -0
  26. package/dist/local/graphQuery.js.map +1 -0
  27. package/dist/paths.js +1 -1
  28. package/dist/paths.js.map +1 -1
  29. package/dist/pi-extension.d.ts.map +1 -1
  30. package/dist/pi-extension.js +57 -17
  31. package/dist/pi-extension.js.map +1 -1
  32. package/dist/service.d.ts +10 -10
  33. package/dist/service.d.ts.map +1 -1
  34. package/dist/service.js +72 -30
  35. package/dist/service.js.map +1 -1
  36. package/dist/settings.d.ts +38 -0
  37. package/dist/settings.d.ts.map +1 -0
  38. package/dist/settings.js +68 -0
  39. package/dist/settings.js.map +1 -0
  40. package/dist/sidecar/process.d.ts.map +1 -1
  41. package/dist/sidecar/process.js +16 -4
  42. package/dist/sidecar/process.js.map +1 -1
  43. package/dist/trainer/sessionLoader.d.ts +2 -2
  44. package/dist/trainer/sessionLoader.d.ts.map +1 -1
  45. package/dist/trainer/sessionLoader.js +115 -39
  46. package/dist/trainer/sessionLoader.js.map +1 -1
  47. package/package.json +5 -4
  48. package/src/adapters/ollamaClient.ts +179 -0
  49. package/src/adapters/openaiCompatClient.ts +155 -0
  50. package/src/cli.ts +4 -3
  51. package/src/fallback/sessionIndex.ts +78 -40
  52. package/src/fallback/sessionSearch.ts +107 -27
  53. package/src/index.ts +26 -0
  54. package/src/local/graphQuery.ts +228 -0
  55. package/src/paths.ts +1 -1
  56. package/src/pi-extension.ts +79 -17
  57. package/src/service.ts +78 -31
  58. package/src/settings.ts +126 -0
  59. package/src/sidecar/process.ts +19 -4
  60. 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 { defaultMemoryConfig } from "./config.js";
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: ReturnType<typeof defaultMemoryConfig>): Promise<void> {
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 = defaultMemoryConfig();
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
- let entries: string[];
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 name of entries) {
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 session: PiSessionFile;
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
- const sessionId = session.id ?? path.basename(name, ".json");
190
- const messages: Array<{ role: string; content: string; index: number }> = [];
191
- for (let i = 0; i < (session.messages?.length ?? 0); i++) {
192
- const msg = session.messages![i]!;
193
- const text = messageText(msg.content);
194
- if (text.trim()) {
195
- messages.push({ role: msg.role ?? "unknown", content: text, index: i });
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 JSON files (one directory level).
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
- let entries: string[];
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
- for (const name of entries) {
84
- if (!name.endsWith(".json")) continue;
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 session: PiSessionFile;
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
- const raw = await fs.readFile(filePath, "utf8");
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
- const sessionId = session.id ?? path.basename(name, ".json");
103
- const title = session.title ?? "";
104
- const createdAt = session.created_at ?? "";
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
- for (let i = 0; i < (session.messages?.length ?? 0); i++) {
107
- const msg = session.messages![i]!;
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 ?? "unknown",
193
+ role: msg.role,
114
194
  snippet: makeSnippet(text, terms[0]!),
115
- msg_index: i,
195
+ msg_index: msgIndex,
116
196
  created_at: createdAt,
117
197
  });
118
- if (hits.length >= limit) return hits;
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,