om-memory-system 3.4.0-next.21 → 3.4.0-next.26

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 (82) hide show
  1. package/README.md +21 -7
  2. package/dist/adapters/opencode/backfill-models.js +5 -1
  3. package/dist/adapters/opencode/backfill-startup.d.ts +6 -0
  4. package/dist/adapters/opencode/backfill-startup.js +5 -0
  5. package/dist/adapters/opencode/import-command.js +1 -0
  6. package/dist/adapters/opencode/user-prompt.d.ts +2 -1
  7. package/dist/adapters/opencode/user-prompt.js +2 -9
  8. package/dist/adapters/pi/backfill-models.d.ts +4 -0
  9. package/dist/adapters/pi/backfill-models.js +3 -0
  10. package/dist/adapters/pi/capture.d.ts +1 -1
  11. package/dist/adapters/pi/capture.js +1 -1
  12. package/dist/adapters/pi/extension.js +7 -0
  13. package/dist/adapters/pi/import-command.js +1 -0
  14. package/dist/cli/index.js +13 -1
  15. package/dist/config.d.ts +11 -4
  16. package/dist/config.js +29 -6
  17. package/dist/core/internal-prompt.d.ts +6 -0
  18. package/dist/core/internal-prompt.js +11 -0
  19. package/dist/importer/auto-backfill.d.ts +5 -0
  20. package/dist/importer/auto-backfill.js +29 -10
  21. package/dist/importer/backfill-controls.d.ts +37 -0
  22. package/dist/importer/backfill-controls.js +119 -0
  23. package/dist/importer/backfill-lock.d.ts +1 -0
  24. package/dist/importer/backfill-lock.js +2 -2
  25. package/dist/importer/backfill-model.d.ts +1 -1
  26. package/dist/importer/backfill-model.js +2 -0
  27. package/dist/importer/external-api-test.d.ts +18 -0
  28. package/dist/importer/external-api-test.js +50 -0
  29. package/dist/importer/external-backfill-models.d.ts +15 -0
  30. package/dist/importer/external-backfill-models.js +14 -0
  31. package/dist/importer/import-path-maps.d.ts +14 -0
  32. package/dist/importer/import-path-maps.js +49 -0
  33. package/dist/importer/import-progress.d.ts +27 -0
  34. package/dist/importer/import-progress.js +38 -0
  35. package/dist/importer/import-runs.d.ts +43 -0
  36. package/dist/importer/import-runs.js +141 -0
  37. package/dist/importer/import-sessions.js +9 -9
  38. package/dist/importer/importer.d.ts +2 -1
  39. package/dist/importer/importer.js +2 -2
  40. package/dist/importer/map-suggestions.d.ts +39 -0
  41. package/dist/importer/map-suggestions.js +133 -0
  42. package/dist/importer/opencode-import.d.ts +2 -1
  43. package/dist/{adapters/pi/conversation.d.ts → importer/pi-conversation.d.ts} +1 -1
  44. package/dist/importer/profile-import.js +3 -2
  45. package/dist/importer/run-import.d.ts +21 -3
  46. package/dist/importer/run-import.js +82 -2
  47. package/dist/importer/session-loader.d.ts +1 -1
  48. package/dist/importer/web-import-api.d.ts +3 -0
  49. package/dist/importer/web-import-api.js +3 -0
  50. package/dist/importer/web-import-jobs.js +1 -0
  51. package/dist/index.js +9 -4
  52. package/dist/services/ai/live-model-choice.d.ts +4 -1
  53. package/dist/services/ai/live-model-choice.js +18 -0
  54. package/dist/services/api-handlers.d.ts +6 -1
  55. package/dist/services/api-handlers.js +71 -62
  56. package/dist/services/backfill-state.d.ts +30 -0
  57. package/dist/services/backfill-state.js +47 -0
  58. package/dist/services/capture-diagnostics.d.ts +2 -8
  59. package/dist/services/capture-diagnostics.js +3 -49
  60. package/dist/services/global-config-writer.d.ts +2 -0
  61. package/dist/services/global-config-writer.js +37 -6
  62. package/dist/services/global-version.d.ts +31 -0
  63. package/dist/services/global-version.js +78 -0
  64. package/dist/services/memory-key-source.d.ts +35 -0
  65. package/dist/services/memory-key-source.js +123 -0
  66. package/dist/services/package-version.d.ts +2 -0
  67. package/dist/services/package-version.js +21 -0
  68. package/dist/services/private-path.d.ts +13 -0
  69. package/dist/services/private-path.js +54 -0
  70. package/dist/services/settings-snapshot.d.ts +17 -0
  71. package/dist/services/settings-snapshot.js +51 -14
  72. package/dist/services/turso/vector-search.d.ts +1 -1
  73. package/dist/services/turso/vector-search.js +5 -2
  74. package/dist/services/web-server.d.ts +2 -0
  75. package/dist/services/web-server.js +74 -1
  76. package/dist/web/assets/index-DvZhF1BJ.css +2 -0
  77. package/dist/web/assets/index-jqmX5i_i.js +124 -0
  78. package/dist/web/index.html +2 -2
  79. package/package.json +6 -1
  80. package/dist/web/assets/index-B-_gMSn9.js +0 -124
  81. package/dist/web/assets/index-D5ak5KsI.css +0 -2
  82. /package/dist/{adapters/pi/conversation.js → importer/pi-conversation.js} +0 -0
@@ -0,0 +1,35 @@
1
+ import { restrictToCurrentUser } from "./private-path.js";
2
+ /** How the External API card supplies `memoryApiKey`; a pasted value never leaves this module. */
3
+ export type MemoryKeySourceRequest = {
4
+ source: "env";
5
+ name: string;
6
+ } | {
7
+ source: "file";
8
+ path: string;
9
+ } | {
10
+ source: "paste";
11
+ name: string;
12
+ value: string;
13
+ replace?: boolean;
14
+ };
15
+ export declare class MemoryKeySourceError extends Error {
16
+ readonly status: number;
17
+ constructor(message: string, status?: number);
18
+ }
19
+ export declare const secretsDirectory: () => string;
20
+ /** Check a request's shape; error messages never include the pasted value. */
21
+ export declare function parseMemoryKeySource(body: unknown): MemoryKeySourceRequest;
22
+ /**
23
+ * Turn a key source into an `env://` or `file://` reference. A pasted key is
24
+ * written to `~/.config/omms/secrets/<name>.key`, readable only by the user,
25
+ * and replaces an existing file only when the request confirms it.
26
+ */
27
+ export declare function storeMemoryKeySource(request: MemoryKeySourceRequest, options?: {
28
+ directory?: string;
29
+ platform?: NodeJS.Platform;
30
+ restrict?: typeof restrictToCurrentUser;
31
+ }): Promise<{
32
+ reference: string;
33
+ source: "env" | "file";
34
+ path?: string;
35
+ }>;
@@ -0,0 +1,123 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { promises as fs, statSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute, join } from "node:path";
5
+ import { restrictToCurrentUser } from "./private-path.js";
6
+ export class MemoryKeySourceError extends Error {
7
+ status;
8
+ constructor(message, status = 400) {
9
+ super(message);
10
+ this.status = status;
11
+ this.name = "MemoryKeySourceError";
12
+ }
13
+ }
14
+ export const secretsDirectory = () => join(homedir(), ".config", "omms", "secrets");
15
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
16
+ const KEY_FILE_NAME = /^[A-Za-z0-9_-]{1,64}$/;
17
+ function expandHome(path) {
18
+ if (path === "~")
19
+ return homedir();
20
+ if (path.startsWith("~/") || path.startsWith("~\\"))
21
+ return join(homedir(), path.slice(2));
22
+ return path;
23
+ }
24
+ /** Check a request's shape; error messages never include the pasted value. */
25
+ export function parseMemoryKeySource(body) {
26
+ const request = (body ?? {});
27
+ if (request.source === "env") {
28
+ if (typeof request.name !== "string" || !ENV_NAME.test(request.name)) {
29
+ throw new MemoryKeySourceError("Enter a variable name such as ZAI_API_KEY");
30
+ }
31
+ return { source: "env", name: request.name };
32
+ }
33
+ if (request.source === "file") {
34
+ if (typeof request.path !== "string" || !request.path.trim()) {
35
+ throw new MemoryKeySourceError("Enter the path of a key file");
36
+ }
37
+ return { source: "file", path: request.path.trim() };
38
+ }
39
+ if (request.source === "paste") {
40
+ if (typeof request.name !== "string" || !KEY_FILE_NAME.test(request.name)) {
41
+ throw new MemoryKeySourceError("Name the key file with letters, digits, dashes, or underscores");
42
+ }
43
+ if (typeof request.value !== "string" || !request.value.trim()) {
44
+ throw new MemoryKeySourceError("Paste the key");
45
+ }
46
+ return {
47
+ source: "paste",
48
+ name: request.name,
49
+ value: request.value,
50
+ replace: request.replace === true,
51
+ };
52
+ }
53
+ throw new MemoryKeySourceError("Choose a key source");
54
+ }
55
+ /**
56
+ * Turn a key source into an `env://` or `file://` reference. A pasted key is
57
+ * written to `~/.config/omms/secrets/<name>.key`, readable only by the user,
58
+ * and replaces an existing file only when the request confirms it.
59
+ */
60
+ export async function storeMemoryKeySource(request, options = {}) {
61
+ if (request.source === "env")
62
+ return { reference: `env://${request.name}`, source: "env" };
63
+ if (request.source === "file") {
64
+ const path = expandHome(request.path);
65
+ let isFile;
66
+ try {
67
+ isFile = isAbsolute(path) && statSync(path).isFile();
68
+ }
69
+ catch {
70
+ isFile = false;
71
+ }
72
+ if (!isFile)
73
+ throw new MemoryKeySourceError("The key file does not exist");
74
+ return { reference: `file://${path}`, source: "file", path };
75
+ }
76
+ const directory = options.directory ?? secretsDirectory();
77
+ const platform = options.platform ?? process.platform;
78
+ const restrict = options.restrict ?? restrictToCurrentUser;
79
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
80
+ try {
81
+ restrict(directory, 0o700, platform);
82
+ }
83
+ catch {
84
+ throw new MemoryKeySourceError("The secrets folder could not be made private", 500);
85
+ }
86
+ const path = join(directory, `${request.name}.key`);
87
+ const content = `${request.value.trim()}\n`;
88
+ if (!request.replace) {
89
+ try {
90
+ await fs.writeFile(path, content, { flag: "wx", mode: 0o600 });
91
+ }
92
+ catch (error) {
93
+ if (error.code === "EEXIST") {
94
+ throw new MemoryKeySourceError(`A key file named ${request.name}.key exists. Confirm to replace it.`, 409);
95
+ }
96
+ throw new MemoryKeySourceError("The key file could not be written", 500);
97
+ }
98
+ try {
99
+ restrict(path, 0o600, platform);
100
+ }
101
+ catch {
102
+ // Never leave a key readable by others: remove it so a retry starts clean.
103
+ await fs.rm(path, { force: true });
104
+ throw new MemoryKeySourceError("The key file could not be made private", 500);
105
+ }
106
+ }
107
+ else {
108
+ const temp = join(directory, `.${randomUUID()}.tmp`);
109
+ try {
110
+ await fs.writeFile(temp, content, { flag: "wx", mode: 0o600 });
111
+ restrict(temp, 0o600, platform);
112
+ await fs.rename(temp, path);
113
+ }
114
+ catch {
115
+ throw new MemoryKeySourceError("The key file could not be written", 500);
116
+ }
117
+ finally {
118
+ await fs.rm(temp, { force: true });
119
+ }
120
+ }
121
+ // A replaced file was made private as the temp file before it moved into place.
122
+ return { reference: `file://${path}`, source: "file", path };
123
+ }
@@ -0,0 +1,2 @@
1
+ /** This package's version, found by walking up to its `package.json` from source or `dist/`. */
2
+ export declare function packageVersion(from?: string): string;
@@ -0,0 +1,21 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /** This package's version, found by walking up to its `package.json` from source or `dist/`. */
5
+ export function packageVersion(from = import.meta.url) {
6
+ let dir = dirname(fileURLToPath(from));
7
+ for (;;) {
8
+ try {
9
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
10
+ if (pkg.name === "om-memory-system" && pkg.version)
11
+ return pkg.version;
12
+ }
13
+ catch {
14
+ /* Try the parent. */
15
+ }
16
+ const parent = dirname(dir);
17
+ if (parent === dir)
18
+ return "unknown";
19
+ dir = parent;
20
+ }
21
+ }
@@ -0,0 +1,13 @@
1
+ type WindowsAclRunner = (command: string, args: string[], options: {
2
+ env: NodeJS.ProcessEnv;
3
+ timeout: number;
4
+ windowsHide: boolean;
5
+ stdio: "ignore";
6
+ }) => void;
7
+ /**
8
+ * Restrict a path to the current user: `chmod` on macOS and Linux, and on
9
+ * Windows an access list with inheritance removed and one full-control rule
10
+ * for the owner, who must be the current user.
11
+ */
12
+ export declare function restrictToCurrentUser(path: string, mode: number, platform?: NodeJS.Platform, run?: WindowsAclRunner): void;
13
+ export {};
@@ -0,0 +1,54 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { chmodSync } from "node:fs";
3
+ const WINDOWS_USER_ONLY_ACL = `$ErrorActionPreference = 'Stop'
4
+ $path = $env:OMMS_PRIVATE_PATH
5
+ $isDirectory = [System.IO.Directory]::Exists($path)
6
+ $acl = if ($isDirectory) {
7
+ [System.IO.Directory]::GetAccessControl($path)
8
+ } else {
9
+ [System.IO.File]::GetAccessControl($path)
10
+ }
11
+ $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
12
+ $owner = $acl.GetOwner([System.Security.Principal.SecurityIdentifier])
13
+ if ($owner.Value -ne $currentUser.Value) {
14
+ throw 'Path has a different owner'
15
+ }
16
+ $acl.SetAccessRuleProtection($true, $false)
17
+ foreach ($rule in @($acl.Access)) {
18
+ [void]$acl.RemoveAccessRuleSpecific($rule)
19
+ }
20
+ $inheritance = if ($isDirectory) {
21
+ [System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
22
+ } else {
23
+ [System.Security.AccessControl.InheritanceFlags]::None
24
+ }
25
+ $rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
26
+ $currentUser,
27
+ [System.Security.AccessControl.FileSystemRights]::FullControl,
28
+ $inheritance,
29
+ [System.Security.AccessControl.PropagationFlags]::None,
30
+ [System.Security.AccessControl.AccessControlType]::Allow
31
+ )
32
+ $acl.AddAccessRule($rule)
33
+ if ($isDirectory) {
34
+ [System.IO.Directory]::SetAccessControl($path, $acl)
35
+ } else {
36
+ [System.IO.File]::SetAccessControl($path, $acl)
37
+ }`;
38
+ /**
39
+ * Restrict a path to the current user: `chmod` on macOS and Linux, and on
40
+ * Windows an access list with inheritance removed and one full-control rule
41
+ * for the owner, who must be the current user.
42
+ */
43
+ export function restrictToCurrentUser(path, mode, platform = process.platform, run = execFileSync) {
44
+ if (platform !== "win32") {
45
+ chmodSync(path, mode);
46
+ return;
47
+ }
48
+ run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_USER_ONLY_ACL], {
49
+ env: { ...process.env, OMMS_PRIVATE_PATH: path },
50
+ timeout: 10_000,
51
+ windowsHide: true,
52
+ stdio: "ignore",
53
+ });
54
+ }
@@ -11,14 +11,31 @@ export declare function getSettingsSnapshot(directory: string): {
11
11
  [k: string]: {
12
12
  set: boolean;
13
13
  source: string | null;
14
+ reference: string | null;
14
15
  };
15
16
  };
16
17
  fallback: {
17
18
  model: string | null;
18
19
  configured: boolean;
19
20
  };
21
+ externalKey: {
22
+ source: string | null;
23
+ reference: string | null;
24
+ resolvesInWebApp: boolean;
25
+ warning: string | null;
26
+ };
20
27
  effective: {
21
28
  opencode: import("./ai/live-model-choice.js").AutoCaptureProviderStatus;
22
29
  pi: import("./ai/live-model-choice.js").PiLiveModelChoice;
23
30
  };
24
31
  };
32
+ /**
33
+ * Where `memoryApiKey` comes from and whether it resolves in this web
34
+ * server's own process. The key value itself is never returned.
35
+ */
36
+ export declare function memoryKeyStatus(raw: unknown, resolved?: string | undefined): {
37
+ source: string | null;
38
+ reference: string | null;
39
+ resolvesInWebApp: boolean;
40
+ warning: string | null;
41
+ };
@@ -16,6 +16,20 @@ const editable = [
16
16
  "opencodeBackfillModel",
17
17
  "piBackfillModel",
18
18
  "webServerAutoStart",
19
+ "memoryProvider",
20
+ "memoryApiUrl",
21
+ "memoryModel",
22
+ "importPathMaps",
23
+ ];
24
+ /** Keys a project config cannot override, so the page always shows the global value. */
25
+ const globalOnly = [
26
+ "autoBackfill",
27
+ "opencodeBackfillModel",
28
+ "piBackfillModel",
29
+ "webServerAutoStart",
30
+ "memoryProvider",
31
+ "memoryApiUrl",
32
+ "importPathMaps",
19
33
  ];
20
34
  function readSettingsFile(path) {
21
35
  if (!path)
@@ -39,13 +53,7 @@ export function getSettingsSnapshot(directory) {
39
53
  const projectOverrides = Object.hasOwn(project, key) &&
40
54
  (key === "captureTrace"
41
55
  ? project[key] === false
42
- : !key.endsWith("RetentionDays") &&
43
- ![
44
- "autoBackfill",
45
- "opencodeBackfillModel",
46
- "piBackfillModel",
47
- "webServerAutoStart",
48
- ].includes(key));
56
+ : !key.endsWith("RetentionDays") && !globalOnly.includes(key));
49
57
  const source = projectOverrides
50
58
  ? "project"
51
59
  : Object.hasOwn(global, key)
@@ -56,17 +64,20 @@ export function getSettingsSnapshot(directory) {
56
64
  }));
57
65
  const secrets = Object.fromEntries(["memoryApiKey", "embeddingApiKey", "webServerApiToken", "webServerAuthPassword"].map((key) => {
58
66
  const raw = global[key];
67
+ const source = typeof raw === "string"
68
+ ? raw.startsWith("env://")
69
+ ? "env"
70
+ : raw.startsWith("file://")
71
+ ? "file"
72
+ : "literal"
73
+ : null;
59
74
  return [
60
75
  key,
61
76
  {
62
77
  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,
78
+ source,
79
+ // The variable name or file path, never a literal value.
80
+ reference: memoryKeyStatus(raw).reference,
70
81
  },
71
82
  ];
72
83
  }));
@@ -75,9 +86,35 @@ export function getSettingsSnapshot(directory) {
75
86
  settings,
76
87
  secrets,
77
88
  fallback: { model: CONFIG.memoryModel ?? null, configured: isExternalModelReady(CONFIG) },
89
+ externalKey: memoryKeyStatus(global.memoryApiKey),
78
90
  effective: {
79
91
  opencode: getAutoCaptureProviderStatus(CONFIG),
80
92
  pi: resolvePiLiveModel(CONFIG),
81
93
  },
82
94
  };
83
95
  }
96
+ /**
97
+ * Where `memoryApiKey` comes from and whether it resolves in this web
98
+ * server's own process. The key value itself is never returned.
99
+ */
100
+ export function memoryKeyStatus(raw, resolved = CONFIG.memoryApiKey) {
101
+ const source = typeof raw !== "string" || !raw
102
+ ? null
103
+ : raw.startsWith("env://")
104
+ ? "env"
105
+ : raw.startsWith("file://")
106
+ ? "file"
107
+ : "literal";
108
+ return {
109
+ source,
110
+ reference: source === "env"
111
+ ? raw.slice("env://".length)
112
+ : source === "file"
113
+ ? raw.slice("file://".length)
114
+ : null,
115
+ resolvesInWebApp: Boolean(resolved),
116
+ warning: source === "env"
117
+ ? "A login web app does not see variables set only in a shell profile; a key file works everywhere."
118
+ : null,
119
+ };
120
+ }
@@ -12,7 +12,7 @@ export declare class TursoVectorSearch {
12
12
  warnings: string[];
13
13
  }>;
14
14
  deleteVector(db: TursoDb, memoryId: string): Promise<void>;
15
- updateVector(db: TursoDb, memoryId: string, vector: Float32Array, tagsVector?: Float32Array): Promise<void>;
15
+ updateVector(db: TursoDb, memoryId: string, vector: Float32Array, tagsVector?: Float32Array, tags?: string): Promise<void>;
16
16
  listMemories(db: TursoDb, containerTag: string, limit: number): Promise<Record<string, unknown>[]>;
17
17
  getAllMemories(db: TursoDb): Promise<Record<string, unknown>[]>;
18
18
  getAllMemoriesWithExtractedVectors(db: TursoDb): Promise<Array<Record<string, unknown> & {
@@ -263,9 +263,12 @@ export class TursoVectorSearch {
263
263
  async deleteVector(db, memoryId) {
264
264
  await db.run(`DELETE FROM memories WHERE id = ?`, [memoryId]);
265
265
  }
266
- async updateVector(db, memoryId, vector, tagsVector) {
266
+ async updateVector(db, memoryId, vector, tagsVector, tags) {
267
267
  const contentVector = vectorToJson(vector);
268
- if (tagsVector) {
268
+ if (tagsVector && tags !== undefined) {
269
+ await db.execute(`UPDATE memories SET tags = ?, vector = vector32(?), tags_vector = vector32(?) WHERE id = ?`, [tags, contentVector, vectorToJson(tagsVector), memoryId]);
270
+ }
271
+ else if (tagsVector) {
269
272
  await db.execute(`UPDATE memories SET vector = vector32(?), tags_vector = vector32(?) WHERE id = ?`, [contentVector, vectorToJson(tagsVector), memoryId]);
270
273
  }
271
274
  else {
@@ -36,8 +36,10 @@ export declare class WebServer {
36
36
  private takeoverFailures;
37
37
  private readonly maxFallbackPort;
38
38
  private settingsImportJobs?;
39
+ private backfillControlsInstance?;
39
40
  /** Run an import-page action; errors carry their own status and never file contents. */
40
41
  private importResponse;
42
+ private backfillControls;
41
43
  private importJobs;
42
44
  constructor(config: WebServerConfig);
43
45
  setOnTakeoverCallback(callback: () => Promise<void>): void;
@@ -146,6 +146,7 @@ export class WebServer {
146
146
  takeoverFailures = 0;
147
147
  maxFallbackPort;
148
148
  settingsImportJobs;
149
+ backfillControlsInstance;
149
150
  /** Run an import-page action; errors carry their own status and never file contents. */
150
151
  async importResponse(action, status = 200) {
151
152
  try {
@@ -156,6 +157,10 @@ export class WebServer {
156
157
  return this.jsonResponse({ error: error instanceof Error ? error.message : "Import request failed" }, typeof code === "number" ? code : 400);
157
158
  }
158
159
  }
160
+ async backfillControls() {
161
+ const { BackfillControls } = await import("../importer/web-import-api.js");
162
+ return (this.backfillControlsInstance ??= new BackfillControls());
163
+ }
159
164
  async importJobs() {
160
165
  const { SettingsImportJobs } = await import("../importer/web-import-jobs.js");
161
166
  return (this.settingsImportJobs ??= new SettingsImportJobs());
@@ -422,6 +427,34 @@ export class WebServer {
422
427
  opencode: await readBackfillStatus("opencode"),
423
428
  });
424
429
  }
430
+ if (path === "/api/settings/backfill/runs" && method === "GET") {
431
+ return this.jsonResponse(await (await this.backfillControls()).status());
432
+ }
433
+ const backfillAction = /^\/api\/settings\/backfill\/(pi|opencode)\/(run|pause|resume)$/.exec(path);
434
+ if (backfillAction && method === "POST") {
435
+ const host = backfillAction[1];
436
+ const controls = await this.backfillControls();
437
+ const cwd = this.config.directory ?? process.cwd();
438
+ return this.importResponse(() => backfillAction[2] === "pause"
439
+ ? controls.pause(host)
440
+ : backfillAction[2] === "resume"
441
+ ? controls.resume(host, cwd)
442
+ : controls.runNow(host, cwd));
443
+ }
444
+ if (path === "/api/settings/version" && method === "GET") {
445
+ const [{ packageVersion }, { globalCommandVersion }] = await Promise.all([
446
+ import("./package-version.js"),
447
+ import("./global-version.js"),
448
+ ]);
449
+ const running = packageVersion();
450
+ const global = await globalCommandVersion();
451
+ return this.jsonResponse({
452
+ running,
453
+ global: global.version,
454
+ globalPath: global.path,
455
+ mismatch: global.version !== null && global.version !== running,
456
+ });
457
+ }
425
458
  if (path === "/api/settings/web-autostart" && method === "GET") {
426
459
  const { webAutostartStatus } = await import("./web-autostart.js");
427
460
  return this.jsonResponse(webAutostartStatus());
@@ -460,6 +493,45 @@ export class WebServer {
460
493
  }
461
494
  return this.jsonResponse(result);
462
495
  }
496
+ if (path === "/api/settings/external-api/key" && method === "POST") {
497
+ // The body can hold a pasted key: never log it, and never echo it back.
498
+ const body = (await req.json());
499
+ const { MemoryKeySourceError, parseMemoryKeySource, storeMemoryKeySource } = await import("./memory-key-source.js");
500
+ try {
501
+ const request = parseMemoryKeySource(body);
502
+ if (request.source === "paste" &&
503
+ !isLoopbackHost(this.config.host) &&
504
+ !auth?.isEnabled()) {
505
+ return this.jsonResponse({ error: "Saving a key requires Basic Auth on network hosts" }, 403);
506
+ }
507
+ if (typeof body?.revision !== "string") {
508
+ return this.jsonResponse({ error: "Revision required" }, 400);
509
+ }
510
+ const stored = await storeMemoryKeySource(request);
511
+ const { writeGlobalConfigKeys } = await import("./global-config-writer.js");
512
+ const result = await writeGlobalConfigKeys({ memoryApiKey: stored.reference }, body.revision);
513
+ const { refreshConfigIfChanged } = await import("../config.js");
514
+ refreshConfigIfChanged(this.config.directory ?? process.cwd());
515
+ const { memoryKeyStatus } = await import("./settings-snapshot.js");
516
+ return this.jsonResponse({ ...result, key: memoryKeyStatus(stored.reference) });
517
+ }
518
+ catch (error) {
519
+ const status = error instanceof MemoryKeySourceError
520
+ ? error.status
521
+ : (error.status ?? 400);
522
+ const message = error instanceof Error ? error.message : "Key source not saved";
523
+ const secret = typeof body?.value === "string" ? body.value.trim() : "";
524
+ return this.jsonResponse({ error: secret ? message.replaceAll(secret, "[redacted]") : message }, status);
525
+ }
526
+ }
527
+ if (path === "/api/settings/external-api/test" && method === "POST") {
528
+ const { testExternalApi } = await import("../importer/web-import-api.js");
529
+ return this.jsonResponse(await testExternalApi());
530
+ }
531
+ if (path === "/api/settings/import-maps" && method === "GET") {
532
+ const { directoryMapsView } = await import("../importer/web-import-api.js");
533
+ return this.jsonResponse(await directoryMapsView());
534
+ }
463
535
  if (path === "/api/settings/models" && method === "GET") {
464
536
  const { listOpencodeSettingsModels, listPiSettingsModels } = await import("./settings-models.js");
465
537
  const host = url.searchParams.get("host");
@@ -569,7 +641,8 @@ export class WebServer {
569
641
  const page = parseInt(url.searchParams.get("page") || "1");
570
642
  const pageSize = parseInt(url.searchParams.get("pageSize") || "20");
571
643
  const includePrompts = url.searchParams.get("includePrompts") !== "false";
572
- const result = await handleListMemories(tag, page, pageSize, includePrompts);
644
+ const keyword = url.searchParams.get("keyword") || undefined;
645
+ const result = await handleListMemories(tag, page, pageSize, includePrompts, keyword);
573
646
  return this.jsonResponse(result);
574
647
  }
575
648
  if (path === "/api/memories" && method === "POST") {