om-memory-system 5.6.1 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/opencode/self-update.d.ts +19 -0
- package/dist/adapters/opencode/self-update.js +42 -0
- package/dist/adapters/opencode/tui-status.d.ts +12 -2
- package/dist/adapters/opencode/tui-status.js +84 -11
- package/dist/adapters/pi/extension.js +19 -3
- package/dist/adapters/pi/self-update.d.ts +25 -0
- package/dist/adapters/pi/self-update.js +146 -0
- package/dist/cli/space-command.js +21 -1
- package/dist/config.js +11 -1
- package/dist/services/host-copies.d.ts +52 -0
- package/dist/services/host-copies.js +150 -0
- package/dist/services/host-self-update.d.ts +108 -0
- package/dist/services/host-self-update.js +254 -0
- package/dist/services/space-routes.d.ts +10 -1
- package/dist/services/space-routes.js +63 -20
- package/dist/services/spaces/backup.js +3 -0
- package/dist/services/spaces/operations.js +3 -1
- package/dist/services/spaces/registry-store.d.ts +5 -3
- package/dist/services/spaces/registry-store.js +38 -9
- package/dist/services/spaces/registry.d.ts +11 -5
- package/dist/services/spaces/registry.js +31 -23
- package/dist/services/spaces/restore.js +1 -1
- package/dist/services/spaces/space-label.d.ts +16 -0
- package/dist/services/spaces/space-label.js +69 -0
- package/dist/services/web-route-modules.d.ts +3 -1
- package/dist/services/web-route-modules.js +3 -1
- package/dist/services/web-server.js +1 -0
- package/dist/v2/plugin.js +60 -1
- package/dist/web/assets/{index-C6WnDE6h.js → index-B94xLmhL.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
1
3
|
import { basename } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
2
5
|
import * as config from "../config.js";
|
|
3
6
|
import { HostAppearanceError, createAppearance } from "./host-appearance.js";
|
|
4
7
|
import { log } from "./logger.js";
|
|
@@ -10,6 +13,7 @@ import { DEFAULT_SPACE_ID, SpaceError, findSpace, listSpaces, isValidSpaceId, re
|
|
|
10
13
|
import { archiveStoredSpace, createStoredSpace, renameStoredSpace, spaceFiles, switchStoredSpace, unarchiveStoredSpace, } from "./spaces/registry-store.js";
|
|
11
14
|
import { listBackups, restoreBackup } from "./spaces/restore.js";
|
|
12
15
|
import { ommsHomeDir } from "./spaces/storage-resolution.js";
|
|
16
|
+
import { isLoopbackAddress } from "./web-api-auth.js";
|
|
13
17
|
const ok = (data) => ({ status: 200, body: { success: true, data } });
|
|
14
18
|
const fail = (status, code, error) => ({
|
|
15
19
|
status,
|
|
@@ -18,10 +22,8 @@ const fail = (status, code, error) => ({
|
|
|
18
22
|
export const STATUS_BY_CODE = {
|
|
19
23
|
"invalid-name": 400,
|
|
20
24
|
"invalid-id": 400,
|
|
21
|
-
reserved: 400,
|
|
22
25
|
unknown: 404,
|
|
23
26
|
"name-taken": 409,
|
|
24
|
-
"id-taken": 409,
|
|
25
27
|
protected: 409,
|
|
26
28
|
active: 409,
|
|
27
29
|
archived: 409,
|
|
@@ -53,21 +55,25 @@ function appearanceOf(files) {
|
|
|
53
55
|
return createAppearance(files.appearanceRoot, isValidSpaceId, "Unknown space");
|
|
54
56
|
}
|
|
55
57
|
function describeSpaces(files, registry, globalFolder, looks) {
|
|
56
|
-
return listSpaces(registry).map((space) =>
|
|
57
|
-
|
|
58
|
-
id: space.id,
|
|
59
|
-
name: space.name,
|
|
60
|
-
createdAt: space.createdAt || null,
|
|
61
|
-
archived: space.archived,
|
|
62
|
-
archivedAt: space.archivedAt ?? null,
|
|
63
|
-
active: space.id === registry.activeSpaceId,
|
|
64
|
-
sizeBytes: folderSize(resolveSpaceFolder(space.id, {
|
|
58
|
+
return listSpaces(registry).map((space) => {
|
|
59
|
+
const folder = resolveSpaceFolder(space.id, {
|
|
65
60
|
globalStoragePath: globalFolder,
|
|
66
61
|
spacesRoot: files.spacesRoot,
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
look: looks[space.id] ?? { kind: "default" },
|
|
65
|
+
id: space.id,
|
|
66
|
+
name: space.name,
|
|
67
|
+
createdAt: space.createdAt || null,
|
|
68
|
+
archived: space.archived,
|
|
69
|
+
archivedAt: space.archivedAt ?? null,
|
|
70
|
+
active: space.id === registry.activeSpaceId,
|
|
71
|
+
folder,
|
|
72
|
+
sizeBytes: folderSize(folder),
|
|
73
|
+
disabled: disabledReasons(space, registry.activeSpaceId),
|
|
74
|
+
links: spaceLinks(registry, space.id),
|
|
75
|
+
};
|
|
76
|
+
});
|
|
71
77
|
}
|
|
72
78
|
function describeBackup(path, summary) {
|
|
73
79
|
return {
|
|
@@ -98,12 +104,12 @@ async function snapshot(files, cwd) {
|
|
|
98
104
|
function nameOf(body) {
|
|
99
105
|
return typeof body.name === "string" ? body.name : "";
|
|
100
106
|
}
|
|
101
|
-
export async function routeSpaces(path, method, readBody, cwd) {
|
|
107
|
+
export async function routeSpaces(path, method, readBody, cwd, options = {}) {
|
|
102
108
|
if (path !== "/api/spaces" && !path.startsWith("/api/spaces/"))
|
|
103
109
|
return undefined;
|
|
104
110
|
const files = spaceFiles(ommsHomeDir());
|
|
105
111
|
try {
|
|
106
|
-
return await dispatch(path, method, readBody, cwd, files);
|
|
112
|
+
return await dispatch(path, method, readBody, cwd, files, options);
|
|
107
113
|
}
|
|
108
114
|
catch (error) {
|
|
109
115
|
if (error instanceof SpaceError) {
|
|
@@ -115,7 +121,7 @@ export async function routeSpaces(path, method, readBody, cwd) {
|
|
|
115
121
|
return fail(500, "error", error instanceof Error ? error.message : "Spaces request failed");
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
|
-
async function dispatch(path, method, readBody, cwd, files) {
|
|
124
|
+
async function dispatch(path, method, readBody, cwd, files, options) {
|
|
119
125
|
if (path === "/api/spaces" && method === "GET")
|
|
120
126
|
return ok(await snapshot(files, cwd));
|
|
121
127
|
if (path === "/api/spaces" && method === "POST") {
|
|
@@ -147,7 +153,7 @@ async function dispatch(path, method, readBody, cwd, files) {
|
|
|
147
153
|
}
|
|
148
154
|
return ok({ look: (await appearance.read([space.id]))[space.id] });
|
|
149
155
|
}
|
|
150
|
-
const match = /^\/api\/spaces\/([^/]+)(?:\/(switch|rename|archive|unarchive|backup))?$/.exec(path);
|
|
156
|
+
const match = /^\/api\/spaces\/([^/]+)(?:\/(switch|rename|archive|unarchive|backup|open-folder))?$/.exec(path);
|
|
151
157
|
if (!match)
|
|
152
158
|
return undefined;
|
|
153
159
|
const id = decodeURIComponent(match[1]);
|
|
@@ -160,7 +166,11 @@ async function dispatch(path, method, readBody, cwd, files) {
|
|
|
160
166
|
else if (action === "unarchive")
|
|
161
167
|
await unarchiveStoredSpace(files, id);
|
|
162
168
|
else if (action === "rename")
|
|
163
|
-
await renameStoredSpace(files, id, nameOf(await readBody())
|
|
169
|
+
await renameStoredSpace(files, id, nameOf(await readBody()), {
|
|
170
|
+
defaultFolder: globalFolder(),
|
|
171
|
+
});
|
|
172
|
+
else if (action === "open-folder")
|
|
173
|
+
return await openFolder(files, id, options);
|
|
164
174
|
else
|
|
165
175
|
return ok(await backUp(files, id));
|
|
166
176
|
config.refreshConfigIfChanged(cwd);
|
|
@@ -173,6 +183,39 @@ async function dispatch(path, method, readBody, cwd, files) {
|
|
|
173
183
|
function globalFolder() {
|
|
174
184
|
return config.CONFIG.skillStoragePath ?? config.CONFIG.storagePath;
|
|
175
185
|
}
|
|
186
|
+
/** The file manager command of this platform. The folder is its only argument. */
|
|
187
|
+
const openerCommand = process.platform === "win32" ? "explorer" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
188
|
+
const execFileAsync = promisify(execFile);
|
|
189
|
+
/** The real opener. execFile runs no shell, so a path can never be reinterpreted as a command. */
|
|
190
|
+
async function startFileManager(command, args) {
|
|
191
|
+
// The opener hands off to the file manager and exits, so this await reports a missing
|
|
192
|
+
// opener without waiting on the file manager itself.
|
|
193
|
+
await execFileAsync(command, args);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Opens the space folder in the platform file manager. Refused unless the caller is this
|
|
197
|
+
* machine, and the folder always comes from the registry, never from the request.
|
|
198
|
+
*/
|
|
199
|
+
async function openFolder(files, id, options) {
|
|
200
|
+
if (!isLoopbackAddress(options.remoteAddress)) {
|
|
201
|
+
return fail(403, "forbidden", "Only the local machine can open a space folder. Use the web app on this computer.");
|
|
202
|
+
}
|
|
203
|
+
const space = requireSpace(files, id);
|
|
204
|
+
const folder = resolveSpaceFolder(space.id, {
|
|
205
|
+
globalStoragePath: globalFolder(),
|
|
206
|
+
spacesRoot: files.spacesRoot,
|
|
207
|
+
});
|
|
208
|
+
if (!existsSync(folder)) {
|
|
209
|
+
return fail(409, "missing-folder", `The space folder is missing: ${folder}. Restore the space from a backup, then try again.`);
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
await (options.startOpener ?? startFileManager)(openerCommand, [folder]);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
return fail(500, "opener", `Could not open the folder: ${folder}. ${error instanceof Error ? error.message : "The file manager did not start."}`);
|
|
216
|
+
}
|
|
217
|
+
return ok({ folder });
|
|
218
|
+
}
|
|
176
219
|
async function backUp(files, id) {
|
|
177
220
|
const space = requireSpace(files, id);
|
|
178
221
|
const { backupPath, manifest } = await backupSpace({
|
|
@@ -58,6 +58,9 @@ function isStoreFile(relPath) {
|
|
|
58
58
|
const parts = relPath.split("/");
|
|
59
59
|
if (parts.some((part) => SKIPPED_FOLDERS.includes(part)))
|
|
60
60
|
return false;
|
|
61
|
+
// The root space.json label is live-instance state; a restored space gets a fresh one.
|
|
62
|
+
if (parts.length === 1 && parts[0] === "space.json")
|
|
63
|
+
return false;
|
|
61
64
|
return !SKIPPED_SUFFIXES.some((suffix) => relPath.endsWith(suffix));
|
|
62
65
|
}
|
|
63
66
|
function uniqueBackupFolder(backupsRoot, id, now) {
|
|
@@ -222,12 +222,14 @@ export async function executeSpaceOperation(args, context) {
|
|
|
222
222
|
}[action];
|
|
223
223
|
const registry = await change(context.files, name);
|
|
224
224
|
const active = activeOf(registry);
|
|
225
|
+
// The caller may have passed the id, so the message names the display name.
|
|
226
|
+
const displayName = findSpace(registry, name).name;
|
|
225
227
|
return {
|
|
226
228
|
success: true,
|
|
227
229
|
activeSpace: active,
|
|
228
230
|
message: action === "use"
|
|
229
231
|
? `Active space is now "${active.name}". It applies from the next turn.`
|
|
230
|
-
: `Space "${
|
|
232
|
+
: `Space "${displayName}" is now ${action === "archive" ? "archived" : "not archived"}.`,
|
|
231
233
|
};
|
|
232
234
|
}
|
|
233
235
|
catch (error) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SpaceEntry, type SpaceRegistry } from "./registry.js";
|
|
1
|
+
import { type CreateSpaceOptions, type SpaceEntry, type SpaceRegistry } from "./registry.js";
|
|
2
2
|
export interface SpaceFiles {
|
|
3
3
|
/** `<omms folder>/spaces.json` */
|
|
4
4
|
registryPath: string;
|
|
@@ -24,12 +24,14 @@ export declare function updateRegistry<T>(files: SpaceFiles, change: (registry:
|
|
|
24
24
|
registry: SpaceRegistry;
|
|
25
25
|
result: T;
|
|
26
26
|
}>;
|
|
27
|
-
export declare function createStoredSpace(files: SpaceFiles, name: string): Promise<SpaceEntry>;
|
|
27
|
+
export declare function createStoredSpace(files: SpaceFiles, name: string, options?: CreateSpaceOptions): Promise<SpaceEntry>;
|
|
28
28
|
export declare const archiveStoredSpace: (files: SpaceFiles, idOrName: string) => Promise<SpaceRegistry>;
|
|
29
29
|
export declare const unarchiveStoredSpace: (files: SpaceFiles, idOrName: string) => Promise<SpaceRegistry>;
|
|
30
30
|
export declare const switchStoredSpace: (files: SpaceFiles, idOrName: string) => Promise<SpaceRegistry>;
|
|
31
31
|
export declare const removeStoredSpace: (files: SpaceFiles, idOrName: string) => Promise<SpaceRegistry>;
|
|
32
|
-
export declare function renameStoredSpace(files: SpaceFiles, idOrName: string, name: string
|
|
32
|
+
export declare function renameStoredSpace(files: SpaceFiles, idOrName: string, name: string, options?: {
|
|
33
|
+
defaultFolder?: string;
|
|
34
|
+
}): Promise<SpaceRegistry>;
|
|
33
35
|
/** Adds a link between two stored spaces under the lock (design D5). */
|
|
34
36
|
export declare function linkStoredSpace(files: SpaceFiles, idOrName: string, targetIdOrName: string): Promise<SpaceRegistry>;
|
|
35
37
|
/** Removes a link between two stored spaces under the lock (design D5). */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { SpaceError, archiveSpace, createSpace, linkSpace, readRegistryFile, removeSpace, renameSpace, switchSpace, unarchiveSpace, unlinkSpace, } from "./registry.js";
|
|
3
|
+
import { SpaceError, archiveSpace, createSpace, findSpace, linkSpace, listSpaces, readRegistryFile, removeSpace, renameSpace, switchSpace, unarchiveSpace, unlinkSpace, DEFAULT_SPACE_ID, } from "./registry.js";
|
|
4
|
+
import { writeSpaceLabel } from "./space-label.js";
|
|
4
5
|
/**
|
|
5
6
|
* The one write path for `spaces.json` (design D5). The web server, the CLI and the
|
|
6
7
|
* `memory` tool all change the registry through `updateRegistry`. Readers never lock.
|
|
@@ -109,13 +110,25 @@ export async function updateRegistry(files, change) {
|
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
|
-
export async function createStoredSpace(files, name) {
|
|
113
|
+
export async function createStoredSpace(files, name, options = {}) {
|
|
113
114
|
const { result } = await updateRegistry(files, (registry) => {
|
|
114
|
-
|
|
115
|
+
// A leftover folder must not be adopted by a new space, so its name counts as a taken id.
|
|
116
|
+
const folderIds = existsSync(files.spacesRoot) ? readdirSync(files.spacesRoot) : [];
|
|
117
|
+
const created = createSpace(registry, name, new Date().toISOString(), {
|
|
118
|
+
...options,
|
|
119
|
+
avoidIds: [...(options.avoidIds ?? []), ...folderIds],
|
|
120
|
+
});
|
|
115
121
|
// The folder comes first, so the registry never names a space that has no folder.
|
|
116
122
|
mkdirSync(join(files.spacesRoot, created.space.id), { recursive: true });
|
|
117
123
|
return { registry: created.registry, result: created.space };
|
|
118
124
|
});
|
|
125
|
+
// The label only names the folder; a failed write must not fail the create.
|
|
126
|
+
try {
|
|
127
|
+
writeSpaceLabel(join(files.spacesRoot, result.id), result);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Start-up backfills the label later.
|
|
131
|
+
}
|
|
119
132
|
return result;
|
|
120
133
|
}
|
|
121
134
|
const simple = (apply) => async (files, idOrName) => (await updateRegistry(files, (registry) => ({
|
|
@@ -126,11 +139,27 @@ export const archiveStoredSpace = simple(archiveSpace);
|
|
|
126
139
|
export const unarchiveStoredSpace = simple(unarchiveSpace);
|
|
127
140
|
export const switchStoredSpace = simple(switchSpace);
|
|
128
141
|
export const removeStoredSpace = simple(removeSpace);
|
|
129
|
-
export async function renameStoredSpace(files, idOrName, name) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
export async function renameStoredSpace(files, idOrName, name, options = {}) {
|
|
143
|
+
const { registry, result: renamed } = await updateRegistry(files, (current) => {
|
|
144
|
+
// Found by id: the new name may equal another space's id, which findSpace would pick.
|
|
145
|
+
const id = findSpace(current, idOrName)?.id;
|
|
146
|
+
const next = renameSpace(current, idOrName, name);
|
|
147
|
+
return { registry: next, result: listSpaces(next).find((space) => space.id === id) };
|
|
148
|
+
});
|
|
149
|
+
if (renamed) {
|
|
150
|
+
// Default lives in the global storage folder, which only the caller knows.
|
|
151
|
+
const folder = renamed.id === DEFAULT_SPACE_ID ? options.defaultFolder : join(files.spacesRoot, renamed.id);
|
|
152
|
+
if (folder) {
|
|
153
|
+
// The label only names the folder; a failed write must not fail the rename.
|
|
154
|
+
try {
|
|
155
|
+
writeSpaceLabel(folder, renamed);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Start-up backfills the label later.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return registry;
|
|
134
163
|
}
|
|
135
164
|
/** Adds a link between two stored spaces under the lock (design D5). */
|
|
136
165
|
export async function linkStoredSpace(files, idOrName, targetIdOrName) {
|
|
@@ -28,7 +28,7 @@ export interface SpaceRegistry {
|
|
|
28
28
|
/** User-created spaces only. Default is added by `listSpaces`. */
|
|
29
29
|
spaces: SpaceEntry[];
|
|
30
30
|
}
|
|
31
|
-
export type SpaceErrorCode = "invalid-name" | "name-taken" | "
|
|
31
|
+
export type SpaceErrorCode = "invalid-name" | "name-taken" | "protected" | "active" | "archived" | "not-archived" | "unknown" | "invalid-id" | "unreadable" | "lock-timeout";
|
|
32
32
|
/** A refused space operation. The message is safe to show to the user. */
|
|
33
33
|
export declare class SpaceError extends Error {
|
|
34
34
|
readonly code: SpaceErrorCode;
|
|
@@ -43,14 +43,20 @@ export interface SpacePaths {
|
|
|
43
43
|
export declare function emptyRegistry(): SpaceRegistry;
|
|
44
44
|
/** Every space in display order: Default first, then user spaces in creation order. */
|
|
45
45
|
export declare function listSpaces(registry: SpaceRegistry): SpaceEntry[];
|
|
46
|
-
/** Lowercase letters, digits and single hyphens, at most 40 characters. Empty when the name has none. */
|
|
47
|
-
export declare function spaceIdFromName(name: string): string;
|
|
48
46
|
/** True for a space id that is safe to use in a path: lowercase letters, digits and single hyphens. */
|
|
49
47
|
export declare function isValidSpaceId(id: string): boolean;
|
|
50
48
|
/** Finds a space by id, or by display name without regard to case. */
|
|
51
49
|
export declare function findSpace(registry: SpaceRegistry, idOrName: string): SpaceEntry | undefined;
|
|
52
|
-
/**
|
|
53
|
-
export declare function
|
|
50
|
+
/** Draws a random space id: `space-` plus 10 lowercase letters and digits (ADR for random ids). */
|
|
51
|
+
export declare function drawRandomSpaceId(): string;
|
|
52
|
+
export interface CreateSpaceOptions {
|
|
53
|
+
/** Draws a candidate id. Tests inject a fixed source. */
|
|
54
|
+
drawId?: () => string;
|
|
55
|
+
/** Ids to refuse beyond the registry, such as folders already on disk. */
|
|
56
|
+
avoidIds?: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
/** Adds a space with a random id. Returns the new registry and the new entry. The active space does not change. */
|
|
59
|
+
export declare function createSpace(registry: SpaceRegistry, name: string, now: string, options?: CreateSpaceOptions): {
|
|
54
60
|
registry: SpaceRegistry;
|
|
55
61
|
space: SpaceEntry;
|
|
56
62
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomInt } from "node:crypto";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join, resolve, sep } from "node:path";
|
|
3
4
|
/**
|
|
@@ -36,14 +37,6 @@ function defaultEntry(registry) {
|
|
|
36
37
|
export function listSpaces(registry) {
|
|
37
38
|
return [defaultEntry(registry), ...registry.spaces];
|
|
38
39
|
}
|
|
39
|
-
/** Lowercase letters, digits and single hyphens, at most 40 characters. Empty when the name has none. */
|
|
40
|
-
export function spaceIdFromName(name) {
|
|
41
|
-
const id = name
|
|
42
|
-
.toLowerCase()
|
|
43
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
44
|
-
.replace(/^-+|-+$/g, "");
|
|
45
|
-
return id.slice(0, MAX_SPACE_ID_LENGTH).replace(/-+$/, "");
|
|
46
|
-
}
|
|
47
40
|
const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
48
41
|
/** True for a space id that is safe to use in a path: lowercase letters, digits and single hyphens. */
|
|
49
42
|
export function isValidSpaceId(id) {
|
|
@@ -67,10 +60,10 @@ function requireSpace(registry, idOrName) {
|
|
|
67
60
|
return space;
|
|
68
61
|
}
|
|
69
62
|
function validTargets(registry) {
|
|
70
|
-
const
|
|
63
|
+
const targets = listSpaces(registry)
|
|
71
64
|
.filter((space) => !space.archived)
|
|
72
|
-
.map((space) => space.id);
|
|
73
|
-
return `Spaces that can become active: ${
|
|
65
|
+
.map((space) => `${space.name} (${space.id})`);
|
|
66
|
+
return `Spaces that can become active: ${targets.join(", ")}.`;
|
|
74
67
|
}
|
|
75
68
|
function checkedName(name) {
|
|
76
69
|
const trimmed = name.trim();
|
|
@@ -79,23 +72,38 @@ function checkedName(name) {
|
|
|
79
72
|
}
|
|
80
73
|
return trimmed;
|
|
81
74
|
}
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
const RANDOM_ID_SUFFIX_LENGTH = 10;
|
|
76
|
+
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
77
|
+
/** The most draws before createSpace gives up, so a broken source cannot loop forever. */
|
|
78
|
+
const MAX_ID_DRAWS = 100;
|
|
79
|
+
/** Draws a random space id: `space-` plus 10 lowercase letters and digits (ADR for random ids). */
|
|
80
|
+
export function drawRandomSpaceId() {
|
|
81
|
+
let suffix = "";
|
|
82
|
+
for (let i = 0; i < RANDOM_ID_SUFFIX_LENGTH; i++) {
|
|
83
|
+
suffix += ID_ALPHABET[randomInt(ID_ALPHABET.length)];
|
|
84
|
+
}
|
|
85
|
+
return `space-${suffix}`;
|
|
86
|
+
}
|
|
87
|
+
/** Adds a space with a random id. Returns the new registry and the new entry. The active space does not change. */
|
|
88
|
+
export function createSpace(registry, name, now, options = {}) {
|
|
84
89
|
const display = checkedName(name);
|
|
85
90
|
const existing = findByName(registry, display);
|
|
86
91
|
if (existing) {
|
|
87
92
|
throw new SpaceError("name-taken", `A space named "${existing.name}" already exists.`);
|
|
88
93
|
}
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
const drawId = options.drawId ?? drawRandomSpaceId;
|
|
95
|
+
const avoid = options.avoidIds ?? [];
|
|
96
|
+
let id = "";
|
|
97
|
+
for (let draws = 0;; draws++) {
|
|
98
|
+
if (draws >= MAX_ID_DRAWS) {
|
|
99
|
+
throw new SpaceError("invalid-id", "Could not draw an unused space id. Try again.");
|
|
100
|
+
}
|
|
101
|
+
id = drawId();
|
|
102
|
+
const taken = id === DEFAULT_SPACE_ID ||
|
|
103
|
+
avoid.includes(id) ||
|
|
104
|
+
registry.spaces.some((space) => space.id === id);
|
|
105
|
+
if (isValidSpaceId(id) && !taken)
|
|
106
|
+
break;
|
|
99
107
|
}
|
|
100
108
|
const space = { id, name: display, createdAt: now, archived: false };
|
|
101
109
|
return { registry: { ...registry, spaces: [...registry.spaces, space] }, space };
|
|
@@ -72,7 +72,7 @@ async function createWithFreeName(files, name, now) {
|
|
|
72
72
|
return await createStoredSpace(files, restoredName(name, now, attempt));
|
|
73
73
|
}
|
|
74
74
|
catch (error) {
|
|
75
|
-
const taken = error instanceof SpaceError &&
|
|
75
|
+
const taken = error instanceof SpaceError && error.code === "name-taken";
|
|
76
76
|
if (!taken || attempt >= 50)
|
|
77
77
|
throw error;
|
|
78
78
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type SpaceEntry, type SpaceRegistry } from "./registry.js";
|
|
2
|
+
/** Writes the label of one space folder atomically: temp file in the folder, then rename. */
|
|
3
|
+
export declare function writeSpaceLabel(folder: string, space: SpaceEntry): void;
|
|
4
|
+
/**
|
|
5
|
+
* Adds or repairs the label of every existing space folder. A folder that does
|
|
6
|
+
* not exist is skipped: the pass never creates a folder and touches no other
|
|
7
|
+
* file. One failing folder does not stop the rest.
|
|
8
|
+
*/
|
|
9
|
+
export declare function backfillSpaceLabels(registry: SpaceRegistry, files: {
|
|
10
|
+
spacesRoot: string;
|
|
11
|
+
}, globalStoragePath: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Reads the registry from the OMMS folder and runs the backfill pass over it.
|
|
14
|
+
* Best effort: callers wrap it so a failure never reaches start-up.
|
|
15
|
+
*/
|
|
16
|
+
export declare function backfillSpaceLabelsFromDisk(ommsDir: string, globalStoragePath: string): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { listSpaces, readRegistryFile, resolveSpaceFolder, } from "./registry.js";
|
|
4
|
+
/**
|
|
5
|
+
* The `space.json` label inside every space folder (space-folder-ids design). It
|
|
6
|
+
* names a folder found on disk. OMMS writes it but never reads it to decide
|
|
7
|
+
* anything: the registry stays the only source for ids, names and folders.
|
|
8
|
+
*/
|
|
9
|
+
const LABEL_FILE = "space.json";
|
|
10
|
+
/** Writes the label of one space folder atomically: temp file in the folder, then rename. */
|
|
11
|
+
export function writeSpaceLabel(folder, space) {
|
|
12
|
+
const target = join(folder, LABEL_FILE);
|
|
13
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
14
|
+
try {
|
|
15
|
+
writeFileSync(temporary, `${JSON.stringify(spaceLabelOf(space), null, 2)}\n`);
|
|
16
|
+
renameSync(temporary, target);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (existsSync(temporary))
|
|
20
|
+
unlinkSync(temporary);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function spaceLabelOf(space) {
|
|
25
|
+
// The Default space has no creation time in the registry, so its label keeps the empty string.
|
|
26
|
+
return { id: space.id, name: space.name, createdAt: space.createdAt };
|
|
27
|
+
}
|
|
28
|
+
/** The name in the folder label, or undefined when the file is missing or broken. */
|
|
29
|
+
function labelName(folder) {
|
|
30
|
+
try {
|
|
31
|
+
const raw = JSON.parse(readFileSync(join(folder, LABEL_FILE), "utf-8"));
|
|
32
|
+
return typeof raw?.name === "string" ? raw.name : undefined;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// A missing or broken label is rewritten, never trusted.
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Adds or repairs the label of every existing space folder. A folder that does
|
|
41
|
+
* not exist is skipped: the pass never creates a folder and touches no other
|
|
42
|
+
* file. One failing folder does not stop the rest.
|
|
43
|
+
*/
|
|
44
|
+
export function backfillSpaceLabels(registry, files, globalStoragePath) {
|
|
45
|
+
for (const space of listSpaces(registry)) {
|
|
46
|
+
try {
|
|
47
|
+
const folder = resolveSpaceFolder(space.id, {
|
|
48
|
+
globalStoragePath,
|
|
49
|
+
spacesRoot: files.spacesRoot,
|
|
50
|
+
});
|
|
51
|
+
if (!existsSync(folder))
|
|
52
|
+
continue;
|
|
53
|
+
if (labelName(folder) === space.name)
|
|
54
|
+
continue;
|
|
55
|
+
writeSpaceLabel(folder, space);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// The label is metadata; the registry entry is what counts.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Reads the registry from the OMMS folder and runs the backfill pass over it.
|
|
64
|
+
* Best effort: callers wrap it so a failure never reaches start-up.
|
|
65
|
+
*/
|
|
66
|
+
export function backfillSpaceLabelsFromDisk(ommsDir, globalStoragePath) {
|
|
67
|
+
const { registry } = readRegistryFile(join(ommsDir, "spaces.json"));
|
|
68
|
+
backfillSpaceLabels(registry, { spacesRoot: join(ommsDir, "spaces") }, globalStoragePath);
|
|
69
|
+
}
|
|
@@ -10,4 +10,6 @@ export type RouteModuleResult = RouteResult & {
|
|
|
10
10
|
* ceiling. Each returns undefined for paths it does not own. The request body
|
|
11
11
|
* is read once, and only by a route that needs it.
|
|
12
12
|
*/
|
|
13
|
-
export declare function routeModules(path: string, method: string, url: URL, req: Request, ctx: SkillRouteContext
|
|
13
|
+
export declare function routeModules(path: string, method: string, url: URL, req: Request, ctx: SkillRouteContext & {
|
|
14
|
+
remoteAddress?: string;
|
|
15
|
+
}): Promise<RouteModuleResult | undefined>;
|
|
@@ -26,7 +26,9 @@ export async function routeModules(path, method, url, req, ctx) {
|
|
|
26
26
|
const transfers = await routeSpaceTransfers(path, method, req, ctx.cwd);
|
|
27
27
|
if (transfers !== undefined)
|
|
28
28
|
return transfers;
|
|
29
|
-
const spaces = await routeSpaces(path, method, readBody, ctx.cwd
|
|
29
|
+
const spaces = await routeSpaces(path, method, readBody, ctx.cwd, {
|
|
30
|
+
remoteAddress: ctx.remoteAddress,
|
|
31
|
+
});
|
|
30
32
|
if (spaces !== undefined)
|
|
31
33
|
return spaces;
|
|
32
34
|
return routeChat(path, method, readBody, config);
|
|
@@ -1375,6 +1375,7 @@ export class WebServer {
|
|
|
1375
1375
|
}
|
|
1376
1376
|
const routed = await routeModules(path, method, url, req, {
|
|
1377
1377
|
cwd: this.config.directory ?? process.cwd(),
|
|
1378
|
+
remoteAddress,
|
|
1378
1379
|
startRecordPast: async (cwd) => (await import("../importer/web-import-api.js")).startRecordPast(this.importJobs, cwd),
|
|
1379
1380
|
});
|
|
1380
1381
|
if (routed)
|
package/dist/v2/plugin.js
CHANGED
|
@@ -1,8 +1,64 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
1
2
|
import { OmmsPlugin } from "../index.js";
|
|
2
3
|
import { loadOpencodeProvider } from "../adapters/opencode/opencode-provider-loader.js";
|
|
3
4
|
import { registerV2Adapter } from "./adapter.js";
|
|
4
5
|
import { createV2MemoryBridge } from "./memory-bridge.js";
|
|
5
6
|
import { createLegacyClient } from "./legacy-client.js";
|
|
7
|
+
/**
|
|
8
|
+
* Remove old OMMS copies from the OpenCode cache. It runs in the background and
|
|
9
|
+
* is not awaited, so a slow or failing clean-up never delays or breaks setup.
|
|
10
|
+
* `OMMS_DISABLE_UPDATE_CHECK` does not stop it: removing old copies saves disk
|
|
11
|
+
* space and installs nothing. It keeps the copy that `~/.omms/runtime.json` names.
|
|
12
|
+
*/
|
|
13
|
+
function startCacheCleanup() {
|
|
14
|
+
void (async () => {
|
|
15
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
16
|
+
const [{ installedFromNpm }, { pruneOldCacheCopies }, { readRuntimeRecord }, { ommsDir }, { packageVersion }, { log },] = await Promise.all([
|
|
17
|
+
import("../services/host-self-update.js"),
|
|
18
|
+
import("../services/host-copies.js"),
|
|
19
|
+
import("../services/runtime-record.js"),
|
|
20
|
+
import("../services/runtime-handoff.js"),
|
|
21
|
+
import("../services/package-version.js"),
|
|
22
|
+
import("../services/logger.js"),
|
|
23
|
+
]);
|
|
24
|
+
if (!installedFromNpm(modulePath))
|
|
25
|
+
return;
|
|
26
|
+
const recorded = readRuntimeRecord(ommsDir());
|
|
27
|
+
await pruneOldCacheCopies({
|
|
28
|
+
modulePath,
|
|
29
|
+
runningVersion: packageVersion(),
|
|
30
|
+
keepRoots: recorded ? [recorded.root] : [],
|
|
31
|
+
now: Date.now(),
|
|
32
|
+
log: (message, data) => log(message, data),
|
|
33
|
+
});
|
|
34
|
+
})().catch(() => { });
|
|
35
|
+
}
|
|
36
|
+
let serviceUpdateStarted = false;
|
|
37
|
+
/**
|
|
38
|
+
* Check npm for a newer OMMS from the background service too, so OpenCode
|
|
39
|
+
* without a TUI (OpenChamber, `opencode serve`) also updates. Once per
|
|
40
|
+
* process: setup can run for each project. Not awaited; a failure is ignored.
|
|
41
|
+
*/
|
|
42
|
+
function startServiceSelfUpdate() {
|
|
43
|
+
if (serviceUpdateStarted)
|
|
44
|
+
return;
|
|
45
|
+
serviceUpdateStarted = true;
|
|
46
|
+
void (async () => {
|
|
47
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
48
|
+
const [{ opencodeSelfUpdateDeps, startOpencodeServiceSelfUpdate }, { startHostSelfUpdate }, { packageVersion }, { log },] = await Promise.all([
|
|
49
|
+
import("../adapters/opencode/self-update.js"),
|
|
50
|
+
import("../services/host-self-update.js"),
|
|
51
|
+
import("../services/package-version.js"),
|
|
52
|
+
import("../services/logger.js"),
|
|
53
|
+
]);
|
|
54
|
+
const deps = opencodeSelfUpdateDeps({
|
|
55
|
+
modulePath,
|
|
56
|
+
currentVersion: packageVersion(),
|
|
57
|
+
log: (message, data) => log(message, data),
|
|
58
|
+
});
|
|
59
|
+
startOpencodeServiceSelfUpdate({ run: () => startHostSelfUpdate(deps) });
|
|
60
|
+
})().catch(() => { });
|
|
61
|
+
}
|
|
6
62
|
const OmmsPluginV2 = {
|
|
7
63
|
id: "omms",
|
|
8
64
|
async setup(ctx) {
|
|
@@ -18,7 +74,10 @@ const OmmsPluginV2 = {
|
|
|
18
74
|
// context. Route internal structured-output calls through the adapter.
|
|
19
75
|
const { setV2Client } = await loadOpencodeProvider();
|
|
20
76
|
setV2Client(legacyClient);
|
|
21
|
-
|
|
77
|
+
const adapter = await registerV2Adapter(ctx, legacy, createV2MemoryBridge(ctx.location.directory));
|
|
78
|
+
startCacheCleanup();
|
|
79
|
+
startServiceSelfUpdate();
|
|
80
|
+
return adapter;
|
|
22
81
|
},
|
|
23
82
|
};
|
|
24
83
|
export default OmmsPluginV2;
|