@yugabytedb/meko-mcp 2.2.0 → 2.5.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.
Files changed (67) hide show
  1. package/README.md +168 -61
  2. package/assets/hooks-handlers/lib/capture-cleanup-cli.js +2134 -0
  3. package/assets/hooks-handlers/lib/capture-notices.js +224 -14
  4. package/assets/hooks-handlers/lib/capture-state.js +206 -1
  5. package/assets/hooks-handlers/lib/capture-status-cli.js +6 -2
  6. package/assets/hooks-handlers/lib/capture.js +1364 -139
  7. package/assets/hooks-handlers/lib/checkpoint-timer.js +445 -18
  8. package/assets/hooks-handlers/lib/journal-bridge.js +218 -0
  9. package/assets/hooks-handlers/lib/kiro-handler.js +26 -4
  10. package/assets/hooks-handlers/lib/opencode-handler.js +643 -0
  11. package/assets/hooks-handlers/lib/opencode-normalizer.js +145 -0
  12. package/assets/hooks-handlers/lib/terminal-handoff.js +10 -1
  13. package/assets/opencode-plugin/meko.js +213 -0
  14. package/assets/scripts/meko-statusline.sh +102 -19
  15. package/assets/skills/meko-mcp-tools/SKILL.md +59 -16
  16. package/assets/skills/meko-mcp-tools/references/_sections.md +2 -2
  17. package/assets/skills/meko-mcp-tools/references/tools-agent-id-conventions.md +15 -7
  18. package/assets/skills/meko-mcp-tools/references/tools-cookbook.md +275 -19
  19. package/assets/skills/meko-mcp-tools/references/tools-datapack-workflow.md +7 -4
  20. package/assets/skills/meko-mcp-tools/references/tools-known-limitations.md +39 -11
  21. package/assets/skills/meko-mcp-tools/references/tools-memory-vs-conversation.md +10 -6
  22. package/assets/skills/meko-mcp-tools/references/tools-overview.md +44 -29
  23. package/assets/skills/meko-mcp-tools/references/tools-rag-workflow.md +79 -14
  24. package/assets/skills/meko-mcp-tools/references/tools-troubleshooting.md +47 -1
  25. package/assets/skills/meko-mcp-tools/references/tools-user-id-conventions.md +6 -39
  26. package/assets/skills/meko-mcp-tools-desktop/SKILL.md +57 -21
  27. package/assets/skills/meko-mcp-tools-desktop/desktop/README.md +4 -4
  28. package/assets/skills/meko-mcp-tools-desktop/references/_sections.md +1 -1
  29. package/assets/skills/meko-mcp-tools-desktop/references/tools-agent-id-conventions.md +8 -6
  30. package/assets/skills/meko-mcp-tools-desktop/references/tools-cookbook.md +284 -38
  31. package/assets/skills/meko-mcp-tools-desktop/references/tools-known-limitations.md +32 -10
  32. package/assets/skills/meko-mcp-tools-desktop/references/tools-memory-vs-conversation.md +16 -12
  33. package/assets/skills/meko-mcp-tools-desktop/references/tools-overview.md +46 -31
  34. package/assets/skills/meko-mcp-tools-desktop/references/tools-rag-workflow.md +79 -14
  35. package/assets/skills/meko-mcp-tools-desktop/references/tools-troubleshooting.md +69 -1
  36. package/assets/skills/meko-select-datapack-desktop/SKILL.md +93 -14
  37. package/assets/skills/meko-select-datapack-desktop/references/cookbook.md +44 -12
  38. package/assets/skills/meko-select-datapack-desktop/references/role-display-future.md +14 -10
  39. package/assets/skills/meko-select-datapack-desktop/references/troubleshooting.md +17 -5
  40. package/bin/create-meko-setup.mjs +186 -30
  41. package/lib/autofix.mjs +100 -0
  42. package/lib/browser-login.mjs +495 -0
  43. package/lib/capture-cleanup.mjs +63 -0
  44. package/lib/clients/claude-code.mjs +1 -0
  45. package/lib/clients/codex.mjs +80 -35
  46. package/lib/clients/common.mjs +7 -0
  47. package/lib/clients/index.mjs +2 -0
  48. package/lib/clients/kiro.mjs +5 -5
  49. package/lib/clients/opencode.mjs +537 -0
  50. package/lib/config.mjs +68 -1
  51. package/lib/installer.mjs +18 -2
  52. package/lib/migrate/adapters/claude-code.mjs +11 -2
  53. package/lib/migrate/adapters/codex.mjs +9 -2
  54. package/lib/migrate/adapters/memory-md.mjs +27 -8
  55. package/lib/migrate/adapters/opencode.mjs +148 -0
  56. package/lib/migrate/claude-parser.mjs +153 -9
  57. package/lib/migrate/datapack.mjs +254 -16
  58. package/lib/migrate/id.mjs +195 -13
  59. package/lib/migrate/mcp-client.mjs +59 -1
  60. package/lib/migrate/opencode-normalizer.mjs +132 -0
  61. package/lib/migrate/orchestrator.mjs +294 -78
  62. package/lib/migrate/progress.mjs +239 -5
  63. package/lib/migrate/registry.mjs +72 -0
  64. package/lib/migrate.mjs +97 -9
  65. package/lib/validate.mjs +111 -22
  66. package/package.json +5 -5
  67. package/scripts/bundle-assets.mjs +4 -0
@@ -1,6 +1,6 @@
1
1
  # Cookbook — meko-select-datapack-desktop
2
2
 
3
- Concrete transcripts of the four flows the skill supports on Claude Desktop.
3
+ Concrete transcripts of the flows the skill supports on Claude Desktop.
4
4
 
5
5
  ## Flow 1 — First pin (no existing pin memory)
6
6
 
@@ -8,11 +8,11 @@ Concrete transcripts of the four flows the skill supports on Claude Desktop.
8
8
 
9
9
  **Skill:**
10
10
  ```
11
- > memory_search(query="meko_active_datapack", agent_id="claude_desktop", limit=1)
11
+ > memory_search(query="meko_active_datapack", agent_id="claude_desktop", conversation_id="<conv-id>", limit=1)
12
12
  [empty]
13
13
 
14
- > datapack_list(conversation_id="<conv-id>")
15
- [14 entries returned, filtering by "prod-research"]
14
+ > datapack_search(conversation_id="<conv-id>", query="prod-research", limit=10, include_total=true)
15
+ [1 row, has_more=false, next_cursor="", total=1]
16
16
 
17
17
  One match: `prod-research`. Pinning.
18
18
 
@@ -23,7 +23,7 @@ One match: `prod-research`. Pinning.
23
23
  metadata='{"type":"active-datapack-pin"}')
24
24
  [returns id=mem_abc123]
25
25
 
26
- > memory_search(query="meko_active_datapack", agent_id="claude_desktop", limit=1)
26
+ > memory_search(query="meko_active_datapack", agent_id="claude_desktop", conversation_id="<conv-id>", limit=1)
27
27
  [verifies the row exists]
28
28
 
29
29
  Pinned datapack `prod-research` (`8b1c…f4a2`) for Claude Desktop.
@@ -41,11 +41,11 @@ datapack_id flows through automatically.
41
41
 
42
42
  **Skill:**
43
43
  ```
44
- > memory_search(query="meko_active_datapack", agent_id="claude_desktop", limit=1)
44
+ > memory_search(query="meko_active_datapack", agent_id="claude_desktop", conversation_id="<conv-id>", limit=1)
45
45
  [returns id=mem_abc123, text="meko_active_datapack=8b1c…f4a2 name=prod-research …"]
46
46
 
47
- > datapack_list(conversation_id="<conv-id>")
48
- [14 entries returned, filtering by "q2-roadmap-deck"]
47
+ > datapack_search(conversation_id="<conv-id>", query="q2-roadmap-deck", limit=10, include_total=true)
48
+ [1 row, has_more=false, next_cursor="", total=1]
49
49
 
50
50
  One match: `q2-roadmap-deck`. Switching pin.
51
51
 
@@ -66,11 +66,11 @@ The `memory_update` is critical here — using `memory_add` again would create a
66
66
 
67
67
  **Skill:**
68
68
  ```
69
- > memory_search(query="meko_active_datapack", agent_id="claude_desktop", limit=1)
69
+ > memory_search(query="meko_active_datapack", agent_id="claude_desktop", conversation_id="<conv-id>", limit=1)
70
70
  [empty]
71
71
 
72
- > datapack_list(conversation_id="<conv-id>")
73
- [1 entry returned]
72
+ > datapack_search(conversation_id="<conv-id>", limit=10, include_total=true)
73
+ [1 row, has_more=false, next_cursor="", total=1]
74
74
 
75
75
  Only one datapack: `meko-local-setup`. Auto-selecting.
76
76
 
@@ -90,7 +90,7 @@ Pinned datapack `meko-local-setup` (`a17d…cc91`). Run this skill again with
90
90
 
91
91
  **Skill:**
92
92
  ```
93
- > memory_search(query="meko_active_datapack", agent_id="claude_desktop", limit=1)
93
+ > memory_search(query="meko_active_datapack", agent_id="claude_desktop", conversation_id="<conv-id>", limit=1)
94
94
  [returns id=mem_abc123]
95
95
 
96
96
  > memory_delete_by_id(
@@ -119,3 +119,35 @@ This isn't a user-triggered flow — it's the per-turn lookup the skill teaches
119
119
  ```
120
120
 
121
121
  Cache the UUID for the rest of the turn — don't call `memory_search` once per Meko tool call.
122
+
123
+ ## Flow 6: Browse and page
124
+
125
+ **User:** `list my datapacks`
126
+
127
+ **Skill:**
128
+ ```
129
+ > datapack_search(conversation_id="<conv-id>", limit=10, include_total=true)
130
+ [10 rows, has_more=true, next_cursor="eyJ2Ijox…", total=14]
131
+
132
+ Your datapacks (showing 1-10 of 14):
133
+ …
134
+ ```
135
+
136
+ **User:** `next`
137
+
138
+ ```
139
+ > datapack_search(conversation_id="<conv-id>", limit=10, cursor="eyJ2Ijox…")
140
+ [4 rows, has_more=false, next_cursor=""]
141
+
142
+ Your datapacks (showing 11-14 of 14):
143
+ …
144
+
145
+ Reply with: a number (11-14) to pin · a substring to search · `prev` to page
146
+ · `clear` to unset · `cancel` to leave the current pin alone
147
+ ```
148
+
149
+ The skill remembers each cursor it used. On `prev` it drops the last one and repeats the call for page 1, which had no cursor. The total (14) is reused from page 1. A substring reply calls `datapack_search(query="<text>", limit=10, include_total=true)` and starts a new listing.
150
+
151
+ ## Flow 7: Older server without `datapack_search`
152
+
153
+ The tool list has `datapack_list` but no `datapack_search`. The skill calls `datapack_list(conversation_id="<conv-id>")` once, then sorts, pages and filters that array in the conversation. The table and replies look the same.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Today: render `grant` verbatim
4
4
 
5
- The deployed Meko `GET /datapacks` response includes a `grant` field on every datapack. Possible values (per maintainer guidance, 2026-05-26):
5
+ The deployed Meko `GET /datapacks` response includes a `grant` field on every datapack, and so does every row that `datapack_search` and `datapack_list` return. Possible values (per maintainer guidance, 2026-05-26):
6
6
 
7
7
  - `"owner"` — datapacks the caller created.
8
8
  - `"maintainer"` — write access to a shared datapack including memories AND collective learnings/knowledge.
@@ -20,21 +20,25 @@ Use the field directly in the **Role** column:
20
20
 
21
21
  Do not invent values, do not normalize the casing, do not display `"Owner"` if the field says `"owner"`. The deployed schema is the source of truth.
22
22
 
23
- Note that the upstream Go source (the `Datapack` struct in `api_server/internal/models/models.go` of the `meko` API-server repo) does NOT declare `grant` — the deployed server enriches the response beyond what the public Go code lists. Don't rely on the public struct for the wire schema; trust the actual response.
23
+ Note that the server's `Datapack` model doesn't declare `grant`: the deployed server enriches the response beyond what the public Go code lists. Don't rely on the public struct for the wire schema; trust the actual response.
24
24
 
25
- ## Other fields the live response includes
25
+ ## Counts are not on the list response
26
26
 
27
- Beyond the columns the skill renders by default, `datapack_list` returns counts that may be useful in some flows:
27
+ Neither `datapack_search` nor `datapack_list` computes counts; their rows carry no count fields. If the user asks for counts, call `datapack_describe(datapack_id=<id>, conversation_id="<conversation id>")` and read them from that response:
28
28
 
29
- - `memory_count` — total memories the caller has under this datapack (per `(user_id, agent_id)` scoping).
30
- - `knowledge_count` — knowledge-base entries on the datapack.
31
- - `learnings_count` — promoted-to-shared memories on the datapack.
29
+ - `memory_count`: all memories stored in the datapack.
30
+ - `collective_memory_count`: memories promoted to shared knowledge.
31
+ - `learnings_count`: memories awaiting a promotion decision.
32
+ - `knowledge_base_file_count`: uploaded KB documents (`null` if the status endpoint is unreachable).
33
+ - `knowledge_chunk_count`: KB chunks across all documents. `knowledge_count` is a deprecated alias.
32
34
 
33
- These aren't in the spec column set. Don't add them silently. If the user asks for counts, render them on request — and read the numbers from the live response, never invent them.
35
+ Don't add counts to the table silently, and never invent the numbers.
34
36
 
35
- ## Sharing UI vs API state
37
+ ## Sharing a datapack
36
38
 
37
- The Cloud console "Share <datapack>" page (`meko_ui/src/features/datapacks/pages/DatapackSharePage.tsx`) shows interactive controls with "Coming Soon" overlays on a few specific actions (e.g. invite-by-email link sharing, transfer-ownership). Those overlays mean *those specific UI affordances* aren't shipped — NOT that the underlying `grant`/sharing model is absent. Contributor/maintainer/viewer grants are functional in the data layer today; only the user-facing self-serve invite flow is gated.
39
+ An owner or maintainer shares a datapack from the Cloud console's **Share** page. Each datapack has one share link. The owner or maintainer can turn it on or off, reset it, and choose whether people who open it join as viewer or contributor. Anyone signed in who opens an active link joins straight away. There are no MCP tools to manage sharing.
40
+
41
+ A viewer is read-only, and the server enforces it: over MCP, conversation and memory writes return `read_only_access`. If the user picks a datapack where they're a viewer, tell them that Meko won't record conversations or save memories there.
38
42
 
39
43
  ## Why the skill ships without Mine / Shared / All filter tabs
40
44
 
@@ -18,12 +18,12 @@ You (or an earlier run) created duplicates by using `memory_add` instead of `mem
18
18
 
19
19
  The cookbook's Flow 2 demonstrates the correct switch path.
20
20
 
21
- ## Symptom: A pinned datapack returns "not found" when used
21
+ ## Symptom: A pinned datapack returns `datapack_access_denied` when used
22
22
 
23
- The pin became stale — the datapack was deleted in another client. Tell the user:
23
+ The pin became stale: the datapack was deleted, or its share to you was revoked, in another client. The server checks ownership and shares on every call that passes a `datapack_id`, so the call fails with `datapack_access_denied` (older servers returned a not-found error). The check also fails closed on a transient server database error, so retry the call once before treating the pin as stale. Tell the user:
24
24
 
25
25
  ```
26
- The pinned datapack `<name>` (`<id>`) no longer exists on the server.
26
+ The pinned datapack `<name>` (`<id>`) no longer exists or is no longer shared with you.
27
27
  Run `meko-select-datapack-desktop` to pick a fresh one, or `clear` to unpin.
28
28
  ```
29
29
 
@@ -33,20 +33,32 @@ Then either re-run the skill or delete the pin memory. Do not silently fall back
33
33
 
34
34
  Never auto-pick when there's ambiguity. Re-render just the matching rows numbered from 1, and ask the user to pick a number from the filtered subset. Same as the Claude Code variant.
35
35
 
36
- ## Symptom: `datapack_list` returns an empty array
36
+ ## Symptom: The account has no datapacks (`total` is 0, or `datapack_list` returns an empty array)
37
37
 
38
38
  The user has zero datapacks. Print:
39
39
 
40
40
  ```
41
41
  You don't have any datapacks yet. Create one with:
42
42
 
43
- datapack_create(name="<your-datapack-name>")
43
+ datapack_create(name="<your-datapack-name>", conversation_id="<conversation id>")
44
44
 
45
45
  Or visit the Meko Cloud console (Datapacks → New datapack).
46
46
  ```
47
47
 
48
48
  Stop. Don't `memory_add` an empty pin.
49
49
 
50
+ ## Symptom: `next` or `prev` returns an error about the cursor
51
+
52
+ The cursor is tied to the search it came from. It stops working if you change `query`, `sort` or `grant` while paging, or if you edit it. Always send the cursor back unchanged, with the same filters as the call that returned it. Tell the user the page could not be loaded and start again from page 1 of the same search (no cursor).
53
+
54
+ ## Symptom: `datapack_list` returns `too_many_datapacks`
55
+
56
+ The account can see more datapacks than `datapack_list` returns in one array (1000 by default). The server that sends this error also has `datapack_search`. Use the main flow with `datapack_search`; if it is missing from your tool list, the list is stale, so reload the Meko tools.
57
+
58
+ ## Symptom: The tool list has no `datapack_search`
59
+
60
+ The Meko server is older than that tool. Use the skill's fallback flow with `datapack_list`: one call, then sort, page and filter the array in the conversation. On an older server, this call can fail for an account with a very large number of datapacks; that account needs a server that has `datapack_search`.
61
+
50
62
  ## Symptom: User typed `cancel` after the table rendered
51
63
 
52
64
  Leave the existing pin memory alone. Print:
@@ -7,7 +7,8 @@ import { dirname, join } from 'node:path';
7
7
 
8
8
  import { install, uninstall } from '../lib/installer.mjs';
9
9
  import { askChoice, askMultiChoice, askText, askSecret, askYesNo, close as closePrompts } from '../lib/prompt.mjs';
10
- import { banner, error, dim, info, warn } from '../lib/logger.mjs';
10
+ import { banner, error, dim, info, success, warn } from '../lib/logger.mjs';
11
+ import { BrowserLoginError, consoleUrlFor, loginAndCreateApiKey } from '../lib/browser-login.mjs';
11
12
  import {
12
13
  isPackagedInstall,
13
14
  MEKO_PRODUCTION_CONSOLE_URL,
@@ -19,6 +20,8 @@ import { parseClientList } from '../lib/clients/aliases.mjs';
19
20
  import { startMigration, runChildMigration, readStatus, stopMigration } from '../lib/migrate.mjs';
20
21
  import { checkLatest } from '../lib/version-check.mjs';
21
22
  import { runCaptureStatus } from '../lib/capture-status.mjs';
23
+ import { captureCleanupArgs, runCaptureCleanup } from '../lib/capture-cleanup.mjs';
24
+ import { applyAutofixSetting, describeAutofix } from '../lib/autofix.mjs';
22
25
 
23
26
  // ---------------------------------------------------------------------------
24
27
  // Flag definitions
@@ -27,6 +30,7 @@ import { runCaptureStatus } from '../lib/capture-status.mjs';
27
30
  const options = {
28
31
  url: { type: 'string' },
29
32
  'api-key': { type: 'string' },
33
+ 'no-browser': { type: 'boolean', default: false },
30
34
  local: { type: 'boolean', default: false },
31
35
  scope: { type: 'string', default: 'user' },
32
36
  name: { type: 'string', default: 'meko' },
@@ -36,6 +40,8 @@ const options = {
36
40
  'skip-validation': { type: 'boolean', default: false },
37
41
  statusline: { type: 'boolean', default: false },
38
42
  'no-statusline': { type: 'boolean', default: false },
43
+ autofix: { type: 'boolean', default: false },
44
+ 'no-autofix': { type: 'boolean', default: false },
39
45
  'no-canary': { type: 'boolean', default: false },
40
46
  'dry-run': { type: 'boolean', default: false },
41
47
  'check-updates': { type: 'boolean', default: false },
@@ -48,6 +54,11 @@ const options = {
48
54
  // --- Capture health (Capture V2 Workstream D) ---
49
55
  'capture-status': { type: 'boolean', default: false },
50
56
  json: { type: 'boolean', default: false },
57
+ 'capture-cleanup': { type: 'string' },
58
+ session: { type: 'string', multiple: true },
59
+ all: { type: 'boolean', default: false },
60
+ 'max-exchanges': { type: 'string' },
61
+ 'pace-ms': { type: 'string' },
51
62
 
52
63
  // --- Migrate subcommand ---
53
64
  migrate: { type: 'boolean', default: false },
@@ -75,11 +86,11 @@ function readVersion() {
75
86
  return pkg.version;
76
87
  }
77
88
 
78
- const ALL_CLIENTS = ['claude-code', 'claude-desktop', 'cursor', 'codex', 'kiro'];
89
+ const ALL_CLIENTS = ['claude-code', 'claude-desktop', 'cursor', 'codex', 'kiro', 'opencode'];
79
90
 
80
91
  function printHelp() {
81
92
  const text = `
82
- Meko Setup for Claude Code, Claude Desktop, Cursor, Codex, and Kiro
93
+ Meko Setup for Claude Code, Claude Desktop, Cursor, Codex, Kiro, and OpenCode
83
94
 
84
95
  Install and configure Meko MCP Server in one command.
85
96
 
@@ -90,7 +101,10 @@ Examples:
90
101
  # Interactive setup (recommended for first-time users):
91
102
  create-meko-setup
92
103
 
93
- # Cloud Meko for Claude Code (non-interactive):
104
+ # Cloud Meko for Claude Code (signs in through your browser):
105
+ create-meko-setup --url https://mcp.mekodata.ai/mcp --client claude-code
106
+
107
+ # Cloud Meko with an existing API key (unattended, e.g. CI):
94
108
  create-meko-setup --url https://mcp.mekodata.ai/mcp --api-key "$MEKO_API_KEY" --yes
95
109
 
96
110
  # Local Meko with Cursor config:
@@ -108,11 +122,13 @@ Examples:
108
122
  # Install for Kiro (IDE and kiro-cli share the same config):
109
123
  create-meko-setup --client kiro --yes
110
124
 
125
+ create-meko-setup --client opencode --scope user --yes
126
+
111
127
  # Local Meko (docker-compose stack, Claude Code):
112
128
  create-meko-setup --local --yes
113
129
 
114
130
  Options:
115
- --client <list> Target clients: one or more of claude-code, claude-desktop, cursor, codex, kiro.
131
+ --client <list> Target clients: one or more of claude-code, claude-desktop, cursor, codex, kiro, opencode.
116
132
  "claude" is accepted as an alias for claude-code;
117
133
  "kiro-cli" and "kiro-ide" for kiro.
118
134
  Comma-separated, or "all" to target every supported client (default: claude-code)
@@ -123,7 +139,10 @@ Options:
123
139
  untrusted IDE workspaces and non-managed CLI agents fall
124
140
  back to agent-driven persistence.
125
141
  --url <url> MCP server URL (from cloud.mekodata.ai > Datapacks > Connect)
126
- --api-key <key> API key for Cloud Meko authentication
142
+ --api-key <key> Use an existing Cloud Meko API key instead of signing in
143
+ through the browser. Also read from $MEKO_API_KEY.
144
+ Required when no terminal is attached (CI, scripts).
145
+ --no-browser Print the sign-in URL instead of opening a browser
127
146
  --local Use local Meko at http://localhost:8000/mcp
128
147
  --scope <scope> Config scope: user or project (default: user)
129
148
  codex always installs at user scope (no project-level
@@ -136,8 +155,13 @@ Options:
136
155
  Default: ON under --yes, prompted otherwise.
137
156
  Claude Code only.
138
157
  --no-statusline Skip statusline wiring even under --yes.
158
+ --no-autofix Turn off automatic capture repair: the capture timer
159
+ otherwise runs --capture-cleanup fix every 30 minutes
160
+ (MEKO_AUTOFIX_INTERVAL) when a session is stuck.
161
+ --autofix Turn automatic capture repair back on.
139
162
  --skip-validation Skip post-install validation
140
- --no-canary Skip the canary datapack_list call. The
163
+ --no-canary Skip the canary datapack_search (or, on older
164
+ servers, datapack_list) call. The
141
165
  connectivity + tools/list checks still run.
142
166
  --dry-run Show what would be done without making changes
143
167
  --check-updates Compare installed version against npm registry and exit
@@ -153,7 +177,31 @@ Capture health (durable outbox / recovery):
153
177
  Exit 0 = healthy|catching_up, 2 = degraded_retrying,
154
178
  3 = blocked_action_required.
155
179
  --json With --capture-status, print machine-readable JSON
156
- matching the capture-health cache shape.
180
+ matching the capture-health cache shape. Also
181
+ accepted by --capture-cleanup.
182
+ --capture-cleanup <action>
183
+ Clean up stuck capture on this machine:
184
+ status list sessions with unsent work and the
185
+ background capture processes (read-only)
186
+ fix diagnose each stuck session and fix it:
187
+ repair, drain, or move unsent turns to a
188
+ new conversation; reports what only you
189
+ can fix (start here)
190
+ stop stop SessionEnd workers and in-flight
191
+ drains, and park all unsent work so no
192
+ hook retries it (nothing is deleted)
193
+ resume unpark and hand back to the hooks
194
+ drain send now, one session at a time; stops
195
+ and re-parks at the first failure
196
+ discard skip a session's unsent exchanges for
197
+ good (needs --yes)
198
+ repair clear stale failure fields and leftover
199
+ PID, lease, and job files
200
+ Options: --session <id> or a bare session ID
201
+ (repeatable, prefix ok),
202
+ --all, --dry-run, --json, --yes,
203
+ --max-exchanges N (drain, default 200),
204
+ --pace-ms N (drain, default 1000).
157
205
 
158
206
  Migrate existing conversations and memories into Meko:
159
207
  --migrate Start a detached migration of existing Claude Code
@@ -267,12 +315,53 @@ function parseRate(raw) {
267
315
  return n;
268
316
  }
269
317
 
318
+ // Placeholder shown in --dry-run output in place of a key that browser
319
+ // sign-in would create. Never sent anywhere: dry runs make no changes.
320
+ const DRY_RUN_SIGN_IN_KEY = 'mko_tkn_<created-by-browser-sign-in>';
321
+
322
+ /**
323
+ * Sign in through the browser and create an API key for this install.
324
+ * Returns the key, or null after printing why sign-in failed.
325
+ */
326
+ async function signInWithBrowser({ url, noBrowser, dryRun }) {
327
+ if (dryRun) {
328
+ dim('Would sign in with your browser and create an API key (skipped: --dry-run).');
329
+ return DRY_RUN_SIGN_IN_KEY;
330
+ }
331
+ info('Signing in to Meko with your browser to create an API key...');
332
+ try {
333
+ const key = await loginAndCreateApiKey({ url, noBrowser, notify: (msg) => dim(msg) });
334
+ const expires = key.expiresAt ? ` (expires ${String(key.expiresAt).slice(0, 10)})` : '';
335
+ success(`Signed in. Created API key "${key.name}"${expires}.`);
336
+ dim(`Manage or revoke it at ${consoleUrlFor(url)} > Settings > API Keys.`);
337
+ return key.apiKey;
338
+ } catch (err) {
339
+ error(err instanceof BrowserLoginError ? err.message : `Browser sign-in failed: ${err?.message ?? err}`);
340
+ return null;
341
+ }
342
+ }
343
+
270
344
  // ---------------------------------------------------------------------------
271
345
  // Main
272
346
  // ---------------------------------------------------------------------------
273
347
 
274
348
  async function main() {
275
- const { values: flags } = parseArgs({ options, strict: true });
349
+ const { values: flags, positionals } = parseArgs({
350
+ options,
351
+ strict: true,
352
+ allowPositionals: true,
353
+ });
354
+ // Only --capture-cleanup takes bare arguments: session IDs, so
355
+ // `--capture-cleanup drain c0acdeb2` works like `--session c0acdeb2`.
356
+ if (positionals.length > 0) {
357
+ if (flags['capture-cleanup'] === undefined) {
358
+ error(
359
+ `Unexpected argument '${positionals[0]}'. This command does not take positional arguments`,
360
+ );
361
+ process.exit(1);
362
+ }
363
+ flags.session = [...(flags.session || []), ...positionals];
364
+ }
276
365
  const projectRoot = process.cwd();
277
366
 
278
367
  // --help
@@ -297,8 +386,17 @@ async function main() {
297
386
  if (out.stderr) process.stderr.write(out.stderr);
298
387
  process.exit(out.code);
299
388
  }
389
+ // --capture-cleanup <action>: inspect, fix, stop, park, drain, or discard
390
+ // stuck capture sessions. Exit codes: 0 done, 1 error, 2 partly done.
391
+ if (flags['capture-cleanup'] !== undefined) {
392
+ const out = runCaptureCleanup(
393
+ captureCleanupArgs(flags['capture-cleanup'], flags),
394
+ );
395
+ if (out.error) error(out.error);
396
+ process.exit(out.code);
397
+ }
300
398
  if (flags.json) {
301
- error('--json requires --capture-status');
399
+ error('--json requires --capture-status or --capture-cleanup');
302
400
  process.exit(1);
303
401
  }
304
402
 
@@ -456,6 +554,10 @@ async function main() {
456
554
  // submitting an empty value at the full-flow prompt would re-ask the
457
555
  // same question.
458
556
  let promptedForApiKey = false;
557
+ // Set when the user picked "Sign in with browser" at the interactive auth
558
+ // prompt. Someone answered that prompt, so sign-in runs even when stdin
559
+ // is not a TTY (e.g. piped answers in the smoke harness).
560
+ let choseBrowserSignIn = false;
459
561
 
460
562
  // --local shortcut
461
563
  if (flags.local) {
@@ -478,6 +580,7 @@ async function main() {
478
580
  cursor: 'Cursor',
479
581
  codex: 'OpenAI Codex (CLI + App)',
480
582
  kiro: 'Kiro (IDE + kiro-cli)',
583
+ opencode: 'OpenCode CLI/TUI',
481
584
  }[id];
482
585
  return {
483
586
  label: id,
@@ -538,7 +641,7 @@ async function main() {
538
641
  // When only Cloud is available, skip the prompt entirely — asking to
539
642
  // pick between one option is pointless UX.
540
643
  const connectionOptions = [
541
- { label: 'Cloud Meko', description: 'Hosted at mekodata.ai (requires an API key)' },
644
+ { label: 'Cloud Meko', description: 'Hosted at mekodata.ai (sign in with your browser)' },
542
645
  ];
543
646
  if (!isPackagedInstall()) {
544
647
  connectionOptions.push({
@@ -567,11 +670,24 @@ async function main() {
567
670
  suggestedUrl?.url ?? undefined,
568
671
  );
569
672
 
570
- // Step 3 — API key. Normalize: terminal paste can drag in trailing
571
- // whitespace/CR or accidental surrounding quotes, all of which would
572
- // sail straight into `Authorization: Bearer ...` and 401.
573
- apiKey = normalizeApiKey(await askSecret('API key:'));
574
- promptedForApiKey = true;
673
+ // Step 3 — authentication. Browser sign-in is the default and runs
674
+ // in the Cloud-URL check below; pasting a key is the advanced path.
675
+ // An existing --api-key or $MEKO_API_KEY skips the question.
676
+ if (!apiKey && !normalizeApiKey(process.env.MEKO_API_KEY)) {
677
+ const authChoice = await askChoice('How do you want to authenticate?', [
678
+ { label: 'Sign in with browser', description: 'Recommended. Creates an API key for you' },
679
+ { label: 'Paste an API key', description: `From ${consoleUrlFor(url)} > Settings > API Keys` },
680
+ ]);
681
+ if (authChoice === 'Paste an API key') {
682
+ // Normalize: terminal paste can drag in trailing whitespace/CR or
683
+ // accidental surrounding quotes, all of which would sail straight
684
+ // into `Authorization: Bearer ...` and 401.
685
+ apiKey = normalizeApiKey(await askSecret('API key:'));
686
+ promptedForApiKey = true;
687
+ } else {
688
+ choseBrowserSignIn = true;
689
+ }
690
+ }
575
691
  } else {
576
692
  url = 'http://localhost:8000/mcp';
577
693
  apiKey = null;
@@ -610,25 +726,38 @@ async function main() {
610
726
 
611
727
  // Validation — Cloud URL requires an API key.
612
728
  // Resolution order: --api-key (already in `apiKey`) → $MEKO_API_KEY →
613
- // interactive prompt (only on a TTY, not under --yes, and only if the
614
- // full interactive flow above didn't already ask). The prompt fallback
615
- // exists so a user who pastes the cloud one-liner from the UI (which
616
- // only shows --url and --client) isn't dead-ended on a hard error when
617
- // they forgot to supply the key. Skipping it after a full-flow prompt
618
- // avoids re-asking the same question when the user submitted empty.
729
+ // browser sign-in, which creates a key through the server's
730
+ // POST /api-keys. Sign-in also covers the cloud one-liner from the UI,
731
+ // which only shows --url and --client. It is skipped when the full
732
+ // interactive flow already took a pasted key (an empty paste is an
733
+ // error, not a silent switch to the browser), and whenever stdin is not
734
+ // a terminal, with or without --yes: a script or CI step may have nobody
735
+ // at the browser, and a 5-minute callback wait would stall it. The one
736
+ // exception is an explicit "Sign in with browser" answer at the prompt.
619
737
  if (isCloudUrl(url) && !apiKey) {
620
738
  apiKey = normalizeApiKey(process.env.MEKO_API_KEY);
739
+ let signInFailed = false;
621
740
  if (apiKey) {
622
741
  dim('Using API key from MEKO_API_KEY environment variable.');
623
- } else if (!flags.yes && !promptedForApiKey && process.stdin.isTTY) {
624
- dim('No --api-key or MEKO_API_KEY env var found; prompting for API key.');
625
- apiKey = normalizeApiKey(await askSecret('API key:'));
742
+ } else if (!promptedForApiKey && (process.stdin.isTTY || choseBrowserSignIn)) {
743
+ apiKey = await signInWithBrowser({
744
+ url,
745
+ noBrowser: flags['no-browser'],
746
+ dryRun: flags['dry-run'],
747
+ });
748
+ signInFailed = !apiKey;
626
749
  }
627
750
  if (!apiKey) {
628
- error(
629
- 'API key is required for Cloud Meko. ' +
630
- 'Provide --api-key, set MEKO_API_KEY env var, or run interactively.',
631
- );
751
+ let message;
752
+ if (signInFailed) {
753
+ message = `Browser sign-in did not complete. To use an existing key instead, create one at ${consoleUrlFor(url)} > Settings > API Keys and rerun with --api-key, or set MEKO_API_KEY.`;
754
+ } else if (promptedForApiKey) {
755
+ message = 'No API key entered. Rerun and choose Sign in with browser, or pass --api-key.';
756
+ } else {
757
+ message = 'API key is required for Cloud Meko. ' +
758
+ 'Provide --api-key, set MEKO_API_KEY env var, or run in a terminal to sign in with your browser.';
759
+ }
760
+ error(message);
632
761
  process.exit(1);
633
762
  }
634
763
  }
@@ -663,6 +792,23 @@ async function main() {
663
792
  }
664
793
  }
665
794
 
795
+ // Automatic capture repair runs inside the capture timer the hooks start,
796
+ // so installing the hooks deploys it. Save --autofix / --no-autofix once,
797
+ // for every client (they share one capture directory).
798
+ if (errors.length < clients.length) {
799
+ if (flags.autofix && flags['no-autofix']) {
800
+ warn('Both --autofix and --no-autofix were given; keeping the current setting.');
801
+ }
802
+ const enable =
803
+ flags['no-autofix'] && !flags.autofix ? false : flags.autofix && !flags['no-autofix'] ? true : undefined;
804
+ try {
805
+ const res = applyAutofixSetting({ enable, dryRun: flags['dry-run'], projectRoot });
806
+ info(describeAutofix(res.enabled));
807
+ } catch (err) {
808
+ warn(`Could not save the automatic repair setting: ${err.message}`);
809
+ }
810
+ }
811
+
666
812
  if (errors.length === clients.length) {
667
813
  process.exit(1);
668
814
  } else if (errors.length > 0) {
@@ -760,15 +906,23 @@ async function maybeOfferMigration({ url, apiKey, scope, projectRoot, datapack }
760
906
  dryRun: true,
761
907
  datapack: datapack ?? null,
762
908
  });
909
+ if (dry && dry.ok === false) {
910
+ dim(`Skipped migration offer: ${dry.message || "dry run failed"}`);
911
+ return;
912
+ }
763
913
  const s = dry?.message?.match(/Claude Code sessions: *(\d+)/);
764
914
  const c = dry?.message?.match(/Cursor sessions: *(\d+)/);
915
+ const x = dry?.message?.match(/Codex sessions: *(\d+)/);
916
+ const o = dry?.message?.match(/OpenCode sessions: *(\d+)/);
765
917
  const m = dry?.message?.match(/Memory items \(MD\): *(\d+)/);
766
918
  const counts = {
767
919
  claude: s ? Number(s[1]) : 0,
768
920
  cursor: c ? Number(c[1]) : 0,
921
+ codex: x ? Number(x[1]) : 0,
922
+ opencode: o ? Number(o[1]) : 0,
769
923
  memory: m ? Number(m[1]) : 0,
770
924
  };
771
- const total = counts.claude + counts.cursor + counts.memory;
925
+ const total = counts.claude + counts.cursor + counts.codex + counts.opencode + counts.memory;
772
926
  if (total === 0) return;
773
927
 
774
928
  console.log('');
@@ -776,6 +930,8 @@ async function maybeOfferMigration({ url, apiKey, scope, projectRoot, datapack }
776
930
  `Found existing data to migrate: ` +
777
931
  `${counts.claude} Claude Code sessions, ` +
778
932
  `${counts.cursor} Cursor sessions, ` +
933
+ `${counts.codex} Codex sessions, ` +
934
+ `${counts.opencode} OpenCode sessions, ` +
779
935
  `${counts.memory} memory items.`,
780
936
  );
781
937
  const proceed = await askYesNo(