om-memory-system 3.0.0 → 3.1.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{LICENSE → LICENSE.md} +4 -2
- package/README.md +108 -532
- package/dist/adapters/opencode/auto-capture-summary.js +23 -17
- package/dist/adapters/opencode/import-command.d.ts +42 -0
- package/dist/adapters/opencode/import-command.js +154 -0
- package/dist/adapters/pi/extension.js +4 -5
- package/dist/adapters/pi/import-command.d.ts +7 -19
- package/dist/adapters/pi/import-command.js +46 -161
- package/dist/adapters/pi/live-model.d.ts +17 -0
- package/dist/adapters/pi/live-model.js +86 -0
- package/dist/adapters/pi/profile.d.ts +7 -17
- package/dist/adapters/pi/profile.js +28 -66
- package/dist/adapters/pi/provider.d.ts +8 -1
- package/dist/adapters/pi/provider.js +34 -6
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +94 -0
- package/dist/config.d.ts +7 -20
- package/dist/config.js +24 -61
- package/dist/core/extraction.d.ts +2 -2
- package/dist/core/extraction.js +8 -3
- package/dist/core/profile-analysis.d.ts +16 -0
- package/dist/core/profile-analysis.js +47 -0
- package/dist/importer/import-args.d.ts +44 -0
- package/dist/importer/import-args.js +219 -0
- package/dist/importer/importer.d.ts +33 -9
- package/dist/importer/importer.js +153 -89
- package/dist/importer/ledger.d.ts +9 -2
- package/dist/importer/ledger.js +16 -2
- package/dist/importer/model-selection.d.ts +16 -0
- package/dist/importer/model-selection.js +84 -0
- package/dist/importer/opencode-import.d.ts +28 -0
- package/dist/importer/opencode-import.js +87 -0
- package/dist/importer/opencode-project.d.ts +11 -0
- package/dist/importer/opencode-project.js +38 -0
- package/dist/importer/opencode-reader.d.ts +19 -0
- package/dist/importer/opencode-reader.js +214 -0
- package/dist/importer/profile-import.d.ts +29 -0
- package/dist/importer/profile-import.js +111 -0
- package/dist/importer/run-import.d.ts +49 -0
- package/dist/importer/run-import.js +123 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +37 -1
- package/dist/services/ai/internal-capture-sessions.d.ts +2 -0
- package/dist/services/ai/internal-capture-sessions.js +7 -0
- package/dist/services/ai/live-model-choice.d.ts +52 -0
- package/dist/services/ai/live-model-choice.js +83 -0
- package/dist/services/ai/profile-llm-client.js +7 -5
- package/dist/services/user-memory-learning.js +9 -8
- package/dist/services/user-profile/ai-cleanup.js +8 -5
- package/dist/services/user-profile/user-profile-manager.js +11 -10
- package/dist/v2/adapter.js +27 -0
- package/package.json +5 -2
|
@@ -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
|
|
1
|
+
import { type ExistingProfileInput, type ProfileAnalysisResult, type ModelPort } from "../../core/profile-analysis.js";
|
|
2
2
|
import type { PiModelHandle } from "./provider.js";
|
|
3
|
-
export
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
//
|
|
233
|
+
// Model for auto-capture and profile learning (same rule in OpenCode and Pi)
|
|
233
234
|
// ============================================
|
|
234
235
|
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
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
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
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
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
package/dist/core/extraction.js
CHANGED
|
@@ -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
|
|
10
|
-
|
|
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
|