@mnemom/mnemom 0.17.2 → 0.17.3-next.1
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/commands/agent.js +8 -2
- package/dist/commands/config.d.ts +12 -10
- package/dist/commands/config.js +40 -17
- package/dist/commands/update.d.ts +2 -2
- package/dist/commands/update.js +3 -3
- package/dist/index.js +1 -1
- package/dist/lib/agent-ledger.d.ts +20 -6
- package/dist/lib/agent-ledger.js +52 -11
- package/dist/lib/cli-config.d.ts +15 -2
- package/dist/lib/cli-config.js +11 -8
- package/dist/lib/update/decide.d.ts +21 -2
- package/dist/lib/update/decide.js +19 -5
- package/dist/lib/update/doctor.js +3 -1
- package/dist/lib/update/min-version.js +3 -1
- package/dist/lib/update/run.js +1 -0
- package/dist/lib/update/startup.js +7 -2
- package/package.json +1 -1
package/dist/commands/agent.js
CHANGED
|
@@ -33,7 +33,8 @@ import { basename, join, dirname } from "node:path";
|
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
34
34
|
import { MNEMOM_DIR } from "../lib/config.js";
|
|
35
35
|
import { getAgentByName } from "../lib/api.js";
|
|
36
|
-
import { LEDGER_FILE, appendSessionRecord, toLedgerContract } from "../lib/agent-ledger.js";
|
|
36
|
+
import { LEDGER_FILE, appendSessionRecord, resolveRetentionDays, toLedgerContract, } from "../lib/agent-ledger.js";
|
|
37
|
+
import { loadCliConfig } from "../lib/cli-config.js";
|
|
37
38
|
import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
|
|
38
39
|
import { fmt } from "../lib/format.js";
|
|
39
40
|
import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
@@ -377,7 +378,12 @@ export async function recordSessionLaunch(ctx) {
|
|
|
377
378
|
contract: toLedgerContract(ctx.envelope?.contract ?? null),
|
|
378
379
|
goal_mode: goalAlignmentMode(ctx.envelope, ctx.goalMode),
|
|
379
380
|
cc_session_id: ctx.ccSessionId ?? null,
|
|
380
|
-
}, {
|
|
381
|
+
}, {
|
|
382
|
+
dir: MNEMOM_DIR,
|
|
383
|
+
now: () => new Date(),
|
|
384
|
+
pid: process.pid,
|
|
385
|
+
retentionDays: resolveRetentionDays(loadCliConfig().sessions?.retentionDays, process.env),
|
|
386
|
+
});
|
|
381
387
|
if (!result.ok) {
|
|
382
388
|
say(fmt.warn(`mnemom agent: could not record this session in ~/.mnemom/${LEDGER_FILE} (${result.error}) — launching anyway.`));
|
|
383
389
|
}
|
|
@@ -1,31 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `mnemom config list|get|set|unset` — CLI-wide preferences in
|
|
3
|
-
* ~/.mnemom/config.json
|
|
3
|
+
* ~/.mnemom/config.json: auto-update and local session retention. (Launcher settings for
|
|
4
4
|
* `mnemom agent` stay in `mnemom agent config` / launcher.toml.)
|
|
5
5
|
*
|
|
6
6
|
* Keys are data (CONFIG_KEYS) so list/get/set/unset share one source of truth.
|
|
7
7
|
*/
|
|
8
|
-
import { type
|
|
9
|
-
import {
|
|
8
|
+
import { type InstallHint } from "../lib/update/decide.js";
|
|
9
|
+
import { setPref, type CliConfig } from "../lib/cli-config.js";
|
|
10
10
|
export interface CliConfigKey {
|
|
11
11
|
key: string;
|
|
12
12
|
describe: string;
|
|
13
13
|
env: string;
|
|
14
14
|
/** Stored value (undefined when unset). */
|
|
15
|
-
stored(
|
|
16
|
-
/** Value in effect after env + default. */
|
|
17
|
-
effective(
|
|
15
|
+
stored(config: CliConfig): string | undefined;
|
|
16
|
+
/** Value in effect after env + default (`hint` picks the channel default). */
|
|
17
|
+
effective(config: CliConfig, env: Record<string, string | undefined>, hint: InstallHint): string;
|
|
18
18
|
/** Validate + persist; throws with a user-facing message on a bad value. */
|
|
19
|
-
set(raw: string, persist: typeof
|
|
20
|
-
unset(persist: typeof
|
|
19
|
+
set(raw: string, persist: typeof setPref): void;
|
|
20
|
+
unset(persist: typeof setPref): void;
|
|
21
21
|
}
|
|
22
22
|
export declare const CONFIG_KEYS: readonly CliConfigKey[];
|
|
23
23
|
export declare function findKey(key: string): CliConfigKey;
|
|
24
24
|
export interface ConfigDeps {
|
|
25
|
-
|
|
25
|
+
config(): CliConfig;
|
|
26
26
|
env: Record<string, string | undefined>;
|
|
27
|
-
persist: typeof
|
|
27
|
+
persist: typeof setPref;
|
|
28
28
|
log(line: string): void;
|
|
29
|
+
/** Running version + last-followed channel (picks the default channel). */
|
|
30
|
+
hint: InstallHint;
|
|
29
31
|
}
|
|
30
32
|
/** One line per key: `key = effective (source) — description`. */
|
|
31
33
|
export declare function configListCommand(opts?: {
|
package/dist/commands/config.js
CHANGED
|
@@ -1,40 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `mnemom config list|get|set|unset` — CLI-wide preferences in
|
|
3
|
-
* ~/.mnemom/config.json
|
|
3
|
+
* ~/.mnemom/config.json: auto-update and local session retention. (Launcher settings for
|
|
4
4
|
* `mnemom agent` stay in `mnemom agent config` / launcher.toml.)
|
|
5
5
|
*
|
|
6
6
|
* Keys are data (CONFIG_KEYS) so list/get/set/unset share one source of truth.
|
|
7
7
|
*/
|
|
8
8
|
import { isUpdateChannel, isUpdateMode, resolveChannel, resolveMode, UPDATE_CHANNELS, UPDATE_MODES, } from "../lib/update/decide.js";
|
|
9
|
-
import {
|
|
9
|
+
import { DEFAULT_RETENTION_DAYS, parseRetentionDays, resolveRetentionDays, RETENTION_ENV, } from "../lib/agent-ledger.js";
|
|
10
|
+
import { loadCliConfig, setPref } from "../lib/cli-config.js";
|
|
11
|
+
import { loadUpdateState } from "../lib/update/state.js";
|
|
12
|
+
import { CLI_VERSION } from "../version.js";
|
|
10
13
|
export const CONFIG_KEYS = [
|
|
11
14
|
{
|
|
12
15
|
key: "update.channel",
|
|
13
|
-
describe: `Release channel for auto-update: ${UPDATE_CHANNELS.join(" | ")} (next requires an invite)`,
|
|
16
|
+
describe: `Release channel for auto-update: ${UPDATE_CHANNELS.join(" | ")} (default: next on a next build, else latest; next requires an invite)`,
|
|
14
17
|
env: "MNEMOM_UPDATE_CHANNEL",
|
|
15
|
-
stored: (
|
|
16
|
-
effective: (
|
|
18
|
+
stored: (c) => c.update?.channel,
|
|
19
|
+
effective: (c, env, hint) => resolveChannel(c.update ?? {}, env, undefined, hint),
|
|
17
20
|
set: (raw, persist) => {
|
|
18
21
|
const v = raw.trim();
|
|
19
22
|
if (!isUpdateChannel(v))
|
|
20
23
|
throw new Error(`update.channel must be one of ${UPDATE_CHANNELS.join(", ")} (got '${raw}').`);
|
|
21
|
-
persist("channel", v);
|
|
24
|
+
persist("update", "channel", v);
|
|
22
25
|
},
|
|
23
|
-
unset: (persist) => persist("channel", undefined),
|
|
26
|
+
unset: (persist) => persist("update", "channel", undefined),
|
|
24
27
|
},
|
|
25
28
|
{
|
|
26
29
|
key: "update.mode",
|
|
27
30
|
describe: `Auto-update behaviour: ${UPDATE_MODES.join(" | ")}`,
|
|
28
31
|
env: "MNEMOM_UPDATE_MODE",
|
|
29
|
-
stored: (
|
|
30
|
-
effective: (
|
|
32
|
+
stored: (c) => c.update?.mode,
|
|
33
|
+
effective: (c, env) => resolveMode(c.update ?? {}, env),
|
|
31
34
|
set: (raw, persist) => {
|
|
32
35
|
const v = raw.trim();
|
|
33
36
|
if (!isUpdateMode(v))
|
|
34
37
|
throw new Error(`update.mode must be one of ${UPDATE_MODES.join(", ")} (got '${raw}').`);
|
|
35
|
-
persist("mode", v);
|
|
38
|
+
persist("update", "mode", v);
|
|
36
39
|
},
|
|
37
|
-
unset: (persist) => persist("mode", undefined),
|
|
40
|
+
unset: (persist) => persist("update", "mode", undefined),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "sessions.retention_days",
|
|
44
|
+
describe: `Days \`mnemom agent sessions\` keeps a launch in ~/.mnemom/sessions.jsonl (default ${DEFAULT_RETENTION_DAYS}; older ones are removed at the next launch)`,
|
|
45
|
+
env: RETENTION_ENV,
|
|
46
|
+
stored: (c) => c.sessions?.retentionDays?.toString(),
|
|
47
|
+
effective: (c, env) => String(resolveRetentionDays(c.sessions?.retentionDays, env)),
|
|
48
|
+
set: (raw, persist) => {
|
|
49
|
+
const days = parseRetentionDays(raw);
|
|
50
|
+
if (days === null)
|
|
51
|
+
throw new Error(`sessions.retention_days must be a whole number of days, at least 1 (got '${raw}').`);
|
|
52
|
+
persist("sessions", "retentionDays", days);
|
|
53
|
+
},
|
|
54
|
+
unset: (persist) => persist("sessions", "retentionDays", undefined),
|
|
38
55
|
},
|
|
39
56
|
];
|
|
40
57
|
export function findKey(key) {
|
|
@@ -45,19 +62,25 @@ export function findKey(key) {
|
|
|
45
62
|
}
|
|
46
63
|
function defaultDeps() {
|
|
47
64
|
return {
|
|
48
|
-
|
|
65
|
+
config: loadCliConfig,
|
|
49
66
|
env: process.env,
|
|
50
|
-
persist:
|
|
67
|
+
persist: setPref,
|
|
51
68
|
log: (line) => console.log(line),
|
|
69
|
+
hint: { current: CLI_VERSION, followed: loadUpdateState().followChannel },
|
|
52
70
|
};
|
|
53
71
|
}
|
|
54
72
|
/** One line per key: `key = effective (source) — description`. */
|
|
55
73
|
export function configListCommand(opts = {}, deps = defaultDeps()) {
|
|
56
|
-
const
|
|
74
|
+
const config = deps.config();
|
|
57
75
|
const rows = CONFIG_KEYS.map((k) => {
|
|
58
|
-
const stored = k.stored(
|
|
76
|
+
const stored = k.stored(config);
|
|
59
77
|
const source = deps.env[k.env] ? `env ${k.env}` : stored !== undefined ? "config" : "default";
|
|
60
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
key: k.key,
|
|
80
|
+
value: k.effective(config, deps.env, deps.hint),
|
|
81
|
+
source,
|
|
82
|
+
describe: k.describe,
|
|
83
|
+
};
|
|
61
84
|
});
|
|
62
85
|
if (opts.json) {
|
|
63
86
|
deps.log(JSON.stringify(rows, null, 2));
|
|
@@ -67,7 +90,7 @@ export function configListCommand(opts = {}, deps = defaultDeps()) {
|
|
|
67
90
|
deps.log(`${r.key} = ${r.value} (${r.source}) — ${r.describe}`);
|
|
68
91
|
}
|
|
69
92
|
export function configGetCommand(key, deps = defaultDeps()) {
|
|
70
|
-
deps.log(findKey(key).effective(deps.
|
|
93
|
+
deps.log(findKey(key).effective(deps.config(), deps.env, deps.hint));
|
|
71
94
|
}
|
|
72
95
|
export function configSetCommand(key, value, deps = defaultDeps()) {
|
|
73
96
|
const spec = findKey(key);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* command is consent). `--check` never installs; `--force` permits a downgrade
|
|
7
7
|
* to the channel's version (e.g. leaving next for latest).
|
|
8
8
|
*/
|
|
9
|
-
import { type ResolvedSettings } from "../lib/update/decide.js";
|
|
9
|
+
import { type InstallHint, type ResolvedSettings } from "../lib/update/decide.js";
|
|
10
10
|
import { type RunDeps, type RunResult } from "../lib/update/run.js";
|
|
11
11
|
export declare const NEXT_NOT_ENTITLED_HINT: string;
|
|
12
12
|
export interface UpdateOptions {
|
|
@@ -16,7 +16,7 @@ export interface UpdateOptions {
|
|
|
16
16
|
json?: boolean;
|
|
17
17
|
}
|
|
18
18
|
/** Validate flags → the settings for this foreground pass, or an error message. */
|
|
19
|
-
export declare function foregroundSettings(opts: UpdateOptions, prefs?: import("../lib/update/decide.js").UpdatePrefs, env?: Record<string, string | undefined
|
|
19
|
+
export declare function foregroundSettings(opts: UpdateOptions, prefs?: import("../lib/update/decide.js").UpdatePrefs, env?: Record<string, string | undefined>, hint?: InstallHint): ResolvedSettings | string;
|
|
20
20
|
/** Human-readable report of one pass. Pure, for testing. */
|
|
21
21
|
export declare function formatUpdateResult(result: RunResult, current: string): string[];
|
|
22
22
|
/** Exit status: non-zero only when something actually failed. */
|
package/dist/commands/update.js
CHANGED
|
@@ -10,17 +10,17 @@ import chalk from "chalk";
|
|
|
10
10
|
import { CLI_VERSION } from "../version.js";
|
|
11
11
|
import { canSelfUpdate, isUpdateChannel, manualUpgradeCommand, resolveChannel, UPDATE_CHANNELS, } from "../lib/update/decide.js";
|
|
12
12
|
import { runUpdate } from "../lib/update/run.js";
|
|
13
|
-
import { acquireLock } from "../lib/update/state.js";
|
|
13
|
+
import { acquireLock, loadUpdateState } from "../lib/update/state.js";
|
|
14
14
|
import { entryRealpath, foregroundCredential, loadUpdatePrefs } from "../lib/update/system.js";
|
|
15
15
|
import { realRunDeps } from "../lib/update/worker.js";
|
|
16
16
|
export const NEXT_NOT_ENTITLED_HINT = "The next channel is available to invited accounts. Ask your Mnemom contact for an invite " +
|
|
17
17
|
"(and make sure you're logged in: mnemom login). Using latest for now.";
|
|
18
18
|
/** Validate flags → the settings for this foreground pass, or an error message. */
|
|
19
|
-
export function foregroundSettings(opts, prefs = loadUpdatePrefs(), env = process.env) {
|
|
19
|
+
export function foregroundSettings(opts, prefs = loadUpdatePrefs(), env = process.env, hint = { current: CLI_VERSION, followed: loadUpdateState().followChannel }) {
|
|
20
20
|
if (opts.channel !== undefined && !isUpdateChannel(opts.channel)) {
|
|
21
21
|
return `--channel must be one of ${UPDATE_CHANNELS.join(", ")} (got '${opts.channel}').`;
|
|
22
22
|
}
|
|
23
|
-
const channel = resolveChannel(prefs, env, opts.channel);
|
|
23
|
+
const channel = resolveChannel(prefs, env, opts.channel, hint);
|
|
24
24
|
return { channel, mode: opts.check ? "notify" : "auto" };
|
|
25
25
|
}
|
|
26
26
|
/** Human-readable report of one pass. Pure, for testing. */
|
package/dist/index.js
CHANGED
|
@@ -1898,7 +1898,7 @@ program
|
|
|
1898
1898
|
.action((options) => runAction(() => updateCommand(options)));
|
|
1899
1899
|
const configCmd = program
|
|
1900
1900
|
.command("config")
|
|
1901
|
-
.description("CLI-wide preferences in ~/.mnemom/config.json (auto-update
|
|
1901
|
+
.description("CLI-wide preferences in ~/.mnemom/config.json (auto-update, session retention)");
|
|
1902
1902
|
configCmd
|
|
1903
1903
|
.command("list")
|
|
1904
1904
|
.description("Show every setting, its effective value, and where it came from")
|
|
@@ -9,12 +9,24 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Writes are best-effort: `appendSessionRecord` never throws — a failure comes
|
|
11
11
|
* back as `{ ok: false, error }` and the caller decides how loudly to warn.
|
|
12
|
+
*
|
|
13
|
+
* Retention mirrors Claude Code's `cleanupPeriodDays`: launches older than
|
|
14
|
+
* `sessions.retention_days` (default 30) are dropped when the next one is
|
|
15
|
+
* recorded. No count cap — a long retention keeps everything in the window.
|
|
12
16
|
*/
|
|
13
17
|
import type { LaunchShape, SealedContract } from "./agent.js";
|
|
14
18
|
/** The ledger's file name under MNEMOM_DIR. */
|
|
15
19
|
export declare const LEDGER_FILE = "sessions.jsonl";
|
|
16
|
-
/**
|
|
17
|
-
export declare const
|
|
20
|
+
/** Days a launch stays in the ledger when `sessions.retention_days` is unset. */
|
|
21
|
+
export declare const DEFAULT_RETENTION_DAYS = 30;
|
|
22
|
+
export declare const RETENTION_ENV = "MNEMOM_SESSIONS_RETENTION_DAYS";
|
|
23
|
+
/** A retention setting must be a whole number of days, at least 1. */
|
|
24
|
+
export declare function parseRetentionDays(raw: string): number | null;
|
|
25
|
+
/**
|
|
26
|
+
* Precedence: MNEMOM_SESSIONS_RETENTION_DAYS > config.json > 30. An invalid value from either
|
|
27
|
+
* source is ignored (config.json can be hand-edited past `mnemom config set`'s validation).
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveRetentionDays(stored: unknown, env: Record<string, string | undefined>): number;
|
|
18
30
|
/** The sealed-contract fields the ledger keeps (no goal_id — the statement is the identity). */
|
|
19
31
|
export interface SessionRecordContract {
|
|
20
32
|
statement: string;
|
|
@@ -76,6 +88,8 @@ export interface LedgerDeps {
|
|
|
76
88
|
now: () => Date;
|
|
77
89
|
/** The writing process's pid (names the rewrite temp file). */
|
|
78
90
|
pid: number;
|
|
91
|
+
/** Drop launches older than this many days (default DEFAULT_RETENTION_DAYS). */
|
|
92
|
+
retentionDays?: number;
|
|
79
93
|
}
|
|
80
94
|
export type AppendResult = {
|
|
81
95
|
ok: true;
|
|
@@ -93,11 +107,11 @@ export declare function toLedgerContract(sealed: SealedContract | null): Session
|
|
|
93
107
|
/** The ledger's path under `dir`. */
|
|
94
108
|
export declare function ledgerPath(dir: string): string;
|
|
95
109
|
/**
|
|
96
|
-
* Append one launch to the ledger,
|
|
110
|
+
* Append one launch to the ledger, dropping expired ones:
|
|
97
111
|
* - `dir` is created 0o700 when missing; the file is created 0o600;
|
|
98
|
-
* -
|
|
99
|
-
* otherwise the
|
|
100
|
-
*
|
|
112
|
+
* - when no launch is older than the retention window the new line is
|
|
113
|
+
* appended; otherwise the surviving lines plus this one are rewritten via a
|
|
114
|
+
* temp file + rename, so a reader never sees a torn file.
|
|
101
115
|
* Never throws — a failure is returned as `{ ok: false, error }`.
|
|
102
116
|
*/
|
|
103
117
|
export declare function appendSessionRecord(input: SessionRecordInput, deps: LedgerDeps): AppendResult;
|
package/dist/lib/agent-ledger.js
CHANGED
|
@@ -9,13 +9,36 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Writes are best-effort: `appendSessionRecord` never throws — a failure comes
|
|
11
11
|
* back as `{ ok: false, error }` and the caller decides how loudly to warn.
|
|
12
|
+
*
|
|
13
|
+
* Retention mirrors Claude Code's `cleanupPeriodDays`: launches older than
|
|
14
|
+
* `sessions.retention_days` (default 30) are dropped when the next one is
|
|
15
|
+
* recorded. No count cap — a long retention keeps everything in the window.
|
|
12
16
|
*/
|
|
13
17
|
import { appendFileSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
14
18
|
import { join } from "node:path";
|
|
15
19
|
/** The ledger's file name under MNEMOM_DIR. */
|
|
16
20
|
export const LEDGER_FILE = "sessions.jsonl";
|
|
17
|
-
/**
|
|
18
|
-
export const
|
|
21
|
+
/** Days a launch stays in the ledger when `sessions.retention_days` is unset. */
|
|
22
|
+
export const DEFAULT_RETENTION_DAYS = 30;
|
|
23
|
+
export const RETENTION_ENV = "MNEMOM_SESSIONS_RETENTION_DAYS";
|
|
24
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
/** A retention setting must be a whole number of days, at least 1. */
|
|
26
|
+
export function parseRetentionDays(raw) {
|
|
27
|
+
const v = raw.trim();
|
|
28
|
+
if (!/^\d+$/.test(v))
|
|
29
|
+
return null;
|
|
30
|
+
const n = Number(v);
|
|
31
|
+
return Number.isSafeInteger(n) && n >= 1 ? n : null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Precedence: MNEMOM_SESSIONS_RETENTION_DAYS > config.json > 30. An invalid value from either
|
|
35
|
+
* source is ignored (config.json can be hand-edited past `mnemom config set`'s validation).
|
|
36
|
+
*/
|
|
37
|
+
export function resolveRetentionDays(stored, env) {
|
|
38
|
+
const fromEnv = env[RETENTION_ENV] === undefined ? null : parseRetentionDays(env[RETENTION_ENV]);
|
|
39
|
+
const fromConfig = typeof stored === "number" && Number.isSafeInteger(stored) && stored >= 1 ? stored : null;
|
|
40
|
+
return fromEnv ?? fromConfig ?? DEFAULT_RETENTION_DAYS;
|
|
41
|
+
}
|
|
19
42
|
/**
|
|
20
43
|
* Project a sealed contract onto the ledger's contract shape (drops `goal_id`,
|
|
21
44
|
* keeps only defined fields). `null` in, `null` out.
|
|
@@ -58,25 +81,43 @@ function readLedgerText(path) {
|
|
|
58
81
|
}
|
|
59
82
|
}
|
|
60
83
|
/**
|
|
61
|
-
*
|
|
84
|
+
* True when `line` is a launch that started before `cutoffMs`. A line whose
|
|
85
|
+
* age can't be read is kept — never delete what we can't date.
|
|
86
|
+
*/
|
|
87
|
+
function isExpired(line, cutoffMs) {
|
|
88
|
+
try {
|
|
89
|
+
const startedAt = JSON.parse(line).started_at;
|
|
90
|
+
if (typeof startedAt !== "string")
|
|
91
|
+
return false;
|
|
92
|
+
const t = Date.parse(startedAt);
|
|
93
|
+
return !Number.isNaN(t) && t < cutoffMs;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Append one launch to the ledger, dropping expired ones:
|
|
62
101
|
* - `dir` is created 0o700 when missing; the file is created 0o600;
|
|
63
|
-
* -
|
|
64
|
-
* otherwise the
|
|
65
|
-
*
|
|
102
|
+
* - when no launch is older than the retention window the new line is
|
|
103
|
+
* appended; otherwise the surviving lines plus this one are rewritten via a
|
|
104
|
+
* temp file + rename, so a reader never sees a torn file.
|
|
66
105
|
* Never throws — a failure is returned as `{ ok: false, error }`.
|
|
67
106
|
*/
|
|
68
107
|
export function appendSessionRecord(input, deps) {
|
|
69
108
|
const path = ledgerPath(deps.dir);
|
|
70
109
|
try {
|
|
71
|
-
const
|
|
110
|
+
const now = deps.now();
|
|
111
|
+
const record = { v: 1, started_at: now.toISOString(), ...input };
|
|
72
112
|
mkdirSync(deps.dir, { recursive: true, mode: 0o700 });
|
|
73
113
|
const line = `${JSON.stringify(record)}\n`;
|
|
74
114
|
const text = readLedgerText(path);
|
|
75
115
|
const existing = text === null ? [] : nonEmptyLines(text);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
116
|
+
const cutoff = now.getTime() - (deps.retentionDays ?? DEFAULT_RETENTION_DAYS) * DAY_MS;
|
|
117
|
+
const kept = existing.filter((l) => !isExpired(l, cutoff));
|
|
118
|
+
if (kept.length < existing.length) {
|
|
119
|
+
const tmp = join(deps.dir, `.${LEDGER_FILE}.tmp-${deps.pid}-${now.getTime()}`);
|
|
120
|
+
writeFileSync(tmp, `${[...kept, JSON.stringify(record)].join("\n")}\n`, { mode: 0o600 });
|
|
80
121
|
renameSync(tmp, path);
|
|
81
122
|
}
|
|
82
123
|
else {
|
package/dist/lib/cli-config.d.ts
CHANGED
|
@@ -23,10 +23,20 @@ export interface ActiveOrg {
|
|
|
23
23
|
slug: string;
|
|
24
24
|
name: string;
|
|
25
25
|
}
|
|
26
|
+
/** Local session-ledger preferences (`mnemom config set sessions.retention_days`). */
|
|
27
|
+
export interface SessionPrefs {
|
|
28
|
+
retentionDays?: number;
|
|
29
|
+
}
|
|
26
30
|
export interface CliConfig {
|
|
27
31
|
activeOrg?: ActiveOrg;
|
|
28
32
|
/** Auto-update preferences (`mnemom config set update.channel|update.mode`). */
|
|
29
33
|
update?: UpdatePrefs;
|
|
34
|
+
sessions?: SessionPrefs;
|
|
35
|
+
}
|
|
36
|
+
/** The config.json sections `mnemom config` edits. */
|
|
37
|
+
export interface PrefSections {
|
|
38
|
+
update: UpdatePrefs;
|
|
39
|
+
sessions: SessionPrefs;
|
|
30
40
|
}
|
|
31
41
|
/** Load the config; a missing or corrupt file degrades to `{}`. */
|
|
32
42
|
export declare function loadCliConfig(): CliConfig;
|
|
@@ -35,7 +45,10 @@ export declare function setActiveOrg(org: ActiveOrg | null): void;
|
|
|
35
45
|
/** The active org, or undefined when none is set. */
|
|
36
46
|
export declare function getActiveOrg(): ActiveOrg | undefined;
|
|
37
47
|
/**
|
|
38
|
-
* Set (or with `undefined`, clear) one
|
|
39
|
-
*
|
|
48
|
+
* Set (or with `undefined`, clear) one preference in a config.json section;
|
|
49
|
+
* an emptied section is removed. Validation is the caller's job
|
|
50
|
+
* (commands/config.ts) — this only persists.
|
|
40
51
|
*/
|
|
52
|
+
export declare function setPref<S extends keyof PrefSections, K extends keyof PrefSections[S] & string>(section: S, key: K, value: PrefSections[S][K] | undefined): void;
|
|
53
|
+
/** One auto-update preference — `setPref("update", …)`. */
|
|
41
54
|
export declare function setUpdatePref<K extends keyof UpdatePrefs>(key: K, value: UpdatePrefs[K] | undefined): void;
|
package/dist/lib/cli-config.js
CHANGED
|
@@ -69,15 +69,18 @@ export function getActiveOrg() {
|
|
|
69
69
|
return loadCliConfig().activeOrg;
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
|
-
* Set (or with `undefined`, clear) one
|
|
73
|
-
*
|
|
72
|
+
* Set (or with `undefined`, clear) one preference in a config.json section;
|
|
73
|
+
* an emptied section is removed. Validation is the caller's job
|
|
74
|
+
* (commands/config.ts) — this only persists.
|
|
74
75
|
*/
|
|
75
|
-
export function
|
|
76
|
+
export function setPref(section, key, value) {
|
|
76
77
|
const config = loadCliConfig();
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
else
|
|
81
|
-
config.update = update;
|
|
78
|
+
const next = Object.fromEntries(Object.entries({ ...config[section], [key]: value }).filter(([, v]) => v !== undefined));
|
|
79
|
+
// An emptied section becomes undefined, which JSON.stringify omits.
|
|
80
|
+
config[section] = Object.keys(next).length > 0 ? next : undefined;
|
|
82
81
|
saveCliConfig(config);
|
|
83
82
|
}
|
|
83
|
+
/** One auto-update preference — `setPref("update", …)`. */
|
|
84
|
+
export function setUpdatePref(key, value) {
|
|
85
|
+
setPref("update", key, value);
|
|
86
|
+
}
|
|
@@ -62,6 +62,8 @@ export interface UpdateState {
|
|
|
62
62
|
entitlement?: EntitlementCache;
|
|
63
63
|
/** Install method the worker last detected (startup can't afford `npm root -g`). */
|
|
64
64
|
method?: InstallMethod;
|
|
65
|
+
/** Channel the last successful update installed from; feeds defaultChannel. */
|
|
66
|
+
followChannel?: UpdateChannel;
|
|
65
67
|
}
|
|
66
68
|
export interface UpdatePrefs {
|
|
67
69
|
channel?: UpdateChannel;
|
|
@@ -75,8 +77,23 @@ export interface ResolvedSettings {
|
|
|
75
77
|
}
|
|
76
78
|
export declare function isUpdateChannel(v: unknown): v is UpdateChannel;
|
|
77
79
|
export declare function isUpdateMode(v: unknown): v is UpdateMode;
|
|
78
|
-
/**
|
|
79
|
-
export
|
|
80
|
+
/** What the unconfigured default is derived from. */
|
|
81
|
+
export interface InstallHint {
|
|
82
|
+
/** The running CLI version. */
|
|
83
|
+
current?: string;
|
|
84
|
+
/** The channel the last successful update installed from (update state). */
|
|
85
|
+
followed?: UpdateChannel;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The channel when nothing is configured: `next` if this build was installed
|
|
89
|
+
* from `@next` (a prerelease, e.g. a Mnemom Code invite) or the updater last
|
|
90
|
+
* installed from `next` — the latter keeps a `next` follower on `next` after
|
|
91
|
+
* `latest` overtakes it and they land on a stable build. Otherwise `latest`.
|
|
92
|
+
* The entitlement check still applies to a defaulted `next`.
|
|
93
|
+
*/
|
|
94
|
+
export declare function defaultChannel(hint?: InstallHint): UpdateChannel;
|
|
95
|
+
/** Channel precedence: flag > MNEMOM_UPDATE_CHANNEL > config.json > defaultChannel(hint). */
|
|
96
|
+
export declare function resolveChannel(prefs: UpdatePrefs, env: Record<string, string | undefined>, flag?: UpdateChannel, hint?: InstallHint): UpdateChannel;
|
|
80
97
|
/** Mode precedence: flag > MNEMOM_UPDATE_MODE > config.json > auto. */
|
|
81
98
|
export declare function resolveMode(prefs: UpdatePrefs, env: Record<string, string | undefined>, flag?: UpdateMode): UpdateMode;
|
|
82
99
|
/**
|
|
@@ -91,6 +108,8 @@ export declare function resolveSettings(input: {
|
|
|
91
108
|
env: Record<string, string | undefined>;
|
|
92
109
|
isTTY: boolean;
|
|
93
110
|
flags?: UpdatePrefs;
|
|
111
|
+
/** Picks the default channel when none is configured. */
|
|
112
|
+
hint?: InstallHint;
|
|
94
113
|
}): ResolvedSettings;
|
|
95
114
|
/** The channel actually honoured: `next` requires a positive entitlement. */
|
|
96
115
|
export declare function effectiveChannel(requested: UpdateChannel, entitledToNext: boolean): UpdateChannel;
|
|
@@ -45,10 +45,24 @@ export function isUpdateMode(v) {
|
|
|
45
45
|
function truthyEnv(v) {
|
|
46
46
|
return v !== undefined && v !== "" && v !== "0" && v.toLowerCase() !== "false";
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The channel when nothing is configured: `next` if this build was installed
|
|
50
|
+
* from `@next` (a prerelease, e.g. a Mnemom Code invite) or the updater last
|
|
51
|
+
* installed from `next` — the latter keeps a `next` follower on `next` after
|
|
52
|
+
* `latest` overtakes it and they land on a stable build. Otherwise `latest`.
|
|
53
|
+
* The entitlement check still applies to a defaulted `next`.
|
|
54
|
+
*/
|
|
55
|
+
export function defaultChannel(hint = {}) {
|
|
56
|
+
const onPrerelease = hint.current !== undefined && isPrerelease(hint.current);
|
|
57
|
+
return onPrerelease || hint.followed === "next" ? "next" : "latest";
|
|
58
|
+
}
|
|
59
|
+
/** Channel precedence: flag > MNEMOM_UPDATE_CHANNEL > config.json > defaultChannel(hint). */
|
|
60
|
+
export function resolveChannel(prefs, env, flag, hint) {
|
|
50
61
|
const fromEnv = env.MNEMOM_UPDATE_CHANNEL;
|
|
51
|
-
return flag ??
|
|
62
|
+
return (flag ??
|
|
63
|
+
(isUpdateChannel(fromEnv) ? fromEnv : undefined) ??
|
|
64
|
+
prefs.channel ??
|
|
65
|
+
defaultChannel(hint));
|
|
52
66
|
}
|
|
53
67
|
/** Mode precedence: flag > MNEMOM_UPDATE_MODE > config.json > auto. */
|
|
54
68
|
export function resolveMode(prefs, env, flag) {
|
|
@@ -63,8 +77,8 @@ export function resolveMode(prefs, env, flag) {
|
|
|
63
77
|
* every command.
|
|
64
78
|
*/
|
|
65
79
|
export function resolveSettings(input) {
|
|
66
|
-
const { prefs, env, isTTY, flags } = input;
|
|
67
|
-
const channel = resolveChannel(prefs, env, flags?.channel);
|
|
80
|
+
const { prefs, env, isTTY, flags, hint } = input;
|
|
81
|
+
const channel = resolveChannel(prefs, env, flags?.channel, hint);
|
|
68
82
|
const mode = resolveMode(prefs, env, flags?.mode);
|
|
69
83
|
if (truthyEnv(env.MNEMOM_DISABLE_AUTOUPDATE)) {
|
|
70
84
|
return { channel, mode: "off", offReason: "MNEMOM_DISABLE_AUTOUPDATE is set" };
|
|
@@ -53,11 +53,13 @@ export function updateDoctorRow(input) {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
export async function probeUpdateStatus() {
|
|
56
|
+
const state = loadUpdateState();
|
|
56
57
|
const settings = resolveSettings({
|
|
57
58
|
prefs: loadUpdatePrefs(),
|
|
58
59
|
env: process.env,
|
|
59
60
|
isTTY: process.stdout.isTTY === true,
|
|
61
|
+
hint: { current: CLI_VERSION, followed: state.followChannel },
|
|
60
62
|
});
|
|
61
63
|
const method = classifyInstall(entryRealpath(process.argv[1]), await npmGlobalRoot());
|
|
62
|
-
return updateDoctorRow({ current: CLI_VERSION, settings, method, state
|
|
64
|
+
return updateDoctorRow({ current: CLI_VERSION, settings, method, state });
|
|
63
65
|
}
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { canSelfUpdate, PACKAGE_NAME, resolveSettings, } from "./decide.js";
|
|
9
|
+
import { CLI_VERSION } from "../../version.js";
|
|
9
10
|
import { classifyInstall } from "./install-method.js";
|
|
10
11
|
import { isValidVersion, isPrerelease } from "./semver.js";
|
|
11
|
-
import { acquireLock } from "./state.js";
|
|
12
|
+
import { acquireLock, loadUpdateState } from "./state.js";
|
|
12
13
|
import { entryRealpath, loadUpdatePrefs, npmInvocation } from "./system.js";
|
|
13
14
|
/**
|
|
14
15
|
* Returns true when the CLI was upgraded. Only in `auto` mode — `notify` means
|
|
@@ -54,6 +55,7 @@ export function defaultFloorUpgradeDeps() {
|
|
|
54
55
|
prefs: loadUpdatePrefs(),
|
|
55
56
|
env: process.env,
|
|
56
57
|
isTTY: process.stdout.isTTY === true,
|
|
58
|
+
hint: { current: CLI_VERSION, followed: loadUpdateState().followChannel },
|
|
57
59
|
}),
|
|
58
60
|
method: () => {
|
|
59
61
|
const root = spawnNpmSync(inv, ["root", "-g"], { timeout: 15_000 });
|
package/dist/lib/update/run.js
CHANGED
|
@@ -86,6 +86,7 @@ export async function runUpdate(deps, settings, opts = {}) {
|
|
|
86
86
|
available: undefined,
|
|
87
87
|
installBlocked: undefined,
|
|
88
88
|
pending: { from: deps.current, to: action.version, channel },
|
|
89
|
+
followChannel: channel,
|
|
89
90
|
});
|
|
90
91
|
return { ...result, installed: action.version };
|
|
91
92
|
}
|
|
@@ -16,13 +16,18 @@ import { entryRealpath, loadUpdatePrefs } from "./system.js";
|
|
|
16
16
|
const SKIP_COMMANDS = new Set(["update", "config"]);
|
|
17
17
|
export function maybeRunUpdateHook(deps) {
|
|
18
18
|
try {
|
|
19
|
-
const
|
|
19
|
+
const state = deps.loadState();
|
|
20
|
+
const settings = resolveSettings({
|
|
21
|
+
prefs: deps.prefs(),
|
|
22
|
+
env: deps.env,
|
|
23
|
+
isTTY: deps.stdoutTTY,
|
|
24
|
+
hint: { current: deps.current, followed: state.followChannel },
|
|
25
|
+
});
|
|
20
26
|
if (settings.mode === "off")
|
|
21
27
|
return;
|
|
22
28
|
if (classifyInstall(deps.entryPath, undefined) === "dev")
|
|
23
29
|
return;
|
|
24
30
|
const now = deps.now();
|
|
25
|
-
const state = deps.loadState();
|
|
26
31
|
const notice = startupNotice({
|
|
27
32
|
current: deps.current,
|
|
28
33
|
state,
|