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.
Files changed (115) hide show
  1. package/README.md +3 -5
  2. package/dist/adapters/opencode/auto-capture-summary.js +50 -8
  3. package/dist/adapters/opencode/backfill-models.d.ts +25 -0
  4. package/dist/adapters/opencode/backfill-models.js +39 -0
  5. package/dist/adapters/opencode/backfill-startup.d.ts +7 -0
  6. package/dist/adapters/opencode/backfill-startup.js +12 -0
  7. package/dist/adapters/opencode/import-command.d.ts +3 -7
  8. package/dist/adapters/opencode/import-command.js +6 -54
  9. package/dist/adapters/pi/backfill-models.d.ts +9 -0
  10. package/dist/adapters/pi/backfill-models.js +30 -0
  11. package/dist/adapters/pi/extension.js +53 -0
  12. package/dist/adapters/pi/import-command.js +3 -0
  13. package/dist/adapters/pi/profile.js +2 -1
  14. package/dist/adapters/pi/provider.js +41 -14
  15. package/dist/cli/index.js +13 -1
  16. package/dist/cli/web-command.d.ts +3 -0
  17. package/dist/cli/web-command.js +72 -0
  18. package/dist/config.d.ts +30 -1
  19. package/dist/config.js +131 -9
  20. package/dist/core/capture-context.js +1 -1
  21. package/dist/core/capture.d.ts +4 -0
  22. package/dist/core/capture.js +30 -0
  23. package/dist/core/extraction.d.ts +23 -1
  24. package/dist/core/extraction.js +41 -0
  25. package/dist/core/host.d.ts +23 -0
  26. package/dist/core/host.js +9 -1
  27. package/dist/core/profile-analysis.js +9 -1
  28. package/dist/importer/auto-backfill.d.ts +20 -0
  29. package/dist/importer/auto-backfill.js +165 -0
  30. package/dist/importer/backfill-lock.d.ts +3 -0
  31. package/dist/importer/backfill-lock.js +44 -0
  32. package/dist/importer/backfill-model.d.ts +10 -0
  33. package/dist/importer/backfill-model.js +13 -0
  34. package/dist/importer/discovery.d.ts +32 -5
  35. package/dist/importer/discovery.js +88 -36
  36. package/dist/importer/import-args.d.ts +23 -1
  37. package/dist/importer/import-args.js +29 -1
  38. package/dist/importer/import-project.d.ts +16 -0
  39. package/dist/importer/import-project.js +35 -0
  40. package/dist/importer/import-readiness.d.ts +47 -0
  41. package/dist/importer/import-readiness.js +54 -0
  42. package/dist/importer/import-sessions.d.ts +98 -0
  43. package/dist/importer/import-sessions.js +222 -0
  44. package/dist/importer/import-sources.d.ts +47 -0
  45. package/dist/importer/import-sources.js +147 -0
  46. package/dist/importer/importer.d.ts +31 -1
  47. package/dist/importer/importer.js +96 -24
  48. package/dist/importer/manual-import-guard.d.ts +4 -0
  49. package/dist/importer/manual-import-guard.js +9 -0
  50. package/dist/importer/opencode-import.d.ts +11 -1
  51. package/dist/importer/opencode-import.js +133 -69
  52. package/dist/importer/opencode-project.d.ts +1 -4
  53. package/dist/importer/opencode-project.js +3 -36
  54. package/dist/importer/opencode-reader.d.ts +25 -5
  55. package/dist/importer/opencode-reader.js +92 -106
  56. package/dist/importer/opencode-snapshot.d.ts +82 -0
  57. package/dist/importer/opencode-snapshot.js +276 -0
  58. package/dist/importer/profile-import.d.ts +1 -0
  59. package/dist/importer/profile-import.js +7 -1
  60. package/dist/importer/run-import.d.ts +10 -0
  61. package/dist/importer/run-import.js +21 -0
  62. package/dist/importer/settings-health.d.ts +24 -0
  63. package/dist/importer/settings-health.js +127 -0
  64. package/dist/importer/web-import-api.d.ts +8 -0
  65. package/dist/importer/web-import-api.js +8 -0
  66. package/dist/importer/web-import-jobs.d.ts +53 -0
  67. package/dist/importer/web-import-jobs.js +227 -0
  68. package/dist/index.js +34 -0
  69. package/dist/services/ai/live-model-choice.js +6 -3
  70. package/dist/services/ai/opencode-import-models.d.ts +11 -0
  71. package/dist/services/ai/opencode-import-models.js +55 -0
  72. package/dist/services/ai/opencode-provider.d.ts +9 -0
  73. package/dist/services/ai/opencode-provider.js +31 -4
  74. package/dist/services/ai/providers/anthropic-messages.js +14 -6
  75. package/dist/services/ai/providers/base-provider.d.ts +8 -0
  76. package/dist/services/ai/providers/base-provider.js +12 -0
  77. package/dist/services/ai/providers/google-gemini.js +21 -7
  78. package/dist/services/ai/providers/openai-chat-completion.js +20 -7
  79. package/dist/services/ai/providers/openai-responses.js +11 -0
  80. package/dist/services/backfill-state.d.ts +25 -0
  81. package/dist/services/backfill-state.js +68 -0
  82. package/dist/services/capture-attempt-store.d.ts +21 -0
  83. package/dist/services/capture-attempt-store.js +108 -0
  84. package/dist/services/capture-diagnostics.d.ts +66 -0
  85. package/dist/services/capture-diagnostics.js +175 -0
  86. package/dist/services/cleanup-service.js +9 -0
  87. package/dist/services/global-config-writer.d.ts +10 -0
  88. package/dist/services/global-config-writer.js +110 -0
  89. package/dist/services/live-captured-entries.d.ts +3 -0
  90. package/dist/services/live-captured-entries.js +34 -0
  91. package/dist/services/log-path.d.ts +2 -0
  92. package/dist/services/log-path.js +15 -0
  93. package/dist/services/logger.js +1 -12
  94. package/dist/services/safe-health-error.d.ts +2 -0
  95. package/dist/services/safe-health-error.js +11 -0
  96. package/dist/services/settings-log.d.ts +5 -0
  97. package/dist/services/settings-log.js +36 -0
  98. package/dist/services/settings-models.d.ts +35 -0
  99. package/dist/services/settings-models.js +39 -0
  100. package/dist/services/settings-snapshot.d.ts +24 -0
  101. package/dist/services/settings-snapshot.js +83 -0
  102. package/dist/services/settings-traces.d.ts +7 -0
  103. package/dist/services/settings-traces.js +39 -0
  104. package/dist/services/user-memory-learning.js +2 -1
  105. package/dist/services/web-autostart.d.ts +29 -0
  106. package/dist/services/web-autostart.js +208 -0
  107. package/dist/services/web-server.d.ts +5 -0
  108. package/dist/services/web-server.js +187 -3
  109. package/dist/v2/legacy-client.js +19 -2
  110. package/dist/web/assets/index-B-_gMSn9.js +124 -0
  111. package/dist/web/assets/index-D5ak5KsI.css +2 -0
  112. package/dist/web/index.html +2 -2
  113. package/package.json +2 -1
  114. package/dist/web/assets/index-DPjEpD1h.css +0 -2
  115. package/dist/web/assets/index-do26c4JJ.js +0 -122
@@ -0,0 +1,39 @@
1
+ export async function listOpencodeSettingsModels(client) {
2
+ try {
3
+ const active = client ?? (await import("./ai/opencode-provider.js")).getV2Client();
4
+ if (!active)
5
+ return { available: false, reason: "OpenCode model list unavailable" };
6
+ const result = await active.provider.list();
7
+ if (!result.data)
8
+ return { available: false, reason: "OpenCode model list unavailable" };
9
+ const connected = new Set(result.data.connected);
10
+ const models = result.data.all
11
+ .filter((provider) => connected.has(provider.id))
12
+ .flatMap((provider) => Object.entries(provider.models).map(([model, info]) => ({
13
+ provider: provider.id,
14
+ model,
15
+ name: info.name,
16
+ })));
17
+ return { available: true, models };
18
+ }
19
+ catch {
20
+ return { available: false, reason: "OpenCode model list unavailable" };
21
+ }
22
+ }
23
+ export async function listPiSettingsModels(loadSdk = () => import("@earendil-works/pi-coding-agent")) {
24
+ try {
25
+ const { ModelRuntime } = await loadSdk();
26
+ // Refresh local auth availability without fetching models or writing Pi credentials.
27
+ const runtime = await ModelRuntime.create({ refreshOnCreate: true, allowModelNetwork: false });
28
+ const models = runtime
29
+ .getModels()
30
+ .filter((model) => runtime.hasConfiguredAuth(model.provider))
31
+ .map((model) => ({ provider: model.provider, model: model.id, name: model.name }));
32
+ return models.length
33
+ ? { available: true, models }
34
+ : { available: false, models, reason: "No signed-in Pi models found" };
35
+ }
36
+ catch {
37
+ return { available: false, reason: "Pi model list unavailable; enter provider/model" };
38
+ }
39
+ }
@@ -0,0 +1,24 @@
1
+ export declare function getSettingsSnapshot(directory: string): {
2
+ revision: string;
3
+ settings: {
4
+ [k: string]: {
5
+ value: unknown;
6
+ source: string;
7
+ globalValue: {} | undefined;
8
+ };
9
+ };
10
+ secrets: {
11
+ [k: string]: {
12
+ set: boolean;
13
+ source: string | null;
14
+ };
15
+ };
16
+ fallback: {
17
+ model: string | null;
18
+ configured: boolean;
19
+ };
20
+ effective: {
21
+ opencode: import("./ai/live-model-choice.js").AutoCaptureProviderStatus;
22
+ pi: import("./ai/live-model-choice.js").PiLiveModelChoice;
23
+ };
24
+ };
@@ -0,0 +1,83 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parse } from "jsonc-parser";
4
+ import { CONFIG, getGlobalConfigSourcePath } from "../config.js";
5
+ import { readGlobalConfigRevision } from "./global-config-writer.js";
6
+ import { getAutoCaptureProviderStatus, isExternalModelReady, resolvePiLiveModel, } from "./ai/live-model-choice.js";
7
+ const editable = [
8
+ "opencodeProvider",
9
+ "opencodeModel",
10
+ "piProvider",
11
+ "piModel",
12
+ "captureTrace",
13
+ "captureTraceRetentionDays",
14
+ "captureAttemptRetentionDays",
15
+ "autoBackfill",
16
+ "opencodeBackfillModel",
17
+ "piBackfillModel",
18
+ "webServerAutoStart",
19
+ ];
20
+ function readSettingsFile(path) {
21
+ if (!path)
22
+ return {};
23
+ const errors = [];
24
+ const result = parse(readFileSync(path, "utf8"), errors, { allowTrailingComma: true });
25
+ if (errors.length || !result || typeof result !== "object" || Array.isArray(result)) {
26
+ throw new Error("Config file cannot be parsed");
27
+ }
28
+ return result;
29
+ }
30
+ function projectFile(directory) {
31
+ return ["omms.jsonc", "omms.json", "opencode-mem.jsonc", "opencode-mem.json"]
32
+ .map((name) => join(directory, ".opencode", name))
33
+ .find((path) => existsSync(path));
34
+ }
35
+ export function getSettingsSnapshot(directory) {
36
+ const global = readSettingsFile(getGlobalConfigSourcePath());
37
+ const project = readSettingsFile(projectFile(directory));
38
+ const settings = Object.fromEntries(editable.map((key) => {
39
+ const projectOverrides = Object.hasOwn(project, key) &&
40
+ (key === "captureTrace"
41
+ ? project[key] === false
42
+ : !key.endsWith("RetentionDays") &&
43
+ ![
44
+ "autoBackfill",
45
+ "opencodeBackfillModel",
46
+ "piBackfillModel",
47
+ "webServerAutoStart",
48
+ ].includes(key));
49
+ const source = projectOverrides
50
+ ? "project"
51
+ : Object.hasOwn(global, key)
52
+ ? "global"
53
+ : "default";
54
+ const value = projectOverrides ? project[key] : (global[key] ?? CONFIG[key]);
55
+ return [key, { value, source, globalValue: global[key] ?? CONFIG[key] }];
56
+ }));
57
+ const secrets = Object.fromEntries(["memoryApiKey", "embeddingApiKey", "webServerApiToken", "webServerAuthPassword"].map((key) => {
58
+ const raw = global[key];
59
+ return [
60
+ key,
61
+ {
62
+ set: typeof raw === "string" && raw.length > 0,
63
+ source: typeof raw === "string"
64
+ ? raw.startsWith("env://")
65
+ ? "env"
66
+ : raw.startsWith("file://")
67
+ ? "file"
68
+ : "literal"
69
+ : null,
70
+ },
71
+ ];
72
+ }));
73
+ return {
74
+ revision: readGlobalConfigRevision(),
75
+ settings,
76
+ secrets,
77
+ fallback: { model: CONFIG.memoryModel ?? null, configured: isExternalModelReady(CONFIG) },
78
+ effective: {
79
+ opencode: getAutoCaptureProviderStatus(CONFIG),
80
+ pi: resolvePiLiveModel(CONFIG),
81
+ },
82
+ };
83
+ }
@@ -0,0 +1,7 @@
1
+ export declare function listSettingsTraces(): Promise<{
2
+ file: string;
3
+ date: string | undefined;
4
+ size: number;
5
+ }[]>;
6
+ export declare function readSettingsTrace(file: string): Promise<string>;
7
+ export declare function deleteSettingsTrace(file: string): Promise<void>;
@@ -0,0 +1,39 @@
1
+ import { lstat, readdir, readFile, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { getTraceDirectory } from "./capture-diagnostics.js";
4
+ const traceName = /^capture-(\d{4}-\d{2}-\d{2})\.jsonl$/;
5
+ function tracePath(file) {
6
+ if (!traceName.test(file))
7
+ throw new Error("Invalid trace file name");
8
+ return join(getTraceDirectory(), file);
9
+ }
10
+ export async function listSettingsTraces() {
11
+ let files;
12
+ try {
13
+ files = await readdir(getTraceDirectory());
14
+ }
15
+ catch (error) {
16
+ if (error.code === "ENOENT")
17
+ return [];
18
+ throw error;
19
+ }
20
+ const traces = await Promise.all(files
21
+ .filter((file) => traceName.test(file))
22
+ .map(async (file) => {
23
+ const stats = await lstat(tracePath(file));
24
+ return stats.isFile() ? { file, date: traceName.exec(file)[1], size: stats.size } : null;
25
+ }));
26
+ return traces.filter((trace) => trace !== null).sort((a, b) => b.file.localeCompare(a.file));
27
+ }
28
+ export async function readSettingsTrace(file) {
29
+ const path = tracePath(file);
30
+ if (!(await lstat(path)).isFile())
31
+ throw new Error("Invalid trace file");
32
+ return readFile(path, "utf8");
33
+ }
34
+ export async function deleteSettingsTrace(file) {
35
+ const path = tracePath(file);
36
+ if (!(await lstat(path)).isFile())
37
+ throw new Error("Invalid trace file");
38
+ await unlink(path);
39
+ }
@@ -1,6 +1,6 @@
1
1
  import { getTags } from "./tags.js";
2
2
  import { log } from "./logger.js";
3
- import { CONFIG } from "../config.js";
3
+ import { CONFIG, refreshConfigIfChanged } from "../config.js";
4
4
  import { resolveOpencodeHostModel } from "./ai/live-model-choice.js";
5
5
  import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
6
6
  import { userProfileManager } from "./user-profile/user-profile-manager.js";
@@ -42,6 +42,7 @@ async function runAutomaticProfileCleanup(userId) {
42
42
  export async function performUserProfileLearning(ctx, directory) {
43
43
  if (isLearningRunning)
44
44
  return;
45
+ refreshConfigIfChanged(directory);
45
46
  if (!CONFIG.autoCaptureProviderStatus || !CONFIG.autoCaptureProviderStatus.ready) {
46
47
  log("user-profile-learning: skipped (provider not ready)", {
47
48
  issues: CONFIG.autoCaptureProviderStatus?.issues ?? ["status undefined"],
@@ -0,0 +1,29 @@
1
+ import { CONFIG } from "../config.js";
2
+ type State = "installed" | "not-installed" | "unsupported" | "no-runtime" | "no-package";
3
+ export type WebAutostartStatus = {
4
+ state: State;
5
+ path?: string;
6
+ runtime?: string;
7
+ packagePath?: string;
8
+ command?: string;
9
+ };
10
+ export interface WebAutostartOptions {
11
+ home?: string;
12
+ platform?: string;
13
+ runtime?: string | null;
14
+ packageRoot?: string | null;
15
+ systemctlAvailable?: boolean;
16
+ run?: (command: string, args: string[]) => void;
17
+ start?: boolean;
18
+ }
19
+ /** OpenCode's executable is not a JavaScript runtime. */
20
+ export declare function resolveWebRuntime(execPath?: string, platform?: NodeJS.Platform): string | null;
21
+ /** Read the login item's state without changing any system files. */
22
+ export declare function webAutostartStatus(options?: WebAutostartOptions): WebAutostartStatus;
23
+ /** Install or update only the fixed-name OMMS item. `start` launches it now. */
24
+ export declare function installWebAutostart(options?: WebAutostartOptions): WebAutostartStatus;
25
+ /** Remove only an item carrying the OMMS marker. */
26
+ export declare function removeWebAutostart(options?: WebAutostartOptions): WebAutostartStatus;
27
+ /** Host start checks the global settings. A failed item cannot block a session. */
28
+ export declare function reconcileWebAutostart(config?: Pick<typeof CONFIG, "webServerAutoStart" | "webServerEnabled">, options?: WebAutostartOptions): void;
29
+ export {};
@@ -0,0 +1,208 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import { accessSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, constants, } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { basename, dirname, join, delimiter, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { CONFIG } from "../config.js";
8
+ import { log } from "./logger.js";
9
+ const MARKER = "OMMS login item";
10
+ const NAME = "io.github.cmdaltctr.omms.web";
11
+ function executable(name) {
12
+ for (const part of (process.env.PATH ?? "").split(delimiter)) {
13
+ if (!part)
14
+ continue;
15
+ const path = resolve(part, name);
16
+ try {
17
+ accessSync(path, constants.X_OK);
18
+ return path;
19
+ }
20
+ catch {
21
+ /* Search the next directory. */
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ /** OpenCode's executable is not a JavaScript runtime. */
27
+ export function resolveWebRuntime(execPath = process.execPath, platform = process.platform) {
28
+ const name = basename(execPath).toLowerCase();
29
+ if (["node", "bun", "node.exe", "bun.exe"].includes(name))
30
+ return execPath;
31
+ const suffix = platform === "win32" ? ".exe" : "";
32
+ return executable(`node${suffix}`) ?? executable(`bun${suffix}`);
33
+ }
34
+ function packageRoot() {
35
+ let dir = dirname(fileURLToPath(import.meta.url));
36
+ for (;;) {
37
+ const file = join(dir, "package.json");
38
+ try {
39
+ if (JSON.parse(readFileSync(file, "utf8")).name === "om-memory-system")
40
+ return dir;
41
+ }
42
+ catch {
43
+ /* Try the parent. */
44
+ }
45
+ const parent = dirname(dir);
46
+ if (parent === dir)
47
+ return null;
48
+ dir = parent;
49
+ }
50
+ }
51
+ function itemPath(home, platform) {
52
+ if (platform === "darwin")
53
+ return join(home, "Library", "LaunchAgents", `${NAME}.plist`);
54
+ if (platform === "linux")
55
+ return join(home, ".config", "systemd", "user", "omms-web.service");
56
+ if (platform === "win32")
57
+ return join(home, "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "omms-web.cmd");
58
+ return null;
59
+ }
60
+ function details(options) {
61
+ const home = options.home ?? homedir();
62
+ const platform = options.platform ?? process.platform;
63
+ const path = itemPath(home, platform);
64
+ const runtime = options.runtime === undefined ? resolveWebRuntime() : options.runtime;
65
+ const root = options.packageRoot === undefined ? packageRoot() : options.packageRoot;
66
+ const supported = platform !== "linux" || (options.systemctlAvailable ?? Boolean(executable("systemctl")));
67
+ return { home, platform, path, runtime, root, supported };
68
+ }
69
+ const xml = (value) => value
70
+ .replaceAll("&", "&amp;")
71
+ .replaceAll("<", "&lt;")
72
+ .replaceAll(">", "&gt;")
73
+ .replaceAll('"', "&quot;");
74
+ const systemd = (value) => `"${value.replaceAll("%", "%%").replaceAll('"', '\\"')}"`;
75
+ const windows = (value) => `"${value.replaceAll("%", "%%")}"`;
76
+ function itemContent(platform, runtime, cli) {
77
+ if (platform === "darwin")
78
+ return `<?xml version="1.0" encoding="UTF-8"?>
79
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
80
+ <plist version="1.0"><dict>
81
+ <!-- ${MARKER} -->
82
+ <key>Label</key><string>${NAME}</string>
83
+ <key>ProgramArguments</key><array><string>${xml(runtime)}</string><string>${xml(cli)}</string><string>web</string><string>--login-item</string></array>
84
+ <key>RunAtLoad</key><true/><key>KeepAlive</key><false/>
85
+ <key>StandardOutPath</key><string>/dev/null</string><key>StandardErrorPath</key><string>/dev/null</string>
86
+ </dict></plist>\n`;
87
+ if (platform === "linux")
88
+ return `# ${MARKER}
89
+ [Unit]\nDescription=OMMS web app\n[Service]\nType=simple\nExecStart=${systemd(runtime)} ${systemd(cli)} web --login-item\n[Install]\nWantedBy=default.target\n`;
90
+ return `@echo off\r\nrem ${MARKER}\r\nstart "" /min ${windows(runtime)} ${windows(cli)} web --login-item\r\n`;
91
+ }
92
+ function owned(path) {
93
+ return readFileSync(path, "utf8").includes(MARKER);
94
+ }
95
+ function runCommand(options, home, command, args) {
96
+ if (options.run)
97
+ options.run(command, args);
98
+ else
99
+ execFileSync(command, args, { cwd: home, stdio: "ignore", timeout: 5_000 });
100
+ }
101
+ /** Read the login item's state without changing any system files. */
102
+ export function webAutostartStatus(options = {}) {
103
+ const { path, runtime, root, supported } = details(options);
104
+ if (!path || !supported)
105
+ return { state: "unsupported" };
106
+ if (!runtime)
107
+ return { state: "no-runtime", path };
108
+ if (!root)
109
+ return { state: "no-package", path, runtime };
110
+ if (!existsSync(path))
111
+ return { state: "not-installed", path, runtime, packagePath: root };
112
+ if (!owned(path))
113
+ throw new Error("Login item at the OMMS path is not owned by OMMS");
114
+ return {
115
+ state: "installed",
116
+ path,
117
+ runtime,
118
+ packagePath: root,
119
+ command: readFileSync(path, "utf8"),
120
+ };
121
+ }
122
+ /** Install or update only the fixed-name OMMS item. `start` launches it now. */
123
+ export function installWebAutostart(options = {}) {
124
+ const status = webAutostartStatus(options);
125
+ if (status.state === "unsupported" ||
126
+ status.state === "no-runtime" ||
127
+ status.state === "no-package")
128
+ return status;
129
+ const { home, platform, path, runtime, root } = details(options);
130
+ const file = path;
131
+ const content = itemContent(platform, runtime, join(root, "dist", "cli", "index.js"));
132
+ mkdirSync(dirname(file), { recursive: true });
133
+ const changed = !existsSync(file) || readFileSync(file, "utf8") !== content;
134
+ if (changed) {
135
+ const temp = `${file}.${randomUUID()}.tmp`;
136
+ try {
137
+ writeFileSync(temp, content, { mode: 0o600 });
138
+ renameSync(temp, file);
139
+ }
140
+ finally {
141
+ if (existsSync(temp))
142
+ unlinkSync(temp);
143
+ }
144
+ }
145
+ if (platform === "linux") {
146
+ if (changed)
147
+ runCommand(options, home, "systemctl", ["--user", "daemon-reload"]);
148
+ // Re-enable even if the unit file is unchanged; the user service may have been disabled.
149
+ runCommand(options, home, "systemctl", ["--user", "enable", "omms-web.service"]);
150
+ if (options.start)
151
+ runCommand(options, home, "systemctl", ["--user", "start", "omms-web.service"]);
152
+ }
153
+ if (platform === "darwin" && options.start) {
154
+ const domain = `gui/${process.getuid?.() ?? 0}`;
155
+ try {
156
+ runCommand(options, home, "launchctl", ["bootout", domain, file]);
157
+ }
158
+ catch {
159
+ /* Not loaded. */
160
+ }
161
+ runCommand(options, home, "launchctl", ["bootstrap", domain, file]);
162
+ }
163
+ return webAutostartStatus(options);
164
+ }
165
+ /** Remove only an item carrying the OMMS marker. */
166
+ export function removeWebAutostart(options = {}) {
167
+ const { home, platform, path, supported } = details(options);
168
+ if (!path)
169
+ return { state: "unsupported" };
170
+ if (!existsSync(path))
171
+ return { state: "not-installed", path };
172
+ if (!owned(path))
173
+ throw new Error("Login item at the OMMS path is not owned by OMMS");
174
+ if (platform === "darwin" && options.start) {
175
+ try {
176
+ runCommand(options, home, "launchctl", ["bootout", `gui/${process.getuid?.() ?? 0}`, path]);
177
+ }
178
+ catch {
179
+ /* A registered item may not be loaded. */
180
+ }
181
+ }
182
+ if (platform === "linux" && supported) {
183
+ runCommand(options, home, "systemctl", [
184
+ "--user",
185
+ "disable",
186
+ ...(options.start ? ["--now"] : []),
187
+ "omms-web.service",
188
+ ]);
189
+ }
190
+ unlinkSync(path);
191
+ return { state: "not-installed", path };
192
+ }
193
+ /** Host start checks the global settings. A failed item cannot block a session. */
194
+ export function reconcileWebAutostart(config = CONFIG, options = {}) {
195
+ if (process.env.OMMS_DISABLE_WEB_AUTOSTART === "1")
196
+ return;
197
+ try {
198
+ if (config.webServerAutoStart && config.webServerEnabled)
199
+ installWebAutostart(options);
200
+ else
201
+ removeWebAutostart(options);
202
+ }
203
+ catch (error) {
204
+ log("OMMS login item reconciliation failed", {
205
+ code: error instanceof Error ? error.name : "unknown",
206
+ });
207
+ }
208
+ }
@@ -19,6 +19,7 @@ export declare function nextFallbackPort(currentPort: number, failedTakeovers: n
19
19
  interface WebServerConfig {
20
20
  port: number;
21
21
  host: string;
22
+ directory?: string;
22
23
  enabled: boolean;
23
24
  auth?: WebAuth;
24
25
  apiToken?: string;
@@ -34,6 +35,10 @@ export declare class WebServer {
34
35
  private portsExhaustedNotified;
35
36
  private takeoverFailures;
36
37
  private readonly maxFallbackPort;
38
+ private settingsImportJobs?;
39
+ /** Run an import-page action; errors carry their own status and never file contents. */
40
+ private importResponse;
41
+ private importJobs;
37
42
  constructor(config: WebServerConfig);
38
43
  setOnTakeoverCallback(callback: () => Promise<void>): void;
39
44
  setOnPortsExhaustedCallback(callback: () => void): void;