@yugabytedb/meko-mcp 1.2.0 → 2.0.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 +28 -19
- package/assets/hooks-handlers/before-submit-prompt.sh +2 -2
- package/assets/hooks-handlers/lib/capture.js +209 -144
- package/assets/hooks-handlers/lib/checkpoint-timer.js +23 -11
- package/assets/hooks-handlers/session-start.sh +6 -2
- package/assets/scripts/meko-statusline.sh +119 -0
- package/assets/scripts/package-desktop-skill.sh +27 -24
- package/assets/skills/meko-mcp-tools/SKILL.md +42 -50
- package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +75 -150
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +30 -36
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +6 -22
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +9 -8
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +35 -42
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +28 -92
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +5 -9
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +1 -81
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +100 -107
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +41 -11
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +71 -150
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +12 -18
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +12 -15
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +35 -42
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +28 -92
- package/assets/skills/meko-mcp-tools-desktop/references/tools-scope-permissions.md +5 -9
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +1 -81
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +198 -0
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +121 -0
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +66 -0
- package/bin/create-meko-setup.mjs +29 -0
- package/lib/clients/claude-code.mjs +133 -6
- package/lib/clients/claude-desktop.mjs +37 -12
- package/lib/clients/codex.mjs +306 -5
- package/lib/clients/common.mjs +41 -1
- package/lib/clients/cursor.mjs +75 -19
- package/lib/config.mjs +26 -0
- package/lib/installer.mjs +24 -0
- package/lib/mcp-transport.mjs +5 -0
- package/lib/validate.mjs +0 -1
- package/package.json +1 -1
- package/scripts/bundle-assets.mjs +8 -0
- package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +0 -38
- package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +0 -30
|
@@ -211,10 +211,18 @@ function extractToolResultContent(msg) {
|
|
|
211
211
|
}
|
|
212
212
|
// Fallback to toolUseResult summary if no content blocks found
|
|
213
213
|
if (parts.length === 0 && msg.toolUseResult != null) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
let summary;
|
|
215
|
+
if (typeof msg.toolUseResult === "string") {
|
|
216
|
+
summary = msg.toolUseResult;
|
|
217
|
+
} else {
|
|
218
|
+
// toolUseResult is arbitrary tool output; JSON.stringify can throw on
|
|
219
|
+
// circular refs or BigInt. A capture summary must never crash the hook.
|
|
220
|
+
try {
|
|
221
|
+
summary = JSON.stringify(msg.toolUseResult);
|
|
222
|
+
} catch {
|
|
223
|
+
summary = "(unserializable tool result)";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
218
226
|
parts.push(truncate(summary, MAX_TOOL_RESULT_LEN));
|
|
219
227
|
}
|
|
220
228
|
return parts.length > 0 ? "TOOL RESULT:\n" + parts.join("\n") : "";
|
|
@@ -323,20 +331,87 @@ function readWatermark(wmPath) {
|
|
|
323
331
|
}
|
|
324
332
|
}
|
|
325
333
|
|
|
326
|
-
function writeWatermark(wmPath, convId, lineNum, agentId) {
|
|
334
|
+
function writeWatermark(wmPath, convId, lineNum, agentId, datapackPin = null) {
|
|
327
335
|
fs.mkdirSync(path.dirname(wmPath), { recursive: true });
|
|
328
336
|
const existing = readWatermark(wmPath);
|
|
337
|
+
const existingPin =
|
|
338
|
+
existing.datapack_id && existing.datapack_name
|
|
339
|
+
? {
|
|
340
|
+
datapack_id: existing.datapack_id,
|
|
341
|
+
datapack_name: existing.datapack_name,
|
|
342
|
+
}
|
|
343
|
+
: null;
|
|
344
|
+
const pin = datapackPin || existingPin;
|
|
329
345
|
fs.writeFileSync(
|
|
330
346
|
wmPath,
|
|
331
347
|
JSON.stringify({
|
|
332
348
|
conversation_id: convId,
|
|
333
349
|
last_line_number: lineNum,
|
|
334
350
|
agent_id: agentId != null ? agentId : existing.agent_id || "",
|
|
351
|
+
datapack_id: pin ? pin.datapack_id : null,
|
|
352
|
+
datapack_name: pin ? pin.datapack_name : null,
|
|
335
353
|
updated_at: new Date().toISOString(),
|
|
336
354
|
})
|
|
337
355
|
);
|
|
338
356
|
}
|
|
339
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Convert an `agent_id` (e.g. `claude_code:meko-mcp-server`) into a
|
|
360
|
+
* filesystem-safe slug (`claude_code_meko-mcp-server`). The skill computes
|
|
361
|
+
* the same slug from the SessionStart-injected `agent_id`, so writer and
|
|
362
|
+
* reader agree on the path without sharing any session UUID.
|
|
363
|
+
*/
|
|
364
|
+
function datapackPinSlug(agentId) {
|
|
365
|
+
const trimmed = (typeof agentId === "string" ? agentId : "").trim();
|
|
366
|
+
if (!trimmed) return COMMON_BUCKET_AGENT_ID;
|
|
367
|
+
return trimmed.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || COMMON_BUCKET_AGENT_ID;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Read the active-datapack pin written by the meko-select-datapack skill.
|
|
372
|
+
*
|
|
373
|
+
* The pin is a sidecar file at `<WATERMARK_DIR>/pin-<slug(agent_id)>.json`
|
|
374
|
+
* with shape `{ datapack_id, datapack_name, selected_at }`. Project-scoped:
|
|
375
|
+
* keyed by the same `agent_id` the hook derives for memory writes
|
|
376
|
+
* (`claude_code:<repo-basename>`), so a pin set in one Claude Code window
|
|
377
|
+
* for a repo applies to every other window in that repo, and survives
|
|
378
|
+
* `/clear`, `/compact`, and Claude Code restart.
|
|
379
|
+
*
|
|
380
|
+
* Hooks are read-only — only the skill writes this file. Returns null if
|
|
381
|
+
* the file is absent, malformed, or missing required fields, so SessionStart
|
|
382
|
+
* can proceed unchanged when no pin is set.
|
|
383
|
+
*/
|
|
384
|
+
function readDatapackPin(agentId) {
|
|
385
|
+
const slug = datapackPinSlug(agentId);
|
|
386
|
+
if (!slug) return null;
|
|
387
|
+
const pinPath = path.join(WATERMARK_DIR, `pin-${slug}.json`);
|
|
388
|
+
try {
|
|
389
|
+
const obj = JSON.parse(fs.readFileSync(pinPath, "utf-8"));
|
|
390
|
+
const id = typeof obj.datapack_id === "string" ? obj.datapack_id.trim() : "";
|
|
391
|
+
const name = typeof obj.datapack_name === "string" ? obj.datapack_name.trim() : "";
|
|
392
|
+
if (!id || !name) return null;
|
|
393
|
+
return { datapack_id: id, datapack_name: name, selected_at: obj.selected_at || "" };
|
|
394
|
+
} catch {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function datapackPinFromWatermark(watermark) {
|
|
400
|
+
if (!watermark) return null;
|
|
401
|
+
const id = typeof watermark.datapack_id === "string" ? watermark.datapack_id.trim() : "";
|
|
402
|
+
const name = typeof watermark.datapack_name === "string" ? watermark.datapack_name.trim() : "";
|
|
403
|
+
return id && name ? { datapack_id: id, datapack_name: name } : null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function buildActiveDatapackBlock(pin) {
|
|
407
|
+
if (!pin) return "";
|
|
408
|
+
return `
|
|
409
|
+
|
|
410
|
+
### Active datapack
|
|
411
|
+
|
|
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_*, db_*, knowledgebase_*, conversation_*) 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
|
+
}
|
|
414
|
+
|
|
340
415
|
// note: Subagents cannot auto-discover the parent's watermark today.
|
|
341
416
|
// The watermark dir is flat and keyed by session UUID, and a subagent
|
|
342
417
|
// process has no way to learn its parent's session UUID:
|
|
@@ -366,6 +441,13 @@ function mcpPost(jsonBody) {
|
|
|
366
441
|
"Content-Type": "application/json",
|
|
367
442
|
Accept: "application/json, text/event-stream",
|
|
368
443
|
"Content-Length": Buffer.byteLength(body),
|
|
444
|
+
// Cloud Meko sits behind a WAF that 403s requests with no User-Agent
|
|
445
|
+
// (Node's http/https send none by default). Without this the SessionStart
|
|
446
|
+
// / PreCompact / SessionEnd hooks all fail against mcp.mekodata.ai and
|
|
447
|
+
// automatic capture silently never runs — the hook falls back to the
|
|
448
|
+
// agent-driven path every session. Send an explicit UA so the WAF admits
|
|
449
|
+
// the request. (Verified: prod 403s without UA, 200s with one.)
|
|
450
|
+
"User-Agent": "meko-capture/1.0 (+https://github.com/yugabyte/meko-mcp-server)",
|
|
369
451
|
};
|
|
370
452
|
if (MEKO_API_KEY) {
|
|
371
453
|
headers["Authorization"] = `Bearer ${MEKO_API_KEY}`;
|
|
@@ -570,13 +652,15 @@ function peekTranscriptMetadata(transcriptPath) {
|
|
|
570
652
|
* On any error the caller falls back to an empty list — memory preload is a
|
|
571
653
|
* best-effort enrichment, not a correctness requirement.
|
|
572
654
|
*/
|
|
573
|
-
async function fetchRecentMemories(agentId, { limit = 10, budget = 2000 } = {}) {
|
|
574
|
-
const
|
|
655
|
+
async function fetchRecentMemories(agentId, datapackId = null, { limit = 10, budget = 2000 } = {}) {
|
|
656
|
+
const payload = {
|
|
575
657
|
scope: "read",
|
|
576
658
|
agent_id: agentId,
|
|
577
659
|
conversation_id: "00000000-0000-0000-0000-000000000000",
|
|
578
660
|
limit,
|
|
579
|
-
}
|
|
661
|
+
};
|
|
662
|
+
if (datapackId) payload.datapack_id = datapackId;
|
|
663
|
+
const response = await mcpCall("memory_get_all", payload);
|
|
580
664
|
const result = extractToolResult(response);
|
|
581
665
|
if (!result) return [];
|
|
582
666
|
|
|
@@ -594,7 +678,7 @@ async function fetchRecentMemories(agentId, { limit = 10, budget = 2000 } = {})
|
|
|
594
678
|
return summaries;
|
|
595
679
|
}
|
|
596
680
|
|
|
597
|
-
async function createConversation(sessionId, agentId, metadata = null) {
|
|
681
|
+
async function createConversation(sessionId, agentId, metadata = null, datapackId = null) {
|
|
598
682
|
const payload = {
|
|
599
683
|
scope: "write",
|
|
600
684
|
agent_id: agentId,
|
|
@@ -609,14 +693,15 @@ async function createConversation(sessionId, agentId, metadata = null) {
|
|
|
609
693
|
startedAt: metadata.startedAt ?? null,
|
|
610
694
|
});
|
|
611
695
|
}
|
|
696
|
+
if (datapackId) payload.datapack_id = datapackId;
|
|
612
697
|
const response = await mcpCall("conversation_create", payload);
|
|
613
698
|
const result = extractToolResult(response);
|
|
614
699
|
return result && result.id ? result.id : null;
|
|
615
700
|
}
|
|
616
701
|
|
|
617
|
-
async function addMessage(convId, agentId, exchange) {
|
|
702
|
+
async function addMessage(convId, agentId, exchange, datapackId = null) {
|
|
618
703
|
const seed = `${convId}:${exchange.user_uuid}`;
|
|
619
|
-
|
|
704
|
+
const payload = {
|
|
620
705
|
scope: "write",
|
|
621
706
|
conversation_id: convId,
|
|
622
707
|
agent_id: agentId,
|
|
@@ -624,31 +709,51 @@ async function addMessage(convId, agentId, exchange) {
|
|
|
624
709
|
output: exchange.output,
|
|
625
710
|
reasoning: exchange.reasoning,
|
|
626
711
|
seed: seed,
|
|
627
|
-
}
|
|
712
|
+
};
|
|
713
|
+
if (datapackId) payload.datapack_id = datapackId;
|
|
714
|
+
const response = await mcpCall("conversation_add_message", payload);
|
|
715
|
+
// FAIL CLOSED. The MCP transport resolves successfully even when the tool
|
|
716
|
+
// failed, and failures take several shapes: an `isError` result envelope, a
|
|
717
|
+
// JSON body with an `error` field, or a non-JSON / missing-content / otherwise
|
|
718
|
+
// malformed body (which extractToolResult reports as null). If we treat any of
|
|
719
|
+
// these as success, the caller counts the turn as captured and advances the
|
|
720
|
+
// watermark — permanently dropping it. So only a *valid success body* counts;
|
|
721
|
+
// anything else throws into the caller's failed++/watermark-hold/retry path
|
|
722
|
+
// (dedup-by-seed makes the retry safe).
|
|
723
|
+
if (response && response.result && response.result.isError) {
|
|
724
|
+
throw new Error(
|
|
725
|
+
`conversation_add_message returned isError: ${JSON.stringify(response.result)}`,
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const result = extractToolResult(response);
|
|
729
|
+
if (result === null) {
|
|
730
|
+
throw new Error(
|
|
731
|
+
"conversation_add_message: no parseable result body (non-JSON, empty, or malformed response)",
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
if (result.error) {
|
|
735
|
+
throw new Error(`conversation_add_message failed: ${JSON.stringify(result.error)}`);
|
|
736
|
+
}
|
|
737
|
+
return result;
|
|
628
738
|
}
|
|
629
739
|
|
|
630
740
|
// --- Hook output ---
|
|
631
741
|
|
|
632
|
-
function hookEventName(hookType) {
|
|
633
|
-
switch (hookType) {
|
|
634
|
-
case "session-start": return "SessionStart";
|
|
635
|
-
case "pre-compact": return "PreCompact";
|
|
636
|
-
default: return "SessionEnd";
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
742
|
function hookOutput(additionalContext) {
|
|
743
|
+
// Codex PreCompact accepts only the common hook output fields.
|
|
744
|
+
// `additionalContext` is valid hook-specific output for SessionStart, but
|
|
745
|
+
// emitting it for lifecycle capture hooks makes Codex reject the JSON.
|
|
746
|
+
if (process.argv[2] !== "session-start") return "{}";
|
|
747
|
+
|
|
641
748
|
// Emit the native coding-agent harness field while preserving the nested
|
|
642
749
|
// Claude-compatible shape for clients that still consume it.
|
|
643
750
|
const output = {
|
|
644
751
|
hookSpecificOutput: {
|
|
645
|
-
hookEventName:
|
|
752
|
+
hookEventName: "SessionStart",
|
|
646
753
|
additionalContext: additionalContext || "",
|
|
647
754
|
},
|
|
648
755
|
};
|
|
649
|
-
|
|
650
|
-
output.additional_context = additionalContext || "";
|
|
651
|
-
}
|
|
756
|
+
output.additional_context = additionalContext || "";
|
|
652
757
|
return JSON.stringify(output);
|
|
653
758
|
}
|
|
654
759
|
|
|
@@ -656,9 +761,9 @@ function nativeOutput(additionalContext) {
|
|
|
656
761
|
return JSON.stringify({ additional_context: additionalContext || "" });
|
|
657
762
|
}
|
|
658
763
|
|
|
659
|
-
function beforeSubmitOutput(
|
|
660
|
-
const output = {
|
|
661
|
-
if (userMessage) output.
|
|
764
|
+
function beforeSubmitOutput(permission = "allow", userMessage = "") {
|
|
765
|
+
const output = { permission };
|
|
766
|
+
if (userMessage) output.userMessage = userMessage;
|
|
662
767
|
return JSON.stringify(output);
|
|
663
768
|
}
|
|
664
769
|
|
|
@@ -701,16 +806,6 @@ function writeSessionCache(sessionId, payload) {
|
|
|
701
806
|
}
|
|
702
807
|
}
|
|
703
808
|
|
|
704
|
-
function readSessionCache(sessionId) {
|
|
705
|
-
const filePath = sessionCachePath(sessionId);
|
|
706
|
-
if (!filePath) return null;
|
|
707
|
-
try {
|
|
708
|
-
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
709
|
-
} catch {
|
|
710
|
-
return null;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
809
|
function workspaceKeyFromHookInput(hookInput, transcriptPath) {
|
|
715
810
|
const roots = hookInput && Array.isArray(hookInput.workspace_roots)
|
|
716
811
|
? hookInput.workspace_roots
|
|
@@ -724,28 +819,6 @@ function workspaceKeyFromHookInput(hookInput, transcriptPath) {
|
|
|
724
819
|
return cwd || process.cwd();
|
|
725
820
|
}
|
|
726
821
|
|
|
727
|
-
function readWorkspaceSessionCache(workspaceKey) {
|
|
728
|
-
if (!workspaceKey) return null;
|
|
729
|
-
const safe = workspaceKey.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
730
|
-
try {
|
|
731
|
-
return JSON.parse(fs.readFileSync(path.join(SESSION_CACHE_DIR, `workspace-${safe}.json`), "utf-8"));
|
|
732
|
-
} catch {
|
|
733
|
-
return null;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function selectSessionCache(sessionId, workspaceKey) {
|
|
738
|
-
const exactCache = sessionId ? readSessionCache(sessionId) : null;
|
|
739
|
-
const workspaceCache = readWorkspaceSessionCache(workspaceKey);
|
|
740
|
-
const cache =
|
|
741
|
-
exactCache && exactCache.status === "ready"
|
|
742
|
-
? exactCache
|
|
743
|
-
: workspaceCache && workspaceCache.status === "ready"
|
|
744
|
-
? workspaceCache
|
|
745
|
-
: exactCache || workspaceCache;
|
|
746
|
-
return cache;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
822
|
// --- Main ---
|
|
750
823
|
|
|
751
824
|
// --- SessionStart: create conversation + watermark deterministically ---
|
|
@@ -768,6 +841,8 @@ function findMostRecentProjectWatermark(transcriptPath, currentSessionId) {
|
|
|
768
841
|
sessionId: sid,
|
|
769
842
|
conversation_id: wm.conversation_id,
|
|
770
843
|
agent_id: wm.agent_id || "",
|
|
844
|
+
datapack_id: wm.datapack_id || null,
|
|
845
|
+
datapack_name: wm.datapack_name || null,
|
|
771
846
|
updated_at: ts,
|
|
772
847
|
};
|
|
773
848
|
}
|
|
@@ -782,6 +857,7 @@ function findMostRecentProjectWatermark(transcriptPath, currentSessionId) {
|
|
|
782
857
|
function buildSessionStartContext(convId, sessionId, agentId, memories, opts) {
|
|
783
858
|
const resolvedAgentId = agentId || COMMON_BUCKET_AGENT_ID;
|
|
784
859
|
const resumed = Boolean(opts && opts.resumed);
|
|
860
|
+
const pin = opts && opts.datapackPin ? opts.datapackPin : null;
|
|
785
861
|
const opening = resumed
|
|
786
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.`
|
|
787
863
|
: `Meko conversation **${convId}** was created automatically by the SessionStart hook. The watermark file is written — PreCompact and SessionEnd hooks will capture this session's transcript verbatim.`;
|
|
@@ -795,6 +871,7 @@ These facts were preloaded for you — inspect them before acting so you don't r
|
|
|
795
871
|
|
|
796
872
|
${memories.join("\n")}`
|
|
797
873
|
: "";
|
|
874
|
+
const datapackBlock = buildActiveDatapackBlock(pin);
|
|
798
875
|
|
|
799
876
|
return `## Meko Session Active
|
|
800
877
|
|
|
@@ -806,59 +883,48 @@ ${opening}
|
|
|
806
883
|
- **conversation_id**: Use "${convId}" for all MCP tool calls that accept it
|
|
807
884
|
- **scope**: Only "read", "write", or "admin"
|
|
808
885
|
|
|
809
|
-
###
|
|
886
|
+
### Memory capture is automatic — do NOT proactively call memory_add
|
|
810
887
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
-
|
|
888
|
+
This session's turns are captured to Meko by the hooks above, and the server extracts durable memories from them on its own. **Any fact the user states in conversation — their name, role, preferences, org conventions, corrections — is captured without you doing anything.** Do not call memory_add for facts the user says out loud; it duplicates what extraction already stores.
|
|
889
|
+
|
|
890
|
+
Call memory_add (with the session's agent_id + conversation_id) ONLY for the narrow cases extraction cannot reach:
|
|
891
|
+
- The user explicitly says "remember this" / "save this to memory".
|
|
892
|
+
- A durable fact lives only in YOUR output or a tool result, not in the user's words — extraction reads the user turn only, so it never sees these.
|
|
893
|
+
- The user negates or corrects a prior fact and the stale one must be overwritten — extraction is additive, so also fix the old memory (memory_search to find it, then memory_update / memory_delete_by_id).
|
|
815
894
|
|
|
816
|
-
###
|
|
895
|
+
### Recall and before destructive actions
|
|
817
896
|
|
|
818
|
-
|
|
897
|
+
- User asks "what do you know about X?" → call memory_search first.
|
|
898
|
+
- Before any action that deletes, overwrites, or restructures user content (Write-overwrite, large Edit deletions, rm, git reset --hard, destructive SQL), call memory_search for relevant feedback and confirm with the user if any prior guidance applies.
|
|
819
899
|
|
|
820
900
|
### Use Meko, not local storage
|
|
821
901
|
|
|
822
|
-
|
|
902
|
+
Never write memories to local files (e.g., ~/.claude/projects/.../memory/). Meko — via automatic capture plus the narrow memory_add cases above — is the only memory store.${memoryBlock}${datapackBlock}`;
|
|
823
903
|
}
|
|
824
904
|
|
|
825
|
-
function buildSessionStartFallbackContext(agentId) {
|
|
905
|
+
function buildSessionStartFallbackContext(agentId, opts) {
|
|
826
906
|
const resolvedAgentId = agentId || COMMON_BUCKET_AGENT_ID;
|
|
907
|
+
const datapackBlock = buildActiveDatapackBlock(opts && opts.datapackPin ? opts.datapackPin : null);
|
|
827
908
|
return `## Meko Memory Instructions
|
|
828
909
|
|
|
829
|
-
The SessionStart hook could not reach the Meko MCP server to create a conversation. Automatic capture
|
|
910
|
+
The SessionStart hook could not reach the Meko MCP server to create a conversation. **Automatic conversation capture is NOT running this session**, so the server-side extraction that normally saves memories for you will not fire. Fall back to saving facts explicitly.
|
|
830
911
|
|
|
831
|
-
### What you should
|
|
912
|
+
### What you should do while capture is down
|
|
832
913
|
|
|
833
|
-
- **Use Meko MCP tools** for memory_add, memory_search, etc. when available
|
|
834
914
|
- **agent_id**: use "${resolvedAgentId}" for project-scoped writes; use "${COMMON_BUCKET_AGENT_ID}" for cross-project common facts
|
|
835
915
|
- **conversation_id**: Use nil UUID "00000000-0000-0000-0000-000000000000" as placeholder
|
|
836
916
|
- **scope**: Only "read", "write", or "admin"
|
|
837
917
|
|
|
838
|
-
### Proactive storage (do NOT wait to be asked)
|
|
918
|
+
### Proactive storage (capture is down — do NOT wait to be asked)
|
|
839
919
|
|
|
920
|
+
Because nothing is capturing this session, explicit saves are the only path — this is the exception, not the normal mode:
|
|
840
921
|
- User shares personal info, preferences, or role → call memory_add immediately
|
|
841
922
|
- User shares org conventions or team standards → call memory_add immediately
|
|
842
923
|
- User asks "what do you know about X?" → call memory_search first
|
|
843
924
|
|
|
844
925
|
### Use Meko, not local storage
|
|
845
926
|
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
function buildBootstrapPendingContext(agentId) {
|
|
850
|
-
const resolvedAgentId = agentId || COMMON_BUCKET_AGENT_ID;
|
|
851
|
-
return `## Meko Bootstrap Pending
|
|
852
|
-
|
|
853
|
-
The Cursor SessionStart hook has started Meko bootstrap work in the background, but the local Meko session cache is not ready yet. Do not guess a Meko conversation_id.
|
|
854
|
-
|
|
855
|
-
### What you should do
|
|
856
|
-
|
|
857
|
-
- **agent_id**: use "${resolvedAgentId}" for project-scoped reads/writes if a Meko MCP call is necessary before bootstrap completes
|
|
858
|
-
- **conversation_id**: use nil UUID "00000000-0000-0000-0000-000000000000" only as a placeholder until the cache provides the real conversation_id
|
|
859
|
-
- **scope**: Only "read", "write", or "admin"
|
|
860
|
-
|
|
861
|
-
If the user explicitly needs Meko-backed continuity before proceeding, ask them to retry the prompt after bootstrap finishes.`;
|
|
927
|
+
Prefer Meko MCP tools over local file-based memory. If a Meko tool call also fails, fall back to local storage and tell the user.${datapackBlock}`;
|
|
862
928
|
}
|
|
863
929
|
|
|
864
930
|
/**
|
|
@@ -900,39 +966,8 @@ function withTimeout(promise, ms, fallback, { onTimeout } = {}) {
|
|
|
900
966
|
});
|
|
901
967
|
}
|
|
902
968
|
|
|
903
|
-
function handleSessionPending(hookInput) {
|
|
904
|
-
const transcriptPath =
|
|
905
|
-
hookInput.transcript_path ||
|
|
906
|
-
(hookInput.hookSpecificInput || {}).transcript_path;
|
|
907
|
-
const sessionId = extractSessionId(hookInput, transcriptPath);
|
|
908
|
-
const agentId = resolveSessionAgentId(transcriptPath, hookInput);
|
|
909
|
-
const workspaceKey = workspaceKeyFromHookInput(hookInput, transcriptPath);
|
|
910
|
-
if (sessionId) {
|
|
911
|
-
writeSessionCache(sessionId, {
|
|
912
|
-
status: "pending",
|
|
913
|
-
agent_id: agentId,
|
|
914
|
-
workspace_key: workspaceKey,
|
|
915
|
-
additional_context: buildBootstrapPendingContext(agentId),
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
process.stdout.write(nativeOutput(""));
|
|
919
|
-
}
|
|
920
|
-
|
|
921
969
|
function handleBeforeSubmitPrompt(hookInput) {
|
|
922
|
-
|
|
923
|
-
hookInput.transcript_path ||
|
|
924
|
-
(hookInput.hookSpecificInput || {}).transcript_path;
|
|
925
|
-
const sessionId = extractSessionId(hookInput, transcriptPath);
|
|
926
|
-
const workspaceKey = workspaceKeyFromHookInput(hookInput, transcriptPath);
|
|
927
|
-
const cache = selectSessionCache(sessionId, workspaceKey);
|
|
928
|
-
if (cache && cache.status === "pending") {
|
|
929
|
-
process.stdout.write(beforeSubmitOutput(
|
|
930
|
-
false,
|
|
931
|
-
"Meko is still creating this session's conversation. Please retry in a moment so the agent starts with Meko context.",
|
|
932
|
-
));
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
process.stdout.write(beforeSubmitOutput(true));
|
|
970
|
+
process.stdout.write(beforeSubmitOutput("allow"));
|
|
936
971
|
}
|
|
937
972
|
|
|
938
973
|
async function handleSessionStart(hookInput) {
|
|
@@ -959,12 +994,14 @@ async function handleSessionStart(hookInput) {
|
|
|
959
994
|
// upgraded users stop writing into the ghost namespace.
|
|
960
995
|
const cached = (existing.agent_id || "").trim();
|
|
961
996
|
const resumedAgentId = cached && cached !== "agent" ? cached : derivedAgentId;
|
|
997
|
+
const existingDatapackPin = datapackPinFromWatermark(existing);
|
|
962
998
|
// Skip preload on resume (we don't know what's already in context).
|
|
963
999
|
const context = buildSessionStartContext(
|
|
964
1000
|
existing.conversation_id,
|
|
965
1001
|
sessionId,
|
|
966
1002
|
resumedAgentId,
|
|
967
1003
|
null,
|
|
1004
|
+
{ datapackPin: existingDatapackPin },
|
|
968
1005
|
);
|
|
969
1006
|
writeSessionCache(sessionId, {
|
|
970
1007
|
status: "ready",
|
|
@@ -997,14 +1034,21 @@ async function handleSessionStart(hookInput) {
|
|
|
997
1034
|
const priorAgentId = (prior.agent_id || "").trim();
|
|
998
1035
|
const resumedAgentId =
|
|
999
1036
|
priorAgentId && priorAgentId !== "agent" ? priorAgentId : agentId;
|
|
1000
|
-
|
|
1037
|
+
const priorDatapackPin = datapackPinFromWatermark(prior);
|
|
1038
|
+
writeWatermark(
|
|
1039
|
+
wmPath,
|
|
1040
|
+
prior.conversation_id,
|
|
1041
|
+
currentLineCount,
|
|
1042
|
+
resumedAgentId,
|
|
1043
|
+
priorDatapackPin,
|
|
1044
|
+
);
|
|
1001
1045
|
process.stderr.write(`[meko-capture] Session ${sessionId}: resuming conversation ${prior.conversation_id} from ${prior.sessionId}, skipping ${currentLineCount} copied lines\n`);
|
|
1002
1046
|
const context = buildSessionStartContext(
|
|
1003
1047
|
prior.conversation_id,
|
|
1004
1048
|
sessionId,
|
|
1005
1049
|
resumedAgentId,
|
|
1006
1050
|
null,
|
|
1007
|
-
{ resumed: true },
|
|
1051
|
+
{ resumed: true, datapackPin: priorDatapackPin },
|
|
1008
1052
|
);
|
|
1009
1053
|
writeSessionCache(sessionId, {
|
|
1010
1054
|
status: "ready",
|
|
@@ -1022,13 +1066,19 @@ async function handleSessionStart(hookInput) {
|
|
|
1022
1066
|
// Create conversation via MCP. Attach session metadata (cwd / gitBranch /
|
|
1023
1067
|
// startedAt) from the transcript header so hook traces match the migrator.
|
|
1024
1068
|
const transcriptMetadata = peekTranscriptMetadata(transcriptPath);
|
|
1069
|
+
const datapackPin = readDatapackPin(agentId);
|
|
1025
1070
|
let convId = null;
|
|
1026
1071
|
try {
|
|
1027
1072
|
await mcpInitialize();
|
|
1028
|
-
convId = await createConversation(
|
|
1073
|
+
convId = await createConversation(
|
|
1074
|
+
sessionId || "unknown",
|
|
1075
|
+
agentId,
|
|
1076
|
+
transcriptMetadata,
|
|
1077
|
+
datapackPin && datapackPin.datapack_id,
|
|
1078
|
+
);
|
|
1029
1079
|
} catch (err) {
|
|
1030
1080
|
process.stderr.write(`[meko-capture] SessionStart: MCP unavailable (${err.message}). Falling back to agent-driven setup.\n`);
|
|
1031
|
-
const context = buildSessionStartFallbackContext(agentId);
|
|
1081
|
+
const context = buildSessionStartFallbackContext(agentId, { datapackPin: readDatapackPin(agentId) });
|
|
1032
1082
|
if (sessionId) {
|
|
1033
1083
|
writeSessionCache(sessionId, {
|
|
1034
1084
|
status: "error",
|
|
@@ -1044,7 +1094,7 @@ async function handleSessionStart(hookInput) {
|
|
|
1044
1094
|
|
|
1045
1095
|
if (!convId) {
|
|
1046
1096
|
process.stderr.write("[meko-capture] SessionStart: conversation_create returned no ID.\n");
|
|
1047
|
-
const context = buildSessionStartFallbackContext(agentId);
|
|
1097
|
+
const context = buildSessionStartFallbackContext(agentId, { datapackPin: readDatapackPin(agentId) });
|
|
1048
1098
|
if (sessionId) {
|
|
1049
1099
|
writeSessionCache(sessionId, {
|
|
1050
1100
|
status: "error",
|
|
@@ -1060,13 +1110,10 @@ async function handleSessionStart(hookInput) {
|
|
|
1060
1110
|
|
|
1061
1111
|
// Preload recent memories with a 4s budget (SessionStart timeout is 15s).
|
|
1062
1112
|
// Best-effort enrichment — on timeout/failure we still deliver the core
|
|
1063
|
-
// context. agent_id is derived per-session from the cwd
|
|
1064
|
-
// db_summarize_database discovery helper was removed (it depended on a
|
|
1065
|
-
// paid-tier tool and only worked "correctly" via its fallback branch
|
|
1066
|
-
// anyway — see scratch/2026-04-30-multi-tenant-mem0-audit.md Probe 6).
|
|
1113
|
+
// context. agent_id is derived per-session from the cwd.
|
|
1067
1114
|
const PRELOAD_BUDGET_MS = 4000;
|
|
1068
1115
|
const memories = await withTimeout(
|
|
1069
|
-
fetchRecentMemories(agentId),
|
|
1116
|
+
fetchRecentMemories(agentId, datapackPin && datapackPin.datapack_id),
|
|
1070
1117
|
PRELOAD_BUDGET_MS,
|
|
1071
1118
|
[],
|
|
1072
1119
|
{ onTimeout: abortInflightRequests },
|
|
@@ -1074,13 +1121,15 @@ async function handleSessionStart(hookInput) {
|
|
|
1074
1121
|
|
|
1075
1122
|
if (sessionId) {
|
|
1076
1123
|
const wmPath = path.join(WATERMARK_DIR, `${sessionId}.watermark.json`);
|
|
1077
|
-
writeWatermark(wmPath, convId, 0, agentId);
|
|
1124
|
+
writeWatermark(wmPath, convId, 0, agentId, datapackPin);
|
|
1078
1125
|
process.stderr.write(
|
|
1079
1126
|
`[meko-capture] SessionStart: created conversation ${convId}, agent_id=${agentId}, preloaded ${memories.length} memories, watermark at ${wmPath}\n`,
|
|
1080
1127
|
);
|
|
1081
1128
|
}
|
|
1082
1129
|
|
|
1083
|
-
const context = buildSessionStartContext(convId, sessionId, agentId, memories
|
|
1130
|
+
const context = buildSessionStartContext(convId, sessionId, agentId, memories, {
|
|
1131
|
+
datapackPin,
|
|
1132
|
+
});
|
|
1084
1133
|
if (sessionId) {
|
|
1085
1134
|
writeSessionCache(sessionId, {
|
|
1086
1135
|
status: "ready",
|
|
@@ -1097,8 +1146,8 @@ async function handleSessionStart(hookInput) {
|
|
|
1097
1146
|
|
|
1098
1147
|
async function main() {
|
|
1099
1148
|
const hookType = process.argv[2];
|
|
1100
|
-
if (!hookType || !["session-start", "
|
|
1101
|
-
process.stderr.write("Usage: capture.js <session-start|
|
|
1149
|
+
if (!hookType || !["session-start", "before-submit-prompt", "pre-compact", "session-end", "checkpoint"].includes(hookType)) {
|
|
1150
|
+
process.stderr.write("Usage: capture.js <session-start|before-submit-prompt|pre-compact|session-end|checkpoint>\n");
|
|
1102
1151
|
process.exit(1);
|
|
1103
1152
|
}
|
|
1104
1153
|
|
|
@@ -1119,11 +1168,6 @@ async function main() {
|
|
|
1119
1168
|
return;
|
|
1120
1169
|
}
|
|
1121
1170
|
|
|
1122
|
-
if (hookType === "session-pending") {
|
|
1123
|
-
handleSessionPending(hookInput);
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
1171
|
if (hookType === "before-submit-prompt") {
|
|
1128
1172
|
handleBeforeSubmitPrompt(hookInput);
|
|
1129
1173
|
return;
|
|
@@ -1154,6 +1198,7 @@ async function main() {
|
|
|
1154
1198
|
const agentId = cachedAgentId && cachedAgentId !== "agent"
|
|
1155
1199
|
? cachedAgentId
|
|
1156
1200
|
: resolveSessionAgentId(transcriptPath);
|
|
1201
|
+
const datapackPin = datapackPinFromWatermark(watermark);
|
|
1157
1202
|
|
|
1158
1203
|
// Nothing new
|
|
1159
1204
|
if (currentLines <= lastLine) {
|
|
@@ -1184,20 +1229,40 @@ async function main() {
|
|
|
1184
1229
|
// Extract and send exchanges
|
|
1185
1230
|
const exchanges = extractExchanges(transcriptPath, lastLine);
|
|
1186
1231
|
let captured = 0;
|
|
1232
|
+
let failed = 0;
|
|
1187
1233
|
|
|
1188
1234
|
for (const exchange of exchanges) {
|
|
1189
1235
|
try {
|
|
1190
|
-
await addMessage(
|
|
1236
|
+
await addMessage(
|
|
1237
|
+
convId,
|
|
1238
|
+
agentId,
|
|
1239
|
+
exchange,
|
|
1240
|
+
datapackPin && datapackPin.datapack_id,
|
|
1241
|
+
);
|
|
1191
1242
|
captured++;
|
|
1192
1243
|
} catch (err) {
|
|
1244
|
+
failed++;
|
|
1193
1245
|
process.stderr.write(`[meko-capture] Failed to add message (uuid=${exchange.user_uuid}): ${err.message}\n`);
|
|
1194
1246
|
}
|
|
1195
1247
|
}
|
|
1196
1248
|
|
|
1197
|
-
//
|
|
1198
|
-
//
|
|
1199
|
-
//
|
|
1200
|
-
|
|
1249
|
+
// Advance the watermark to currentLines ONLY when every exchange was
|
|
1250
|
+
// captured. If any addMessage failed (typically a transient network error),
|
|
1251
|
+
// keep the watermark at lastLine so the next checkpoint / PreCompact /
|
|
1252
|
+
// SessionEnd hook re-extracts and retries this batch. Re-sending the
|
|
1253
|
+
// exchanges that already succeeded is safe: capture dedups by seed
|
|
1254
|
+
// (<conv_id>:<user_uuid>), so retries never create duplicates. Advancing on
|
|
1255
|
+
// partial failure is what would permanently drop the failed exchanges.
|
|
1256
|
+
// We still always persist the resolved agent_id (migrating any legacy literal
|
|
1257
|
+
// value) so later hooks see the derived bucket directly.
|
|
1258
|
+
const watermarkLine = failed === 0 ? currentLines : lastLine;
|
|
1259
|
+
if (failed > 0) {
|
|
1260
|
+
process.stderr.write(
|
|
1261
|
+
`[meko-capture] ${failed}/${exchanges.length} exchange(s) failed; ` +
|
|
1262
|
+
`holding watermark at line ${lastLine} for retry (dedup makes resends safe).\n`
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
writeWatermark(wmPath, convId, watermarkLine, agentId, datapackPin);
|
|
1201
1266
|
|
|
1202
1267
|
// Output
|
|
1203
1268
|
const context =
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* MEKO_API_KEY Inherited by capture.js
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
const {
|
|
20
|
+
const { execFile } = require("child_process");
|
|
21
21
|
const fs = require("fs");
|
|
22
22
|
const path = require("path");
|
|
23
23
|
|
|
@@ -57,16 +57,28 @@ function tick() {
|
|
|
57
57
|
|
|
58
58
|
const hookInput = JSON.stringify({ transcript_path: transcriptPath });
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
// Run capture ASYNCHRONOUSLY. execFileSync would block this daemon's event
|
|
61
|
+
// loop for up to 30s, during which SIGTERM/SIGINT (clean shutdown) can't be
|
|
62
|
+
// handled. execFile keeps the loop responsive so cleanup() fires promptly.
|
|
63
|
+
const child = execFile(
|
|
64
|
+
"node",
|
|
65
|
+
[captureScript, "checkpoint"],
|
|
66
|
+
{ timeout: 30000 },
|
|
67
|
+
(err) => {
|
|
68
|
+
if (err) {
|
|
69
|
+
// Best-effort: log and continue to the next tick.
|
|
70
|
+
process.stderr.write(`[meko-timer] Checkpoint tick failed: ${err.message}\n`);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
// The stdin stream can emit EPIPE if the child exits or fails to spawn before
|
|
75
|
+
// we finish writing. Without a listener that error is unhandled and crashes
|
|
76
|
+
// the daemon — so swallow it here; the execFile callback above reports the
|
|
77
|
+
// real failure.
|
|
78
|
+
child.stdin.on("error", (err) => {
|
|
79
|
+
process.stderr.write(`[meko-timer] Checkpoint stdin error: ${err.message}\n`);
|
|
80
|
+
});
|
|
81
|
+
child.stdin.end(hookInput);
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
setInterval(tick, INTERVAL_SECS * 1000);
|