@yugabytedb/meko-mcp 2.0.0 → 2.1.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 +31 -6
- package/assets/hooks-handlers/codex-session-end.sh +78 -0
- package/assets/hooks-handlers/lib/capture.js +163 -14
- package/assets/hooks-handlers/lib/checkpoint-timer.js +44 -2
- package/assets/hooks-handlers/session-end.sh +10 -0
- package/assets/hooks-handlers/session-start.sh +8 -1
- package/assets/hooks-handlers/test_checkpoint_timer.sh +162 -0
- package/assets/scripts/package-desktop-skill.sh +32 -1
- package/assets/skills/meko-mcp-tools/SKILL.md +47 -41
- package/assets/skills/meko-mcp-tools/references/_sections.md +2 -4
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +29 -25
- package/assets/skills/meko-mcp-tools/references/tools-conversation-dedup.md +5 -7
- package/assets/skills/meko-mcp-tools/references/tools-conversation-reasoning.md +50 -0
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +63 -46
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +6 -6
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +4 -4
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +17 -22
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +40 -39
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +1 -2
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +4 -18
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +9 -8
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +49 -42
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +6 -6
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +2 -2
- package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +28 -24
- package/assets/skills/meko-mcp-tools-desktop/references/tools-conversation-reasoning.md +50 -0
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +61 -36
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +4 -4
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +16 -19
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +40 -39
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +1 -2
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +3 -17
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +31 -21
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +8 -8
- package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +41 -0
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +1 -1
- package/bin/create-meko-setup.mjs +45 -6
- package/lib/clients/claude-code.mjs +8 -4
- package/lib/clients/claude-desktop.mjs +14 -0
- package/lib/clients/codex.mjs +23 -8
- package/lib/clients/common.mjs +2 -0
- package/lib/clients/cursor.mjs +7 -2
- package/lib/detect.mjs +7 -2
- package/lib/prompt.mjs +1 -1
- package/lib/url-selection.mjs +55 -0
- package/package.json +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +0 -71
- package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +0 -71
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ Each client is installed sequentially. A failure on one does not abort the other
|
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
110
|
npx @yugabytedb/meko-mcp \
|
|
111
|
-
--url https://
|
|
111
|
+
--url https://mcp.mekodata.ai/mcp \
|
|
112
112
|
--api-key "$MEKO_API_KEY" \
|
|
113
113
|
--yes
|
|
114
114
|
```
|
|
@@ -121,7 +121,7 @@ The installer performs up to 5 steps (some are skippable via flags):
|
|
|
121
121
|
|------|--------|------------------------|
|
|
122
122
|
| 1 | Register MCP server | `claude mcp add meko <url> --transport http --scope user` (with `-H "Authorization: Bearer <key>"` for cloud) |
|
|
123
123
|
| 2 | Set hook environment vars | Writes `MEKO_MCP_URL` and `MEKO_API_KEY` into `~/.claude/settings.json` under the `env` key. Also prunes any stale Meko hook entries in `settings.json.hooks` that point at a prior install path (e.g. a previous `npx` cache or homebrew-global copy) so only the current install's hooks fire. |
|
|
124
|
-
| 3 | Register plugin marketplace | `claude plugin marketplace add yugabyte/meko-
|
|
124
|
+
| 3 | Register public plugin marketplace | `claude plugin marketplace add https://github.com/yugabyte/meko-skills.git --scope user` |
|
|
125
125
|
| 4 | Install skills plugin | `claude plugin install meko-agent-skills --scope user` |
|
|
126
126
|
| 5 | Validate setup | Config check via `claude mcp list`, MCP `initialize` handshake, `tools/list` verification, and a canary `datapack_list` call |
|
|
127
127
|
|
|
@@ -173,7 +173,7 @@ Usage:
|
|
|
173
173
|
| Flag | Description | Default |
|
|
174
174
|
|------|-------------|---------|
|
|
175
175
|
| `--client <list>` | Target client(s): one or more of `claude-code`, `claude-desktop`, `cursor`, `codex`. Comma-separated, or `all`. `codex` covers both the OpenAI Codex CLI and the Codex Desktop App (they share `~/.codex/config.toml`). | `claude-code` |
|
|
176
|
-
| `--url <url>` | MCP server URL (from
|
|
176
|
+
| `--url <url>` | MCP server URL (from cloud.mekodata.ai > Datapacks > Connect) | Shared existing endpoint, otherwise production; conflicts prompt or require `--url` under `--yes` |
|
|
177
177
|
| `--api-key <key>` | API key for Cloud Meko authentication. If a Cloud `--url` is supplied without this flag, the installer falls back to `$MEKO_API_KEY`, then to an interactive prompt on a TTY. Only fails hard when running under `--yes` or with no TTY attached. | _(interactive prompt or `$MEKO_API_KEY` env)_ |
|
|
178
178
|
| `--local` | Use local Meko at `http://localhost:8000/mcp` (no API key needed) | `false` |
|
|
179
179
|
| `--scope <scope>` | Config scope: `user` or `project` (`claude-desktop` uses user scope config) | `user` |
|
|
@@ -359,7 +359,7 @@ Cursor previously had a hybrid hook mode that also wrote Claude-compatible hooks
|
|
|
359
359
|
|
|
360
360
|
| | Cloud Meko | Local Meko |
|
|
361
361
|
|---|-----------|-----------|
|
|
362
|
-
| **URL** |
|
|
362
|
+
| **URL** | `https://mcp.mekodata.ai/mcp` (or the endpoint shown by cloud.mekodata.ai) | `http://localhost:8000/mcp` |
|
|
363
363
|
| **Authentication** | Requires `--api-key` or `$MEKO_API_KEY` env var | None |
|
|
364
364
|
| **MCP registration** | Includes `-H "Authorization: Bearer <key>"` header | No auth header |
|
|
365
365
|
| **Hook env vars** | Both `MEKO_MCP_URL` and `MEKO_API_KEY` written to settings | Only `MEKO_MCP_URL` written |
|
|
@@ -505,6 +505,31 @@ which claude
|
|
|
505
505
|
claude --version
|
|
506
506
|
```
|
|
507
507
|
|
|
508
|
+
### MCP server `meko` already exists in the user config
|
|
509
|
+
|
|
510
|
+
```
|
|
511
|
+
[1/4] Failed to register MCP server.
|
|
512
|
+
MCP server meko already exists in the user config
|
|
513
|
+
Install for claude-code failed: MCP server registration failed.
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Claude Code's `claude mcp add` refuses to overwrite an existing registration, so a re-run — or a previous install that failed partway through — aborts at step 1. Remove the stale entry, then re-run the installer:
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
claude mcp list # confirm the "meko" entry and which scope it is on
|
|
520
|
+
claude mcp remove meko --scope user # or --scope local / --scope project, matching what `list` showed
|
|
521
|
+
claude mcp list # confirm it is gone
|
|
522
|
+
npx @yugabytedb/meko-mcp@latest # re-run the installer
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
If `claude mcp remove` errors or you want to edit the file directly, the user-scope config lives at `~/.claude.json` — open it, find the `"mcpServers"` block, and delete the `"meko": { ... }` entry. Claude Desktop and Claude Code keep **separate** configs, so a clean Desktop install does not imply a clean Code install.
|
|
526
|
+
|
|
527
|
+
Logs worth collecting before filing a bug:
|
|
528
|
+
|
|
529
|
+
- **Installer log** — `ls -lt ~/.meko*/logs 2>/dev/null; ls -lt /tmp/meko-installer* 2>/dev/null` — share the newest file.
|
|
530
|
+
- **Claude Code MCP log** (macOS) — `ls -lt ~/Library/Caches/claude-cli-nodejs/*/mcp-logs-meko/ 2>/dev/null` — most recent `*.txt` shows the registration attempt and any stderr from the spawned process.
|
|
531
|
+
- **Current config snippet** — `grep -A 20 '"meko"' ~/.claude.json` — confirms what state Claude Code was in when the install was attempted.
|
|
532
|
+
|
|
508
533
|
### Connection refused
|
|
509
534
|
|
|
510
535
|
```
|
|
@@ -537,7 +562,7 @@ The MCP server is enforcing strict Streamable HTTP compliance: clients must adve
|
|
|
537
562
|
Server returned 401. Verify your API key is correct and has not expired.
|
|
538
563
|
```
|
|
539
564
|
|
|
540
|
-
- Confirm the API key at
|
|
565
|
+
- Confirm the API key at cloud.mekodata.ai > Datapacks > Connect.
|
|
541
566
|
- If using `$MEKO_API_KEY` env var, ensure it is exported in the current shell.
|
|
542
567
|
- Confirm the key environment matches the URL: PATs minted on `app.mekodev.com` only validate against `mcp.mekodev.com`; PATs minted on `cloud.mekodata.ai` only validate against `mcp.mekodata.ai`.
|
|
543
568
|
- The installer normalizes API keys (trims surrounding whitespace and a single matching pair of quotes) so a paste with a trailing newline or a literal `"…"` from a `.env` file is fine — but your raw shell variable might still be contaminated. Echo it (`echo "[$MEKO_API_KEY]"`) and check the brackets bracket the key tightly with no extra characters.
|
|
@@ -714,7 +739,7 @@ npm run test:integration
|
|
|
714
739
|
|
|
715
740
|
# Cloud round-trip tests (opt-in; regression coverage for MEKO-20).
|
|
716
741
|
# Requires credentials in the shell:
|
|
717
|
-
export MEKO_TEST_CLOUD_URL=https
|
|
742
|
+
export MEKO_TEST_CLOUD_URL=https://mcp.mekodata.ai/mcp
|
|
718
743
|
export MEKO_TEST_API_KEY=<your-cloud-api-key>
|
|
719
744
|
npm run test:integration:cloud
|
|
720
745
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Codex SessionEnd hook.
|
|
4
|
+
#
|
|
5
|
+
# Codex caps SessionEnd hooks at three seconds. The foreground invocation
|
|
6
|
+
# therefore persists the hook input, starts a detached worker, and exits
|
|
7
|
+
# immediately. The worker reuses the shared capture.js implementation and
|
|
8
|
+
# removes the persisted input when capture finishes.
|
|
9
|
+
|
|
10
|
+
set -u
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
CAPTURE_SCRIPT="$SCRIPT_DIR/lib/capture.js"
|
|
14
|
+
STATE_DIR="${MEKO_WATERMARK_DIR:-$HOME/.claude/meko-capture}"
|
|
15
|
+
JOB_DIR="$STATE_DIR/session-end-jobs"
|
|
16
|
+
LOG_FILE="$STATE_DIR/session-end.log"
|
|
17
|
+
|
|
18
|
+
run_worker() {
|
|
19
|
+
JOB_FILE="${1:-}"
|
|
20
|
+
if [ -z "$JOB_FILE" ] || [ ! -f "$JOB_FILE" ]; then
|
|
21
|
+
printf '[meko-capture] Codex SessionEnd worker received no readable job file\n' >&2
|
|
22
|
+
return 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
trap 'rm -f "$JOB_FILE"' EXIT HUP INT TERM
|
|
26
|
+
node "$CAPTURE_SCRIPT" session-end <"$JOB_FILE"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if [ "${1:-}" = "--worker" ]; then
|
|
30
|
+
run_worker "${2:-}"
|
|
31
|
+
exit $?
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Hook input can contain local paths and session metadata. Keep the spool
|
|
35
|
+
# directory and files private to the current user.
|
|
36
|
+
umask 077
|
|
37
|
+
mkdir -p "$JOB_DIR"
|
|
38
|
+
JOB_FILE="$(mktemp "$JOB_DIR/session-end.XXXXXX")" || {
|
|
39
|
+
printf '[meko-capture] Codex SessionEnd could not create a job file\n' >&2
|
|
40
|
+
printf '{}'
|
|
41
|
+
exit 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if ! cat >"$JOB_FILE"; then
|
|
45
|
+
rm -f "$JOB_FILE"
|
|
46
|
+
printf '[meko-capture] Codex SessionEnd could not persist hook input\n' >&2
|
|
47
|
+
printf '{}'
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Stop the periodic timer before handing final capture to the worker. This is
|
|
52
|
+
# deliberately best-effort: a missing or stale PID must not block SessionEnd.
|
|
53
|
+
TRANSCRIPT_PATH="$(node - "$JOB_FILE" <<'NODE' 2>/dev/null
|
|
54
|
+
const fs = require("fs");
|
|
55
|
+
const d = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
56
|
+
const p = d.transcript_path || (d.hookSpecificInput || {}).transcript_path || "";
|
|
57
|
+
process.stdout.write(p);
|
|
58
|
+
NODE
|
|
59
|
+
)"
|
|
60
|
+
|
|
61
|
+
if [ -n "$TRANSCRIPT_PATH" ]; then
|
|
62
|
+
SESSION_ID="$(basename "$TRANSCRIPT_PATH" .jsonl)"
|
|
63
|
+
PID_FILE="$STATE_DIR/${SESSION_ID}.timer.pid"
|
|
64
|
+
if [ -f "$PID_FILE" ]; then
|
|
65
|
+
kill "$(cat "$PID_FILE")" 2>/dev/null || true
|
|
66
|
+
rm -f "$PID_FILE"
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Redirect every inherited descriptor before returning to Codex. nohup keeps
|
|
71
|
+
# the worker alive after the hook shell exits; disown is available in bash but
|
|
72
|
+
# remains best-effort for non-interactive shells.
|
|
73
|
+
nohup bash "$0" --worker "$JOB_FILE" \
|
|
74
|
+
</dev/null >>"$LOG_FILE" 2>&1 &
|
|
75
|
+
WORKER_PID=$!
|
|
76
|
+
disown "$WORKER_PID" 2>/dev/null || true
|
|
77
|
+
|
|
78
|
+
printf '{}'
|
|
@@ -367,6 +367,45 @@ function datapackPinSlug(agentId) {
|
|
|
367
367
|
return trimmed.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || COMMON_BUCKET_AGENT_ID;
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
function captureErrorPath(agentId) {
|
|
371
|
+
return path.join(WATERMARK_DIR, `last-capture-error-${datapackPinSlug(agentId)}.json`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function readCaptureError(agentId) {
|
|
375
|
+
try {
|
|
376
|
+
const error = JSON.parse(fs.readFileSync(captureErrorPath(agentId), "utf-8"));
|
|
377
|
+
return error && typeof error === "object" ? error : null;
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function writeCaptureError(agentId, error) {
|
|
384
|
+
try {
|
|
385
|
+
fs.mkdirSync(WATERMARK_DIR, { recursive: true });
|
|
386
|
+
fs.writeFileSync(
|
|
387
|
+
captureErrorPath(agentId),
|
|
388
|
+
JSON.stringify({
|
|
389
|
+
...error,
|
|
390
|
+
agent_id: agentId,
|
|
391
|
+
occurred_at: new Date().toISOString(),
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
process.stderr.write(`[meko-capture] Could not persist capture error: ${err.message}\n`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function clearCaptureError(agentId) {
|
|
400
|
+
try {
|
|
401
|
+
fs.unlinkSync(captureErrorPath(agentId));
|
|
402
|
+
} catch (err) {
|
|
403
|
+
if (err && err.code !== "ENOENT") {
|
|
404
|
+
process.stderr.write(`[meko-capture] Could not clear capture error: ${err.message}\n`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
370
409
|
/**
|
|
371
410
|
* Read the active-datapack pin written by the meko-select-datapack skill.
|
|
372
411
|
*
|
|
@@ -409,7 +448,7 @@ function buildActiveDatapackBlock(pin) {
|
|
|
409
448
|
|
|
410
449
|
### Active datapack
|
|
411
450
|
|
|
412
|
-
The user pinned datapack **${pin.datapack_name}** (\`${pin.datapack_id}\`) for this project via the \`meko-select-datapack\` skill. Pass \`datapack_id="${pin.datapack_id}"\` to every Meko MCP tool call (memory_*,
|
|
451
|
+
The user pinned datapack **${pin.datapack_name}** (\`${pin.datapack_id}\`) for this project via the \`meko-select-datapack\` skill. Pass \`datapack_id="${pin.datapack_id}"\` to every Meko MCP tool call that accepts it (memory_*, knowledgebase_search, conversation_*, artifact_*) unless the user explicitly overrides for a single call. The pin is project-scoped (keyed by \`agent_id\`) and survives \`/clear\`, \`/compact\`, and Claude Code restart. Automatic capture keeps the datapack selected when this conversation was created; switching or clearing takes effect for automatic capture on the next new session.`;
|
|
413
452
|
}
|
|
414
453
|
|
|
415
454
|
// note: Subagents cannot auto-discover the parent's watermark today.
|
|
@@ -470,6 +509,16 @@ function mcpPost(jsonBody) {
|
|
|
470
509
|
res.on("data", (chunk) => (data += chunk));
|
|
471
510
|
res.on("end", () => {
|
|
472
511
|
inflightRequests.delete(req);
|
|
512
|
+
if (res.statusCode >= 400) {
|
|
513
|
+
const snippet = data.trim().replace(/\s+/g, " ").slice(0, 200);
|
|
514
|
+
const err = new Error(
|
|
515
|
+
`MCP HTTP ${res.statusCode}${snippet ? `: ${snippet}` : ""}`,
|
|
516
|
+
);
|
|
517
|
+
err.statusCode = res.statusCode;
|
|
518
|
+
err.responseBody = data.slice(0, 1000);
|
|
519
|
+
reject(err);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
473
522
|
// Notifications return 202/204 with no body — that's OK
|
|
474
523
|
if (!data.trim()) {
|
|
475
524
|
resolve({ body: null, headers: res.headers });
|
|
@@ -535,6 +584,51 @@ function parseMcpResponseBody(data) {
|
|
|
535
584
|
return JSON.parse(payload);
|
|
536
585
|
}
|
|
537
586
|
|
|
587
|
+
function classifyPersistentCaptureFailure(err) {
|
|
588
|
+
const message = err && err.message ? String(err.message) : String(err || "");
|
|
589
|
+
const normalized = message.toLowerCase();
|
|
590
|
+
const statusCode = Number(err && err.statusCode);
|
|
591
|
+
const statusMatch = normalized.match(/\bmcp http (402|403)\b/);
|
|
592
|
+
let persistentStatus = null;
|
|
593
|
+
if (statusCode === 402 || statusCode === 403) {
|
|
594
|
+
persistentStatus = statusCode;
|
|
595
|
+
} else if (statusMatch) {
|
|
596
|
+
persistentStatus = Number(statusMatch[1]);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (persistentStatus) {
|
|
600
|
+
return {
|
|
601
|
+
code: `http_${persistentStatus}`,
|
|
602
|
+
reason: `Meko rejected capture with HTTP ${persistentStatus}`,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const persistentPatterns = [
|
|
607
|
+
["free_tier_limit_reached", /free[_\s-]?tier.*limit|free_tier_limit_reached/],
|
|
608
|
+
["quota_exceeded", /quota[_\s-]?(reached|exceeded)|(?:reached|exceeded).*quota/],
|
|
609
|
+
["usage_limit_reached", /usage[_\s-]?limit[_\s-]?(reached|exceeded)/],
|
|
610
|
+
["plan_limit_reached", /plan[_\s-]?limit[_\s-]?(reached|exceeded)/],
|
|
611
|
+
["payment_required", /payment[_\s-]?required/],
|
|
612
|
+
["subscription_required", /subscription[_\s-]?(required|inactive|expired)/],
|
|
613
|
+
["entitlement_denied", /entitlement.*(denied|required|missing)|(?:denied|required|missing).*entitlement/],
|
|
614
|
+
];
|
|
615
|
+
for (const [code, pattern] of persistentPatterns) {
|
|
616
|
+
if (pattern.test(normalized)) {
|
|
617
|
+
const reasons = {
|
|
618
|
+
free_tier_limit_reached: "the free-tier capture limit was reached",
|
|
619
|
+
quota_exceeded: "the capture quota was exceeded",
|
|
620
|
+
usage_limit_reached: "the capture usage limit was reached",
|
|
621
|
+
plan_limit_reached: "the current plan's capture limit was reached",
|
|
622
|
+
payment_required: "capture requires payment",
|
|
623
|
+
subscription_required: "capture requires an active subscription",
|
|
624
|
+
entitlement_denied: "the account is not entitled to capture conversations",
|
|
625
|
+
};
|
|
626
|
+
return { code, reason: reasons[code] };
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
|
|
538
632
|
/**
|
|
539
633
|
* Abort all pending MCP requests. Used by the preload timeout path so the
|
|
540
634
|
* SessionStart hook can return promptly instead of waiting for a slow server
|
|
@@ -654,7 +748,6 @@ function peekTranscriptMetadata(transcriptPath) {
|
|
|
654
748
|
*/
|
|
655
749
|
async function fetchRecentMemories(agentId, datapackId = null, { limit = 10, budget = 2000 } = {}) {
|
|
656
750
|
const payload = {
|
|
657
|
-
scope: "read",
|
|
658
751
|
agent_id: agentId,
|
|
659
752
|
conversation_id: "00000000-0000-0000-0000-000000000000",
|
|
660
753
|
limit,
|
|
@@ -680,7 +773,6 @@ async function fetchRecentMemories(agentId, datapackId = null, { limit = 10, bud
|
|
|
680
773
|
|
|
681
774
|
async function createConversation(sessionId, agentId, metadata = null, datapackId = null) {
|
|
682
775
|
const payload = {
|
|
683
|
-
scope: "write",
|
|
684
776
|
agent_id: agentId,
|
|
685
777
|
title: "Claude Code session (auto-captured)",
|
|
686
778
|
session_id: sessionId,
|
|
@@ -702,7 +794,6 @@ async function createConversation(sessionId, agentId, metadata = null, datapackI
|
|
|
702
794
|
async function addMessage(convId, agentId, exchange, datapackId = null) {
|
|
703
795
|
const seed = `${convId}:${exchange.user_uuid}`;
|
|
704
796
|
const payload = {
|
|
705
|
-
scope: "write",
|
|
706
797
|
conversation_id: convId,
|
|
707
798
|
agent_id: agentId,
|
|
708
799
|
input: exchange.input,
|
|
@@ -745,14 +836,20 @@ function hookOutput(additionalContext) {
|
|
|
745
836
|
// emitting it for lifecycle capture hooks makes Codex reject the JSON.
|
|
746
837
|
if (process.argv[2] !== "session-start") return "{}";
|
|
747
838
|
|
|
748
|
-
//
|
|
749
|
-
//
|
|
839
|
+
// Codex rejects unknown top-level fields, so its SessionStart response must
|
|
840
|
+
// contain only the hook-specific output defined by the Codex wire schema.
|
|
750
841
|
const output = {
|
|
751
842
|
hookSpecificOutput: {
|
|
752
843
|
hookEventName: "SessionStart",
|
|
753
844
|
additionalContext: additionalContext || "",
|
|
754
845
|
},
|
|
755
846
|
};
|
|
847
|
+
if (process.env.MEKO_HOOK_CLIENT === "codex") {
|
|
848
|
+
return JSON.stringify(output);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Preserve the native coding-agent harness field for clients that consume
|
|
852
|
+
// it alongside the nested Claude-compatible shape.
|
|
756
853
|
output.additional_context = additionalContext || "";
|
|
757
854
|
return JSON.stringify(output);
|
|
758
855
|
}
|
|
@@ -858,9 +955,17 @@ function buildSessionStartContext(convId, sessionId, agentId, memories, opts) {
|
|
|
858
955
|
const resolvedAgentId = agentId || COMMON_BUCKET_AGENT_ID;
|
|
859
956
|
const resumed = Boolean(opts && opts.resumed);
|
|
860
957
|
const pin = opts && opts.datapackPin ? opts.datapackPin : null;
|
|
958
|
+
const captureError = opts && opts.captureError ? opts.captureError : null;
|
|
861
959
|
const opening = resumed
|
|
862
|
-
? `Resuming Meko conversation **${convId}** from a prior Claude Code session. The watermark file is written — PreCompact and SessionEnd hooks will append this session's new exchanges to the same conversation.`
|
|
863
|
-
: `Meko conversation **${convId}** was created automatically by the SessionStart hook. The watermark file is written — PreCompact and SessionEnd hooks
|
|
960
|
+
? `Resuming Meko conversation **${convId}** from a prior Claude Code session. The watermark file is written — PreCompact and SessionEnd hooks will attempt to append this session's new exchanges to the same conversation.`
|
|
961
|
+
: `Meko conversation **${convId}** was created automatically by the SessionStart hook. The watermark file is written — PreCompact and SessionEnd hooks are configured to capture this session's transcript.`;
|
|
962
|
+
const captureWarning = captureError
|
|
963
|
+
? `
|
|
964
|
+
|
|
965
|
+
### Automatic capture warning — tell the user
|
|
966
|
+
|
|
967
|
+
The last automatic capture attempt was persistently rejected because ${captureError.reason || "Meko rejected the write"}. **Do not claim that this session's turns are being saved.** The hook kept ${Number(captureError.held_exchanges || captureError.failed_exchanges) || 1} exchange(s) queued for retry instead of dropping them. Tell the user that automatic conversation capture is currently unhealthy and include this reason. This warning remains until a later hook write succeeds.`
|
|
968
|
+
: "";
|
|
864
969
|
const memoryBlock =
|
|
865
970
|
Array.isArray(memories) && memories.length > 0
|
|
866
971
|
? `
|
|
@@ -876,16 +981,16 @@ ${memories.join("\n")}`
|
|
|
876
981
|
return `## Meko Session Active
|
|
877
982
|
|
|
878
983
|
${opening}
|
|
984
|
+
${captureWarning}
|
|
879
985
|
|
|
880
986
|
### What you MUST do with Meko tools
|
|
881
987
|
|
|
882
988
|
- **agent_id**: use "${resolvedAgentId}" verbatim for every Meko MCP tool call in this session. This was derived from the cwd as \`<client>:<repo-basename>\` so memories stay scoped to this project. For genuinely cross-project facts (user identity, global preferences) pass agent_id="${COMMON_BUCKET_AGENT_ID}" — that's the common bucket any agent can read regardless of project.
|
|
883
989
|
- **conversation_id**: Use "${convId}" for all MCP tool calls that accept it
|
|
884
|
-
- **scope**: Only "read", "write", or "admin"
|
|
885
990
|
|
|
886
|
-
### Memory capture is automatic — do NOT proactively call memory_add
|
|
991
|
+
### Memory capture is automatic only when hook delivery succeeds — do NOT proactively call memory_add
|
|
887
992
|
|
|
888
|
-
|
|
993
|
+
The hooks above attempt to capture this session's turns, and the server extracts durable memories from successfully delivered turns on its own. Do not call memory_add for facts the user says out loud; when hook delivery succeeds, that would duplicate what extraction already stores. If the warning above is present, tell the user instead of pretending an explicit memory write will bypass the same server rejection.
|
|
889
994
|
|
|
890
995
|
Call memory_add (with the session's agent_id + conversation_id) ONLY for the narrow cases extraction cannot reach:
|
|
891
996
|
- The user explicitly says "remember this" / "save this to memory".
|
|
@@ -913,7 +1018,6 @@ The SessionStart hook could not reach the Meko MCP server to create a conversati
|
|
|
913
1018
|
|
|
914
1019
|
- **agent_id**: use "${resolvedAgentId}" for project-scoped writes; use "${COMMON_BUCKET_AGENT_ID}" for cross-project common facts
|
|
915
1020
|
- **conversation_id**: Use nil UUID "00000000-0000-0000-0000-000000000000" as placeholder
|
|
916
|
-
- **scope**: Only "read", "write", or "admin"
|
|
917
1021
|
|
|
918
1022
|
### Proactive storage (capture is down — do NOT wait to be asked)
|
|
919
1023
|
|
|
@@ -1001,7 +1105,10 @@ async function handleSessionStart(hookInput) {
|
|
|
1001
1105
|
sessionId,
|
|
1002
1106
|
resumedAgentId,
|
|
1003
1107
|
null,
|
|
1004
|
-
{
|
|
1108
|
+
{
|
|
1109
|
+
datapackPin: existingDatapackPin,
|
|
1110
|
+
captureError: readCaptureError(resumedAgentId),
|
|
1111
|
+
},
|
|
1005
1112
|
);
|
|
1006
1113
|
writeSessionCache(sessionId, {
|
|
1007
1114
|
status: "ready",
|
|
@@ -1048,7 +1155,11 @@ async function handleSessionStart(hookInput) {
|
|
|
1048
1155
|
sessionId,
|
|
1049
1156
|
resumedAgentId,
|
|
1050
1157
|
null,
|
|
1051
|
-
{
|
|
1158
|
+
{
|
|
1159
|
+
resumed: true,
|
|
1160
|
+
datapackPin: priorDatapackPin,
|
|
1161
|
+
captureError: readCaptureError(resumedAgentId),
|
|
1162
|
+
},
|
|
1052
1163
|
);
|
|
1053
1164
|
writeSessionCache(sessionId, {
|
|
1054
1165
|
status: "ready",
|
|
@@ -1129,6 +1240,7 @@ async function handleSessionStart(hookInput) {
|
|
|
1129
1240
|
|
|
1130
1241
|
const context = buildSessionStartContext(convId, sessionId, agentId, memories, {
|
|
1131
1242
|
datapackPin,
|
|
1243
|
+
captureError: readCaptureError(agentId),
|
|
1132
1244
|
});
|
|
1133
1245
|
if (sessionId) {
|
|
1134
1246
|
writeSessionCache(sessionId, {
|
|
@@ -1210,6 +1322,20 @@ async function main() {
|
|
|
1210
1322
|
try {
|
|
1211
1323
|
await mcpInitialize();
|
|
1212
1324
|
} catch (err) {
|
|
1325
|
+
const persistent = classifyPersistentCaptureFailure(err);
|
|
1326
|
+
if (persistent) {
|
|
1327
|
+
const pendingExchanges = extractExchanges(transcriptPath, lastLine).length;
|
|
1328
|
+
writeCaptureError(agentId, {
|
|
1329
|
+
code: persistent.code,
|
|
1330
|
+
reason: persistent.reason,
|
|
1331
|
+
held_exchanges: pendingExchanges || 1,
|
|
1332
|
+
session_id: sessionId,
|
|
1333
|
+
conversation_id: convId || null,
|
|
1334
|
+
});
|
|
1335
|
+
process.stderr.write(
|
|
1336
|
+
`[meko-capture] Persistent capture rejection recorded for the next SessionStart: ${persistent.reason}.\n`,
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1213
1339
|
process.stderr.write(`[meko-capture] Failed to initialize MCP session: ${err.message}\n`);
|
|
1214
1340
|
process.stdout.write(hookOutput(""));
|
|
1215
1341
|
return;
|
|
@@ -1230,6 +1356,7 @@ async function main() {
|
|
|
1230
1356
|
const exchanges = extractExchanges(transcriptPath, lastLine);
|
|
1231
1357
|
let captured = 0;
|
|
1232
1358
|
let failed = 0;
|
|
1359
|
+
const persistentFailures = [];
|
|
1233
1360
|
|
|
1234
1361
|
for (const exchange of exchanges) {
|
|
1235
1362
|
try {
|
|
@@ -1242,10 +1369,32 @@ async function main() {
|
|
|
1242
1369
|
captured++;
|
|
1243
1370
|
} catch (err) {
|
|
1244
1371
|
failed++;
|
|
1372
|
+
const persistent = classifyPersistentCaptureFailure(err);
|
|
1373
|
+
if (persistent) persistentFailures.push(persistent);
|
|
1245
1374
|
process.stderr.write(`[meko-capture] Failed to add message (uuid=${exchange.user_uuid}): ${err.message}\n`);
|
|
1246
1375
|
}
|
|
1247
1376
|
}
|
|
1248
1377
|
|
|
1378
|
+
if (persistentFailures.length > 0) {
|
|
1379
|
+
const persistent = persistentFailures[0];
|
|
1380
|
+
writeCaptureError(agentId, {
|
|
1381
|
+
code: persistent.code,
|
|
1382
|
+
reason: persistent.reason,
|
|
1383
|
+
// Every failed exchange remains held by the watermark, including any
|
|
1384
|
+
// transient failures that happened in the same persistently rejected batch.
|
|
1385
|
+
held_exchanges: failed,
|
|
1386
|
+
session_id: sessionId,
|
|
1387
|
+
conversation_id: convId,
|
|
1388
|
+
});
|
|
1389
|
+
process.stderr.write(
|
|
1390
|
+
`[meko-capture] Persistent capture rejection recorded for the next SessionStart: ${persistent.reason}.\n`,
|
|
1391
|
+
);
|
|
1392
|
+
} else if (captured > 0) {
|
|
1393
|
+
// A successful write proves the prior persistent rejection has cleared.
|
|
1394
|
+
// Transient failures may still hold this batch's watermark independently.
|
|
1395
|
+
clearCaptureError(agentId);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1249
1398
|
// Advance the watermark to currentLines ONLY when every exchange was
|
|
1250
1399
|
// captured. If any addMessage failed (typically a transient network error),
|
|
1251
1400
|
// keep the watermark at lastLine so the next checkpoint / PreCompact /
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Environment:
|
|
14
14
|
* MEKO_CHECKPOINT_INTERVAL Seconds between ticks (default: 600)
|
|
15
|
+
* MEKO_TIMER_MAX_AGE Hard self-exit age in seconds (default: 86400 = 24h)
|
|
16
|
+
* MEKO_TIMER_IDLE_EXIT Self-exit after the transcript is untouched this
|
|
17
|
+
* long, in seconds (default: 7200 = 2h)
|
|
18
|
+
* MEKO_TIMER_STARTUP_GRACE Wait this long for the transcript to appear
|
|
19
|
+
* before self-exiting (default: 1800 = 30min)
|
|
15
20
|
* MEKO_WATERMARK_DIR Watermark directory (default: ~/.claude/meko-capture)
|
|
16
21
|
* MEKO_MCP_URL Inherited by capture.js
|
|
17
22
|
* MEKO_API_KEY Inherited by capture.js
|
|
@@ -22,6 +27,14 @@ const fs = require("fs");
|
|
|
22
27
|
const path = require("path");
|
|
23
28
|
|
|
24
29
|
const INTERVAL_SECS = parseInt(process.env.MEKO_CHECKPOINT_INTERVAL || "600", 10);
|
|
30
|
+
// Self-exit backstops so an orphaned daemon (its pid file removed out of band,
|
|
31
|
+
// a missed SessionEnd, a blocked SIGTERM) cannot tick forever. Both are
|
|
32
|
+
// generous enough not to interrupt a genuinely long, active session.
|
|
33
|
+
const MAX_AGE_SECS = parseInt(process.env.MEKO_TIMER_MAX_AGE, 10) || 86400;
|
|
34
|
+
const IDLE_EXIT_SECS = parseInt(process.env.MEKO_TIMER_IDLE_EXIT, 10) || 7200;
|
|
35
|
+
const STARTUP_GRACE_SECS =
|
|
36
|
+
parseInt(process.env.MEKO_TIMER_STARTUP_GRACE, 10) || 1800;
|
|
37
|
+
const startedAt = Date.now();
|
|
25
38
|
const WATERMARK_DIR =
|
|
26
39
|
process.env.MEKO_WATERMARK_DIR ||
|
|
27
40
|
path.join(process.env.HOME || "~", ".claude", "meko-capture");
|
|
@@ -49,8 +62,37 @@ process.on("SIGTERM", cleanup);
|
|
|
49
62
|
process.on("SIGINT", cleanup);
|
|
50
63
|
|
|
51
64
|
function tick() {
|
|
52
|
-
//
|
|
53
|
-
|
|
65
|
+
// Self-exit backstop: never outlive the hard max age. Guards against orphans
|
|
66
|
+
// whose pid file was removed out of band (so session-end.sh can't kill them)
|
|
67
|
+
// or whose SessionEnd never fired (force-quit / terminal closed).
|
|
68
|
+
if (Date.now() - startedAt >= MAX_AGE_SECS * 1000) {
|
|
69
|
+
process.stderr.write(
|
|
70
|
+
`[meko-timer] Max age (${MAX_AGE_SECS}s) reached; self-exiting to avoid orphan.\n`,
|
|
71
|
+
);
|
|
72
|
+
cleanup();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Exit if the transcript is gone (session ended but we weren't killed yet).
|
|
77
|
+
// SessionStart can precede transcript creation by up to 20 minutes, so a
|
|
78
|
+
// missing file during the startup grace period is expected.
|
|
79
|
+
let stat;
|
|
80
|
+
try {
|
|
81
|
+
stat = fs.statSync(transcriptPath);
|
|
82
|
+
} catch {
|
|
83
|
+
if (Date.now() - startedAt < STARTUP_GRACE_SECS * 1000) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
cleanup();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Exit if the transcript has been idle far longer than a checkpoint window —
|
|
91
|
+
// the session is effectively over even though we were never signalled.
|
|
92
|
+
if (Date.now() - stat.mtimeMs >= IDLE_EXIT_SECS * 1000) {
|
|
93
|
+
process.stderr.write(
|
|
94
|
+
`[meko-timer] Transcript idle > ${IDLE_EXIT_SECS}s; self-exiting.\n`,
|
|
95
|
+
);
|
|
54
96
|
cleanup();
|
|
55
97
|
return;
|
|
56
98
|
}
|
|
@@ -21,6 +21,16 @@ if [ -n "$TRANSCRIPT_PATH" ]; then
|
|
|
21
21
|
kill "$(cat "$PID_FILE")" 2>/dev/null || true
|
|
22
22
|
rm -f "$PID_FILE"
|
|
23
23
|
fi
|
|
24
|
+
# Fallback: the pid file can go missing while the daemon lives (removed out of
|
|
25
|
+
# band, a missed/blocked signal). Match the running checkpoint-timer.js by the
|
|
26
|
+
# clean session id rather than the absolute transcript path, which may contain
|
|
27
|
+
# regex metacharacters that make pgrep fail or match incorrectly.
|
|
28
|
+
if command -v pgrep >/dev/null 2>&1; then
|
|
29
|
+
for pid in $(pgrep -f "checkpoint-timer.js .*${SESSION_ID}" 2>/dev/null); do
|
|
30
|
+
[ "$pid" = "$$" ] && continue
|
|
31
|
+
kill "$pid" 2>/dev/null || true
|
|
32
|
+
done
|
|
33
|
+
fi
|
|
24
34
|
fi
|
|
25
35
|
|
|
26
36
|
# Run final capture
|
|
@@ -16,7 +16,14 @@ TRANSCRIPT_PATH="$(printf '%s' "$HOOK_INPUT" | node -e '
|
|
|
16
16
|
# Spawn background checkpoint timer (periodic capture without interrupting the agent).
|
|
17
17
|
# Send the daemon's stderr to a log file instead of /dev/null so startup
|
|
18
18
|
# failures and crashes are diagnosable; stdout is still discarded.
|
|
19
|
-
|
|
19
|
+
#
|
|
20
|
+
# NOTE: we deliberately do NOT gate on [ -f "$TRANSCRIPT_PATH" ]. SessionStart
|
|
21
|
+
# fires before Claude Code writes the transcript .jsonl, so that file usually
|
|
22
|
+
# does not exist yet at hook time (measured gaps of +3s to +20min). Gating on
|
|
23
|
+
# its existence meant the timer never spawned and the whole session went
|
|
24
|
+
# uncaptured. checkpoint-timer.js self-guards a missing transcript on each
|
|
25
|
+
# tick(), so spawning unconditionally (when the path is known) is safe.
|
|
26
|
+
if [ -n "$TRANSCRIPT_PATH" ]; then
|
|
20
27
|
MEKO_LOG_DIR="${MEKO_WATERMARK_DIR:-$HOME/.claude/meko-capture}"
|
|
21
28
|
mkdir -p "$MEKO_LOG_DIR"
|
|
22
29
|
nohup node "$SCRIPT_DIR/lib/checkpoint-timer.js" "$TRANSCRIPT_PATH" \
|