mthds 0.2.1 → 0.3.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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/binaries.d.ts +14 -1
  3. package/dist/agent/binaries.js +21 -6
  4. package/dist/agent/binaries.js.map +1 -1
  5. package/dist/agent/commands/bootstrap.d.ts +15 -0
  6. package/dist/agent/commands/bootstrap.js +152 -0
  7. package/dist/agent/commands/bootstrap.js.map +1 -0
  8. package/dist/agent/commands/doctor.js +25 -17
  9. package/dist/agent/commands/doctor.js.map +1 -1
  10. package/dist/agent/commands/install.js +2 -2
  11. package/dist/agent/commands/install.js.map +1 -1
  12. package/dist/agent/commands/update-check.d.ts +16 -0
  13. package/dist/agent/commands/update-check.js +169 -0
  14. package/dist/agent/commands/update-check.js.map +1 -0
  15. package/dist/agent/commands/upgrade.d.ts +17 -0
  16. package/dist/agent/commands/upgrade.js +155 -0
  17. package/dist/agent/commands/upgrade.js.map +1 -0
  18. package/dist/agent/passthrough.d.ts +1 -0
  19. package/dist/agent/passthrough.js +93 -32
  20. package/dist/agent/passthrough.js.map +1 -1
  21. package/dist/agent/snooze.d.ts +32 -0
  22. package/dist/agent/snooze.js +108 -0
  23. package/dist/agent/snooze.js.map +1 -0
  24. package/dist/agent/update-cache.d.ts +43 -0
  25. package/dist/agent/update-cache.js +125 -0
  26. package/dist/agent/update-cache.js.map +1 -0
  27. package/dist/agent-cli.js +56 -11
  28. package/dist/agent-cli.js.map +1 -1
  29. package/dist/cli/commands/install.js +3 -3
  30. package/dist/cli/commands/install.js.map +1 -1
  31. package/dist/config/credentials.d.ts +2 -0
  32. package/dist/config/credentials.js +64 -33
  33. package/dist/config/credentials.js.map +1 -1
  34. package/dist/installer/runtime/installer.d.ts +21 -6
  35. package/dist/installer/runtime/installer.js +90 -22
  36. package/dist/installer/runtime/installer.js.map +1 -1
  37. package/dist/installer/runtime/version-check.d.ts +31 -0
  38. package/dist/installer/runtime/version-check.js +78 -0
  39. package/dist/installer/runtime/version-check.js.map +1 -0
  40. package/package.json +1 -1
@@ -0,0 +1,155 @@
1
+ /**
2
+ * mthds-agent upgrade -- upgrade outdated/missing Python binary dependencies.
3
+ *
4
+ * Stdout protocol (plain text, consumed by skill upgrade-flow):
5
+ * - UPGRADE_NOT_NEEDED -- all binaries ok
6
+ * - UPGRADE_COMPLETE <json> -- all targets succeeded
7
+ * - UPGRADE_PARTIAL <json> -- some targets succeeded, some failed
8
+ * - UPGRADE_FAILED <json> -- all targets failed
9
+ *
10
+ * Architecture: De-duplicates by uv_package to guard against multiple binaries
11
+ * sharing a PyPI package. Currently pipelex-agent uses "pipelex" and plxt uses
12
+ * "pipelex-tools", so de-duplication does not trigger — but the guard prevents
13
+ * double-installs if future binaries share a package. The binary check list
14
+ * depends on the configured runner: always plxt; add pipelex-agent when
15
+ * runner === "pipelex".
16
+ */
17
+ import { writeFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { requireUv, uvToolInstallSync } from "../../installer/runtime/installer.js";
20
+ import { checkBinaryVersion } from "../../installer/runtime/version-check.js";
21
+ import { BINARY_RECOVERY } from "../binaries.js";
22
+ import { agentError, AGENT_ERROR_DOMAINS } from "../output.js";
23
+ import { clearCache, ensureStateDir, STATE_DIR } from "../update-cache.js";
24
+ import { clearSnooze } from "../snooze.js";
25
+ import { loadCredentials } from "../../config/credentials.js";
26
+ import { Runners } from "../../runners/types.js";
27
+ function errorMsg(err) {
28
+ return err instanceof Error ? err.message : String(err);
29
+ }
30
+ // ── Main ───────────────────────────────────────────────────────────
31
+ export async function agentUpgrade() {
32
+ // requireUv() — fatal if missing
33
+ try {
34
+ requireUv();
35
+ }
36
+ catch (err) {
37
+ agentError(errorMsg(err), "InstallError", {
38
+ error_domain: AGENT_ERROR_DOMAINS.INSTALL,
39
+ });
40
+ return; // unreachable — agentError calls process.exit, but explicit for TypeScript
41
+ }
42
+ const creds = loadCredentials();
43
+ const binaryKeys = ["plxt"];
44
+ if (creds.runner === Runners.PIPELEX) {
45
+ binaryKeys.push("pipelex-agent");
46
+ }
47
+ // Collect outdated/missing as targets, skip unparseable
48
+ const targets = [];
49
+ for (const key of binaryKeys) {
50
+ const recovery = BINARY_RECOVERY[key];
51
+ if (!recovery) {
52
+ process.stderr.write(`Warning: no recovery info for binary "${key}" — skipping upgrade check. This is a bug.\n`);
53
+ continue;
54
+ }
55
+ const check = checkBinaryVersion(recovery);
56
+ if (check.status === "outdated" || check.status === "missing") {
57
+ targets.push({
58
+ binaryKey: key,
59
+ recovery,
60
+ oldVersion: check.installed_version,
61
+ });
62
+ }
63
+ // "ok" and "unparseable" are both skipped — unparseable means the binary
64
+ // exists but we can't parse its version, so don't blindly reinstall.
65
+ }
66
+ if (targets.length === 0) {
67
+ process.stdout.write("UPGRADE_NOT_NEEDED\n");
68
+ return;
69
+ }
70
+ // De-duplicate by uv_package to avoid double-installing shared packages
71
+ const seen = new Map();
72
+ for (const t of targets) {
73
+ if (!seen.has(t.recovery.uv_package)) {
74
+ seen.set(t.recovery.uv_package, t);
75
+ }
76
+ }
77
+ const uniqueTargets = Array.from(seen.values());
78
+ // Install each unique uv_package, catching errors per-target
79
+ const succeeded = new Map();
80
+ const failed = new Map();
81
+ for (const target of uniqueTargets) {
82
+ try {
83
+ uvToolInstallSync(target.recovery.uv_package, target.recovery.version_constraint);
84
+ succeeded.set(target.recovery.uv_package, target);
85
+ }
86
+ catch (err) {
87
+ failed.set(target.recovery.uv_package, errorMsg(err));
88
+ }
89
+ }
90
+ // Post-check successful targets to get new version
91
+ const upgradedEntries = {};
92
+ for (const [uvPkg, target] of succeeded) {
93
+ let newVersion = null;
94
+ try {
95
+ const postCheck = checkBinaryVersion(target.recovery);
96
+ if (postCheck.status === "ok" || postCheck.status === "unparseable") {
97
+ newVersion = postCheck.installed_version;
98
+ }
99
+ else if (postCheck.status === "missing") {
100
+ // PATH issue — binary was installed but not yet visible.
101
+ // Treat as successful since uvToolInstallSync didn't throw.
102
+ newVersion = null;
103
+ }
104
+ else {
105
+ // "outdated" — install ran but didn't satisfy the constraint
106
+ process.stderr.write(`Warning: ${uvPkg} was installed but version ${postCheck.installed_version} still does not meet ${postCheck.version_constraint}.\n`);
107
+ newVersion = postCheck.installed_version;
108
+ }
109
+ }
110
+ catch (err) {
111
+ process.stderr.write(`Warning: post-upgrade version check failed for ${uvPkg}: ${errorMsg(err)}.\n`);
112
+ newVersion = null;
113
+ }
114
+ const oldV = target.oldVersion ?? "missing";
115
+ const newV = newVersion ?? "unknown";
116
+ upgradedEntries[uvPkg] = `${oldV}->${newV}`;
117
+ }
118
+ const failedEntries = Object.fromEntries(failed);
119
+ // Build marker with old versions keyed by binary name.
120
+ // The update-check preamble reads and clears this marker to detect a recent upgrade.
121
+ const markerData = {};
122
+ for (const target of targets) {
123
+ // Use binary key as marker key (e.g. "pipelex_agent", "plxt")
124
+ const markerKey = target.binaryKey.replace(/-/g, "_");
125
+ markerData[markerKey] = target.oldVersion ?? "missing";
126
+ }
127
+ // Determine outcome and emit result
128
+ const allSucceeded = failed.size === 0;
129
+ const allFailed = succeeded.size === 0;
130
+ if (allSucceeded) {
131
+ // Write marker, clear cache + snooze
132
+ try {
133
+ ensureStateDir();
134
+ writeFileSync(join(STATE_DIR, "just-upgraded-from"), JSON.stringify(markerData), "utf-8");
135
+ }
136
+ catch (err) {
137
+ // Marker write failure should not prevent reporting success
138
+ process.stderr.write(`Warning: could not write upgrade marker: ${errorMsg(err)}.\n`);
139
+ }
140
+ clearCache();
141
+ clearSnooze();
142
+ process.stdout.write("UPGRADE_COMPLETE " + JSON.stringify({ upgraded: upgradedEntries }) + "\n");
143
+ }
144
+ else if (allFailed) {
145
+ // Do NOT clear cache/snooze, do NOT write marker
146
+ process.stdout.write("UPGRADE_FAILED " + JSON.stringify({ failed: failedEntries }) + "\n");
147
+ }
148
+ else {
149
+ // Partial — do NOT clear cache/snooze, do NOT write marker
150
+ process.stdout.write("UPGRADE_PARTIAL " +
151
+ JSON.stringify({ upgraded: upgradedEntries, failed: failedEntries }) +
152
+ "\n");
153
+ }
154
+ }
155
+ //# sourceMappingURL=upgrade.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../../src/agent/commands/upgrade.ts"],"names":[],"mappings":"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"}
@@ -4,4 +4,5 @@
4
4
  */
5
5
  export declare function passthrough(bin: string, args: string[], options?: {
6
6
  autoInstall?: boolean;
7
+ skipVersionCheck?: boolean;
7
8
  }): void;
@@ -4,39 +4,100 @@
4
4
  */
5
5
  import { spawnSync } from "node:child_process";
6
6
  import { agentError, AGENT_ERROR_DOMAINS } from "./output.js";
7
- import { BINARY_RECOVERY } from "./binaries.js";
8
- import { isBinaryInstalled } from "../installer/runtime/check.js";
9
- import { installPipelexSync, installPlxtSync } from "../installer/runtime/installer.js";
10
- const AUTO_INSTALL_FN = {
11
- pipelex: installPipelexSync,
12
- "pipelex-agent": installPipelexSync,
13
- plxt: installPlxtSync,
14
- };
7
+ import { BINARY_RECOVERY, buildInstallCommand } from "./binaries.js";
8
+ import { checkBinaryVersion } from "../installer/runtime/version-check.js";
9
+ import { uvToolInstallSync } from "../installer/runtime/installer.js";
15
10
  export function passthrough(bin, args, options) {
16
11
  const recovery = BINARY_RECOVERY[bin];
17
- // Auto-install: attempt to install the binary before spawning
18
- if (options?.autoInstall && recovery?.auto_installable && !isBinaryInstalled(bin)) {
19
- const installFn = AUTO_INSTALL_FN[bin];
20
- if (installFn) {
21
- try {
22
- installFn();
23
- }
24
- catch (err) {
25
- const msg = err instanceof Error ? err.message : String(err);
26
- agentError(`Failed to auto-install ${bin}: ${msg}`, "InstallError", {
27
- error_domain: AGENT_ERROR_DOMAINS.INSTALL,
28
- hint: `Install manually: ${recovery.install_command}`,
29
- recovery,
30
- });
31
- }
32
- // Verify install succeeded
33
- if (!isBinaryInstalled(bin)) {
34
- agentError(`${bin} was installed but is not reachable in PATH.`, "InstallError", {
35
- error_domain: AGENT_ERROR_DOMAINS.INSTALL,
36
- hint: "You may need to restart your shell or add the install directory to your PATH.",
37
- recovery,
38
- });
39
- }
12
+ // Version check runs unconditionally when recovery info exists (unless
13
+ // explicitly skipped). autoInstall only controls whether to attempt a fix
14
+ // (install/upgrade). When autoInstall is false and status is missing/outdated,
15
+ // emit a structured error with install/upgrade instructions.
16
+ if (!options?.skipVersionCheck && recovery) {
17
+ const check = checkBinaryVersion(recovery);
18
+ const autoInstall = options?.autoInstall && recovery.auto_installable;
19
+ switch (check.status) {
20
+ case "ok":
21
+ break;
22
+ case "missing":
23
+ if (autoInstall) {
24
+ try {
25
+ uvToolInstallSync(recovery.uv_package, recovery.version_constraint);
26
+ }
27
+ catch (err) {
28
+ const msg = err instanceof Error ? err.message : String(err);
29
+ agentError(`Failed to auto-install ${bin}: ${msg}`, "InstallError", {
30
+ error_domain: AGENT_ERROR_DOMAINS.INSTALL,
31
+ hint: `Install manually: ${buildInstallCommand(recovery)}`,
32
+ recovery,
33
+ });
34
+ }
35
+ // Verify install succeeded and version constraint is satisfied
36
+ {
37
+ const postCheck = checkBinaryVersion(recovery);
38
+ if (postCheck.status === "missing") {
39
+ agentError(`${bin} was installed but is not reachable in PATH.`, "InstallError", {
40
+ error_domain: AGENT_ERROR_DOMAINS.INSTALL,
41
+ hint: "You may need to restart your shell or add the install directory to your PATH.",
42
+ recovery,
43
+ });
44
+ }
45
+ else if (postCheck.status !== "ok") {
46
+ process.stderr.write(JSON.stringify({
47
+ warning: true,
48
+ message: `Install of ${bin} may not have taken effect (status: ${postCheck.status}, installed: ${postCheck.installed_version}, needs: ${recovery.version_constraint}).`,
49
+ }) + "\n");
50
+ }
51
+ }
52
+ }
53
+ else {
54
+ agentError(`${bin} is not installed. Install it to continue.`, "BinaryNotFoundError", {
55
+ error_domain: AGENT_ERROR_DOMAINS.BINARY,
56
+ hint: `Install: ${buildInstallCommand(recovery)}`,
57
+ recovery,
58
+ });
59
+ }
60
+ break;
61
+ case "outdated":
62
+ if (autoInstall) {
63
+ try {
64
+ uvToolInstallSync(recovery.uv_package, recovery.version_constraint);
65
+ }
66
+ catch (err) {
67
+ const msg = err instanceof Error ? err.message : String(err);
68
+ agentError(`Failed to upgrade ${bin} (installed ${check.installed_version}, needs ${recovery.version_constraint}): ${msg}`, "InstallError", {
69
+ error_domain: AGENT_ERROR_DOMAINS.INSTALL,
70
+ hint: `Upgrade manually: ${buildInstallCommand(recovery)}`,
71
+ recovery,
72
+ });
73
+ }
74
+ // Verify upgrade actually satisfied the constraint
75
+ {
76
+ const recheck = checkBinaryVersion(recovery);
77
+ if (recheck.status !== "ok") {
78
+ process.stderr.write(JSON.stringify({
79
+ warning: true,
80
+ message: `Upgrade of ${bin} may not have taken effect (status: ${recheck.status}, installed: ${recheck.installed_version}, needs: ${recovery.version_constraint}).`,
81
+ }) + "\n");
82
+ }
83
+ }
84
+ }
85
+ else {
86
+ agentError(`${bin} is outdated (installed ${check.installed_version}, needs ${recovery.version_constraint}). Upgrade to continue.`, "InstallError", {
87
+ error_domain: AGENT_ERROR_DOMAINS.INSTALL,
88
+ hint: `Upgrade: ${buildInstallCommand(recovery)}`,
89
+ recovery,
90
+ });
91
+ }
92
+ break;
93
+ case "unparseable":
94
+ // Warn to stderr but don't block execution
95
+ process.stderr.write(JSON.stringify({
96
+ warning: true,
97
+ message: `Could not parse version for ${bin}. Proceeding anyway.`,
98
+ version_constraint: check.version_constraint,
99
+ }) + "\n");
100
+ break;
40
101
  }
41
102
  }
42
103
  const result = spawnSync(bin, args, {
@@ -45,7 +106,7 @@ export function passthrough(bin, args, options) {
45
106
  if (result.error) {
46
107
  if (result.error.code === "ENOENT") {
47
108
  const hint = recovery
48
- ? `Install ${recovery.package}: ${recovery.install_command}`
109
+ ? `Install ${recovery.package}: ${buildInstallCommand(recovery)}`
49
110
  : "Make sure the required CLI binary is installed and in your PATH.";
50
111
  agentError(`${bin} not found. Make sure it is installed and in your PATH.`, "BinaryNotFoundError", {
51
112
  error_domain: AGENT_ERROR_DOMAINS.BINARY,
@@ -1 +1 @@
1
- {"version":3,"file":"passthrough.js","sourceRoot":"","sources":["../../src/agent/passthrough.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAExF,MAAM,eAAe,GAA+B;IAClD,OAAO,EAAE,kBAAkB;IAC3B,eAAe,EAAE,kBAAkB;IACnC,IAAI,EAAE,eAAe;CACtB,CAAC;AAEF,MAAM,UAAU,WAAW,CACzB,GAAW,EACX,IAAc,EACd,OAAmC;IAEnC,MAAM,QAAQ,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IAEtC,8DAA8D;IAC9D,IAAI,OAAO,EAAE,WAAW,IAAI,QAAQ,EAAE,gBAAgB,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;QAClF,MAAM,SAAS,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,SAAS,EAAE,CAAC;YACd,IAAI,CAAC;gBACH,SAAS,EAAE,CAAC;YACd,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAC7D,UAAU,CACR,0BAA0B,GAAG,KAAK,GAAG,EAAE,EACvC,cAAc,EACd;oBACE,YAAY,EAAE,mBAAmB,CAAC,OAAO;oBACzC,IAAI,EAAE,qBAAqB,QAAQ,CAAC,eAAe,EAAE;oBACrD,QAAQ;iBACT,CACF,CAAC;YACJ,CAAC;YACD,2BAA2B;YAC3B,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC5B,UAAU,CACR,GAAG,GAAG,8CAA8C,EACpD,cAAc,EACd;oBACE,YAAY,EAAE,mBAAmB,CAAC,OAAO;oBACzC,IAAI,EAAE,+EAA+E;oBACrF,QAAQ;iBACT,CACF,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE;QAClC,KAAK,EAAE,SAAS;KACjB,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,IAAK,MAAM,CAAC,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC9D,MAAM,IAAI,GAAG,QAAQ;gBACnB,CAAC,CAAC,WAAW,QAAQ,CAAC,OAAO,KAAK,QAAQ,CAAC,eAAe,EAAE;gBAC5D,CAAC,CAAC,kEAAkE,CAAC;YACvE,UAAU,CACR,GAAG,GAAG,yDAAyD,EAC/D,qBAAqB,EACrB;gBACE,YAAY,EAAE,mBAAmB,CAAC,MAAM;gBACxC,IAAI;gBACJ,QAAQ;aACT,CACF,CAAC;QACJ,CAAC;QACD,UAAU,CACR,mBAAmB,GAAG,KAAK,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EACjD,kBAAkB,EAClB,EAAE,YAAY,EAAE,mBAAmB,CAAC,MAAM,EAAE,CAC7C,CAAC;IACJ,CAAC;IAED,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;AACnC,CAAC"}
1
+ {"version":3,"file":"passthrough.js","sourceRoot":"","sources":["../../src/agent/passthrough.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Snooze state for update-check upgrade prompts.
3
+ *
4
+ * Manages ~/.mthds/state/update-snoozed — a single-line file:
5
+ * <versionKey> <level> <epoch>
6
+ *
7
+ * Version key is a plain concatenation of binary statuses (human-readable).
8
+ * Escalating backoff: level 1 = 24h, level 2 = 48h, level 3+ = 7d.
9
+ * Snooze resets when the version key changes (any binary constraint updated).
10
+ */
11
+ import type { CachePayload } from "./update-cache.js";
12
+ export interface SnoozeState {
13
+ versionKey: string;
14
+ level: number;
15
+ epoch: number;
16
+ }
17
+ /**
18
+ * Compute a human-readable version key from a cache payload.
19
+ * Format: "status1:status2:status3" with constraints appended for non-ok entries.
20
+ * Changes whenever any binary's status or constraint changes.
21
+ */
22
+ export declare function computeVersionKey(payload: CachePayload): string;
23
+ /** Read current snooze state. Returns null if missing or corrupt. */
24
+ export declare function readSnooze(): SnoozeState | null;
25
+ /**
26
+ * Write snooze state. Escalates level if same versionKey, resets if different.
27
+ */
28
+ export declare function writeSnooze(versionKey: string): void;
29
+ /** Check if snooze is active for the given versionKey. */
30
+ export declare function isSnoozed(versionKey: string): boolean;
31
+ /** Clear snooze file. */
32
+ export declare function clearSnooze(): void;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Snooze state for update-check upgrade prompts.
3
+ *
4
+ * Manages ~/.mthds/state/update-snoozed — a single-line file:
5
+ * <versionKey> <level> <epoch>
6
+ *
7
+ * Version key is a plain concatenation of binary statuses (human-readable).
8
+ * Escalating backoff: level 1 = 24h, level 2 = 48h, level 3+ = 7d.
9
+ * Snooze resets when the version key changes (any binary constraint updated).
10
+ */
11
+ import { join } from "node:path";
12
+ import { readFileSync, writeFileSync, unlinkSync } from "node:fs";
13
+ import { STATE_DIR, ensureStateDir } from "./update-cache.js";
14
+ // ── Constants ──────────────────────────────────────────────────────
15
+ const SNOOZE_PATH = join(STATE_DIR, "update-snoozed");
16
+ const SNOOZE_DURATIONS_MS = {
17
+ 1: 24 * 60 * 60 * 1000, // 24h
18
+ 2: 48 * 60 * 60 * 1000, // 48h
19
+ };
20
+ const SNOOZE_DEFAULT_MS = 7 * 24 * 60 * 60 * 1000; // 7d for level 3+
21
+ // ── Functions ──────────────────────────────────────────────────────
22
+ /**
23
+ * Compute a human-readable version key from a cache payload.
24
+ * Format: "status1:status2:status3" with constraints appended for non-ok entries.
25
+ * Changes whenever any binary's status or constraint changes.
26
+ */
27
+ export function computeVersionKey(payload) {
28
+ const parts = [
29
+ payload.mthds_agent.s + (payload.mthds_agent.r ?? ""),
30
+ ];
31
+ if (payload.pipelex_agent) {
32
+ parts.push(payload.pipelex_agent.s + (payload.pipelex_agent.r ?? ""));
33
+ }
34
+ parts.push(payload.plxt.s + (payload.plxt.r ?? ""));
35
+ return parts.join(":");
36
+ }
37
+ /** Read current snooze state. Returns null if missing or corrupt. */
38
+ export function readSnooze() {
39
+ let content;
40
+ try {
41
+ content = readFileSync(SNOOZE_PATH, "utf-8").trim();
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ // Format: "<versionKey> <level> <epoch>"
47
+ // The version key may contain colons but not spaces, so split from the right.
48
+ const lastSpace = content.lastIndexOf(" ");
49
+ if (lastSpace === -1)
50
+ return null;
51
+ const epochStr = content.slice(lastSpace + 1);
52
+ const rest = content.slice(0, lastSpace);
53
+ const secondLastSpace = rest.lastIndexOf(" ");
54
+ if (secondLastSpace === -1)
55
+ return null;
56
+ const versionKey = rest.slice(0, secondLastSpace);
57
+ const levelStr = rest.slice(secondLastSpace + 1);
58
+ const level = parseInt(levelStr, 10);
59
+ const epoch = parseInt(epochStr, 10);
60
+ if (isNaN(level) || isNaN(epoch) || level <= 0 || !versionKey)
61
+ return null;
62
+ return { versionKey, level, epoch };
63
+ }
64
+ /**
65
+ * Write snooze state. Escalates level if same versionKey, resets if different.
66
+ */
67
+ export function writeSnooze(versionKey) {
68
+ ensureStateDir();
69
+ const existing = readSnooze();
70
+ let level;
71
+ if (existing && existing.versionKey === versionKey) {
72
+ level = existing.level + 1;
73
+ }
74
+ else {
75
+ level = 1;
76
+ }
77
+ const content = `${versionKey} ${level} ${Date.now()}\n`;
78
+ try {
79
+ writeFileSync(SNOOZE_PATH, content, "utf-8");
80
+ }
81
+ catch (err) {
82
+ const code = err.code;
83
+ process.stderr.write(`Warning: could not write snooze state (${code ?? String(err)}).\n`);
84
+ }
85
+ }
86
+ /** Check if snooze is active for the given versionKey. */
87
+ export function isSnoozed(versionKey) {
88
+ const state = readSnooze();
89
+ if (!state)
90
+ return false;
91
+ // Different version key means the constraint changed — snooze is invalid
92
+ if (state.versionKey !== versionKey)
93
+ return false;
94
+ const duration = SNOOZE_DURATIONS_MS[state.level] ?? SNOOZE_DEFAULT_MS;
95
+ const elapsed = Date.now() - state.epoch;
96
+ // Negative elapsed beyond 1 minute means clock skew — treat snooze as expired
97
+ return elapsed >= -60_000 && elapsed < duration;
98
+ }
99
+ /** Clear snooze file. */
100
+ export function clearSnooze() {
101
+ try {
102
+ unlinkSync(SNOOZE_PATH);
103
+ }
104
+ catch {
105
+ // File may not exist — that's fine
106
+ }
107
+ }
108
+ //# sourceMappingURL=snooze.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snooze.js","sourceRoot":"","sources":["../../src/agent/snooze.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Cache for update-check results.
3
+ *
4
+ * Manages ~/.mthds/state/last-update-check — a two-line file:
5
+ * Line 1: aggregate status (UP_TO_DATE or UPGRADE_AVAILABLE)
6
+ * Line 2: JSON payload with per-binary check results
7
+ *
8
+ * TTL is based on file mtime (like gstack), not an embedded timestamp.
9
+ * Split TTL: 60 min for UP_TO_DATE, 720 min for UPGRADE_AVAILABLE.
10
+ */
11
+ import type { VersionStatus } from "../installer/runtime/version-check.js";
12
+ export type AggregateStatus = "UP_TO_DATE" | "UPGRADE_AVAILABLE";
13
+ export interface BinaryCheckEntry {
14
+ /** Version status: "ok", "outdated", "missing", "unparseable" */
15
+ s: VersionStatus;
16
+ /** Installed version, or null */
17
+ v: string | null;
18
+ /** Required constraint (present when s !== "ok") */
19
+ r?: string;
20
+ }
21
+ export interface CachePayload {
22
+ mthds_agent: BinaryCheckEntry;
23
+ pipelex_agent?: BinaryCheckEntry;
24
+ plxt: BinaryCheckEntry;
25
+ }
26
+ export interface CacheResult {
27
+ aggregate: AggregateStatus;
28
+ payload: CachePayload;
29
+ }
30
+ export declare const STATE_DIR: string;
31
+ /** Ensure the state directory exists. */
32
+ export declare function ensureStateDir(): void;
33
+ /** Compute aggregate status from a payload. */
34
+ export declare function computeAggregate(payload: CachePayload): AggregateStatus;
35
+ /**
36
+ * Read the update-check cache.
37
+ * Returns null if the file is missing, corrupt, or expired.
38
+ */
39
+ export declare function readCache(): CacheResult | null;
40
+ /** Write cache. Creates state directory if needed. */
41
+ export declare function writeCache(result: CacheResult): void;
42
+ /** Delete cache file (used by --force and after upgrade). */
43
+ export declare function clearCache(): void;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Cache for update-check results.
3
+ *
4
+ * Manages ~/.mthds/state/last-update-check — a two-line file:
5
+ * Line 1: aggregate status (UP_TO_DATE or UPGRADE_AVAILABLE)
6
+ * Line 2: JSON payload with per-binary check results
7
+ *
8
+ * TTL is based on file mtime (like gstack), not an embedded timestamp.
9
+ * Split TTL: 60 min for UP_TO_DATE, 720 min for UPGRADE_AVAILABLE.
10
+ */
11
+ import { join } from "node:path";
12
+ import { homedir } from "node:os";
13
+ import { mkdirSync, readFileSync, writeFileSync, unlinkSync, statSync, } from "node:fs";
14
+ // ── Constants ──────────────────────────────────────────────────────
15
+ export const STATE_DIR = join(homedir(), ".mthds", "state");
16
+ const CACHE_PATH = join(STATE_DIR, "last-update-check");
17
+ const TTL_UP_TO_DATE_MS = 60 * 60 * 1000; // 60 min
18
+ const TTL_UPGRADE_AVAILABLE_MS = 720 * 60 * 1000; // 720 min (12 hours)
19
+ const VALID_AGGREGATES = new Set([
20
+ "UP_TO_DATE",
21
+ "UPGRADE_AVAILABLE",
22
+ ]);
23
+ // ── Validation ──────────────────────────────────────────────────────
24
+ function isValidPayload(p) {
25
+ if (!p || typeof p !== "object")
26
+ return false;
27
+ const obj = p;
28
+ // mthds_agent and plxt are always required
29
+ for (const key of ["mthds_agent", "plxt"]) {
30
+ const entry = obj[key];
31
+ if (!entry || typeof entry !== "object")
32
+ return false;
33
+ if (typeof entry.s !== "string")
34
+ return false;
35
+ }
36
+ // pipelex_agent is optional, but if present must be valid
37
+ if (obj.pipelex_agent !== undefined) {
38
+ const entry = obj.pipelex_agent;
39
+ if (!entry || typeof entry !== "object")
40
+ return false;
41
+ if (typeof entry.s !== "string")
42
+ return false;
43
+ }
44
+ return true;
45
+ }
46
+ // ── Functions ──────────────────────────────────────────────────────
47
+ /** Ensure the state directory exists. */
48
+ export function ensureStateDir() {
49
+ mkdirSync(STATE_DIR, { recursive: true });
50
+ }
51
+ /** Compute aggregate status from a payload. */
52
+ export function computeAggregate(payload) {
53
+ const entries = [payload.mthds_agent, payload.plxt];
54
+ if (payload.pipelex_agent)
55
+ entries.push(payload.pipelex_agent);
56
+ // Treat "unparseable" same as "ok" — the binary exists, we just can't parse
57
+ // its version. Treating it as UPGRADE_AVAILABLE would cause an infinite loop:
58
+ // preamble says upgrade available -> upgrade skips unparseable -> repeat.
59
+ return entries.every((e) => e.s === "ok" || e.s === "unparseable")
60
+ ? "UP_TO_DATE"
61
+ : "UPGRADE_AVAILABLE";
62
+ }
63
+ /**
64
+ * Read the update-check cache.
65
+ * Returns null if the file is missing, corrupt, or expired.
66
+ */
67
+ export function readCache() {
68
+ // Stat first so worst-case TOCTOU treats fresh data as stale (safe direction)
69
+ let mtimeMs;
70
+ let content;
71
+ try {
72
+ mtimeMs = statSync(CACHE_PATH).mtimeMs;
73
+ content = readFileSync(CACHE_PATH, "utf-8");
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ const lines = content.split("\n");
79
+ if (lines.length < 2)
80
+ return null;
81
+ const aggregate = lines[0].trim();
82
+ if (!VALID_AGGREGATES.has(aggregate))
83
+ return null;
84
+ let payload;
85
+ try {
86
+ const parsed = JSON.parse(lines[1]);
87
+ if (!isValidPayload(parsed))
88
+ return null;
89
+ payload = parsed;
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ // Check TTL based on file mtime
95
+ const ttl = aggregate === "UP_TO_DATE"
96
+ ? TTL_UP_TO_DATE_MS
97
+ : TTL_UPGRADE_AVAILABLE_MS;
98
+ const age = Date.now() - mtimeMs;
99
+ // Negative age beyond 1 minute means clock skew — treat as expired
100
+ if (age < -60_000 || age > ttl)
101
+ return null;
102
+ return { aggregate: aggregate, payload };
103
+ }
104
+ /** Write cache. Creates state directory if needed. */
105
+ export function writeCache(result) {
106
+ try {
107
+ ensureStateDir();
108
+ const content = result.aggregate + "\n" + JSON.stringify(result.payload) + "\n";
109
+ writeFileSync(CACHE_PATH, content, "utf-8");
110
+ }
111
+ catch (err) {
112
+ const code = err.code;
113
+ process.stderr.write(`Warning: could not write update-check cache (${code ?? String(err)}). Check will run again next time.\n`);
114
+ }
115
+ }
116
+ /** Delete cache file (used by --force and after upgrade). */
117
+ export function clearCache() {
118
+ try {
119
+ unlinkSync(CACHE_PATH);
120
+ }
121
+ catch {
122
+ // File may not exist — that's fine
123
+ }
124
+ }
125
+ //# sourceMappingURL=update-cache.js.map