om-memory-system 3.0.0 → 3.1.0-next.3
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/{LICENSE → LICENSE.md} +4 -2
- package/README.md +108 -532
- package/dist/adapters/opencode/auto-capture-summary.js +23 -17
- package/dist/adapters/opencode/import-command.d.ts +42 -0
- package/dist/adapters/opencode/import-command.js +154 -0
- package/dist/adapters/pi/extension.js +4 -5
- package/dist/adapters/pi/import-command.d.ts +7 -19
- package/dist/adapters/pi/import-command.js +46 -161
- package/dist/adapters/pi/live-model.d.ts +17 -0
- package/dist/adapters/pi/live-model.js +86 -0
- package/dist/adapters/pi/profile.d.ts +7 -17
- package/dist/adapters/pi/profile.js +28 -66
- package/dist/adapters/pi/provider.d.ts +8 -1
- package/dist/adapters/pi/provider.js +34 -6
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +94 -0
- package/dist/config.d.ts +7 -20
- package/dist/config.js +24 -61
- package/dist/core/extraction.d.ts +2 -2
- package/dist/core/extraction.js +8 -3
- package/dist/core/profile-analysis.d.ts +16 -0
- package/dist/core/profile-analysis.js +47 -0
- package/dist/importer/import-args.d.ts +44 -0
- package/dist/importer/import-args.js +219 -0
- package/dist/importer/importer.d.ts +33 -9
- package/dist/importer/importer.js +153 -89
- package/dist/importer/ledger.d.ts +9 -2
- package/dist/importer/ledger.js +16 -2
- package/dist/importer/model-selection.d.ts +16 -0
- package/dist/importer/model-selection.js +84 -0
- package/dist/importer/opencode-import.d.ts +28 -0
- package/dist/importer/opencode-import.js +87 -0
- package/dist/importer/opencode-project.d.ts +11 -0
- package/dist/importer/opencode-project.js +38 -0
- package/dist/importer/opencode-reader.d.ts +19 -0
- package/dist/importer/opencode-reader.js +214 -0
- package/dist/importer/profile-import.d.ts +29 -0
- package/dist/importer/profile-import.js +111 -0
- package/dist/importer/run-import.d.ts +49 -0
- package/dist/importer/run-import.js +123 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +37 -1
- package/dist/services/ai/internal-capture-sessions.d.ts +2 -0
- package/dist/services/ai/internal-capture-sessions.js +7 -0
- package/dist/services/ai/live-model-choice.d.ts +52 -0
- package/dist/services/ai/live-model-choice.js +83 -0
- package/dist/services/ai/profile-llm-client.js +7 -5
- package/dist/services/user-memory-learning.js +9 -8
- package/dist/services/user-profile/ai-cleanup.js +8 -5
- package/dist/services/user-profile/user-profile-manager.js +11 -10
- package/dist/v2/adapter.js +27 -0
- package/package.json +5 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
2
|
import { captureConversation } from "../core/capture.js";
|
|
3
3
|
import { memoryClient } from "../services/client.js";
|
|
4
4
|
import { extractScopeFromContainerTag } from "../services/memory-scope.js";
|
|
@@ -8,11 +8,11 @@ import { tursoShardManager } from "../services/turso/shard-manager.js";
|
|
|
8
8
|
import { extractPiConversationWindows, } from "../adapters/pi/conversation.js";
|
|
9
9
|
import { discoverPiSessions } from "./discovery.js";
|
|
10
10
|
import { PiImportLedger, importLedgerDbPath } from "./ledger.js";
|
|
11
|
-
export function buildImportKey(sessionId, window) {
|
|
11
|
+
export function buildImportKey(sessionId, window, host = "pi") {
|
|
12
12
|
const terminalEntryId = window.sourceEntryIds?.[window.sourceEntryIds.length - 1];
|
|
13
13
|
if (!terminalEntryId)
|
|
14
14
|
return null;
|
|
15
|
-
return
|
|
15
|
+
return `${host}:${sessionId}:${window.userEntryId}:${terminalEntryId}`;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Find a stored memory by its deterministic import identity. Used to
|
|
@@ -32,6 +32,20 @@ export async function findMemoryIdByImportId(projectHash, importId) {
|
|
|
32
32
|
}
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Project tag for scope filtering only. Symlinked paths (macOS /var vs
|
|
37
|
+
* /private/var) name one project, so compare through the real path.
|
|
38
|
+
*/
|
|
39
|
+
export function projectFilterTag(directory) {
|
|
40
|
+
let real = directory;
|
|
41
|
+
try {
|
|
42
|
+
real = realpathSync.native(directory);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// A missing directory keeps its recorded path.
|
|
46
|
+
}
|
|
47
|
+
return getTags(real).project.tag;
|
|
48
|
+
}
|
|
35
49
|
function resolveSessionDirectory(cwd, pathMaps) {
|
|
36
50
|
if (!cwd)
|
|
37
51
|
return null;
|
|
@@ -62,116 +76,52 @@ async function lookupExistingMemory(projectHash, key, options) {
|
|
|
62
76
|
return null;
|
|
63
77
|
}
|
|
64
78
|
}
|
|
65
|
-
|
|
79
|
+
/** Process host-labelled work units through the shared capture and ledger pipeline. */
|
|
80
|
+
export async function importHistorySource(source, host, deps, filters, report) {
|
|
66
81
|
const dryRun = Boolean(filters.dryRun);
|
|
67
|
-
const pathMaps = filters.pathMaps ?? [];
|
|
68
82
|
const ledger = deps.ledger ?? new PiImportLedger();
|
|
69
|
-
// Dry-run performs no ledger writes: only read state from an existing
|
|
70
|
-
// ledger file, and never create one.
|
|
71
83
|
const ledgerFileExists = existsSync(importLedgerDbPath());
|
|
72
84
|
const ledgerAccessible = !dryRun || ledgerFileExists;
|
|
73
|
-
|
|
85
|
+
// A dry run only reads the ledger (peek), so it never writes its schema.
|
|
86
|
+
if (!dryRun)
|
|
74
87
|
await ledger.get("__warmup__");
|
|
75
|
-
}
|
|
76
88
|
if (!dryRun) {
|
|
77
89
|
const embeddingInitError = memoryClient.getEmbeddingInitError?.();
|
|
78
90
|
if (embeddingInitError) {
|
|
79
91
|
throw new Error(`Memory system is not ready for import: ${embeddingInitError}. Start the extension and retry.`);
|
|
80
92
|
}
|
|
81
93
|
}
|
|
82
|
-
const currentTag = filters.scope === "current-project" ? getTags(filters.currentDirectory).project.tag : null;
|
|
83
|
-
const discovery = discoverPiSessions({ root: filters.root, maxSessions: filters.maxSessions });
|
|
84
|
-
const report = {
|
|
85
|
-
dryRun,
|
|
86
|
-
root: filters.root ?? "",
|
|
87
|
-
sessionsDiscovered: discovery.sessions.length,
|
|
88
|
-
sessionsLoaded: 0,
|
|
89
|
-
sessionsFilteredOut: 0,
|
|
90
|
-
sessionsUnrecognized: discovery.unrecognized.length,
|
|
91
|
-
unresolvableSessions: [],
|
|
92
|
-
loadErrors: [],
|
|
93
|
-
unitsTotal: 0,
|
|
94
|
-
unitsImported: 0,
|
|
95
|
-
unitsWouldImport: 0,
|
|
96
|
-
unitsSkipped: 0,
|
|
97
|
-
unitsFailed: 0,
|
|
98
|
-
unitsAlreadyHandled: 0,
|
|
99
|
-
skipReasons: {},
|
|
100
|
-
projects: [],
|
|
101
|
-
units: [],
|
|
102
|
-
};
|
|
103
|
-
const projectAggregates = new Map();
|
|
104
94
|
const candidates = [];
|
|
105
|
-
for (const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (discovered.sessionId !== wanted && discovered.file !== wanted) {
|
|
109
|
-
report.sessionsFilteredOut++;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
let session;
|
|
114
|
-
try {
|
|
115
|
-
session = deps.loadSession(discovered.file);
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
report.loadErrors.push({
|
|
119
|
-
file: discovered.file,
|
|
120
|
-
error: error instanceof Error ? error.message : String(error),
|
|
121
|
-
});
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
const directory = resolveSessionDirectory(session.cwd, pathMaps);
|
|
125
|
-
if (!directory) {
|
|
126
|
-
report.unresolvableSessions.push({ file: session.sourceFile, cwd: session.cwd });
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
const tags = getTags(directory);
|
|
130
|
-
if (currentTag && tags.project.tag !== currentTag) {
|
|
131
|
-
report.sessionsFilteredOut++;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
const { hash } = extractScopeFromContainerTag(tags.project.tag);
|
|
135
|
-
report.sessionsLoaded++;
|
|
136
|
-
let aggregate = projectAggregates.get(tags.project.tag);
|
|
137
|
-
if (!aggregate) {
|
|
138
|
-
aggregate = {
|
|
139
|
-
tag: tags.project.tag,
|
|
140
|
-
hash,
|
|
141
|
-
directory,
|
|
142
|
-
sessions: new Set(),
|
|
143
|
-
units: 0,
|
|
144
|
-
};
|
|
145
|
-
projectAggregates.set(tags.project.tag, aggregate);
|
|
146
|
-
}
|
|
147
|
-
aggregate.sessions.add(session.sessionId);
|
|
148
|
-
const windows = extractPiConversationWindows(session.branch);
|
|
149
|
-
for (const window of windows) {
|
|
95
|
+
for await (const session of source) {
|
|
96
|
+
const { hash } = extractScopeFromContainerTag(getTags(session.directory).project.tag);
|
|
97
|
+
for (const window of session.units) {
|
|
150
98
|
if (!windowWithinDateRange(window, filters))
|
|
151
99
|
continue;
|
|
152
|
-
|
|
153
|
-
if (!key)
|
|
100
|
+
if (!buildImportKey(session.sessionId, window, host))
|
|
154
101
|
continue;
|
|
155
|
-
candidates.push({ session,
|
|
156
|
-
aggregate.units++;
|
|
102
|
+
candidates.push({ ...session, hash, window });
|
|
157
103
|
}
|
|
158
104
|
}
|
|
159
105
|
report.unitsTotal = candidates.length;
|
|
160
106
|
const storageAccessible = ledgerFileExists;
|
|
161
107
|
let processed = 0;
|
|
162
108
|
for (const candidate of candidates) {
|
|
163
|
-
const {
|
|
164
|
-
const key = buildImportKey(
|
|
109
|
+
const { sessionId, directory, sourceFile, hash, window } = candidate;
|
|
110
|
+
const key = buildImportKey(sessionId, window, host);
|
|
165
111
|
processed++;
|
|
166
112
|
deps.onProgress?.(processed, report.unitsTotal, preview(window.userPrompt));
|
|
167
113
|
const unit = {
|
|
168
114
|
key,
|
|
169
|
-
sessionId:
|
|
115
|
+
sessionId: sessionId,
|
|
170
116
|
userEntryId: window.userEntryId,
|
|
171
117
|
promptPreview: preview(window.userPrompt),
|
|
172
118
|
status: "would-import",
|
|
173
119
|
};
|
|
174
|
-
const existing = ledgerAccessible
|
|
120
|
+
const existing = !ledgerAccessible
|
|
121
|
+
? null
|
|
122
|
+
: dryRun
|
|
123
|
+
? await ledger.peek(key)
|
|
124
|
+
: await ledger.get(key);
|
|
175
125
|
const terminalHandled = existing &&
|
|
176
126
|
(existing.status === "imported" || existing.status === "skipped") &&
|
|
177
127
|
!filters.force;
|
|
@@ -202,14 +152,14 @@ export async function importPiHistory(deps, filters) {
|
|
|
202
152
|
}
|
|
203
153
|
await ledger.begin({
|
|
204
154
|
key,
|
|
205
|
-
sessionId:
|
|
206
|
-
sourceFile:
|
|
155
|
+
sessionId: sessionId,
|
|
156
|
+
sourceFile: sourceFile,
|
|
207
157
|
projectHash: hash,
|
|
208
158
|
});
|
|
209
159
|
try {
|
|
210
160
|
const result = await captureConversation({
|
|
211
|
-
host
|
|
212
|
-
hostSessionId:
|
|
161
|
+
host,
|
|
162
|
+
hostSessionId: sessionId,
|
|
213
163
|
sourceType: "history-import",
|
|
214
164
|
projectDirectory: directory,
|
|
215
165
|
userPrompt: window.userPrompt,
|
|
@@ -222,7 +172,7 @@ export async function importPiHistory(deps, filters) {
|
|
|
222
172
|
: window.sourceTimestamp !== undefined
|
|
223
173
|
? { sourceTimestamp: window.sourceTimestamp }
|
|
224
174
|
: {}),
|
|
225
|
-
sourceFile:
|
|
175
|
+
sourceFile: sourceFile,
|
|
226
176
|
importId: key,
|
|
227
177
|
}, deps.provider);
|
|
228
178
|
if (result.status === "skipped") {
|
|
@@ -249,6 +199,111 @@ export async function importPiHistory(deps, filters) {
|
|
|
249
199
|
}
|
|
250
200
|
report.units.push(unit);
|
|
251
201
|
}
|
|
202
|
+
}
|
|
203
|
+
export async function importPiHistory(deps, filters) {
|
|
204
|
+
const dryRun = Boolean(filters.dryRun);
|
|
205
|
+
const pathMaps = filters.pathMaps ?? [];
|
|
206
|
+
const currentTag = filters.scope === "current-project" ? projectFilterTag(filters.currentDirectory) : null;
|
|
207
|
+
const discovery = discoverPiSessions({ root: filters.root, maxSessions: filters.maxSessions });
|
|
208
|
+
const report = {
|
|
209
|
+
dryRun,
|
|
210
|
+
root: filters.root ?? "",
|
|
211
|
+
sessionsDiscovered: discovery.sessions.length,
|
|
212
|
+
sessionsLoaded: 0,
|
|
213
|
+
sessionsFilteredOut: 0,
|
|
214
|
+
sessionsUnrecognized: discovery.unrecognized.length,
|
|
215
|
+
unresolvableSessions: [],
|
|
216
|
+
loadErrors: [],
|
|
217
|
+
unitsTotal: 0,
|
|
218
|
+
unitsImported: 0,
|
|
219
|
+
unitsWouldImport: 0,
|
|
220
|
+
unitsSkipped: 0,
|
|
221
|
+
unitsFailed: 0,
|
|
222
|
+
unitsAlreadyHandled: 0,
|
|
223
|
+
skipReasons: {},
|
|
224
|
+
projects: [],
|
|
225
|
+
units: [],
|
|
226
|
+
};
|
|
227
|
+
const projectAggregates = new Map();
|
|
228
|
+
const candidates = [];
|
|
229
|
+
for (const discovered of discovery.sessions) {
|
|
230
|
+
if (filters.session) {
|
|
231
|
+
const wanted = filters.session;
|
|
232
|
+
if (discovered.sessionId !== wanted && discovered.file !== wanted) {
|
|
233
|
+
report.sessionsFilteredOut++;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
let session;
|
|
238
|
+
try {
|
|
239
|
+
session = deps.loadSession(discovered.file);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
report.loadErrors.push({
|
|
243
|
+
file: discovered.file,
|
|
244
|
+
error: error instanceof Error ? error.message : String(error),
|
|
245
|
+
});
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const directory = resolveSessionDirectory(session.cwd, pathMaps);
|
|
249
|
+
if (!directory) {
|
|
250
|
+
report.unresolvableSessions.push({ file: session.sourceFile, cwd: session.cwd });
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const tags = getTags(directory);
|
|
254
|
+
if (currentTag && projectFilterTag(directory) !== currentTag) {
|
|
255
|
+
report.sessionsFilteredOut++;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const { hash } = extractScopeFromContainerTag(tags.project.tag);
|
|
259
|
+
report.sessionsLoaded++;
|
|
260
|
+
let aggregate = projectAggregates.get(tags.project.tag);
|
|
261
|
+
if (!aggregate) {
|
|
262
|
+
aggregate = {
|
|
263
|
+
tag: tags.project.tag,
|
|
264
|
+
hash,
|
|
265
|
+
directory,
|
|
266
|
+
sessions: new Set(),
|
|
267
|
+
units: 0,
|
|
268
|
+
};
|
|
269
|
+
projectAggregates.set(tags.project.tag, aggregate);
|
|
270
|
+
}
|
|
271
|
+
aggregate.sessions.add(session.sessionId);
|
|
272
|
+
const windows = extractPiConversationWindows(session.branch);
|
|
273
|
+
for (const window of windows) {
|
|
274
|
+
if (!windowWithinDateRange(window, filters))
|
|
275
|
+
continue;
|
|
276
|
+
const key = buildImportKey(session.sessionId, window);
|
|
277
|
+
if (!key)
|
|
278
|
+
continue;
|
|
279
|
+
candidates.push({ session, directory, tag: tags.project.tag, hash, window });
|
|
280
|
+
aggregate.units++;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// One source session per Pi session, so tags resolve once rather than per window.
|
|
284
|
+
const grouped = new Map();
|
|
285
|
+
for (const { session, directory, window } of candidates) {
|
|
286
|
+
const key = `${session.sessionId}\0${session.sourceFile}`;
|
|
287
|
+
const entry = grouped.get(key);
|
|
288
|
+
if (entry) {
|
|
289
|
+
entry.units.push(window);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
grouped.set(key, {
|
|
293
|
+
sessionId: session.sessionId,
|
|
294
|
+
directory,
|
|
295
|
+
sourceFile: session.sourceFile,
|
|
296
|
+
units: [window],
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const sourceSessions = [...grouped.values()];
|
|
301
|
+
if (filters.skipMemories) {
|
|
302
|
+
report.unitsTotal = candidates.length;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
await importHistorySource(sourceSessions, "pi", deps, filters, report);
|
|
306
|
+
}
|
|
252
307
|
report.projects = [...projectAggregates.values()].map((aggregate) => ({
|
|
253
308
|
tag: aggregate.tag,
|
|
254
309
|
hash: aggregate.hash,
|
|
@@ -256,5 +311,14 @@ export async function importPiHistory(deps, filters) {
|
|
|
256
311
|
sessions: aggregate.sessions.size,
|
|
257
312
|
units: aggregate.units,
|
|
258
313
|
}));
|
|
314
|
+
if (deps.profile) {
|
|
315
|
+
const { importProfileFromHistory } = await import("./profile-import.js");
|
|
316
|
+
report.profile = await importProfileFromHistory(sourceSessions, {
|
|
317
|
+
host: "pi",
|
|
318
|
+
dryRun,
|
|
319
|
+
model: deps.profile.model,
|
|
320
|
+
batchSize: deps.profile.batchSize,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
259
323
|
return report;
|
|
260
324
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Durable ledger for
|
|
2
|
+
* Durable ledger for historical-session imports across hosts.
|
|
3
3
|
*
|
|
4
4
|
* One row per deterministic import key. Records terminal states so reruns
|
|
5
5
|
* never reprocess handled work, plus in-progress states so a crash between
|
|
@@ -22,11 +22,16 @@ export interface ImportLedgerRow {
|
|
|
22
22
|
updatedAt: number;
|
|
23
23
|
}
|
|
24
24
|
export declare function importLedgerDbPath(storagePath?: string): string;
|
|
25
|
-
export declare class
|
|
25
|
+
export declare class ImportLedger {
|
|
26
26
|
private initPromise;
|
|
27
27
|
private ready;
|
|
28
28
|
private db;
|
|
29
29
|
get(key: string): Promise<ImportLedgerRow | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Read-only lookup for dry runs: never creates the table or its indexes.
|
|
32
|
+
* A ledger file without the table reads as empty.
|
|
33
|
+
*/
|
|
34
|
+
peek(key: string): Promise<ImportLedgerRow | null>;
|
|
30
35
|
countByStatus(): Promise<Record<string, number>>;
|
|
31
36
|
/** Upsert an in-progress marker before processing a unit. */
|
|
32
37
|
begin(input: {
|
|
@@ -42,3 +47,5 @@ export declare class PiImportLedger {
|
|
|
42
47
|
/** Retryable failure: the unit is reprocessed on the next run. */
|
|
43
48
|
fail(key: string, error: string): Promise<void>;
|
|
44
49
|
}
|
|
50
|
+
/** Backwards-compatible name for existing Pi imports. */
|
|
51
|
+
export { ImportLedger as PiImportLedger };
|
package/dist/importer/ledger.js
CHANGED
|
@@ -2,7 +2,7 @@ import { join } from "node:path";
|
|
|
2
2
|
import { CONFIG } from "../config.js";
|
|
3
3
|
import { tursoConnectionManager } from "../services/turso/connection-manager.js";
|
|
4
4
|
/**
|
|
5
|
-
* Durable ledger for
|
|
5
|
+
* Durable ledger for historical-session imports across hosts.
|
|
6
6
|
*
|
|
7
7
|
* One row per deterministic import key. Records terminal states so reruns
|
|
8
8
|
* never reprocess handled work, plus in-progress states so a crash between
|
|
@@ -28,7 +28,7 @@ function rowToEntry(row) {
|
|
|
28
28
|
updatedAt: Number(row.updated_at),
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
export class
|
|
31
|
+
export class ImportLedger {
|
|
32
32
|
initPromise = null;
|
|
33
33
|
async ready() {
|
|
34
34
|
if (this.initPromise)
|
|
@@ -75,6 +75,18 @@ export class PiImportLedger {
|
|
|
75
75
|
const row = await db.get(`SELECT * FROM import_ledger WHERE key = ?`, [key]);
|
|
76
76
|
return row ? rowToEntry(row) : null;
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Read-only lookup for dry runs: never creates the table or its indexes.
|
|
80
|
+
* A ledger file without the table reads as empty.
|
|
81
|
+
*/
|
|
82
|
+
async peek(key) {
|
|
83
|
+
const db = await tursoConnectionManager.getConnection(importLedgerDbPath());
|
|
84
|
+
const table = await db.get(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'import_ledger'`);
|
|
85
|
+
if (!table)
|
|
86
|
+
return null;
|
|
87
|
+
const row = await db.get(`SELECT * FROM import_ledger WHERE key = ?`, [key]);
|
|
88
|
+
return row ? rowToEntry(row) : null;
|
|
89
|
+
}
|
|
78
90
|
async countByStatus() {
|
|
79
91
|
const db = await this.db();
|
|
80
92
|
const rows = await db.all(`SELECT status, COUNT(*) AS count FROM import_ledger GROUP BY status`);
|
|
@@ -135,3 +147,5 @@ export class PiImportLedger {
|
|
|
135
147
|
`, [key, error, Date.now()]);
|
|
136
148
|
}
|
|
137
149
|
}
|
|
150
|
+
/** Backwards-compatible name for existing Pi imports. */
|
|
151
|
+
export { ImportLedger as PiImportLedger };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CaptureSummaryProvider } from "../core/host.js";
|
|
2
|
+
import type { ModelPort } from "../core/profile-analysis.js";
|
|
3
|
+
export interface ImportModelFlags {
|
|
4
|
+
provider?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
apiUrl?: string;
|
|
7
|
+
apiKeyEnv?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SelectedImportModel {
|
|
10
|
+
provider: string;
|
|
11
|
+
modelId: string;
|
|
12
|
+
capture: CaptureSummaryProvider;
|
|
13
|
+
profile: ModelPort;
|
|
14
|
+
}
|
|
15
|
+
/** Merge import-only model settings without changing the saved omms config. */
|
|
16
|
+
export declare function selectImportModel(flags: ImportModelFlags): SelectedImportModel;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { CONFIG } from "../config.js";
|
|
3
|
+
import { buildBoundedSummaryPrompt } from "../core/capture-context.js";
|
|
4
|
+
import { buildCaptureSystemPrompt, captureSummaryToolSchema, parseCaptureSummary, } from "../core/extraction.js";
|
|
5
|
+
import { AIProviderFactory } from "../services/ai/ai-provider-factory.js";
|
|
6
|
+
import { buildMemoryProviderConfig } from "../services/ai/provider-config.js";
|
|
7
|
+
import { resolveSecretValue } from "../services/secret-resolver.js";
|
|
8
|
+
/** Merge import-only model settings without changing the saved omms config. */
|
|
9
|
+
export function selectImportModel(flags) {
|
|
10
|
+
const providerName = flags.provider ?? CONFIG.memoryProvider;
|
|
11
|
+
// The saved URL belongs to the saved provider; never send a request for another one there.
|
|
12
|
+
const providerOverridden = flags.provider !== undefined && flags.provider !== CONFIG.memoryProvider;
|
|
13
|
+
if (providerOverridden && !flags.apiUrl && providerName !== "orcarouter") {
|
|
14
|
+
throw new Error("--api-url is required when --provider differs from memoryProvider");
|
|
15
|
+
}
|
|
16
|
+
const modelId = flags.model ?? CONFIG.memoryModel;
|
|
17
|
+
const apiUrl = flags.apiUrl ?? CONFIG.memoryApiUrl;
|
|
18
|
+
const key = flags.apiKeyEnv
|
|
19
|
+
? resolveSecretValue(`env://${flags.apiKeyEnv}`)
|
|
20
|
+
: CONFIG.memoryApiKey;
|
|
21
|
+
if (!modelId && providerName !== "orcarouter")
|
|
22
|
+
throw new Error("Missing memoryModel or --model");
|
|
23
|
+
if (!apiUrl && providerName !== "orcarouter")
|
|
24
|
+
throw new Error("Missing memoryApiUrl or --api-url");
|
|
25
|
+
if (!key)
|
|
26
|
+
throw new Error("Missing memoryApiKey or --api-key-env");
|
|
27
|
+
if (!AIProviderFactory.getSupportedProviders().includes(providerName)) {
|
|
28
|
+
throw new Error(`Unsupported import provider: ${providerName}`);
|
|
29
|
+
}
|
|
30
|
+
const provider = AIProviderFactory.createProvider(providerName, buildMemoryProviderConfig({
|
|
31
|
+
...CONFIG,
|
|
32
|
+
memoryProvider: providerName,
|
|
33
|
+
memoryModel: modelId,
|
|
34
|
+
memoryApiUrl: apiUrl,
|
|
35
|
+
memoryApiKey: key,
|
|
36
|
+
}));
|
|
37
|
+
const capture = {
|
|
38
|
+
async summarize(request) {
|
|
39
|
+
const { detectLanguage, getLanguageName } = await import("../services/language-detector.js");
|
|
40
|
+
const target = CONFIG.autoCaptureLanguage && CONFIG.autoCaptureLanguage !== "auto"
|
|
41
|
+
? CONFIG.autoCaptureLanguage
|
|
42
|
+
: detectLanguage(request.userPrompt);
|
|
43
|
+
const system = buildCaptureSystemPrompt(getLanguageName(target));
|
|
44
|
+
const tool = {
|
|
45
|
+
type: "function",
|
|
46
|
+
function: {
|
|
47
|
+
name: "save_memory",
|
|
48
|
+
description: "Save a technical memory",
|
|
49
|
+
parameters: captureSummaryToolSchema,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const result = await provider.executeToolCall(system, buildBoundedSummaryPrompt(request.context, system, tool), tool, `history-capture-${randomUUID()}`);
|
|
53
|
+
if (!result.success || !result.data) {
|
|
54
|
+
throw new Error("History capture model call failed");
|
|
55
|
+
}
|
|
56
|
+
const raw = typeof result.data === "string" ? result.data : JSON.stringify(result.data);
|
|
57
|
+
const parsed = parseCaptureSummary(raw);
|
|
58
|
+
if (!parsed)
|
|
59
|
+
throw new Error("History capture returned an invalid summary");
|
|
60
|
+
return parsed;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
const profile = {
|
|
64
|
+
provider: providerName,
|
|
65
|
+
modelId: modelId ?? "",
|
|
66
|
+
async complete(system, prompt) {
|
|
67
|
+
const { z } = await import("zod");
|
|
68
|
+
const { createUserProfileAnalysisSchema } = await import("../core/extraction.js");
|
|
69
|
+
const tool = {
|
|
70
|
+
type: "function",
|
|
71
|
+
function: {
|
|
72
|
+
name: "update_user_profile",
|
|
73
|
+
description: "Analyse historical user prompts",
|
|
74
|
+
parameters: z.toJSONSchema(createUserProfileAnalysisSchema(z)),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const result = await provider.executeToolCall(system, prompt, tool, `history-profile-${randomUUID()}`);
|
|
78
|
+
if (!result.success || !result.data)
|
|
79
|
+
throw new Error("History profile model call failed");
|
|
80
|
+
return typeof result.data === "string" ? result.data : JSON.stringify(result.data);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
return { provider: providerName, modelId: modelId ?? "", capture, profile };
|
|
84
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CaptureSummaryProvider } from "../core/host.js";
|
|
2
|
+
import type { ModelPort } from "../core/profile-analysis.js";
|
|
3
|
+
import { type ImportPathMap, type ImportReport } from "./importer.js";
|
|
4
|
+
import { type UnresolvedProject } from "./opencode-project.js";
|
|
5
|
+
import { type ProfileImportReport } from "./profile-import.js";
|
|
6
|
+
export interface OpencodeImportOptions {
|
|
7
|
+
dbPath?: string;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
since?: number;
|
|
10
|
+
until?: number;
|
|
11
|
+
session?: string;
|
|
12
|
+
project?: string;
|
|
13
|
+
maxSessions?: number;
|
|
14
|
+
pathMaps?: ImportPathMap[];
|
|
15
|
+
force?: boolean;
|
|
16
|
+
skipMemories?: boolean;
|
|
17
|
+
skipProfile?: boolean;
|
|
18
|
+
profileBatch?: number;
|
|
19
|
+
provider?: CaptureSummaryProvider;
|
|
20
|
+
profileModel?: ModelPort;
|
|
21
|
+
}
|
|
22
|
+
export interface OpencodeImportReport extends ImportReport {
|
|
23
|
+
childSessionsFolded: number;
|
|
24
|
+
unresolvedProjects: UnresolvedProject[];
|
|
25
|
+
profile?: ProfileImportReport;
|
|
26
|
+
}
|
|
27
|
+
/** Import resolved OpenCode history through the shared memory and profile steps. */
|
|
28
|
+
export declare function importOpencodeHistory(options: OpencodeImportOptions): Promise<OpencodeImportReport>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getTags } from "../services/tags.js";
|
|
4
|
+
import { extractScopeFromContainerTag } from "../services/memory-scope.js";
|
|
5
|
+
import { importHistorySource, projectFilterTag, } from "./importer.js";
|
|
6
|
+
import { resolveOpencodeSessions } from "./opencode-project.js";
|
|
7
|
+
import { readOpencodeHistory } from "./opencode-reader.js";
|
|
8
|
+
import { importProfileFromHistory } from "./profile-import.js";
|
|
9
|
+
/** Import resolved OpenCode history through the shared memory and profile steps. */
|
|
10
|
+
export async function importOpencodeHistory(options) {
|
|
11
|
+
if (!options.dryRun && !options.skipMemories && !options.provider) {
|
|
12
|
+
throw new Error("OpenCode history import needs a capture model");
|
|
13
|
+
}
|
|
14
|
+
if (!options.dryRun && !options.skipProfile && !options.profileModel) {
|
|
15
|
+
throw new Error("OpenCode history import needs a profile model");
|
|
16
|
+
}
|
|
17
|
+
const dbPath = options.dbPath ?? join(homedir(), ".local/share/opencode/opencode.db");
|
|
18
|
+
// Resolve the filter before opening the reader: the reader only closes once iterated.
|
|
19
|
+
const projectTag = options.project ? projectFilterTag(options.project) : null;
|
|
20
|
+
const reader = readOpencodeHistory(dbPath, options);
|
|
21
|
+
const unresolved = new Map();
|
|
22
|
+
const sessions = [];
|
|
23
|
+
const projects = new Map();
|
|
24
|
+
let filtered = 0;
|
|
25
|
+
for await (const session of resolveOpencodeSessions(reader.sessions, options.pathMaps ?? [], unresolved)) {
|
|
26
|
+
const info = getTags(session.directory).project;
|
|
27
|
+
if (projectTag && projectTag !== projectFilterTag(session.directory)) {
|
|
28
|
+
filtered++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const project = projects.get(info.tag) ?? {
|
|
32
|
+
tag: info.tag,
|
|
33
|
+
hash: extractScopeFromContainerTag(info.tag).hash,
|
|
34
|
+
directory: session.directory,
|
|
35
|
+
sessions: 0,
|
|
36
|
+
units: 0,
|
|
37
|
+
sessionIds: new Set(),
|
|
38
|
+
};
|
|
39
|
+
project.sessionIds.add(session.sessionId);
|
|
40
|
+
project.sessions = project.sessionIds.size;
|
|
41
|
+
project.units += session.units.length;
|
|
42
|
+
projects.set(info.tag, project);
|
|
43
|
+
sessions.push(session);
|
|
44
|
+
}
|
|
45
|
+
const report = {
|
|
46
|
+
dryRun: Boolean(options.dryRun),
|
|
47
|
+
root: dbPath,
|
|
48
|
+
sessionsDiscovered: reader.topLevelSessions,
|
|
49
|
+
sessionsLoaded: sessions.length,
|
|
50
|
+
sessionsFilteredOut: filtered,
|
|
51
|
+
sessionsUnrecognized: 0,
|
|
52
|
+
unresolvableSessions: [],
|
|
53
|
+
loadErrors: [],
|
|
54
|
+
unitsTotal: 0,
|
|
55
|
+
unitsImported: 0,
|
|
56
|
+
unitsWouldImport: 0,
|
|
57
|
+
unitsSkipped: 0,
|
|
58
|
+
unitsFailed: 0,
|
|
59
|
+
unitsAlreadyHandled: 0,
|
|
60
|
+
skipReasons: {},
|
|
61
|
+
units: [],
|
|
62
|
+
projects: [...projects.values()].map(({ sessionIds: _ids, ...project }) => project),
|
|
63
|
+
childSessionsFolded: reader.childSessions,
|
|
64
|
+
unresolvedProjects: [...unresolved.values()],
|
|
65
|
+
};
|
|
66
|
+
if (!options.skipMemories) {
|
|
67
|
+
await importHistorySource(sessions, "opencode", {
|
|
68
|
+
provider: options.provider ?? {
|
|
69
|
+
summarize: async () => {
|
|
70
|
+
throw new Error("A dry-run cannot call the capture model");
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}, options, report);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
report.unitsTotal = sessions.reduce((total, session) => total + session.units.length, 0);
|
|
77
|
+
}
|
|
78
|
+
if (!options.skipProfile) {
|
|
79
|
+
report.profile = await importProfileFromHistory(sessions, {
|
|
80
|
+
host: "opencode",
|
|
81
|
+
dryRun: options.dryRun,
|
|
82
|
+
batchSize: options.profileBatch,
|
|
83
|
+
model: options.profileModel,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return report;
|
|
87
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ImportPathMap } from "./importer.js";
|
|
2
|
+
import type { OpencodeSourceSession } from "./opencode-reader.js";
|
|
3
|
+
/** Prefer the recorded directory, then an explicit map, then the project root. */
|
|
4
|
+
export declare function resolveOpencodeProject(recordedDirectory: string, projectWorktree: string | null, maps?: ImportPathMap[]): string | null;
|
|
5
|
+
export interface UnresolvedProject {
|
|
6
|
+
directory: string;
|
|
7
|
+
sessions: number;
|
|
8
|
+
units: number;
|
|
9
|
+
}
|
|
10
|
+
/** Resolve each session without buffering its conversation windows. */
|
|
11
|
+
export declare function resolveOpencodeSessions(sessions: AsyncIterable<OpencodeSourceSession>, maps: ImportPathMap[], unresolved: Map<string, UnresolvedProject>): AsyncGenerator<OpencodeSourceSession>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
function isDirectory(path) {
|
|
3
|
+
if (!path)
|
|
4
|
+
return false;
|
|
5
|
+
try {
|
|
6
|
+
return statSync(path).isDirectory();
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Prefer the recorded directory, then an explicit map, then the project root. */
|
|
13
|
+
export function resolveOpencodeProject(recordedDirectory, projectWorktree, maps = []) {
|
|
14
|
+
if (isDirectory(recordedDirectory))
|
|
15
|
+
return recordedDirectory;
|
|
16
|
+
const mapped = maps.find((item) => item.from === recordedDirectory)?.to;
|
|
17
|
+
if (isDirectory(mapped))
|
|
18
|
+
return mapped;
|
|
19
|
+
return projectWorktree !== "/" && isDirectory(projectWorktree) ? projectWorktree : null;
|
|
20
|
+
}
|
|
21
|
+
/** Resolve each session without buffering its conversation windows. */
|
|
22
|
+
export async function* resolveOpencodeSessions(sessions, maps, unresolved) {
|
|
23
|
+
for await (const session of sessions) {
|
|
24
|
+
const directory = resolveOpencodeProject(session.recordedDirectory, session.projectWorktree, maps);
|
|
25
|
+
if (directory) {
|
|
26
|
+
yield { ...session, directory };
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const entry = unresolved.get(session.recordedDirectory) ?? {
|
|
30
|
+
directory: session.recordedDirectory,
|
|
31
|
+
sessions: 0,
|
|
32
|
+
units: 0,
|
|
33
|
+
};
|
|
34
|
+
entry.sessions++;
|
|
35
|
+
entry.units += session.units.length;
|
|
36
|
+
unresolved.set(entry.directory, entry);
|
|
37
|
+
}
|
|
38
|
+
}
|