@mnemom/mnemom 0.17.2-next.7 → 0.17.2-next.8

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.
Files changed (43) hide show
  1. package/dist/commands/agent-doctor.d.ts +1 -1
  2. package/dist/commands/agent-doctor.js +5 -1
  3. package/dist/commands/agent-setup.d.ts +34 -3
  4. package/dist/commands/agent-setup.js +86 -11
  5. package/dist/commands/agent.js +11 -3
  6. package/dist/commands/config.d.ts +36 -0
  7. package/dist/commands/config.js +82 -0
  8. package/dist/commands/update.d.ts +30 -0
  9. package/dist/commands/update.js +90 -0
  10. package/dist/index.js +41 -0
  11. package/dist/lib/agent.d.ts +13 -7
  12. package/dist/lib/agent.js +12 -12
  13. package/dist/lib/cli-config.d.ts +8 -0
  14. package/dist/lib/cli-config.js +13 -0
  15. package/dist/lib/update/action.d.ts +9 -0
  16. package/dist/lib/update/action.js +17 -0
  17. package/dist/lib/update/decide.d.ts +152 -0
  18. package/dist/lib/update/decide.js +195 -0
  19. package/dist/lib/update/doctor.d.ts +18 -0
  20. package/dist/lib/update/doctor.js +63 -0
  21. package/dist/lib/update/install-method.d.ts +12 -0
  22. package/dist/lib/update/install-method.js +33 -0
  23. package/dist/lib/update/min-version.d.ts +34 -0
  24. package/dist/lib/update/min-version.js +69 -0
  25. package/dist/lib/update/registry.d.ts +27 -0
  26. package/dist/lib/update/registry.js +50 -0
  27. package/dist/lib/update/run.d.ts +54 -0
  28. package/dist/lib/update/run.js +100 -0
  29. package/dist/lib/update/semver.d.ts +22 -0
  30. package/dist/lib/update/semver.js +65 -0
  31. package/dist/lib/update/startup.d.ts +27 -0
  32. package/dist/lib/update/startup.js +75 -0
  33. package/dist/lib/update/state.d.ts +23 -0
  34. package/dist/lib/update/state.js +83 -0
  35. package/dist/lib/update/system.d.ts +55 -0
  36. package/dist/lib/update/system.js +104 -0
  37. package/dist/lib/update/worker.d.ts +42 -0
  38. package/dist/lib/update/worker.js +104 -0
  39. package/dist/lib/version-gate.d.ts +2 -1
  40. package/dist/lib/version-gate.js +9 -1
  41. package/dist/update-worker.d.ts +6 -0
  42. package/dist/update-worker.js +8 -0
  43. package/package.json +1 -1
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The real-world side of the updater: spawn npm, read credentials, read prefs.
3
+ * Each function takes its process-level dependencies as injectable parameters
4
+ * so the branches are testable without touching the machine.
5
+ */
6
+ import { execFile as nodeExecFile } from "node:child_process";
7
+ import { existsSync, realpathSync } from "node:fs";
8
+ import { win32 } from "node:path";
9
+ import { getAuthInfo, resolveAuth } from "../auth.js";
10
+ import { loadCliConfig } from "../cli-config.js";
11
+ import { PACKAGE_NAME } from "./decide.js";
12
+ import { isValidVersion } from "./semver.js";
13
+ export const INSTALL_TIMEOUT_MS = 5 * 60 * 1000;
14
+ /**
15
+ * Never `shell: true` (npm's registry-/header-derived arguments must not reach
16
+ * a shell). POSIX runs `npm` directly. On Windows `npm` is a `.cmd` shim that
17
+ * can't be spawned without a shell, so run the npm CLI script with this node
18
+ * binary instead — it ships beside node.exe in every standard Windows install.
19
+ * No such script → null, and the caller treats npm as unavailable (notify only).
20
+ */
21
+ export function npmInvocation(platform = process.platform, execPath = process.execPath, exists = existsSync) {
22
+ if (platform !== "win32")
23
+ return { file: "npm", prefix: [] };
24
+ const cli = win32.join(win32.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js");
25
+ return exists(cli) ? { file: execPath, prefix: [cli] } : null;
26
+ }
27
+ function run(inv, args, timeout, execFile) {
28
+ return new Promise((resolve) => {
29
+ execFile(inv.file, [...inv.prefix, ...args], { timeout, windowsHide: true }, (err, stdout, stderr) => resolve({ err, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") }));
30
+ });
31
+ }
32
+ /** `npm root -g`, or undefined when npm isn't available / fails. */
33
+ export async function npmGlobalRoot(execFile = nodeExecFile, inv = npmInvocation()) {
34
+ if (!inv)
35
+ return undefined;
36
+ const { err, stdout } = await run(inv, ["root", "-g"], 15_000, execFile);
37
+ const root = stdout.trim();
38
+ return err || !root ? undefined : root;
39
+ }
40
+ /** Permission failures can't be fixed by retrying — the caller switches to notify. */
41
+ export function isPermissionError(text) {
42
+ return /\bE(ACCES|PERM)\b/.test(text);
43
+ }
44
+ /**
45
+ * `npm install -g @mnemom/mnemom@<exact version>` — exact, never a tag (no
46
+ * tag-move race). The version is re-validated here, at the sink, even though
47
+ * `pickTarget` already filtered it.
48
+ */
49
+ export async function npmInstallGlobal(version, execFile = nodeExecFile, inv = npmInvocation()) {
50
+ if (!isValidVersion(version)) {
51
+ return {
52
+ ok: false,
53
+ eacces: false,
54
+ message: `refusing to install invalid version '${version}'`,
55
+ };
56
+ }
57
+ if (!inv)
58
+ return { ok: false, eacces: false, message: "npm is not available" };
59
+ const { err, stderr } = await run(inv, ["install", "-g", "--no-fund", "--no-audit", `${PACKAGE_NAME}@${version}`], INSTALL_TIMEOUT_MS, execFile);
60
+ if (!err)
61
+ return { ok: true };
62
+ const detail = `${err.message}\n${stderr}`;
63
+ const lastLine = stderr.trim().split("\n").pop() || err.message;
64
+ return { ok: false, eacces: isPermissionError(detail), message: lastLine.slice(0, 300) };
65
+ }
66
+ /** realpath of the entry script (the global bin is a symlink); falls back to the raw path. */
67
+ export function entryRealpath(argv1, realpath = realpathSync) {
68
+ if (!argv1)
69
+ return "";
70
+ try {
71
+ return realpath(argv1);
72
+ }
73
+ catch {
74
+ return argv1;
75
+ }
76
+ }
77
+ /**
78
+ * Credential for the BACKGROUND worker. Deliberately never refreshes: a
79
+ * detached refresh racing a foreground command could rotate the refresh token
80
+ * out from under it. An expired stored token just means "unauthenticated" for
81
+ * this pass (the cached entitlement still applies).
82
+ */
83
+ export function backgroundCredential(env = process.env, auth = getAuthInfo(), nowSec = Math.floor(Date.now() / 1000)) {
84
+ if (env.MNEMOM_TOKEN)
85
+ return { name: "Authorization", value: `Bearer ${env.MNEMOM_TOKEN}` };
86
+ if (auth && auth.expiresAt > nowSec + 60) {
87
+ return { name: "Authorization", value: `Bearer ${auth.accessToken}` };
88
+ }
89
+ if (env.MNEMOM_API_KEY)
90
+ return { name: "X-Mnemom-Api-Key", value: env.MNEMOM_API_KEY };
91
+ return null;
92
+ }
93
+ /** Credential for the FOREGROUND `mnemom update` — normal resolution, refresh allowed. */
94
+ export async function foregroundCredential(resolve = resolveAuth) {
95
+ const cred = await resolve();
96
+ if (cred.type === "jwt")
97
+ return { name: "Authorization", value: `Bearer ${cred.token}` };
98
+ if (cred.type === "api-key")
99
+ return { name: "X-Mnemom-Api-Key", value: cred.key };
100
+ return null;
101
+ }
102
+ export function loadUpdatePrefs() {
103
+ return loadCliConfig().update ?? {};
104
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The detached background worker (entry: src/update-worker.ts) and the real
3
+ * `RunDeps` wiring shared with `mnemom update`.
4
+ *
5
+ * The parent passes the settings it resolved (`<entry> <channel> <mode> <pid>`): the
6
+ * worker runs with stdio ignored, so re-resolving here would always see a
7
+ * non-TTY and force `off`.
8
+ */
9
+ import { type ResolvedSettings } from "./decide.js";
10
+ import { type ApiCredential } from "./registry.js";
11
+ import { type InstallResult, type RunDeps, type RunResult } from "./run.js";
12
+ export interface WorkerArgs {
13
+ entry: string;
14
+ settings: ResolvedSettings;
15
+ /** The CLI process that spawned us; installs wait until it has exited. */
16
+ parentPid?: number;
17
+ }
18
+ export declare function parseWorkerArgs(args: string[]): WorkerArgs | null;
19
+ export declare const PARENT_POLL_MS = 5000;
20
+ /** Give up waiting on a very long session; the next run picks the update up. */
21
+ export declare const PARENT_WAIT_MAX_MS: number;
22
+ /** `kill(pid, 0)` probes liveness without signalling. EPERM still means alive. */
23
+ export declare function isAlive(pid: number, kill?: (pid: number, sig: 0) => void): boolean;
24
+ /** Resolve true once `pid` is gone, false if it outlives `maxMs`. */
25
+ export declare function waitForExit(pid: number | undefined, deps?: {
26
+ alive(pid: number): boolean;
27
+ sleep(ms: number): Promise<void>;
28
+ now(): number;
29
+ }, maxMs?: number): Promise<boolean>;
30
+ export declare function realRunDeps(entry: string, credential: () => ApiCredential | Promise<ApiCredential>): RunDeps;
31
+ /**
32
+ * The install lock is held ONLY around `npm install` — never while waiting for
33
+ * the parent to exit (a 12h wait would look stale at LOCK_STALE_MS and let a
34
+ * second installer in). Lock busy → skip this pass; the other installer wins.
35
+ */
36
+ export declare function lockedInstall(parentPid: number | undefined, deps: {
37
+ waitForExit(pid: number | undefined): Promise<boolean>;
38
+ acquireLock(): (() => void) | null;
39
+ install(version: string): Promise<InstallResult>;
40
+ }): Pick<RunDeps, "beforeInstall" | "install">;
41
+ export declare function runWorker(args: string[], run: (args: WorkerArgs) => Promise<RunResult>): Promise<"bad-args" | RunResult>;
42
+ export declare function defaultWorkerRun(credential: () => ApiCredential): (args: WorkerArgs) => Promise<RunResult>;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The detached background worker (entry: src/update-worker.ts) and the real
3
+ * `RunDeps` wiring shared with `mnemom update`.
4
+ *
5
+ * The parent passes the settings it resolved (`<entry> <channel> <mode> <pid>`): the
6
+ * worker runs with stdio ignored, so re-resolving here would always see a
7
+ * non-TTY and force `off`.
8
+ */
9
+ import { CLI_VERSION } from "../../version.js";
10
+ import { getApiUrl } from "../config.js";
11
+ import { isUpdateChannel, isUpdateMode } from "./decide.js";
12
+ import { classifyInstall } from "./install-method.js";
13
+ import { fetchChannelEntitlement, fetchDistTags } from "./registry.js";
14
+ import { runUpdate } from "./run.js";
15
+ import { acquireLock, loadUpdateState, saveUpdateState } from "./state.js";
16
+ import { npmGlobalRoot, npmInstallGlobal } from "./system.js";
17
+ export function parseWorkerArgs(args) {
18
+ const [entry, channel, mode, pid] = args;
19
+ if (!entry || !isUpdateChannel(channel) || !isUpdateMode(mode))
20
+ return null;
21
+ const parentPid = Number.parseInt(pid ?? "", 10);
22
+ return { entry, settings: { channel, mode }, parentPid: parentPid > 0 ? parentPid : undefined };
23
+ }
24
+ export const PARENT_POLL_MS = 5000;
25
+ /** Give up waiting on a very long session; the next run picks the update up. */
26
+ export const PARENT_WAIT_MAX_MS = 12 * 60 * 60 * 1000;
27
+ /** `kill(pid, 0)` probes liveness without signalling. EPERM still means alive. */
28
+ export function isAlive(pid, kill = (p, s) => void process.kill(p, s)) {
29
+ try {
30
+ kill(pid, 0);
31
+ return true;
32
+ }
33
+ catch (err) {
34
+ return err.code === "EPERM";
35
+ }
36
+ }
37
+ /** Resolve true once `pid` is gone, false if it outlives `maxMs`. */
38
+ export async function waitForExit(pid, deps = {
39
+ alive: (p) => isAlive(p),
40
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
41
+ now: Date.now,
42
+ }, maxMs = PARENT_WAIT_MAX_MS) {
43
+ if (pid === undefined)
44
+ return true;
45
+ const deadline = deps.now() + maxMs;
46
+ while (deps.alive(pid)) {
47
+ if (deps.now() >= deadline)
48
+ return false;
49
+ await deps.sleep(PARENT_POLL_MS);
50
+ }
51
+ return true;
52
+ }
53
+ export function realRunDeps(entry, credential) {
54
+ return {
55
+ current: CLI_VERSION,
56
+ now: Date.now,
57
+ loadState: () => loadUpdateState(),
58
+ saveState: (patch) => saveUpdateState(patch),
59
+ fetchDistTags: () => fetchDistTags(fetch),
60
+ fetchEntitlement: async () => fetchChannelEntitlement(fetch, getApiUrl(), await credential()),
61
+ installMethod: async () => classifyInstall(entry, await npmGlobalRoot()),
62
+ install: (version) => npmInstallGlobal(version),
63
+ };
64
+ }
65
+ /**
66
+ * The install lock is held ONLY around `npm install` — never while waiting for
67
+ * the parent to exit (a 12h wait would look stale at LOCK_STALE_MS and let a
68
+ * second installer in). Lock busy → skip this pass; the other installer wins.
69
+ */
70
+ export function lockedInstall(parentPid, deps) {
71
+ let release = null;
72
+ return {
73
+ beforeInstall: async () => {
74
+ if (!(await deps.waitForExit(parentPid)))
75
+ return false;
76
+ release = deps.acquireLock();
77
+ return release !== null;
78
+ },
79
+ install: async (version) => {
80
+ try {
81
+ return await deps.install(version);
82
+ }
83
+ finally {
84
+ release?.();
85
+ }
86
+ },
87
+ };
88
+ }
89
+ export async function runWorker(args, run) {
90
+ const parsed = parseWorkerArgs(args);
91
+ if (!parsed)
92
+ return "bad-args";
93
+ return run(parsed);
94
+ }
95
+ export function defaultWorkerRun(credential) {
96
+ return ({ entry, settings, parentPid }) => runUpdate({
97
+ ...realRunDeps(entry, credential),
98
+ ...lockedInstall(parentPid, {
99
+ waitForExit: (pid) => waitForExit(pid),
100
+ acquireLock: () => acquireLock(),
101
+ install: (version) => npmInstallGlobal(version),
102
+ }),
103
+ }, settings);
104
+ }
@@ -28,8 +28,9 @@ export declare function isBelowVersion(current: string, floor: string): boolean;
28
28
  *
29
29
  * @param exit injectable for tests; defaults to process.exit
30
30
  * @param write injectable for tests; defaults to process.stderr.write
31
+ * @param upgrade injectable for tests; defaults to the auto-update floor upgrade
31
32
  */
32
- export declare function enforceMinCliVersion(response: Pick<Response, "headers">, exit?: (code: number) => never, write?: (chunk: string) => void): void;
33
+ export declare function enforceMinCliVersion(response: Pick<Response, "headers">, exit?: (code: number) => never, write?: (chunk: string) => void, upgrade?: (floor: string) => boolean): void;
33
34
  /**
34
35
  * Drop-in wrapper around global fetch for Mnemom API calls: issues the request,
35
36
  * enforces the advertised minimum CLI version on the response, then returns it.
@@ -14,6 +14,7 @@
14
14
  * wrong outcome.
15
15
  */
16
16
  import { CLI_VERSION } from "../version.js";
17
+ import { defaultFloorUpgradeDeps, upgradeToFloor } from "./update/min-version.js";
17
18
  const MIN_CLI_HEADER = "x-mnemom-min-cli";
18
19
  /**
19
20
  * Compare dotted numeric versions. Returns true iff `current` is strictly below
@@ -53,8 +54,9 @@ let exited = false;
53
54
  *
54
55
  * @param exit injectable for tests; defaults to process.exit
55
56
  * @param write injectable for tests; defaults to process.stderr.write
57
+ * @param upgrade injectable for tests; defaults to the auto-update floor upgrade
56
58
  */
57
- export function enforceMinCliVersion(response, exit = process.exit, write = (chunk) => void process.stderr.write(chunk)) {
59
+ export function enforceMinCliVersion(response, exit = process.exit, write = (chunk) => void process.stderr.write(chunk), upgrade = (floor) => upgradeToFloor(floor, defaultFloorUpgradeDeps())) {
58
60
  // Defensive: some callers/tests provide a Response-like object without a real
59
61
  // Headers instance. Treat a missing/incompatible headers bag as "no floor".
60
62
  const headers = response?.headers;
@@ -66,6 +68,12 @@ export function enforceMinCliVersion(response, exit = process.exit, write = (chu
66
68
  if (exited)
67
69
  return;
68
70
  exited = true;
71
+ // Auto-update on + self-updatable install: fix it now, then ask for a re-run
72
+ // (this process is still the old code).
73
+ if (upgrade(floor)) {
74
+ write(`\n✓ Updated the Mnemom CLI (was v${CLI_VERSION}; server minimum v${floor}). Re-run your command.\n\n`);
75
+ exit(1);
76
+ }
69
77
  write(`\n✖ Your Mnemom CLI (v${CLI_VERSION}) is too old for this server (minimum v${floor}).\n` +
70
78
  ` Old CLIs can silently drop newer request fields — e.g. \`--org\` on \`mnemom agent claim\`.\n\n` +
71
79
  ` Upgrade: npm i -g @mnemom/mnemom@latest\n\n`);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Detached auto-update worker — spawned by lib/update/startup.ts with
3
+ * `<entry> <channel> <mode> <parent-pid>`. All logic lives in lib/update/worker.ts; this
4
+ * driver only wires the real process in and swallows every error.
5
+ */
6
+ export {};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Detached auto-update worker — spawned by lib/update/startup.ts with
3
+ * `<entry> <channel> <mode> <parent-pid>`. All logic lives in lib/update/worker.ts; this
4
+ * driver only wires the real process in and swallows every error.
5
+ */
6
+ import { backgroundCredential } from "./lib/update/system.js";
7
+ import { defaultWorkerRun, runWorker } from "./lib/update/worker.js";
8
+ runWorker(process.argv.slice(2), defaultWorkerRun(() => backgroundCredential())).catch(() => { });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.2-next.7",
3
+ "version": "0.17.2-next.8",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {