@great-arrow/cli 0.1.53 → 0.1.55

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 +352 -63
  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.53";
9405
+ var CLI_VERSION = "0.1.55";
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) {
@@ -10890,12 +10978,14 @@ function sleep(ms) {
10890
10978
  }
10891
10979
  var GadClient = class {
10892
10980
  token;
10981
+ getToken;
10893
10982
  apiUrl;
10894
10983
  repoUrl;
10895
10984
  workspaceId;
10896
10985
  timeout;
10897
10986
  constructor(options) {
10898
10987
  this.token = options.token;
10988
+ this.getToken = options.getToken;
10899
10989
  this.apiUrl = options.apiUrl.replace(/\/$/, "");
10900
10990
  this.repoUrl = options.repoUrl ?? detectGitOrigin();
10901
10991
  this.workspaceId = options.workspaceId?.trim() || getCommandWorkspaceTarget({ hasRepo: Boolean(this.repoUrl) }) || null;
@@ -10931,8 +11021,28 @@ var GadClient = class {
10931
11021
  const controller = new AbortController();
10932
11022
  const timer = setTimeout(() => controller.abort(), effectiveTimeout);
10933
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
+ }
10934
11043
  const response = await fetch(url, {
10935
11044
  ...init,
11045
+ ...headers ? { headers: Object.fromEntries(headers) } : {},
10936
11046
  signal: controller.signal
10937
11047
  });
10938
11048
  clearTimeout(timer);
@@ -11070,6 +11180,68 @@ var GadClient = class {
11070
11180
  }
11071
11181
  return payload;
11072
11182
  }
11183
+ /**
11184
+ * Own one chat response through body consumption, with the normal workspace
11185
+ * attribution. A received turn is never replayed. The consumer handles either
11186
+ * the versioned local handoff or the actual frontier stream inside this scope.
11187
+ */
11188
+ async withChatResponse(body, consume, options = {}) {
11189
+ const deadlineMs = options.deadlineMs ?? 3e5;
11190
+ if (!Number.isInteger(deadlineMs) || deadlineMs < 1 || deadlineMs > 3e5) {
11191
+ throw new CliError(
11192
+ "Chat deadline must be between 1 and 300000 milliseconds.",
11193
+ 3 /* VALIDATION_ERROR */
11194
+ );
11195
+ }
11196
+ const controller = new AbortController();
11197
+ const signal = options.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal;
11198
+ const timer = setTimeout(
11199
+ () => controller.abort(new Error("Chat response deadline exceeded.")),
11200
+ deadlineMs
11201
+ );
11202
+ let rejectAbort;
11203
+ let response;
11204
+ const aborted = new Promise((_, reject) => {
11205
+ rejectAbort = () => reject(signal.reason ?? new Error("Chat stopped."));
11206
+ if (signal.aborted) rejectAbort();
11207
+ else signal.addEventListener("abort", rejectAbort, { once: true });
11208
+ });
11209
+ try {
11210
+ return await Promise.race([
11211
+ (async () => {
11212
+ signal.throwIfAborted();
11213
+ response = await fetch(`${this.apiUrl}/api/chat`, {
11214
+ method: "POST",
11215
+ headers: {
11216
+ ...this.buildHeaders(),
11217
+ Accept: "text/event-stream, application/json"
11218
+ },
11219
+ body: JSON.stringify(body),
11220
+ redirect: "error",
11221
+ cache: "no-store",
11222
+ signal
11223
+ });
11224
+ signal.throwIfAborted();
11225
+ if (!response.ok) await this.handleResponse(response);
11226
+ return consume(response, signal);
11227
+ })(),
11228
+ aborted
11229
+ ]);
11230
+ } finally {
11231
+ clearTimeout(timer);
11232
+ if (rejectAbort) signal.removeEventListener("abort", rejectAbort);
11233
+ controller.abort();
11234
+ const body2 = response?.body;
11235
+ if (body2 && !body2.locked)
11236
+ void (async () => {
11237
+ try {
11238
+ await body2.cancel();
11239
+ } catch (error) {
11240
+ debugSwallow("chat.response.cancel", error);
11241
+ }
11242
+ })();
11243
+ }
11244
+ }
11073
11245
  /**
11074
11246
  * Make a GET request to a REST API endpoint.
11075
11247
  *
@@ -11181,6 +11353,7 @@ function whoamiCommand() {
11181
11353
  const auth = await resolveAuth();
11182
11354
  const client = new GadClient({
11183
11355
  token: auth.token,
11356
+ getToken: auth.getToken,
11184
11357
  apiUrl: auth.apiUrl
11185
11358
  });
11186
11359
  let result;
@@ -11291,7 +11464,11 @@ var detailFields = [
11291
11464
  ];
11292
11465
  async function buildClient() {
11293
11466
  const auth = await resolveAuth();
11294
- return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11467
+ return new GadClient({
11468
+ token: auth.token,
11469
+ getToken: auth.getToken,
11470
+ apiUrl: auth.apiUrl
11471
+ });
11295
11472
  }
11296
11473
  function extractPreview(item) {
11297
11474
  if (typeof item.content === "string") {
@@ -11469,7 +11646,11 @@ var memberColumns = [
11469
11646
  ];
11470
11647
  async function createClient() {
11471
11648
  const auth = await resolveAuth();
11472
- return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11649
+ return new GadClient({
11650
+ token: auth.token,
11651
+ getToken: auth.getToken,
11652
+ apiUrl: auth.apiUrl
11653
+ });
11473
11654
  }
11474
11655
  var listCommand = new Command("list").description("List all accessible workspaces").option("--json", "Output as JSON").action(async (options) => {
11475
11656
  const jsonFlag = Boolean(
@@ -11663,7 +11844,11 @@ var listCommand2 = new Command("list").description("List open todos sorted by pr
11663
11844
  noColor: parentOpts.noColor || false
11664
11845
  });
11665
11846
  const auth = await resolveAuth();
11666
- 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
+ });
11667
11852
  const args = {
11668
11853
  status: options.status ?? "open"
11669
11854
  };
@@ -11689,7 +11874,11 @@ var createCommand = new Command("create").description("Create a new todo card").
11689
11874
  noColor: false
11690
11875
  });
11691
11876
  const auth = await resolveAuth();
11692
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11877
+ const client = new GadClient({
11878
+ token: auth.token,
11879
+ getToken: auth.getToken,
11880
+ apiUrl: auth.apiUrl
11881
+ });
11693
11882
  const args = { title };
11694
11883
  if (options.priority) {
11695
11884
  args.priority = options.priority;
@@ -11717,7 +11906,11 @@ var doneCommand = new Command("done").description("Mark a todo as done").argumen
11717
11906
  noColor: false
11718
11907
  });
11719
11908
  const auth = await resolveAuth();
11720
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11909
+ const client = new GadClient({
11910
+ token: auth.token,
11911
+ getToken: auth.getToken,
11912
+ apiUrl: auth.apiUrl
11913
+ });
11721
11914
  await client.mcpCall("todo_update", { id, status: "done" });
11722
11915
  fmt.success(`Todo ${id} marked as done.`);
11723
11916
  });
@@ -11736,7 +11929,11 @@ var updateCommand = new Command("update").description("Update a todo's status").
11736
11929
  process.exit(3);
11737
11930
  }
11738
11931
  const auth = await resolveAuth();
11739
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11932
+ const client = new GadClient({
11933
+ token: auth.token,
11934
+ getToken: auth.getToken,
11935
+ apiUrl: auth.apiUrl
11936
+ });
11740
11937
  await client.mcpCall("todo_update", { id, status: options.status });
11741
11938
  fmt.success(`Todo ${id} updated to status: ${options.status}`);
11742
11939
  });
@@ -11778,7 +11975,11 @@ var getCommand = new Command("get").description("Show one card's full state (ass
11778
11975
  noColor: parentOpts.noColor || false
11779
11976
  });
11780
11977
  const auth = await resolveAuth();
11781
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
11978
+ const client = new GadClient({
11979
+ token: auth.token,
11980
+ getToken: auth.getToken,
11981
+ apiUrl: auth.apiUrl
11982
+ });
11782
11983
  const result = await client.mcpCall("todo_get", { id });
11783
11984
  if (!result || typeof result !== "object" || result.error) {
11784
11985
  fmt.error(`Todo ${id} not found in this workspace.`);
@@ -11838,7 +12039,11 @@ var assignCommand = new Command("assign").description("Assign a card to a specia
11838
12039
  process.exit(3);
11839
12040
  }
11840
12041
  const auth = await resolveAuth();
11841
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
12042
+ const client = new GadClient({
12043
+ token: auth.token,
12044
+ getToken: auth.getToken,
12045
+ apiUrl: auth.apiUrl
12046
+ });
11842
12047
  const result = await client.mcpCall("todo_assign", built.args);
11843
12048
  if (isToolError(result)) {
11844
12049
  fmt.error(toolErrorMessage(result));
@@ -11866,7 +12071,11 @@ var runCommand = new Command("run").description("Start the agent already queued
11866
12071
  noColor: false
11867
12072
  });
11868
12073
  const auth = await resolveAuth();
11869
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
12074
+ const client = new GadClient({
12075
+ token: auth.token,
12076
+ getToken: auth.getToken,
12077
+ apiUrl: auth.apiUrl
12078
+ });
11870
12079
  const result = await client.post(
11871
12080
  `/api/todos/${encodeURIComponent(id)}/run`,
11872
12081
  {}
@@ -11954,7 +12163,11 @@ var claimCommand = new Command("claim").description("Preflight + atomically clai
11954
12163
  noColor: parentOpts.noColor || false
11955
12164
  });
11956
12165
  const auth = await resolveAuth();
11957
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
12166
+ const client = new GadClient({
12167
+ token: auth.token,
12168
+ getToken: auth.getToken,
12169
+ apiUrl: auth.apiUrl
12170
+ });
11958
12171
  if (!options.force) {
11959
12172
  tryGit(["fetch", "--quiet", "origin", "main"]);
11960
12173
  const log = tryGit(["log", "--format=%s", "-n", "10", "origin/main"]) ?? tryGit(["log", "--format=%s", "-n", "10", "FETCH_HEAD"]) ?? "";
@@ -12532,6 +12745,7 @@ function workCommand() {
12532
12745
  }
12533
12746
  const client = new GadClient({
12534
12747
  token: auth.token,
12748
+ getToken: auth.getToken,
12535
12749
  apiUrl: auth.apiUrl
12536
12750
  });
12537
12751
  const data = await client.get("/api/work/status", {
@@ -12556,6 +12770,7 @@ function workCommand() {
12556
12770
  const auth = await resolveAuth();
12557
12771
  const client = new GadClient({
12558
12772
  token: auth.token,
12773
+ getToken: auth.getToken,
12559
12774
  apiUrl: auth.apiUrl
12560
12775
  });
12561
12776
  const report = options.file ? JSON.parse(readFileSync(options.file, "utf8")) : await collectWorkStatus({
@@ -13089,7 +13304,11 @@ function normalizeResponse(value) {
13089
13304
  }
13090
13305
  async function createClient2() {
13091
13306
  const auth = await resolveAuth();
13092
- return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
13307
+ return new GadClient({
13308
+ token: auth.token,
13309
+ getToken: auth.getToken,
13310
+ apiUrl: auth.apiUrl
13311
+ });
13093
13312
  }
13094
13313
  function renderGuardrails(data, cmd) {
13095
13314
  const output = globalOutputOpts(cmd);
@@ -14216,7 +14435,7 @@ async function ensureHookCredential(opts) {
14216
14435
  }
14217
14436
 
14218
14437
  // src/lib/mcp-doctor.ts
14219
- import { createHash as createHash2 } from "node:crypto";
14438
+ import { createHash as createHash3 } from "node:crypto";
14220
14439
 
14221
14440
  // src/lib/mcp-target.ts
14222
14441
  var GREAT_ARROW_HOSTS = /* @__PURE__ */ new Set([
@@ -14555,7 +14774,7 @@ function connectorGuidance(entries) {
14555
14774
  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).';
14556
14775
  }
14557
14776
  function fingerprint(value) {
14558
- return createHash2("sha256").update(value).digest("hex").slice(0, 12);
14777
+ return createHash3("sha256").update(value).digest("hex").slice(0, 12);
14559
14778
  }
14560
14779
  function describeStoredCredential(raw) {
14561
14780
  const trimmed = raw?.trim();
@@ -14829,7 +15048,11 @@ async function checkApi(auth) {
14829
15048
  hint: "Fix authentication first."
14830
15049
  };
14831
15050
  }
14832
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
15051
+ const client = new GadClient({
15052
+ token: auth.token,
15053
+ getToken: auth.getToken,
15054
+ apiUrl: auth.apiUrl
15055
+ });
14833
15056
  try {
14834
15057
  await client.get("/api/health");
14835
15058
  return {
@@ -14855,7 +15078,11 @@ async function checkCliVersion(auth) {
14855
15078
  hint: "Fix authentication first, then run 'gad doctor' again."
14856
15079
  };
14857
15080
  }
14858
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
15081
+ const client = new GadClient({
15082
+ token: auth.token,
15083
+ getToken: auth.getToken,
15084
+ apiUrl: auth.apiUrl
15085
+ });
14859
15086
  try {
14860
15087
  const result = await client.get("/api/mcp/version");
14861
15088
  const cli = result.updates?.cli ?? result.cli ?? {};
@@ -14901,7 +15128,11 @@ async function checkIntegrations(auth) {
14901
15128
  hint: "Fix authentication first."
14902
15129
  };
14903
15130
  }
14904
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
15131
+ const client = new GadClient({
15132
+ token: auth.token,
15133
+ getToken: auth.getToken,
15134
+ apiUrl: auth.apiUrl
15135
+ });
14905
15136
  try {
14906
15137
  const result = await client.mcpCall("integrations_list", {});
14907
15138
  const data = result ?? {};
@@ -14942,7 +15173,11 @@ async function checkMcp(auth) {
14942
15173
  hint: "Fix authentication first."
14943
15174
  };
14944
15175
  }
14945
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
15176
+ const client = new GadClient({
15177
+ token: auth.token,
15178
+ getToken: auth.getToken,
15179
+ apiUrl: auth.apiUrl
15180
+ });
14946
15181
  try {
14947
15182
  await client.mcpCall("workspace_introspect", {});
14948
15183
  return {
@@ -15550,7 +15785,11 @@ var integrationListColumns = [
15550
15785
  ];
15551
15786
  async function createClient3() {
15552
15787
  const auth = await resolveAuth();
15553
- return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
15788
+ return new GadClient({
15789
+ token: auth.token,
15790
+ getToken: auth.getToken,
15791
+ apiUrl: auth.apiUrl
15792
+ });
15554
15793
  }
15555
15794
  function formatErrorDetail(rawError) {
15556
15795
  if (rawError === null || rawError === void 0) return null;
@@ -15661,13 +15900,13 @@ var integrationsCommand = new Command("integrations").description("Manage connec
15661
15900
  var integrations_default = integrationsCommand;
15662
15901
 
15663
15902
  // src/commands/mcp.ts
15664
- import { createHash as createHash3 } from "node:crypto";
15903
+ import { createHash as createHash4 } from "node:crypto";
15665
15904
  import { hostname } from "node:os";
15666
15905
 
15667
15906
  // src/lib/mcp-bridge.ts
15668
15907
  import { createInterface as createInterface2 } from "node:readline";
15669
15908
  import { execFileSync as execFileSync6 } from "node:child_process";
15670
- import { randomUUID } from "node:crypto";
15909
+ import { randomUUID as randomUUID2 } from "node:crypto";
15671
15910
  var BRIDGE_CLIENT_ID = "gad-cli";
15672
15911
  var DEFAULT_API_URL2 = "https://www.greatarrow.ai";
15673
15912
  var DEFAULT_TIMEOUT_MS3 = 9e4;
@@ -15830,7 +16069,7 @@ async function runStdioBridge(options) {
15830
16069
  `));
15831
16070
  const endpoint = `${options.auth.apiUrl}/api/mcp`;
15832
16071
  const timeoutMs = Number(env4.MEMORY_TIMEOUT_MS ?? DEFAULT_TIMEOUT_MS3);
15833
- const sessionId = (env4.GAD_SESSION_ID ?? env4.MEMORY_SESSION_ID)?.trim() || `${BRIDGE_CLIENT_ID}:${Date.now()}-${randomUUID().slice(0, 8)}`;
16072
+ const sessionId = (env4.GAD_SESSION_ID ?? env4.MEMORY_SESSION_ID)?.trim() || `${BRIDGE_CLIENT_ID}:${Date.now()}-${randomUUID2().slice(0, 8)}`;
15834
16073
  const repoUrl = (env4.GAD_REPO_URL ?? env4.MEMORY_REPO_URL)?.trim() || detectGitOrigin3();
15835
16074
  const workspaceId = (env4.GAD_WORKSPACE_ID ?? env4.GAD_WORKSPACE)?.trim() || getCommandWorkspaceTarget({ hasRepo: Boolean(repoUrl) });
15836
16075
  const headers = buildBridgeHeaders({
@@ -15994,7 +16233,11 @@ async function mcpJsonAtRiskOfCommit(dir) {
15994
16233
  }
15995
16234
  async function createClient4() {
15996
16235
  const auth = await resolveAuth();
15997
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
16236
+ const client = new GadClient({
16237
+ token: auth.token,
16238
+ getToken: auth.getToken,
16239
+ apiUrl: auth.apiUrl
16240
+ });
15998
16241
  return { client, apiUrl: auth.apiUrl };
15999
16242
  }
16000
16243
  function generateMcpConfig(client, apiUrl, token, repoUrl) {
@@ -16029,7 +16272,7 @@ function generateMcpConfig(client, apiUrl, token, repoUrl) {
16029
16272
  Authorization: `Bearer ${token}`,
16030
16273
  "X-MCP-Client": CLIENT_IDS[client],
16031
16274
  "X-Device-Name": deviceName,
16032
- "X-Machine-Id": createHash3("sha256").update(rawHost).digest("hex"),
16275
+ "X-Machine-Id": createHash4("sha256").update(rawHost).digest("hex"),
16033
16276
  [GAD_INSTALL_HEADER]: GAD_INSTALL_VERSION,
16034
16277
  ...repoUrl ? { "X-Repo-URL": repoUrl } : {}
16035
16278
  };
@@ -16294,7 +16537,11 @@ var testCommand = new Command("test").description(
16294
16537
  const c = createColor(false);
16295
16538
  const auth = await resolveAuth();
16296
16539
  const apiUrl = auth.apiUrl;
16297
- const client = new GadClient({ token: auth.token, apiUrl });
16540
+ const client = new GadClient({
16541
+ token: auth.token,
16542
+ getToken: auth.getToken,
16543
+ apiUrl
16544
+ });
16298
16545
  const verbose = !jsonFlag;
16299
16546
  const checks = [
16300
16547
  await checkDns(apiUrl, verbose, c),
@@ -16833,6 +17080,7 @@ function smokeCommand() {
16833
17080
  const auth = await resolveAuth();
16834
17081
  const client = new GadClient({
16835
17082
  token: auth.token,
17083
+ getToken: auth.getToken,
16836
17084
  apiUrl: auth.apiUrl
16837
17085
  });
16838
17086
  if (!isJson && !isQuiet) {
@@ -16880,7 +17128,11 @@ var agentListColumns = [
16880
17128
  ];
16881
17129
  async function buildClient2() {
16882
17130
  const auth = await resolveAuth();
16883
- return new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
17131
+ return new GadClient({
17132
+ token: auth.token,
17133
+ getToken: auth.getToken,
17134
+ apiUrl: auth.apiUrl
17135
+ });
16884
17136
  }
16885
17137
  function invokeOutcome(data) {
16886
17138
  if (typeof data === "string") {
@@ -17030,6 +17282,7 @@ function askCommand() {
17030
17282
  const auth = await resolveAuth();
17031
17283
  const client = new GadClient({
17032
17284
  token: auth.token,
17285
+ getToken: auth.getToken,
17033
17286
  apiUrl: auth.apiUrl
17034
17287
  });
17035
17288
  const fmt = createFormatter({
@@ -18022,26 +18275,36 @@ function authHeaders(token) {
18022
18275
  function origin(apiUrl) {
18023
18276
  return apiUrl.replace(/\/+$/, "");
18024
18277
  }
18025
- function appAwareFetch(apiUrl, token, base) {
18026
- return (input, init) => {
18278
+ function appAwareFetch(apiUrl, token, base, getToken2) {
18279
+ return async (input, init) => {
18027
18280
  const target = typeof input === "string" ? input : String(input);
18028
- if (!target.startsWith("/")) return base(input, init);
18029
- return base(`${origin(apiUrl)}${target}`, {
18281
+ const relative = target.startsWith("/") && !target.startsWith("//");
18282
+ if (!relative && !target.startsWith(`${origin(apiUrl)}/`))
18283
+ return base(input, init);
18284
+ const currentToken = getToken2 ? await withTimeout(15e3, () => getToken2()) : token;
18285
+ return base(relative ? `${origin(apiUrl)}${target}` : target, {
18030
18286
  ...init,
18031
18287
  headers: {
18032
18288
  ...init?.headers,
18033
- ...authHeaders(token)
18289
+ ...authHeaders(currentToken)
18034
18290
  }
18035
18291
  });
18036
18292
  };
18037
18293
  }
18038
18294
  async function withTimeout(ms, run2) {
18039
18295
  const controller = new AbortController();
18040
- const timer = setTimeout(() => controller.abort(), ms);
18296
+ let timer;
18297
+ const deadline = new Promise((_resolve, reject) => {
18298
+ timer = setTimeout(() => {
18299
+ const error = new DOMException("Request timed out", "AbortError");
18300
+ controller.abort(error);
18301
+ reject(error);
18302
+ }, ms);
18303
+ });
18041
18304
  try {
18042
- return await run2(controller.signal);
18305
+ return await Promise.race([run2(controller.signal), deadline]);
18043
18306
  } finally {
18044
- clearTimeout(timer);
18307
+ if (timer) clearTimeout(timer);
18045
18308
  }
18046
18309
  }
18047
18310
  function readHandoff(payload) {
@@ -18106,7 +18369,7 @@ async function askForTheTurn(opts) {
18106
18369
  if (!response.ok) {
18107
18370
  throw new LocalTurnFailed(
18108
18371
  "handoff",
18109
- 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}).`,
18372
+ 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}).`,
18110
18373
  true
18111
18374
  );
18112
18375
  }
@@ -18168,14 +18431,14 @@ async function handBack(opts) {
18168
18431
  }
18169
18432
  async function answerOnThisComputer(opts) {
18170
18433
  const base = opts.fetchImpl ?? fetch;
18171
- const doFetch = appAwareFetch(opts.apiUrl, opts.token, base);
18434
+ const doFetch = appAwareFetch(opts.apiUrl, opts.token, base, opts.getToken);
18172
18435
  const handoff = await askForTheTurn({
18173
18436
  apiUrl: opts.apiUrl,
18174
18437
  token: opts.token,
18175
18438
  question: opts.question,
18176
18439
  sessionId: opts.sessionId ?? null,
18177
18440
  timezone: opts.timezone,
18178
- doFetch: base
18441
+ doFetch
18179
18442
  });
18180
18443
  if (!handoff) {
18181
18444
  return { answeredHere: false, reason: "answered_by_great_arrow" };
@@ -18217,7 +18480,7 @@ async function answerOnThisComputer(opts) {
18217
18480
  outputTokens: result.outputTokens,
18218
18481
  promptCharacters: result.promptCharacters,
18219
18482
  latencyMs: Date.now() - startedAt,
18220
- doFetch: base
18483
+ doFetch
18221
18484
  });
18222
18485
  return {
18223
18486
  answeredHere: true,
@@ -18237,7 +18500,7 @@ async function answerOnThisComputer(opts) {
18237
18500
  outputTokens: void 0,
18238
18501
  promptCharacters: 0,
18239
18502
  latencyMs: Date.now() - startedAt,
18240
- doFetch: base
18503
+ doFetch
18241
18504
  });
18242
18505
  throw new LocalTurnFailed(
18243
18506
  "writing",
@@ -18470,7 +18733,11 @@ function checkCommand() {
18470
18733
  const provider = readProvider(options.app);
18471
18734
  const baseUrl = typeof options.url === "string" && options.url ? options.url : LOCAL_PROVIDER_PRESETS[provider].baseUrl;
18472
18735
  const auth = await resolveAuth();
18473
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
18736
+ const client = new GadClient({
18737
+ token: auth.token,
18738
+ getToken: auth.getToken,
18739
+ apiUrl: auth.apiUrl
18740
+ });
18474
18741
  const fmt = createFormatter({
18475
18742
  json: g.json,
18476
18743
  quiet: g.quiet,
@@ -18545,7 +18812,11 @@ function statusCommand2() {
18545
18812
  return new Command("status").description("Show what is set up for models on this computer").action(async (_options, command) => {
18546
18813
  const g = globalOutputOpts(command);
18547
18814
  const auth = await resolveAuth();
18548
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
18815
+ const client = new GadClient({
18816
+ token: auth.token,
18817
+ getToken: auth.getToken,
18818
+ apiUrl: auth.apiUrl
18819
+ });
18549
18820
  const payload = await client.mcpCall(
18550
18821
  "model_local_get",
18551
18822
  {},
@@ -18613,7 +18884,11 @@ function offCommand() {
18613
18884
  return new Command("off").description("Stop answering with a model on this computer").action(async (_options, command) => {
18614
18885
  const g = globalOutputOpts(command);
18615
18886
  const auth = await resolveAuth();
18616
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
18887
+ const client = new GadClient({
18888
+ token: auth.token,
18889
+ getToken: auth.getToken,
18890
+ apiUrl: auth.apiUrl
18891
+ });
18617
18892
  const fmt = createFormatter({
18618
18893
  json: g.json,
18619
18894
  quiet: g.quiet,
@@ -18644,6 +18919,7 @@ function askHereCommand() {
18644
18919
  report = await answerOnThisComputer({
18645
18920
  apiUrl: auth.apiUrl,
18646
18921
  token: auth.token,
18922
+ getToken: auth.getToken,
18647
18923
  question,
18648
18924
  sessionId: typeof options.session === "string" ? options.session : null,
18649
18925
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
@@ -18757,6 +19033,7 @@ function statusCommand3() {
18757
19033
  const auth = await resolveAuth();
18758
19034
  const client = new GadClient({
18759
19035
  token: auth.token,
19036
+ getToken: auth.getToken,
18760
19037
  apiUrl: auth.apiUrl
18761
19038
  });
18762
19039
  const fmt = createFormatter({
@@ -18934,7 +19211,11 @@ function openCommand() {
18934
19211
  ).option("--print", "Print the URL instead of opening a browser").option("--json", "Output the resolved surface as JSON").action(async (surface, options, command) => {
18935
19212
  const g = globalOutputOpts(command);
18936
19213
  const auth = await resolveAuth();
18937
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
19214
+ const client = new GadClient({
19215
+ token: auth.token,
19216
+ getToken: auth.getToken,
19217
+ apiUrl: auth.apiUrl
19218
+ });
18938
19219
  const result = await client.mcpCall("surfaces_list", {
18939
19220
  query: surface
18940
19221
  });
@@ -19022,7 +19303,11 @@ function nextCommand() {
19022
19303
  noColor: g.noColor
19023
19304
  });
19024
19305
  const auth = await resolveAuth();
19025
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
19306
+ const client = new GadClient({
19307
+ token: auth.token,
19308
+ getToken: auth.getToken,
19309
+ apiUrl: auth.apiUrl
19310
+ });
19026
19311
  let identityStatus = null;
19027
19312
  try {
19028
19313
  const who = await client.mcpCall("workspace_introspect", {});
@@ -19085,7 +19370,11 @@ function discoverCommand() {
19085
19370
  noColor: g.noColor
19086
19371
  });
19087
19372
  const auth = await resolveAuth();
19088
- const client = new GadClient({ token: auth.token, apiUrl: auth.apiUrl });
19373
+ const client = new GadClient({
19374
+ token: auth.token,
19375
+ getToken: auth.getToken,
19376
+ apiUrl: auth.apiUrl
19377
+ });
19089
19378
  const parsed = opts.limit ? Number.parseInt(opts.limit, 10) : NaN;
19090
19379
  const args = Number.isFinite(parsed) && parsed > 0 ? { limit: parsed } : {};
19091
19380
  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.53",
3
+ "version": "0.1.55",
4
4
  "description": "GreatArrow.ai CLI tool",
5
5
  "type": "module",
6
6
  "bin": {