mergestorm 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,21 +29,43 @@ node packages/mergestorm-cli/dist/cli.js <command>
29
29
  npm run dev --workspace=mergestorm -- <command>
30
30
  ```
31
31
 
32
+ ## Interactive shell
33
+
34
+ On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL with auth/credits status, green prompt, and in-shell commands. Non-TTY (CI/pipes) prints usage instead — no hanging prompt.
35
+
36
+ ```
37
+ $ mergestorm
38
+ __ __ ___ ___ …
39
+ ● msk_live_a1b2… · pro · standard credits: 63 of 100 left
40
+
41
+ mergestorm> credits
42
+ mergestorm> review
43
+ mergestorm> exit
44
+ ```
45
+
32
46
  ## Commands
33
47
 
48
+ One-shot subcommands work the same inside the shell and from argv (scripts/CI):
49
+
34
50
  ```
35
- mergestorm login Sign in via browser; stores an API key in ~/.mergestorm/config.json
36
- mergestorm login --key Paste an existing API key instead (headless/CI)
37
- mergestorm logout Remove the stored API key
38
- mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
39
- mergestorm status <job_id> Poll a review job and print its result
51
+ mergestorm Interactive shell (TTY only)
52
+ mergestorm shell Explicit shell entry
53
+ mergestorm login Sign in via browser; stores an API key in ~/.mergestorm/config.json
54
+ mergestorm login --key Paste an existing API key instead (headless/CI)
55
+ mergestorm logout Remove the stored API key
56
+ mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
57
+ mergestorm status <job_id> Poll a review job and print JSON
58
+ mergestorm credits [--json] Standard + premium credit balance
59
+ mergestorm jobs [n] [--json] Recent review jobs (default 10, max 50)
60
+ mergestorm whoami [--json] Key prefix, plan, API base, config path
61
+ mergestorm thread <slug> Jobs in a review thread
40
62
  ```
41
63
 
42
64
  ### `login`
43
65
 
44
66
  Runs a browser device-authorization flow: prints a short code, opens `https://mergestorm.ai/cli/auth`, and — once you approve — mints and stores an API key in `~/.mergestorm/config.json` (mode `600`). No key copy/paste needed.
45
67
 
46
- For headless or CI environments, use `mergestorm login --key` to paste an existing `msk_live_…` key (create one on the dashboard [Account page](https://mergestorm.ai/dashboard/account) → **API keys**).
68
+ For headless or CI environments, use `mergestorm login --key` to paste an existing `msk_live_…` key (create one on the dashboard [API tab](https://mergestorm.ai/dashboard/api)).
47
69
 
48
70
  ### `logout`
49
71
 
@@ -59,11 +81,15 @@ mergestorm review develop # develop...HEAD
59
81
  mergestorm review main feat/x # main...feat/x
60
82
  ```
61
83
 
62
- Exit code is non-zero on quota limit (402), submission failure, a `failed`/`quota_exceeded` job, or a poll timeout.
84
+ Exit code is non-zero on quota limit (402), submission failure, a `failed`/`quota_exceeded` job, or a poll timeout. In the interactive shell, Ctrl+C during the wait detaches (job keeps running) — use `status <job_id>` later.
63
85
 
64
86
  ### `status <job_id>`
65
87
 
66
- Fetches a job by id and prints the raw JSON (status, verdict, summary, findings).
88
+ Fetches a job by id and prints the raw JSON (status, verdict, summary, findings). In the shell, output is formatted for reading.
89
+
90
+ ### `credits` / `jobs` / `whoami`
91
+
92
+ Require a logged-in key and API support for `GET /api/v1/me` and `GET /api/v1/reviews`. Older API deployments degrade gracefully (banner shows local key only).
67
93
 
68
94
  ## Context pack (`.mergestorm/`)
69
95
 
@@ -75,6 +101,7 @@ Any `*.md` / `*.txt` files under `./.mergestorm/` (one level of nesting) are att
75
101
  |----------|---------|---------|
76
102
  | `MERGESTORM_API_KEY` | API key (overrides the stored config) | — |
77
103
  | `MERGESTORM_API_URL` | API base URL | `https://api.mergestorm.ai` |
104
+ | `NO_COLOR` | Disable ANSI colors | — |
78
105
 
79
106
  ## See also
80
107
 
package/dist/api.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { type Config } from "./config.js";
2
+ export declare function apiFetch(cfg: Config, route: string, init?: RequestInit & {
3
+ json?: unknown;
4
+ }): Promise<{
5
+ status: number;
6
+ body: unknown;
7
+ }>;
8
+ export declare function devicePost(base: string, route: string, json: unknown): Promise<{
9
+ status: number;
10
+ body: any;
11
+ }>;
12
+ export type MeResponse = {
13
+ key: {
14
+ prefix: string;
15
+ name: string;
16
+ created_at: string;
17
+ last_used_at: string | null;
18
+ };
19
+ plan_key: string;
20
+ plan_label_key?: string;
21
+ usage: {
22
+ standard: {
23
+ used: number;
24
+ limit: number;
25
+ remaining: number;
26
+ };
27
+ premium: {
28
+ used: number;
29
+ limit: number;
30
+ remaining: number;
31
+ };
32
+ };
33
+ };
34
+ export type JobListItem = {
35
+ job_id: string;
36
+ thread_slug: string | null;
37
+ status: string;
38
+ verdict: string | null;
39
+ summary: string | null;
40
+ base_label: string | null;
41
+ head_label: string | null;
42
+ created_at: string;
43
+ finished_at: string | null;
44
+ credits: number;
45
+ };
46
+ /** Fetch /api/v1/me; returns null on 404/network so callers can degrade. */
47
+ export declare function getMe(cfg?: Config): Promise<MeResponse | null>;
48
+ export declare function listJobs(limit?: number, cfg?: Config): Promise<JobListItem[]>;
package/dist/api.js ADDED
@@ -0,0 +1,71 @@
1
+ import { apiBase, loadConfig, resolveApiKey } from "./config.js";
2
+ import { CommandError } from "./errors.js";
3
+ export async function apiFetch(cfg, route, init = {}) {
4
+ const key = resolveApiKey(cfg);
5
+ if (!key) {
6
+ throw new CommandError("No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.");
7
+ }
8
+ const headers = {
9
+ Authorization: `Bearer ${key}`,
10
+ ...init.headers,
11
+ };
12
+ let body = init.body;
13
+ if (init.json !== undefined) {
14
+ headers["Content-Type"] = "application/json";
15
+ body = JSON.stringify(init.json);
16
+ }
17
+ const res = await fetch(`${apiBase(cfg)}${route}`, { ...init, headers, body });
18
+ const text = await res.text();
19
+ let parsed = text;
20
+ try {
21
+ parsed = JSON.parse(text);
22
+ }
23
+ catch {
24
+ // keep text
25
+ }
26
+ return { status: res.status, body: parsed };
27
+ }
28
+ export async function devicePost(base, route, json) {
29
+ const res = await fetch(`${base}${route}`, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify(json),
33
+ });
34
+ const text = await res.text();
35
+ let parsed = text;
36
+ try {
37
+ parsed = JSON.parse(text);
38
+ }
39
+ catch {
40
+ // keep text
41
+ }
42
+ return { status: res.status, body: parsed };
43
+ }
44
+ /** Fetch /api/v1/me; returns null on 404/network so callers can degrade. */
45
+ export async function getMe(cfg) {
46
+ const resolved = cfg ?? (await loadConfig());
47
+ try {
48
+ const { status, body } = await apiFetch(resolved, "/api/v1/me");
49
+ if (status === 404)
50
+ return null;
51
+ if (status !== 200)
52
+ return null;
53
+ return body;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ export async function listJobs(limit = 10, cfg) {
60
+ const resolved = cfg ?? (await loadConfig());
61
+ const capped = Math.min(Math.max(limit, 1), 50);
62
+ const { status, body } = await apiFetch(resolved, `/api/v1/reviews?limit=${capped}`);
63
+ if (status === 404) {
64
+ throw new CommandError("Job list is not available on this server yet. Update the API or use the dashboard.");
65
+ }
66
+ if (status !== 200) {
67
+ throw new CommandError(`Failed to list jobs (HTTP ${status}): ${JSON.stringify(body)}`);
68
+ }
69
+ const items = body.items;
70
+ return Array.isArray(items) ? items : [];
71
+ }
package/dist/cli.js CHANGED
@@ -1,381 +1,93 @@
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 { cmdCredits } from "./commands/credits.js";
6
+ import { cmdJobs, cmdThread } from "./commands/jobs.js";
7
+ import { cmdLogin } from "./commands/login.js";
8
+ import { cmdLogout } from "./commands/logout.js";
9
+ import { cmdReview } from "./commands/review.js";
10
+ import { cmdStatus } from "./commands/status.js";
11
+ import { cmdWhoami } from "./commands/whoami.js";
12
+ import { runShell } from "./shell.js";
13
+ import { ansi } from "./ui/ansi.js";
14
+ import { printWordmark } from "./ui/banner.js";
118
15
  function usage() {
119
- printBanner();
16
+ printWordmark();
120
17
  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
18
+ mergestorm Interactive shell (TTY)
19
+ mergestorm shell Explicit shell entry
20
+ mergestorm login Sign in via browser and store an API key
21
+ mergestorm login --key Paste an existing API key instead (headless/CI)
22
+ mergestorm logout Remove the stored API key
23
+ mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
24
+ mergestorm status <job_id> Poll a review job (JSON)
25
+ mergestorm credits [--json] Credit balance
26
+ mergestorm jobs [n] [--json] Recent review jobs
27
+ mergestorm whoami [--json] Key + plan + API base
28
+ mergestorm thread <slug> Jobs in a review thread
126
29
 
127
30
  Env:
128
31
  MERGESTORM_API_KEY API key (overrides config)
129
32
  MERGESTORM_API_URL API base (default ${DEFAULT_API})
130
33
  `);
131
34
  }
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 };
35
+ async function runCommand(cmd, args) {
36
+ if (cmd === "login")
37
+ return cmdLogin(args);
38
+ if (cmd === "logout")
39
+ return cmdLogout();
40
+ if (cmd === "review")
41
+ return cmdReview(args);
42
+ if (cmd === "status") {
43
+ if (!args[0])
44
+ throw new CommandError("usage: mergestorm status <job_id>");
45
+ return cmdStatus(args[0], { format: "json" });
46
+ }
47
+ if (cmd === "credits" || cmd === "usage")
48
+ return cmdCredits(args);
49
+ if (cmd === "jobs")
50
+ return cmdJobs(args);
51
+ if (cmd === "whoami")
52
+ return cmdWhoami(args);
53
+ if (cmd === "thread") {
54
+ if (!args[0])
55
+ throw new CommandError("usage: mergestorm thread <slug>");
56
+ return cmdThread(args[0], args.slice(1));
57
+ }
58
+ if (cmd === "shell")
59
+ return runShell();
60
+ usage();
61
+ throw new CommandError(`unknown command: ${cmd}`);
161
62
  }
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;
63
+ async function main() {
64
+ const [, , cmd, ...args] = process.argv;
65
+ if (!cmd) {
66
+ if (input.isTTY && output.isTTY) {
67
+ await runShell();
169
68
  return;
170
69
  }
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;
70
+ usage();
202
71
  return;
203
72
  }
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;
73
+ if (cmd === "-h" || cmd === "--help") {
74
+ usage();
253
75
  return;
254
76
  }
255
- console.error("\nLogin timed out. Run `mergestorm login` again.");
256
- process.exitCode = 1;
257
- }
258
- async function cmdLogout() {
259
77
  try {
260
- await rm(configPath(), { force: true });
261
- console.log("Logged out. Removed stored API key.");
78
+ await runCommand(cmd, args);
262
79
  }
263
80
  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;
81
+ if (err instanceof CommandError) {
82
+ console.error(err.message);
83
+ process.exitCode = err.exitCode;
312
84
  return;
313
85
  }
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));
86
+ console.error(err instanceof Error ? err.message : err);
350
87
  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
88
  }
375
- usage();
376
- process.exitCode = 1;
377
89
  }
378
90
  main().catch((err) => {
379
- console.error(err instanceof Error ? err.message : err);
91
+ console.error(ansi.red(err instanceof Error ? err.message : String(err)));
380
92
  process.exitCode = 1;
381
93
  });
@@ -0,0 +1 @@
1
+ export declare function cmdCredits(args: string[]): Promise<void>;
@@ -0,0 +1,22 @@
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
+ export async function cmdCredits(args) {
6
+ const asJson = args.includes("--json");
7
+ const cfg = await loadConfig();
8
+ const me = await getMe(cfg);
9
+ if (!me) {
10
+ throw new CommandError("Credits are not available (not logged in, offline, or API too old). " +
11
+ "Try `mergestorm login` or open https://mergestorm.ai/billing");
12
+ }
13
+ if (asJson) {
14
+ console.log(JSON.stringify(me.usage, null, 2));
15
+ return;
16
+ }
17
+ const s = me.usage.standard;
18
+ const p = me.usage.premium;
19
+ console.log(` Standard ${s.used} used / ${s.limit} (${s.remaining} remaining)`);
20
+ console.log(` Premium ${p.used} used / ${p.limit} (${p.remaining} remaining)`);
21
+ console.log(ansi.dim(" Resets with your billing cycle · manage at https://mergestorm.ai/billing"));
22
+ }
@@ -0,0 +1,2 @@
1
+ export declare function cmdJobs(args: string[]): Promise<void>;
2
+ export declare function cmdThread(slug: string, args?: string[]): Promise<void>;