mergestorm 0.3.63 → 0.3.66

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 (79) hide show
  1. package/README.md +30 -10
  2. package/dist/api.d.ts +68 -11
  3. package/dist/api.js +206 -8
  4. package/dist/automation-catalog.d.ts +17 -3
  5. package/dist/automation-catalog.js +4 -1
  6. package/dist/cli.js +21 -19
  7. package/dist/client/index.d.ts +6 -4
  8. package/dist/client/index.js +6 -4
  9. package/dist/commands/account.d.ts +3 -0
  10. package/dist/commands/account.js +75 -0
  11. package/dist/commands/browse.d.ts +1 -9
  12. package/dist/commands/browse.js +10 -8
  13. package/dist/commands/dismiss.d.ts +0 -2
  14. package/dist/commands/dismiss.js +2 -2
  15. package/dist/commands/help.js +3 -2
  16. package/dist/commands/issue.d.ts +24 -6
  17. package/dist/commands/issue.js +153 -31
  18. package/dist/commands/login.d.ts +2 -1
  19. package/dist/commands/login.js +50 -14
  20. package/dist/commands/logout.js +6 -7
  21. package/dist/commands/memory.d.ts +29 -0
  22. package/dist/commands/memory.js +154 -0
  23. package/dist/commands/registry.d.ts +1 -0
  24. package/dist/commands/registry.js +52 -11
  25. package/dist/commands/review-client.d.ts +0 -3
  26. package/dist/commands/review-client.js +3 -3
  27. package/dist/commands/review.d.ts +1 -1
  28. package/dist/commands/review.js +1 -1
  29. package/dist/commands/settings.d.ts +3 -4
  30. package/dist/commands/settings.js +51 -5
  31. package/dist/commands/stack.d.ts +20 -5
  32. package/dist/commands/stack.js +106 -7
  33. package/dist/commands/whoami.js +14 -10
  34. package/dist/config.d.ts +56 -2
  35. package/dist/config.js +271 -13
  36. package/dist/errors.d.ts +1 -1
  37. package/dist/gh.d.ts +0 -4
  38. package/dist/gh.js +4 -4
  39. package/dist/git.d.ts +1 -1
  40. package/dist/git.js +2 -2
  41. package/dist/memory-api.d.ts +74 -0
  42. package/dist/memory-api.js +69 -0
  43. package/dist/node-version.d.ts +0 -2
  44. package/dist/node-version.js +1 -1
  45. package/dist/pr-body.d.ts +4 -0
  46. package/dist/pr-body.js +15 -0
  47. package/dist/repo-identity.d.ts +2 -1
  48. package/dist/repo-identity.js +8 -4
  49. package/dist/server-publish.d.ts +5 -26
  50. package/dist/server-publish.js +47 -40
  51. package/dist/stack-blockers.d.ts +15 -36
  52. package/dist/stack-blockers.js +69 -33
  53. package/dist/stack-dto.d.ts +4 -0
  54. package/dist/stack-dto.js +15 -2
  55. package/dist/stack-meta.d.ts +2 -2
  56. package/dist/stack-meta.js +1 -1
  57. package/dist/stack-watch-loop.d.ts +2 -13
  58. package/dist/stack-watch-loop.js +35 -14
  59. package/dist/stack-watch.d.ts +2 -1
  60. package/dist/stack-watch.js +6 -2
  61. package/dist/submit-pr-base.d.ts +0 -1
  62. package/dist/submit-pr-base.js +1 -1
  63. package/dist/ui/ansi.d.ts +0 -10
  64. package/dist/ui/ansi.js +2 -2
  65. package/dist/ui/banner.d.ts +3 -1
  66. package/dist/ui/banner.js +13 -3
  67. package/dist/ui/box.d.ts +0 -2
  68. package/dist/ui/box.js +1 -1
  69. package/dist/ui/prompt.d.ts +0 -11
  70. package/dist/ui/prompt.js +7 -7
  71. package/dist/ui/tabs.d.ts +2 -2
  72. package/dist/ui/tabs.js +1 -1
  73. package/dist/ui/width.d.ts +0 -9
  74. package/dist/ui/width.js +3 -3
  75. package/dist/web-api.d.ts +17 -0
  76. package/dist/web-api.js +30 -0
  77. package/package.json +3 -1
  78. package/skill/SKILL.md +4 -0
  79. package/skill/mergestorm-bounce-watch/SKILL.md +1 -1
@@ -2,8 +2,8 @@ import { spawn } from "node:child_process";
2
2
  import { platform } from "node:os";
3
3
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { apiBase, configPath, loadConfig, mergestormHome, saveConfig, } from "../config.js";
6
- import { devicePost, getMe } from "../api.js";
5
+ import { accountNameFromMe, apiBase, configFor, configPath, mergestormHome, provisionalAccountName, readStore, saveAccount, } from "../config.js";
6
+ import { accountLabel, devicePost, getMe, revokeOwnKey } from "../api.js";
7
7
  import { CommandError } from "../errors.js";
8
8
  import { ansi } from "../ui/ansi.js";
9
9
  import { present } from "../ui/present.js";
@@ -40,6 +40,33 @@ export async function validateApiKey(cfg) {
40
40
  throw err;
41
41
  }
42
42
  }
43
+ async function loginConfig() {
44
+ const store = await readStore();
45
+ return configFor(store, store.activeAccount);
46
+ }
47
+ export function loggedInLine(account) {
48
+ const label = accountLabel(account);
49
+ return label ? `Logged in as ${label}.` : "Logged in.";
50
+ }
51
+ async function storeLogin(cfg, me, opts) {
52
+ const apiKey = cfg.apiKey ?? "";
53
+ const name = accountNameFromMe(me) ?? provisionalAccountName(apiKey);
54
+ const before = await readStore();
55
+ const replaced = Object.hasOwn(before.accounts, name) ? configFor(before, name) : undefined;
56
+ try {
57
+ await saveAccount(name, { apiKey, apiBase: cfg.apiBase }, { activate: true });
58
+ }
59
+ catch (err) {
60
+ if (opts.minted && err instanceof CommandError && err.code === "account_limit") {
61
+ await revokeOwnKey(cfg, { timeoutMs: 8_000 });
62
+ }
63
+ throw err;
64
+ }
65
+ if (replaced?.apiKey && replaced.apiKey !== apiKey) {
66
+ await revokeOwnKey({ ...replaced, ignoreEnv: true }, { timeoutMs: 8_000 });
67
+ }
68
+ return name;
69
+ }
43
70
  async function loginWithKey() {
44
71
  // Always use muted secret input on TTY so the key never lands in
45
72
  // question()/history echo.
@@ -47,12 +74,12 @@ async function loginWithKey() {
47
74
  if (!key.startsWith("msk_live_")) {
48
75
  throw new CommandError("Key should start with msk_live_");
49
76
  }
50
- const cfg = await loadConfig();
51
- cfg.apiKey = key;
52
- const me = await validateApiKey(cfg);
53
- await saveConfig(cfg);
77
+ const cfg = await loginConfig();
78
+ const candidate = { apiKey: key, apiBase: apiBase(cfg), ignoreEnv: true };
79
+ const me = await validateApiKey(candidate);
80
+ const name = await storeLogin(candidate, me, { minted: false });
54
81
  const prefix = me.key?.prefix?.trim() || "msk_live_…";
55
- await present("Login", [` Verified ${prefix}… · saved to ${configPath()}`]);
82
+ await present("Login", [` Logged in as ${name}. Verified ${prefix}… · saved to ${configPath()}`]);
56
83
  }
57
84
  export function pendingLoginPath() {
58
85
  return path.join(mergestormHome(), "login-pending.json");
@@ -140,12 +167,21 @@ async function pollDeviceLogin(pending, retryCommand, recovery) {
140
167
  throw new CommandError(`Login failed: network error. Check your connection and run \`${retryCommand}\` again.`);
141
168
  }
142
169
  if (poll.status === 200 && poll.body?.api_key) {
143
- const next = await loadConfig();
144
- next.apiKey = poll.body.api_key;
145
- next.apiBase = pending.apiBase;
146
- await saveConfig(next);
170
+ const minted = { apiKey: poll.body.api_key, apiBase: pending.apiBase, ignoreEnv: true };
147
171
  await clearPendingLogin(pending.deviceCode);
148
- await present("Login", [` Logged in. Key saved to ${configPath()}`]);
172
+ let me = accountLabel(poll.body?.account)
173
+ ? { account: poll.body.account }
174
+ : null;
175
+ if (!me) {
176
+ try {
177
+ me = await getMe(minted, { timeoutMs: 8_000 });
178
+ }
179
+ catch {
180
+ me = null;
181
+ }
182
+ }
183
+ const name = await storeLogin(minted, me, { minted: true });
184
+ await present("Login", [` Logged in as ${name}. Key saved to ${configPath()}`]);
149
185
  return;
150
186
  }
151
187
  const err = poll.body?.error;
@@ -177,7 +213,7 @@ async function pollDeviceLogin(pending, retryCommand, recovery) {
177
213
  throw new CommandError(`Login timed out. ${recovery}`);
178
214
  }
179
215
  async function loginStart() {
180
- const cfg = await loadConfig();
216
+ const cfg = await loginConfig();
181
217
  const base = apiBase(cfg);
182
218
  const pending = pendingFromStart(base, await startDeviceLogin(base));
183
219
  await savePendingLogin(pending);
@@ -213,7 +249,7 @@ export async function cmdLogin(args, deps = { openBrowser }) {
213
249
  await loginFinish();
214
250
  return;
215
251
  }
216
- const cfg = await loadConfig();
252
+ const cfg = await loginConfig();
217
253
  const base = apiBase(cfg);
218
254
  const pending = pendingFromStart(base, await startDeviceLogin(base));
219
255
  console.log("To sign in, open this URL and confirm the code:\n");
@@ -1,12 +1,11 @@
1
- import { clearConfig } from "../config.js";
1
+ import { loadConfig } from "../config.js";
2
2
  import { CommandError } from "../errors.js";
3
3
  import { present } from "../ui/present.js";
4
+ import { logoutAccount } from "./account.js";
4
5
  export async function cmdLogout() {
5
- try {
6
- await clearConfig();
7
- await present("Logout", [" Logged out. Removed stored API key."]);
8
- }
9
- catch (err) {
10
- throw new CommandError(`Could not remove config: ${err instanceof Error ? err.message : String(err)}`);
6
+ const cfg = await loadConfig();
7
+ if (!cfg.account) {
8
+ throw new CommandError("Not logged in. Nothing to log out of.");
11
9
  }
10
+ await present("Logout", await logoutAccount(cfg.account));
12
11
  }
@@ -0,0 +1,29 @@
1
+ import { type Config } from "../config.js";
2
+ import { deleteMemory, getMemoryMode, listMemories, saveMemory, searchMemories, setMemoryMode, updateMemory, type MemoryItem, type MemoryKind, type MemoryScope } from "../memory-api.js";
3
+ type Flags = {
4
+ positional: string[];
5
+ json: boolean;
6
+ keepBoth?: boolean;
7
+ repo?: string;
8
+ kind?: MemoryKind;
9
+ scope?: MemoryScope;
10
+ limit?: number;
11
+ };
12
+ export declare function parseMemoryFlags(args: string[], allowed: ReadonlyArray<"repo" | "kind" | "scope" | "limit" | "keep-both">): Flags;
13
+ export declare function formatMemoryLine(memory: MemoryItem): string;
14
+ export type MemoryDeps = {
15
+ loadConfig?: () => Promise<Config>;
16
+ chatId?: string | null;
17
+ print?: (line: string) => void;
18
+ api?: Partial<{
19
+ listMemories: typeof listMemories;
20
+ searchMemories: typeof searchMemories;
21
+ saveMemory: typeof saveMemory;
22
+ updateMemory: typeof updateMemory;
23
+ deleteMemory: typeof deleteMemory;
24
+ getMemoryMode: typeof getMemoryMode;
25
+ setMemoryMode: typeof setMemoryMode;
26
+ }>;
27
+ };
28
+ export declare function cmdMemory(args: string[], deps?: MemoryDeps): Promise<void>;
29
+ export {};
@@ -0,0 +1,154 @@
1
+ import { loadConfig } from "../config.js";
2
+ import { CommandError, REVIEW_EXIT } from "../errors.js";
3
+ import { MEMORY_KINDS, MEMORY_SEARCH_LIMIT_MAX, deleteMemory, getMemoryMode, isMemoryKind, isMemoryMode, isMemoryScope, listMemories, memoryChatId, saveMemory, searchMemories, setMemoryMode, updateMemory, } from "../memory-api.js";
4
+ const MEMORY_USAGE = [
5
+ "usage: mergestorm memory list [--scope user|repo] [--repo <owner/repo>] [--kind <kind>] [--json]",
6
+ " mergestorm memory search <query> [--repo <owner/repo>] [--limit <1-20>] [--json]",
7
+ " mergestorm memory add <text> --kind <kind> [--repo <owner/repo>] [--keep-both] [--json]",
8
+ " mergestorm memory update <id> <text> [--kind <kind>] [--json]",
9
+ " mergestorm memory delete <id> [--json]",
10
+ " mergestorm memory mode [off|on_request|all] [--json]",
11
+ ` kind is one of ${MEMORY_KINDS.join(", ")}. With --repo a memory is about that repository, otherwise about you.`,
12
+ " add refuses text close to a memory you have; update that memory, or add again with --keep-both when they are different things.",
13
+ " search covers your memories, and with --repo that repository's memories and standing rules too.",
14
+ " Inside a Breeze chat (MERGESTORM_CHAT_ID), add and search follow that chat's memory setting.",
15
+ ].join("\n");
16
+ const REPO = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/;
17
+ function usage(message) {
18
+ return new CommandError(message ? `${message}\n${MEMORY_USAGE}` : MEMORY_USAGE, REVIEW_EXIT.usage, "usage");
19
+ }
20
+ export function parseMemoryFlags(args, allowed) {
21
+ const flags = { positional: [], json: false };
22
+ const value = (flag, raw) => {
23
+ if (raw === undefined || raw.startsWith("--"))
24
+ throw usage(`${flag} needs a value`);
25
+ return raw.trim();
26
+ };
27
+ for (let i = 0; i < args.length; i++) {
28
+ const arg = args[i];
29
+ const name = arg.startsWith("--") ? arg.slice(2) : null;
30
+ if (arg === "--json")
31
+ flags.json = true;
32
+ else if (name === "keep-both" && allowed.includes("keep-both"))
33
+ flags.keepBoth = true;
34
+ else if (name === "repo" && allowed.includes("repo")) {
35
+ const repo = value(arg, args[++i]).replace(/^https?:\/\/github\.com\//i, "").replace(/\.git$/i, "");
36
+ if (!REPO.test(repo))
37
+ throw usage("--repo needs <owner/repo>");
38
+ flags.repo = repo.toLowerCase();
39
+ }
40
+ else if (name === "kind" && allowed.includes("kind")) {
41
+ const kind = value(arg, args[++i]);
42
+ if (!isMemoryKind(kind))
43
+ throw usage(`--kind is one of ${MEMORY_KINDS.join(", ")}`);
44
+ flags.kind = kind;
45
+ }
46
+ else if (name === "scope" && allowed.includes("scope")) {
47
+ const scope = value(arg, args[++i]);
48
+ if (!isMemoryScope(scope))
49
+ throw usage("--scope is user or repo");
50
+ flags.scope = scope;
51
+ }
52
+ else if (name === "limit" && allowed.includes("limit")) {
53
+ const limit = Number(value(arg, args[++i]));
54
+ if (!Number.isInteger(limit) || limit < 1 || limit > MEMORY_SEARCH_LIMIT_MAX)
55
+ throw usage(`--limit is a whole number from 1 to ${MEMORY_SEARCH_LIMIT_MAX}`);
56
+ flags.limit = limit;
57
+ }
58
+ else if (arg.startsWith("--"))
59
+ throw usage(`unknown argument ${arg}`);
60
+ else
61
+ flags.positional.push(arg);
62
+ }
63
+ return flags;
64
+ }
65
+ export function formatMemoryLine(memory) {
66
+ const where = memory.repo ? `${memory.kind}, ${memory.repo}` : memory.kind;
67
+ return `${memory.id} (${where}) ${memory.text}`;
68
+ }
69
+ export async function cmdMemory(args, deps = {}) {
70
+ const print = deps.print ?? ((line) => console.log(line));
71
+ const api = { listMemories, searchMemories, saveMemory, updateMemory, deleteMemory, getMemoryMode, setMemoryMode, ...deps.api };
72
+ const sub = args[0]?.toLowerCase();
73
+ const rest = args.slice(1);
74
+ if (!sub || sub === "-h" || sub === "--help" || sub === "help" || rest.some((arg) => arg === "-h" || arg === "--help")) {
75
+ print(MEMORY_USAGE);
76
+ return;
77
+ }
78
+ const config = () => (deps.loadConfig ?? loadConfig)();
79
+ const chatId = deps.chatId === undefined ? memoryChatId() : deps.chatId;
80
+ const emit = (json, data, lines) => {
81
+ if (json)
82
+ print(JSON.stringify(data, null, 2));
83
+ else
84
+ for (const line of lines())
85
+ print(line);
86
+ };
87
+ if (sub === "list" || sub === "ls") {
88
+ const flags = parseMemoryFlags(rest, ["repo", "kind", "scope"]);
89
+ if (flags.positional.length > 0)
90
+ throw usage(`unexpected argument ${flags.positional[0]}`);
91
+ const listed = await api.listMemories({ ...(flags.scope ? { scope: flags.scope } : {}), ...(flags.repo ? { repo: flags.repo } : {}), ...(flags.kind ? { kind: flags.kind } : {}) }, await config());
92
+ emit(flags.json, listed, () => [
93
+ `Memory is ${listed.memory_mode}. ${listed.items.length === 1 ? "1 memory" : `${listed.items.length} memories`}.`,
94
+ ...listed.items.map(formatMemoryLine),
95
+ ]);
96
+ return;
97
+ }
98
+ if (sub === "search") {
99
+ const flags = parseMemoryFlags(rest, ["repo", "limit"]);
100
+ const query = flags.positional.join(" ").trim();
101
+ if (!query)
102
+ throw usage("search needs a query");
103
+ const found = await api.searchMemories({ query, ...(flags.repo ? { repo: flags.repo } : {}), ...(flags.limit ? { limit: flags.limit } : {}), chatId }, await config());
104
+ emit(flags.json, found, () => {
105
+ if (found.memory_mode === "off")
106
+ return ["Memory is off, so nothing was searched."];
107
+ const all = [...found.rules, ...found.items];
108
+ return all.length === 0 ? ["Nothing in memory matches."] : all.map(formatMemoryLine);
109
+ });
110
+ return;
111
+ }
112
+ if (sub === "add" || sub === "save" || sub === "remember") {
113
+ const flags = parseMemoryFlags(rest, ["repo", "kind", "keep-both"]);
114
+ const text = flags.positional.join(" ").trim();
115
+ if (!text)
116
+ throw usage("add needs the text to remember");
117
+ if (!flags.kind)
118
+ throw usage(`add needs --kind (${MEMORY_KINDS.join(", ")})`);
119
+ const saved = await api.saveMemory({ text, kind: flags.kind, ...(flags.repo ? { repo: flags.repo } : {}), chatId, ...(flags.keepBoth ? { keepBoth: true } : {}) }, await config());
120
+ emit(flags.json, saved, () => [saved.merged ? `Memory ${saved.id} already says this.` : `Saved to memory as ${saved.id}.`]);
121
+ return;
122
+ }
123
+ if (sub === "update" || sub === "edit") {
124
+ const flags = parseMemoryFlags(rest, ["kind"]);
125
+ const [id, ...words] = flags.positional;
126
+ const text = words.join(" ").trim();
127
+ if (!id || !text)
128
+ throw usage("update needs a memory id and the new text");
129
+ const updated = await api.updateMemory(id, { text, ...(flags.kind ? { kind: flags.kind } : {}) }, await config());
130
+ emit(flags.json, updated, () => [`Memory ${updated.id} updated.`]);
131
+ return;
132
+ }
133
+ if (sub === "delete" || sub === "rm" || sub === "forget") {
134
+ const flags = parseMemoryFlags(rest, []);
135
+ const [id, extra] = flags.positional;
136
+ if (!id || extra !== undefined)
137
+ throw usage("delete needs one memory id");
138
+ const deleted = await api.deleteMemory(id, await config());
139
+ emit(flags.json, deleted, () => [`Memory ${deleted.id} deleted.`]);
140
+ return;
141
+ }
142
+ if (sub === "mode") {
143
+ const flags = parseMemoryFlags(rest, []);
144
+ const [mode, extra] = flags.positional;
145
+ if (extra !== undefined)
146
+ throw usage(`unexpected argument ${extra}`);
147
+ if (mode !== undefined && !isMemoryMode(mode))
148
+ throw usage("mode is off, on_request or all");
149
+ const result = mode === undefined ? await api.getMemoryMode(await config()) : await api.setMemoryMode(mode, await config());
150
+ emit(flags.json, result, () => [`Memory is ${result.memory_mode}.`]);
151
+ return;
152
+ }
153
+ throw usage(`unknown memory subcommand: ${sub}`);
154
+ }
@@ -3,6 +3,7 @@ import type { CommandSpec } from "../ui/prompt.js";
3
3
  export type CommandContext = {
4
4
  mode: "oneshot" | "shell";
5
5
  signal?: AbortSignal;
6
+ account?: string;
6
7
  /** Shell: refresh banner after login/logout. */
7
8
  afterAuth?: () => Promise<void>;
8
9
  };
@@ -1,5 +1,6 @@
1
- import { DEFAULT_API } from "../config.js";
1
+ import { DEFAULT_API, splitAccountFlag, withAccount } from "../config.js";
2
2
  import { CommandError } from "../errors.js";
3
+ import { cmdAccount } from "./account.js";
3
4
  import { cmdBranches, cmdChain } from "./branches.js";
4
5
  import { canBrowse, openTabsBrowser } from "./browse.js";
5
6
  import { cmdCredits } from "./credits.js";
@@ -8,6 +9,7 @@ import { cmdIssue } from "./issue.js";
8
9
  import { cmdJobs, cmdThread } from "./jobs.js";
9
10
  import { cmdLogin } from "./login.js";
10
11
  import { cmdLogout } from "./logout.js";
12
+ import { cmdMemory } from "./memory.js";
11
13
  import { cmdQueue } from "./queue.js";
12
14
  import { cmdPr } from "./pr.js";
13
15
  import { cmdReview } from "./review.js";
@@ -23,9 +25,9 @@ import { cmdWhoami } from "./whoami.js";
23
25
  export const COMMAND_REGISTRY = [
24
26
  {
25
27
  name: "login",
26
- summary: "Sign in via browser (or paste a key)",
28
+ summary: "Sign in via browser (or paste a key); adds an account, up to 3",
27
29
  usage: [
28
- "mergestorm login Sign in via browser and store an API key",
30
+ "mergestorm login Sign in via browser and add the account (up to 3 saved)",
29
31
  "mergestorm login --start Print a sign-in URL and code, then exit (agents)",
30
32
  "mergestorm login --finish Store the API key once the code from --start is approved",
31
33
  "mergestorm login --key Paste an existing API key instead (headless/CI)",
@@ -38,14 +40,28 @@ export const COMMAND_REGISTRY = [
38
40
  },
39
41
  {
40
42
  name: "logout",
41
- summary: "Remove the stored API key",
42
- usage: ["mergestorm logout Remove the stored API key"],
43
+ summary: "Log out of the active account and revoke its key",
44
+ usage: ["mergestorm logout Log out of the active account (or --account <name>) and revoke its key"],
43
45
  async run(_args, ctx) {
44
46
  await cmdLogout();
45
47
  if (ctx.afterAuth)
46
48
  await ctx.afterAuth();
47
49
  },
48
50
  },
51
+ {
52
+ name: "account",
53
+ summary: "Saved accounts: list, use <name>, remove <name>",
54
+ usage: [
55
+ "mergestorm account list [--json] Saved accounts (up to 3), the active one marked",
56
+ "mergestorm account use <name> Make <name> the active account",
57
+ "mergestorm account remove <name> Log out of <name> and revoke its key",
58
+ ],
59
+ async run(args, ctx) {
60
+ await cmdAccount(args);
61
+ if (ctx.afterAuth)
62
+ await ctx.afterAuth();
63
+ },
64
+ },
49
65
  {
50
66
  name: "review",
51
67
  summary: "Review git diff (default origin/HEAD or main or master)",
@@ -101,11 +117,15 @@ export const COMMAND_REGISTRY = [
101
117
  },
102
118
  {
103
119
  name: "issue",
104
- summary: "Open a GitHub issue through Mergestorm, no gh needed",
120
+ summary: "Open a GitHub issue through Mergestorm and wait for its Vortex review",
105
121
  usage: [
106
- "mergestorm issue create --title <title> [--body <text> | --body-file <path>] [--label <name>]… [--repo <owner/repo>] [--json]",
122
+ "mergestorm issue create --title <title> [--body <text> | --body-file <path>] [--label <name>]… [--repo <owner/repo>] [--wait [--timeout <s>]] [--json]",
107
123
  " Opens the issue as the Surge App; needs write access on GitHub, uses existing labels only",
108
124
  " The same title again returns the open issue it already made when it is among the newest 100 open issues; an older one, or two calls at once, can still create a duplicate",
125
+ " --wait then waits for the Vortex issue review, like issue wait",
126
+ "mergestorm issue wait <n> [--repo <owner/repo>] [--timeout <s>] [--json]",
127
+ " Waits for the Vortex issue review (default 900s); exits 0 once approved or when issue reviews are off",
128
+ " Exits 4 with the points to answer: reply in a comment on the issue, wait again, and open the PR only after approval",
109
129
  ],
110
130
  async run(args) {
111
131
  await cmdIssue(args);
@@ -185,7 +205,7 @@ export const COMMAND_REGISTRY = [
185
205
  {
186
206
  name: "whoami",
187
207
  summary: "Key + plan (Status tab on a TTY)",
188
- usage: ["mergestorm whoami [--json] Key + plan + API base"],
208
+ usage: ["mergestorm whoami [--json] Account + key + plan + API base"],
189
209
  async run(args, ctx) {
190
210
  await cmdWhoami(args, { mode: ctx.mode });
191
211
  },
@@ -200,10 +220,12 @@ export const COMMAND_REGISTRY = [
200
220
  " [--cyclone-skip-ci on|off]",
201
221
  " [--cyclone-patch-unverified on|off] Patch languages we cannot typecheck",
202
222
  " [--vortex-seam on|off] [--vortex-skip-all-clear on|off]",
223
+ " [--issue-review on|off] Account default for Vortex issue reviews; --repo overrides it per repository",
203
224
  " [--auto-land on|off] Auto land default for new stacks",
204
225
  " [--auto-land-settle <seconds>] How long Auto land waits before queueing (15-300, default 60)",
205
226
  " [--merge-queue-batch on|off] Test several queued pull requests on one CI run",
206
- " [--merge-queue-batch-size <n>] Most pull requests per batch (2-8, default 4)",
227
+ " [--merge-queue-batch-size <n>] Most pull requests per batch (2-10, default 4)",
228
+ " [--merge-queue-overdrive on|off] Account default: batch up to 10 queued pull requests; --repo overrides it per repository",
207
229
  " [--ignore-bot add <login>|remove <login>|clear]",
208
230
  " [--vortex-skip-check none|neutral] [--vortex-findings failure|neutral|success]",
209
231
  " [--cyclone-fail-check failure|neutral]",
@@ -217,6 +239,21 @@ export const COMMAND_REGISTRY = [
217
239
  await cmdSettings(args, { mode: ctx.mode });
218
240
  },
219
241
  },
242
+ {
243
+ name: "memory",
244
+ summary: "Your memories: list, search, add, update, delete, mode",
245
+ usage: [
246
+ "mergestorm memory list [--scope user|repo] [--repo <owner/repo>] [--kind <kind>] [--json]",
247
+ "mergestorm memory search <query> [--repo <owner/repo>] [--limit <1-20>] [--json]",
248
+ "mergestorm memory add <text> --kind rule|style|preference|fact|project [--repo <owner/repo>] [--keep-both] [--json]",
249
+ "mergestorm memory update <id> <text> [--kind <kind>] [--json]",
250
+ "mergestorm memory delete <id> [--json]",
251
+ "mergestorm memory mode [off|on_request|all] [--json] Read or set whether Breeze remembers",
252
+ ],
253
+ async run(args) {
254
+ await cmdMemory(args);
255
+ },
256
+ },
220
257
  {
221
258
  name: "skill",
222
259
  summary: "Install Mergestorm skills for Claude, Cursor, or agents",
@@ -331,9 +368,12 @@ export function formatUsageBody() {
331
368
  return `${cmdLines}
332
369
 
333
370
  mg is a short alias for mergestorm (same binary).
371
+ --account <name> before or after any command runs it as that saved account
372
+ without changing the active one; MERGESTORM_ACCOUNT=<name> does the same.
334
373
 
335
374
  Env:
336
- MERGESTORM_API_KEY API key (overrides config)
375
+ MERGESTORM_API_KEY API key (overrides every saved account)
376
+ MERGESTORM_ACCOUNT Saved account to run as (default: the active one)
337
377
  MERGESTORM_API_URL API base (default ${DEFAULT_API})
338
378
  MERGESTORM_HOME State directory for login and stack state (default ~/.mergestorm)
339
379
  `;
@@ -349,6 +389,7 @@ export async function dispatchCommand(name, args, ctx) {
349
389
  // `shell` as a subcommand is oneshot-only.
350
390
  if (cmd.name === "shell" && ctx.mode === "shell")
351
391
  return false;
352
- await cmd.run(args, ctx);
392
+ const split = splitAccountFlag(args);
393
+ await withAccount(split.account ?? ctx.account, () => cmd.run(split.args, ctx));
353
394
  return true;
354
395
  }
@@ -10,10 +10,8 @@ export declare const REVIEW_POLL_STRETCH_AFTER_MS = 60000;
10
10
  export declare const REVIEW_POLL_STRETCH_SPAN_MS = 60000;
11
11
  /** Same thread slug contract as POST /api/v1/reviews. */
12
12
  export declare const REVIEW_THREAD_SLUG_RE: RegExp;
13
- export declare const MAX_CONTEXT_TEXT_CHARS = 60000;
14
13
  export declare const MAX_CONTEXT_FILES = 10;
15
14
  export declare const MAX_CONTEXT_FILE_CHARS = 16000;
16
- export declare const MAX_CONTEXT_FILES_TOTAL = 60000;
17
15
  export declare const MAX_IDEMPOTENCY_KEY_CHARS = 128;
18
16
  export type ReviewInput = {
19
17
  thread: string;
@@ -57,7 +55,6 @@ export type LoadReviewContextOptions = {
57
55
  };
58
56
  /** Load --context / --context-file and enforce the API caps client-side. */
59
57
  export declare function loadReviewContext(opts: LoadReviewContextOptions): Promise<ReviewContextPayload>;
60
- export declare function reviewIdempotencyKey(input: ReviewInput, opts?: SubmitReviewOptions, afterJobId?: string | null): string;
61
58
  export declare function submitReview(cfg: Config, input: ReviewInput, opts?: SubmitReviewOptions): Promise<{
62
59
  status: number;
63
60
  row: ReviewJobRow;
@@ -13,10 +13,10 @@ export const REVIEW_POLL_STRETCH_AFTER_MS = 60_000;
13
13
  export const REVIEW_POLL_STRETCH_SPAN_MS = 60_000;
14
14
  /** Same thread slug contract as POST /api/v1/reviews. */
15
15
  export const REVIEW_THREAD_SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,119}$/;
16
- export const MAX_CONTEXT_TEXT_CHARS = 60_000;
16
+ const MAX_CONTEXT_TEXT_CHARS = 60_000;
17
17
  export const MAX_CONTEXT_FILES = 10;
18
18
  export const MAX_CONTEXT_FILE_CHARS = 16_000;
19
- export const MAX_CONTEXT_FILES_TOTAL = 60_000;
19
+ const MAX_CONTEXT_FILES_TOTAL = 60_000;
20
20
  export const MAX_IDEMPOTENCY_KEY_CHARS = 128;
21
21
  export async function collectReviewInput(base, head, cwd = process.cwd(), sandboxRoot) {
22
22
  let diff;
@@ -129,7 +129,7 @@ export async function loadReviewContext(opts) {
129
129
  }
130
130
  const REPLAYED_JOB_RESUBMIT_STATUSES = ["failed", "quota_exceeded"];
131
131
  const MAX_REPLAYED_JOB_RESUBMITS = 5;
132
- export function reviewIdempotencyKey(input, opts = {}, afterJobId = null) {
132
+ function reviewIdempotencyKey(input, opts = {}, afterJobId = null) {
133
133
  const request = [
134
134
  input.thread, input.branch, input.repo ?? null, input.baseLabel, input.headLabel, input.diff, input.files,
135
135
  opts.routerMode ?? null, [...new Set(opts.specialists ?? [])].sort(), opts.context ?? null, opts.contextFiles ?? [], opts.webhookUrl ?? null,
@@ -10,7 +10,7 @@ export declare const VORTEX_ROUTER_CAPS: {
10
10
  /**
11
11
  * Pinnable L0 lanes only. Keep this list byte-identical to
12
12
  * `pinnableVortexSpecialistIds(VORTEX_SPECIALIST_IDS)` in review-findings
13
- * (`cli-lockstep.test.ts`). Do not add `governance` (round-gated) or `seam`
13
+ * (`index.test.ts`). Do not add `governance` (round-gated) or `seam`
14
14
  * (phase-gated). Those L1 lanes are not local pins.
15
15
  */
16
16
  export declare const VORTEX_SPECIALIST_IDS: readonly ["security", "performance", "architecture", "tests", "data", "api", "frontend"];
@@ -16,7 +16,7 @@ export const VORTEX_ROUTER_CAPS = { standard: 3, max: 5 };
16
16
  /**
17
17
  * Pinnable L0 lanes only. Keep this list byte-identical to
18
18
  * `pinnableVortexSpecialistIds(VORTEX_SPECIALIST_IDS)` in review-findings
19
- * (`cli-lockstep.test.ts`). Do not add `governance` (round-gated) or `seam`
19
+ * (`index.test.ts`). Do not add `governance` (round-gated) or `seam`
20
20
  * (phase-gated). Those L1 lanes are not local pins.
21
21
  */
22
22
  export const VORTEX_SPECIALIST_IDS = [
@@ -1,16 +1,16 @@
1
- import { type RepoSettings, type RepoSettingsPatch, type SettingsPatch, type SettingsResponse } from "../api.js";
1
+ import { type RepoSettingsPatch, type SettingsPatch, type SettingsResponse } from "../api.js";
2
2
  /**
3
3
  * CLI flag → Bearer settings key. Boolean flags take on|off. The connected flags are
4
4
  * read-only on the API and deliberately have no flag here.
5
5
  */
6
- export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "auto_resolve_conflicts_enabled" | "auto_fix_ci_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check">;
6
+ export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "auto_resolve_conflicts_enabled" | "auto_fix_ci_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "issue_review_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "merge_queue_lands_alone_paths" | "merge_queue_overdrive" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check">;
7
7
  type IgnoreBotOperation = {
8
8
  action: "add" | "remove";
9
9
  login: string;
10
10
  } | {
11
11
  action: "clear";
12
12
  };
13
- export type RepoSettingsScope = {
13
+ type RepoSettingsScope = {
14
14
  owner: string;
15
15
  repo: string;
16
16
  patch: RepoSettingsPatch;
@@ -22,7 +22,6 @@ export type SettingsArgs = {
22
22
  repo?: RepoSettingsScope;
23
23
  listRepos?: boolean;
24
24
  };
25
- export declare function formatRepoSettingsLines(repos: readonly RepoSettings[]): string[];
26
25
  /** Parse boolean and enum flags plus ordered ignore-list operations. */
27
26
  export declare function parseSettingsArgs(rawArgs: string[]): SettingsArgs;
28
27
  /** Static printout includes the Config toggles plus enum and login settings. */
@@ -14,7 +14,26 @@ const REPO_FLAG_LABELS = {
14
14
  auto_patch_enabled: "Auto patch",
15
15
  auto_review_enabled: "Auto review",
16
16
  auto_land_default: "Auto land (new stacks)",
17
+ merge_queue_batch_enabled: "Merge queue batching",
18
+ merge_queue_batch_size: "Merge queue batch size (pull requests)",
19
+ merge_queue_lands_alone_paths: "Merge queue lands-alone paths",
20
+ merge_queue_overdrive: "Merge queue overdrive",
21
+ issue_review_enabled: "Vortex issue reviews",
17
22
  };
23
+ function parsePathsFlagValue(flag, value, defaults) {
24
+ if (value === undefined || value === "" || value.startsWith("--")) {
25
+ throw new CommandError(`${flag} takes a comma-separated list of path patterns, default, or none.`, 2, "usage");
26
+ }
27
+ if (value === "default")
28
+ return [...defaults];
29
+ if (value === "none")
30
+ return [];
31
+ const paths = [...new Set(value.split(",").map((entry) => entry.trim()).filter((entry) => entry !== ""))];
32
+ if (paths.length === 0 || paths.some((entry) => /\s/.test(entry))) {
33
+ throw new CommandError(`${flag} takes a comma-separated list of path patterns, default, or none.`, 2, "usage");
34
+ }
35
+ return paths;
36
+ }
18
37
  function parseRepoSlug(raw) {
19
38
  const match = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(raw ?? "");
20
39
  if (!match)
@@ -61,20 +80,43 @@ function parseRepoSettingsArgs(slug, args) {
61
80
  throw new CommandError(`Unknown settings flag: ${flag}. See \`mergestorm --help\`.`, 2, "usage");
62
81
  }
63
82
  if (!REPO_SETTINGS_KEYS.includes(key)) {
64
- throw new CommandError(`${flag} is an account setting. With --repo only --auto-patch, --auto-review and --auto-land can be set.`, 2, "usage");
83
+ throw new CommandError(`${flag} is an account setting. With --repo only --auto-patch, --auto-review, --auto-land, --merge-queue-batch, --merge-queue-batch-size, --merge-queue-lands-alone, --merge-queue-overdrive and --issue-review can be set.`, 2, "usage");
65
84
  }
66
85
  const value = eq >= 0 ? arg.slice(eq + 1) : args[++i];
67
- if (value !== "on" && value !== "off" && value !== "inherit") {
86
+ const row = BEARER_SETTINGS.find((row) => row.key === key);
87
+ if (value === "inherit") {
88
+ scope.patch[key] = null;
89
+ continue;
90
+ }
91
+ if ("kind" in row && row.kind === "count") {
92
+ const amount = value !== undefined && /^\d+$/.test(value) ? Number(value) : Number.NaN;
93
+ if (!Number.isInteger(amount) || amount < row.min || amount > row.max) {
94
+ throw new CommandError(`${flag} takes a whole number from ${row.min} through ${row.max}, or inherit, with --repo.`, 2, "usage");
95
+ }
96
+ scope.patch.merge_queue_batch_size = amount;
97
+ continue;
98
+ }
99
+ if ("kind" in row && row.kind === "paths") {
100
+ scope.patch.merge_queue_lands_alone_paths = parsePathsFlagValue(flag, value, row.defaults);
101
+ continue;
102
+ }
103
+ if (value !== "on" && value !== "off") {
68
104
  throw new CommandError(`${flag} takes on, off or inherit with --repo.`, 2, "usage");
69
105
  }
70
- scope.patch[key] = value === "inherit" ? null : value === "on";
106
+ scope.patch[key] = value === "on";
71
107
  }
72
108
  return { json, patch: {}, repo: scope };
73
109
  }
74
110
  function repoSettingWords(value) {
75
- return value === null ? "inherit (follows the account)" : value ? "on" : "off";
111
+ if (value === null)
112
+ return "inherit (follows the account)";
113
+ if (Array.isArray(value))
114
+ return value.length === 0 ? "(none)" : value.join(", ");
115
+ if (typeof value === "number")
116
+ return String(value);
117
+ return value ? "on" : "off";
76
118
  }
77
- export function formatRepoSettingsLines(repos) {
119
+ function formatRepoSettingsLines(repos) {
78
120
  if (repos.length === 0)
79
121
  return [" No repositories are connected to this account."];
80
122
  const width = Math.max(...Object.values(REPO_FLAG_LABELS).map((label) => label.length));
@@ -149,6 +191,10 @@ export function parseSettingsArgs(rawArgs) {
149
191
  }
150
192
  const value = eq >= 0 ? arg.slice(eq + 1) : args[++i];
151
193
  const row = BEARER_SETTINGS.find((row) => row.key === key);
194
+ if ("kind" in row && row.kind === "paths") {
195
+ Object.assign(patch, { [key]: parsePathsFlagValue(flag, value, row.defaults) });
196
+ continue;
197
+ }
152
198
  if ("kind" in row && row.kind === "logins") {
153
199
  if (value === "clear")
154
200
  ignoreBots.push({ action: "clear" });