@yugabytedb/meko-mcp 2.3.0 → 2.5.0
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 +72 -33
- 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 +129 -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 +5 -5
- package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +193 -9
- 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 +16 -11
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +67 -8
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +3 -1
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +7 -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 +193 -9
- 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 +16 -11
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +67 -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 +117 -24
- package/lib/autofix.mjs +100 -0
- package/lib/browser-login.mjs +495 -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 +1 -1
- package/lib/validate.mjs +111 -22
- package/package.json +5 -5
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
"use strict";
|
|
19
19
|
|
|
20
|
-
const { execFile } = require("child_process");
|
|
20
|
+
const { execFile, spawn } = require("child_process");
|
|
21
21
|
const fs = require("fs");
|
|
22
22
|
const path = require("path");
|
|
23
23
|
const crypto = require("crypto");
|
|
@@ -39,6 +39,12 @@ const {
|
|
|
39
39
|
legacyErrorsSafeToRemove,
|
|
40
40
|
isResumableState,
|
|
41
41
|
readState,
|
|
42
|
+
atomicWriteJson,
|
|
43
|
+
autofixEnabled,
|
|
44
|
+
autofixIntervalSeconds,
|
|
45
|
+
autofixLastPath,
|
|
46
|
+
readAutofixLast,
|
|
47
|
+
envInt,
|
|
42
48
|
} = require("./capture-state");
|
|
43
49
|
|
|
44
50
|
const INTERVAL_SECS = Math.max(1, checkpointInterval());
|
|
@@ -47,6 +53,23 @@ const MAX_AGE_SECS = Math.max(1, timerMaxAge());
|
|
|
47
53
|
const IDLE_EXIT_SECS = parseInt(process.env.MEKO_TIMER_IDLE_EXIT, 10) || 7200;
|
|
48
54
|
const STARTUP_GRACE_SECS =
|
|
49
55
|
parseInt(process.env.MEKO_TIMER_STARTUP_GRACE, 10) || 1800;
|
|
56
|
+
const FOLLOWUP_SECS =
|
|
57
|
+
parseInt(process.env.MEKO_TIMER_FOLLOWUP_INTERVAL, 10) || 30;
|
|
58
|
+
|
|
59
|
+
// Automatic repair: one `--capture-cleanup fix --auto` run per machine every
|
|
60
|
+
// MEKO_AUTOFIX_INTERVAL seconds (default 1800). Any timer may run it; a lock
|
|
61
|
+
// file keeps it to one at a time.
|
|
62
|
+
const AUTOFIX_CHECK_SECS = Math.max(1, envInt("MEKO_AUTOFIX_CHECK_INTERVAL", 60));
|
|
63
|
+
const AUTOFIX_FIRST_CHECK_SECS = Math.max(1, envInt("MEKO_AUTOFIX_FIRST_CHECK", 30));
|
|
64
|
+
const AUTOFIX_TIMEOUT_MS = Math.max(60, envInt("MEKO_AUTOFIX_TIMEOUT", 900)) * 1000;
|
|
65
|
+
const AUTOFIX_MAX_EXCHANGES = Math.max(1, envInt("MEKO_AUTOFIX_MAX_EXCHANGES", 100));
|
|
66
|
+
const cleanupScript = path.join(__dirname, "capture-cleanup-cli.js");
|
|
67
|
+
let autofixInFlight = false;
|
|
68
|
+
// The running automatic-repair child and its lock, so cleanup() can stop it:
|
|
69
|
+
// the timer exits through process.exit, which skips runAutofix's finally.
|
|
70
|
+
let autofixChild = null;
|
|
71
|
+
let autofixLockPath = null;
|
|
72
|
+
let exiting = false;
|
|
50
73
|
|
|
51
74
|
const startedAt = Date.now();
|
|
52
75
|
const STATE_DIR = watermarkDir();
|
|
@@ -153,7 +176,107 @@ function cleanup() {
|
|
|
153
176
|
} catch {
|
|
154
177
|
/* already gone */
|
|
155
178
|
}
|
|
156
|
-
|
|
179
|
+
if (exiting) return;
|
|
180
|
+
exiting = true;
|
|
181
|
+
// Stopping an automatic run waits for it to exit, which needs the event
|
|
182
|
+
// loop (a synchronous wait would leave it a zombie that still looks alive).
|
|
183
|
+
stopAutofixChild()
|
|
184
|
+
.catch(() => {})
|
|
185
|
+
.finally(() => process.exit(0));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function groupAlive(pgid) {
|
|
189
|
+
if (!Number.isInteger(pgid) || pgid <= 0) return false;
|
|
190
|
+
try {
|
|
191
|
+
process.kill(-pgid, 0);
|
|
192
|
+
return true;
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return Boolean(err && err.code === "EPERM");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function signalGroup(pgid, signal) {
|
|
199
|
+
try {
|
|
200
|
+
process.kill(-pgid, signal);
|
|
201
|
+
} catch {
|
|
202
|
+
/* already gone */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Stop an in-flight automatic repair: its whole process group, so the
|
|
210
|
+
* capture.js child the cleanup CLI is blocked on stops too. The lock is
|
|
211
|
+
* released only once the group is gone; until then it names the run's pid,
|
|
212
|
+
* so no other timer starts a second run while this one is still sending.
|
|
213
|
+
*/
|
|
214
|
+
async function stopAutofixChild() {
|
|
215
|
+
const child = autofixChild;
|
|
216
|
+
if (child && child.exitCode === null && child.signalCode === null) {
|
|
217
|
+
const closed = new Promise((r) => child.once("close", r));
|
|
218
|
+
signalGroup(child.pid, "SIGTERM");
|
|
219
|
+
const exited = await Promise.race([closed.then(() => true), sleep(3000).then(() => false)]);
|
|
220
|
+
if (!exited) {
|
|
221
|
+
signalGroup(child.pid, "SIGKILL");
|
|
222
|
+
await Promise.race([closed, sleep(1000)]);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (child) {
|
|
226
|
+
// The rest of the group (capture.js) was re-parented when the CLI died.
|
|
227
|
+
for (let waited = 0; waited < 3000 && groupAlive(child.pid); waited += 100) await sleep(100);
|
|
228
|
+
if (groupAlive(child.pid)) signalGroup(child.pid, "SIGKILL");
|
|
229
|
+
}
|
|
230
|
+
releaseAutofixLock(child ? child.pid : null);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function releaseAutofixLock(runPid) {
|
|
234
|
+
if (!autofixLockPath) return;
|
|
235
|
+
if (runPid && groupAlive(runPid)) return;
|
|
236
|
+
try {
|
|
237
|
+
const held = JSON.parse(fs.readFileSync(autofixLockPath, "utf8"));
|
|
238
|
+
if (Number(held.pid) === process.pid) fs.unlinkSync(autofixLockPath);
|
|
239
|
+
} catch {
|
|
240
|
+
/* already gone */
|
|
241
|
+
}
|
|
242
|
+
autofixLockPath = null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// MEKO-918: an OpenCode timer outlives edits to its hook env file. Without a
|
|
246
|
+
// re-read, a timer started during an outage sends every attempt to the dead
|
|
247
|
+
// URL until it exits: MEKO_TIMER_IDLE_EXIT after the session's last turn, or
|
|
248
|
+
// MEKO_TIMER_MAX_AGE (24 h) after it started.
|
|
249
|
+
//
|
|
250
|
+
// The installer always writes MEKO_MCP_URL and MEKO_API_KEY, so a file that
|
|
251
|
+
// lacks them was emptied by uninstall or by hand. Drop the inherited values
|
|
252
|
+
// then, so the timer stops sending with removed credentials. A file that
|
|
253
|
+
// can't be parsed keeps the inherited environment.
|
|
254
|
+
const RELOADED_CREDENTIAL_KEYS = ["MEKO_MCP_URL", "MEKO_API_KEY"];
|
|
255
|
+
|
|
256
|
+
function captureChildEnv() {
|
|
257
|
+
if (
|
|
258
|
+
process.env.MEKO_HOOK_CLIENT !== "opencode" ||
|
|
259
|
+
!process.env.MEKO_OPENCODE_ENV_FILE
|
|
260
|
+
) {
|
|
261
|
+
return process.env;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const { status, set, unset } = require("./opencode-handler").readHookEnvironment(
|
|
265
|
+
process.env.MEKO_OPENCODE_ENV_FILE,
|
|
266
|
+
);
|
|
267
|
+
if (status === "unreadable") return process.env;
|
|
268
|
+
const env = { ...process.env, ...set };
|
|
269
|
+
for (const key of unset) delete env[key];
|
|
270
|
+
for (const key of RELOADED_CREDENTIAL_KEYS) {
|
|
271
|
+
if (!(key in set)) delete env[key];
|
|
272
|
+
}
|
|
273
|
+
return env;
|
|
274
|
+
} catch (err) {
|
|
275
|
+
process.stderr.write(
|
|
276
|
+
`[meko-timer] Could not reload OpenCode hook environment: ${err.message}\n`,
|
|
277
|
+
);
|
|
278
|
+
return process.env;
|
|
279
|
+
}
|
|
157
280
|
}
|
|
158
281
|
|
|
159
282
|
function runCaptureChild(mode, payload, timeoutMs) {
|
|
@@ -161,7 +284,7 @@ function runCaptureChild(mode, payload, timeoutMs) {
|
|
|
161
284
|
const child = execFile(
|
|
162
285
|
process.execPath,
|
|
163
286
|
[captureScript, mode],
|
|
164
|
-
{ timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024 },
|
|
287
|
+
{ timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024, env: captureChildEnv() },
|
|
165
288
|
(err, stdout, stderr) => {
|
|
166
289
|
let result = null;
|
|
167
290
|
const text = (stdout || "").trim();
|
|
@@ -264,7 +387,9 @@ function refreshStartupPersistentQueue(scan, forcePersistentRetry) {
|
|
|
264
387
|
* Returns true if more recoverable work may remain for a later cycle.
|
|
265
388
|
*/
|
|
266
389
|
async function runRecoveryCycle() {
|
|
267
|
-
|
|
390
|
+
// Once cleanup() starts, start no new capture.js child: it would outlive
|
|
391
|
+
// this timer's exit.
|
|
392
|
+
if (recoveryInFlight || exiting) return workRemains;
|
|
268
393
|
recoveryInFlight = true;
|
|
269
394
|
const forcePersistentRetry = startupRecoveryPending;
|
|
270
395
|
let moreWork = false;
|
|
@@ -399,10 +524,64 @@ function selfIsResumable() {
|
|
|
399
524
|
return read.ok ? isResumableState(read.state) : false;
|
|
400
525
|
}
|
|
401
526
|
|
|
527
|
+
// MEKO-918: recovery skips the daemon's own session, so without this check a
|
|
528
|
+
// failed live drain waits for the next full tick (600 s by default) instead
|
|
529
|
+
// of retrying at next_retry_at. Each scheduled retry runs once: a checkpoint
|
|
530
|
+
// that exits before the network leaves next_retry_at unchanged.
|
|
531
|
+
//
|
|
532
|
+
// The retry has its own in-flight promise rather than tickInFlight. A tick
|
|
533
|
+
// that fires during a retry waits for it and then runs, so the exit checks
|
|
534
|
+
// and recovery cycle are not skipped for a whole interval.
|
|
535
|
+
let ownRetryAttemptedFor = null;
|
|
536
|
+
let ownRetryInFlight = null;
|
|
537
|
+
|
|
538
|
+
function ownRetryDue(nowMs) {
|
|
539
|
+
const read = readState(sessionId);
|
|
540
|
+
if (!read.ok) return false;
|
|
541
|
+
const state = read.state;
|
|
542
|
+
// needs_conversation counts: on a watermark without a conversation_id,
|
|
543
|
+
// writeSessionIntent resets a failed attempt's retry_wait to
|
|
544
|
+
// needs_conversation and keeps next_retry_at. OpenCode does this on every
|
|
545
|
+
// snapshot (ensureCaptureIntent).
|
|
546
|
+
if (["idle", "complete", "draining"].includes(state.delivery)) return false;
|
|
547
|
+
if (!(Number(state.queued_exchanges || 0) > 0)) return false;
|
|
548
|
+
const retryMs = Date.parse(state.next_retry_at || "");
|
|
549
|
+
if (!Number.isFinite(retryMs) || retryMs > nowMs) return false;
|
|
550
|
+
return state.next_retry_at !== ownRetryAttemptedFor;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function retryOwnSession() {
|
|
554
|
+
if (exiting || tickInFlight || recoveryInFlight || ownRetryInFlight) {
|
|
555
|
+
return Promise.resolve();
|
|
556
|
+
}
|
|
557
|
+
ownRetryInFlight = runOwnRetry().finally(() => {
|
|
558
|
+
ownRetryInFlight = null;
|
|
559
|
+
});
|
|
560
|
+
return ownRetryInFlight;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function runOwnRetry() {
|
|
564
|
+
const read = readState(sessionId);
|
|
565
|
+
ownRetryAttemptedFor = read.ok ? read.state.next_retry_at : null;
|
|
566
|
+
const live = await runCaptureChild(
|
|
567
|
+
"checkpoint",
|
|
568
|
+
{ transcript_path: transcriptPath, force_retry: false },
|
|
569
|
+
CAPTURE_TIMEOUT_MS,
|
|
570
|
+
);
|
|
571
|
+
if (!live.ok) {
|
|
572
|
+
process.stderr.write(
|
|
573
|
+
`[meko-timer] Own-session retry failed: ${live.error}\n`,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
rebuildHealth();
|
|
577
|
+
}
|
|
578
|
+
|
|
402
579
|
async function tick() {
|
|
403
|
-
if (tickInFlight) return;
|
|
580
|
+
if (tickInFlight || exiting) return;
|
|
404
581
|
tickInFlight = true;
|
|
405
582
|
try {
|
|
583
|
+
// Never run two checkpoints of this session at once.
|
|
584
|
+
if (ownRetryInFlight) await ownRetryInFlight.catch(() => {});
|
|
406
585
|
if (Date.now() - startedAt >= MAX_AGE_SECS * 1000) {
|
|
407
586
|
process.stderr.write(
|
|
408
587
|
`[meko-timer] Max age (${MAX_AGE_SECS}s) reached; self-exiting (outbox left pending).\n`,
|
|
@@ -500,6 +679,192 @@ async function tick() {
|
|
|
500
679
|
}
|
|
501
680
|
}
|
|
502
681
|
|
|
682
|
+
function autofixDue(nowMs) {
|
|
683
|
+
if (!autofixEnabled() || !fs.existsSync(cleanupScript)) return false;
|
|
684
|
+
const last = readAutofixLast();
|
|
685
|
+
const ranAt = last ? Date.parse(last.ran_at) : NaN;
|
|
686
|
+
return !Number.isFinite(ranAt) || nowMs - ranAt >= autofixIntervalSeconds() * 1000;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function acquireAutofixLock(nowMs) {
|
|
690
|
+
const lockPath = path.join(STATE_DIR, "autofix.lock");
|
|
691
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
692
|
+
try {
|
|
693
|
+
fs.writeFileSync(
|
|
694
|
+
lockPath,
|
|
695
|
+
JSON.stringify({ pid: process.pid, at: new Date(nowMs).toISOString() }),
|
|
696
|
+
{ flag: "wx", mode: 0o600 },
|
|
697
|
+
);
|
|
698
|
+
return lockPath;
|
|
699
|
+
} catch (err) {
|
|
700
|
+
if (!err || err.code !== "EEXIST") return null;
|
|
701
|
+
let held = null;
|
|
702
|
+
try {
|
|
703
|
+
held = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
704
|
+
} catch {
|
|
705
|
+
held = null;
|
|
706
|
+
}
|
|
707
|
+
const heldAt = held ? Date.parse(held.at) : NaN;
|
|
708
|
+
// Stale only when neither the timer that took it nor the run it
|
|
709
|
+
// started is alive: a run outlives its timer if the timer is killed.
|
|
710
|
+
const holderAlive = held && (isProcessAlive(Number(held.pid)) || groupAlive(Number(held.run_pid)));
|
|
711
|
+
const stale =
|
|
712
|
+
!held ||
|
|
713
|
+
!holderAlive ||
|
|
714
|
+
!Number.isFinite(heldAt) ||
|
|
715
|
+
nowMs - heldAt > AUTOFIX_TIMEOUT_MS * 2;
|
|
716
|
+
if (!stale) return null;
|
|
717
|
+
try {
|
|
718
|
+
fs.unlinkSync(lockPath);
|
|
719
|
+
} catch {
|
|
720
|
+
/* another timer removed it first */
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function summarizeAutofix(stdout, err) {
|
|
728
|
+
let report = null;
|
|
729
|
+
try {
|
|
730
|
+
report = JSON.parse(stdout);
|
|
731
|
+
} catch {
|
|
732
|
+
report = null;
|
|
733
|
+
}
|
|
734
|
+
const errors = report && Array.isArray(report.errors) ? report.errors : [];
|
|
735
|
+
const sessions = report && Array.isArray(report.sessions) ? report.sessions : [];
|
|
736
|
+
const parts = [];
|
|
737
|
+
if (sessions.length) {
|
|
738
|
+
const byOutcome = {};
|
|
739
|
+
for (const x of sessions) byOutcome[x.outcome] = (byOutcome[x.outcome] || 0) + 1;
|
|
740
|
+
parts.push(Object.entries(byOutcome).map(([k, v]) => `${k}=${v}`).join(" "));
|
|
741
|
+
}
|
|
742
|
+
if (errors.length) parts.push(`errors: ${errors.join("; ")}`);
|
|
743
|
+
if (!report) parts.push(err ? `failed: ${err.message}` : "failed: no report");
|
|
744
|
+
else if (err && !errors.length) parts.push(`exit ${err.code != null ? err.code : "?"}`);
|
|
745
|
+
return parts.length ? parts.join("; ") : "nothing to fix";
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* A run that crashed or timed out never wrote its own summary. Replace the
|
|
750
|
+
* placeholder so status and the status bar don't show a run that never
|
|
751
|
+
* finished, or the previous run's needs_you list as current.
|
|
752
|
+
*/
|
|
753
|
+
function finishAutofixPlaceholder(error) {
|
|
754
|
+
const last = readAutofixLast();
|
|
755
|
+
if (!last || !last.running) return;
|
|
756
|
+
try {
|
|
757
|
+
atomicWriteJson(autofixLastPath(), {
|
|
758
|
+
...last,
|
|
759
|
+
running: false,
|
|
760
|
+
error,
|
|
761
|
+
counts: {},
|
|
762
|
+
needs_you: [],
|
|
763
|
+
});
|
|
764
|
+
} catch {
|
|
765
|
+
/* informational */
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function runAutofix() {
|
|
770
|
+
if (autofixInFlight || exiting) return;
|
|
771
|
+
const now = Date.now();
|
|
772
|
+
if (!autofixDue(now)) return;
|
|
773
|
+
// A client whose hook credentials were removed has nothing to repair with.
|
|
774
|
+
const childEnv = { ...captureChildEnv(), MEKO_CLEANUP_ENV_ONLY: "1" };
|
|
775
|
+
if (!childEnv.MEKO_MCP_URL) return;
|
|
776
|
+
const lockPath = acquireAutofixLock(now);
|
|
777
|
+
if (!lockPath) return;
|
|
778
|
+
autofixLockPath = lockPath;
|
|
779
|
+
autofixInFlight = true;
|
|
780
|
+
try {
|
|
781
|
+
// Another timer may have finished a run between the check and the lock.
|
|
782
|
+
if (!autofixDue(Date.now())) return;
|
|
783
|
+
// Record the attempt first: if the run crashes, the next one still
|
|
784
|
+
// waits a full interval instead of every timer retrying every minute.
|
|
785
|
+
const previous = readAutofixLast() || {};
|
|
786
|
+
try {
|
|
787
|
+
atomicWriteJson(autofixLastPath(), {
|
|
788
|
+
schema_version: 1,
|
|
789
|
+
ran_at: new Date().toISOString(),
|
|
790
|
+
mode: "auto",
|
|
791
|
+
running: true,
|
|
792
|
+
counts: {},
|
|
793
|
+
needs_you: Array.isArray(previous.needs_you) ? previous.needs_you : [],
|
|
794
|
+
});
|
|
795
|
+
} catch {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
await new Promise((resolve) => {
|
|
799
|
+
// Its own process group (execFile ignores `detached`), so stopping it
|
|
800
|
+
// also stops the capture.js child it is blocked on.
|
|
801
|
+
const child = spawn(
|
|
802
|
+
process.execPath,
|
|
803
|
+
[
|
|
804
|
+
cleanupScript,
|
|
805
|
+
"fix",
|
|
806
|
+
"--auto",
|
|
807
|
+
"--json",
|
|
808
|
+
"--max-exchanges",
|
|
809
|
+
String(AUTOFIX_MAX_EXCHANGES),
|
|
810
|
+
"--pace-ms",
|
|
811
|
+
"500",
|
|
812
|
+
],
|
|
813
|
+
{ env: childEnv, detached: true, stdio: ["ignore", "pipe", "ignore"] },
|
|
814
|
+
);
|
|
815
|
+
autofixChild = child;
|
|
816
|
+
let stdout = "";
|
|
817
|
+
child.stdout.on("data", (chunk) => {
|
|
818
|
+
if (stdout.length < 16 * 1024 * 1024) stdout += chunk;
|
|
819
|
+
});
|
|
820
|
+
let timedOut = false;
|
|
821
|
+
const timer = setTimeout(() => {
|
|
822
|
+
timedOut = true;
|
|
823
|
+
signalGroup(child.pid, "SIGTERM");
|
|
824
|
+
setTimeout(() => signalGroup(child.pid, "SIGKILL"), 3000).unref();
|
|
825
|
+
}, AUTOFIX_TIMEOUT_MS);
|
|
826
|
+
// Record the run in the lock, so the lock isn't taken as stale while
|
|
827
|
+
// the run is alive.
|
|
828
|
+
try {
|
|
829
|
+
atomicWriteJson(lockPath, { pid: process.pid, run_pid: child.pid, at: new Date(now).toISOString() });
|
|
830
|
+
} catch {
|
|
831
|
+
/* the lock still names this timer */
|
|
832
|
+
}
|
|
833
|
+
let finished = false;
|
|
834
|
+
const finish = (err) => {
|
|
835
|
+
if (finished) return;
|
|
836
|
+
finished = true;
|
|
837
|
+
clearTimeout(timer);
|
|
838
|
+
const failure = timedOut
|
|
839
|
+
? new Error(`timed out after ${AUTOFIX_TIMEOUT_MS / 1000}s`)
|
|
840
|
+
: err;
|
|
841
|
+
// Anything left in the group (capture.js) after the CLI failed.
|
|
842
|
+
if (failure) signalGroup(child.pid, "SIGTERM");
|
|
843
|
+
const summary = summarizeAutofix(stdout, failure);
|
|
844
|
+
if (failure) finishAutofixPlaceholder(summary);
|
|
845
|
+
process.stderr.write(`[meko-timer] Automatic repair: ${summary}\n`);
|
|
846
|
+
resolve();
|
|
847
|
+
};
|
|
848
|
+
child.on("error", (err) => finish(err));
|
|
849
|
+
child.on("close", (code, signal) => {
|
|
850
|
+
if (code === 0 || code === 2) return finish(null);
|
|
851
|
+
const err = new Error(signal ? `stopped by ${signal}` : `exit ${code}`);
|
|
852
|
+
err.code = code;
|
|
853
|
+
finish(err);
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
} finally {
|
|
857
|
+
autofixInFlight = false;
|
|
858
|
+
const runPid = autofixChild ? autofixChild.pid : null;
|
|
859
|
+
autofixChild = null;
|
|
860
|
+
if (!exiting) {
|
|
861
|
+
// Wait for any capture.js left in the run's group before releasing.
|
|
862
|
+
for (let waited = 0; runPid && waited < 3000 && groupAlive(runPid); waited += 100) await sleep(100);
|
|
863
|
+
releaseAutofixLock(runPid);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
503
868
|
if (!acquirePidFile()) {
|
|
504
869
|
process.exit(0);
|
|
505
870
|
}
|
|
@@ -518,13 +883,34 @@ setInterval(() => {
|
|
|
518
883
|
});
|
|
519
884
|
}, INTERVAL_SECS * 1000);
|
|
520
885
|
|
|
886
|
+
// Automatic repair check, once a minute; the first one runs shortly after
|
|
887
|
+
// startup so a stuck machine is repaired without waiting a full interval.
|
|
888
|
+
setTimeout(() => {
|
|
889
|
+
runAutofix().catch((err) => {
|
|
890
|
+
process.stderr.write(`[meko-timer] Automatic repair failed: ${err.message}\n`);
|
|
891
|
+
});
|
|
892
|
+
}, AUTOFIX_FIRST_CHECK_SECS * 1000).unref();
|
|
893
|
+
setInterval(() => {
|
|
894
|
+
runAutofix().catch((err) => {
|
|
895
|
+
process.stderr.write(`[meko-timer] Automatic repair failed: ${err.message}\n`);
|
|
896
|
+
});
|
|
897
|
+
}, AUTOFIX_CHECK_SECS * 1000);
|
|
898
|
+
|
|
521
899
|
// While backlog remains, also schedule a shorter recovery follow-up without
|
|
522
|
-
// waiting for the full checkpoint interval (self-draining).
|
|
900
|
+
// waiting for the full checkpoint interval (self-draining). The same loop
|
|
901
|
+
// retries this daemon's own session once its next_retry_at passes.
|
|
523
902
|
setInterval(() => {
|
|
524
|
-
if (
|
|
903
|
+
if (exiting || recoveryInFlight || tickInFlight || ownRetryInFlight) return;
|
|
904
|
+
if (ownRetryDue(Date.now())) {
|
|
905
|
+
retryOwnSession().catch((err) => {
|
|
906
|
+
process.stderr.write(`[meko-timer] Own-session retry failed: ${err.message}\n`);
|
|
907
|
+
});
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (!workRemains) return;
|
|
525
911
|
runRecoveryCycle().catch((err) => {
|
|
526
912
|
process.stderr.write(
|
|
527
913
|
`[meko-timer] Follow-up recovery failed: ${err.message}\n`,
|
|
528
914
|
);
|
|
529
915
|
});
|
|
530
|
-
}, Math.min(
|
|
916
|
+
}, Math.min(FOLLOWUP_SECS, INTERVAL_SECS) * 1000);
|
|
@@ -12,8 +12,17 @@ const DEFAULT_RECOVERY_MAX_TIMERS = 16;
|
|
|
12
12
|
function captureLib() {
|
|
13
13
|
return require("./capture");
|
|
14
14
|
}
|
|
15
|
-
function
|
|
16
|
-
|
|
15
|
+
function hookEnvPath(filePath) {
|
|
16
|
+
return filePath || process.env.MEKO_OPENCODE_ENV_FILE || DEFAULT_ENV_PATH;
|
|
17
|
+
}
|
|
18
|
+
// Read the env file without touching process.env. The checkpoint timer calls
|
|
19
|
+
// this before each capture child so an edited URL or key takes effect.
|
|
20
|
+
// status is "loaded", "missing" (ENOENT), or "unreadable".
|
|
21
|
+
function readHookEnvironment(filePath) {
|
|
22
|
+
const target = hookEnvPath(filePath);
|
|
23
|
+
const set = {};
|
|
24
|
+
const unset = [];
|
|
25
|
+
let status = "loaded";
|
|
17
26
|
try {
|
|
18
27
|
const parsed = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
19
28
|
for (const key of [
|
|
@@ -26,16 +35,25 @@ function loadHookEnvironment(filePath) {
|
|
|
26
35
|
"MEKO_DATAPACK_NAME",
|
|
27
36
|
]) {
|
|
28
37
|
if (typeof parsed[key] === "string" && parsed[key] !== "") {
|
|
29
|
-
|
|
38
|
+
set[key] = parsed[key];
|
|
30
39
|
} else if (key === "MEKO_API_KEY" && parsed[key] === "") {
|
|
31
|
-
|
|
40
|
+
unset.push(key);
|
|
32
41
|
}
|
|
33
42
|
}
|
|
34
43
|
} catch (err) {
|
|
35
|
-
if (
|
|
44
|
+
if (err && err.code === "ENOENT") {
|
|
45
|
+
status = "missing";
|
|
46
|
+
} else {
|
|
47
|
+
status = "unreadable";
|
|
36
48
|
process.stderr.write(`[meko-opencode] Could not read hook environment: ${err.message}\n`);
|
|
37
49
|
}
|
|
38
50
|
}
|
|
51
|
+
return { target, status, set, unset };
|
|
52
|
+
}
|
|
53
|
+
function loadHookEnvironment(filePath) {
|
|
54
|
+
const { target, set, unset } = readHookEnvironment(filePath);
|
|
55
|
+
Object.assign(process.env, set);
|
|
56
|
+
for (const key of unset) delete process.env[key];
|
|
39
57
|
process.env.MEKO_HOOK_CLIENT = "opencode";
|
|
40
58
|
return target;
|
|
41
59
|
}
|
|
@@ -412,7 +430,11 @@ function launchRecoveryTimer(journalPath) {
|
|
|
412
430
|
detached: true,
|
|
413
431
|
windowsHide: true,
|
|
414
432
|
stdio: "ignore",
|
|
415
|
-
env: {
|
|
433
|
+
env: {
|
|
434
|
+
...process.env,
|
|
435
|
+
MEKO_HOOK_CLIENT: "opencode",
|
|
436
|
+
MEKO_OPENCODE_ENV_FILE: hookEnvPath(),
|
|
437
|
+
},
|
|
416
438
|
});
|
|
417
439
|
child.unref();
|
|
418
440
|
}
|
|
@@ -602,6 +624,7 @@ async function main() {
|
|
|
602
624
|
await handleStagedRequest(request);
|
|
603
625
|
}
|
|
604
626
|
module.exports = {
|
|
627
|
+
readHookEnvironment,
|
|
605
628
|
loadHookEnvironment,
|
|
606
629
|
eventPaths,
|
|
607
630
|
appendCanonicalExchanges,
|
|
@@ -197,7 +197,23 @@ fi
|
|
|
197
197
|
WATERMARK_DIR="$PIN_DIR"
|
|
198
198
|
HEALTH_FILE="${WATERMARK_DIR}/capture-health.json"
|
|
199
199
|
CAPTURE_SUFFIX=""
|
|
200
|
-
|
|
200
|
+
# Hooks from 2.5.0 store a ready-made "statusline" text in the health cache
|
|
201
|
+
# (statuslineText() in capture-notices.js): needs you, paused, retrying with
|
|
202
|
+
# the next automatic repair, auto-fixed. An empty value means show nothing.
|
|
203
|
+
# Older caches fall back to the status/queued suffix below.
|
|
204
|
+
HAVE_STATUSLINE_FIELD=0
|
|
205
|
+
if [[ -f "$HEALTH_FILE" ]] && grep -qE '"statusline"[[:space:]]*:' "$HEALTH_FILE"; then
|
|
206
|
+
HAVE_STATUSLINE_FIELD=1
|
|
207
|
+
CAPTURE_SUFFIX="$(grep -oE '"statusline"[[:space:]]*:[[:space:]]*"[^"]*"' "$HEALTH_FILE" | sed -E 's/^"statusline"[[:space:]]*:[[:space:]]*"(.*)"$/\1/' | head -n 1)"
|
|
208
|
+
# The text can carry an expiry: after statusline_until (epoch seconds),
|
|
209
|
+
# show statusline_after instead ("auto-fixed N" fades, "auto-fix at"
|
|
210
|
+
# becomes "auto-fix soon").
|
|
211
|
+
UNTIL="$(grep -oE '"statusline_until"[[:space:]]*:[[:space:]]*[0-9]+' "$HEALTH_FILE" | sed -E 's/.*:[[:space:]]*([0-9]+)$/\1/' | head -n 1)"
|
|
212
|
+
if [[ -n "$UNTIL" && "$(date +%s)" -gt "$UNTIL" ]]; then
|
|
213
|
+
CAPTURE_SUFFIX="$(grep -oE '"statusline_after"[[:space:]]*:[[:space:]]*"[^"]*"' "$HEALTH_FILE" | sed -E 's/^"statusline_after"[[:space:]]*:[[:space:]]*"(.*)"$/\1/' | head -n 1)"
|
|
214
|
+
fi
|
|
215
|
+
fi
|
|
216
|
+
if [[ "$HAVE_STATUSLINE_FIELD" -eq 0 && -f "$HEALTH_FILE" ]]; then
|
|
201
217
|
HEALTH_STATUS="$(grep -oE '"status"[[:space:]]*:[[:space:]]*"[^"]+"' "$HEALTH_FILE" | sed -E 's/.*"([^"]+)"$/\1/' | head -n 1)"
|
|
202
218
|
QUEUED="$(grep -oE '"queued_exchanges"[[:space:]]*:[[:space:]]*[0-9]+' "$HEALTH_FILE" | sed -E 's/.*:[[:space:]]*([0-9]+)$/\1/' | head -n 1)"
|
|
203
219
|
QUEUED="${QUEUED:-0}"
|
|
@@ -4,13 +4,13 @@ description: Behavioral guide for AI agents using Meko MCP tools. Triggers when
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: Meko
|
|
7
|
-
version: "3.
|
|
7
|
+
version: "3.4.0"
|
|
8
8
|
tags: mcp, tools, datapack, memory, conversation, rag, meko
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Meko MCP Tools — Agent Behavioral Guide
|
|
12
12
|
|
|
13
|
-
Meko is agent-native data infrastructure that enables continuous learning from context windows through collective memory and shared knowledge. This skill teaches you how to use Meko's
|
|
13
|
+
Meko is agent-native data infrastructure that enables continuous learning from context windows through collective memory and shared knowledge. This skill teaches you how to use Meko's 28 MCP tools available in production.
|
|
14
14
|
|
|
15
15
|
**Read this first.** On Claude Code, conversation capture is automatic (SessionStart / PreCompact / SessionEnd hooks) and the Meko server extracts durable memories from the captured turns on its own. Facts the user states in conversation are saved for you — you do **not** proactively call `memory_add` for them. Your active jobs are: **recall** what's already known (`context_search` first, then `memory_search` or `knowledgebase_search` for a single source), and the **few** save cases automatic extraction cannot reach. Details below.
|
|
16
16
|
|
|
@@ -101,8 +101,8 @@ Recall first; save only what capture cannot reach; never re-store a just-stated
|
|
|
101
101
|
| User asks "what do you know about X?" | `context_search`. One call returns separate memory, knowledge-base, and past-conversation buckets; report each. Confirm an empty memory or KB bucket with `memory_search` or `knowledgebase_search` before saying nothing is stored. |
|
|
102
102
|
| User asks "what do we (as a team) know about X?" or about uploaded documents | `knowledgebase_search` (returns uploaded docs + promoted memories). |
|
|
103
103
|
| User asks to share private memories | Follow the exact-ID `memory_promote` confirmation workflow below. |
|
|
104
|
-
| User provides structured/tabular data (CSV, data dictionary) |
|
|
105
|
-
| User wants to add documents to the team's knowledge base |
|
|
104
|
+
| User provides structured/tabular data (CSV, data dictionary) | If it's an actual file the user wants in the team's knowledge base, `knowledgebase_upload_document` accepts `.csv` directly (confirm first; see below). If it's pasted/described data with no file behind it, still don't `memory_add` it row-by-row — ask the user for it as a file, or point them at the UI's Add Knowledge upload. |
|
|
105
|
+
| User wants to add documents to the team's knowledge base | Uploading adds the file to the datapack's shared knowledge base, which every member can read, so ask first: name the file and the datapack, say the team will see it, and upload only after the user says yes. If you (the agent) have the file's bytes, call `knowledgebase_upload_document`. Otherwise point the user at the Cloud UI flow — Datapack → Actions → **Add Knowledge** (PDF/TXT/MD/JSON/CSV/XML/HTML, 5MB Free / 50MB Standard / 60MB Pro+ each, 10/batch). KB *lifecycle* (index/source management) is still UI-only. |
|
|
106
106
|
|
|
107
107
|
### When to call memory_add
|
|
108
108
|
|
|
@@ -283,7 +283,7 @@ On the first Meko tool call in a fresh session (no prior Meko tool call since pr
|
|
|
283
283
|
|
|
284
284
|
| File | What it covers |
|
|
285
285
|
|------|---------------|
|
|
286
|
-
| `tools-overview.md` | Complete catalog of all
|
|
286
|
+
| `tools-overview.md` | Complete catalog of all 28 tools available in production, with decision tree |
|
|
287
287
|
| `tools-cookbook.md` | Per-tool examples with correct parameters, responses, and error cases |
|
|
288
288
|
| `tools-memory-vs-conversation.md` | When to use memory tools vs conversation tools |
|
|
289
289
|
| `tools-datapack-workflow.md` | Datapack lifecycle and datapack_id routing |
|
|
@@ -6,7 +6,7 @@ description: Maps reference files to their topics for the meko-mcp-tools skill
|
|
|
6
6
|
# Reference Sections
|
|
7
7
|
|
|
8
8
|
## Tool Selection & Catalog
|
|
9
|
-
- [tools-overview.md](tools-overview.md) — Complete catalog of all
|
|
9
|
+
- [tools-overview.md](tools-overview.md) — Complete catalog of all 28 tools available in production, with a decision tree
|
|
10
10
|
|
|
11
11
|
## Tool-Specific Guidance
|
|
12
12
|
- [tools-memory-vs-conversation.md](tools-memory-vs-conversation.md) — Decision framework: memory_add vs conversation_create/conversation_add_message
|