Version not found. Please check the version and try again.

@great-arrow/cli 0.1.3 → 0.1.4

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 +467 -99
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4703,6 +4703,271 @@ var updateCommand = new Command("update").description("Update a todo's status").
4703
4703
  var todoCommand = new Command("todo").description("Manage kanban todos").addCommand(listCommand2).addCommand(createCommand2).addCommand(doneCommand).addCommand(updateCommand);
4704
4704
  var todo_default = todoCommand;
4705
4705
 
4706
+ // src/commands/doctor.ts
4707
+ import { existsSync } from "node:fs";
4708
+ import { copyFile, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
4709
+ import { homedir as homedir2 } from "node:os";
4710
+ import { dirname as dirname2, join as join2 } from "node:path";
4711
+
4712
+ // src/lib/codex-config.ts
4713
+ import { execFileSync } from "node:child_process";
4714
+ var CODEX_SERVER_NAME = "great-arrow";
4715
+ var CODEX_CLIENT_ID = "codex-cli";
4716
+ var SAFE_GLOBAL_MCP_SERVERS = /* @__PURE__ */ new Set([
4717
+ CODEX_SERVER_NAME,
4718
+ "great-arrow-kiro-terminal",
4719
+ "chrome-devtools",
4720
+ "node_repl",
4721
+ "playwright",
4722
+ "postman",
4723
+ "stripe",
4724
+ "vercel",
4725
+ "xcodebuildmcp"
4726
+ ]);
4727
+ function normalizeApiUrl(apiUrl) {
4728
+ return apiUrl.replace(/\/+$/, "");
4729
+ }
4730
+ function mcpEndpoint(apiUrl) {
4731
+ return `${normalizeApiUrl(apiUrl)}/api/mcp`;
4732
+ }
4733
+ function tomlEscape(value) {
4734
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
4735
+ }
4736
+ function detectGitOrigin2(cwd = process.cwd()) {
4737
+ try {
4738
+ const env = { ...process.env };
4739
+ for (const key of Object.keys(env)) {
4740
+ if (key.startsWith("GIT_")) delete env[key];
4741
+ }
4742
+ const url = execFileSync(
4743
+ "git",
4744
+ ["-C", cwd, "remote", "get-url", "origin"],
4745
+ {
4746
+ encoding: "utf8",
4747
+ stdio: ["ignore", "pipe", "ignore"],
4748
+ env,
4749
+ timeout: 1e3
4750
+ }
4751
+ ).trim();
4752
+ return url || null;
4753
+ } catch {
4754
+ return null;
4755
+ }
4756
+ }
4757
+ function repoKey(repoUrl) {
4758
+ if (!repoUrl) return null;
4759
+ const trimmed = repoUrl.trim();
4760
+ if (!trimmed) return null;
4761
+ const scpLike = trimmed.match(/^git@([^:]+):(.+)$/);
4762
+ const asUrl = scpLike ? `https://${scpLike[1]}/${scpLike[2]}` : trimmed.replace(/^ssh:\/\/git@/i, "https://");
4763
+ try {
4764
+ const parsed = new URL(asUrl);
4765
+ const host = parsed.hostname.toLowerCase();
4766
+ let path = parsed.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
4767
+ path = path.replace(/\.git$/i, "").toLowerCase();
4768
+ return path ? `${host}/${path}` : null;
4769
+ } catch {
4770
+ return null;
4771
+ }
4772
+ }
4773
+ function generateCodexConfigToml(apiUrl, repoUrl) {
4774
+ const repoHeader = repoUrl ? `X-Repo-URL = "${tomlEscape(repoUrl)}"
4775
+ ` : "";
4776
+ return `[mcp_servers.${CODEX_SERVER_NAME}]
4777
+ transport = "streamable_http"
4778
+ url = "${tomlEscape(mcpEndpoint(apiUrl))}"
4779
+ bearer_token_env_var = "GREAT_ARROW_TOKEN"
4780
+
4781
+ [mcp_servers.${CODEX_SERVER_NAME}.http_headers]
4782
+ X-MCP-Client = "${CODEX_CLIENT_ID}"
4783
+ ${repoHeader}`;
4784
+ }
4785
+ function generateCodexEnv(token) {
4786
+ return `GREAT_ARROW_TOKEN=${token}
4787
+ `;
4788
+ }
4789
+ function extractCodexGreatArrowApiUrl(content) {
4790
+ const text = groupedServerText(splitTomlSections(content)).get(
4791
+ CODEX_SERVER_NAME
4792
+ );
4793
+ if (!text) return null;
4794
+ const match = text.match(/^\s*url\s*=\s*"([^"]+)"\s*$/m);
4795
+ if (!match?.[1]) return null;
4796
+ const url = match[1].trim();
4797
+ return url.endsWith("/api/mcp") ? url.slice(0, -"/api/mcp".length) : url.replace(/\/+$/, "");
4798
+ }
4799
+ function splitTomlSections(content) {
4800
+ const hasTrailingNewline = content.endsWith("\n");
4801
+ const lines = content.split(/\n/);
4802
+ if (hasTrailingNewline) lines.pop();
4803
+ const sections = [];
4804
+ let table = null;
4805
+ let current = [];
4806
+ const flush = () => {
4807
+ if (current.length === 0) return;
4808
+ sections.push({ table, text: `${current.join("\n")}
4809
+ ` });
4810
+ current = [];
4811
+ };
4812
+ for (const line of lines) {
4813
+ const match = line.match(/^\s*\[([^\]]+)]\s*$/);
4814
+ if (match) {
4815
+ flush();
4816
+ table = match[1] ?? null;
4817
+ }
4818
+ current.push(line);
4819
+ }
4820
+ flush();
4821
+ return sections;
4822
+ }
4823
+ function tableServerName(table) {
4824
+ if (!table) return null;
4825
+ const match = table.match(/^mcp_servers\.(?:"([^"]+)"|([^.\s]+))(?:\.|$)/);
4826
+ return match?.[1] ?? match?.[2] ?? null;
4827
+ }
4828
+ function groupedServerText(sections) {
4829
+ const grouped = /* @__PURE__ */ new Map();
4830
+ for (const section of sections) {
4831
+ const server = tableServerName(section.table);
4832
+ if (!server) continue;
4833
+ grouped.set(server, `${grouped.get(server) ?? ""}${section.text}`);
4834
+ }
4835
+ return grouped;
4836
+ }
4837
+ function removeServerSections(content, servers) {
4838
+ const sections = splitTomlSections(content);
4839
+ return sections.filter((section) => {
4840
+ const server = tableServerName(section.table);
4841
+ return !server || !servers.has(server);
4842
+ }).map((section) => section.text).join("").replace(/\n{3,}/g, "\n\n");
4843
+ }
4844
+ function upsertCodexServerBlock(existingContent, apiUrl, repoUrl) {
4845
+ const block = generateCodexConfigToml(apiUrl, repoUrl).trimEnd();
4846
+ const withoutGreatArrow = removeServerSections(
4847
+ existingContent,
4848
+ /* @__PURE__ */ new Set([CODEX_SERVER_NAME])
4849
+ ).replace(/\s*$/, "");
4850
+ if (!withoutGreatArrow) return `${block}
4851
+ `;
4852
+ return `${withoutGreatArrow}
4853
+
4854
+ ${block}
4855
+ `;
4856
+ }
4857
+ function extractConfiguredRepoKeys(text) {
4858
+ const keys = /* @__PURE__ */ new Set();
4859
+ const repoSetting = /(?:X-Repo-URL|MEMORY_REPO_URL|GAD_REPO_URL)\s*=\s*"([^"]+)"/gi;
4860
+ for (const match of text.matchAll(repoSetting)) {
4861
+ const key = repoKey(match[1]);
4862
+ if (key) keys.add(key);
4863
+ }
4864
+ const gitUrls = /(?:https:\/\/|ssh:\/\/git@|git@)(?:github\.com|gitlab\.com|bitbucket\.org)[:/][^\s"',\]]+/gi;
4865
+ for (const match of text.matchAll(gitUrls)) {
4866
+ const key = repoKey(match[0]);
4867
+ if (key) keys.add(key);
4868
+ }
4869
+ return [...keys];
4870
+ }
4871
+ function hasJobbyJobMarker(server, text) {
4872
+ return /jobby\s*job|jobbyjob|jobbyjob\.ai/i.test(`${server}
4873
+ ${text}`);
4874
+ }
4875
+ function isUnmanagedServer(server) {
4876
+ return !SAFE_GLOBAL_MCP_SERVERS.has(server);
4877
+ }
4878
+ function inspectCodexConfig(content, expectedRepoUrl) {
4879
+ const expectedRepoKey = repoKey(expectedRepoUrl);
4880
+ const grouped = groupedServerText(splitTomlSections(content));
4881
+ const serverNames = [...grouped.keys()].sort();
4882
+ const greatArrowText = grouped.get(CODEX_SERVER_NAME) ?? "";
4883
+ const hasGreatArrowServer = grouped.has(CODEX_SERVER_NAME);
4884
+ const greatArrowRepoKeys = extractConfiguredRepoKeys(greatArrowText);
4885
+ const greatArrowHasRepoHeader = Boolean(
4886
+ expectedRepoKey && greatArrowRepoKeys.includes(expectedRepoKey)
4887
+ );
4888
+ const unmanagedServers = serverNames.filter(isUnmanagedServer);
4889
+ const staleRepoServers = [];
4890
+ for (const [server, text] of grouped.entries()) {
4891
+ if (server === CODEX_SERVER_NAME) continue;
4892
+ const keys = extractConfiguredRepoKeys(text);
4893
+ const mismatchedExplicitRepo = expectedRepoKey && keys.length > 0 && !keys.some((key) => key === expectedRepoKey);
4894
+ if (mismatchedExplicitRepo || hasJobbyJobMarker(server, text)) {
4895
+ staleRepoServers.push(server);
4896
+ }
4897
+ }
4898
+ staleRepoServers.sort();
4899
+ const issues = [];
4900
+ issues.push(
4901
+ hasGreatArrowServer ? {
4902
+ code: "great_arrow_present",
4903
+ level: "pass",
4904
+ message: "Codex has a Great Arrow MCP server block."
4905
+ } : {
4906
+ code: "great_arrow_missing",
4907
+ level: "fail",
4908
+ message: "Codex is missing the Great Arrow MCP server block.",
4909
+ hint: "Run `gad mcp install codex` from the repo root."
4910
+ }
4911
+ );
4912
+ if (expectedRepoKey) {
4913
+ issues.push(
4914
+ greatArrowHasRepoHeader ? {
4915
+ code: "repo_header_present",
4916
+ level: "pass",
4917
+ message: `Great Arrow is pinned to ${expectedRepoUrl}.`
4918
+ } : {
4919
+ code: "repo_header_missing",
4920
+ level: "warn",
4921
+ message: "Great Arrow is not pinned with X-Repo-URL.",
4922
+ hint: "Run `gad mcp install codex` from the repo root to add repo routing."
4923
+ }
4924
+ );
4925
+ }
4926
+ if (staleRepoServers.length > 0) {
4927
+ issues.push({
4928
+ code: "stale_repo_servers",
4929
+ level: "fail",
4930
+ message: `Found stale repo-scoped MCP server(s): ${staleRepoServers.join(", ")}.`,
4931
+ hint: "Run `gad doctor codex --enforce` to remove the stale server blocks."
4932
+ });
4933
+ } else {
4934
+ issues.push({
4935
+ code: "no_stale_repo_servers",
4936
+ level: "pass",
4937
+ message: "No stale repo-scoped MCP server blocks found."
4938
+ });
4939
+ }
4940
+ if (unmanagedServers.length > 0) {
4941
+ issues.push({
4942
+ code: "unmanaged_servers",
4943
+ level: "warn",
4944
+ message: `Codex has unmanaged MCP server(s): ${unmanagedServers.join(", ")}.`,
4945
+ hint: "Review these manually if they are not intentionally global."
4946
+ });
4947
+ }
4948
+ return {
4949
+ expectedRepoUrl: expectedRepoUrl ?? null,
4950
+ expectedRepoKey,
4951
+ hasGreatArrowServer,
4952
+ greatArrowHasRepoHeader,
4953
+ unmanagedServers,
4954
+ staleRepoServers,
4955
+ issues
4956
+ };
4957
+ }
4958
+ function repairCodexConfig(content, staleRepoServers) {
4959
+ if (staleRepoServers.length === 0) {
4960
+ return { content, removedServers: [] };
4961
+ }
4962
+ const removed = [...new Set(staleRepoServers)].sort();
4963
+ const repaired = removeServerSections(content, new Set(removed));
4964
+ return {
4965
+ content: repaired.endsWith("\n") ? repaired : `${repaired}
4966
+ `,
4967
+ removedServers: removed
4968
+ };
4969
+ }
4970
+
4706
4971
  // src/commands/doctor.ts
4707
4972
  function renderCheck(check, color2) {
4708
4973
  const { status, name, message, hint } = check;
@@ -4841,6 +5106,133 @@ async function checkMcp(auth) {
4841
5106
  };
4842
5107
  }
4843
5108
  }
5109
+ function defaultCodexConfigPath() {
5110
+ return join2(homedir2(), ".codex", "config.toml");
5111
+ }
5112
+ function codexIssuesToChecks(issues) {
5113
+ return issues.map((issue) => {
5114
+ const check = {
5115
+ name: `Codex Config: ${issue.code}`,
5116
+ status: issue.level,
5117
+ message: issue.server ? `${issue.message} (${issue.server})` : issue.message
5118
+ };
5119
+ if (issue.hint) check.hint = issue.hint;
5120
+ return check;
5121
+ });
5122
+ }
5123
+ function codexCommand() {
5124
+ return new Command("codex").description(
5125
+ "Check Codex MCP config for wrong-repo entries and optional repair"
5126
+ ).option(
5127
+ "--config <path>",
5128
+ "Path to Codex config.toml",
5129
+ defaultCodexConfigPath()
5130
+ ).option(
5131
+ "--expected-repo <url>",
5132
+ "Expected repo URL (defaults to current git origin)"
5133
+ ).option(
5134
+ "--enforce",
5135
+ "Back up config, rewrite Great Arrow repo pin, and remove stale repo-scoped MCP servers"
5136
+ ).option("--json", "Output as JSON").option("--quiet", "Suppress decorative output").option("--no-color", "Disable color output").action(
5137
+ async (options, command) => {
5138
+ const g = globalOutputOpts(command);
5139
+ const color2 = createColor(g.noColor);
5140
+ const fmt = createFormatter({
5141
+ json: g.json,
5142
+ quiet: g.quiet,
5143
+ isTTY: isTTY(),
5144
+ noColor: g.noColor
5145
+ });
5146
+ const configPath = options.config ?? defaultCodexConfigPath();
5147
+ const expectedRepoUrl = options.expectedRepo ?? detectGitOrigin2();
5148
+ let content = existsSync(configPath) ? await readFile2(configPath, "utf8") : "";
5149
+ let backupPath = null;
5150
+ let removedServers = [];
5151
+ if (options.enforce) {
5152
+ const apiUrl = extractCodexGreatArrowApiUrl(content) ?? (await resolveAuth()).apiUrl;
5153
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5154
+ await mkdir2(dirname2(configPath), { recursive: true });
5155
+ if (content.trim()) {
5156
+ backupPath = `${configPath}.bak.${timestamp}`;
5157
+ await copyFile(configPath, backupPath);
5158
+ }
5159
+ content = upsertCodexServerBlock(content, apiUrl, expectedRepoUrl);
5160
+ const preRepair = inspectCodexConfig(content, expectedRepoUrl);
5161
+ const repaired = repairCodexConfig(
5162
+ content,
5163
+ preRepair.staleRepoServers
5164
+ );
5165
+ content = repaired.content;
5166
+ removedServers = repaired.removedServers;
5167
+ await writeFile2(configPath, content, "utf8");
5168
+ }
5169
+ const inspection = inspectCodexConfig(content, expectedRepoUrl);
5170
+ const checks = codexIssuesToChecks(inspection.issues);
5171
+ if (options.enforce) {
5172
+ const enforcementCheck = {
5173
+ name: "Codex Config: enforcement",
5174
+ status: "pass",
5175
+ message: removedServers.length > 0 ? `Removed stale server(s): ${removedServers.join(", ")}` : "No stale server blocks needed removal"
5176
+ };
5177
+ if (backupPath) {
5178
+ enforcementCheck.hint = `Backup written to ${backupPath}`;
5179
+ }
5180
+ checks.push(enforcementCheck);
5181
+ }
5182
+ if (g.json) {
5183
+ process.stdout.write(
5184
+ JSON.stringify(
5185
+ {
5186
+ configPath,
5187
+ expectedRepoUrl,
5188
+ enforce: Boolean(options.enforce),
5189
+ backupPath,
5190
+ removedServers,
5191
+ inspection,
5192
+ checks
5193
+ },
5194
+ null,
5195
+ 2
5196
+ ) + "\n"
5197
+ );
5198
+ } else {
5199
+ if (!expectedRepoUrl) {
5200
+ process.stdout.write(
5201
+ renderCheck(
5202
+ {
5203
+ name: "Codex Config: repo detection",
5204
+ status: "warn",
5205
+ message: "No git origin detected for this directory",
5206
+ hint: "Run from a repo root or pass --expected-repo."
5207
+ },
5208
+ color2
5209
+ ) + "\n"
5210
+ );
5211
+ }
5212
+ for (const check of checks) {
5213
+ process.stdout.write(renderCheck(check, color2) + "\n");
5214
+ }
5215
+ if (!g.quiet) {
5216
+ const failed = checks.filter((c) => c.status === "fail").length;
5217
+ const warned = checks.filter((c) => c.status === "warn").length;
5218
+ process.stdout.write("\n");
5219
+ if (failed === 0 && warned === 0) {
5220
+ fmt.success("Codex config guard passed");
5221
+ } else if (failed === 0) {
5222
+ fmt.warn(`Codex config guard passed with ${warned} warning(s)`);
5223
+ } else {
5224
+ fmt.error(
5225
+ `Codex config guard found ${failed} failure(s), ${warned} warning(s)`
5226
+ );
5227
+ }
5228
+ }
5229
+ }
5230
+ if (checks.some((check) => check.status === "fail")) {
5231
+ process.exitCode = 1;
5232
+ }
5233
+ }
5234
+ );
5235
+ }
4844
5236
  function doctorCommand() {
4845
5237
  const cmd = new Command("doctor").description("Run a comprehensive health check").option("--json", "Output as JSON").option("--quiet", "Suppress decorative output").option("--no-color", "Disable color output").action(async (options, command) => {
4846
5238
  const g = globalOutputOpts(command);
@@ -4889,6 +5281,7 @@ function doctorCommand() {
4889
5281
  }
4890
5282
  }
4891
5283
  });
5284
+ cmd.addCommand(codexCommand());
4892
5285
  return cmd;
4893
5286
  }
4894
5287
 
@@ -5051,26 +5444,20 @@ var SUPPORTED_CLIENTS = [
5051
5444
  "codex",
5052
5445
  "gemini"
5053
5446
  ];
5054
- var MCP_SERVER_NAME = "great-arrow";
5447
+ var MCP_SERVER_NAME = CODEX_SERVER_NAME;
5055
5448
  var CLIENT_IDS = {
5056
5449
  claude: "claude-code-cli",
5057
5450
  cursor: "cursor",
5058
5451
  kiro: "kiro",
5059
- codex: "codex-cli",
5452
+ codex: CODEX_CLIENT_ID,
5060
5453
  gemini: "gemini-cli"
5061
5454
  };
5062
- function normalizeApiUrl(apiUrl) {
5063
- return apiUrl.replace(/\/+$/, "");
5064
- }
5065
- function mcpEndpoint(apiUrl) {
5066
- return `${normalizeApiUrl(apiUrl)}/api/mcp`;
5067
- }
5068
5455
  async function createClient3() {
5069
5456
  const auth = await resolveAuth();
5070
5457
  const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
5071
5458
  return { client, apiUrl: auth.apiUrl };
5072
5459
  }
5073
- function generateMcpConfig(client, apiUrl, token) {
5460
+ function generateMcpConfig(client, apiUrl, token, repoUrl) {
5074
5461
  const endpoint = mcpEndpoint(apiUrl);
5075
5462
  if (client === "codex") {
5076
5463
  return {
@@ -5084,7 +5471,11 @@ function generateMcpConfig(client, apiUrl, token) {
5084
5471
  MEMORY_API_URL: apiUrl,
5085
5472
  MEMORY_CLIENT_NAME: CLIENT_IDS.codex,
5086
5473
  GAD_TOKEN: token,
5087
- GAD_API_URL: apiUrl
5474
+ GAD_API_URL: apiUrl,
5475
+ ...repoUrl ? {
5476
+ MEMORY_REPO_URL: repoUrl,
5477
+ GAD_REPO_URL: repoUrl
5478
+ } : {}
5088
5479
  }
5089
5480
  }
5090
5481
  }
@@ -5097,44 +5488,13 @@ function generateMcpConfig(client, apiUrl, token) {
5097
5488
  url: endpoint,
5098
5489
  headers: {
5099
5490
  Authorization: `Bearer ${token}`,
5100
- "X-MCP-Client": CLIENT_IDS[client]
5491
+ "X-MCP-Client": CLIENT_IDS[client],
5492
+ ...repoUrl ? { "X-Repo-URL": repoUrl } : {}
5101
5493
  }
5102
5494
  }
5103
5495
  }
5104
5496
  };
5105
5497
  }
5106
- function tomlEscape(value) {
5107
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
5108
- }
5109
- function generateCodexConfigToml(apiUrl) {
5110
- return `[mcp_servers.${MCP_SERVER_NAME}]
5111
- transport = "streamable_http"
5112
- url = "${tomlEscape(mcpEndpoint(apiUrl))}"
5113
- bearer_token_env_var = "GREAT_ARROW_TOKEN"
5114
- `;
5115
- }
5116
- function generateCodexEnv(token) {
5117
- return `GREAT_ARROW_TOKEN=${token}
5118
- `;
5119
- }
5120
- function upsertCodexServerBlock(existingContent, apiUrl) {
5121
- const block = generateCodexConfigToml(apiUrl).trimEnd();
5122
- const serverBlockPattern = /^\[mcp_servers\.great-arrow\]\n[\s\S]*?(?=^\[|\s*$)/m;
5123
- if (!existingContent.trim()) {
5124
- return `${block}
5125
- `;
5126
- }
5127
- if (serverBlockPattern.test(existingContent)) {
5128
- const updated = existingContent.replace(serverBlockPattern, block);
5129
- return updated.endsWith("\n") ? updated : `${updated}
5130
- `;
5131
- }
5132
- const trimmedRight = existingContent.replace(/\s*$/, "");
5133
- return `${trimmedRight}
5134
-
5135
- ${block}
5136
- `;
5137
- }
5138
5498
  function upsertEnvValue(existingContent, key, value) {
5139
5499
  const lines = existingContent.split(/\r?\n/).filter((line) => line.trim() !== "" && !line.startsWith(`${key}=`));
5140
5500
  lines.push(`${key}=${value}`);
@@ -5150,29 +5510,29 @@ async function pathExists(path) {
5150
5510
  return false;
5151
5511
  }
5152
5512
  }
5153
- async function installCodexConfig(apiUrl, token) {
5154
- const { homedir: homedir2 } = await import("node:os");
5155
- const { join: join2 } = await import("node:path");
5156
- const { chmod, copyFile, mkdir: mkdir2, readFile: readFile2, writeFile: writeFile2 } = await import("node:fs/promises");
5157
- const codexDir = join2(homedir2(), ".codex");
5158
- const configPath = join2(codexDir, "config.toml");
5159
- const envPath = join2(codexDir, ".env");
5513
+ async function installCodexConfig(apiUrl, token, repoUrl) {
5514
+ const { homedir: homedir3 } = await import("node:os");
5515
+ const { join: join3 } = await import("node:path");
5516
+ const { chmod, copyFile: copyFile2, mkdir: mkdir3, readFile: readFile3, writeFile: writeFile3 } = await import("node:fs/promises");
5517
+ const codexDir = join3(homedir3(), ".codex");
5518
+ const configPath = join3(codexDir, "config.toml");
5519
+ const envPath = join3(codexDir, ".env");
5160
5520
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5161
- await mkdir2(codexDir, { recursive: true });
5162
- const existingConfig = await pathExists(configPath) ? await readFile2(configPath, "utf8") : "";
5521
+ await mkdir3(codexDir, { recursive: true });
5522
+ const existingConfig = await pathExists(configPath) ? await readFile3(configPath, "utf8") : "";
5163
5523
  if (existingConfig.trim()) {
5164
- await copyFile(configPath, `${configPath}.bak.${timestamp}`);
5524
+ await copyFile2(configPath, `${configPath}.bak.${timestamp}`);
5165
5525
  }
5166
- await writeFile2(
5526
+ await writeFile3(
5167
5527
  configPath,
5168
- upsertCodexServerBlock(existingConfig, apiUrl),
5528
+ upsertCodexServerBlock(existingConfig, apiUrl, repoUrl),
5169
5529
  "utf8"
5170
5530
  );
5171
- const existingEnv = await pathExists(envPath) ? await readFile2(envPath, "utf8") : "";
5531
+ const existingEnv = await pathExists(envPath) ? await readFile3(envPath, "utf8") : "";
5172
5532
  if (existingEnv.trim()) {
5173
- await copyFile(envPath, `${envPath}.bak.${timestamp}`);
5533
+ await copyFile2(envPath, `${envPath}.bak.${timestamp}`);
5174
5534
  }
5175
- await writeFile2(
5535
+ await writeFile3(
5176
5536
  envPath,
5177
5537
  upsertEnvValue(existingEnv, "GREAT_ARROW_TOKEN", token),
5178
5538
  "utf8"
@@ -5387,54 +5747,62 @@ ${summary}.
5387
5747
  var installCommand = new Command("install").description("Generate MCP configuration for a supported AI client").argument(
5388
5748
  "<client>",
5389
5749
  `AI client to configure (${SUPPORTED_CLIENTS.join(", ")})`
5390
- ).option("--stdout", "Print config to stdout instead of writing file").action(async (clientName, options) => {
5391
- const fmt = createFormatter({
5392
- json: false,
5393
- quiet: false,
5394
- isTTY: isTTY(),
5395
- noColor: false
5396
- });
5397
- if (!SUPPORTED_CLIENTS.includes(clientName)) {
5398
- fmt.error(
5399
- `Unsupported client: "${clientName}". Supported clients: ${SUPPORTED_CLIENTS.join(", ")}`
5400
- );
5401
- process.exit(3);
5402
- }
5403
- const auth = await resolveAuth();
5404
- if (clientName === "codex") {
5405
- if (options.stdout) {
5406
- process.stdout.write(`# ~/.codex/config.toml
5750
+ ).option("--stdout", "Print config to stdout instead of writing file").option(
5751
+ "--repo-url <url>",
5752
+ "Repo URL to pin MCP requests to (defaults to current git origin)"
5753
+ ).action(
5754
+ async (clientName, options) => {
5755
+ const fmt = createFormatter({
5756
+ json: false,
5757
+ quiet: false,
5758
+ isTTY: isTTY(),
5759
+ noColor: false
5760
+ });
5761
+ if (!SUPPORTED_CLIENTS.includes(clientName)) {
5762
+ fmt.error(
5763
+ `Unsupported client: "${clientName}". Supported clients: ${SUPPORTED_CLIENTS.join(", ")}`
5764
+ );
5765
+ process.exit(3);
5766
+ }
5767
+ const auth = await resolveAuth();
5768
+ const repoUrl = options.repoUrl ?? detectGitOrigin2();
5769
+ if (clientName === "codex") {
5770
+ if (options.stdout) {
5771
+ process.stdout.write(`# ~/.codex/config.toml
5407
5772
  `);
5408
- process.stdout.write(generateCodexConfigToml(auth.apiUrl));
5409
- process.stdout.write(`
5773
+ process.stdout.write(generateCodexConfigToml(auth.apiUrl, repoUrl));
5774
+ process.stdout.write(`
5410
5775
  # ~/.codex/.env
5411
5776
  `);
5412
- process.stdout.write(generateCodexEnv(auth.token));
5777
+ process.stdout.write(generateCodexEnv(auth.token));
5778
+ return;
5779
+ }
5780
+ const { configPath, envPath } = await installCodexConfig(
5781
+ auth.apiUrl,
5782
+ auth.token,
5783
+ repoUrl
5784
+ );
5785
+ fmt.success(`Wrote Codex MCP config to ${configPath} and ${envPath}`);
5413
5786
  return;
5414
5787
  }
5415
- const { configPath, envPath } = await installCodexConfig(
5788
+ const config = generateMcpConfig(
5789
+ clientName,
5416
5790
  auth.apiUrl,
5417
- auth.token
5791
+ auth.token,
5792
+ repoUrl
5418
5793
  );
5419
- fmt.success(`Wrote Codex MCP config to ${configPath} and ${envPath}`);
5420
- return;
5421
- }
5422
- const config = generateMcpConfig(
5423
- clientName,
5424
- auth.apiUrl,
5425
- auth.token
5426
- );
5427
- const configJson = JSON.stringify(config, null, 2);
5428
- if (options.stdout) {
5429
- process.stdout.write(configJson + "\n");
5430
- return;
5794
+ const configJson = JSON.stringify(config, null, 2);
5795
+ if (options.stdout) {
5796
+ process.stdout.write(configJson + "\n");
5797
+ return;
5798
+ }
5799
+ const { writeFile: writeFile3 } = await import("node:fs/promises");
5800
+ const { join: join3 } = await import("node:path");
5801
+ const outputPath = join3(process.cwd(), ".mcp.json");
5802
+ await writeFile3(outputPath, configJson + "\n", "utf-8");
5803
+ fmt.success(`Wrote ${clientName} MCP config to .mcp.json`);
5431
5804
  }
5432
- const { writeFile: writeFile2 } = await import("node:fs/promises");
5433
- const { join: join2 } = await import("node:path");
5434
- const outputPath = join2(process.cwd(), ".mcp.json");
5435
- await writeFile2(outputPath, configJson + "\n", "utf-8");
5436
- fmt.success(`Wrote ${clientName} MCP config to .mcp.json`);
5437
- });
5805
+ );
5438
5806
  var mcpCommand = new Command("mcp").description("Manage and diagnose the MCP server connection").addCommand(statusCommand).addCommand(logsCommand).addCommand(testCommand).addCommand(installCommand);
5439
5807
  var mcp_default = mcpCommand;
5440
5808
 
@@ -6057,7 +6425,7 @@ if (nodeVersion < 20) {
6057
6425
  console.error(" Upgrade Node.js: https://nodejs.org/");
6058
6426
  process.exit(1);
6059
6427
  }
6060
- var VERSION = "0.1.3";
6428
+ var VERSION = "0.1.4";
6061
6429
  var program2 = new Command().name("gad").description("Great Arrow Digital CLI \u2014 terminal access to the GAD platform").version(VERSION, "-v, --version").option("--json", "Output as JSON").option("--quiet", "Suppress decorative output").option("--no-color", "Disable color output");
6062
6430
  program2.addCommand(login_default);
6063
6431
  program2.addCommand(logoutCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@great-arrow/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Great Arrow Digital CLI tool",
5
5
  "type": "module",
6
6
  "bin": {