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
@@ -0,0 +1,86 @@
1
+ import { CONFIG } from "../../config.js";
2
+ import { resolvePiLiveModel, isExternalModelReady } from "../../services/ai/live-model-choice.js";
3
+ import { log } from "../../services/logger.js";
4
+ import { adaptPiProfileModel } from "./profile.js";
5
+ import { createPiCaptureProvider, resolveImportModel, resolveModelFromContext, } from "./provider.js";
6
+ async function externalModels() {
7
+ const { selectImportModel } = await import("../../importer/model-selection.js");
8
+ return selectImportModel({});
9
+ }
10
+ function primaryPiModel(ctx) {
11
+ const choice = resolvePiLiveModel(CONFIG);
12
+ switch (choice.kind) {
13
+ case "manual":
14
+ return "external";
15
+ case "session":
16
+ return resolveImportModel(ctx);
17
+ case "pi":
18
+ return resolveModelFromContext(ctx, `${choice.provider}/${choice.model}`);
19
+ case "unready":
20
+ throw new Error(`omms: live capture is not configured: ${choice.issues.join("; ")}`);
21
+ }
22
+ }
23
+ function describeMissing() {
24
+ const choice = resolvePiLiveModel(CONFIG);
25
+ return choice.kind === "pi"
26
+ ? `Pi model ${choice.provider}/${choice.model} was not found`
27
+ : "this Pi session has no model";
28
+ }
29
+ /** Run the Pi model, switching to the external API when it fails and one is configured. */
30
+ async function withFallback(primary, run, external, notify) {
31
+ let failure = primary ? null : new Error(`omms: ${describeMissing()}`);
32
+ if (primary) {
33
+ try {
34
+ return await run(primary);
35
+ }
36
+ catch (error) {
37
+ failure = error;
38
+ }
39
+ }
40
+ if (!isExternalModelReady(CONFIG))
41
+ throw failure;
42
+ const reason = failure instanceof Error ? failure.message : String(failure);
43
+ log("Pi live model failed, falling back to external API", { error: reason });
44
+ await Promise.resolve(notify?.({
45
+ title: "Using fallback provider",
46
+ message: `Pi model failed (${reason.length > 100 ? `${reason.slice(0, 100)}...` : reason}); using configured fallback.`,
47
+ variant: "warning",
48
+ duration: 5000,
49
+ })).catch(() => { });
50
+ return external();
51
+ }
52
+ export function createPiLiveModels(ctx, notify) {
53
+ const capture = {
54
+ async summarize(request) {
55
+ const primary = primaryPiModel(ctx);
56
+ if (primary === "external")
57
+ return (await externalModels()).capture.summarize(request);
58
+ return withFallback(primary, (model) => createPiCaptureProvider(() => model).summarize(request), async () => (await externalModels()).capture.summarize(request), notify);
59
+ },
60
+ };
61
+ const profile = () => {
62
+ let primary;
63
+ try {
64
+ primary = primaryPiModel(ctx);
65
+ }
66
+ catch (error) {
67
+ log("Pi profile learning: no live model", { error: String(error) });
68
+ return null;
69
+ }
70
+ if (primary === "external") {
71
+ return {
72
+ provider: "external",
73
+ modelId: "",
74
+ complete: async (system, prompt) => (await externalModels()).profile.complete(system, prompt),
75
+ };
76
+ }
77
+ if (!primary && !isExternalModelReady(CONFIG))
78
+ return null;
79
+ return {
80
+ provider: primary?.provider ?? "external",
81
+ modelId: primary?.modelId ?? "",
82
+ complete: (system, prompt) => withFallback(primary, (model) => adaptPiProfileModel(model).complete(system, prompt), async () => (await externalModels()).profile.complete(system, prompt), notify),
83
+ };
84
+ };
85
+ return { capture, profile };
86
+ }
@@ -1,28 +1,18 @@
1
- import type { UserProfileData } from "../../services/user-profile/types.js";
1
+ import { type ExistingProfileInput, type ProfileAnalysisResult, type ModelPort } from "../../core/profile-analysis.js";
2
2
  import type { PiModelHandle } from "./provider.js";
3
- export interface ExistingProfileInput {
4
- id: string;
5
- profileData: string;
6
- }
7
- export interface ProfileAnalysisResult {
8
- raw: UserProfileData;
9
- merged: UserProfileData | null;
10
- }
3
+ export type { ExistingProfileInput, ProfileAnalysisResult } from "../../core/profile-analysis.js";
11
4
  export interface PiProfileAnalyzer {
12
5
  analyzeProfile(context: string, existingProfile: ExistingProfileInput | null): Promise<ProfileAnalysisResult | null>;
13
6
  }
14
- /**
15
- * Bridge the shared user-profile analysis onto Pi's model runtime. Uses the
16
- * same schema and system prompt as the OpenCode structured-output path, so
17
- * analysis results are validated with the shared schema before they reach the
18
- * profile storage layer. Model failures throw (mirroring provider semantics);
19
- * a missing model returns null so callers can skip without disabling anything.
20
- */
7
+ export declare function adaptPiProfileModel(model: PiModelHandle): ModelPort;
8
+ /** Adapt Pi's model registry to the shared profile analyser. */
21
9
  export declare function createPiProfileAnalyzer(resolveModel: () => PiModelHandle | null, timeoutMs?: number): PiProfileAnalyzer;
22
10
  export interface PiProfileLearningInput {
23
11
  directory: string;
24
12
  prompts: string[];
25
- resolveModel: () => PiModelHandle | null;
13
+ resolveModel?: () => PiModelHandle | null;
14
+ /** Live model with its external-API fallback; takes precedence over resolveModel. */
15
+ resolveProfileModel?: () => ModelPort | null;
26
16
  notify?: (notification: {
27
17
  title: string;
28
18
  message: string;
@@ -1,25 +1,7 @@
1
- import { z } from "zod";
2
1
  import { CONFIG } from "../../config.js";
3
- import { createUserProfileAnalysisSchema, extractJsonObject } from "../../core/extraction.js";
2
+ import { analyzeProfile, } from "../../core/profile-analysis.js";
4
3
  import { log } from "../../services/logger.js";
5
4
  import { getTags } from "../../services/tags.js";
6
- /**
7
- * System prompt mirrored from the OpenCode profile-learning path
8
- * (src/services/user-memory-learning.ts, analyzeUserProfile). Kept local so
9
- * the Pi adapter does not import an OpenCode-coupled module; update both when
10
- * the analysis contract changes.
11
- */
12
- function buildProfileAnalysisSystemPrompt(existingProfile) {
13
- return `You are a user behavior analyst for a coding assistant.
14
-
15
- Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
16
-
17
- CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts.
18
-
19
- CRITICAL: All JSON string values MUST escape double quotes with backslash. Do NOT use unescaped quotation marks inside string values.
20
-
21
- Respond with a single JSON object matching the update_user_profile contract.`;
22
- }
23
5
  function replyText(reply) {
24
6
  return reply.content
25
7
  .filter((block) => block.type === "text" && typeof block.text === "string")
@@ -27,58 +9,30 @@ function replyText(reply) {
27
9
  .join("\n")
28
10
  .trim();
29
11
  }
30
- /**
31
- * Bridge the shared user-profile analysis onto Pi's model runtime. Uses the
32
- * same schema and system prompt as the OpenCode structured-output path, so
33
- * analysis results are validated with the shared schema before they reach the
34
- * profile storage layer. Model failures throw (mirroring provider semantics);
35
- * a missing model returns null so callers can skip without disabling anything.
36
- */
12
+ export function adaptPiProfileModel(model) {
13
+ return {
14
+ provider: model.provider,
15
+ modelId: model.modelId,
16
+ async complete(systemPrompt, userPrompt) {
17
+ const reply = await model.complete({
18
+ systemPrompt,
19
+ messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }],
20
+ });
21
+ if (reply.stopReason === "error") {
22
+ throw new Error(`pi profile analysis: model call failed: ${reply.errorMessage || "unknown error"}`);
23
+ }
24
+ return replyText(reply);
25
+ },
26
+ };
27
+ }
28
+ /** Adapt Pi's model registry to the shared profile analyser. */
37
29
  export function createPiProfileAnalyzer(resolveModel, timeoutMs = 120000) {
38
30
  return {
39
31
  async analyzeProfile(context, existingProfile) {
40
32
  const model = resolveModel();
41
33
  if (!model)
42
34
  return null;
43
- // Unref'd and cleared so the losing timeout timer never keeps the host
44
- // process alive after the race settles.
45
- let timeoutId;
46
- try {
47
- const reply = await Promise.race([
48
- model.complete({
49
- systemPrompt: buildProfileAnalysisSystemPrompt(Boolean(existingProfile)),
50
- messages: [{ role: "user", content: context, timestamp: Date.now() }],
51
- }),
52
- new Promise((_, reject) => {
53
- timeoutId = setTimeout(() => reject(new Error("pi profile analysis: timeout")), timeoutMs);
54
- timeoutId?.unref?.();
55
- }),
56
- ]);
57
- if (reply.stopReason === "error") {
58
- throw new Error(`pi profile analysis: model call failed: ${reply.errorMessage || "unknown error"}`);
59
- }
60
- const schema = createUserProfileAnalysisSchema(z);
61
- const parsed = schema.safeParse(extractJsonObject(replyText(reply)));
62
- if (!parsed.success) {
63
- log("pi profile analysis: model reply failed shared schema validation", {
64
- provider: model.provider,
65
- modelId: model.modelId,
66
- });
67
- throw new Error("pi profile analysis: invalid profile payload");
68
- }
69
- const rawData = parsed.data;
70
- if (existingProfile) {
71
- const { userProfileManager } = await import("../../services/user-profile/user-profile-manager.js");
72
- const existingData = JSON.parse(existingProfile.profileData);
73
- const merged = await userProfileManager.mergeProfileData(existingData, rawData, undefined, existingProfile.id);
74
- return { raw: rawData, merged };
75
- }
76
- return { raw: rawData, merged: null };
77
- }
78
- finally {
79
- if (timeoutId)
80
- clearTimeout(timeoutId);
81
- }
35
+ return analyzeProfile(adaptPiProfileModel(model), context, existingProfile, timeoutMs);
82
36
  },
83
37
  };
84
38
  }
@@ -103,7 +57,15 @@ export async function performPiProfileLearning(input) {
103
57
  const userId = tags.user.userEmail;
104
58
  const { userProfileManager } = await import("../../services/user-profile/user-profile-manager.js");
105
59
  const existing = await userProfileManager.getActiveProfile(userId);
106
- const analyzer = createPiProfileAnalyzer(input.resolveModel);
60
+ const resolveProfileModel = input.resolveProfileModel;
61
+ const analyzer = resolveProfileModel
62
+ ? {
63
+ async analyzeProfile(context, existingProfile) {
64
+ const model = resolveProfileModel();
65
+ return model ? analyzeProfile(model, context, existingProfile) : null;
66
+ },
67
+ }
68
+ : createPiProfileAnalyzer(input.resolveModel ?? (() => null));
107
69
  const context = input.prompts.map((prompt, i) => `${i + 1}. ${prompt}`).join("\n");
108
70
  const analysis = await analyzer.analyzeProfile(context, existing ? { id: existing.id, profileData: existing.profileData } : null);
109
71
  if (!analysis) {
@@ -10,8 +10,15 @@ export interface PiModelContext {
10
10
  /**
11
11
  * Active-model-first resolution: the explicit `piProvider`/`piModel`
12
12
  * configuration when both are set, otherwise the active `ctx.model`.
13
+ * A supplied import override resolves strictly through Pi's model registry.
13
14
  */
14
- export declare function resolveModelFromContext(ctx: PiModelContext): PiModelHandle | null;
15
+ export declare function resolveModelFromContext(ctx: PiModelContext, override?: string): PiModelHandle | null;
16
+ /**
17
+ * History-import resolution: an explicit `provider/id` through Pi's model
18
+ * registry, otherwise the session's current model. `piProvider`/`piModel`
19
+ * steer live capture only; an import follows the session the user is in.
20
+ */
21
+ export declare function resolveImportModel(ctx: PiModelContext, override?: string): PiModelHandle | null;
15
22
  /**
16
23
  * Structural view of the pieces we use from Pi's ModelRegistry/Model so the
17
24
  * bridge stays testable without importing Pi runtime types.
@@ -6,17 +6,42 @@ import { log } from "../../services/logger.js";
6
6
  /**
7
7
  * Active-model-first resolution: the explicit `piProvider`/`piModel`
8
8
  * configuration when both are set, otherwise the active `ctx.model`.
9
+ * A supplied import override resolves strictly through Pi's model registry.
9
10
  */
10
- export function resolveModelFromContext(ctx) {
11
+ export function resolveModelFromContext(ctx, override) {
11
12
  const registry = ctx?.modelRegistry;
12
13
  if (!registry)
13
14
  return null;
14
15
  let model = null;
15
- if (CONFIG.piProvider && CONFIG.piModel) {
16
- model = registry.find(CONFIG.piProvider, CONFIG.piModel) ?? null;
16
+ if (override !== undefined) {
17
+ const separator = override.indexOf("/");
18
+ if (separator < 1 || separator === override.length - 1)
19
+ return null;
20
+ model = registry.find(override.slice(0, separator), override.slice(separator + 1)) ?? null;
21
+ if (!model)
22
+ return null;
17
23
  }
18
- if (!model)
19
- model = ctx?.model ?? null;
24
+ else {
25
+ if (CONFIG.piProvider && CONFIG.piModel) {
26
+ model = registry.find(CONFIG.piProvider, CONFIG.piModel) ?? null;
27
+ }
28
+ if (!model)
29
+ model = ctx?.model ?? null;
30
+ }
31
+ return toModelHandle(registry, model);
32
+ }
33
+ /**
34
+ * History-import resolution: an explicit `provider/id` through Pi's model
35
+ * registry, otherwise the session's current model. `piProvider`/`piModel`
36
+ * steer live capture only; an import follows the session the user is in.
37
+ */
38
+ export function resolveImportModel(ctx, override) {
39
+ if (override !== undefined)
40
+ return resolveModelFromContext(ctx, override);
41
+ const registry = ctx?.modelRegistry;
42
+ return registry ? toModelHandle(registry, ctx.model) : null;
43
+ }
44
+ function toModelHandle(registry, model) {
20
45
  if (!model)
21
46
  return null;
22
47
  return {
@@ -63,11 +88,14 @@ export function createPiCaptureProvider(resolveModel) {
63
88
  if (reply.stopReason === "error") {
64
89
  throw new Error(`omms: Pi extraction call failed: ${reply.errorMessage || "unknown error"}`);
65
90
  }
66
- const summary = parseCaptureSummary(replyText(reply));
91
+ const rawReply = replyText(reply);
92
+ const summary = parseCaptureSummary(rawReply);
67
93
  if (!summary) {
68
94
  log("Pi capture: model reply was not a valid capture summary", {
69
95
  provider: model.provider,
70
96
  modelId: model.modelId,
97
+ // The reply can carry conversation content, so log only its size.
98
+ replyLength: rawReply.length,
71
99
  });
72
100
  throw new Error("omms: Pi extraction returned an invalid summary payload");
73
101
  }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { type HistoryImportArgs, type ImportHost } from "../importer/import-args.js";
3
+ export declare function parseImportArgs(argv: string[]): {
4
+ host: ImportHost;
5
+ args: HistoryImportArgs;
6
+ };
7
+ export declare function runCli(argv: string[]): Promise<number>;
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import { pathToFileURL } from "node:url";
4
+ import { historyImportUsage, importNeedsModel, parseHistoryImportArgs, } from "../importer/import-args.js";
5
+ const COMMANDS = {
6
+ "import-opencode-history": "opencode",
7
+ "import-pi-history": "pi",
8
+ };
9
+ const usage = `Usage: om-memory-system <command> [options]
10
+
11
+ Commands:
12
+ import-opencode-history Import OpenCode history into omms
13
+ import-pi-history Import Pi history into omms
14
+
15
+ Both commands take the same options; run one with --help to see them.
16
+ Inside a session, /memory-import-opencode-history and /memory-import-pi-history
17
+ use that session's model instead of an API key.`;
18
+ export function parseImportArgs(argv) {
19
+ const [command, ...rest] = argv;
20
+ const host = command ? COMMANDS[command] : undefined;
21
+ if (!host)
22
+ throw new Error("Expected import-opencode-history or import-pi-history; use --help");
23
+ const args = parseHistoryImportArgs(rest, { host, surface: "cli" });
24
+ if (!args.help && args.errors.length > 0)
25
+ throw new Error(args.errors.join("; "));
26
+ return { host, args };
27
+ }
28
+ export async function runCli(argv) {
29
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
30
+ console.log(usage);
31
+ return 0;
32
+ }
33
+ const apiKeyEnv = argv.find((_, index) => argv[index - 1] === "--api-key-env") ??
34
+ argv.find((value) => value.startsWith("--api-key-env="))?.slice("--api-key-env=".length);
35
+ try {
36
+ const { host, args } = parseImportArgs(argv);
37
+ if (args.help) {
38
+ console.log(historyImportUsage(host, "cli"));
39
+ return 0;
40
+ }
41
+ const { initConfig, initConfigWithLegacyMigration } = await import("../config.js");
42
+ if (args.dryRun)
43
+ initConfig(process.cwd());
44
+ else
45
+ initConfigWithLegacyMigration(process.cwd());
46
+ let models = {};
47
+ let model;
48
+ if (importNeedsModel(args)) {
49
+ const { selectImportModel } = await import("../importer/model-selection.js");
50
+ const selected = selectImportModel({
51
+ provider: args.provider,
52
+ model: args.model,
53
+ apiUrl: args.apiUrl,
54
+ apiKeyEnv: args.apiKeyEnv,
55
+ });
56
+ models = {
57
+ ...(!args.skipMemories ? { capture: selected.capture } : {}),
58
+ ...(!args.skipProfile ? { profile: selected.profile } : {}),
59
+ };
60
+ model = `${selected.provider}/${selected.modelId}`;
61
+ console.log(`Import model: ${model}`);
62
+ }
63
+ const { formatHistoryImportReport, historyImportFailed, runHistoryImport } = await import("../importer/run-import.js");
64
+ const report = await runHistoryImport(host, args, { cwd: process.cwd(), models });
65
+ console.log(formatHistoryImportReport(host, report, model));
66
+ return historyImportFailed(report) ? 1 : 0;
67
+ }
68
+ catch (error) {
69
+ const { CONFIG } = await import("../config.js");
70
+ const secrets = [apiKeyEnv ? process.env[apiKeyEnv] : undefined, CONFIG.memoryApiKey].filter((value) => Boolean(value));
71
+ let message = error instanceof Error ? error.message : String(error);
72
+ for (const secret of secrets)
73
+ message = message.replaceAll(secret, "[redacted]");
74
+ console.error(`Import failed: ${message}`);
75
+ return 1;
76
+ }
77
+ }
78
+ function isDirectRun() {
79
+ if (!process.argv[1])
80
+ return false;
81
+ try {
82
+ // Resolve symlinks: npm/npx expose the bin through node_modules/.bin links,
83
+ // so comparing the raw argv[1] against the module URL would never match.
84
+ return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ if (isDirectRun()) {
91
+ runCli(process.argv.slice(2)).then((exitCode) => {
92
+ process.exitCode = exitCode;
93
+ });
94
+ }
package/dist/config.d.ts CHANGED
@@ -31,7 +31,10 @@ interface OmmsConfig {
31
31
  memoryExtraParams?: Record<string, unknown>;
32
32
  opencodeProvider?: string;
33
33
  opencodeModel?: string;
34
- /** Pi adapter: explicit provider/model for capture extraction. Default: inherit active Pi model. */
34
+ /**
35
+ * Pi adapter: provider/model for live capture and profile learning ("inherit" = session
36
+ * model). Unset: the external API when configured, otherwise the session model.
37
+ */
35
38
  piProvider?: string;
36
39
  piModel?: string;
37
40
  aiSessionRetentionDays?: number;
@@ -114,7 +117,7 @@ declare function buildConfig(fileConfig: OmmsConfig): {
114
117
  opencodeModel: string | undefined;
115
118
  piProvider: string | undefined;
116
119
  piModel: string | undefined;
117
- autoCaptureProviderStatus: AutoCaptureProviderStatus;
120
+ autoCaptureProviderStatus: import("./config.js").AutoCaptureProviderStatus;
118
121
  aiSessionRetentionDays: number;
119
122
  webServerEnabled: boolean;
120
123
  webServerPort: number;
@@ -196,7 +199,7 @@ export declare let CONFIG: {
196
199
  opencodeModel: string | undefined;
197
200
  piProvider: string | undefined;
198
201
  piModel: string | undefined;
199
- autoCaptureProviderStatus: AutoCaptureProviderStatus;
202
+ autoCaptureProviderStatus: import("./config.js").AutoCaptureProviderStatus;
200
203
  aiSessionRetentionDays: number;
201
204
  webServerEnabled: boolean;
202
205
  webServerPort: number;
@@ -249,24 +252,8 @@ export declare let CONFIG: {
249
252
  };
250
253
  };
251
254
  type RuntimeConfig = ReturnType<typeof buildConfig>;
252
- interface AutoCaptureProviderRuntimeConfig {
253
- opencodeProvider?: string;
254
- opencodeModel?: string;
255
- memoryProvider?: string;
256
- memoryModel?: string;
257
- memoryApiUrl?: string;
258
- memoryApiKey?: string;
259
- }
260
- export type AutoCaptureProviderStatus = {
261
- ready: true;
262
- mode: "opencode" | "manual";
263
- issues: [];
264
- } | {
265
- ready: false;
266
- issues: string[];
267
- };
255
+ export { getAutoCaptureProviderStatus, type AutoCaptureProviderStatus, } from "./services/ai/live-model-choice.js";
268
256
  export { isPlaceholderApiKey };
269
- export declare function getAutoCaptureProviderStatus(config: AutoCaptureProviderRuntimeConfig): AutoCaptureProviderStatus;
270
257
  export declare function hasAutoCaptureProviderConfig(config?: RuntimeConfig): boolean;
271
258
  export declare function initConfig(directory: string): void;
272
259
  /**
package/dist/config.js CHANGED
@@ -5,6 +5,7 @@ import { stripJsoncComments } from "./services/jsonc.js";
5
5
  import { log } from "./services/logger.js";
6
6
  import { resolveSecretValue } from "./services/secret-resolver.js";
7
7
  import { isPlaceholderApiKey } from "./services/ai/api-key-placeholder.js";
8
+ import { getAutoCaptureProviderStatus } from "./services/ai/live-model-choice.js";
8
9
  import { resolveDefaultStoragePath, runLegacyStoreMigration, legacyMigrationPaths, } from "./services/legacy-migration.js";
9
10
  const OMMS_CONFIG_DIR = join(homedir(), ".config", "omms");
10
11
  const DATA_DIR = join(homedir(), ".omms");
@@ -229,35 +230,38 @@ const CONFIG_TEMPLATE = `{
229
230
  },
230
231
 
231
232
  // ============================================
232
- // OpenCode Provider Settings (RECOMMENDED)
233
+ // Model for auto-capture and profile learning (same rule in OpenCode and Pi)
233
234
  // ============================================
234
235
 
235
- // Use any provider that is already authenticated in opencode for auto-capture
236
- // and user profile learning. The plugin calls opencode's session.prompt API
237
- // (with structured output) instead of talking to provider HTTPS endpoints
238
- // directly, so opencode owns the auth, token refresh, and provider routing.
236
+ // Which model summarises your work, in this order:
237
+ // 1. The host model below: opencodeProvider/opencodeModel in OpenCode,
238
+ // piProvider/piModel in Pi. Set the model to "inherit" to follow
239
+ // whatever model the session is using.
240
+ // 2. If no host model is set: the external API (memoryModel/memoryApiUrl/
241
+ // memoryApiKey further down).
242
+ // 3. If neither is set: the session's own model.
243
+ // If the host model fails and the external API is configured, the external
244
+ // API is used instead.
239
245
  //
240
- // No separate API key is needed in this plugin — whatever you configured in
241
- // opencode (OAuth like Claude Pro/Max, GitHub Copilot personal/business,
242
- // bring-your-own API key, custom provider, ...) just works.
246
+ // Host models go through OpenCode's or Pi's own sign-in (OAuth like Claude
247
+ // Pro/Max, GitHub Copilot, ChatGPT, or your own key there), so no separate API
248
+ // key is needed here.
243
249
  //
244
- // If NOT set, falls back to the manual config (memoryApiKey/memoryApiUrl/memoryModel below).
245
- //
246
- // Examples (the provider name must be one returned by 'opencode providers list'):
247
- // Anthropic (OAuth/API key): "opencodeProvider": "anthropic", "opencodeModel": "claude-haiku-4-5-20251001"
248
- // OpenAI (API key): "opencodeProvider": "openai", "opencodeModel": "gpt-4o-mini"
249
- // GitHub Copilot: "opencodeProvider": "github-copilot", "opencodeModel": "gpt-4o-mini"
250
+ // Examples (OpenCode names from 'opencode providers list'; Pi names from its model list):
251
+ // "opencodeProvider": "anthropic", "opencodeModel": "claude-haiku-4-5-20251001"
252
+ // "opencodeProvider": "openai", "opencodeModel": "inherit"
253
+ // "piProvider": "openai-codex", "piModel": "gpt-5.6-luna"
250
254
  //
251
255
  // "opencodeProvider": "anthropic",
252
256
  // "opencodeModel": "claude-haiku-4-5-20251001",
257
+ // "piProvider": "openai-codex",
258
+ // "piModel": "gpt-5.6-luna",
253
259
 
254
260
  // ============================================
255
261
  // Auto-Capture Settings
256
262
  // ============================================
257
263
 
258
- // IMPORTANT: Auto-capture only runs after either opencodeProvider/opencodeModel
259
- // above is configured, or the manual fallback below is uncommented with real values.
260
- // It runs in background without blocking your main session
264
+ // Auto-capture runs in the background without blocking your main session.
261
265
  // Note: Ollama may not support tool calling. Use OpenAI, Anthropic, or Groq for best results.
262
266
 
263
267
  "autoCaptureEnabled": true,
@@ -267,7 +271,8 @@ const CONFIG_TEMPLATE = `{
267
271
  // Any service that follows the OpenAI Chat Completions API can use it via custom "memoryApiUrl".
268
272
  "memoryProvider": "openai-chat",
269
273
 
270
- // Manual fallback. Uncomment all 3 lines and replace memoryApiKey before use:
274
+ // External API (step 2 above, and the fallback when a host model fails).
275
+ // Uncomment all 3 lines and replace memoryApiKey before use:
271
276
  // "memoryModel": "gpt-4o-mini",
272
277
  // "memoryApiUrl": "https://api.openai.com/v1",
273
278
  // "memoryApiKey": "sk-...",
@@ -636,50 +641,8 @@ function buildConfig(fileConfig) {
636
641
  const _globalFileConfig = loadConfigFromPaths(CONFIG_FILES);
637
642
  let lastFileConfig = _globalFileConfig;
638
643
  export let CONFIG = buildConfig(_globalFileConfig);
639
- function hasValue(value) {
640
- return typeof value === "string" && value.trim().length > 0;
641
- }
644
+ export { getAutoCaptureProviderStatus, } from "./services/ai/live-model-choice.js";
642
645
  export { isPlaceholderApiKey };
643
- export function getAutoCaptureProviderStatus(config) {
644
- const hasOpencodeProvider = hasValue(config.opencodeProvider);
645
- const hasOpencodeModel = hasValue(config.opencodeModel);
646
- if (hasOpencodeProvider && hasOpencodeModel) {
647
- return { ready: true, mode: "opencode", issues: [] };
648
- }
649
- const issues = [];
650
- if (!hasOpencodeProvider)
651
- issues.push("opencodeProvider is not configured");
652
- if (!hasOpencodeModel)
653
- issues.push("opencodeModel is not configured");
654
- const hasMemoryModel = hasValue(config.memoryModel);
655
- const hasMemoryApiUrl = hasValue(config.memoryApiUrl);
656
- const hasMemoryApiKey = hasValue(config.memoryApiKey);
657
- const hasPlaceholderMemoryApiKey = isPlaceholderApiKey(config.memoryApiKey);
658
- // The orcarouter provider presets its endpoint and default model, so only
659
- // an API key is required for the manual fallback path.
660
- if (config.memoryProvider === "orcarouter") {
661
- if (!hasMemoryApiKey)
662
- issues.push("memoryApiKey is not configured");
663
- if (hasPlaceholderMemoryApiKey)
664
- issues.push("memoryApiKey contains a placeholder value");
665
- if (hasMemoryApiKey && !hasPlaceholderMemoryApiKey) {
666
- return { ready: true, mode: "manual", issues: [] };
667
- }
668
- return { ready: false, issues };
669
- }
670
- if (!hasMemoryModel)
671
- issues.push("memoryModel is not configured");
672
- if (!hasMemoryApiUrl)
673
- issues.push("memoryApiUrl is not configured");
674
- if (!hasMemoryApiKey)
675
- issues.push("memoryApiKey is not configured");
676
- if (hasPlaceholderMemoryApiKey)
677
- issues.push("memoryApiKey contains a placeholder value");
678
- if (hasMemoryModel && hasMemoryApiUrl && hasMemoryApiKey && !hasPlaceholderMemoryApiKey) {
679
- return { ready: true, mode: "manual", issues: [] };
680
- }
681
- return { ready: false, issues };
682
- }
683
646
  export function hasAutoCaptureProviderConfig(config = CONFIG) {
684
647
  return getAutoCaptureProviderStatus(config).ready;
685
648
  }
@@ -8,9 +8,9 @@ import type { CaptureSummary } from "./host.js";
8
8
  * downstream capture pipeline always receives well-formed results.
9
9
  */
10
10
  export declare const captureSummarySchema: z.ZodObject<{
11
- summary: z.ZodString;
11
+ summary: z.ZodDefault<z.ZodString>;
12
12
  type: z.ZodString;
13
- tags: z.ZodArray<z.ZodString>;
13
+ tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
14
14
  }, z.core.$strip>;
15
15
  /**
16
16
  * JSON schema description embedded into the summary request prompt. Kept as a
@@ -6,10 +6,15 @@ import { z } from "zod";
6
6
  * the same summary shape and validate model output against this schema so the
7
7
  * downstream capture pipeline always receives well-formed results.
8
8
  */
9
- export const captureSummarySchema = z.object({
10
- summary: z.string(),
9
+ export const captureSummarySchema = z
10
+ .object({
11
+ summary: z.string().default(""),
11
12
  type: z.string(),
12
- tags: z.array(z.string()),
13
+ tags: z.array(z.string()).default([]),
14
+ })
15
+ .refine((value) => value.type === "skip" || value.summary.trim().length > 0, {
16
+ message: "A non-skip capture needs a summary",
17
+ path: ["summary"],
13
18
  });
14
19
  /**
15
20
  * JSON schema description embedded into the summary request prompt. Kept as a