hermes-git 0.3.6 → 0.3.8

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 (2) hide show
  1. package/dist/index.js +1542 -672
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14820,15 +14820,15 @@ var require_route = __commonJS((exports, module) => {
14820
14820
  };
14821
14821
  }
14822
14822
  function wrapConversion(toModel, graph) {
14823
- const path4 = [graph[toModel].parent, toModel];
14823
+ const path5 = [graph[toModel].parent, toModel];
14824
14824
  let fn = conversions[graph[toModel].parent][toModel];
14825
14825
  let cur = graph[toModel].parent;
14826
14826
  while (graph[cur].parent) {
14827
- path4.unshift(graph[cur].parent);
14827
+ path5.unshift(graph[cur].parent);
14828
14828
  fn = link(conversions[graph[cur].parent][cur], fn);
14829
14829
  cur = graph[cur].parent;
14830
14830
  }
14831
- fn.conversion = path4;
14831
+ fn.conversion = path5;
14832
14832
  return fn;
14833
14833
  }
14834
14834
  module.exports = function(fromModel) {
@@ -15099,7 +15099,7 @@ var require_wrap_ansi = __commonJS((exports, module) => {
15099
15099
  }
15100
15100
  return words.slice(0, last).join(" ") + words.slice(last).join("");
15101
15101
  };
15102
- var exec2 = (string, columns, options = {}) => {
15102
+ var exec3 = (string, columns, options = {}) => {
15103
15103
  if (options.trim !== false && string.trim() === "") {
15104
15104
  return "";
15105
15105
  }
@@ -15173,7 +15173,7 @@ var require_wrap_ansi = __commonJS((exports, module) => {
15173
15173
  module.exports = (string, columns, options) => {
15174
15174
  return String(string).normalize().replace(/\r\n/g, `
15175
15175
  `).split(`
15176
- `).map((line) => exec2(line, columns, options)).join(`
15176
+ `).map((line) => exec3(line, columns, options)).join(`
15177
15177
  `);
15178
15178
  };
15179
15179
  });
@@ -24156,16 +24156,16 @@ var require_os_tmpdir = __commonJS((exports, module) => {
24156
24156
  var isWindows = process.platform === "win32";
24157
24157
  var trailingSlashRe = isWindows ? /[^:]\\$/ : /.\/$/;
24158
24158
  module.exports = function() {
24159
- var path4;
24159
+ var path5;
24160
24160
  if (isWindows) {
24161
- path4 = process.env.TEMP || process.env.TMP || (process.env.SystemRoot || process.env.windir) + "\\temp";
24161
+ path5 = process.env.TEMP || process.env.TMP || (process.env.SystemRoot || process.env.windir) + "\\temp";
24162
24162
  } else {
24163
- path4 = process.env.TMPDIR || process.env.TMP || process.env.TEMP || "/tmp";
24163
+ path5 = process.env.TMPDIR || process.env.TMP || process.env.TEMP || "/tmp";
24164
24164
  }
24165
- if (trailingSlashRe.test(path4)) {
24166
- path4 = path4.slice(0, -1);
24165
+ if (trailingSlashRe.test(path5)) {
24166
+ path5 = path5.slice(0, -1);
24167
24167
  }
24168
- return path4;
24168
+ return path5;
24169
24169
  };
24170
24170
  });
24171
24171
 
@@ -24179,7 +24179,7 @@ var require_tmp = __commonJS((exports, module) => {
24179
24179
  * MIT Licensed
24180
24180
  */
24181
24181
  var fs = __require("fs");
24182
- var path4 = __require("path");
24182
+ var path5 = __require("path");
24183
24183
  var crypto2 = __require("crypto");
24184
24184
  var osTmpDir = require_os_tmpdir();
24185
24185
  var _c = process.binding("constants");
@@ -24221,7 +24221,7 @@ var require_tmp = __commonJS((exports, module) => {
24221
24221
  }
24222
24222
  function _generateTmpName(opts) {
24223
24223
  if (opts.name) {
24224
- return path4.join(opts.dir || tmpDir, opts.name);
24224
+ return path5.join(opts.dir || tmpDir, opts.name);
24225
24225
  }
24226
24226
  if (opts.template) {
24227
24227
  return opts.template.replace(TEMPLATE_PATTERN, _randomChars(6));
@@ -24232,7 +24232,7 @@ var require_tmp = __commonJS((exports, module) => {
24232
24232
  _randomChars(12),
24233
24233
  opts.postfix || ""
24234
24234
  ].join("");
24235
- return path4.join(opts.dir || tmpDir, name);
24235
+ return path5.join(opts.dir || tmpDir, name);
24236
24236
  }
24237
24237
  function tmpName(options, callback) {
24238
24238
  var args = _parseArguments(options, callback), opts = args[0], cb = args[1], tries = opts.name ? 1 : opts.tries || DEFAULT_TRIES;
@@ -24320,7 +24320,7 @@ var require_tmp = __commonJS((exports, module) => {
24320
24320
  do {
24321
24321
  var dir2 = dirs.pop(), deferred = false, files = fs.readdirSync(dir2);
24322
24322
  for (var i = 0, length = files.length;i < length; i++) {
24323
- var file2 = path4.join(dir2, files[i]), stat = fs.lstatSync(file2);
24323
+ var file2 = path5.join(dir2, files[i]), stat = fs.lstatSync(file2);
24324
24324
  if (stat.isDirectory()) {
24325
24325
  if (!deferred) {
24326
24326
  deferred = true;
@@ -34338,6 +34338,13 @@ var chalk = createChalk();
34338
34338
  var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
34339
34339
  var source_default = chalk;
34340
34340
 
34341
+ // src/lib/ai.ts
34342
+ import { exec } from "child_process";
34343
+ import { promisify } from "util";
34344
+ import { mkdtemp, readFile as readFile2, rm } from "fs/promises";
34345
+ import { tmpdir } from "os";
34346
+ import path4 from "path";
34347
+
34341
34348
  // src/lib/env.ts
34342
34349
  import { readFile, writeFile, mkdir, chmod } from "fs/promises";
34343
34350
  import { existsSync } from "fs";
@@ -34420,7 +34427,7 @@ async function getResolvedEnv() {
34420
34427
  return { value: global.geminiApiKey, source: "global-config" };
34421
34428
  return { value: null, source: null };
34422
34429
  })();
34423
- const validProviders = ["anthropic", "openai", "gemini"];
34430
+ const validProviders = ["anthropic", "openai", "gemini", "claude-code", "codex"];
34424
34431
  const provider = validProviders.includes(rawProvider.value) ? rawProvider.value : null;
34425
34432
  _resolved = {
34426
34433
  provider,
@@ -34453,6 +34460,7 @@ function formatSource(source) {
34453
34460
  }
34454
34461
 
34455
34462
  // src/lib/ai.ts
34463
+ var execAsync = promisify(exec);
34456
34464
  async function makeAnthropicProvider(apiKey) {
34457
34465
  const { default: Anthropic2 } = await Promise.resolve().then(() => (init_sdk(), exports_sdk));
34458
34466
  const client = new Anthropic2({ apiKey });
@@ -34517,6 +34525,70 @@ async function makeGeminiProvider(apiKey) {
34517
34525
  }
34518
34526
  };
34519
34527
  }
34528
+ async function makeClaudeCodeProvider() {
34529
+ try {
34530
+ await execAsync("claude --version", { timeout: 5000 });
34531
+ } catch {
34532
+ throw new Error("claude CLI not found — install Claude Code first");
34533
+ }
34534
+ return {
34535
+ name: "Claude Code",
34536
+ model: "claude (via CLI)",
34537
+ async complete(prompt) {
34538
+ const env3 = { ...process.env };
34539
+ delete env3.CLAUDECODE;
34540
+ const escaped = prompt.replace(/'/g, `'"'"'`);
34541
+ const { stdout, stderr } = await execAsync(`claude -p '${escaped}' --no-session-persistence --tools ""`, { env: env3, timeout: 60000 });
34542
+ const text = (stdout || stderr).trim();
34543
+ if (!text)
34544
+ throw new Error("Empty response from Claude Code CLI");
34545
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
34546
+ }
34547
+ };
34548
+ }
34549
+ async function makeCodexProvider() {
34550
+ try {
34551
+ await execAsync("codex --version", { timeout: 5000 });
34552
+ } catch {
34553
+ throw new Error("codex CLI not found — install OpenAI Codex first");
34554
+ }
34555
+ return {
34556
+ name: "Codex",
34557
+ model: "codex (via CLI)",
34558
+ async complete(prompt) {
34559
+ const tmpDir = await mkdtemp(path4.join(tmpdir(), "hermes-"));
34560
+ const promptFile = path4.join(tmpDir, "prompt.txt");
34561
+ const outputFile = path4.join(tmpDir, "output.txt");
34562
+ try {
34563
+ const { writeFile: writeFile2 } = await import("fs/promises");
34564
+ await writeFile2(promptFile, prompt, "utf-8");
34565
+ await execAsync(`codex exec --color never -o "${outputFile}" - < "${promptFile}"`, { timeout: 60000 });
34566
+ const text = (await readFile2(outputFile, "utf-8")).trim();
34567
+ if (!text)
34568
+ throw new Error("Empty response from Codex CLI");
34569
+ return text;
34570
+ } finally {
34571
+ await rm(tmpDir, { recursive: true, force: true });
34572
+ }
34573
+ }
34574
+ };
34575
+ }
34576
+ async function isCodexAvailable() {
34577
+ try {
34578
+ await execAsync("codex --version", { timeout: 3000 });
34579
+ return true;
34580
+ } catch {
34581
+ return false;
34582
+ }
34583
+ }
34584
+ async function isClaudeCodeAvailable() {
34585
+ try {
34586
+ await execAsync("claude --version", { timeout: 3000 });
34587
+ return true;
34588
+ } catch {
34589
+ return false;
34590
+ }
34591
+ }
34520
34592
  var _provider = null;
34521
34593
  async function getProvider() {
34522
34594
  if (_provider)
@@ -34538,7 +34610,9 @@ async function getProvider() {
34538
34610
  if (!env3.geminiApiKey)
34539
34611
  throw new MissingKeyError("gemini");
34540
34612
  return makeGeminiProvider(env3.geminiApiKey);
34541
- }
34613
+ },
34614
+ "claude-code": () => makeClaudeCodeProvider(),
34615
+ codex: () => makeCodexProvider()
34542
34616
  };
34543
34617
  _provider = await factories[env3.provider]();
34544
34618
  return _provider;
@@ -34555,6 +34629,14 @@ async function getProvider() {
34555
34629
  _provider = await makeGeminiProvider(env3.geminiApiKey);
34556
34630
  return _provider;
34557
34631
  }
34632
+ if (await isClaudeCodeAvailable()) {
34633
+ _provider = await makeClaudeCodeProvider();
34634
+ return _provider;
34635
+ }
34636
+ if (await isCodexAvailable()) {
34637
+ _provider = await makeCodexProvider();
34638
+ return _provider;
34639
+ }
34558
34640
  throw new NoProviderError;
34559
34641
  }
34560
34642
 
@@ -34613,15 +34695,18 @@ class NoProviderError extends Error {
34613
34695
  constructor() {
34614
34696
  super([
34615
34697
  "",
34616
- source_default.red(" No AI provider configured"),
34698
+ source_default.red(" No AI provider configured"),
34617
34699
  "",
34618
- source_default.bold("Set one of these environment variables:"),
34700
+ source_default.bold(" Option 1 use a local AI CLI (no API key needed):"),
34701
+ source_default.dim(" Claude Code: ") + source_default.cyan("hermes config set provider claude-code"),
34702
+ source_default.dim(" Codex CLI: ") + source_default.cyan("hermes config set provider codex"),
34619
34703
  "",
34620
- source_default.white(" Anthropic ") + source_default.dim('export ANTHROPIC_API_KEY="sk-ant-..."') + source_default.dim(" → console.anthropic.com"),
34621
- source_default.white(" OpenAI ") + source_default.dim('export OPENAI_API_KEY="sk-..." ') + source_default.dim("platform.openai.com/api-keys"),
34622
- source_default.white(" Gemini ") + source_default.dim('export GEMINI_API_KEY="AIza..." ') + source_default.dim(" aistudio.google.com/app/apikey"),
34704
+ source_default.bold(" Option 2 set an API key:"),
34705
+ source_default.white(" Anthropic ") + source_default.dim('export ANTHROPIC_API_KEY="sk-ant-..." → console.anthropic.com'),
34706
+ source_default.white(" OpenAI ") + source_default.dim('export OPENAI_API_KEY="sk-..." platform.openai.com/api-keys'),
34707
+ source_default.white(" Gemini ") + source_default.dim('export GEMINI_API_KEY="AIza..." → aistudio.google.com/app/apikey'),
34623
34708
  "",
34624
- source_default.dim("Or pin a provider: export HERMES_PROVIDER=anthropic|openai|gemini"),
34709
+ source_default.dim(" Or run: hermes config setup"),
34625
34710
  ""
34626
34711
  ].join(`
34627
34712
  `));
@@ -34671,6 +34756,24 @@ Ensure all Git commands are:
34671
34756
  }
34672
34757
  return response;
34673
34758
  }
34759
+ async function getAllAvailableProviders() {
34760
+ const env3 = await getResolvedEnv();
34761
+ const providers = [];
34762
+ if (env3.anthropicApiKey) {
34763
+ providers.push(await makeAnthropicProvider(env3.anthropicApiKey));
34764
+ } else if (await isClaudeCodeAvailable()) {
34765
+ providers.push(await makeClaudeCodeProvider());
34766
+ }
34767
+ if (env3.openaiApiKey) {
34768
+ providers.push(await makeOpenAIProvider(env3.openaiApiKey));
34769
+ } else if (await isCodexAvailable()) {
34770
+ providers.push(await makeCodexProvider());
34771
+ }
34772
+ if (env3.geminiApiKey) {
34773
+ providers.push(await makeGeminiProvider(env3.geminiApiKey));
34774
+ }
34775
+ return providers;
34776
+ }
34674
34777
  async function getActiveProvider() {
34675
34778
  const provider = await getProvider();
34676
34779
  return { name: provider.name, model: provider.model };
@@ -34697,9 +34800,9 @@ function stripMarkdownCodeBlock(text) {
34697
34800
  }
34698
34801
 
34699
34802
  // src/lib/git.ts
34700
- import { exec } from "child_process";
34701
- import { promisify } from "util";
34702
- var execAsync = promisify(exec);
34803
+ import { exec as exec2 } from "child_process";
34804
+ import { promisify as promisify2 } from "util";
34805
+ var execAsync2 = promisify2(exec2);
34703
34806
  async function getRepoState() {
34704
34807
  try {
34705
34808
  const [
@@ -34710,12 +34813,12 @@ async function getRepoState() {
34710
34813
  mergeStatus,
34711
34814
  cherryPickStatus
34712
34815
  ] = await Promise.all([
34713
- execAsync("git rev-parse --abbrev-ref HEAD").then((r) => r.stdout.trim()),
34714
- execAsync("git status --porcelain").then((r) => r.stdout.trim()),
34715
- execAsync("git rev-parse --abbrev-ref @{upstream}").then((r) => r.stdout.trim()).catch(() => null),
34716
- execAsync("git rev-parse --git-dir").then((r) => execAsync(`test -d ${r.stdout.trim()}/rebase-merge`)).then(() => true).catch(() => false),
34717
- execAsync("git rev-parse --git-dir").then((r) => execAsync(`test -f ${r.stdout.trim()}/MERGE_HEAD`)).then(() => true).catch(() => false),
34718
- execAsync("git rev-parse --git-dir").then((r) => execAsync(`test -f ${r.stdout.trim()}/CHERRY_PICK_HEAD`)).then(() => true).catch(() => false)
34816
+ execAsync2("git rev-parse --abbrev-ref HEAD").then((r) => r.stdout.trim()).catch(() => execAsync2("git symbolic-ref --short HEAD").then((r) => r.stdout.trim()).catch(() => "main")),
34817
+ execAsync2("git status --porcelain").then((r) => r.stdout.trim()),
34818
+ execAsync2("git rev-parse --abbrev-ref @{upstream}").then((r) => r.stdout.trim()).catch(() => null),
34819
+ execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -d ${r.stdout.trim()}/rebase-merge`)).then(() => true).catch(() => false),
34820
+ execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -f ${r.stdout.trim()}/MERGE_HEAD`)).then(() => true).catch(() => false),
34821
+ execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -f ${r.stdout.trim()}/CHERRY_PICK_HEAD`)).then(() => true).catch(() => false)
34719
34822
  ]);
34720
34823
  const statusLines = status.split(`
34721
34824
  `).filter((line) => line);
@@ -34724,7 +34827,7 @@ async function getRepoState() {
34724
34827
  let ahead = 0;
34725
34828
  let behind = 0;
34726
34829
  if (tracking) {
34727
- const aheadBehind = await execAsync(`git rev-list --left-right --count ${tracking}...HEAD`).then((r) => r.stdout.trim().split("\t")).catch(() => ["0", "0"]);
34830
+ const aheadBehind = await execAsync2(`git rev-list --left-right --count ${tracking}...HEAD`).then((r) => r.stdout.trim().split("\t")).catch(() => ["0", "0"]);
34728
34831
  behind = parseInt(aheadBehind[0], 10);
34729
34832
  ahead = parseInt(aheadBehind[1], 10);
34730
34833
  }
@@ -34746,7 +34849,7 @@ async function getRepoState() {
34746
34849
  }
34747
34850
  async function isGitInstalled() {
34748
34851
  try {
34749
- await execAsync("git --version");
34852
+ await execAsync2("git --version");
34750
34853
  return true;
34751
34854
  } catch {
34752
34855
  return false;
@@ -34754,7 +34857,7 @@ async function isGitInstalled() {
34754
34857
  }
34755
34858
  async function isGitRepository() {
34756
34859
  try {
34757
- await execAsync("git rev-parse --git-dir");
34860
+ await execAsync2("git rev-parse --git-dir");
34758
34861
  return true;
34759
34862
  } catch {
34760
34863
  return false;
@@ -34762,7 +34865,7 @@ async function isGitRepository() {
34762
34865
  }
34763
34866
  async function hasCommits() {
34764
34867
  try {
34765
- await execAsync("git rev-parse HEAD");
34868
+ await execAsync2("git rev-parse HEAD");
34766
34869
  return true;
34767
34870
  } catch {
34768
34871
  return false;
@@ -34770,7 +34873,7 @@ async function hasCommits() {
34770
34873
  }
34771
34874
  async function executeGitCommand(command) {
34772
34875
  try {
34773
- const { stdout, stderr } = await execAsync(command);
34876
+ const { stdout, stderr } = await execAsync2(command);
34774
34877
  return stdout || stderr;
34775
34878
  } catch (error3) {
34776
34879
  throw new Error(`Git command failed: ${error3.message}`);
@@ -34778,7 +34881,7 @@ async function executeGitCommand(command) {
34778
34881
  }
34779
34882
  async function getConflictedFiles() {
34780
34883
  try {
34781
- const { stdout } = await execAsync("git diff --name-only --diff-filter=U");
34884
+ const { stdout } = await execAsync2("git diff --name-only --diff-filter=U");
34782
34885
  return stdout.trim().split(`
34783
34886
  `).filter((line) => line);
34784
34887
  } catch {
@@ -34833,494 +34936,187 @@ function displayConflictExplanation(explanation, files) {
34833
34936
  console.log(explanation);
34834
34937
  }
34835
34938
 
34836
- // src/commands/plan.ts
34837
- function planCommand(program2) {
34838
- program2.command("plan").description("Analyze the current repository state and propose a safe Git plan").argument("<intent>", "What you want to achieve").action(async (intent) => {
34839
- try {
34840
- const { name, model } = await getActiveProvider();
34841
- console.log(`\uD83D\uDD0D Analyzing repository state... ${source_default.dim(`[${name} / ${model}]`)}
34842
- `);
34843
- const repoState = await getRepoState();
34844
- const analysis = await analyzeGitState(repoState, intent);
34845
- displayPlan(analysis, repoState);
34846
- console.log(source_default.dim(`
34847
- \uD83D\uDCA1 No changes have been made. Review the plan above.`));
34848
- } catch (error3) {
34849
- console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
34850
- process.exit(1);
34851
- }
34852
- });
34853
- }
34854
-
34855
- // src/lib/config.ts
34856
- import { readFile as readFile2 } from "fs/promises";
34857
- import { existsSync as existsSync2 } from "fs";
34858
- async function loadConfig() {
34859
- const configPath = ".hermes/config.json";
34860
- if (!existsSync2(configPath)) {
34861
- return null;
34862
- }
34863
- try {
34864
- const content = await readFile2(configPath, "utf-8");
34865
- return JSON.parse(content);
34866
- } catch (error3) {
34867
- console.warn("⚠️ Could not load .hermes/config.json");
34868
- return null;
34939
+ // node_modules/@inquirer/core/dist/esm/lib/key.mjs
34940
+ var isUpKey = (key) => key.name === "up" || key.name === "k" || key.ctrl && key.name === "p";
34941
+ var isDownKey = (key) => key.name === "down" || key.name === "j" || key.ctrl && key.name === "n";
34942
+ var isSpaceKey = (key) => key.name === "space";
34943
+ var isBackspaceKey = (key) => key.name === "backspace";
34944
+ var isNumberKey = (key) => "123456789".includes(key.name);
34945
+ var isEnterKey = (key) => key.name === "enter" || key.name === "return";
34946
+ // node_modules/@inquirer/core/dist/esm/lib/errors.mjs
34947
+ class AbortPromptError extends Error {
34948
+ name = "AbortPromptError";
34949
+ message = "Prompt was aborted";
34950
+ constructor(options) {
34951
+ super();
34952
+ this.cause = options?.cause;
34869
34953
  }
34870
34954
  }
34871
- function generateBranchName(pattern, description, ticket) {
34872
- let branchName = pattern;
34873
- branchName = branchName.replace("{description}", slugify(description));
34874
- branchName = branchName.replace("{ticket}", ticket || "");
34875
- branchName = branchName.replace(/\/{2,}/g, "/");
34876
- branchName = branchName.replace(/\/$/, "");
34877
- return branchName;
34955
+
34956
+ class CancelPromptError extends Error {
34957
+ name = "CancelPromptError";
34958
+ message = "Prompt was canceled";
34878
34959
  }
34879
- function slugify(text) {
34880
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
34960
+
34961
+ class ExitPromptError extends Error {
34962
+ name = "ExitPromptError";
34881
34963
  }
34882
34964
 
34883
- // src/lib/stats.ts
34884
- import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
34885
- import { existsSync as existsSync3 } from "fs";
34886
- var STATS_FILE = ".hermes/stats.json";
34887
- var MAX_HISTORY = 1000;
34888
- async function loadStats() {
34889
- if (!existsSync3(STATS_FILE)) {
34890
- return createEmptyStats();
34891
- }
34892
- try {
34893
- const content = await readFile3(STATS_FILE, "utf-8");
34894
- return JSON.parse(content);
34895
- } catch {
34896
- return createEmptyStats();
34897
- }
34965
+ class HookError extends Error {
34966
+ name = "HookError";
34898
34967
  }
34899
- async function saveStats(stats) {
34900
- try {
34901
- await mkdir2(".hermes", { recursive: true });
34902
- if (stats.commandHistory.length > MAX_HISTORY) {
34903
- stats.commandHistory = stats.commandHistory.slice(-MAX_HISTORY);
34904
- }
34905
- await writeFile2(STATS_FILE, JSON.stringify(stats, null, 2));
34906
- } catch (error3) {}
34968
+
34969
+ class ValidationError extends Error {
34970
+ name = "ValidationError";
34907
34971
  }
34908
- async function recordCommand(command, args, duration, success, gitCommandsRun = 0) {
34909
- const stats = await loadStats();
34910
- const entry = {
34911
- timestamp: Date.now(),
34912
- command,
34913
- args,
34914
- duration,
34915
- success,
34916
- gitCommandsRun
34972
+ // node_modules/@inquirer/core/dist/esm/lib/use-prefix.mjs
34973
+ import { AsyncResource as AsyncResource2 } from "node:async_hooks";
34974
+
34975
+ // node_modules/@inquirer/core/dist/esm/lib/hook-engine.mjs
34976
+ import { AsyncLocalStorage, AsyncResource } from "node:async_hooks";
34977
+ var hookStorage = new AsyncLocalStorage;
34978
+ function createStore(rl) {
34979
+ const store = {
34980
+ rl,
34981
+ hooks: [],
34982
+ hooksCleanup: [],
34983
+ hooksEffect: [],
34984
+ index: 0,
34985
+ handleChange() {}
34917
34986
  };
34918
- stats.totalCommands++;
34919
- stats.totalGitCommands += gitCommandsRun;
34920
- stats.lastUsed = Date.now();
34921
- stats.commandHistory.push(entry);
34922
- await saveStats(stats);
34987
+ return store;
34923
34988
  }
34924
- async function getStatsSummary(days = 30) {
34925
- const stats = await loadStats();
34926
- const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
34927
- const recentEntries = stats.commandHistory.filter((e) => e.timestamp >= cutoff);
34928
- const commandCounts = {};
34929
- let successCount = 0;
34930
- recentEntries.forEach((entry) => {
34931
- commandCounts[entry.command] = (commandCounts[entry.command] || 0) + 1;
34932
- if (entry.success)
34933
- successCount++;
34989
+ function withHooks(rl, cb) {
34990
+ const store = createStore(rl);
34991
+ return hookStorage.run(store, () => {
34992
+ function cycle(render) {
34993
+ store.handleChange = () => {
34994
+ store.index = 0;
34995
+ render();
34996
+ };
34997
+ store.handleChange();
34998
+ }
34999
+ return cb(cycle);
34934
35000
  });
34935
- const topCommands = Object.entries(commandCounts).sort(([, a], [, b]) => b - a).slice(0, 5).map(([cmd, count]) => ({ command: cmd, count }));
34936
- const totalDays = Math.max(1, Math.ceil((Date.now() - stats.startDate) / (24 * 60 * 60 * 1000)));
34937
- return {
34938
- totalCommands: recentEntries.length,
34939
- allTimeCommands: stats.totalCommands,
34940
- gitCommandsRun: recentEntries.reduce((sum, e) => sum + e.gitCommandsRun, 0),
34941
- allTimeGitCommands: stats.totalGitCommands,
34942
- successRate: recentEntries.length ? successCount / recentEntries.length : 0,
34943
- topCommands,
34944
- daysActive: totalDays,
34945
- commandsPerDay: stats.totalCommands / totalDays
35001
+ }
35002
+ function getStore() {
35003
+ const store = hookStorage.getStore();
35004
+ if (!store) {
35005
+ throw new HookError("[Inquirer] Hook functions can only be called from within a prompt");
35006
+ }
35007
+ return store;
35008
+ }
35009
+ function readline() {
35010
+ return getStore().rl;
35011
+ }
35012
+ function withUpdates(fn) {
35013
+ const wrapped = (...args) => {
35014
+ const store = getStore();
35015
+ let shouldUpdate = false;
35016
+ const oldHandleChange = store.handleChange;
35017
+ store.handleChange = () => {
35018
+ shouldUpdate = true;
35019
+ };
35020
+ const returnValue = fn(...args);
35021
+ if (shouldUpdate) {
35022
+ oldHandleChange();
35023
+ }
35024
+ store.handleChange = oldHandleChange;
35025
+ return returnValue;
34946
35026
  };
35027
+ return AsyncResource.bind(wrapped);
34947
35028
  }
34948
- function createEmptyStats() {
34949
- return {
34950
- totalCommands: 0,
34951
- totalGitCommands: 0,
34952
- commandHistory: [],
34953
- startDate: Date.now(),
34954
- lastUsed: Date.now()
35029
+ function withPointer(cb) {
35030
+ const store = getStore();
35031
+ const { index } = store;
35032
+ const pointer = {
35033
+ get() {
35034
+ return store.hooks[index];
35035
+ },
35036
+ set(value) {
35037
+ store.hooks[index] = value;
35038
+ },
35039
+ initialized: index in store.hooks
34955
35040
  };
35041
+ const returnValue = cb(pointer);
35042
+ store.index++;
35043
+ return returnValue;
34956
35044
  }
34957
-
34958
- // src/commands/start.ts
34959
- function startCommand(program2) {
34960
- program2.command("start").description("Start a new piece of work safely").argument("<task>", "Description of the task").action(async (task) => {
34961
- const startTime = Date.now();
34962
- let gitCommandsRun = 0;
34963
- try {
34964
- console.log(`\uD83D\uDE80 Starting new task...
34965
- `);
34966
- const config = await loadConfig();
34967
- const repoState = await getRepoState();
34968
- let suggestedBranchName;
34969
- if (config) {
34970
- suggestedBranchName = generateBranchName(config.branches.featurePattern, task);
34971
- console.log(`\uD83D\uDCA1 Suggested branch: ${suggestedBranchName}
34972
- `);
34973
- }
34974
- const planResponse = await getGitPlan(repoState, `Start working on: ${task}. ${suggestedBranchName ? `Suggested branch name: ${suggestedBranchName}.` : ""} Provide base branch, conventional branch name, and Git commands to create and switch to the branch.`);
34975
- let plan;
34976
- try {
34977
- plan = JSON.parse(planResponse);
34978
- } catch {
34979
- console.log(`\uD83D\uDCAD Hermes suggests:
34980
- `);
34981
- console.log(planResponse);
34982
- console.log(`
34983
- ⚠️ Could not auto-execute. Please review the plan above.`);
34984
- return;
34985
- }
34986
- if (plan.baseBranch && plan.branchName) {
34987
- console.log(`\uD83D\uDCCD Base branch: ${plan.baseBranch}`);
34988
- console.log(`\uD83C\uDF3F New branch: ${plan.branchName}
34989
- `);
34990
- }
34991
- if (plan.explanation) {
34992
- console.log(`\uD83D\uDCAD ${plan.explanation}
34993
- `);
34994
- }
34995
- if (plan.commands && Array.isArray(plan.commands)) {
34996
- for (const command of plan.commands) {
34997
- let cmdString;
34998
- if (typeof command === "string") {
34999
- cmdString = command;
35000
- } else if (typeof command === "object" && command.command) {
35001
- cmdString = command.command;
35002
- } else if (typeof command === "object" && command.cmd) {
35003
- cmdString = command.cmd;
35004
- } else {
35005
- console.warn("⚠️ Skipping invalid command:", command);
35006
- continue;
35007
- }
35008
- validateGitCommand(cmdString);
35009
- displayStep(cmdString);
35010
- await executeGitCommand(cmdString);
35011
- gitCommandsRun++;
35012
- }
35013
- const branchName = plan.branchName || "new branch";
35014
- displaySuccess(`Successfully created and switched to ${branchName}`);
35015
- if (config?.preferences.learningMode) {
35016
- console.log(`
35017
- \uD83D\uDCA1 Learning tip: Branch created from clean state ensures no unexpected commits`);
35018
- }
35019
- } else {
35020
- console.log("⚠️ No commands to execute. See analysis above.");
35021
- }
35022
- const duration = (Date.now() - startTime) / 1000;
35023
- await recordCommand("start", [task], duration, true, gitCommandsRun);
35024
- } catch (error3) {
35025
- const duration = (Date.now() - startTime) / 1000;
35026
- await recordCommand("start", [task], duration, false, gitCommandsRun);
35027
- console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
35028
- process.exit(1);
35029
- }
35030
- });
35045
+ function handleChange() {
35046
+ getStore().handleChange();
35031
35047
  }
35032
-
35033
- // src/commands/wip.ts
35034
- function wipCommand(program2) {
35035
- program2.command("wip").description("Save work safely when things get messy").option("-m, --message <message>", "Custom WIP message").action(async (options) => {
35036
- try {
35037
- console.log(`\uD83D\uDCBE Saving work in progress...
35038
- `);
35039
- const repoState = await getRepoState();
35040
- const messageNote = options.message ? ` with message: "${options.message}"` : "";
35041
- const planResponse = await getGitPlan(repoState, `Save work in progress${messageNote}. Decide whether to commit or stash. Return JSON with: approach, commands[], explanation.`);
35042
- let plan;
35043
- try {
35044
- plan = JSON.parse(planResponse);
35045
- } catch {
35046
- console.log(`\uD83D\uDCAD Hermes suggests:
35047
- `);
35048
- console.log(planResponse);
35049
- console.log(`
35050
- ⚠️ Could not auto-execute. Please review the plan above.`);
35051
- return;
35052
- }
35053
- if (plan.explanation) {
35054
- console.log(`\uD83D\uDCAD ${plan.explanation}
35055
- `);
35048
+ var effectScheduler = {
35049
+ queue(cb) {
35050
+ const store = getStore();
35051
+ const { index } = store;
35052
+ store.hooksEffect.push(() => {
35053
+ store.hooksCleanup[index]?.();
35054
+ const cleanFn = cb(readline());
35055
+ if (cleanFn != null && typeof cleanFn !== "function") {
35056
+ throw new ValidationError("useEffect return value must be a cleanup function or nothing.");
35056
35057
  }
35057
- if (plan.commands && Array.isArray(plan.commands)) {
35058
- for (const command of plan.commands) {
35059
- let cmdString;
35060
- if (typeof command === "string") {
35061
- cmdString = command;
35062
- } else if (typeof command === "object" && command.command) {
35063
- cmdString = command.command;
35064
- } else if (typeof command === "object" && command.cmd) {
35065
- cmdString = command.cmd;
35066
- } else {
35067
- console.warn("⚠️ Skipping invalid command:", command);
35068
- continue;
35069
- }
35070
- validateGitCommand(cmdString);
35071
- displayStep(cmdString);
35072
- await executeGitCommand(cmdString);
35073
- }
35074
- const approach = plan.approach || "selected method";
35075
- displaySuccess(`Work saved using ${approach}`);
35076
- } else {
35077
- console.log("⚠️ No commands to execute.");
35058
+ store.hooksCleanup[index] = cleanFn;
35059
+ });
35060
+ },
35061
+ run() {
35062
+ const store = getStore();
35063
+ withUpdates(() => {
35064
+ store.hooksEffect.forEach((effect) => {
35065
+ effect();
35066
+ });
35067
+ store.hooksEffect.length = 0;
35068
+ })();
35069
+ },
35070
+ clearAll() {
35071
+ const store = getStore();
35072
+ store.hooksCleanup.forEach((cleanFn) => {
35073
+ cleanFn?.();
35074
+ });
35075
+ store.hooksEffect.length = 0;
35076
+ store.hooksCleanup.length = 0;
35077
+ }
35078
+ };
35079
+
35080
+ // node_modules/@inquirer/core/dist/esm/lib/use-state.mjs
35081
+ function useState(defaultValue) {
35082
+ return withPointer((pointer) => {
35083
+ const setFn = (newValue) => {
35084
+ if (pointer.get() !== newValue) {
35085
+ pointer.set(newValue);
35086
+ handleChange();
35078
35087
  }
35079
- } catch (error3) {
35080
- console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
35081
- process.exit(1);
35088
+ };
35089
+ if (pointer.initialized) {
35090
+ return [pointer.get(), setFn];
35082
35091
  }
35092
+ const value = typeof defaultValue === "function" ? defaultValue() : defaultValue;
35093
+ pointer.set(value);
35094
+ return [value, setFn];
35083
35095
  });
35084
35096
  }
35085
35097
 
35086
- // src/commands/sync.ts
35087
- function syncCommand(program2) {
35088
- program2.command("sync").description("Bring your branch up to date safely").option("--from <branch>", "Source branch to sync from (default: main)").action(async (options) => {
35089
- try {
35090
- console.log(`\uD83D\uDD04 Syncing branch...
35091
- `);
35092
- const repoState = await getRepoState();
35093
- const fromNote = options.from ? ` from ${options.from}` : " from main";
35094
- const planResponse = await getGitPlan(repoState, `Sync branch${fromNote}. Evaluate if rebase or merge is safer. Check if branch is shared. Return JSON with: approach, isRisky, riskExplanation, commands[], explanation.`);
35095
- let plan;
35096
- try {
35097
- plan = JSON.parse(planResponse);
35098
- } catch {
35099
- console.log(`\uD83D\uDCAD Hermes suggests:
35100
- `);
35101
- console.log(planResponse);
35102
- console.log(`
35103
- ⚠️ Could not auto-execute. Please review the plan above.`);
35104
- return;
35105
- }
35106
- if (plan.isRisky && plan.riskExplanation) {
35107
- displayWarning(plan.riskExplanation);
35108
- console.log();
35109
- }
35110
- if (plan.explanation) {
35111
- console.log(`\uD83D\uDCAD ${plan.explanation}
35112
- `);
35113
- }
35114
- if (plan.commands && Array.isArray(plan.commands)) {
35115
- for (const command of plan.commands) {
35116
- let cmdString;
35117
- if (typeof command === "string") {
35118
- cmdString = command;
35119
- } else if (typeof command === "object" && command.command) {
35120
- cmdString = command.command;
35121
- } else if (typeof command === "object" && command.cmd) {
35122
- cmdString = command.cmd;
35123
- } else {
35124
- console.warn("⚠️ Skipping invalid command:", command);
35125
- continue;
35126
- }
35127
- validateGitCommand(cmdString);
35128
- displayStep(cmdString);
35129
- await executeGitCommand(cmdString);
35130
- }
35131
- const approach = plan.approach || "selected method";
35132
- displaySuccess(`Branch synced using ${approach}`);
35133
- } else {
35134
- console.log("⚠️ No commands to execute.");
35135
- }
35136
- } catch (error3) {
35137
- console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
35138
- process.exit(1);
35098
+ // node_modules/@inquirer/core/dist/esm/lib/use-effect.mjs
35099
+ function useEffect(cb, depArray) {
35100
+ withPointer((pointer) => {
35101
+ const oldDeps = pointer.get();
35102
+ const hasChanged = !Array.isArray(oldDeps) || depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));
35103
+ if (hasChanged) {
35104
+ effectScheduler.queue(cb);
35139
35105
  }
35106
+ pointer.set(depArray);
35140
35107
  });
35141
35108
  }
35142
35109
 
35143
- // node_modules/@inquirer/core/dist/esm/lib/key.mjs
35144
- var isUpKey = (key) => key.name === "up" || key.name === "k" || key.ctrl && key.name === "p";
35145
- var isDownKey = (key) => key.name === "down" || key.name === "j" || key.ctrl && key.name === "n";
35146
- var isSpaceKey = (key) => key.name === "space";
35147
- var isBackspaceKey = (key) => key.name === "backspace";
35148
- var isNumberKey = (key) => "123456789".includes(key.name);
35149
- var isEnterKey = (key) => key.name === "enter" || key.name === "return";
35150
- // node_modules/@inquirer/core/dist/esm/lib/errors.mjs
35151
- class AbortPromptError extends Error {
35152
- name = "AbortPromptError";
35153
- message = "Prompt was aborted";
35154
- constructor(options) {
35155
- super();
35156
- this.cause = options?.cause;
35157
- }
35158
- }
35159
-
35160
- class CancelPromptError extends Error {
35161
- name = "CancelPromptError";
35162
- message = "Prompt was canceled";
35163
- }
35110
+ // node_modules/@inquirer/core/dist/esm/lib/theme.mjs
35111
+ var import_yoctocolors_cjs = __toESM(require_yoctocolors_cjs(), 1);
35164
35112
 
35165
- class ExitPromptError extends Error {
35166
- name = "ExitPromptError";
35167
- }
35168
-
35169
- class HookError extends Error {
35170
- name = "HookError";
35171
- }
35172
-
35173
- class ValidationError extends Error {
35174
- name = "ValidationError";
35175
- }
35176
- // node_modules/@inquirer/core/dist/esm/lib/use-prefix.mjs
35177
- import { AsyncResource as AsyncResource2 } from "node:async_hooks";
35178
-
35179
- // node_modules/@inquirer/core/dist/esm/lib/hook-engine.mjs
35180
- import { AsyncLocalStorage, AsyncResource } from "node:async_hooks";
35181
- var hookStorage = new AsyncLocalStorage;
35182
- function createStore(rl) {
35183
- const store = {
35184
- rl,
35185
- hooks: [],
35186
- hooksCleanup: [],
35187
- hooksEffect: [],
35188
- index: 0,
35189
- handleChange() {}
35190
- };
35191
- return store;
35192
- }
35193
- function withHooks(rl, cb) {
35194
- const store = createStore(rl);
35195
- return hookStorage.run(store, () => {
35196
- function cycle(render) {
35197
- store.handleChange = () => {
35198
- store.index = 0;
35199
- render();
35200
- };
35201
- store.handleChange();
35202
- }
35203
- return cb(cycle);
35204
- });
35205
- }
35206
- function getStore() {
35207
- const store = hookStorage.getStore();
35208
- if (!store) {
35209
- throw new HookError("[Inquirer] Hook functions can only be called from within a prompt");
35210
- }
35211
- return store;
35212
- }
35213
- function readline() {
35214
- return getStore().rl;
35215
- }
35216
- function withUpdates(fn) {
35217
- const wrapped = (...args) => {
35218
- const store = getStore();
35219
- let shouldUpdate = false;
35220
- const oldHandleChange = store.handleChange;
35221
- store.handleChange = () => {
35222
- shouldUpdate = true;
35223
- };
35224
- const returnValue = fn(...args);
35225
- if (shouldUpdate) {
35226
- oldHandleChange();
35227
- }
35228
- store.handleChange = oldHandleChange;
35229
- return returnValue;
35230
- };
35231
- return AsyncResource.bind(wrapped);
35232
- }
35233
- function withPointer(cb) {
35234
- const store = getStore();
35235
- const { index } = store;
35236
- const pointer = {
35237
- get() {
35238
- return store.hooks[index];
35239
- },
35240
- set(value) {
35241
- store.hooks[index] = value;
35242
- },
35243
- initialized: index in store.hooks
35244
- };
35245
- const returnValue = cb(pointer);
35246
- store.index++;
35247
- return returnValue;
35248
- }
35249
- function handleChange() {
35250
- getStore().handleChange();
35251
- }
35252
- var effectScheduler = {
35253
- queue(cb) {
35254
- const store = getStore();
35255
- const { index } = store;
35256
- store.hooksEffect.push(() => {
35257
- store.hooksCleanup[index]?.();
35258
- const cleanFn = cb(readline());
35259
- if (cleanFn != null && typeof cleanFn !== "function") {
35260
- throw new ValidationError("useEffect return value must be a cleanup function or nothing.");
35261
- }
35262
- store.hooksCleanup[index] = cleanFn;
35263
- });
35264
- },
35265
- run() {
35266
- const store = getStore();
35267
- withUpdates(() => {
35268
- store.hooksEffect.forEach((effect) => {
35269
- effect();
35270
- });
35271
- store.hooksEffect.length = 0;
35272
- })();
35273
- },
35274
- clearAll() {
35275
- const store = getStore();
35276
- store.hooksCleanup.forEach((cleanFn) => {
35277
- cleanFn?.();
35278
- });
35279
- store.hooksEffect.length = 0;
35280
- store.hooksCleanup.length = 0;
35281
- }
35282
- };
35283
-
35284
- // node_modules/@inquirer/core/dist/esm/lib/use-state.mjs
35285
- function useState(defaultValue) {
35286
- return withPointer((pointer) => {
35287
- const setFn = (newValue) => {
35288
- if (pointer.get() !== newValue) {
35289
- pointer.set(newValue);
35290
- handleChange();
35291
- }
35292
- };
35293
- if (pointer.initialized) {
35294
- return [pointer.get(), setFn];
35295
- }
35296
- const value = typeof defaultValue === "function" ? defaultValue() : defaultValue;
35297
- pointer.set(value);
35298
- return [value, setFn];
35299
- });
35300
- }
35301
-
35302
- // node_modules/@inquirer/core/dist/esm/lib/use-effect.mjs
35303
- function useEffect(cb, depArray) {
35304
- withPointer((pointer) => {
35305
- const oldDeps = pointer.get();
35306
- const hasChanged = !Array.isArray(oldDeps) || depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));
35307
- if (hasChanged) {
35308
- effectScheduler.queue(cb);
35309
- }
35310
- pointer.set(depArray);
35311
- });
35312
- }
35313
-
35314
- // node_modules/@inquirer/core/dist/esm/lib/theme.mjs
35315
- var import_yoctocolors_cjs = __toESM(require_yoctocolors_cjs(), 1);
35316
-
35317
- // node_modules/@inquirer/figures/dist/esm/index.js
35318
- import process3 from "node:process";
35319
- function isUnicodeSupported() {
35320
- if (process3.platform !== "win32") {
35321
- return process3.env["TERM"] !== "linux";
35322
- }
35323
- return Boolean(process3.env["WT_SESSION"]) || Boolean(process3.env["TERMINUS_SUBLIME"]) || process3.env["ConEmuTask"] === "{cmd::Cmder}" || process3.env["TERM_PROGRAM"] === "Terminus-Sublime" || process3.env["TERM_PROGRAM"] === "vscode" || process3.env["TERM"] === "xterm-256color" || process3.env["TERM"] === "alacritty" || process3.env["TERMINAL_EMULATOR"] === "JetBrains-JediTerm";
35113
+ // node_modules/@inquirer/figures/dist/esm/index.js
35114
+ import process3 from "node:process";
35115
+ function isUnicodeSupported() {
35116
+ if (process3.platform !== "win32") {
35117
+ return process3.env["TERM"] !== "linux";
35118
+ }
35119
+ return Boolean(process3.env["WT_SESSION"]) || Boolean(process3.env["TERMINUS_SUBLIME"]) || process3.env["ConEmuTask"] === "{cmd::Cmder}" || process3.env["TERM_PROGRAM"] === "Terminus-Sublime" || process3.env["TERM_PROGRAM"] === "vscode" || process3.env["TERM"] === "xterm-256color" || process3.env["TERM"] === "alacritty" || process3.env["TERMINAL_EMULATOR"] === "JetBrains-JediTerm";
35324
35120
  }
35325
35121
  var common = {
35326
35122
  circleQuestionMark: "(?)",
@@ -37131,9 +36927,9 @@ var import_mute_stream2 = __toESM(require_lib(), 1);
37131
36927
  import readline3 from "node:readline";
37132
36928
  var import_ansi_escapes5 = __toESM(require_ansi_escapes(), 1);
37133
36929
  var _ = {
37134
- set: (obj, path4 = "", value) => {
36930
+ set: (obj, path5 = "", value) => {
37135
36931
  let pointer = obj;
37136
- path4.split(".").forEach((key2, index, arr) => {
36932
+ path5.split(".").forEach((key2, index, arr) => {
37137
36933
  if (key2 === "__proto__" || key2 === "constructor")
37138
36934
  return;
37139
36935
  if (index === arr.length - 1) {
@@ -37144,8 +36940,8 @@ var _ = {
37144
36940
  pointer = pointer[key2];
37145
36941
  });
37146
36942
  },
37147
- get: (obj, path4 = "", defaultValue) => {
37148
- const travel = (regexp) => String.prototype.split.call(path4, regexp).filter(Boolean).reduce((res, key2) => res !== null && res !== undefined ? res[key2] : res, obj);
36943
+ get: (obj, path5 = "", defaultValue) => {
36944
+ const travel = (regexp) => String.prototype.split.call(path5, regexp).filter(Boolean).reduce((res, key2) => res !== null && res !== undefined ? res[key2] : res, obj);
37149
36945
  const result = travel(/[,[\]]+?/) || travel(/[,.[\]]+?/);
37150
36946
  return result === undefined || result === obj ? defaultValue : result;
37151
36947
  }
@@ -37383,8 +37179,537 @@ var inquirer = {
37383
37179
  };
37384
37180
  var esm_default12 = inquirer;
37385
37181
 
37182
+ // src/lib/council.ts
37183
+ async function broadcastToCouncil(prompt2) {
37184
+ const providers = await getAllAvailableProviders();
37185
+ if (providers.length === 0) {
37186
+ throw new Error("No AI providers available. Run hermes config setup.");
37187
+ }
37188
+ const results = await Promise.allSettled(providers.map(async (p) => {
37189
+ const response = await p.complete(prompt2);
37190
+ return { providerName: p.name, model: p.model, response };
37191
+ }));
37192
+ return results.map((r, i) => r.status === "fulfilled" ? r.value : { providerName: providers[i].name, model: providers[i].model, response: "", error: r.reason?.message ?? "Unknown error" });
37193
+ }
37194
+ async function synthesizePerspectives(originalPrompt, perspectives, userFeedback) {
37195
+ const valid = perspectives.filter((p) => !p.error);
37196
+ if (valid.length === 0)
37197
+ throw new Error("No valid perspectives to synthesize");
37198
+ if (valid.length === 1)
37199
+ return valid[0].response;
37200
+ const perspectiveBlock = valid.map((p) => `### ${p.providerName} (${p.model})
37201
+ ${p.response}`).join(`
37202
+
37203
+ `);
37204
+ const feedbackLine = userFeedback ? `
37205
+ User feedback to incorporate: "${userFeedback}"
37206
+ ` : "";
37207
+ const prompt2 = `You are synthesizing advice from multiple AI assistants into one clear, actionable recommendation.
37208
+
37209
+ Original request: "${originalPrompt}"
37210
+ ${feedbackLine}
37211
+ Each assistant's perspective:
37212
+
37213
+ ${perspectiveBlock}
37214
+
37215
+ Synthesize these into a single, concise, actionable response. Where they agree, state the consensus confidently. Where they differ, explain the trade-off briefly and make a clear recommendation. Do not attribute individual opinions.`;
37216
+ return getAISuggestion(prompt2);
37217
+ }
37218
+ async function runCouncilSession(prompt2, options = {}) {
37219
+ const session = { originalPrompt: prompt2, rounds: [] };
37220
+ console.log(`
37221
+ ` + source_default.bold("Council") + (options.label ? source_default.dim(` — ${options.label}`) : "") + `
37222
+ `);
37223
+ let currentPrompt = prompt2;
37224
+ let round = 0;
37225
+ while (true) {
37226
+ round++;
37227
+ process.stdout.write(source_default.dim(` Consulting council${round > 1 ? ` (round ${round})` : ""}...`));
37228
+ const perspectives = await broadcastToCouncil(currentPrompt);
37229
+ session.rounds.push({ feedback: round > 1 ? currentPrompt : undefined, perspectives });
37230
+ process.stdout.write("\r" + " ".repeat(50) + "\r");
37231
+ displayPerspectives(perspectives);
37232
+ const validPerspectives = perspectives.filter((p) => !p.error);
37233
+ const choices = [];
37234
+ validPerspectives.forEach((p, i) => {
37235
+ choices.push({ name: `Accept [${i + 1}] ${p.providerName}`, value: `accept:${i}` });
37236
+ });
37237
+ if (validPerspectives.length > 1) {
37238
+ choices.push({ name: "Synthesize — have the council agree on one answer", value: "synthesize" });
37239
+ }
37240
+ choices.push({ name: "Refine — give feedback and re-ask the council", value: "refine" });
37241
+ choices.push({ name: "Follow-up — ask the council a new question", value: "followup" });
37242
+ choices.push({ name: "Exit — discard", value: "exit" });
37243
+ const { action } = await esm_default12.prompt([{
37244
+ type: "list",
37245
+ name: "action",
37246
+ message: "What would you like to do?",
37247
+ choices
37248
+ }]);
37249
+ if (action === "exit")
37250
+ return null;
37251
+ if (action.startsWith("accept:")) {
37252
+ const idx = parseInt(action.split(":")[1], 10);
37253
+ return validPerspectives[idx].response;
37254
+ }
37255
+ if (action === "synthesize") {
37256
+ process.stdout.write(source_default.dim(" Synthesizing...\r"));
37257
+ const synthesis = await synthesizePerspectives(prompt2, perspectives);
37258
+ process.stdout.write(" ".repeat(30) + "\r");
37259
+ console.log(`
37260
+ ` + source_default.dim(" ─".repeat(27)));
37261
+ console.log(" " + source_default.bold.cyan("Synthesis"));
37262
+ console.log(source_default.dim(" ─".repeat(27)));
37263
+ wrapText(synthesis).forEach((l) => console.log(" " + l));
37264
+ console.log(source_default.dim(" ─".repeat(27)) + `
37265
+ `);
37266
+ const { synthAction } = await esm_default12.prompt([{
37267
+ type: "list",
37268
+ name: "synthAction",
37269
+ message: "Use this synthesis?",
37270
+ choices: [
37271
+ { name: "Yes, accept", value: "accept" },
37272
+ { name: "Refine further", value: "refine" },
37273
+ { name: "Exit", value: "exit" }
37274
+ ]
37275
+ }]);
37276
+ if (synthAction === "accept")
37277
+ return synthesis;
37278
+ if (synthAction === "exit")
37279
+ return null;
37280
+ }
37281
+ const isFollowUp = action === "followup";
37282
+ const { feedback } = await esm_default12.prompt([{
37283
+ type: "input",
37284
+ name: "feedback",
37285
+ message: isFollowUp ? "Your question:" : "Your feedback (what to change or add):",
37286
+ validate: (v) => v.trim().length > 0 || "Please enter something"
37287
+ }]);
37288
+ if (isFollowUp) {
37289
+ currentPrompt = feedback.trim();
37290
+ } else {
37291
+ const perspectiveContext = perspectives.filter((p) => !p.error).map((p) => `[${p.providerName}]: ${p.response.slice(0, 500)}`).join(`
37292
+
37293
+ `);
37294
+ currentPrompt = `Original request: "${prompt2}"
37295
+
37296
+ Previous perspectives from the council:
37297
+ ${perspectiveContext}
37298
+
37299
+ User feedback: "${feedback.trim()}"
37300
+
37301
+ Respond with an improved answer that addresses the feedback.`;
37302
+ }
37303
+ console.log();
37304
+ }
37305
+ }
37306
+ function displayPerspectives(perspectives) {
37307
+ const valid = perspectives.filter((p) => !p.error);
37308
+ const failed = perspectives.filter((p) => p.error);
37309
+ perspectives.filter((p) => !p.error).forEach((p, i) => {
37310
+ console.log(source_default.dim(" ─".repeat(27)));
37311
+ console.log(` ${source_default.bold.cyan(`[${i + 1}]`)} ${source_default.bold(p.providerName)} ${source_default.dim(p.model)}`);
37312
+ console.log(source_default.dim(" ─".repeat(27)));
37313
+ wrapText(p.response).forEach((l) => console.log(" " + l));
37314
+ console.log();
37315
+ });
37316
+ if (failed.length > 0) {
37317
+ failed.forEach((p) => {
37318
+ console.log(source_default.dim(` [${p.providerName}] unavailable: ${p.error}`));
37319
+ });
37320
+ console.log();
37321
+ }
37322
+ if (valid.length === 0) {
37323
+ throw new Error("All council members failed to respond.");
37324
+ }
37325
+ }
37326
+ function wrapText(text, width = 72) {
37327
+ const lines2 = [];
37328
+ for (const paragraph of text.split(`
37329
+ `)) {
37330
+ if (paragraph.trim() === "") {
37331
+ lines2.push("");
37332
+ continue;
37333
+ }
37334
+ const words = paragraph.split(" ");
37335
+ let line = "";
37336
+ for (const word of words) {
37337
+ if ((line + " " + word).trim().length > width) {
37338
+ if (line)
37339
+ lines2.push(line);
37340
+ line = word;
37341
+ } else {
37342
+ line = line ? line + " " + word : word;
37343
+ }
37344
+ }
37345
+ if (line)
37346
+ lines2.push(line);
37347
+ }
37348
+ return lines2;
37349
+ }
37350
+
37351
+ // src/commands/plan.ts
37352
+ function planCommand(program2) {
37353
+ program2.command("plan").description("Analyze the current repository state and propose a safe Git plan").argument("<intent>", "What you want to achieve").option("--council", "Force council mode (all available providers)").option("--no-council", "Skip council mode, use single provider only").action(async (intent, options) => {
37354
+ try {
37355
+ const repoState = await getRepoState();
37356
+ const providers = await getAllAvailableProviders();
37357
+ const useCouncil = options.council === true ? true : options.council === false ? false : providers.length >= 2;
37358
+ if (useCouncil && providers.length >= 2) {
37359
+ const providerList = providers.map((p) => source_default.dim(`[${p.name}]`)).join(" ");
37360
+ console.log(`
37361
+ ${providerList}
37362
+ `);
37363
+ const prompt2 = buildPlanPrompt(intent, repoState);
37364
+ const result = await runCouncilSession(prompt2, { label: intent });
37365
+ if (result) {
37366
+ displayPlan(result, repoState);
37367
+ console.log(source_default.dim(`
37368
+ No changes made. Review the plan above.
37369
+ `));
37370
+ } else {
37371
+ console.log(source_default.dim(`
37372
+ Exited without selecting a plan.
37373
+ `));
37374
+ }
37375
+ } else {
37376
+ const { name, model } = await getActiveProvider();
37377
+ console.log(`
37378
+ Analyzing... ${source_default.dim(`[${name} / ${model}]`)}
37379
+ `);
37380
+ const analysis = await analyzeGitState(repoState, intent);
37381
+ displayPlan(analysis, repoState);
37382
+ console.log(source_default.dim(`
37383
+ No changes made. Review the plan above.
37384
+ `));
37385
+ if (providers.length >= 2) {
37386
+ console.log(source_default.dim(` Tip: run with --council to get perspectives from all providers.
37387
+ `));
37388
+ }
37389
+ }
37390
+ } catch (error3) {
37391
+ console.error(source_default.red(`
37392
+ ✖ ` + (error3 instanceof Error ? error3.message : error3)));
37393
+ process.exit(1);
37394
+ }
37395
+ });
37396
+ }
37397
+ function buildPlanPrompt(intent, repoState) {
37398
+ return `You are a Git expert. A developer wants to: "${intent}"
37399
+
37400
+ Current repository state:
37401
+ - Branch: ${repoState.currentBranch}
37402
+ - Clean: ${repoState.isClean}
37403
+ - Uncommitted changes: ${repoState.hasUncommittedChanges}
37404
+ - Untracked files: ${repoState.hasUntrackedFiles}
37405
+ - Ahead: ${repoState.ahead} commits, Behind: ${repoState.behind} commits
37406
+ - In rebase: ${repoState.isInRebase}, In merge: ${repoState.isInMerge}
37407
+ - Remote tracking: ${repoState.remoteTracking ?? "none"}
37408
+
37409
+ Provide a clear, safe Git plan with:
37410
+ 1. Recommended approach and reasoning
37411
+ 2. Step-by-step git commands (must start with "git ")
37412
+ 3. Any risks or things to watch out for
37413
+
37414
+ Be specific and concise.`;
37415
+ }
37416
+
37417
+ // src/lib/config.ts
37418
+ import { readFile as readFile3 } from "fs/promises";
37419
+ import { existsSync as existsSync2 } from "fs";
37420
+ async function loadConfig() {
37421
+ const configPath = ".hermes/config.json";
37422
+ if (!existsSync2(configPath)) {
37423
+ return null;
37424
+ }
37425
+ try {
37426
+ const content = await readFile3(configPath, "utf-8");
37427
+ return JSON.parse(content);
37428
+ } catch (error3) {
37429
+ console.warn("⚠️ Could not load .hermes/config.json");
37430
+ return null;
37431
+ }
37432
+ }
37433
+ function generateBranchName(pattern, description, ticket) {
37434
+ let branchName = pattern;
37435
+ branchName = branchName.replace("{description}", slugify(description));
37436
+ branchName = branchName.replace("{ticket}", ticket || "");
37437
+ branchName = branchName.replace(/\/{2,}/g, "/");
37438
+ branchName = branchName.replace(/\/$/, "");
37439
+ return branchName;
37440
+ }
37441
+ function slugify(text) {
37442
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
37443
+ }
37444
+ function isProtectedBranch(branch, config) {
37445
+ if (!config) {
37446
+ return ["main", "master", "production", "staging"].includes(branch);
37447
+ }
37448
+ return config.project.protectedBranches.includes(branch);
37449
+ }
37450
+
37451
+ // src/lib/stats.ts
37452
+ import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
37453
+ import { existsSync as existsSync3 } from "fs";
37454
+ var STATS_FILE = ".hermes/stats.json";
37455
+ var MAX_HISTORY = 1000;
37456
+ async function loadStats() {
37457
+ if (!existsSync3(STATS_FILE)) {
37458
+ return createEmptyStats();
37459
+ }
37460
+ try {
37461
+ const content = await readFile4(STATS_FILE, "utf-8");
37462
+ return JSON.parse(content);
37463
+ } catch {
37464
+ return createEmptyStats();
37465
+ }
37466
+ }
37467
+ async function saveStats(stats) {
37468
+ try {
37469
+ await mkdir2(".hermes", { recursive: true });
37470
+ if (stats.commandHistory.length > MAX_HISTORY) {
37471
+ stats.commandHistory = stats.commandHistory.slice(-MAX_HISTORY);
37472
+ }
37473
+ await writeFile2(STATS_FILE, JSON.stringify(stats, null, 2));
37474
+ } catch (error3) {}
37475
+ }
37476
+ async function recordCommand(command, args, duration, success, gitCommandsRun = 0) {
37477
+ const stats = await loadStats();
37478
+ const entry = {
37479
+ timestamp: Date.now(),
37480
+ command,
37481
+ args,
37482
+ duration,
37483
+ success,
37484
+ gitCommandsRun
37485
+ };
37486
+ stats.totalCommands++;
37487
+ stats.totalGitCommands += gitCommandsRun;
37488
+ stats.lastUsed = Date.now();
37489
+ stats.commandHistory.push(entry);
37490
+ await saveStats(stats);
37491
+ }
37492
+ async function getStatsSummary(days = 30) {
37493
+ const stats = await loadStats();
37494
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
37495
+ const recentEntries = stats.commandHistory.filter((e) => e.timestamp >= cutoff);
37496
+ const commandCounts = {};
37497
+ let successCount = 0;
37498
+ recentEntries.forEach((entry) => {
37499
+ commandCounts[entry.command] = (commandCounts[entry.command] || 0) + 1;
37500
+ if (entry.success)
37501
+ successCount++;
37502
+ });
37503
+ const topCommands = Object.entries(commandCounts).sort(([, a], [, b]) => b - a).slice(0, 5).map(([cmd, count]) => ({ command: cmd, count }));
37504
+ const totalDays = Math.max(1, Math.ceil((Date.now() - stats.startDate) / (24 * 60 * 60 * 1000)));
37505
+ return {
37506
+ totalCommands: recentEntries.length,
37507
+ allTimeCommands: stats.totalCommands,
37508
+ gitCommandsRun: recentEntries.reduce((sum, e) => sum + e.gitCommandsRun, 0),
37509
+ allTimeGitCommands: stats.totalGitCommands,
37510
+ successRate: recentEntries.length ? successCount / recentEntries.length : 0,
37511
+ topCommands,
37512
+ daysActive: totalDays,
37513
+ commandsPerDay: stats.totalCommands / totalDays
37514
+ };
37515
+ }
37516
+ function createEmptyStats() {
37517
+ return {
37518
+ totalCommands: 0,
37519
+ totalGitCommands: 0,
37520
+ commandHistory: [],
37521
+ startDate: Date.now(),
37522
+ lastUsed: Date.now()
37523
+ };
37524
+ }
37525
+
37526
+ // src/commands/start.ts
37527
+ function startCommand(program2) {
37528
+ program2.command("start").description("Start a new piece of work safely").argument("<task>", "Description of the task").action(async (task) => {
37529
+ const startTime = Date.now();
37530
+ let gitCommandsRun = 0;
37531
+ try {
37532
+ console.log(`\uD83D\uDE80 Starting new task...
37533
+ `);
37534
+ const config = await loadConfig();
37535
+ const repoState = await getRepoState();
37536
+ let suggestedBranchName;
37537
+ if (config) {
37538
+ suggestedBranchName = generateBranchName(config.branches.featurePattern, task);
37539
+ console.log(`\uD83D\uDCA1 Suggested branch: ${suggestedBranchName}
37540
+ `);
37541
+ }
37542
+ const planResponse = await getGitPlan(repoState, `Start working on: ${task}. ${suggestedBranchName ? `Suggested branch name: ${suggestedBranchName}.` : ""} Provide base branch, conventional branch name, and Git commands to create and switch to the branch.`);
37543
+ let plan;
37544
+ try {
37545
+ plan = JSON.parse(planResponse);
37546
+ } catch {
37547
+ console.log(`\uD83D\uDCAD Hermes suggests:
37548
+ `);
37549
+ console.log(planResponse);
37550
+ console.log(`
37551
+ ⚠️ Could not auto-execute. Please review the plan above.`);
37552
+ return;
37553
+ }
37554
+ if (plan.baseBranch && plan.branchName) {
37555
+ console.log(`\uD83D\uDCCD Base branch: ${plan.baseBranch}`);
37556
+ console.log(`\uD83C\uDF3F New branch: ${plan.branchName}
37557
+ `);
37558
+ }
37559
+ if (plan.explanation) {
37560
+ console.log(`\uD83D\uDCAD ${plan.explanation}
37561
+ `);
37562
+ }
37563
+ if (plan.commands && Array.isArray(plan.commands)) {
37564
+ for (const command of plan.commands) {
37565
+ let cmdString;
37566
+ if (typeof command === "string") {
37567
+ cmdString = command;
37568
+ } else if (typeof command === "object" && command.command) {
37569
+ cmdString = command.command;
37570
+ } else if (typeof command === "object" && command.cmd) {
37571
+ cmdString = command.cmd;
37572
+ } else {
37573
+ console.warn("⚠️ Skipping invalid command:", command);
37574
+ continue;
37575
+ }
37576
+ validateGitCommand(cmdString);
37577
+ displayStep(cmdString);
37578
+ await executeGitCommand(cmdString);
37579
+ gitCommandsRun++;
37580
+ }
37581
+ const branchName = plan.branchName || "new branch";
37582
+ displaySuccess(`Successfully created and switched to ${branchName}`);
37583
+ if (config?.preferences.learningMode) {
37584
+ console.log(`
37585
+ \uD83D\uDCA1 Learning tip: Branch created from clean state ensures no unexpected commits`);
37586
+ }
37587
+ } else {
37588
+ console.log("⚠️ No commands to execute. See analysis above.");
37589
+ }
37590
+ const duration = (Date.now() - startTime) / 1000;
37591
+ await recordCommand("start", [task], duration, true, gitCommandsRun);
37592
+ } catch (error3) {
37593
+ const duration = (Date.now() - startTime) / 1000;
37594
+ await recordCommand("start", [task], duration, false, gitCommandsRun);
37595
+ console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
37596
+ process.exit(1);
37597
+ }
37598
+ });
37599
+ }
37600
+
37601
+ // src/commands/wip.ts
37602
+ function wipCommand(program2) {
37603
+ program2.command("wip").description("Save work safely when things get messy").option("-m, --message <message>", "Custom WIP message").action(async (options) => {
37604
+ try {
37605
+ console.log(`\uD83D\uDCBE Saving work in progress...
37606
+ `);
37607
+ const repoState = await getRepoState();
37608
+ const messageNote = options.message ? ` with message: "${options.message}"` : "";
37609
+ const planResponse = await getGitPlan(repoState, `Save work in progress${messageNote}. Decide whether to commit or stash. Return JSON with: approach, commands[], explanation.`);
37610
+ let plan;
37611
+ try {
37612
+ plan = JSON.parse(planResponse);
37613
+ } catch {
37614
+ console.log(`\uD83D\uDCAD Hermes suggests:
37615
+ `);
37616
+ console.log(planResponse);
37617
+ console.log(`
37618
+ ⚠️ Could not auto-execute. Please review the plan above.`);
37619
+ return;
37620
+ }
37621
+ if (plan.explanation) {
37622
+ console.log(`\uD83D\uDCAD ${plan.explanation}
37623
+ `);
37624
+ }
37625
+ if (plan.commands && Array.isArray(plan.commands)) {
37626
+ for (const command of plan.commands) {
37627
+ let cmdString;
37628
+ if (typeof command === "string") {
37629
+ cmdString = command;
37630
+ } else if (typeof command === "object" && command.command) {
37631
+ cmdString = command.command;
37632
+ } else if (typeof command === "object" && command.cmd) {
37633
+ cmdString = command.cmd;
37634
+ } else {
37635
+ console.warn("⚠️ Skipping invalid command:", command);
37636
+ continue;
37637
+ }
37638
+ validateGitCommand(cmdString);
37639
+ displayStep(cmdString);
37640
+ await executeGitCommand(cmdString);
37641
+ }
37642
+ const approach = plan.approach || "selected method";
37643
+ displaySuccess(`Work saved using ${approach}`);
37644
+ } else {
37645
+ console.log("⚠️ No commands to execute.");
37646
+ }
37647
+ } catch (error3) {
37648
+ console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
37649
+ process.exit(1);
37650
+ }
37651
+ });
37652
+ }
37653
+
37654
+ // src/commands/sync.ts
37655
+ function syncCommand(program2) {
37656
+ program2.command("sync").description("Bring your branch up to date safely").option("--from <branch>", "Source branch to sync from (default: main)").action(async (options) => {
37657
+ try {
37658
+ console.log(`\uD83D\uDD04 Syncing branch...
37659
+ `);
37660
+ const repoState = await getRepoState();
37661
+ const fromNote = options.from ? ` from ${options.from}` : " from main";
37662
+ const planResponse = await getGitPlan(repoState, `Sync branch${fromNote}. Evaluate if rebase or merge is safer. Check if branch is shared. Return JSON with: approach, isRisky, riskExplanation, commands[], explanation.`);
37663
+ let plan;
37664
+ try {
37665
+ plan = JSON.parse(planResponse);
37666
+ } catch {
37667
+ console.log(`\uD83D\uDCAD Hermes suggests:
37668
+ `);
37669
+ console.log(planResponse);
37670
+ console.log(`
37671
+ ⚠️ Could not auto-execute. Please review the plan above.`);
37672
+ return;
37673
+ }
37674
+ if (plan.isRisky && plan.riskExplanation) {
37675
+ displayWarning(plan.riskExplanation);
37676
+ console.log();
37677
+ }
37678
+ if (plan.explanation) {
37679
+ console.log(`\uD83D\uDCAD ${plan.explanation}
37680
+ `);
37681
+ }
37682
+ if (plan.commands && Array.isArray(plan.commands)) {
37683
+ for (const command of plan.commands) {
37684
+ let cmdString;
37685
+ if (typeof command === "string") {
37686
+ cmdString = command;
37687
+ } else if (typeof command === "object" && command.command) {
37688
+ cmdString = command.command;
37689
+ } else if (typeof command === "object" && command.cmd) {
37690
+ cmdString = command.cmd;
37691
+ } else {
37692
+ console.warn("⚠️ Skipping invalid command:", command);
37693
+ continue;
37694
+ }
37695
+ validateGitCommand(cmdString);
37696
+ displayStep(cmdString);
37697
+ await executeGitCommand(cmdString);
37698
+ }
37699
+ const approach = plan.approach || "selected method";
37700
+ displaySuccess(`Branch synced using ${approach}`);
37701
+ } else {
37702
+ console.log("⚠️ No commands to execute.");
37703
+ }
37704
+ } catch (error3) {
37705
+ console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
37706
+ process.exit(1);
37707
+ }
37708
+ });
37709
+ }
37710
+
37386
37711
  // src/commands/conflict.ts
37387
- import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
37712
+ import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
37388
37713
  function conflictCommand(program2) {
37389
37714
  const conflict = program2.command("conflict").description("Understand and resolve merge conflicts");
37390
37715
  conflict.command("explain").description("Understand why a conflict exists").action(async () => {
@@ -37418,7 +37743,7 @@ function conflictCommand(program2) {
37418
37743
  \uD83D\uDCC4 ${file}`);
37419
37744
  let fileContent;
37420
37745
  try {
37421
- fileContent = await readFile4(file, "utf-8");
37746
+ fileContent = await readFile5(file, "utf-8");
37422
37747
  } catch (error3) {
37423
37748
  console.log(`⚠️ Could not read ${file}, skipping...`);
37424
37749
  continue;
@@ -37526,8 +37851,8 @@ function worktreeCommand(program2) {
37526
37851
  displayStep(cmdString);
37527
37852
  await executeGitCommand(cmdString);
37528
37853
  }
37529
- const path4 = plan.worktreePath || "new worktree";
37530
- displaySuccess(`Worktree created at ${path4}`);
37854
+ const path5 = plan.worktreePath || "new worktree";
37855
+ displaySuccess(`Worktree created at ${path5}`);
37531
37856
  } else {
37532
37857
  console.log("⚠️ No commands to execute.");
37533
37858
  }
@@ -37541,10 +37866,10 @@ function worktreeCommand(program2) {
37541
37866
  // src/commands/init.ts
37542
37867
  import { writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
37543
37868
  import { existsSync as existsSync4 } from "fs";
37544
- import path4 from "path";
37545
- import { exec as exec2 } from "child_process";
37546
- import { promisify as promisify2 } from "util";
37547
- var execAsync2 = promisify2(exec2);
37869
+ import path5 from "path";
37870
+ import { exec as exec3 } from "child_process";
37871
+ import { promisify as promisify3 } from "util";
37872
+ var execAsync3 = promisify3(exec3);
37548
37873
  function initCommand(program2) {
37549
37874
  program2.command("init").description("Initialize Hermes configuration for this repository").option("--quick", "Skip interactive prompts, use defaults").action(async (options) => {
37550
37875
  try {
@@ -37561,7 +37886,7 @@ function initCommand(program2) {
37561
37886
  `);
37562
37887
  if (options.quick) {
37563
37888
  console.log("Running: git init");
37564
- await execAsync2("git init");
37889
+ await execAsync3("git init");
37565
37890
  console.log(`✓ Git repository initialized
37566
37891
  `);
37567
37892
  } else {
@@ -37577,7 +37902,7 @@ function initCommand(program2) {
37577
37902
  console.log("Cancelled. Please run this command in a Git repository.");
37578
37903
  process.exit(1);
37579
37904
  }
37580
- await execAsync2("git init");
37905
+ await execAsync3("git init");
37581
37906
  console.log(`✓ Git repository initialized
37582
37907
  `);
37583
37908
  }
@@ -37603,7 +37928,7 @@ function initCommand(program2) {
37603
37928
  currentBranch = repoState.currentBranch;
37604
37929
  } else {
37605
37930
  try {
37606
- const { stdout } = await execAsync2("git config --get init.defaultBranch");
37931
+ const { stdout } = await execAsync3("git config --get init.defaultBranch");
37607
37932
  currentBranch = stdout.trim() || "main";
37608
37933
  } catch {
37609
37934
  currentBranch = "main";
@@ -37655,7 +37980,7 @@ function createDefaultConfig(currentBranch) {
37655
37980
  return {
37656
37981
  version: "0.1.0",
37657
37982
  project: {
37658
- name: path4.basename(process.cwd()),
37983
+ name: path5.basename(process.cwd()),
37659
37984
  mainBranch: currentBranch === "master" ? "master" : "main",
37660
37985
  protectedBranches: ["main", "master", "production", "staging"]
37661
37986
  },
@@ -37687,7 +38012,7 @@ async function interactiveConfig(repoInfo) {
37687
38012
  type: "input",
37688
38013
  name: "projectName",
37689
38014
  message: "Project name:",
37690
- default: path4.basename(process.cwd())
38015
+ default: path5.basename(process.cwd())
37691
38016
  },
37692
38017
  {
37693
38018
  type: "input",
@@ -37770,7 +38095,8 @@ var GITIGNORE_ENTRIES = [
37770
38095
  { pattern: ".env.*", comment: null },
37771
38096
  { pattern: "!.env.example", comment: null },
37772
38097
  { pattern: ".hermes/backups/", comment: null },
37773
- { pattern: ".hermes/stats.json", comment: null }
38098
+ { pattern: ".hermes/stats.json", comment: null },
38099
+ { pattern: ".hermes/plans/", comment: null }
37774
38100
  ];
37775
38101
  async function updateGitignore() {
37776
38102
  const { appendFile, readFile: rf } = await import("fs/promises");
@@ -37850,60 +38176,425 @@ function statsCommand(program2) {
37850
38176
  }
37851
38177
 
37852
38178
  // src/commands/workflow.ts
38179
+ import { exec as exec7 } from "child_process";
38180
+ import { promisify as promisify7 } from "util";
38181
+
38182
+ // src/lib/standup.ts
38183
+ import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
38184
+ import { existsSync as existsSync5 } from "fs";
38185
+ import { exec as exec4 } from "child_process";
38186
+ import { promisify as promisify4 } from "util";
38187
+ import path6 from "path";
38188
+ var execAsync4 = promisify4(exec4);
38189
+ var PLANS_DIR = ".hermes/plans";
38190
+ function toDateString(d) {
38191
+ return d.toISOString().slice(0, 10);
38192
+ }
38193
+ function today() {
38194
+ return toDateString(new Date);
38195
+ }
38196
+ function lastWorkday() {
38197
+ const d = new Date;
38198
+ const day = d.getDay();
38199
+ const daysBack = day === 1 ? 3 : day === 0 ? 2 : 1;
38200
+ d.setDate(d.getDate() - daysBack);
38201
+ return toDateString(d);
38202
+ }
38203
+ function planPath(date) {
38204
+ return path6.join(PLANS_DIR, `${date}.md`);
38205
+ }
38206
+ async function readPlan(date) {
38207
+ const file = planPath(date);
38208
+ if (!existsSync5(file))
38209
+ return null;
38210
+ try {
38211
+ const text = (await readFile6(file, "utf-8")).trim();
38212
+ return text || null;
38213
+ } catch {
38214
+ return null;
38215
+ }
38216
+ }
38217
+ async function writePlan(date, content) {
38218
+ await mkdir4(PLANS_DIR, { recursive: true });
38219
+ await writeFile5(planPath(date), content.trim() + `
38220
+ `);
38221
+ }
38222
+ function todayKey() {
38223
+ return today();
38224
+ }
38225
+ function lastWorkdayKey() {
38226
+ return lastWorkday();
38227
+ }
38228
+ async function getCommitsSince(sinceDate) {
38229
+ try {
38230
+ const authorEmail = await execAsync4("git config user.email").then((r) => r.stdout.trim()).catch(() => "");
38231
+ const format = "%H\x1F%s\x1F%as";
38232
+ const authorFlag = authorEmail ? `--author=${JSON.stringify(authorEmail)}` : "";
38233
+ const { stdout } = await execAsync4(`git log --since="${sinceDate} 00:00" --format="${format}" ${authorFlag} --no-merges`);
38234
+ return stdout.trim().split(`
38235
+ `).filter(Boolean).map((line) => {
38236
+ const [hash, subject, date] = line.split("\x1F");
38237
+ return { hash: hash.slice(0, 7), subject, date };
38238
+ });
38239
+ } catch {
38240
+ return [];
38241
+ }
38242
+ }
38243
+ async function detectBlockers(repoState) {
38244
+ const blockers = [];
38245
+ if (repoState.isInRebase) {
38246
+ blockers.push({ severity: "block", message: "Rebase in progress — resolve before continuing" });
38247
+ }
38248
+ if (repoState.isInMerge) {
38249
+ blockers.push({ severity: "block", message: "Merge in progress — resolve conflicts" });
38250
+ }
38251
+ if (repoState.isInCherryPick) {
38252
+ blockers.push({ severity: "block", message: "Cherry-pick in progress" });
38253
+ }
38254
+ if (repoState.behind > 0) {
38255
+ blockers.push({
38256
+ severity: "warn",
38257
+ message: `Branch is ${repoState.behind} commit${repoState.behind !== 1 ? "s" : ""} behind remote — run hermes sync`
38258
+ });
38259
+ }
38260
+ if (repoState.hasUncommittedChanges) {
38261
+ blockers.push({ severity: "warn", message: "Uncommitted changes in working tree" });
38262
+ }
38263
+ try {
38264
+ const { stdout } = await execAsync4(`git log --format="%as" ${repoState.currentBranch} --not --remotes=origin/main --not --remotes=origin/master -- 2>/dev/null | tail -1`);
38265
+ const oldest = stdout.trim();
38266
+ if (oldest) {
38267
+ const daysOld = Math.floor((Date.now() - new Date(oldest).getTime()) / 86400000);
38268
+ if (daysOld >= 5) {
38269
+ blockers.push({
38270
+ severity: "warn",
38271
+ message: `Branch has unmerged work going back ${daysOld} days — consider opening a PR`
38272
+ });
38273
+ }
38274
+ }
38275
+ } catch {}
38276
+ return blockers;
38277
+ }
38278
+ async function suggestTodayPlan(ctx) {
38279
+ const prompt2 = `You are helping a developer plan their day based on their git activity.
38280
+
38281
+ Suggest a concise, practical focus for today in 1-2 sentences.
38282
+ Be specific — reference the actual work in progress from the branch name and commits.
38283
+ Do NOT use bullet points or markdown. Return plain text only.
38284
+
38285
+ Context:
38286
+ - Current branch: ${ctx.currentBranch}
38287
+ - Yesterday's plan: ${ctx.yesterdayPlan ?? "(not recorded)"}
38288
+ - Yesterday's commits: ${ctx.yesterdayCommits.length > 0 ? ctx.yesterdayCommits.map((c) => c.subject).join(", ") : "none"}
38289
+ - Today's commits so far: ${ctx.todayCommits.length > 0 ? ctx.todayCommits.map((c) => c.subject).join(", ") : "none"}
38290
+ - Uncommitted work in progress: ${ctx.hasUncommittedChanges ? "yes" : "no"}
38291
+ - Blockers: ${ctx.blockers.length > 0 ? ctx.blockers.map((b) => b.message).join("; ") : "none"}
38292
+
38293
+ Suggest what to focus on today:`;
38294
+ return getAISuggestion(prompt2);
38295
+ }
38296
+
38297
+ // src/lib/integrations/claude-code.ts
38298
+ import { readFile as readFile7 } from "fs/promises";
38299
+ import { existsSync as existsSync6 } from "fs";
38300
+ import { exec as exec5 } from "child_process";
38301
+ import { promisify as promisify5 } from "util";
38302
+ import { homedir as homedir2 } from "os";
38303
+ import path7 from "path";
38304
+ var execAsync5 = promisify5(exec5);
38305
+ function pathToProjectKey(absPath) {
38306
+ return absPath.replace(/\//g, "-");
38307
+ }
38308
+ async function getProjectSessionIds() {
38309
+ const projectKey = pathToProjectKey(process.cwd());
38310
+ const projectDir = path7.join(homedir2(), ".claude", "projects", projectKey);
38311
+ if (!existsSync6(projectDir))
38312
+ return [];
38313
+ try {
38314
+ const { stdout } = await execAsync5(`ls -1 "${projectDir}"`);
38315
+ return stdout.trim().split(`
38316
+ `).filter((name) => name && !name.endsWith(".jsonl"));
38317
+ } catch {
38318
+ return [];
38319
+ }
38320
+ }
38321
+ async function getClaudeCodeTodos() {
38322
+ const sessionIds = await getProjectSessionIds();
38323
+ if (sessionIds.length === 0)
38324
+ return [];
38325
+ const todosDir = path7.join(homedir2(), ".claude", "todos");
38326
+ if (!existsSync6(todosDir))
38327
+ return [];
38328
+ const todos = [];
38329
+ for (const sessionId of sessionIds) {
38330
+ try {
38331
+ const { stdout: files } = await execAsync5(`ls -1 "${todosDir}" | grep "^${sessionId}"`);
38332
+ for (const file of files.trim().split(`
38333
+ `).filter(Boolean)) {
38334
+ try {
38335
+ const raw = await readFile7(path7.join(todosDir, file), "utf-8");
38336
+ const parsed = JSON.parse(raw);
38337
+ if (Array.isArray(parsed)) {
38338
+ for (const todo of parsed) {
38339
+ if (todo.status === "pending" || todo.status === "in_progress") {
38340
+ todos.push(todo);
38341
+ }
38342
+ }
38343
+ }
38344
+ } catch {}
38345
+ }
38346
+ } catch {}
38347
+ }
38348
+ const seen = new Set;
38349
+ return todos.filter((t) => {
38350
+ if (seen.has(t.content))
38351
+ return false;
38352
+ seen.add(t.content);
38353
+ return true;
38354
+ });
38355
+ }
38356
+
38357
+ // src/lib/integrations/github.ts
38358
+ import { exec as exec6 } from "child_process";
38359
+ import { promisify as promisify6 } from "util";
38360
+ var execAsync6 = promisify6(exec6);
38361
+ async function getAssignedIssues() {
38362
+ try {
38363
+ await execAsync6("gh --version");
38364
+ } catch {
38365
+ return null;
38366
+ }
38367
+ try {
38368
+ const { stdout } = await execAsync6("gh issue list --assignee @me --state open --limit 20 --json number,title,url,labels");
38369
+ const raw = JSON.parse(stdout);
38370
+ return raw.map((i) => ({
38371
+ number: i.number,
38372
+ title: i.title,
38373
+ url: i.url,
38374
+ labels: i.labels.map((l) => l.name)
38375
+ }));
38376
+ } catch {
38377
+ return null;
38378
+ }
38379
+ }
38380
+
38381
+ // src/commands/workflow.ts
38382
+ var execAsync7 = promisify7(exec7);
37853
38383
  function workflowCommand(program2) {
37854
38384
  const workflow = program2.command("workflow").description("Run predefined workflow shortcuts");
37855
- workflow.command("pr-ready").description("Prepare branch for pull request").action(async () => {
38385
+ workflow.command("pr-ready").description("Prepare branch for pull request (fetch, rebase, push)").action(async () => {
37856
38386
  try {
37857
- console.log(`\uD83D\uDCE6 Preparing branch for PR...
38387
+ const config = await loadConfig();
38388
+ const mainBranch = config?.project?.mainBranch ?? "main";
38389
+ const repoState = await getRepoState();
38390
+ if (isProtectedBranch(repoState.currentBranch, config)) {
38391
+ console.error(source_default.red(` ✖ You're on ${source_default.bold(repoState.currentBranch)}.`) + source_default.dim(" Switch to a feature branch first."));
38392
+ process.exit(1);
38393
+ }
38394
+ console.log(`
38395
+ Preparing ${source_default.cyan(repoState.currentBranch)} for PR against ` + `${source_default.dim(mainBranch)}...
37858
38396
  `);
38397
+ let stashed = false;
38398
+ if (!repoState.isClean) {
38399
+ console.log(source_default.yellow(" ⚠ Uncommitted changes detected."));
38400
+ const { action } = await esm_default12.prompt([{
38401
+ type: "list",
38402
+ name: "action",
38403
+ message: "What would you like to do?",
38404
+ choices: [
38405
+ { name: "Stash changes, rebase, then restore them", value: "stash" },
38406
+ { name: "Abort", value: "abort" }
38407
+ ]
38408
+ }]);
38409
+ if (action === "abort") {
38410
+ console.log(" Aborted.");
38411
+ return;
38412
+ }
38413
+ displayStep('git stash push -m "hermes pr-ready auto-stash"');
38414
+ await executeGitCommand('git stash push -m "hermes pr-ready auto-stash"');
38415
+ stashed = true;
38416
+ console.log();
38417
+ }
37859
38418
  const steps = [
37860
38419
  { cmd: "git fetch origin", desc: "Fetching latest changes" },
37861
- { cmd: "git rebase origin/main", desc: "Rebasing on main" },
38420
+ { cmd: `git rebase origin/${mainBranch}`, desc: `Rebasing on ${mainBranch}` },
37862
38421
  { cmd: "git push --force-with-lease", desc: "Pushing changes safely" }
37863
38422
  ];
37864
38423
  for (const { cmd, desc } of steps) {
37865
- console.log(`
37866
- ${desc}...`);
38424
+ console.log(` ${desc}...`);
37867
38425
  displayStep(cmd);
37868
38426
  await executeGitCommand(cmd);
37869
38427
  }
38428
+ if (stashed) {
38429
+ console.log(`
38430
+ Restoring stashed changes...`);
38431
+ displayStep("git stash pop");
38432
+ await executeGitCommand("git stash pop");
38433
+ }
37870
38434
  displaySuccess("Branch ready for PR!");
37871
- console.log("\n\uD83D\uDCA1 Next: Create PR with `gh pr create` or use your Git hosting UI");
38435
+ console.log(source_default.dim(`
38436
+ Next: gh pr create or open your hosting UI`));
38437
+ console.log();
37872
38438
  } catch (error3) {
37873
- console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
38439
+ console.error(source_default.red(`
38440
+ ✖ ` + (error3 instanceof Error ? error3.message : error3)));
37874
38441
  process.exit(1);
37875
38442
  }
37876
38443
  });
37877
- workflow.command("daily-sync").description("Daily workflow: fetch, show status, suggest actions").action(async () => {
38444
+ workflow.command("daily-sync").description("Daily standup: what you did, what's next, and any blockers").action(async () => {
37878
38445
  try {
37879
- console.log(`\uD83C\uDF05 Running daily sync...
37880
- `);
37881
- displayStep("git fetch --all --prune");
37882
- await executeGitCommand("git fetch --all --prune");
37883
- const repoState = await getRepoState();
37884
38446
  console.log(`
37885
- \uD83D\uDCCA Status:`);
37886
- console.log(` Current branch: ${repoState.currentBranch}`);
37887
- console.log(` Status: ${repoState.isClean ? " Clean" : "⚠️ Uncommitted changes"}`);
37888
- if (repoState.behind > 0) {
37889
- console.log(` Behind main: ${repoState.behind} commits`);
37890
- console.log("\n\uD83D\uDCA1 Suggestion: Run `hermes sync` to catch up");
37891
- } else {
37892
- console.log(" ✅ Up to date with remote");
38447
+ ` + source_default.bold("Daily sync") + source_default.dim(` — gathering data...
38448
+ `));
38449
+ displayStep("git fetch --all --prune");
38450
+ await executeGitCommand("git fetch --all --prune").catch(() => {});
38451
+ const todayDate = todayKey();
38452
+ const yesterdayDate = lastWorkdayKey();
38453
+ const [repoState, stashList, mergedBranches, yesterdayCommits, todayCommits, yesterdayPlan] = await Promise.all([
38454
+ getRepoState(),
38455
+ execAsync7("git stash list").then((r) => r.stdout.trim().split(`
38456
+ `).filter(Boolean)).catch(() => []),
38457
+ execAsync7("git branch --merged HEAD").then((r) => r.stdout.trim().split(`
38458
+ `).map((b) => b.trim().replace(/^\* /, "")).filter((b) => b && !["main", "master", "staging", "production", "develop"].includes(b))).catch(() => []),
38459
+ getCommitsSince(yesterdayDate),
38460
+ getCommitsSince(todayDate),
38461
+ readPlan(yesterdayDate)
38462
+ ]);
38463
+ const blockers = await detectBlockers(repoState);
38464
+ let todayPlan = await readPlan(todayDate);
38465
+ if (!todayPlan) {
38466
+ console.log(source_default.yellow(` No plan set for today (${todayDate}).
38467
+ `));
38468
+ const [claudeTodos, githubIssues] = await Promise.all([
38469
+ getClaudeCodeTodos().catch(() => []),
38470
+ getAssignedIssues().catch(() => null)
38471
+ ]);
38472
+ const choices = [
38473
+ { name: "Type it myself", value: "manual" },
38474
+ { name: "Let AI suggest based on my repo", value: "ai" }
38475
+ ];
38476
+ if (claudeTodos.length > 0) {
38477
+ choices.push({
38478
+ name: `Import from Claude Code ${source_default.dim(`(${claudeTodos.length} pending task${claudeTodos.length !== 1 ? "s" : ""})`)}`,
38479
+ value: "claude"
38480
+ });
38481
+ }
38482
+ if (githubIssues !== null) {
38483
+ choices.push({
38484
+ name: `Import from GitHub issues ${source_default.dim(`(${githubIssues.length} open)`)}`,
38485
+ value: "github"
38486
+ });
38487
+ }
38488
+ choices.push({ name: "Skip for now", value: "skip" });
38489
+ const { planMode } = await esm_default12.prompt([{
38490
+ type: "list",
38491
+ name: "planMode",
38492
+ message: "What are you focusing on today?",
38493
+ choices
38494
+ }]);
38495
+ todayPlan = await resolvePlan(planMode, {
38496
+ repoState,
38497
+ yesterdayPlan,
38498
+ yesterdayCommits,
38499
+ todayCommits,
38500
+ blockers,
38501
+ claudeTodos,
38502
+ githubIssues: githubIssues ?? []
38503
+ });
38504
+ if (todayPlan) {
38505
+ await writePlan(todayDate, todayPlan);
38506
+ }
38507
+ console.log();
38508
+ }
38509
+ const yesterdaySection = yesterdayCommits.length > 0 ? yesterdayCommits.map((c) => `- ${c.subject}`).join(`
38510
+ `) : "(no commits found)";
38511
+ const todayCommitsSection = todayCommits.length > 0 ? todayCommits.map((c) => `- ${c.subject}`).join(`
38512
+ `) : "(no commits yet today)";
38513
+ const blockersSection = blockers.length > 0 ? blockers.map((b) => `- [${b.severity}] ${b.message}`).join(`
38514
+ `) : "(none)";
38515
+ const prompt2 = `You are a developer writing a daily standup update for their team.
38516
+
38517
+ Use the data below to write a clear, concise standup. Write it in first person.
38518
+ Keep each section to 1-3 sentences. Be specific — mention actual work items from the commit subjects.
38519
+ If there are no commits, infer from the plan what was likely being worked on.
38520
+
38521
+ ---
38522
+ YESTERDAY'S INTENDED PLAN:
38523
+ ${yesterdayPlan ?? "(not recorded)"}
38524
+
38525
+ COMMITS FROM YESTERDAY / LAST WORKDAY:
38526
+ ${yesterdaySection}
38527
+
38528
+ COMMITS FROM TODAY SO FAR:
38529
+ ${todayCommitsSection}
38530
+
38531
+ TODAY'S PLAN:
38532
+ ${todayPlan}
38533
+
38534
+ BLOCKERS / REPO STATE:
38535
+ ${blockersSection}
38536
+
38537
+ CURRENT BRANCH: ${repoState.currentBranch}
38538
+ ---
38539
+
38540
+ Format your response as exactly three labelled sections:
38541
+
38542
+ Yesterday: <what was accomplished>
38543
+ Today: <what will be worked on>
38544
+ Blockers: <blockers or "None">
38545
+
38546
+ No bullet points, no markdown, no extra commentary.`;
38547
+ console.log(` Generating standup...
38548
+ `);
38549
+ const standup = await getAISuggestion(prompt2);
38550
+ const divider = source_default.dim(" " + "─".repeat(52));
38551
+ console.log(divider);
38552
+ standup.split(`
38553
+ `).forEach((line) => {
38554
+ const trimmed = line.trim();
38555
+ if (!trimmed)
38556
+ return;
38557
+ const colonIdx = trimmed.indexOf(":");
38558
+ if (colonIdx > 0 && colonIdx < 15) {
38559
+ const label = trimmed.slice(0, colonIdx);
38560
+ const body = trimmed.slice(colonIdx + 1).trim();
38561
+ console.log(` ${source_default.bold.cyan(label + ":")} ${body}`);
38562
+ } else {
38563
+ console.log(" " + trimmed);
38564
+ }
38565
+ });
38566
+ console.log(divider);
38567
+ const notes = [];
38568
+ if (repoState.behind > 0)
38569
+ notes.push(source_default.yellow(`${repoState.behind} commits behind`) + source_default.dim(" — hermes sync"));
38570
+ if (repoState.ahead > 0)
38571
+ notes.push(source_default.cyan(`${repoState.ahead} commits ahead`) + source_default.dim(" — git push"));
38572
+ if (!repoState.isClean)
38573
+ notes.push(source_default.yellow("uncommitted changes"));
38574
+ if (mergedBranches.length > 0)
38575
+ notes.push(source_default.dim(`${mergedBranches.length} merged branch${mergedBranches.length !== 1 ? "es" : ""} to clean up — hermes workflow cleanup`));
38576
+ if (stashList.length > 0)
38577
+ notes.push(source_default.dim(`${stashList.length} stash${stashList.length !== 1 ? "es" : ""} saved`));
38578
+ if (notes.length > 0) {
38579
+ console.log(`
38580
+ ${source_default.bold("Repo notes")}`);
38581
+ notes.forEach((n) => console.log(` • ${n}`));
37893
38582
  }
37894
- displaySuccess("Daily sync complete!");
38583
+ console.log();
37895
38584
  } catch (error3) {
37896
- console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
38585
+ console.error(source_default.red(`
38586
+ ✖ ` + (error3 instanceof Error ? error3.message : error3)));
37897
38587
  process.exit(1);
37898
38588
  }
37899
38589
  });
37900
- workflow.command("quick-commit").description("Stage changed files and commit with an AI-generated message").option("-a, --all", "Stage all changes (default: only already-staged files)").action(async (options) => {
38590
+ workflow.command("quick-commit").description("Stage changed files and commit with an AI-generated message").option("-a, --all", "Stage all changes (default: only already-staged files)").option("-p, --push", "Push after committing").action(async (options) => {
37901
38591
  try {
37902
- console.log(`⚡ Quick commit...
38592
+ console.log(`
38593
+ Quick commit...
37903
38594
  `);
37904
38595
  const repoState = await getRepoState();
37905
38596
  if (repoState.isClean) {
37906
- console.log(" Nothing to commit");
38597
+ console.log(" " + source_default.green("✓") + " Nothing to commit");
37907
38598
  return;
37908
38599
  }
37909
38600
  if (options.all) {
@@ -37913,14 +38604,14 @@ ${desc}...`);
37913
38604
  const diff = await executeGitCommand("git diff --cached --stat");
37914
38605
  const diffFull = await executeGitCommand("git diff --cached");
37915
38606
  if (!diff.trim()) {
37916
- console.log("⚠️ No staged changes. Use --all to stage everything, or `git add` specific files first.");
38607
+ console.log(" " + source_default.yellow("⚠") + " No staged changes. Use --all to stage everything, or stage files first.");
37917
38608
  return;
37918
38609
  }
38610
+ console.log(" Staged changes:");
38611
+ diff.trim().split(`
38612
+ `).forEach((l) => console.log(" " + source_default.dim(l)));
37919
38613
  console.log(`
37920
- \uD83D\uDCDD Staged changes:`);
37921
- console.log(diff);
37922
- console.log(`
37923
- \uD83E\uDD16 Generating commit message...`);
38614
+ Generating commit message...`);
37924
38615
  const message = await getAISuggestion(`Generate a concise git commit message for these changes.
37925
38616
 
37926
38617
  Rules:
@@ -37933,68 +38624,219 @@ Rules:
37933
38624
  Diff:
37934
38625
  ${diffFull.slice(0, 8000)}`);
37935
38626
  console.log(`
37936
- \uD83D\uDCAC Proposed message:
37937
- ${source_default.cyan(message)}
38627
+ Proposed: ${source_default.cyan(message)}
37938
38628
  `);
37939
- const { action } = await esm_default12.prompt([
37940
- {
37941
- type: "list",
37942
- name: "action",
37943
- message: "Commit with this message?",
37944
- choices: [
37945
- { name: "Yes, commit", value: "commit" },
37946
- { name: "Edit message", value: "edit" },
37947
- { name: "Cancel", value: "cancel" }
37948
- ]
37949
- }
37950
- ]);
38629
+ const { action } = await esm_default12.prompt([{
38630
+ type: "list",
38631
+ name: "action",
38632
+ message: "Commit with this message?",
38633
+ choices: [
38634
+ { name: "Yes, commit", value: "commit" },
38635
+ { name: "Edit message", value: "edit" },
38636
+ { name: "Cancel", value: "cancel" }
38637
+ ]
38638
+ }]);
37951
38639
  if (action === "cancel") {
37952
- console.log("Cancelled. Changes remain staged.");
38640
+ console.log(" Cancelled. Changes remain staged.");
37953
38641
  return;
37954
38642
  }
37955
38643
  let finalMessage = message;
37956
38644
  if (action === "edit") {
37957
- const { edited } = await esm_default12.prompt([
37958
- {
37959
- type: "input",
37960
- name: "edited",
37961
- message: "Commit message:",
37962
- default: message
37963
- }
37964
- ]);
38645
+ const { edited } = await esm_default12.prompt([{
38646
+ type: "input",
38647
+ name: "edited",
38648
+ message: "Commit message:",
38649
+ default: message
38650
+ }]);
37965
38651
  finalMessage = edited;
37966
38652
  }
37967
38653
  displayStep(`git commit -m "${finalMessage}"`);
37968
38654
  await executeGitCommand(`git commit -m ${JSON.stringify(finalMessage)}`);
37969
38655
  displaySuccess("Committed!");
38656
+ if (options.push) {
38657
+ console.log();
38658
+ const freshState = await getRepoState();
38659
+ const pushCmd = freshState.remoteTracking ? "git push" : `git push -u origin ${freshState.currentBranch}`;
38660
+ displayStep(pushCmd);
38661
+ await executeGitCommand(pushCmd);
38662
+ displaySuccess("Pushed!");
38663
+ }
38664
+ console.log();
37970
38665
  } catch (error3) {
37971
- console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
38666
+ console.error(source_default.red(`
38667
+ ✖ ` + (error3 instanceof Error ? error3.message : error3)));
38668
+ process.exit(1);
38669
+ }
38670
+ });
38671
+ workflow.command("cleanup").description("Delete branches that have already been merged").option("--remote", "Also delete the corresponding remote tracking branches").action(async (options) => {
38672
+ try {
38673
+ const config = await loadConfig();
38674
+ const mainBranch = config?.project?.mainBranch ?? "main";
38675
+ const protected_ = config?.project?.protectedBranches ?? ["main", "master", "staging", "production"];
38676
+ console.log(`
38677
+ Scanning for merged branches...
38678
+ `);
38679
+ displayStep(`git fetch origin --prune`);
38680
+ await executeGitCommand("git fetch origin --prune").catch(() => {});
38681
+ const { stdout: branchOutput } = await execAsync7(`git branch --merged ${mainBranch}`);
38682
+ const repoState = await getRepoState();
38683
+ const merged = branchOutput.split(`
38684
+ `).map((b) => b.trim().replace(/^\* /, "")).filter((b) => b && !protected_.includes(b) && b !== repoState.currentBranch);
38685
+ if (merged.length === 0) {
38686
+ console.log(" " + source_default.green("✓") + " No merged branches to clean up.");
38687
+ console.log();
38688
+ return;
38689
+ }
38690
+ console.log(` Found ${source_default.yellow(merged.length)} merged branch${merged.length !== 1 ? "es" : ""}:
38691
+ `);
38692
+ merged.forEach((b) => console.log(` ${source_default.dim("•")} ${b}`));
38693
+ console.log();
38694
+ const { selected } = await esm_default12.prompt([{
38695
+ type: "checkbox",
38696
+ name: "selected",
38697
+ message: "Select branches to delete:",
38698
+ choices: merged.map((b) => ({ name: b, value: b, checked: true }))
38699
+ }]);
38700
+ if (selected.length === 0) {
38701
+ console.log(" Nothing selected. Exiting.");
38702
+ return;
38703
+ }
38704
+ for (const branch of selected) {
38705
+ displayStep(`git branch -d ${branch}`);
38706
+ await executeGitCommand(`git branch -d ${branch}`);
38707
+ if (options.remote) {
38708
+ try {
38709
+ displayStep(`git push origin --delete ${branch}`);
38710
+ await executeGitCommand(`git push origin --delete ${branch}`);
38711
+ } catch {
38712
+ console.log(source_default.dim(` (remote branch ${branch} not found — skipping)`));
38713
+ }
38714
+ }
38715
+ }
38716
+ displaySuccess(`Deleted ${selected.length} branch${selected.length !== 1 ? "es" : ""}!`);
38717
+ console.log();
38718
+ } catch (error3) {
38719
+ console.error(source_default.red(`
38720
+ ✖ ` + (error3 instanceof Error ? error3.message : error3)));
37972
38721
  process.exit(1);
37973
38722
  }
37974
38723
  });
37975
38724
  workflow.command("list").description("List available workflow shortcuts").action(async () => {
37976
38725
  const config = await loadConfig();
37977
- console.log(`\uD83D\uDD04 Available Workflows:
38726
+ console.log(`
38727
+ ` + source_default.bold("Built-in workflows") + `
37978
38728
  `);
37979
- console.log("Built-in:");
37980
- console.log(" • pr-ready - Sync, rebase, and push for PR");
37981
- console.log(" daily-sync - Fetch updates and show status");
37982
- console.log(" quick-commit - Stage and commit all changes");
37983
- if (config?.workflows) {
38729
+ console.log(` ${source_default.cyan("pr-ready")} Rebase on main and push — safe PR prep with stash guard`);
38730
+ console.log(` ${source_default.cyan("daily-sync")} Fetch, show status, surface what to act on`);
38731
+ console.log(` ${source_default.cyan("quick-commit")} AI commit message; add ${source_default.dim("--push")} to commit + push`);
38732
+ console.log(` ${source_default.cyan("cleanup")} Delete merged branches; add ${source_default.dim("--remote")} to also clean remote`);
38733
+ if (config?.workflows && Object.keys(config.workflows).length > 0) {
37984
38734
  console.log(`
37985
- Project-specific:`);
37986
- Object.keys(config.workflows).forEach((name) => {
37987
- const steps = config.workflows[name];
37988
- console.log(` ${name.padEnd(15)} - ${steps.join(" → ")}`);
37989
- });
38735
+ ` + source_default.bold("Project workflows") + source_default.dim(" (.hermes/config.json)") + `
38736
+ `);
38737
+ for (const [name, steps] of Object.entries(config.workflows)) {
38738
+ console.log(` ${source_default.cyan(name.padEnd(14))} ${source_default.dim(steps.join(" → "))}`);
38739
+ }
37990
38740
  } else {
37991
- console.log("\n\uD83D\uDCA1 Run `hermes init` to define custom workflows");
38741
+ console.log(`
38742
+ ` + source_default.dim("No project workflows defined. Run hermes init to set them up."));
37992
38743
  }
38744
+ console.log();
37993
38745
  });
37994
38746
  }
38747
+ async function resolvePlan(mode, ctx) {
38748
+ if (mode === "skip")
38749
+ return null;
38750
+ if (mode === "manual") {
38751
+ const { typed } = await esm_default12.prompt([{
38752
+ type: "input",
38753
+ name: "typed",
38754
+ message: "Today's plan:",
38755
+ validate: (v) => v.trim().length > 0 || "Please enter something"
38756
+ }]);
38757
+ return typed.trim();
38758
+ }
38759
+ if (mode === "ai") {
38760
+ process.stdout.write(source_default.dim(`
38761
+ Thinking...\r`));
38762
+ const suggested = await suggestTodayPlan({
38763
+ currentBranch: ctx.repoState.currentBranch,
38764
+ yesterdayPlan: ctx.yesterdayPlan,
38765
+ yesterdayCommits: ctx.yesterdayCommits,
38766
+ todayCommits: ctx.todayCommits,
38767
+ hasUncommittedChanges: ctx.repoState.hasUncommittedChanges,
38768
+ blockers: ctx.blockers
38769
+ });
38770
+ process.stdout.write(" ".repeat(30) + "\r");
38771
+ return confirmOrEdit(suggested);
38772
+ }
38773
+ if (mode === "claude") {
38774
+ const { selected } = await esm_default12.prompt([{
38775
+ type: "checkbox",
38776
+ name: "selected",
38777
+ message: "Select tasks to include in today's plan:",
38778
+ choices: ctx.claudeTodos.map((t) => ({
38779
+ name: `${t.status === "in_progress" ? source_default.yellow("(in progress) ") : ""}${t.content}` + source_default.dim(` [${t.priority}]`),
38780
+ value: t.content,
38781
+ checked: t.status === "in_progress"
38782
+ }))
38783
+ }]);
38784
+ if (selected.length === 0)
38785
+ return null;
38786
+ const plan = selected.join("; ");
38787
+ return confirmOrEdit(plan);
38788
+ }
38789
+ if (mode === "github") {
38790
+ if (ctx.githubIssues.length === 0) {
38791
+ console.log(source_default.dim(" No open issues assigned to you."));
38792
+ return null;
38793
+ }
38794
+ const { selected } = await esm_default12.prompt([{
38795
+ type: "checkbox",
38796
+ name: "selected",
38797
+ message: "Select issues to include in today's plan:",
38798
+ choices: ctx.githubIssues.map((i) => ({
38799
+ name: `#${i.number} ${i.title}` + (i.labels.length > 0 ? source_default.dim(` [${i.labels.join(", ")}]`) : ""),
38800
+ value: `#${i.number} ${i.title}`,
38801
+ checked: true
38802
+ }))
38803
+ }]);
38804
+ if (selected.length === 0)
38805
+ return null;
38806
+ const plan = selected.join("; ");
38807
+ return confirmOrEdit(plan);
38808
+ }
38809
+ return null;
38810
+ }
38811
+ async function confirmOrEdit(suggestion) {
38812
+ console.log(`
38813
+ ${source_default.bold("Plan:")} ${source_default.cyan(suggestion)}
38814
+ `);
38815
+ const { action } = await esm_default12.prompt([{
38816
+ type: "list",
38817
+ name: "action",
38818
+ message: "Use this?",
38819
+ choices: [
38820
+ { name: "Yes", value: "accept" },
38821
+ { name: "Edit", value: "edit" },
38822
+ { name: "Skip", value: "skip" }
38823
+ ]
38824
+ }]);
38825
+ if (action === "accept")
38826
+ return suggestion;
38827
+ if (action === "skip")
38828
+ return null;
38829
+ const { edited } = await esm_default12.prompt([{
38830
+ type: "input",
38831
+ name: "edited",
38832
+ message: "Today's plan:",
38833
+ default: suggestion
38834
+ }]);
38835
+ return edited.trim() || null;
38836
+ }
37995
38837
 
37996
38838
  // src/commands/config.ts
37997
- var VALID_PROVIDERS = ["anthropic", "openai", "gemini"];
38839
+ var VALID_PROVIDERS = ["anthropic", "openai", "gemini", "claude-code"];
37998
38840
  var KEY_ALIASES = {
37999
38841
  provider: "provider",
38000
38842
  "anthropic-key": "anthropicApiKey",
@@ -38002,7 +38844,7 @@ var KEY_ALIASES = {
38002
38844
  "gemini-key": "geminiApiKey"
38003
38845
  };
38004
38846
  var KEY_DESCRIPTIONS = {
38005
- provider: "Default AI provider (anthropic | openai | gemini)",
38847
+ provider: "Default AI provider (anthropic | openai | gemini | claude-code)",
38006
38848
  anthropicApiKey: "Anthropic API key",
38007
38849
  openaiApiKey: "OpenAI API key",
38008
38850
  geminiApiKey: "Gemini API key"
@@ -38100,55 +38942,71 @@ function configCommand(program2) {
38100
38942
  console.log(source_default.dim(` ${GLOBAL_CONFIG_PATH}
38101
38943
  `));
38102
38944
  const globalConfig = await loadGlobalConfig();
38103
- const { provider } = await esm_default12.prompt([
38104
- {
38105
- type: "list",
38106
- name: "provider",
38107
- message: "Which AI provider do you want to use?",
38108
- choices: [
38109
- { name: "Anthropic (Claude)", value: "anthropic" },
38110
- { name: "OpenAI (GPT-4o)", value: "openai" },
38111
- { name: "Google (Gemini)", value: "gemini" },
38112
- { name: "Auto-detect (use whichever key is available)", value: "" }
38113
- ],
38114
- default: globalConfig.provider || ""
38115
- }
38945
+ const [claudeAvailable, codexAvailable] = await Promise.all([
38946
+ isClaudeCodeAvailable(),
38947
+ isCodexAvailable()
38116
38948
  ]);
38949
+ const cliChoices = [
38950
+ claudeAvailable ? { name: `Claude Code CLI ${source_default.green("(detected)")}`, value: "claude-code" } : { name: `Claude Code CLI ${source_default.dim("(not found)")}`, value: "claude-code", disabled: true },
38951
+ codexAvailable ? { name: `OpenAI Codex CLI ${source_default.green("(detected)")}`, value: "codex" } : { name: `OpenAI Codex CLI ${source_default.dim("(not found)")}`, value: "codex", disabled: true }
38952
+ ];
38953
+ const providerChoices = [
38954
+ ...cliChoices,
38955
+ { name: "Anthropic API (ANTHROPIC_API_KEY)", value: "anthropic" },
38956
+ { name: "OpenAI API (OPENAI_API_KEY)", value: "openai" },
38957
+ { name: "Google Gemini (GEMINI_API_KEY)", value: "gemini" },
38958
+ { name: "Auto-detect (use whichever is available)", value: "" }
38959
+ ];
38960
+ const defaultProvider = globalConfig.provider || (claudeAvailable ? "claude-code" : codexAvailable ? "codex" : "");
38961
+ const { provider } = await esm_default12.prompt([{
38962
+ type: "list",
38963
+ name: "provider",
38964
+ message: "Which AI provider do you want to use?",
38965
+ choices: providerChoices,
38966
+ default: defaultProvider
38967
+ }]);
38117
38968
  if (provider) {
38118
38969
  globalConfig.provider = provider;
38119
38970
  } else {
38120
38971
  delete globalConfig.provider;
38121
38972
  }
38122
- const askProviders = provider ? [provider] : VALID_PROVIDERS;
38973
+ if (provider === "claude-code" || provider === "codex") {
38974
+ await saveGlobalConfig(globalConfig);
38975
+ const label = provider === "claude-code" ? "claude CLI" : "codex CLI";
38976
+ console.log(`
38977
+ ${source_default.green("✓")} Provider set to ${source_default.cyan(provider)}`);
38978
+ console.log(source_default.dim(` Hermes will use the ${label} — no API key required.
38979
+ `));
38980
+ return;
38981
+ }
38982
+ const apiProviders = provider ? [provider] : ["anthropic", "openai", "gemini"];
38123
38983
  const keyPrompts = {
38124
38984
  anthropic: { field: "anthropicApiKey", envVar: "ANTHROPIC_API_KEY", hint: "sk-ant-..." },
38125
38985
  openai: { field: "openaiApiKey", envVar: "OPENAI_API_KEY", hint: "sk-..." },
38126
38986
  gemini: { field: "geminiApiKey", envVar: "GEMINI_API_KEY", hint: "AIza..." }
38127
38987
  };
38128
- for (const p of askProviders) {
38988
+ for (const p of apiProviders) {
38129
38989
  const { field, envVar, hint } = keyPrompts[p];
38130
38990
  const existing = globalConfig[field];
38131
38991
  const fromEnv = process.env[envVar];
38132
38992
  if (fromEnv) {
38133
- console.log(source_default.dim(` ${envVar} already set in environment, skipping.`));
38993
+ console.log(source_default.dim(` ${envVar} already set in environment skipping.`));
38134
38994
  continue;
38135
38995
  }
38136
- const { key: key2 } = await esm_default12.prompt([
38137
- {
38138
- type: "password",
38139
- name: "key",
38140
- message: `${p.charAt(0).toUpperCase() + p.slice(1)} API key (${hint}):`,
38141
- default: existing ? "(keep existing)" : "",
38142
- mask: "*"
38143
- }
38144
- ]);
38996
+ const { key: key2 } = await esm_default12.prompt([{
38997
+ type: "password",
38998
+ name: "key",
38999
+ message: `${p.charAt(0).toUpperCase() + p.slice(1)} API key (${hint}):`,
39000
+ default: existing ? "(keep existing)" : "",
39001
+ mask: "*"
39002
+ }]);
38145
39003
  if (key2 && key2 !== "(keep existing)") {
38146
39004
  globalConfig[field] = key2;
38147
39005
  }
38148
39006
  }
38149
39007
  await saveGlobalConfig(globalConfig);
38150
39008
  console.log(`
38151
- ${source_default.green("✓")} Configuration saved to ${source_default.dim(GLOBAL_CONFIG_PATH)}`);
39009
+ ${source_default.green("✓")} Configuration saved to ${source_default.dim(GLOBAL_CONFIG_PATH)}`);
38152
39010
  console.log(source_default.dim(` File permissions set to 600 (owner read/write only)
38153
39011
  `));
38154
39012
  console.log(` Run ${source_default.cyan("hermes config list")} to verify your setup.`);
@@ -38167,15 +39025,15 @@ Valid keys:`));
38167
39025
  }
38168
39026
 
38169
39027
  // src/commands/guard.ts
38170
- import { writeFile as writeFile5, readFile as readFile5, chmod as chmod2, mkdir as mkdir4 } from "fs/promises";
38171
- import { existsSync as existsSync5 } from "fs";
38172
- import { exec as exec4 } from "child_process";
38173
- import { promisify as promisify4 } from "util";
39028
+ import { writeFile as writeFile6, readFile as readFile8, chmod as chmod2, mkdir as mkdir5 } from "fs/promises";
39029
+ import { existsSync as existsSync7 } from "fs";
39030
+ import { exec as exec9 } from "child_process";
39031
+ import { promisify as promisify9 } from "util";
38174
39032
 
38175
39033
  // src/lib/secrets.ts
38176
- import { exec as exec3 } from "child_process";
38177
- import { promisify as promisify3 } from "util";
38178
- var execAsync3 = promisify3(exec3);
39034
+ import { exec as exec8 } from "child_process";
39035
+ import { promisify as promisify8 } from "util";
39036
+ var execAsync8 = promisify8(exec8);
38179
39037
  var BLOCKED_FILENAMES = [
38180
39038
  { pattern: /^\.env$/, description: "Environment variable file" },
38181
39039
  { pattern: /^\.env\..+/, description: "Environment variable file" },
@@ -38304,13 +39162,13 @@ var SECRET_PATTERNS = [
38304
39162
  }
38305
39163
  ];
38306
39164
  async function getStagedFiles() {
38307
- const { stdout } = await execAsync3("git diff --cached --name-only --diff-filter=ACMR");
39165
+ const { stdout } = await execAsync8("git diff --cached --name-only --diff-filter=ACMR");
38308
39166
  return stdout.trim().split(`
38309
39167
  `).filter(Boolean);
38310
39168
  }
38311
39169
  async function readStagedFile(file) {
38312
39170
  try {
38313
- const { stdout } = await execAsync3(`git show ":${file}"`, {
39171
+ const { stdout } = await execAsync8(`git show ":${file}"`, {
38314
39172
  maxBuffer: 2 * 1024 * 1024
38315
39173
  });
38316
39174
  return stdout;
@@ -38408,7 +39266,7 @@ async function scanStagedFiles() {
38408
39266
  }
38409
39267
 
38410
39268
  // src/commands/guard.ts
38411
- var execAsync4 = promisify4(exec4);
39269
+ var execAsync9 = promisify9(exec9);
38412
39270
  function guardCommand(program2) {
38413
39271
  const guard = program2.command("guard").description("Scan staged files for secrets and sensitive content before committing");
38414
39272
  guard.option("--hook", "Run in non-interactive hook mode (exit 1 on blockers)").action(async (options) => {
@@ -38416,13 +39274,13 @@ function guardCommand(program2) {
38416
39274
  });
38417
39275
  guard.command("install-hook").description("Install hermes guard as a git pre-commit hook").action(async () => {
38418
39276
  try {
38419
- const gitDir = await execAsync4("git rev-parse --git-dir").then((r) => r.stdout.trim()).catch(() => null);
39277
+ const gitDir = await execAsync9("git rev-parse --git-dir").then((r) => r.stdout.trim()).catch(() => null);
38420
39278
  if (!gitDir) {
38421
39279
  console.error(source_default.red("❌ Not a git repository"));
38422
39280
  process.exit(1);
38423
39281
  }
38424
39282
  const hooksDir = `${gitDir}/hooks`;
38425
- await mkdir4(hooksDir, { recursive: true });
39283
+ await mkdir5(hooksDir, { recursive: true });
38426
39284
  const hookPath = `${hooksDir}/pre-commit`;
38427
39285
  const hookScript = [
38428
39286
  "#!/bin/sh",
@@ -38431,19 +39289,19 @@ function guardCommand(program2) {
38431
39289
  ].join(`
38432
39290
  `) + `
38433
39291
  `;
38434
- if (existsSync5(hookPath)) {
38435
- const existing = await readFile5(hookPath, "utf-8");
39292
+ if (existsSync7(hookPath)) {
39293
+ const existing = await readFile8(hookPath, "utf-8");
38436
39294
  if (existing.includes("hermes guard")) {
38437
39295
  console.log(source_default.dim("Hook already installed."));
38438
39296
  return;
38439
39297
  }
38440
- await writeFile5(hookPath, existing.trimEnd() + `
39298
+ await writeFile6(hookPath, existing.trimEnd() + `
38441
39299
 
38442
39300
  ` + hookScript.split(`
38443
39301
  `).slice(1).join(`
38444
39302
  `));
38445
39303
  } else {
38446
- await writeFile5(hookPath, hookScript);
39304
+ await writeFile6(hookPath, hookScript);
38447
39305
  }
38448
39306
  await chmod2(hookPath, 493);
38449
39307
  console.log(`${source_default.green("✓")} Pre-commit hook installed at ${source_default.dim(hookPath)}`);
@@ -38455,17 +39313,17 @@ function guardCommand(program2) {
38455
39313
  });
38456
39314
  guard.command("uninstall-hook").description("Remove hermes guard from the git pre-commit hook").action(async () => {
38457
39315
  try {
38458
- const gitDir = await execAsync4("git rev-parse --git-dir").then((r) => r.stdout.trim()).catch(() => null);
39316
+ const gitDir = await execAsync9("git rev-parse --git-dir").then((r) => r.stdout.trim()).catch(() => null);
38459
39317
  if (!gitDir) {
38460
39318
  console.error(source_default.red("❌ Not a git repository"));
38461
39319
  process.exit(1);
38462
39320
  }
38463
39321
  const hookPath = `${gitDir}/hooks/pre-commit`;
38464
- if (!existsSync5(hookPath)) {
39322
+ if (!existsSync7(hookPath)) {
38465
39323
  console.log(source_default.dim("No pre-commit hook found."));
38466
39324
  return;
38467
39325
  }
38468
- const content = await readFile5(hookPath, "utf-8");
39326
+ const content = await readFile8(hookPath, "utf-8");
38469
39327
  if (!content.includes("hermes guard")) {
38470
39328
  console.log(source_default.dim("hermes guard is not in the pre-commit hook."));
38471
39329
  return;
@@ -38482,7 +39340,7 @@ function guardCommand(program2) {
38482
39340
  await unlink(hookPath);
38483
39341
  console.log(`${source_default.green("✓")} Pre-commit hook removed.`);
38484
39342
  } else {
38485
- await writeFile5(hookPath, cleaned);
39343
+ await writeFile6(hookPath, cleaned);
38486
39344
  console.log(`${source_default.green("✓")} hermes guard removed from pre-commit hook.`);
38487
39345
  }
38488
39346
  } catch (error3) {
@@ -38555,7 +39413,7 @@ async function runScan(hookMode) {
38555
39413
  }
38556
39414
  if (action === "unstage") {
38557
39415
  for (const f of result.blocked) {
38558
- await execAsync4(`git restore --staged "${f.file}"`);
39416
+ await execAsync9(`git restore --staged "${f.file}"`);
38559
39417
  console.log(source_default.dim(` Unstaged: ${f.file}`));
38560
39418
  }
38561
39419
  console.log(source_default.yellow(`
@@ -38672,9 +39530,14 @@ Be direct and specific. 3-4 sentences per finding max. No preamble.`);
38672
39530
  }
38673
39531
 
38674
39532
  // src/commands/update.ts
38675
- import { exec as exec5 } from "child_process";
38676
- import { promisify as promisify5 } from "util";
38677
- var execAsync5 = promisify5(exec5);
39533
+ import { exec as exec10 } from "child_process";
39534
+ import { promisify as promisify10 } from "util";
39535
+ import { writeFile as writeFile7, mkdir as mkdir6 } from "fs/promises";
39536
+ import { homedir as homedir3 } from "os";
39537
+ import path8 from "path";
39538
+ var CACHE_DIR = path8.join(homedir3(), ".hermes", "cache");
39539
+ var CACHE_FILE = path8.join(CACHE_DIR, "update-check.json");
39540
+ var execAsync10 = promisify10(exec10);
38678
39541
  var PACKAGE_NAME = "hermes-git";
38679
39542
  function updateCommand(program2, currentVersion) {
38680
39543
  program2.command("update").description("Update hermes to the latest version").option("--check", "Check for updates without installing").option("--bun", "Use bun instead of npm to install").action(async (options) => {
@@ -38704,7 +39567,7 @@ function updateCommand(program2, currentVersion) {
38704
39567
  const installCmd = buildInstallCommand(pm);
38705
39568
  console.log(` Running: ${source_default.cyan(installCmd)}
38706
39569
  `);
38707
- const { stdout, stderr } = await execAsync5(installCmd, { timeout: 120000 });
39570
+ const { stdout, stderr } = await execAsync10(installCmd, { timeout: 120000 });
38708
39571
  const output = (stdout + stderr).trim();
38709
39572
  if (output) {
38710
39573
  output.split(`
@@ -38713,6 +39576,10 @@ function updateCommand(program2, currentVersion) {
38713
39576
  });
38714
39577
  console.log();
38715
39578
  }
39579
+ try {
39580
+ await mkdir6(CACHE_DIR, { recursive: true });
39581
+ await writeFile7(CACHE_FILE, JSON.stringify({ lastChecked: Date.now(), latestVersion: latest }));
39582
+ } catch {}
38716
39583
  console.log(source_default.green(" ✓ Updated to ") + source_default.bold(`v${latest}`) + source_default.dim(" Restart your terminal if the version does not change."));
38717
39584
  } catch (error3) {
38718
39585
  console.error(source_default.red(`
@@ -38751,20 +39618,20 @@ async function detectPackageManager() {
38751
39618
  return "bun";
38752
39619
  }
38753
39620
  try {
38754
- const { stdout } = await execAsync5("bun --version", { timeout: 3000 });
39621
+ const { stdout } = await execAsync10("bun --version", { timeout: 3000 });
38755
39622
  if (stdout.trim()) {
38756
39623
  try {
38757
- const { stdout: list } = await execAsync5("bun pm ls -g 2>/dev/null", { timeout: 3000 });
39624
+ const { stdout: list } = await execAsync10("bun pm ls -g 2>/dev/null", { timeout: 3000 });
38758
39625
  if (list.includes(PACKAGE_NAME))
38759
39626
  return "bun";
38760
39627
  } catch {}
38761
39628
  }
38762
39629
  } catch {}
38763
39630
  try {
38764
- const { stdout } = await execAsync5("pnpm --version", { timeout: 3000 });
39631
+ const { stdout } = await execAsync10("pnpm --version", { timeout: 3000 });
38765
39632
  if (stdout.trim()) {
38766
39633
  try {
38767
- const { stdout: list } = await execAsync5("pnpm list -g --depth=0 2>/dev/null", { timeout: 3000 });
39634
+ const { stdout: list } = await execAsync10("pnpm list -g --depth=0 2>/dev/null", { timeout: 3000 });
38768
39635
  if (list.includes(PACKAGE_NAME))
38769
39636
  return "pnpm";
38770
39637
  } catch {}
@@ -38784,12 +39651,13 @@ function buildInstallCommand(pm) {
38784
39651
  }
38785
39652
 
38786
39653
  // src/commands/commit.ts
38787
- import { exec as exec6 } from "child_process";
38788
- import { promisify as promisify6 } from "util";
38789
- var execAsync6 = promisify6(exec6);
39654
+ import { exec as exec11, execFile } from "child_process";
39655
+ import { promisify as promisify11 } from "util";
39656
+ var execAsync11 = promisify11(exec11);
39657
+ var execFileAsync = promisify11(execFile);
38790
39658
  async function getStagedFiles2() {
38791
39659
  try {
38792
- const { stdout } = await execAsync6("git diff --cached --name-status");
39660
+ const { stdout } = await execAsync11("git diff --cached --name-status");
38793
39661
  return stdout.trim().split(`
38794
39662
  `).filter(Boolean).map((line) => {
38795
39663
  const [status, ...rest] = line.split("\t");
@@ -38801,12 +39669,12 @@ async function getStagedFiles2() {
38801
39669
  }
38802
39670
  async function getUnstagedFiles() {
38803
39671
  try {
38804
- const { stdout } = await execAsync6("git status --porcelain");
39672
+ const { stdout } = await execAsync11("git status --porcelain");
38805
39673
  return stdout.trim().split(`
38806
39674
  `).filter(Boolean).map((line) => {
38807
39675
  const status = line.slice(0, 2).trim();
38808
- const path5 = line.slice(3).trim();
38809
- return { status, path: path5 };
39676
+ const path9 = line.slice(3).trim();
39677
+ return { status, path: path9 };
38810
39678
  }).filter(({ status }) => status === "??" || status[0] === " " || status.length === 1);
38811
39679
  } catch {
38812
39680
  return [];
@@ -38814,7 +39682,7 @@ async function getUnstagedFiles() {
38814
39682
  }
38815
39683
  async function getRecentBranchCommits(n = 5) {
38816
39684
  try {
38817
- const { stdout } = await execAsync6(`git log --oneline -${n} --no-merges 2>/dev/null`);
39685
+ const { stdout } = await execAsync11(`git log --oneline -${n} --no-merges 2>/dev/null`);
38818
39686
  return stdout.trim();
38819
39687
  } catch {
38820
39688
  return "";
@@ -38884,8 +39752,8 @@ function commitCommand(program2) {
38884
39752
  try {
38885
39753
  const repoState = await getRepoState();
38886
39754
  if (options.all) {
38887
- displayStep("git add -u");
38888
- await execAsync6("git add -u");
39755
+ displayStep("git add -A");
39756
+ await execAsync11("git add -A");
38889
39757
  }
38890
39758
  const staged = await getStagedFiles2();
38891
39759
  if (staged.length === 0) {
@@ -38916,8 +39784,8 @@ function commitCommand(program2) {
38916
39784
  console.log();
38917
39785
  console.log(` Analyzing... ${source_default.dim(`[${name} / ${model}]`)}`);
38918
39786
  const [stagedDiff, stagedStat] = await Promise.all([
38919
- execAsync6("git diff --cached").then((r) => r.stdout),
38920
- execAsync6("git diff --cached --stat").then((r) => r.stdout)
39787
+ execAsync11("git diff --cached").then((r) => r.stdout),
39788
+ execAsync11("git diff --cached --stat").then((r) => r.stdout)
38921
39789
  ]);
38922
39790
  const analysis = await analyzeChanges(repoState.currentBranch, stagedDiff, stagedStat, recentCommits);
38923
39791
  console.log();
@@ -39044,14 +39912,14 @@ function commitCommand(program2) {
39044
39912
  for (const f of files) {
39045
39913
  const cmd = `git add ${JSON.stringify(f)}`;
39046
39914
  displayStep(cmd);
39047
- await execAsync6(cmd);
39915
+ await execAsync11(cmd);
39048
39916
  }
39049
39917
  console.log(source_default.dim(`
39050
39918
  Staged ${files.length} file(s). Re-running analysis...
39051
39919
  `));
39052
39920
  const [newDiff, newStat] = await Promise.all([
39053
- execAsync6("git diff --cached").then((r) => r.stdout),
39054
- execAsync6("git diff --cached --stat").then((r) => r.stdout)
39921
+ execAsync11("git diff --cached").then((r) => r.stdout),
39922
+ execAsync11("git diff --cached --stat").then((r) => r.stdout)
39055
39923
  ]);
39056
39924
  const newAnalysis = await analyzeChanges(repoState.currentBranch, newDiff, newStat, recentCommits);
39057
39925
  analysis.summary = newAnalysis.summary;
@@ -39084,9 +39952,9 @@ function commitCommand(program2) {
39084
39952
 
39085
39953
  ${finalBody}` : finalMessage;
39086
39954
  displayStep(`git commit -m "${finalMessage}"${finalBody ? " (with body)" : ""}`);
39087
- await execAsync6(`git commit -m ${JSON.stringify(fullMessage)}`);
39955
+ await execFileAsync("git", ["commit", "-m", fullMessage]);
39088
39956
  displaySuccess("Committed!");
39089
- const { stdout: hash } = await execAsync6("git rev-parse --short HEAD");
39957
+ const { stdout: hash } = await execAsync11("git rev-parse --short HEAD");
39090
39958
  console.log(source_default.dim(` ${hash.trim()} ${finalMessage}
39091
39959
  `));
39092
39960
  await recordCommand("commit", [], Date.now() - start, true);
@@ -39102,14 +39970,14 @@ ${finalBody}` : finalMessage;
39102
39970
  }
39103
39971
 
39104
39972
  // src/lib/update-notifier.ts
39105
- import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
39106
- import { existsSync as existsSync6 } from "fs";
39107
- import { homedir as homedir2 } from "os";
39108
- import path5 from "path";
39973
+ import { readFile as readFile9, writeFile as writeFile8, mkdir as mkdir7 } from "fs/promises";
39974
+ import { existsSync as existsSync8 } from "fs";
39975
+ import { homedir as homedir4 } from "os";
39976
+ import path9 from "path";
39109
39977
  var PACKAGE_NAME2 = "hermes-git";
39110
39978
  var CHECK_INTERVAL = 12 * 60 * 60 * 1000;
39111
- var CACHE_DIR = path5.join(homedir2(), ".hermes", "cache");
39112
- var CACHE_FILE = path5.join(CACHE_DIR, "update-check.json");
39979
+ var CACHE_DIR2 = path9.join(homedir4(), ".hermes", "cache");
39980
+ var CACHE_FILE2 = path9.join(CACHE_DIR2, "update-check.json");
39113
39981
  async function fetchDistTags() {
39114
39982
  try {
39115
39983
  const res = await fetch(`https://registry.npmjs.org/-/package/${PACKAGE_NAME2}/dist-tags`, { signal: AbortSignal.timeout(5000) });
@@ -39122,17 +39990,17 @@ async function fetchDistTags() {
39122
39990
  }
39123
39991
  async function readCache() {
39124
39992
  try {
39125
- if (!existsSync6(CACHE_FILE))
39993
+ if (!existsSync8(CACHE_FILE2))
39126
39994
  return null;
39127
- return JSON.parse(await readFile6(CACHE_FILE, "utf-8"));
39995
+ return JSON.parse(await readFile9(CACHE_FILE2, "utf-8"));
39128
39996
  } catch {
39129
39997
  return null;
39130
39998
  }
39131
39999
  }
39132
40000
  async function writeCache(cache) {
39133
40001
  try {
39134
- await mkdir5(CACHE_DIR, { recursive: true });
39135
- await writeFile6(CACHE_FILE, JSON.stringify(cache, null, 2));
40002
+ await mkdir7(CACHE_DIR2, { recursive: true });
40003
+ await writeFile8(CACHE_FILE2, JSON.stringify(cache, null, 2));
39136
40004
  } catch {}
39137
40005
  }
39138
40006
  function compareVersions2(a, b) {
@@ -39201,7 +40069,7 @@ async function checkForUpdates(currentVersion) {
39201
40069
  }
39202
40070
 
39203
40071
  // src/lib/banner.ts
39204
- import { readFileSync, existsSync as existsSync7 } from "fs";
40072
+ import { readFileSync, existsSync as existsSync9 } from "fs";
39205
40073
  var ART_LINES = [
39206
40074
  "██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗",
39207
40075
  "██║ ██║██╔════╝██╔══██╗████╗ ████║██╔════╝██╔════╝",
@@ -39243,7 +40111,7 @@ var BUILTIN_WORKFLOWS = [
39243
40111
  ];
39244
40112
  function loadProjectWorkflows() {
39245
40113
  try {
39246
- if (!existsSync7(".hermes/config.json"))
40114
+ if (!existsSync9(".hermes/config.json"))
39247
40115
  return [];
39248
40116
  const config = JSON.parse(readFileSync(".hermes/config.json", "utf-8"));
39249
40117
  const workflows = config?.workflows;
@@ -39280,8 +40148,10 @@ function printWorkflows() {
39280
40148
  }
39281
40149
 
39282
40150
  // src/index.ts
40151
+ import { createRequire as createRequire2 } from "module";
39283
40152
  var program2 = new Command;
39284
- var CURRENT_VERSION = "0.3.6";
40153
+ var __require2 = createRequire2(import.meta.url);
40154
+ var { version: CURRENT_VERSION } = __require2("../package.json");
39285
40155
  program2.name("hermes").description("Intent-driven Git, guided by AI").version(CURRENT_VERSION).action(() => {
39286
40156
  printBanner(CURRENT_VERSION);
39287
40157
  printWorkflows();