mergestorm 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,29 +20,56 @@ npm install -g mergestorm
20
20
  mergestorm login
21
21
  ```
22
22
 
23
- ### From this monorepo (development)
23
+ ### From this monorepo (development / local UI testing)
24
+
25
+ You do **not** need to bump/publish npm to try shell layout changes. From the repo root:
24
26
 
25
27
  ```bash
28
+ # Fastest — runs TypeScript via tsx (no build, no version bump):
29
+ npm run dev --workspace=mergestorm
30
+
31
+ # Or build once, then run the compiled binary:
32
+ npm run build --workspace=mergestorm
33
+ node packages/mergestorm-cli/dist/cli.js
34
+
35
+ # Optional: expose a local binary as `mergestorm` without publishing
26
36
  npm run build --workspace=mergestorm
27
- node packages/mergestorm-cli/dist/cli.js <command>
28
- # or skip the build during dev (runs src/cli.ts via tsx):
29
- npm run dev --workspace=mergestorm -- <command>
37
+ npm link --workspace=mergestorm
38
+ mergestorm # uses your local build
39
+ # later: npm unlink -g mergestorm && npm install -g mergestorm
30
40
  ```
31
41
 
32
42
  ## Interactive shell
33
43
 
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.
44
+ On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL: a **full-width** welcome panel (logo + status on the left, tips / what's new on the right) and a **full-width** bordered input box with a live `/` slash-command dropdown. Non-TTY (CI/pipes) prints usage instead — no hanging prompt.
35
45
 
36
46
  ```
37
- $ mergestorm
38
- __ __ ___ ___ …
39
- ● msk_live_a1b2… · pro · standard credits: 63 of 100 left
47
+ ╭─ mergestorm v0.3.1 ──────────────────────────────────────────────────────────╮
48
+ │ ▟██████████▛ Tips for getting started │
49
+ │ ▜████████▛ review review main...HEAD │
50
+ │ ▝▜████▛▘ branches pick a reviewed branch │
51
+ │ ▜██▛ /help list all commands │
52
+ │ ██ │
53
+ │ ▝▘ What's new in v0.3.1 │
54
+ │ ● msk… · maelstrom • Full-width welcome + input │
55
+ │ [████░░░░░░░░░░] 12% used │
56
+ ╰──────────────────────────────────────────────────────────────────────────────╯
57
+ mergestorm
58
+ ╭──────────────────────────────────────────────────────────────────────────────╮
59
+ │ › /re │
60
+ ╰──────────────────────────────────────────────────────────────────────────────╯
61
+ /review Review git diff (default main...HEAD)
40
62
 
41
63
  mergestorm> credits
42
- mergestorm> review
64
+ mergestorm> branches
65
+ mergestorm> chain
43
66
  mergestorm> exit
44
67
  ```
45
68
 
69
+ Bare command names still work exactly as before (`review`, `help`, `exit`, …) — typing `/` at the start of a line just opens the live autocomplete dropdown (filtered as you type, navigate with Up/Down, accept with Tab/Enter/Right). Arrow-key history, Ctrl+A/E, Ctrl+U/K work as in a regular shell. At the idle prompt, **Ctrl+C twice** (within ~1.5s) exits the shell; a single Ctrl+C clears the line and shows a confirm hint. While `review` / `status` is waiting, Ctrl+C detaches or aborts that wait (job may keep running) — it does not exit the shell. Ctrl+D on an empty line, or `exit` / `quit`, also leave the shell. On a dumb terminal, without color, or when piped, the box falls back to plain ASCII borders (or no shell at all for non-TTY input).
70
+
71
+ `credits` / `usage` print a usage meter — long bar + `N% used`, then `Resets … (UTC)` — for standard and premium. `branches` (alias `chains`) lists recently reviewed branches; on a TTY you arrow-select one to open its review-chain timeline. `chain [slug]` shows that timeline directly (defaults to `local/<current-git-branch>`).
72
+
46
73
  ## Commands
47
74
 
48
75
  One-shot subcommands work the same inside the shell and from argv (scripts/CI):
@@ -55,8 +82,10 @@ mergestorm login --key Paste an existing API key instead (headless/CI)
55
82
  mergestorm logout Remove the stored API key
56
83
  mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
57
84
  mergestorm status <job_id> Poll a review job and print JSON
58
- mergestorm credits [--json] Standard + premium credit balance
85
+ mergestorm credits [--json] Standard + premium credit balance (usage bars)
59
86
  mergestorm jobs [n] [--json] Recent review jobs (default 10, max 50)
87
+ mergestorm branches [n] Recently reviewed branches (arrow-pick on TTY)
88
+ mergestorm chain [slug] Branch review-chain timeline (default: current branch)
60
89
  mergestorm whoami [--json] Key prefix, plan, API base, config path
61
90
  mergestorm thread <slug> Jobs in a review thread
62
91
  ```
@@ -91,6 +120,10 @@ Fetches a job by id and prints the raw JSON (status, verdict, summary, findings)
91
120
 
92
121
  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).
93
122
 
123
+ ### `branches` / `chain`
124
+
125
+ `branches` calls `GET /api/v1/threads` for recently active review threads (branches). On a TTY it opens an arrow-key selector; Enter loads that thread's job timeline. `chain [slug]` is the direct timeline view — omit the slug to use `local/<current-git-branch>` (same slug `review` stamps).
126
+
94
127
  ## Context pack (`.mergestorm/`)
95
128
 
96
129
  Any `*.md` / `*.txt` files under `./.mergestorm/` (one level of nesting) are attached as context so the reviewer understands intent, conventions, or migration notes without you resending history.
package/dist/api.d.ts CHANGED
@@ -18,16 +18,18 @@ export type MeResponse = {
18
18
  };
19
19
  plan_key: string;
20
20
  plan_label_key?: string;
21
+ /** Start of next UTC month (usage meters reset). Older APIs omit this. */
22
+ resets_at?: string;
21
23
  usage: {
22
24
  standard: {
23
25
  used: number;
24
- limit: number;
25
- remaining: number;
26
+ limit: number | null;
27
+ remaining: number | null;
26
28
  };
27
29
  premium: {
28
30
  used: number;
29
- limit: number;
30
- remaining: number;
31
+ limit: number | null;
32
+ remaining: number | null;
31
33
  };
32
34
  };
33
35
  };
@@ -43,6 +45,41 @@ export type JobListItem = {
43
45
  finished_at: string | null;
44
46
  credits: number;
45
47
  };
48
+ export type ThreadListItem = {
49
+ slug: string;
50
+ branch: string | null;
51
+ owner: string | null;
52
+ repo: string | null;
53
+ pr_number: number | null;
54
+ status: string;
55
+ job_count: number;
56
+ last_verdict: string | null;
57
+ last_activity_at: string;
58
+ };
59
+ export type ThreadDetail = {
60
+ thread_id: string;
61
+ slug: string;
62
+ source?: string;
63
+ branch: string | null;
64
+ owner: string | null;
65
+ repo: string | null;
66
+ pr_number: number | null;
67
+ status: string;
68
+ pr_linked_at: string | null;
69
+ job_count: number;
70
+ created_at: string;
71
+ jobs: {
72
+ job_id: string;
73
+ status: string;
74
+ summary?: string | null;
75
+ verdict?: string | null;
76
+ thread_job_number?: number | null;
77
+ created_at: string;
78
+ finished_at?: string | null;
79
+ }[];
80
+ };
46
81
  /** Fetch /api/v1/me; returns null on 404/network so callers can degrade. */
47
82
  export declare function getMe(cfg?: Config): Promise<MeResponse | null>;
48
83
  export declare function listJobs(limit?: number, cfg?: Config): Promise<JobListItem[]>;
84
+ /** Recently reviewed branches/threads (Bearer GET /api/v1/threads). */
85
+ export declare function listThreads(limit?: number, cfg?: Config): Promise<ThreadListItem[]>;
package/dist/api.js CHANGED
@@ -69,3 +69,17 @@ export async function listJobs(limit = 10, cfg) {
69
69
  const items = body.items;
70
70
  return Array.isArray(items) ? items : [];
71
71
  }
72
+ /** Recently reviewed branches/threads (Bearer GET /api/v1/threads). */
73
+ export async function listThreads(limit = 20, cfg) {
74
+ const resolved = cfg ?? (await loadConfig());
75
+ const capped = Math.min(Math.max(limit, 1), 50);
76
+ const { status, body } = await apiFetch(resolved, `/api/v1/threads?limit=${capped}`);
77
+ if (status === 404) {
78
+ throw new CommandError("Branch list is not available on this server yet. Deploy the API update or use the dashboard.");
79
+ }
80
+ if (status !== 200) {
81
+ throw new CommandError(`Failed to list branches (HTTP ${status}): ${JSON.stringify(body)}`);
82
+ }
83
+ const items = body.items;
84
+ return Array.isArray(items) ? items : [];
85
+ }
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { stdin as input, stdout as output } from "node:process";
3
3
  import { DEFAULT_API } from "./config.js";
4
4
  import { CommandError } from "./errors.js";
5
+ import { cmdBranches, cmdChain } from "./commands/branches.js";
5
6
  import { cmdCredits } from "./commands/credits.js";
6
7
  import { cmdJobs, cmdThread } from "./commands/jobs.js";
7
8
  import { cmdLogin } from "./commands/login.js";
@@ -22,8 +23,10 @@ function usage() {
22
23
  mergestorm logout Remove the stored API key
23
24
  mergestorm review [base] [head] Review git diff base...head (default: main...HEAD)
24
25
  mergestorm status <job_id> Poll a review job (JSON)
25
- mergestorm credits [--json] Credit balance
26
+ mergestorm credits [--json] Credit balance (usage bars)
26
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)
27
30
  mergestorm whoami [--json] Key + plan + API base
28
31
  mergestorm thread <slug> Jobs in a review thread
29
32
 
@@ -48,6 +51,10 @@ async function runCommand(cmd, args) {
48
51
  return cmdCredits(args);
49
52
  if (cmd === "jobs")
50
53
  return cmdJobs(args);
54
+ if (cmd === "branches" || cmd === "chains")
55
+ return cmdBranches(args);
56
+ if (cmd === "chain")
57
+ return cmdChain(args);
51
58
  if (cmd === "whoami")
52
59
  return cmdWhoami(args);
53
60
  if (cmd === "thread") {
@@ -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
+ }
@@ -2,6 +2,7 @@ import { getMe } from "../api.js";
2
2
  import { loadConfig } from "../config.js";
3
3
  import { CommandError } from "../errors.js";
4
4
  import { ansi } from "../ui/ansi.js";
5
+ import { formatResetLabel, usageBar } from "../ui/usage.js";
5
6
  export async function cmdCredits(args) {
6
7
  const asJson = args.includes("--json");
7
8
  const cfg = await loadConfig();
@@ -11,12 +12,17 @@ export async function cmdCredits(args) {
11
12
  "Try `mergestorm login` or open https://mergestorm.ai/billing");
12
13
  }
13
14
  if (asJson) {
14
- console.log(JSON.stringify(me.usage, null, 2));
15
+ console.log(JSON.stringify({ usage: me.usage, resets_at: me.resets_at ?? null }, null, 2));
15
16
  return;
16
17
  }
17
18
  const s = me.usage.standard;
18
19
  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"));
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))}`);
22
28
  }
@@ -1,2 +1,6 @@
1
+ import { type ThreadDetail } from "../api.js";
2
+ export declare function relativeWhen(iso: string): string;
1
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[];
2
6
  export declare function cmdThread(slug: string, args?: string[]): Promise<void>;
@@ -2,7 +2,7 @@ import { apiFetch, listJobs } from "../api.js";
2
2
  import { loadConfig } from "../config.js";
3
3
  import { CommandError } from "../errors.js";
4
4
  import { ansi } from "../ui/ansi.js";
5
- function relativeWhen(iso) {
5
+ export function relativeWhen(iso) {
6
6
  const t = new Date(iso).getTime();
7
7
  if (Number.isNaN(t))
8
8
  return "—";
@@ -45,7 +45,37 @@ export async function cmdJobs(args) {
45
45
  console.log(ansi.dim(fmt(header)));
46
46
  for (const r of rows)
47
47
  console.log(fmt(r));
48
- console.log(ansi.dim(" full history: https://mergestorm.ai/dashboard/api"));
48
+ console.log(ansi.dim(" branches: `branches` · full history: https://mergestorm.ai/dashboard/api"));
49
+ }
50
+ /** Numbered chain timeline for a thread (branch/PR meta when present). */
51
+ export function formatThreadTimeline(data) {
52
+ const lines = [];
53
+ const title = data.branch?.trim()
54
+ ? `${data.branch} (${data.slug})`
55
+ : data.slug;
56
+ lines.push(` ${ansi.bold(title)} · ${data.job_count} job(s) · ${data.status}`);
57
+ if (data.owner && data.repo) {
58
+ const pr = data.pr_number != null
59
+ ? `https://github.com/${data.owner}/${data.repo}/pull/${data.pr_number}`
60
+ : `${data.owner}/${data.repo}`;
61
+ lines.push(` ${ansi.dim(pr)}`);
62
+ }
63
+ const jobs = [...(data.jobs ?? [])].sort((a, b) => {
64
+ const an = a.thread_job_number ?? Infinity;
65
+ const bn = b.thread_job_number ?? Infinity;
66
+ if (an !== bn)
67
+ return an - bn;
68
+ return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
69
+ });
70
+ if (jobs.length === 0) {
71
+ lines.push(ansi.dim(" (no jobs in this chain yet)"));
72
+ return lines;
73
+ }
74
+ for (const j of jobs) {
75
+ const n = j.thread_job_number != null ? `#${j.thread_job_number}` : "#?";
76
+ lines.push(` ${n.padEnd(4)} ${j.job_id.slice(0, 8)} ${(j.status ?? "-").padEnd(12)} ${(j.verdict ?? "-").padEnd(16)} ${relativeWhen(j.created_at)}`);
77
+ }
78
+ return lines;
49
79
  }
50
80
  export async function cmdThread(slug, args = []) {
51
81
  const asJson = args.includes("--json");
@@ -62,8 +92,16 @@ export async function cmdThread(slug, args = []) {
62
92
  return;
63
93
  }
64
94
  const data = body;
65
- console.log(` Thread ${ansi.bold(data.slug)} · ${data.job_count} job(s)`);
66
- for (const j of data.jobs ?? []) {
67
- console.log(` ${j.job_id.slice(0, 8)} ${j.status.padEnd(12)} ${(j.verdict ?? "-").padEnd(16)} ${relativeWhen(j.created_at)}`);
95
+ for (const line of formatThreadTimeline({
96
+ ...data,
97
+ branch: data.branch ?? null,
98
+ owner: data.owner ?? null,
99
+ repo: data.repo ?? null,
100
+ pr_number: data.pr_number ?? null,
101
+ status: data.status ?? "active",
102
+ pr_linked_at: data.pr_linked_at ?? null,
103
+ jobs: data.jobs ?? [],
104
+ })) {
105
+ console.log(line);
68
106
  }
69
107
  }
@@ -20,6 +20,8 @@ function openBrowser(url) {
20
20
  }
21
21
  }
22
22
  async function loginWithKey(existingRl) {
23
+ if (!existingRl)
24
+ input.resume();
23
25
  const rl = existingRl ?? createInterface({ input, output });
24
26
  const ownRl = !existingRl;
25
27
  try {
@@ -3,4 +3,9 @@ export type ReviewOptions = {
3
3
  /** When true, show Ctrl+C detach hint during poll. */
4
4
  interactive?: boolean;
5
5
  };
6
+ /** Parse `origin` remote into GitHub owner/repo when possible. */
7
+ export declare function parseGithubOriginRepo(): {
8
+ owner: string;
9
+ repo: string;
10
+ } | null;
6
11
  export declare function cmdReview(args: string[], opts?: ReviewOptions): Promise<void>;
@@ -3,6 +3,21 @@ import { loadConfig } from "../config.js";
3
3
  import { CommandError, DetachedError } from "../errors.js";
4
4
  import { collectChangedFiles, collectContextFiles, git } from "../git.js";
5
5
  import { ansi } from "../ui/ansi.js";
6
+ /** Parse `origin` remote into GitHub owner/repo when possible. */
7
+ export function parseGithubOriginRepo() {
8
+ try {
9
+ const url = git(["remote", "get-url", "origin"]).trim();
10
+ if (!url)
11
+ return null;
12
+ const m = url.match(/^(?:https?:\/\/|git@)?github\.com[:/]([A-Za-z0-9_-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/i);
13
+ if (!m?.[1] || !m[2])
14
+ return null;
15
+ return { owner: m[1], repo: m[2] };
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
6
21
  function sleep(ms, signal) {
7
22
  return new Promise((resolve, reject) => {
8
23
  if (signal?.aborted) {
@@ -32,11 +47,14 @@ export async function cmdReview(args, opts = {}) {
32
47
  const context_files = await collectContextFiles(process.cwd());
33
48
  const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]).trim() || "local";
34
49
  const thread = `local/${branch}`.replace(/[^a-zA-Z0-9._/-]/g, "-").slice(0, 120);
50
+ const originRepo = parseGithubOriginRepo();
35
51
  console.log(`Submitting review (${Buffer.byteLength(diff, "utf8")} byte diff, ${files.length} file(s), ${context_files.length} context file(s))…`);
36
52
  const { status, body } = await apiFetch(cfg, "/api/v1/reviews", {
37
53
  method: "POST",
38
54
  json: {
39
55
  thread,
56
+ branch,
57
+ ...(originRepo ? { repo: `${originRepo.owner}/${originRepo.repo}` } : {}),
40
58
  base_label: base,
41
59
  head_label: head,
42
60
  diff,
@@ -90,9 +108,11 @@ export async function cmdReview(args, opts = {}) {
90
108
  if (opts.interactive && meBefore) {
91
109
  const meAfter = await getMe(cfg);
92
110
  if (meAfter) {
93
- const used = Math.max(0, meBefore.usage.standard.remaining - meAfter.usage.standard.remaining);
111
+ const beforeLeft = meBefore.usage.standard.remaining ?? 0;
112
+ const afterLeft = meAfter.usage.standard.remaining ?? 0;
113
+ const used = Math.max(0, beforeLeft - afterLeft);
94
114
  if (used > 0) {
95
- console.log(ansi.dim(`\n${used} credit${used === 1 ? "" : "s"} used · ${meAfter.usage.standard.remaining} remaining`));
115
+ console.log(ansi.dim(`\n${used} credit${used === 1 ? "" : "s"} used · ${afterLeft} remaining`));
96
116
  }
97
117
  }
98
118
  }
package/dist/shell.d.ts CHANGED
@@ -1 +1,8 @@
1
+ import { type CommandSpec } from "./ui/prompt.js";
2
+ export declare const COMMANDS: CommandSpec[];
3
+ /** Strips an optional leading "/" so both "review" and "/review" work. */
4
+ export declare function parseLine(line: string): {
5
+ cmd: string;
6
+ args: string[];
7
+ } | null;
1
8
  export declare function runShell(): Promise<void>;