om-memory-system 3.0.0 → 3.1.0-next.4

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 (52) hide show
  1. package/{LICENSE → LICENSE.md} +4 -2
  2. package/README.md +108 -532
  3. package/dist/adapters/opencode/auto-capture-summary.js +23 -17
  4. package/dist/adapters/opencode/import-command.d.ts +42 -0
  5. package/dist/adapters/opencode/import-command.js +154 -0
  6. package/dist/adapters/pi/extension.js +4 -5
  7. package/dist/adapters/pi/import-command.d.ts +7 -19
  8. package/dist/adapters/pi/import-command.js +46 -161
  9. package/dist/adapters/pi/live-model.d.ts +17 -0
  10. package/dist/adapters/pi/live-model.js +86 -0
  11. package/dist/adapters/pi/profile.d.ts +7 -17
  12. package/dist/adapters/pi/profile.js +28 -66
  13. package/dist/adapters/pi/provider.d.ts +8 -1
  14. package/dist/adapters/pi/provider.js +34 -6
  15. package/dist/cli/index.d.ts +7 -0
  16. package/dist/cli/index.js +94 -0
  17. package/dist/config.d.ts +7 -20
  18. package/dist/config.js +24 -61
  19. package/dist/core/extraction.d.ts +2 -2
  20. package/dist/core/extraction.js +8 -3
  21. package/dist/core/profile-analysis.d.ts +16 -0
  22. package/dist/core/profile-analysis.js +47 -0
  23. package/dist/importer/import-args.d.ts +44 -0
  24. package/dist/importer/import-args.js +219 -0
  25. package/dist/importer/importer.d.ts +33 -9
  26. package/dist/importer/importer.js +153 -89
  27. package/dist/importer/ledger.d.ts +9 -2
  28. package/dist/importer/ledger.js +16 -2
  29. package/dist/importer/model-selection.d.ts +16 -0
  30. package/dist/importer/model-selection.js +84 -0
  31. package/dist/importer/opencode-import.d.ts +28 -0
  32. package/dist/importer/opencode-import.js +87 -0
  33. package/dist/importer/opencode-project.d.ts +11 -0
  34. package/dist/importer/opencode-project.js +38 -0
  35. package/dist/importer/opencode-reader.d.ts +19 -0
  36. package/dist/importer/opencode-reader.js +214 -0
  37. package/dist/importer/profile-import.d.ts +29 -0
  38. package/dist/importer/profile-import.js +111 -0
  39. package/dist/importer/run-import.d.ts +49 -0
  40. package/dist/importer/run-import.js +123 -0
  41. package/dist/index.d.ts +4 -0
  42. package/dist/index.js +37 -1
  43. package/dist/services/ai/internal-capture-sessions.d.ts +2 -0
  44. package/dist/services/ai/internal-capture-sessions.js +7 -0
  45. package/dist/services/ai/live-model-choice.d.ts +52 -0
  46. package/dist/services/ai/live-model-choice.js +83 -0
  47. package/dist/services/ai/profile-llm-client.js +7 -5
  48. package/dist/services/user-memory-learning.js +9 -8
  49. package/dist/services/user-profile/ai-cleanup.js +8 -5
  50. package/dist/services/user-profile/user-profile-manager.js +11 -10
  51. package/dist/v2/adapter.js +27 -0
  52. 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 `pi:${sessionId}:${window.userEntryId}:${terminalEntryId}`;
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
- export async function importPiHistory(deps, filters) {
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
- if (ledgerAccessible) {
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 discovered of discovery.sessions) {
106
- if (filters.session) {
107
- const wanted = filters.session;
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
- const key = buildImportKey(session.sessionId, window);
153
- if (!key)
100
+ if (!buildImportKey(session.sessionId, window, host))
154
101
  continue;
155
- candidates.push({ session, directory, tag: tags.project.tag, hash, window });
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 { session, directory, hash, window } = candidate;
164
- const key = buildImportKey(session.sessionId, window);
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: session.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 ? await ledger.get(key) : null;
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: session.sessionId,
206
- sourceFile: session.sourceFile,
155
+ sessionId: sessionId,
156
+ sourceFile: sourceFile,
207
157
  projectHash: hash,
208
158
  });
209
159
  try {
210
160
  const result = await captureConversation({
211
- host: "pi",
212
- hostSessionId: session.sessionId,
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: session.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 the Pi historical-session import.
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 PiImportLedger {
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 };
@@ -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 the Pi historical-session import.
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 PiImportLedger {
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
+ }