@yugabytedb/meko-mcp 1.0.3
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 +505 -0
- package/assets/hooks-handlers/lib/capture.js +863 -0
- package/assets/hooks-handlers/lib/checkpoint-timer.js +72 -0
- package/assets/hooks-handlers/pre-compact.sh +3 -0
- package/assets/hooks-handlers/session-end.sh +27 -0
- package/assets/hooks-handlers/session-start.sh +24 -0
- package/assets/hooks-handlers/test_single_conversation.sh +169 -0
- package/assets/skills/meko-mcp-tools/SKILL.md +192 -0
- package/assets/skills/meko-mcp-tools/references/_sections.md +31 -0
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +39 -0
- package/assets/skills/meko-mcp-tools/references/tools-conversation-dedup.md +49 -0
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +373 -0
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +56 -0
- package/assets/skills/meko-mcp-tools/references/tools-db-query-patterns.md +38 -0
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +61 -0
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +128 -0
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +97 -0
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +63 -0
- package/assets/skills/meko-mcp-tools/references/tools-scope-permissions.md +79 -0
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +143 -0
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +93 -0
- package/assets/skills/meko-mcp-tools/references/tools-write-guardrails.md +30 -0
- package/bin/create-meko-setup.mjs +499 -0
- package/lib/claude-code.mjs +14 -0
- package/lib/clients/claude-code.mjs +265 -0
- package/lib/clients/claude-desktop.mjs +256 -0
- package/lib/clients/common.mjs +107 -0
- package/lib/clients/cursor.mjs +332 -0
- package/lib/clients/index.mjs +14 -0
- package/lib/config.mjs +369 -0
- package/lib/detect.mjs +125 -0
- package/lib/installer.mjs +255 -0
- package/lib/logger.mjs +97 -0
- package/lib/mcp-transport.mjs +204 -0
- package/lib/migrate/adapters/claude-code.mjs +165 -0
- package/lib/migrate/adapters/cursor.mjs +221 -0
- package/lib/migrate/adapters/memory-md.mjs +230 -0
- package/lib/migrate/claude-parser.mjs +306 -0
- package/lib/migrate/daemon.mjs +67 -0
- package/lib/migrate/id.mjs +83 -0
- package/lib/migrate/lockfile.mjs +121 -0
- package/lib/migrate/logger.mjs +79 -0
- package/lib/migrate/mcp-client.mjs +224 -0
- package/lib/migrate/orchestrator.mjs +443 -0
- package/lib/migrate/progress.mjs +76 -0
- package/lib/migrate/state.mjs +177 -0
- package/lib/migrate.mjs +328 -0
- package/lib/prompt.mjs +212 -0
- package/lib/validate.mjs +259 -0
- package/package.json +49 -0
- package/scripts/bundle-assets.mjs +49 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* capture.js — Unified conversation capture for Meko MCP plugin.
|
|
4
|
+
*
|
|
5
|
+
* Handles transcript parsing, watermark management, and MCP API calls.
|
|
6
|
+
* No external dependencies — uses only Node.js built-ins (fs, http/https, crypto).
|
|
7
|
+
*
|
|
8
|
+
* Usage (called by hook shell wrappers):
|
|
9
|
+
* node capture.js session-start # creates conversation + watermark, outputs additionalContext
|
|
10
|
+
* node capture.js pre-compact # reads hook input from stdin
|
|
11
|
+
* node capture.js session-end # reads hook input from stdin
|
|
12
|
+
*
|
|
13
|
+
* Environment:
|
|
14
|
+
* MEKO_MCP_URL MCP server URL (default: http://localhost:8000/mcp)
|
|
15
|
+
* MEKO_AGENT_ID Agent identifier (default: claude_code)
|
|
16
|
+
* MEKO_API_KEY API key for Cloud Meko auth (optional, omit for local)
|
|
17
|
+
* MEKO_API_TIMEOUT Request timeout in seconds (default: 10)
|
|
18
|
+
* MEKO_WATERMARK_DIR Watermark directory (default: ~/.claude/meko-capture)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require("fs");
|
|
22
|
+
const path = require("path");
|
|
23
|
+
const http = require("http");
|
|
24
|
+
const https = require("https");
|
|
25
|
+
|
|
26
|
+
// --- Config ---
|
|
27
|
+
|
|
28
|
+
const MEKO_MCP_URL = process.env.MEKO_MCP_URL || "http://localhost:8000/mcp";
|
|
29
|
+
const MEKO_AGENT_ID = process.env.MEKO_AGENT_ID || "claude_code";
|
|
30
|
+
const MEKO_API_KEY = process.env.MEKO_API_KEY || "";
|
|
31
|
+
const MEKO_API_TIMEOUT = parseInt(process.env.MEKO_API_TIMEOUT || "10", 10) * 1000;
|
|
32
|
+
const WATERMARK_DIR =
|
|
33
|
+
process.env.MEKO_WATERMARK_DIR ||
|
|
34
|
+
path.join(process.env.HOME || "~", ".claude", "meko-capture");
|
|
35
|
+
|
|
36
|
+
let mcpRequestId = 0;
|
|
37
|
+
let mcpSessionId = null; // MCP Streamable HTTP session ID
|
|
38
|
+
const inflightRequests = new Set(); // tracked so preload timeouts can abort
|
|
39
|
+
|
|
40
|
+
// --- Transcript parsing ---
|
|
41
|
+
|
|
42
|
+
function countLines(filePath) {
|
|
43
|
+
const buf = fs.readFileSync(filePath);
|
|
44
|
+
let count = 0;
|
|
45
|
+
for (let i = 0; i < buf.length; i++) {
|
|
46
|
+
if (buf[i] === 0x0a) count++;
|
|
47
|
+
}
|
|
48
|
+
if (buf.length > 0 && buf[buf.length - 1] !== 0x0a) count++;
|
|
49
|
+
return count;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- Truncation limits (configurable via env) ---
|
|
53
|
+
|
|
54
|
+
const MAX_THINKING_LEN = parseInt(process.env.MEKO_MAX_THINKING_LEN || "2000", 10);
|
|
55
|
+
const MAX_TOOL_INPUT_LEN = parseInt(process.env.MEKO_MAX_TOOL_INPUT_LEN || "500", 10);
|
|
56
|
+
const MAX_TOOL_RESULT_LEN = parseInt(process.env.MEKO_MAX_TOOL_RESULT_LEN || "3000", 10);
|
|
57
|
+
|
|
58
|
+
function truncate(str, max) {
|
|
59
|
+
if (!str || str.length <= max) return str;
|
|
60
|
+
return str.slice(0, max) + "... [truncated]";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Content extraction helpers ---
|
|
64
|
+
|
|
65
|
+
function extractText(content) {
|
|
66
|
+
if (typeof content === "string") return content;
|
|
67
|
+
if (Array.isArray(content)) {
|
|
68
|
+
return content
|
|
69
|
+
.filter((b) => typeof b === "string" || (b && b.type === "text"))
|
|
70
|
+
.map((b) => (typeof b === "string" ? b : b.text || ""))
|
|
71
|
+
.join("\n");
|
|
72
|
+
}
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractThinking(contentBlocks) {
|
|
77
|
+
if (!Array.isArray(contentBlocks)) return "";
|
|
78
|
+
const parts = [];
|
|
79
|
+
for (const block of contentBlocks) {
|
|
80
|
+
if (block && block.type === "thinking" && block.thinking) {
|
|
81
|
+
parts.push(truncate(block.thinking, MAX_THINKING_LEN));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return parts.join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractToolCalls(contentBlocks) {
|
|
88
|
+
if (!Array.isArray(contentBlocks)) return "";
|
|
89
|
+
const parts = [];
|
|
90
|
+
for (const block of contentBlocks) {
|
|
91
|
+
if (block && block.type === "tool_use") {
|
|
92
|
+
const name = block.name || "unknown";
|
|
93
|
+
let inputStr = "";
|
|
94
|
+
if (block.input && typeof block.input === "object") {
|
|
95
|
+
try {
|
|
96
|
+
inputStr = truncate(JSON.stringify(block.input), MAX_TOOL_INPUT_LEN);
|
|
97
|
+
} catch {
|
|
98
|
+
inputStr = "(unserializable)";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
parts.push(`TOOL CALL: ${name}(${inputStr})`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return parts.join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isToolResultMessage(msg) {
|
|
108
|
+
if (msg.toolUseResult != null) return true;
|
|
109
|
+
const content = (msg.message || {}).content;
|
|
110
|
+
if (Array.isArray(content)) {
|
|
111
|
+
return content.some((b) => b && b.type === "tool_result");
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function extractToolResultContent(msg) {
|
|
117
|
+
const parts = [];
|
|
118
|
+
const content = (msg.message || {}).content;
|
|
119
|
+
if (Array.isArray(content)) {
|
|
120
|
+
for (const block of content) {
|
|
121
|
+
if (block && block.type === "tool_result") {
|
|
122
|
+
const resultContent = block.content;
|
|
123
|
+
if (typeof resultContent === "string") {
|
|
124
|
+
parts.push(truncate(resultContent, MAX_TOOL_RESULT_LEN));
|
|
125
|
+
} else if (Array.isArray(resultContent)) {
|
|
126
|
+
const text = extractText(resultContent);
|
|
127
|
+
if (text) parts.push(truncate(text, MAX_TOOL_RESULT_LEN));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Fallback to toolUseResult summary if no content blocks found
|
|
133
|
+
if (parts.length === 0 && msg.toolUseResult != null) {
|
|
134
|
+
const summary =
|
|
135
|
+
typeof msg.toolUseResult === "string"
|
|
136
|
+
? msg.toolUseResult
|
|
137
|
+
: JSON.stringify(msg.toolUseResult);
|
|
138
|
+
parts.push(truncate(summary, MAX_TOOL_RESULT_LEN));
|
|
139
|
+
}
|
|
140
|
+
return parts.length > 0 ? "TOOL RESULT:\n" + parts.join("\n") : "";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- Turn-assembly exchange extraction ---
|
|
144
|
+
|
|
145
|
+
function extractExchanges(filePath, startLine) {
|
|
146
|
+
const data = fs.readFileSync(filePath, "utf-8");
|
|
147
|
+
const lines = data.split("\n");
|
|
148
|
+
|
|
149
|
+
// Step 1: Parse relevant lines
|
|
150
|
+
const entries = [];
|
|
151
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
152
|
+
const line = lines[i].trim();
|
|
153
|
+
if (!line) continue;
|
|
154
|
+
try {
|
|
155
|
+
const obj = JSON.parse(line);
|
|
156
|
+
if (obj.type === "user" || obj.type === "assistant") {
|
|
157
|
+
entries.push(obj);
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Step 2: Group into exchanges using turn-assembly.
|
|
165
|
+
// A real user message (no toolUseResult) starts a new exchange.
|
|
166
|
+
// All subsequent assistant lines and tool-result user lines belong
|
|
167
|
+
// to that exchange until the next real user message.
|
|
168
|
+
const exchanges = [];
|
|
169
|
+
let current = null;
|
|
170
|
+
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (entry.type === "user" && !isToolResultMessage(entry)) {
|
|
173
|
+
if (current) exchanges.push(current);
|
|
174
|
+
current = {
|
|
175
|
+
userMsg: entry,
|
|
176
|
+
assistantEntries: [],
|
|
177
|
+
toolResultEntries: [],
|
|
178
|
+
};
|
|
179
|
+
} else if (current) {
|
|
180
|
+
if (entry.type === "assistant") {
|
|
181
|
+
current.assistantEntries.push(entry);
|
|
182
|
+
} else if (entry.type === "user" && isToolResultMessage(entry)) {
|
|
183
|
+
current.toolResultEntries.push(entry);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (current) exchanges.push(current);
|
|
188
|
+
|
|
189
|
+
// Step 3: Build output for each exchange
|
|
190
|
+
const skipPattern =
|
|
191
|
+
/^<(system-reminder|command-|local-command|available-deferred)/;
|
|
192
|
+
const results = [];
|
|
193
|
+
|
|
194
|
+
for (const ex of exchanges) {
|
|
195
|
+
const input = extractText((ex.userMsg.message || {}).content);
|
|
196
|
+
if (!input || skipPattern.test(input)) continue;
|
|
197
|
+
if (ex.assistantEntries.length === 0) continue;
|
|
198
|
+
|
|
199
|
+
const outputParts = [];
|
|
200
|
+
const reasoningParts = [];
|
|
201
|
+
|
|
202
|
+
for (const aEntry of ex.assistantEntries) {
|
|
203
|
+
const content = (aEntry.message || {}).content;
|
|
204
|
+
if (!Array.isArray(content)) continue;
|
|
205
|
+
|
|
206
|
+
const text = extractText(content);
|
|
207
|
+
if (text) outputParts.push(text);
|
|
208
|
+
|
|
209
|
+
const thinking = extractThinking(content);
|
|
210
|
+
if (thinking) reasoningParts.push("THINKING:\n" + thinking);
|
|
211
|
+
|
|
212
|
+
const toolCalls = extractToolCalls(content);
|
|
213
|
+
if (toolCalls) reasoningParts.push(toolCalls);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const trEntry of ex.toolResultEntries) {
|
|
217
|
+
const resultText = extractToolResultContent(trEntry);
|
|
218
|
+
if (resultText) reasoningParts.push(resultText);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
results.push({
|
|
222
|
+
user_uuid: ex.userMsg.uuid || "unknown",
|
|
223
|
+
input: input,
|
|
224
|
+
output: outputParts.join("\n\n"),
|
|
225
|
+
reasoning: reasoningParts.join("\n---\n"),
|
|
226
|
+
timestamp:
|
|
227
|
+
ex.assistantEntries[ex.assistantEntries.length - 1].timestamp ||
|
|
228
|
+
ex.userMsg.timestamp ||
|
|
229
|
+
"",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return results;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// --- Watermark ---
|
|
237
|
+
|
|
238
|
+
function readWatermark(wmPath) {
|
|
239
|
+
try {
|
|
240
|
+
return JSON.parse(fs.readFileSync(wmPath, "utf-8"));
|
|
241
|
+
} catch {
|
|
242
|
+
return { conversation_id: "", last_line_number: 0, agent_id: "" };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function writeWatermark(wmPath, convId, lineNum, agentId) {
|
|
247
|
+
fs.mkdirSync(path.dirname(wmPath), { recursive: true });
|
|
248
|
+
const existing = readWatermark(wmPath);
|
|
249
|
+
fs.writeFileSync(
|
|
250
|
+
wmPath,
|
|
251
|
+
JSON.stringify({
|
|
252
|
+
conversation_id: convId,
|
|
253
|
+
last_line_number: lineNum,
|
|
254
|
+
agent_id: agentId != null ? agentId : existing.agent_id || "",
|
|
255
|
+
updated_at: new Date().toISOString(),
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// --- MCP JSON-RPC client (Streamable HTTP with session init) ---
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Low-level HTTP POST to the MCP endpoint.
|
|
264
|
+
* Returns { body: <parsed JSON>, headers: <response headers> }.
|
|
265
|
+
*/
|
|
266
|
+
function mcpPost(jsonBody) {
|
|
267
|
+
const body = JSON.stringify(jsonBody);
|
|
268
|
+
return new Promise((resolve, reject) => {
|
|
269
|
+
const url = new URL(MEKO_MCP_URL);
|
|
270
|
+
const transport = url.protocol === "https:" ? https : http;
|
|
271
|
+
const headers = {
|
|
272
|
+
"Content-Type": "application/json",
|
|
273
|
+
Accept: "application/json, text/event-stream",
|
|
274
|
+
"Content-Length": Buffer.byteLength(body),
|
|
275
|
+
};
|
|
276
|
+
if (MEKO_API_KEY) {
|
|
277
|
+
headers["API_KEY"] = MEKO_API_KEY;
|
|
278
|
+
}
|
|
279
|
+
if (mcpSessionId) {
|
|
280
|
+
headers["Mcp-Session-Id"] = mcpSessionId;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const req = transport.request(
|
|
284
|
+
{
|
|
285
|
+
hostname: url.hostname,
|
|
286
|
+
port: url.port,
|
|
287
|
+
path: url.pathname + url.search,
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers,
|
|
290
|
+
timeout: MEKO_API_TIMEOUT,
|
|
291
|
+
},
|
|
292
|
+
(res) => {
|
|
293
|
+
let data = "";
|
|
294
|
+
res.on("data", (chunk) => (data += chunk));
|
|
295
|
+
res.on("end", () => {
|
|
296
|
+
inflightRequests.delete(req);
|
|
297
|
+
// Notifications return 202/204 with no body — that's OK
|
|
298
|
+
if (!data.trim()) {
|
|
299
|
+
resolve({ body: null, headers: res.headers });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const parsed = JSON.parse(data);
|
|
304
|
+
if (parsed.error) {
|
|
305
|
+
reject(new Error(JSON.stringify(parsed.error)));
|
|
306
|
+
} else {
|
|
307
|
+
resolve({ body: parsed, headers: res.headers });
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
reject(new Error(`Invalid JSON response: ${data.slice(0, 200)}`));
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
inflightRequests.add(req);
|
|
317
|
+
req.on("error", (err) => {
|
|
318
|
+
inflightRequests.delete(req);
|
|
319
|
+
reject(err);
|
|
320
|
+
});
|
|
321
|
+
req.on("timeout", () => {
|
|
322
|
+
inflightRequests.delete(req);
|
|
323
|
+
req.destroy();
|
|
324
|
+
reject(new Error("MCP request timed out"));
|
|
325
|
+
});
|
|
326
|
+
req.write(body);
|
|
327
|
+
req.end();
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Abort all pending MCP requests. Used by the preload timeout path so the
|
|
333
|
+
* SessionStart hook can return promptly instead of waiting for a slow server
|
|
334
|
+
* to finish responding.
|
|
335
|
+
*/
|
|
336
|
+
function abortInflightRequests() {
|
|
337
|
+
for (const req of inflightRequests) {
|
|
338
|
+
try {
|
|
339
|
+
req.destroy();
|
|
340
|
+
} catch {
|
|
341
|
+
// no-op
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
inflightRequests.clear();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Initialize MCP session — required before any tools/call.
|
|
349
|
+
* Sends initialize + notifications/initialized per Streamable HTTP spec.
|
|
350
|
+
*/
|
|
351
|
+
async function mcpInitialize() {
|
|
352
|
+
mcpRequestId++;
|
|
353
|
+
const initResult = await mcpPost({
|
|
354
|
+
jsonrpc: "2.0",
|
|
355
|
+
method: "initialize",
|
|
356
|
+
params: {
|
|
357
|
+
protocolVersion: "2025-03-26",
|
|
358
|
+
capabilities: {},
|
|
359
|
+
clientInfo: { name: "meko-capture", version: "1.0.0" },
|
|
360
|
+
},
|
|
361
|
+
id: mcpRequestId,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Extract session ID from response header
|
|
365
|
+
const sid =
|
|
366
|
+
initResult.headers["mcp-session-id"] ||
|
|
367
|
+
initResult.headers["Mcp-Session-Id"];
|
|
368
|
+
if (sid) {
|
|
369
|
+
mcpSessionId = sid;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Send initialized notification (fire-and-forget, no id)
|
|
373
|
+
await mcpPost({
|
|
374
|
+
jsonrpc: "2.0",
|
|
375
|
+
method: "notifications/initialized",
|
|
376
|
+
params: {},
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function mcpCall(toolName, args) {
|
|
381
|
+
mcpRequestId++;
|
|
382
|
+
const result = await mcpPost({
|
|
383
|
+
jsonrpc: "2.0",
|
|
384
|
+
method: "tools/call",
|
|
385
|
+
params: { name: toolName, arguments: args },
|
|
386
|
+
id: mcpRequestId,
|
|
387
|
+
});
|
|
388
|
+
return result.body;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function extractToolResult(response) {
|
|
392
|
+
try {
|
|
393
|
+
const text = response.result.content[0].text;
|
|
394
|
+
return JSON.parse(text);
|
|
395
|
+
} catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Discover the user's agent_id namespace from existing Mem0 collections.
|
|
402
|
+
*
|
|
403
|
+
* Mirrors the protocol in skills/skills/meko-mcp-tools/SKILL.md lines 41-49:
|
|
404
|
+
* call `db_summarize_database`, scan for `<agent>_mem0_collection` tables,
|
|
405
|
+
* pick the one with the highest `row_count`. Falls back to "agent" when the
|
|
406
|
+
* user has no prior memories. Never returns null — always returns a usable
|
|
407
|
+
* string.
|
|
408
|
+
*
|
|
409
|
+
* Cached in the watermark file so subsequent SessionStart hooks skip the
|
|
410
|
+
* `db_summarize_database` round-trip.
|
|
411
|
+
*/
|
|
412
|
+
async function discoverAgentId() {
|
|
413
|
+
const response = await mcpCall("db_summarize_database", {
|
|
414
|
+
scope: "read",
|
|
415
|
+
conversation_id: "00000000-0000-0000-0000-000000000000",
|
|
416
|
+
});
|
|
417
|
+
const result = extractToolResult(response);
|
|
418
|
+
if (!result) return "agent";
|
|
419
|
+
|
|
420
|
+
// The tool may return tables in several shapes depending on server version.
|
|
421
|
+
// Defensive: look for any array of { name, row_count } under common keys.
|
|
422
|
+
const tables = []
|
|
423
|
+
.concat(result.tables || [])
|
|
424
|
+
.concat(result.schemas?.flatMap?.((s) => s.tables || []) || []);
|
|
425
|
+
if (!Array.isArray(tables) || tables.length === 0) return "agent";
|
|
426
|
+
|
|
427
|
+
let best = { agent: "", rows: -1 };
|
|
428
|
+
for (const t of tables) {
|
|
429
|
+
const name = t.name || t.table_name || "";
|
|
430
|
+
const rows = Number(t.row_count ?? t.rows ?? 0);
|
|
431
|
+
const m = /^(.+)_mem0_collection$/.exec(name);
|
|
432
|
+
if (!m) continue;
|
|
433
|
+
if (rows > best.rows) {
|
|
434
|
+
best = { agent: m[1], rows };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return best.rows > 0 ? best.agent : "agent";
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Fetch recent memories for the chosen agent_id namespace.
|
|
442
|
+
*
|
|
443
|
+
* Returns an array of short summaries (one per memory), truncated to a global
|
|
444
|
+
* byte budget so the SessionStart `additionalContext` payload stays compact.
|
|
445
|
+
* On any error the caller falls back to an empty list — memory preload is a
|
|
446
|
+
* best-effort enrichment, not a correctness requirement.
|
|
447
|
+
*/
|
|
448
|
+
async function fetchRecentMemories(agentId, { limit = 10, budget = 2000 } = {}) {
|
|
449
|
+
const response = await mcpCall("memory_get_all", {
|
|
450
|
+
scope: "read",
|
|
451
|
+
agent_id: agentId,
|
|
452
|
+
conversation_id: "00000000-0000-0000-0000-000000000000",
|
|
453
|
+
limit,
|
|
454
|
+
});
|
|
455
|
+
const result = extractToolResult(response);
|
|
456
|
+
if (!result) return [];
|
|
457
|
+
|
|
458
|
+
const entries = Array.isArray(result) ? result : result.memories || result.results || [];
|
|
459
|
+
const summaries = [];
|
|
460
|
+
let used = 0;
|
|
461
|
+
for (const entry of entries) {
|
|
462
|
+
const text = entry.memory || entry.text || entry.content || "";
|
|
463
|
+
if (!text) continue;
|
|
464
|
+
const line = `- ${String(text).replace(/\s+/g, " ").trim()}`;
|
|
465
|
+
if (used + line.length + 1 > budget) break;
|
|
466
|
+
summaries.push(line);
|
|
467
|
+
used += line.length + 1;
|
|
468
|
+
}
|
|
469
|
+
return summaries;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function createConversation(sessionId) {
|
|
473
|
+
const response = await mcpCall("conversation_create", {
|
|
474
|
+
scope: "write",
|
|
475
|
+
agent_id: MEKO_AGENT_ID,
|
|
476
|
+
title: "Claude Code session (auto-captured)",
|
|
477
|
+
session_id: sessionId,
|
|
478
|
+
});
|
|
479
|
+
const result = extractToolResult(response);
|
|
480
|
+
return result && result.id ? result.id : null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function addMessage(convId, exchange) {
|
|
484
|
+
const seed = `${convId}:${exchange.user_uuid}`;
|
|
485
|
+
await mcpCall("conversation_add_message", {
|
|
486
|
+
scope: "write",
|
|
487
|
+
conversation_id: convId,
|
|
488
|
+
agent_id: MEKO_AGENT_ID,
|
|
489
|
+
input: exchange.input,
|
|
490
|
+
output: exchange.output,
|
|
491
|
+
reasoning: exchange.reasoning,
|
|
492
|
+
seed: seed,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// --- Hook output ---
|
|
497
|
+
|
|
498
|
+
function hookEventName(hookType) {
|
|
499
|
+
switch (hookType) {
|
|
500
|
+
case "session-start": return "SessionStart";
|
|
501
|
+
case "pre-compact": return "PreCompact";
|
|
502
|
+
default: return "SessionEnd";
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function hookOutput(additionalContext) {
|
|
507
|
+
return JSON.stringify({
|
|
508
|
+
hookSpecificOutput: {
|
|
509
|
+
hookEventName: hookEventName(process.argv[2]),
|
|
510
|
+
additionalContext: additionalContext || "",
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// --- Main ---
|
|
516
|
+
|
|
517
|
+
// --- SessionStart: create conversation + watermark deterministically ---
|
|
518
|
+
|
|
519
|
+
function findMostRecentProjectWatermark(transcriptPath, currentSessionId) {
|
|
520
|
+
try {
|
|
521
|
+
const projectDir = path.dirname(transcriptPath);
|
|
522
|
+
const files = fs.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl"));
|
|
523
|
+
let best = null;
|
|
524
|
+
for (const f of files) {
|
|
525
|
+
const sid = path.basename(f, ".jsonl");
|
|
526
|
+
if (sid === currentSessionId) continue;
|
|
527
|
+
const wmPath = path.join(WATERMARK_DIR, `${sid}.watermark.json`);
|
|
528
|
+
if (!fs.existsSync(wmPath)) continue;
|
|
529
|
+
const wm = readWatermark(wmPath);
|
|
530
|
+
if (!wm.conversation_id) continue;
|
|
531
|
+
const ts = wm.updated_at || "";
|
|
532
|
+
if (!best || ts > best.updated_at) {
|
|
533
|
+
best = {
|
|
534
|
+
sessionId: sid,
|
|
535
|
+
conversation_id: wm.conversation_id,
|
|
536
|
+
agent_id: wm.agent_id || "",
|
|
537
|
+
updated_at: ts,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return best;
|
|
542
|
+
} catch (err) {
|
|
543
|
+
process.stderr.write(`[meko-capture] findMostRecentProjectWatermark failed: ${err.message}\n`);
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function buildSessionStartContext(convId, sessionId, agentId, memories, opts) {
|
|
549
|
+
const resolvedAgentId = agentId || "claude_code";
|
|
550
|
+
const resumed = Boolean(opts && opts.resumed);
|
|
551
|
+
const opening = resumed
|
|
552
|
+
? `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.`
|
|
553
|
+
: `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.`;
|
|
554
|
+
const memoryBlock =
|
|
555
|
+
Array.isArray(memories) && memories.length > 0
|
|
556
|
+
? `
|
|
557
|
+
|
|
558
|
+
### Memories from prior sessions
|
|
559
|
+
|
|
560
|
+
These facts were preloaded for you — inspect them before acting so you don't re-ask questions the user has already answered:
|
|
561
|
+
|
|
562
|
+
${memories.join("\n")}`
|
|
563
|
+
: "";
|
|
564
|
+
|
|
565
|
+
return `## Meko Session Active
|
|
566
|
+
|
|
567
|
+
${opening}
|
|
568
|
+
|
|
569
|
+
### What you MUST do with Meko tools
|
|
570
|
+
|
|
571
|
+
- **agent_id**: use "${resolvedAgentId}" (discovered from the user's existing namespace — DO NOT substitute "claude_code" just because you're running in Claude Code; see the meko-mcp-tools skill for why).
|
|
572
|
+
- **conversation_id**: Use "${convId}" for all MCP tool calls that accept it
|
|
573
|
+
- **scope**: Only "read", "write", or "admin"
|
|
574
|
+
|
|
575
|
+
### Proactive storage (do NOT wait to be asked)
|
|
576
|
+
|
|
577
|
+
- User shares personal info, preferences, or role → call memory_add immediately
|
|
578
|
+
- User shares org conventions or team standards → call memory_add immediately
|
|
579
|
+
- User asks "what do you know about X?" → call memory_search first
|
|
580
|
+
- User provides structured/tabular data → call db_run_write_query (CREATE TABLE + INSERT)
|
|
581
|
+
|
|
582
|
+
### Before destructive actions
|
|
583
|
+
|
|
584
|
+
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.
|
|
585
|
+
|
|
586
|
+
### Use Meko, not local storage
|
|
587
|
+
|
|
588
|
+
When Meko MCP tools are available, ALWAYS use them instead of local file-based memory (e.g., writing to ~/.claude/projects/.../memory/). If a Meko tool call fails, fall back to local storage and tell the user.${memoryBlock}`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function buildSessionStartFallbackContext() {
|
|
592
|
+
return `## Meko Memory Instructions
|
|
593
|
+
|
|
594
|
+
The SessionStart hook could not reach the Meko MCP server to create a conversation. Automatic capture may not work for this session.
|
|
595
|
+
|
|
596
|
+
### What you should still do
|
|
597
|
+
|
|
598
|
+
- **Use Meko MCP tools** for memory_add, memory_search, etc. when available
|
|
599
|
+
- **agent_id**: ALWAYS use "claude_code" (underscore, NEVER hyphens)
|
|
600
|
+
- **conversation_id**: Use nil UUID "00000000-0000-0000-0000-000000000000" as placeholder
|
|
601
|
+
- **scope**: Only "read", "write", or "admin"
|
|
602
|
+
|
|
603
|
+
### Proactive storage (do NOT wait to be asked)
|
|
604
|
+
|
|
605
|
+
- User shares personal info, preferences, or role → call memory_add immediately
|
|
606
|
+
- User shares org conventions or team standards → call memory_add immediately
|
|
607
|
+
- User asks "what do you know about X?" → call memory_search first
|
|
608
|
+
|
|
609
|
+
### Use Meko, not local storage
|
|
610
|
+
|
|
611
|
+
When Meko MCP tools are available, ALWAYS use them instead of local file-based memory. If a Meko tool call fails, fall back to local storage and tell the user.`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Race a promise against a timeout; on timeout resolve to the fallback value.
|
|
616
|
+
* Memory preload is best-effort — if it stalls, the SessionStart hook must
|
|
617
|
+
* still deliver the conversation + instructions under the 15 s harness budget.
|
|
618
|
+
*/
|
|
619
|
+
function withTimeout(promise, ms, fallback, { onTimeout } = {}) {
|
|
620
|
+
return new Promise((resolve) => {
|
|
621
|
+
let settled = false;
|
|
622
|
+
const timer = setTimeout(() => {
|
|
623
|
+
if (!settled) {
|
|
624
|
+
settled = true;
|
|
625
|
+
if (typeof onTimeout === "function") {
|
|
626
|
+
try {
|
|
627
|
+
onTimeout();
|
|
628
|
+
} catch {
|
|
629
|
+
// no-op
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
resolve(fallback);
|
|
633
|
+
}
|
|
634
|
+
}, ms);
|
|
635
|
+
promise
|
|
636
|
+
.then((value) => {
|
|
637
|
+
if (!settled) {
|
|
638
|
+
settled = true;
|
|
639
|
+
clearTimeout(timer);
|
|
640
|
+
resolve(value);
|
|
641
|
+
}
|
|
642
|
+
})
|
|
643
|
+
.catch(() => {
|
|
644
|
+
if (!settled) {
|
|
645
|
+
settled = true;
|
|
646
|
+
clearTimeout(timer);
|
|
647
|
+
resolve(fallback);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function handleSessionStart(hookInput) {
|
|
654
|
+
const transcriptPath =
|
|
655
|
+
hookInput.transcript_path ||
|
|
656
|
+
(hookInput.hookSpecificInput || {}).transcript_path;
|
|
657
|
+
|
|
658
|
+
const sessionId = transcriptPath
|
|
659
|
+
? path.basename(transcriptPath, ".jsonl")
|
|
660
|
+
: null;
|
|
661
|
+
|
|
662
|
+
const source = hookInput.source || "";
|
|
663
|
+
|
|
664
|
+
// Check if watermark already exists (same session ID re-fires SessionStart, e.g. /clear, /compact)
|
|
665
|
+
if (sessionId) {
|
|
666
|
+
const wmPath = path.join(WATERMARK_DIR, `${sessionId}.watermark.json`);
|
|
667
|
+
const existing = readWatermark(wmPath);
|
|
668
|
+
if (existing.conversation_id) {
|
|
669
|
+
process.stderr.write(`[meko-capture] Session ${sessionId}: reusing existing conversation ${existing.conversation_id}\n`);
|
|
670
|
+
// Reuse cached agent_id + skip preload on resume (we don't know what's
|
|
671
|
+
// already in context). The skill still has the discovery protocol as a
|
|
672
|
+
// safety net.
|
|
673
|
+
process.stdout.write(
|
|
674
|
+
hookOutput(
|
|
675
|
+
buildSessionStartContext(
|
|
676
|
+
existing.conversation_id,
|
|
677
|
+
sessionId,
|
|
678
|
+
existing.agent_id || "",
|
|
679
|
+
null,
|
|
680
|
+
),
|
|
681
|
+
),
|
|
682
|
+
);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Resume: Claude Code created a new session ID for a resumed thread. Find the most recent
|
|
688
|
+
// prior watermark for this project and reuse its conversation_id so continuity is preserved.
|
|
689
|
+
// Initialize last_line_number to the current line count so the copied history (which Claude
|
|
690
|
+
// Code replays into the new JSONL) is not re-ingested.
|
|
691
|
+
if (source === "resume" && transcriptPath && sessionId) {
|
|
692
|
+
const prior = findMostRecentProjectWatermark(transcriptPath, sessionId);
|
|
693
|
+
if (prior && prior.conversation_id) {
|
|
694
|
+
const currentLineCount = fs.existsSync(transcriptPath) ? countLines(transcriptPath) : 0;
|
|
695
|
+
const wmPath = path.join(WATERMARK_DIR, `${sessionId}.watermark.json`);
|
|
696
|
+
writeWatermark(wmPath, prior.conversation_id, currentLineCount);
|
|
697
|
+
process.stderr.write(`[meko-capture] Session ${sessionId}: resuming conversation ${prior.conversation_id} from ${prior.sessionId}, skipping ${currentLineCount} copied lines\n`);
|
|
698
|
+
process.stdout.write(
|
|
699
|
+
hookOutput(
|
|
700
|
+
buildSessionStartContext(
|
|
701
|
+
prior.conversation_id,
|
|
702
|
+
sessionId,
|
|
703
|
+
prior.agent_id || "",
|
|
704
|
+
null,
|
|
705
|
+
{ resumed: true },
|
|
706
|
+
),
|
|
707
|
+
),
|
|
708
|
+
);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
process.stderr.write(`[meko-capture] Session ${sessionId}: source=resume but no prior project watermark found; creating new conversation\n`);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Create conversation via MCP
|
|
715
|
+
let convId = null;
|
|
716
|
+
try {
|
|
717
|
+
await mcpInitialize();
|
|
718
|
+
convId = await createConversation(sessionId || "unknown");
|
|
719
|
+
} catch (err) {
|
|
720
|
+
process.stderr.write(`[meko-capture] SessionStart: MCP unavailable (${err.message}). Falling back to agent-driven setup.\n`);
|
|
721
|
+
process.stdout.write(hookOutput(buildSessionStartFallbackContext()));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (!convId) {
|
|
726
|
+
process.stderr.write("[meko-capture] SessionStart: conversation_create returned no ID.\n");
|
|
727
|
+
process.stdout.write(hookOutput(buildSessionStartFallbackContext()));
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Discover agent_id + preload memories with a combined 4s budget (SessionStart
|
|
732
|
+
// timeout is 15s). Both are enrichment — on timeout/failure we still deliver
|
|
733
|
+
// the core SessionStart context with the MEKO_AGENT_ID default.
|
|
734
|
+
const PRELOAD_BUDGET_MS = 4000;
|
|
735
|
+
const preloadResult = await withTimeout(
|
|
736
|
+
(async () => {
|
|
737
|
+
const agentId = await discoverAgentId();
|
|
738
|
+
const memories = await fetchRecentMemories(agentId);
|
|
739
|
+
return { agentId, memories };
|
|
740
|
+
})(),
|
|
741
|
+
PRELOAD_BUDGET_MS,
|
|
742
|
+
{ agentId: MEKO_AGENT_ID, memories: [] },
|
|
743
|
+
{ onTimeout: abortInflightRequests },
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
// Write watermark (now cachinG agent_id too)
|
|
747
|
+
if (sessionId) {
|
|
748
|
+
const wmPath = path.join(WATERMARK_DIR, `${sessionId}.watermark.json`);
|
|
749
|
+
writeWatermark(wmPath, convId, 0, preloadResult.agentId);
|
|
750
|
+
process.stderr.write(
|
|
751
|
+
`[meko-capture] SessionStart: created conversation ${convId}, agent_id=${preloadResult.agentId}, preloaded ${preloadResult.memories.length} memories, watermark at ${wmPath}\n`,
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
process.stdout.write(
|
|
756
|
+
hookOutput(
|
|
757
|
+
buildSessionStartContext(
|
|
758
|
+
convId,
|
|
759
|
+
sessionId,
|
|
760
|
+
preloadResult.agentId,
|
|
761
|
+
preloadResult.memories,
|
|
762
|
+
),
|
|
763
|
+
),
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// --- Main ---
|
|
768
|
+
|
|
769
|
+
async function main() {
|
|
770
|
+
const hookType = process.argv[2];
|
|
771
|
+
if (!hookType || !["session-start", "pre-compact", "session-end", "checkpoint"].includes(hookType)) {
|
|
772
|
+
process.stderr.write("Usage: capture.js <session-start|pre-compact|session-end|checkpoint>\n");
|
|
773
|
+
process.exit(1);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Read hook input from stdin
|
|
777
|
+
let hookInput;
|
|
778
|
+
try {
|
|
779
|
+
const stdin = fs.readFileSync(0, "utf-8");
|
|
780
|
+
hookInput = JSON.parse(stdin);
|
|
781
|
+
} catch (err) {
|
|
782
|
+
process.stderr.write(`[meko-capture] Failed to parse hook input from stdin: ${err.message}\n`);
|
|
783
|
+
process.stdout.write(hookOutput(""));
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// SessionStart: create conversation + watermark, output context
|
|
788
|
+
if (hookType === "session-start") {
|
|
789
|
+
await handleSessionStart(hookInput);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// PreCompact / SessionEnd: capture transcript exchanges
|
|
794
|
+
const transcriptPath =
|
|
795
|
+
hookInput.transcript_path ||
|
|
796
|
+
(hookInput.hookSpecificInput || {}).transcript_path;
|
|
797
|
+
|
|
798
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) {
|
|
799
|
+
process.stdout.write(hookOutput(""));
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const sessionId = path.basename(transcriptPath, ".jsonl");
|
|
804
|
+
const wmPath = path.join(WATERMARK_DIR, `${sessionId}.watermark.json`);
|
|
805
|
+
const watermark = readWatermark(wmPath);
|
|
806
|
+
let convId = watermark.conversation_id || "";
|
|
807
|
+
const lastLine = watermark.last_line_number || 0;
|
|
808
|
+
const currentLines = countLines(transcriptPath);
|
|
809
|
+
|
|
810
|
+
// Nothing new
|
|
811
|
+
if (currentLines <= lastLine) {
|
|
812
|
+
process.stdout.write(hookOutput(""));
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Initialize MCP session
|
|
817
|
+
try {
|
|
818
|
+
await mcpInitialize();
|
|
819
|
+
} catch (err) {
|
|
820
|
+
process.stderr.write(`[meko-capture] Failed to initialize MCP session: ${err.message}\n`);
|
|
821
|
+
process.stdout.write(hookOutput(""));
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Only SessionStart creates conversations. All other modes require
|
|
826
|
+
// an existing watermark. Skip gracefully if missing.
|
|
827
|
+
if (!convId) {
|
|
828
|
+
process.stderr.write(
|
|
829
|
+
`[meko-capture] ${hookType}: No conversation in watermark for session ${sessionId}. ` +
|
|
830
|
+
`SessionStart hook may not have run yet.\n`
|
|
831
|
+
);
|
|
832
|
+
process.stdout.write(hookOutput(""));
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Extract and send exchanges
|
|
837
|
+
const exchanges = extractExchanges(transcriptPath, lastLine);
|
|
838
|
+
let captured = 0;
|
|
839
|
+
|
|
840
|
+
for (const exchange of exchanges) {
|
|
841
|
+
try {
|
|
842
|
+
await addMessage(convId, exchange);
|
|
843
|
+
captured++;
|
|
844
|
+
} catch (err) {
|
|
845
|
+
process.stderr.write(`[meko-capture] Failed to add message (uuid=${exchange.user_uuid}): ${err.message}\n`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Update watermark
|
|
850
|
+
writeWatermark(wmPath, convId, currentLines);
|
|
851
|
+
|
|
852
|
+
// Output
|
|
853
|
+
const context =
|
|
854
|
+
hookType === "pre-compact"
|
|
855
|
+
? `Pre-compact: captured ${captured} exchanges to Meko conversation ${convId}`
|
|
856
|
+
: "";
|
|
857
|
+
process.stdout.write(hookOutput(context));
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
main().catch((err) => {
|
|
861
|
+
process.stderr.write(`[meko-capture] Fatal error: ${err.message}\n`);
|
|
862
|
+
process.stdout.write(hookOutput(""));
|
|
863
|
+
});
|