om-memory-system 5.7.0-next.218 → 5.9.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/cli/web-command.js +1 -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.d.ts +1 -1
- package/dist/services/web-server.js +2 -1
- package/dist/services/web-update-runtime.d.ts +1 -1
- package/dist/services/web-update-runtime.js +2 -1
- package/dist/services/web-update.d.ts +8 -0
- package/dist/services/web-update.js +27 -0
- package/dist/v2/plugin.js +60 -1
- package/dist/web/assets/index-Bhk1P5Z7.js +170 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-C6WnDE6h.js +0 -170
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type HostSelfUpdateDeps } from "../../services/host-self-update.js";
|
|
2
|
+
/** The background service checks npm at start and then this often, like the footer. */
|
|
3
|
+
export declare const OPENCODE_SERVICE_CHECK_MS: number;
|
|
4
|
+
/** Production inputs for OpenCode: npm, the OpenCode marker file and a detached `opencode plugin update`. */
|
|
5
|
+
export declare function opencodeSelfUpdateDeps(options: {
|
|
6
|
+
modulePath: string;
|
|
7
|
+
currentVersion: string;
|
|
8
|
+
log: HostSelfUpdateDeps["log"];
|
|
9
|
+
home?: string;
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
}): HostSelfUpdateDeps;
|
|
12
|
+
/**
|
|
13
|
+
* Run the update check now and then every `intervalMs`. A failed check never
|
|
14
|
+
* throws. The timer does not keep the process alive. Returns a stop function.
|
|
15
|
+
*/
|
|
16
|
+
export declare function startOpencodeServiceSelfUpdate(options: {
|
|
17
|
+
run: () => unknown;
|
|
18
|
+
intervalMs?: number;
|
|
19
|
+
}): () => void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { hostSelfUpdateDeps, installedFromNpm, unpinnedCacheFolder, } from "../../services/host-self-update.js";
|
|
4
|
+
// OpenCode's update inputs, shared by the TUI footer and the background
|
|
5
|
+
// service. The service runs the check too, so OpenCode without a TUI (for
|
|
6
|
+
// example OpenChamber or `opencode serve`) also updates. The marker file stops
|
|
7
|
+
// the two from starting the same update twice.
|
|
8
|
+
/** The background service checks npm at start and then this often, like the footer. */
|
|
9
|
+
export const OPENCODE_SERVICE_CHECK_MS = 6 * 3_600_000;
|
|
10
|
+
/** Production inputs for OpenCode: npm, the OpenCode marker file and a detached `opencode plugin update`. */
|
|
11
|
+
export function opencodeSelfUpdateDeps(options) {
|
|
12
|
+
const { modulePath } = options;
|
|
13
|
+
return hostSelfUpdateDeps({
|
|
14
|
+
logLabel: "OpenCode plugin self-update",
|
|
15
|
+
currentVersion: options.currentVersion,
|
|
16
|
+
command: "opencode",
|
|
17
|
+
args: ["plugin", "update", "om-memory-system"],
|
|
18
|
+
markerPath: join(options.home ?? homedir(), ".omms", "opencode-plugin-update.json"),
|
|
19
|
+
modulePath,
|
|
20
|
+
installSource: () => !installedFromNpm(modulePath) ? "local" : unpinnedCacheFolder(modulePath) ? "npm" : "pinned",
|
|
21
|
+
log: options.log,
|
|
22
|
+
...(options.env ? { env: options.env } : {}),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Run the update check now and then every `intervalMs`. A failed check never
|
|
27
|
+
* throws. The timer does not keep the process alive. Returns a stop function.
|
|
28
|
+
*/
|
|
29
|
+
export function startOpencodeServiceSelfUpdate(options) {
|
|
30
|
+
const check = () => {
|
|
31
|
+
try {
|
|
32
|
+
void Promise.resolve(options.run()).catch(() => { });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// The update is housekeeping. It never breaks the service.
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
check();
|
|
39
|
+
const timer = setInterval(check, options.intervalMs ?? OPENCODE_SERVICE_CHECK_MS);
|
|
40
|
+
timer.unref?.();
|
|
41
|
+
return () => clearInterval(timer);
|
|
42
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { type HostSelfUpdateResult, type HostUpdateStatus } from "../../services/host-self-update.js";
|
|
1
2
|
/** What the OpenCode footer shows: the web app's state and a newer release, if any. */
|
|
2
3
|
export interface TuiStatusState {
|
|
3
4
|
web: "warming" | "connected" | "offline";
|
|
4
5
|
update: string | null;
|
|
6
|
+
/** "installed" once the background update has finished; otherwise "available". */
|
|
7
|
+
updateState?: "available" | "installed";
|
|
5
8
|
}
|
|
6
9
|
export declare function tuiStatusText(state: TuiStatusState): string;
|
|
7
10
|
export interface TuiStatusDeps {
|
|
@@ -9,15 +12,22 @@ export interface TuiStatusDeps {
|
|
|
9
12
|
healthUrl: string;
|
|
10
13
|
fetch: typeof fetch;
|
|
11
14
|
setText: (text: string) => void;
|
|
12
|
-
/** Shown once per
|
|
15
|
+
/** Shown once per update state and version. */
|
|
13
16
|
notify: (message: string) => void;
|
|
14
17
|
checkUpdates?: boolean;
|
|
15
18
|
healthIntervalMs?: number;
|
|
16
19
|
updateIntervalMs?: number;
|
|
20
|
+
/** Reads npm and starts the background update. Without it the check only reads npm. */
|
|
21
|
+
selfUpdate?: () => Promise<HostSelfUpdateResult>;
|
|
22
|
+
/** The state the update marker records for a version. */
|
|
23
|
+
readUpdateStatus?: (version: string) => HostUpdateStatus | null;
|
|
24
|
+
watchIntervalMs?: number;
|
|
25
|
+
watchLimitMs?: number;
|
|
17
26
|
}
|
|
18
27
|
/**
|
|
19
28
|
* Keep the footer text current: the web app is polled, and npm is asked for a
|
|
20
|
-
* newer release at start and then every few hours.
|
|
29
|
+
* newer release at start and then every few hours. A newer release installs in
|
|
30
|
+
* the background when the host allows it. Returns a stop function.
|
|
21
31
|
*/
|
|
22
32
|
export declare function startTuiStatus(deps: TuiStatusDeps): () => void;
|
|
23
33
|
/** Wire the status to this install: its version, the configured web app port, and global fetch. */
|
|
@@ -1,17 +1,27 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { hostUpdateStatus, startHostSelfUpdate, updateNotice, updateSuffix, watchHostUpdate, } from "../../services/host-self-update.js";
|
|
1
3
|
import { ommsStatusLabel } from "../../services/status-glyphs.js";
|
|
4
|
+
import { opencodeSelfUpdateDeps } from "./self-update.js";
|
|
2
5
|
import { availableUpdate, latestNpmVersion } from "../../services/update-check.js";
|
|
6
|
+
const UPDATE_COMMAND = "opencode plugin update om-memory-system";
|
|
7
|
+
// The background service keeps the old copy loaded until it restarts too.
|
|
8
|
+
const RESTART = "restart OpenCode and run opencode service restart";
|
|
3
9
|
export function tuiStatusText(state) {
|
|
4
10
|
const web = ommsStatusLabel(state.web === "offline" ? "web app off" : state.web);
|
|
5
|
-
return state.update
|
|
11
|
+
return state.update
|
|
12
|
+
? `${web}${updateSuffix(state.update, state.updateState ?? "available")}`
|
|
13
|
+
: web;
|
|
6
14
|
}
|
|
7
15
|
/**
|
|
8
16
|
* Keep the footer text current: the web app is polled, and npm is asked for a
|
|
9
|
-
* newer release at start and then every few hours.
|
|
17
|
+
* newer release at start and then every few hours. A newer release installs in
|
|
18
|
+
* the background when the host allows it. Returns a stop function.
|
|
10
19
|
*/
|
|
11
20
|
export function startTuiStatus(deps) {
|
|
12
21
|
const state = { web: "warming", update: null };
|
|
13
22
|
let stopped = false;
|
|
14
|
-
let
|
|
23
|
+
let stopWatch;
|
|
24
|
+
const shown = new Set();
|
|
15
25
|
const show = () => {
|
|
16
26
|
if (!stopped)
|
|
17
27
|
deps.setText(tuiStatusText(state));
|
|
@@ -26,13 +36,65 @@ export function startTuiStatus(deps) {
|
|
|
26
36
|
}
|
|
27
37
|
show();
|
|
28
38
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
/** One toast per update state and version. */
|
|
40
|
+
const toast = (update, notice) => {
|
|
41
|
+
const key = `${notice}:${update}`;
|
|
42
|
+
if (stopped || shown.has(key))
|
|
43
|
+
return;
|
|
44
|
+
shown.add(key);
|
|
45
|
+
deps.notify(updateNotice({
|
|
46
|
+
hostName: "OpenCode",
|
|
47
|
+
command: UPDATE_COMMAND,
|
|
48
|
+
restart: RESTART,
|
|
49
|
+
current: deps.currentVersion,
|
|
50
|
+
update,
|
|
51
|
+
state: notice,
|
|
52
|
+
}));
|
|
53
|
+
};
|
|
54
|
+
const settle = (update, outcome) => {
|
|
55
|
+
state.updateState = outcome === "installed" ? "installed" : "available";
|
|
32
56
|
show();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
toast(update, outcome === "installed" ? "installed" : "available");
|
|
58
|
+
};
|
|
59
|
+
const selfUpdate = deps.selfUpdate ??
|
|
60
|
+
(async () => ({
|
|
61
|
+
code: "local",
|
|
62
|
+
update: availableUpdate(deps.currentVersion, await latestNpmVersion(deps.fetch)),
|
|
63
|
+
}));
|
|
64
|
+
const checkUpdate = async () => {
|
|
65
|
+
try {
|
|
66
|
+
// The restart installs the release; there is nothing more to ask npm.
|
|
67
|
+
if (state.updateState === "installed")
|
|
68
|
+
return;
|
|
69
|
+
const { code, update } = await selfUpdate();
|
|
70
|
+
state.update = update;
|
|
71
|
+
state.updateState = "available";
|
|
72
|
+
show();
|
|
73
|
+
if (!update || stopped)
|
|
74
|
+
return;
|
|
75
|
+
if (code !== "started" && code !== "recent")
|
|
76
|
+
return toast(update, "available");
|
|
77
|
+
const status = (deps.readUpdateStatus ?? (() => null))(update);
|
|
78
|
+
if (status === "installed" || status === "failed")
|
|
79
|
+
return settle(update, status);
|
|
80
|
+
toast(update, "installing");
|
|
81
|
+
stopWatch?.();
|
|
82
|
+
stopWatch = watchHostUpdate({
|
|
83
|
+
read: () => (deps.readUpdateStatus ?? (() => null))(update),
|
|
84
|
+
onSettled: (outcome) => {
|
|
85
|
+
try {
|
|
86
|
+
settle(update, outcome);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// A failed toast must not break the timer.
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
intervalMs: deps.watchIntervalMs,
|
|
93
|
+
maxMs: deps.watchLimitMs,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// A failed update check must not break the footer.
|
|
36
98
|
}
|
|
37
99
|
};
|
|
38
100
|
show();
|
|
@@ -47,20 +109,31 @@ export function startTuiStatus(deps) {
|
|
|
47
109
|
timer.unref?.();
|
|
48
110
|
return () => {
|
|
49
111
|
stopped = true;
|
|
112
|
+
stopWatch?.();
|
|
50
113
|
for (const timer of timers)
|
|
51
114
|
clearInterval(timer);
|
|
52
115
|
};
|
|
53
116
|
}
|
|
54
117
|
/** Wire the status to this install: its version, the configured web app port, and global fetch. */
|
|
55
118
|
export async function startInstalledTuiStatus(callbacks) {
|
|
56
|
-
const [{ CONFIG }, { packageVersion }] = await Promise.all([
|
|
119
|
+
const [{ CONFIG }, { packageVersion }, { log }] = await Promise.all([
|
|
57
120
|
import("../../config.js"),
|
|
58
121
|
import("../../services/package-version.js"),
|
|
122
|
+
import("../../services/logger.js"),
|
|
59
123
|
]);
|
|
60
124
|
const host = CONFIG.webServerHost === "0.0.0.0" ? "127.0.0.1" : CONFIG.webServerHost;
|
|
125
|
+
const currentVersion = packageVersion();
|
|
126
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
127
|
+
const update = opencodeSelfUpdateDeps({
|
|
128
|
+
modulePath,
|
|
129
|
+
currentVersion,
|
|
130
|
+
log: (message, data) => log(message, data),
|
|
131
|
+
});
|
|
61
132
|
return startTuiStatus({
|
|
62
133
|
...callbacks,
|
|
63
|
-
currentVersion
|
|
134
|
+
currentVersion,
|
|
135
|
+
selfUpdate: () => startHostSelfUpdate(update),
|
|
136
|
+
readUpdateStatus: (version) => hostUpdateStatus(update, version),
|
|
64
137
|
healthUrl: `http://${host.includes(":") ? `[${host}]` : host}:${CONFIG.webServerPort}/api/health`,
|
|
65
138
|
fetch: globalThis.fetch.bind(globalThis),
|
|
66
139
|
checkUpdates: process.env.OMMS_DISABLE_UPDATE_CHECK !== "1",
|
|
@@ -14,6 +14,7 @@ import { registerPiSkillsTool } from "./skills-tool.js";
|
|
|
14
14
|
import { performPiProfileLearning } from "./profile.js";
|
|
15
15
|
import { createPiRecallRows } from "./recall-rows.js";
|
|
16
16
|
import { createPiSessionContext } from "./session-context.js";
|
|
17
|
+
import { createPiSelfUpdate } from "./self-update.js";
|
|
17
18
|
import { DEFAULT_RETRIEVAL_MAX_TOKENS, tokensToByteCeiling } from "../../core/context-budget.js";
|
|
18
19
|
import { ommsStatusLabel } from "../../services/status-glyphs.js";
|
|
19
20
|
const GLOBAL_PLUGIN_WARMUP_KEY = Symbol.for("omms.plugin.warmedup");
|
|
@@ -82,21 +83,30 @@ export default function ommsPiExtension(pi) {
|
|
|
82
83
|
let latestCtx = null;
|
|
83
84
|
let backfillSessionId = null;
|
|
84
85
|
let statusVersion = 0;
|
|
86
|
+
// The glyph on screen, so a new update suffix can redraw the label. Null once cleared.
|
|
87
|
+
let shownStatus = null;
|
|
88
|
+
const selfUpdate = createPiSelfUpdate();
|
|
85
89
|
const recallRows = createPiRecallRows();
|
|
86
90
|
const sessionContext = createPiSessionContext(recallRows);
|
|
91
|
+
// Every label carries the update suffix, so it stays when the glyph changes.
|
|
92
|
+
const drawStatus = (ctx, status) => {
|
|
93
|
+
shownStatus = status;
|
|
94
|
+
if (ctx.hasUI)
|
|
95
|
+
ctx.ui.setStatus(OMMS_STATUS_KEY, ommsStatusLabel(status) + selfUpdate.suffix());
|
|
96
|
+
};
|
|
87
97
|
const startStatus = (ctx, status) => {
|
|
88
98
|
const version = ++statusVersion;
|
|
89
|
-
|
|
90
|
-
ctx.ui.setStatus(OMMS_STATUS_KEY, ommsStatusLabel(status));
|
|
99
|
+
drawStatus(ctx, status);
|
|
91
100
|
return version;
|
|
92
101
|
};
|
|
93
102
|
const finishStatus = (ctx, version, status) => {
|
|
94
103
|
if (version !== statusVersion || !ctx.hasUI)
|
|
95
104
|
return;
|
|
96
|
-
ctx
|
|
105
|
+
drawStatus(ctx, status);
|
|
97
106
|
};
|
|
98
107
|
const clearStatus = (ctx) => {
|
|
99
108
|
statusVersion++;
|
|
109
|
+
shownStatus = null;
|
|
100
110
|
if (ctx.hasUI)
|
|
101
111
|
ctx.ui.setStatus(OMMS_STATUS_KEY, undefined);
|
|
102
112
|
};
|
|
@@ -108,6 +118,11 @@ export default function ommsPiExtension(pi) {
|
|
|
108
118
|
};
|
|
109
119
|
pi.on("session_start", async (event, ctx) => {
|
|
110
120
|
const status = startStatus(ctx, "warming");
|
|
121
|
+
// The update check does not delay the session and cannot stop memory work.
|
|
122
|
+
void selfUpdate.start({ hasUI: ctx.hasUI, notify: (message, level) => ctx.ui.notify(message, level) }, () => {
|
|
123
|
+
if (shownStatus)
|
|
124
|
+
drawStatus(ctx, shownStatus);
|
|
125
|
+
});
|
|
111
126
|
try {
|
|
112
127
|
latestCtx = ctx;
|
|
113
128
|
initConfigWithLegacyMigration(ctx.cwd);
|
|
@@ -309,6 +324,7 @@ export default function ommsPiExtension(pi) {
|
|
|
309
324
|
}
|
|
310
325
|
});
|
|
311
326
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
327
|
+
selfUpdate.stop();
|
|
312
328
|
clearStatus(ctx);
|
|
313
329
|
recallRows.reset(ctx);
|
|
314
330
|
sessionContext.reset();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { watchHostUpdate, type HostSelfUpdateDeps } from "../../services/host-self-update.js";
|
|
2
|
+
/** What the update needs from one Pi session. */
|
|
3
|
+
export interface PiUpdateSession {
|
|
4
|
+
hasUI: boolean;
|
|
5
|
+
notify: (message: string, level: "info" | "warning") => void;
|
|
6
|
+
}
|
|
7
|
+
export interface PiSelfUpdate {
|
|
8
|
+
/** Check npm and start the update. Never rejects. The caller does not wait for it. */
|
|
9
|
+
start(session: PiUpdateSession, onSuffixChange: () => void): Promise<void>;
|
|
10
|
+
/** Footer text to add after the status glyph, or an empty string. */
|
|
11
|
+
suffix(): string;
|
|
12
|
+
/** Stop the marker poll and drop the session. Keeps the suffix for the next session. */
|
|
13
|
+
stop(): void;
|
|
14
|
+
}
|
|
15
|
+
export interface PiSelfUpdateOptions {
|
|
16
|
+
hostDeps?: () => HostSelfUpdateDeps;
|
|
17
|
+
watch?: typeof watchHostUpdate;
|
|
18
|
+
log?: (message: string, data: Record<string, unknown>, level?: "error") => void;
|
|
19
|
+
}
|
|
20
|
+
/** Production inputs for Pi: npm, the Pi marker file and a detached `pi update`. */
|
|
21
|
+
export declare function piHostSelfUpdateDeps(options?: {
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
modulePath?: string;
|
|
24
|
+
}): HostSelfUpdateDeps;
|
|
25
|
+
export declare function createPiSelfUpdate(options?: PiSelfUpdateOptions): PiSelfUpdate;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { hostSelfUpdateDeps, hostUpdateStatus, installedFromNpm, startHostSelfUpdate, unpinnedPackageSource, updateNotice, updateSuffix, watchHostUpdate, } from "../../services/host-self-update.js";
|
|
6
|
+
import { log } from "../../services/logger.js";
|
|
7
|
+
import { packageVersion } from "../../services/package-version.js";
|
|
8
|
+
// Pi does not update its packages by itself. At session start OMMS runs
|
|
9
|
+
// `pi update npm:om-memory-system` in the background when npm has a newer
|
|
10
|
+
// release, then tells the user to restart. The decision lives in
|
|
11
|
+
// `src/services/host-self-update.ts`; this file adds the Pi wording, the
|
|
12
|
+
// footer suffix and the install check.
|
|
13
|
+
const PI_UPDATE_ARGS = ["update", "npm:om-memory-system"];
|
|
14
|
+
const PI_UPDATE_COMMAND = `pi ${PI_UPDATE_ARGS.join(" ")}`;
|
|
15
|
+
/** Pi's settings file lists the package source. `PI_CODING_AGENT_DIR` moves Pi's folder. */
|
|
16
|
+
function readPiSettings(env) {
|
|
17
|
+
const dir = env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
18
|
+
try {
|
|
19
|
+
return readFileSync(join(dir, "settings.json"), "utf8");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Production inputs for Pi: npm, the Pi marker file and a detached `pi update`. */
|
|
26
|
+
export function piHostSelfUpdateDeps(options = {}) {
|
|
27
|
+
const env = options.env ?? process.env;
|
|
28
|
+
const modulePath = options.modulePath ?? fileURLToPath(import.meta.url);
|
|
29
|
+
const installSource = () => {
|
|
30
|
+
if (!installedFromNpm(modulePath))
|
|
31
|
+
return "local";
|
|
32
|
+
return unpinnedPackageSource(readPiSettings(env)) ? "npm" : "pinned";
|
|
33
|
+
};
|
|
34
|
+
return hostSelfUpdateDeps({
|
|
35
|
+
logLabel: "Pi plugin self-update",
|
|
36
|
+
currentVersion: packageVersion(),
|
|
37
|
+
command: "pi",
|
|
38
|
+
args: PI_UPDATE_ARGS,
|
|
39
|
+
markerPath: join(homedir(), ".omms", "pi-plugin-update.json"),
|
|
40
|
+
modulePath,
|
|
41
|
+
installSource,
|
|
42
|
+
log,
|
|
43
|
+
env,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function createPiSelfUpdate(options = {}) {
|
|
47
|
+
const hostDeps = options.hostDeps ?? (() => piHostSelfUpdateDeps());
|
|
48
|
+
const watch = options.watch ?? watchHostUpdate;
|
|
49
|
+
const logError = options.log ?? log;
|
|
50
|
+
// One notification per state per version for the life of the Pi process.
|
|
51
|
+
const notified = new Set();
|
|
52
|
+
let suffix = "";
|
|
53
|
+
let stopWatch = null;
|
|
54
|
+
let generation = 0;
|
|
55
|
+
let current = null;
|
|
56
|
+
// Sessions that start while a check runs share it, so one release spawns one update.
|
|
57
|
+
let inFlight = null;
|
|
58
|
+
// Once installed, only a restart helps: a later session must not run the update again.
|
|
59
|
+
let installed = false;
|
|
60
|
+
const attempt = (action) => {
|
|
61
|
+
try {
|
|
62
|
+
action();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// A stale Pi context must not stop the update path.
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const notifyOnce = (key, message, level) => {
|
|
69
|
+
if (!current || notified.has(key))
|
|
70
|
+
return;
|
|
71
|
+
notified.add(key);
|
|
72
|
+
const { session } = current;
|
|
73
|
+
attempt(() => session.notify(message, level));
|
|
74
|
+
};
|
|
75
|
+
const show = (deps, update, state) => {
|
|
76
|
+
if (state === "installed")
|
|
77
|
+
installed = true;
|
|
78
|
+
if (state !== "installing") {
|
|
79
|
+
const next = updateSuffix(update, state);
|
|
80
|
+
if (next !== suffix) {
|
|
81
|
+
suffix = next;
|
|
82
|
+
const change = current?.onSuffixChange;
|
|
83
|
+
if (change)
|
|
84
|
+
attempt(change);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const text = updateNotice({
|
|
88
|
+
hostName: "Pi",
|
|
89
|
+
command: PI_UPDATE_COMMAND,
|
|
90
|
+
current: deps.currentVersion,
|
|
91
|
+
update,
|
|
92
|
+
state,
|
|
93
|
+
});
|
|
94
|
+
notifyOnce(`${state}:${update}`, text, state === "available" ? "warning" : "info");
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
async start(session, onSuffixChange) {
|
|
98
|
+
const mine = ++generation;
|
|
99
|
+
stopWatch?.();
|
|
100
|
+
stopWatch = null;
|
|
101
|
+
current = session.hasUI ? { session, onSuffixChange } : null;
|
|
102
|
+
if (installed)
|
|
103
|
+
return;
|
|
104
|
+
try {
|
|
105
|
+
const deps = hostDeps();
|
|
106
|
+
inFlight ??= startHostSelfUpdate(deps).finally(() => {
|
|
107
|
+
inFlight = null;
|
|
108
|
+
});
|
|
109
|
+
const { code, update } = await inFlight;
|
|
110
|
+
// No interface, no label or notice. A shutdown or newer session replaces this one.
|
|
111
|
+
if (!update || !session.hasUI || mine !== generation)
|
|
112
|
+
return;
|
|
113
|
+
if (code !== "started" && code !== "recent") {
|
|
114
|
+
show(deps, update, "available");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const read = () => hostUpdateStatus(deps, update);
|
|
118
|
+
const status = read();
|
|
119
|
+
if (status === "installed")
|
|
120
|
+
show(deps, update, "installed");
|
|
121
|
+
else if (status === "failed")
|
|
122
|
+
show(deps, update, "available");
|
|
123
|
+
else {
|
|
124
|
+
show(deps, update, "installing");
|
|
125
|
+
stopWatch = watch({
|
|
126
|
+
read,
|
|
127
|
+
onSettled: (outcome) => {
|
|
128
|
+
if (mine === generation)
|
|
129
|
+
show(deps, update, outcome === "installed" ? "installed" : "available");
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
logError("Pi plugin self-update failed", { code: error instanceof Error ? error.name : "unknown" }, "error");
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
suffix: () => suffix,
|
|
139
|
+
stop() {
|
|
140
|
+
generation++;
|
|
141
|
+
stopWatch?.();
|
|
142
|
+
stopWatch = null;
|
|
143
|
+
current = null;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -107,6 +107,18 @@ function parseSpaceArgs(argv) {
|
|
|
107
107
|
parsed.directory = parsed.values.get("directory");
|
|
108
108
|
return parsed;
|
|
109
109
|
}
|
|
110
|
+
/** One line per space for the list action: the display name first, the id after it. */
|
|
111
|
+
function spaceListLines(result) {
|
|
112
|
+
const spaces = (result.spaces ?? []);
|
|
113
|
+
return spaces.map((space) => {
|
|
114
|
+
let line = `${space.name} (${space.id})`;
|
|
115
|
+
if (space.active)
|
|
116
|
+
line += " [active]";
|
|
117
|
+
if (space.archived)
|
|
118
|
+
line += " [archived]";
|
|
119
|
+
return line;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
110
122
|
/** Run `om-memory-system space ...` and return the process exit code. */
|
|
111
123
|
export async function runSpaceCommand(argv, io = {}) {
|
|
112
124
|
const stdout = io.stdout ?? ((text) => console.log(text));
|
|
@@ -184,6 +196,14 @@ export async function runSpaceCommand(argv, io = {}) {
|
|
|
184
196
|
catch (error) {
|
|
185
197
|
result = { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
186
198
|
}
|
|
187
|
-
|
|
199
|
+
if (action === "list" && result.success !== false) {
|
|
200
|
+
// The list action prints one text line per space; every other action prints JSON.
|
|
201
|
+
if (typeof result.problem === "string")
|
|
202
|
+
stderr(result.problem);
|
|
203
|
+
stdout(spaceListLines(result).join("\n"));
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
stdout(JSON.stringify(result, null, 2));
|
|
207
|
+
}
|
|
188
208
|
return result.success === false ? 1 : 0;
|
|
189
209
|
}
|
package/dist/cli/web-command.js
CHANGED
|
@@ -156,7 +156,7 @@ export async function runWebCommand(args, options = {}, online, handover = {}, u
|
|
|
156
156
|
const { existsSync } = await import("node:fs");
|
|
157
157
|
// Every restart onto the new copy runs the launcher, the login item's included.
|
|
158
158
|
const canRestart = () => existsSync(launcherPath(ommsDir()));
|
|
159
|
-
server.setWebUpdate(startWebUpdate(() => powerAction("update"), canRestart));
|
|
159
|
+
server.setWebUpdate(startWebUpdate(() => powerAction("update"), canRestart, () => server.powerActionRunning));
|
|
160
160
|
// A newer `web install` asked this web app to give up the port.
|
|
161
161
|
server.setOnStepAside(async () => {
|
|
162
162
|
clearInterval(keeper);
|
package/dist/config.js
CHANGED
|
@@ -20,6 +20,7 @@ import { checkedKeywordBoost, checkedKeywordMode, } from "./utils/keyword-boost.
|
|
|
20
20
|
import { runLegacyStoreMigration, legacyMigrationPaths } from "./services/legacy-migration.js";
|
|
21
21
|
import { expandPath, storagePathFrom, withSkillStorage, } from "./services/spaces/storage-resolution.js";
|
|
22
22
|
import { resolveActiveSpace } from "./services/spaces/active-space.js";
|
|
23
|
+
import { backfillSpaceLabelsFromDisk } from "./services/spaces/space-label.js";
|
|
23
24
|
import { resetStoreHandles } from "./services/spaces/store-handles.js";
|
|
24
25
|
const OMMS_CONFIG_DIR = join(homedir(), ".config", "omms");
|
|
25
26
|
const DATA_DIR = join(homedir(), ".omms");
|
|
@@ -555,7 +556,16 @@ export function initConfig(directory, options = {}) {
|
|
|
555
556
|
delete projectOverrides.webServerEnabled;
|
|
556
557
|
const merged = { ...globalConfig, ...projectOverrides };
|
|
557
558
|
// The active memory space picks the storage folder unless the project sets its own (ADR-041).
|
|
558
|
-
|
|
559
|
+
const globalStoragePath = storagePathFrom(globalConfig);
|
|
560
|
+
activeSpace = resolveActiveSpace(DATA_DIR, globalStoragePath, "storagePath" in projectConfig);
|
|
561
|
+
// The label pass repairs space.json files at the first registry read. It is
|
|
562
|
+
// metadata only, so a failure must never block start-up.
|
|
563
|
+
try {
|
|
564
|
+
backfillSpaceLabelsFromDisk(DATA_DIR, globalStoragePath);
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
// Best effort: the registry stays the source of truth for names.
|
|
568
|
+
}
|
|
559
569
|
if (activeSpace.storagePath)
|
|
560
570
|
merged.storagePath = activeSpace.storagePath;
|
|
561
571
|
const nextConfig = withSkillStorage(buildConfig(merged), globalConfig);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** The clean-up leaves everything alone until the running copy is this old. */
|
|
2
|
+
export declare const CACHE_PRUNE_MIN_AGE_MS: number;
|
|
3
|
+
export interface HostCopy {
|
|
4
|
+
/** The package folder: `.../node_modules/om-memory-system`. */
|
|
5
|
+
root: string;
|
|
6
|
+
/** The `version` in its `package.json`, or null when that cannot be read. */
|
|
7
|
+
version: string | null;
|
|
8
|
+
/** The timestamp folder name as a number (install time in ms since 1970). */
|
|
9
|
+
stamp: number;
|
|
10
|
+
}
|
|
11
|
+
export interface HostCopies {
|
|
12
|
+
root: string;
|
|
13
|
+
version: string | null;
|
|
14
|
+
/** The running copy's timestamp, or null when its folder is not a timestamp folder in an OpenCode cache. */
|
|
15
|
+
stamp: number | null;
|
|
16
|
+
/** Every other timestamp folder in the same cache folder. Empty without a timestamp. */
|
|
17
|
+
siblings: HostCopy[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The running copy and, when it sits in a timestamp folder, the other copies in
|
|
21
|
+
* the same cache folder. Null when `modulePath` is not inside
|
|
22
|
+
* `node_modules/om-memory-system/`. Never throws.
|
|
23
|
+
*/
|
|
24
|
+
export declare function listHostCopies(modulePath: string): HostCopies | null;
|
|
25
|
+
/**
|
|
26
|
+
* The versions on disk: the running copy's and each sibling's. Pi updates in
|
|
27
|
+
* place, so its running copy shows the new version after `pi update`. OpenCode
|
|
28
|
+
* shows it in a new sibling folder. Never throws.
|
|
29
|
+
*/
|
|
30
|
+
export declare function installedVersions(modulePath: string): string[];
|
|
31
|
+
export interface PruneOptions {
|
|
32
|
+
modulePath: string;
|
|
33
|
+
runningVersion: string;
|
|
34
|
+
/** Package folders that must stay, such as the copy `~/.omms/runtime.json` records. */
|
|
35
|
+
keepRoots: string[];
|
|
36
|
+
now: number;
|
|
37
|
+
/** Removes a folder and everything in it. Defaults to an asynchronous `rm`, so a large folder does not block the service. */
|
|
38
|
+
remove?: (folder: string) => void | Promise<void>;
|
|
39
|
+
log: (message: string, data: Record<string, unknown>) => void | Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Remove old OMMS copies from the running copy's cache folder. A copy goes only
|
|
43
|
+
* when its timestamp and its version are both older than the running copy's,
|
|
44
|
+
* it is not in `keepRoots`, and the running copy is at least
|
|
45
|
+
* `CACHE_PRUNE_MIN_AGE_MS` old. A copy that cannot be removed stays. Logs one
|
|
46
|
+
* `OpenCode cache clean-up` record with counts for each pass in a timestamp
|
|
47
|
+
* folder, also when the counts are zero. Never throws.
|
|
48
|
+
*/
|
|
49
|
+
export declare function pruneOldCacheCopies(options: PruneOptions): Promise<{
|
|
50
|
+
removed: number;
|
|
51
|
+
kept: number;
|
|
52
|
+
}>;
|