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.
- package/README.md +21 -7
- package/dist/adapters/opencode/backfill-models.js +5 -1
- package/dist/adapters/opencode/backfill-startup.d.ts +6 -0
- package/dist/adapters/opencode/backfill-startup.js +5 -0
- package/dist/adapters/opencode/import-command.js +1 -0
- package/dist/adapters/opencode/user-prompt.d.ts +2 -1
- package/dist/adapters/opencode/user-prompt.js +2 -9
- package/dist/adapters/pi/backfill-models.d.ts +4 -0
- package/dist/adapters/pi/backfill-models.js +3 -0
- package/dist/adapters/pi/capture.d.ts +1 -1
- package/dist/adapters/pi/capture.js +1 -1
- package/dist/adapters/pi/extension.js +7 -0
- package/dist/adapters/pi/import-command.js +1 -0
- package/dist/cli/index.js +13 -1
- package/dist/config.d.ts +11 -4
- package/dist/config.js +29 -6
- package/dist/core/internal-prompt.d.ts +6 -0
- package/dist/core/internal-prompt.js +11 -0
- package/dist/importer/auto-backfill.d.ts +5 -0
- package/dist/importer/auto-backfill.js +29 -10
- package/dist/importer/backfill-controls.d.ts +37 -0
- package/dist/importer/backfill-controls.js +119 -0
- package/dist/importer/backfill-lock.d.ts +1 -0
- package/dist/importer/backfill-lock.js +2 -2
- package/dist/importer/backfill-model.d.ts +1 -1
- package/dist/importer/backfill-model.js +2 -0
- package/dist/importer/external-api-test.d.ts +18 -0
- package/dist/importer/external-api-test.js +50 -0
- package/dist/importer/external-backfill-models.d.ts +15 -0
- package/dist/importer/external-backfill-models.js +14 -0
- package/dist/importer/import-path-maps.d.ts +14 -0
- package/dist/importer/import-path-maps.js +49 -0
- package/dist/importer/import-progress.d.ts +27 -0
- package/dist/importer/import-progress.js +38 -0
- package/dist/importer/import-runs.d.ts +43 -0
- package/dist/importer/import-runs.js +141 -0
- package/dist/importer/import-sessions.js +9 -9
- package/dist/importer/importer.d.ts +2 -1
- package/dist/importer/importer.js +2 -2
- package/dist/importer/map-suggestions.d.ts +39 -0
- package/dist/importer/map-suggestions.js +133 -0
- package/dist/importer/opencode-import.d.ts +2 -1
- package/dist/{adapters/pi/conversation.d.ts → importer/pi-conversation.d.ts} +1 -1
- package/dist/importer/profile-import.js +3 -2
- package/dist/importer/run-import.d.ts +21 -3
- package/dist/importer/run-import.js +82 -2
- package/dist/importer/session-loader.d.ts +1 -1
- package/dist/importer/web-import-api.d.ts +3 -0
- package/dist/importer/web-import-api.js +3 -0
- package/dist/importer/web-import-jobs.js +1 -0
- package/dist/index.js +9 -4
- package/dist/services/ai/live-model-choice.d.ts +4 -1
- package/dist/services/ai/live-model-choice.js +18 -0
- package/dist/services/api-handlers.d.ts +6 -1
- package/dist/services/api-handlers.js +71 -62
- package/dist/services/backfill-state.d.ts +30 -0
- package/dist/services/backfill-state.js +47 -0
- package/dist/services/capture-diagnostics.d.ts +2 -8
- package/dist/services/capture-diagnostics.js +3 -49
- package/dist/services/global-config-writer.d.ts +2 -0
- package/dist/services/global-config-writer.js +37 -6
- package/dist/services/global-version.d.ts +31 -0
- package/dist/services/global-version.js +78 -0
- package/dist/services/memory-key-source.d.ts +35 -0
- package/dist/services/memory-key-source.js +123 -0
- package/dist/services/package-version.d.ts +2 -0
- package/dist/services/package-version.js +21 -0
- package/dist/services/private-path.d.ts +13 -0
- package/dist/services/private-path.js +54 -0
- package/dist/services/settings-snapshot.d.ts +17 -0
- package/dist/services/settings-snapshot.js +51 -14
- package/dist/services/turso/vector-search.d.ts +1 -1
- package/dist/services/turso/vector-search.js +5 -2
- package/dist/services/web-server.d.ts +2 -0
- package/dist/services/web-server.js +74 -1
- package/dist/web/assets/index-DvZhF1BJ.css +2 -0
- package/dist/web/assets/index-jqmX5i_i.js +124 -0
- package/dist/web/index.html +2 -2
- package/package.json +6 -1
- package/dist/web/assets/index-B-_gMSn9.js +0 -124
- package/dist/web/assets/index-D5ak5KsI.css +0 -2
- /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,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
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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") {
|