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.
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,6 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { CONFIG } from "../../config.js";
3
+ import { resolveOpencodeHostModel } from "../../services/ai/live-model-choice.js";
3
4
  import { buildBoundedSummaryPrompt } from "../../core/capture-context.js";
5
+ import { parseCaptureSummary } from "../../core/extraction.js";
4
6
  import { log } from "../../services/logger.js";
5
7
  import { loadOpencodeProvider } from "../../services/ai/opencode-provider-loader.js";
6
8
  function buildSystemPrompt(languageName) {
@@ -26,21 +28,18 @@ CAPTURE if: code changed, bug fixed, feature added, decision made`;
26
28
  }
27
29
  export async function generateOpenCodeAutoCaptureSummary(request, notify) {
28
30
  let opencodeProviderError;
29
- if (CONFIG.opencodeProvider && CONFIG.opencodeModel) {
31
+ const hostModel = resolveOpencodeHostModel(CONFIG);
32
+ if (hostModel) {
30
33
  try {
31
- if (CONFIG.memoryModel) {
34
+ if (CONFIG.memoryModel && CONFIG.opencodeProvider) {
32
35
  log("opencodeProvider takes precedence over memoryModel for auto-capture");
33
36
  }
34
- const { isProviderConnected, getV2Client, generateStructuredOutput } = await loadOpencodeProvider();
35
- let providerID = CONFIG.opencodeProvider;
36
- let modelID = CONFIG.opencodeModel;
37
- if (modelID === "inherit") {
38
- if (!request.prompt?.providerId || !request.prompt?.modelId) {
39
- throw new Error("omms: opencodeModel is 'inherit' but no session model was recorded for this prompt");
40
- }
41
- providerID = request.prompt.providerId;
42
- modelID = request.prompt.modelId;
43
- }
37
+ const { isProviderConnected, getV2Client, generateStructuredOutput, resolveOpencodeModelRef, } = await loadOpencodeProvider();
38
+ // "inherit" (explicit, or no model configured at all) follows this prompt's session model.
39
+ const { providerID, modelID } = resolveOpencodeModelRef({
40
+ ...hostModel,
41
+ prompt: request.prompt,
42
+ });
44
43
  if (!isProviderConnected(providerID)) {
45
44
  throw new Error(`opencode provider '${providerID}' is not connected. Check your opencode provider configuration.`);
46
45
  }
@@ -139,9 +138,16 @@ export async function generateOpenCodeAutoCaptureSummary(request, notify) {
139
138
  if (!result.success || !result.data) {
140
139
  throw new Error(result.error || "Failed to generate summary");
141
140
  }
142
- return {
143
- summary: result.data.summary,
144
- type: result.data.type,
145
- tags: (result.data.tags || []).map((tag) => tag.toLowerCase().trim()),
146
- };
141
+ const rawReply = JSON.stringify(result.data);
142
+ const summary = parseCaptureSummary(rawReply);
143
+ if (!summary) {
144
+ log("OpenCode capture: model reply was not a valid capture summary", {
145
+ provider: CONFIG.memoryProvider,
146
+ modelId: CONFIG.memoryModel,
147
+ // The reply can carry conversation content, so log only its size.
148
+ replyLength: rawReply.length,
149
+ });
150
+ throw new Error("omms: OpenCode extraction returned an invalid summary payload");
151
+ }
152
+ return summary;
147
153
  }
@@ -0,0 +1,42 @@
1
+ import { type HistoryImportModels } from "../../importer/run-import.js";
2
+ /**
3
+ * OpenCode command surface for the historical-session importer:
4
+ *
5
+ * /memory-import-opencode-history --dry-run
6
+ * /memory-import-opencode-history
7
+ *
8
+ * The same options and model rules as Pi's command: this session's model by
9
+ * default, or `--model provider/id` from OpenCode's connected providers. Model
10
+ * calls go through OpenCode's own sign-in, so no omms API key is involved.
11
+ */
12
+ export declare const OPENCODE_IMPORT_COMMAND = "memory-import-opencode-history";
13
+ export declare const OPENCODE_IMPORT_DESCRIPTION = "Import historical OpenCode sessions into the shared memory store (try --dry-run first)";
14
+ export declare const OPENCODE_IMPORT_USAGE: string;
15
+ export interface OpencodeModelRef {
16
+ providerID: string;
17
+ modelID: string;
18
+ }
19
+ export interface OpencodeImportCommandInput {
20
+ argsText: string;
21
+ /** Session directory; relative paths and the default project resolve here. */
22
+ directory: string;
23
+ /** The model the invoking session currently uses, if the host can tell. */
24
+ sessionModel: () => Promise<OpencodeModelRef | null>;
25
+ notify?: (message: string) => void;
26
+ }
27
+ /** Capture and profile calls routed through OpenCode's structured-output sessions. */
28
+ export declare function createOpencodeImportModels(ref: OpencodeModelRef, directory: string): Promise<Required<HistoryImportModels>>;
29
+ /** Parse, run, and describe one import; always resolves to text for the session. */
30
+ export declare function runOpencodeImportCommand(input: OpencodeImportCommandInput): Promise<string>;
31
+ /** The model of the session's latest user message, from the V1 message list. */
32
+ export declare function latestUserMessageModel(client: {
33
+ session: {
34
+ messages: (input: {
35
+ path: {
36
+ id: string;
37
+ };
38
+ }) => Promise<{
39
+ data?: any;
40
+ }>;
41
+ };
42
+ }, sessionID: string): Promise<OpencodeModelRef | null>;
@@ -0,0 +1,154 @@
1
+ import { CONFIG, isConfigured } from "../../config.js";
2
+ import { buildBoundedSummaryPrompt } from "../../core/capture-context.js";
3
+ import { historyImportUsage, importNeedsModel, parseHistoryImportArgs, tokenizeImportArgs, } from "../../importer/import-args.js";
4
+ import { formatHistoryImportReport, summarizeHistoryImportReport, runHistoryImport, } from "../../importer/run-import.js";
5
+ import { loadOpencodeProvider } from "../../services/ai/opencode-provider-loader.js";
6
+ import { memoryClient } from "../../services/client.js";
7
+ import { log } from "../../services/logger.js";
8
+ /**
9
+ * OpenCode command surface for the historical-session importer:
10
+ *
11
+ * /memory-import-opencode-history --dry-run
12
+ * /memory-import-opencode-history
13
+ *
14
+ * The same options and model rules as Pi's command: this session's model by
15
+ * default, or `--model provider/id` from OpenCode's connected providers. Model
16
+ * calls go through OpenCode's own sign-in, so no omms API key is involved.
17
+ */
18
+ export const OPENCODE_IMPORT_COMMAND = "memory-import-opencode-history";
19
+ export const OPENCODE_IMPORT_DESCRIPTION = "Import historical OpenCode sessions into the shared memory store (try --dry-run first)";
20
+ export const OPENCODE_IMPORT_USAGE = historyImportUsage("opencode", "session");
21
+ let importCommandRunning = false;
22
+ function splitModel(value) {
23
+ const separator = value.indexOf("/");
24
+ return { providerID: value.slice(0, separator), modelID: value.slice(separator + 1) };
25
+ }
26
+ /** Capture and profile calls routed through OpenCode's structured-output sessions. */
27
+ export async function createOpencodeImportModels(ref, directory) {
28
+ const { getV2Client, generateStructuredOutput } = await loadOpencodeProvider();
29
+ const client = getV2Client();
30
+ if (!client)
31
+ throw new Error("the OpenCode client is not ready; retry in a moment");
32
+ const { z } = await import("zod");
33
+ const { buildCaptureSystemPrompt, createUserProfileAnalysisSchema, parseCaptureSummary } = await import("../../core/extraction.js");
34
+ const { detectLanguage, getLanguageName } = await import("../../services/language-detector.js");
35
+ const captureSchema = z.object({
36
+ summary: z.string().optional(),
37
+ type: z.string(),
38
+ tags: z.array(z.string()).optional(),
39
+ });
40
+ const capture = {
41
+ async summarize(request) {
42
+ const target = CONFIG.autoCaptureLanguage && CONFIG.autoCaptureLanguage !== "auto"
43
+ ? CONFIG.autoCaptureLanguage
44
+ : detectLanguage(request.userPrompt);
45
+ const systemPrompt = buildCaptureSystemPrompt(getLanguageName(target));
46
+ const result = await generateStructuredOutput({
47
+ client,
48
+ ...ref,
49
+ systemPrompt,
50
+ userPrompt: buildBoundedSummaryPrompt(request.context, systemPrompt, z.toJSONSchema(captureSchema)),
51
+ schema: captureSchema,
52
+ directory,
53
+ });
54
+ const parsed = parseCaptureSummary(JSON.stringify(result));
55
+ if (!parsed)
56
+ throw new Error("History capture returned an invalid summary");
57
+ return parsed;
58
+ },
59
+ };
60
+ const profileSchema = createUserProfileAnalysisSchema(z);
61
+ const profile = {
62
+ provider: ref.providerID,
63
+ modelId: ref.modelID,
64
+ async complete(systemPrompt, userPrompt) {
65
+ const result = await generateStructuredOutput({
66
+ client,
67
+ ...ref,
68
+ systemPrompt,
69
+ userPrompt,
70
+ schema: profileSchema,
71
+ directory,
72
+ });
73
+ return JSON.stringify(result);
74
+ },
75
+ };
76
+ return { capture, profile };
77
+ }
78
+ /** Parse, run, and describe one import; always resolves to text for the session. */
79
+ export async function runOpencodeImportCommand(input) {
80
+ const name = OPENCODE_IMPORT_COMMAND;
81
+ // Some hosts pass the command name through with its arguments.
82
+ const argsText = input.argsText.trim().replace(new RegExp(`^/?${name}(\\s|$)`), "");
83
+ const parsed = parseHistoryImportArgs(tokenizeImportArgs(argsText), {
84
+ host: "opencode",
85
+ surface: "session",
86
+ });
87
+ if (parsed.help)
88
+ return OPENCODE_IMPORT_USAGE;
89
+ if (parsed.errors.length > 0)
90
+ return `${name}: ${parsed.errors.join("; ")}`;
91
+ if (!isConfigured())
92
+ return `${name}: memory system not configured`;
93
+ if (importCommandRunning)
94
+ return `${name}: an import is already running`;
95
+ importCommandRunning = true;
96
+ try {
97
+ let ref = null;
98
+ if (parsed.model || importNeedsModel(parsed)) {
99
+ ref = parsed.model ? splitModel(parsed.model) : await input.sessionModel();
100
+ if (!ref)
101
+ return `${name}: this session has no model yet; pass --model provider/id`;
102
+ const { isProviderConnected } = await loadOpencodeProvider();
103
+ if (!isProviderConnected(ref.providerID)) {
104
+ return `${name}: OpenCode provider "${ref.providerID}" is not connected`;
105
+ }
106
+ }
107
+ const models = ref && !parsed.dryRun ? await createOpencodeImportModels(ref, input.directory) : {};
108
+ if (!parsed.dryRun) {
109
+ try {
110
+ await memoryClient.warmup();
111
+ }
112
+ catch (error) {
113
+ log("OpenCode import: memory warmup failed", { error: String(error) });
114
+ }
115
+ }
116
+ let lastProgress = 0;
117
+ const report = await runHistoryImport("opencode", parsed, {
118
+ cwd: input.directory,
119
+ models,
120
+ onProgress: (processed, total, promptPreview) => {
121
+ if (processed - lastProgress >= 25 || processed === total) {
122
+ lastProgress = processed;
123
+ input.notify?.(`OpenCode import: ${processed}/${total} units (${promptPreview})`);
124
+ }
125
+ },
126
+ });
127
+ log("OpenCode history import report", summarizeHistoryImportReport(report));
128
+ return formatHistoryImportReport("opencode", report, ref ? `${ref.providerID}/${ref.modelID}` : undefined);
129
+ }
130
+ catch (error) {
131
+ const message = error instanceof Error ? error.message : String(error);
132
+ log("OpenCode history import failed", { error: message });
133
+ return `${name} failed: ${message}`;
134
+ }
135
+ finally {
136
+ importCommandRunning = false;
137
+ }
138
+ }
139
+ /** The model of the session's latest user message, from the V1 message list. */
140
+ export async function latestUserMessageModel(client, sessionID) {
141
+ try {
142
+ const messages = (await client.session.messages({ path: { id: sessionID } })).data ?? [];
143
+ for (let i = messages.length - 1; i >= 0; i--) {
144
+ const model = messages[i]?.info?.role === "user" ? messages[i].info.model : undefined;
145
+ if (model?.providerID && model?.modelID) {
146
+ return { providerID: model.providerID, modelID: model.modelID };
147
+ }
148
+ }
149
+ }
150
+ catch (error) {
151
+ log("OpenCode import: could not read the session model", { error: String(error) });
152
+ }
153
+ return null;
154
+ }
@@ -5,8 +5,7 @@ import { getLanguageName } from "../../services/language-detector.js";
5
5
  import { log } from "../../services/logger.js";
6
6
  import { memoryClient } from "../../services/client.js";
7
7
  import { capturePiSettledWorkUnit, createPiCaptureState } from "./capture.js";
8
- import { createPiCaptureProvider } from "./provider.js";
9
- import { resolveModelFromContext } from "./provider.js";
8
+ import { createPiLiveModels } from "./live-model.js";
10
9
  import { registerPiHistoryImportCommand } from "./import-command.js";
11
10
  import { performPiProfileLearning } from "./profile.js";
12
11
  import { buildRetrievalSection, wrapRetrievalSection } from "../../core/retrieval.js";
@@ -93,7 +92,6 @@ export default function ommsPiExtension(pi) {
93
92
  const level = notification.variant === "success" ? "info" : notification.variant;
94
93
  ctx.ui.notify(`${notification.title}: ${notification.message}`, level);
95
94
  };
96
- const resolveModel = (ctx) => resolveModelFromContext(ctx);
97
95
  pi.on("session_start", async (_event, ctx) => {
98
96
  const status = startStatus(ctx, "warming");
99
97
  try {
@@ -148,7 +146,8 @@ export default function ommsPiExtension(pi) {
148
146
  const status = startStatus(ctx, "capturing");
149
147
  try {
150
148
  const entries = toSessionEntries(ctx.sessionManager.getBranch());
151
- const provider = createPiCaptureProvider(() => resolveModel(ctx));
149
+ const liveModels = createPiLiveModels(ctx, notify(ctx));
150
+ const provider = liveModels.capture;
152
151
  const captureResult = await capturePiSettledWorkUnit({
153
152
  sessionId: ctx.sessionManager.getSessionId(),
154
153
  directory: ctx.cwd,
@@ -172,7 +171,7 @@ export default function ommsPiExtension(pi) {
172
171
  await performPiProfileLearning({
173
172
  directory: ctx.cwd,
174
173
  prompts: batch,
175
- resolveModel: () => resolveModel(ctx),
174
+ resolveProfileModel: liveModels.profile,
176
175
  notify: notify(ctx),
177
176
  });
178
177
  }
@@ -1,29 +1,17 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { type ImportPathMap } from "../../importer/importer.js";
2
+ import { type HistoryImportArgs } from "../../importer/import-args.js";
3
3
  /**
4
4
  * Pi command surface for the historical-session importer:
5
5
  *
6
6
  * /memory-import-pi-history --dry-run
7
7
  * /memory-import-pi-history
8
8
  *
9
- * Runs over the shared importer service. Extraction inherits the active Pi
10
- * model (or the piProvider/piModel override). Source session files are only
11
- * ever read.
9
+ * Runs over the shared importer service with the same options as the OpenCode
10
+ * command and the CLI. Extraction uses this session's current model, or
11
+ * `--model provider/id` from Pi's signed-in models. Source session files are
12
+ * only ever read.
12
13
  */
13
14
  export declare const PI_IMPORT_COMMAND = "memory-import-pi-history";
14
- export declare const PI_IMPORT_USAGE = "Usage: /memory-import-pi-history [options]\n\nOptions:\n --dry-run Discover, map, and report; write nothing\n --force Reprocess units with terminal ledger states\n --scope=current-project Import only sessions from this project (default)\n --scope=all-projects Import every discovered session\n --session=<id-or-file> Import one exact session\n --since=<date> Only work units at/after this time\n --until=<date> Only work units at/before this time\n --max-sessions=<n> Limit discovery to the oldest n sessions\n --map=<oldPath>=<newPath> Remap a recorded cwd that no longer exists\n --root=<dir> Session root (default ~/.pi/agent/sessions)\n\nDates accept ISO 8601 (2026-01-01, 2026-01-01T10:00:00Z) or epoch\nmilliseconds. --since/--until filter on each work unit's user-entry\ntimestamp, inclusive. Unresolvable recorded cwds are skipped and listed\nin the report; use --map to import them into an existing directory.";
15
- export interface ParsedImportArgs {
16
- dryRun: boolean;
17
- force: boolean;
18
- scope: "current-project" | "all-projects";
19
- session?: string;
20
- since?: number;
21
- until?: number;
22
- maxSessions?: number;
23
- root?: string;
24
- pathMaps: ImportPathMap[];
25
- help: boolean;
26
- errors: string[];
27
- }
28
- export declare function parseImportArgs(raw: string): ParsedImportArgs;
15
+ export declare const PI_IMPORT_USAGE: string;
16
+ export declare function parseImportArgs(raw: string): HistoryImportArgs;
29
17
  export declare function registerPiHistoryImportCommand(pi: ExtensionAPI, getExtensionContext: () => ExtensionContext | null): void;
@@ -1,149 +1,25 @@
1
- import { initConfigWithLegacyMigration, isConfigured } from "../../config.js";
1
+ import { initConfig, initConfigWithLegacyMigration, isConfigured } from "../../config.js";
2
2
  import { log } from "../../services/logger.js";
3
3
  import { memoryClient } from "../../services/client.js";
4
- import { importPiHistory, } from "../../importer/importer.js";
5
- import { loadPiSessionForImport } from "../../importer/session-loader.js";
6
- import { createPiCaptureProvider, resolveModelFromContext } from "./provider.js";
4
+ import { historyImportUsage, importNeedsModel, parseHistoryImportArgs, tokenizeImportArgs, } from "../../importer/import-args.js";
5
+ import { formatHistoryImportReport, summarizeHistoryImportReport, runHistoryImport, } from "../../importer/run-import.js";
6
+ import { createPiCaptureProvider, resolveImportModel } from "./provider.js";
7
+ import { adaptPiProfileModel } from "./profile.js";
7
8
  /**
8
9
  * Pi command surface for the historical-session importer:
9
10
  *
10
11
  * /memory-import-pi-history --dry-run
11
12
  * /memory-import-pi-history
12
13
  *
13
- * Runs over the shared importer service. Extraction inherits the active Pi
14
- * model (or the piProvider/piModel override). Source session files are only
15
- * ever read.
14
+ * Runs over the shared importer service with the same options as the OpenCode
15
+ * command and the CLI. Extraction uses this session's current model, or
16
+ * `--model provider/id` from Pi's signed-in models. Source session files are
17
+ * only ever read.
16
18
  */
17
19
  export const PI_IMPORT_COMMAND = "memory-import-pi-history";
18
- export const PI_IMPORT_USAGE = `Usage: /${PI_IMPORT_COMMAND} [options]
19
-
20
- Options:
21
- --dry-run Discover, map, and report; write nothing
22
- --force Reprocess units with terminal ledger states
23
- --scope=current-project Import only sessions from this project (default)
24
- --scope=all-projects Import every discovered session
25
- --session=<id-or-file> Import one exact session
26
- --since=<date> Only work units at/after this time
27
- --until=<date> Only work units at/before this time
28
- --max-sessions=<n> Limit discovery to the oldest n sessions
29
- --map=<oldPath>=<newPath> Remap a recorded cwd that no longer exists
30
- --root=<dir> Session root (default ~/.pi/agent/sessions)
31
-
32
- Dates accept ISO 8601 (2026-01-01, 2026-01-01T10:00:00Z) or epoch
33
- milliseconds. --since/--until filter on each work unit's user-entry
34
- timestamp, inclusive. Unresolvable recorded cwds are skipped and listed
35
- in the report; use --map to import them into an existing directory.`;
36
- function parseTimestamp(raw) {
37
- if (!raw)
38
- return undefined;
39
- if (/^\d+$/.test(raw))
40
- return Number(raw);
41
- const parsed = Date.parse(raw);
42
- return Number.isNaN(parsed) ? undefined : parsed;
43
- }
20
+ export const PI_IMPORT_USAGE = historyImportUsage("pi", "session");
44
21
  export function parseImportArgs(raw) {
45
- const result = {
46
- dryRun: false,
47
- force: false,
48
- scope: "current-project",
49
- pathMaps: [],
50
- help: false,
51
- errors: [],
52
- };
53
- for (const token of raw.trim().split(/\s+/).filter(Boolean)) {
54
- if (token === "--help" || token === "-h") {
55
- result.help = true;
56
- continue;
57
- }
58
- if (token === "--dry-run") {
59
- result.dryRun = true;
60
- continue;
61
- }
62
- if (token === "--force") {
63
- result.force = true;
64
- continue;
65
- }
66
- const equals = token.indexOf("=");
67
- const flag = equals === -1 ? token : token.slice(0, equals);
68
- const value = equals === -1 ? "" : token.slice(equals + 1);
69
- switch (flag) {
70
- case "--scope":
71
- if (value === "current-project" || value === "all-projects") {
72
- result.scope = value;
73
- }
74
- else {
75
- result.errors.push(`--scope must be current-project or all-projects, got "${value}"`);
76
- }
77
- break;
78
- case "--session":
79
- if (value)
80
- result.session = value;
81
- else
82
- result.errors.push("--session requires a value");
83
- break;
84
- case "--since":
85
- result.since = parseTimestamp(value);
86
- if (result.since === undefined)
87
- result.errors.push(`--since is not a valid date: "${value}"`);
88
- break;
89
- case "--until":
90
- result.until = parseTimestamp(value);
91
- if (result.until === undefined)
92
- result.errors.push(`--until is not a valid date: "${value}"`);
93
- break;
94
- case "--max-sessions": {
95
- const parsed = Number(value);
96
- if (Number.isInteger(parsed) && parsed >= 0)
97
- result.maxSessions = parsed;
98
- else
99
- result.errors.push(`--max-sessions must be a non-negative integer, got "${value}"`);
100
- break;
101
- }
102
- case "--root":
103
- if (value)
104
- result.root = value;
105
- else
106
- result.errors.push("--root requires a value");
107
- break;
108
- case "--map": {
109
- const separator = value.indexOf("=");
110
- if (separator > 0 && separator < value.length - 1) {
111
- result.pathMaps.push({
112
- from: value.slice(0, separator),
113
- to: value.slice(separator + 1),
114
- });
115
- }
116
- else {
117
- result.errors.push(`--map must be <oldPath>=<newPath>, got "${value}"`);
118
- }
119
- break;
120
- }
121
- default:
122
- result.errors.push(`Unknown option: "${token}"`);
123
- }
124
- }
125
- return result;
126
- }
127
- function formatReport(report) {
128
- const lines = [
129
- `Pi history import ${report.dryRun ? "(dry-run)" : ""}`.trim(),
130
- ` sessions: ${report.sessionsLoaded} loaded, ${report.sessionsFilteredOut} filtered, ` +
131
- `${report.sessionsUnrecognized} unrecognized, ` +
132
- `${report.unresolvableSessions.length} unresolvable cwd`,
133
- ` work units: ${report.unitsTotal} total, ` +
134
- (report.dryRun
135
- ? `${report.unitsWouldImport} would import`
136
- : `${report.unitsImported} imported, ${report.unitsSkipped} skipped, ` +
137
- `${report.unitsFailed} failed`) +
138
- `, ${report.unitsAlreadyHandled} already handled`,
139
- ];
140
- for (const project of report.projects) {
141
- lines.push(` project ${project.tag} (${project.sessions} sessions, ${project.units} units) -> ${project.directory}`);
142
- }
143
- if (report.loadErrors.length > 0) {
144
- lines.push(` load errors: ${report.loadErrors.length}`);
145
- }
146
- return lines.join("\n");
22
+ return parseHistoryImportArgs(tokenizeImportArgs(raw), { host: "pi", surface: "session" });
147
23
  }
148
24
  let importCommandRunning = false;
149
25
  export function registerPiHistoryImportCommand(pi, getExtensionContext) {
@@ -177,47 +53,56 @@ export function registerPiHistoryImportCommand(pi, getExtensionContext) {
177
53
  notify("memory-import-pi-history: an import is already running");
178
54
  return;
179
55
  }
180
- // Import against the current session's project and configuration.
181
- initConfigWithLegacyMigration(ctx.cwd);
56
+ // A dry-run must not trigger the one-time storage migration.
57
+ if (parsed.dryRun)
58
+ initConfig(ctx.cwd);
59
+ else
60
+ initConfigWithLegacyMigration(ctx.cwd);
182
61
  if (typeof commandCtx.waitForIdle === "function") {
183
62
  await commandCtx.waitForIdle();
184
63
  }
64
+ // The command context carries the live session model; fall back to the
65
+ // last session context for hosts whose command context lacks a registry.
66
+ const modelCtx = commandCtx?.modelRegistry ? commandCtx : ctx;
67
+ const selectedModel = parsed.model || importNeedsModel(parsed)
68
+ ? resolveImportModel(modelCtx, parsed.model)
69
+ : null;
70
+ if ((parsed.model || importNeedsModel(parsed)) && !selectedModel) {
71
+ notify(parsed.model
72
+ ? `memory-import-pi-history: model not found: ${parsed.model}`
73
+ : "memory-import-pi-history: this session has no model; pass --model provider/id");
74
+ return;
75
+ }
185
76
  importCommandRunning = true;
186
77
  try {
187
78
  // Best-effort warmup so failures surface as per-unit errors instead of
188
79
  // every unit failing on embedding initialisation.
189
- try {
190
- await memoryClient.warmup();
191
- }
192
- catch (error) {
193
- log("Pi import: memory warmup failed", { error: String(error) });
80
+ if (!parsed.dryRun) {
81
+ try {
82
+ await memoryClient.warmup();
83
+ }
84
+ catch (error) {
85
+ log("Pi import: memory warmup failed", { error: String(error) });
86
+ }
194
87
  }
195
- const filters = {
196
- scope: parsed.scope,
197
- currentDirectory: ctx.cwd,
198
- ...(parsed.session !== undefined ? { session: parsed.session } : {}),
199
- ...(parsed.since !== undefined ? { since: parsed.since } : {}),
200
- ...(parsed.until !== undefined ? { until: parsed.until } : {}),
201
- ...(parsed.maxSessions !== undefined ? { maxSessions: parsed.maxSessions } : {}),
202
- ...(parsed.root !== undefined ? { root: parsed.root } : {}),
203
- ...(parsed.force ? { force: true } : {}),
204
- ...(parsed.dryRun ? { dryRun: true } : {}),
205
- pathMaps: parsed.pathMaps,
206
- };
207
- const provider = createPiCaptureProvider(() => resolveModelFromContext(ctx));
208
88
  let lastProgressNotify = 0;
209
- const report = await importPiHistory({
210
- loadSession: loadPiSessionForImport,
211
- provider,
89
+ const report = await runHistoryImport("pi", parsed, {
90
+ cwd: ctx.cwd,
91
+ models: selectedModel && !parsed.dryRun
92
+ ? {
93
+ capture: createPiCaptureProvider(() => selectedModel),
94
+ profile: adaptPiProfileModel(selectedModel),
95
+ }
96
+ : {},
212
97
  onProgress: (processed, total, promptPreview) => {
213
98
  if (processed - lastProgressNotify >= 25 || processed === total) {
214
99
  lastProgressNotify = processed;
215
100
  notify(`Pi import: ${processed}/${total} units (${promptPreview})`);
216
101
  }
217
102
  },
218
- }, filters);
219
- log("Pi history import report", { report });
220
- notify(formatReport(report));
103
+ });
104
+ log("Pi history import report", summarizeHistoryImportReport(report));
105
+ notify(formatHistoryImportReport("pi", report, selectedModel ? `${selectedModel.provider}/${selectedModel.modelId}` : undefined));
221
106
  }
222
107
  catch (error) {
223
108
  const message = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,17 @@
1
+ import type { AutoCaptureNotification, CaptureSummaryProvider } from "../../core/host.js";
2
+ import type { ModelPort } from "../../core/profile-analysis.js";
3
+ import { type PiModelContext } from "./provider.js";
4
+ /**
5
+ * Pi live capture and profile learning, by the same rule as OpenCode:
6
+ * `piProvider`/`piModel` ("inherit" = this session's model), else the
7
+ * external API (`memoryModel`/`memoryApiUrl`/`memoryApiKey`), else this
8
+ * session's model. A failing Pi model falls back to the external API when it
9
+ * is configured, as a failing OpenCode model does.
10
+ */
11
+ export interface PiLiveModels {
12
+ capture: CaptureSummaryProvider;
13
+ profile: () => ModelPort | null;
14
+ }
15
+ type Notify = (notification: AutoCaptureNotification) => Promise<void> | void;
16
+ export declare function createPiLiveModels(ctx: PiModelContext, notify?: Notify): PiLiveModels;
17
+ export {};