@yugabytedb/meko-mcp 2.3.0 → 2.5.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 (42) hide show
  1. package/README.md +88 -35
  2. package/assets/hooks-handlers/lib/capture-cleanup-cli.js +735 -74
  3. package/assets/hooks-handlers/lib/capture-notices.js +94 -1
  4. package/assets/hooks-handlers/lib/capture-state.js +111 -0
  5. package/assets/hooks-handlers/lib/capture.js +130 -4
  6. package/assets/hooks-handlers/lib/checkpoint-timer.js +394 -8
  7. package/assets/hooks-handlers/lib/opencode-handler.js +29 -6
  8. package/assets/scripts/meko-statusline.sh +17 -1
  9. package/assets/skills/meko-mcp-tools/SKILL.md +6 -5
  10. package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
  11. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +194 -10
  12. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +6 -3
  13. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +11 -6
  14. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +4 -4
  15. package/assets/skills/meko-mcp-tools/references/tools-overview.md +19 -14
  16. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +69 -8
  17. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +3 -1
  18. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +8 -7
  19. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +4 -4
  20. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
  21. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +194 -10
  22. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +10 -5
  23. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +4 -4
  24. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +19 -14
  25. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +68 -8
  26. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +3 -1
  27. package/assets/skills/meko-select-datapack-desktop/SKILL.md +91 -12
  28. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +39 -7
  29. package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +8 -6
  30. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +13 -1
  31. package/bin/create-meko-setup.mjs +152 -35
  32. package/lib/autofix.mjs +100 -0
  33. package/lib/browser-login.mjs +495 -0
  34. package/lib/clients/codex.mjs +1 -24
  35. package/lib/clients/common.mjs +25 -0
  36. package/lib/clients/opencode.mjs +6 -1
  37. package/lib/migrate/datapack.mjs +254 -16
  38. package/lib/migrate/mcp-client.mjs +34 -0
  39. package/lib/migrate/orchestrator.mjs +1 -1
  40. package/lib/migrate.mjs +2 -6
  41. package/lib/validate.mjs +111 -22
  42. package/package.json +5 -5
@@ -7,8 +7,10 @@ import { dirname, join } from 'node:path';
7
7
 
8
8
  import { install, uninstall } from '../lib/installer.mjs';
9
9
  import { askChoice, askMultiChoice, askText, askSecret, askYesNo, close as closePrompts } from '../lib/prompt.mjs';
10
- import { banner, error, dim, info, warn } from '../lib/logger.mjs';
10
+ import { banner, error, dim, info, success, warn } from '../lib/logger.mjs';
11
+ import { BrowserLoginError, consoleUrlFor, loginAndCreateApiKey } from '../lib/browser-login.mjs';
11
12
  import {
13
+ isLocalUrl,
12
14
  isPackagedInstall,
13
15
  MEKO_PRODUCTION_CONSOLE_URL,
14
16
  selfInvocation,
@@ -20,6 +22,7 @@ import { startMigration, runChildMigration, readStatus, stopMigration } from '..
20
22
  import { checkLatest } from '../lib/version-check.mjs';
21
23
  import { runCaptureStatus } from '../lib/capture-status.mjs';
22
24
  import { captureCleanupArgs, runCaptureCleanup } from '../lib/capture-cleanup.mjs';
25
+ import { applyAutofixSetting, describeAutofix } from '../lib/autofix.mjs';
23
26
 
24
27
  // ---------------------------------------------------------------------------
25
28
  // Flag definitions
@@ -28,6 +31,7 @@ import { captureCleanupArgs, runCaptureCleanup } from '../lib/capture-cleanup.mj
28
31
  const options = {
29
32
  url: { type: 'string' },
30
33
  'api-key': { type: 'string' },
34
+ 'no-browser': { type: 'boolean', default: false },
31
35
  local: { type: 'boolean', default: false },
32
36
  scope: { type: 'string', default: 'user' },
33
37
  name: { type: 'string', default: 'meko' },
@@ -37,6 +41,8 @@ const options = {
37
41
  'skip-validation': { type: 'boolean', default: false },
38
42
  statusline: { type: 'boolean', default: false },
39
43
  'no-statusline': { type: 'boolean', default: false },
44
+ autofix: { type: 'boolean', default: false },
45
+ 'no-autofix': { type: 'boolean', default: false },
40
46
  'no-canary': { type: 'boolean', default: false },
41
47
  'dry-run': { type: 'boolean', default: false },
42
48
  'check-updates': { type: 'boolean', default: false },
@@ -81,6 +87,9 @@ function readVersion() {
81
87
  return pkg.version;
82
88
  }
83
89
 
90
+ // The docker-compose dev stack that --local points at. It takes no API key.
91
+ const LOCAL_DEV_MCP_URL = 'http://localhost:8000/mcp';
92
+
84
93
  const ALL_CLIENTS = ['claude-code', 'claude-desktop', 'cursor', 'codex', 'kiro', 'opencode'];
85
94
 
86
95
  function printHelp() {
@@ -96,7 +105,10 @@ Examples:
96
105
  # Interactive setup (recommended for first-time users):
97
106
  create-meko-setup
98
107
 
99
- # Cloud Meko for Claude Code (non-interactive):
108
+ # Cloud Meko for Claude Code (signs in through your browser):
109
+ create-meko-setup --url https://mcp.mekodata.ai/mcp --client claude-code
110
+
111
+ # Cloud Meko with an existing API key (unattended, e.g. CI):
100
112
  create-meko-setup --url https://mcp.mekodata.ai/mcp --api-key "$MEKO_API_KEY" --yes
101
113
 
102
114
  # Local Meko with Cursor config:
@@ -131,8 +143,13 @@ Options:
131
143
  untrusted IDE workspaces and non-managed CLI agents fall
132
144
  back to agent-driven persistence.
133
145
  --url <url> MCP server URL (from cloud.mekodata.ai > Datapacks > Connect)
134
- --api-key <key> API key for Cloud Meko authentication
135
- --local Use local Meko at http://localhost:8000/mcp
146
+ --api-key <key> Use an existing Meko API key (Cloud, or Meko Local's
147
+ client token) instead of signing in through the
148
+ browser. Also read from $MEKO_API_KEY for any URL
149
+ (ignored with --local). Required for Cloud when no
150
+ terminal is attached (CI, scripts).
151
+ --no-browser Print the sign-in URL instead of opening a browser
152
+ --local Use local Meko at http://localhost:8000/mcp (no API key)
136
153
  --scope <scope> Config scope: user or project (default: user)
137
154
  codex always installs at user scope (no project-level
138
155
  config); --scope=project degrades with a warning.
@@ -144,8 +161,13 @@ Options:
144
161
  Default: ON under --yes, prompted otherwise.
145
162
  Claude Code only.
146
163
  --no-statusline Skip statusline wiring even under --yes.
164
+ --no-autofix Turn off automatic capture repair: the capture timer
165
+ otherwise runs --capture-cleanup fix every 30 minutes
166
+ (MEKO_AUTOFIX_INTERVAL) when a session is stuck.
167
+ --autofix Turn automatic capture repair back on.
147
168
  --skip-validation Skip post-install validation
148
- --no-canary Skip the canary datapack_list call. The
169
+ --no-canary Skip the canary datapack_search (or, on older
170
+ servers, datapack_list) call. The
149
171
  connectivity + tools/list checks still run.
150
172
  --dry-run Show what would be done without making changes
151
173
  --check-updates Compare installed version against npm registry and exit
@@ -167,6 +189,10 @@ Capture health (durable outbox / recovery):
167
189
  Clean up stuck capture on this machine:
168
190
  status list sessions with unsent work and the
169
191
  background capture processes (read-only)
192
+ fix diagnose each stuck session and fix it:
193
+ repair, drain, or move unsent turns to a
194
+ new conversation; reports what only you
195
+ can fix (start here)
170
196
  stop stop SessionEnd workers and in-flight
171
197
  drains, and park all unsent work so no
172
198
  hook retries it (nothing is deleted)
@@ -205,10 +231,6 @@ Migrate existing conversations and memories into Meko:
205
231
  console.log(text);
206
232
  }
207
233
 
208
- function isCloudUrl(url) {
209
- return url && !url.includes('localhost') && !url.includes('127.0.0.1');
210
- }
211
-
212
234
  /**
213
235
  * Sanitize a raw API key from any source (CLI flag, env var, askSecret prompt).
214
236
  *
@@ -295,6 +317,32 @@ function parseRate(raw) {
295
317
  return n;
296
318
  }
297
319
 
320
+ // Placeholder shown in --dry-run output in place of a key that browser
321
+ // sign-in would create. Never sent anywhere: dry runs make no changes.
322
+ const DRY_RUN_SIGN_IN_KEY = 'mko_tkn_<created-by-browser-sign-in>';
323
+
324
+ /**
325
+ * Sign in through the browser and create an API key for this install.
326
+ * Returns the key, or null after printing why sign-in failed.
327
+ */
328
+ async function signInWithBrowser({ url, noBrowser, dryRun }) {
329
+ if (dryRun) {
330
+ dim('Would sign in with your browser and create an API key (skipped: --dry-run).');
331
+ return DRY_RUN_SIGN_IN_KEY;
332
+ }
333
+ info('Signing in to Meko with your browser to create an API key...');
334
+ try {
335
+ const key = await loginAndCreateApiKey({ url, noBrowser, notify: (msg) => dim(msg) });
336
+ const expires = key.expiresAt ? ` (expires ${String(key.expiresAt).slice(0, 10)})` : '';
337
+ success(`Signed in. Created API key "${key.name}"${expires}.`);
338
+ dim(`Manage or revoke it at ${consoleUrlFor(url)} > Settings > API Keys.`);
339
+ return key.apiKey;
340
+ } catch (err) {
341
+ error(err instanceof BrowserLoginError ? err.message : `Browser sign-in failed: ${err?.message ?? err}`);
342
+ return null;
343
+ }
344
+ }
345
+
298
346
  // ---------------------------------------------------------------------------
299
347
  // Main
300
348
  // ---------------------------------------------------------------------------
@@ -340,7 +388,7 @@ async function main() {
340
388
  if (out.stderr) process.stderr.write(out.stderr);
341
389
  process.exit(out.code);
342
390
  }
343
- // --capture-cleanup <action>: inspect, stop, park, drain, or discard
391
+ // --capture-cleanup <action>: inspect, fix, stop, park, drain, or discard
344
392
  // stuck capture sessions. Exit codes: 0 done, 1 error, 2 partly done.
345
393
  if (flags['capture-cleanup'] !== undefined) {
346
394
  const out = runCaptureCleanup(
@@ -433,7 +481,7 @@ async function main() {
433
481
  }
434
482
  let url = flags.url ?? process.env.MEKO_MCP_URL ?? null;
435
483
  let apiKey = normalizeApiKey(flags['api-key'] ?? process.env.MEKO_API_KEY);
436
- if (flags.local) { url = 'http://localhost:8000/mcp'; apiKey = null; }
484
+ if (flags.local) { url = LOCAL_DEV_MCP_URL; apiKey = null; }
437
485
 
438
486
  let parentRate;
439
487
  try { parentRate = parseRate(flags.rate); }
@@ -508,11 +556,21 @@ async function main() {
508
556
  // submitting an empty value at the full-flow prompt would re-ask the
509
557
  // same question.
510
558
  let promptedForApiKey = false;
559
+ // Set when the user picked "Sign in with browser" at the interactive auth
560
+ // prompt. Someone answered that prompt, so sign-in runs even when stdin
561
+ // is not a TTY (e.g. piped answers in the smoke harness).
562
+ let choseBrowserSignIn = false;
563
+ // Set for --local, the interactive "Local Meko" choice, and a --yes run
564
+ // that reuses an existing LOCAL_DEV_MCP_URL entry: the docker-compose dev
565
+ // stack takes no key, so $MEKO_API_KEY is not read for it. An explicit
566
+ // --url, local or not, still reads it.
567
+ let keylessLocal = false;
511
568
 
512
569
  // --local shortcut
513
570
  if (flags.local) {
514
- url = 'http://localhost:8000/mcp';
571
+ url = LOCAL_DEV_MCP_URL;
515
572
  apiKey = null;
573
+ keylessLocal = true;
516
574
  }
517
575
 
518
576
  // Client picker — runs for any interactive invocation (including
@@ -578,6 +636,11 @@ async function main() {
578
636
  process.exit(1);
579
637
  }
580
638
  url = suggestedUrl.url;
639
+ // Reusing the dev stack an earlier --local install registered: keep it
640
+ // keyless, as --local would.
641
+ if (suggestedUrl.source === 'existing' && url === LOCAL_DEV_MCP_URL) {
642
+ keylessLocal = true;
643
+ }
581
644
  }
582
645
  }
583
646
 
@@ -591,7 +654,7 @@ async function main() {
591
654
  // When only Cloud is available, skip the prompt entirely — asking to
592
655
  // pick between one option is pointless UX.
593
656
  const connectionOptions = [
594
- { label: 'Cloud Meko', description: 'Hosted at mekodata.ai (requires an API key)' },
657
+ { label: 'Cloud Meko', description: 'Hosted at mekodata.ai (sign in with your browser)' },
595
658
  ];
596
659
  if (!isPackagedInstall()) {
597
660
  connectionOptions.push({
@@ -620,14 +683,28 @@ async function main() {
620
683
  suggestedUrl?.url ?? undefined,
621
684
  );
622
685
 
623
- // Step 3 — API key. Normalize: terminal paste can drag in trailing
624
- // whitespace/CR or accidental surrounding quotes, all of which would
625
- // sail straight into `Authorization: Bearer ...` and 401.
626
- apiKey = normalizeApiKey(await askSecret('API key:'));
627
- promptedForApiKey = true;
686
+ // Step 3 — authentication. Browser sign-in is the default and runs
687
+ // in the Cloud-URL check below; pasting a key is the advanced path.
688
+ // An existing --api-key or $MEKO_API_KEY skips the question.
689
+ if (!apiKey && !normalizeApiKey(process.env.MEKO_API_KEY)) {
690
+ const authChoice = await askChoice('How do you want to authenticate?', [
691
+ { label: 'Sign in with browser', description: 'Recommended. Creates an API key for you' },
692
+ { label: 'Paste an API key', description: `From ${consoleUrlFor(url)} > Settings > API Keys` },
693
+ ]);
694
+ if (authChoice === 'Paste an API key') {
695
+ // Normalize: terminal paste can drag in trailing whitespace/CR or
696
+ // accidental surrounding quotes, all of which would sail straight
697
+ // into `Authorization: Bearer ...` and 401.
698
+ apiKey = normalizeApiKey(await askSecret('API key:'));
699
+ promptedForApiKey = true;
700
+ } else {
701
+ choseBrowserSignIn = true;
702
+ }
703
+ }
628
704
  } else {
629
- url = 'http://localhost:8000/mcp';
705
+ url = LOCAL_DEV_MCP_URL;
630
706
  apiKey = null;
707
+ keylessLocal = true;
631
708
  }
632
709
 
633
710
  // Step 4 — skills
@@ -661,27 +738,50 @@ async function main() {
661
738
  process.exit(1);
662
739
  }
663
740
 
664
- // Validation — Cloud URL requires an API key.
665
- // Resolution order: --api-key (already in `apiKey`) → $MEKO_API_KEY →
666
- // interactive prompt (only on a TTY, not under --yes, and only if the
667
- // full interactive flow above didn't already ask). The prompt fallback
668
- // exists so a user who pastes the cloud one-liner from the UI (which
669
- // only shows --url and --client) isn't dead-ended on a hard error when
670
- // they forgot to supply the key. Skipping it after a full-flow prompt
671
- // avoids re-asking the same question when the user submitted empty.
672
- if (isCloudUrl(url) && !apiKey) {
741
+ // API key resolution order: --api-key (already in `apiKey`) →
742
+ // $MEKO_API_KEY → browser sign-in (Cloud only).
743
+ //
744
+ // $MEKO_API_KEY applies to every URL, local ones included: Meko Local
745
+ // (the `meko` server, MCP on 127.0.0.1:7778) requires its client token,
746
+ // and AgentK passes that token only through the environment so it stays
747
+ // out of `ps`. Only the keyless dev stack (see `keylessLocal`) skips it.
748
+ if (!apiKey && !keylessLocal) {
673
749
  apiKey = normalizeApiKey(process.env.MEKO_API_KEY);
674
750
  if (apiKey) {
675
751
  dim('Using API key from MEKO_API_KEY environment variable.');
676
- } else if (!flags.yes && !promptedForApiKey && process.stdin.isTTY) {
677
- dim('No --api-key or MEKO_API_KEY env var found; prompting for API key.');
678
- apiKey = normalizeApiKey(await askSecret('API key:'));
752
+ }
753
+ }
754
+
755
+ // Validation — Cloud URL requires an API key. Browser sign-in creates one
756
+ // through the server's POST /api-keys. Sign-in also covers the cloud
757
+ // one-liner from the UI, which only shows --url and --client. It is
758
+ // skipped when the full interactive flow already took a pasted key (an
759
+ // empty paste is an error, not a silent switch to the browser), and
760
+ // whenever stdin is not a terminal, with or without --yes: a script or CI
761
+ // step may have nobody at the browser, and a 5-minute callback wait would
762
+ // stall it. The one exception is an explicit "Sign in with browser" answer
763
+ // at the prompt. A local URL with no key installs without one.
764
+ if (!isLocalUrl(url) && !apiKey) {
765
+ let signInFailed = false;
766
+ if (!promptedForApiKey && (process.stdin.isTTY || choseBrowserSignIn)) {
767
+ apiKey = await signInWithBrowser({
768
+ url,
769
+ noBrowser: flags['no-browser'],
770
+ dryRun: flags['dry-run'],
771
+ });
772
+ signInFailed = !apiKey;
679
773
  }
680
774
  if (!apiKey) {
681
- error(
682
- 'API key is required for Cloud Meko. ' +
683
- 'Provide --api-key, set MEKO_API_KEY env var, or run interactively.',
684
- );
775
+ let message;
776
+ if (signInFailed) {
777
+ message = `Browser sign-in did not complete. To use an existing key instead, create one at ${consoleUrlFor(url)} > Settings > API Keys and rerun with --api-key, or set MEKO_API_KEY.`;
778
+ } else if (promptedForApiKey) {
779
+ message = 'No API key entered. Rerun and choose Sign in with browser, or pass --api-key.';
780
+ } else {
781
+ message = 'API key is required for Cloud Meko. ' +
782
+ 'Provide --api-key, set MEKO_API_KEY env var, or run in a terminal to sign in with your browser.';
783
+ }
784
+ error(message);
685
785
  process.exit(1);
686
786
  }
687
787
  }
@@ -716,6 +816,23 @@ async function main() {
716
816
  }
717
817
  }
718
818
 
819
+ // Automatic capture repair runs inside the capture timer the hooks start,
820
+ // so installing the hooks deploys it. Save --autofix / --no-autofix once,
821
+ // for every client (they share one capture directory).
822
+ if (errors.length < clients.length) {
823
+ if (flags.autofix && flags['no-autofix']) {
824
+ warn('Both --autofix and --no-autofix were given; keeping the current setting.');
825
+ }
826
+ const enable =
827
+ flags['no-autofix'] && !flags.autofix ? false : flags.autofix && !flags['no-autofix'] ? true : undefined;
828
+ try {
829
+ const res = applyAutofixSetting({ enable, dryRun: flags['dry-run'], projectRoot });
830
+ info(describeAutofix(res.enabled));
831
+ } catch (err) {
832
+ warn(`Could not save the automatic repair setting: ${err.message}`);
833
+ }
834
+ }
835
+
719
836
  if (errors.length === clients.length) {
720
837
  process.exit(1);
721
838
  } else if (errors.length > 0) {
@@ -0,0 +1,100 @@
1
+ /**
2
+ * autofix.mjs: the installer side of automatic capture repair.
3
+ *
4
+ * The checkpoint timer runs `--capture-cleanup fix --auto` every
5
+ * MEKO_AUTOFIX_INTERVAL seconds. The on/off choice lives in
6
+ * autofix-config.json in the capture directory, which capture-state.js
7
+ * autofixSetting() reads. MEKO_AUTOFIX in the hook environment overrides it.
8
+ *
9
+ * Most clients share one capture directory, but OpenCode hooks keep the one
10
+ * saved in their hook env file at first install, so the file is written to
11
+ * every capture directory the installer knows about.
12
+ */
13
+
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { dirname, isAbsolute, join, resolve } from "node:path";
16
+
17
+ import { getCaptureRoot, getOpenCodeHookEnvPath } from "./config.mjs";
18
+
19
+ const CONFIG_FILE = "autofix-config.json";
20
+
21
+ function savedOpenCodeRoot(scope, projectRoot) {
22
+ try {
23
+ const file = getOpenCodeHookEnvPath(scope, projectRoot);
24
+ if (!existsSync(file)) return null;
25
+ const value = JSON.parse(readFileSync(file, "utf8")).MEKO_WATERMARK_DIR;
26
+ if (typeof value !== "string" || !value) return null;
27
+ return isAbsolute(value) ? value : resolve(value);
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /** Every capture directory a client's timer may read the setting from. */
34
+ export function captureRoots({ projectRoot = process.cwd() } = {}) {
35
+ const roots = [getCaptureRoot()];
36
+ for (const scope of ["user", "project"]) {
37
+ const saved = savedOpenCodeRoot(scope, projectRoot);
38
+ if (saved && !roots.includes(saved)) roots.push(saved);
39
+ }
40
+ return roots;
41
+ }
42
+
43
+ export function autofixConfigPath() {
44
+ return join(getCaptureRoot(), CONFIG_FILE);
45
+ }
46
+
47
+ /** @returns {boolean|null} the saved choice in the main capture directory, or null. */
48
+ export function readAutofixSetting() {
49
+ try {
50
+ const cfg = JSON.parse(readFileSync(autofixConfigPath(), "utf8"));
51
+ return typeof cfg.enabled === "boolean" ? cfg.enabled : null;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Save --autofix / --no-autofix in every capture directory. `enable`
59
+ * undefined keeps an earlier choice, so a plain reinstall doesn't turn
60
+ * repair back on.
61
+ *
62
+ * @param {{ enable?: boolean, dryRun?: boolean, projectRoot?: string }} opts
63
+ * @returns {{ enabled: boolean, changed: boolean, paths: string[] }}
64
+ */
65
+ export function applyAutofixSetting({ enable, dryRun = false, projectRoot } = {}) {
66
+ const saved = readAutofixSetting();
67
+ const paths = captureRoots({ projectRoot }).map((root) => join(root, CONFIG_FILE));
68
+ if (enable === undefined) {
69
+ return { enabled: saved === null ? true : saved, changed: false, paths };
70
+ }
71
+ if (!dryRun) {
72
+ for (const file of paths) {
73
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
74
+ writeFileSync(
75
+ file,
76
+ `${JSON.stringify({ enabled: enable, updated_at: new Date().toISOString() })}\n`,
77
+ { mode: 0o600 },
78
+ );
79
+ }
80
+ }
81
+ return { enabled: enable, changed: saved !== enable, paths };
82
+ }
83
+
84
+ /** Same rule as capture-state.js autofixIntervalSeconds(). */
85
+ function intervalSeconds(env) {
86
+ const n = Number.parseInt(env.MEKO_AUTOFIX_INTERVAL || "", 10);
87
+ return Math.max(60, Number.isFinite(n) ? n : 1800);
88
+ }
89
+
90
+ /** One line for the end of an install. */
91
+ export function describeAutofix(enabled, env = process.env) {
92
+ const envOverride = env.MEKO_AUTOFIX;
93
+ if (envOverride != null && envOverride !== "") {
94
+ return `Automatic capture repair follows MEKO_AUTOFIX=${envOverride} in this environment.`;
95
+ }
96
+ const minutes = Math.max(1, Math.round(intervalSeconds(env) / 60));
97
+ return enabled
98
+ ? `Automatic capture repair is on: every ${minutes} minute${minutes === 1 ? "" : "s"} the capture timer fixes stuck sessions (--capture-cleanup fix). Turn it off with --no-autofix.`
99
+ : "Automatic capture repair is off. Turn it on with --autofix, or run --capture-cleanup fix yourself.";
100
+ }