githits 0.4.7 → 0.4.9

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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "GitHits plugins for Claude Code - code examples from global open source",
9
- "version": "0.4.7"
9
+ "version": "0.4.9"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "githits",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Code examples from global open source for developers and AI assistants",
5
5
  "author": {
6
6
  "name": "GitHits"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "githits",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Code examples from global open source for developers and AI assistants",
5
5
  "author": {
6
6
  "name": "GitHits"
package/README.md CHANGED
@@ -12,7 +12,7 @@ npx githits init
12
12
 
13
13
  `init` walks you through setup: choose the local MCP server or Agent Skills, detect your coding tools, sign in, and connect everything to GitHits.
14
14
 
15
- Supported tools: Claude Code, Cursor, Windsurf, VS Code / Copilot, Cline, Claude Desktop, Codex CLI, Pi, Gemini CLI, Google Antigravity, and OpenCode.
15
+ Supported tools: Claude Code, Cursor, Windsurf, VS Code / Copilot, Cline, Claude Desktop, Codex CLI, Pi, Gemini CLI, Google Antigravity, OpenCode, and Hermes Agent.
16
16
 
17
17
  If you are using a tool that is not listed above, use the manual MCP setup instructions near the end of this README.
18
18
 
package/dist/cli.js CHANGED
@@ -51,11 +51,11 @@ import {
51
51
  shouldRunUpdateCheck,
52
52
  startTelemetrySpan,
53
53
  withTelemetrySpan
54
- } from "./shared/chunk-tm11qwgr.js";
54
+ } from "./shared/chunk-v23wr0eq.js";
55
55
  import {
56
56
  __require,
57
57
  version
58
- } from "./shared/chunk-7b4f7fd5.js";
58
+ } from "./shared/chunk-yqx01jh9.js";
59
59
 
60
60
  // src/cli.ts
61
61
  import { Command } from "commander";
@@ -8001,7 +8001,7 @@ grep run. --symbol-field hydrates enclosing symbol metadata (appears under each
8001
8001
  match in --verbose output; full payload in --json).`;
8002
8002
  function registerCodeGrepCommand(pkgCommand) {
8003
8003
  return pkgCommand.command("grep").summary("Deterministic text grep over indexed dependency source").description(PKG_GREP_DESCRIPTION).argument("[spec-or-pattern]", "Spec mode: package spec (e.g. npm:express). Repo mode (with --repo-url): the pattern.").argument("[pattern-or-prefix]", "Spec mode: the pattern. Repo mode: optional path-prefix.").argument("[path-prefix]", "Spec mode only: optional path-prefix. Ignored with --repo-url.").option("--repo-url <url>", "Repository URL addressing (defaults to the repo default branch)").option("--git-ref <ref>", "Optional tag, commit, branch, or HEAD for --repo-url.").option("--path <path>", "Exact file path to grep").option("--glob <glob>", "Glob scope (repeatable)", collectRepeatable2, []).option("--ext <ext>", "Extension filter without leading dot (repeatable)", collectRepeatable2, []).option("--regex", "Interpret the pattern as RE2 regex").option("--case-sensitive", "Enable ASCII case-sensitive matching").option("-C, --context <n>", "Context lines before and after each match (0-10)").option("-B, --before-context <n>", "Context lines before each match (0-10)").option("-A, --after-context <n>", "Context lines after each match (0-10)").option("--exclude-docs", "Skip files classified as documentation").option("--exclude-tests", "Skip files classified as tests").option("--limit <n>", "Max matches to return on this page (1-1000, default 50)").option("--per-file-limit <n>", "Cap matches per file within this page (0-1000, 0 = unlimited)").option("--cursor <cursor>", "Opaque nextCursor from a previous grep result").option("--symbol-field <field>", `Repeatable; surfaces in --json and under each --verbose match. ${GREP_REPO_SYMBOL_FIELDS_NOTE}`, collectRepeatable2, []).option("--wait <ms>", `Indexing wait timeout (0-${MAX_WAIT_TIMEOUT_MS}, default ${DEFAULT_WAIT_TIMEOUT_MS})`).option("-v, --verbose", "Render grouped output with file headers").option("--json", "Emit the JSON envelope").action(async (arg1, arg2, arg3, options) => {
8004
- const { createContainer: createContainer2 } = await import("./shared/chunk-70bn2pmx.js");
8004
+ const { createContainer: createContainer2 } = await import("./shared/chunk-phdms6y7.js");
8005
8005
  const deps = await createContainer2();
8006
8006
  await pkgGrepAction(arg1, arg2, arg3, options, {
8007
8007
  codeNavigationService: deps.codeNavigationService,
@@ -8465,7 +8465,7 @@ function registerExampleCommand(program) {
8465
8465
  });
8466
8466
  }
8467
8467
  async function loadContainer() {
8468
- const { createContainer: createContainer2 } = await import("./shared/chunk-70bn2pmx.js");
8468
+ const { createContainer: createContainer2 } = await import("./shared/chunk-phdms6y7.js");
8469
8469
  return createContainer2();
8470
8470
  }
8471
8471
  // src/commands/feedback.ts
@@ -8529,11 +8529,21 @@ import {
8529
8529
  parse as parseJsonc,
8530
8530
  printParseErrorCode
8531
8531
  } from "jsonc-parser";
8532
+ import {
8533
+ isMap,
8534
+ isScalar,
8535
+ parseDocument,
8536
+ parse as parseYaml,
8537
+ stringify as stringifyYaml
8538
+ } from "yaml";
8539
+ function normalizeConfigContent(content) {
8540
+ if (content.charCodeAt(0) === 65279) {
8541
+ return content.slice(1);
8542
+ }
8543
+ return content;
8544
+ }
8532
8545
  function parseConfigObject(content) {
8533
- let normalizedContent = content;
8534
- if (normalizedContent.charCodeAt(0) === 65279) {
8535
- normalizedContent = normalizedContent.slice(1);
8536
- }
8546
+ const normalizedContent = normalizeConfigContent(content);
8537
8547
  const trimmed = normalizedContent.trim();
8538
8548
  if (trimmed === "") {
8539
8549
  return {
@@ -8581,6 +8591,221 @@ function parseConfigObject(content) {
8581
8591
  };
8582
8592
  }
8583
8593
  }
8594
+ function parseYamlConfigObject(content) {
8595
+ const normalizedContent = normalizeConfigContent(content);
8596
+ if (normalizedContent.trim() === "") {
8597
+ return { value: {} };
8598
+ }
8599
+ try {
8600
+ const parsed = parseYaml(normalizedContent);
8601
+ if (parsed === null || parsed === undefined) {
8602
+ return { value: {} };
8603
+ }
8604
+ if (!isPlainObject(parsed)) {
8605
+ return { error: "Config file root is not a YAML object" };
8606
+ }
8607
+ return { value: parsed };
8608
+ } catch (err) {
8609
+ return {
8610
+ error: `Invalid YAML: ${err instanceof Error ? err.message : String(err)}`
8611
+ };
8612
+ }
8613
+ }
8614
+ function formatYamlError(error2) {
8615
+ if (error2 instanceof Error) {
8616
+ return error2.message;
8617
+ }
8618
+ return String(error2);
8619
+ }
8620
+ function parseYamlConfigDocument(content) {
8621
+ const normalizedContent = normalizeConfigContent(content);
8622
+ const source = normalizedContent.trim() === "" ? `{}
8623
+ ` : normalizedContent;
8624
+ let doc;
8625
+ try {
8626
+ doc = parseDocument(source);
8627
+ } catch (err) {
8628
+ return {
8629
+ status: "error",
8630
+ error: `Invalid YAML: ${formatYamlError(err)}`
8631
+ };
8632
+ }
8633
+ if (doc.errors.length > 0) {
8634
+ const firstError = doc.errors[0];
8635
+ return {
8636
+ status: "error",
8637
+ error: `Invalid YAML: ${formatYamlError(firstError)}`
8638
+ };
8639
+ }
8640
+ const contents = doc.contents;
8641
+ if (!contents || !isMap(contents)) {
8642
+ return {
8643
+ status: "error",
8644
+ error: "Config file root is not a YAML object"
8645
+ };
8646
+ }
8647
+ return {
8648
+ status: "ok",
8649
+ doc,
8650
+ root: contents
8651
+ };
8652
+ }
8653
+ function toYamlConfigShapeError(serversKey) {
8654
+ return `"${serversKey}" is not a YAML object`;
8655
+ }
8656
+ function toYamlKeyString(key) {
8657
+ if (typeof key === "string") {
8658
+ return key;
8659
+ }
8660
+ if (!isScalar(key) || typeof key.value !== "string") {
8661
+ return null;
8662
+ }
8663
+ return key.value;
8664
+ }
8665
+ function getYamlMatchingServerKeys(serversMap, serverName) {
8666
+ const normalizedTarget = serverName.toLowerCase();
8667
+ return serversMap.items.map((pair) => toYamlKeyString(pair.key)).filter((key) => typeof key === "string" && key.toLowerCase() === normalizedTarget);
8668
+ }
8669
+ function yamlNodeToJsValue(value) {
8670
+ if (typeof value === "object" && value !== null && "toJSON" in value && typeof value.toJSON === "function") {
8671
+ return value.toJSON();
8672
+ }
8673
+ return value;
8674
+ }
8675
+ function createEmptyYamlMapNode() {
8676
+ const map = parseDocument(`{}
8677
+ `).contents;
8678
+ if (!map || !isMap(map)) {
8679
+ throw new Error("Failed to initialize YAML object node");
8680
+ }
8681
+ return map;
8682
+ }
8683
+ function ensureYamlServersMapForSetup(root, serversKey) {
8684
+ const existingServers = root.get(serversKey, true);
8685
+ if (existingServers === undefined || existingServers === null || isScalar(existingServers) && existingServers.value === null) {
8686
+ root.set(serversKey, createEmptyYamlMapNode());
8687
+ const initializedServers = root.get(serversKey, true);
8688
+ if (!initializedServers || !isMap(initializedServers)) {
8689
+ return {
8690
+ status: "error",
8691
+ error: toYamlConfigShapeError(serversKey)
8692
+ };
8693
+ }
8694
+ return {
8695
+ status: "ok",
8696
+ serversMap: initializedServers
8697
+ };
8698
+ }
8699
+ if (!isMap(existingServers)) {
8700
+ return {
8701
+ status: "error",
8702
+ error: toYamlConfigShapeError(serversKey)
8703
+ };
8704
+ }
8705
+ return {
8706
+ status: "ok",
8707
+ serversMap: existingServers
8708
+ };
8709
+ }
8710
+ function getYamlServersMapForUninstall(root, serversKey) {
8711
+ const existingServers = root.get(serversKey, true);
8712
+ if (existingServers === undefined || existingServers === null || isScalar(existingServers) && existingServers.value === null) {
8713
+ return { status: "not_configured" };
8714
+ }
8715
+ if (!isMap(existingServers)) {
8716
+ return {
8717
+ status: "error",
8718
+ error: toYamlConfigShapeError(serversKey)
8719
+ };
8720
+ }
8721
+ return {
8722
+ status: "ok",
8723
+ serversMap: existingServers
8724
+ };
8725
+ }
8726
+ function renderYamlDocument(doc) {
8727
+ const rendered = doc.toString();
8728
+ return rendered.endsWith(`
8729
+ `) ? rendered : `${rendered}
8730
+ `;
8731
+ }
8732
+ function mergeYamlServerConfig(existingContent, serversKey, serverName, serverConfig) {
8733
+ const parsed = parseYamlConfigDocument(existingContent);
8734
+ if (parsed.status === "error") {
8735
+ return { status: "parse_error", error: parsed.error };
8736
+ }
8737
+ const serversResult = ensureYamlServersMapForSetup(parsed.root, serversKey);
8738
+ if (serversResult.status !== "ok") {
8739
+ return {
8740
+ status: "parse_error",
8741
+ error: serversResult.status === "error" ? serversResult.error : toYamlConfigShapeError(serversKey)
8742
+ };
8743
+ }
8744
+ const { serversMap } = serversResult;
8745
+ const matchingKeys = getYamlMatchingServerKeys(serversMap, serverName);
8746
+ if (matchingKeys.length === 1 && matchingKeys[0] === serverName) {
8747
+ const existingValue = yamlNodeToJsValue(serversMap.get(serverName, true));
8748
+ if (isEquivalentConfiguredValue(existingValue, serverConfig)) {
8749
+ return { status: "already_configured" };
8750
+ }
8751
+ }
8752
+ for (const key of matchingKeys) {
8753
+ serversMap.delete(key);
8754
+ }
8755
+ const hadExisting = matchingKeys.length > 0;
8756
+ serversMap.set(serverName, serverConfig);
8757
+ return {
8758
+ status: hadExisting ? "updated" : "added",
8759
+ content: renderYamlDocument(parsed.doc)
8760
+ };
8761
+ }
8762
+ function removeYamlServerConfig(existingContent, serversKey, serverName) {
8763
+ const parsed = parseYamlConfigDocument(existingContent);
8764
+ if (parsed.status === "error") {
8765
+ return { status: "parse_error", error: parsed.error };
8766
+ }
8767
+ const serversResult = getYamlServersMapForUninstall(parsed.root, serversKey);
8768
+ if (serversResult.status === "not_configured") {
8769
+ return { status: "not_configured" };
8770
+ }
8771
+ if (serversResult.status === "error") {
8772
+ return {
8773
+ status: "parse_error",
8774
+ error: serversResult.error
8775
+ };
8776
+ }
8777
+ const matchingKeys = getYamlMatchingServerKeys(serversResult.serversMap, serverName);
8778
+ if (matchingKeys.length === 0) {
8779
+ return { status: "not_configured" };
8780
+ }
8781
+ for (const key of matchingKeys) {
8782
+ serversResult.serversMap.delete(key);
8783
+ }
8784
+ return {
8785
+ status: "removed",
8786
+ content: renderYamlDocument(parsed.doc)
8787
+ };
8788
+ }
8789
+ function parseConfigObjectForFormat(content, format = "json") {
8790
+ if (format === "yaml") {
8791
+ return parseYamlConfigObject(content);
8792
+ }
8793
+ const parsed = parseConfigObject(content);
8794
+ if (parsed.format === "invalid") {
8795
+ return { error: parsed.error };
8796
+ }
8797
+ return { value: parsed.value };
8798
+ }
8799
+ function renderConfigObjectForFormat(config, format = "json") {
8800
+ if (format === "yaml") {
8801
+ const rendered = stringifyYaml(config);
8802
+ return rendered.endsWith(`
8803
+ `) ? rendered : `${rendered}
8804
+ `;
8805
+ }
8806
+ return `${JSON.stringify(config, null, 2)}
8807
+ `;
8808
+ }
8584
8809
  function isPlainObject(value) {
8585
8810
  return typeof value === "object" && value !== null && !Array.isArray(value);
8586
8811
  }
@@ -8688,9 +8913,12 @@ function getMatchingServerKeys(servers, serverName) {
8688
8913
  const normalizedTarget = serverName.toLowerCase();
8689
8914
  return Object.keys(servers).filter((key) => key.toLowerCase() === normalizedTarget);
8690
8915
  }
8691
- function mergeServerConfig(existingContent, serversKey, serverName, serverConfig) {
8692
- const parsedConfig = parseConfigObject(existingContent);
8693
- if (parsedConfig.format === "invalid") {
8916
+ function mergeServerConfig(existingContent, serversKey, serverName, serverConfig, format = "json") {
8917
+ if (format === "yaml") {
8918
+ return mergeYamlServerConfig(existingContent, serversKey, serverName, serverConfig);
8919
+ }
8920
+ const parsedConfig = parseConfigObjectForFormat(existingContent, format);
8921
+ if ("error" in parsedConfig) {
8694
8922
  return {
8695
8923
  status: "parse_error",
8696
8924
  error: parsedConfig.error
@@ -8719,13 +8947,15 @@ function mergeServerConfig(existingContent, serversKey, serverName, serverConfig
8719
8947
  serversObj[serverName] = serverConfig;
8720
8948
  return {
8721
8949
  status: hadExisting ? "updated" : "added",
8722
- content: `${JSON.stringify(config, null, 2)}
8723
- `
8950
+ content: renderConfigObjectForFormat(config, format)
8724
8951
  };
8725
8952
  }
8726
- function removeServerConfig(existingContent, serversKey, serverName) {
8727
- const parsedConfig = parseConfigObject(existingContent);
8728
- if (parsedConfig.format === "invalid") {
8953
+ function removeServerConfig(existingContent, serversKey, serverName, format = "json") {
8954
+ if (format === "yaml") {
8955
+ return removeYamlServerConfig(existingContent, serversKey, serverName);
8956
+ }
8957
+ const parsedConfig = parseConfigObjectForFormat(existingContent, format);
8958
+ if ("error" in parsedConfig) {
8729
8959
  return {
8730
8960
  status: "parse_error",
8731
8961
  error: parsedConfig.error
@@ -8752,8 +8982,7 @@ function removeServerConfig(existingContent, serversKey, serverName) {
8752
8982
  }
8753
8983
  return {
8754
8984
  status: "removed",
8755
- content: `${JSON.stringify(config, null, 2)}
8756
- `
8985
+ content: renderConfigObjectForFormat(config, format)
8757
8986
  };
8758
8987
  }
8759
8988
  function formatUninstallPreview(config) {
@@ -8770,8 +8999,8 @@ function formatUninstallPreview(config) {
8770
8999
  async function isAlreadyConfigured(config, fs) {
8771
9000
  try {
8772
9001
  const content = await fs.readFile(config.configPath);
8773
- const parsedConfig = parseConfigObject(content);
8774
- if (parsedConfig.format === "invalid") {
9002
+ const parsedConfig = parseConfigObjectForFormat(content, config.format);
9003
+ if ("error" in parsedConfig) {
8775
9004
  return false;
8776
9005
  }
8777
9006
  const parsed = parsedConfig.value;
@@ -8792,21 +9021,21 @@ async function isAlreadyConfigured(config, fs) {
8792
9021
  async function getConfigUninstallCheckStatus(config, fs) {
8793
9022
  try {
8794
9023
  const content = await fs.readFile(config.configPath);
8795
- const parsedConfig = parseConfigObject(content);
8796
- if (parsedConfig.format === "invalid") {
9024
+ const parsedConfig = parseConfigObjectForFormat(content, config.format);
9025
+ if ("error" in parsedConfig) {
8797
9026
  return {
8798
9027
  status: "failed",
8799
9028
  message: `Cannot parse ${config.configPath}: ${parsedConfig.error}. File left unchanged.`
8800
9029
  };
8801
9030
  }
8802
9031
  const servers = parsedConfig.value[config.serversKey];
8803
- if (servers === undefined) {
9032
+ if (servers === undefined || servers === null) {
8804
9033
  return { status: "not_configured" };
8805
9034
  }
8806
9035
  if (typeof servers !== "object" || servers === null || Array.isArray(servers)) {
8807
9036
  return {
8808
9037
  status: "failed",
8809
- message: `Cannot parse ${config.configPath}: "${config.serversKey}" is not a JSON object. File left unchanged.`
9038
+ message: `Cannot parse ${config.configPath}: "${config.serversKey}" is not a ${config.format === "yaml" ? "YAML" : "JSON"} object. File left unchanged.`
8810
9039
  };
8811
9040
  }
8812
9041
  const hasEntry = getMatchingServerKeys(servers, config.serverName).length > 0;
@@ -9065,7 +9294,7 @@ async function executeConfigFileSetup(setup, fs) {
9065
9294
  };
9066
9295
  }
9067
9296
  }
9068
- const result = mergeServerConfig(existingContent, setup.serversKey, setup.serverName, setup.serverConfig);
9297
+ const result = mergeServerConfig(existingContent, setup.serversKey, setup.serverName, setup.serverConfig, setup.format);
9069
9298
  if (result.status === "already_configured") {
9070
9299
  return {
9071
9300
  status: "already_configured",
@@ -9130,7 +9359,7 @@ async function executeConfigFileUninstall(setup, fs) {
9130
9359
  message: `Cannot read ${setup.configPath}: ${err instanceof Error ? err.message : String(err)}`
9131
9360
  };
9132
9361
  }
9133
- const result = removeServerConfig(existingContent, setup.serversKey, setup.serverName);
9362
+ const result = removeServerConfig(existingContent, setup.serversKey, setup.serverName, setup.format);
9134
9363
  if (result.status === "not_configured") {
9135
9364
  return {
9136
9365
  status: "not_configured",
@@ -9218,6 +9447,16 @@ function getPiAgentDir(fs) {
9218
9447
  function getPiMcpConfigPath(fs) {
9219
9448
  return fs.joinPath(getPiAgentDir(fs), "mcp.json");
9220
9449
  }
9450
+ function getHermesHomeDir(fs) {
9451
+ const configuredDir = process.env.HERMES_HOME?.trim();
9452
+ if (configuredDir) {
9453
+ return expandHomePath(fs, configuredDir);
9454
+ }
9455
+ return fs.joinPath(fs.getHomeDir(), ".hermes");
9456
+ }
9457
+ function getHermesConfigPath(fs) {
9458
+ return fs.joinPath(getHermesHomeDir(fs), "config.yaml");
9459
+ }
9221
9460
  function getOpenCodeDesktopDetectPaths(fs) {
9222
9461
  const userDataRoot = getUserDataRoot(fs);
9223
9462
  return [
@@ -9612,6 +9851,25 @@ var openCode = {
9612
9851
  }
9613
9852
  })
9614
9853
  };
9854
+ var hermesAgent = {
9855
+ name: "Hermes Agent",
9856
+ id: "hermes-agent",
9857
+ detectionMethod: "hybrid",
9858
+ setupMethod: "config-file",
9859
+ detectPaths: (fs) => [getHermesHomeDir(fs)],
9860
+ detectBinary: async (exec) => isExecutableAvailable(exec, "hermes-agent"),
9861
+ getSetupConfig: (fs) => ({
9862
+ method: "config-file",
9863
+ format: "yaml",
9864
+ configPath: getHermesConfigPath(fs),
9865
+ serversKey: "mcp_servers",
9866
+ serverName: GITHITS_SERVER_NAME,
9867
+ serverConfig: {
9868
+ command: GITHITS_MCP_COMMAND,
9869
+ args: [...GITHITS_MCP_ARGS]
9870
+ }
9871
+ })
9872
+ };
9615
9873
  var agentDefinitions = [
9616
9874
  claudeCode,
9617
9875
  cursor,
@@ -9623,7 +9881,8 @@ var agentDefinitions = [
9623
9881
  pi,
9624
9882
  geminiCli,
9625
9883
  googleAntigravity,
9626
- openCode
9884
+ openCode,
9885
+ hermesAgent
9627
9886
  ];
9628
9887
  async function scanSingleAgent(agent, fs, execService) {
9629
9888
  let detected = false;
@@ -9761,6 +10020,14 @@ function getPiConfigFileUninstall(agent, fileSystemService) {
9761
10020
  function formatCommand(command, useColors) {
9762
10021
  return colorizeBrand(command, "secondary", useColors, { bold: true });
9763
10022
  }
10023
+ var AGENT_SAFE_CLI = "npx -y githits@latest";
10024
+ var AGENT_DETECT_COMMAND = `${AGENT_SAFE_CLI} init --detect-agents`;
10025
+ var AGENT_INSTALL_COMMAND = `${AGENT_SAFE_CLI} init --install-agents`;
10026
+ var AGENT_LOGIN_COMMAND = `${AGENT_SAFE_CLI} login`;
10027
+ var AGENT_LOGIN_NO_BROWSER_COMMAND = `${AGENT_SAFE_CLI} login --no-browser`;
10028
+ var AGENTIC_INIT_YES_WARNING = "Do not run `githits init -y` or `githits init --yes` unless the user explicitly asks to configure every detected tool.";
10029
+ var AGENTIC_INIT_VERIFY_INSTRUCTION = "After a successful --install-agents run, verify with --detect-agents --json instead of running init again.";
10030
+ var AGENTIC_INIT_JSON_VERIFY_INSTRUCTION = "Do not run init again after a successful --install-agents run; verify with --detect-agents --json instead.";
9764
10031
  function printReadyNextSteps() {
9765
10032
  console.log(" GitHits is now connected to your coding agents.");
9766
10033
  console.log();
@@ -9793,6 +10060,57 @@ function printAuthNotCheckedNextSteps(useColors) {
9793
10060
  console.log(" If your agent asks you to sign in, run:");
9794
10061
  console.log(` ${formatCommand("npx githits@latest login", useColors)}`);
9795
10062
  }
10063
+ function printAgenticLoginInstructions(useColors) {
10064
+ console.log(" Authentication is still required before agents can use GitHits.");
10065
+ console.log();
10066
+ console.log(" Agent instruction:");
10067
+ console.log();
10068
+ console.log(" Ask the user:");
10069
+ console.log(` "GitHits needs to open a browser sign-in page. The sign-in happens in your browser, not in this chat or terminal. I will not see or handle your credentials. Should I run \`${AGENT_LOGIN_COMMAND}\` now?"`);
10070
+ console.log();
10071
+ console.log(" If the user approves, run:");
10072
+ console.log(` ${formatCommand(AGENT_LOGIN_COMMAND, useColors)}`);
10073
+ console.log();
10074
+ console.log(" If the browser cannot open, run:");
10075
+ console.log(` ${formatCommand(AGENT_LOGIN_NO_BROWSER_COMMAND, useColors)}`);
10076
+ }
10077
+ function printAgenticAlreadyAuthenticated() {
10078
+ console.log(" GitHits MCP is installed and you are already signed in.");
10079
+ console.log();
10080
+ console.log(" Open a new coding agent session so it reloads MCP config.");
10081
+ }
10082
+ function printAgenticAuthNotChecked(useColors) {
10083
+ console.log(" GitHits MCP is installed. Sign-in status was not checked.");
10084
+ console.log();
10085
+ console.log(" If the user is not already signed in, ask before running:");
10086
+ console.log(` ${formatCommand(AGENT_LOGIN_COMMAND, useColors)}`);
10087
+ }
10088
+ function printNonInteractiveInitGuidance(useColors) {
10089
+ console.log(" This setup is interactive. Because this session is non-interactive, no changes were made.");
10090
+ console.log();
10091
+ console.log(" If you are an AI coding agent helping a user install GitHits:");
10092
+ console.log();
10093
+ console.log(" 1. Run:");
10094
+ console.log(` ${formatCommand(AGENT_DETECT_COMMAND, useColors)}`);
10095
+ console.log();
10096
+ console.log(" 2. Show the detected tools to the user.");
10097
+ console.log();
10098
+ console.log(" 3. Ask which tools should receive the GitHits MCP server.");
10099
+ console.log();
10100
+ console.log(" 4. Only after approval, run:");
10101
+ console.log(` ${formatCommand(`${AGENT_INSTALL_COMMAND} <ids>`, useColors)}`);
10102
+ console.log();
10103
+ console.log(` ${AGENTIC_INIT_YES_WARNING}`);
10104
+ console.log(` ${AGENTIC_INIT_VERIFY_INSTRUCTION}`);
10105
+ }
10106
+ function printNonInteractiveYesRejected(useColors) {
10107
+ console.error("Non-interactive `githits init --yes` is not supported because it can configure tools without explicit per-tool approval.");
10108
+ console.error();
10109
+ console.error("Use the agent-safe staged flow instead:");
10110
+ console.error(` ${formatCommand(AGENT_DETECT_COMMAND, useColors)}`);
10111
+ console.error(` ${formatCommand(`${AGENT_INSTALL_COMMAND} <ids>`, useColors)}`);
10112
+ process.exitCode = 1;
10113
+ }
9796
10114
  var GITHITS_ASCII_LOGO = String.raw`
9797
10115
  ____ _ _ _ _ _ _
9798
10116
  / ___(_) |_| | | (_) |_ ___
@@ -9922,7 +10240,7 @@ function printTask(status, label, detail, useColors) {
9922
10240
  }
9923
10241
  function printInitIntro(useColors) {
9924
10242
  console.log(colorizeLogo(GITHITS_ASCII_LOGO, useColors));
9925
- console.log(" Your agent can read your local codebase.");
10243
+ console.log(" Your agent can only read your local codebase.");
9926
10244
  console.log();
9927
10245
  console.log(" GitHits lets it navigate the open-source code your app depends on.");
9928
10246
  console.log();
@@ -10059,6 +10377,287 @@ function printScanSummary(scan, useColors) {
10059
10377
  console.log(` Found ${detected} supported tool${detected !== 1 ? "s" : ""}.`);
10060
10378
  }
10061
10379
  }
10380
+ function buildStagedAgentEntries(scan) {
10381
+ const statuses = new Map;
10382
+ for (const agent of scan.needsSetup)
10383
+ statuses.set(agent.id, "needs_setup");
10384
+ for (const agent of scan.alreadyConfigured) {
10385
+ statuses.set(agent.id, "already_configured");
10386
+ }
10387
+ for (const agent of scan.notDetected)
10388
+ statuses.set(agent.id, "not_detected");
10389
+ return agentDefinitions.map((agent) => ({
10390
+ id: agent.id,
10391
+ name: agent.name,
10392
+ status: statuses.get(agent.id) ?? "not_detected"
10393
+ }));
10394
+ }
10395
+ function printAgenticDetectSummary(scan, useColors) {
10396
+ const entries = buildStagedAgentEntries(scan);
10397
+ const detected = entries.filter((entry) => entry.status !== "not_detected");
10398
+ const installable = entries.filter((entry) => entry.status === "needs_setup");
10399
+ const notDetected = entries.filter((entry) => entry.status === "not_detected");
10400
+ console.log("Detected supported tools:");
10401
+ console.log();
10402
+ if (detected.length === 0) {
10403
+ console.log(" None detected.");
10404
+ } else {
10405
+ console.log(" ID Tool Status");
10406
+ for (const entry of detected) {
10407
+ console.log(` ${entry.id.padEnd(18)} ${entry.name.padEnd(21)} ${entry.status.replace("_", " ")}`);
10408
+ }
10409
+ }
10410
+ console.log();
10411
+ console.log("Not detected:");
10412
+ console.log(` ${notDetected.length > 0 ? notDetected.map((entry) => entry.id).join(", ") : "none"}`);
10413
+ console.log();
10414
+ if (detected.length === 0) {
10415
+ console.log("No supported AI coding tools detected.");
10416
+ console.log();
10417
+ console.log("Next step for agents:");
10418
+ console.log(" Tell the user to install a supported coding tool, then run detection again.");
10419
+ return;
10420
+ }
10421
+ if (installable.length === 0) {
10422
+ console.log("No detected tools need setup.");
10423
+ console.log();
10424
+ console.log("Next step for agents:");
10425
+ console.log(" Tell the user that GitHits is already configured for detected tools.");
10426
+ console.log(` ${AGENTIC_INIT_YES_WARNING}`);
10427
+ console.log(" Do not run init again as a verification step.");
10428
+ return;
10429
+ }
10430
+ const installableIds = installable.map((entry) => entry.id);
10431
+ console.log("Next step for agents:");
10432
+ console.log();
10433
+ console.log(" Ask the user:");
10434
+ console.log(` "GitHits can be installed for ${installable.map((entry) => entry.name).join(", ")}. Which should I configure?"`);
10435
+ console.log();
10436
+ console.log(" If the user approves all detected tools needing setup, run:");
10437
+ console.log(` ${formatCommand(`${AGENT_INSTALL_COMMAND} ${installableIds.join(",")}`, useColors)}`);
10438
+ console.log();
10439
+ console.log(` ${AGENTIC_INIT_YES_WARNING}`);
10440
+ console.log(` ${AGENTIC_INIT_VERIFY_INSTRUCTION}`);
10441
+ }
10442
+ function printAgenticDetectJson(scan) {
10443
+ const entries = buildStagedAgentEntries(scan);
10444
+ const installableIds = entries.filter((entry) => entry.status === "needs_setup").map((entry) => entry.id);
10445
+ console.log(JSON.stringify({
10446
+ mode: "detect-agents",
10447
+ agents: entries,
10448
+ installableIds,
10449
+ suggestedCommand: installableIds.length > 0 ? `${AGENT_INSTALL_COMMAND} ${installableIds.join(",")}` : null,
10450
+ instructions: [
10451
+ "Show detected tools to the user.",
10452
+ "Ask which tools should receive the GitHits MCP server.",
10453
+ "Only run --install-agents with user-approved IDs.",
10454
+ AGENTIC_INIT_YES_WARNING,
10455
+ AGENTIC_INIT_JSON_VERIFY_INSTRUCTION
10456
+ ]
10457
+ }, null, 2));
10458
+ }
10459
+ function getStagedModeCount(options) {
10460
+ return [
10461
+ options.detectAgents === true,
10462
+ options.installAgents !== undefined
10463
+ ].filter(Boolean).length;
10464
+ }
10465
+ function failInitArgument(message, json) {
10466
+ if (json) {
10467
+ console.error(JSON.stringify({ error: message, code: "INVALID_ARGUMENT" }));
10468
+ } else {
10469
+ console.error(message);
10470
+ }
10471
+ process.exitCode = 1;
10472
+ }
10473
+ function validateInitModeOptions(options) {
10474
+ const stagedModeCount = getStagedModeCount(options);
10475
+ if (stagedModeCount > 1) {
10476
+ failInitArgument("Use only one staged init mode: --detect-agents or --install-agents.", options.json);
10477
+ return false;
10478
+ }
10479
+ if (options.yes && stagedModeCount > 0) {
10480
+ failInitArgument("--yes cannot be combined with --detect-agents or --install-agents.", options.json);
10481
+ return false;
10482
+ }
10483
+ if (options.json && stagedModeCount === 0) {
10484
+ failInitArgument("--json is only supported with --detect-agents or --install-agents.", options.json);
10485
+ return false;
10486
+ }
10487
+ return true;
10488
+ }
10489
+ function parseAgentIdList(value) {
10490
+ if (!value)
10491
+ return [];
10492
+ const ids = value.split(",").map((id) => id.trim()).filter((id) => id.length > 0);
10493
+ return [...new Set(ids)];
10494
+ }
10495
+ function findAgentsByIds(scan, ids) {
10496
+ const detected = [...scan.needsSetup, ...scan.alreadyConfigured];
10497
+ return ids.map((id) => detected.find((agent) => agent.id === id)).filter((agent) => Boolean(agent));
10498
+ }
10499
+ function validateInstallAgentIds(scan, ids) {
10500
+ const supportedIds = new Set(agentDefinitions.map((agent) => agent.id));
10501
+ const detectedIds = [...scan.needsSetup, ...scan.alreadyConfigured].map((agent) => agent.id);
10502
+ const detectedIdSet = new Set(detectedIds);
10503
+ if (ids.length === 0) {
10504
+ return {
10505
+ ok: false,
10506
+ message: detectedIds.length > 0 ? `Provide at least one agent ID. Detected IDs: ${detectedIds.join(", ")}.` : "Provide at least one agent ID. No supported agents are currently detected.",
10507
+ detectedIds
10508
+ };
10509
+ }
10510
+ const unknown = ids.filter((id) => !supportedIds.has(id));
10511
+ if (unknown.length > 0) {
10512
+ return {
10513
+ ok: false,
10514
+ message: `Unsupported agent ID${unknown.length !== 1 ? "s" : ""}: ${unknown.join(", ")}.`,
10515
+ detectedIds
10516
+ };
10517
+ }
10518
+ const undetected = ids.filter((id) => !detectedIdSet.has(id));
10519
+ if (undetected.length > 0) {
10520
+ return {
10521
+ ok: false,
10522
+ message: `Agent ID${undetected.length !== 1 ? "s" : ""} not detected: ${undetected.join(", ")}. Detected IDs: ${detectedIds.length > 0 ? detectedIds.join(", ") : "none"}.`,
10523
+ detectedIds
10524
+ };
10525
+ }
10526
+ return { ok: true };
10527
+ }
10528
+ function printInstallValidationFailure(failure, json) {
10529
+ if (json) {
10530
+ console.error(JSON.stringify({
10531
+ error: failure.message,
10532
+ code: "INVALID_ARGUMENT",
10533
+ detectedIds: failure.detectedIds
10534
+ }));
10535
+ } else {
10536
+ console.error(failure.message);
10537
+ }
10538
+ process.exitCode = 1;
10539
+ }
10540
+ function hasUsableInstallOutcome(outcomes) {
10541
+ return outcomes.some((outcome) => outcome.status === "success" || outcome.status === "already_configured");
10542
+ }
10543
+ async function getStagedInstallAuthStatus(createLoginDeps) {
10544
+ if (!createLoginDeps)
10545
+ return "not_checked";
10546
+ try {
10547
+ const loginDeps = await createLoginDeps();
10548
+ if (typeof loginDeps.hasValidToken === "boolean") {
10549
+ return loginDeps.hasValidToken ? "authenticated" : "required";
10550
+ }
10551
+ const tokens = await loginDeps.authStorage.loadTokens(loginDeps.mcpUrl);
10552
+ const expired = tokens?.expiresAt ? new Date(tokens.expiresAt) < new Date : false;
10553
+ return tokens && !expired ? "authenticated" : "required";
10554
+ } catch {
10555
+ return "not_checked";
10556
+ }
10557
+ }
10558
+ function buildAgenticInstallAuthPayload(authStatus) {
10559
+ if (authStatus === "authenticated") {
10560
+ return { required: false, status: "authenticated" };
10561
+ }
10562
+ if (authStatus === "required") {
10563
+ return {
10564
+ required: true,
10565
+ status: "required",
10566
+ command: AGENT_LOGIN_COMMAND,
10567
+ noBrowserCommand: AGENT_LOGIN_NO_BROWSER_COMMAND
10568
+ };
10569
+ }
10570
+ return {
10571
+ required: null,
10572
+ status: "not_checked",
10573
+ command: AGENT_LOGIN_COMMAND,
10574
+ noBrowserCommand: AGENT_LOGIN_NO_BROWSER_COMMAND
10575
+ };
10576
+ }
10577
+ function buildAgenticInstallInstructions(authStatus) {
10578
+ if (authStatus === "authenticated") {
10579
+ return ["Open a new coding agent session so it reloads MCP config."];
10580
+ }
10581
+ if (authStatus === "required") {
10582
+ return [
10583
+ `Ask the user before running ${AGENT_LOGIN_COMMAND}.`,
10584
+ "Browser sign-in happens outside chat and terminal input.",
10585
+ "Do not ask the user to paste passwords, tokens, cookies, or OAuth codes into chat."
10586
+ ];
10587
+ }
10588
+ return [
10589
+ "Sign-in status was not checked.",
10590
+ `If the user is not already signed in, ask before running ${AGENT_LOGIN_COMMAND}.`
10591
+ ];
10592
+ }
10593
+ function printAgenticInstallJson(outcomes, authStatus) {
10594
+ const canAuthenticate = hasUsableInstallOutcome(outcomes);
10595
+ console.log(JSON.stringify({
10596
+ mode: "install-agents",
10597
+ outcomes,
10598
+ auth: canAuthenticate ? buildAgenticInstallAuthPayload(authStatus) : {
10599
+ required: false,
10600
+ status: "not_applicable",
10601
+ reason: "Fix installation errors before starting sign-in."
10602
+ },
10603
+ instructions: canAuthenticate ? buildAgenticInstallInstructions(authStatus) : ["Fix installation errors before asking the user to sign in."]
10604
+ }, null, 2));
10605
+ }
10606
+ async function runDetectAgentsMode(options, fileSystemService, execService, useColors) {
10607
+ const scan = await scanAgents(agentDefinitions, fileSystemService, execService);
10608
+ if (options.json) {
10609
+ printAgenticDetectJson(scan);
10610
+ return;
10611
+ }
10612
+ printAgenticDetectSummary(scan, useColors);
10613
+ }
10614
+ async function runInstallAgentsMode(options, fileSystemService, execService, createLoginDeps, useColors) {
10615
+ const requestedIds = parseAgentIdList(options.installAgents);
10616
+ const scan = await scanAgents(agentDefinitions, fileSystemService, execService);
10617
+ const validation = validateInstallAgentIds(scan, requestedIds);
10618
+ if (!validation.ok) {
10619
+ printInstallValidationFailure(validation, options.json);
10620
+ return;
10621
+ }
10622
+ const agents = findAgentsByIds(scan, requestedIds);
10623
+ if (!options.json) {
10624
+ console.log("Installing GitHits MCP:");
10625
+ console.log();
10626
+ }
10627
+ const outcomes = await installSelectedAgents(agents, scan, fileSystemService, execService, useColors, !options.json);
10628
+ const failed = outcomes.filter((outcome) => outcome.status === "failed");
10629
+ const canAuthenticate = hasUsableInstallOutcome(outcomes);
10630
+ const authStatus = canAuthenticate ? await getStagedInstallAuthStatus(createLoginDeps) : "not_checked";
10631
+ if (failed.length > 0) {
10632
+ process.exitCode = 1;
10633
+ }
10634
+ if (options.json) {
10635
+ printAgenticInstallJson(outcomes, authStatus);
10636
+ return;
10637
+ }
10638
+ console.log();
10639
+ if (failed.length === 0) {
10640
+ console.log("GitHits MCP installation complete.");
10641
+ } else {
10642
+ console.log("GitHits MCP installation completed with errors.");
10643
+ for (const outcome of failed) {
10644
+ console.log(` ${outcome.name}: ${outcome.message ?? "Unknown error"}`);
10645
+ }
10646
+ }
10647
+ console.log();
10648
+ if (canAuthenticate) {
10649
+ if (authStatus === "authenticated") {
10650
+ printAgenticAlreadyAuthenticated();
10651
+ } else if (authStatus === "required") {
10652
+ printAgenticLoginInstructions(useColors);
10653
+ } else {
10654
+ printAgenticAuthNotChecked(useColors);
10655
+ }
10656
+ } else {
10657
+ console.log("Fix installation errors before starting sign-in.");
10658
+ }
10659
+ console.log();
10660
+ }
10062
10661
  function printAuthExplanation() {
10063
10662
  console.log(" GitHits authentication is required before your agent can use GitHits tools.");
10064
10663
  console.log();
@@ -10184,6 +10783,62 @@ async function verifyAgentConfigured(agent, fileSystemService, execService) {
10184
10783
  message: `${agent.name} verification failed: agent not detected after setup.`
10185
10784
  };
10186
10785
  }
10786
+ async function executeAgentSetupWithVerification(agent, fileSystemService, execService) {
10787
+ const config = getResolvedSetupConfig(agent, fileSystemService);
10788
+ let result = config.method === "cli" ? await executeCliSetup(config, execService) : config.method === "config-file" ? await executeConfigFileSetup(config, fileSystemService) : await executeCompositeSetup(config, fileSystemService, execService);
10789
+ if (result.status === "success" || result.status === "already_configured") {
10790
+ const verification = await verifyAgentConfigured(agent, fileSystemService, execService);
10791
+ if (!verification.ok) {
10792
+ result = {
10793
+ status: "failed",
10794
+ message: agent.id === "gemini-cli" ? "Gemini installation did not complete. Retry, or run: gemini extensions install --consent https://github.com/githits-com/githits-cli" : verification.message ?? `${agent.name} verification failed after setup.`
10795
+ };
10796
+ }
10797
+ }
10798
+ return result;
10799
+ }
10800
+ async function installSelectedAgents(agents, scan, fileSystemService, execService, useColors, printResults) {
10801
+ const alreadyConfiguredIds = new Set(scan.alreadyConfigured.map((agent) => agent.id));
10802
+ const outcomes = [];
10803
+ const installTasks = createInstallTaskReporter(useColors);
10804
+ for (const agent of agents) {
10805
+ if (alreadyConfiguredIds.has(agent.id)) {
10806
+ outcomes.push({
10807
+ id: agent.id,
10808
+ name: agent.name,
10809
+ status: "already_configured"
10810
+ });
10811
+ if (printResults) {
10812
+ printTask("warning", agent.name, "already configured", useColors);
10813
+ }
10814
+ continue;
10815
+ }
10816
+ const finishTask = printResults ? installTasks.start(agent.name) : () => {};
10817
+ let result;
10818
+ try {
10819
+ result = await executeAgentSetupWithVerification(agent, fileSystemService, execService);
10820
+ } finally {
10821
+ finishTask();
10822
+ }
10823
+ outcomes.push({
10824
+ id: agent.id,
10825
+ name: agent.name,
10826
+ status: result.status,
10827
+ message: result.status === "failed" ? result.message : undefined
10828
+ });
10829
+ if (!printResults) {
10830
+ continue;
10831
+ }
10832
+ if (result.status === "success") {
10833
+ printTask("success", agent.name, "configured and verified", useColors);
10834
+ } else if (result.status === "already_configured") {
10835
+ printTask("warning", agent.name, "already configured", useColors);
10836
+ } else {
10837
+ printTask("failed", agent.name, result.message, useColors);
10838
+ }
10839
+ }
10840
+ return outcomes;
10841
+ }
10187
10842
  async function verifyAgentUnconfigured(agent, fileSystemService, execService) {
10188
10843
  const postCheck = await scanAgents([agent], fileSystemService, execService);
10189
10844
  if (postCheck.needsSetup.some((a) => a.id === agent.id)) {
@@ -10303,7 +10958,28 @@ async function scanAgentsForUninstall(fileSystemService, execService) {
10303
10958
  async function initAction(options, deps) {
10304
10959
  const useColors = shouldUseColors();
10305
10960
  const { fileSystemService, promptService, execService, createLoginDeps } = deps;
10961
+ const isInteractive = deps.isInteractive ?? true;
10962
+ if (!validateInitModeOptions(options)) {
10963
+ return;
10964
+ }
10965
+ if (options.detectAgents) {
10966
+ await runDetectAgentsMode(options, fileSystemService, execService, useColors);
10967
+ return;
10968
+ }
10969
+ if (options.installAgents !== undefined) {
10970
+ await runInstallAgentsMode(options, fileSystemService, execService, createLoginDeps, useColors);
10971
+ return;
10972
+ }
10306
10973
  printInitIntro(useColors);
10974
+ if (!isInteractive) {
10975
+ if (options.yes) {
10976
+ printNonInteractiveYesRejected(useColors);
10977
+ return;
10978
+ }
10979
+ printNonInteractiveInitGuidance(useColors);
10980
+ console.log();
10981
+ return;
10982
+ }
10307
10983
  if (!options.yes) {
10308
10984
  let intent;
10309
10985
  try {
@@ -10322,7 +10998,7 @@ async function initAction(options, deps) {
10322
10998
  return;
10323
10999
  }
10324
11000
  if (intent === "later") {
10325
- console.log("\n No changes made. Run `githits init` whenever you're ready.\n");
11001
+ console.log("\n No changes made. Run `npx githits@latest init` whenever you're ready.\n");
10326
11002
  return;
10327
11003
  }
10328
11004
  }
@@ -10361,7 +11037,6 @@ async function initAction(options, deps) {
10361
11037
  } else {
10362
11038
  printTask("success", "Selected all detected tools", "--yes", useColors);
10363
11039
  }
10364
- const outcomes = [];
10365
11040
  if (toSetup.length === 0 && scan.needsSetup.length > 0) {
10366
11041
  printTask("skipped", "Setup skipped", "no tools selected", useColors);
10367
11042
  console.log();
@@ -10379,39 +11054,7 @@ async function initAction(options, deps) {
10379
11054
  console.log();
10380
11055
  return;
10381
11056
  }
10382
- const installTasks = createInstallTaskReporter(useColors);
10383
- for (const agent of toSetup) {
10384
- const config = getResolvedSetupConfig(agent, fileSystemService);
10385
- const finishTask = installTasks.start(agent.name);
10386
- let result;
10387
- try {
10388
- result = config.method === "cli" ? await executeCliSetup(config, execService) : config.method === "config-file" ? await executeConfigFileSetup(config, fileSystemService) : await executeCompositeSetup(config, fileSystemService, execService);
10389
- if (result.status === "success" || result.status === "already_configured") {
10390
- const verification = await verifyAgentConfigured(agent, fileSystemService, execService);
10391
- if (!verification.ok) {
10392
- result = {
10393
- status: "failed",
10394
- message: agent.id === "gemini-cli" ? "Gemini installation did not complete. Retry, or run: gemini extensions install --consent https://github.com/githits-com/githits-cli" : verification.message ?? `${agent.name} verification failed after setup.`
10395
- };
10396
- }
10397
- }
10398
- } finally {
10399
- finishTask();
10400
- }
10401
- outcomes.push({
10402
- id: agent.id,
10403
- name: agent.name,
10404
- status: result.status,
10405
- message: result.status === "failed" ? result.message : undefined
10406
- });
10407
- if (result.status === "success") {
10408
- printTask("success", agent.name, "configured and verified", useColors);
10409
- } else if (result.status === "already_configured") {
10410
- printTask("warning", agent.name, "already configured", useColors);
10411
- } else {
10412
- printTask("failed", agent.name, result.message, useColors);
10413
- }
10414
- }
11057
+ const outcomes = await installSelectedAgents(toSetup, scan, fileSystemService, execService, useColors, true);
10415
11058
  console.log();
10416
11059
  const configured = outcomes.filter((o) => o.status === "success").length;
10417
11060
  const alreadyDone = outcomes.filter((o) => o.status === "already_configured").length + scan.alreadyConfigured.length;
@@ -10594,7 +11237,7 @@ Scans for available agents that currently have GitHits configured, then removes
10594
11237
  only the GitHits MCP/plugin configuration with your confirmation. Authentication
10595
11238
  tokens are not removed; use \`githits logout\` to remove stored credentials.`;
10596
11239
  function registerInitCommand(program) {
10597
- const initCommand = program.command("init").summary("Connect GitHits to your coding agents").description(INIT_DESCRIPTION).option("-y, --yes", "Skip prompts, configure all detected tools").option("--skip-login", "Skip authentication step").action(async (options) => {
11240
+ const initCommand = program.command("init").summary("Connect GitHits to your coding agents").description(INIT_DESCRIPTION).option("-y, --yes", "Skip prompts, configure all detected tools").option("--skip-login", "Skip authentication step").option("--detect-agents", "Scan supported agents without installing").option("--install-agents <ids>", "Install MCP server for comma-separated agent IDs from --detect-agents").option("--json", "Emit JSON for --detect-agents or --install-agents").action(async (options) => {
10598
11241
  const fileSystemService = new FileSystemServiceImpl;
10599
11242
  const promptService = new PromptServiceImpl;
10600
11243
  const execService = new ExecServiceImpl;
@@ -10602,7 +11245,8 @@ function registerInitCommand(program) {
10602
11245
  fileSystemService,
10603
11246
  promptService,
10604
11247
  execService,
10605
- createLoginDeps: () => createContainer()
11248
+ createLoginDeps: () => createContainer(),
11249
+ isInteractive: process.stdin.isTTY === true && process.stdout.isTTY === true
10606
11250
  });
10607
11251
  });
10608
11252
  initCommand.command("uninstall").summary("Remove MCP server from your coding agents").description(INIT_UNINSTALL_DESCRIPTION).option("-y, --yes", "Skip prompts, uninstall from all configured agents").action(async (options) => {
@@ -10665,17 +11309,10 @@ function registerLanguagesCommand(program) {
10665
11309
  // src/commands/logout.ts
10666
11310
  async function logoutAction(deps) {
10667
11311
  const { authStorage, mcpUrl } = deps;
10668
- const auth = await authStorage.loadTokens(mcpUrl);
10669
11312
  await authStorage.clearAuthSession(mcpUrl);
10670
- if (!auth) {
10671
- console.log(`Not currently logged in.
11313
+ console.log(`Logged out.
10672
11314
  `);
10673
- console.log(` Environment: ${mcpUrl}`);
10674
- } else {
10675
- console.log(`Logged out.
10676
- `);
10677
- console.log(` Environment: ${mcpUrl}`);
10678
- }
11315
+ console.log(` Environment: ${mcpUrl}`);
10679
11316
  }
10680
11317
  var LOGOUT_DESCRIPTION = `Remove stored credentials.
10681
11318
 
@@ -13005,7 +13642,7 @@ function requireSearchService(deps) {
13005
13642
  return deps.codeNavigationService;
13006
13643
  }
13007
13644
  async function loadContainer2() {
13008
- const { createContainer: createContainer2 } = await import("./shared/chunk-70bn2pmx.js");
13645
+ const { createContainer: createContainer2 } = await import("./shared/chunk-phdms6y7.js");
13009
13646
  return createContainer2();
13010
13647
  }
13011
13648
  function parseTargetSpecs(specs) {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  version
3
- } from "./shared/chunk-7b4f7fd5.js";
3
+ } from "./shared/chunk-yqx01jh9.js";
4
4
  export {
5
5
  version
6
6
  };
@@ -4,8 +4,8 @@ import {
4
4
  createAuthStatusDependencies,
5
5
  createContainer,
6
6
  loadAutoLoginAuthSessionMetadata
7
- } from "./chunk-tm11qwgr.js";
8
- import"./chunk-7b4f7fd5.js";
7
+ } from "./chunk-v23wr0eq.js";
8
+ import"./chunk-yqx01jh9.js";
9
9
  export {
10
10
  loadAutoLoginAuthSessionMetadata,
11
11
  createContainer,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  __require,
3
3
  version
4
- } from "./chunk-7b4f7fd5.js";
4
+ } from "./chunk-yqx01jh9.js";
5
5
 
6
6
  // src/services/app-config-paths.ts
7
7
  var APP_DIR = "githits";
@@ -6217,7 +6217,8 @@ class TokenManager {
6217
6217
  authStorage;
6218
6218
  mcpUrl;
6219
6219
  cachedToken = null;
6220
- refreshPromise = null;
6220
+ softRefreshPromise = null;
6221
+ forceRefreshPromise = null;
6221
6222
  constructor(deps) {
6222
6223
  this.authService = deps.authService;
6223
6224
  this.authStorage = deps.authStorage;
@@ -6225,6 +6226,9 @@ class TokenManager {
6225
6226
  }
6226
6227
  async getToken() {
6227
6228
  return withTelemetrySpan("token-manager.get-token", async () => {
6229
+ if (this.forceRefreshPromise) {
6230
+ return (await this.forceRefreshPromise).accessToken;
6231
+ }
6228
6232
  if (!this.cachedToken) {
6229
6233
  this.cachedToken = await withTelemetrySpan("token-manager.load-tokens", () => this.authStorage.loadTokens(this.mcpUrl));
6230
6234
  if (!this.cachedToken)
@@ -6235,7 +6239,9 @@ class TokenManager {
6235
6239
  if (!shouldRefresh) {
6236
6240
  return currentToken;
6237
6241
  }
6238
- const refreshedToken = await this.doRefresh();
6242
+ const refreshedToken = await this.doRefresh({
6243
+ allowFreshExternalToken: true
6244
+ });
6239
6245
  if (refreshedToken) {
6240
6246
  return refreshedToken;
6241
6247
  }
@@ -6246,27 +6252,56 @@ class TokenManager {
6246
6252
  });
6247
6253
  }
6248
6254
  async forceRefresh() {
6249
- return withTelemetrySpan("token-manager.force-refresh", () => this.doRefresh());
6250
- }
6251
- async doRefresh() {
6252
- if (this.refreshPromise) {
6253
- return this.refreshPromise;
6255
+ return withTelemetrySpan("token-manager.force-refresh", () => this.doRefresh({ allowFreshExternalToken: false }));
6256
+ }
6257
+ async doRefresh(options) {
6258
+ const result = await this.doRefreshResult(options);
6259
+ return result.accessToken;
6260
+ }
6261
+ async doRefreshResult(options) {
6262
+ if (!options.allowFreshExternalToken) {
6263
+ if (this.forceRefreshPromise)
6264
+ return this.forceRefreshPromise;
6265
+ this.forceRefreshPromise = (async () => {
6266
+ const softResult = await this.softRefreshPromise?.catch(() => {
6267
+ return;
6268
+ });
6269
+ if (softResult?.accessToken && softResult.refreshedViaEndpoint) {
6270
+ return softResult;
6271
+ }
6272
+ return this.executeRefresh(options);
6273
+ })();
6274
+ try {
6275
+ return await this.forceRefreshPromise;
6276
+ } finally {
6277
+ this.forceRefreshPromise = null;
6278
+ }
6254
6279
  }
6255
- this.refreshPromise = this.executeRefresh();
6280
+ if (this.forceRefreshPromise)
6281
+ return this.forceRefreshPromise;
6282
+ if (this.softRefreshPromise)
6283
+ return this.softRefreshPromise;
6284
+ this.softRefreshPromise = this.executeRefresh(options);
6256
6285
  try {
6257
- return await this.refreshPromise;
6286
+ return await this.softRefreshPromise;
6258
6287
  } finally {
6259
- this.refreshPromise = null;
6288
+ this.softRefreshPromise = null;
6260
6289
  }
6261
6290
  }
6262
- async executeRefresh() {
6291
+ async executeRefresh(options) {
6263
6292
  return withTelemetrySpan("token-manager.refresh", async () => {
6264
- const tokens = this.cachedToken ?? await withTelemetrySpan("token-manager.load-tokens", () => this.authStorage.loadTokens(this.mcpUrl));
6265
- if (!tokens)
6266
- return;
6293
+ const candidate = await this.loadRefreshCandidate();
6294
+ if (!candidate)
6295
+ return refreshResult(undefined, false);
6296
+ if (candidate.externallyUpdated && options.allowFreshExternalToken) {
6297
+ const { shouldRefresh } = shouldRefreshToken(candidate.tokens, PROACTIVE_REFRESH_RATIO, new Date);
6298
+ if (!shouldRefresh)
6299
+ return refreshResult(candidate.tokens.accessToken, false);
6300
+ }
6301
+ const tokens = candidate.tokens;
6267
6302
  const client = await withTelemetrySpan("token-manager.load-client", () => this.authStorage.loadClient(this.mcpUrl));
6268
6303
  if (!client)
6269
- return;
6304
+ return refreshResult(undefined, false);
6270
6305
  let response;
6271
6306
  try {
6272
6307
  const metadata = await withTelemetrySpan("token-manager.discover-endpoints", () => this.authService.discoverEndpoints(this.mcpUrl));
@@ -6279,21 +6314,25 @@ class TokenManager {
6279
6314
  } catch {
6280
6315
  const reloadedToken = await this.loadExternallyUpdatedToken(tokens);
6281
6316
  if (reloadedToken)
6282
- return reloadedToken.accessToken;
6317
+ return refreshResult(reloadedToken.accessToken, false);
6283
6318
  const isExpired = tokens.expiresAt ? new Date >= new Date(tokens.expiresAt) : false;
6319
+ if (candidate.externallyUpdated && !isExpired) {
6320
+ return refreshResult(tokens.accessToken, false);
6321
+ }
6284
6322
  if (isExpired) {
6285
6323
  const currentStoredTokens = await this.loadExternallyUpdatedToken(tokens);
6286
- if (currentStoredTokens)
6287
- return currentStoredTokens.accessToken;
6324
+ if (currentStoredTokens) {
6325
+ return refreshResult(currentStoredTokens.accessToken, false);
6326
+ }
6288
6327
  const cleared = await withTelemetrySpan("token-manager.clear-tokens-if-unchanged", () => this.authStorage.clearTokensIfUnchanged(this.mcpUrl, tokens));
6289
6328
  if (!cleared) {
6290
6329
  const currentToken = await this.authStorage.loadTokens(this.mcpUrl);
6291
6330
  this.cachedToken = currentToken;
6292
- return currentToken?.accessToken;
6331
+ return refreshResult(currentToken?.accessToken, false);
6293
6332
  }
6294
6333
  this.cachedToken = null;
6295
6334
  }
6296
- return;
6335
+ return refreshResult(undefined, false);
6297
6336
  }
6298
6337
  const newTokenData = {
6299
6338
  accessToken: response.accessToken,
@@ -6301,30 +6340,53 @@ class TokenManager {
6301
6340
  expiresAt: new Date(Date.now() + response.expiresIn * 1000).toISOString(),
6302
6341
  createdAt: new Date().toISOString()
6303
6342
  };
6304
- const externallyUpdatedToken = await this.loadExternallyUpdatedToken(tokens, {
6305
- treatMissingAsExternalUpdate: true
6306
- });
6307
- if (externallyUpdatedToken === null)
6308
- return;
6309
- if (externallyUpdatedToken)
6310
- return externallyUpdatedToken.accessToken;
6311
6343
  const saved = await withTelemetrySpan("token-manager.save-tokens", () => this.authStorage.saveTokensIfUnchanged(this.mcpUrl, tokens, newTokenData));
6312
6344
  if (!saved) {
6313
- const currentToken = await this.authStorage.loadTokens(this.mcpUrl);
6314
- this.cachedToken = currentToken;
6315
- return currentToken?.accessToken;
6345
+ return this.resolveSuccessfulRefreshConflict(tokens, response, newTokenData);
6316
6346
  }
6317
6347
  this.cachedToken = newTokenData;
6318
- return response.accessToken;
6348
+ return refreshResult(response.accessToken, true);
6319
6349
  });
6320
6350
  }
6321
- async loadExternallyUpdatedToken(failedTokens, options = {}) {
6322
- const storedTokens = await withTelemetrySpan("token-manager.reload-tokens", () => this.authStorage.loadTokens(this.mcpUrl));
6351
+ async resolveSuccessfulRefreshConflict(refreshedFrom, response, newTokenData) {
6352
+ const currentToken = await withTelemetrySpan("token-manager.reload-tokens", () => this.authStorage.loadTokens(this.mcpUrl));
6353
+ if (!currentToken) {
6354
+ this.cachedToken = null;
6355
+ return refreshResult(undefined, false);
6356
+ }
6357
+ if (!response.refreshToken || currentToken.refreshToken !== refreshedFrom.refreshToken) {
6358
+ this.cachedToken = currentToken;
6359
+ return refreshResult(currentToken.accessToken, false);
6360
+ }
6361
+ const saved = await withTelemetrySpan("token-manager.save-rotated-tokens-after-conflict", () => this.authStorage.saveTokensIfUnchanged(this.mcpUrl, currentToken, newTokenData));
6362
+ if (saved) {
6363
+ this.cachedToken = newTokenData;
6364
+ return refreshResult(newTokenData.accessToken, true);
6365
+ }
6366
+ const latestToken = await withTelemetrySpan("token-manager.reload-tokens", () => this.authStorage.loadTokens(this.mcpUrl));
6367
+ this.cachedToken = latestToken;
6368
+ return refreshResult(latestToken?.accessToken, false);
6369
+ }
6370
+ async loadRefreshCandidate() {
6371
+ const storedTokens = await withTelemetrySpan("token-manager.load-tokens", () => this.authStorage.loadTokens(this.mcpUrl));
6372
+ if (!this.cachedToken) {
6373
+ this.cachedToken = storedTokens;
6374
+ return storedTokens ? { tokens: storedTokens, externallyUpdated: false } : null;
6375
+ }
6323
6376
  if (!storedTokens) {
6324
- if (options.treatMissingAsExternalUpdate)
6325
- this.cachedToken = null;
6326
- return options.treatMissingAsExternalUpdate ? null : undefined;
6377
+ this.cachedToken = null;
6378
+ return null;
6327
6379
  }
6380
+ if (!areSameTokenData(storedTokens, this.cachedToken)) {
6381
+ this.cachedToken = storedTokens;
6382
+ return { tokens: storedTokens, externallyUpdated: true };
6383
+ }
6384
+ return { tokens: this.cachedToken, externallyUpdated: false };
6385
+ }
6386
+ async loadExternallyUpdatedToken(failedTokens) {
6387
+ const storedTokens = await withTelemetrySpan("token-manager.reload-tokens", () => this.authStorage.loadTokens(this.mcpUrl));
6388
+ if (!storedTokens)
6389
+ return;
6328
6390
  if (areSameTokenData(storedTokens, failedTokens))
6329
6391
  return;
6330
6392
  this.cachedToken = storedTokens;
@@ -6334,6 +6396,9 @@ class TokenManager {
6334
6396
  function areSameTokenData(a, b) {
6335
6397
  return a.accessToken === b.accessToken && a.refreshToken === b.refreshToken && a.expiresAt === b.expiresAt && a.createdAt === b.createdAt;
6336
6398
  }
6399
+ function refreshResult(accessToken, refreshedViaEndpoint) {
6400
+ return { accessToken, refreshedViaEndpoint };
6401
+ }
6337
6402
  // src/services/update-check-service.ts
6338
6403
  import semver from "semver";
6339
6404
  var NPM_DIST_TAGS_URL = "https://registry.npmjs.org/-/package/githits/dist-tags";
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
3
  // package.json
4
- var version = "0.4.7";
4
+ var version = "0.4.9";
5
5
 
6
6
  export { __require, version };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "githits",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Code examples from global open source for developers and AI assistants.",
5
5
  "mcpServers": {
6
6
  "githits": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "githits",
3
3
  "description": "CLI companion for GitHits - code examples from global open source for developers and AI assistants",
4
- "version": "0.4.7",
4
+ "version": "0.4.9",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -83,6 +83,7 @@
83
83
  "open": "^11.0.0",
84
84
  "semver": "^7.8.0",
85
85
  "smol-toml": "^1.6.1",
86
+ "yaml": "^2.9.0",
86
87
  "zod": "^4.4.3"
87
88
  },
88
89
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "githits",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Code examples from global open source for developers and AI assistants",
5
5
  "author": {
6
6
  "name": "GitHits"