@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
package/src/settings.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
defaultMemoryConfig,
|
|
8
|
+
normalizeMemoryConfig,
|
|
9
|
+
type ExtractorType,
|
|
10
|
+
type MemoryConfig,
|
|
11
|
+
type MemoryProvider,
|
|
12
|
+
type TrainerConfig,
|
|
13
|
+
} from "./config.js";
|
|
14
|
+
import type { OllamaConfig } from "./adapters/ollamaClient.js";
|
|
15
|
+
import type { OpenAICompatConfig } from "./adapters/openaiCompatClient.js";
|
|
16
|
+
|
|
17
|
+
export interface MemorySettingsFile {
|
|
18
|
+
provider?: MemoryProvider;
|
|
19
|
+
tlmPath?: string;
|
|
20
|
+
socketPath?: string;
|
|
21
|
+
bundleRoot?: string;
|
|
22
|
+
sidecarReadyTimeoutMs?: number;
|
|
23
|
+
queryTimeoutMs?: number;
|
|
24
|
+
clientRequestTimeoutMs?: number;
|
|
25
|
+
sessionsDir?: string;
|
|
26
|
+
memoryMdPaths?: string[];
|
|
27
|
+
/** LLM for intent detection, rerank, and LLM extraction. Overrides default helper model. */
|
|
28
|
+
helperModel?: string;
|
|
29
|
+
/** Ollama local API config for helper / trainer / rerank. */
|
|
30
|
+
ollama?: Partial<OllamaConfig>;
|
|
31
|
+
/** OpenAI-compatible endpoint config (vLLM, SGLang, LM Studio, etc.) */
|
|
32
|
+
vllm?: Partial<OpenAICompatConfig>;
|
|
33
|
+
trainer?: Partial<TrainerConfig>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LoadedMemorySettings {
|
|
37
|
+
config: MemoryConfig;
|
|
38
|
+
helperModel: string | undefined;
|
|
39
|
+
ollama: OllamaConfig | null;
|
|
40
|
+
vllm: OpenAICompatConfig | null;
|
|
41
|
+
configPath: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Default path: ~/.pi/agent/memory.json */
|
|
45
|
+
export function defaultMemoryConfigPath(): string {
|
|
46
|
+
return path.join(getAgentDir(), "memory.json");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readMemorySettingsFile(configPath = defaultMemoryConfigPath()): MemorySettingsFile {
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
52
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
53
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
return parsed as MemorySettingsFile;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Failed to load pi-memory config from ${configPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Load pi-memory config from ~/.pi/agent/memory.json. */
|
|
68
|
+
export function loadMemorySettings(
|
|
69
|
+
overrides: Partial<MemoryConfig> = {},
|
|
70
|
+
configPath = defaultMemoryConfigPath(),
|
|
71
|
+
): LoadedMemorySettings {
|
|
72
|
+
const fileSettings = readMemorySettingsFile(configPath);
|
|
73
|
+
const { helperModel, ollama, vllm, trainer, ...configFields } = fileSettings;
|
|
74
|
+
|
|
75
|
+
const config = normalizeMemoryConfig({
|
|
76
|
+
...configFields,
|
|
77
|
+
...(trainer ? { trainer } : {}),
|
|
78
|
+
...overrides,
|
|
79
|
+
} as Partial<MemoryConfig> & Record<string, unknown>);
|
|
80
|
+
|
|
81
|
+
let resolvedOllama: OllamaConfig | null = null;
|
|
82
|
+
if (ollama && ollama.model) {
|
|
83
|
+
resolvedOllama = {
|
|
84
|
+
baseUrl: ollama.baseUrl?.trim() || "http://localhost:11434",
|
|
85
|
+
model: ollama.model.trim(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let resolvedVllm: OpenAICompatConfig | null = null;
|
|
90
|
+
if (vllm && vllm.model) {
|
|
91
|
+
resolvedVllm = {
|
|
92
|
+
baseUrl: vllm.baseUrl?.trim() || "http://localhost:8000",
|
|
93
|
+
model: vllm.model.trim(),
|
|
94
|
+
apiKey: vllm.apiKey?.trim() || undefined,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
config,
|
|
100
|
+
helperModel: helperModel?.trim() || undefined,
|
|
101
|
+
ollama: resolvedOllama,
|
|
102
|
+
vllm: resolvedVllm,
|
|
103
|
+
configPath,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Convenience alias when only MemoryConfig is needed. */
|
|
108
|
+
export function loadMemoryConfig(
|
|
109
|
+
overrides: Partial<MemoryConfig> = {},
|
|
110
|
+
configPath = defaultMemoryConfigPath(),
|
|
111
|
+
): MemoryConfig {
|
|
112
|
+
return loadMemorySettings(overrides, configPath).config;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function resolveHelperModelSpec(
|
|
116
|
+
flagValue: string | boolean | undefined,
|
|
117
|
+
settingsHelperModel: string | undefined,
|
|
118
|
+
): string | undefined {
|
|
119
|
+
if (typeof flagValue === "string" && flagValue.trim()) {
|
|
120
|
+
return flagValue.trim();
|
|
121
|
+
}
|
|
122
|
+
return settingsHelperModel;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export { defaultMemoryConfig };
|
|
126
|
+
export type { ExtractorType, MemoryProvider, TrainerConfig };
|
package/src/sidecar/process.ts
CHANGED
|
@@ -27,8 +27,9 @@ export class SidecarProcess {
|
|
|
27
27
|
await access(bin, constants.X_OK);
|
|
28
28
|
return bin;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const resolved = await which(bin);
|
|
31
|
+
if (!resolved) throw new Error(`${bin} not found in PATH`);
|
|
32
|
+
return resolved;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
async spawn(): Promise<void> {
|
|
@@ -57,8 +58,8 @@ export class SidecarProcess {
|
|
|
57
58
|
detached: process.platform !== "win32",
|
|
58
59
|
});
|
|
59
60
|
|
|
60
|
-
this.child.on("error", (
|
|
61
|
-
|
|
61
|
+
this.child.on("error", () => {
|
|
62
|
+
/* handled by waitReady timeout */
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
this.child.unref?.();
|
|
@@ -126,6 +127,20 @@ export class SidecarProcess {
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
async function which(cmd: string): Promise<string | null> {
|
|
131
|
+
const { execFileSync } = await import("node:child_process");
|
|
132
|
+
try {
|
|
133
|
+
const result = execFileSync(
|
|
134
|
+
process.platform === "win32" ? "where" : "which",
|
|
135
|
+
[cmd],
|
|
136
|
+
{ encoding: "utf8", timeout: 3_000 },
|
|
137
|
+
).trim();
|
|
138
|
+
return result.split("\n")[0]?.trim() || null;
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
129
144
|
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
130
145
|
return new Promise((resolve, reject) => {
|
|
131
146
|
if (signal?.aborted) {
|
|
@@ -29,6 +29,21 @@ interface PiSessionFile {
|
|
|
29
29
|
messages?: PiSessionMessage[];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
interface JsonlSessionHeader {
|
|
33
|
+
type: "session";
|
|
34
|
+
id?: string;
|
|
35
|
+
timestamp?: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface JsonlMessageLine {
|
|
40
|
+
type: "message";
|
|
41
|
+
message?: {
|
|
42
|
+
role?: string;
|
|
43
|
+
content?: unknown;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
function messageText(content: unknown): string {
|
|
33
48
|
if (typeof content === "string") return content;
|
|
34
49
|
if (!Array.isArray(content)) return "";
|
|
@@ -53,72 +68,143 @@ export interface SessionLoaderOptions {
|
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
71
|
+
* Collect all session files recursively (supports project subdirectories).
|
|
72
|
+
* Returns both .json and .jsonl files.
|
|
58
73
|
*/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
): Promise<LoadedSession[]> {
|
|
62
|
-
const { sessionsDir, modifiedAfter } = opts;
|
|
63
|
-
if (!sessionsDir.trim()) return [];
|
|
64
|
-
|
|
65
|
-
let entries: string[];
|
|
74
|
+
async function collectSessionFiles(dir: string): Promise<string[]> {
|
|
75
|
+
let names: string[];
|
|
66
76
|
try {
|
|
67
|
-
|
|
77
|
+
names = await fs.readdir(dir);
|
|
68
78
|
} catch {
|
|
69
79
|
return [];
|
|
70
80
|
}
|
|
71
81
|
|
|
72
|
-
const
|
|
82
|
+
const files: string[] = [];
|
|
83
|
+
for (const name of names) {
|
|
84
|
+
const full = path.join(dir, name);
|
|
85
|
+
let st;
|
|
86
|
+
try { st = await fs.stat(full); } catch { continue; }
|
|
87
|
+
if (st.isDirectory()) {
|
|
88
|
+
const sub = await collectSessionFiles(full);
|
|
89
|
+
files.push(...sub);
|
|
90
|
+
} else if (st.isFile() && (name.endsWith(".json") || name.endsWith(".jsonl"))) {
|
|
91
|
+
files.push(full);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return files;
|
|
95
|
+
}
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
function parseJsonlSession(raw: string, filePath: string): LoadedSession | null {
|
|
98
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
99
|
+
if (lines.length === 0) return null;
|
|
77
100
|
|
|
78
|
-
|
|
101
|
+
let header: JsonlSessionHeader | null = null;
|
|
102
|
+
const turns: SessionTurn[] = [];
|
|
103
|
+
let turnIndex = 0;
|
|
104
|
+
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
let obj: Record<string, unknown>;
|
|
79
107
|
try {
|
|
80
|
-
|
|
108
|
+
obj = JSON.parse(line) as Record<string, unknown>;
|
|
81
109
|
} catch {
|
|
82
110
|
continue;
|
|
83
111
|
}
|
|
84
|
-
if (!st.isFile()) continue;
|
|
85
112
|
|
|
86
|
-
if (
|
|
113
|
+
if (obj.type === "session" && !header) {
|
|
114
|
+
header = obj as unknown as JsonlSessionHeader;
|
|
87
115
|
continue;
|
|
88
116
|
}
|
|
89
117
|
|
|
90
|
-
|
|
118
|
+
if (obj.type === "message") {
|
|
119
|
+
const msg = (obj as unknown as JsonlMessageLine).message;
|
|
120
|
+
if (!msg?.role || !msg.content) continue;
|
|
121
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
122
|
+
const text = messageText(msg.content);
|
|
123
|
+
if (!text.trim()) continue;
|
|
124
|
+
turns.push({ role: msg.role, content: text, turnIndex: turnIndex++ });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (turns.length === 0) return null;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
id: header?.id ?? path.basename(filePath, path.extname(filePath)),
|
|
132
|
+
title: header?.title ?? "",
|
|
133
|
+
createdAt: header?.timestamp ?? "",
|
|
134
|
+
filePath,
|
|
135
|
+
modifiedAt: new Date(),
|
|
136
|
+
turns,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseJsonSession(raw: string, filePath: string): LoadedSession | null {
|
|
141
|
+
let session: PiSessionFile;
|
|
142
|
+
try {
|
|
143
|
+
session = JSON.parse(raw) as PiSessionFile;
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!session.messages || session.messages.length === 0) return null;
|
|
149
|
+
|
|
150
|
+
const turns: SessionTurn[] = [];
|
|
151
|
+
for (let i = 0; i < session.messages.length; i++) {
|
|
152
|
+
const msg = session.messages[i]!;
|
|
153
|
+
const text = messageText(msg.content);
|
|
154
|
+
if (!text.trim()) continue;
|
|
155
|
+
turns.push({ role: msg.role ?? "unknown", content: text, turnIndex: i });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (turns.length === 0) return null;
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
id: session.id ?? path.basename(filePath, ".json"),
|
|
162
|
+
title: session.title ?? "",
|
|
163
|
+
createdAt: session.created_at ?? "",
|
|
164
|
+
filePath,
|
|
165
|
+
modifiedAt: new Date(),
|
|
166
|
+
turns,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Scan session files (JSON + JSONL, recursive subdirectories), parse Pi session
|
|
172
|
+
* format, optionally filter by modified-after timestamp for incremental training.
|
|
173
|
+
*/
|
|
174
|
+
export async function loadSessions(
|
|
175
|
+
opts: SessionLoaderOptions,
|
|
176
|
+
): Promise<LoadedSession[]> {
|
|
177
|
+
const { sessionsDir, modifiedAfter } = opts;
|
|
178
|
+
if (!sessionsDir.trim()) return [];
|
|
179
|
+
|
|
180
|
+
const filePaths = await collectSessionFiles(sessionsDir);
|
|
181
|
+
const sessions: LoadedSession[] = [];
|
|
182
|
+
|
|
183
|
+
for (const filePath of filePaths) {
|
|
184
|
+
let st: Awaited<ReturnType<typeof fs.stat>>;
|
|
91
185
|
try {
|
|
92
|
-
|
|
93
|
-
session = JSON.parse(raw) as PiSessionFile;
|
|
186
|
+
st = await fs.stat(filePath);
|
|
94
187
|
} catch {
|
|
95
188
|
continue;
|
|
96
189
|
}
|
|
190
|
+
if (!st.isFile()) continue;
|
|
191
|
+
if (modifiedAfter && st.mtime <= modifiedAfter) continue;
|
|
97
192
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const text = messageText(msg.content);
|
|
104
|
-
if (!text.trim()) continue;
|
|
105
|
-
turns.push({
|
|
106
|
-
role: msg.role ?? "unknown",
|
|
107
|
-
content: text,
|
|
108
|
-
turnIndex: i,
|
|
109
|
-
});
|
|
193
|
+
let raw: string;
|
|
194
|
+
try {
|
|
195
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
196
|
+
} catch {
|
|
197
|
+
continue;
|
|
110
198
|
}
|
|
111
199
|
|
|
112
|
-
|
|
200
|
+
const isJsonl = filePath.endsWith(".jsonl");
|
|
201
|
+
const parsed = isJsonl
|
|
202
|
+
? parseJsonlSession(raw, filePath)
|
|
203
|
+
: parseJsonSession(raw, filePath);
|
|
113
204
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
createdAt: session.created_at ?? "",
|
|
118
|
-
filePath,
|
|
119
|
-
modifiedAt: st.mtime,
|
|
120
|
-
turns,
|
|
121
|
-
});
|
|
205
|
+
if (!parsed) continue;
|
|
206
|
+
parsed.modifiedAt = st.mtime;
|
|
207
|
+
sessions.push(parsed);
|
|
122
208
|
}
|
|
123
209
|
|
|
124
210
|
sessions.sort((a, b) => a.modifiedAt.getTime() - b.modifiedAt.getTime());
|