@yugabytedb/meko-mcp 2.2.0 → 2.3.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 +106 -38
- package/assets/hooks-handlers/lib/capture-cleanup-cli.js +1473 -0
- package/assets/hooks-handlers/lib/capture-notices.js +131 -14
- package/assets/hooks-handlers/lib/capture-state.js +95 -1
- package/assets/hooks-handlers/lib/capture-status-cli.js +6 -2
- package/assets/hooks-handlers/lib/capture.js +1237 -137
- package/assets/hooks-handlers/lib/checkpoint-timer.js +51 -10
- package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
- package/assets/hooks-handlers/lib/kiro-handler.js +26 -4
- package/assets/hooks-handlers/lib/opencode-handler.js +620 -0
- package/assets/hooks-handlers/lib/opencode-normalizer.js +145 -0
- package/assets/hooks-handlers/lib/terminal-handoff.js +10 -1
- package/assets/opencode-plugin/meko.js +213 -0
- package/assets/scripts/meko-statusline.sh +85 -18
- package/assets/skills/meko-mcp-tools/SKILL.md +57 -14
- package/assets/skills/meko-mcp-tools/references/_sections.md +2 -2
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +15 -7
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +86 -14
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +29 -6
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +8 -4
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +32 -22
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +14 -8
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +45 -1
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +6 -39
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +54 -18
- package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +1 -1
- package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
- package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +8 -6
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +95 -33
- package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +22 -5
- package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +14 -10
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +35 -25
- package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +14 -8
- package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +67 -1
- package/assets/skills/meko-select-datapack-desktop/SKILL.md +4 -4
- package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +5 -5
- package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +7 -5
- package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +4 -4
- package/bin/create-meko-setup.mjs +70 -7
- package/lib/capture-cleanup.mjs +63 -0
- package/lib/clients/claude-code.mjs +1 -0
- package/lib/clients/codex.mjs +80 -35
- package/lib/clients/common.mjs +7 -0
- package/lib/clients/index.mjs +2 -0
- package/lib/clients/kiro.mjs +5 -5
- package/lib/clients/opencode.mjs +532 -0
- package/lib/config.mjs +68 -1
- package/lib/installer.mjs +18 -2
- package/lib/migrate/adapters/claude-code.mjs +11 -2
- package/lib/migrate/adapters/codex.mjs +9 -2
- package/lib/migrate/adapters/memory-md.mjs +27 -8
- package/lib/migrate/adapters/opencode.mjs +148 -0
- package/lib/migrate/claude-parser.mjs +153 -9
- package/lib/migrate/id.mjs +195 -13
- package/lib/migrate/mcp-client.mjs +25 -1
- package/lib/migrate/opencode-normalizer.mjs +132 -0
- package/lib/migrate/orchestrator.mjs +293 -77
- package/lib/migrate/progress.mjs +239 -5
- package/lib/migrate/registry.mjs +72 -0
- package/lib/migrate.mjs +96 -8
- package/package.json +1 -1
- package/scripts/bundle-assets.mjs +4 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import os from "node:os";
|
|
13
|
-
import { agentIdForClaude, slugifyCwd } from "../id.mjs";
|
|
13
|
+
import { agentIdForClaude, deriveLegacyAgentId, slugifyCwd } from "../id.mjs";
|
|
14
14
|
import { parseSessionStream, peekSessionMetadata } from "../claude-parser.mjs";
|
|
15
15
|
|
|
16
16
|
function homeDir() {
|
|
@@ -46,6 +46,7 @@ export function userClaudeMdPath(opts = {}) {
|
|
|
46
46
|
* @property {string} slug
|
|
47
47
|
* @property {string} cwd Derived from the first session in the slug dir.
|
|
48
48
|
* @property {string} agentId
|
|
49
|
+
* @property {string} legacyAgentId Pre-MEKO-590 resume-state alias.
|
|
49
50
|
* @property {number} size
|
|
50
51
|
* @property {number} mtimeMs
|
|
51
52
|
* @property {boolean} likelyInProgress
|
|
@@ -96,12 +97,20 @@ export async function listSessions(opts) {
|
|
|
96
97
|
let stat;
|
|
97
98
|
try { stat = fs.statSync(full); } catch { continue; }
|
|
98
99
|
const sessionUuid = e.name.replace(/\.jsonl$/, "");
|
|
100
|
+
const legacyAgentId = deriveLegacyAgentId({
|
|
101
|
+
client: "claude_code",
|
|
102
|
+
cwd: slugCwd,
|
|
103
|
+
envOverride: process.env.MEKO_AGENT_ID,
|
|
104
|
+
});
|
|
99
105
|
out.push({
|
|
100
106
|
sessionUuid,
|
|
101
107
|
path: full,
|
|
102
108
|
slug,
|
|
103
109
|
cwd: slugCwd,
|
|
104
|
-
|
|
110
|
+
// Historical: `slugCwd` is a cwd recorded in an old transcript (or
|
|
111
|
+
// reverse-slugged from the project dir name) and may no longer exist.
|
|
112
|
+
agentId: agentIdForClaude(slugCwd, { historical: true }),
|
|
113
|
+
legacyAgentId,
|
|
105
114
|
size: stat.size,
|
|
106
115
|
mtimeMs: stat.mtimeMs,
|
|
107
116
|
likelyInProgress: stat.mtimeMs > Date.now() - 60_000,
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
import fs from "node:fs";
|
|
35
35
|
import path from "node:path";
|
|
36
36
|
import os from "node:os";
|
|
37
|
-
import { agentIdForCodex } from "../id.mjs";
|
|
37
|
+
import { agentIdForCodex, deriveLegacyAgentId } from "../id.mjs";
|
|
38
38
|
|
|
39
39
|
// Cap for the first-record read. Codex embeds `base_instructions` in the
|
|
40
40
|
// session_meta line, which routinely exceeds 16 KiB; read generously so the
|
|
@@ -80,6 +80,7 @@ export function sessionsRoot(opts = {}) {
|
|
|
80
80
|
* @property {string} path Absolute path to the rollout .jsonl.
|
|
81
81
|
* @property {string} cwd Project root from session_meta.
|
|
82
82
|
* @property {string} agentId `codex:<repo-basename>`.
|
|
83
|
+
* @property {string} legacyAgentId Pre-MEKO-590 resume-state alias.
|
|
83
84
|
* @property {"codex"} source
|
|
84
85
|
* @property {number} mtimeMs
|
|
85
86
|
* @property {boolean} likelyInProgress
|
|
@@ -202,7 +203,13 @@ export function listSessions(opts) {
|
|
|
202
203
|
sessionUuid: sessionUuidForRollout(full),
|
|
203
204
|
path: full,
|
|
204
205
|
cwd: meta.cwd,
|
|
205
|
-
|
|
206
|
+
// Historical: cwd recorded in the rollout, may be gone from disk.
|
|
207
|
+
agentId: agentIdForCodex(meta.cwd, { historical: true }),
|
|
208
|
+
legacyAgentId: deriveLegacyAgentId({
|
|
209
|
+
client: "codex",
|
|
210
|
+
cwd: meta.cwd,
|
|
211
|
+
envOverride: process.env.MEKO_AGENT_ID,
|
|
212
|
+
}),
|
|
206
213
|
source: "codex",
|
|
207
214
|
mtimeMs: stat.mtimeMs,
|
|
208
215
|
likelyInProgress: stat.mtimeMs > Date.now() - 60_000,
|
|
@@ -14,6 +14,7 @@ import os from "node:os";
|
|
|
14
14
|
import {
|
|
15
15
|
CLAUDE_CODE_USER_AGENT_ID,
|
|
16
16
|
agentIdForClaude,
|
|
17
|
+
deriveLegacyAgentId,
|
|
17
18
|
memoryHash,
|
|
18
19
|
slugifyCwd,
|
|
19
20
|
} from "../id.mjs";
|
|
@@ -26,6 +27,7 @@ function homeDir() {
|
|
|
26
27
|
/**
|
|
27
28
|
* @typedef {Object} MemoryItem
|
|
28
29
|
* @property {string} agentId
|
|
30
|
+
* @property {string} [legacyAgentId] Pre-MEKO-590 state alias.
|
|
29
31
|
* @property {string} text
|
|
30
32
|
* @property {string} hash
|
|
31
33
|
* @property {{source: string, scope: "user"|"project", sourcePath: string, heading?: string, name?: string, description?: string, type?: string}} metadata
|
|
@@ -62,22 +64,35 @@ export async function enumerateMemoryItems(opts) {
|
|
|
62
64
|
if (!stat.isDirectory()) continue;
|
|
63
65
|
const cwd = await cwdForSlugDir(dir, slug);
|
|
64
66
|
if (!cwd) continue;
|
|
65
|
-
|
|
67
|
+
// Historical: recorded/reverse-slugged cwd, may be gone from disk.
|
|
68
|
+
const agentId = agentIdForClaude(cwd, { historical: true });
|
|
69
|
+
const legacyAgentId = deriveLegacyAgentId({
|
|
70
|
+
client: "claude_code",
|
|
71
|
+
cwd,
|
|
72
|
+
envOverride: process.env.MEKO_AGENT_ID,
|
|
73
|
+
});
|
|
66
74
|
const projMd = path.join(cwd, ".claude", "CLAUDE.md");
|
|
67
|
-
items.push(...readClaudeMd(projMd, agentId, "project", cwd));
|
|
75
|
+
items.push(...readClaudeMd(projMd, agentId, "project", cwd, legacyAgentId));
|
|
68
76
|
const memDir = path.join(dir, "memory");
|
|
69
|
-
items.push(...readMemoryDir(memDir, agentId, "project", cwd));
|
|
77
|
+
items.push(...readMemoryDir(memDir, agentId, "project", cwd, legacyAgentId));
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
} else {
|
|
73
81
|
const cwd = opts.projectCwd;
|
|
74
82
|
if (!cwd) return [];
|
|
75
|
-
|
|
83
|
+
// Keep project CLAUDE.md and indexed memories in the same bucket as the
|
|
84
|
+
// historical session adapter, including when the cwd is no longer a repo.
|
|
85
|
+
const agentId = agentIdForClaude(cwd, { historical: true });
|
|
86
|
+
const legacyAgentId = deriveLegacyAgentId({
|
|
87
|
+
client: "claude_code",
|
|
88
|
+
cwd,
|
|
89
|
+
envOverride: process.env.MEKO_AGENT_ID,
|
|
90
|
+
});
|
|
76
91
|
const projMd = path.join(cwd, ".claude", "CLAUDE.md");
|
|
77
|
-
items.push(...readClaudeMd(projMd, agentId, "project", cwd));
|
|
92
|
+
items.push(...readClaudeMd(projMd, agentId, "project", cwd, legacyAgentId));
|
|
78
93
|
const slug = slugifyCwd(cwd);
|
|
79
94
|
const memDir = path.join(home, ".claude", "projects", slug, "memory");
|
|
80
|
-
items.push(...readMemoryDir(memDir, agentId, "project", cwd));
|
|
95
|
+
items.push(...readMemoryDir(memDir, agentId, "project", cwd, legacyAgentId));
|
|
81
96
|
}
|
|
82
97
|
|
|
83
98
|
return items;
|
|
@@ -94,9 +109,10 @@ export async function enumerateMemoryItems(opts) {
|
|
|
94
109
|
* so downstream consumers can correlate even after the agent_id is
|
|
95
110
|
* sanitized down to a basename. Null for user-scope items (which use the
|
|
96
111
|
* `meko_agent` common bucket).
|
|
112
|
+
* @param {string|null} [legacyAgentId] Pre-MEKO-590 compatibility alias.
|
|
97
113
|
* @returns {MemoryItem[]}
|
|
98
114
|
*/
|
|
99
|
-
export function readClaudeMd(filePath, agentId, scope, cwd = null) {
|
|
115
|
+
export function readClaudeMd(filePath, agentId, scope, cwd = null, legacyAgentId = null) {
|
|
100
116
|
let raw;
|
|
101
117
|
try {
|
|
102
118
|
raw = fs.readFileSync(filePath, "utf8");
|
|
@@ -112,6 +128,7 @@ export function readClaudeMd(filePath, agentId, scope, cwd = null) {
|
|
|
112
128
|
const name = heading || path.basename(filePath);
|
|
113
129
|
return {
|
|
114
130
|
agentId,
|
|
131
|
+
...(legacyAgentId && legacyAgentId !== agentId ? { legacyAgentId } : {}),
|
|
115
132
|
text,
|
|
116
133
|
hash: memoryHash(text, name),
|
|
117
134
|
name,
|
|
@@ -166,9 +183,10 @@ export function splitTopLevelSections(md) {
|
|
|
166
183
|
* @param {string} agentId
|
|
167
184
|
* @param {"user"|"project"} scope
|
|
168
185
|
* @param {string|null} [cwd] - Absolute project path; attached to metadata.
|
|
186
|
+
* @param {string|null} [legacyAgentId] Pre-MEKO-590 compatibility alias.
|
|
169
187
|
* @returns {MemoryItem[]}
|
|
170
188
|
*/
|
|
171
|
-
export function readMemoryDir(memDir, agentId, scope, cwd = null) {
|
|
189
|
+
export function readMemoryDir(memDir, agentId, scope, cwd = null, legacyAgentId = null) {
|
|
172
190
|
let entries;
|
|
173
191
|
try {
|
|
174
192
|
entries = fs.readdirSync(memDir);
|
|
@@ -192,6 +210,7 @@ export function readMemoryDir(memDir, agentId, scope, cwd = null) {
|
|
|
192
210
|
const name = frontmatter.name || entry.replace(/\.md$/, "");
|
|
193
211
|
out.push({
|
|
194
212
|
agentId,
|
|
213
|
+
...(legacyAgentId && legacyAgentId !== agentId ? { legacyAgentId } : {}),
|
|
195
214
|
text,
|
|
196
215
|
hash: memoryHash(text, name),
|
|
197
216
|
name,
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { agentIdForOpenCode } from "../id.mjs";
|
|
4
|
+
import { normalizeOpenCodeSession } from "../opencode-normalizer.mjs";
|
|
5
|
+
function defaultRunExport(sessionId, opts = {}) {
|
|
6
|
+
const runner = opts.runExport ?? defaultRunExportCommand;
|
|
7
|
+
return runner(sessionId, opts);
|
|
8
|
+
}
|
|
9
|
+
function defaultRunExportCommand(sessionId, opts = {}) {
|
|
10
|
+
const bin = opts.opencodeBin ?? probeOpenCode()?.path ?? "opencode";
|
|
11
|
+
const result = spawnSync(bin, ["export", sessionId], {
|
|
12
|
+
encoding: "utf8",
|
|
13
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
14
|
+
timeout: 10_000,
|
|
15
|
+
});
|
|
16
|
+
if (result.error) throw result.error;
|
|
17
|
+
if (result.status !== 0) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`opencode export ${sessionId} failed (${result.status}): ${(result.stderr || "").trim()}`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return parseJsonStdout(result.stdout);
|
|
23
|
+
}
|
|
24
|
+
function defaultRunList(opts = {}) {
|
|
25
|
+
const runner = opts.runList ?? defaultRunListCommand;
|
|
26
|
+
return runner(opts);
|
|
27
|
+
}
|
|
28
|
+
function defaultRunListCommand(opts = {}) {
|
|
29
|
+
// OpenCode 1.18.x exposes roots only here; child sessions are not
|
|
30
|
+
// enumerable through the supported CLI.
|
|
31
|
+
const bin = opts.opencodeBin ?? probeOpenCode()?.path ?? "opencode";
|
|
32
|
+
const result = spawnSync(bin, ["session", "list", "--format", "json"], {
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
35
|
+
timeout: 10_000,
|
|
36
|
+
});
|
|
37
|
+
if (result.error) throw result.error;
|
|
38
|
+
if (result.status !== 0) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`opencode session list failed (${result.status}): ${(result.stderr || "").trim()}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return parseJsonStdout(result.stdout);
|
|
44
|
+
}
|
|
45
|
+
function parseJsonStdout(stdout) {
|
|
46
|
+
const trimmed = String(stdout || "").trim();
|
|
47
|
+
if (!trimmed) return [];
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(trimmed);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
throw new Error(`invalid OpenCode JSON output: ${err.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function probeOpenCode(opts = {}) {
|
|
55
|
+
const probe = opts.probe ?? opts.probeOpenCode ?? defaultProbeOpenCode;
|
|
56
|
+
return probe();
|
|
57
|
+
}
|
|
58
|
+
export function isSupportedOpenCodeVersion(version) {
|
|
59
|
+
const match = String(version || "").match(/(\d+)\.(\d+)\.(\d+)/);
|
|
60
|
+
return Boolean(match && Number(match[1]) === 1 && Number(match[2]) === 18);
|
|
61
|
+
}
|
|
62
|
+
function defaultProbeOpenCode() {
|
|
63
|
+
try {
|
|
64
|
+
const which = process.platform === "win32" ? "where" : "which";
|
|
65
|
+
const pathResult = execFileSync(which, ["opencode"], { encoding: "utf8", timeout: 10_000 }).trim().split(/\r?\n/)[0];
|
|
66
|
+
const version = execFileSync(pathResult, ["--version"], { encoding: "utf8", timeout: 10_000 }).trim();
|
|
67
|
+
return { found: true, path: pathResult, version };
|
|
68
|
+
} catch {
|
|
69
|
+
return { found: false, path: "", version: "" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function sessionScopeMatches(sessionDir, opts) {
|
|
73
|
+
if (opts.scope !== "project") return true;
|
|
74
|
+
return path.resolve(sessionDir) === path.resolve(opts.projectCwd || process.cwd());
|
|
75
|
+
}
|
|
76
|
+
function firstString(...values) {
|
|
77
|
+
for (const value of values) {
|
|
78
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
79
|
+
}
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
export async function listSessions(opts = {}) {
|
|
83
|
+
const installation = probeOpenCode(opts);
|
|
84
|
+
if (!installation?.found) return [];
|
|
85
|
+
if (!isSupportedOpenCodeVersion(installation.version)) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const listed = await Promise.resolve(defaultRunList(opts));
|
|
89
|
+
const rows = Array.isArray(listed) ? listed : (listed?.sessions || listed?.data);
|
|
90
|
+
if (!Array.isArray(rows)) {
|
|
91
|
+
throw new Error("invalid OpenCode session list shape: expected an array");
|
|
92
|
+
}
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
if (!row || typeof row !== "object") continue;
|
|
96
|
+
const sessionId = firstString(row.id, row.sessionID, row.sessionId);
|
|
97
|
+
if (!sessionId) continue;
|
|
98
|
+
const cwd = firstString(row.directory, row.cwd, row.projectPath);
|
|
99
|
+
if (!cwd || !sessionScopeMatches(cwd, opts)) continue;
|
|
100
|
+
const mtimeMs = timestampMs(row.updated, row.updatedAt, row.time?.updated);
|
|
101
|
+
out.push({
|
|
102
|
+
sessionUuid: sessionId,
|
|
103
|
+
path: sessionId,
|
|
104
|
+
cwd,
|
|
105
|
+
agentId: agentIdForOpenCode(cwd, { historical: true }),
|
|
106
|
+
mtimeMs,
|
|
107
|
+
likelyInProgress: mtimeMs > Date.now() - 60_000,
|
|
108
|
+
parentSessionId: firstString(row.parentID, row.parentId) || null,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
function timestampMs(...values) {
|
|
114
|
+
for (const value of values) {
|
|
115
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
116
|
+
if (typeof value === "string" && value.trim()) {
|
|
117
|
+
const parsed = Date.parse(value);
|
|
118
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
export async function parseSession(ref, opts = {}) {
|
|
124
|
+
let exportShape = ref.exportData;
|
|
125
|
+
if (!exportShape) {
|
|
126
|
+
exportShape = await Promise.resolve(defaultRunExport(ref.sessionUuid || ref.path, opts));
|
|
127
|
+
}
|
|
128
|
+
const normalized = normalizeOpenCodeSession(exportShape, {
|
|
129
|
+
terminalError: Boolean(ref.terminalError),
|
|
130
|
+
interrupted: Boolean(ref.interrupted),
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
sessionUuid: normalized.sessionId,
|
|
134
|
+
firstUserText: normalized.firstUserText,
|
|
135
|
+
startedAt: normalized.startedAt,
|
|
136
|
+
exchanges: normalized.exchanges.map((ex) => ({
|
|
137
|
+
user_uuid: ex.userId,
|
|
138
|
+
input: ex.input,
|
|
139
|
+
output: ex.output,
|
|
140
|
+
reasoning: "",
|
|
141
|
+
timestamp: ex.timestamp,
|
|
142
|
+
interrupted: ex.interrupted,
|
|
143
|
+
})),
|
|
144
|
+
partial: Boolean(normalized.partial),
|
|
145
|
+
parentSessionId: normalized.parentSessionId,
|
|
146
|
+
cwd: normalized.cwd,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
* - no CLI / side effects; only exports
|
|
6
6
|
* - streaming via readline so sessions larger than available RAM
|
|
7
7
|
* don't need to be loaded whole
|
|
8
|
-
* - filters isSidechain /
|
|
9
|
-
*
|
|
8
|
+
* - filters isSidechain / attachment-type entries up front, and drops
|
|
9
|
+
* `isMeta` entries other than skill context and subagent hand-backs
|
|
10
|
+
* - folds skill bodies, task notifications and subagent hand-backs into
|
|
11
|
+
* the turn they belong to, with the same rules as capture.js
|
|
12
|
+
* (MEKO-710). The whole session is parsed at once, so there is no parse
|
|
13
|
+
* window; a session still being written holds its trailing turn.
|
|
10
14
|
*
|
|
11
15
|
* Kept deliberately independent of `skills/hooks-handlers/lib/capture.js`:
|
|
12
16
|
* installer packages ship without that sibling directory, and mixing
|
|
@@ -15,6 +19,7 @@
|
|
|
15
19
|
*/
|
|
16
20
|
|
|
17
21
|
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
18
23
|
import readline from "node:readline";
|
|
19
24
|
|
|
20
25
|
export const MAX_THINKING_LEN = Number(process.env.MEKO_MAX_THINKING_LEN ?? 2000);
|
|
@@ -24,6 +29,8 @@ export const MAX_TOOL_RESULT_LEN = Number(process.env.MEKO_MAX_TOOL_RESULT_LEN ?
|
|
|
24
29
|
const SKIP_INPUT_PATTERN =
|
|
25
30
|
/^<(system-reminder|command-|local-command|available-deferred)/;
|
|
26
31
|
|
|
32
|
+
const SKILL_BODY_PATTERN = /^Base directory for this skill: *(\S+)/;
|
|
33
|
+
|
|
27
34
|
export function truncate(str, max) {
|
|
28
35
|
if (!str || str.length <= max) return str;
|
|
29
36
|
return str.slice(0, max) + "... [truncated]";
|
|
@@ -104,6 +111,84 @@ export function extractToolResultContent(msg) {
|
|
|
104
111
|
return "TOOL RESULT:\n" + parts.join("\n---\n");
|
|
105
112
|
}
|
|
106
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Render a Claude Code skill or prompt command entry as the command the user
|
|
116
|
+
* typed (`/commit fix typo`). Only the `<command-message>`-led shape loads a
|
|
117
|
+
* skill; built-in commands lead with `<command-name>` and return "".
|
|
118
|
+
* Mirrors skillCommandInput in capture.js.
|
|
119
|
+
*/
|
|
120
|
+
export function skillCommandInput(text) {
|
|
121
|
+
if (typeof text !== "string" || !/^<command-message>/.test(text)) return "";
|
|
122
|
+
const name = /<command-name>([\s\S]*?)<\/command-name>/.exec(text);
|
|
123
|
+
if (!name) return "";
|
|
124
|
+
const args = /<command-args>([\s\S]*?)<\/command-args>/.exec(text);
|
|
125
|
+
return [name[1].trim(), args ? args[1].trim() : ""].filter(Boolean).join(" ");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Is this a background-task or subagent notification? Mirrors capture.js. */
|
|
129
|
+
export function isTaskNotification(obj) {
|
|
130
|
+
if (obj?.type !== "user" || isToolResultMessage(obj)) return false;
|
|
131
|
+
if (obj.origin && obj.origin.kind === "task-notification") return true;
|
|
132
|
+
return /^\s*<task-notification>/.test(extractText(obj.message?.content));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Is this a subagent's hand-back report? Claude Code writes it as an
|
|
137
|
+
* `isMeta` entry tagged `origin: { kind: "peer", handback: true }`. Other
|
|
138
|
+
* peer messages are requests from another session. Mirrors capture.js.
|
|
139
|
+
*/
|
|
140
|
+
export function isSubagentHandback(obj) {
|
|
141
|
+
if (obj?.type !== "user" || isToolResultMessage(obj)) return false;
|
|
142
|
+
return obj.origin?.kind === "peer" && obj.origin.handback === true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function handbackInput(obj) {
|
|
146
|
+
const name = obj.origin?.name || obj.origin?.from || "";
|
|
147
|
+
return name ? `Agent message: ${name}` : "Agent message";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Is this an `isMeta` entry Claude Code injected while loading a skill? The
|
|
152
|
+
* model-invoked shape carries `sourceToolUseID`; a typed `/skill` body starts
|
|
153
|
+
* with "Base directory for this skill:". Mirrors capture.js.
|
|
154
|
+
*/
|
|
155
|
+
export function isSkillContext(obj) {
|
|
156
|
+
if (obj?.type !== "user" || obj.isMeta !== true) return false;
|
|
157
|
+
if (isToolResultMessage(obj)) return false;
|
|
158
|
+
if (obj.sourceToolUseID) return true;
|
|
159
|
+
return SKILL_BODY_PATTERN.test(extractText(obj.message?.content));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function taskNotificationInput(text) {
|
|
163
|
+
const summary = /<summary>([\s\S]*?)<\/summary>/.exec(text || "");
|
|
164
|
+
return `Task notification: ${summary ? summary[1].trim() : "background task update"}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function skillContextInput(text) {
|
|
168
|
+
const dir = SKILL_BODY_PATTERN.exec(text || "");
|
|
169
|
+
const name = dir ? path.basename(dir[1].replace(/\/+$/, "")) : "";
|
|
170
|
+
return name ? `Skill loaded: ${name}` : "Skill context loaded";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The rendered input for a turn. A typed skill command that got its body is
|
|
175
|
+
* unwrapped to `/name args`; a turn opened by injected context gets a one-line
|
|
176
|
+
* summary so that context is never sent as user input.
|
|
177
|
+
*/
|
|
178
|
+
function turnInput(turn) {
|
|
179
|
+
const text = extractText(turn.userMsg.message?.content);
|
|
180
|
+
if (isTaskNotification(turn.userMsg)) return taskNotificationInput(text);
|
|
181
|
+
if (isSubagentHandback(turn.userMsg)) return handbackInput(turn.userMsg);
|
|
182
|
+
if (isSkillContext(turn.userMsg)) return skillContextInput(text);
|
|
183
|
+
return turn.injectedEntries.some((e) => e.isMeta === true && !isSubagentHandback(e))
|
|
184
|
+
? skillCommandInput(text) || text
|
|
185
|
+
: text;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isCapturableInput(input) {
|
|
189
|
+
return !!input && !SKIP_INPUT_PATTERN.test(input);
|
|
190
|
+
}
|
|
191
|
+
|
|
107
192
|
/**
|
|
108
193
|
* Should this JSON entry be dropped before we start pairing turns?
|
|
109
194
|
* Mirrors the plan: skip sidechains, meta turns, and the non-message types.
|
|
@@ -177,6 +262,10 @@ export async function parseSessionStream(sessionPath) {
|
|
|
177
262
|
// Turn-assembly state.
|
|
178
263
|
const exchanges = [];
|
|
179
264
|
let current = null;
|
|
265
|
+
// The last entry kept for turn assembly, and whether consecutive injected
|
|
266
|
+
// isMeta entries are still arriving after a skill command or Skill call.
|
|
267
|
+
let prev = null;
|
|
268
|
+
let insideInjectedRun = false;
|
|
180
269
|
|
|
181
270
|
for await (const raw of rl) {
|
|
182
271
|
const line = raw.trim();
|
|
@@ -191,28 +280,63 @@ export async function parseSessionStream(sessionPath) {
|
|
|
191
280
|
if (!cwd && typeof obj.cwd === "string") cwd = obj.cwd;
|
|
192
281
|
if (!gitBranch && typeof obj.gitBranch === "string") gitBranch = obj.gitBranch;
|
|
193
282
|
if (!startedAt && typeof obj.timestamp === "string") startedAt = obj.timestamp;
|
|
194
|
-
if (!firstUserText && obj.type === "user" && !isSkippable(obj) && !isToolResultMessage(obj)) {
|
|
283
|
+
if (!firstUserText && obj.type === "user" && !isSkippable(obj) && !isToolResultMessage(obj) && !isTaskNotification(obj)) {
|
|
195
284
|
const txt = extractText(obj.message?.content);
|
|
196
285
|
if (txt && !SKIP_INPUT_PATTERN.test(txt)) firstUserText = txt.slice(0, 240);
|
|
197
286
|
}
|
|
198
287
|
|
|
199
|
-
|
|
288
|
+
// Skill context and hand-backs are isMeta but belong to their turn, so
|
|
289
|
+
// test them before isSkippable drops every other isMeta entry. A
|
|
290
|
+
// hand-back is never skill context, even if it carries sourceToolUseID.
|
|
291
|
+
const handback = isSubagentHandback(obj) && obj.isSidechain !== true;
|
|
292
|
+
const skillContext =
|
|
293
|
+
!handback && isSkillContext(obj) && obj.isSidechain !== true;
|
|
294
|
+
if (!skillContext && !handback && isSkippable(obj)) continue;
|
|
295
|
+
|
|
296
|
+
const isUserText = obj.type === "user" && !isToolResultMessage(obj);
|
|
297
|
+
let foldIntoCurrent = false;
|
|
298
|
+
if (isUserText && current) {
|
|
299
|
+
if (skillContext) {
|
|
300
|
+
const prevIsSkillCommand =
|
|
301
|
+
prev?.type === "user" &&
|
|
302
|
+
skillCommandInput(extractText(prev.message?.content)) !== "";
|
|
303
|
+
foldIntoCurrent =
|
|
304
|
+
!!obj.sourceToolUseID || insideInjectedRun || prevIsSkillCommand;
|
|
305
|
+
} else if (isTaskNotification(obj) || handback) {
|
|
306
|
+
// Folding into a turn that is never captured (a `/compact` or
|
|
307
|
+
// `<local-command-…>` turn) would drop the reply with it.
|
|
308
|
+
foldIntoCurrent = isCapturableInput(turnInput(current));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
insideInjectedRun = skillContext && (!!obj.sourceToolUseID || foldIntoCurrent);
|
|
200
312
|
|
|
201
|
-
if (
|
|
313
|
+
if (isUserText && !foldIntoCurrent) {
|
|
202
314
|
if (current) {
|
|
203
315
|
const built = buildExchange(current);
|
|
204
316
|
if (built) exchanges.push(built);
|
|
205
317
|
}
|
|
206
|
-
current = {
|
|
318
|
+
current = {
|
|
319
|
+
userMsg: obj,
|
|
320
|
+
assistantEntries: [],
|
|
321
|
+
toolResultEntries: [],
|
|
322
|
+
injectedEntries: [],
|
|
323
|
+
};
|
|
207
324
|
} else if (current) {
|
|
208
325
|
if (obj.type === "assistant") {
|
|
209
326
|
current.assistantEntries.push(obj);
|
|
210
327
|
} else if (obj.type === "user" && isToolResultMessage(obj)) {
|
|
211
328
|
current.toolResultEntries.push(obj);
|
|
329
|
+
} else if (isUserText) {
|
|
330
|
+
current.injectedEntries.push(obj);
|
|
212
331
|
}
|
|
213
332
|
}
|
|
333
|
+
prev = obj;
|
|
214
334
|
}
|
|
215
|
-
|
|
335
|
+
// A session still being written can get more entries for its trailing
|
|
336
|
+
// turn, such as a task notification and its reply. Hold that turn, like
|
|
337
|
+
// the Codex adapter does: if it were sent now, resume would skip it on the
|
|
338
|
+
// next run and the new entries would never be migrated.
|
|
339
|
+
if (current && !partial) {
|
|
216
340
|
const built = buildExchange(current);
|
|
217
341
|
if (built) exchanges.push(built);
|
|
218
342
|
}
|
|
@@ -236,8 +360,8 @@ function baseName(p) {
|
|
|
236
360
|
}
|
|
237
361
|
|
|
238
362
|
function buildExchange(ex) {
|
|
239
|
-
const input =
|
|
240
|
-
if (!
|
|
363
|
+
const input = turnInput(ex);
|
|
364
|
+
if (!isCapturableInput(input)) return null;
|
|
241
365
|
if (ex.assistantEntries.length === 0) return null;
|
|
242
366
|
|
|
243
367
|
const outputParts = [];
|
|
@@ -257,9 +381,29 @@ function buildExchange(ex) {
|
|
|
257
381
|
const trText = extractToolResultContent(tr);
|
|
258
382
|
if (trText) reasoningParts.push(trText);
|
|
259
383
|
}
|
|
384
|
+
const anchoredOnInjection =
|
|
385
|
+
isTaskNotification(ex.userMsg) || isSubagentHandback(ex.userMsg) || isSkillContext(ex.userMsg);
|
|
386
|
+
const injected = anchoredOnInjection
|
|
387
|
+
? [ex.userMsg, ...ex.injectedEntries]
|
|
388
|
+
: ex.injectedEntries;
|
|
389
|
+
for (const inj of injected) {
|
|
390
|
+
const text = extractText(inj.message?.content);
|
|
391
|
+
if (text) {
|
|
392
|
+
const label = isTaskNotification(inj)
|
|
393
|
+
? "TASK NOTIFICATION:\n"
|
|
394
|
+
: isSubagentHandback(inj)
|
|
395
|
+
? "AGENT MESSAGE:\n"
|
|
396
|
+
: "INJECTED CONTEXT:\n";
|
|
397
|
+
reasoningParts.push(label + truncate(text, MAX_TOOL_RESULT_LEN));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
260
400
|
|
|
261
401
|
return {
|
|
262
402
|
user_uuid: ex.userMsg.uuid || "unknown",
|
|
403
|
+
// Uuids of the injected entries folded into this turn. Older migrator
|
|
404
|
+
// versions sent a task notification as its own exchange, so a resume
|
|
405
|
+
// cursor saved by them can name one of these.
|
|
406
|
+
folded_uuids: ex.injectedEntries.map((e) => e.uuid).filter(Boolean),
|
|
263
407
|
input,
|
|
264
408
|
output: outputParts.join("\n\n"),
|
|
265
409
|
reasoning: reasoningParts.join("\n---\n"),
|