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.
- package/dist/index.js +467 -99
- 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 =
|
|
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:
|
|
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:
|
|
5155
|
-
const { join:
|
|
5156
|
-
const { chmod, copyFile, mkdir:
|
|
5157
|
-
const codexDir =
|
|
5158
|
-
const configPath =
|
|
5159
|
-
const envPath =
|
|
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
|
|
5162
|
-
const existingConfig = await pathExists(configPath) ? await
|
|
5521
|
+
await mkdir3(codexDir, { recursive: true });
|
|
5522
|
+
const existingConfig = await pathExists(configPath) ? await readFile3(configPath, "utf8") : "";
|
|
5163
5523
|
if (existingConfig.trim()) {
|
|
5164
|
-
await
|
|
5524
|
+
await copyFile2(configPath, `${configPath}.bak.${timestamp}`);
|
|
5165
5525
|
}
|
|
5166
|
-
await
|
|
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
|
|
5531
|
+
const existingEnv = await pathExists(envPath) ? await readFile3(envPath, "utf8") : "";
|
|
5172
5532
|
if (existingEnv.trim()) {
|
|
5173
|
-
await
|
|
5533
|
+
await copyFile2(envPath, `${envPath}.bak.${timestamp}`);
|
|
5174
5534
|
}
|
|
5175
|
-
await
|
|
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").
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
);
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
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
|
-
|
|
5409
|
-
|
|
5773
|
+
process.stdout.write(generateCodexConfigToml(auth.apiUrl, repoUrl));
|
|
5774
|
+
process.stdout.write(`
|
|
5410
5775
|
# ~/.codex/.env
|
|
5411
5776
|
`);
|
|
5412
|
-
|
|
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
|
|
5788
|
+
const config = generateMcpConfig(
|
|
5789
|
+
clientName,
|
|
5416
5790
|
auth.apiUrl,
|
|
5417
|
-
auth.token
|
|
5791
|
+
auth.token,
|
|
5792
|
+
repoUrl
|
|
5418
5793
|
);
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
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
|
-
|
|
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.
|
|
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);
|