@yugabytedb/meko-mcp 2.1.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 +233 -40
- package/assets/hooks-handlers/codex-session-end.sh +4 -69
- package/assets/hooks-handlers/cursor-session-start.sh +19 -1
- package/assets/hooks-handlers/lib/capture-cleanup-cli.js +1473 -0
- package/assets/hooks-handlers/lib/capture-notices.js +438 -0
- package/assets/hooks-handlers/lib/capture-state.js +2173 -0
- package/assets/hooks-handlers/lib/capture-status-cli.js +86 -0
- package/assets/hooks-handlers/lib/capture.js +2782 -403
- package/assets/hooks-handlers/lib/checkpoint-timer.js +485 -81
- package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
- package/assets/hooks-handlers/lib/kiro-handler.js +760 -0
- 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 +427 -0
- package/assets/hooks-handlers/session-end.sh +3 -34
- package/assets/opencode-plugin/meko.js +213 -0
- package/assets/scripts/meko-statusline.sh +123 -16
- package/assets/skills/meko-mcp-tools/SKILL.md +116 -126
- package/assets/skills/meko-mcp-tools/references/_sections.md +3 -3
- package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +25 -18
- package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +99 -24
- package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +1 -1
- package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +53 -6
- package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +25 -79
- package/assets/skills/meko-mcp-tools/references/tools-overview.md +37 -26
- package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +14 -8
- package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +66 -1
- package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +7 -40
- package/assets/skills/meko-mcp-tools-desktop/SKILL.md +56 -20
- 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 +15 -8
- package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +99 -36
- 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 +18 -14
- package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +39 -28
- 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 +8 -8
- 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 +113 -12
- package/lib/capture-cleanup.mjs +63 -0
- package/lib/capture-status.mjs +67 -0
- package/lib/clients/aliases.mjs +5 -0
- package/lib/clients/claude-code.mjs +1 -0
- package/lib/clients/claude-desktop.mjs +120 -5
- package/lib/clients/codex.mjs +85 -37
- package/lib/clients/common.mjs +7 -0
- package/lib/clients/index.mjs +4 -0
- package/lib/clients/kiro.mjs +705 -0
- package/lib/clients/opencode.mjs +532 -0
- package/lib/config.mjs +158 -1
- package/lib/installer.mjs +18 -1
- package/lib/migrate/adapters/claude-code.mjs +11 -2
- package/lib/migrate/adapters/codex.mjs +497 -0
- 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 +217 -10
- package/lib/migrate/mcp-client.mjs +25 -1
- package/lib/migrate/opencode-normalizer.mjs +132 -0
- package/lib/migrate/orchestrator.mjs +299 -56
- package/lib/migrate/progress.mjs +239 -5
- package/lib/migrate/registry.mjs +72 -0
- package/lib/migrate.mjs +100 -10
- package/lib/prompt.mjs +16 -1
- package/package.json +4 -4
- package/scripts/bundle-assets.mjs +4 -0
package/lib/installer.mjs
CHANGED
|
@@ -67,7 +67,7 @@ export async function install(options) {
|
|
|
67
67
|
|
|
68
68
|
info("Checking prerequisites...");
|
|
69
69
|
try {
|
|
70
|
-
await adapter.detectPrerequisites({ verbose, log: LOG, scope, projectRoot });
|
|
70
|
+
await adapter.detectPrerequisites({ verbose, log: LOG, scope, projectRoot, dryRun });
|
|
71
71
|
} catch (err) {
|
|
72
72
|
error(err.message);
|
|
73
73
|
throw err;
|
|
@@ -134,6 +134,7 @@ export async function install(options) {
|
|
|
134
134
|
|
|
135
135
|
try {
|
|
136
136
|
adapter.setHookEnvironment({
|
|
137
|
+
name,
|
|
137
138
|
url,
|
|
138
139
|
apiKey,
|
|
139
140
|
scope,
|
|
@@ -152,6 +153,22 @@ export async function install(options) {
|
|
|
152
153
|
}
|
|
153
154
|
currentStep++;
|
|
154
155
|
|
|
156
|
+
// Runs even when MCP registration was skipped so a rotated key still
|
|
157
|
+
// reaches the launchd env / shell rc. See yugabyte/meko-mcp-server#337.
|
|
158
|
+
if (typeof adapter.syncCloudAuthEnvironment === "function") {
|
|
159
|
+
try {
|
|
160
|
+
await adapter.syncCloudAuthEnvironment({
|
|
161
|
+
apiKey,
|
|
162
|
+
dryRun,
|
|
163
|
+
verbose,
|
|
164
|
+
log: LOG,
|
|
165
|
+
yes,
|
|
166
|
+
});
|
|
167
|
+
} catch (err) {
|
|
168
|
+
warn(`Cloud auth environment sync failed (non-fatal): ${err.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
155
172
|
if (!skipSkills) {
|
|
156
173
|
try {
|
|
157
174
|
const result = adapter.installSkills({
|
|
@@ -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,
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/codex.mjs — Enumerate + parse Codex CLI/Desktop session rollouts.
|
|
3
|
+
*
|
|
4
|
+
* Codex writes one JSONL "rollout" file per session under:
|
|
5
|
+
* <CODEX_HOME|~/.codex>/sessions/<YYYY>/<MM>/<DD>/rollout-<ts>-<uuid>.jsonl
|
|
6
|
+
*
|
|
7
|
+
* Each line is `{timestamp, type, payload}`. The shapes we consume:
|
|
8
|
+
* - type "session_meta": payload has `session_id` and `cwd` (the project
|
|
9
|
+
* root the session ran in — the source of truth for agent_id derivation).
|
|
10
|
+
* - type "event_msg", payload.type "user_message": the AUTHORITATIVE marker
|
|
11
|
+
* for a real user prompt. Codex also writes `response_item`/`message`
|
|
12
|
+
* role=user records for injected context (`<recommended_plugins>`,
|
|
13
|
+
* AGENTS.md, the Meko SessionStart block); those have NO mirrored
|
|
14
|
+
* user_message event, so we use the event stream to tell a real prompt
|
|
15
|
+
* from injected context.
|
|
16
|
+
* - type "response_item", payload.type "message", role "assistant": one
|
|
17
|
+
* slice of the assistant's reply. A single turn emits several — phase
|
|
18
|
+
* "commentary" updates then phase "final_answer" — so we accumulate ALL
|
|
19
|
+
* of them into one assistant side and only close the turn at the
|
|
20
|
+
* completion boundary (next user prompt or `event_msg` task_complete).
|
|
21
|
+
* - type "response_item", payload.type "reasoning" /
|
|
22
|
+
* "custom_tool_call" / "custom_tool_call_output": reasoning + tool
|
|
23
|
+
* activity, folded into the turn's `reasoning` (parity with the
|
|
24
|
+
* Claude Code / Cursor adapters).
|
|
25
|
+
*
|
|
26
|
+
* Completion-boundary discriminator (kept consistent with the live parser in
|
|
27
|
+
* PR #234): assistant `phase: "final_answer"` and/or the `event_msg`
|
|
28
|
+
* `payload.type: "task_complete"` record close a turn.
|
|
29
|
+
*
|
|
30
|
+
* Unknown line/block shapes are skipped, never fatal — Codex's on-disk
|
|
31
|
+
* schema drifts across CLI versions.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import fs from "node:fs";
|
|
35
|
+
import path from "node:path";
|
|
36
|
+
import os from "node:os";
|
|
37
|
+
import { agentIdForCodex, deriveLegacyAgentId } from "../id.mjs";
|
|
38
|
+
|
|
39
|
+
// Cap for the first-record read. Codex embeds `base_instructions` in the
|
|
40
|
+
// session_meta line, which routinely exceeds 16 KiB; read generously so the
|
|
41
|
+
// full first record parses. 4 MiB is far above any real header while still
|
|
42
|
+
// bounding a pathological file.
|
|
43
|
+
const HEADER_READ_CAP = 4 * 1024 * 1024;
|
|
44
|
+
|
|
45
|
+
function homeDir() {
|
|
46
|
+
return process.env.HOME || os.homedir();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The `.codex` home. Honors `CODEX_HOME` (production sets it to relocate all
|
|
51
|
+
* Codex state); otherwise `<home>/.codex`. The injectable `opts.home` override
|
|
52
|
+
* is preserved for tests and only applies to the default location.
|
|
53
|
+
*
|
|
54
|
+
* @param {object} [opts]
|
|
55
|
+
* @param {string} [opts.home]
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function codexHome(opts = {}) {
|
|
59
|
+
const envHome = (process.env.CODEX_HOME || "").trim();
|
|
60
|
+
if (envHome) return envHome;
|
|
61
|
+
return path.join(opts.home ?? homeDir(), ".codex");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The sessions root Codex writes rollout files under.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} [opts]
|
|
68
|
+
* @param {string} [opts.home]
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
export function sessionsRoot(opts = {}) {
|
|
72
|
+
return path.join(codexHome(opts), "sessions");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @typedef {Object} CodexSessionRef
|
|
77
|
+
* @property {string} sessionUuid Rollout filename basename (matches
|
|
78
|
+
* capture.js::extractSessionId, so migrator + live capture dedup to the
|
|
79
|
+
* same conversation container).
|
|
80
|
+
* @property {string} path Absolute path to the rollout .jsonl.
|
|
81
|
+
* @property {string} cwd Project root from session_meta.
|
|
82
|
+
* @property {string} agentId `codex:<repo-basename>`.
|
|
83
|
+
* @property {string} legacyAgentId Pre-MEKO-590 resume-state alias.
|
|
84
|
+
* @property {"codex"} source
|
|
85
|
+
* @property {number} mtimeMs
|
|
86
|
+
* @property {boolean} likelyInProgress
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Recursively collect rollout-*.jsonl paths under the date-partitioned tree.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} dir
|
|
93
|
+
* @returns {string[]}
|
|
94
|
+
*/
|
|
95
|
+
function collectRolloutFiles(dir) {
|
|
96
|
+
let entries;
|
|
97
|
+
try {
|
|
98
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const e of entries) {
|
|
104
|
+
const full = path.join(dir, e.name);
|
|
105
|
+
if (e.isDirectory()) {
|
|
106
|
+
out.push(...collectRolloutFiles(full));
|
|
107
|
+
} else if (e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")) {
|
|
108
|
+
out.push(full);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Read the COMPLETE first JSONL record (through the first newline) and, if it
|
|
116
|
+
* is the `session_meta` header, harvest `session_id` + `cwd` + `timestamp`.
|
|
117
|
+
* Reads in chunks up to HEADER_READ_CAP so a large `base_instructions` block
|
|
118
|
+
* (>16 KiB, common in current rollouts) still parses — a bounded prefix would
|
|
119
|
+
* truncate the line, fail to parse, and silently drop the session.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} filePath
|
|
122
|
+
* @returns {{ sessionId: string|null, cwd: string|null, startedAt: string|null }}
|
|
123
|
+
*/
|
|
124
|
+
export function peekSessionMeta(filePath) {
|
|
125
|
+
const empty = { sessionId: null, cwd: null, startedAt: null };
|
|
126
|
+
let fd;
|
|
127
|
+
try {
|
|
128
|
+
fd = fs.openSync(filePath, "r");
|
|
129
|
+
const chunkSize = 65536;
|
|
130
|
+
const chunk = Buffer.alloc(chunkSize);
|
|
131
|
+
let acc = "";
|
|
132
|
+
let offset = 0;
|
|
133
|
+
let newlineIdx = -1;
|
|
134
|
+
while (offset < HEADER_READ_CAP) {
|
|
135
|
+
const bytesRead = fs.readSync(fd, chunk, 0, chunkSize, offset);
|
|
136
|
+
if (bytesRead === 0) break;
|
|
137
|
+
acc += chunk.toString("utf-8", 0, bytesRead);
|
|
138
|
+
offset += bytesRead;
|
|
139
|
+
newlineIdx = acc.indexOf("\n");
|
|
140
|
+
if (newlineIdx !== -1) break;
|
|
141
|
+
}
|
|
142
|
+
const firstLine = (newlineIdx === -1 ? acc : acc.slice(0, newlineIdx)).trim();
|
|
143
|
+
if (!firstLine) return empty;
|
|
144
|
+
const obj = JSON.parse(firstLine);
|
|
145
|
+
if (obj && obj.type === "session_meta" && obj.payload && typeof obj.payload === "object") {
|
|
146
|
+
const p = obj.payload;
|
|
147
|
+
const str = (v) => (typeof v === "string" ? v : null);
|
|
148
|
+
return {
|
|
149
|
+
sessionId: str(p.session_id) || str(p.id),
|
|
150
|
+
cwd: str(p.cwd),
|
|
151
|
+
startedAt: str(p.timestamp) || str(obj.timestamp),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return empty;
|
|
155
|
+
} catch {
|
|
156
|
+
return empty;
|
|
157
|
+
} finally {
|
|
158
|
+
if (fd !== undefined) {
|
|
159
|
+
try { fs.closeSync(fd); } catch { /* FD already gone */ }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The session identifier used for conversation dedup. Standardized on the
|
|
166
|
+
* rollout FILENAME BASENAME so it matches capture.js::extractSessionId
|
|
167
|
+
* (`path.basename(transcriptPath, ".jsonl")`) — live hook capture and the
|
|
168
|
+
* migrator then create/reconnect the SAME conversation container instead of
|
|
169
|
+
* two divergent ones for the same session.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} rolloutPath
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
export function sessionUuidForRollout(rolloutPath) {
|
|
175
|
+
return path.basename(rolloutPath, ".jsonl");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {object} opts
|
|
180
|
+
* @param {"user"|"project"} opts.scope
|
|
181
|
+
* @param {string} [opts.projectCwd]
|
|
182
|
+
* @param {string} [opts.home]
|
|
183
|
+
* @returns {CodexSessionRef[]}
|
|
184
|
+
*/
|
|
185
|
+
export function listSessions(opts) {
|
|
186
|
+
const root = sessionsRoot(opts);
|
|
187
|
+
if (!fs.existsSync(root)) return [];
|
|
188
|
+
|
|
189
|
+
const wantedCwd = opts.scope === "project" ? opts.projectCwd : null;
|
|
190
|
+
|
|
191
|
+
const out = [];
|
|
192
|
+
for (const full of collectRolloutFiles(root)) {
|
|
193
|
+
const meta = peekSessionMeta(full);
|
|
194
|
+
if (!meta.cwd) continue;
|
|
195
|
+
if (wantedCwd && meta.cwd !== wantedCwd) continue;
|
|
196
|
+
let stat;
|
|
197
|
+
try {
|
|
198
|
+
stat = fs.statSync(full);
|
|
199
|
+
} catch {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
out.push({
|
|
203
|
+
sessionUuid: sessionUuidForRollout(full),
|
|
204
|
+
path: full,
|
|
205
|
+
cwd: meta.cwd,
|
|
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
|
+
}),
|
|
213
|
+
source: "codex",
|
|
214
|
+
mtimeMs: stat.mtimeMs,
|
|
215
|
+
likelyInProgress: stat.mtimeMs > Date.now() - 60_000,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Parse a rollout file: pair real user prompts with the full assistant reply.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} sessionPath
|
|
225
|
+
* @returns {{
|
|
226
|
+
* sessionUuid: string,
|
|
227
|
+
* firstUserText: string,
|
|
228
|
+
* cwd: string|null,
|
|
229
|
+
* startedAt: string|null,
|
|
230
|
+
* exchanges: Array<{user_uuid: string, input: string, output: string, reasoning: string, timestamp: string}>,
|
|
231
|
+
* partial: boolean,
|
|
232
|
+
* }}
|
|
233
|
+
*/
|
|
234
|
+
export function parseSession(sessionPath) {
|
|
235
|
+
const { records, realPrompts, hasUserMessageEvents } = collectRecords(sessionPath);
|
|
236
|
+
|
|
237
|
+
let cwd = null;
|
|
238
|
+
let startedAt = null;
|
|
239
|
+
let firstUserText = "";
|
|
240
|
+
|
|
241
|
+
// Turn assembly. A real user prompt opens a turn; every assistant slice and
|
|
242
|
+
// reasoning/tool record until the next real prompt (or a task_complete)
|
|
243
|
+
// belongs to it. `turnIdx` — the positional id fallback for id-less Codex
|
|
244
|
+
// user records — increments ONLY when a turn is emitted, so a turn that is
|
|
245
|
+
// held on one run gets the SAME id when it is finally emitted on a later run
|
|
246
|
+
// (all preceding emitted turns are identical), keeping seed dedup stable.
|
|
247
|
+
const exchanges = [];
|
|
248
|
+
let turnIdx = 0;
|
|
249
|
+
// current: { input, uuid, timestamp, lastTimestamp, outputs, reasoning,
|
|
250
|
+
// hasAssistant, sawPhase, sawFinalAnswer, boundaryClosed }
|
|
251
|
+
let current = null;
|
|
252
|
+
|
|
253
|
+
// A turn is safe to emit once it has reached its completion boundary — the
|
|
254
|
+
// SAME boundary the live parser in PR #234 uses:
|
|
255
|
+
// - a `task_complete` event closed it (boundaryClosed), OR
|
|
256
|
+
// - an assistant slice with phase `final_answer` arrived, OR
|
|
257
|
+
// - it is the legacy/simple shape: assistant content but no phase info at
|
|
258
|
+
// all (older rollouts don't tag phases) — complete as soon as any
|
|
259
|
+
// assistant message exists, like non-Codex turns.
|
|
260
|
+
// A turn still streaming commentary with no boundary, or with no assistant
|
|
261
|
+
// reply yet, is NOT complete and must be held (see the EOF flush below) so
|
|
262
|
+
// the whole turn is captured once on a later run rather than sent early and
|
|
263
|
+
// then permanently skipped by lastUuid resume when its final answer arrives.
|
|
264
|
+
const isComplete = (t) =>
|
|
265
|
+
t.boundaryClosed || t.sawFinalAnswer || (t.hasAssistant && !t.sawPhase);
|
|
266
|
+
|
|
267
|
+
// Emit `current` as an exchange. `allowIncomplete` is true only when a new
|
|
268
|
+
// real prompt supersedes it (the turn is definitively over — no more content
|
|
269
|
+
// will ever be appended to it) or a task_complete closed it; at EOF we pass
|
|
270
|
+
// false so a still-in-progress trailing turn is HELD, not emitted.
|
|
271
|
+
const flush = (allowIncomplete) => {
|
|
272
|
+
if (!current) return;
|
|
273
|
+
const t = current;
|
|
274
|
+
current = null;
|
|
275
|
+
if (!allowIncomplete && !isComplete(t)) return; // hold: do not emit or advance turnIdx
|
|
276
|
+
const output = t.outputs.join("\n\n");
|
|
277
|
+
const reasoning = t.reasoning.join("\n---\n");
|
|
278
|
+
if (t.input || output) {
|
|
279
|
+
exchanges.push({
|
|
280
|
+
user_uuid: t.uuid || `codex-${turnIdx}`,
|
|
281
|
+
input: t.input,
|
|
282
|
+
output,
|
|
283
|
+
reasoning,
|
|
284
|
+
timestamp: t.lastTimestamp || t.timestamp || "",
|
|
285
|
+
});
|
|
286
|
+
turnIdx++;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
for (const rec of records) {
|
|
291
|
+
if (rec.kind === "meta") {
|
|
292
|
+
if (!cwd) cwd = rec.cwd;
|
|
293
|
+
if (!startedAt) startedAt = rec.timestamp;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (!startedAt && rec.timestamp) startedAt = rec.timestamp;
|
|
297
|
+
|
|
298
|
+
if (rec.kind === "user") {
|
|
299
|
+
// Only real prompts open a turn. When user_message events exist they are
|
|
300
|
+
// authoritative: a role=user response_item whose text isn't among them is
|
|
301
|
+
// injected context (plugins/AGENTS.md/Meko block) and is dropped.
|
|
302
|
+
if (hasUserMessageEvents && !realPrompts.has(rec.text)) continue;
|
|
303
|
+
// A new real prompt means the previous turn is definitively over — emit
|
|
304
|
+
// it even if it never saw an explicit boundary (it won't get more).
|
|
305
|
+
flush(true);
|
|
306
|
+
if (!firstUserText && rec.text) firstUserText = rec.text.slice(0, 240);
|
|
307
|
+
current = {
|
|
308
|
+
input: rec.text,
|
|
309
|
+
uuid: rec.uuid,
|
|
310
|
+
timestamp: rec.timestamp,
|
|
311
|
+
lastTimestamp: rec.timestamp,
|
|
312
|
+
outputs: [],
|
|
313
|
+
reasoning: [],
|
|
314
|
+
hasAssistant: false,
|
|
315
|
+
sawPhase: false,
|
|
316
|
+
sawFinalAnswer: false,
|
|
317
|
+
boundaryClosed: false,
|
|
318
|
+
};
|
|
319
|
+
} else if (rec.kind === "assistant") {
|
|
320
|
+
if (!current) continue; // assistant with no preceding real prompt — skip
|
|
321
|
+
current.hasAssistant = true;
|
|
322
|
+
if (rec.phase) current.sawPhase = true;
|
|
323
|
+
if (rec.phase === "final_answer") current.sawFinalAnswer = true;
|
|
324
|
+
if (rec.text) current.outputs.push(rec.text);
|
|
325
|
+
if (rec.timestamp) current.lastTimestamp = rec.timestamp;
|
|
326
|
+
} else if (rec.kind === "reasoning" || rec.kind === "tool_call" || rec.kind === "tool_result") {
|
|
327
|
+
if (!current) continue;
|
|
328
|
+
if (rec.text) current.reasoning.push(rec.text);
|
|
329
|
+
if (rec.timestamp) current.lastTimestamp = rec.timestamp;
|
|
330
|
+
} else if (rec.kind === "task_complete") {
|
|
331
|
+
// Completion boundary: the current turn is finished — emit it.
|
|
332
|
+
if (current) {
|
|
333
|
+
current.boundaryClosed = true;
|
|
334
|
+
if (rec.timestamp) current.lastTimestamp = rec.timestamp;
|
|
335
|
+
}
|
|
336
|
+
flush(true);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// EOF: hold a still-incomplete trailing turn (no boundary / no assistant yet)
|
|
340
|
+
// so it is captured whole on a later run once its completion is appended.
|
|
341
|
+
flush(false);
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
sessionUuid: sessionUuidForRollout(sessionPath),
|
|
345
|
+
firstUserText,
|
|
346
|
+
cwd,
|
|
347
|
+
startedAt,
|
|
348
|
+
exchanges,
|
|
349
|
+
// Codex keeps a session's rollout open across turns and writes a
|
|
350
|
+
// `task_complete` after EACH one, so there is no reliable end-of-session
|
|
351
|
+
// marker: an interactive session can sit idle for minutes (well past any
|
|
352
|
+
// mtime window) and then append another prompt. Finalizing from a quiet
|
|
353
|
+
// period or a trailing task_complete would mark the session done and make
|
|
354
|
+
// the orchestrator skip it forever, dropping every later turn. So Codex
|
|
355
|
+
// rollouts stay rescan-capable (`partial: true`) unconditionally; the
|
|
356
|
+
// orchestrator re-reads them each run, lastUuid resume skips already-sent
|
|
357
|
+
// turns, and seed dedup (<conv>:<user_uuid>) makes the re-scan a no-op when
|
|
358
|
+
// nothing new was appended. Combined with the incomplete-trailing-turn hold
|
|
359
|
+
// above, this guarantees any appended turn is eventually migrated exactly
|
|
360
|
+
// once.
|
|
361
|
+
partial: true,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Read + classify every JSONL record we care about, and collect the set of
|
|
367
|
+
* authoritative real-prompt texts from `event_msg`/`user_message`.
|
|
368
|
+
*/
|
|
369
|
+
function collectRecords(filePath) {
|
|
370
|
+
let raw;
|
|
371
|
+
try {
|
|
372
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
373
|
+
} catch {
|
|
374
|
+
return { records: [], realPrompts: new Set(), hasUserMessageEvents: false };
|
|
375
|
+
}
|
|
376
|
+
const records = [];
|
|
377
|
+
const realPrompts = new Set();
|
|
378
|
+
let hasUserMessageEvents = false;
|
|
379
|
+
|
|
380
|
+
for (const line of raw.split("\n")) {
|
|
381
|
+
const trimmed = line.trim();
|
|
382
|
+
if (!trimmed) continue;
|
|
383
|
+
let obj;
|
|
384
|
+
try {
|
|
385
|
+
obj = JSON.parse(trimmed);
|
|
386
|
+
} catch {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const payload = obj.payload;
|
|
390
|
+
if (!payload || typeof payload !== "object") continue;
|
|
391
|
+
const ts = obj.timestamp || null;
|
|
392
|
+
|
|
393
|
+
if (obj.type === "session_meta") {
|
|
394
|
+
records.push({
|
|
395
|
+
kind: "meta",
|
|
396
|
+
cwd: typeof payload.cwd === "string" ? payload.cwd : null,
|
|
397
|
+
timestamp: ts || (typeof payload.timestamp === "string" ? payload.timestamp : null),
|
|
398
|
+
});
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (obj.type === "event_msg") {
|
|
403
|
+
if (payload.type === "user_message" && typeof payload.message === "string") {
|
|
404
|
+
hasUserMessageEvents = true;
|
|
405
|
+
realPrompts.add(payload.message);
|
|
406
|
+
} else if (payload.type === "task_complete") {
|
|
407
|
+
records.push({ kind: "task_complete", timestamp: ts });
|
|
408
|
+
}
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (obj.type !== "response_item") continue;
|
|
413
|
+
|
|
414
|
+
if (payload.type === "message") {
|
|
415
|
+
const role = payload.role;
|
|
416
|
+
if (role === "user") {
|
|
417
|
+
records.push({
|
|
418
|
+
kind: "user", uuid: payload.id || null, timestamp: ts,
|
|
419
|
+
text: extractTextBlocks(payload.content),
|
|
420
|
+
});
|
|
421
|
+
} else if (role === "assistant") {
|
|
422
|
+
records.push({
|
|
423
|
+
kind: "assistant",
|
|
424
|
+
timestamp: ts,
|
|
425
|
+
phase: payload.phase || null,
|
|
426
|
+
text: extractTextBlocks(payload.content),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
// developer / system messages are Codex-internal — dropped.
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (payload.type === "reasoning") {
|
|
434
|
+
const text = extractReasoningSummary(payload.summary);
|
|
435
|
+
if (text) records.push({ kind: "reasoning", timestamp: ts, text: `THINKING:\n${text}` });
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (payload.type === "custom_tool_call") {
|
|
440
|
+
const name = typeof payload.name === "string" ? payload.name : "unknown";
|
|
441
|
+
const input = typeof payload.input === "string"
|
|
442
|
+
? payload.input.slice(0, 500)
|
|
443
|
+
: (payload.input ? JSON.stringify(payload.input).slice(0, 500) : "");
|
|
444
|
+
records.push({ kind: "tool_call", timestamp: ts, text: `TOOL CALL: ${name}(${input})` });
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (payload.type === "custom_tool_call_output") {
|
|
449
|
+
const output = typeof payload.output === "string"
|
|
450
|
+
? payload.output
|
|
451
|
+
: (payload.output != null ? JSON.stringify(payload.output) : "");
|
|
452
|
+
if (output) {
|
|
453
|
+
records.push({ kind: "tool_result", timestamp: ts, text: `TOOL RESULT:\n${output.slice(0, 3000)}` });
|
|
454
|
+
}
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return { records, realPrompts, hasUserMessageEvents };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Codex message content blocks are `{type:"input_text"|"output_text"|"text",
|
|
464
|
+
* text}`. Concatenate the text; ignore non-text blocks.
|
|
465
|
+
*/
|
|
466
|
+
function extractTextBlocks(content) {
|
|
467
|
+
if (typeof content === "string") return content;
|
|
468
|
+
if (!Array.isArray(content)) return "";
|
|
469
|
+
const parts = [];
|
|
470
|
+
for (const b of content) {
|
|
471
|
+
if (!b) continue;
|
|
472
|
+
if (typeof b === "string") { parts.push(b); continue; }
|
|
473
|
+
if (
|
|
474
|
+
(b.type === "input_text" || b.type === "output_text" || b.type === "text")
|
|
475
|
+
&& typeof b.text === "string"
|
|
476
|
+
) {
|
|
477
|
+
parts.push(b.text);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return parts.join("\n");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Codex reasoning items carry a `summary` array of `{type:"summary_text",
|
|
485
|
+
* text}` blocks. Concatenate their text.
|
|
486
|
+
*/
|
|
487
|
+
function extractReasoningSummary(summary) {
|
|
488
|
+
if (typeof summary === "string") return summary;
|
|
489
|
+
if (!Array.isArray(summary)) return "";
|
|
490
|
+
const parts = [];
|
|
491
|
+
for (const b of summary) {
|
|
492
|
+
if (!b) continue;
|
|
493
|
+
if (typeof b === "string") { parts.push(b); continue; }
|
|
494
|
+
if (typeof b.text === "string") parts.push(b.text);
|
|
495
|
+
}
|
|
496
|
+
return parts.join("\n");
|
|
497
|
+
}
|
|
@@ -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,
|