@great-arrow/cli 0.1.52 → 0.1.54

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 +348 -76
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9402,7 +9402,7 @@ function isNodeError(value) {
9402
9402
 
9403
9403
  // src/lib/version.ts
9404
9404
  var CLI_PACKAGE_NAME = "@great-arrow/cli";
9405
- var CLI_VERSION = "0.1.52";
9405
+ var CLI_VERSION = "0.1.54";
9406
9406
  var GAD_INSTALL_VERSION = "2026.08.08";
9407
9407
  var GAD_INSTALL_MIN_VERSION = "2026.06.22";
9408
9408
  var GAD_INSTALL_HEADER = "X-GAD-Install-Version";
@@ -9411,6 +9411,9 @@ var GAD_CLI_VERSION_HEADER = "X-GAD-CLI-Version";
9411
9411
  // src/commands/login.ts
9412
9412
  import { execFile } from "node:child_process";
9413
9413
 
9414
+ // src/lib/credentials.ts
9415
+ import { randomUUID } from "node:crypto";
9416
+
9414
9417
  // src/lib/keychain.ts
9415
9418
  import { mkdir, writeFile, readFile, unlink } from "node:fs/promises";
9416
9419
  import { homedir } from "node:os";
@@ -9532,7 +9535,12 @@ async function loadStoredCredential() {
9532
9535
  return parseStoredValue(await getToken());
9533
9536
  }
9534
9537
  async function saveOAuthCredential(credential) {
9535
- return setToken(JSON.stringify(credential));
9538
+ return setToken(
9539
+ JSON.stringify({
9540
+ ...credential,
9541
+ localSessionId: credential.localSessionId || randomUUID()
9542
+ })
9543
+ );
9536
9544
  }
9537
9545
  async function savePatToken(token) {
9538
9546
  return setToken(token);
@@ -9903,14 +9911,33 @@ function toCredential(env4, apiUrl, clientId, now) {
9903
9911
  }
9904
9912
  var TOKEN_POST_TIMEOUT_MS = 8e3;
9905
9913
  async function postToken(apiUrl, form, fetchImpl, timeoutMs = TOKEN_POST_TIMEOUT_MS) {
9906
- const res = await fetchImpl(`${trimSlash2(apiUrl)}/api/oauth/token`, {
9907
- method: "POST",
9908
- headers: TOKEN_HEADERS,
9909
- body: new URLSearchParams(form).toString(),
9910
- signal: AbortSignal.timeout(timeoutMs)
9914
+ const controller = new AbortController();
9915
+ let timer;
9916
+ const deadline = new Promise((_resolve, reject) => {
9917
+ timer = setTimeout(() => {
9918
+ const error = new DOMException(
9919
+ "Token exchange timed out.",
9920
+ "TimeoutError"
9921
+ );
9922
+ controller.abort(error);
9923
+ reject(error);
9924
+ }, timeoutMs);
9911
9925
  });
9912
- const json = await res.json().catch(() => ({}));
9913
- return { ok: res.ok, status: res.status, json };
9926
+ const request = async () => {
9927
+ const res = await fetchImpl(`${trimSlash2(apiUrl)}/api/oauth/token`, {
9928
+ method: "POST",
9929
+ headers: TOKEN_HEADERS,
9930
+ body: new URLSearchParams(form).toString(),
9931
+ signal: controller.signal
9932
+ });
9933
+ const json = await res.json().catch(() => ({}));
9934
+ return { ok: res.ok, status: res.status, json };
9935
+ };
9936
+ try {
9937
+ return await Promise.race([request(), deadline]);
9938
+ } finally {
9939
+ if (timer) clearTimeout(timer);
9940
+ }
9914
9941
  }
9915
9942
  async function exchangeCode(opts) {
9916
9943
  const now = opts.now ?? Date.now();
@@ -10364,6 +10391,20 @@ var login_default = loginCommand;
10364
10391
  import { mkdir as mkdir3, open, rm, stat } from "node:fs/promises";
10365
10392
  import { homedir as homedir4 } from "node:os";
10366
10393
  import { join as join4 } from "node:path";
10394
+
10395
+ // src/lib/oauth/session-identity.ts
10396
+ import { createHash as createHash2 } from "node:crypto";
10397
+ function credentialSessionId(credential) {
10398
+ return credential.localSessionId || createHash2("sha256").update(
10399
+ JSON.stringify([
10400
+ credential.clientId,
10401
+ credential.apiUrl,
10402
+ credential.refreshToken
10403
+ ])
10404
+ ).digest("hex");
10405
+ }
10406
+
10407
+ // src/lib/auth.ts
10367
10408
  var REFRESH_SKEW_MS = 12e4;
10368
10409
  var inflightRefresh = null;
10369
10410
  var REFRESH_LOCK_STALE_MS = 15e3;
@@ -10387,7 +10428,12 @@ async function acquireRefreshLock() {
10387
10428
  await handle.close();
10388
10429
  return true;
10389
10430
  } catch (err) {
10390
- if (err.code !== "EEXIST") return true;
10431
+ if (err.code !== "EEXIST") {
10432
+ throw networkError(
10433
+ "Could not lock session renewal.",
10434
+ "Check local credential storage and retry; your session has been kept."
10435
+ );
10436
+ }
10391
10437
  }
10392
10438
  const age = await stat(path10).then((st) => Date.now() - st.mtimeMs).catch(() => Number.POSITIVE_INFINITY);
10393
10439
  if (age > REFRESH_LOCK_STALE_MS) {
@@ -10416,8 +10462,13 @@ async function storedPairOtherThan(ours) {
10416
10462
  } catch (err) {
10417
10463
  debugSwallow("refresh.reread", err);
10418
10464
  }
10419
- if (stored?.kind !== "oauth") return null;
10420
- if (stored.credential.clientId !== ours.clientId || stored.credential.apiUrl.replace(/\/$/, "") !== ours.apiUrl.replace(/\/$/, "")) {
10465
+ if (stored?.kind !== "oauth") {
10466
+ throw authError(
10467
+ "The original session changed or ended during renewal.",
10468
+ "Restart this command with your current connection."
10469
+ );
10470
+ }
10471
+ if (ours.localSessionId && credentialSessionId(stored.credential) !== credentialSessionId(ours) || stored.credential.clientId !== ours.clientId || stored.credential.apiUrl.replace(/\/$/, "") !== ours.apiUrl.replace(/\/$/, "")) {
10421
10472
  throw networkError(
10422
10473
  "The stored session changed while renewing it.",
10423
10474
  "Retry the command with the current session."
@@ -10443,11 +10494,32 @@ async function resolveAuth() {
10443
10494
  if (stored?.kind === "oauth") {
10444
10495
  const fresh = await ensureFreshAccessToken(stored.credential);
10445
10496
  warnLegacyApiUrl(fresh.apiUrl);
10497
+ const originalSessionId = credentialSessionId(fresh);
10498
+ const originalApiUrl = fresh.apiUrl;
10499
+ const originalClientId = fresh.clientId;
10446
10500
  return withWorkspace({
10447
10501
  token: fresh.accessToken,
10448
10502
  apiUrl: fresh.apiUrl,
10449
10503
  tokenType: "oauth",
10450
- expiresAt: fresh.accessExpiresAt
10504
+ expiresAt: fresh.accessExpiresAt,
10505
+ getToken: async () => {
10506
+ const latestConfig = await loadConfig();
10507
+ const latest = await loadStoredCredential();
10508
+ if ((latestConfig.api_url || DEFAULT_API_URL) !== configApiUrl || (latestConfig.default_workspace || void 0) !== activeWorkspace || process.env.GAD_TOKEN || latest?.kind !== "oauth" || credentialSessionId(latest.credential) !== originalSessionId || latest.credential.apiUrl !== originalApiUrl || latest.credential.clientId !== originalClientId) {
10509
+ throw authError(
10510
+ "The original session or workspace changed or ended.",
10511
+ "Restart this command with your current connection."
10512
+ );
10513
+ }
10514
+ const renewed = await ensureFreshAccessToken(latest.credential);
10515
+ if (credentialSessionId(renewed) !== originalSessionId) {
10516
+ throw authError(
10517
+ "The original session changed.",
10518
+ "Restart this command with your current connection."
10519
+ );
10520
+ }
10521
+ return renewed.accessToken;
10522
+ }
10451
10523
  });
10452
10524
  }
10453
10525
  if (stored?.kind === "pat") {
@@ -10502,10 +10574,6 @@ async function ensureFreshAccessToken(credential) {
10502
10574
  if (Date.now() < credential.accessExpiresAt - REFRESH_SKEW_MS) {
10503
10575
  return credential;
10504
10576
  }
10505
- if (Date.now() >= credential.refreshExpiresAt) {
10506
- await clearStoredCredential();
10507
- throw sessionExpired();
10508
- }
10509
10577
  inflightRefresh ??= doRefresh(credential);
10510
10578
  try {
10511
10579
  return await inflightRefresh;
@@ -10529,12 +10597,32 @@ async function doRefresh(credential) {
10529
10597
  "Retry shortly; your saved session has been kept."
10530
10598
  );
10531
10599
  }
10532
- const refreshed = await refreshTokens({
10600
+ if (Date.now() >= current.refreshExpiresAt) {
10601
+ await clearStoredCredential();
10602
+ throw sessionExpired();
10603
+ }
10604
+ const rotated = await refreshTokens({
10533
10605
  apiUrl: current.apiUrl,
10534
10606
  clientId: current.clientId,
10535
10607
  refreshToken: current.refreshToken
10536
10608
  });
10537
- await saveOAuthCredential(refreshed);
10609
+ const refreshed = {
10610
+ ...rotated,
10611
+ localSessionId: credentialSessionId(current)
10612
+ };
10613
+ const saved = await loadStoredCredential();
10614
+ if (saved?.kind !== "oauth" || credentialSessionId(saved.credential) !== credentialSessionId(current)) {
10615
+ throw authError(
10616
+ "The original session changed or ended during renewal.",
10617
+ "Restart this command with your current connection."
10618
+ );
10619
+ }
10620
+ if (!await saveOAuthCredential(refreshed)) {
10621
+ throw networkError(
10622
+ "Could not persist the renewed session.",
10623
+ "Check your credential storage before retrying."
10624
+ );
10625
+ }
10538
10626
  return refreshed;
10539
10627
  } catch (err) {
10540
10628
  if (err instanceof RefreshRejected) {
@@ -10678,6 +10766,42 @@ function parseMcpPayload(result) {
10678
10766
  }
10679
10767
  }
10680
10768
 
10769
+ // ../../src/lib/observability/scrub-pii.ts
10770
+ var EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
10771
+ var SSN_RE = /\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g;
10772
+ var CREDIT_CARD_RE = /\b\d(?:[ -]?\d){12,18}\b/g;
10773
+ var PHONE_RE = /(?:\+?\b1[-.\s]?)?(?:\([2-9]\d{2}\)\s?|\b[2-9]\d{2}[-.\s]?)\d{3}[-.\s]?\d{4}\b/g;
10774
+ function luhnValid(candidate) {
10775
+ const digits2 = candidate.replace(/\D/g, "");
10776
+ if (digits2.length < 13 || digits2.length > 19) return false;
10777
+ let sum = 0;
10778
+ let double = false;
10779
+ for (let i = digits2.length - 1; i >= 0; i--) {
10780
+ let n = digits2.charCodeAt(i) - 48;
10781
+ if (double) {
10782
+ n *= 2;
10783
+ if (n > 9) n -= 9;
10784
+ }
10785
+ sum += n;
10786
+ double = !double;
10787
+ }
10788
+ return sum % 10 === 0;
10789
+ }
10790
+ var SECRET_TOKEN_RE = /\b(?:gad_[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9-]{20,}|Bearer\s+[A-Za-z0-9._~+/=-]{8,})\b/gi;
10791
+ var SENSITIVE_QUERY_VALUE_RE = /((?:^|[?&])(?:code|token|access_token|refresh_token|id_token|api_key|apikey|api_secret|apisecret|key|secret|client_secret|password|state)=)[^&#\s]+/gi;
10792
+ function scrubPiiFromText(s) {
10793
+ return s.replace(EMAIL_RE, "[redacted-email]").replace(
10794
+ CREDIT_CARD_RE,
10795
+ (m) => luhnValid(m) ? "[redacted-credit-card]" : m
10796
+ ).replace(SSN_RE, "[redacted-ssn]").replace(PHONE_RE, "[redacted-phone]");
10797
+ }
10798
+ function scrubTelemetryText(s) {
10799
+ return scrubPiiFromText(s).replace(SENSITIVE_QUERY_VALUE_RE, "$1[redacted]").replace(
10800
+ SECRET_TOKEN_RE,
10801
+ (match) => match.toLowerCase().startsWith("bearer ") ? "Bearer [redacted]" : "[redacted-secret]"
10802
+ );
10803
+ }
10804
+
10681
10805
  // src/lib/telemetry.ts
10682
10806
  var MAX_EVENTS_PER_PROCESS = 20;
10683
10807
  var MAX_STRING = 240;
@@ -10685,12 +10809,14 @@ var sent = 0;
10685
10809
  function enabled() {
10686
10810
  if (process.env.GAD_TELEMETRY === "0" || process.env.GAD_TELEMETRY === "false")
10687
10811
  return false;
10688
- return process.env.GAD_TELEMETRY === "1" || process.env.GAD_TELEMETRY === "true";
10812
+ if (process.env.GAD_TELEMETRY === "1" || process.env.GAD_TELEMETRY === "true")
10813
+ return true;
10814
+ return void 0;
10689
10815
  }
10690
10816
  function redact(value, depth = 0) {
10691
10817
  if (depth > 3) return "[redacted]";
10692
10818
  if (typeof value === "string") {
10693
- const scrubbed = value.replace(/Bearer\s+\S+/gi, "Bearer [redacted]").replace(
10819
+ const scrubbed = scrubTelemetryText(value).replace(/Bearer\s+\S+/gi, "Bearer [redacted]").replace(
10694
10820
  /(token|secret|password|authorization)[=:]\s*[^\s,]+/gi,
10695
10821
  "$1=[redacted]"
10696
10822
  );
@@ -10709,7 +10835,9 @@ function redact(value, depth = 0) {
10709
10835
  }
10710
10836
  async function captureCliEvent(input) {
10711
10837
  if (sent >= MAX_EVENTS_PER_PROCESS) return;
10712
- if (!enabled()) {
10838
+ const envConsent = enabled();
10839
+ if (envConsent === false) return;
10840
+ if (envConsent === void 0) {
10713
10841
  try {
10714
10842
  const config = await loadConfig();
10715
10843
  if (config.telemetry_enabled !== true) return;
@@ -10717,28 +10845,33 @@ async function captureCliEvent(input) {
10717
10845
  return;
10718
10846
  }
10719
10847
  }
10848
+ if (sent >= MAX_EVENTS_PER_PROCESS) return;
10720
10849
  sent++;
10721
10850
  const error = input.error instanceof Error ? { name: input.error.name, message: input.error.message } : input.error;
10722
10851
  const payload = {
10723
10852
  platform: "cli",
10724
10853
  event: input.name,
10725
10854
  level: input.error ? "error" : "info",
10726
- message: error && typeof error === "object" && "message" in error ? String(error.message) : input.name,
10855
+ message: error && typeof error === "object" && "message" in error ? redact(String(error.message)) : redact(input.name),
10727
10856
  version: process.env.GAD_CLI_VERSION,
10728
10857
  code: input.context?.code,
10729
10858
  context: redact(input.context ?? {}),
10730
10859
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
10731
10860
  };
10732
10861
  try {
10733
- await fetch(`${input.apiUrl.replace(/\/$/, "")}/api/telemetry`, {
10734
- method: "POST",
10735
- headers: {
10736
- "content-type": "application/json",
10737
- ...input.token ? { authorization: `Bearer ${input.token}` } : {}
10738
- },
10739
- body: JSON.stringify(payload),
10740
- signal: AbortSignal.timeout(1500)
10741
- });
10862
+ const response = await fetch(
10863
+ `${input.apiUrl.replace(/\/$/, "")}/api/telemetry`,
10864
+ {
10865
+ method: "POST",
10866
+ headers: {
10867
+ "content-type": "application/json"
10868
+ },
10869
+ body: JSON.stringify(redact(payload)),
10870
+ signal: AbortSignal.timeout(1500)
10871
+ }
10872
+ );
10873
+ if (!response.ok)
10874
+ debugSwallow("telemetry.send", new Error(`HTTP ${response.status}`));
10742
10875
  } catch (err) {
10743
10876
  debugSwallow("telemetry.send", err);
10744
10877
  }
@@ -10845,12 +10978,14 @@ function sleep(ms) {
10845
10978
  }
10846
10979
  var GadClient = class {
10847
10980
  token;
10981
+ getToken;
10848
10982
  apiUrl;
10849
10983
  repoUrl;
10850
10984
  workspaceId;
10851
10985
  timeout;
10852
10986
  constructor(options) {
10853
10987
  this.token = options.token;
10988
+ this.getToken = options.getToken;
10854
10989
  this.apiUrl = options.apiUrl.replace(/\/$/, "");
10855
10990
  this.repoUrl = options.repoUrl ?? detectGitOrigin();
10856
10991
  this.workspaceId = options.workspaceId?.trim() || getCommandWorkspaceTarget({ hasRepo: Boolean(this.repoUrl) }) || null;
@@ -10886,8 +11021,28 @@ var GadClient = class {
10886
11021
  const controller = new AbortController();
10887
11022
  const timer = setTimeout(() => controller.abort(), effectiveTimeout);
10888
11023
  try {
11024
+ const headers = this.getToken ? new Headers(init.headers) : void 0;
11025
+ if (headers && this.getToken) {
11026
+ const token = await Promise.race([
11027
+ this.getToken(),
11028
+ new Promise((_resolve, reject) => {
11029
+ controller.signal.addEventListener(
11030
+ "abort",
11031
+ () => reject(
11032
+ networkError(
11033
+ "Session renewal timed out.",
11034
+ "Retry shortly; your saved session has been kept."
11035
+ )
11036
+ ),
11037
+ { once: true }
11038
+ );
11039
+ })
11040
+ ]);
11041
+ headers.set("Authorization", `Bearer ${token}`);
11042
+ }
10889
11043
  const response = await fetch(url, {
10890
11044
  ...init,
11045
+ ...headers ? { headers: Object.fromEntries(headers) } : {},
10891
11046
  signal: controller.signal
10892
11047
  });
10893
11048
  clearTimeout(timer);
@@ -11136,6 +11291,7 @@ function whoamiCommand() {
11136
11291
  const auth = await resolveAuth();
11137
11292
  const client = new GadClient({
11138
11293
  token: auth.token,
11294
+ getToken: auth.getToken,
11139
11295
  apiUrl: auth.apiUrl
11140
11296
  });
11141
11297
  let result;
@@ -11246,7 +11402,11 @@ var detailFields = [
11246
11402
  ];
11247
11403
  async function buildClient() {
11248
11404
  const auth = await resolveAuth();
11249
- return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11405
+ return new GadClient({
11406
+ token: auth.token,
11407
+ getToken: auth.getToken,
11408
+ apiUrl: auth.apiUrl
11409
+ });
11250
11410
  }
11251
11411
  function extractPreview(item) {
11252
11412
  if (typeof item.content === "string") {
@@ -11424,7 +11584,11 @@ var memberColumns = [
11424
11584
  ];
11425
11585
  async function createClient() {
11426
11586
  const auth = await resolveAuth();
11427
- return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11587
+ return new GadClient({
11588
+ token: auth.token,
11589
+ getToken: auth.getToken,
11590
+ apiUrl: auth.apiUrl
11591
+ });
11428
11592
  }
11429
11593
  var listCommand = new Command("list").description("List all accessible workspaces").option("--json", "Output as JSON").action(async (options) => {
11430
11594
  const jsonFlag = Boolean(
@@ -11618,7 +11782,11 @@ var listCommand2 = new Command("list").description("List open todos sorted by pr
11618
11782
  noColor: parentOpts.noColor || false
11619
11783
  });
11620
11784
  const auth = await resolveAuth();
11621
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11785
+ const client = new GadClient({
11786
+ token: auth.token,
11787
+ getToken: auth.getToken,
11788
+ apiUrl: auth.apiUrl
11789
+ });
11622
11790
  const args = {
11623
11791
  status: options.status ?? "open"
11624
11792
  };
@@ -11644,7 +11812,11 @@ var createCommand = new Command("create").description("Create a new todo card").
11644
11812
  noColor: false
11645
11813
  });
11646
11814
  const auth = await resolveAuth();
11647
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11815
+ const client = new GadClient({
11816
+ token: auth.token,
11817
+ getToken: auth.getToken,
11818
+ apiUrl: auth.apiUrl
11819
+ });
11648
11820
  const args = { title };
11649
11821
  if (options.priority) {
11650
11822
  args.priority = options.priority;
@@ -11672,7 +11844,11 @@ var doneCommand = new Command("done").description("Mark a todo as done").argumen
11672
11844
  noColor: false
11673
11845
  });
11674
11846
  const auth = await resolveAuth();
11675
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11847
+ const client = new GadClient({
11848
+ token: auth.token,
11849
+ getToken: auth.getToken,
11850
+ apiUrl: auth.apiUrl
11851
+ });
11676
11852
  await client.mcpCall("todo_update", { id, status: "done" });
11677
11853
  fmt.success(`Todo ${id} marked as done.`);
11678
11854
  });
@@ -11691,7 +11867,11 @@ var updateCommand = new Command("update").description("Update a todo's status").
11691
11867
  process.exit(3);
11692
11868
  }
11693
11869
  const auth = await resolveAuth();
11694
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11870
+ const client = new GadClient({
11871
+ token: auth.token,
11872
+ getToken: auth.getToken,
11873
+ apiUrl: auth.apiUrl
11874
+ });
11695
11875
  await client.mcpCall("todo_update", { id, status: options.status });
11696
11876
  fmt.success(`Todo ${id} updated to status: ${options.status}`);
11697
11877
  });
@@ -11733,7 +11913,11 @@ var getCommand = new Command("get").description("Show one card's full state (ass
11733
11913
  noColor: parentOpts.noColor || false
11734
11914
  });
11735
11915
  const auth = await resolveAuth();
11736
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11916
+ const client = new GadClient({
11917
+ token: auth.token,
11918
+ getToken: auth.getToken,
11919
+ apiUrl: auth.apiUrl
11920
+ });
11737
11921
  const result = await client.mcpCall("todo_get", { id });
11738
11922
  if (!result || typeof result !== "object" || result.error) {
11739
11923
  fmt.error(`Todo ${id} not found in this workspace.`);
@@ -11793,7 +11977,11 @@ var assignCommand = new Command("assign").description("Assign a card to a specia
11793
11977
  process.exit(3);
11794
11978
  }
11795
11979
  const auth = await resolveAuth();
11796
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11980
+ const client = new GadClient({
11981
+ token: auth.token,
11982
+ getToken: auth.getToken,
11983
+ apiUrl: auth.apiUrl
11984
+ });
11797
11985
  const result = await client.mcpCall("todo_assign", built.args);
11798
11986
  if (isToolError(result)) {
11799
11987
  fmt.error(toolErrorMessage(result));
@@ -11821,7 +12009,11 @@ var runCommand = new Command("run").description("Start the agent already queued
11821
12009
  noColor: false
11822
12010
  });
11823
12011
  const auth = await resolveAuth();
11824
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
12012
+ const client = new GadClient({
12013
+ token: auth.token,
12014
+ getToken: auth.getToken,
12015
+ apiUrl: auth.apiUrl
12016
+ });
11825
12017
  const result = await client.post(
11826
12018
  `/api/todos/${encodeURIComponent(id)}/run`,
11827
12019
  {}
@@ -11909,7 +12101,11 @@ var claimCommand = new Command("claim").description("Preflight + atomically clai
11909
12101
  noColor: parentOpts.noColor || false
11910
12102
  });
11911
12103
  const auth = await resolveAuth();
11912
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
12104
+ const client = new GadClient({
12105
+ token: auth.token,
12106
+ getToken: auth.getToken,
12107
+ apiUrl: auth.apiUrl
12108
+ });
11913
12109
  if (!options.force) {
11914
12110
  tryGit(["fetch", "--quiet", "origin", "main"]);
11915
12111
  const log = tryGit(["log", "--format=%s", "-n", "10", "origin/main"]) ?? tryGit(["log", "--format=%s", "-n", "10", "FETCH_HEAD"]) ?? "";
@@ -12487,6 +12683,7 @@ function workCommand() {
12487
12683
  }
12488
12684
  const client = new GadClient({
12489
12685
  token: auth.token,
12686
+ getToken: auth.getToken,
12490
12687
  apiUrl: auth.apiUrl
12491
12688
  });
12492
12689
  const data = await client.get("/api/work/status", {
@@ -12511,6 +12708,7 @@ function workCommand() {
12511
12708
  const auth = await resolveAuth();
12512
12709
  const client = new GadClient({
12513
12710
  token: auth.token,
12711
+ getToken: auth.getToken,
12514
12712
  apiUrl: auth.apiUrl
12515
12713
  });
12516
12714
  const report = options.file ? JSON.parse(readFileSync(options.file, "utf8")) : await collectWorkStatus({
@@ -13044,7 +13242,11 @@ function normalizeResponse(value) {
13044
13242
  }
13045
13243
  async function createClient2() {
13046
13244
  const auth = await resolveAuth();
13047
- return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
13245
+ return new GadClient({
13246
+ token: auth.token,
13247
+ getToken: auth.getToken,
13248
+ apiUrl: auth.apiUrl
13249
+ });
13048
13250
  }
13049
13251
  function renderGuardrails(data, cmd) {
13050
13252
  const output = globalOutputOpts(cmd);
@@ -14171,7 +14373,7 @@ async function ensureHookCredential(opts) {
14171
14373
  }
14172
14374
 
14173
14375
  // src/lib/mcp-doctor.ts
14174
- import { createHash as createHash2 } from "node:crypto";
14376
+ import { createHash as createHash3 } from "node:crypto";
14175
14377
 
14176
14378
  // src/lib/mcp-target.ts
14177
14379
  var GREAT_ARROW_HOSTS = /* @__PURE__ */ new Set([
@@ -14510,7 +14712,7 @@ function connectorGuidance(entries) {
14510
14712
  return 'If you ALSO installed the Great Arrow connector on claude.ai, Claude Code counts it as a second server and nags "needs authentication" forever. Silence the duplicate by adding its name to "deniedMcpServers" in ~/.claude/settings.json ("disabledMcpServers" is not a valid key; "disabledMcpjsonServers" only covers .mcp.json servers).';
14511
14713
  }
14512
14714
  function fingerprint(value) {
14513
- return createHash2("sha256").update(value).digest("hex").slice(0, 12);
14715
+ return createHash3("sha256").update(value).digest("hex").slice(0, 12);
14514
14716
  }
14515
14717
  function describeStoredCredential(raw) {
14516
14718
  const trimmed = raw?.trim();
@@ -14784,7 +14986,11 @@ async function checkApi(auth) {
14784
14986
  hint: "Fix authentication first."
14785
14987
  };
14786
14988
  }
14787
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
14989
+ const client = new GadClient({
14990
+ token: auth.token,
14991
+ getToken: auth.getToken,
14992
+ apiUrl: auth.apiUrl
14993
+ });
14788
14994
  try {
14789
14995
  await client.get("/api/health");
14790
14996
  return {
@@ -14810,7 +15016,11 @@ async function checkCliVersion(auth) {
14810
15016
  hint: "Fix authentication first, then run 'gad doctor' again."
14811
15017
  };
14812
15018
  }
14813
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
15019
+ const client = new GadClient({
15020
+ token: auth.token,
15021
+ getToken: auth.getToken,
15022
+ apiUrl: auth.apiUrl
15023
+ });
14814
15024
  try {
14815
15025
  const result = await client.get("/api/mcp/version");
14816
15026
  const cli = result.updates?.cli ?? result.cli ?? {};
@@ -14856,7 +15066,11 @@ async function checkIntegrations(auth) {
14856
15066
  hint: "Fix authentication first."
14857
15067
  };
14858
15068
  }
14859
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
15069
+ const client = new GadClient({
15070
+ token: auth.token,
15071
+ getToken: auth.getToken,
15072
+ apiUrl: auth.apiUrl
15073
+ });
14860
15074
  try {
14861
15075
  const result = await client.mcpCall("integrations_list", {});
14862
15076
  const data = result ?? {};
@@ -14897,7 +15111,11 @@ async function checkMcp(auth) {
14897
15111
  hint: "Fix authentication first."
14898
15112
  };
14899
15113
  }
14900
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
15114
+ const client = new GadClient({
15115
+ token: auth.token,
15116
+ getToken: auth.getToken,
15117
+ apiUrl: auth.apiUrl
15118
+ });
14901
15119
  try {
14902
15120
  await client.mcpCall("workspace_introspect", {});
14903
15121
  return {
@@ -15505,7 +15723,11 @@ var integrationListColumns = [
15505
15723
  ];
15506
15724
  async function createClient3() {
15507
15725
  const auth = await resolveAuth();
15508
- return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
15726
+ return new GadClient({
15727
+ token: auth.token,
15728
+ getToken: auth.getToken,
15729
+ apiUrl: auth.apiUrl
15730
+ });
15509
15731
  }
15510
15732
  function formatErrorDetail(rawError) {
15511
15733
  if (rawError === null || rawError === void 0) return null;
@@ -15616,13 +15838,13 @@ var integrationsCommand = new Command("integrations").description("Manage connec
15616
15838
  var integrations_default = integrationsCommand;
15617
15839
 
15618
15840
  // src/commands/mcp.ts
15619
- import { createHash as createHash3 } from "node:crypto";
15841
+ import { createHash as createHash4 } from "node:crypto";
15620
15842
  import { hostname } from "node:os";
15621
15843
 
15622
15844
  // src/lib/mcp-bridge.ts
15623
15845
  import { createInterface as createInterface2 } from "node:readline";
15624
15846
  import { execFileSync as execFileSync6 } from "node:child_process";
15625
- import { randomUUID } from "node:crypto";
15847
+ import { randomUUID as randomUUID2 } from "node:crypto";
15626
15848
  var BRIDGE_CLIENT_ID = "gad-cli";
15627
15849
  var DEFAULT_API_URL2 = "https://www.greatarrow.ai";
15628
15850
  var DEFAULT_TIMEOUT_MS3 = 9e4;
@@ -15785,7 +16007,7 @@ async function runStdioBridge(options) {
15785
16007
  `));
15786
16008
  const endpoint = `${options.auth.apiUrl}/api/mcp`;
15787
16009
  const timeoutMs = Number(env4.MEMORY_TIMEOUT_MS ?? DEFAULT_TIMEOUT_MS3);
15788
- const sessionId = (env4.GAD_SESSION_ID ?? env4.MEMORY_SESSION_ID)?.trim() || `${BRIDGE_CLIENT_ID}:${Date.now()}-${randomUUID().slice(0, 8)}`;
16010
+ const sessionId = (env4.GAD_SESSION_ID ?? env4.MEMORY_SESSION_ID)?.trim() || `${BRIDGE_CLIENT_ID}:${Date.now()}-${randomUUID2().slice(0, 8)}`;
15789
16011
  const repoUrl = (env4.GAD_REPO_URL ?? env4.MEMORY_REPO_URL)?.trim() || detectGitOrigin3();
15790
16012
  const workspaceId = (env4.GAD_WORKSPACE_ID ?? env4.GAD_WORKSPACE)?.trim() || getCommandWorkspaceTarget({ hasRepo: Boolean(repoUrl) });
15791
16013
  const headers = buildBridgeHeaders({
@@ -15949,7 +16171,11 @@ async function mcpJsonAtRiskOfCommit(dir) {
15949
16171
  }
15950
16172
  async function createClient4() {
15951
16173
  const auth = await resolveAuth();
15952
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
16174
+ const client = new GadClient({
16175
+ token: auth.token,
16176
+ getToken: auth.getToken,
16177
+ apiUrl: auth.apiUrl
16178
+ });
15953
16179
  return { client, apiUrl: auth.apiUrl };
15954
16180
  }
15955
16181
  function generateMcpConfig(client, apiUrl, token, repoUrl) {
@@ -15984,7 +16210,7 @@ function generateMcpConfig(client, apiUrl, token, repoUrl) {
15984
16210
  Authorization: `Bearer ${token}`,
15985
16211
  "X-MCP-Client": CLIENT_IDS[client],
15986
16212
  "X-Device-Name": deviceName,
15987
- "X-Machine-Id": createHash3("sha256").update(rawHost).digest("hex"),
16213
+ "X-Machine-Id": createHash4("sha256").update(rawHost).digest("hex"),
15988
16214
  [GAD_INSTALL_HEADER]: GAD_INSTALL_VERSION,
15989
16215
  ...repoUrl ? { "X-Repo-URL": repoUrl } : {}
15990
16216
  };
@@ -16249,7 +16475,11 @@ var testCommand = new Command("test").description(
16249
16475
  const c = createColor(false);
16250
16476
  const auth = await resolveAuth();
16251
16477
  const apiUrl = auth.apiUrl;
16252
- const client = new GadClient({ token: auth.token, apiUrl });
16478
+ const client = new GadClient({
16479
+ token: auth.token,
16480
+ getToken: auth.getToken,
16481
+ apiUrl
16482
+ });
16253
16483
  const verbose = !jsonFlag;
16254
16484
  const checks = [
16255
16485
  await checkDns(apiUrl, verbose, c),
@@ -16788,6 +17018,7 @@ function smokeCommand() {
16788
17018
  const auth = await resolveAuth();
16789
17019
  const client = new GadClient({
16790
17020
  token: auth.token,
17021
+ getToken: auth.getToken,
16791
17022
  apiUrl: auth.apiUrl
16792
17023
  });
16793
17024
  if (!isJson && !isQuiet) {
@@ -16835,7 +17066,11 @@ var agentListColumns = [
16835
17066
  ];
16836
17067
  async function buildClient2() {
16837
17068
  const auth = await resolveAuth();
16838
- return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
17069
+ return new GadClient({
17070
+ token: auth.token,
17071
+ getToken: auth.getToken,
17072
+ apiUrl: auth.apiUrl
17073
+ });
16839
17074
  }
16840
17075
  function invokeOutcome(data) {
16841
17076
  if (typeof data === "string") {
@@ -16985,6 +17220,7 @@ function askCommand() {
16985
17220
  const auth = await resolveAuth();
16986
17221
  const client = new GadClient({
16987
17222
  token: auth.token,
17223
+ getToken: auth.getToken,
16988
17224
  apiUrl: auth.apiUrl
16989
17225
  });
16990
17226
  const fmt = createFormatter({
@@ -17977,26 +18213,36 @@ function authHeaders(token) {
17977
18213
  function origin(apiUrl) {
17978
18214
  return apiUrl.replace(/\/+$/, "");
17979
18215
  }
17980
- function appAwareFetch(apiUrl, token, base) {
17981
- return (input, init) => {
18216
+ function appAwareFetch(apiUrl, token, base, getToken2) {
18217
+ return async (input, init) => {
17982
18218
  const target = typeof input === "string" ? input : String(input);
17983
- if (!target.startsWith("/")) return base(input, init);
17984
- return base(`${origin(apiUrl)}${target}`, {
18219
+ const relative = target.startsWith("/") && !target.startsWith("//");
18220
+ if (!relative && !target.startsWith(`${origin(apiUrl)}/`))
18221
+ return base(input, init);
18222
+ const currentToken = getToken2 ? await withTimeout(15e3, () => getToken2()) : token;
18223
+ return base(relative ? `${origin(apiUrl)}${target}` : target, {
17985
18224
  ...init,
17986
18225
  headers: {
17987
18226
  ...init?.headers,
17988
- ...authHeaders(token)
18227
+ ...authHeaders(currentToken)
17989
18228
  }
17990
18229
  });
17991
18230
  };
17992
18231
  }
17993
18232
  async function withTimeout(ms, run2) {
17994
18233
  const controller = new AbortController();
17995
- const timer = setTimeout(() => controller.abort(), ms);
18234
+ let timer;
18235
+ const deadline = new Promise((_resolve, reject) => {
18236
+ timer = setTimeout(() => {
18237
+ const error = new DOMException("Request timed out", "AbortError");
18238
+ controller.abort(error);
18239
+ reject(error);
18240
+ }, ms);
18241
+ });
17996
18242
  try {
17997
- return await run2(controller.signal);
18243
+ return await Promise.race([run2(controller.signal), deadline]);
17998
18244
  } finally {
17999
- clearTimeout(timer);
18245
+ if (timer) clearTimeout(timer);
18000
18246
  }
18001
18247
  }
18002
18248
  function readHandoff(payload) {
@@ -18061,7 +18307,7 @@ async function askForTheTurn(opts) {
18061
18307
  if (!response.ok) {
18062
18308
  throw new LocalTurnFailed(
18063
18309
  "handoff",
18064
- response.status === 401 || response.status === 403 ? "Sign in again \u2014 this computer is not allowed to ask for work." : `Great Arrow refused that (${response.status}).`,
18310
+ response.status === 401 ? "Sign in again \u2014 this computer is not allowed to ask for work." : response.status === 403 ? "This connection needs permission. Review its scopes and workspace access at /account/connections." : `Great Arrow refused that (${response.status}).`,
18065
18311
  true
18066
18312
  );
18067
18313
  }
@@ -18123,14 +18369,14 @@ async function handBack(opts) {
18123
18369
  }
18124
18370
  async function answerOnThisComputer(opts) {
18125
18371
  const base = opts.fetchImpl ?? fetch;
18126
- const doFetch = appAwareFetch(opts.apiUrl, opts.token, base);
18372
+ const doFetch = appAwareFetch(opts.apiUrl, opts.token, base, opts.getToken);
18127
18373
  const handoff = await askForTheTurn({
18128
18374
  apiUrl: opts.apiUrl,
18129
18375
  token: opts.token,
18130
18376
  question: opts.question,
18131
18377
  sessionId: opts.sessionId ?? null,
18132
18378
  timezone: opts.timezone,
18133
- doFetch: base
18379
+ doFetch
18134
18380
  });
18135
18381
  if (!handoff) {
18136
18382
  return { answeredHere: false, reason: "answered_by_great_arrow" };
@@ -18172,7 +18418,7 @@ async function answerOnThisComputer(opts) {
18172
18418
  outputTokens: result.outputTokens,
18173
18419
  promptCharacters: result.promptCharacters,
18174
18420
  latencyMs: Date.now() - startedAt,
18175
- doFetch: base
18421
+ doFetch
18176
18422
  });
18177
18423
  return {
18178
18424
  answeredHere: true,
@@ -18192,7 +18438,7 @@ async function answerOnThisComputer(opts) {
18192
18438
  outputTokens: void 0,
18193
18439
  promptCharacters: 0,
18194
18440
  latencyMs: Date.now() - startedAt,
18195
- doFetch: base
18441
+ doFetch
18196
18442
  });
18197
18443
  throw new LocalTurnFailed(
18198
18444
  "writing",
@@ -18425,7 +18671,11 @@ function checkCommand() {
18425
18671
  const provider = readProvider(options.app);
18426
18672
  const baseUrl = typeof options.url === "string" && options.url ? options.url : LOCAL_PROVIDER_PRESETS[provider].baseUrl;
18427
18673
  const auth = await resolveAuth();
18428
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
18674
+ const client = new GadClient({
18675
+ token: auth.token,
18676
+ getToken: auth.getToken,
18677
+ apiUrl: auth.apiUrl
18678
+ });
18429
18679
  const fmt = createFormatter({
18430
18680
  json: g.json,
18431
18681
  quiet: g.quiet,
@@ -18500,7 +18750,11 @@ function statusCommand2() {
18500
18750
  return new Command("status").description("Show what is set up for models on this computer").action(async (_options, command) => {
18501
18751
  const g = globalOutputOpts(command);
18502
18752
  const auth = await resolveAuth();
18503
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
18753
+ const client = new GadClient({
18754
+ token: auth.token,
18755
+ getToken: auth.getToken,
18756
+ apiUrl: auth.apiUrl
18757
+ });
18504
18758
  const payload = await client.mcpCall(
18505
18759
  "model_local_get",
18506
18760
  {},
@@ -18568,7 +18822,11 @@ function offCommand() {
18568
18822
  return new Command("off").description("Stop answering with a model on this computer").action(async (_options, command) => {
18569
18823
  const g = globalOutputOpts(command);
18570
18824
  const auth = await resolveAuth();
18571
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
18825
+ const client = new GadClient({
18826
+ token: auth.token,
18827
+ getToken: auth.getToken,
18828
+ apiUrl: auth.apiUrl
18829
+ });
18572
18830
  const fmt = createFormatter({
18573
18831
  json: g.json,
18574
18832
  quiet: g.quiet,
@@ -18599,6 +18857,7 @@ function askHereCommand() {
18599
18857
  report = await answerOnThisComputer({
18600
18858
  apiUrl: auth.apiUrl,
18601
18859
  token: auth.token,
18860
+ getToken: auth.getToken,
18602
18861
  question,
18603
18862
  sessionId: typeof options.session === "string" ? options.session : null,
18604
18863
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
@@ -18712,6 +18971,7 @@ function statusCommand3() {
18712
18971
  const auth = await resolveAuth();
18713
18972
  const client = new GadClient({
18714
18973
  token: auth.token,
18974
+ getToken: auth.getToken,
18715
18975
  apiUrl: auth.apiUrl
18716
18976
  });
18717
18977
  const fmt = createFormatter({
@@ -18889,7 +19149,11 @@ function openCommand() {
18889
19149
  ).option("--print", "Print the URL instead of opening a browser").option("--json", "Output the resolved surface as JSON").action(async (surface, options, command) => {
18890
19150
  const g = globalOutputOpts(command);
18891
19151
  const auth = await resolveAuth();
18892
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
19152
+ const client = new GadClient({
19153
+ token: auth.token,
19154
+ getToken: auth.getToken,
19155
+ apiUrl: auth.apiUrl
19156
+ });
18893
19157
  const result = await client.mcpCall("surfaces_list", {
18894
19158
  query: surface
18895
19159
  });
@@ -18977,7 +19241,11 @@ function nextCommand() {
18977
19241
  noColor: g.noColor
18978
19242
  });
18979
19243
  const auth = await resolveAuth();
18980
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
19244
+ const client = new GadClient({
19245
+ token: auth.token,
19246
+ getToken: auth.getToken,
19247
+ apiUrl: auth.apiUrl
19248
+ });
18981
19249
  let identityStatus = null;
18982
19250
  try {
18983
19251
  const who = await client.mcpCall("workspace_introspect", {});
@@ -19040,7 +19308,11 @@ function discoverCommand() {
19040
19308
  noColor: g.noColor
19041
19309
  });
19042
19310
  const auth = await resolveAuth();
19043
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
19311
+ const client = new GadClient({
19312
+ token: auth.token,
19313
+ getToken: auth.getToken,
19314
+ apiUrl: auth.apiUrl
19315
+ });
19044
19316
  const parsed = opts.limit ? Number.parseInt(opts.limit, 10) : NaN;
19045
19317
  const args = Number.isFinite(parsed) && parsed > 0 ? { limit: parsed } : {};
19046
19318
  const result = await client.mcpCall("did_you_know", args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@great-arrow/cli",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
4
4
  "description": "GreatArrow.ai CLI tool",
5
5
  "type": "module",
6
6
  "bin": {