@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.
- package/README.md +88 -35
- package/assets/hooks-handlers/lib/capture-cleanup-cli.js +735 -74
- package/assets/hooks-handlers/lib/capture-notices.js +94 -1
- package/assets/hooks-handlers/lib/capture-state.js +111 -0
- package/assets/hooks-handlers/lib/capture.js +130 -4
- package/assets/hooks-handlers/lib/checkpoint-timer.js +394 -8
- package/assets/hooks-handlers/lib/opencode-handler.js +29 -6
- package/assets/scripts/meko-statusline.sh +17 -1
- package/assets/skills/meko-mcp-tools/SKILL.md +6 -5
- package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +194 -10
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +6 -3
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +11 -6
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +4 -4
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +19 -14
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +69 -8
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +3 -1
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +8 -7
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +4 -4
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +194 -10
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +10 -5
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +4 -4
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +19 -14
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +68 -8
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +3 -1
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +91 -12
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +39 -7
- package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +8 -6
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +13 -1
- package/bin/create-meko-setup.mjs +152 -35
- package/lib/autofix.mjs +100 -0
- package/lib/browser-login.mjs +495 -0
- package/lib/clients/codex.mjs +1 -24
- package/lib/clients/common.mjs +25 -0
- package/lib/clients/opencode.mjs +6 -1
- package/lib/migrate/datapack.mjs +254 -16
- package/lib/migrate/mcp-client.mjs +34 -0
- package/lib/migrate/orchestrator.mjs +1 -1
- package/lib/migrate.mjs +2 -6
- package/lib/validate.mjs +111 -22
- 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 (
|
|
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
|
|
135
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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 —
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
apiKey
|
|
627
|
-
|
|
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 =
|
|
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
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
|
|
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
|
-
}
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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) {
|
package/lib/autofix.mjs
ADDED
|
@@ -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
|
+
}
|