@great-arrow/cli 0.9.3 → 0.9.6

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 +511 -371
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3339,11 +3339,11 @@ var require_util = __commonJS({
3339
3339
  if (files.includes("node_modules") || files.includes("package.json") || files.includes("package.json5") || files.includes("package.yaml") || files.includes("pnpm-workspace.yaml")) {
3340
3340
  return name24;
3341
3341
  }
3342
- const dirname10 = path10.dirname(name24);
3343
- if (dirname10 === name24) {
3342
+ const dirname11 = path10.dirname(name24);
3343
+ if (dirname11 === name24) {
3344
3344
  return original;
3345
3345
  }
3346
- return find(dirname10, original);
3346
+ return find(dirname11, original);
3347
3347
  } catch (error62) {
3348
3348
  if (name24 === original) {
3349
3349
  if (error62.code === "ENOENT") {
@@ -9483,7 +9483,7 @@ function isNodeError(value) {
9483
9483
 
9484
9484
  // src/lib/version.ts
9485
9485
  var CLI_PACKAGE_NAME = "@great-arrow/cli";
9486
- var CLI_VERSION = "0.9.3";
9486
+ var CLI_VERSION = "0.9.6";
9487
9487
  var GAD_INSTALL_VERSION = "2026.08.08";
9488
9488
  var GAD_INSTALL_MIN_VERSION = "2026.06.22";
9489
9489
  var GAD_INSTALL_HEADER = "X-GAD-Install-Version";
@@ -10969,83 +10969,6 @@ function sessionExpired() {
10969
10969
  return authError("Session expired.", "Run 'gad login' to re-authenticate.");
10970
10970
  }
10971
10971
 
10972
- // src/commands/auth.ts
10973
- var EXIT_SESSION_EXPIRED = 2;
10974
- var EXIT_NOT_OAUTH = 3;
10975
- function authCommand() {
10976
- const cmd = new Command("auth").description("Authentication helpers");
10977
- cmd.command("token").description("Print a fresh OAuth access token, refreshing it if needed").option("--json", "Output {accessToken, accessExpiresAt, apiUrl} as JSON").action(async (options, actionCommand2) => {
10978
- try {
10979
- const t = await getFreshOAuthAccessToken();
10980
- writeCliLocator();
10981
- const jsonOutput = options.json || actionCommand2.optsWithGlobals().json;
10982
- process.stdout.write(
10983
- jsonOutput ? `${JSON.stringify(t)}
10984
- ` : `${t.accessToken}
10985
- `
10986
- );
10987
- } catch (error62) {
10988
- if (error62 instanceof NotOAuthCredential) {
10989
- process.stderr.write(`${error62.message}
10990
- `);
10991
- process.exitCode = EXIT_NOT_OAUTH;
10992
- return;
10993
- }
10994
- if (error62 instanceof CliError && error62.code === 1) {
10995
- const hint = error62.hint ? ` ${error62.hint}` : "";
10996
- process.stderr.write(`${error62.message}${hint}
10997
- `);
10998
- process.exitCode = EXIT_SESSION_EXPIRED;
10999
- return;
11000
- }
11001
- throw error62;
11002
- }
11003
- });
11004
- return cmd;
11005
- }
11006
-
11007
- // src/commands/logout.ts
11008
- var logoutCommand = new Command("logout").description("Remove stored authentication credentials").action(async (_options, cmd) => {
11009
- const parent = cmd.parent;
11010
- const json3 = parent?.opts().json ?? false;
11011
- const quiet = parent?.opts().quiet ?? false;
11012
- const noColor = parent?.opts().noColor ?? false;
11013
- const fmt = createFormatter({
11014
- json: json3,
11015
- quiet,
11016
- isTTY: isTTY(),
11017
- noColor
11018
- });
11019
- const stored = await loadStoredCredential();
11020
- if (stored?.kind === "oauth") {
11021
- await revokeToken({
11022
- apiUrl: stored.credential.apiUrl,
11023
- clientId: stored.credential.clientId,
11024
- token: stored.credential.refreshToken,
11025
- tokenTypeHint: "refresh_token"
11026
- });
11027
- }
11028
- let clearError = null;
11029
- try {
11030
- await clearStoredCredential();
11031
- } catch (err2) {
11032
- clearError = err2;
11033
- }
11034
- const config2 = await loadConfig();
11035
- if (config2.token) {
11036
- config2.token = "";
11037
- await saveConfig(config2);
11038
- }
11039
- if (clearError) {
11040
- fmt.error(
11041
- clearError instanceof Error ? clearError.message : "Could not remove stored authentication credentials."
11042
- );
11043
- process.exitCode = 1 /* AUTH_ERROR */;
11044
- return;
11045
- }
11046
- fmt.success("Logged out successfully.");
11047
- });
11048
-
11049
10972
  // src/lib/client.ts
11050
10973
  import { execSync } from "node:child_process";
11051
10974
 
@@ -11547,6 +11470,273 @@ async function withNativeResponse(context2, path10, init, consume, parentSignal)
11547
11470
  );
11548
11471
  }
11549
11472
 
11473
+ // src/lib/keys-handoff.ts
11474
+ import { spawn } from "node:child_process";
11475
+
11476
+ // src/commands/open.ts
11477
+ import { execFile as execFile2 } from "node:child_process";
11478
+
11479
+ // src/lib/global-opts.ts
11480
+ function globalOutputOpts(cmd) {
11481
+ const merged = cmd.optsWithGlobals();
11482
+ return {
11483
+ json: Boolean(merged.json),
11484
+ quiet: Boolean(merged.quiet),
11485
+ // --no-color sets `color: false`; some commands declare `--no-color`
11486
+ // locally which surfaces as `noColor`.
11487
+ noColor: merged.color === false || Boolean(merged.noColor)
11488
+ };
11489
+ }
11490
+
11491
+ // src/commands/open.ts
11492
+ var PLANE_NOTE = {
11493
+ workspace: "follows your active workspace",
11494
+ cross: "spans every workspace you belong to",
11495
+ account: "your account, not a workspace"
11496
+ };
11497
+ function browserLaunchCommand(url2, platform2 = process.platform) {
11498
+ let parsed;
11499
+ try {
11500
+ parsed = new URL(url2);
11501
+ } catch {
11502
+ return null;
11503
+ }
11504
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
11505
+ if (platform2 === "darwin") return { file: "open", args: [url2] };
11506
+ if (platform2 === "win32") return { file: "explorer.exe", args: [url2] };
11507
+ return { file: "xdg-open", args: [url2] };
11508
+ }
11509
+ function openBrowser2(url2) {
11510
+ const command = browserLaunchCommand(url2);
11511
+ if (!command) return Promise.resolve(false);
11512
+ return new Promise((resolve7) => {
11513
+ execFile2(command.file, command.args, (error62) => resolve7(!error62));
11514
+ });
11515
+ }
11516
+ function toSurfaceRow(value) {
11517
+ if (typeof value !== "object" || value === null) return null;
11518
+ const r = value;
11519
+ if (typeof r.id !== "string" || typeof r.href !== "string") return null;
11520
+ if (!r.href.startsWith("/") || r.href.startsWith("//")) return null;
11521
+ const plane = r.plane === "workspace" || r.plane === "cross" || r.plane === "account" ? r.plane : "workspace";
11522
+ return {
11523
+ id: r.id,
11524
+ label: typeof r.label === "string" ? r.label : r.id,
11525
+ href: r.href,
11526
+ plane,
11527
+ group: typeof r.group === "string" ? r.group : ""
11528
+ };
11529
+ }
11530
+ function surfaceUrl(apiUrl, href) {
11531
+ return `${apiUrl.replace(/\/$/, "")}${href}`;
11532
+ }
11533
+ function openCommand() {
11534
+ return new Command("open").description("Open an app surface in the browser (e.g. gad open library)").argument(
11535
+ "<surface>",
11536
+ "Surface id, path, label or keyword \u2014 e.g. library, /leads, 'Pitch page'"
11537
+ ).option("--print", "Print the URL instead of opening a browser").option("--json", "Output the resolved surface as JSON").action(async (surface, options, command) => {
11538
+ const g = globalOutputOpts(command);
11539
+ const auth = await resolveAuth();
11540
+ const client2 = new GadClient({
11541
+ token: auth.token,
11542
+ getToken: auth.getToken,
11543
+ apiUrl: auth.apiUrl
11544
+ });
11545
+ const result = await client2.mcpCall("surfaces_list", {
11546
+ query: surface
11547
+ });
11548
+ const match = toSurfaceRow(result?.match);
11549
+ const candidates = (Array.isArray(result?.candidates) ? result.candidates : []).map(toSurfaceRow).filter((r) => r !== null);
11550
+ if (!match) {
11551
+ throw new CliError(
11552
+ candidates.length === 0 ? `No app surface matches '${surface}'.` : `'${surface}' matches ${candidates.length} surfaces.`,
11553
+ 3 /* VALIDATION_ERROR */,
11554
+ candidates.length === 0 ? "Run 'gad open --help', or ask an AI for surfaces_list to see every page." : `Did you mean: ${candidates.map((c2) => c2.id).join(", ")}?`
11555
+ );
11556
+ }
11557
+ const url2 = surfaceUrl(auth.apiUrl, match.href);
11558
+ if (g.json) {
11559
+ process.stdout.write(JSON.stringify({ ...match, url: url2 }) + "\n");
11560
+ return;
11561
+ }
11562
+ const c = createColor(g.noColor || g.quiet);
11563
+ if (options.print) {
11564
+ process.stdout.write(url2 + "\n");
11565
+ return;
11566
+ }
11567
+ const opened = await openBrowser2(url2);
11568
+ process.stdout.write(
11569
+ `${match.label} ${c.dim(`(${PLANE_NOTE[match.plane]})`)}
11570
+ ${c.cyan(url2)}
11571
+ `
11572
+ );
11573
+ if (!opened) {
11574
+ process.stderr.write(
11575
+ `${c.dim("Couldn't launch a browser \u2014 open the URL above.")}
11576
+ `
11577
+ );
11578
+ }
11579
+ });
11580
+ }
11581
+
11582
+ // src/lib/keys-paths.ts
11583
+ function keyRotationPath() {
11584
+ return "/settings/key-rotation";
11585
+ }
11586
+ function keyRotationRunPath(runId) {
11587
+ return `/settings/key-rotation/runs/${encodeURIComponent(runId)}`;
11588
+ }
11589
+ function keyRotationHandoffPath(jti) {
11590
+ return `/settings/key-rotation/handoff/${encodeURIComponent(jti)}`;
11591
+ }
11592
+
11593
+ // src/lib/keys-handoff.ts
11594
+ var HANDOFF_POLL_INTERVAL_MS = 2e3;
11595
+ var HANDOFF_DEADLINE_MS = 5 * 6e4;
11596
+ var BROWSER_LAUNCH_TIMEOUT_MS = 5e3;
11597
+ function openInBrowser(url2) {
11598
+ const command = browserLaunchCommand(url2);
11599
+ if (!command) return Promise.resolve(false);
11600
+ return new Promise((resolve7) => {
11601
+ let settled = false;
11602
+ const done = (ok2) => {
11603
+ if (settled) return;
11604
+ settled = true;
11605
+ clearTimeout(timer);
11606
+ resolve7(ok2);
11607
+ };
11608
+ const timer = setTimeout(() => done(false), BROWSER_LAUNCH_TIMEOUT_MS);
11609
+ timer.unref?.();
11610
+ try {
11611
+ const child = spawn(command.file, command.args, {
11612
+ detached: true,
11613
+ stdio: "ignore"
11614
+ });
11615
+ child.once("error", () => done(false));
11616
+ child.once("spawn", () => {
11617
+ child.unref();
11618
+ done(true);
11619
+ });
11620
+ } catch {
11621
+ done(false);
11622
+ }
11623
+ });
11624
+ }
11625
+ var defaultSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
11626
+ function stepUpError(message, hint) {
11627
+ return new CliError(message, 1 /* AUTH_ERROR */, hint);
11628
+ }
11629
+ async function requestBrowserStepUp(client2, kind, subjectId, opts = {}) {
11630
+ const openUrl = opts.openUrl ?? openInBrowser;
11631
+ const notify = opts.notify ?? ((line) => process.stderr.write(`${line}
11632
+ `));
11633
+ const sleep3 = opts.sleep ?? defaultSleep;
11634
+ const now = opts.now ?? Date.now;
11635
+ const interval = opts.pollIntervalMs ?? HANDOFF_POLL_INTERVAL_MS;
11636
+ const deadline = now() + (opts.deadlineMs ?? HANDOFF_DEADLINE_MS);
11637
+ const created = await client2.post("/api/key-rotation/handoffs", {
11638
+ kind,
11639
+ subjectId
11640
+ });
11641
+ const jti = typeof created?.jti === "string" ? created.jti : null;
11642
+ if (!jti || !/^[A-Za-z0-9-]{1,64}$/.test(jti)) {
11643
+ throw stepUpError("The server did not return a browser approval.");
11644
+ }
11645
+ const url2 = `${client2.apiOrigin}${keyRotationHandoffPath(jti)}`;
11646
+ notify(`Approve this in your browser (passkey or two-step code): ${url2}`);
11647
+ if (!await openUrl(url2)) {
11648
+ notify("Couldn't launch a browser. Open the URL above to approve.");
11649
+ }
11650
+ while (now() < deadline) {
11651
+ const polled = await client2.get(
11652
+ `/api/key-rotation/handoffs/${encodeURIComponent(jti)}`
11653
+ );
11654
+ const status = polled?.status;
11655
+ if (status === "approved") {
11656
+ if (typeof polled.token === "string" && polled.token.length > 0) {
11657
+ return polled.token;
11658
+ }
11659
+ throw stepUpError(
11660
+ "This approval was already collected by another request.",
11661
+ "Run the command again to request a new approval."
11662
+ );
11663
+ }
11664
+ if (status === "consumed") {
11665
+ throw stepUpError(
11666
+ "This approval was already used.",
11667
+ "Run the command again to request a new approval."
11668
+ );
11669
+ }
11670
+ if (status === "expired") {
11671
+ throw stepUpError(
11672
+ "The browser approval expired.",
11673
+ "Run the command again and approve within 5 minutes."
11674
+ );
11675
+ }
11676
+ await sleep3(interval);
11677
+ }
11678
+ throw stepUpError(
11679
+ "Timed out waiting for the browser approval.",
11680
+ "Run the command again and approve within 5 minutes."
11681
+ );
11682
+ }
11683
+
11684
+ // src/lib/admin-access.ts
11685
+ async function requestAdminAccess(client2, apiOrigin, opts = {}) {
11686
+ const now = opts.now ?? Date.now;
11687
+ const deadline = now() + (opts.deadlineMs ?? 5 * 6e4);
11688
+ const notify = opts.notify ?? ((line) => process.stderr.write(`${line}
11689
+ `));
11690
+ const created = await client2.requestOnce("POST", "/api/admin/access", {
11691
+ timeoutMs: 15e3
11692
+ });
11693
+ const request = created.body;
11694
+ if (created.status !== 200 || !request?.requestId || !/^[a-f0-9-]{36}$/.test(request.requestId))
11695
+ throw new CliError(
11696
+ "Could not request admin approval.",
11697
+ 1 /* AUTH_ERROR */,
11698
+ "Check this connection has admin scope and belongs to a super admin."
11699
+ );
11700
+ if (request.status === "approved") return;
11701
+ if (request.status !== "pending")
11702
+ throw new CliError("Admin approval is unavailable.", 1 /* AUTH_ERROR */);
11703
+ const url2 = new URL(`/admin/compliance/audit/${request.requestId}`, apiOrigin).href;
11704
+ notify(`Approve 15-minute admin access with your passkey: ${url2}`);
11705
+ if (!await (opts.openUrl ?? openInBrowser)(url2))
11706
+ notify("Open the link above in your browser to approve.");
11707
+ const sleep3 = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
11708
+ while (now() < deadline) {
11709
+ const result = await client2.requestOnce("GET", "/api/admin/access", {
11710
+ params: { requestId: request.requestId },
11711
+ timeoutMs: 15e3
11712
+ });
11713
+ const status = result.body;
11714
+ if (result.status !== 200)
11715
+ throw new CliError(
11716
+ "Admin approval could not be checked.",
11717
+ 1 /* AUTH_ERROR */,
11718
+ "The connection may have changed. Run the command again."
11719
+ );
11720
+ if (status?.status === "approved") {
11721
+ notify(
11722
+ `Admin access approved until ${status.expiresAt}. No key was copied or saved.`
11723
+ );
11724
+ return;
11725
+ }
11726
+ if (status?.status !== "pending")
11727
+ throw new CliError(
11728
+ "Admin approval was denied or expired.",
11729
+ 1 /* AUTH_ERROR */
11730
+ );
11731
+ await sleep3(opts.pollIntervalMs ?? 2e3);
11732
+ }
11733
+ throw new CliError(
11734
+ "Timed out waiting for admin approval.",
11735
+ 1 /* AUTH_ERROR */,
11736
+ "Run the command again to request a new approval."
11737
+ );
11738
+ }
11739
+
11550
11740
  // src/lib/client.ts
11551
11741
  var DEFAULT_TIMEOUT_MS2 = 3e4;
11552
11742
  var PRESENCE_TIMEOUT_MS = 4e4;
@@ -11811,6 +12001,16 @@ var GadClient = class {
11811
12001
  * @returns The decoded JSON payload from the tool result content
11812
12002
  */
11813
12003
  async mcpCall(toolName, args, timeoutMs) {
12004
+ try {
12005
+ return await this.mcpCallOnce(toolName, args, timeoutMs);
12006
+ } catch (error62) {
12007
+ if (!(error62 instanceof McpToolError) || !["step_up_required", "needs_grant"].includes(error62.toolCode))
12008
+ throw error62;
12009
+ await requestAdminAccess(this, this.apiUrl);
12010
+ return this.mcpCallOnce(toolName, args, timeoutMs);
12011
+ }
12012
+ }
12013
+ async mcpCallOnce(toolName, args, timeoutMs) {
11814
12014
  const url2 = `${this.apiUrl}/api/mcp`;
11815
12015
  const body = JSON.stringify({
11816
12016
  jsonrpc: "2.0",
@@ -12101,18 +12301,94 @@ var GadClient = class {
12101
12301
  }
12102
12302
  };
12103
12303
 
12104
- // src/lib/global-opts.ts
12105
- function globalOutputOpts(cmd) {
12106
- const merged = cmd.optsWithGlobals();
12107
- return {
12108
- json: Boolean(merged.json),
12109
- quiet: Boolean(merged.quiet),
12110
- // --no-color sets `color: false`; some commands declare `--no-color`
12111
- // locally which surfaces as `noColor`.
12112
- noColor: merged.color === false || Boolean(merged.noColor)
12113
- };
12304
+ // src/commands/auth.ts
12305
+ var EXIT_SESSION_EXPIRED = 2;
12306
+ var EXIT_NOT_OAUTH = 3;
12307
+ function authCommand() {
12308
+ const cmd = new Command("auth").description("Authentication helpers");
12309
+ cmd.command("admin").description(
12310
+ "Approve 15-minute admin access in your browser; no key is printed"
12311
+ ).action(async () => {
12312
+ const auth = await resolveAuth();
12313
+ const client2 = new GadClient({
12314
+ token: auth.token,
12315
+ getToken: auth.getToken,
12316
+ apiUrl: auth.apiUrl
12317
+ });
12318
+ await requestAdminAccess(client2, auth.apiUrl);
12319
+ });
12320
+ cmd.command("token").description("Print a fresh OAuth access token, refreshing it if needed").option("--json", "Output {accessToken, accessExpiresAt, apiUrl} as JSON").action(async (options, actionCommand2) => {
12321
+ try {
12322
+ const t = await getFreshOAuthAccessToken();
12323
+ writeCliLocator();
12324
+ const jsonOutput = options.json || actionCommand2.optsWithGlobals().json;
12325
+ process.stdout.write(
12326
+ jsonOutput ? `${JSON.stringify(t)}
12327
+ ` : `${t.accessToken}
12328
+ `
12329
+ );
12330
+ } catch (error62) {
12331
+ if (error62 instanceof NotOAuthCredential) {
12332
+ process.stderr.write(`${error62.message}
12333
+ `);
12334
+ process.exitCode = EXIT_NOT_OAUTH;
12335
+ return;
12336
+ }
12337
+ if (error62 instanceof CliError && error62.code === 1) {
12338
+ const hint = error62.hint ? ` ${error62.hint}` : "";
12339
+ process.stderr.write(`${error62.message}${hint}
12340
+ `);
12341
+ process.exitCode = EXIT_SESSION_EXPIRED;
12342
+ return;
12343
+ }
12344
+ throw error62;
12345
+ }
12346
+ });
12347
+ return cmd;
12114
12348
  }
12115
12349
 
12350
+ // src/commands/logout.ts
12351
+ var logoutCommand = new Command("logout").description("Remove stored authentication credentials").action(async (_options, cmd) => {
12352
+ const parent = cmd.parent;
12353
+ const json3 = parent?.opts().json ?? false;
12354
+ const quiet = parent?.opts().quiet ?? false;
12355
+ const noColor = parent?.opts().noColor ?? false;
12356
+ const fmt = createFormatter({
12357
+ json: json3,
12358
+ quiet,
12359
+ isTTY: isTTY(),
12360
+ noColor
12361
+ });
12362
+ const stored = await loadStoredCredential();
12363
+ if (stored?.kind === "oauth") {
12364
+ await revokeToken({
12365
+ apiUrl: stored.credential.apiUrl,
12366
+ clientId: stored.credential.clientId,
12367
+ token: stored.credential.refreshToken,
12368
+ tokenTypeHint: "refresh_token"
12369
+ });
12370
+ }
12371
+ let clearError = null;
12372
+ try {
12373
+ await clearStoredCredential();
12374
+ } catch (err2) {
12375
+ clearError = err2;
12376
+ }
12377
+ const config2 = await loadConfig();
12378
+ if (config2.token) {
12379
+ config2.token = "";
12380
+ await saveConfig(config2);
12381
+ }
12382
+ if (clearError) {
12383
+ fmt.error(
12384
+ clearError instanceof Error ? clearError.message : "Could not remove stored authentication credentials."
12385
+ );
12386
+ process.exitCode = 1 /* AUTH_ERROR */;
12387
+ return;
12388
+ }
12389
+ fmt.success("Logged out successfully.");
12390
+ });
12391
+
12116
12392
  // src/commands/whoami.ts
12117
12393
  function firstThingsToTry(data) {
12118
12394
  const o = data.onboarding;
@@ -13079,7 +13355,7 @@ var claim_default = claimCommand;
13079
13355
  import { readFileSync } from "node:fs";
13080
13356
 
13081
13357
  // src/lib/work-status.ts
13082
- import { execFile as execFile2, execFileSync as execFileSync2 } from "node:child_process";
13358
+ import { execFile as execFile3, execFileSync as execFileSync2 } from "node:child_process";
13083
13359
 
13084
13360
  // ../../src/lib/work-risk/push-state.ts
13085
13361
  var REMOTE_BRANCH_REF_PREFIX = "refs/remotes/origin/";
@@ -13131,7 +13407,7 @@ var WORKTREE_STATUS_CONCURRENCY = 8;
13131
13407
  var WORKTREE_STATUS_TIMEOUT_MS = 2e3;
13132
13408
  function gitAsync(args, cwd) {
13133
13409
  return new Promise((resolve7) => {
13134
- execFile2(
13410
+ execFile3(
13135
13411
  "git",
13136
13412
  args,
13137
13413
  {
@@ -15479,7 +15755,7 @@ function serverMapsFromJson(parsed, serversKey) {
15479
15755
  return maps;
15480
15756
  }
15481
15757
  function findCodexEntries(location, content) {
15482
- const entries = [];
15758
+ const entries2 = [];
15483
15759
  const sections = content.split(/^\s*\[/m).slice(1);
15484
15760
  const seen = /* @__PURE__ */ new Set();
15485
15761
  for (const raw of sections) {
@@ -15493,7 +15769,7 @@ function findCodexEntries(location, content) {
15493
15769
  const transport = codexTransportFields(body);
15494
15770
  if (!isGreatArrowServer(name23, transport)) continue;
15495
15771
  seen.add(name23);
15496
- entries.push({
15772
+ entries2.push({
15497
15773
  clientId: location.clientId,
15498
15774
  client: location.client,
15499
15775
  scope: location.scope,
@@ -15502,7 +15778,7 @@ function findCodexEntries(location, content) {
15502
15778
  surface: typeof transport.command === "string" ? "stdio" : "http"
15503
15779
  });
15504
15780
  }
15505
- return entries;
15781
+ return entries2;
15506
15782
  }
15507
15783
  function findGreatArrowEntries(location, content, options = {}) {
15508
15784
  if (location.format === "toml") return findCodexEntries(location, content);
@@ -15514,7 +15790,7 @@ function findGreatArrowEntries(location, content, options = {}) {
15514
15790
  }
15515
15791
  if (!parsed) return [];
15516
15792
  const serversKey = location.serversKey ?? "mcpServers";
15517
- const entries = [];
15793
+ const entries2 = [];
15518
15794
  for (const { servers, projectDir } of serverMapsFromJson(
15519
15795
  parsed,
15520
15796
  serversKey
@@ -15523,7 +15799,7 @@ function findGreatArrowEntries(location, content, options = {}) {
15523
15799
  const entry = asObject3(value);
15524
15800
  if (!entry) continue;
15525
15801
  if (!isGreatArrowServer(name23, entry)) continue;
15526
- entries.push({
15802
+ entries2.push({
15527
15803
  clientId: location.clientId,
15528
15804
  client: location.client,
15529
15805
  scope: projectDir ? "project" : location.scope,
@@ -15538,7 +15814,7 @@ function findGreatArrowEntries(location, content, options = {}) {
15538
15814
  });
15539
15815
  }
15540
15816
  }
15541
- return entries;
15817
+ return entries2;
15542
15818
  }
15543
15819
  function hasGreatArrowHooks(settingsContent) {
15544
15820
  let parsed = null;
@@ -15573,9 +15849,9 @@ function compareEntries(a, b) {
15573
15849
  if (a.scope !== b.scope) return a.scope === "user" ? -1 : 1;
15574
15850
  return a.path.localeCompare(b.path);
15575
15851
  }
15576
- function detectDuplicates(entries) {
15852
+ function detectDuplicates(entries2) {
15577
15853
  const byClient = /* @__PURE__ */ new Map();
15578
- for (const entry of entries) {
15854
+ for (const entry of entries2) {
15579
15855
  const list = byClient.get(entry.clientId) ?? [];
15580
15856
  list.push(entry);
15581
15857
  byClient.set(entry.clientId, list);
@@ -15602,8 +15878,8 @@ function detectDuplicates(entries) {
15602
15878
  }
15603
15879
  return findings.sort((a, b) => a.clientId.localeCompare(b.clientId));
15604
15880
  }
15605
- function connectorGuidance(entries) {
15606
- const hasLocalClaude = entries.some(
15881
+ function connectorGuidance(entries2) {
15882
+ const hasLocalClaude = entries2.some(
15607
15883
  (entry) => entry.clientId === "claude-code" || entry.clientId === "claude-desktop"
15608
15884
  );
15609
15885
  if (!hasLocalClaude) return null;
@@ -17294,12 +17570,12 @@ var logsCommand = new Command("logs").description("Tail MCP connection log with
17294
17570
  ]);
17295
17571
  return;
17296
17572
  }
17297
- const entries = Array.isArray(data) ? data : [];
17298
- if (entries.length === 0) {
17573
+ const entries2 = Array.isArray(data) ? data : [];
17574
+ if (entries2.length === 0) {
17299
17575
  fmt.info("No recent MCP connection events.");
17300
17576
  return;
17301
17577
  }
17302
- for (const entry of entries) {
17578
+ for (const entry of entries2) {
17303
17579
  renderLogEntry(entry, c);
17304
17580
  }
17305
17581
  } catch {
@@ -17646,15 +17922,15 @@ async function scanConnections() {
17646
17922
  join19(home, ".claude", "settings.json")
17647
17923
  );
17648
17924
  const hooksInstalled = hookSettings ? hasGreatArrowHooks(hookSettings) : false;
17649
- const entries = [];
17925
+ const entries2 = [];
17650
17926
  for (const location of knownConfigLocations(home, process.cwd())) {
17651
17927
  const content = await readIfExists(location.path);
17652
17928
  if (content === null) continue;
17653
- entries.push(
17929
+ entries2.push(
17654
17930
  ...findGreatArrowEntries(location, content, { hooksInstalled })
17655
17931
  );
17656
17932
  }
17657
- return entries;
17933
+ return entries2;
17658
17934
  }
17659
17935
  async function probeEndpoint() {
17660
17936
  try {
@@ -17739,8 +18015,8 @@ var doctorSubcommand = new Command("doctor").description(
17739
18015
  const home = homedir12();
17740
18016
  const proxyPath = await resolveProxyScript(options.proxy);
17741
18017
  const diagnostics = proxyPath ? await runProxyDiagnostics(proxyPath) : null;
17742
- const entries = await scanConnections();
17743
- const duplicates = detectDuplicates(entries);
18018
+ const entries2 = await scanConnections();
18019
+ const duplicates = detectDuplicates(entries2);
17744
18020
  const credentialFile = await readIfExists(
17745
18021
  join19(home, ".great-arrow", "credential")
17746
18022
  );
@@ -17763,7 +18039,7 @@ var doctorSubcommand = new Command("doctor").description(
17763
18039
  }),
17764
18040
  await probeEndpoint()
17765
18041
  ];
17766
- if (entries.length === 0) {
18042
+ if (entries2.length === 0) {
17767
18043
  findings.push({
17768
18044
  code: "no_connections",
17769
18045
  level: "warn",
@@ -17788,14 +18064,14 @@ var doctorSubcommand = new Command("doctor").description(
17788
18064
  now: Date.now()
17789
18065
  });
17790
18066
  const applied = options.migrate ? await applyMigration(plan) : [];
17791
- const guidance = connectorGuidance(entries);
18067
+ const guidance = connectorGuidance(entries2);
17792
18068
  if (jsonFlag) {
17793
18069
  process.stdout.write(
17794
18070
  JSON.stringify(
17795
18071
  {
17796
18072
  proxyPath,
17797
18073
  diagnostics,
17798
- entries,
18074
+ entries: entries2,
17799
18075
  duplicates,
17800
18076
  findings,
17801
18077
  migration: { plan, applied, migrated: Boolean(options.migrate) },
@@ -17808,10 +18084,10 @@ var doctorSubcommand = new Command("doctor").description(
17808
18084
  } else {
17809
18085
  process.stdout.write(`${c.dim("Connections")}
17810
18086
  `);
17811
- if (entries.length === 0) {
18087
+ if (entries2.length === 0) {
17812
18088
  process.stdout.write(" (none found)\n");
17813
18089
  }
17814
- for (const entry of entries) {
18090
+ for (const entry of entries2) {
17815
18091
  process.stdout.write(` ${entry.client}: ${describeEntry(entry)}
17816
18092
  `);
17817
18093
  }
@@ -20733,9 +21009,9 @@ function assertNever(_x) {
20733
21009
  }
20734
21010
  function assert(_) {
20735
21011
  }
20736
- function getEnumValues(entries) {
20737
- const numericValues = Object.values(entries).filter((v) => typeof v === "number");
20738
- const values = Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v]) => v);
21012
+ function getEnumValues(entries2) {
21013
+ const numericValues = Object.values(entries2).filter((v) => typeof v === "number");
21014
+ const values = Object.entries(entries2).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v]) => v);
20739
21015
  return values;
20740
21016
  }
20741
21017
  function joinValues(array3, separator = "|") {
@@ -22570,16 +22846,16 @@ var $ZodCheckProperties = /* @__PURE__ */ $constructor("$ZodCheckProperties", (i
22570
22846
  hide(inst, Symbol.iterator, function* () {
22571
22847
  yield inst;
22572
22848
  });
22573
- let entries;
22849
+ let entries2;
22574
22850
  inst._zod.check = (payload) => {
22575
22851
  if (payload.value == null) {
22576
22852
  payload.issues.push({ expected: "object", code: "invalid_type", input: payload.value, inst });
22577
22853
  return void 0;
22578
22854
  }
22579
- entries ?? (entries = Reflect.ownKeys(def.shape).map((key) => [key, def.shape[key]]));
22855
+ entries2 ?? (entries2 = Reflect.ownKeys(def.shape).map((key) => [key, def.shape[key]]));
22580
22856
  const input2 = payload.value;
22581
22857
  let proms;
22582
- for (const [key, schema] of entries) {
22858
+ for (const [key, schema] of entries2) {
22583
22859
  const result = schema._zod.run({ value: input2[key], issues: [] }, {});
22584
22860
  if (result instanceof Promise) {
22585
22861
  proms ?? (proms = []);
@@ -35397,18 +35673,18 @@ function _set(Class2, valueType, params) {
35397
35673
  }
35398
35674
  // @__NO_SIDE_EFFECTS__
35399
35675
  function _enum(Class2, values, params) {
35400
- const entries = Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values;
35676
+ const entries2 = Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values;
35401
35677
  return new Class2({
35402
35678
  type: "enum",
35403
- entries,
35679
+ entries: entries2,
35404
35680
  ...normalizeParams(params)
35405
35681
  });
35406
35682
  }
35407
35683
  // @__NO_SIDE_EFFECTS__
35408
- function _nativeEnum(Class2, entries, params) {
35684
+ function _nativeEnum(Class2, entries2, params) {
35409
35685
  return new Class2({
35410
35686
  type: "enum",
35411
- entries,
35687
+ entries: entries2,
35412
35688
  ...normalizeParams(params)
35413
35689
  });
35414
35690
  }
@@ -38391,17 +38667,17 @@ var ZodEnum = /* @__PURE__ */ $constructor("ZodEnum", (inst, def) => {
38391
38667
  };
38392
38668
  });
38393
38669
  function _enum2(values, params) {
38394
- const entries = Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values;
38670
+ const entries2 = Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values;
38395
38671
  return new ZodEnum({
38396
38672
  type: "enum",
38397
- entries,
38673
+ entries: entries2,
38398
38674
  ...util_exports.normalizeParams(params)
38399
38675
  });
38400
38676
  }
38401
- function nativeEnum(entries, params) {
38677
+ function nativeEnum(entries2, params) {
38402
38678
  return new ZodEnum({
38403
38679
  type: "enum",
38404
- entries,
38680
+ entries: entries2,
38405
38681
  ...util_exports.normalizeParams(params)
38406
38682
  });
38407
38683
  }
@@ -49178,11 +49454,11 @@ function firstString(...candidates) {
49178
49454
  }
49179
49455
  return void 0;
49180
49456
  }
49181
- function collect(entries, read) {
49182
- if (!Array.isArray(entries)) return [];
49457
+ function collect(entries2, read) {
49458
+ if (!Array.isArray(entries2)) return [];
49183
49459
  const out2 = [];
49184
49460
  const seen = /* @__PURE__ */ new Set();
49185
- for (const entry of entries) {
49461
+ for (const entry of entries2) {
49186
49462
  if (out2.length >= MAX_LOCAL_MODELS) break;
49187
49463
  if (!entry || typeof entry !== "object") continue;
49188
49464
  const { id, label, contextLength } = read(entry);
@@ -50050,98 +50326,6 @@ function renderStatus2(payload, plain, apiUrl) {
50050
50326
  process.stdout.write(brandedBlock("status", lines) + "\n");
50051
50327
  }
50052
50328
 
50053
- // src/commands/open.ts
50054
- import { execFile as execFile3 } from "node:child_process";
50055
- var PLANE_NOTE = {
50056
- workspace: "follows your active workspace",
50057
- cross: "spans every workspace you belong to",
50058
- account: "your account, not a workspace"
50059
- };
50060
- function browserLaunchCommand(url2, platform2 = process.platform) {
50061
- let parsed;
50062
- try {
50063
- parsed = new URL(url2);
50064
- } catch {
50065
- return null;
50066
- }
50067
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
50068
- if (platform2 === "darwin") return { file: "open", args: [url2] };
50069
- if (platform2 === "win32") return { file: "explorer.exe", args: [url2] };
50070
- return { file: "xdg-open", args: [url2] };
50071
- }
50072
- function openBrowser2(url2) {
50073
- const command = browserLaunchCommand(url2);
50074
- if (!command) return Promise.resolve(false);
50075
- return new Promise((resolve7) => {
50076
- execFile3(command.file, command.args, (error62) => resolve7(!error62));
50077
- });
50078
- }
50079
- function toSurfaceRow(value) {
50080
- if (typeof value !== "object" || value === null) return null;
50081
- const r = value;
50082
- if (typeof r.id !== "string" || typeof r.href !== "string") return null;
50083
- if (!r.href.startsWith("/") || r.href.startsWith("//")) return null;
50084
- const plane = r.plane === "workspace" || r.plane === "cross" || r.plane === "account" ? r.plane : "workspace";
50085
- return {
50086
- id: r.id,
50087
- label: typeof r.label === "string" ? r.label : r.id,
50088
- href: r.href,
50089
- plane,
50090
- group: typeof r.group === "string" ? r.group : ""
50091
- };
50092
- }
50093
- function surfaceUrl(apiUrl, href) {
50094
- return `${apiUrl.replace(/\/$/, "")}${href}`;
50095
- }
50096
- function openCommand() {
50097
- return new Command("open").description("Open an app surface in the browser (e.g. gad open library)").argument(
50098
- "<surface>",
50099
- "Surface id, path, label or keyword \u2014 e.g. library, /leads, 'Pitch page'"
50100
- ).option("--print", "Print the URL instead of opening a browser").option("--json", "Output the resolved surface as JSON").action(async (surface, options, command) => {
50101
- const g = globalOutputOpts(command);
50102
- const auth = await resolveAuth();
50103
- const client2 = new GadClient({
50104
- token: auth.token,
50105
- getToken: auth.getToken,
50106
- apiUrl: auth.apiUrl
50107
- });
50108
- const result = await client2.mcpCall("surfaces_list", {
50109
- query: surface
50110
- });
50111
- const match = toSurfaceRow(result?.match);
50112
- const candidates = (Array.isArray(result?.candidates) ? result.candidates : []).map(toSurfaceRow).filter((r) => r !== null);
50113
- if (!match) {
50114
- throw new CliError(
50115
- candidates.length === 0 ? `No app surface matches '${surface}'.` : `'${surface}' matches ${candidates.length} surfaces.`,
50116
- 3 /* VALIDATION_ERROR */,
50117
- candidates.length === 0 ? "Run 'gad open --help', or ask an AI for surfaces_list to see every page." : `Did you mean: ${candidates.map((c2) => c2.id).join(", ")}?`
50118
- );
50119
- }
50120
- const url2 = surfaceUrl(auth.apiUrl, match.href);
50121
- if (g.json) {
50122
- process.stdout.write(JSON.stringify({ ...match, url: url2 }) + "\n");
50123
- return;
50124
- }
50125
- const c = createColor(g.noColor || g.quiet);
50126
- if (options.print) {
50127
- process.stdout.write(url2 + "\n");
50128
- return;
50129
- }
50130
- const opened = await openBrowser2(url2);
50131
- process.stdout.write(
50132
- `${match.label} ${c.dim(`(${PLANE_NOTE[match.plane]})`)}
50133
- ${c.cyan(url2)}
50134
- `
50135
- );
50136
- if (!opened) {
50137
- process.stderr.write(
50138
- `${c.dim("Couldn't launch a browser \u2014 open the URL above.")}
50139
- `
50140
- );
50141
- }
50142
- });
50143
- }
50144
-
50145
50329
  // src/commands/next.ts
50146
50330
  var PICKUP_LIMIT = 10;
50147
50331
  function pickupArgs(identityStatus) {
@@ -50334,7 +50518,7 @@ function callCommand() {
50334
50518
  // src/lib/runner/approval.ts
50335
50519
  import { createHash as createHash6 } from "node:crypto";
50336
50520
  import { chmod as chmod4, mkdir as mkdir9, readFile as readFile8, rename as rename3, writeFile as writeFile7 } from "node:fs/promises";
50337
- import { join as join14 } from "node:path";
50521
+ import { dirname as dirname9, join as join14 } from "node:path";
50338
50522
 
50339
50523
  // src/lib/runner/repo.ts
50340
50524
  import { chmod as chmod3, mkdir as mkdir8, realpath as realpath2, writeFile as writeFile6 } from "node:fs/promises";
@@ -50342,7 +50526,7 @@ import { homedir as homedir10 } from "node:os";
50342
50526
  import { isAbsolute as isAbsolute3, join as join13, relative, resolve as resolve5 } from "node:path";
50343
50527
 
50344
50528
  // src/lib/runner/git.ts
50345
- import { spawn } from "node:child_process";
50529
+ import { spawn as spawn2 } from "node:child_process";
50346
50530
  var GIT_DEADLINE_MS = 12e4;
50347
50531
  var HOST_PATH = "/usr/bin:/bin:/usr/sbin:/sbin";
50348
50532
  var MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
@@ -50386,7 +50570,7 @@ function safeGitBytes(cwd, args, opts = {}) {
50386
50570
  return new Promise((resolve7) => {
50387
50571
  const failed = (stderr) => resolve7({ code: 1, stdout: Buffer.alloc(0), stderr });
50388
50572
  if (opts.signal?.aborted) return failed("aborted");
50389
- const child = spawn("git", argv, {
50573
+ const child = spawn2("git", argv, {
50390
50574
  cwd,
50391
50575
  env: { ...gitEnv(), ...opts.env ?? {} },
50392
50576
  stdio: ["pipe", "pipe", "pipe"],
@@ -50464,11 +50648,12 @@ function worktreeGitBytes(layout, args, opts = {}) {
50464
50648
  }
50465
50649
 
50466
50650
  // src/lib/runner/text.ts
50651
+ var FORMAT = new RegExp("(?!\\u200d)\\p{Cf}", "gu");
50467
50652
  function cleanLine(text3) {
50468
- return text3.replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ").trim();
50653
+ return text3.replace(FORMAT, "").replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ").trim();
50469
50654
  }
50470
50655
  function cleanText(text3) {
50471
- return text3.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f]+/g, " ");
50656
+ return text3.replace(FORMAT, "").replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f]+/g, " ");
50472
50657
  }
50473
50658
  var PREVIEW_LINES = 3;
50474
50659
  var PREVIEW_CHARS = 240;
@@ -50493,8 +50678,9 @@ function preview(text3) {
50493
50678
  }
50494
50679
 
50495
50680
  // src/lib/runner/sandbox.ts
50681
+ import { readdirSync } from "node:fs";
50496
50682
  import { lstat, readFile as readFile7, realpath } from "node:fs/promises";
50497
- import { dirname as dirname8, join as join12, resolve as resolve4 } from "node:path";
50683
+ import { basename as basename2, dirname as dirname8, join as join12, resolve as resolve4 } from "node:path";
50498
50684
  var TASK_FILE = ".gad/task.md";
50499
50685
  var CLAUDE_ALLOWED_DOMAINS = [
50500
50686
  "api.anthropic.com",
@@ -50595,13 +50781,44 @@ var CLI_READ_HARDENING = [
50595
50781
  ".cargo/credentials.toml",
50596
50782
  ".terraform.d/credentials.tfrc.json"
50597
50783
  ];
50598
- function approvalFile(layout) {
50599
- return join12(dirname8(dirname8(layout.worktree)), "approved.json");
50784
+ var CLI_ONLY_READ_HARDENING = ["Library/Keychains"];
50785
+ function runsDirOf(layout) {
50786
+ return dirname8(dirname8(dirname8(layout.worktree)));
50787
+ }
50788
+ function approvalsDir(layout) {
50789
+ return join12(dirname8(runsDirOf(layout)), "approvals");
50790
+ }
50791
+ function entries(dir) {
50792
+ try {
50793
+ return readdirSync(dir);
50794
+ } catch {
50795
+ return [];
50796
+ }
50797
+ }
50798
+ var UUID5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
50799
+ function siblingPaths(layout) {
50800
+ const wtDir = dirname8(layout.worktree);
50801
+ const runDir = dirname8(wtDir);
50802
+ const runsDir = dirname8(runDir);
50803
+ if (basename2(wtDir) !== "wt" || !UUID5.test(basename2(runDir))) return [];
50804
+ const others = (dir, keep) => entries(dir).map((name23) => join12(dir, name23)).filter((path10) => path10 !== keep);
50805
+ return [
50806
+ ...others(runsDir, runDir),
50807
+ ...others(runDir, wtDir),
50808
+ ...others(wtDir, layout.worktree)
50809
+ ];
50810
+ }
50811
+ function cliOnlyDenyRead(layout) {
50812
+ return [
50813
+ ...CLI_ONLY_READ_HARDENING.map((rel) => join12(layout.home, rel)),
50814
+ approvalsDir(layout),
50815
+ ...siblingPaths(layout)
50816
+ ];
50600
50817
  }
50601
50818
  function denyRead(layout) {
50602
50819
  return [
50603
50820
  ...CLI_READ_HARDENING.map((rel) => join12(layout.home, rel)),
50604
- approvalFile(layout)
50821
+ ...cliOnlyDenyRead(layout)
50605
50822
  ];
50606
50823
  }
50607
50824
  var CREDENTIAL_SET = new Set(
@@ -50741,14 +50958,14 @@ function kickoff(runId, todoId) {
50741
50958
  }
50742
50959
 
50743
50960
  // src/lib/runner/repo.ts
50744
- var UUID5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
50961
+ var UUID6 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
50745
50962
  var BRANCH_NAME = /^(?!-)(?!.*\.\.)[A-Za-z0-9._/-]+$/;
50746
50963
  var GITHUB_REMOTE = /^(?:https?:\/\/(?:[^@/]+@)?github\.com\/|ssh:\/\/git@github\.com(?::\d+)?\/|git@github\.com:)([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/;
50747
50964
  function runsRoot() {
50748
50965
  return join13(homedir10(), ".gad", "runs");
50749
50966
  }
50750
50967
  function isRunId(id) {
50751
- return UUID5.test(id);
50968
+ return UUID6.test(id);
50752
50969
  }
50753
50970
  var short = (id) => id.slice(0, 8);
50754
50971
  function runnerBranch(runId, todoId) {
@@ -50859,9 +51076,12 @@ function sha256(text3) {
50859
51076
  return createHash6("sha256").update(text3, "utf8").digest("hex");
50860
51077
  }
50861
51078
  var plainTitle = cleanLine;
51079
+ function approvalsDir2(runsDir) {
51080
+ return join14(dirname9(runsDir), "approvals");
51081
+ }
50862
51082
  function approvedFile(runsDir, runId) {
50863
51083
  if (!isRunId(runId)) throw new Error("run id must be a UUID");
50864
- return join14(runsDir, runId, "approved.json");
51084
+ return join14(approvalsDir2(runsDir), `${runId}.json`);
50865
51085
  }
50866
51086
  function approveShown(cards) {
50867
51087
  return new Map(
@@ -50873,9 +51093,8 @@ function approveShown(cards) {
50873
51093
  }
50874
51094
  async function saveApproved(runsDir, runId, approved) {
50875
51095
  const file2 = approvedFile(runsDir, runId);
50876
- const dir = join14(runsDir, runId);
51096
+ const dir = approvalsDir2(runsDir);
50877
51097
  await mkdir9(dir, { recursive: true, mode: 448 });
50878
- await chmod4(runsDir, 448);
50879
51098
  await chmod4(dir, 448);
50880
51099
  const body = [...approved].map(([todoId, c]) => ({ todoId, ...c }));
50881
51100
  const tmp = `${file2}.tmp`;
@@ -50977,13 +51196,19 @@ async function workCard(deps, todoId, card) {
50977
51196
  if (ran.exit !== 0)
50978
51197
  return { outcome: "failed", detail: `agent_exit_${ran.exit ?? "signal"}` };
50979
51198
  const result = await deps.publish(prepared, todoId, card.title, deps.signal).catch(() => ({ failed: "publish_failed" }));
50980
- if (deps.signal.aborted) return { outcome: "stopped" };
51199
+ const takeBack = (sha) => () => deps.unpublish(prepared, sha).catch(() => false);
51200
+ if (deps.signal.aborted) {
51201
+ if (typeof result === "object" && "pushed" in result)
51202
+ await sayTakenBack(deps, prepared.branch, takeBack(result.pushed));
51203
+ return { outcome: "stopped" };
51204
+ }
50981
51205
  if (result === "no_changes") return { outcome: "no_changes" };
50982
51206
  if ("pushed" in result)
50983
51207
  return {
50984
51208
  outcome: "pushed",
50985
51209
  branch: prepared.branch,
50986
- headSha: result.pushed
51210
+ headSha: result.pushed,
51211
+ takeBack: takeBack(result.pushed)
50987
51212
  };
50988
51213
  return {
50989
51214
  outcome: "failed",
@@ -50993,6 +51218,12 @@ async function workCard(deps, todoId, card) {
50993
51218
  } : {}
50994
51219
  };
50995
51220
  }
51221
+ async function sayTakenBack(deps, branch, takeBack) {
51222
+ if (!await takeBack())
51223
+ deps.print(
51224
+ ` Could not delete the pushed branch ${branch}; delete it on GitHub if you do not want it.`
51225
+ );
51226
+ }
50996
51227
  function reportFields(o) {
50997
51228
  if (o.outcome === "pushed")
50998
51229
  return { outcome: o.outcome, branch: o.branch, head_sha: o.headSha };
@@ -51105,6 +51336,8 @@ async function runLoop(deps, input2) {
51105
51336
  if (!(error62 instanceof RunnerCallError && error62.code === "card_released"))
51106
51337
  throw error62;
51107
51338
  deps.print(`${label} \u2026 taken back on the board`);
51339
+ if (outcome.outcome === "pushed")
51340
+ await sayTakenBack(deps, outcome.branch, outcome.takeBack);
51108
51341
  continue;
51109
51342
  } finally {
51110
51343
  if (claimed) await release(deps, next.todoId);
@@ -51327,7 +51560,7 @@ import { join as join17 } from "node:path";
51327
51560
  import { createInterface as createInterface3 } from "node:readline";
51328
51561
 
51329
51562
  // src/lib/runner/agent.ts
51330
- import { execFile as execFile4, spawn as spawn2 } from "node:child_process";
51563
+ import { execFile as execFile4, spawn as spawn3 } from "node:child_process";
51331
51564
  import { accessSync, chmodSync as chmodSync3, closeSync, constants as constants2, openSync } from "node:fs";
51332
51565
  import { delimiter, join as join15 } from "node:path";
51333
51566
  var KILL_GRACE_MS = 5e3;
@@ -51395,7 +51628,7 @@ async function runAgent(input2) {
51395
51628
  const env4 = { ...stripCredentialEnv(process.env), GIT_TERMINAL_PROMPT: "0" };
51396
51629
  let child;
51397
51630
  try {
51398
- child = spawn2(input2.bin, agentArgv(input2), {
51631
+ child = spawn3(input2.bin, agentArgv(input2), {
51399
51632
  cwd: input2.layout.worktree,
51400
51633
  detached: true,
51401
51634
  stdio: ["ignore", fd, fd],
@@ -51466,7 +51699,7 @@ async function reapGroup(pgid, waitMs = REAP_WAIT_MS) {
51466
51699
 
51467
51700
  // src/lib/runner/holders.ts
51468
51701
  import { execFile as execFile5 } from "node:child_process";
51469
- import { readdirSync, readlinkSync, statSync as statSync2 } from "node:fs";
51702
+ import { readdirSync as readdirSync2, readlinkSync, statSync as statSync2 } from "node:fs";
51470
51703
  var LSOF_DEADLINE_MS = 2e4;
51471
51704
  var CHECKS = 5;
51472
51705
  var PAUSE_MS = 250;
@@ -51579,15 +51812,15 @@ async function lsofUsers(root, uid, exec2 = run3) {
51579
51812
  return users;
51580
51813
  }
51581
51814
  var errno = (error62) => error62.code;
51582
- function procUsers(root, uid, fs6 = { readdirSync, statSync: statSync2, readlinkSync }) {
51583
- let entries;
51815
+ function procUsers(root, uid, fs6 = { readdirSync: readdirSync2, statSync: statSync2, readlinkSync }) {
51816
+ let entries2;
51584
51817
  try {
51585
- entries = fs6.readdirSync("/proc").filter((e) => /^\d+$/.test(e));
51818
+ entries2 = fs6.readdirSync("/proc").filter((e) => /^\d+$/.test(e));
51586
51819
  } catch {
51587
51820
  return null;
51588
51821
  }
51589
51822
  const users = [];
51590
- for (const pid of entries) {
51823
+ for (const pid of entries2) {
51591
51824
  try {
51592
51825
  if (fs6.statSync(`/proc/${pid}`).uid !== uid) continue;
51593
51826
  } catch (error62) {
@@ -52093,6 +52326,17 @@ async function publishBranch(layout, branch, base, message, signal, options = {}
52093
52326
  throw error62;
52094
52327
  }
52095
52328
  }
52329
+ async function unpublishBranch(layout, branch, sha) {
52330
+ if (!RUNNER_BRANCH.test(branch) || !SHA.test(sha)) return false;
52331
+ const push = await worktreeGit(layout, [
52332
+ "push",
52333
+ "--quiet",
52334
+ `--force-with-lease=refs/heads/${branch}:${sha}`,
52335
+ "origin",
52336
+ `:refs/heads/${branch}`
52337
+ ]);
52338
+ return push.code === 0;
52339
+ }
52096
52340
 
52097
52341
  // src/commands/board-run.ts
52098
52342
  function parseData(result) {
@@ -52199,6 +52443,7 @@ var work = ({
52199
52443
  commitMessage(title, view.runId, todoId),
52200
52444
  signal
52201
52445
  ),
52446
+ unpublish: (prepared, headSha2) => unpublishBranch(prepared, prepared.branch, headSha2),
52202
52447
  sleep: sleep2,
52203
52448
  note: (todoId, line) => {
52204
52449
  try {
@@ -52610,7 +52855,7 @@ import { createInterface as createInterface4 } from "node:readline";
52610
52855
  import { Writable } from "node:stream";
52611
52856
 
52612
52857
  // src/lib/keys-apply.ts
52613
- import { spawn as spawn3 } from "node:child_process";
52858
+ import { spawn as spawn4 } from "node:child_process";
52614
52859
  import {
52615
52860
  chmod as chmod6,
52616
52861
  mkdir as mkdir10,
@@ -52621,7 +52866,7 @@ import {
52621
52866
  writeFile as writeFile9
52622
52867
  } from "node:fs/promises";
52623
52868
  import { homedir as homedir11 } from "node:os";
52624
- import { basename as basename2, dirname as dirname9, isAbsolute as isAbsolute4, join as join18, relative as relative2, sep } from "node:path";
52869
+ import { basename as basename3, dirname as dirname10, isAbsolute as isAbsolute4, join as join18, relative as relative2, sep } from "node:path";
52625
52870
  var GH_APPLY_TIMEOUT_MS = 3e4;
52626
52871
  var GIT_APPLY_TIMEOUT_MS = 3e4;
52627
52872
  function runWithStdin(file2, args, input2, timeoutMs) {
@@ -52635,7 +52880,7 @@ function runWithStdin(file2, args, input2, timeoutMs) {
52635
52880
  };
52636
52881
  let child;
52637
52882
  try {
52638
- child = spawn3(file2, args, { stdio: ["pipe", "ignore", "ignore"] });
52883
+ child = spawn4(file2, args, { stdio: ["pipe", "ignore", "ignore"] });
52639
52884
  } catch {
52640
52885
  resolve7("not_found");
52641
52886
  return;
@@ -52778,7 +53023,7 @@ async function applySshKey(path10, privatePem) {
52778
53023
  const content = privatePem.endsWith("\n") ? privatePem : `${privatePem}
52779
53024
  `;
52780
53025
  try {
52781
- await mkdir10(dirname9(file2), { recursive: true, mode: 448 });
53026
+ await mkdir10(dirname10(file2), { recursive: true, mode: 448 });
52782
53027
  await writeAtomic(file2, content, 384);
52783
53028
  } catch {
52784
53029
  return { ok: false, code: "ssh_write_failed" };
@@ -52799,14 +53044,14 @@ var SSH_NOT_A_KEY = /* @__PURE__ */ new Set([
52799
53044
  async function sshTargetRefusal(path10, sshDir = join18(homedir11(), ".ssh")) {
52800
53045
  const file2 = expandHome(path10);
52801
53046
  if (!isAbsolute4(file2)) return { ok: false, code: "target_invalid" };
52802
- if (SSH_NOT_A_KEY.has(basename2(file2))) {
53047
+ if (SSH_NOT_A_KEY.has(basename3(file2))) {
52803
53048
  return { ok: false, code: "target_not_a_key" };
52804
53049
  }
52805
53050
  let root;
52806
53051
  let parent;
52807
53052
  try {
52808
53053
  root = await realpath3(sshDir);
52809
- parent = await realpath3(dirname9(file2));
53054
+ parent = await realpath3(dirname10(file2));
52810
53055
  } catch {
52811
53056
  return { ok: false, code: "target_outside_ssh_dir" };
52812
53057
  }
@@ -52953,111 +53198,6 @@ function openFromServer(sealed, devicePrivateKeyB64) {
52953
53198
  }
52954
53199
  }
52955
53200
 
52956
- // src/lib/keys-handoff.ts
52957
- import { spawn as spawn4 } from "node:child_process";
52958
-
52959
- // src/lib/keys-paths.ts
52960
- function keyRotationPath() {
52961
- return "/settings/key-rotation";
52962
- }
52963
- function keyRotationRunPath(runId) {
52964
- return `/settings/key-rotation/runs/${encodeURIComponent(runId)}`;
52965
- }
52966
- function keyRotationHandoffPath(jti) {
52967
- return `/settings/key-rotation/handoff/${encodeURIComponent(jti)}`;
52968
- }
52969
-
52970
- // src/lib/keys-handoff.ts
52971
- var HANDOFF_POLL_INTERVAL_MS = 2e3;
52972
- var HANDOFF_DEADLINE_MS = 5 * 6e4;
52973
- var BROWSER_LAUNCH_TIMEOUT_MS = 5e3;
52974
- function openInBrowser(url2) {
52975
- const command = browserLaunchCommand(url2);
52976
- if (!command) return Promise.resolve(false);
52977
- return new Promise((resolve7) => {
52978
- let settled = false;
52979
- const done = (ok2) => {
52980
- if (settled) return;
52981
- settled = true;
52982
- clearTimeout(timer);
52983
- resolve7(ok2);
52984
- };
52985
- const timer = setTimeout(() => done(false), BROWSER_LAUNCH_TIMEOUT_MS);
52986
- timer.unref?.();
52987
- try {
52988
- const child = spawn4(command.file, command.args, {
52989
- detached: true,
52990
- stdio: "ignore"
52991
- });
52992
- child.once("error", () => done(false));
52993
- child.once("spawn", () => {
52994
- child.unref();
52995
- done(true);
52996
- });
52997
- } catch {
52998
- done(false);
52999
- }
53000
- });
53001
- }
53002
- var defaultSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
53003
- function stepUpError(message, hint) {
53004
- return new CliError(message, 1 /* AUTH_ERROR */, hint);
53005
- }
53006
- async function requestBrowserStepUp(client2, kind, subjectId, opts = {}) {
53007
- const openUrl = opts.openUrl ?? openInBrowser;
53008
- const notify = opts.notify ?? ((line) => process.stderr.write(`${line}
53009
- `));
53010
- const sleep3 = opts.sleep ?? defaultSleep;
53011
- const now = opts.now ?? Date.now;
53012
- const interval = opts.pollIntervalMs ?? HANDOFF_POLL_INTERVAL_MS;
53013
- const deadline = now() + (opts.deadlineMs ?? HANDOFF_DEADLINE_MS);
53014
- const created = await client2.post("/api/key-rotation/handoffs", {
53015
- kind,
53016
- subjectId
53017
- });
53018
- const jti = typeof created?.jti === "string" ? created.jti : null;
53019
- if (!jti || !/^[A-Za-z0-9-]{1,64}$/.test(jti)) {
53020
- throw stepUpError("The server did not return a browser approval.");
53021
- }
53022
- const url2 = `${client2.apiOrigin}${keyRotationHandoffPath(jti)}`;
53023
- notify(`Approve this in your browser (passkey or two-step code): ${url2}`);
53024
- if (!await openUrl(url2)) {
53025
- notify("Couldn't launch a browser. Open the URL above to approve.");
53026
- }
53027
- while (now() < deadline) {
53028
- const polled = await client2.get(
53029
- `/api/key-rotation/handoffs/${encodeURIComponent(jti)}`
53030
- );
53031
- const status = polled?.status;
53032
- if (status === "approved") {
53033
- if (typeof polled.token === "string" && polled.token.length > 0) {
53034
- return polled.token;
53035
- }
53036
- throw stepUpError(
53037
- "This approval was already collected by another request.",
53038
- "Run the command again to request a new approval."
53039
- );
53040
- }
53041
- if (status === "consumed") {
53042
- throw stepUpError(
53043
- "This approval was already used.",
53044
- "Run the command again to request a new approval."
53045
- );
53046
- }
53047
- if (status === "expired") {
53048
- throw stepUpError(
53049
- "The browser approval expired.",
53050
- "Run the command again and approve within 5 minutes."
53051
- );
53052
- }
53053
- await sleep3(interval);
53054
- }
53055
- throw stepUpError(
53056
- "Timed out waiting for the browser approval.",
53057
- "Run the command again and approve within 5 minutes."
53058
- );
53059
- }
53060
-
53061
53201
  // src/lib/keys-device.ts
53062
53202
  var DEFAULT_ENV_FILE = ".env.local";
53063
53203
  var DETECT_TIMEOUT_MS = 5e3;
@@ -53066,7 +53206,7 @@ var REPORT_KEY = /^[A-Z_][A-Z0-9_]{0,127}$/;
53066
53206
  var REPORT_PATH = /^[ -~]{1,200}$/;
53067
53207
  var TOKEN_SHAPED = /[A-Za-z0-9]{33,}/;
53068
53208
  var SECRET_PREFIX = /(gh[pousr]_|github_pat_|-----BEGIN)/;
53069
- var UUID6 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
53209
+ var UUID7 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
53070
53210
  function deviceSlot(workspaceId) {
53071
53211
  return `key-rotation-device:${workspaceId}`;
53072
53212
  }
@@ -53244,7 +53384,7 @@ async function enroll(client2, workspaceId, opts = {}) {
53244
53384
  publicKey: pair.publicKey,
53245
53385
  report
53246
53386
  });
53247
- const enrollmentId = typeof begun?.enrollmentId === "string" && UUID6.test(begun.enrollmentId) ? begun.enrollmentId : null;
53387
+ const enrollmentId = typeof begun?.enrollmentId === "string" && UUID7.test(begun.enrollmentId) ? begun.enrollmentId : null;
53248
53388
  if (!enrollmentId) {
53249
53389
  throw new CliError(
53250
53390
  "The server did not start the enrollment.",
@@ -53262,7 +53402,7 @@ async function enroll(client2, workspaceId, opts = {}) {
53262
53402
  action: "enroll",
53263
53403
  approvalToken
53264
53404
  });
53265
- const deviceId = typeof done?.deviceId === "string" && UUID6.test(done.deviceId) ? done.deviceId : null;
53405
+ const deviceId = typeof done?.deviceId === "string" && UUID7.test(done.deviceId) ? done.deviceId : null;
53266
53406
  if (!deviceId) {
53267
53407
  throw new CliError(
53268
53408
  "The server did not complete the enrollment.",
@@ -53291,7 +53431,7 @@ async function enroll(client2, workspaceId, opts = {}) {
53291
53431
  }
53292
53432
 
53293
53433
  // src/commands/keys.ts
53294
- var UUID7 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
53434
+ var UUID8 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
53295
53435
  function patRequired(apiUrl) {
53296
53436
  return authError(
53297
53437
  "gad keys needs a personal access token; this sign-in is an OAuth session.",
@@ -53344,7 +53484,7 @@ async function keysContext() {
53344
53484
  });
53345
53485
  const value = await probe.mcpCall("workspace_introspect", {}, 5e3);
53346
53486
  const workspaceId = value?.workspace?.id;
53347
- if (value?.error || !workspaceId || !UUID7.test(workspaceId)) {
53487
+ if (value?.error || !workspaceId || !UUID8.test(workspaceId)) {
53348
53488
  throw validationError(
53349
53489
  "GreatArrow.ai could not confirm your workspace.",
53350
53490
  "Run 'gad whoami', or choose one with 'gad --workspace <id>'."
@@ -53365,7 +53505,7 @@ async function keysContext() {
53365
53505
  };
53366
53506
  }
53367
53507
  function requireRunId(runId) {
53368
- if (!UUID7.test(runId)) {
53508
+ if (!UUID8.test(runId)) {
53369
53509
  throw validationError(
53370
53510
  `'${stripControlChars(runId)}' is not a run id.`,
53371
53511
  "Copy the run id from 'gad keys list' or the run page."
@@ -53494,7 +53634,7 @@ function enrollCommand() {
53494
53634
  function toUpdateRow(v) {
53495
53635
  if (typeof v !== "object" || v === null) return null;
53496
53636
  const r = v;
53497
- if (typeof r.id !== "string" || !UUID7.test(r.id)) return null;
53637
+ if (typeof r.id !== "string" || !UUID8.test(r.id)) return null;
53498
53638
  const ref = {};
53499
53639
  if (typeof r.targetRef === "object" && r.targetRef !== null) {
53500
53640
  for (const [k, val] of Object.entries(r.targetRef)) {
@@ -54381,14 +54521,14 @@ var Temp = {
54381
54521
  }
54382
54522
  },
54383
54523
  truncate: (filePath) => {
54384
- const basename3 = path3.basename(filePath);
54385
- if (basename3.length <= LIMIT_BASENAME_LENGTH)
54524
+ const basename4 = path3.basename(filePath);
54525
+ if (basename4.length <= LIMIT_BASENAME_LENGTH)
54386
54526
  return filePath;
54387
- const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename3);
54527
+ const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename4);
54388
54528
  if (!truncable)
54389
54529
  return filePath;
54390
- const truncationLength = basename3.length - LIMIT_BASENAME_LENGTH;
54391
- return `${filePath.slice(0, -basename3.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
54530
+ const truncationLength = basename4.length - LIMIT_BASENAME_LENGTH;
54531
+ return `${filePath.slice(0, -basename4.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
54392
54532
  }
54393
54533
  };
54394
54534
  node_default(Temp.purgeSyncAll);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@great-arrow/cli",
3
- "version": "0.9.3",
3
+ "version": "0.9.6",
4
4
  "description": "GreatArrow.ai CLI tool",
5
5
  "type": "module",
6
6
  "bin": {