om-memory-system 5.5.0 → 5.7.0-next.218
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 +1 -1
- package/dist/adapters/pi/extension.js +5 -0
- package/dist/cli/space-command.d.ts +6 -5
- package/dist/cli/space-command.js +154 -13
- package/dist/cli/space-transfer-command.d.ts +25 -0
- package/dist/cli/space-transfer-command.js +79 -0
- package/dist/core/memory-operations.d.ts +12 -2
- package/dist/core/memory-operations.js +14 -4
- package/dist/core/recall-summary.js +7 -5
- package/dist/core/retrieval.js +5 -2
- package/dist/core/space-tool.d.ts +9 -4
- package/dist/core/space-tool.js +35 -3
- package/dist/services/active-space-access.d.ts +6 -0
- package/dist/services/active-space-access.js +8 -0
- package/dist/services/chat/chat-runtime.js +1 -1
- package/dist/services/client.d.ts +6 -16
- package/dist/services/client.js +54 -62
- package/dist/services/context.d.ts +2 -0
- package/dist/services/context.js +12 -1
- package/dist/services/linked-search.d.ts +108 -0
- package/dist/services/linked-search.js +219 -0
- package/dist/services/space-routes.d.ts +2 -0
- package/dist/services/space-routes.js +4 -3
- package/dist/services/space-transfer-routes.d.ts +10 -0
- package/dist/services/space-transfer-routes.js +344 -0
- package/dist/services/spaces/active-space.js +14 -7
- package/dist/services/spaces/delete.d.ts +2 -0
- package/dist/services/spaces/delete.js +4 -0
- package/dist/services/spaces/export-space.d.ts +60 -0
- package/dist/services/spaces/export-space.js +367 -0
- package/dist/services/spaces/identifier-scrub.d.ts +26 -0
- package/dist/services/spaces/identifier-scrub.js +40 -0
- package/dist/services/spaces/import-space.d.ts +85 -0
- package/dist/services/spaces/import-space.js +349 -0
- package/dist/services/spaces/operations.d.ts +57 -5
- package/dist/services/spaces/operations.js +161 -5
- package/dist/services/spaces/read-store-cache.d.ts +13 -0
- package/dist/services/spaces/read-store-cache.js +34 -0
- package/dist/services/spaces/registry-store.d.ts +4 -0
- package/dist/services/spaces/registry-store.js +15 -1
- package/dist/services/spaces/registry.d.ts +32 -0
- package/dist/services/spaces/registry.js +81 -7
- package/dist/services/spaces/space-store.d.ts +50 -0
- package/dist/services/spaces/space-store.js +72 -0
- package/dist/services/spaces/transfer-service.d.ts +41 -0
- package/dist/services/spaces/transfer-service.js +408 -0
- package/dist/services/spaces/transfer-types.d.ts +163 -0
- package/dist/services/spaces/transfer-types.js +10 -0
- package/dist/services/spaces/transfer.d.ts +31 -0
- package/dist/services/spaces/transfer.js +276 -0
- package/dist/services/turso/connection-manager.d.ts +4 -2
- package/dist/services/turso/connection-manager.js +11 -5
- package/dist/services/turso/cross-process-write-lock.d.ts +1 -1
- package/dist/services/turso/cross-process-write-lock.js +5 -5
- package/dist/services/turso/operation-lock.d.ts +2 -2
- package/dist/services/turso/operation-lock.js +5 -5
- package/dist/services/turso/shard-manager.d.ts +8 -0
- package/dist/services/turso/shard-manager.js +23 -22
- package/dist/services/turso/vector-search.d.ts +6 -2
- package/dist/services/turso/vector-search.js +8 -4
- package/dist/services/user-profile/user-profile-manager.d.ts +5 -1
- package/dist/services/user-profile/user-profile-manager.js +13 -10
- package/dist/services/web-route-modules.d.ts +5 -1
- package/dist/services/web-route-modules.js +4 -0
- package/dist/services/web-server.js +1 -1
- package/dist/shared/api/space-export-schemas.d.ts +159 -0
- package/dist/shared/api/space-export-schemas.js +72 -0
- package/dist/v2/adapter.d.ts +15 -0
- package/dist/v2/adapter.js +5 -0
- package/dist/web/assets/index-2BNRLGqe.css +2 -0
- package/dist/web/assets/index-C6WnDE6h.js +170 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/skills/omms-memory/SKILL.md +16 -14
- package/dist/web/assets/index-Dcvz3EXt.js +0 -170
- package/dist/web/assets/index-b8k5Tuei.css +0 -2
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import * as config from "../config.js";
|
|
5
|
+
import { log } from "./logger.js";
|
|
6
|
+
import { packageVersion } from "./package-version.js";
|
|
7
|
+
import { getUserTagInfo } from "./tags.js";
|
|
8
|
+
import { STATUS_BY_CODE } from "./space-routes.js";
|
|
9
|
+
import { exportSpace } from "./spaces/export-space.js";
|
|
10
|
+
import { IMPORT_MAX_FILE_BYTES, importSpace, validateSpaceImportDocument, } from "./spaces/import-space.js";
|
|
11
|
+
import { SpaceError, findSpace, readRegistryFile, resolveSpaceFolder, spaceLinks, } from "./spaces/registry.js";
|
|
12
|
+
import { linkStoredSpace, spaceFiles, unlinkStoredSpace, } from "./spaces/registry-store.js";
|
|
13
|
+
import { ommsHomeDir } from "./spaces/storage-resolution.js";
|
|
14
|
+
import { applySpaceTransfer, previewSpaceTransfer, } from "./spaces/transfer-service.js";
|
|
15
|
+
const ok = (data) => ({
|
|
16
|
+
status: 200,
|
|
17
|
+
body: { success: true, data },
|
|
18
|
+
});
|
|
19
|
+
const fail = (status, code, error) => ({
|
|
20
|
+
status,
|
|
21
|
+
body: { success: false, code, error },
|
|
22
|
+
});
|
|
23
|
+
/** A refusal the handler wants mapped to a status and code, like a bad date. */
|
|
24
|
+
class RouteError extends Error {
|
|
25
|
+
status;
|
|
26
|
+
code;
|
|
27
|
+
constructor(status, code, message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.code = code;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** The service refusals of `transfer-service.ts`, as HTTP statuses. */
|
|
34
|
+
const STATUS_BY_REFUSAL = {
|
|
35
|
+
"unknown-space": 404,
|
|
36
|
+
"same-space": 400,
|
|
37
|
+
"archived-space": 409,
|
|
38
|
+
"registry-problem": 500,
|
|
39
|
+
};
|
|
40
|
+
/** The JSON envelope around the uploaded file (name, folder) may add this much. */
|
|
41
|
+
const IMPORT_ENVELOPE_BYTES = 64 * 1024;
|
|
42
|
+
/** The import cap of the spec, enforced on the raw request body. */
|
|
43
|
+
const IMPORT_BODY_CAP = IMPORT_MAX_FILE_BYTES + IMPORT_ENVELOPE_BYTES;
|
|
44
|
+
/** One transfer apply at a time per process (design D4); previews run freely. */
|
|
45
|
+
let applyInFlight = false;
|
|
46
|
+
function embeddingSettings() {
|
|
47
|
+
return {
|
|
48
|
+
model: config.CONFIG.embeddingModel,
|
|
49
|
+
dimensions: config.CONFIG.embeddingDimensions,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function transferContext(files, deps) {
|
|
53
|
+
const { storagePath, skillStoragePath, maxVectorsPerShard } = config.CONFIG;
|
|
54
|
+
return {
|
|
55
|
+
files,
|
|
56
|
+
paths: { globalStoragePath: skillStoragePath ?? storagePath, spacesRoot: files.spacesRoot },
|
|
57
|
+
embedding: embeddingSettings(),
|
|
58
|
+
maxVectorsPerShard,
|
|
59
|
+
...(deps?.embed ? { embed: deps.embed } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function defaultEmbed(text) {
|
|
63
|
+
return import("./embedding.js").then(({ embeddingService }) => embeddingService.embedWithTimeout(text, { task: "document" }));
|
|
64
|
+
}
|
|
65
|
+
/** The importing user: user-scope memories take this user's container tag. */
|
|
66
|
+
function importingUser(cwd) {
|
|
67
|
+
const info = getUserTagInfo(cwd);
|
|
68
|
+
return {
|
|
69
|
+
userTag: info.tag,
|
|
70
|
+
...(info.displayName ? { displayName: info.displayName } : {}),
|
|
71
|
+
...(info.userName ? { userName: info.userName } : {}),
|
|
72
|
+
...(info.userEmail ? { userEmail: info.userEmail } : {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function readJson(req) {
|
|
76
|
+
return (await req.json().catch(() => ({})));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Reads one import body under the spec's 100 MB cap. The only body limit the
|
|
80
|
+
* web server has lives here, scoped to the import routes.
|
|
81
|
+
*/
|
|
82
|
+
async function readCappedJson(req) {
|
|
83
|
+
const declared = Number(req.headers.get("content-length"));
|
|
84
|
+
if (declared > IMPORT_BODY_CAP) {
|
|
85
|
+
throw new RouteError(413, "file-too-large", `The upload is larger than the import limit of ${IMPORT_MAX_FILE_BYTES} bytes.`);
|
|
86
|
+
}
|
|
87
|
+
const text = await req.text();
|
|
88
|
+
if (Buffer.byteLength(text, "utf-8") > IMPORT_BODY_CAP) {
|
|
89
|
+
throw new RouteError(413, "file-too-large", `The upload is larger than the import limit of ${IMPORT_MAX_FILE_BYTES} bytes.`);
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(text);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new RouteError(400, "invalid-file", "The request body is not valid JSON.");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** One date field: absent, epoch milliseconds, or an ISO 8601 string. */
|
|
99
|
+
function dateMs(body, field) {
|
|
100
|
+
const value = body[field];
|
|
101
|
+
if (value === undefined || value === null || value === "")
|
|
102
|
+
return undefined;
|
|
103
|
+
const parsed = typeof value === "number" ? value : Date.parse(String(value));
|
|
104
|
+
if (!Number.isFinite(parsed)) {
|
|
105
|
+
throw new RouteError(400, "invalid-date", `"${field}" must be a date or epoch milliseconds.`);
|
|
106
|
+
}
|
|
107
|
+
return parsed;
|
|
108
|
+
}
|
|
109
|
+
/** The transfer filter: the whole history by default, one project tag at most. */
|
|
110
|
+
function filterFrom(body) {
|
|
111
|
+
const projects = body.projects;
|
|
112
|
+
let projectTag;
|
|
113
|
+
if (Array.isArray(projects)) {
|
|
114
|
+
if (projects.length > 1) {
|
|
115
|
+
throw new RouteError(400, "invalid", "Filter by one project or by none.");
|
|
116
|
+
}
|
|
117
|
+
projectTag = typeof projects[0] === "string" ? projects[0] : undefined;
|
|
118
|
+
}
|
|
119
|
+
else if (typeof projects === "string" && projects) {
|
|
120
|
+
projectTag = projects;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
since: dateMs(body, "since") ?? 0,
|
|
124
|
+
until: dateMs(body, "until") ?? Number.MAX_SAFE_INTEGER,
|
|
125
|
+
...(projectTag ? { projectTag } : {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function endpoints(body) {
|
|
129
|
+
const from = body.from;
|
|
130
|
+
const to = body.to;
|
|
131
|
+
if (typeof from !== "string" || !from.trim() || typeof to !== "string" || !to.trim()) {
|
|
132
|
+
throw new RouteError(400, "invalid", '"from" and "to" must name two spaces.');
|
|
133
|
+
}
|
|
134
|
+
return { from: from.trim(), to: to.trim() };
|
|
135
|
+
}
|
|
136
|
+
function transferRefusal(code, error) {
|
|
137
|
+
return fail(STATUS_BY_REFUSAL[code], code, error);
|
|
138
|
+
}
|
|
139
|
+
/** The download file name of one export: `omms-space-export-<id>-<timestamp>.json`. */
|
|
140
|
+
function exportFileName(spaceId) {
|
|
141
|
+
const stamp = new Date().toISOString().replaceAll(":", "-").replaceAll(".", "-");
|
|
142
|
+
return `omms-space-export-${spaceId}-${stamp}.json`;
|
|
143
|
+
}
|
|
144
|
+
async function handleExport(id, req) {
|
|
145
|
+
const files = spaceFiles(ommsHomeDir());
|
|
146
|
+
const read = readRegistryFile(files.registryPath);
|
|
147
|
+
const space = findSpace(read.registry, id);
|
|
148
|
+
if (!space)
|
|
149
|
+
return fail(404, "unknown", `No space named "${id}".`);
|
|
150
|
+
const body = await readJson(req);
|
|
151
|
+
const { storagePath, skillStoragePath } = config.CONFIG;
|
|
152
|
+
// One name for the file, the header and the clean-up, so all three agree.
|
|
153
|
+
const fileName = exportFileName(space.id);
|
|
154
|
+
const outPath = join(tmpdir(), fileName);
|
|
155
|
+
const since = dateMs(body, "since");
|
|
156
|
+
const until = dateMs(body, "until");
|
|
157
|
+
try {
|
|
158
|
+
const result = await exportSpace({
|
|
159
|
+
folder: resolveSpaceFolder(space.id, {
|
|
160
|
+
globalStoragePath: skillStoragePath ?? storagePath,
|
|
161
|
+
spacesRoot: files.spacesRoot,
|
|
162
|
+
}),
|
|
163
|
+
outPath,
|
|
164
|
+
...(since !== undefined ? { since } : {}),
|
|
165
|
+
...(until !== undefined ? { until } : {}),
|
|
166
|
+
includeProfile: body.includeProfile === true,
|
|
167
|
+
embeddingModel: config.CONFIG.embeddingModel,
|
|
168
|
+
embeddingDimensions: config.CONFIG.embeddingDimensions,
|
|
169
|
+
ommsVersion: packageVersion(),
|
|
170
|
+
homePath: homedir(),
|
|
171
|
+
});
|
|
172
|
+
if (!result.written)
|
|
173
|
+
return ok(result);
|
|
174
|
+
// The bytes the CLI would write go out unchanged, as an attachment.
|
|
175
|
+
const bytes = readFileSync(outPath);
|
|
176
|
+
return {
|
|
177
|
+
status: 200,
|
|
178
|
+
body: undefined,
|
|
179
|
+
response: new Response(new Uint8Array(bytes), {
|
|
180
|
+
headers: {
|
|
181
|
+
"content-type": "application/json",
|
|
182
|
+
"content-disposition": `attachment; filename="${fileName}"`,
|
|
183
|
+
"x-omms-export-info": JSON.stringify({
|
|
184
|
+
counts: result.counts,
|
|
185
|
+
warnings: result.warnings,
|
|
186
|
+
}),
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
const message = error instanceof Error ? error.message : "The export failed.";
|
|
193
|
+
return fail(500, "export-failed", message);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
if (existsSync(outPath))
|
|
197
|
+
unlinkSync(outPath);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function handleImportCheck(req, cwd) {
|
|
201
|
+
const body = await readCappedJson(req);
|
|
202
|
+
if (body.document === undefined) {
|
|
203
|
+
return fail(400, "invalid-file", "No export document was given.");
|
|
204
|
+
}
|
|
205
|
+
const validated = validateSpaceImportDocument(body.document, embeddingSettings(), importingUser(cwd).userTag);
|
|
206
|
+
if (!validated.ok)
|
|
207
|
+
return fail(400, "invalid-file", validated.reason);
|
|
208
|
+
const projects = [];
|
|
209
|
+
for (const memory of validated.document.memories) {
|
|
210
|
+
if (memory.scope === "project" && memory.projectTag && !projects.includes(memory.projectTag)) {
|
|
211
|
+
projects.push(memory.projectTag);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return ok({
|
|
215
|
+
counts: validated.counts,
|
|
216
|
+
projects,
|
|
217
|
+
// Structured, so the dialog composes its localised warning (web spec: every
|
|
218
|
+
// string exists in en, zh and ar). `warning` stays for non-UI callers.
|
|
219
|
+
embedding: {
|
|
220
|
+
fileModel: validated.document.embedding.model,
|
|
221
|
+
fileDimensions: validated.document.embedding.dimensions,
|
|
222
|
+
currentModel: config.CONFIG.embeddingModel,
|
|
223
|
+
currentDimensions: config.CONFIG.embeddingDimensions,
|
|
224
|
+
},
|
|
225
|
+
...(validated.warning ? { warning: validated.warning } : {}),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async function handleImport(req, cwd, deps) {
|
|
229
|
+
const files = spaceFiles(ommsHomeDir());
|
|
230
|
+
const body = await readCappedJson(req);
|
|
231
|
+
if (body.document === undefined) {
|
|
232
|
+
return fail(400, "invalid-file", "No export document was given.");
|
|
233
|
+
}
|
|
234
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
235
|
+
if (!name)
|
|
236
|
+
return fail(400, "invalid", "A name for the new space is required.");
|
|
237
|
+
// Precise refusals first, so no space is ever created for a bad request.
|
|
238
|
+
const validated = validateSpaceImportDocument(body.document, embeddingSettings(), importingUser(cwd).userTag);
|
|
239
|
+
if (!validated.ok)
|
|
240
|
+
return fail(400, "invalid-file", validated.reason);
|
|
241
|
+
if (findSpace(readRegistryFile(files.registryPath).registry, name)) {
|
|
242
|
+
return fail(409, "name-taken", `A space named "${name}" already exists.`);
|
|
243
|
+
}
|
|
244
|
+
const retagToProject = typeof body.retagToProject === "string" && body.retagToProject.trim()
|
|
245
|
+
? body.retagToProject.trim()
|
|
246
|
+
: undefined;
|
|
247
|
+
if (retagToProject !== undefined && !existsSync(retagToProject)) {
|
|
248
|
+
return fail(400, "missing-folder", `The project folder "${retagToProject}" does not exist, so no space was created.`);
|
|
249
|
+
}
|
|
250
|
+
const result = await importSpace({
|
|
251
|
+
document: body.document,
|
|
252
|
+
spaceName: name,
|
|
253
|
+
files,
|
|
254
|
+
...(retagToProject !== undefined ? { retagToProject } : {}),
|
|
255
|
+
embedding: embeddingSettings(),
|
|
256
|
+
embed: deps?.embed ?? defaultEmbed,
|
|
257
|
+
user: importingUser(cwd),
|
|
258
|
+
});
|
|
259
|
+
if (!result.ok)
|
|
260
|
+
return fail(400, "import-failed", result.reason ?? "The import failed.");
|
|
261
|
+
return ok(result);
|
|
262
|
+
}
|
|
263
|
+
async function handleLinks(id, req) {
|
|
264
|
+
const files = spaceFiles(ommsHomeDir());
|
|
265
|
+
const body = await readJson(req);
|
|
266
|
+
const add = typeof body.add === "string" && body.add.trim() ? body.add.trim() : undefined;
|
|
267
|
+
const remove = typeof body.remove === "string" && body.remove.trim() ? body.remove.trim() : undefined;
|
|
268
|
+
if ((add !== undefined) === (remove !== undefined)) {
|
|
269
|
+
return fail(400, "invalid", 'Give either "add" or "remove", naming one space.');
|
|
270
|
+
}
|
|
271
|
+
if (add)
|
|
272
|
+
await linkStoredSpace(files, id, add);
|
|
273
|
+
else
|
|
274
|
+
await unlinkStoredSpace(files, id, remove);
|
|
275
|
+
return ok({ links: spaceLinks(readRegistryFile(files.registryPath).registry, id) });
|
|
276
|
+
}
|
|
277
|
+
async function dispatch(path, req, cwd, deps) {
|
|
278
|
+
if (path === "/api/spaces/transfer/preview") {
|
|
279
|
+
const body = await readJson(req);
|
|
280
|
+
const { from, to } = endpoints(body);
|
|
281
|
+
const outcome = await previewSpaceTransfer({ from, to, filter: filterFrom(body) }, transferContext(spaceFiles(ommsHomeDir()), deps));
|
|
282
|
+
if (!outcome.ok)
|
|
283
|
+
return transferRefusal(outcome.code, outcome.error);
|
|
284
|
+
return ok({ preview: outcome.preview });
|
|
285
|
+
}
|
|
286
|
+
if (path === "/api/spaces/transfer") {
|
|
287
|
+
const body = await readJson(req);
|
|
288
|
+
const { from, to } = endpoints(body);
|
|
289
|
+
if (body.mode !== "copy" && body.mode !== "move") {
|
|
290
|
+
return fail(400, "invalid", '"mode" must be "copy" or "move".');
|
|
291
|
+
}
|
|
292
|
+
if (body.confirm !== true) {
|
|
293
|
+
return fail(400, "confirm-required", "Set confirm to true to run the transfer.");
|
|
294
|
+
}
|
|
295
|
+
if (applyInFlight) {
|
|
296
|
+
return fail(STATUS_BY_CODE["transfer-busy"], "transfer-busy", "Another transfer is running. Try again when it finishes.");
|
|
297
|
+
}
|
|
298
|
+
applyInFlight = true;
|
|
299
|
+
try {
|
|
300
|
+
const outcome = await applySpaceTransfer({
|
|
301
|
+
from,
|
|
302
|
+
to,
|
|
303
|
+
filter: filterFrom(body),
|
|
304
|
+
mode: body.mode,
|
|
305
|
+
...(typeof body.previewMatched === "number"
|
|
306
|
+
? { previewMatched: body.previewMatched }
|
|
307
|
+
: {}),
|
|
308
|
+
}, transferContext(spaceFiles(ommsHomeDir()), deps));
|
|
309
|
+
if (!outcome.ok)
|
|
310
|
+
return transferRefusal(outcome.code, outcome.error);
|
|
311
|
+
return ok({ result: outcome.result });
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
applyInFlight = false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const exported = /^\/api\/spaces\/([^/]+)\/export$/.exec(path);
|
|
318
|
+
if (exported)
|
|
319
|
+
return handleExport(decodeURIComponent(exported[1]), req);
|
|
320
|
+
if (path === "/api/spaces/import/check")
|
|
321
|
+
return handleImportCheck(req, cwd);
|
|
322
|
+
if (path === "/api/spaces/import")
|
|
323
|
+
return handleImport(req, cwd, deps);
|
|
324
|
+
const links = /^\/api\/spaces\/([^/]+)\/links$/.exec(path);
|
|
325
|
+
if (links)
|
|
326
|
+
return handleLinks(decodeURIComponent(links[1]), req);
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
export async function routeSpaceTransfers(path, method, req, cwd, deps) {
|
|
330
|
+
if (method !== "POST" || !path.startsWith("/api/spaces/"))
|
|
331
|
+
return undefined;
|
|
332
|
+
try {
|
|
333
|
+
return await dispatch(path, req, cwd, deps);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (error instanceof RouteError)
|
|
337
|
+
return fail(error.status, error.code, error.message);
|
|
338
|
+
if (error instanceof SpaceError) {
|
|
339
|
+
return fail(STATUS_BY_CODE[error.code] ?? 400, error.code, error.message);
|
|
340
|
+
}
|
|
341
|
+
log("space transfer route: error", { path, name: error instanceof Error ? error.name : typeof error }, "error");
|
|
342
|
+
return fail(500, "error", error instanceof Error ? error.message : "Request failed");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { log } from "../logger.js";
|
|
3
4
|
import { DEFAULT_SPACE_ID, DEFAULT_SPACE_NAME, findSpace, readRegistryFile, resolveSpaceFolder, } from "./registry.js";
|
|
4
5
|
/**
|
|
5
6
|
* Picks the active space for config loading (design D2). Reads paths from arguments
|
|
6
7
|
* and never imports CONFIG. A problem always falls back to the Default space.
|
|
7
8
|
*/
|
|
8
9
|
export function resolveActiveSpace(ommsDir, globalStoragePath, projectSetsStoragePath) {
|
|
9
|
-
const fallback = (problem) =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const fallback = (problem, name = DEFAULT_SPACE_NAME) => {
|
|
11
|
+
// The spec wants an unusable registry named in the log, not only in the web status.
|
|
12
|
+
if (problem)
|
|
13
|
+
log("Active space fallback to Default", { problem }, "warn");
|
|
14
|
+
return {
|
|
15
|
+
id: DEFAULT_SPACE_ID,
|
|
16
|
+
name,
|
|
17
|
+
customStoragePath: projectSetsStoragePath,
|
|
18
|
+
problem,
|
|
19
|
+
};
|
|
20
|
+
};
|
|
15
21
|
const { registry, problem } = readRegistryFile(join(ommsDir, "spaces.json"));
|
|
16
22
|
if (problem)
|
|
17
23
|
return fallback(problem);
|
|
18
24
|
const space = findSpace(registry, registry.activeSpaceId);
|
|
25
|
+
// Default may carry a custom registry name; an unreadable registry stays "Default" (D4).
|
|
19
26
|
if (!space || space.id === DEFAULT_SPACE_ID)
|
|
20
|
-
return fallback();
|
|
27
|
+
return fallback(undefined, space?.name);
|
|
21
28
|
if (projectSetsStoragePath) {
|
|
22
29
|
return { id: space.id, name: space.name, customStoragePath: true };
|
|
23
30
|
}
|
|
@@ -5,6 +5,8 @@ export interface DeleteDeps {
|
|
|
5
5
|
/** Replaced in tests only. */
|
|
6
6
|
backup: typeof backupSpace;
|
|
7
7
|
removeFolder(folder: string): void;
|
|
8
|
+
/** Releases the cached read store before the folder goes (design D2, Windows-safe). */
|
|
9
|
+
closeReadStore(folder: string): Promise<void>;
|
|
8
10
|
}
|
|
9
11
|
export interface DeleteResult {
|
|
10
12
|
backupPath: string;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { rmSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { backupSpace } from "./backup.js";
|
|
4
|
+
import { closeCachedSpaceReadStore } from "./read-store-cache.js";
|
|
4
5
|
import { SpaceError, findSpace, readRegistryFile, removeSpace, } from "./registry.js";
|
|
5
6
|
import { removeStoredSpace } from "./registry-store.js";
|
|
6
7
|
const defaultDeps = {
|
|
7
8
|
backup: backupSpace,
|
|
8
9
|
removeFolder: (folder) => rmSync(folder, { recursive: true, force: false }),
|
|
10
|
+
closeReadStore: (folder) => closeCachedSpaceReadStore(folder),
|
|
9
11
|
};
|
|
10
12
|
/**
|
|
11
13
|
* Deletes a space (design D8): a verified backup first, then the registry entry, then the folder.
|
|
@@ -31,6 +33,8 @@ export async function deleteSpace(files, idOrName, options, overrides = {}) {
|
|
|
31
33
|
});
|
|
32
34
|
await removeStoredSpace(files, checked.id);
|
|
33
35
|
try {
|
|
36
|
+
// A linked search may still hold the folder open through the cached read store.
|
|
37
|
+
await deps.closeReadStore(folder);
|
|
34
38
|
deps.removeFolder(folder);
|
|
35
39
|
return { backupPath, space: checked };
|
|
36
40
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** What the caller passes in; nothing is read from CONFIG here (pattern of live-model-choice.ts). */
|
|
2
|
+
export interface ExportSpaceOptions {
|
|
3
|
+
/** The space folder to read. Read-only: the space is never changed. */
|
|
4
|
+
folder: string;
|
|
5
|
+
/** Where the JSON file is written, atomically. The caller picks the path. */
|
|
6
|
+
outPath: string;
|
|
7
|
+
/** Inclusive lower bound on the memory creation time, in epoch milliseconds. */
|
|
8
|
+
since?: number;
|
|
9
|
+
/** Inclusive upper bound on the memory creation time, in epoch milliseconds. */
|
|
10
|
+
until?: number;
|
|
11
|
+
/** Opt-in that adds the user-scope memories and the learned profile. */
|
|
12
|
+
includeProfile?: boolean;
|
|
13
|
+
/** Recorded in the file so an import can warn on a model mismatch. */
|
|
14
|
+
embeddingModel: string;
|
|
15
|
+
embeddingDimensions: number;
|
|
16
|
+
/** The OMMS version recorded in the file. */
|
|
17
|
+
ommsVersion: string;
|
|
18
|
+
/** The exporting user's home folder, checked by the final identifier scan. */
|
|
19
|
+
homePath: string;
|
|
20
|
+
}
|
|
21
|
+
/** Totals of one export, for the CLI and web dialogs to report. */
|
|
22
|
+
export interface ExportSpaceCounts {
|
|
23
|
+
/** Project-scope memories written to the file. */
|
|
24
|
+
projectMemories: number;
|
|
25
|
+
/** User-scope memories written to the file (opt-in only). */
|
|
26
|
+
userMemories: number;
|
|
27
|
+
/** Memories left out because they are fully private. */
|
|
28
|
+
privateSkipped: number;
|
|
29
|
+
/** Profile items written to the file (opt-in only). */
|
|
30
|
+
profileItems: number;
|
|
31
|
+
/** Home-folder paths the scrubber rewrote, across the whole file. */
|
|
32
|
+
pathsReplaced: number;
|
|
33
|
+
/** Email addresses the scrubber rewrote, across the whole file. */
|
|
34
|
+
emailsReplaced: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ExportSpaceResult {
|
|
37
|
+
/** False when the selection held no memory: no file was written. */
|
|
38
|
+
written: boolean;
|
|
39
|
+
/** Why nothing was written; set only when `written` is false. */
|
|
40
|
+
reason?: string;
|
|
41
|
+
/** Absolute path of the written file; set only when `written` is true. */
|
|
42
|
+
outputPath?: string;
|
|
43
|
+
counts: ExportSpaceCounts;
|
|
44
|
+
/** Human-facing notes, always including the free-text-names limit. */
|
|
45
|
+
warnings: string[];
|
|
46
|
+
}
|
|
47
|
+
/** The version of this OMMS build, read from package.json; "unknown" when unreadable. */
|
|
48
|
+
export declare function getOmmsVersion(): string;
|
|
49
|
+
/**
|
|
50
|
+
* The final safety scan (design D7): fail the export when a home-folder path
|
|
51
|
+
* or an email address survived the per-string scrubbing. System paths and
|
|
52
|
+
* URLs are deliberate non-matches, because memories often quote them.
|
|
53
|
+
*/
|
|
54
|
+
export declare function scanExportContent(serialised: string, homePath: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* Export one space to a JSON file (design D7). Reads the space through a
|
|
57
|
+
* read-only store, so the space is never changed. Throws when the final
|
|
58
|
+
* identifier scan fails; returns `written: false` for an empty selection.
|
|
59
|
+
*/
|
|
60
|
+
export declare function exportSpace(options: ExportSpaceOptions): Promise<ExportSpaceResult>;
|