@tryarcanist/cli 0.1.132 → 0.1.134

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +15 -1
  2. package/dist/index.js +241 -57
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -131,7 +131,7 @@ arcanist sessions create your-org/your-repo "retry-safe create" --idempotency-ke
131
131
 
132
132
  `--backend` picks the agent runtime backend: `codex` (default) or `claude_code`. `--model` must be valid for the chosen backend — codex runs OpenAI models (`gpt-5.4` default, `gpt-5.5`), `claude_code` runs Anthropic models (`claude-opus-4-8` default, `claude-sonnet-4-6`) — and the CLI rejects a mismatch before any network call. `claude_code` sessions require an Anthropic API key configured in Settings.
133
133
 
134
- `--wait` blocks until the created prompt finishes and exits non-zero if the prompt finishes with `status: failed`, making it suitable for cron or other schedulers that alert on command failure. `--poll-interval <ms>` tunes the completion check frequency.
134
+ `--wait` blocks until the created prompt finishes, prints the resulting PR/branch line in human-readable mode when it is already available, and exits non-zero if the prompt finishes with `status: failed`, making it suitable for cron or other schedulers that alert on command failure. `--poll-interval <ms>` tunes the completion check frequency.
135
135
 
136
136
  Use `--base-branch <branch>` to target a non-default base branch. The CLI only validates that the branch value is non-empty; branch existence is checked later by the session runtime.
137
137
 
@@ -176,6 +176,8 @@ arcanist sessions get abc123
176
176
  arcanist sessions get abc123 --json
177
177
  ```
178
178
 
179
+ Human-readable output includes `PR: <url> (<branch>)` when the session has opened a PR, or `Branch: <branch>` when a produced branch is known but no PR URL is present yet. If publishing is still settling, rerun `sessions get`; JSON mode returns the raw session payload, with result fields at `.session.prUrl`, `.session.publishedBranch`, and `.session.lastBranch`.
180
+
179
181
  ### `arcanist sessions list`
180
182
 
181
183
  ```bash
@@ -324,6 +326,18 @@ arcanist sessions create your-org/your-repo "verify the new rate limiter is acti
324
326
 
325
327
  For cron, prefer `ARCANIST_TOKEN` or a logged-in `~/.arcanist/config.json` over passing `--token` on the command line.
326
328
 
329
+ To capture the PR URL after a successful run, use `create --wait` for the exit code, then read the session state:
330
+
331
+ ```bash
332
+ SESSION_ID=$(arcanist sessions create your-org/your-repo "fix the flaky test" --wait --json | jq -r .sessionId)
333
+ for _ in {1..20}; do
334
+ PR_URL=$(arcanist sessions get "$SESSION_ID" --json | jq -r '.session.prUrl // empty')
335
+ [ -n "$PR_URL" ] && break
336
+ sleep 5
337
+ done
338
+ [ -n "$PR_URL" ] || { echo "PR URL not ready for $SESSION_ID" >&2; exit 1; }
339
+ ```
340
+
327
341
  ## Troubleshooting
328
342
 
329
343
  - **401 on login verification**: token may be invalid or expired. Regenerate in **Settings > CLI Tokens**.
package/dist/index.js CHANGED
@@ -1023,22 +1023,6 @@ function noChangeOutcomeCopy(reason) {
1023
1023
  }
1024
1024
  }
1025
1025
 
1026
- // ../../shared/session/transient-disconnect.ts
1027
- var TRANSIENT_DISCONNECT_VISIBILITY_MS = 15e3;
1028
- function nextDisconnectMask(mask, previous, current, now) {
1029
- if (!current || current.sandboxSubstate !== "reconnecting") return null;
1030
- if (mask) return mask;
1031
- if (!previous || previous.sandboxSubstate === "reconnecting") return null;
1032
- return { since: now, heldPhase: previous.phase, heldSubstate: previous.sandboxSubstate ?? "none" };
1033
- }
1034
- function isDisconnectMaskActive(mask, now) {
1035
- return mask !== null && now - mask.since < TRANSIENT_DISCONNECT_VISIBILITY_MS;
1036
- }
1037
- function applyDisconnectMask(view, mask, now) {
1038
- if (!isDisconnectMaskActive(mask, now)) return view;
1039
- return { ...view, phase: mask.heldPhase, sandboxSubstate: mask.heldSubstate };
1040
- }
1041
-
1042
1026
  // ../../shared/transcript/malformed-search.ts
1043
1027
  var MALFORMED_SEARCH_BLOCKED_TRANSCRIPT_PREFIX = "Arcanist blocked this malformed search command before execution.";
1044
1028
  var BASH_UNMATCHED_QUOTE_EOF_RE = /\/bin\/bash:\s+-c:\s+line\s+\d+:\s+unexpected EOF while looking for matching [`'"][`'"]?/i;
@@ -2063,6 +2047,23 @@ ${event.answer ? `**Answer:** ${event.answer}
2063
2047
  function formatNumber(value) {
2064
2048
  return value.toLocaleString();
2065
2049
  }
2050
+ function stringField(value) {
2051
+ return typeof value === "string" && value.trim() ? value : null;
2052
+ }
2053
+ function formatSessionResult(session) {
2054
+ const prUrl = stringField(session.prUrl);
2055
+ const publishedBranch = stringField(session.publishedBranch);
2056
+ const lastBranch = stringField(session.lastBranch);
2057
+ const baseBranch = stringField(session.baseBranch);
2058
+ const branch = publishedBranch ?? (lastBranch && lastBranch !== baseBranch ? lastBranch : null);
2059
+ if (prUrl) {
2060
+ return `PR: ${prUrl}${branch ? ` (${branch})` : ""}`;
2061
+ }
2062
+ if (branch) {
2063
+ return `Branch: ${branch}`;
2064
+ }
2065
+ return null;
2066
+ }
2066
2067
  function latestCompletedPromptResult(prompts) {
2067
2068
  for (let i = prompts.length - 1; i >= 0; i--) {
2068
2069
  if (prompts[i].status === "completed") return prompts[i].result;
@@ -2314,6 +2315,22 @@ function renderWatchEvent(event, state) {
2314
2315
  }
2315
2316
  }
2316
2317
 
2318
+ // ../../shared/session/transient-disconnect.ts
2319
+ var TRANSIENT_DISCONNECT_VISIBILITY_MS = 15e3;
2320
+ function nextDisconnectMask(mask, previous, current, now) {
2321
+ if (!current || current.sandboxSubstate !== "reconnecting") return null;
2322
+ if (mask) return mask;
2323
+ if (!previous || previous.sandboxSubstate === "reconnecting") return null;
2324
+ return { since: now, heldPhase: previous.phase, heldSubstate: previous.sandboxSubstate ?? "none" };
2325
+ }
2326
+ function isDisconnectMaskActive(mask, now) {
2327
+ return mask !== null && now - mask.since < TRANSIENT_DISCONNECT_VISIBILITY_MS;
2328
+ }
2329
+ function applyDisconnectMask(view, mask, now) {
2330
+ if (!isDisconnectMaskActive(mask, now)) return view;
2331
+ return { ...view, phase: mask.heldPhase, sandboxSubstate: mask.heldSubstate };
2332
+ }
2333
+
2317
2334
  // src/commands/watch.ts
2318
2335
  function parsePollInterval(raw) {
2319
2336
  if (!raw) return DEFAULT_WATCH_POLL_INTERVAL_MS;
@@ -2621,6 +2638,15 @@ async function createCommand(repoUrl, promptArg, options, command) {
2621
2638
  if (sessionData.sessionUrl) console.log(`URL: ${sessionData.sessionUrl}`);
2622
2639
  }
2623
2640
  await waitForCreatedPrompt(sessionId, promptId, sessionData.sessionUrl, waitPollIntervalMs, runtime, command);
2641
+ if (!isJson(command, options)) {
2642
+ try {
2643
+ const sessionPayload = await apiFetch(config, `/api/sessions/${sessionId}`);
2644
+ const sessionState = sessionPayload.session && typeof sessionPayload.session === "object" ? sessionPayload.session : sessionPayload;
2645
+ const resultLine = formatSessionResult(sessionState);
2646
+ if (resultLine) console.log(resultLine);
2647
+ } catch {
2648
+ }
2649
+ }
2624
2650
  }
2625
2651
  if (isJson(command, options)) {
2626
2652
  const output = { sessionId };
@@ -2640,6 +2666,155 @@ async function createCommand(repoUrl, promptArg, options, command) {
2640
2666
  if (sessionData.sessionUrl) console.log(`URL: ${sessionData.sessionUrl}`);
2641
2667
  }
2642
2668
 
2669
+ // src/commands/egress.ts
2670
+ import { readFileSync as readFileSync2 } from "fs";
2671
+
2672
+ // ../../shared/types/business-egress-policy.ts
2673
+ var MAX_BUSINESS_EGRESS_DOMAINS = 100;
2674
+ var MAX_DOMAIN_LENGTH = 253;
2675
+ function normalizeEgressDomains(entries, key = "egressAllowlist") {
2676
+ const unique = [...new Set(entries.map((entry) => entry.trim().toLowerCase()).filter(Boolean))].sort();
2677
+ if (unique.length > MAX_BUSINESS_EGRESS_DOMAINS) {
2678
+ throw new Error(`${key} must contain at most ${MAX_BUSINESS_EGRESS_DOMAINS} domain names`);
2679
+ }
2680
+ for (const entry of unique) {
2681
+ assertValidEgressDomain(entry, key);
2682
+ }
2683
+ return unique;
2684
+ }
2685
+ function assertValidEgressDomain(value, key = "egressAllowlist") {
2686
+ if (value.includes("*") || value.startsWith(".") || value.endsWith(".") || value.includes("/") || value.includes(":")) {
2687
+ throw new Error(`${key} entries must be exact domain names`);
2688
+ }
2689
+ if (value.length > MAX_DOMAIN_LENGTH) {
2690
+ throw new Error(`${key} entries must be exact domain names`);
2691
+ }
2692
+ const labels = value.split(".");
2693
+ if (labels.length === 4 && labels.every((label) => /^\d+$/.test(label) && Number(label) >= 0 && Number(label) <= 255)) {
2694
+ throw new Error(`${key} entries must be exact domain names`);
2695
+ }
2696
+ if (labels.length < 2 || labels.some(
2697
+ (label) => label.length < 1 || label.length > 63 || !/^[a-z0-9-]+$/.test(label) || label.startsWith("-") || label.endsWith("-")
2698
+ )) {
2699
+ throw new Error(`${key} entries must be exact domain names`);
2700
+ }
2701
+ }
2702
+
2703
+ // ../../shared/egress-allowlist/parser.ts
2704
+ var EGRESS_ALLOWLIST_SOURCE_PATH = ".arcanist/egress-allowlist.txt";
2705
+ function parseEgressAllowlistSourceFile(content, key = EGRESS_ALLOWLIST_SOURCE_PATH) {
2706
+ const domains = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
2707
+ return normalizeEgressDomains(domains, key);
2708
+ }
2709
+
2710
+ // src/commands/egress.ts
2711
+ function parseRepoArg(value) {
2712
+ const trimmed = value.trim().replace(/^https:\/\/github\.com\//, "").replace(/\.git$/, "");
2713
+ const [owner, repo, extra] = trimmed.split("/");
2714
+ if (!owner || !repo || extra) throw new CliError("user", "repo must be owner/name or a GitHub URL");
2715
+ return { owner, repo };
2716
+ }
2717
+ function encodePathSegment(value) {
2718
+ return encodeURIComponent(value);
2719
+ }
2720
+ async function resolveBusinessId(config, options) {
2721
+ if (options.business && options.business.trim()) return options.business.trim();
2722
+ const whoami = await apiFetch(config, "/api/auth/whoami");
2723
+ if (whoami.businessId && whoami.businessId.trim()) return whoami.businessId;
2724
+ throw new CliError("user", "--business is required when the authenticated token has no business context.");
2725
+ }
2726
+ async function egressSourceSetCommand(sourceRepoArg, options = {}, command) {
2727
+ const runtime = getRuntimeOptions(command, options);
2728
+ const config = requireConfig(runtime);
2729
+ const businessId = await resolveBusinessId(config, options);
2730
+ const source = parseRepoArg(sourceRepoArg);
2731
+ const payload = await apiFetch(
2732
+ config,
2733
+ `/api/businesses/${encodePathSegment(businessId)}/egress-allowlist/source`,
2734
+ {
2735
+ method: "PUT",
2736
+ body: JSON.stringify({ sourceRepoOwner: source.owner, sourceRepoName: source.repo })
2737
+ }
2738
+ );
2739
+ if (isJson(command, options)) {
2740
+ writeJson(payload);
2741
+ return;
2742
+ }
2743
+ console.log(`Set egress allowlist source to ${source.owner}/${source.repo}:${EGRESS_ALLOWLIST_SOURCE_PATH}.`);
2744
+ }
2745
+ async function egressSourceGetCommand(options = {}, command) {
2746
+ const runtime = getRuntimeOptions(command, options);
2747
+ const config = requireConfig(runtime);
2748
+ const businessId = await resolveBusinessId(config, options);
2749
+ const payload = await apiFetch(
2750
+ config,
2751
+ `/api/businesses/${encodePathSegment(businessId)}/egress-allowlist/source`
2752
+ );
2753
+ if (isJson(command, options)) {
2754
+ writeJson(payload);
2755
+ return;
2756
+ }
2757
+ if (!payload.source) {
2758
+ console.log("No egress allowlist source configured.");
2759
+ return;
2760
+ }
2761
+ console.log(
2762
+ `${payload.source.sourceRepoOwner}/${payload.source.sourceRepoName}:${payload.path} (${payload.defaultBranch ?? "default branch"})`
2763
+ );
2764
+ console.log(`${payload.domains.length} ${payload.domains.length === 1 ? "domain" : "domains"}.`);
2765
+ }
2766
+ async function egressAddCommand(domains, options = {}, command) {
2767
+ const runtime = getRuntimeOptions(command, options);
2768
+ const config = requireConfig(runtime);
2769
+ const businessId = await resolveBusinessId(config, options);
2770
+ const normalized = normalizeEgressDomains(domains, "domains");
2771
+ const payload = await apiFetch(
2772
+ config,
2773
+ `/api/businesses/${encodePathSegment(businessId)}/egress-allowlist/pull-request`,
2774
+ {
2775
+ method: "POST",
2776
+ body: JSON.stringify({ domains: normalized, ...options.reason ? { reason: options.reason } : {} })
2777
+ }
2778
+ );
2779
+ if (isJson(command, options)) {
2780
+ writeJson(payload);
2781
+ return;
2782
+ }
2783
+ if (payload.status === "unchanged") {
2784
+ console.log("All requested domains are already present in the source file.");
2785
+ return;
2786
+ }
2787
+ console.log(`Egress allowlist PR ${payload.status}: ${payload.prUrl}`);
2788
+ console.log(`Added: ${payload.addedDomains.join(", ")}`);
2789
+ }
2790
+ async function egressSyncCommand(options = {}, command) {
2791
+ const runtime = getRuntimeOptions(command, options);
2792
+ const config = requireConfig(runtime);
2793
+ const businessId = await resolveBusinessId(config, options);
2794
+ const payload = await apiFetch(
2795
+ config,
2796
+ `/api/businesses/${encodePathSegment(businessId)}/egress-allowlist/sync`,
2797
+ { method: "POST" }
2798
+ );
2799
+ if (isJson(command, options)) {
2800
+ writeJson(payload);
2801
+ return;
2802
+ }
2803
+ console.log(
2804
+ `Applied ${payload.egressAllowlist.length} egress allowlist domains from ${payload.source.sourceRepoOwner}/${payload.source.sourceRepoName}:${payload.path}.`
2805
+ );
2806
+ }
2807
+ async function egressValidateCommand(path = EGRESS_ALLOWLIST_SOURCE_PATH, options = {}, command) {
2808
+ const content = readFileSync2(path, "utf8");
2809
+ const domains = parseEgressAllowlistSourceFile(content, path);
2810
+ if (isJson(command, options)) {
2811
+ writeJson({ ok: true, path, domains });
2812
+ return;
2813
+ }
2814
+ console.log(`Valid egress allowlist: ${path}`);
2815
+ console.log(`${domains.length} ${domains.length === 1 ? "domain" : "domains"}.`);
2816
+ }
2817
+
2643
2818
  // src/commands/login.ts
2644
2819
  async function loginCommand(options, command) {
2645
2820
  const runtime = getRuntimeOptions(command, options);
@@ -2709,7 +2884,7 @@ async function messageCommand(sessionId, promptArg, options = {}, command) {
2709
2884
 
2710
2885
  // src/commands/sandbox.ts
2711
2886
  import { execFileSync } from "child_process";
2712
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2887
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2713
2888
  import { dirname } from "path";
2714
2889
 
2715
2890
  // ../../shared/sandbox-layer/parser.ts
@@ -3270,7 +3445,7 @@ function assertCleanAndPushed() {
3270
3445
  function assertManifestExists(path) {
3271
3446
  if (!existsSync(path)) throw new CliError("user", `Missing sandbox manifest: ${path}`);
3272
3447
  }
3273
- function parseRepoArg(value, fallback) {
3448
+ function parseRepoArg2(value, fallback) {
3274
3449
  if (!value) return fallback();
3275
3450
  const parsed = parseGithubRepoFullName(value);
3276
3451
  if (!parsed) throw new CliError("user", "repo must be owner/name or a GitHub URL");
@@ -3279,10 +3454,10 @@ function parseRepoArg(value, fallback) {
3279
3454
  function repoPath(repo) {
3280
3455
  return `${repo.owner}/${repo.repo}`;
3281
3456
  }
3282
- function encodePathSegment(value) {
3457
+ function encodePathSegment2(value) {
3283
3458
  return encodeURIComponent(value);
3284
3459
  }
3285
- async function resolveBusinessId(config, options) {
3460
+ async function resolveBusinessId2(config, options) {
3286
3461
  if (options.business && options.business.trim()) return options.business.trim();
3287
3462
  const whoami = await apiFetch(config, "/api/auth/whoami");
3288
3463
  if (whoami.businessId && whoami.businessId.trim()) return whoami.businessId;
@@ -3421,7 +3596,7 @@ function buildLogsPath(businessId, buildId, options = {}) {
3421
3596
  if (options.afterSequence != null) params.set("afterSequence", String(options.afterSequence));
3422
3597
  if (options.limit != null) params.set("limit", String(options.limit));
3423
3598
  const suffix = params.toString() ? `?${params.toString()}` : "";
3424
- return `/api/businesses/${encodePathSegment(businessId)}/sandbox-layer/build-requests/${encodePathSegment(
3599
+ return `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/build-requests/${encodePathSegment2(
3425
3600
  buildId
3426
3601
  )}/logs${suffix}`;
3427
3602
  }
@@ -3438,7 +3613,7 @@ async function waitForBuild(config, businessId, buildId, options) {
3438
3613
  }
3439
3614
  const status = await apiFetch(
3440
3615
  config,
3441
- `/api/businesses/${encodePathSegment(businessId)}/sandbox-layer/build-requests/${encodePathSegment(buildId)}`
3616
+ `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/build-requests/${encodePathSegment2(buildId)}`
3442
3617
  );
3443
3618
  if (TERMINAL_BUILD_STATUSES.has(status.buildRequest.status)) {
3444
3619
  return status.buildRequest;
@@ -3527,12 +3702,12 @@ async function sandboxInitCommand(options = {}, command) {
3527
3702
  async function sandboxValidateCommand(options = {}, command) {
3528
3703
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
3529
3704
  assertManifestExists(manifestPath);
3530
- const manifestText = readFileSync2(manifestPath, "utf8");
3705
+ const manifestText = readFileSync3(manifestPath, "utf8");
3531
3706
  let layerPath;
3532
3707
  try {
3533
3708
  layerPath = parseSandboxLayerManifest(manifestPath, manifestText).layer.dockerfile;
3534
3709
  assertManifestExists(layerPath);
3535
- const layerText = readFileSync2(layerPath, "utf8");
3710
+ const layerText = readFileSync3(layerPath, "utf8");
3536
3711
  const parsed = await parseSandboxLayerSource({ manifestPath, manifestText, layerPath, layerText });
3537
3712
  const payload = formatValidationPayload({ manifestPath, layerPath, parsed });
3538
3713
  if (isJson(command, options)) {
@@ -3556,13 +3731,13 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
3556
3731
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
3557
3732
  assertManifestExists(manifestPath);
3558
3733
  if (!options.ref) assertCleanAndPushed();
3559
- const sourceRepo = parseRepoArg(sourceRepoArg, currentRepo);
3560
- const targetRepo = options.targetRepo ? parseRepoArg(options.targetRepo, currentRepo) : null;
3561
- const businessId = await resolveBusinessId(config, options);
3734
+ const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
3735
+ const targetRepo = options.targetRepo ? parseRepoArg2(options.targetRepo, currentRepo) : null;
3736
+ const businessId = await resolveBusinessId2(config, options);
3562
3737
  const ref = options.ref ?? currentRef();
3563
3738
  const payload = await apiFetch(
3564
3739
  config,
3565
- `/api/businesses/${encodePathSegment(businessId)}/repos/${encodePathSegment(sourceRepo.owner)}/${encodePathSegment(
3740
+ `/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(sourceRepo.owner)}/${encodePathSegment2(
3566
3741
  sourceRepo.repo
3567
3742
  )}/sandbox-layer/build-requests`,
3568
3743
  {
@@ -3592,11 +3767,11 @@ async function sandboxBuildCommand(sourceRepoArg, options = {}, command) {
3592
3767
  async function sandboxStatusCommand(repoArg, options = {}, command) {
3593
3768
  const runtime = getRuntimeOptions(command, options);
3594
3769
  const config = requireConfig(runtime);
3595
- const businessId = await resolveBusinessId(config, options);
3596
- const repo = parseRepoArg(repoArg, currentRepo);
3770
+ const businessId = await resolveBusinessId2(config, options);
3771
+ const repo = parseRepoArg2(repoArg, currentRepo);
3597
3772
  const payload = await apiFetch(
3598
3773
  config,
3599
- `/api/businesses/${encodePathSegment(businessId)}/repos/${encodePathSegment(repo.owner)}/${encodePathSegment(
3774
+ `/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(repo.owner)}/${encodePathSegment2(
3600
3775
  repo.repo
3601
3776
  )}/sandbox-layer/resolution`
3602
3777
  );
@@ -3609,10 +3784,10 @@ async function sandboxStatusCommand(repoArg, options = {}, command) {
3609
3784
  async function sandboxHistoryCommand(sourceRepoArg, options = {}, command) {
3610
3785
  const runtime = getRuntimeOptions(command, options);
3611
3786
  const config = requireConfig(runtime);
3612
- const businessId = await resolveBusinessId(config, options);
3613
- const sourceRepo = parseRepoArg(sourceRepoArg, currentRepo);
3787
+ const businessId = await resolveBusinessId2(config, options);
3788
+ const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
3614
3789
  const params = new URLSearchParams({ sourceRepo: repoPath(sourceRepo) });
3615
- if (options.targetRepo) params.set("targetRepo", repoPath(parseRepoArg(options.targetRepo, currentRepo)));
3790
+ if (options.targetRepo) params.set("targetRepo", repoPath(parseRepoArg2(options.targetRepo, currentRepo)));
3616
3791
  if (options.status) params.set("status", options.status);
3617
3792
  if (options.limit) {
3618
3793
  const limit = Number(options.limit);
@@ -3623,7 +3798,7 @@ async function sandboxHistoryCommand(sourceRepoArg, options = {}, command) {
3623
3798
  }
3624
3799
  const payload = await apiFetch(
3625
3800
  config,
3626
- `/api/businesses/${encodePathSegment(businessId)}/sandbox-layer/build-requests?${params.toString()}`
3801
+ `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/build-requests?${params.toString()}`
3627
3802
  );
3628
3803
  if (isJson(command, options)) {
3629
3804
  writeJson(payload);
@@ -3640,7 +3815,7 @@ async function sandboxRebuildStaleCommand(options = {}, command) {
3640
3815
  if (options.all && options.business) {
3641
3816
  throw new CliError("user", "Use either --all or --business, not both.");
3642
3817
  }
3643
- const businessId = options.all ? null : await resolveBusinessId(config, options);
3818
+ const businessId = options.all ? null : await resolveBusinessId2(config, options);
3644
3819
  let payload = await apiFetch(config, "/api/admin/sandbox-layer/rebuild-campaigns", {
3645
3820
  method: "POST",
3646
3821
  body: JSON.stringify({
@@ -3668,7 +3843,7 @@ async function followRebuildCampaign(config, campaignId) {
3668
3843
  for (; ; ) {
3669
3844
  const payload = await apiFetch(
3670
3845
  config,
3671
- `/api/admin/sandbox-layer/rebuild-campaigns/${encodePathSegment(campaignId)}`
3846
+ `/api/admin/sandbox-layer/rebuild-campaigns/${encodePathSegment2(campaignId)}`
3672
3847
  );
3673
3848
  if (isCampaignTerminal(payload.campaign.status)) return payload;
3674
3849
  await sleep2(DEFAULT_POLL_INTERVAL_MS);
@@ -3697,7 +3872,7 @@ function printRebuildCampaign(payload) {
3697
3872
  async function sandboxLogsCommand(buildId, options = {}, command) {
3698
3873
  const runtime = getRuntimeOptions(command, options);
3699
3874
  const config = requireConfig(runtime);
3700
- const businessId = await resolveBusinessId(config, options);
3875
+ const businessId = await resolveBusinessId2(config, options);
3701
3876
  let afterSequence = options.afterSequence == null ? void 0 : Number(options.afterSequence);
3702
3877
  if (afterSequence != null && (!Number.isInteger(afterSequence) || afterSequence < 0)) {
3703
3878
  throw new CliError("user", "--after-sequence must be a non-negative integer.");
@@ -3721,12 +3896,12 @@ async function sandboxLogsCommand(buildId, options = {}, command) {
3721
3896
  async function sandboxAssignDefaultCommand(sourceRepoArg, options = {}, command) {
3722
3897
  const runtime = getRuntimeOptions(command, options);
3723
3898
  const config = requireConfig(runtime);
3724
- const businessId = await resolveBusinessId(config, options);
3725
- const sourceRepo = parseRepoArg(sourceRepoArg, currentRepo);
3899
+ const businessId = await resolveBusinessId2(config, options);
3900
+ const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
3726
3901
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
3727
3902
  const payload = await apiFetch(
3728
3903
  config,
3729
- `/api/businesses/${encodePathSegment(businessId)}/sandbox-layer/default-source`,
3904
+ `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/default-source`,
3730
3905
  { method: "PUT", body: JSON.stringify(sourceBody(sourceRepo, manifestPath)) }
3731
3906
  );
3732
3907
  if (isJson(command, options)) {
@@ -3741,13 +3916,13 @@ async function sandboxAssignDefaultCommand(sourceRepoArg, options = {}, command)
3741
3916
  async function sandboxAssignRepoCommand(targetRepoArg, sourceRepoArg, options = {}, command) {
3742
3917
  const runtime = getRuntimeOptions(command, options);
3743
3918
  const config = requireConfig(runtime);
3744
- const businessId = await resolveBusinessId(config, options);
3745
- const targetRepo = parseRepoArg(targetRepoArg, currentRepo);
3746
- const sourceRepo = parseRepoArg(sourceRepoArg, currentRepo);
3919
+ const businessId = await resolveBusinessId2(config, options);
3920
+ const targetRepo = parseRepoArg2(targetRepoArg, currentRepo);
3921
+ const sourceRepo = parseRepoArg2(sourceRepoArg, currentRepo);
3747
3922
  const manifestPath = options.manifest ?? DEFAULT_MANIFEST_PATH;
3748
3923
  const payload = await apiFetch(
3749
3924
  config,
3750
- `/api/businesses/${encodePathSegment(businessId)}/repos/${encodePathSegment(targetRepo.owner)}/${encodePathSegment(
3925
+ `/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(targetRepo.owner)}/${encodePathSegment2(
3751
3926
  targetRepo.repo
3752
3927
  )}/sandbox-layer/assignment`,
3753
3928
  { method: "PUT", body: JSON.stringify(sourceBody(sourceRepo, manifestPath)) }
@@ -3766,11 +3941,11 @@ async function sandboxAssignRepoCommand(targetRepoArg, sourceRepoArg, options =
3766
3941
  async function sandboxUnassignDefaultCommand(options = {}, command) {
3767
3942
  const runtime = getRuntimeOptions(command, options);
3768
3943
  const config = requireConfig(runtime);
3769
- const businessId = await resolveBusinessId(config, options);
3944
+ const businessId = await resolveBusinessId2(config, options);
3770
3945
  if (!options.yes) await confirmOrThrow("Clear the business default sandbox layer source?");
3771
3946
  const payload = await apiFetch(
3772
3947
  config,
3773
- `/api/businesses/${encodePathSegment(businessId)}/sandbox-layer/default-source`,
3948
+ `/api/businesses/${encodePathSegment2(businessId)}/sandbox-layer/default-source`,
3774
3949
  { method: "DELETE" }
3775
3950
  );
3776
3951
  if (isJson(command, options)) {
@@ -3782,12 +3957,12 @@ async function sandboxUnassignDefaultCommand(options = {}, command) {
3782
3957
  async function sandboxUnassignRepoCommand(targetRepoArg, options = {}, command) {
3783
3958
  const runtime = getRuntimeOptions(command, options);
3784
3959
  const config = requireConfig(runtime);
3785
- const businessId = await resolveBusinessId(config, options);
3786
- const targetRepo = parseRepoArg(targetRepoArg, currentRepo);
3960
+ const businessId = await resolveBusinessId2(config, options);
3961
+ const targetRepo = parseRepoArg2(targetRepoArg, currentRepo);
3787
3962
  if (!options.yes) await confirmOrThrow(`Clear sandbox layer assignment for ${repoPath(targetRepo)}?`);
3788
3963
  const payload = await apiFetch(
3789
3964
  config,
3790
- `/api/businesses/${encodePathSegment(businessId)}/repos/${encodePathSegment(targetRepo.owner)}/${encodePathSegment(
3965
+ `/api/businesses/${encodePathSegment2(businessId)}/repos/${encodePathSegment2(targetRepo.owner)}/${encodePathSegment2(
3791
3966
  targetRepo.repo
3792
3967
  )}/sandbox-layer/assignment`,
3793
3968
  { method: "DELETE" }
@@ -3846,6 +4021,8 @@ async function getSessionCommand(sessionId, options, command) {
3846
4021
  console.log(`Status: ${String(session.phase ?? session.status ?? "unknown")}`);
3847
4022
  if (session.repoUrl) console.log(`Repo: ${String(session.repoUrl)}`);
3848
4023
  if (session.title) console.log(`Title: ${String(session.title)}`);
4024
+ const resultLine = formatSessionResult(session);
4025
+ if (resultLine) console.log(resultLine);
3849
4026
  }
3850
4027
  async function sessionEventsCommand(sessionId, options, command) {
3851
4028
  const afterSequence = resolveSequenceOption(options.afterSequence, options.after, "--after-sequence", "--after");
@@ -3969,8 +4146,8 @@ function parseStopBlocked(err) {
3969
4146
  }
3970
4147
 
3971
4148
  // src/commands/test-creds.ts
3972
- import { readFileSync as readFileSync3 } from "fs";
3973
- function parseRepoArg2(repo) {
4149
+ import { readFileSync as readFileSync4 } from "fs";
4150
+ function parseRepoArg3(repo) {
3974
4151
  const trimmed = repo.trim();
3975
4152
  const slashIndex = trimmed.indexOf("/");
3976
4153
  if (slashIndex <= 0 || slashIndex === trimmed.length - 1) {
@@ -3987,13 +4164,13 @@ function readValue(options) {
3987
4164
  throw new CliError("user", "Provide exactly one of --value, --value-file, or --value-stdin.");
3988
4165
  }
3989
4166
  if (options.value !== void 0) return options.value;
3990
- const raw = options.valueFile ? readFileSync3(options.valueFile, "utf8") : readFileSync3(0, "utf8");
4167
+ const raw = options.valueFile ? readFileSync4(options.valueFile, "utf8") : readFileSync4(0, "utf8");
3991
4168
  return raw.replace(/\r?\n$/, "");
3992
4169
  }
3993
4170
  async function listTestCredentialsCommand(repo, options, command) {
3994
4171
  const runtime = getRuntimeOptions(command, options);
3995
4172
  const config = requireConfig(runtime);
3996
- const repoArg = parseRepoArg2(repo);
4173
+ const repoArg = parseRepoArg3(repo);
3997
4174
  const payload = await apiFetch(config, basePath(options.business, repoArg));
3998
4175
  if (isJson(command, options)) {
3999
4176
  writeJson(payload);
@@ -4011,7 +4188,7 @@ async function listTestCredentialsCommand(repo, options, command) {
4011
4188
  async function setTestCredentialCommand(repo, name, options, command) {
4012
4189
  const runtime = getRuntimeOptions(command, options);
4013
4190
  const config = requireConfig(runtime);
4014
- const repoArg = parseRepoArg2(repo);
4191
+ const repoArg = parseRepoArg3(repo);
4015
4192
  const value = readValue(options);
4016
4193
  const payload = await apiFetch(
4017
4194
  config,
@@ -4029,12 +4206,12 @@ async function deleteTestCredentialCommand(repo, name, options, command) {
4029
4206
  throw new CliError("user", "`test-creds delete --json` requires --yes.");
4030
4207
  }
4031
4208
  if (options.yes !== true) {
4032
- const repoArg2 = parseRepoArg2(repo);
4209
+ const repoArg2 = parseRepoArg3(repo);
4033
4210
  await confirmOrThrow(`Delete test credential '${name}' for ${repoArg2.owner}/${repoArg2.name}?`);
4034
4211
  }
4035
4212
  const runtime = getRuntimeOptions(command, options);
4036
4213
  const config = requireConfig(runtime);
4037
- const repoArg = parseRepoArg2(repo);
4214
+ const repoArg = parseRepoArg3(repo);
4038
4215
  const payload = await apiFetch(
4039
4216
  config,
4040
4217
  `${basePath(options.business, repoArg)}/${encodeURIComponent(name)}`,
@@ -4321,6 +4498,13 @@ sandboxAssign.command("repo").description("Assign a sandbox layer source to one
4321
4498
  var sandboxUnassign = sandbox.command("unassign").description("Remove sandbox layer source assignments");
4322
4499
  sandboxUnassign.command("default").description("Clear the business default sandbox layer source").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--yes", "Skip confirmation").action((options, command) => sandboxUnassignDefaultCommand(options, command));
4323
4500
  sandboxUnassign.command("repo").description("Clear a target repo sandbox layer assignment").argument("<target-repo>", "Target repository owner/name").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--yes", "Skip confirmation").action((targetRepo, options, command) => sandboxUnassignRepoCommand(targetRepo, options, command));
4501
+ var egress = program.command("egress").description("Workspace egress allowlist commands");
4502
+ var egressSource = egress.command("source").description("Manage the workspace egress allowlist source repo");
4503
+ egressSource.command("set").description("Set the source repo for .arcanist/egress-allowlist.txt").argument("<source-repo>", "Source repository owner/name").option("--business <id>", "Business ID; defaults to authenticated user's business").action((sourceRepo, options, command) => egressSourceSetCommand(sourceRepo, options, command));
4504
+ egressSource.command("get").description("Show the configured egress allowlist source").option("--business <id>", "Business ID; defaults to authenticated user's business").action((options, command) => egressSourceGetCommand(options, command));
4505
+ egress.command("add").description("Open or update a PR adding domains to the egress allowlist source file").argument("<domains...>", "Exact domain names to add").option("--business <id>", "Business ID; defaults to authenticated user's business").option("--reason <text>", "Reason to include in the PR body").action((domains, options, command) => egressAddCommand(domains, options, command));
4506
+ egress.command("sync").description("Apply the merged egress allowlist source file to the runtime business policy").option("--business <id>", "Business ID; defaults to authenticated user's business").action((options, command) => egressSyncCommand(options, command));
4507
+ egress.command("validate").description("Validate a local egress allowlist source file").argument("[path]", "Path to validate", ".arcanist/egress-allowlist.txt").action((path, options, command) => egressValidateCommand(path, options, command));
4324
4508
  var tokens = program.command("tokens").description("CLI token commands");
4325
4509
  tokens.command("list").description("List CLI tokens").option("--limit <n>", "Maximum tokens to return").option("--cursor <cursor>", "Pagination cursor").action((options, command) => listTokensCommand(options, command));
4326
4510
  tokens.command("create").description("Create a CLI token and print it once").option("--scope <scope>", "Token scope: read or write", "read").option("--expires-in-days <days>", "Token expiry in days").addHelpText(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.132",
3
+ "version": "0.1.134",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {