om-memory-system 4.2.0 → 4.3.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/README.md CHANGED
@@ -137,8 +137,9 @@ import guides, and the [CLI reference](docs/cli.md).
137
137
  ## Keeping OMMS up to date
138
138
 
139
139
  Pi shows a notice when a new version is out; update with
140
- `pi update npm:om-memory-system`. On OpenCode v2, run `opencode plugin check`
141
- and `opencode plugin update om-memory-system`. Restart the agent afterwards.
140
+ `pi update npm:om-memory-system`. On OpenCode v2, the footer shows when a new version is out;
141
+ run `opencode plugin update om-memory-system`. Restart the agent afterwards.
142
+ [UPDATES.md](UPDATES.md) explains how updates work on every host.
142
143
  To stay on one version, install it with the number, for example
143
144
  `pi install npm:om-memory-system@3.1.1` or
144
145
  `opencode plugin add om-memory-system@3.1.1`; a pinned install is never
@@ -0,0 +1,28 @@
1
+ /** What the OpenCode footer shows: the web app's state and a newer release, if any. */
2
+ export interface TuiStatusState {
3
+ web: "warming" | "connected" | "offline";
4
+ update: string | null;
5
+ }
6
+ export declare function tuiStatusText(state: TuiStatusState): string;
7
+ /** The `latest` version on npm, or null when the registry cannot be reached. */
8
+ export declare function latestNpmVersion(fetchFn: typeof fetch, timeoutMs?: number): Promise<string | null>;
9
+ /** A newer release than `current`, ignoring prereleases; null otherwise. */
10
+ export declare function availableUpdate(current: string, latest: string | null): string | null;
11
+ export interface TuiStatusDeps {
12
+ currentVersion: string;
13
+ healthUrl: string;
14
+ fetch: typeof fetch;
15
+ setText: (text: string) => void;
16
+ /** Shown once per start when a newer release is on npm. */
17
+ notify: (message: string) => void;
18
+ checkUpdates?: boolean;
19
+ healthIntervalMs?: number;
20
+ updateIntervalMs?: number;
21
+ }
22
+ /**
23
+ * Keep the footer text current: the web app is polled, and npm is asked for a
24
+ * newer release at start and then every few hours. Returns a stop function.
25
+ */
26
+ export declare function startTuiStatus(deps: TuiStatusDeps): () => void;
27
+ /** Wire the status to this install: its version, the configured web app port, and global fetch. */
28
+ export declare function startInstalledTuiStatus(callbacks: Pick<TuiStatusDeps, "setText" | "notify">): Promise<() => void>;
@@ -0,0 +1,89 @@
1
+ import { isOlderVersion } from "../../services/version-compare.js";
2
+ export function tuiStatusText(state) {
3
+ const web = state.web === "offline" ? "omms:web app off" : `omms:${state.web}`;
4
+ return state.update ? `${web} · ${state.update} available` : web;
5
+ }
6
+ /** The `latest` version on npm, or null when the registry cannot be reached. */
7
+ export async function latestNpmVersion(fetchFn, timeoutMs = 5_000) {
8
+ try {
9
+ const response = await fetchFn("https://registry.npmjs.org/om-memory-system/latest", {
10
+ signal: AbortSignal.timeout(timeoutMs),
11
+ headers: { accept: "application/json" },
12
+ });
13
+ if (!response.ok)
14
+ return null;
15
+ const body = (await response.json());
16
+ return typeof body.version === "string" ? body.version : null;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ /** A newer release than `current`, ignoring prereleases; null otherwise. */
23
+ export function availableUpdate(current, latest) {
24
+ if (!latest || latest.includes("-"))
25
+ return null;
26
+ return isOlderVersion(current, latest) ? latest : null;
27
+ }
28
+ /**
29
+ * Keep the footer text current: the web app is polled, and npm is asked for a
30
+ * newer release at start and then every few hours. Returns a stop function.
31
+ */
32
+ export function startTuiStatus(deps) {
33
+ const state = { web: "warming", update: null };
34
+ let stopped = false;
35
+ let notified = null;
36
+ const show = () => {
37
+ if (!stopped)
38
+ deps.setText(tuiStatusText(state));
39
+ };
40
+ const checkHealth = async () => {
41
+ try {
42
+ const response = await deps.fetch(deps.healthUrl, { signal: AbortSignal.timeout(3_000) });
43
+ state.web = response.ok ? "connected" : "offline";
44
+ }
45
+ catch {
46
+ state.web = "offline";
47
+ }
48
+ show();
49
+ };
50
+ const checkUpdate = async () => {
51
+ const update = availableUpdate(deps.currentVersion, await latestNpmVersion(deps.fetch));
52
+ state.update = update;
53
+ show();
54
+ if (update && notified !== update && !stopped) {
55
+ notified = update;
56
+ deps.notify(`om-memory-system ${update} is available (running ${deps.currentVersion}). Run: opencode plugin update om-memory-system, then restart OpenCode.`);
57
+ }
58
+ };
59
+ show();
60
+ void checkHealth();
61
+ if (deps.checkUpdates !== false)
62
+ void checkUpdate();
63
+ const timers = [setInterval(() => void checkHealth(), deps.healthIntervalMs ?? 30_000)];
64
+ if (deps.checkUpdates !== false) {
65
+ timers.push(setInterval(() => void checkUpdate(), deps.updateIntervalMs ?? 6 * 3_600_000));
66
+ }
67
+ for (const timer of timers)
68
+ timer.unref?.();
69
+ return () => {
70
+ stopped = true;
71
+ for (const timer of timers)
72
+ clearInterval(timer);
73
+ };
74
+ }
75
+ /** Wire the status to this install: its version, the configured web app port, and global fetch. */
76
+ export async function startInstalledTuiStatus(callbacks) {
77
+ const [{ CONFIG }, { packageVersion }] = await Promise.all([
78
+ import("../../config.js"),
79
+ import("../../services/package-version.js"),
80
+ ]);
81
+ const host = CONFIG.webServerHost === "0.0.0.0" ? "127.0.0.1" : CONFIG.webServerHost;
82
+ return startTuiStatus({
83
+ ...callbacks,
84
+ currentVersion: packageVersion(),
85
+ healthUrl: `http://${host.includes(":") ? `[${host}]` : host}:${CONFIG.webServerPort}/api/health`,
86
+ fetch: globalThis.fetch.bind(globalThis),
87
+ checkUpdates: process.env.OMMS_DISABLE_UPDATE_CHECK !== "1",
88
+ });
89
+ }
@@ -18,6 +18,15 @@ export interface WebAutostartOptions {
18
18
  }
19
19
  /** OpenCode's executable is not a JavaScript runtime. */
20
20
  export declare function resolveWebRuntime(execPath?: string, platform?: NodeJS.Platform): string | null;
21
+ /** The package folder a written login item runs, read back from its command line. */
22
+ export declare function itemPackageRoot(file: string): string | null;
23
+ /**
24
+ * The newest OMMS copy among this one, the global install beside the runtime,
25
+ * and the copy the item already runs. A host starts OMMS from its own cache,
26
+ * which can be older than the global install; that copy must not downgrade
27
+ * the login item.
28
+ */
29
+ export declare function preferredPackageRoot(candidates: readonly (string | null | undefined)[]): string | null;
21
30
  /** Read the login item's state without changing any system files. */
22
31
  export declare function webAutostartStatus(options?: WebAutostartOptions): WebAutostartStatus;
23
32
  /** Install or update only the fixed-name OMMS item. `start` launches it now. */
@@ -6,6 +6,7 @@ import { basename, dirname, join, delimiter, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { CONFIG } from "../config.js";
8
8
  import { log } from "./logger.js";
9
+ import { compareVersions } from "./version-compare.js";
9
10
  const MARKER = "OMMS login item";
10
11
  const NAME = "io.github.cmdaltctr.omms.web";
11
12
  function executable(name) {
@@ -69,6 +70,67 @@ function packageRoot() {
69
70
  dir = parent;
70
71
  }
71
72
  }
73
+ function packageVersionAt(root) {
74
+ try {
75
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
76
+ if (pkg.name !== "om-memory-system" || !existsSync(join(root, "dist", "cli", "index.js"))) {
77
+ return null;
78
+ }
79
+ return typeof pkg.version === "string" ? pkg.version : null;
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ /** The package folder a written login item runs, read back from its command line. */
86
+ export function itemPackageRoot(file) {
87
+ try {
88
+ const content = readFileSync(file, "utf8");
89
+ const match = /([^"<>\r\n]*?)[\\/]dist[\\/]cli[\\/]index\.js/.exec(content);
90
+ const path = match?.[1]?.replace(/^.*?<string>/, "").trim();
91
+ if (!path)
92
+ return null;
93
+ // Only plists escape XML. Decode ampersands last to preserve literal entity text.
94
+ return content.includes("<plist")
95
+ ? path
96
+ .replaceAll("&quot;", '"')
97
+ .replaceAll("&apos;", "'")
98
+ .replaceAll("&lt;", "<")
99
+ .replaceAll("&gt;", ">")
100
+ .replaceAll("&amp;", "&")
101
+ : path;
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ }
107
+ /**
108
+ * The newest OMMS copy among this one, the global install beside the runtime,
109
+ * and the copy the item already runs. A host starts OMMS from its own cache,
110
+ * which can be older than the global install; that copy must not downgrade
111
+ * the login item.
112
+ */
113
+ export function preferredPackageRoot(candidates) {
114
+ let best = null;
115
+ for (const root of candidates) {
116
+ if (!root)
117
+ continue;
118
+ const version = packageVersionAt(root);
119
+ if (!version || compareVersions(version, version) === null)
120
+ continue;
121
+ if (!best || (compareVersions(version, best.version) ?? 0) > 0)
122
+ best = { root, version };
123
+ }
124
+ return best?.root ?? null;
125
+ }
126
+ function globalPackageRoot(runtime, platform) {
127
+ if (!runtime)
128
+ return null;
129
+ const prefix = dirname(dirname(runtime));
130
+ return platform === "win32"
131
+ ? join(dirname(runtime), "node_modules", "om-memory-system")
132
+ : join(prefix, "lib", "node_modules", "om-memory-system");
133
+ }
72
134
  function itemPath(home, platform) {
73
135
  if (platform === "darwin")
74
136
  return join(home, "Library", "LaunchAgents", `${NAME}.plist`);
@@ -83,7 +145,11 @@ function details(options) {
83
145
  const platform = options.platform ?? process.platform;
84
146
  const path = itemPath(home, platform);
85
147
  const runtime = options.runtime === undefined ? resolveWebRuntime() : options.runtime;
86
- const root = options.packageRoot === undefined ? packageRoot() : options.packageRoot;
148
+ const own = options.packageRoot === undefined ? packageRoot() : options.packageRoot;
149
+ const current = path && existsSync(path) ? itemPackageRoot(path) : null;
150
+ const root = options.packageRoot === undefined
151
+ ? preferredPackageRoot([own, globalPackageRoot(runtime ?? null, platform), current])
152
+ : own;
87
153
  const supported = platform !== "linux" || (options.systemctlAvailable ?? Boolean(executable("systemctl")));
88
154
  return { home, platform, path, runtime, root, supported };
89
155
  }