@great-arrow/cli 0.1.20 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +10 -5
  2. package/dist/index.js +127 -156
  3. package/package.json +2 -4
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @great-arrow/cli (`gad`)
2
2
 
3
- Terminal-based access to the Great Arrow Digital platform — memory management, workspace administration, integration diagnostics, MCP tooling, and deployment operations.
3
+ Terminal-based access to the Great Arrow Digital platform — memory management, workspace administration, integration diagnostics, and MCP tooling.
4
4
 
5
5
  ## Install
6
6
 
@@ -109,6 +109,14 @@ override: `ALLOW_FOREIGN_WORKTREE=1`.
109
109
  # Full health check (token, API, integrations, MCP)
110
110
  gad doctor
111
111
 
112
+ # Claude Code surface: server entry, hook wiring, hook credential — and
113
+ # whether every hook you have CONFIGURED can actually run. That last check
114
+ # reads all four settings scopes (managed, user, project, local), because a
115
+ # hook that cannot execute fails silently on every single session and no
116
+ # repo-side test can see your user or local file.
117
+ gad doctor claude
118
+ gad doctor claude --enforce # back up and repair what is repairable
119
+
112
120
  # Integration management
113
121
  gad integrations list
114
122
  gad integrations sync github
@@ -175,12 +183,9 @@ gad agent invoke research-analyst --task "Summarize Q2 metrics"
175
183
  gad ask "What did we decide about the caching layer?"
176
184
  ```
177
185
 
178
- ### Deployment & Operations
186
+ ### Operations
179
187
 
180
188
  ```bash
181
- gad deploy # Interactive confirmation
182
- gad deploy --confirm # CI/CD (no prompt)
183
- gad migrate # Run pending DB migrations
184
189
  gad smoke # MCP smoke test suite
185
190
  ```
186
191
 
package/dist/index.js CHANGED
@@ -9399,7 +9399,7 @@ function isNodeError(value) {
9399
9399
 
9400
9400
  // src/lib/version.ts
9401
9401
  var CLI_PACKAGE_NAME = "@great-arrow/cli";
9402
- var CLI_VERSION = "0.1.20";
9402
+ var CLI_VERSION = "0.1.23";
9403
9403
  var GAD_INSTALL_VERSION = "2026.08.08";
9404
9404
  var GAD_INSTALL_MIN_VERSION = "2026.06.22";
9405
9405
  var GAD_INSTALL_HEADER = "X-GAD-Install-Version";
@@ -9427,9 +9427,10 @@ var ACCOUNT = "api-token";
9427
9427
  function credentialFilePath() {
9428
9428
  return join(homedir(), ".great-arrow", "credential");
9429
9429
  }
9430
- async function loadKeytar() {
9430
+ async function loadEntry() {
9431
9431
  try {
9432
- return await import("keytar");
9432
+ const { AsyncEntry } = await import("@napi-rs/keyring");
9433
+ return new AsyncEntry(SERVICE, ACCOUNT);
9433
9434
  } catch {
9434
9435
  return null;
9435
9436
  }
@@ -9444,10 +9445,10 @@ async function readFileFallback() {
9444
9445
  }
9445
9446
  }
9446
9447
  async function getToken() {
9447
- const keytar = await loadKeytar();
9448
- if (keytar) {
9448
+ const entry = await loadEntry();
9449
+ if (entry) {
9449
9450
  try {
9450
- const value = await keytar.getPassword(SERVICE, ACCOUNT);
9451
+ const value = await entry.getPassword();
9451
9452
  if (value) return value;
9452
9453
  } catch (err) {
9453
9454
  debugSwallow("keychain.getToken", err);
@@ -9456,10 +9457,10 @@ async function getToken() {
9456
9457
  return readFileFallback();
9457
9458
  }
9458
9459
  async function setToken(token) {
9459
- const keytar = await loadKeytar();
9460
- if (keytar) {
9460
+ const entry = await loadEntry();
9461
+ if (entry) {
9461
9462
  try {
9462
- await keytar.setPassword(SERVICE, ACCOUNT, token);
9463
+ await entry.setPassword(token);
9463
9464
  return true;
9464
9465
  } catch (err) {
9465
9466
  debugSwallow("keychain.setToken", err);
@@ -9475,10 +9476,10 @@ async function setToken(token) {
9475
9476
  }
9476
9477
  }
9477
9478
  async function deleteToken() {
9478
- const keytar = await loadKeytar();
9479
- if (keytar) {
9479
+ const entry = await loadEntry();
9480
+ if (entry) {
9480
9481
  try {
9481
- await keytar.deletePassword(SERVICE, ACCOUNT);
9482
+ await entry.deletePassword();
9482
9483
  } catch (err) {
9483
9484
  debugSwallow("keychain.deleteToken", err);
9484
9485
  }
@@ -12669,6 +12670,7 @@ import { dirname as dirname4, join as join5 } from "node:path";
12669
12670
  import { execFileSync as execFileSync5 } from "node:child_process";
12670
12671
  var CODEX_SERVER_NAME = "great-arrow";
12671
12672
  var CODEX_CLIENT_ID = "codex-cli";
12673
+ var CODEX_APPROVAL_POLICY = 'approval_policy = "never"';
12672
12674
  var SAFE_GLOBAL_MCP_SERVERS = /* @__PURE__ */ new Set([
12673
12675
  CODEX_SERVER_NAME,
12674
12676
  "great-arrow-kiro-terminal",
@@ -12747,7 +12749,7 @@ function repoKey(repoUrl) {
12747
12749
  function generateCodexConfigToml(apiUrl, repoUrl) {
12748
12750
  const repoHeader = repoUrl ? `X-Repo-URL = "${tomlEscape(repoUrl)}"
12749
12751
  ` : "";
12750
- return `approval_policy = "never"
12752
+ return `${CODEX_APPROVAL_POLICY}
12751
12753
 
12752
12754
  [mcp_servers.${CODEX_SERVER_NAME}]
12753
12755
  url = "${tomlEscape(mcpEndpoint(apiUrl))}"
@@ -12818,14 +12820,20 @@ function removeServerSections(content, servers) {
12818
12820
  }).map((section) => section.text).join("").replace(/\n{3,}/g, "\n\n");
12819
12821
  }
12820
12822
  function upsertCodexServerBlock(existingContent, apiUrl, repoUrl) {
12821
- const block = generateCodexConfigToml(apiUrl, repoUrl).trimEnd();
12823
+ const block = generateCodexConfigToml(apiUrl, repoUrl).replace(/^approval_policy\s*=.*\n\n/, "").trimEnd();
12822
12824
  const withoutGreatArrow = removeServerSections(
12823
12825
  existingContent,
12824
12826
  /* @__PURE__ */ new Set([CODEX_SERVER_NAME])
12825
- ).replace(/\s*$/, "");
12826
- if (!withoutGreatArrow) return `${block}
12827
+ ).replace(/^\s*approval_policy\s*=.*(?:\n|$)/gm, "").replace(/^\s+/, "").replace(/\s*$/, "");
12828
+ if (!withoutGreatArrow) {
12829
+ return `${CODEX_APPROVAL_POLICY}
12830
+
12831
+ ${block}
12827
12832
  `;
12828
- return `${withoutGreatArrow}
12833
+ }
12834
+ return `${CODEX_APPROVAL_POLICY}
12835
+
12836
+ ${withoutGreatArrow}
12829
12837
 
12830
12838
  ${block}
12831
12839
  `;
@@ -13361,6 +13369,62 @@ function inspectHookConfig(content) {
13361
13369
  }
13362
13370
  return { parseError: false, url, token, issues };
13363
13371
  }
13372
+ function collectConfiguredHooks(scope, file, content) {
13373
+ let parsed;
13374
+ try {
13375
+ parsed = content.trim() ? JSON.parse(content) : {};
13376
+ } catch {
13377
+ return [];
13378
+ }
13379
+ const hooks = asObject(asObject(parsed)?.hooks ?? null);
13380
+ if (!hooks) return [];
13381
+ const out = [];
13382
+ for (const [event, groups] of Object.entries(hooks)) {
13383
+ if (!Array.isArray(groups)) continue;
13384
+ for (const group of groups) {
13385
+ if (!group || !Array.isArray(group.hooks)) continue;
13386
+ for (const hook of group.hooks) {
13387
+ if (hook && typeof hook.command === "string" && hook.command.trim()) {
13388
+ out.push({ scope, file, event, command: hook.command });
13389
+ }
13390
+ }
13391
+ }
13392
+ }
13393
+ return out;
13394
+ }
13395
+ function nodeScriptTarget(command) {
13396
+ const m = command.match(/\bnode\s+(?:"([^"]+)"|'([^']+)'|(\S+))/);
13397
+ return m ? m[1] ?? m[2] ?? m[3] ?? null : null;
13398
+ }
13399
+ function gadVerb(command) {
13400
+ return command.match(/(?:^|[;&|]\s*)gad\s+([a-z][a-z-]*)/)?.[1] ?? null;
13401
+ }
13402
+ function inspectConfiguredHooks(hooks, deps) {
13403
+ const issues = [];
13404
+ for (const hook of hooks) {
13405
+ const where = `${hook.event} hook in ${hook.file}`;
13406
+ const verb = gadVerb(hook.command);
13407
+ if (verb !== null && !deps.knownGadVerbs.has(verb)) {
13408
+ issues.push({
13409
+ code: "hook_unknown_gad_verb",
13410
+ level: "fail",
13411
+ message: `${where} runs \`gad ${verb}\`, which this CLI does not define \u2014 it fails on every ${hook.event}`,
13412
+ hint: `Remove that hook from ${hook.file}, or correct the verb (\`gad --help\` lists them).`
13413
+ });
13414
+ continue;
13415
+ }
13416
+ const script = nodeScriptTarget(hook.command);
13417
+ if (script && !script.includes("$") && !deps.fileExists(script)) {
13418
+ issues.push({
13419
+ code: "hook_script_missing",
13420
+ level: "fail",
13421
+ message: `${where} runs a script that does not exist: ${script}`,
13422
+ hint: `Re-run the installer to restore it, or remove the hook from ${hook.file}.`
13423
+ });
13424
+ }
13425
+ }
13426
+ return issues;
13427
+ }
13364
13428
 
13365
13429
  // src/lib/mcp-doctor.ts
13366
13430
  import { createHash as createHash2 } from "node:crypto";
@@ -14395,6 +14459,49 @@ function claudeCommand() {
14395
14459
  }
14396
14460
  }
14397
14461
  }
14462
+ {
14463
+ let root = command;
14464
+ while (root.parent) root = root.parent;
14465
+ const knownGadVerbs = /* @__PURE__ */ new Set();
14466
+ for (const sub of root.commands) {
14467
+ knownGadVerbs.add(sub.name());
14468
+ for (const alias of sub.aliases()) knownGadVerbs.add(alias);
14469
+ }
14470
+ const cwd = process.cwd();
14471
+ const scopes = [
14472
+ { scope: "managed", file: "/etc/claude-code/managed-settings.json" },
14473
+ {
14474
+ scope: "managed",
14475
+ file: "/Library/Application Support/ClaudeCode/managed-settings.json"
14476
+ },
14477
+ { scope: "user", file: settingsPath },
14478
+ { scope: "project", file: join5(cwd, ".claude", "settings.json") },
14479
+ { scope: "local", file: join5(cwd, ".claude", "settings.local.json") }
14480
+ ];
14481
+ const configured = [];
14482
+ for (const { scope, file } of scopes) {
14483
+ if (!existsSync3(file)) continue;
14484
+ configured.push(
14485
+ ...collectConfiguredHooks(
14486
+ scope,
14487
+ file,
14488
+ await readFile3(file, "utf8")
14489
+ )
14490
+ );
14491
+ }
14492
+ const hookIssues = inspectConfiguredHooks(configured, {
14493
+ knownGadVerbs,
14494
+ fileExists: existsSync3
14495
+ });
14496
+ toChecks("Configured hooks", hookIssues);
14497
+ if (configured.length > 0 && hookIssues.length === 0) {
14498
+ checks.push({
14499
+ name: "Configured hooks: all runnable",
14500
+ status: "pass",
14501
+ message: `${configured.length} command hook(s) across ${new Set(configured.map((h) => h.file)).size} settings file(s) resolve`
14502
+ });
14503
+ }
14504
+ }
14398
14505
  if (options.enforce) {
14399
14506
  const enforcement = {
14400
14507
  name: "Claude surface: enforcement",
@@ -15444,8 +15551,9 @@ async function readIfExists(path10) {
15444
15551
  }
15445
15552
  async function readKeychainOnly() {
15446
15553
  try {
15447
- const keytar = await import("keytar");
15448
- return await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
15554
+ const { AsyncEntry } = await import("@napi-rs/keyring");
15555
+ const entry = new AsyncEntry(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
15556
+ return await entry.getPassword() ?? null;
15449
15557
  } catch {
15450
15558
  return null;
15451
15559
  }
@@ -15716,135 +15824,6 @@ ${c.dim("Credential migration")}
15716
15824
  var mcpCommand = new Command("mcp").description("Manage and diagnose the MCP server connection").addCommand(startCommand).addCommand(statusCommand).addCommand(logsCommand).addCommand(testCommand).addCommand(doctorSubcommand).addCommand(installCommand);
15717
15825
  var mcp_default = mcpCommand;
15718
15826
 
15719
- // src/commands/deploy.ts
15720
- var DEPLOY_TIMEOUT_MS = 9e4;
15721
- function deployCommand() {
15722
- const cmd = new Command("deploy").description("Trigger a production deployment").option("--confirm", "Skip interactive confirmation (required in CI)").option("--json", "Output as JSON").option("--quiet", "Suppress decorative output").option("--no-color", "Disable color output").action(async (options, command) => {
15723
- const g = globalOutputOpts(command);
15724
- const fmt = createFormatter({
15725
- json: g.json,
15726
- quiet: g.quiet,
15727
- isTTY: isTTY(),
15728
- noColor: g.noColor
15729
- });
15730
- if (!options.confirm) {
15731
- if (!isTTY()) {
15732
- fmt.error(
15733
- "Cannot deploy without confirmation in non-interactive mode."
15734
- );
15735
- fmt.info("Pass --confirm to bypass interactive confirmation in CI.");
15736
- process.exit(3 /* VALIDATION_ERROR */);
15737
- }
15738
- const confirmed = await confirm(
15739
- "\u26A0\uFE0F This will deploy to production. Continue?"
15740
- );
15741
- if (!confirmed) {
15742
- fmt.info("Deploy cancelled.");
15743
- return;
15744
- }
15745
- }
15746
- const auth = await resolveAuth();
15747
- const client = new GadClient({
15748
- token: auth.token,
15749
- apiUrl: auth.apiUrl,
15750
- timeout: DEPLOY_TIMEOUT_MS
15751
- });
15752
- if (!options.quiet) {
15753
- fmt.info("Deploying to production...");
15754
- }
15755
- const result = await client.mcpCall("deploy_to_vercel", {});
15756
- if (g.json) {
15757
- process.stdout.write(JSON.stringify(result) + "\n");
15758
- return;
15759
- }
15760
- if (result.error) {
15761
- fmt.error(`Deployment failed: ${result.error}`);
15762
- process.exit(2 /* NETWORK_ERROR */);
15763
- }
15764
- if (result.url) {
15765
- fmt.success(`Deployment triggered successfully`);
15766
- fmt.info(`URL: ${result.url}`);
15767
- } else {
15768
- fmt.success("Deployment triggered successfully");
15769
- }
15770
- if (result.status) {
15771
- fmt.info(`Status: ${result.status}`);
15772
- }
15773
- if (result.buildId) {
15774
- fmt.info(`Build ID: ${result.buildId}`);
15775
- }
15776
- });
15777
- return cmd;
15778
- }
15779
-
15780
- // src/commands/migrate.ts
15781
- async function ensureConfirmation(hasConfirmFlag, tty2) {
15782
- if (hasConfirmFlag) {
15783
- return true;
15784
- }
15785
- if (!tty2) {
15786
- process.stderr.write(
15787
- "\u2717 --confirm flag is required in non-interactive environments.\n Use: gad migrate --confirm\n"
15788
- );
15789
- process.exit(3 /* VALIDATION_ERROR */);
15790
- }
15791
- const confirmed = await confirm(
15792
- "This will run pending database migrations. Continue?"
15793
- );
15794
- if (!confirmed) {
15795
- process.stderr.write("Aborted.\n");
15796
- }
15797
- return confirmed;
15798
- }
15799
- function displayResults(result, fmt) {
15800
- const applied = result?.applied ?? 0;
15801
- const migrations = result?.migrations;
15802
- if (applied === 0) {
15803
- fmt.success("No pending migrations. Database is up to date.");
15804
- return;
15805
- }
15806
- fmt.success(
15807
- `${applied} migration${applied === 1 ? "" : "s"} applied successfully.`
15808
- );
15809
- if (migrations && migrations.length > 0) {
15810
- fmt.table(migrations, [
15811
- { key: "name", header: "Migration", width: 40 },
15812
- { key: "status", header: "Status", width: 12 }
15813
- ]);
15814
- }
15815
- }
15816
- var migrateCommand = new Command("migrate").description("Run pending database migrations").option("--confirm", "Skip confirmation prompt (required in non-TTY)").option("--json", "Output results as JSON").option("--quiet", "Suppress decorative output").action(async (opts, command) => {
15817
- const g = globalOutputOpts(command);
15818
- const tty2 = isTTY();
15819
- const fmt = createFormatter({
15820
- json: g.json,
15821
- quiet: g.quiet,
15822
- isTTY: tty2,
15823
- noColor: !tty2
15824
- });
15825
- const confirmed = await ensureConfirmation(Boolean(opts.confirm), tty2);
15826
- if (!confirmed) {
15827
- return;
15828
- }
15829
- const auth = await resolveAuth();
15830
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
15831
- fmt.info("Running pending migrations\u2026");
15832
- const result = await client.mcpCall("apply_migration", {});
15833
- if (g.json) {
15834
- const applied = result?.applied ?? 0;
15835
- const status = result?.status ?? "complete";
15836
- fmt.table(
15837
- [{ applied, status }],
15838
- [
15839
- { key: "applied", header: "Applied" },
15840
- { key: "status", header: "Status" }
15841
- ]
15842
- );
15843
- } else {
15844
- displayResults(result, fmt);
15845
- }
15846
- });
15847
-
15848
15827
  // src/commands/smoke.ts
15849
15828
  var SMOKE_TESTS = [
15850
15829
  {
@@ -17632,8 +17611,6 @@ var COMMANDS = [
17632
17611
  "doctor",
17633
17612
  "integrations",
17634
17613
  "mcp",
17635
- "deploy",
17636
- "migrate",
17637
17614
  "smoke",
17638
17615
  "agent",
17639
17616
  "ask",
@@ -17706,8 +17683,6 @@ _gad() {
17706
17683
  'doctor:Run health checks'
17707
17684
  'integrations:Manage integrations (list, sync)'
17708
17685
  'mcp:MCP server management (status, logs, test, install)'
17709
- 'deploy:Deploy to production'
17710
- 'migrate:Run database migrations'
17711
17686
  'smoke:Run smoke tests'
17712
17687
  'agent:Manage agents (list, invoke)'
17713
17688
  'ask:Ask the orchestrator a question'
@@ -17766,8 +17741,6 @@ function generateFish() {
17766
17741
  doctor: "Run health checks",
17767
17742
  integrations: "Manage integrations",
17768
17743
  mcp: "MCP server management",
17769
- deploy: "Deploy to production",
17770
- migrate: "Run database migrations",
17771
17744
  smoke: "Run smoke tests",
17772
17745
  agent: "Manage agents",
17773
17746
  ask: "Ask the orchestrator a question",
@@ -21268,8 +21241,6 @@ program2.addCommand(mcp_default);
21268
21241
  program2.addCommand(agentCommand);
21269
21242
  program2.addCommand(askCommand());
21270
21243
  program2.addCommand(localCommand());
21271
- program2.addCommand(deployCommand());
21272
- program2.addCommand(migrateCommand);
21273
21244
  program2.addCommand(smokeCommand());
21274
21245
  program2.addCommand(openCommand());
21275
21246
  program2.addCommand(completionsCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@great-arrow/cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.23",
4
4
  "description": "GreatArrow.ai CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,7 @@
17
17
  "typecheck": "tsc --noEmit"
18
18
  },
19
19
  "dependencies": {
20
+ "@napi-rs/keyring": "1.3.0",
20
21
  "commander": "^15.0.0",
21
22
  "update-notifier": "^7.3.1"
22
23
  },
@@ -44,9 +45,6 @@
44
45
  "url": "https://www.greatarrow.ai/support"
45
46
  },
46
47
  "author": "GreatArrow.ai <support@greatarrow.ai>",
47
- "optionalDependencies": {
48
- "keytar": "^7.9.0"
49
- },
50
48
  "overrides": {
51
49
  "postcss": ">=8.5.23",
52
50
  "nanoid": "^3.3.17"