om-memory-system 3.2.0-next.9 → 3.3.0-next.18
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/README.md +3 -5
- package/dist/adapters/opencode/auto-capture-summary.js +50 -8
- package/dist/adapters/opencode/backfill-models.d.ts +25 -0
- package/dist/adapters/opencode/backfill-models.js +39 -0
- package/dist/adapters/opencode/backfill-startup.d.ts +7 -0
- package/dist/adapters/opencode/backfill-startup.js +12 -0
- package/dist/adapters/opencode/import-command.d.ts +3 -7
- package/dist/adapters/opencode/import-command.js +6 -54
- package/dist/adapters/pi/backfill-models.d.ts +9 -0
- package/dist/adapters/pi/backfill-models.js +30 -0
- package/dist/adapters/pi/extension.js +53 -0
- package/dist/adapters/pi/import-command.js +3 -0
- package/dist/adapters/pi/profile.js +2 -1
- package/dist/adapters/pi/provider.js +41 -14
- package/dist/cli/index.js +13 -1
- package/dist/cli/web-command.d.ts +3 -0
- package/dist/cli/web-command.js +72 -0
- package/dist/config.d.ts +30 -1
- package/dist/config.js +131 -9
- package/dist/core/capture-context.js +1 -1
- package/dist/core/capture.d.ts +4 -0
- package/dist/core/capture.js +30 -0
- package/dist/core/extraction.d.ts +23 -1
- package/dist/core/extraction.js +41 -0
- package/dist/core/host.d.ts +23 -0
- package/dist/core/host.js +9 -1
- package/dist/core/profile-analysis.js +9 -1
- package/dist/importer/auto-backfill.d.ts +20 -0
- package/dist/importer/auto-backfill.js +165 -0
- package/dist/importer/backfill-lock.d.ts +3 -0
- package/dist/importer/backfill-lock.js +44 -0
- package/dist/importer/backfill-model.d.ts +10 -0
- package/dist/importer/backfill-model.js +13 -0
- package/dist/importer/discovery.d.ts +32 -5
- package/dist/importer/discovery.js +88 -36
- package/dist/importer/import-args.d.ts +23 -1
- package/dist/importer/import-args.js +29 -1
- package/dist/importer/import-project.d.ts +16 -0
- package/dist/importer/import-project.js +35 -0
- package/dist/importer/import-readiness.d.ts +47 -0
- package/dist/importer/import-readiness.js +54 -0
- package/dist/importer/import-sessions.d.ts +98 -0
- package/dist/importer/import-sessions.js +222 -0
- package/dist/importer/import-sources.d.ts +47 -0
- package/dist/importer/import-sources.js +147 -0
- package/dist/importer/importer.d.ts +31 -1
- package/dist/importer/importer.js +96 -24
- package/dist/importer/manual-import-guard.d.ts +4 -0
- package/dist/importer/manual-import-guard.js +9 -0
- package/dist/importer/opencode-import.d.ts +11 -1
- package/dist/importer/opencode-import.js +133 -69
- package/dist/importer/opencode-project.d.ts +1 -4
- package/dist/importer/opencode-project.js +3 -36
- package/dist/importer/opencode-reader.d.ts +25 -5
- package/dist/importer/opencode-reader.js +92 -106
- package/dist/importer/opencode-snapshot.d.ts +82 -0
- package/dist/importer/opencode-snapshot.js +276 -0
- package/dist/importer/profile-import.d.ts +1 -0
- package/dist/importer/profile-import.js +7 -1
- package/dist/importer/run-import.d.ts +10 -0
- package/dist/importer/run-import.js +21 -0
- package/dist/importer/settings-health.d.ts +24 -0
- package/dist/importer/settings-health.js +127 -0
- package/dist/importer/web-import-api.d.ts +8 -0
- package/dist/importer/web-import-api.js +8 -0
- package/dist/importer/web-import-jobs.d.ts +53 -0
- package/dist/importer/web-import-jobs.js +227 -0
- package/dist/index.js +34 -0
- package/dist/services/ai/live-model-choice.js +6 -3
- package/dist/services/ai/opencode-import-models.d.ts +11 -0
- package/dist/services/ai/opencode-import-models.js +55 -0
- package/dist/services/ai/opencode-provider.d.ts +9 -0
- package/dist/services/ai/opencode-provider.js +31 -4
- package/dist/services/ai/providers/anthropic-messages.js +14 -6
- package/dist/services/ai/providers/base-provider.d.ts +8 -0
- package/dist/services/ai/providers/base-provider.js +12 -0
- package/dist/services/ai/providers/google-gemini.js +21 -7
- package/dist/services/ai/providers/openai-chat-completion.js +20 -7
- package/dist/services/ai/providers/openai-responses.js +11 -0
- package/dist/services/backfill-state.d.ts +25 -0
- package/dist/services/backfill-state.js +68 -0
- package/dist/services/capture-attempt-store.d.ts +21 -0
- package/dist/services/capture-attempt-store.js +108 -0
- package/dist/services/capture-diagnostics.d.ts +66 -0
- package/dist/services/capture-diagnostics.js +175 -0
- package/dist/services/cleanup-service.js +9 -0
- package/dist/services/global-config-writer.d.ts +10 -0
- package/dist/services/global-config-writer.js +110 -0
- package/dist/services/live-captured-entries.d.ts +3 -0
- package/dist/services/live-captured-entries.js +34 -0
- package/dist/services/log-path.d.ts +2 -0
- package/dist/services/log-path.js +15 -0
- package/dist/services/logger.js +1 -12
- package/dist/services/safe-health-error.d.ts +2 -0
- package/dist/services/safe-health-error.js +11 -0
- package/dist/services/settings-log.d.ts +5 -0
- package/dist/services/settings-log.js +36 -0
- package/dist/services/settings-models.d.ts +35 -0
- package/dist/services/settings-models.js +39 -0
- package/dist/services/settings-snapshot.d.ts +24 -0
- package/dist/services/settings-snapshot.js +83 -0
- package/dist/services/settings-traces.d.ts +7 -0
- package/dist/services/settings-traces.js +39 -0
- package/dist/services/user-memory-learning.js +2 -1
- package/dist/services/web-autostart.d.ts +29 -0
- package/dist/services/web-autostart.js +208 -0
- package/dist/services/web-server.d.ts +5 -0
- package/dist/services/web-server.js +187 -3
- package/dist/v2/legacy-client.js +19 -2
- package/dist/web/assets/index-B-_gMSn9.js +124 -0
- package/dist/web/assets/index-D5ak5KsI.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/dist/web/assets/index-DPjEpD1h.css +0 -2
- package/dist/web/assets/index-do26c4JJ.js +0 -122
|
@@ -18,6 +18,15 @@ export async function runHistoryImport(host, args, run) {
|
|
|
18
18
|
...(args.since !== undefined ? { since: args.since } : {}),
|
|
19
19
|
...(args.until !== undefined ? { until: args.until } : {}),
|
|
20
20
|
...(args.session ? { session: args.session } : {}),
|
|
21
|
+
...(run.selection
|
|
22
|
+
? {
|
|
23
|
+
selectionKeys: run.selection.keys,
|
|
24
|
+
cutoff: run.selection.cutoff,
|
|
25
|
+
...(run.selection.snapshotKey
|
|
26
|
+
? { snapshot: { key: run.selection.snapshotKey, mode: "reuse" } }
|
|
27
|
+
: {}),
|
|
28
|
+
}
|
|
29
|
+
: {}),
|
|
21
30
|
...(project ? { project } : {}),
|
|
22
31
|
...(args.maxSessions ? { maxSessions: args.maxSessions } : {}),
|
|
23
32
|
...(args.profileBatch ? { profileBatch: args.profileBatch } : {}),
|
|
@@ -25,6 +34,8 @@ export async function runHistoryImport(host, args, run) {
|
|
|
25
34
|
force: args.force,
|
|
26
35
|
skipMemories: args.skipMemories,
|
|
27
36
|
skipProfile: args.skipProfile,
|
|
37
|
+
...(run.signal ? { signal: run.signal } : {}),
|
|
38
|
+
...(run.onProgress ? { onProgress: run.onProgress } : {}),
|
|
28
39
|
...(run.models.capture ? { provider: capture } : {}),
|
|
29
40
|
...(run.models.profile ? { profileModel: run.models.profile } : {}),
|
|
30
41
|
});
|
|
@@ -35,6 +46,7 @@ export async function runHistoryImport(host, args, run) {
|
|
|
35
46
|
loadSession: loadPiSessionForImport,
|
|
36
47
|
provider: capture,
|
|
37
48
|
...(run.onProgress ? { onProgress: run.onProgress } : {}),
|
|
49
|
+
...(run.signal ? { signal: run.signal } : {}),
|
|
38
50
|
...(!args.skipProfile
|
|
39
51
|
? {
|
|
40
52
|
profile: {
|
|
@@ -47,6 +59,7 @@ export async function runHistoryImport(host, args, run) {
|
|
|
47
59
|
scope: args.scope,
|
|
48
60
|
currentDirectory: project ?? run.cwd,
|
|
49
61
|
...(args.session ? { session: args.session } : {}),
|
|
62
|
+
...(run.selection ? { selectionKeys: run.selection.keys, cutoff: run.selection.cutoff } : {}),
|
|
50
63
|
...(args.since !== undefined ? { since: args.since } : {}),
|
|
51
64
|
...(args.until !== undefined ? { until: args.until } : {}),
|
|
52
65
|
...(args.maxSessions ? { maxSessions: args.maxSessions } : {}),
|
|
@@ -79,6 +92,12 @@ export function formatHistoryImportReport(host, report, model) {
|
|
|
79
92
|
for (const unresolvable of report.unresolvableSessions) {
|
|
80
93
|
lines.push(` unresolved ${unresolvable.cwd ?? "(no cwd)"}: ${unresolvable.file} (use --map)`);
|
|
81
94
|
}
|
|
95
|
+
if (report.unitsUntimed) {
|
|
96
|
+
lines.push(` turns without a timestamp: ${report.unitsUntimed} (date limits cannot exclude them)`);
|
|
97
|
+
}
|
|
98
|
+
if (report.unitsHeldBack) {
|
|
99
|
+
lines.push(` newer turns held back: ${report.unitsHeldBack}; list the sessions again and import to include them`);
|
|
100
|
+
}
|
|
82
101
|
if (report.loadErrors.length > 0)
|
|
83
102
|
lines.push(` load errors: ${report.loadErrors.length}`);
|
|
84
103
|
if (report.profile) {
|
|
@@ -103,6 +122,8 @@ export function summarizeHistoryImportReport(report) {
|
|
|
103
122
|
unitsSkipped: report.unitsSkipped,
|
|
104
123
|
unitsFailed: report.unitsFailed,
|
|
105
124
|
unitsAlreadyHandled: report.unitsAlreadyHandled,
|
|
125
|
+
unitsHeldBack: report.unitsHeldBack ?? 0,
|
|
126
|
+
unitsUntimed: report.unitsUntimed ?? 0,
|
|
106
127
|
projects: report.projects.length,
|
|
107
128
|
unresolved: (report.unresolvedProjects?.length ?? 0) + report.unresolvableSessions.length,
|
|
108
129
|
loadErrors: report.loadErrors.length,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { safeHealthError } from "../services/safe-health-error.js";
|
|
2
|
+
export type HealthRow = {
|
|
3
|
+
check: string;
|
|
4
|
+
status: "pass" | "warn" | "fail";
|
|
5
|
+
reason: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function captureFailureHealth(byModel: Array<{
|
|
8
|
+
total: number;
|
|
9
|
+
failed: number;
|
|
10
|
+
}>, byReason: Array<{
|
|
11
|
+
reason: string;
|
|
12
|
+
count: number;
|
|
13
|
+
}>): HealthRow;
|
|
14
|
+
export interface HealthInput {
|
|
15
|
+
directory: string;
|
|
16
|
+
host: string;
|
|
17
|
+
authEnabled: boolean;
|
|
18
|
+
apiTokenSet: boolean;
|
|
19
|
+
testModels: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Run checks independently so a failed dependency cannot hide the remaining checks. */
|
|
22
|
+
export declare function runSettingsHealth(input: HealthInput): Promise<{
|
|
23
|
+
checks: HealthRow[];
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { CONFIG } from "../config.js";
|
|
2
|
+
import { getAutoCaptureProviderStatus, resolvePiLiveModel, } from "../services/ai/live-model-choice.js";
|
|
3
|
+
import { isLoopbackHost } from "../services/web-api-auth.js";
|
|
4
|
+
import { safeHealthError } from "../services/safe-health-error.js";
|
|
5
|
+
export { safeHealthError } from "../services/safe-health-error.js";
|
|
6
|
+
const PROBE = "Reply with a short acknowledgement.";
|
|
7
|
+
export function captureFailureHealth(byModel, byReason) {
|
|
8
|
+
const total = byModel.reduce((count, row) => count + row.total, 0);
|
|
9
|
+
const failed = byModel.reduce((count, row) => count + row.failed, 0);
|
|
10
|
+
const reason = [...byReason].sort((a, b) => b.count - a.count)[0]?.reason;
|
|
11
|
+
return {
|
|
12
|
+
check: "Capture failures (24 hours)",
|
|
13
|
+
status: total > 0 && failed / total > 0.2 ? "warn" : "pass",
|
|
14
|
+
reason: total
|
|
15
|
+
? `${failed}/${total} failed${reason ? `; most common reason: ${reason}` : ""}`
|
|
16
|
+
: "No recent attempts",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Run checks independently so a failed dependency cannot hide the remaining checks. */
|
|
20
|
+
export async function runSettingsHealth(input) {
|
|
21
|
+
const checks = [];
|
|
22
|
+
async function check(name, action) {
|
|
23
|
+
try {
|
|
24
|
+
checks.push({ check: name, status: "pass", reason: await action() });
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
checks.push({
|
|
28
|
+
check: name,
|
|
29
|
+
status: "fail",
|
|
30
|
+
reason: safeHealthError(error, [
|
|
31
|
+
CONFIG.memoryApiKey,
|
|
32
|
+
CONFIG.embeddingApiKey,
|
|
33
|
+
CONFIG.webServerApiToken,
|
|
34
|
+
]),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
await check("Config files", async () => {
|
|
39
|
+
const { getSettingsSnapshot } = await import("../services/settings-snapshot.js");
|
|
40
|
+
getSettingsSnapshot(input.directory);
|
|
41
|
+
return "Global and project config parsed";
|
|
42
|
+
});
|
|
43
|
+
await check("Memory store", async () => {
|
|
44
|
+
const { tursoConnectionManager } = await import("../services/turso/connection-manager.js");
|
|
45
|
+
const { join } = await import("node:path");
|
|
46
|
+
const db = await tursoConnectionManager.getConnection(join(CONFIG.storagePath, "user-prompts.db"));
|
|
47
|
+
await db.all("SELECT 1");
|
|
48
|
+
return "Store opened and queried";
|
|
49
|
+
});
|
|
50
|
+
await check("Embedding", async () => {
|
|
51
|
+
const { embeddingService } = await import("../services/embedding.js");
|
|
52
|
+
const result = await embeddingService.embedWithTimeout("OMMS health check", { task: "query" });
|
|
53
|
+
if (!result.length)
|
|
54
|
+
throw new Error("Embedding returned no values");
|
|
55
|
+
return "Test text embedded";
|
|
56
|
+
});
|
|
57
|
+
const secure = isLoopbackHost(input.host) || input.authEnabled || input.apiTokenSet;
|
|
58
|
+
checks.push({
|
|
59
|
+
check: "Web binding",
|
|
60
|
+
status: secure ? "pass" : "fail",
|
|
61
|
+
reason: secure
|
|
62
|
+
? "Binding and access settings are safe"
|
|
63
|
+
: "Network binding requires an API token or Basic Auth",
|
|
64
|
+
});
|
|
65
|
+
const opencode = getAutoCaptureProviderStatus(CONFIG);
|
|
66
|
+
checks.push({
|
|
67
|
+
check: "OpenCode model",
|
|
68
|
+
status: opencode.ready ? "pass" : "fail",
|
|
69
|
+
reason: opencode.ready ? `Resolved: ${opencode.mode}` : opencode.issues.join("; "),
|
|
70
|
+
});
|
|
71
|
+
const pi = resolvePiLiveModel(CONFIG);
|
|
72
|
+
checks.push({
|
|
73
|
+
check: "Pi model",
|
|
74
|
+
status: pi.kind === "unready" ? "fail" : "pass",
|
|
75
|
+
reason: pi.kind === "unready" ? pi.issues.join("; ") : `Resolved: ${pi.kind}`,
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
const { queryCaptureAttempts } = await import("../services/capture-attempt-store.js");
|
|
79
|
+
const { byModel, byReason } = await queryCaptureAttempts(Date.now() - 86400000, Date.now());
|
|
80
|
+
checks.push(captureFailureHealth(byModel, byReason.map((row) => ({ count: row.count, reason: String(row.reason) }))));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
checks.push({
|
|
84
|
+
check: "Capture failures (24 hours)",
|
|
85
|
+
status: "fail",
|
|
86
|
+
reason: safeHealthError(error, [CONFIG.memoryApiKey]),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (input.testModels) {
|
|
90
|
+
await check("OpenCode model test", async () => {
|
|
91
|
+
const { resolveOpencodeHostModel } = await import("../services/ai/live-model-choice.js");
|
|
92
|
+
const ref = resolveOpencodeHostModel(CONFIG);
|
|
93
|
+
if (ref?.modelID === "inherit")
|
|
94
|
+
throw new Error("A session model needs an active session to test");
|
|
95
|
+
const capture = ref
|
|
96
|
+
? (await (await import("../services/ai/opencode-import-models.js")).createOpencodeImportModels(ref, input.directory)).capture
|
|
97
|
+
: (await import("./model-selection.js")).selectImportModel({}).capture;
|
|
98
|
+
await capture.summarize({
|
|
99
|
+
userPrompt: PROBE,
|
|
100
|
+
context: PROBE,
|
|
101
|
+
sessionId: "health",
|
|
102
|
+
projectDirectory: input.directory,
|
|
103
|
+
});
|
|
104
|
+
return "Fixed prompt completed";
|
|
105
|
+
});
|
|
106
|
+
if (pi.kind === "manual") {
|
|
107
|
+
await check("Pi model test", async () => {
|
|
108
|
+
const { selectImportModel } = await import("./model-selection.js");
|
|
109
|
+
await selectImportModel({}).capture.summarize({
|
|
110
|
+
userPrompt: PROBE,
|
|
111
|
+
context: PROBE,
|
|
112
|
+
sessionId: "health",
|
|
113
|
+
projectDirectory: input.directory,
|
|
114
|
+
});
|
|
115
|
+
return "Fixed prompt completed";
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
checks.push({
|
|
120
|
+
check: "Pi model test",
|
|
121
|
+
status: "warn",
|
|
122
|
+
reason: "Pi models need an active Pi session to test",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { checks };
|
|
127
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The import pieces the web server loads for the Settings page, behind one
|
|
3
|
+
* dynamic import so the server's boundary with the importer stays small.
|
|
4
|
+
*/
|
|
5
|
+
export { importReadiness } from "./import-readiness.js";
|
|
6
|
+
export { browseImportSources, validateImportSource } from "./import-sources.js";
|
|
7
|
+
export { listImportSessions, validateSessionListRequest } from "./import-sessions.js";
|
|
8
|
+
export { opencodeSnapshots, sweepOrphanSnapshots } from "./opencode-snapshot.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The import pieces the web server loads for the Settings page, behind one
|
|
3
|
+
* dynamic import so the server's boundary with the importer stays small.
|
|
4
|
+
*/
|
|
5
|
+
export { importReadiness } from "./import-readiness.js";
|
|
6
|
+
export { browseImportSources, validateImportSource } from "./import-sources.js";
|
|
7
|
+
export { listImportSessions, validateSessionListRequest } from "./import-sessions.js";
|
|
8
|
+
export { opencodeSnapshots, sweepOrphanSnapshots } from "./opencode-snapshot.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ImportHost, type WebImportOptions } from "./import-args.js";
|
|
2
|
+
import { type ImportReadiness } from "./import-readiness.js";
|
|
3
|
+
import { resolveImportSelection, type ImportSelection } from "./import-sessions.js";
|
|
4
|
+
import { runHistoryImport, summarizeHistoryImportReport } from "./run-import.js";
|
|
5
|
+
type JobState = "running" | "cancelling" | "cancelled" | "done" | "failed";
|
|
6
|
+
type Job = {
|
|
7
|
+
id: string;
|
|
8
|
+
host: ImportHost;
|
|
9
|
+
dryRun: boolean;
|
|
10
|
+
state: JobState;
|
|
11
|
+
/** Sessions in the resolved selection. */
|
|
12
|
+
sessions: number;
|
|
13
|
+
processed: number;
|
|
14
|
+
total: number;
|
|
15
|
+
report?: string;
|
|
16
|
+
summary?: ReturnType<typeof summarizeHistoryImportReport>;
|
|
17
|
+
error?: string;
|
|
18
|
+
};
|
|
19
|
+
type Request = {
|
|
20
|
+
host: ImportHost;
|
|
21
|
+
source: unknown;
|
|
22
|
+
selection: ImportSelection;
|
|
23
|
+
options: WebImportOptions;
|
|
24
|
+
modelChoice?: string;
|
|
25
|
+
};
|
|
26
|
+
type Runner = typeof runHistoryImport;
|
|
27
|
+
/** A refused job; `status` is the HTTP status the page receives. */
|
|
28
|
+
export declare class ImportJobError extends Error {
|
|
29
|
+
readonly status: number;
|
|
30
|
+
constructor(message: string, status?: number);
|
|
31
|
+
}
|
|
32
|
+
export declare function validateWebImportRequest(value: unknown): Request;
|
|
33
|
+
export interface ImportJobDeps {
|
|
34
|
+
runner?: Runner;
|
|
35
|
+
resolveSelection?: typeof resolveImportSelection;
|
|
36
|
+
readiness?: () => Promise<ImportReadiness>;
|
|
37
|
+
}
|
|
38
|
+
/** One in-memory slot per web server, shared by previews and imports. */
|
|
39
|
+
export declare class SettingsImportJobs {
|
|
40
|
+
private job?;
|
|
41
|
+
private controller?;
|
|
42
|
+
private starting;
|
|
43
|
+
private readonly runner;
|
|
44
|
+
private readonly resolveSelection;
|
|
45
|
+
private readonly readiness;
|
|
46
|
+
constructor(deps?: ImportJobDeps);
|
|
47
|
+
current(): Job | null;
|
|
48
|
+
private busy;
|
|
49
|
+
start(request: unknown, directory: string): Promise<Job>;
|
|
50
|
+
private begin;
|
|
51
|
+
cancel(): Job;
|
|
52
|
+
}
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { CONFIG } from "../config.js";
|
|
4
|
+
import { importNeedsModel, parseHistoryImportArgs, webImportTokens, } from "./import-args.js";
|
|
5
|
+
import { importBlockedReason, importReadiness } from "./import-readiness.js";
|
|
6
|
+
import { resolveImportSelection, validateImportSelection, } from "./import-sessions.js";
|
|
7
|
+
import { importSourceKey } from "./import-sources.js";
|
|
8
|
+
import { formatHistoryImportReport, runHistoryImport, summarizeHistoryImportReport, } from "./run-import.js";
|
|
9
|
+
import { safeHealthError } from "./settings-health.js";
|
|
10
|
+
/** A refused job; `status` is the HTTP status the page receives. */
|
|
11
|
+
export class ImportJobError extends Error {
|
|
12
|
+
status;
|
|
13
|
+
constructor(message, status = 400) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.name = "ImportJobError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Selection replaces --session and --max-sessions; the source comes as a token.
|
|
20
|
+
const fields = new Set([
|
|
21
|
+
"dryRun",
|
|
22
|
+
"force",
|
|
23
|
+
"skipMemories",
|
|
24
|
+
"skipProfile",
|
|
25
|
+
"scope",
|
|
26
|
+
"project",
|
|
27
|
+
"since",
|
|
28
|
+
"until",
|
|
29
|
+
"profileBatch",
|
|
30
|
+
"pathMaps",
|
|
31
|
+
]);
|
|
32
|
+
export function validateWebImportRequest(value) {
|
|
33
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
34
|
+
throw new Error("Invalid import request");
|
|
35
|
+
const input = value;
|
|
36
|
+
if (input.host !== "pi" && input.host !== "opencode")
|
|
37
|
+
throw new Error("Choose Pi or OpenCode");
|
|
38
|
+
if (!input.options || typeof input.options !== "object" || Array.isArray(input.options))
|
|
39
|
+
throw new Error("Invalid import options");
|
|
40
|
+
const options = input.options;
|
|
41
|
+
for (const [key, option] of Object.entries(options)) {
|
|
42
|
+
if (!fields.has(key))
|
|
43
|
+
throw new Error(`Unknown import option: ${key}`);
|
|
44
|
+
if (["dryRun", "force", "skipMemories", "skipProfile"].includes(key)) {
|
|
45
|
+
if (typeof option !== "boolean")
|
|
46
|
+
throw new Error(`Invalid ${key}`);
|
|
47
|
+
}
|
|
48
|
+
else if (key === "pathMaps") {
|
|
49
|
+
if (!Array.isArray(option) ||
|
|
50
|
+
!option.every((map) => map &&
|
|
51
|
+
typeof map === "object" &&
|
|
52
|
+
typeof map.from === "string" &&
|
|
53
|
+
typeof map.to === "string"))
|
|
54
|
+
throw new Error("Invalid directory maps");
|
|
55
|
+
}
|
|
56
|
+
else if (key === "profileBatch") {
|
|
57
|
+
if (!Number.isSafeInteger(option) || option < 1)
|
|
58
|
+
throw new Error(`Invalid ${key}`);
|
|
59
|
+
}
|
|
60
|
+
else if (key === "since" || key === "until") {
|
|
61
|
+
if (typeof option !== "string" && typeof option !== "number")
|
|
62
|
+
throw new Error(`Invalid ${key}`);
|
|
63
|
+
}
|
|
64
|
+
else if (typeof option !== "string") {
|
|
65
|
+
throw new Error(`Invalid ${key}`);
|
|
66
|
+
}
|
|
67
|
+
else if (key === "project" && !isAbsolute(option)) {
|
|
68
|
+
throw new Error("The project must be an absolute path");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (input.modelChoice !== undefined &&
|
|
72
|
+
(typeof input.modelChoice !== "string" ||
|
|
73
|
+
(input.modelChoice !== "external" && !/^[^/]+\/.+$/.test(input.modelChoice))))
|
|
74
|
+
throw new Error("Choose a signed-in provider/model or the external API");
|
|
75
|
+
return {
|
|
76
|
+
host: input.host,
|
|
77
|
+
source: input.source,
|
|
78
|
+
selection: validateImportSelection(input.selection),
|
|
79
|
+
options: options,
|
|
80
|
+
modelChoice: input.modelChoice,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** One in-memory slot per web server, shared by previews and imports. */
|
|
84
|
+
export class SettingsImportJobs {
|
|
85
|
+
job;
|
|
86
|
+
controller;
|
|
87
|
+
starting = false;
|
|
88
|
+
runner;
|
|
89
|
+
resolveSelection;
|
|
90
|
+
readiness;
|
|
91
|
+
constructor(deps = {}) {
|
|
92
|
+
this.runner = deps.runner ?? runHistoryImport;
|
|
93
|
+
this.resolveSelection = deps.resolveSelection ?? resolveImportSelection;
|
|
94
|
+
this.readiness = deps.readiness ?? (() => importReadiness());
|
|
95
|
+
}
|
|
96
|
+
current() {
|
|
97
|
+
return this.job ? { ...this.job } : null;
|
|
98
|
+
}
|
|
99
|
+
busy() {
|
|
100
|
+
return this.starting || this.job?.state === "running" || this.job?.state === "cancelling";
|
|
101
|
+
}
|
|
102
|
+
async start(request, directory) {
|
|
103
|
+
if (this.busy())
|
|
104
|
+
throw new ImportJobError("An import is already running", 409);
|
|
105
|
+
this.starting = true;
|
|
106
|
+
try {
|
|
107
|
+
return await this.begin(request, directory);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
this.starting = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async begin(request, directory) {
|
|
114
|
+
let input;
|
|
115
|
+
try {
|
|
116
|
+
input = validateWebImportRequest(request);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
throw new ImportJobError(error.message);
|
|
120
|
+
}
|
|
121
|
+
const options = { ...input.options };
|
|
122
|
+
if (input.modelChoice && input.modelChoice !== "external")
|
|
123
|
+
options.model = input.modelChoice;
|
|
124
|
+
const args = parseHistoryImportArgs(webImportTokens(options, input.host), {
|
|
125
|
+
host: input.host,
|
|
126
|
+
surface: "web",
|
|
127
|
+
});
|
|
128
|
+
if (args.errors.length)
|
|
129
|
+
throw new ImportJobError(args.errors.join("; "));
|
|
130
|
+
const needsModel = importNeedsModel(args);
|
|
131
|
+
const blocked = importBlockedReason(await this.readiness(), {
|
|
132
|
+
host: input.host,
|
|
133
|
+
needsModel,
|
|
134
|
+
...(input.modelChoice ? { modelChoice: input.modelChoice } : {}),
|
|
135
|
+
});
|
|
136
|
+
if (blocked)
|
|
137
|
+
throw new ImportJobError(blocked);
|
|
138
|
+
let selection;
|
|
139
|
+
try {
|
|
140
|
+
selection = await this.resolveSelection(input.source, input.selection, {
|
|
141
|
+
host: input.host,
|
|
142
|
+
scope: args.scope,
|
|
143
|
+
...(args.project ? { project: args.project } : {}),
|
|
144
|
+
pathMaps: args.pathMaps,
|
|
145
|
+
cwd: directory,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
const status = error.status ?? 400;
|
|
150
|
+
throw new ImportJobError(error.message, status);
|
|
151
|
+
}
|
|
152
|
+
args.source = selection.identity.realPath;
|
|
153
|
+
const job = {
|
|
154
|
+
id: randomUUID(),
|
|
155
|
+
host: input.host,
|
|
156
|
+
dryRun: args.dryRun,
|
|
157
|
+
state: "running",
|
|
158
|
+
sessions: selection.keys.length,
|
|
159
|
+
processed: 0,
|
|
160
|
+
total: 0,
|
|
161
|
+
};
|
|
162
|
+
const controller = new AbortController();
|
|
163
|
+
this.job = job;
|
|
164
|
+
this.controller = controller;
|
|
165
|
+
void Promise.resolve().then(async () => {
|
|
166
|
+
try {
|
|
167
|
+
let models = {};
|
|
168
|
+
if (needsModel) {
|
|
169
|
+
if (input.modelChoice === "external") {
|
|
170
|
+
const { selectImportModel } = await import("./model-selection.js");
|
|
171
|
+
const selected = selectImportModel({});
|
|
172
|
+
models = { capture: selected.capture, profile: selected.profile };
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const [providerID = "", ...id] = input.modelChoice.split("/");
|
|
176
|
+
const { loadOpencodeProvider } = await import("../services/ai/opencode-provider-loader.js");
|
|
177
|
+
const { isProviderConnected } = await loadOpencodeProvider();
|
|
178
|
+
if (!isProviderConnected(providerID))
|
|
179
|
+
throw new Error("OpenCode provider is not connected");
|
|
180
|
+
const { createOpencodeImportModels } = await import("../services/ai/opencode-import-models.js");
|
|
181
|
+
models = await createOpencodeImportModels({ providerID, modelID: id.join("/") }, directory);
|
|
182
|
+
}
|
|
183
|
+
const { memoryClient } = await import("../services/client.js");
|
|
184
|
+
await memoryClient.warmup();
|
|
185
|
+
}
|
|
186
|
+
const report = await this.runner(input.host, args, {
|
|
187
|
+
cwd: directory,
|
|
188
|
+
models,
|
|
189
|
+
signal: controller.signal,
|
|
190
|
+
selection: {
|
|
191
|
+
keys: selection.keys,
|
|
192
|
+
cutoff: selection.cutoff,
|
|
193
|
+
...(selection.identity.kind === "opencode-db"
|
|
194
|
+
? { snapshotKey: importSourceKey(selection.identity) }
|
|
195
|
+
: {}),
|
|
196
|
+
},
|
|
197
|
+
onProgress: (processed, total) => {
|
|
198
|
+
job.processed = processed;
|
|
199
|
+
job.total = total;
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
job.summary = summarizeHistoryImportReport(report);
|
|
203
|
+
job.report = safeHealthError(formatHistoryImportReport(input.host, report, input.modelChoice), [CONFIG.memoryApiKey]);
|
|
204
|
+
job.state = controller.signal.aborted ? "cancelled" : "done";
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
job.error = safeHealthError(error, [CONFIG.memoryApiKey, CONFIG.embeddingApiKey]);
|
|
208
|
+
job.state = controller.signal.aborted ? "cancelled" : "failed";
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
// A preview keeps the OpenCode copy for the import that follows; an import is the end.
|
|
212
|
+
if (!args.dryRun && selection.identity.kind === "opencode-db") {
|
|
213
|
+
const { opencodeSnapshots } = await import("./opencode-snapshot.js");
|
|
214
|
+
opencodeSnapshots.discard(importSourceKey(selection.identity));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
return { ...job };
|
|
219
|
+
}
|
|
220
|
+
cancel() {
|
|
221
|
+
if (!this.job || !this.controller || this.job.state !== "running")
|
|
222
|
+
throw new Error("No running import");
|
|
223
|
+
this.controller.abort();
|
|
224
|
+
this.job.state = "cancelling";
|
|
225
|
+
return { ...this.job };
|
|
226
|
+
}
|
|
227
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { formatMemoriesForCompaction } from "./core/retrieval.js";
|
|
|
11
11
|
import { performUserProfileLearning } from "./services/user-memory-learning.js";
|
|
12
12
|
import { userPromptManager } from "./services/user-prompt/user-prompt-manager.js";
|
|
13
13
|
import { startWebServer, WebServer } from "./services/web-server.js";
|
|
14
|
+
import { pruneTraces } from "./services/capture-diagnostics.js";
|
|
14
15
|
import { ensureTursoReady } from "./services/turso/ready.js";
|
|
15
16
|
import { tursoConnectionManager } from "./services/turso/connection-manager.js";
|
|
16
17
|
import { WebAuth } from "./services/web-auth.js";
|
|
@@ -180,6 +181,15 @@ function logAutoCaptureProviderStatus() {
|
|
|
180
181
|
export const OmmsPlugin = async (ctx) => {
|
|
181
182
|
const { directory } = ctx;
|
|
182
183
|
initConfigWithLegacyMigration(directory);
|
|
184
|
+
// Runs even with tracing off, so turning it off does not leave old traces behind.
|
|
185
|
+
pruneTraces(CONFIG);
|
|
186
|
+
if (CONFIG.webServerAutoStart !== undefined && process.env.OMMS_DISABLE_WEB_AUTOSTART !== "1") {
|
|
187
|
+
void import("./services/web-autostart.js")
|
|
188
|
+
.then(({ reconcileWebAutostart }) => reconcileWebAutostart(CONFIG))
|
|
189
|
+
.catch((error) => log("OpenCode login item reconciliation failed", {
|
|
190
|
+
code: error instanceof Error ? error.name : "unknown",
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
183
193
|
logAutoCaptureProviderStatus();
|
|
184
194
|
const tags = getTags(directory);
|
|
185
195
|
const autoCaptureHost = createOpenCodeAutoCaptureHost(ctx);
|
|
@@ -200,9 +210,11 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
200
210
|
}
|
|
201
211
|
await configureOpencodeHostTransport(ctx);
|
|
202
212
|
(async () => {
|
|
213
|
+
let connected = [];
|
|
203
214
|
try {
|
|
204
215
|
const providerResult = await ctx.client.provider.list();
|
|
205
216
|
if (providerResult.data?.connected) {
|
|
217
|
+
connected = providerResult.data.connected;
|
|
206
218
|
const { setConnectedProviders } = await loadOpencodeProvider();
|
|
207
219
|
setConnectedProviders(providerResult.data.connected);
|
|
208
220
|
log("opencode providers connected", {
|
|
@@ -219,6 +231,27 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
219
231
|
catch (error) {
|
|
220
232
|
log("Failed to initialize opencode provider state", { error: String(error) });
|
|
221
233
|
}
|
|
234
|
+
if (CONFIG.autoBackfill && process.env.OMMS_DISABLE_AUTO_BACKFILL !== "1" && isConfigured()) {
|
|
235
|
+
void import("./adapters/opencode/backfill-startup.js")
|
|
236
|
+
.then(({ startOpencodeBackfill }) => startOpencodeBackfill({
|
|
237
|
+
connected,
|
|
238
|
+
directory,
|
|
239
|
+
configModel: async () => {
|
|
240
|
+
const response = await ctx.client.config.get();
|
|
241
|
+
return unwrapSdkData(response)?.model ?? null;
|
|
242
|
+
},
|
|
243
|
+
notify: (message) => {
|
|
244
|
+
void ctx.client.tui
|
|
245
|
+
?.showToast({
|
|
246
|
+
body: { title: "Automatic import", message, variant: "info", duration: 5000 },
|
|
247
|
+
})
|
|
248
|
+
.catch(() => { });
|
|
249
|
+
},
|
|
250
|
+
}))
|
|
251
|
+
.catch((error) => log("OpenCode backfill failed", {
|
|
252
|
+
error: error instanceof Error ? error.message : String(error),
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
222
255
|
})();
|
|
223
256
|
let tursoReadyForWeb = !isConfigured();
|
|
224
257
|
if (CONFIG.webServerEnabled && isConfigured()) {
|
|
@@ -248,6 +281,7 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
248
281
|
username: CONFIG.webServerAuthUsername,
|
|
249
282
|
});
|
|
250
283
|
startWebServer({
|
|
284
|
+
directory,
|
|
251
285
|
port: CONFIG.webServerPort,
|
|
252
286
|
host: CONFIG.webServerHost,
|
|
253
287
|
enabled: CONFIG.webServerEnabled,
|
|
@@ -36,6 +36,9 @@ function getManualProviderStatus(config) {
|
|
|
36
36
|
export function getAutoCaptureProviderStatus(config) {
|
|
37
37
|
const hasOpencodeProvider = hasValue(config.opencodeProvider);
|
|
38
38
|
const hasOpencodeModel = hasValue(config.opencodeModel);
|
|
39
|
+
if (config.opencodeModel?.trim() === "inherit") {
|
|
40
|
+
return { ready: true, mode: "session", issues: [] };
|
|
41
|
+
}
|
|
39
42
|
if (hasOpencodeProvider && hasOpencodeModel) {
|
|
40
43
|
return { ready: true, mode: "opencode", issues: [] };
|
|
41
44
|
}
|
|
@@ -65,10 +68,10 @@ export function resolveOpencodeHostModel(config) {
|
|
|
65
68
|
}
|
|
66
69
|
/** Pi's live model, by the same rule as OpenCode's (`piProvider`/`piModel` in place of `opencode*`). */
|
|
67
70
|
export function resolvePiLiveModel(config) {
|
|
71
|
+
if (config.piModel?.trim() === "inherit")
|
|
72
|
+
return { kind: "session" };
|
|
68
73
|
if (hasValue(config.piProvider) && hasValue(config.piModel)) {
|
|
69
|
-
return config.piModel.trim()
|
|
70
|
-
? { kind: "session" }
|
|
71
|
-
: { kind: "pi", provider: config.piProvider.trim(), model: config.piModel.trim() };
|
|
74
|
+
return { kind: "pi", provider: config.piProvider.trim(), model: config.piModel.trim() };
|
|
72
75
|
}
|
|
73
76
|
const manual = getManualProviderStatus(config);
|
|
74
77
|
if (manual.ready)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CaptureSummaryProvider } from "../../core/host.js";
|
|
2
|
+
import type { ModelPort } from "../../core/profile-analysis.js";
|
|
3
|
+
export interface OpencodeModelRef {
|
|
4
|
+
providerID: string;
|
|
5
|
+
modelID: string;
|
|
6
|
+
}
|
|
7
|
+
/** Capture and profile calls routed through OpenCode's structured-output sessions. */
|
|
8
|
+
export declare function createOpencodeImportModels(ref: OpencodeModelRef, directory: string): Promise<{
|
|
9
|
+
capture: CaptureSummaryProvider;
|
|
10
|
+
profile: ModelPort;
|
|
11
|
+
}>;
|