@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/migrate/progress.mjs
CHANGED
|
@@ -1,19 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* progress.mjs — Lightweight progress snapshots for `--migrate-status`.
|
|
3
|
+
*
|
|
4
|
+
* A run's status is a small state machine. Four states are written to
|
|
5
|
+
* `progress.json`:
|
|
6
|
+
*
|
|
7
|
+
* running: written once at daemon start.
|
|
8
|
+
* succeeded: written from the normal completion path.
|
|
9
|
+
* failed: written from the top-level exception handler, with the
|
|
10
|
+
* error class and message.
|
|
11
|
+
* cancelled: written from the SIGINT/SIGTERM handler before exit.
|
|
12
|
+
*
|
|
13
|
+
* A fifth state, `crashed`, is never written. The reader infers it for a
|
|
14
|
+
* `running` record whose PID is dead or whose heartbeat has gone stale,
|
|
15
|
+
* which is how a `kill -9`, an OOM kill, or a laptop sleep gets reported
|
|
16
|
+
* honestly instead of as a live migration. See `classify()`.
|
|
17
|
+
*
|
|
18
|
+
* `heartbeatAt` is refreshed on a timer for the same reason: a silently
|
|
19
|
+
* killed daemon runs no handler, so the only evidence of death is the
|
|
20
|
+
* absence of new writes.
|
|
3
21
|
*/
|
|
4
22
|
|
|
5
23
|
import fs from "node:fs";
|
|
6
24
|
import path from "node:path";
|
|
7
25
|
|
|
26
|
+
/** How often the daemon refreshes `heartbeatAt`. */
|
|
27
|
+
export const HEARTBEAT_INTERVAL_MS = 10_000;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* How long a heartbeat may go unrefreshed before the reader calls the run
|
|
31
|
+
* dead. Three intervals of slack absorbs a paused event loop (a long
|
|
32
|
+
* `memory_add` extraction, a suspended laptop) without flapping.
|
|
33
|
+
*/
|
|
34
|
+
export const HEARTBEAT_STALE_AFTER_MS = HEARTBEAT_INTERVAL_MS * 3;
|
|
35
|
+
|
|
36
|
+
/** Terminal states: a run in one of these will never advance again. */
|
|
37
|
+
export const TERMINAL_STATUSES = ["succeeded", "failed", "cancelled"];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Pre-2.3 runs on disk used `done` for success and `interrupted` for a
|
|
41
|
+
* signal. The reader normalizes them so a status read against a run
|
|
42
|
+
* started by an older installer still lands in the documented state set.
|
|
43
|
+
*/
|
|
44
|
+
const LEGACY_STATUS_ALIASES = {
|
|
45
|
+
done: "succeeded",
|
|
46
|
+
interrupted: "cancelled",
|
|
47
|
+
stopped: "cancelled",
|
|
48
|
+
};
|
|
49
|
+
|
|
8
50
|
/**
|
|
9
51
|
* @typedef {Object} Progress
|
|
10
52
|
* @property {string} runId
|
|
11
53
|
* @property {string} scope
|
|
12
54
|
* @property {string} startedAt
|
|
13
55
|
* @property {string|null} finishedAt
|
|
14
|
-
* @property {
|
|
56
|
+
* @property {string|null} heartbeatAt ISO timestamp, refreshed on a timer
|
|
57
|
+
* while the run is `running`.
|
|
58
|
+
* @property {number|null} pid PID of the daemon that owns the run.
|
|
59
|
+
* @property {"running"|"succeeded"|"failed"|"cancelled"} status
|
|
60
|
+
* @property {string|null} error Set alongside `failed`.
|
|
15
61
|
* @property {string|null} currentAgent
|
|
16
|
-
* @property {string|null} currentSession
|
|
62
|
+
* @property {string|null} currentSession Source session UUID being replayed.
|
|
63
|
+
* @property {string|null} currentMessage Source message (turn) id being sent.
|
|
17
64
|
* @property {Counts} counts
|
|
18
65
|
*
|
|
19
66
|
* @typedef {Object} Counts
|
|
@@ -24,20 +71,26 @@ import path from "node:path";
|
|
|
24
71
|
* @property {number} memoriesFromMD
|
|
25
72
|
* @property {number} warnings
|
|
26
73
|
* @property {number} memoryErrors
|
|
74
|
+
* @property {number} authSkipped Calls dropped on a mid-run 401/403.
|
|
27
75
|
*/
|
|
28
76
|
|
|
29
77
|
/**
|
|
30
78
|
* @returns {Progress}
|
|
31
79
|
*/
|
|
32
|
-
export function initial(runId, scope) {
|
|
80
|
+
export function initial(runId, scope, opts = {}) {
|
|
81
|
+
const now = new Date().toISOString();
|
|
33
82
|
return {
|
|
34
83
|
runId,
|
|
35
84
|
scope,
|
|
36
|
-
startedAt:
|
|
85
|
+
startedAt: now,
|
|
37
86
|
finishedAt: null,
|
|
87
|
+
heartbeatAt: now,
|
|
88
|
+
pid: opts.pid ?? process.pid,
|
|
38
89
|
status: "running",
|
|
90
|
+
error: null,
|
|
39
91
|
currentAgent: null,
|
|
40
92
|
currentSession: null,
|
|
93
|
+
currentMessage: null,
|
|
41
94
|
counts: {
|
|
42
95
|
sessionsPlanned: 0,
|
|
43
96
|
sessionsDone: 0,
|
|
@@ -46,17 +99,26 @@ export function initial(runId, scope) {
|
|
|
46
99
|
memoriesFromMD: 0,
|
|
47
100
|
warnings: 0,
|
|
48
101
|
memoryErrors: 0,
|
|
102
|
+
authSkipped: 0,
|
|
49
103
|
},
|
|
50
104
|
};
|
|
51
105
|
}
|
|
52
106
|
|
|
53
107
|
/**
|
|
54
|
-
* Atomic write.
|
|
108
|
+
* Atomic write. Refreshes `heartbeatAt` on every write of a `running`
|
|
109
|
+
* record, so ordinary progress writes double as heartbeats and the timer
|
|
110
|
+
* only matters during long gaps between them.
|
|
55
111
|
*
|
|
56
112
|
* @param {string} progressPath
|
|
57
113
|
* @param {Progress} progress
|
|
58
114
|
*/
|
|
59
115
|
export function write(progressPath, progress) {
|
|
116
|
+
if (progress && progress.status === "running") {
|
|
117
|
+
// Once `finalize()` has closed this run, a `running` write can only be
|
|
118
|
+
// stale: the live object a caller still holds, or a heartbeat tick.
|
|
119
|
+
if (sealedPaths.has(progressPath)) return;
|
|
120
|
+
progress.heartbeatAt = new Date().toISOString();
|
|
121
|
+
}
|
|
60
122
|
fs.mkdirSync(path.dirname(progressPath), { recursive: true });
|
|
61
123
|
const tmp = `${progressPath}.tmp.${process.pid}.${Date.now()}`;
|
|
62
124
|
fs.writeFileSync(tmp, JSON.stringify(progress, null, 2));
|
|
@@ -74,3 +136,175 @@ export function read(progressPath) {
|
|
|
74
136
|
return null;
|
|
75
137
|
}
|
|
76
138
|
}
|
|
139
|
+
|
|
140
|
+
// --- Heartbeat -------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
// Module singleton. The orchestrator starts the heartbeat and the daemon
|
|
143
|
+
// entrypoint stops it from its catch/signal handlers, which run in the same
|
|
144
|
+
// process after `runMigration` has stopped writing. Keeping the handle here
|
|
145
|
+
// rather than threading it through a return value means the handlers can
|
|
146
|
+
// stop it even on the path where `runMigration` threw and returned nothing.
|
|
147
|
+
let heartbeatTimer = null;
|
|
148
|
+
|
|
149
|
+
// Progress files that `finalize()` has moved to a terminal status. The
|
|
150
|
+
// catch and signal handlers finalize by read-modify-write, so the
|
|
151
|
+
// orchestrator's live `prog` object still says `running`. Sealing the path
|
|
152
|
+
// makes any later write of that object a no-op instead of a resurrection.
|
|
153
|
+
const sealedPaths = new Set();
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Refresh `heartbeatAt` every `HEARTBEAT_INTERVAL_MS` while the run is in
|
|
157
|
+
* flight. The timer is `unref`'d so it never keeps the process alive on its
|
|
158
|
+
* own; `stopHeartbeat()` is still the correct way to end it, because a live
|
|
159
|
+
* timer that fires after a terminal write would resurrect `running`.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} progressPath
|
|
162
|
+
* @param {Progress} prog The live object the orchestrator mutates.
|
|
163
|
+
* @returns {() => void} Stop function; same effect as `stopHeartbeat()`.
|
|
164
|
+
*/
|
|
165
|
+
export function startHeartbeat(progressPath, prog) {
|
|
166
|
+
stopHeartbeat();
|
|
167
|
+
heartbeatTimer = setInterval(() => {
|
|
168
|
+
if (!prog || prog.status !== "running") return;
|
|
169
|
+
try {
|
|
170
|
+
write(progressPath, prog);
|
|
171
|
+
} catch {
|
|
172
|
+
/* best effort; a missed heartbeat only costs a stale-looking read */
|
|
173
|
+
}
|
|
174
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
175
|
+
if (typeof heartbeatTimer.unref === "function") heartbeatTimer.unref();
|
|
176
|
+
return stopHeartbeat;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function stopHeartbeat() {
|
|
180
|
+
if (heartbeatTimer) {
|
|
181
|
+
clearInterval(heartbeatTimer);
|
|
182
|
+
heartbeatTimer = null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Whether a heartbeat timer is live. Lets tests prove the run stopped it. */
|
|
187
|
+
export function isHeartbeatRunning() {
|
|
188
|
+
return heartbeatTimer !== null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Write a terminal status by read-modify-write, preserving whatever counts
|
|
193
|
+
* the orchestrator last flushed.
|
|
194
|
+
*
|
|
195
|
+
* Used by the daemon entrypoint's catch and signal handlers, which don't
|
|
196
|
+
* hold the orchestrator's live `prog` object. Stops the heartbeat and seals
|
|
197
|
+
* the path first, so neither the timer nor a late write of the live object
|
|
198
|
+
* can overwrite the terminal record.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} progressPath
|
|
201
|
+
* @param {object} opts
|
|
202
|
+
* @param {"succeeded"|"failed"|"cancelled"} opts.status
|
|
203
|
+
* @param {string|null} [opts.error]
|
|
204
|
+
* @returns {{record: Progress|null, wrote: boolean}} `record` is null when
|
|
205
|
+
* there was no progress file. `wrote` is false when there was nothing to
|
|
206
|
+
* finalize or the record was already terminal, so callers only mirror a
|
|
207
|
+
* transition into the event log when it actually happened.
|
|
208
|
+
*/
|
|
209
|
+
export function finalize(progressPath, opts) {
|
|
210
|
+
stopHeartbeat();
|
|
211
|
+
sealedPaths.add(progressPath);
|
|
212
|
+
const prog = read(progressPath);
|
|
213
|
+
if (!prog) return { record: null, wrote: false };
|
|
214
|
+
// Don't overwrite an already-terminal record: a signal that arrives while
|
|
215
|
+
// the process is on its way out shouldn't rewrite `succeeded` as
|
|
216
|
+
// `cancelled`.
|
|
217
|
+
if (TERMINAL_STATUSES.includes(normalizeStatus(prog.status))) {
|
|
218
|
+
return { record: prog, wrote: false };
|
|
219
|
+
}
|
|
220
|
+
prog.status = opts.status;
|
|
221
|
+
prog.error = opts.error ?? null;
|
|
222
|
+
prog.finishedAt = new Date().toISOString();
|
|
223
|
+
prog.currentAgent = null;
|
|
224
|
+
prog.currentSession = null;
|
|
225
|
+
prog.currentMessage = null;
|
|
226
|
+
try {
|
|
227
|
+
write(progressPath, prog);
|
|
228
|
+
} catch {
|
|
229
|
+
return { record: prog, wrote: false };
|
|
230
|
+
}
|
|
231
|
+
return { record: prog, wrote: true };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// --- Reader-side classification --------------------------------------------
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Map a legacy on-disk status onto the documented state set.
|
|
238
|
+
*
|
|
239
|
+
* @param {string|undefined} status
|
|
240
|
+
* @returns {string}
|
|
241
|
+
*/
|
|
242
|
+
export function normalizeStatus(status) {
|
|
243
|
+
if (!status) return "unknown";
|
|
244
|
+
return LEGACY_STATUS_ALIASES[status] ?? status;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Decide what a progress record actually means, given process liveness.
|
|
249
|
+
*
|
|
250
|
+
* A `running` record is only believable if the daemon is still alive. Two
|
|
251
|
+
* independent checks, either of which condemns it:
|
|
252
|
+
*
|
|
253
|
+
* - the PID it names has no live process (`isPidAlive` is supplied by the
|
|
254
|
+
* caller, normally `lockfile.isStale` inverted), or
|
|
255
|
+
* - its heartbeat is older than `HEARTBEAT_STALE_AFTER_MS`.
|
|
256
|
+
*
|
|
257
|
+
* The heartbeat check is what catches a run whose PID has been recycled by
|
|
258
|
+
* an unrelated process; the PID check is what reports a `kill -9` within
|
|
259
|
+
* milliseconds instead of waiting out the stale window.
|
|
260
|
+
*
|
|
261
|
+
* @param {Progress} prog
|
|
262
|
+
* @param {object} [opts]
|
|
263
|
+
* @param {number|null} [opts.pid] PID to test; defaults to `prog.pid`.
|
|
264
|
+
* @param {(pid: number) => boolean} [opts.isPidAlive]
|
|
265
|
+
* @param {boolean} [opts.holdsLock] Whether the migrate lockfile still
|
|
266
|
+
* names this run. A live daemon always
|
|
267
|
+
* holds it; it is released on the way
|
|
268
|
+
* out. Pass this only when the caller
|
|
269
|
+
* actually read the lockfile.
|
|
270
|
+
* @param {number} [opts.now] Epoch ms; injectable for tests.
|
|
271
|
+
* @returns {{status: string, crashed: boolean, reason: string|null}}
|
|
272
|
+
*/
|
|
273
|
+
export function classify(prog, opts = {}) {
|
|
274
|
+
const status = normalizeStatus(prog?.status);
|
|
275
|
+
if (status !== "running") return { status, crashed: false, reason: null };
|
|
276
|
+
|
|
277
|
+
const now = opts.now ?? Date.now();
|
|
278
|
+
const pid = opts.pid ?? prog?.pid ?? null;
|
|
279
|
+
|
|
280
|
+
if (typeof pid === "number" && pid > 0 && typeof opts.isPidAlive === "function") {
|
|
281
|
+
if (!opts.isPidAlive(pid)) {
|
|
282
|
+
return { status: "crashed", crashed: true, reason: `pid ${pid} not alive` };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const beat = Date.parse(prog?.heartbeatAt ?? "");
|
|
287
|
+
if (Number.isFinite(beat)) {
|
|
288
|
+
const age = now - beat;
|
|
289
|
+
if (age > HEARTBEAT_STALE_AFTER_MS) {
|
|
290
|
+
return {
|
|
291
|
+
status: "crashed",
|
|
292
|
+
crashed: true,
|
|
293
|
+
reason: `no heartbeat for ${Math.round(age / 1000)}s`,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
} else if (pid === null) {
|
|
297
|
+
// A pre-heartbeat run record: no PID to test, no parseable heartbeat.
|
|
298
|
+
// The lockfile is the last piece of evidence. A daemon holds it for the
|
|
299
|
+
// whole run and releases it on the way out, so a `running` record that
|
|
300
|
+
// no longer owns the lock is a run that ended without saying so:
|
|
301
|
+
// exactly the stuck-forever records this fix exists to explain.
|
|
302
|
+
if (opts.holdsLock === false) {
|
|
303
|
+
return { status: "crashed", crashed: true, reason: "no lock holder" };
|
|
304
|
+
}
|
|
305
|
+
// Otherwise report it as running, but say why we can't vouch for it.
|
|
306
|
+
return { status: "running", crashed: false, reason: "no liveness data" };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return { status: "running", crashed: false, reason: null };
|
|
310
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Session migration adapters: id, displayName, listSessions, parseSession. */
|
|
2
|
+
import * as claudeCodeAdapter from "./adapters/claude-code.mjs";
|
|
3
|
+
import * as cursorAdapter from "./adapters/cursor.mjs";
|
|
4
|
+
import * as codexAdapter from "./adapters/codex.mjs";
|
|
5
|
+
import * as openCodeAdapter from "./adapters/opencode.mjs";
|
|
6
|
+
/** @type {Array<{id: string, displayName: string, listSessions: Function, parseSession: Function}>} */
|
|
7
|
+
export const SESSION_ADAPTERS = [
|
|
8
|
+
{
|
|
9
|
+
id: "claude-code",
|
|
10
|
+
displayName: "Claude Code",
|
|
11
|
+
listSessions: claudeCodeAdapter.listSessions,
|
|
12
|
+
parseSession: (ref) => claudeCodeAdapter.parseSession(ref.path),
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "cursor",
|
|
16
|
+
displayName: "Cursor",
|
|
17
|
+
listSessions: cursorAdapter.listSessions,
|
|
18
|
+
parseSession: (ref) => cursorAdapter.parseSession(ref.path),
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "codex",
|
|
22
|
+
displayName: "Codex",
|
|
23
|
+
listSessions: codexAdapter.listSessions,
|
|
24
|
+
parseSession: (ref) => codexAdapter.parseSession(ref.path),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "opencode",
|
|
28
|
+
displayName: "OpenCode",
|
|
29
|
+
listSessions: openCodeAdapter.listSessions,
|
|
30
|
+
parseSession: (ref, opts) => openCodeAdapter.parseSession(ref, opts),
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
/** @returns {Promise<Array<{ ref: object, source: string }>>} */
|
|
34
|
+
export async function enumerateSessionRefs(opts) {
|
|
35
|
+
const out = [];
|
|
36
|
+
const adapters = opts.sessionAdapters ?? SESSION_ADAPTERS;
|
|
37
|
+
for (const adapter of adapters) {
|
|
38
|
+
let refs;
|
|
39
|
+
try {
|
|
40
|
+
refs = await adapter.listSessions(opts);
|
|
41
|
+
if (!Array.isArray(refs)) {
|
|
42
|
+
throw new Error("listSessions did not return an array");
|
|
43
|
+
}
|
|
44
|
+
for (const ref of refs) {
|
|
45
|
+
out.push({ ref, source: adapter.id, adapter });
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
opts.onSourceError?.({
|
|
49
|
+
source: adapter.id,
|
|
50
|
+
displayName: adapter.displayName,
|
|
51
|
+
error,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/** @returns {Promise<object>|object} */
|
|
58
|
+
export function parseTaggedSession(tagged, opts = {}) {
|
|
59
|
+
const adapter =
|
|
60
|
+
tagged.adapter ?? SESSION_ADAPTERS.find((a) => a.id === tagged.source);
|
|
61
|
+
if (!adapter) throw new Error(`unknown migration source: ${tagged.source}`);
|
|
62
|
+
return adapter.parseSession(tagged.ref, opts);
|
|
63
|
+
}
|
|
64
|
+
/** Legacy summary field names keyed by adapter id. */
|
|
65
|
+
export function sessionsByAdapter(tagged) {
|
|
66
|
+
/** @type {Record<string, number>} */
|
|
67
|
+
const counts = {};
|
|
68
|
+
for (const { source } of tagged) {
|
|
69
|
+
counts[source] = (counts[source] || 0) + 1;
|
|
70
|
+
}
|
|
71
|
+
return counts;
|
|
72
|
+
}
|
package/lib/migrate.mjs
CHANGED
|
@@ -56,7 +56,13 @@ function paths(opts = {}) {
|
|
|
56
56
|
statePath,
|
|
57
57
|
lockPath: path.join(root, "lock"),
|
|
58
58
|
runDir: (runId) => path.join(root, runId),
|
|
59
|
-
|
|
59
|
+
// Where the child's stdio is redirected. Everything the daemon has to
|
|
60
|
+
// say goes through the NDJSON logger instead, so this file is normally
|
|
61
|
+
// empty; it only catches process-level output that escapes the logger
|
|
62
|
+
// (a stack trace from before it opens, a native crash). Don't point
|
|
63
|
+
// users here; point them at `eventLogFile`.
|
|
64
|
+
stdioFile: (runId) => path.join(root, runId, "stdout.log"),
|
|
65
|
+
eventLogFile: (runId) => path.join(root, runId, "log.ndjson"),
|
|
60
66
|
progressFile: (runId) => path.join(root, runId, "progress.json"),
|
|
61
67
|
};
|
|
62
68
|
}
|
|
@@ -212,10 +218,10 @@ export async function startMigration(opts) {
|
|
|
212
218
|
// don't move when the state key changes.
|
|
213
219
|
const urlDatapackKey = datapackKeyFromUrl(opts.url);
|
|
214
220
|
let P = paths({ datapackKey: urlDatapackKey });
|
|
215
|
-
fs.mkdirSync(P.root, { recursive: true });
|
|
216
221
|
|
|
217
222
|
// Dry-run is a local-only planning pass: no HTTP, no lock, no fork.
|
|
218
|
-
// Don't gate it on URL/apiKey
|
|
223
|
+
// Don't gate it on URL/apiKey, and don't create the migration root: a
|
|
224
|
+
// planning pass must not appear as a phantom active run in --migrate-status.
|
|
219
225
|
if (opts.dryRun) {
|
|
220
226
|
const runId = generateRunId();
|
|
221
227
|
const runDir = P.runDir(runId);
|
|
@@ -231,9 +237,29 @@ export async function startMigration(opts) {
|
|
|
231
237
|
dryRun: true,
|
|
232
238
|
noExtract: opts.noExtract,
|
|
233
239
|
concurrency: opts.rate,
|
|
240
|
+
sessionAdapters: opts.sessionAdapters,
|
|
241
|
+
opencodeBin: opts.opencodeBin,
|
|
242
|
+
runExport: opts.runExport,
|
|
234
243
|
});
|
|
235
244
|
const s = summary.summary;
|
|
245
|
+
if (!summary.ok) {
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
runId,
|
|
249
|
+
message: `Dry run failed: ${s.detail || s.error || "unknown migration error"}`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
236
252
|
const planned = s.plannedCalls ?? {};
|
|
253
|
+
const warningLines = [
|
|
254
|
+
...(s.sourceErrors ?? []).map(
|
|
255
|
+
({ source, error }) => ` Warning: ${source} enumeration failed: ${error}\n`,
|
|
256
|
+
),
|
|
257
|
+
...(s.sessionErrors ?? []).map(
|
|
258
|
+
({ source, sessionUuid, error }) =>
|
|
259
|
+
` Warning: ${source} session ${sessionUuid} parse failed: ${error}\n`,
|
|
260
|
+
),
|
|
261
|
+
]
|
|
262
|
+
.join("");
|
|
237
263
|
// Dry-run never opens an MCP session, so we can't resolve names→UUIDs
|
|
238
264
|
// here. Echo what the user asked for; the real run will resolve and
|
|
239
265
|
// print the canonical destination before doing any work.
|
|
@@ -249,8 +275,11 @@ export async function startMigration(opts) {
|
|
|
249
275
|
destLine +
|
|
250
276
|
` Claude Code sessions: ${s.claudeSessions}\n` +
|
|
251
277
|
` Cursor sessions: ${s.cursorSessions}\n` +
|
|
278
|
+
` Codex sessions: ${s.codexSessions}\n` +
|
|
279
|
+
` OpenCode sessions: ${s.openCodeSessions}\n` +
|
|
252
280
|
` Paired user↔asst turns: ${s.pairedTurns}\n` +
|
|
253
281
|
` Memory items (MD): ${s.memoryItems}\n` +
|
|
282
|
+
warningLines +
|
|
254
283
|
` Planned MCP calls:\n` +
|
|
255
284
|
` conversation_create: ${planned.conversation_create ?? 0}\n` +
|
|
256
285
|
` conversation_add_message: ${planned.conversation_add_message ?? 0}\n` +
|
|
@@ -260,6 +289,8 @@ export async function startMigration(opts) {
|
|
|
260
289
|
};
|
|
261
290
|
}
|
|
262
291
|
|
|
292
|
+
fs.mkdirSync(P.root, { recursive: true });
|
|
293
|
+
|
|
263
294
|
// Real run — now URL/apiKey are required (dry-run branch already returned).
|
|
264
295
|
if (!opts.url) {
|
|
265
296
|
return { ok: false, message: "--url (or MEKO_MCP_URL) is required for --migrate." };
|
|
@@ -373,7 +404,7 @@ export async function startMigration(opts) {
|
|
|
373
404
|
scriptPath: opts.scriptPath,
|
|
374
405
|
runId,
|
|
375
406
|
args: passthrough,
|
|
376
|
-
logFilePath: P.
|
|
407
|
+
logFilePath: P.stdioFile(runId),
|
|
377
408
|
env,
|
|
378
409
|
cwd: opts.projectRoot,
|
|
379
410
|
});
|
|
@@ -393,7 +424,7 @@ export async function startMigration(opts) {
|
|
|
393
424
|
message:
|
|
394
425
|
`${datapackBanner}\n` +
|
|
395
426
|
`Migration started. run-id=${runId}, pid=${pid}\n` +
|
|
396
|
-
`Logs: ${P.
|
|
427
|
+
`Logs: ${P.eventLogFile(runId)} (one JSON object per line)\n` +
|
|
397
428
|
`Status: ${selfInvocation()} --migrate-status`,
|
|
398
429
|
};
|
|
399
430
|
}
|
|
@@ -518,11 +549,21 @@ export async function runChildMigration(opts) {
|
|
|
518
549
|
log.warn("legacy_state_migrate_failed", { error: err.message });
|
|
519
550
|
}
|
|
520
551
|
|
|
552
|
+
const progressPath = P.progressFile(opts.runId);
|
|
553
|
+
|
|
521
554
|
let shuttingDown = false;
|
|
522
555
|
const onSignal = (sig) => {
|
|
523
556
|
if (shuttingDown) return;
|
|
524
557
|
shuttingDown = true;
|
|
525
558
|
log.warn("interrupted", { signal: sig });
|
|
559
|
+
// Write the terminal state before releasing the lock. Skipping this is
|
|
560
|
+
// what left a clean `--migrate-stop` reading as `running` forever.
|
|
561
|
+
try {
|
|
562
|
+
const { wrote } = progress.finalize(progressPath, { status: "cancelled", error: `signal ${sig}` });
|
|
563
|
+
// A signal after the orchestrator's own terminal write leaves that
|
|
564
|
+
// status in place, so only log a transition that actually happened.
|
|
565
|
+
if (wrote) log.info("status", { status: "cancelled", signal: sig, runId: opts.runId });
|
|
566
|
+
} catch { /* best effort; we're on our way out */ }
|
|
526
567
|
try { lockfile.release(P.lockPath); } catch { /* ignore */ }
|
|
527
568
|
log.close();
|
|
528
569
|
process.exit(130);
|
|
@@ -552,18 +593,37 @@ export async function runChildMigration(opts) {
|
|
|
552
593
|
return result.ok ? 0 : 1;
|
|
553
594
|
} catch (err) {
|
|
554
595
|
log.error("child_crashed", { error: err.message, stack: err.stack });
|
|
596
|
+
// The orchestrator threw before reaching its own terminal write, so the
|
|
597
|
+
// progress file is still on `running`. Close the state machine here.
|
|
598
|
+
try {
|
|
599
|
+
const { wrote } = progress.finalize(progressPath, {
|
|
600
|
+
status: "failed",
|
|
601
|
+
error: `${err.name ?? "Error"}: ${err.message}`,
|
|
602
|
+
});
|
|
603
|
+
if (wrote) log.info("status", { status: "failed", error: err.message, runId: opts.runId });
|
|
604
|
+
} catch { /* best effort */ }
|
|
555
605
|
log.close();
|
|
556
606
|
try { lockfile.release(P.lockPath); } catch { /* ignore */ }
|
|
557
607
|
return 1;
|
|
608
|
+
} finally {
|
|
609
|
+
// Whatever path we left on, no timer should outlive the run and rewrite
|
|
610
|
+
// a terminal record back to `running`.
|
|
611
|
+
progress.stopHeartbeat();
|
|
558
612
|
}
|
|
559
613
|
}
|
|
560
614
|
|
|
561
615
|
/**
|
|
562
616
|
* Print and return the status of the latest (or requested) migration run.
|
|
563
617
|
*
|
|
618
|
+
* The on-disk `status` field is not taken at face value. A `running` record
|
|
619
|
+
* whose daemon is gone reports as `crashed`, which is how a `kill -9`, an
|
|
620
|
+
* OOM kill, or a reboot shows up. None of them run a handler that could
|
|
621
|
+
* have written a terminal state. `progress.status` stays as written;
|
|
622
|
+
* the returned `status` is the effective one.
|
|
623
|
+
*
|
|
564
624
|
* @param {object} opts
|
|
565
625
|
* @param {string} [opts.runId]
|
|
566
|
-
* @returns {{ok: boolean, runId: string|null, progress: object|null, message: string}}
|
|
626
|
+
* @returns {{ok: boolean, runId: string|null, progress: object|null, status?: string, crashed?: boolean, message: string}}
|
|
567
627
|
*/
|
|
568
628
|
export function readStatus(opts = {}) {
|
|
569
629
|
const P = paths();
|
|
@@ -618,23 +678,53 @@ export function readStatus(opts = {}) {
|
|
|
618
678
|
const RESET = useColor ? "\x1b[0m" : "";
|
|
619
679
|
const note = (s) => (s ? ` ${GRAY}${s}${RESET}` : "");
|
|
620
680
|
|
|
681
|
+
// A `running` record is only worth believing if the daemon is still there.
|
|
682
|
+
// The lockfile names the owning PID; prefer it over the PID the run
|
|
683
|
+
// recorded, since --force can hand the lock to a newer process.
|
|
684
|
+
const holder = lockfile.read(P.lockPath);
|
|
685
|
+
const holdsLock = !!holder && holder.runId === runId;
|
|
686
|
+
const livePid = holdsLock ? holder.pid : (prog.pid ?? null);
|
|
687
|
+
const verdict = progress.classify(prog, {
|
|
688
|
+
pid: livePid,
|
|
689
|
+
holdsLock,
|
|
690
|
+
isPidAlive: (pid) => !lockfile.isStale({ pid }),
|
|
691
|
+
});
|
|
692
|
+
const statusLabel = verdict.reason
|
|
693
|
+
? `${verdict.status} (${verdict.reason})`
|
|
694
|
+
: verdict.status;
|
|
695
|
+
|
|
621
696
|
const lines = [
|
|
622
697
|
`run-id: ${prog.runId}`,
|
|
623
|
-
`status: ${
|
|
698
|
+
`status: ${statusLabel}${note("(running → succeeded | failed | cancelled; crashed is inferred)")}`,
|
|
624
699
|
`scope: ${prog.scope}`,
|
|
625
700
|
`datapack: ${datapackKeyLabel}${note("(resume state lives under state/<key>.json)")}`,
|
|
626
701
|
`target pack: ${datapackTargetLabel}${note("(ingestion destination resolved at run start)")}`,
|
|
627
702
|
`started: ${prog.startedAt}`,
|
|
628
|
-
`finished: ${prog.finishedAt ?? "(in progress)"}`,
|
|
629
|
-
`
|
|
703
|
+
`finished: ${prog.finishedAt ?? (verdict.crashed ? "(never, daemon died)" : "(in progress)")}`,
|
|
704
|
+
`heartbeat: ${prog.heartbeatAt ?? "-"}${note("(refreshed while running; staleness implies crashed)")}`,
|
|
705
|
+
`current: ${prog.currentAgent ?? "-"} / ${prog.currentSession ?? "-"} / ${prog.currentMessage ?? "-"}${note("(<agent-id> / <source-session> / <message-id>)")}`,
|
|
630
706
|
`sessions: ${prog.counts.sessionsDone} / ${prog.counts.sessionsPlanned}${note("(processed / discovered)")}`,
|
|
631
707
|
`messages: ${prog.counts.messagesSent}`,
|
|
632
708
|
`memory pairs: ${prog.counts.memoryPairsSent}${note("(memory_add from Mem0 extractor)")}`,
|
|
633
709
|
`memory (MD): ${prog.counts.memoriesFromMD}${note("(memory_add from CLAUDE.md / memory files)")}`,
|
|
634
710
|
`warnings: ${prog.counts.warnings}${note("(non-fatal; run continues)")}`,
|
|
635
711
|
`memory errs: ${prog.counts.memoryErrors}${note("(failed memory_add calls; re-run with --force to retry)")}`,
|
|
712
|
+
`auth skipped: ${prog.counts.authSkipped ?? 0}${note("(calls dropped on a mid-run 401/403; re-run to retry them)")}`,
|
|
636
713
|
];
|
|
637
|
-
|
|
714
|
+
if (prog.error) {
|
|
715
|
+
lines.push(`error: ${prog.error}`);
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
ok: true,
|
|
719
|
+
runId,
|
|
720
|
+
progress: prog,
|
|
721
|
+
// The effective status, after the liveness check. `progress.status` is
|
|
722
|
+
// the raw on-disk field and can still read `running` for a dead daemon;
|
|
723
|
+
// callers that care whether the run is actually alive want this one.
|
|
724
|
+
status: verdict.status,
|
|
725
|
+
crashed: verdict.crashed,
|
|
726
|
+
message: lines.join("\n"),
|
|
727
|
+
};
|
|
638
728
|
}
|
|
639
729
|
|
|
640
730
|
/**
|
package/lib/prompt.mjs
CHANGED
|
@@ -37,6 +37,21 @@ function question(query) {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Apply terminal erase bytes that survive readline's muted-output path.
|
|
42
|
+
* Some PTYs leave DEL/BS in the answer when readline is attached to a
|
|
43
|
+
* non-TTY Writable, even with terminal mode enabled. Treat them exactly as
|
|
44
|
+
* interactive backspace so a corrected secret never persists control bytes.
|
|
45
|
+
*/
|
|
46
|
+
function applyEraseCharacters(value) {
|
|
47
|
+
const chars = [];
|
|
48
|
+
for (const char of value) {
|
|
49
|
+
if (char === "\x7f" || char === "\b") chars.pop();
|
|
50
|
+
else chars.push(char);
|
|
51
|
+
}
|
|
52
|
+
return chars.join("");
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
// ---------------------------------------------------------------------------
|
|
41
56
|
// askChoice
|
|
42
57
|
// ---------------------------------------------------------------------------
|
|
@@ -218,7 +233,7 @@ export async function askSecret(questionText) {
|
|
|
218
233
|
|
|
219
234
|
secretRL.question("", (answer) => {
|
|
220
235
|
finish();
|
|
221
|
-
resolve(answer);
|
|
236
|
+
resolve(applyEraseCharacters(answer));
|
|
222
237
|
});
|
|
223
238
|
});
|
|
224
239
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yugabytedb/meko-mcp",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.3.0",
|
|
4
|
+
"description": "Installer for Meko across Claude, Cursor, Codex, and Kiro clients",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-meko-setup": "bin/create-meko-setup.mjs",
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
"prepack": "node scripts/bundle-assets.mjs",
|
|
19
19
|
"postpack": "node scripts/bundle-assets.mjs --clean",
|
|
20
20
|
"bundle-assets": "node scripts/bundle-assets.mjs",
|
|
21
|
-
"test": "node --test \"test/*.mjs\"",
|
|
21
|
+
"test": "node --test \"test/*.mjs\" && node --test \"test/serial/*.mjs\"",
|
|
22
22
|
"smoke": "./scripts/prepublish-smoke.sh",
|
|
23
23
|
"test:unit": "node --test test/config.test.mjs test/validate.test.mjs test/clients.test.mjs",
|
|
24
24
|
"test:integration": "RUN_INSTALLER_INTEGRATION=1 node --test test/installer.test.mjs",
|
|
25
25
|
"test:integration:cloud": "RUN_INSTALLER_INTEGRATION=1 node --test test/installer.test.mjs"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
|
-
"node": ">=18.
|
|
28
|
+
"node": ">=20.18.1"
|
|
29
29
|
},
|
|
30
30
|
"keywords": [
|
|
31
31
|
"meko",
|
|
@@ -30,6 +30,10 @@ const PAYLOAD = [
|
|
|
30
30
|
from: join(repoRoot, "skills", "hooks-handlers"),
|
|
31
31
|
to: join(assetsDir, "hooks-handlers"),
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
from: join(repoRoot, "skills", "opencode-plugin"),
|
|
35
|
+
to: join(assetsDir, "opencode-plugin"),
|
|
36
|
+
},
|
|
33
37
|
{
|
|
34
38
|
from: join(repoRoot, "skills", "scripts", "package-desktop-skill.sh"),
|
|
35
39
|
to: join(assetsDir, "scripts", "package-desktop-skill.sh"),
|