hermes-git 0.2.3 → 0.2.5

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 (3) hide show
  1. package/README.md +95 -6
  2. package/dist/index.js +377 -195
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -469,25 +469,97 @@ Hermes is **terminal-native** and **explainable**:
469
469
  ### Copilot CLI Not Found
470
470
 
471
471
  ```bash
472
- Error: GitHub Copilot CLI not found
472
+ GitHub Copilot CLI is not installed
473
473
  ```
474
474
 
475
- **Solution:** Install from [github.com/github/copilot-cli](https://github.com/github/copilot-cli)
475
+ **Problem:** Hermes requires the standalone Copilot CLI (not the deprecated `gh copilot` extension).
476
+
477
+ **Solution:**
478
+
479
+ ```bash
480
+ # Install via npm (recommended)
481
+ npm install -g @github/copilot
482
+
483
+ # Or via Homebrew (macOS/Linux)
484
+ brew install github/gh/copilot-cli
485
+
486
+ # Verify installation
487
+ copilot --version
488
+ ```
489
+
490
+ **Note:** The old `gh copilot` extension was deprecated on October 25, 2025. If you have it installed, you'll see a warning—install the new standalone CLI instead.
491
+
492
+ ---
476
493
 
477
494
  ### Authentication Required
478
495
 
479
496
  ```bash
480
- Error: authentication required
497
+ GitHub Copilot CLI is not authenticated
498
+ ```
499
+
500
+ **Problem:** Copilot CLI needs GitHub authentication to work.
501
+
502
+ **Solution (choose one):**
503
+
504
+ **Option 1: OAuth (Recommended)**
505
+ ```bash
506
+ # Start Copilot CLI interactively
507
+ copilot
508
+
509
+ # In the Copilot prompt, type:
510
+ /login
511
+
512
+ # Follow the browser OAuth flow
513
+ ```
514
+
515
+ **Option 2: Personal Access Token**
516
+ ```bash
517
+ # Create a token at: https://github.com/settings/tokens
518
+ # Enable "Copilot Requests" permission under "Permissions"
519
+
520
+ # Set the token in your environment
521
+ export GH_TOKEN="your_token_here"
522
+
523
+ # Add to your shell profile (~/.bashrc, ~/.zshrc) to persist:
524
+ echo 'export GH_TOKEN="your_token_here"' >> ~/.bashrc
525
+ ```
526
+
527
+ **Option 3: GitHub CLI Integration**
528
+ ```bash
529
+ # If you already have gh CLI installed and authenticated
530
+ gh auth login
531
+
532
+ # Copilot CLI will automatically use those credentials
481
533
  ```
482
534
 
483
- **Solution:** Run `copilot login` and complete OAuth flow
535
+ **Requirements:**
536
+ - Active GitHub Copilot subscription ([subscribe here](https://github.com/features/copilot/plans))
537
+ - If using organization Copilot, "Copilot CLI" must be enabled in org settings
538
+
539
+ ---
540
+
541
+ ### Subscription Issues
542
+
543
+ ```bash
544
+ ❌ GitHub Copilot subscription required
545
+ ```
546
+
547
+ **Solution:**
548
+ - Verify you have an active Copilot subscription at [github.com/settings/copilot](https://github.com/settings/copilot)
549
+ - If using organization Copilot, check with your admin that "Copilot CLI" is enabled
550
+ - Individual plans start at $10/month, or free for students/OSS maintainers
551
+
552
+ ---
484
553
 
485
554
  ### Slow Responses
486
555
 
487
556
  If AI responses are slow, try:
488
- - Use a faster model: `--model claude-sonnet-4.5` (default)
557
+ - Use a faster model: `--model claude-sonnet-4.5` (default, fastest)
489
558
  - Check internet connection
490
559
  - Verify Copilot subscription is active
560
+ - Try running `copilot` directly to test connection
561
+
562
+ ---
491
563
 
492
564
  ### Command Not Working
493
565
 
@@ -495,16 +567,33 @@ If AI responses are slow, try:
495
567
  # Check Hermes version
496
568
  hermes --version
497
569
 
498
- # Verify Copilot CLI
570
+ # Verify Copilot CLI is installed and authenticated
499
571
  copilot --version
572
+ copilot -p "test" --allow-all-tools -s
573
+
574
+ # Check git is working
575
+ git status
500
576
 
501
577
  # Test in a clean repo
502
578
  cd /tmp && git init test && cd test
579
+ git config user.name "Test" && git config user.email "test@example.com"
503
580
  hermes plan "test"
504
581
  ```
505
582
 
506
583
  ---
507
584
 
585
+ ### Getting Help
586
+
587
+ If you're still having issues:
588
+ 1. Check the [GitHub Copilot CLI documentation](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli)
589
+ 2. Verify Copilot CLI works independently: `copilot -p "hello" -s`
590
+ 3. Open an issue at [github.com/simandebvu/hermes-cli/issues](https://github.com/simandebvu/hermes-cli/issues) with:
591
+ - Output of `hermes --version`
592
+ - Output of `copilot --version`
593
+ - The exact error message you're seeing
594
+
595
+ ---
596
+
508
597
  ## Contributing
509
598
 
510
599
  Contributions are welcome! Please see [DEVELOPMENT.md](DEVELOPMENT.md) for:
package/dist/index.js CHANGED
@@ -21939,200 +21939,6 @@ var {
21939
21939
  // src/lib/copilot.ts
21940
21940
  import { exec } from "child_process";
21941
21941
  import { promisify } from "util";
21942
- var execAsync = promisify(exec);
21943
- async function getCopilotSuggestion(prompt, options = {}) {
21944
- try {
21945
- await checkCopilotCLI();
21946
- const {
21947
- model = "claude-sonnet-4.5",
21948
- allowAllTools = true,
21949
- silent = true
21950
- } = options;
21951
- const args = [
21952
- "-p",
21953
- `"${prompt.replace(/"/g, "\\\"")}"`,
21954
- "--model",
21955
- model
21956
- ];
21957
- if (allowAllTools) {
21958
- args.push("--allow-all-tools");
21959
- }
21960
- if (silent) {
21961
- args.push("-s");
21962
- }
21963
- const command = `copilot ${args.join(" ")}`;
21964
- const { stdout, stderr } = await execAsync(command, {
21965
- maxBuffer: 10 * 1024 * 1024,
21966
- timeout: 120000
21967
- });
21968
- if (stderr && !stdout) {
21969
- throw new Error(`Copilot CLI error: ${stderr}`);
21970
- }
21971
- return stdout.trim();
21972
- } catch (error) {
21973
- if (error.code === "ENOENT" || error.message.includes("not found")) {
21974
- throw new Error(`GitHub Copilot CLI not found. Install it from: https://github.com/github/copilot-cli
21975
- ` + "Then authenticate with: copilot login");
21976
- }
21977
- throw new Error(`Copilot CLI error: ${error.message}`);
21978
- }
21979
- }
21980
- async function checkCopilotCLI() {
21981
- try {
21982
- const { stdout } = await execAsync("copilot --version");
21983
- if (!stdout.includes("GitHub Copilot CLI")) {
21984
- throw new Error("Invalid Copilot CLI installation");
21985
- }
21986
- } catch (error) {
21987
- if (error.code === "ENOENT") {
21988
- throw new Error(`GitHub Copilot CLI is not installed.
21989
- ` + `Install from: https://github.com/github/copilot-cli
21990
- ` + "Or with npm: npm install -g @githubnext/github-copilot-cli");
21991
- }
21992
- throw error;
21993
- }
21994
- }
21995
- async function analyzeCopilotGitState(repoState, intent) {
21996
- const prompt = `
21997
- You are a Git safety expert. Analyze this repository state and provide guidance.
21998
-
21999
- Repository State:
22000
- ${JSON.stringify(repoState, null, 2)}
22001
-
22002
- User Intent: "${intent}"
22003
-
22004
- Provide a clear, actionable analysis including:
22005
- 1. Current state summary (clean/dirty, branch position, conflicts, etc.)
22006
- 2. Recommended approach with reasoning
22007
- 3. Potential risks and safety considerations
22008
- 4. Step-by-step Git commands (if applicable)
22009
-
22010
- Be specific about WHY each step is safe and necessary.
22011
- Format your response in clear sections.
22012
- `;
22013
- return getCopilotSuggestion(prompt);
22014
- }
22015
- async function getCopilotGitPlan(repoState, intent, outputFormat = "json") {
22016
- const formatInstruction = outputFormat === "json" ? "Return your response as RAW JSON ONLY (no markdown code blocks, no backticks, just pure JSON) with fields: explanation, commands[], risks[], safetyNotes[]" : "Return your response as formatted text with clear sections";
22017
- const prompt = `
22018
- You are a Git automation expert. Create a safe execution plan.
22019
-
22020
- Repository State:
22021
- ${JSON.stringify(repoState, null, 2)}
22022
-
22023
- User wants to: "${intent}"
22024
-
22025
- ${formatInstruction}
22026
-
22027
- Ensure all Git commands are:
22028
- - Safe (non-destructive when possible)
22029
- - Ordered correctly
22030
- - Include necessary error handling
22031
- - Explain the purpose of each command
22032
- `;
22033
- const response = await getCopilotSuggestion(prompt);
22034
- if (outputFormat === "json") {
22035
- return stripMarkdownCodeBlock(response);
22036
- }
22037
- return response;
22038
- }
22039
- function stripMarkdownCodeBlock(text) {
22040
- const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
22041
- if (codeBlockMatch) {
22042
- return codeBlockMatch[1].trim();
22043
- }
22044
- return text.trim();
22045
- }
22046
-
22047
- // src/lib/git.ts
22048
- import { exec as exec2 } from "child_process";
22049
- import { promisify as promisify2 } from "util";
22050
- var execAsync2 = promisify2(exec2);
22051
- async function getRepoState() {
22052
- try {
22053
- const [
22054
- currentBranch,
22055
- status,
22056
- tracking,
22057
- rebaseStatus,
22058
- mergeStatus,
22059
- cherryPickStatus
22060
- ] = await Promise.all([
22061
- execAsync2("git rev-parse --abbrev-ref HEAD").then((r) => r.stdout.trim()),
22062
- execAsync2("git status --porcelain").then((r) => r.stdout.trim()),
22063
- execAsync2("git rev-parse --abbrev-ref @{upstream}").then((r) => r.stdout.trim()).catch(() => null),
22064
- execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -d ${r.stdout.trim()}/rebase-merge`)).then(() => true).catch(() => false),
22065
- execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -f ${r.stdout.trim()}/MERGE_HEAD`)).then(() => true).catch(() => false),
22066
- execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -f ${r.stdout.trim()}/CHERRY_PICK_HEAD`)).then(() => true).catch(() => false)
22067
- ]);
22068
- const statusLines = status.split(`
22069
- `).filter((line) => line);
22070
- const hasUncommittedChanges = statusLines.some((line) => line.startsWith(" M") || line.startsWith("M "));
22071
- const hasUntrackedFiles = statusLines.some((line) => line.startsWith("??"));
22072
- let ahead = 0;
22073
- let behind = 0;
22074
- if (tracking) {
22075
- const aheadBehind = await execAsync2(`git rev-list --left-right --count ${tracking}...HEAD`).then((r) => r.stdout.trim().split("\t")).catch(() => ["0", "0"]);
22076
- behind = parseInt(aheadBehind[0], 10);
22077
- ahead = parseInt(aheadBehind[1], 10);
22078
- }
22079
- return {
22080
- currentBranch,
22081
- isClean: statusLines.length === 0,
22082
- hasUncommittedChanges,
22083
- hasUntrackedFiles,
22084
- isInRebase: rebaseStatus,
22085
- isInMerge: mergeStatus,
22086
- isInCherryPick: cherryPickStatus,
22087
- remoteTracking: tracking || undefined,
22088
- ahead,
22089
- behind
22090
- };
22091
- } catch (error) {
22092
- throw new Error("Not a Git repository or Git is not installed");
22093
- }
22094
- }
22095
- async function isGitInstalled() {
22096
- try {
22097
- await execAsync2("git --version");
22098
- return true;
22099
- } catch {
22100
- return false;
22101
- }
22102
- }
22103
- async function isGitRepository() {
22104
- try {
22105
- await execAsync2("git rev-parse --git-dir");
22106
- return true;
22107
- } catch {
22108
- return false;
22109
- }
22110
- }
22111
- async function hasCommits() {
22112
- try {
22113
- await execAsync2("git rev-parse HEAD");
22114
- return true;
22115
- } catch {
22116
- return false;
22117
- }
22118
- }
22119
- async function executeGitCommand(command) {
22120
- try {
22121
- const { stdout, stderr } = await execAsync2(command);
22122
- return stdout || stderr;
22123
- } catch (error) {
22124
- throw new Error(`Git command failed: ${error.message}`);
22125
- }
22126
- }
22127
- async function getConflictedFiles() {
22128
- try {
22129
- const { stdout } = await execAsync2("git diff --name-only --diff-filter=U");
22130
- return stdout.trim().split(`
22131
- `).filter((line) => line);
22132
- } catch {
22133
- return [];
22134
- }
22135
- }
22136
21942
 
22137
21943
  // node_modules/chalk/source/vendor/ansi-styles/index.js
22138
21944
  var ANSI_BACKGROUND_OFFSET = 10;
@@ -22623,6 +22429,309 @@ var chalk = createChalk();
22623
22429
  var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
22624
22430
  var source_default = chalk;
22625
22431
 
22432
+ // src/lib/copilot.ts
22433
+ var execAsync = promisify(exec);
22434
+ async function getCopilotSuggestion(prompt, options = {}) {
22435
+ try {
22436
+ await checkCopilotCLI();
22437
+ const {
22438
+ model = "claude-sonnet-4.5",
22439
+ allowAllTools = true,
22440
+ silent = true
22441
+ } = options;
22442
+ const args = [
22443
+ "-p",
22444
+ `"${prompt.replace(/"/g, "\\\"")}"`,
22445
+ "--model",
22446
+ model
22447
+ ];
22448
+ if (allowAllTools) {
22449
+ args.push("--allow-all-tools");
22450
+ }
22451
+ if (silent) {
22452
+ args.push("-s");
22453
+ }
22454
+ const command = `copilot ${args.join(" ")}`;
22455
+ const { stdout, stderr } = await execAsync(command, {
22456
+ maxBuffer: 10 * 1024 * 1024,
22457
+ timeout: 120000
22458
+ });
22459
+ if (stderr && !stdout) {
22460
+ throw new Error(`Copilot CLI error: ${stderr}`);
22461
+ }
22462
+ return stdout.trim();
22463
+ } catch (error) {
22464
+ if (error.code === "ENOENT" || error.message.includes("not found")) {
22465
+ throw new CopilotNotInstalledError;
22466
+ }
22467
+ const errorMessage = (error.message || "").toLowerCase();
22468
+ const stderr = (error.stderr || "").toLowerCase();
22469
+ const combinedError = errorMessage + " " + stderr;
22470
+ if (combinedError.includes("no authentication information") || combinedError.includes("authentication required") || combinedError.includes("not authenticated") || combinedError.includes("copilot_github_token") || combinedError.includes("gh_token") || combinedError.includes("github_token")) {
22471
+ throw new CopilotNotAuthenticatedError(error.message);
22472
+ }
22473
+ if (combinedError.includes("subscription") || combinedError.includes("not authorized") || combinedError.includes("access denied")) {
22474
+ throw new Error(source_default.red(`❌ GitHub Copilot subscription required
22475
+
22476
+ `) + source_default.white(`Hermes requires an active GitHub Copilot subscription.
22477
+ `) + source_default.gray(`Subscribe at: https://github.com/features/copilot/plans
22478
+
22479
+ `) + source_default.dim("Original error: " + error.message));
22480
+ }
22481
+ throw new Error(source_default.red(`❌ Copilot CLI error
22482
+
22483
+ `) + source_default.white(error.message) + `
22484
+
22485
+ ` + source_default.dim("If this persists, try running: copilot --version"));
22486
+ }
22487
+ }
22488
+ async function checkCopilotCLI() {
22489
+ await checkForDeprecatedGhCopilot();
22490
+ try {
22491
+ const { stdout } = await execAsync("copilot --version", { timeout: 5000 });
22492
+ if (!stdout.includes("GitHub Copilot CLI")) {
22493
+ throw new Error("Invalid Copilot CLI installation");
22494
+ }
22495
+ } catch (error) {
22496
+ if (error.code === "ENOENT" || error.message.includes("not found")) {
22497
+ throw new CopilotNotInstalledError;
22498
+ }
22499
+ throw error;
22500
+ }
22501
+ }
22502
+ async function checkForDeprecatedGhCopilot() {
22503
+ try {
22504
+ const { stdout } = await execAsync("gh copilot --version", { timeout: 5000 });
22505
+ if (stdout) {
22506
+ console.warn(source_default.yellow(`
22507
+ ⚠️ Warning: You have the deprecated "gh copilot" extension installed.`));
22508
+ console.warn(source_default.yellow(" The GitHub Copilot extension was deprecated on October 25, 2025."));
22509
+ console.warn(source_default.yellow(` Hermes requires the new standalone "copilot" CLI.
22510
+ `));
22511
+ }
22512
+ } catch {}
22513
+ }
22514
+
22515
+ class CopilotNotInstalledError extends Error {
22516
+ constructor() {
22517
+ const message = [
22518
+ "",
22519
+ source_default.red("❌ GitHub Copilot CLI is not installed"),
22520
+ "",
22521
+ source_default.bold("Hermes requires the standalone Copilot CLI (not the deprecated gh copilot extension)."),
22522
+ "",
22523
+ source_default.cyan("\uD83D\uDCE6 Installation Options:"),
22524
+ "",
22525
+ source_default.white(" npm: ") + source_default.gray("npm install -g @github/copilot"),
22526
+ source_default.white(" Homebrew: ") + source_default.gray("brew install github/gh/copilot-cli"),
22527
+ source_default.white(" Manual: ") + source_default.gray("https://github.com/github/copilot-cli/releases"),
22528
+ "",
22529
+ source_default.cyan("\uD83D\uDD10 After installation, authenticate with:"),
22530
+ "",
22531
+ source_default.white(" copilot") + source_default.gray(" (then run /login command)"),
22532
+ "",
22533
+ source_default.dim("\uD83D\uDCDA Learn more: https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"),
22534
+ ""
22535
+ ].join(`
22536
+ `);
22537
+ super(message);
22538
+ this.name = "CopilotNotInstalledError";
22539
+ }
22540
+ }
22541
+
22542
+ class CopilotNotAuthenticatedError extends Error {
22543
+ constructor(originalError) {
22544
+ const ghAuthStatus = checkGhAuthenticationSync();
22545
+ const message = [
22546
+ "",
22547
+ source_default.red("❌ GitHub Copilot CLI is not authenticated"),
22548
+ "",
22549
+ ghAuthStatus.isAuthenticated ? source_default.yellow("⚠️ Note: You have gh CLI authenticated, but Copilot CLI needs separate authentication.") : source_default.bold("Hermes needs access to GitHub Copilot to provide AI-powered Git guidance."),
22550
+ "",
22551
+ source_default.cyan("\uD83D\uDD10 Quick Fix:"),
22552
+ "",
22553
+ source_default.white(" Run: ") + source_default.bold.green("copilot"),
22554
+ source_default.gray(" Then type: ") + source_default.bold("/login"),
22555
+ source_default.gray(" Follow the OAuth flow in your browser"),
22556
+ "",
22557
+ source_default.cyan("\uD83D\uDD10 Alternative - Personal Access Token:"),
22558
+ "",
22559
+ source_default.gray(" 1. Create token: https://github.com/settings/tokens/new"),
22560
+ source_default.gray(' 2. Enable "Copilot Requests" permission'),
22561
+ source_default.gray(" 3. Run: ") + source_default.white('export GH_TOKEN="your_token_here"'),
22562
+ source_default.gray(" 4. Add to ~/.bashrc or ~/.zshrc to persist"),
22563
+ "",
22564
+ source_default.cyan("\uD83D\uDCCB Requirements:"),
22565
+ source_default.gray(" • Active GitHub Copilot subscription"),
22566
+ source_default.gray(' • If using organization Copilot, "Copilot CLI" must be enabled in org settings'),
22567
+ "",
22568
+ source_default.dim("\uD83D\uDCDA Learn more: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli"),
22569
+ ""
22570
+ ].join(`
22571
+ `);
22572
+ super(message);
22573
+ this.name = "CopilotNotAuthenticatedError";
22574
+ }
22575
+ }
22576
+ function checkGhAuthenticationSync() {
22577
+ try {
22578
+ const { execSync } = __require("child_process");
22579
+ const result = execSync("gh auth status 2>&1", {
22580
+ encoding: "utf-8",
22581
+ timeout: 2000
22582
+ });
22583
+ const isAuthenticated = result.includes("Logged in to github.com");
22584
+ const usernameMatch = result.match(/Logged in to github\.com as (\S+)/);
22585
+ return {
22586
+ isAuthenticated,
22587
+ username: usernameMatch?.[1]
22588
+ };
22589
+ } catch {
22590
+ return { isAuthenticated: false };
22591
+ }
22592
+ }
22593
+ async function analyzeCopilotGitState(repoState, intent) {
22594
+ const prompt = `
22595
+ You are a Git safety expert. Analyze this repository state and provide guidance.
22596
+
22597
+ Repository State:
22598
+ ${JSON.stringify(repoState, null, 2)}
22599
+
22600
+ User Intent: "${intent}"
22601
+
22602
+ Provide a clear, actionable analysis including:
22603
+ 1. Current state summary (clean/dirty, branch position, conflicts, etc.)
22604
+ 2. Recommended approach with reasoning
22605
+ 3. Potential risks and safety considerations
22606
+ 4. Step-by-step Git commands (if applicable)
22607
+
22608
+ Be specific about WHY each step is safe and necessary.
22609
+ Format your response in clear sections.
22610
+ `;
22611
+ return getCopilotSuggestion(prompt);
22612
+ }
22613
+ async function getCopilotGitPlan(repoState, intent, outputFormat = "json") {
22614
+ const formatInstruction = outputFormat === "json" ? "Return your response as RAW JSON ONLY (no markdown code blocks, no backticks, just pure JSON) with fields: explanation, commands[], risks[], safetyNotes[]" : "Return your response as formatted text with clear sections";
22615
+ const prompt = `
22616
+ You are a Git automation expert. Create a safe execution plan.
22617
+
22618
+ Repository State:
22619
+ ${JSON.stringify(repoState, null, 2)}
22620
+
22621
+ User wants to: "${intent}"
22622
+
22623
+ ${formatInstruction}
22624
+
22625
+ Ensure all Git commands are:
22626
+ - Safe (non-destructive when possible)
22627
+ - Ordered correctly
22628
+ - Include necessary error handling
22629
+ - Explain the purpose of each command
22630
+ `;
22631
+ const response = await getCopilotSuggestion(prompt);
22632
+ if (outputFormat === "json") {
22633
+ return stripMarkdownCodeBlock(response);
22634
+ }
22635
+ return response;
22636
+ }
22637
+ function stripMarkdownCodeBlock(text) {
22638
+ const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
22639
+ if (codeBlockMatch) {
22640
+ return codeBlockMatch[1].trim();
22641
+ }
22642
+ return text.trim();
22643
+ }
22644
+
22645
+ // src/lib/git.ts
22646
+ import { exec as exec2 } from "child_process";
22647
+ import { promisify as promisify2 } from "util";
22648
+ var execAsync2 = promisify2(exec2);
22649
+ async function getRepoState() {
22650
+ try {
22651
+ const [
22652
+ currentBranch,
22653
+ status,
22654
+ tracking,
22655
+ rebaseStatus,
22656
+ mergeStatus,
22657
+ cherryPickStatus
22658
+ ] = await Promise.all([
22659
+ execAsync2("git rev-parse --abbrev-ref HEAD").then((r) => r.stdout.trim()),
22660
+ execAsync2("git status --porcelain").then((r) => r.stdout.trim()),
22661
+ execAsync2("git rev-parse --abbrev-ref @{upstream}").then((r) => r.stdout.trim()).catch(() => null),
22662
+ execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -d ${r.stdout.trim()}/rebase-merge`)).then(() => true).catch(() => false),
22663
+ execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -f ${r.stdout.trim()}/MERGE_HEAD`)).then(() => true).catch(() => false),
22664
+ execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -f ${r.stdout.trim()}/CHERRY_PICK_HEAD`)).then(() => true).catch(() => false)
22665
+ ]);
22666
+ const statusLines = status.split(`
22667
+ `).filter((line) => line);
22668
+ const hasUncommittedChanges = statusLines.some((line) => line.startsWith(" M") || line.startsWith("M "));
22669
+ const hasUntrackedFiles = statusLines.some((line) => line.startsWith("??"));
22670
+ let ahead = 0;
22671
+ let behind = 0;
22672
+ if (tracking) {
22673
+ const aheadBehind = await execAsync2(`git rev-list --left-right --count ${tracking}...HEAD`).then((r) => r.stdout.trim().split("\t")).catch(() => ["0", "0"]);
22674
+ behind = parseInt(aheadBehind[0], 10);
22675
+ ahead = parseInt(aheadBehind[1], 10);
22676
+ }
22677
+ return {
22678
+ currentBranch,
22679
+ isClean: statusLines.length === 0,
22680
+ hasUncommittedChanges,
22681
+ hasUntrackedFiles,
22682
+ isInRebase: rebaseStatus,
22683
+ isInMerge: mergeStatus,
22684
+ isInCherryPick: cherryPickStatus,
22685
+ remoteTracking: tracking || undefined,
22686
+ ahead,
22687
+ behind
22688
+ };
22689
+ } catch (error) {
22690
+ throw new Error("Not a Git repository or Git is not installed");
22691
+ }
22692
+ }
22693
+ async function isGitInstalled() {
22694
+ try {
22695
+ await execAsync2("git --version");
22696
+ return true;
22697
+ } catch {
22698
+ return false;
22699
+ }
22700
+ }
22701
+ async function isGitRepository() {
22702
+ try {
22703
+ await execAsync2("git rev-parse --git-dir");
22704
+ return true;
22705
+ } catch {
22706
+ return false;
22707
+ }
22708
+ }
22709
+ async function hasCommits() {
22710
+ try {
22711
+ await execAsync2("git rev-parse HEAD");
22712
+ return true;
22713
+ } catch {
22714
+ return false;
22715
+ }
22716
+ }
22717
+ async function executeGitCommand(command) {
22718
+ try {
22719
+ const { stdout, stderr } = await execAsync2(command);
22720
+ return stdout || stderr;
22721
+ } catch (error) {
22722
+ throw new Error(`Git command failed: ${error.message}`);
22723
+ }
22724
+ }
22725
+ async function getConflictedFiles() {
22726
+ try {
22727
+ const { stdout } = await execAsync2("git diff --name-only --diff-filter=U");
22728
+ return stdout.trim().split(`
22729
+ `).filter((line) => line);
22730
+ } catch {
22731
+ return [];
22732
+ }
22733
+ }
22734
+
22626
22735
  // src/lib/display.ts
22627
22736
  function displayPlan(suggestion, repoState) {
22628
22737
  console.log(source_default.bold.cyan(`\uD83D\uDCCB Recommended Plan:
@@ -25809,9 +25918,81 @@ Project-specific:`);
25809
25918
  });
25810
25919
  }
25811
25920
 
25921
+ // src/lib/update-notifier.ts
25922
+ import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
25923
+ import { existsSync as existsSync4 } from "fs";
25924
+ import { homedir } from "os";
25925
+ import path2 from "path";
25926
+ var PACKAGE_NAME = "hermes-git";
25927
+ var CHECK_INTERVAL = 24 * 60 * 60 * 1000;
25928
+ var CACHE_DIR = path2.join(homedir(), ".hermes", "cache");
25929
+ var CACHE_FILE = path2.join(CACHE_DIR, "update-check.json");
25930
+ async function getLatestVersion() {
25931
+ try {
25932
+ const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`);
25933
+ if (!response.ok)
25934
+ return null;
25935
+ const data = await response.json();
25936
+ return data.version || null;
25937
+ } catch {
25938
+ return null;
25939
+ }
25940
+ }
25941
+ async function readCache() {
25942
+ try {
25943
+ if (!existsSync4(CACHE_FILE))
25944
+ return null;
25945
+ const content = await readFile4(CACHE_FILE, "utf-8");
25946
+ return JSON.parse(content);
25947
+ } catch {
25948
+ return null;
25949
+ }
25950
+ }
25951
+ async function writeCache(cache) {
25952
+ try {
25953
+ await mkdir3(CACHE_DIR, { recursive: true });
25954
+ await writeFile4(CACHE_FILE, JSON.stringify(cache, null, 2));
25955
+ } catch {}
25956
+ }
25957
+ function isNewerVersion(current, latest) {
25958
+ const currentParts = current.split(".").map(Number);
25959
+ const latestParts = latest.split(".").map(Number);
25960
+ for (let i = 0;i < 3; i++) {
25961
+ if (latestParts[i] > currentParts[i])
25962
+ return true;
25963
+ if (latestParts[i] < currentParts[i])
25964
+ return false;
25965
+ }
25966
+ return false;
25967
+ }
25968
+ async function checkForUpdates(currentVersion) {
25969
+ try {
25970
+ const cache = await readCache();
25971
+ const now = Date.now();
25972
+ const shouldCheck = !cache || now - cache.lastChecked > CHECK_INTERVAL;
25973
+ let latestVersion = cache?.latestVersion || null;
25974
+ if (shouldCheck) {
25975
+ latestVersion = await getLatestVersion();
25976
+ await writeCache({
25977
+ lastChecked: now,
25978
+ latestVersion: latestVersion || undefined
25979
+ });
25980
+ }
25981
+ if (latestVersion && isNewerVersion(currentVersion, latestVersion)) {
25982
+ console.log(source_default.yellow(`
25983
+ ┌─────────────────────────────────────────────────────┐`));
25984
+ console.log(source_default.yellow("│") + " " + source_default.bold("Update available!") + " " + source_default.dim(currentVersion) + " → " + source_default.green.bold(latestVersion) + " " + source_default.yellow("│"));
25985
+ console.log(source_default.yellow("│") + " Run: " + source_default.cyan("npm install -g hermes-git@latest") + " " + source_default.yellow("│"));
25986
+ console.log(source_default.yellow(`└─────────────────────────────────────────────────────┘
25987
+ `));
25988
+ }
25989
+ } catch {}
25990
+ }
25991
+
25812
25992
  // src/index.ts
25813
25993
  var program2 = new Command;
25814
- program2.name("hermes").description("\uD83E\uDEBD Intent-driven Git, guided by AI").version("0.2.3");
25994
+ var CURRENT_VERSION = "0.2.4";
25995
+ program2.name("hermes").description("\uD83E\uDEBD Intent-driven Git, guided by AI").version(CURRENT_VERSION);
25815
25996
  initCommand(program2);
25816
25997
  planCommand(program2);
25817
25998
  startCommand(program2);
@@ -25821,4 +26002,5 @@ conflictCommand(program2);
25821
26002
  worktreeCommand(program2);
25822
26003
  statsCommand(program2);
25823
26004
  workflowCommand(program2);
26005
+ checkForUpdates(CURRENT_VERSION).catch(() => {});
25824
26006
  program2.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermes-git",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Intent-driven Git, guided by AI. Turn natural language into safe, explainable Git operations.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",