@yugabytedb/meko-mcp 2.3.0 → 2.5.1

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.
Files changed (42) hide show
  1. package/README.md +88 -35
  2. package/assets/hooks-handlers/lib/capture-cleanup-cli.js +735 -74
  3. package/assets/hooks-handlers/lib/capture-notices.js +94 -1
  4. package/assets/hooks-handlers/lib/capture-state.js +111 -0
  5. package/assets/hooks-handlers/lib/capture.js +130 -4
  6. package/assets/hooks-handlers/lib/checkpoint-timer.js +394 -8
  7. package/assets/hooks-handlers/lib/opencode-handler.js +29 -6
  8. package/assets/scripts/meko-statusline.sh +17 -1
  9. package/assets/skills/meko-mcp-tools/SKILL.md +6 -5
  10. package/assets/skills/meko-mcp-tools/references/_sections.md +1 -1
  11. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +194 -10
  12. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +6 -3
  13. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +11 -6
  14. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +4 -4
  15. package/assets/skills/meko-mcp-tools/references/tools-overview.md +19 -14
  16. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +69 -8
  17. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +3 -1
  18. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +8 -7
  19. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +4 -4
  20. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
  21. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +194 -10
  22. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +10 -5
  23. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +4 -4
  24. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +19 -14
  25. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +68 -8
  26. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +3 -1
  27. package/assets/skills/meko-select-datapack-desktop/SKILL.md +91 -12
  28. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +39 -7
  29. package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +8 -6
  30. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +13 -1
  31. package/bin/create-meko-setup.mjs +152 -35
  32. package/lib/autofix.mjs +100 -0
  33. package/lib/browser-login.mjs +495 -0
  34. package/lib/clients/codex.mjs +1 -24
  35. package/lib/clients/common.mjs +25 -0
  36. package/lib/clients/opencode.mjs +6 -1
  37. package/lib/migrate/datapack.mjs +254 -16
  38. package/lib/migrate/mcp-client.mjs +34 -0
  39. package/lib/migrate/orchestrator.mjs +1 -1
  40. package/lib/migrate.mjs +2 -6
  41. package/lib/validate.mjs +111 -22
  42. package/package.json +5 -5
@@ -18,6 +18,8 @@
18
18
  "use strict";
19
19
 
20
20
  const {
21
+ autofixEnabled,
22
+ autofixIntervalSeconds,
21
23
  computeAggregateHealth,
22
24
  readHealthCache,
23
25
  scanCompleteForDropClamp,
@@ -107,6 +109,94 @@ function statuslineSuffixFromHealth(health) {
107
109
  return `capture:${short}(${queued})`;
108
110
  }
109
111
 
112
+ const NEEDS_YOU_LABELS = {
113
+ auth: "API key",
114
+ account_limit: "plan limit",
115
+ datapack: "datapack access",
116
+ forbidden: "access",
117
+ };
118
+
119
+ /**
120
+ * Claude Code status bar text for capture, or "" when there is nothing to
121
+ * show. Built when the health cache is written and read by
122
+ * meko-statusline.sh, which the npm installer copies and wires into Claude
123
+ * Code's statusLine; the plugin ships no status-line reader, so a plugin-only
124
+ * install shows nothing. `autofix` is the last `--capture-cleanup fix` summary
125
+ * (autofix-last.json). Uses a clock time for the next automatic fix, so the
126
+ * text stays right between cache writes.
127
+ *
128
+ * @param {object} health
129
+ * @param {object|null} autofix
130
+ * @param {number} [nowMs]
131
+ * @returns {string}
132
+ */
133
+ function statuslineText(health, autofix, nowMs) {
134
+ return statuslineFields(health, autofix, nowMs).statusline;
135
+ }
136
+
137
+ /**
138
+ * statuslineText plus when it stops being true: `statusline_until` (epoch
139
+ * seconds) and the text to show after that, `statusline_after`. The
140
+ * status bar compares them with the clock, so "auto-fixed N" disappears after
141
+ * an hour and "auto-fix at 14:30" turns into "auto-fix soon" at 14:30, even
142
+ * when nothing rewrites the cache.
143
+ */
144
+ function statuslineFields(health, autofix, nowMs) {
145
+ const until = (ms, after) => ({ statusline_until: Math.floor(ms / 1000), statusline_after: after });
146
+ const h = health && typeof health === "object" ? health : {};
147
+ const status = h.status || "healthy";
148
+ const now = nowMs != null ? nowMs : Date.now();
149
+ const a = autofix && typeof autofix === "object" ? autofix : null;
150
+ const sessions = Array.isArray(h.sessions) ? h.sessions : [];
151
+ const queued = Number(h.queued_exchanges || 0);
152
+ const stopped = sessions.filter((x) => x && x.parked_reason === "stop");
153
+ const parkedByFix = sessions.filter((x) => x && x.parked_reason && x.parked_reason !== "stop");
154
+ const stuck = sessions.filter((x) => x && x.status !== "healthy" && !x.parked_reason);
155
+
156
+ // A run that never finished (its timer was killed) leaves a placeholder;
157
+ // once it is older than any run can take, its needs_you list is stale.
158
+ const ranAtMs = a ? Date.parse(a.ran_at) : NaN;
159
+ const abandoned = Boolean(a && a.running && (!Number.isFinite(ranAtMs) || now - ranAtMs > 30 * 60 * 1000));
160
+ const needs = a && !abandoned && Array.isArray(a.needs_you) ? a.needs_you : [];
161
+ if (needs.length && (parkedByFix.length || stuck.length)) {
162
+ const labels = [...new Set(needs.map((n) => NEEDS_YOU_LABELS[n.cause] || n.cause))];
163
+ return { statusline: `capture: needs you (${labels.join(", ")})` };
164
+ }
165
+
166
+ if (status === "healthy") {
167
+ const fixed = a && a.counts ? Number(a.counts.fixed || 0) : 0;
168
+ const ranAt = a ? Date.parse(a.ran_at) : NaN;
169
+ if (a && a.mode === "auto" && fixed > 0 && Number.isFinite(ranAt) && now - ranAt < 3600 * 1000) {
170
+ return { statusline: `capture: auto-fixed ${fixed}`, ...until(ranAt + 3600 * 1000, "") };
171
+ }
172
+ return { statusline: "" };
173
+ }
174
+
175
+ const q = `${queued} queued`;
176
+ if (!stuck.length && !parkedByFix.length && stopped.length) {
177
+ return { statusline: `capture: paused (${q})` };
178
+ }
179
+
180
+ let next = "";
181
+ let due = null;
182
+ if (autofixEnabled()) {
183
+ const ranAt = a ? Date.parse(a.ran_at) : NaN;
184
+ due = (Number.isFinite(ranAt) ? ranAt : now) + autofixIntervalSeconds() * 1000;
185
+ next =
186
+ due <= now
187
+ ? ", auto-fix soon"
188
+ : `, auto-fix at ${new Date(due).toTimeString().slice(0, 5)}`;
189
+ }
190
+ const label =
191
+ status === "degraded_retrying" ? "retrying" : status === "blocked_action_required" ? "blocked" : null;
192
+ if (!label) return { statusline: `capture: sending ${queued}` };
193
+ const text = `capture: ${label} (${q})${next}`;
194
+ if (due != null && due > now) {
195
+ return { statusline: text, ...until(due, `capture: ${label} (${q}), auto-fix soon`) };
196
+ }
197
+ return { statusline: text };
198
+ }
199
+
110
200
  /**
111
201
  * Sentence describing exchanges that were skipped because the server will
112
202
  * never accept them, or "" when there are none new to report.
@@ -236,7 +326,8 @@ function buildHealthNotice({ health, previousStatus, previousDropped } = {}) {
236
326
  : "catching_up";
237
327
 
238
328
  const cleanupHint = retriesRunning(h)
239
- ? " If the user wants the retries to stop, offer " +
329
+ ? " To repair it, offer `npx @yugabytedb/meko-mcp@latest " +
330
+ "--capture-cleanup fix`. To only stop the retries, offer " +
240
331
  "`npx @yugabytedb/meko-mcp@latest --capture-cleanup stop`; it parks " +
241
332
  "the backlog without deleting it."
242
333
  : "";
@@ -429,6 +520,8 @@ module.exports = {
429
520
  affectedSessionCount,
430
521
  reasonForHealth,
431
522
  statuslineSuffixFromHealth,
523
+ statuslineText,
524
+ statuslineFields,
432
525
  buildHealthNotice,
433
526
  injectHealthNotice,
434
527
  formatCaptureStatusText,
@@ -131,6 +131,53 @@ function healthCacheFilename() {
131
131
  return envString("MEKO_HEALTH_CACHE", DEFAULTS.HEALTH_CACHE);
132
132
  }
133
133
 
134
+ /**
135
+ * The retry date `--capture-cleanup` parks a session until. Hooks skip the
136
+ * network while failure_class is set and next_retry_at is in the future, so
137
+ * a session with this date is parked, not retrying.
138
+ */
139
+ const CLEANUP_PARK_UNTIL = "9999-12-31T00:00:00.000Z";
140
+
141
+ /**
142
+ * Automatic repair from the checkpoint timer. MEKO_AUTOFIX (on/off) wins when
143
+ * set; otherwise autofix-config.json in the capture directory, which the
144
+ * installer writes for --autofix / --no-autofix. On by default.
145
+ */
146
+ function autofixEnabled() {
147
+ return autofixSetting().enabled;
148
+ }
149
+
150
+ /**
151
+ * Whether automatic repair is on, and what decided it: "env" (MEKO_AUTOFIX),
152
+ * "config" (autofix-config.json, written by --autofix / --no-autofix), or
153
+ * "default" (on).
154
+ */
155
+ function autofixSetting() {
156
+ const raw = process.env.MEKO_AUTOFIX;
157
+ if (raw != null && raw !== "") {
158
+ return { enabled: !/^(0|off|false|no)$/i.test(String(raw)), source: "env", path: null };
159
+ }
160
+ const file = autofixConfigPath();
161
+ try {
162
+ const cfg = JSON.parse(fs.readFileSync(file, "utf8"));
163
+ if (cfg && typeof cfg.enabled === "boolean") {
164
+ return { enabled: cfg.enabled, source: "config", path: file };
165
+ }
166
+ } catch {
167
+ /* no config: default on */
168
+ }
169
+ return { enabled: true, source: "default", path: null };
170
+ }
171
+
172
+ function autofixConfigPath() {
173
+ return path.join(watermarkDir(), "autofix-config.json");
174
+ }
175
+
176
+ /** Seconds between automatic `--capture-cleanup fix` runs on one machine. */
177
+ function autofixIntervalSeconds() {
178
+ return Math.max(60, envInt("MEKO_AUTOFIX_INTERVAL", 1800));
179
+ }
180
+
134
181
  // ---------------------------------------------------------------------------
135
182
  // Path helpers
136
183
  // ---------------------------------------------------------------------------
@@ -162,6 +209,20 @@ function healthCachePath() {
162
209
  return path.join(watermarkDir(), healthCacheFilename());
163
210
  }
164
211
 
212
+ /** Summary of the last `--capture-cleanup fix` run, manual or automatic. */
213
+ function autofixLastPath() {
214
+ return path.join(watermarkDir(), "autofix-last.json");
215
+ }
216
+
217
+ function readAutofixLast() {
218
+ try {
219
+ const value = JSON.parse(fs.readFileSync(autofixLastPath(), "utf8"));
220
+ return value && typeof value === "object" ? value : null;
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+
165
226
  /**
166
227
  * Convert an agent_id into the filesystem slug used by legacy error files and
167
228
  * datapack pins (matches capture.js datapackPinSlug).
@@ -1295,6 +1356,8 @@ const PERSISTENT_PATTERNS = [
1295
1356
  ["payment_required", /payment[_\s-]?required/i],
1296
1357
  ["subscription_required", /subscription[_\s-]?(required|inactive|expired)/i],
1297
1358
  ["entitlement_denied", /entitlement.*(denied|required|missing)|(?:denied|required|missing).*entitlement/i],
1359
+ // A viewer of a shared datapack can't record conversations (MEKO-876).
1360
+ ["read_only_access", /read_only_access/i],
1298
1361
  ];
1299
1362
 
1300
1363
  /**
@@ -1422,6 +1485,26 @@ function timerLooksLive(sessionId) {
1422
1485
  }
1423
1486
  }
1424
1487
 
1488
+ /**
1489
+ * A session whose client may still be writing turns: its lifecycle is
1490
+ * active and its transcript changed within recoveryMinIdle() seconds, or its
1491
+ * checkpoint timer is alive. --capture-cleanup uses it to decide drain or
1492
+ * recover, and capture.js move re-checks it under the lease.
1493
+ */
1494
+ function sessionLooksLive(state, nowMs) {
1495
+ if (!state || state.lifecycle !== "active") return false;
1496
+ const now = nowMs != null ? nowMs : Date.now();
1497
+ if (state.transcript_path) {
1498
+ try {
1499
+ const st = fs.statSync(state.transcript_path);
1500
+ if (now - st.mtimeMs < recoveryMinIdle() * 1000) return true;
1501
+ } catch {
1502
+ // A missing transcript says nothing about liveness; check the timer.
1503
+ }
1504
+ }
1505
+ return timerLooksLive(state.session_id);
1506
+ }
1507
+
1425
1508
  function buildEligibility(state, nowMs, options) {
1426
1509
  const opts = options && typeof options === "object" ? options : {};
1427
1510
  const now = nowMs != null ? nowMs : Date.now();
@@ -1736,6 +1819,16 @@ function computeAggregateHealth(states, options) {
1736
1819
  queued_exchanges: q,
1737
1820
  dropped_exchanges: d,
1738
1821
  lifecycle: state ? state.lifecycle : null,
1822
+ // Why --capture-cleanup parked this session ("stop", "drain failed: ...")
1823
+ // while the park is in effect; null otherwise.
1824
+ parked_reason:
1825
+ state &&
1826
+ state.parked &&
1827
+ typeof state.parked === "object" &&
1828
+ state.failure_class &&
1829
+ state.next_retry_at === CLEANUP_PARK_UNTIL
1830
+ ? String(state.parked.reason || "parked")
1831
+ : null,
1739
1832
  });
1740
1833
  }
1741
1834
 
@@ -1782,6 +1875,16 @@ function writeHealthCache(health, options) {
1782
1875
  ? Math.min(previous, coerceNonNegInt(merged.dropped_exchanges, 0))
1783
1876
  : previous;
1784
1877
  }
1878
+ // A ready-made status bar text, so meko-statusline.sh needs no JSON parsing
1879
+ // beyond one field. capture-notices requires this module, so load it here.
1880
+ try {
1881
+ const fields = require("./capture-notices").statuslineFields(merged, readAutofixLast());
1882
+ merged.statusline = fields.statusline;
1883
+ merged.statusline_until = fields.statusline_until != null ? fields.statusline_until : null;
1884
+ merged.statusline_after = fields.statusline_after != null ? fields.statusline_after : null;
1885
+ } catch {
1886
+ /* keep the cache write even if the text can't be built */
1887
+ }
1785
1888
  try {
1786
1889
  atomicWriteJson(healthCachePath(), merged);
1787
1890
  return { ok: true, path: healthCachePath(), health: merged };
@@ -2117,6 +2220,13 @@ module.exports = {
2117
2220
  workerMaxAgeSeconds,
2118
2221
  timerMaxAge,
2119
2222
  healthCacheFilename,
2223
+ CLEANUP_PARK_UNTIL,
2224
+ autofixEnabled,
2225
+ autofixSetting,
2226
+ autofixConfigPath,
2227
+ autofixIntervalSeconds,
2228
+ autofixLastPath,
2229
+ readAutofixLast,
2120
2230
 
2121
2231
  // paths
2122
2232
  watermarkDir,
@@ -2124,6 +2234,7 @@ module.exports = {
2124
2234
  leasePath,
2125
2235
  mutationLockPath,
2126
2236
  timerPidPath,
2237
+ sessionLooksLive,
2127
2238
  healthCachePath,
2128
2239
  legacyErrorPath,
2129
2240
  agentSlug,
@@ -13,6 +13,10 @@
13
13
  * node capture.js checkpoint # stdin hook JSON → drainSession
14
14
  * node capture.js drain # stdin JSON → drain one session; stdout DrainResult
15
15
  * node capture.js recover # alias of drain (single-session recovery)
16
+ * node capture.js move # stdin {"session_id"} → move the unsent
17
+ * # tail to a new conversation; stdout
18
+ * # {ok, from, to, last_line_number} or
19
+ * # {ok:false, busy?, error}
16
20
  *
17
21
  * drain / recover stdin contract (Workstream C):
18
22
  * {
@@ -1563,6 +1567,9 @@ function classifyPersistentCaptureFailure(err, opts) {
1563
1567
  // access is (re-)granted out of band, so the caller should hold rather
1564
1568
  // than retry forever or silently drop the turn.
1565
1569
  ["datapack_access_denied", /datapack_access_denied/],
1570
+ // MEKO-876: a viewer of a shared datapack can't record conversations.
1571
+ // Account-scoped like the above: it stays refused until the role changes.
1572
+ ["read_only_access", /read_only_access/],
1566
1573
  ];
1567
1574
  for (const [code, pattern] of persistentPatterns) {
1568
1575
  if (pattern.test(normalized)) {
@@ -1575,6 +1582,7 @@ function classifyPersistentCaptureFailure(err, opts) {
1575
1582
  subscription_required: "capture requires an active subscription",
1576
1583
  entitlement_denied: "the account is not entitled to capture conversations",
1577
1584
  datapack_access_denied: "the account does not have access to the target datapack",
1585
+ read_only_access: "the account has viewer (read-only) access to the target datapack",
1578
1586
  };
1579
1587
  return { code, reason: reasons[code], scope: "account", detail };
1580
1588
  }
@@ -1771,6 +1779,111 @@ async function recoverMissingConversation({
1771
1779
  return { ok: true, state: written.state || next, convId: newConvId };
1772
1780
  }
1773
1781
 
1782
+ /**
1783
+ * Move a session's unsent turns to a new conversation on request, without
1784
+ * waiting for capture to see `not_found` twice. Used by
1785
+ * `--capture-cleanup fix` when the recorded conversation can't be served:
1786
+ * its owner reads as `None`, its lookup keeps failing, or a replay loop
1787
+ * bloated it. Holds the drain lease for the whole move, keeps the cursor,
1788
+ * and creates the conversation under the session's recorded owner and
1789
+ * datapack.
1790
+ *
1791
+ * The caller can pass `expected_conversation_id`. Under the lease, the move
1792
+ * is refused (`changed: true`) when the session's conversation no longer
1793
+ * matches it, or when a previous move is still waiting for its new
1794
+ * conversation to accept a turn (`recovered_from_conversation_id`): a hook
1795
+ * drain may have recovered or delivered the session while the caller waited.
1796
+ *
1797
+ * @param {{ session_id: string, expected_conversation_id?: string }} options
1798
+ * @returns {Promise<{ok: boolean, busy?: boolean, changed?: boolean, from?: string, to?: string, last_line_number?: number, error?: string}>}
1799
+ */
1800
+ async function moveSessionToNewConversation(options) {
1801
+ const sessionId = String((options && options.session_id) || "").trim();
1802
+ if (!sessionId) return { ok: false, error: "session_id required" };
1803
+ const lease = captureState.acquireLease(sessionId);
1804
+ if (!lease.ok) {
1805
+ return {
1806
+ ok: false,
1807
+ busy: lease.failure_class === "ownership",
1808
+ error: lease.error || "drain lease unavailable",
1809
+ };
1810
+ }
1811
+ try {
1812
+ const read = captureState.readState(sessionId);
1813
+ if (!read.ok) return { ok: false, error: read.error || "watermark unreadable" };
1814
+ let state = read.state;
1815
+ if (!state.agent_id) return { ok: false, error: "recorded owner missing" };
1816
+ const expected = options && options.expected_conversation_id;
1817
+ // An automatic cleanup run must not end a park the user set with `stop`
1818
+ // after that run scanned the session.
1819
+ if (
1820
+ options &&
1821
+ options.respect_stop &&
1822
+ state.parked &&
1823
+ state.parked.reason === "stop" &&
1824
+ state.failure_class &&
1825
+ state.next_retry_at === captureState.CLEANUP_PARK_UNTIL
1826
+ ) {
1827
+ return { ok: false, paused: true, error: "paused by the user with stop" };
1828
+ }
1829
+ if (state.recovered_from_conversation_id) {
1830
+ return {
1831
+ ok: false,
1832
+ changed: true,
1833
+ error: "a previous move is waiting for its new conversation to accept a turn",
1834
+ };
1835
+ }
1836
+ if (expected && state.conversation_id !== expected) {
1837
+ return {
1838
+ ok: false,
1839
+ changed: true,
1840
+ error: `the session moved to conversation ${state.conversation_id} while waiting`,
1841
+ };
1842
+ }
1843
+ // Checked again here, under the lease: the session may have become live
1844
+ // after --capture-cleanup planned the move. SessionStart gave a live
1845
+ // session's agent its conversation_id, and a move can't tell it the new one.
1846
+ if (options && options.defer_if_live && captureState.sessionLooksLive(state)) {
1847
+ return { ok: false, deferred: true, error: "the session is still open" };
1848
+ }
1849
+ // MEKO-592: a session that started before the user pinned a datapack has
1850
+ // none in its watermark. Re-read the pin the way drainSession does, so
1851
+ // the new conversation is created in the datapack its turns will go to.
1852
+ if (!state.datapack_id) {
1853
+ const pin = readDatapackPin(state.agent_id);
1854
+ if (pin && pin.datapack_id) {
1855
+ state = { ...state, datapack_id: pin.datapack_id, datapack_name: pin.datapack_name || null };
1856
+ }
1857
+ }
1858
+ await mcpInitialize();
1859
+ const moved = await recoverMissingConversation({
1860
+ // cleanup_moved_epoch lets --capture-cleanup count epochs since its own
1861
+ // last move, not the session's lifetime total. The move also ends any
1862
+ // park the cleanup tool set (the retry fields are reset here).
1863
+ state: {
1864
+ ...state,
1865
+ parked: null,
1866
+ cleanup_moved_epoch: Number(state.conversation_epoch || 0) + 1,
1867
+ },
1868
+ sessionId,
1869
+ agentId: state.agent_id,
1870
+ transcriptPath: state.transcript_path,
1871
+ datapackId: state.datapack_id || null,
1872
+ });
1873
+ if (!moved.ok) return { ok: false, error: moved.error };
1874
+ return {
1875
+ ok: true,
1876
+ from: state.conversation_id || null,
1877
+ to: moved.convId,
1878
+ last_line_number: moved.state.last_line_number,
1879
+ };
1880
+ } catch (err) {
1881
+ return { ok: false, error: err.message };
1882
+ } finally {
1883
+ captureState.releaseLease(sessionId, lease.lease.token);
1884
+ }
1885
+ }
1886
+
1774
1887
  /**
1775
1888
  * Abort all pending MCP requests. Used by the preload timeout path so the
1776
1889
  * SessionStart hook can return promptly instead of waiting for a slow server
@@ -1894,6 +2007,7 @@ function peekTranscriptMetadata(transcriptPath) {
1894
2007
  async function fetchRecentMemories(agentId, datapackId = null, { limit = 10, budget = 2000 } = {}) {
1895
2008
  // memory_get_all takes no `limit`, and FastMCP rejects an unknown argument
1896
2009
  // with an isError result, so `limit` caps the page client-side instead.
2010
+ // Don't send `page_size` either: a server older than MEKO-453 rejects it.
1897
2011
  const payload = {
1898
2012
  agent_id: agentId,
1899
2013
  conversation_id: "00000000-0000-0000-0000-000000000000",
@@ -2324,7 +2438,7 @@ function buildSessionStartFallbackContext(agentId, opts) {
2324
2438
  // act on.
2325
2439
  const reason = opts && opts.reason;
2326
2440
  const preamble = reason === "datapack_required"
2327
- ? "The SessionStart hook reached the Meko MCP server, but no datapack is pinned for this project, so the server declined to create a conversation. **Automatic conversation capture is idle**. Use `datapack_list` to find a datapack id, then invoke the `meko-select-datapack` skill (or your client's equivalent) to pin it. The next drain picks up the fresh pin and starts capturing this session's queued turns; until then, save facts explicitly."
2441
+ ? "The SessionStart hook reached the Meko MCP server, but no datapack is pinned for this project, so the server declined to create a conversation. **Automatic conversation capture is idle**. Use `datapack_search(query=...)` (or `datapack_list` on a server without it) to find a datapack id, then invoke the `meko-select-datapack` skill (or your client's equivalent) to pin it. The next drain picks up the fresh pin and starts capturing this session's queued turns; until then, save facts explicitly."
2328
2442
  : "The SessionStart hook could not reach the Meko MCP server to create a conversation. **Automatic conversation capture is NOT running this session**, so the server-side extraction that normally saves memories for you will not fire. Fall back to saving facts explicitly.";
2329
2443
  return `## Meko Memory Instructions
2330
2444
 
@@ -2651,7 +2765,7 @@ async function handleSessionStart(hookInput) {
2651
2765
  const isDatapackRequired = err && err.code === "datapack_id_required";
2652
2766
  if (isDatapackRequired) {
2653
2767
  process.stderr.write(
2654
- `[meko-capture] SessionStart: no datapack pinned; auto-capture is idle. Pin one with datapack_list + meko-select-datapack; the next drain picks up the fresh pin.\n`,
2768
+ `[meko-capture] SessionStart: no datapack pinned; auto-capture is idle. Pin one with datapack_search (or datapack_list) + meko-select-datapack; the next drain picks up the fresh pin.\n`,
2655
2769
  );
2656
2770
  } else if (err && err.isMcpToolError) {
2657
2771
  // Server accepted the call and returned a structured error (e.g.
@@ -3683,10 +3797,11 @@ async function main() {
3683
3797
  "checkpoint",
3684
3798
  "drain",
3685
3799
  "recover",
3800
+ "move",
3686
3801
  ];
3687
3802
  if (!hookType || !known.includes(hookType)) {
3688
3803
  process.stderr.write(
3689
- "Usage: capture.js <session-start|before-submit-prompt|pre-compact|session-end|checkpoint|drain|recover>\n",
3804
+ "Usage: capture.js <session-start|before-submit-prompt|pre-compact|session-end|checkpoint|drain|recover|move>\n",
3690
3805
  );
3691
3806
  process.exit(1);
3692
3807
  }
@@ -3699,7 +3814,11 @@ async function main() {
3699
3814
  process.stderr.write(
3700
3815
  `[meko-capture] Failed to parse hook input from stdin: ${err.message}\n`,
3701
3816
  );
3702
- if (hookType === "drain" || hookType === "recover") {
3817
+ if (hookType === "move") {
3818
+ process.stdout.write(
3819
+ `${JSON.stringify({ ok: false, error: `invalid stdin JSON: ${err.message}` })}\n`,
3820
+ );
3821
+ } else if (hookType === "drain" || hookType === "recover") {
3703
3822
  process.stdout.write(
3704
3823
  JSON.stringify(
3705
3824
  makeDrainResult({
@@ -3720,6 +3839,12 @@ async function main() {
3720
3839
  return;
3721
3840
  }
3722
3841
 
3842
+ if (hookType === "move") {
3843
+ const result = await moveSessionToNewConversation(hookInput);
3844
+ process.stdout.write(JSON.stringify(result) + "\n");
3845
+ return;
3846
+ }
3847
+
3723
3848
  if (hookType === "before-submit-prompt") {
3724
3849
  handleBeforeSubmitPrompt(hookInput);
3725
3850
  return;
@@ -3796,6 +3921,7 @@ module.exports = {
3796
3921
  addMessage,
3797
3922
  isMissingConversationError,
3798
3923
  recoverMissingConversation,
3924
+ moveSessionToNewConversation,
3799
3925
  };
3800
3926
 
3801
3927
  if (require.main === module) {