om-memory-system 3.6.0-next.39 → 3.6.0-next.40
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/dist/adapters/claude-code/hook-client.d.ts +4 -12
- package/dist/adapters/claude-code/hook-client.js +21 -52
- package/dist/adapters/pi/extension.js +18 -0
- package/dist/cli/web-command.js +12 -0
- package/dist/cli/web-power.d.ts +40 -0
- package/dist/cli/web-power.js +61 -0
- package/dist/importer/import-readiness.d.ts +1 -1
- package/dist/importer/import-readiness.js +6 -1
- package/dist/importer/settings-health.js +24 -15
- package/dist/index.js +32 -99
- package/dist/services/web-autostart.d.ts +6 -0
- package/dist/services/web-autostart.js +31 -0
- package/dist/services/web-ensure.d.ts +59 -0
- package/dist/services/web-ensure.js +214 -0
- package/dist/services/web-server.d.ts +6 -0
- package/dist/services/web-server.js +58 -0
- package/dist/web/assets/index-Ctzj_Zg0.js +124 -0
- package/dist/web/assets/index-DG7RjW5B.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BFiVixOK.js +0 -124
- package/dist/web/assets/index-D5MY5Kjs.css +0 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EnsureDeps } from "../../services/web-ensure.js";
|
|
1
2
|
export declare const CLAUDE_HOOK_EVENTS: readonly ["session-start", "user-prompt-submit", "stop"];
|
|
2
3
|
export type ClaudeHookEvent = (typeof CLAUDE_HOOK_EVENTS)[number];
|
|
3
4
|
/** Claude Code caps added context at 10,000 characters; keep a margin. */
|
|
@@ -15,16 +16,6 @@ export interface HookServerSettings {
|
|
|
15
16
|
password: string;
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
|
-
interface SpawnedChild {
|
|
19
|
-
unref(): void;
|
|
20
|
-
on(event: "error", listener: (error: Error) => void): unknown;
|
|
21
|
-
}
|
|
22
|
-
type SpawnFn = (command: string, args: string[], options: {
|
|
23
|
-
detached: true;
|
|
24
|
-
stdio: "ignore";
|
|
25
|
-
cwd: string;
|
|
26
|
-
windowsHide: true;
|
|
27
|
-
}) => SpawnedChild;
|
|
28
19
|
/** A readable byte stream such as `process.stdin`. */
|
|
29
20
|
export interface HookInputStream {
|
|
30
21
|
on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
@@ -37,7 +28,7 @@ export interface ClaudeHookOptions {
|
|
|
37
28
|
stdin?: HookInputStream;
|
|
38
29
|
writeStdout?: (text: string) => void;
|
|
39
30
|
fetch?: typeof fetch;
|
|
40
|
-
spawn?:
|
|
31
|
+
spawn?: EnsureDeps["spawn"];
|
|
41
32
|
now?: () => number;
|
|
42
33
|
sleep?: (ms: number) => Promise<void>;
|
|
43
34
|
log?: (message: string, data: Record<string, unknown>) => void | Promise<void>;
|
|
@@ -46,6 +37,8 @@ export interface ClaudeHookOptions {
|
|
|
46
37
|
loadSettings?: () => Promise<HookServerSettings>;
|
|
47
38
|
/** The `om-memory-system` script that `web` is run from. */
|
|
48
39
|
cliScript?: string;
|
|
40
|
+
/** Overrides for the shared start rule, such as the start lock. */
|
|
41
|
+
ensureDeps?: Partial<EnsureDeps>;
|
|
49
42
|
inputTimeoutMs?: number;
|
|
50
43
|
maxInputBytes?: number;
|
|
51
44
|
/** Overrides the event's request budget. */
|
|
@@ -70,4 +63,3 @@ export declare function readHookInput(stream: HookInputStream, limits: {
|
|
|
70
63
|
export declare function truncateContext(text: string, limit?: number): string;
|
|
71
64
|
/** Run one hook event. Never throws; the result says what happened. */
|
|
72
65
|
export declare function runClaudeHook(event: ClaudeHookEvent, options?: ClaudeHookOptions): Promise<ClaudeHookResult>;
|
|
73
|
-
export {};
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
1
|
import { homedir } from "node:os";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
2
|
import { AUTH_HEADER } from "../../services/auth-token.js";
|
|
5
3
|
// Claude Code hook client: read the hook input, find or start the OMMS web
|
|
6
4
|
// app, send one request, print the added context, and log one metadata line.
|
|
@@ -15,8 +13,6 @@ export const CLAUDE_CONTEXT_LIMIT = 9_500;
|
|
|
15
13
|
export const RETRIEVAL_TAG = "omms-retrieval";
|
|
16
14
|
const INPUT_TIMEOUT_MS = 2_000;
|
|
17
15
|
const MAX_INPUT_BYTES = 1024 * 1024;
|
|
18
|
-
const HEALTH_PROBE_MS = 1_000;
|
|
19
|
-
const POLL_INTERVAL_MS = 250;
|
|
20
16
|
const BUDGETS = {
|
|
21
17
|
"session-start": { startMs: 15_000, requestMs: 3_000 },
|
|
22
18
|
"user-prompt-submit": { startMs: 5_000, requestMs: 3_000 },
|
|
@@ -116,16 +112,12 @@ function productionDefaults() {
|
|
|
116
112
|
stdin: process.stdin,
|
|
117
113
|
writeStdout: (text) => process.stdout.write(text),
|
|
118
114
|
fetch: globalThis.fetch,
|
|
119
|
-
spawn: nodeSpawn,
|
|
120
115
|
now: () => Date.now(),
|
|
121
116
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
122
117
|
// The shared logger only uses the file system; load it lazily anyway.
|
|
123
118
|
log: async (message, data) => (await import("../../services/logger.js")).log(message, data),
|
|
124
119
|
readToken: async () => (await import("../../services/auth-token.js")).getOrCreateAuthToken(),
|
|
125
|
-
resolveRuntime: async () => (await import("../../services/web-autostart.js")).resolveWebRuntime(),
|
|
126
120
|
loadSettings: loadWebSettings,
|
|
127
|
-
// From dist/adapters/claude-code/ to the CLI entry point.
|
|
128
|
-
cliScript: fileURLToPath(new URL("../../cli/index.js", import.meta.url)),
|
|
129
121
|
};
|
|
130
122
|
}
|
|
131
123
|
/** Port, bind address, and auth as `om-memory-system web` reads them (global config from home). */
|
|
@@ -190,7 +182,7 @@ async function runSteps(event, deps, result) {
|
|
|
190
182
|
if (!settings.enabled)
|
|
191
183
|
return "server-disabled";
|
|
192
184
|
const budget = BUDGETS[event];
|
|
193
|
-
const ready = await ensureServer(settings
|
|
185
|
+
const ready = await ensureServer(settings, budget.startMs, deps, result);
|
|
194
186
|
if (ready !== "ok")
|
|
195
187
|
return ready;
|
|
196
188
|
const headers = {
|
|
@@ -238,48 +230,25 @@ async function runSteps(event, deps, result) {
|
|
|
238
230
|
result.outputBytes = Buffer.byteLength(output);
|
|
239
231
|
return "ok";
|
|
240
232
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
/** Use a running web app, or start `om-memory-system web` and wait for it within the budget. */
|
|
259
|
-
async function ensureServer(baseUrl, budgetMs, deps, result) {
|
|
260
|
-
const started = deps.now();
|
|
261
|
-
const remaining = () => budgetMs - (deps.now() - started);
|
|
262
|
-
if (await isHealthy(baseUrl, Math.min(HEALTH_PROBE_MS, remaining()), deps))
|
|
263
|
-
return "ok";
|
|
264
|
-
const runtime = await deps.resolveRuntime();
|
|
265
|
-
if (!runtime)
|
|
266
|
-
return "server-unreachable";
|
|
267
|
-
const child = deps.spawn(runtime, [deps.cliScript, "web"], {
|
|
268
|
-
detached: true,
|
|
269
|
-
stdio: "ignore",
|
|
270
|
-
cwd: homedir(),
|
|
271
|
-
windowsHide: true,
|
|
233
|
+
/** Use a running web app, or start one through the shared rule and wait for it within the budget. */
|
|
234
|
+
async function ensureServer(settings, budgetMs, deps, result) {
|
|
235
|
+
const { ensureWebApp } = await import("../../services/web-ensure.js");
|
|
236
|
+
const answer = await ensureWebApp({
|
|
237
|
+
settings: { enabled: settings.enabled, baseUrl: settings.baseUrl },
|
|
238
|
+
budgetMs,
|
|
239
|
+
wait: true,
|
|
240
|
+
deps: {
|
|
241
|
+
fetch: deps.fetch,
|
|
242
|
+
now: deps.now,
|
|
243
|
+
sleep: deps.sleep,
|
|
244
|
+
...(deps.spawn ? { spawn: deps.spawn } : {}),
|
|
245
|
+
...(deps.resolveRuntime ? { resolveRuntime: deps.resolveRuntime } : {}),
|
|
246
|
+
...(deps.cliScript ? { cliScript: deps.cliScript } : {}),
|
|
247
|
+
...deps.ensureDeps,
|
|
248
|
+
},
|
|
272
249
|
});
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
while (remaining() > 0) {
|
|
278
|
-
await deps.sleep(Math.min(POLL_INTERVAL_MS, remaining()));
|
|
279
|
-
if (remaining() <= 0)
|
|
280
|
-
break;
|
|
281
|
-
if (await isHealthy(baseUrl, Math.min(HEALTH_PROBE_MS, remaining()), deps))
|
|
282
|
-
return "ok";
|
|
283
|
-
}
|
|
284
|
-
return "start-timeout";
|
|
250
|
+
result.spawned = answer === "started";
|
|
251
|
+
if (answer === "running" || answer === "started")
|
|
252
|
+
return "ok";
|
|
253
|
+
return answer === "start-timeout" ? "start-timeout" : "server-unreachable";
|
|
285
254
|
}
|
|
@@ -111,6 +111,24 @@ export default function ommsPiExtension(pi) {
|
|
|
111
111
|
code: error instanceof Error ? error.name : "unknown",
|
|
112
112
|
}));
|
|
113
113
|
}
|
|
114
|
+
if (process.env.OMMS_DISABLE_WEB_AUTOSTART !== "1") {
|
|
115
|
+
// Start the shared web app if none runs. The session does not wait for it.
|
|
116
|
+
void Promise.all([
|
|
117
|
+
import("../../services/web-ensure.js"),
|
|
118
|
+
import("../../services/web-api-auth.js"),
|
|
119
|
+
])
|
|
120
|
+
.then(([{ ensureWebApp }, { webServerUrl }]) => ensureWebApp({
|
|
121
|
+
settings: {
|
|
122
|
+
enabled: CONFIG.webServerEnabled,
|
|
123
|
+
baseUrl: webServerUrl(CONFIG.webServerHost, CONFIG.webServerPort),
|
|
124
|
+
},
|
|
125
|
+
budgetMs: 0,
|
|
126
|
+
wait: false,
|
|
127
|
+
}))
|
|
128
|
+
.catch((error) => log("Pi web app start failed", {
|
|
129
|
+
code: error instanceof Error ? error.name : "unknown",
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
114
132
|
// Retry turns whose capture failed earlier, with this session's capture model.
|
|
115
133
|
void import("../../services/capture-retry-drain.js")
|
|
116
134
|
.then(({ drainCaptureRetries, registerCaptureRetryDrain, startCaptureRetryDrain }) => {
|
package/dist/cli/web-command.js
CHANGED
|
@@ -182,6 +182,18 @@ export async function runWebCommand(args, options = {}, online, handover = {}) {
|
|
|
182
182
|
};
|
|
183
183
|
process.once("SIGINT", stop);
|
|
184
184
|
process.once("SIGTERM", stop);
|
|
185
|
+
// The page's Stop and Restart buttons reach the same process through this callback.
|
|
186
|
+
const { createPowerAction } = await import("./web-power.js");
|
|
187
|
+
server.setOnPowerAction(createPowerAction({
|
|
188
|
+
stopServer: async () => {
|
|
189
|
+
clearInterval(keeper);
|
|
190
|
+
process.off("SIGINT", stop);
|
|
191
|
+
process.off("SIGTERM", stop);
|
|
192
|
+
await server.stop();
|
|
193
|
+
},
|
|
194
|
+
loginItem: action === "--login-item",
|
|
195
|
+
autostart: options,
|
|
196
|
+
}));
|
|
185
197
|
// A newer `web install` asked this web app to give up the port.
|
|
186
198
|
server.setOnStepAside(async () => {
|
|
187
199
|
clearInterval(keeper);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { WebAutostartOptions } from "../services/web-autostart.js";
|
|
2
|
+
export type PowerAction = "stop" | "restart";
|
|
3
|
+
interface SpawnedChild {
|
|
4
|
+
pid?: number;
|
|
5
|
+
unref(): void;
|
|
6
|
+
on(event: "error", listener: (error: Error) => void): unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface PowerDeps {
|
|
9
|
+
spawn: (command: string, args: string[], options: {
|
|
10
|
+
detached: true;
|
|
11
|
+
stdio: "ignore";
|
|
12
|
+
cwd: string;
|
|
13
|
+
windowsHide: true;
|
|
14
|
+
}) => SpawnedChild;
|
|
15
|
+
/** Restart the login item through the service manager. False when it cannot. */
|
|
16
|
+
restartLoginItem: () => boolean | Promise<boolean>;
|
|
17
|
+
/** Write the start lock, naming `pid`, so a host that starts meanwhile waits. */
|
|
18
|
+
writeStartLock: (pid: number) => void | Promise<void>;
|
|
19
|
+
exit: (code: number) => void;
|
|
20
|
+
/** The runtime and arguments of this process, for a detached copy. */
|
|
21
|
+
execPath: string;
|
|
22
|
+
args: string[];
|
|
23
|
+
cwd: string;
|
|
24
|
+
pid: number;
|
|
25
|
+
log: (message: string, data: Record<string, unknown>) => void | Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The callback the web server runs after it replies to Stop or Restart.
|
|
29
|
+
* `stopServer` stops serving and any keep-alive timer. Restart starts a fresh
|
|
30
|
+
* copy on the same port: the service manager for the login item, and a detached
|
|
31
|
+
* copy otherwise. A detached child of the login item would die with its
|
|
32
|
+
* launchd job or systemd control group.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createPowerAction(options: {
|
|
35
|
+
stopServer: () => Promise<void>;
|
|
36
|
+
loginItem: boolean;
|
|
37
|
+
autostart?: WebAutostartOptions;
|
|
38
|
+
deps?: Partial<PowerDeps>;
|
|
39
|
+
}): (action: PowerAction) => Promise<void>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
function productionDeps(options) {
|
|
4
|
+
return {
|
|
5
|
+
spawn: nodeSpawn,
|
|
6
|
+
restartLoginItem: async () => (await import("../services/web-autostart.js")).restartWebAutostart(options),
|
|
7
|
+
writeStartLock: async (pid) => {
|
|
8
|
+
const { nodeLockFs, startLockPath } = await import("../services/web-ensure.js");
|
|
9
|
+
const path = startLockPath();
|
|
10
|
+
nodeLockFs.remove(path);
|
|
11
|
+
nodeLockFs.createExclusive(path, JSON.stringify({ pid, at: Date.now() }));
|
|
12
|
+
},
|
|
13
|
+
exit: (code) => process.exit(code),
|
|
14
|
+
execPath: process.execPath,
|
|
15
|
+
args: process.argv.slice(1),
|
|
16
|
+
cwd: homedir(),
|
|
17
|
+
pid: process.pid,
|
|
18
|
+
log: async (message, data) => (await import("../services/logger.js")).log(message, data),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The callback the web server runs after it replies to Stop or Restart.
|
|
23
|
+
* `stopServer` stops serving and any keep-alive timer. Restart starts a fresh
|
|
24
|
+
* copy on the same port: the service manager for the login item, and a detached
|
|
25
|
+
* copy otherwise. A detached child of the login item would die with its
|
|
26
|
+
* launchd job or systemd control group.
|
|
27
|
+
*/
|
|
28
|
+
export function createPowerAction(options) {
|
|
29
|
+
const deps = { ...productionDeps(options.autostart ?? {}), ...options.deps };
|
|
30
|
+
return async (action) => {
|
|
31
|
+
if (action === "stop") {
|
|
32
|
+
await options.stopServer();
|
|
33
|
+
deps.exit(0);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
// Hold other hosts off until the copy owns the port.
|
|
38
|
+
if (!options.loginItem)
|
|
39
|
+
await deps.writeStartLock(deps.pid);
|
|
40
|
+
await options.stopServer();
|
|
41
|
+
if (options.loginItem && (await deps.restartLoginItem()))
|
|
42
|
+
return deps.exit(0);
|
|
43
|
+
const child = deps.spawn(deps.execPath, deps.args, {
|
|
44
|
+
detached: true,
|
|
45
|
+
stdio: "ignore",
|
|
46
|
+
cwd: deps.cwd,
|
|
47
|
+
windowsHide: true,
|
|
48
|
+
});
|
|
49
|
+
child.on("error", () => undefined);
|
|
50
|
+
child.unref();
|
|
51
|
+
if (child.pid)
|
|
52
|
+
await deps.writeStartLock(child.pid);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
await Promise.resolve(deps.log("Web app restart failed", {
|
|
56
|
+
code: error instanceof Error ? error.name : "unknown",
|
|
57
|
+
})).catch(() => undefined);
|
|
58
|
+
}
|
|
59
|
+
deps.exit(0);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ImportHost } from "./import-args.js";
|
|
2
2
|
/**
|
|
3
|
-
* What a real web import could use right now, checked inside the
|
|
3
|
+
* What a real web import could use right now, checked inside the web app
|
|
4
4
|
* process that runs it. `env://` and `file://` keys were resolved when the
|
|
5
5
|
* config loaded, so a variable set only in the user's shell shows as missing.
|
|
6
6
|
* "Ready" means configured, not tested: the Health section runs test calls.
|
|
@@ -70,5 +70,10 @@ export function importBlockedReason(readiness, request) {
|
|
|
70
70
|
return state === "ready" ? null : EXTERNAL_REASONS[state];
|
|
71
71
|
}
|
|
72
72
|
const connected = readiness.opencode.models.some((model) => `${model.provider}/${model.model}` === request.modelChoice);
|
|
73
|
-
|
|
73
|
+
if (connected)
|
|
74
|
+
return null;
|
|
75
|
+
// The web app has no OpenCode session, so no host model is ever connected here.
|
|
76
|
+
return readiness.opencode.available
|
|
77
|
+
? "The chosen OpenCode model is not connected"
|
|
78
|
+
: "The chosen OpenCode model is not connected here. An import with an OpenCode signed-in model runs from the terminal or with /import in OpenCode. Choose the external API for a web import.";
|
|
74
79
|
}
|
|
@@ -94,22 +94,31 @@ export async function runSettingsHealth(input) {
|
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
if (input.testModels) {
|
|
97
|
-
await
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
:
|
|
105
|
-
|
|
106
|
-
userPrompt: PROBE,
|
|
107
|
-
context: PROBE,
|
|
108
|
-
sessionId: "health",
|
|
109
|
-
projectDirectory: input.directory,
|
|
97
|
+
const { resolveOpencodeHostModel } = await import("../services/ai/live-model-choice.js");
|
|
98
|
+
const { getOpencodeHostModels } = await import("./backfill-controls.js");
|
|
99
|
+
const opencodeRef = resolveOpencodeHostModel(CONFIG);
|
|
100
|
+
if (opencodeRef && !getOpencodeHostModels()) {
|
|
101
|
+
// The web app runs outside OpenCode, so an OpenCode signed-in model cannot be called here.
|
|
102
|
+
checks.push({
|
|
103
|
+
check: "OpenCode model test",
|
|
104
|
+
status: "warn",
|
|
105
|
+
reason: "Skipped: an OpenCode signed-in model can be tested only inside OpenCode. Run a capture in OpenCode, or set the external API as the capture model.",
|
|
110
106
|
});
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
await check("OpenCode model test", async () => {
|
|
110
|
+
const capture = opencodeRef
|
|
111
|
+
? (await createOpencodeImportModels(opencodeRef, input.directory)).capture
|
|
112
|
+
: (await import("./model-selection.js")).selectImportModel({}).capture;
|
|
113
|
+
await capture.summarize({
|
|
114
|
+
userPrompt: PROBE,
|
|
115
|
+
context: PROBE,
|
|
116
|
+
sessionId: "health",
|
|
117
|
+
projectDirectory: input.directory,
|
|
118
|
+
});
|
|
119
|
+
return "Fixed prompt completed";
|
|
120
|
+
});
|
|
121
|
+
}
|
|
113
122
|
if (pi.kind === "manual") {
|
|
114
123
|
await check("Pi model test", async () => {
|
|
115
124
|
const { selectImportModel } = await import("./model-selection.js");
|
package/dist/index.js
CHANGED
|
@@ -9,11 +9,9 @@ import { OPENCODE_IMPORT_COMMAND, OPENCODE_IMPORT_DESCRIPTION, latestUserMessage
|
|
|
9
9
|
import { buildRecentMemoriesSection, formatMemoriesForCompaction } from "./core/retrieval.js";
|
|
10
10
|
import { performUserProfileLearning } from "./adapters/opencode/profile-learning.js";
|
|
11
11
|
import { userPromptManager } from "./services/user-prompt/user-prompt-manager.js";
|
|
12
|
-
import { startWebServer, WebServer } from "./services/web-server.js";
|
|
13
12
|
import { pruneTraces } from "./services/capture-diagnostics.js";
|
|
14
13
|
import { ensureTursoReady } from "./services/turso/ready.js";
|
|
15
14
|
import { tursoConnectionManager } from "./services/turso/connection-manager.js";
|
|
16
|
-
import { WebAuth } from "./services/web-auth.js";
|
|
17
15
|
import { isConfigured, CONFIG, initConfigWithLegacyMigration } from "./config.js";
|
|
18
16
|
import { resolveOpencodeHostModel } from "./services/ai/live-model-choice.js";
|
|
19
17
|
import { log } from "./services/logger.js";
|
|
@@ -193,7 +191,6 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
193
191
|
logAutoCaptureProviderStatus();
|
|
194
192
|
const tags = getTags(directory);
|
|
195
193
|
const autoCaptureHost = createOpenCodeAutoCaptureHost(ctx);
|
|
196
|
-
let webServer = null;
|
|
197
194
|
let idleTimeout = null;
|
|
198
195
|
const GLOBAL_PLUGIN_WARMUP_KEY = Symbol.for("omms.plugin.warmedup");
|
|
199
196
|
if (!globalThis[GLOBAL_PLUGIN_WARMUP_KEY] && isConfigured()) {
|
|
@@ -211,8 +208,8 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
211
208
|
await configureOpencodeHostTransport(ctx);
|
|
212
209
|
// Shared profile dedup, conflict, description, and cleanup calls use OpenCode's host model.
|
|
213
210
|
registerOpencodeProfileModel();
|
|
214
|
-
//
|
|
215
|
-
// Awaited so
|
|
211
|
+
// In-process backfill, capture, and the capture retry drain use OpenCode's models.
|
|
212
|
+
// Awaited so those paths never run before the registry is filled.
|
|
216
213
|
await import("./adapters/opencode/backfill-startup.js")
|
|
217
214
|
.then(({ registerOpencodeImportModels, registerOpencodeCaptureRetryDrain }) => {
|
|
218
215
|
registerOpencodeImportModels();
|
|
@@ -290,96 +287,36 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
290
287
|
}
|
|
291
288
|
}
|
|
292
289
|
}
|
|
293
|
-
if (CONFIG.webServerEnabled &&
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
body: {
|
|
314
|
-
title: "Memory Explorer",
|
|
315
|
-
message: "Took over web server ownership",
|
|
316
|
-
variant: "success",
|
|
317
|
-
duration: 3000,
|
|
318
|
-
},
|
|
319
|
-
})
|
|
320
|
-
.catch(() => { });
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
webServer.setOnPortsExhaustedCallback(() => {
|
|
324
|
-
if (ctx.client?.tui) {
|
|
325
|
-
ctx.client.tui
|
|
326
|
-
.showToast({
|
|
327
|
-
body: {
|
|
328
|
-
title: "Memory Explorer",
|
|
329
|
-
message: `Web UI unavailable: ports ${CONFIG.webServerPort}-${CONFIG.webServerPort + 10} are held by non-responsive processes`,
|
|
330
|
-
variant: "error",
|
|
331
|
-
duration: 5000,
|
|
332
|
-
},
|
|
333
|
-
})
|
|
334
|
-
.catch(() => { });
|
|
290
|
+
if (CONFIG.webServerEnabled &&
|
|
291
|
+
tursoReadyForWeb &&
|
|
292
|
+
process.env.OMMS_DISABLE_WEB_AUTOSTART !== "1") {
|
|
293
|
+
const showToast = (message, variant, duration) => {
|
|
294
|
+
if (!ctx.client?.tui)
|
|
295
|
+
return;
|
|
296
|
+
ctx.client.tui
|
|
297
|
+
.showToast({ body: { title: "Memory Explorer", message, variant, duration } })
|
|
298
|
+
.catch(() => { });
|
|
299
|
+
};
|
|
300
|
+
// The shared web app serves the page. This plugin starts it only when none runs.
|
|
301
|
+
void Promise.all([import("./services/web-ensure.js"), import("./services/web-api-auth.js")])
|
|
302
|
+
.then(([{ ensureWebApp }, { webServerUrl }]) => {
|
|
303
|
+
const baseUrl = webServerUrl(CONFIG.webServerHost, CONFIG.webServerPort);
|
|
304
|
+
return ensureWebApp({
|
|
305
|
+
settings: { enabled: CONFIG.webServerEnabled, baseUrl },
|
|
306
|
+
budgetMs: 10_000,
|
|
307
|
+
}).then((result) => {
|
|
308
|
+
if (result === "running" || result === "started") {
|
|
309
|
+
showToast(`Web UI available at ${baseUrl}`, "info", 3000);
|
|
335
310
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (ctx.client?.tui) {
|
|
339
|
-
ctx.client.tui
|
|
340
|
-
.showToast({
|
|
341
|
-
body: {
|
|
342
|
-
title: "Memory Explorer",
|
|
343
|
-
message: webAuth.isEnabled()
|
|
344
|
-
? `Web UI started at ${url} (auth required)`
|
|
345
|
-
: `Web UI started at ${url}`,
|
|
346
|
-
variant: "success",
|
|
347
|
-
duration: 5000,
|
|
348
|
-
},
|
|
349
|
-
})
|
|
350
|
-
.catch(() => { });
|
|
311
|
+
else if (result === "port-busy") {
|
|
312
|
+
showToast(`Web UI unavailable: another program uses port ${CONFIG.webServerPort}`, "error", 5000);
|
|
351
313
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (ctx.client?.tui) {
|
|
355
|
-
ctx.client.tui
|
|
356
|
-
.showToast({
|
|
357
|
-
body: {
|
|
358
|
-
title: "Memory Explorer",
|
|
359
|
-
message: `Web UI available at ${url}`,
|
|
360
|
-
variant: "info",
|
|
361
|
-
duration: 3000,
|
|
362
|
-
},
|
|
363
|
-
})
|
|
364
|
-
.catch(() => { });
|
|
314
|
+
else if (result === "no-runtime") {
|
|
315
|
+
showToast("Web UI unavailable: no Node or Bun runtime found to start it", "error", 5000);
|
|
365
316
|
}
|
|
366
|
-
}
|
|
317
|
+
});
|
|
367
318
|
})
|
|
368
|
-
.catch((error) => {
|
|
369
|
-
log("Web server failed to start", { error: String(error) });
|
|
370
|
-
if (ctx.client?.tui) {
|
|
371
|
-
ctx.client.tui
|
|
372
|
-
.showToast({
|
|
373
|
-
body: {
|
|
374
|
-
title: "Memory Explorer Error",
|
|
375
|
-
message: `Failed to start: ${String(error)}`,
|
|
376
|
-
variant: "error",
|
|
377
|
-
duration: 5000,
|
|
378
|
-
},
|
|
379
|
-
})
|
|
380
|
-
.catch(() => { });
|
|
381
|
-
}
|
|
382
|
-
});
|
|
319
|
+
.catch((error) => log("Web app start failed", { error: String(error) }));
|
|
383
320
|
}
|
|
384
321
|
let cleanedUp = false;
|
|
385
322
|
const cleanupPlugin = async () => {
|
|
@@ -390,8 +327,6 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
390
327
|
clearTimeout(idleTimeout);
|
|
391
328
|
idleTimeout = null;
|
|
392
329
|
}
|
|
393
|
-
if (webServer)
|
|
394
|
-
await webServer.stop();
|
|
395
330
|
if (memoryClient)
|
|
396
331
|
await memoryClient.close();
|
|
397
332
|
};
|
|
@@ -607,12 +542,10 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
607
542
|
idleTimeout = setTimeout(async () => {
|
|
608
543
|
try {
|
|
609
544
|
await performAutoCapture(autoCaptureHost, sessionID, directory);
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
await cleanupService.runCleanup();
|
|
615
|
-
}
|
|
545
|
+
await performUserProfileLearning(ctx, directory);
|
|
546
|
+
const { cleanupService } = await import("./services/cleanup-service.js");
|
|
547
|
+
if (await cleanupService.shouldRunCleanup())
|
|
548
|
+
await cleanupService.runCleanup();
|
|
616
549
|
}
|
|
617
550
|
catch (error) {
|
|
618
551
|
log("Idle processing error", { error: String(error) });
|
|
@@ -24,6 +24,12 @@ export declare function webAutostartStatus(options?: WebAutostartOptions): WebAu
|
|
|
24
24
|
export declare function installWebAutostart(options?: WebAutostartOptions): WebAutostartStatus;
|
|
25
25
|
/** Remove only an item carrying the OMMS marker. */
|
|
26
26
|
export declare function removeWebAutostart(options?: WebAutostartOptions): WebAutostartStatus;
|
|
27
|
+
/**
|
|
28
|
+
* Restart the login item through the platform service manager. A detached child
|
|
29
|
+
* of the item would die with its launchd job or systemd control group, so the
|
|
30
|
+
* service manager does the restart. Returns false when it cannot.
|
|
31
|
+
*/
|
|
32
|
+
export declare function restartWebAutostart(options?: WebAutostartOptions): boolean;
|
|
27
33
|
/** Host start checks the global settings. A failed item cannot block a session. */
|
|
28
34
|
export declare function reconcileWebAutostart(config?: Pick<typeof CONFIG, "webServerAutoStart" | "webServerEnabled">, options?: WebAutostartOptions): void;
|
|
29
35
|
export {};
|
|
@@ -211,6 +211,37 @@ export function removeWebAutostart(options = {}) {
|
|
|
211
211
|
unlinkSync(path);
|
|
212
212
|
return { state: "not-installed", path };
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Restart the login item through the platform service manager. A detached child
|
|
216
|
+
* of the item would die with its launchd job or systemd control group, so the
|
|
217
|
+
* service manager does the restart. Returns false when it cannot.
|
|
218
|
+
*/
|
|
219
|
+
export function restartWebAutostart(options = {}) {
|
|
220
|
+
const home = options.home ?? homedir();
|
|
221
|
+
const platform = options.platform ?? process.platform;
|
|
222
|
+
try {
|
|
223
|
+
if (platform === "darwin") {
|
|
224
|
+
runCommand(options, home, "launchctl", [
|
|
225
|
+
"kickstart",
|
|
226
|
+
"-k",
|
|
227
|
+
`gui/${process.getuid?.() ?? 0}/${NAME}`,
|
|
228
|
+
]);
|
|
229
|
+
}
|
|
230
|
+
else if (platform === "linux") {
|
|
231
|
+
runCommand(options, home, "systemctl", ["--user", "restart", "omms-web.service"]);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
log("OMMS login item restart failed", {
|
|
240
|
+
code: error instanceof Error ? error.name : "unknown",
|
|
241
|
+
});
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
214
245
|
/** Host start checks the global settings. A failed item cannot block a session. */
|
|
215
246
|
export function reconcileWebAutostart(config = CONFIG, options = {}) {
|
|
216
247
|
if (process.env.OMMS_DISABLE_WEB_AUTOSTART === "1")
|