mergestorm 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +64 -8
  2. package/dist/api.d.ts +85 -0
  3. package/dist/api.js +85 -0
  4. package/dist/cli.js +72 -353
  5. package/dist/commands/branches.d.ts +10 -0
  6. package/dist/commands/branches.js +85 -0
  7. package/dist/commands/credits.d.ts +1 -0
  8. package/dist/commands/credits.js +28 -0
  9. package/dist/commands/jobs.d.ts +6 -0
  10. package/dist/commands/jobs.js +107 -0
  11. package/dist/commands/login.d.ts +2 -0
  12. package/dist/commands/login.js +103 -0
  13. package/dist/commands/logout.d.ts +1 -0
  14. package/dist/commands/logout.js +11 -0
  15. package/dist/commands/review.d.ts +11 -0
  16. package/dist/commands/review.js +129 -0
  17. package/dist/commands/status.d.ts +5 -0
  18. package/dist/commands/status.js +39 -0
  19. package/dist/commands/whoami.d.ts +1 -0
  20. package/dist/commands/whoami.js +35 -0
  21. package/dist/config.d.ts +13 -0
  22. package/dist/config.js +41 -0
  23. package/dist/errors.d.ts +10 -0
  24. package/dist/errors.js +18 -0
  25. package/dist/git.d.ts +9 -0
  26. package/dist/git.js +66 -0
  27. package/dist/shell.d.ts +8 -0
  28. package/dist/shell.js +183 -0
  29. package/dist/ui/ansi.d.ts +12 -0
  30. package/dist/ui/ansi.js +27 -0
  31. package/dist/ui/banner.d.ts +4 -0
  32. package/dist/ui/banner.js +98 -0
  33. package/dist/ui/box.d.ts +11 -0
  34. package/dist/ui/box.js +36 -0
  35. package/dist/ui/logo.d.ts +5 -0
  36. package/dist/ui/logo.js +12 -0
  37. package/dist/ui/prompt.d.ts +18 -0
  38. package/dist/ui/prompt.js +220 -0
  39. package/dist/ui/select.d.ts +13 -0
  40. package/dist/ui/select.js +103 -0
  41. package/dist/ui/usage.d.ts +24 -0
  42. package/dist/ui/usage.js +67 -0
  43. package/dist/version.d.ts +1 -0
  44. package/dist/version.js +11 -0
  45. package/package.json +5 -4
package/dist/cli.js CHANGED
@@ -1,381 +1,100 @@
1
1
  #!/usr/bin/env node
2
- import { spawn, spawnSync } from "node:child_process";
3
- import { mkdir, readFile, writeFile, rm, readdir, stat } from "node:fs/promises";
4
- import { homedir, platform } from "node:os";
5
- import path from "node:path";
6
- import { createInterface } from "node:readline/promises";
7
2
  import { stdin as input, stdout as output } from "node:process";
8
- const DEFAULT_API = "https://api.mergestorm.ai";
9
- const CONTEXT_DIR = ".mergestorm";
10
- function configPath() {
11
- return path.join(homedir(), ".mergestorm", "config.json");
12
- }
13
- async function loadConfig() {
14
- try {
15
- const raw = await readFile(configPath(), "utf8");
16
- return JSON.parse(raw);
17
- }
18
- catch {
19
- return {};
20
- }
21
- }
22
- async function saveConfig(cfg) {
23
- const dir = path.dirname(configPath());
24
- await mkdir(dir, { recursive: true });
25
- await writeFile(configPath(), JSON.stringify(cfg, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
26
- }
27
- function apiBase(cfg) {
28
- return (process.env.MERGESTORM_API_URL?.trim() || cfg.apiBase || DEFAULT_API).replace(/\/$/, "");
29
- }
30
- async function apiFetch(cfg, route, init = {}) {
31
- const key = process.env.MERGESTORM_API_KEY?.trim() || cfg.apiKey;
32
- if (!key) {
33
- throw new Error("No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.");
34
- }
35
- const headers = {
36
- Authorization: `Bearer ${key}`,
37
- ...init.headers,
38
- };
39
- let body = init.body;
40
- if (init.json !== undefined) {
41
- headers["Content-Type"] = "application/json";
42
- body = JSON.stringify(init.json);
43
- }
44
- const res = await fetch(`${apiBase(cfg)}${route}`, { ...init, headers, body });
45
- const text = await res.text();
46
- let parsed = text;
47
- try {
48
- parsed = JSON.parse(text);
49
- }
50
- catch {
51
- // keep text
52
- }
53
- return { status: res.status, body: parsed };
54
- }
55
- function git(args, cwd = process.cwd()) {
56
- const result = spawnSync("git", args, { cwd, encoding: "utf8" });
57
- if (result.status !== 0) {
58
- throw new Error(result.stderr?.trim() || `git ${args.join(" ")} failed`);
59
- }
60
- return result.stdout;
61
- }
62
- async function collectContextFiles(root) {
63
- const pack = path.join(root, CONTEXT_DIR);
64
- let entries;
65
- try {
66
- entries = await readdir(pack, { withFileTypes: true });
67
- }
68
- catch {
69
- return [];
70
- }
71
- const files = [];
72
- for (const entry of entries) {
73
- if (entry.isFile() && (entry.name.toLowerCase().endsWith(".md") || entry.name.toLowerCase().endsWith(".txt"))) {
74
- const content = await readFile(path.join(pack, entry.name), "utf8");
75
- files.push({ path: entry.name, content });
76
- }
77
- else if (entry.isDirectory() && !entry.name.startsWith(".")) {
78
- const nested = await readdir(path.join(pack, entry.name), { withFileTypes: true }).catch(() => []);
79
- for (const child of nested) {
80
- if (child.isFile() && (child.name.toLowerCase().endsWith(".md") || child.name.toLowerCase().endsWith(".txt"))) {
81
- const rel = `${entry.name}/${child.name}`;
82
- const content = await readFile(path.join(pack, entry.name, child.name), "utf8");
83
- files.push({ path: rel, content });
84
- }
85
- }
86
- }
87
- }
88
- return files;
89
- }
90
- async function collectChangedFiles(base, head) {
91
- const names = git(["diff", "--name-only", "--diff-filter=ACMR", `${base}...${head}`])
92
- .split("\n")
93
- .map((s) => s.trim())
94
- .filter(Boolean);
95
- const files = [];
96
- for (const name of names.slice(0, 40)) {
97
- try {
98
- const st = await stat(path.join(process.cwd(), name));
99
- if (!st.isFile() || st.size > 400_000)
100
- continue;
101
- const content = await readFile(path.join(process.cwd(), name), "utf8");
102
- files.push({ path: name, content });
103
- }
104
- catch {
105
- // deleted or unreadable
106
- }
107
- }
108
- return files;
109
- }
110
- function printBanner() {
111
- console.log(`
112
- ┌─────────────────────────────┐
113
- │ MergeStorm CLI │
114
- │ review local diffs fast │
115
- └─────────────────────────────┘
116
- `);
117
- }
3
+ import { DEFAULT_API } from "./config.js";
4
+ import { CommandError } from "./errors.js";
5
+ import { cmdBranches, cmdChain } from "./commands/branches.js";
6
+ import { cmdCredits } from "./commands/credits.js";
7
+ import { cmdJobs, cmdThread } from "./commands/jobs.js";
8
+ import { cmdLogin } from "./commands/login.js";
9
+ import { cmdLogout } from "./commands/logout.js";
10
+ import { cmdReview } from "./commands/review.js";
11
+ import { cmdStatus } from "./commands/status.js";
12
+ import { cmdWhoami } from "./commands/whoami.js";
13
+ import { runShell } from "./shell.js";
14
+ import { ansi } from "./ui/ansi.js";
15
+ import { printWordmark } from "./ui/banner.js";
118
16
  function usage() {
119
- printBanner();
17
+ printWordmark();
120
18
  console.log(`Usage:
121
- mergestorm login Sign in via browser and store an API key
122
- mergestorm login --key Paste an existing API key instead (headless/CI)
123
- mergestorm logout Remove the stored API key
124
- mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
125
- mergestorm status <job_id> Poll a review job
19
+ mergestorm Interactive shell (TTY)
20
+ mergestorm shell Explicit shell entry
21
+ mergestorm login Sign in via browser and store an API key
22
+ mergestorm login --key Paste an existing API key instead (headless/CI)
23
+ mergestorm logout Remove the stored API key
24
+ mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
25
+ mergestorm status <job_id> Poll a review job (JSON)
26
+ mergestorm credits [--json] Credit balance (usage bars)
27
+ mergestorm jobs [n] [--json] Recent review jobs
28
+ mergestorm branches [n] Recently reviewed branches (pick on TTY)
29
+ mergestorm chain [slug] Branch review-chain timeline (default: current)
30
+ mergestorm whoami [--json] Key + plan + API base
31
+ mergestorm thread <slug> Jobs in a review thread
126
32
 
127
33
  Env:
128
34
  MERGESTORM_API_KEY API key (overrides config)
129
35
  MERGESTORM_API_URL API base (default ${DEFAULT_API})
130
36
  `);
131
37
  }
132
- /** Best-effort open a URL in the user's browser; silent if no opener exists. */
133
- function openBrowser(url) {
134
- const os = platform();
135
- const cmd = os === "darwin" ? "open" : os === "win32" ? "cmd" : "xdg-open";
136
- const args = os === "win32" ? ["/c", "start", "", url] : [url];
137
- try {
138
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
139
- child.on("error", () => { });
140
- child.unref();
141
- }
142
- catch {
143
- // no-op — the URL is printed for manual open
144
- }
145
- }
146
- async function devicePost(base, route, json) {
147
- const res = await fetch(`${base}${route}`, {
148
- method: "POST",
149
- headers: { "Content-Type": "application/json" },
150
- body: JSON.stringify(json),
151
- });
152
- const text = await res.text();
153
- let parsed = text;
154
- try {
155
- parsed = JSON.parse(text);
156
- }
157
- catch {
158
- // keep text
159
- }
160
- return { status: res.status, body: parsed };
38
+ async function runCommand(cmd, args) {
39
+ if (cmd === "login")
40
+ return cmdLogin(args);
41
+ if (cmd === "logout")
42
+ return cmdLogout();
43
+ if (cmd === "review")
44
+ return cmdReview(args);
45
+ if (cmd === "status") {
46
+ if (!args[0])
47
+ throw new CommandError("usage: mergestorm status <job_id>");
48
+ return cmdStatus(args[0], { format: "json" });
49
+ }
50
+ if (cmd === "credits" || cmd === "usage")
51
+ return cmdCredits(args);
52
+ if (cmd === "jobs")
53
+ return cmdJobs(args);
54
+ if (cmd === "branches" || cmd === "chains")
55
+ return cmdBranches(args);
56
+ if (cmd === "chain")
57
+ return cmdChain(args);
58
+ if (cmd === "whoami")
59
+ return cmdWhoami(args);
60
+ if (cmd === "thread") {
61
+ if (!args[0])
62
+ throw new CommandError("usage: mergestorm thread <slug>");
63
+ return cmdThread(args[0], args.slice(1));
64
+ }
65
+ if (cmd === "shell")
66
+ return runShell();
67
+ usage();
68
+ throw new CommandError(`unknown command: ${cmd}`);
161
69
  }
162
- async function cmdLoginWithKey() {
163
- const rl = createInterface({ input, output });
164
- try {
165
- const key = (await rl.question("Paste your MergeStorm API key (msk_live_...): ")).trim();
166
- if (!key.startsWith("msk_live_")) {
167
- console.error("Key should start with msk_live_");
168
- process.exitCode = 1;
70
+ async function main() {
71
+ const [, , cmd, ...args] = process.argv;
72
+ if (!cmd) {
73
+ if (input.isTTY && output.isTTY) {
74
+ await runShell();
169
75
  return;
170
76
  }
171
- const cfg = await loadConfig();
172
- cfg.apiKey = key;
173
- await saveConfig(cfg);
174
- console.log(`Saved to ${configPath()}`);
175
- }
176
- finally {
177
- rl.close();
178
- }
179
- }
180
- async function cmdLogin(args) {
181
- printBanner();
182
- if (args.includes("--key")) {
183
- await cmdLoginWithKey();
184
- return;
185
- }
186
- const cfg = await loadConfig();
187
- const base = apiBase(cfg);
188
- let start;
189
- try {
190
- start = await devicePost(base, "/api/v1/cli/device", {});
191
- }
192
- catch {
193
- console.error("Could not start device login. The API may be unreachable or misconfigured.\n" +
194
- "Run `mergestorm login --key` to paste an existing API key instead.");
195
- process.exitCode = 1;
196
- return;
197
- }
198
- if (start.status !== 200 || !start.body?.device_code) {
199
- console.error(`Could not start device login (HTTP ${start.status}). The API may be unreachable or misconfigured.\n` +
200
- `Run \`mergestorm login --key\` to paste an existing API key instead.`);
201
- process.exitCode = 1;
77
+ usage();
202
78
  return;
203
79
  }
204
- const { device_code, user_code, verification_uri, verification_uri_complete, interval, expires_in, } = start.body;
205
- const openUrl = verification_uri_complete || verification_uri;
206
- console.log("To sign in, open this URL and confirm the code:\n");
207
- console.log(` ${openUrl}`);
208
- console.log(`\n Code: ${user_code}\n`);
209
- openBrowser(openUrl);
210
- console.log("Waiting for approval…");
211
- const pollMs = Math.max((interval || 5), 2) * 1000;
212
- const deadline = Date.now() + (expires_in || 600) * 1000;
213
- while (Date.now() < deadline) {
214
- await new Promise((r) => setTimeout(r, pollMs));
215
- let poll;
216
- try {
217
- poll = await devicePost(base, "/api/v1/cli/device/token", { device_code });
218
- }
219
- catch {
220
- console.error("\nLogin failed: network error. Check your connection and run `mergestorm login` again.");
221
- process.exitCode = 1;
222
- return;
223
- }
224
- if (poll.status === 200 && poll.body?.api_key) {
225
- const next = await loadConfig();
226
- next.apiKey = poll.body.api_key;
227
- await saveConfig(next);
228
- console.log(`\nLogged in. Key saved to ${configPath()}`);
229
- return;
230
- }
231
- const err = poll.body?.error;
232
- if (err === "authorization_pending") {
233
- process.stdout.write(".");
234
- continue;
235
- }
236
- if (err === "key_limit_reached") {
237
- console.error("\nYou have reached the max active API keys. Revoke one at https://mergestorm.ai/dashboard/account and try again.");
238
- process.exitCode = 1;
239
- return;
240
- }
241
- if (err === "expired_token") {
242
- console.error("\nLogin code expired. Run `mergestorm login` again.");
243
- process.exitCode = 1;
244
- return;
245
- }
246
- if (err === "already_redeemed") {
247
- console.error("\nThis code was already used. Run `mergestorm login` again.");
248
- process.exitCode = 1;
249
- return;
250
- }
251
- console.error(`\nLogin failed: ${typeof err === "string" ? err : JSON.stringify(poll.body)}`);
252
- process.exitCode = 1;
80
+ if (cmd === "-h" || cmd === "--help") {
81
+ usage();
253
82
  return;
254
83
  }
255
- console.error("\nLogin timed out. Run `mergestorm login` again.");
256
- process.exitCode = 1;
257
- }
258
- async function cmdLogout() {
259
84
  try {
260
- await rm(configPath(), { force: true });
261
- console.log("Logged out. Removed stored API key.");
85
+ await runCommand(cmd, args);
262
86
  }
263
87
  catch (err) {
264
- console.error("Could not remove config:", err instanceof Error ? err.message : err);
265
- process.exitCode = 1;
266
- }
267
- }
268
- async function cmdReview(args) {
269
- const base = args[0] || "main";
270
- const head = args[1] || "HEAD";
271
- const cfg = await loadConfig();
272
- console.log(`Collecting diff ${base}...${head} …`);
273
- const diff = git(["diff", `${base}...${head}`]);
274
- if (!diff.trim()) {
275
- console.log("No changes to review.");
276
- return;
277
- }
278
- const files = await collectChangedFiles(base, head);
279
- const context_files = await collectContextFiles(process.cwd());
280
- const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]).trim() || "local";
281
- const thread = `local/${branch}`.replace(/[^a-zA-Z0-9._/-]/g, "-").slice(0, 120);
282
- console.log(`Submitting review (${Buffer.byteLength(diff, "utf8")} byte diff, ${files.length} file(s), ${context_files.length} context file(s))…`);
283
- const { status, body } = await apiFetch(cfg, "/api/v1/reviews", {
284
- method: "POST",
285
- json: {
286
- thread,
287
- base_label: base,
288
- head_label: head,
289
- diff,
290
- files,
291
- context_files,
292
- },
293
- });
294
- if (status === 402) {
295
- console.error("Monthly review limit reached. Upgrade at https://mergestorm.ai/billing");
296
- process.exitCode = 1;
297
- return;
298
- }
299
- if (status !== 202 && status !== 200) {
300
- console.error("Failed:", JSON.stringify(body, null, 2));
301
- process.exitCode = 1;
302
- return;
303
- }
304
- const job = body;
305
- console.log(`Job ${job.job_id} (${job.status}). Waiting…`);
306
- for (let i = 0; i < 90; i += 1) {
307
- await new Promise((r) => setTimeout(r, 2000));
308
- const poll = await apiFetch(cfg, `/api/v1/reviews/${job.job_id}`);
309
- if (poll.status !== 200) {
310
- console.error("\nPoll failed:", JSON.stringify(poll.body, null, 2));
311
- process.exitCode = 1;
88
+ if (err instanceof CommandError) {
89
+ console.error(err.message);
90
+ process.exitCode = err.exitCode;
312
91
  return;
313
92
  }
314
- const row = poll.body;
315
- if (row.status === "queued" || row.status === "in_progress") {
316
- process.stdout.write(".");
317
- continue;
318
- }
319
- console.log("");
320
- if (row.status === "failed" || row.status === "quota_exceeded") {
321
- console.error(`Review ${row.status}: ${row.error ?? ""}`);
322
- process.exitCode = 1;
323
- return;
324
- }
325
- console.log(`\nVerdict: ${row.verdict ?? "comment"}`);
326
- console.log(row.summary ?? "");
327
- const inline = row.findings?.inline ?? [];
328
- const off = row.findings?.off_diff ?? row.findings?.offDiff ?? [];
329
- for (const c of inline) {
330
- console.log(`\n[${c.severity}] ${c.path}:${c.line}${c.title ? ` — ${c.title}` : ""}`);
331
- console.log(c.body);
332
- }
333
- for (const c of off) {
334
- console.log(`\n[${c.severity}] off-diff ${c.path ?? ""}`);
335
- console.log(c.body);
336
- }
337
- if (inline.length === 0 && off.length === 0) {
338
- console.log("\nNo findings.");
339
- }
340
- return;
341
- }
342
- console.error("\nTimed out waiting for review. Check: mergestorm status " + job.job_id);
343
- process.exitCode = 1;
344
- }
345
- async function cmdStatus(jobId) {
346
- const cfg = await loadConfig();
347
- const { status, body } = await apiFetch(cfg, `/api/v1/reviews/${jobId}`);
348
- if (status !== 200) {
349
- console.error(JSON.stringify(body, null, 2));
93
+ console.error(err instanceof Error ? err.message : err);
350
94
  process.exitCode = 1;
351
- return;
352
- }
353
- console.log(JSON.stringify(body, null, 2));
354
- }
355
- async function main() {
356
- const [, , cmd, ...args] = process.argv;
357
- if (!cmd || cmd === "-h" || cmd === "--help") {
358
- usage();
359
- return;
360
- }
361
- if (cmd === "login")
362
- return cmdLogin(args);
363
- if (cmd === "logout")
364
- return cmdLogout();
365
- if (cmd === "review")
366
- return cmdReview(args);
367
- if (cmd === "status") {
368
- if (!args[0]) {
369
- console.error("usage: mergestorm status <job_id>");
370
- process.exitCode = 1;
371
- return;
372
- }
373
- return cmdStatus(args[0]);
374
95
  }
375
- usage();
376
- process.exitCode = 1;
377
96
  }
378
97
  main().catch((err) => {
379
- console.error(err instanceof Error ? err.message : err);
98
+ console.error(ansi.red(err instanceof Error ? err.message : String(err)));
380
99
  process.exitCode = 1;
381
100
  });
@@ -0,0 +1,10 @@
1
+ import { type ThreadListItem } from "../api.js";
2
+ /** One-line label for a branch/thread row (also used by tests). */
3
+ export declare function formatBranchRow(item: ThreadListItem): string;
4
+ /** Interactive list of recently reviewed branches → open chain on select. */
5
+ export declare function cmdBranches(args?: string[]): Promise<void>;
6
+ /**
7
+ * Show the review-chain timeline for a branch/thread.
8
+ * Defaults to `local/<current-git-branch>` when no slug is given.
9
+ */
10
+ export declare function cmdChain(args?: string[]): Promise<void>;
@@ -0,0 +1,85 @@
1
+ import { stdin as input, stdout as output } from "node:process";
2
+ import { listThreads } from "../api.js";
3
+ import { loadConfig } from "../config.js";
4
+ import { CommandError } from "../errors.js";
5
+ import { git } from "../git.js";
6
+ import { ansi } from "../ui/ansi.js";
7
+ import { selectFromList } from "../ui/select.js";
8
+ import { cmdThread, relativeWhen } from "./jobs.js";
9
+ /** One-line label for a branch/thread row (also used by tests). */
10
+ export function formatBranchRow(item) {
11
+ const name = (item.branch?.trim() || item.slug).slice(0, 36);
12
+ const jobs = `${item.job_count} job${item.job_count === 1 ? "" : "s"}`;
13
+ const verdict = item.last_verdict ?? "-";
14
+ const when = relativeWhen(item.last_activity_at);
15
+ const pr = item.owner && item.repo && item.pr_number != null
16
+ ? ` PR #${item.pr_number}`
17
+ : "";
18
+ const closed = item.status === "closed" ? " closed" : "";
19
+ return `${name.padEnd(36)} ${jobs.padEnd(8)} ${verdict.padEnd(12)} ${when}${pr}${closed}`;
20
+ }
21
+ function printBranchTable(items) {
22
+ const header = ["BRANCH / SLUG", "JOBS", "VERDICT", "WHEN", "PR"];
23
+ const rows = items.map((t) => [
24
+ (t.branch?.trim() || t.slug).slice(0, 36),
25
+ String(t.job_count),
26
+ t.last_verdict ?? "-",
27
+ relativeWhen(t.last_activity_at),
28
+ t.pr_number != null ? `#${t.pr_number}` : "-",
29
+ ]);
30
+ const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i]?.length ?? 0)));
31
+ const fmt = (cols) => " " + cols.map((c, i) => c.padEnd(widths[i])).join(" ");
32
+ console.log(ansi.dim(fmt(header)));
33
+ for (const r of rows)
34
+ console.log(fmt(r));
35
+ console.log(ansi.dim(" open a chain: chain <slug> · or re-run on a TTY to pick"));
36
+ }
37
+ /** Interactive list of recently reviewed branches → open chain on select. */
38
+ export async function cmdBranches(args = []) {
39
+ const asJson = args.includes("--json");
40
+ const nArg = args.find((a) => /^\d+$/.test(a));
41
+ const limit = nArg ? Number.parseInt(nArg, 10) : 20;
42
+ const cfg = await loadConfig();
43
+ const items = await listThreads(limit, cfg);
44
+ if (asJson) {
45
+ console.log(JSON.stringify({ items }, null, 2));
46
+ return;
47
+ }
48
+ if (items.length === 0) {
49
+ console.log(ansi.dim(" No reviewed branches yet. Run `review` on a branch first."));
50
+ return;
51
+ }
52
+ if (!input.isTTY || !output.isTTY) {
53
+ printBranchTable(items);
54
+ return;
55
+ }
56
+ const picked = await selectFromList({
57
+ title: "Recently reviewed branches",
58
+ items,
59
+ render: (item) => formatBranchRow(item),
60
+ });
61
+ if (!picked) {
62
+ console.log(ansi.dim(" cancelled"));
63
+ return;
64
+ }
65
+ await cmdThread(picked.slug, []);
66
+ }
67
+ /**
68
+ * Show the review-chain timeline for a branch/thread.
69
+ * Defaults to `local/<current-git-branch>` when no slug is given.
70
+ */
71
+ export async function cmdChain(args = []) {
72
+ const asJson = args.includes("--json");
73
+ const positional = args.filter((a) => a !== "--json");
74
+ let slug = positional[0]?.trim() ?? "";
75
+ if (!slug) {
76
+ try {
77
+ const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]).trim() || "local";
78
+ slug = `local/${branch}`.replace(/[^a-zA-Z0-9._/-]/g, "-").slice(0, 120);
79
+ }
80
+ catch {
81
+ throw new CommandError("usage: chain <slug> (or run inside a git repo to use the current branch)");
82
+ }
83
+ }
84
+ await cmdThread(slug, asJson ? ["--json"] : []);
85
+ }
@@ -0,0 +1 @@
1
+ export declare function cmdCredits(args: string[]): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { getMe } from "../api.js";
2
+ import { loadConfig } from "../config.js";
3
+ import { CommandError } from "../errors.js";
4
+ import { ansi } from "../ui/ansi.js";
5
+ import { formatResetLabel, usageBar } from "../ui/usage.js";
6
+ export async function cmdCredits(args) {
7
+ const asJson = args.includes("--json");
8
+ const cfg = await loadConfig();
9
+ const me = await getMe(cfg);
10
+ if (!me) {
11
+ throw new CommandError("Credits are not available (not logged in, offline, or API too old). " +
12
+ "Try `mergestorm login` or open https://mergestorm.ai/billing");
13
+ }
14
+ if (asJson) {
15
+ console.log(JSON.stringify({ usage: me.usage, resets_at: me.resets_at ?? null }, null, 2));
16
+ return;
17
+ }
18
+ const s = me.usage.standard;
19
+ const p = me.usage.premium;
20
+ // Claude Usage tab layout: label → long bar + "N% used" → Resets line.
21
+ console.log(` ${ansi.bold("Standard")}`);
22
+ console.log(` ${usageBar(s.used, s.limit)}`);
23
+ console.log("");
24
+ console.log(` ${ansi.bold("Premium")}`);
25
+ console.log(` ${usageBar(p.used, p.limit)}`);
26
+ console.log("");
27
+ console.log(` ${ansi.dim(formatResetLabel(me.resets_at))}`);
28
+ }
@@ -0,0 +1,6 @@
1
+ import { type ThreadDetail } from "../api.js";
2
+ export declare function relativeWhen(iso: string): string;
3
+ export declare function cmdJobs(args: string[]): Promise<void>;
4
+ /** Numbered chain timeline for a thread (branch/PR meta when present). */
5
+ export declare function formatThreadTimeline(data: ThreadDetail): string[];
6
+ export declare function cmdThread(slug: string, args?: string[]): Promise<void>;