reasonix 0.12.21 → 0.12.22

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/dist/cli/index.js CHANGED
@@ -762,13 +762,13 @@ async function runHooks(opts) {
762
762
  const matching = opts.hooks.filter((h) => h.event === event && matchesTool(h, toolName));
763
763
  const outcomes = [];
764
764
  let blocked = false;
765
- const stdin4 = `${JSON.stringify(opts.payload)}
765
+ const stdin5 = `${JSON.stringify(opts.payload)}
766
766
  `;
767
767
  for (const hook of matching) {
768
768
  const start = Date.now();
769
769
  const timeoutMs = hook.timeout ?? DEFAULT_TIMEOUTS_MS[event];
770
770
  const cwd2 = hook.cwd ?? opts.payload.cwd;
771
- const raw = await spawner({ command: hook.command, cwd: cwd2, stdin: stdin4, timeoutMs });
771
+ const raw = await spawner({ command: hook.command, cwd: cwd2, stdin: stdin5, timeoutMs });
772
772
  const decision = decideOutcome(event, raw);
773
773
  outcomes.push({
774
774
  hook,
@@ -10879,8 +10879,8 @@ function ModalCard({
10879
10879
  icon,
10880
10880
  children
10881
10881
  }) {
10882
- const { stdout: stdout3 } = useStdout();
10883
- const cols = stdout3?.columns ?? 80;
10882
+ const { stdout: stdout4 } = useStdout();
10883
+ const cols = stdout4?.columns ?? 80;
10884
10884
  const ruleWidth = Math.min(80, Math.max(28, cols - 4));
10885
10885
  const titleText = icon ? ` ${icon} ${title} ` : ` ${title} `;
10886
10886
  return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { color: accent }, "\u2594".repeat(ruleWidth))), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { backgroundColor: accent, color: "black", bold: true }, titleText), subtitle ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, ` ${subtitle}`) : null), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1, flexDirection: "column" }, children), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: accent, dimColor: true }, "\u2581".repeat(ruleWidth))));
@@ -11443,8 +11443,8 @@ function capLines(lines, maxLines, indent) {
11443
11443
  var MODAL_OVERHEAD_ROWS = 18;
11444
11444
  var MIN_DIFF_ROWS = 8;
11445
11445
  function EditConfirm({ block, onChoose }) {
11446
- const { stdout: stdout3 } = useStdout2();
11447
- const rows = stdout3?.rows ?? 40;
11446
+ const { stdout: stdout4 } = useStdout2();
11447
+ const rows = stdout4?.rows ?? 40;
11448
11448
  const budget2 = Math.max(MIN_DIFF_ROWS, rows - MODAL_OVERHEAD_ROWS);
11449
11449
  const allLines = useMemo(
11450
11450
  () => formatEditBlockDiff(block, { contextLines: 2, maxLines: 1e5, indent: " " }),
@@ -12794,8 +12794,8 @@ var EventRow = React11.memo(function EventRow2({
12794
12794
  return /* @__PURE__ */ React11.createElement(Box9, null, /* @__PURE__ */ React11.createElement(Text8, null, event.text));
12795
12795
  });
12796
12796
  function TurnSeparator() {
12797
- const { stdout: stdout3 } = useStdout3();
12798
- const cols = stdout3?.columns ?? 80;
12797
+ const { stdout: stdout4 } = useStdout3();
12798
+ const cols = stdout4?.columns ?? 80;
12799
12799
  const width = Math.max(16, cols - 2);
12800
12800
  const sideWidth = Math.max(2, Math.floor((width - 5) / 2));
12801
12801
  const leftCells = gradientCells(sideWidth, "\u2500");
@@ -12954,8 +12954,8 @@ function ModeStatusBar({
12954
12954
  return /* @__PURE__ */ React12.createElement(ModeBarFrame, null, /* @__PURE__ */ React12.createElement(ModePill, { label, bg, flash }), /* @__PURE__ */ React12.createElement(Text9, { dimColor: true }, ` ${mid} \xB7 Shift+Tab to flip`), jobsTag);
12955
12955
  }
12956
12956
  function ModeBarFrame({ children }) {
12957
- const { stdout: stdout3 } = useStdout4();
12958
- const cols = stdout3?.columns ?? 80;
12957
+ const { stdout: stdout4 } = useStdout4();
12958
+ const cols = stdout4?.columns ?? 80;
12959
12959
  const ruleWidth = Math.max(20, cols - 2);
12960
12960
  return /* @__PURE__ */ React12.createElement(Box10, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Box10, { paddingX: 1 }, /* @__PURE__ */ React12.createElement(Text9, { color: "#475569", dimColor: true }, "\u254C".repeat(ruleWidth))), /* @__PURE__ */ React12.createElement(Box10, { paddingX: 1 }, children));
12961
12961
  }
@@ -13756,8 +13756,8 @@ function PromptInput({
13756
13756
  if (action.historyHandoff === "prev") onHistoryPrev?.();
13757
13757
  if (action.historyHandoff === "next") onHistoryNext?.();
13758
13758
  }, !disabled);
13759
- const { stdout: stdout3 } = useStdout5();
13760
- const cols = stdout3?.columns ?? 80;
13759
+ const { stdout: stdout4 } = useStdout5();
13760
+ const cols = stdout4?.columns ?? 80;
13761
13761
  const narrow = cols <= 90;
13762
13762
  const promptBody = narrow ? "\u203A " : "you \u203A ";
13763
13763
  const promptPrefix = BAR + promptBody;
@@ -14130,8 +14130,8 @@ function StatsPanel({
14130
14130
  const branchOn = (branchBudget ?? 1) > 1;
14131
14131
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model2] ?? DEFAULT_CONTEXT_TOKENS;
14132
14132
  const ctxRatio = summary.lastPromptTokens / ctxMax;
14133
- const { stdout: stdout3 } = useStdout6();
14134
- const columns = stdout3?.columns ?? 80;
14133
+ const { stdout: stdout4 } = useStdout6();
14134
+ const columns = stdout4?.columns ?? 80;
14135
14135
  const narrow = columns < NARROW_BREAKPOINT;
14136
14136
  const coldStart = summary.turns <= COLD_START_TURNS;
14137
14137
  return /* @__PURE__ */ React21.createElement(Box19, { flexDirection: "column", paddingX: 1, marginBottom: 1 }, /* @__PURE__ */ React21.createElement(
@@ -14310,8 +14310,8 @@ function formatTokens(n) {
14310
14310
  import { Box as Box20, Text as Text18, useStdout as useStdout7 } from "ink";
14311
14311
  import React22 from "react";
14312
14312
  function WelcomeBanner({ inCodeMode }) {
14313
- const { stdout: stdout3 } = useStdout7();
14314
- const cols = stdout3?.columns ?? 80;
14313
+ const { stdout: stdout4 } = useStdout7();
14314
+ const cols = stdout4?.columns ?? 80;
14315
14315
  const ruleWidth = Math.min(60, Math.max(28, cols - 4));
14316
14316
  return /* @__PURE__ */ React22.createElement(Box20, { flexDirection: "column", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React22.createElement(GradientRule, { width: ruleWidth }), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { bold: true, color: COLOR.brand }, "\u25C8 welcome"), /* @__PURE__ */ React22.createElement(Text18, { dimColor: true }, " \xB7 type a message to start")), /* @__PURE__ */ React22.createElement(BarRow, null), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { bold: true, color: COLOR.primary }, "quick start")), /* @__PURE__ */ React22.createElement(Hint, { cmd: "/help", desc: "every command + keyboard shortcut" }), /* @__PURE__ */ React22.createElement(Hint, { cmd: "/skill", desc: "invoke a stored playbook" }), inCodeMode ? /* @__PURE__ */ React22.createElement(React22.Fragment, null, /* @__PURE__ */ React22.createElement(Hint, { cmd: "@path", desc: "inline a file in your message" }), /* @__PURE__ */ React22.createElement(Hint, { cmd: "!cmd", desc: "run a shell command, output goes to context" })) : null, /* @__PURE__ */ React22.createElement(Hint, { cmd: "/exit", desc: "quit (Ctrl+C also works)" }), /* @__PURE__ */ React22.createElement(BarRow, null), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { dimColor: true, italic: true }, "tip:"), /* @__PURE__ */ React22.createElement(Text18, { dimColor: true }, " Ctrl+J inserts a newline \xB7 trailing \\ also continues")), /* @__PURE__ */ React22.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(GradientRule, { width: ruleWidth, thin: true })));
14317
14317
  }
@@ -15718,8 +15718,8 @@ ${gitTail(commit2)}` };
15718
15718
  }
15719
15719
  function gitTail(res) {
15720
15720
  const stderr = res.stderr ?? "";
15721
- const stdout3 = res.stdout ?? "";
15722
- const body = stderr.trim() || stdout3.trim();
15721
+ const stdout4 = res.stdout ?? "";
15722
+ const body = stderr.trim() || stdout4.trim();
15723
15723
  if (body) return body;
15724
15724
  if (res.error) return res.error.message;
15725
15725
  return "(no output from git)";
@@ -17689,19 +17689,19 @@ function App({
17689
17689
  }, [busy]);
17690
17690
  const [ongoingTool, setOngoingTool] = useState10(null);
17691
17691
  const [toolProgress, setToolProgress] = useState10(null);
17692
- const { stdout: stdout3 } = useStdout8();
17692
+ const { stdout: stdout4 } = useStdout8();
17693
17693
  useEffect6(() => {
17694
- if (!stdout3 || !stdout3.isTTY) return;
17695
- stdout3.write("\x1B[?2004h");
17696
- stdout3.write("\x1B[>4;2m");
17694
+ if (!stdout4 || !stdout4.isTTY) return;
17695
+ stdout4.write("\x1B[?2004h");
17696
+ stdout4.write("\x1B[>4;2m");
17697
17697
  return () => {
17698
- stdout3.write("\x1B[?2004l");
17699
- stdout3.write("\x1B[>4m");
17698
+ stdout4.write("\x1B[?2004l");
17699
+ stdout4.write("\x1B[>4m");
17700
17700
  };
17701
- }, [stdout3]);
17701
+ }, [stdout4]);
17702
17702
  const [isResizing, setIsResizing] = useState10(false);
17703
17703
  useEffect6(() => {
17704
- if (!stdout3 || !stdout3.isTTY) return;
17704
+ if (!stdout4 || !stdout4.isTTY) return;
17705
17705
  let timer = null;
17706
17706
  const onResize = () => {
17707
17707
  setIsResizing(true);
@@ -17711,12 +17711,12 @@ function App({
17711
17711
  timer = null;
17712
17712
  }, 400);
17713
17713
  };
17714
- stdout3.on("resize", onResize);
17714
+ stdout4.on("resize", onResize);
17715
17715
  return () => {
17716
- stdout3.off("resize", onResize);
17716
+ stdout4.off("resize", onResize);
17717
17717
  if (timer) clearTimeout(timer);
17718
17718
  };
17719
- }, [stdout3]);
17719
+ }, [stdout4]);
17720
17720
  const { activity: subagentActivity, sinkRef: subagentSinkRef } = useSubagent({
17721
17721
  session,
17722
17722
  setHistorical
@@ -19001,7 +19001,7 @@ function App({
19001
19001
  return;
19002
19002
  }
19003
19003
  if (result.clear && result.info) {
19004
- stdout3?.write("\x1B[2J\x1B[3J\x1B[H");
19004
+ stdout4?.write("\x1B[2J\x1B[3J\x1B[H");
19005
19005
  setHistorical([
19006
19006
  {
19007
19007
  id: `sys-${Date.now()}`,
@@ -19018,7 +19018,7 @@ function App({
19018
19018
  return;
19019
19019
  }
19020
19020
  if (result.clear) {
19021
- stdout3?.write("\x1B[2J\x1B[3J\x1B[H");
19021
+ stdout4?.write("\x1B[2J\x1B[3J\x1B[H");
19022
19022
  setHistorical([]);
19023
19023
  if (codeMode) {
19024
19024
  pendingEdits.current = [];
@@ -19601,7 +19601,7 @@ function App({
19601
19601
  refreshModels,
19602
19602
  proArmed,
19603
19603
  persistPlanState,
19604
- stdout3,
19604
+ stdout4,
19605
19605
  stopLoop,
19606
19606
  startLoop,
19607
19607
  getLoopStatus,
@@ -20586,8 +20586,282 @@ async function codeCommand(opts = {}) {
20586
20586
  });
20587
20587
  }
20588
20588
 
20589
+ // src/cli/commands/commit.ts
20590
+ import { spawn as spawn6, spawnSync as spawnSync3 } from "child_process";
20591
+ import { mkdtempSync, readFileSync as readFileSync22, unlinkSync as unlinkSync6, writeFileSync as writeFileSync13 } from "fs";
20592
+ import { tmpdir } from "os";
20593
+ import { join as join21 } from "path";
20594
+ import { stdin as stdin2, stdout } from "process";
20595
+ import { createInterface } from "readline/promises";
20596
+ var DEFAULT_MODEL = "deepseek-v4-flash";
20597
+ var DIFF_BYTE_CAP = 80 * 1024;
20598
+ var LOG_COUNT = 10;
20599
+ var SYSTEM_PROMPT2 = `You draft git commit messages.
20600
+
20601
+ Output ONLY the commit message \u2014 no preamble, no \`\`\` fences, no "Here's a commit message:" lead-in. The first line of your output IS the commit subject.
20602
+
20603
+ Match the project's existing style:
20604
+ - Look at the recent commits provided. Mirror their voice, conventional-commit prefix usage (or absence), tense, length, body structure.
20605
+ - If recent commits use a "type(scope): summary" prefix, use it. If they don't, don't invent one.
20606
+ - Subject line: one line, \u226472 chars, imperative mood, no trailing period.
20607
+ - Body (optional): explain WHY when the diff isn't self-evident. Wrap at ~72 chars. Skip the body for trivial changes \u2014 repeating the subject in the body is noise.
20608
+
20609
+ The diff is the source of truth for what changed; describe THAT, not your guesses about the broader project. If the diff includes a deletion you can't explain from the surrounding context, name it but don't speculate about why.
20610
+
20611
+ No emojis unless the recent commits use them.
20612
+ No co-author trailers, no "Generated with X" footers.`;
20613
+ function runGit(args, opts = {}) {
20614
+ const result = spawnSync3("git", args, {
20615
+ encoding: "utf8",
20616
+ stdio: ["pipe", "pipe", "pipe"],
20617
+ input: opts.input,
20618
+ maxBuffer: 32 * 1024 * 1024
20619
+ });
20620
+ return {
20621
+ stdout: result.stdout ?? "",
20622
+ stderr: result.stderr ?? "",
20623
+ status: result.status
20624
+ };
20625
+ }
20626
+ function dieIfNotGitRepo() {
20627
+ const r = runGit(["rev-parse", "--is-inside-work-tree"]);
20628
+ if (r.status !== 0) {
20629
+ process.stderr.write("reasonix commit: not inside a git repository.\n");
20630
+ process.exit(1);
20631
+ }
20632
+ }
20633
+ function readDiff() {
20634
+ const staged = runGit(["diff", "--staged", "--no-color"]);
20635
+ if (staged.status !== 0) {
20636
+ process.stderr.write(`reasonix commit: git diff --staged failed: ${staged.stderr.trim()}
20637
+ `);
20638
+ process.exit(1);
20639
+ }
20640
+ if (staged.stdout.trim().length > 0) {
20641
+ return capDiff(staged.stdout, "staged");
20642
+ }
20643
+ const wt = runGit(["diff", "--no-color"]);
20644
+ if (wt.stdout.trim().length === 0) {
20645
+ return null;
20646
+ }
20647
+ return capDiff(wt.stdout, "working-tree");
20648
+ }
20649
+ function capDiff(raw, source) {
20650
+ if (raw.length <= DIFF_BYTE_CAP) {
20651
+ return { diff: raw, source, truncated: false };
20652
+ }
20653
+ const head = raw.slice(0, Math.floor(DIFF_BYTE_CAP * 0.7));
20654
+ const tail = raw.slice(-Math.floor(DIFF_BYTE_CAP * 0.3));
20655
+ return {
20656
+ diff: `${head}
20657
+
20658
+ [\u2026 ${raw.length - DIFF_BYTE_CAP} bytes of diff truncated \u2026]
20659
+
20660
+ ${tail}`,
20661
+ source,
20662
+ truncated: true
20663
+ };
20664
+ }
20665
+ function readRecentCommits() {
20666
+ const r = runGit(["log", `-${LOG_COUNT}`, "--no-merges", "--format=%s%n%b%n---END---"]);
20667
+ if (r.status !== 0) {
20668
+ return "";
20669
+ }
20670
+ return r.stdout.trim();
20671
+ }
20672
+ async function draftMessage(client, model2, diff, recentCommits) {
20673
+ const userParts = [];
20674
+ if (recentCommits) {
20675
+ userParts.push(`Recent commits (style reference):
20676
+
20677
+ ${recentCommits}`);
20678
+ }
20679
+ if (diff.source === "working-tree") {
20680
+ userParts.push(
20681
+ "(NOTE: diff is from the working tree, not the staging area \u2014 nothing is staged yet. The user will stage selectively after seeing the draft.)"
20682
+ );
20683
+ }
20684
+ userParts.push(`Diff to summarize:
20685
+
20686
+ ${diff.diff}`);
20687
+ const resp = await client.chat({
20688
+ model: model2,
20689
+ messages: [
20690
+ { role: "system", content: SYSTEM_PROMPT2 },
20691
+ { role: "user", content: userParts.join("\n\n") }
20692
+ ],
20693
+ temperature: 0.2
20694
+ });
20695
+ return stripCodeFences(resp.content.trim());
20696
+ }
20697
+ function stripCodeFences(s) {
20698
+ const trimmed = s.trim();
20699
+ const fenceOpen = /^```[a-zA-Z]*\n/;
20700
+ const fenceClose = /\n?```$/;
20701
+ if (fenceOpen.test(trimmed) && fenceClose.test(trimmed)) {
20702
+ return trimmed.replace(fenceOpen, "").replace(fenceClose, "").trim();
20703
+ }
20704
+ return trimmed;
20705
+ }
20706
+ function printDraft(message) {
20707
+ const sep3 = "\u2500".repeat(60);
20708
+ process.stdout.write(`
20709
+ ${sep3}
20710
+ ${message}
20711
+ ${sep3}
20712
+
20713
+ `);
20714
+ }
20715
+ async function promptChoice() {
20716
+ const rl = createInterface({ input: stdin2, output: stdout });
20717
+ try {
20718
+ const answer = await rl.question("[a]ccept / [r]egenerate / [e]dit / [c]ancel: ");
20719
+ const k = answer.trim().toLowerCase();
20720
+ if (k === "" || k === "a" || k === "y" || k === "yes") return "accept";
20721
+ if (k === "r" || k === "regen" || k === "regenerate") return "regen";
20722
+ if (k === "e" || k === "edit") return "edit";
20723
+ return "cancel";
20724
+ } finally {
20725
+ rl.close();
20726
+ }
20727
+ }
20728
+ function editInExternal(initial) {
20729
+ const editor = process.env.GIT_EDITOR ?? process.env.VISUAL ?? process.env.EDITOR;
20730
+ if (!editor) {
20731
+ process.stderr.write(
20732
+ "reasonix commit: no $EDITOR / $VISUAL / $GIT_EDITOR set \u2014 can't open editor. Pick [a]ccept and `git commit --amend` afterwards.\n"
20733
+ );
20734
+ return null;
20735
+ }
20736
+ const dir = mkdtempSync(join21(tmpdir(), "reasonix-commit-"));
20737
+ const path5 = join21(dir, "COMMIT_EDITMSG");
20738
+ writeFileSync13(path5, initial, "utf8");
20739
+ const result = spawnSync3(`${editor} "${path5}"`, {
20740
+ stdio: "inherit",
20741
+ shell: true
20742
+ });
20743
+ if (result.status !== 0) {
20744
+ try {
20745
+ unlinkSync6(path5);
20746
+ } catch {
20747
+ }
20748
+ process.stderr.write(
20749
+ `reasonix commit: editor exited ${result.status} \u2014 keeping prior draft.
20750
+ `
20751
+ );
20752
+ return null;
20753
+ }
20754
+ let edited;
20755
+ try {
20756
+ edited = readFileSync22(path5, "utf8");
20757
+ } catch {
20758
+ return null;
20759
+ } finally {
20760
+ try {
20761
+ unlinkSync6(path5);
20762
+ } catch {
20763
+ }
20764
+ }
20765
+ const cleaned = edited.split(/\r?\n/).filter((line) => !/^\s*#/.test(line)).join("\n").trim();
20766
+ return cleaned || null;
20767
+ }
20768
+ function commitWithMessage(message) {
20769
+ const child = spawn6("git", ["commit", "-F", "-"], {
20770
+ stdio: ["pipe", "inherit", "inherit"]
20771
+ });
20772
+ child.stdin.write(message);
20773
+ child.stdin.end();
20774
+ child.on("close", (code) => {
20775
+ if (code !== 0) {
20776
+ process.stderr.write(`reasonix commit: git commit exited ${code}.
20777
+ `);
20778
+ process.exit(code ?? 1);
20779
+ }
20780
+ });
20781
+ }
20782
+ async function commitCommand(opts = {}) {
20783
+ loadDotenv();
20784
+ dieIfNotGitRepo();
20785
+ const apiKey = loadApiKey() ?? process.env.DEEPSEEK_API_KEY;
20786
+ if (!apiKey) {
20787
+ process.stderr.write(
20788
+ "reasonix commit: DEEPSEEK_API_KEY not set. Run `reasonix setup` to save one, or export it.\n"
20789
+ );
20790
+ process.exit(1);
20791
+ }
20792
+ const diff = readDiff();
20793
+ if (!diff) {
20794
+ process.stderr.write(
20795
+ "reasonix commit: no staged changes and working tree is clean \u2014 nothing to commit.\n"
20796
+ );
20797
+ process.exit(1);
20798
+ }
20799
+ if (diff.source === "working-tree") {
20800
+ process.stderr.write(
20801
+ "reasonix commit: nothing staged \u2014 drafting from working-tree diff. Stage your changes and re-run, or use the draft as a starting point.\n"
20802
+ );
20803
+ }
20804
+ if (diff.truncated) {
20805
+ process.stderr.write(
20806
+ "reasonix commit: diff exceeded 80KB; head + tail sent to the model. Large diffs often produce vague drafts \u2014 consider committing in smaller chunks.\n"
20807
+ );
20808
+ }
20809
+ const client = new DeepSeekClient({ apiKey });
20810
+ const model2 = opts.model ?? DEFAULT_MODEL;
20811
+ const recentCommits = readRecentCommits();
20812
+ let message = "";
20813
+ let firstPass = true;
20814
+ while (true) {
20815
+ if (firstPass) {
20816
+ process.stdout.write("Drafting commit message\u2026\n");
20817
+ } else {
20818
+ process.stdout.write("Regenerating\u2026\n");
20819
+ }
20820
+ firstPass = false;
20821
+ try {
20822
+ message = await draftMessage(client, model2, diff, recentCommits);
20823
+ } catch (err) {
20824
+ process.stderr.write(`reasonix commit: model call failed \u2014 ${err.message}
20825
+ `);
20826
+ process.exit(1);
20827
+ }
20828
+ if (!message) {
20829
+ process.stderr.write("reasonix commit: model returned an empty draft. Try again.\n");
20830
+ process.exit(1);
20831
+ }
20832
+ printDraft(message);
20833
+ if (opts.yes) break;
20834
+ if (diff.source === "working-tree") {
20835
+ process.stdout.write(
20836
+ "(no staged changes \u2014 draft printed above for you to copy. Stage with `git add` and re-run to commit.)\n"
20837
+ );
20838
+ return;
20839
+ }
20840
+ const choice = await promptChoice();
20841
+ if (choice === "accept") break;
20842
+ if (choice === "cancel") {
20843
+ process.stderr.write("commit cancelled.\n");
20844
+ return;
20845
+ }
20846
+ if (choice === "edit") {
20847
+ const edited = editInExternal(message);
20848
+ if (edited) {
20849
+ message = edited;
20850
+ printDraft(message);
20851
+ const next = await promptChoice();
20852
+ if (next === "accept") break;
20853
+ if (next === "cancel") {
20854
+ process.stderr.write("commit cancelled.\n");
20855
+ return;
20856
+ }
20857
+ }
20858
+ }
20859
+ }
20860
+ commitWithMessage(message);
20861
+ }
20862
+
20589
20863
  // src/cli/commands/diff.ts
20590
- import { writeFileSync as writeFileSync13 } from "fs";
20864
+ import { writeFileSync as writeFileSync14 } from "fs";
20591
20865
  import { basename as basename3 } from "path";
20592
20866
  import { render as render2 } from "ink";
20593
20867
  import React30 from "react";
@@ -20734,7 +21008,7 @@ async function diffCommand(opts) {
20734
21008
  if (wantMarkdown) {
20735
21009
  console.log(renderSummaryTable(report));
20736
21010
  const md = renderMarkdown(report);
20737
- writeFileSync13(opts.mdPath, md, "utf8");
21011
+ writeFileSync14(opts.mdPath, md, "utf8");
20738
21012
  console.log(`
20739
21013
  markdown report written to ${opts.mdPath}`);
20740
21014
  return;
@@ -20753,7 +21027,7 @@ markdown report written to ${opts.mdPath}`);
20753
21027
  // src/cli/commands/doctor.ts
20754
21028
  import { existsSync as existsSync23, statSync as statSync14 } from "fs";
20755
21029
  import { homedir as homedir10 } from "os";
20756
- import { dirname as dirname16, join as join21, resolve as resolve12 } from "path";
21030
+ import { dirname as dirname16, join as join22, resolve as resolve12 } from "path";
20757
21031
  var TTY = process.stdout.isTTY && process.env.TERM !== "dumb";
20758
21032
  function color(text, code) {
20759
21033
  if (!TTY) return text;
@@ -20876,7 +21150,7 @@ async function checkApiReach() {
20876
21150
  }
20877
21151
  async function checkTokenizer() {
20878
21152
  const candidates = [
20879
- join21(
21153
+ join22(
20880
21154
  dirname16(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")),
20881
21155
  "..",
20882
21156
  "..",
@@ -20884,7 +21158,7 @@ async function checkTokenizer() {
20884
21158
  "data",
20885
21159
  "deepseek-tokenizer.json.gz"
20886
21160
  ),
20887
- join21(process.cwd(), "data", "deepseek-tokenizer.json.gz")
21161
+ join22(process.cwd(), "data", "deepseek-tokenizer.json.gz")
20888
21162
  ];
20889
21163
  for (const p of candidates) {
20890
21164
  if (existsSync23(p)) {
@@ -21007,7 +21281,7 @@ async function checkOllama(projectRoot) {
21007
21281
  }
21008
21282
  async function checkProject(projectRoot) {
21009
21283
  const markers = [".git", "REASONIX.md", "package.json", "pyproject.toml", "Cargo.toml", "go.mod"];
21010
- const found = markers.filter((m) => existsSync23(join21(projectRoot, m)));
21284
+ const found = markers.filter((m) => existsSync23(join22(projectRoot, m)));
21011
21285
  if (found.length === 0) {
21012
21286
  return {
21013
21287
  label: "project ",
@@ -21059,8 +21333,8 @@ async function doctorCommand() {
21059
21333
  import { resolve as resolve13 } from "path";
21060
21334
 
21061
21335
  // src/index/semantic/preflight.ts
21062
- import { stdin as stdin2, stdout } from "process";
21063
- import { createInterface } from "readline/promises";
21336
+ import { stdin as stdin3, stdout as stdout2 } from "process";
21337
+ import { createInterface as createInterface2 } from "readline/promises";
21064
21338
  async function ollamaPreflight(opts) {
21065
21339
  const log = opts.log ?? ((line) => process.stderr.write(line));
21066
21340
  const status2 = await checkOllamaStatus(opts.model, opts.baseUrl);
@@ -21118,7 +21392,7 @@ async function ollamaPreflight(opts) {
21118
21392
  }
21119
21393
  async function confirm(question, defaultYes) {
21120
21394
  const suffix = defaultYes ? "[Y/n]" : "[y/N]";
21121
- const rl = createInterface({ input: stdin2, output: stdout });
21395
+ const rl = createInterface2({ input: stdin3, output: stdout2 });
21122
21396
  try {
21123
21397
  const raw = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
21124
21398
  if (raw === "") return defaultYes;
@@ -21537,12 +21811,12 @@ function oneLine2(s, max = 200) {
21537
21811
  }
21538
21812
 
21539
21813
  // src/cli/commands/run.ts
21540
- import { stdin as stdin3, stdout as stdout2 } from "process";
21541
- import { createInterface as createInterface2 } from "readline/promises";
21814
+ import { stdin as stdin4, stdout as stdout3 } from "process";
21815
+ import { createInterface as createInterface3 } from "readline/promises";
21542
21816
  async function ensureApiKey() {
21543
21817
  const existing = loadApiKey();
21544
21818
  if (existing) return existing;
21545
- if (!stdin3.isTTY) {
21819
+ if (!stdin4.isTTY) {
21546
21820
  process.stderr.write(
21547
21821
  "DEEPSEEK_API_KEY is not set and stdin is not a TTY (cannot prompt).\nSet the env var, or run `reasonix chat` once interactively to save a key.\n"
21548
21822
  );
@@ -21551,7 +21825,7 @@ async function ensureApiKey() {
21551
21825
  process.stdout.write(
21552
21826
  "DeepSeek API key not configured.\nGet one at https://platform.deepseek.com/api_keys\n"
21553
21827
  );
21554
- const rl = createInterface2({ input: stdin3, output: stdout2 });
21828
+ const rl = createInterface3({ input: stdin4, output: stdout3 });
21555
21829
  try {
21556
21830
  while (true) {
21557
21831
  const answer = (await rl.question("API key \u203A ")).trim();
@@ -22022,7 +22296,7 @@ async function setupCommand(_opts = {}) {
22022
22296
  }
22023
22297
 
22024
22298
  // src/cli/commands/update.ts
22025
- import { spawn as spawn6 } from "child_process";
22299
+ import { spawn as spawn7 } from "child_process";
22026
22300
  function planUpdate(input) {
22027
22301
  const diff = compareVersions(input.current, input.latest);
22028
22302
  if (diff > 0) {
@@ -22053,7 +22327,7 @@ function planUpdate(input) {
22053
22327
  }
22054
22328
  function defaultSpawn(argv) {
22055
22329
  return new Promise((resolve14, reject) => {
22056
- const child = spawn6(argv[0], argv.slice(1), {
22330
+ const child = spawn7(argv[0], argv.slice(1), {
22057
22331
  stdio: "inherit",
22058
22332
  shell: process.platform === "win32"
22059
22333
  });
@@ -22348,6 +22622,14 @@ program.command("doctor").description(
22348
22622
  ).action(async () => {
22349
22623
  await doctorCommand();
22350
22624
  });
22625
+ program.command("commit").description(
22626
+ "Draft a commit message from the staged diff (or working tree, if nothing staged), matching your repo's recent commit style. Review interactively before it lands."
22627
+ ).option("-m, --model <id>", "Override the default model (deepseek-v4-flash)").option(
22628
+ "-y, --yes",
22629
+ "Skip the [a]ccept / [r]egenerate prompt and commit the first draft. Useful in scripts."
22630
+ ).action(async (opts) => {
22631
+ await commitCommand({ model: opts.model, yes: !!opts.yes });
22632
+ });
22351
22633
  program.command("sessions [name]").description("List saved chat sessions, or inspect one by name.").option("-v, --verbose", "Include system prompts + tool-call metadata when inspecting").action((name, opts) => {
22352
22634
  sessionsCommand({ name, verbose: !!opts.verbose });
22353
22635
  });