@tryarcanist/cli 0.1.130 → 0.1.132

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 +36 -1
  2. package/dist/index.js +188 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -123,6 +123,7 @@ printf "add tests" | arcanist sessions create your-org/your-repo --prompt-stdin
123
123
  arcanist sessions create your-org/your-repo - --model gpt-5.5
124
124
  arcanist sessions create your-org/your-repo "port to claude" --backend claude_code --model claude-opus-4-8
125
125
  arcanist sessions create your-org/your-repo "refactor auth" --reasoning-effort xhigh
126
+ arcanist sessions create your-org/your-repo "fix release branch" --base-branch release/2026-06
126
127
  arcanist sessions create your-org/your-repo "review the trace" --uploaded-file trace.txt
127
128
  arcanist sessions create your-org/your-repo "verify the deployed change" --cold
128
129
  arcanist sessions create your-org/your-repo "retry-safe create" --idempotency-key 1f0e6f1a-...
@@ -132,6 +133,8 @@ arcanist sessions create your-org/your-repo "retry-safe create" --idempotency-ke
132
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
135
 
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
+
135
138
  Repeatable `--uploaded-file <path>` flags attach local UTF-8 text files to the prompt. Uploaded file names come from the local basename; directory components are not sent.
136
139
 
137
140
  `--cold` is a deprecated no-op retained for backward compatibility. Sessions always start from a fresh sandbox (the warm sandbox pool was removed), so the flag has no effect.
@@ -140,7 +143,7 @@ Repeatable `--uploaded-file <path>` flags attach local UTF-8 text files to the p
140
143
 
141
144
  `--onboarding` creates an onboarding session: the agent authors the repo's `.arcanist.json`, `.arcanist/` runtime files, `ARCANIST.md`, and conditionally `.arcanist/sandbox.yaml` + `.arcanist/sandbox.layer.Dockerfile` (when the base sandbox lacks a needed toolchain), proves what it can inside its sandbox, and opens the setup PR ready for review. Team use; not part of the external API surface.
142
145
 
143
- JSON mode returns `{sessionId, sessionUrl?, repoUrl, model?, agentRuntimeBackend?, reasoningEffort?, promptId?}`. `sessionUrl` is emitted only when the server returns it; `agentRuntimeBackend` only when `--backend` is passed.
146
+ JSON mode returns `{sessionId, sessionUrl?, repoUrl, model?, agentRuntimeBackend?, reasoningEffort?, baseBranch?, promptId?}`. `sessionUrl` is emitted only when the server returns it; `agentRuntimeBackend` only when `--backend` is passed.
144
147
 
145
148
  ### `arcanist sessions send <session-id> [prompt]`
146
149
 
@@ -235,6 +238,38 @@ arcanist sessions usage abc123
235
238
  arcanist sessions usage abc123 --json
236
239
  ```
237
240
 
241
+ ### `arcanist automations create <repo-url> [prompt]`
242
+
243
+ Creates a scheduled automation for a GitHub repo. Create/delete require a write-scoped CLI token; read-scoped tokens can list automations. The server validates repo access, normalizes cron expressions, dedupes retries by repo+cron+prompt, and caps each business at 20 enabled rules.
244
+
245
+ ```bash
246
+ arcanist automations create your-org/your-repo "summarize new regressions" --cron "*/15 * * * *"
247
+ printf "summarize new regressions" | arcanist automations create your-org/your-repo --prompt-stdin --cron "*/15 * * * *" --json
248
+ arcanist automations create https://github.com/your-org/your-repo - --cron "0 14 * * 1-5" --name "Weekday summary"
249
+ ```
250
+
251
+ JSON mode prints the created rule. Human-readable mode prints the rule ID, repo, normalized cron, and next fire time.
252
+
253
+ ### `arcanist automations list`
254
+
255
+ ```bash
256
+ arcanist automations list
257
+ arcanist automations list --limit 20 --json
258
+ arcanist automations list --cursor <cursor>
259
+ arcanist automations list --all --json
260
+ ```
261
+
262
+ Default output reads one page. `--all` follows pagination until completion. JSON mode returns `{items, nextCursor}`.
263
+
264
+ ### `arcanist automations delete <id>`
265
+
266
+ Deletes a scheduled automation. Interactive mode prompts for confirmation; pass `--yes` for non-interactive use. `--json` requires `--yes`.
267
+
268
+ ```bash
269
+ arcanist automations delete rule_123
270
+ arcanist automations delete rule_123 --yes --json
271
+ ```
272
+
238
273
  ### `arcanist tokens list`
239
274
 
240
275
  ```bash
package/dist/index.js CHANGED
@@ -378,6 +378,161 @@ async function whoamiCommand(options, command) {
378
378
  if (payload.tokenScope) console.log(`Token scope: ${String(payload.tokenScope)}`);
379
379
  }
380
380
 
381
+ // ../../shared/github/repo-url.ts
382
+ function parseGithubRepoFullName(value) {
383
+ const shorthand = value.match(/^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/);
384
+ if (shorthand) return { owner: shorthand[1], repo: shorthand[2] };
385
+ const ssh = value.match(/git@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/);
386
+ if (ssh) return { owner: ssh[1], repo: ssh[2] };
387
+ const https = value.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
388
+ if (https) return { owner: https[1], repo: https[2] };
389
+ return null;
390
+ }
391
+
392
+ // src/commands/automations.ts
393
+ var AUTOMATION_ERROR_HINTS = {
394
+ invalid_cron: "Use a standard five-field cron expression, for example: */15 * * * *",
395
+ repo_not_available: "Check that the token owner has GitHub access and Arcanist is installed for that repo.",
396
+ duplicate_rule: "An enabled automation already exists for this repo, cron, and prompt.",
397
+ rule_cap_reached: "Delete or disable an existing automation before creating another one. The cap is 20 enabled rules.",
398
+ installation_unresolved: "Reconnect or reinstall the GitHub integration for that repository, then retry."
399
+ };
400
+ async function createAutomationCommand(repoUrl, promptArg, options, command) {
401
+ const repo = parseAutomationRepo(repoUrl);
402
+ const prompt = await resolvePromptInput(promptArg, options);
403
+ const runtime = getRuntimeOptions(command, options);
404
+ const config = requireConfig(runtime);
405
+ const body = {
406
+ repoOwner: repo.owner,
407
+ repoName: repo.repo,
408
+ cron: options.cron,
409
+ prompt
410
+ };
411
+ if (options.name !== void 0) body.name = options.name;
412
+ const payload = await automationApiFetch(config, "/api/automation/schedules", {
413
+ method: "POST",
414
+ body: JSON.stringify(body)
415
+ });
416
+ if (isJson(command, options)) {
417
+ writeJson(payload.data);
418
+ return;
419
+ }
420
+ printAutomationRule(payload.data);
421
+ }
422
+ async function listAutomationsCommand(options, command) {
423
+ const runtime = getRuntimeOptions(command, options);
424
+ const config = requireConfig(runtime);
425
+ const items = [];
426
+ let cursor = options.cursor;
427
+ let nextCursor = null;
428
+ do {
429
+ const query = new URLSearchParams();
430
+ if (options.limit) query.set("limit", options.limit);
431
+ if (cursor) query.set("cursor", cursor);
432
+ const payload = await automationApiFetch(
433
+ config,
434
+ `/api/automation/schedules${query.size ? `?${query.toString()}` : ""}`
435
+ );
436
+ items.push(...payload.data.items);
437
+ nextCursor = payload.data.nextCursor;
438
+ cursor = nextCursor ?? void 0;
439
+ } while (options.all === true && nextCursor);
440
+ const output = { items, nextCursor: options.all === true ? null : nextCursor };
441
+ if (isJson(command, options)) {
442
+ writeJson(output);
443
+ return;
444
+ }
445
+ if (items.length === 0) {
446
+ console.log("No automations found.");
447
+ return;
448
+ }
449
+ for (const rule of items) {
450
+ console.log(
451
+ `${rule.id} ${rule.repoOwner}/${rule.repoName} ${rule.normalizedCron} ${String(rule.enabled)} ${formatTime(rule.nextFireAt)}`
452
+ );
453
+ }
454
+ if (output.nextCursor) console.log(`Next cursor: ${output.nextCursor}`);
455
+ }
456
+ async function deleteAutomationCommand(id, options, command) {
457
+ if (isJson(command, options) && options.yes !== true) {
458
+ throw new CliError("user", "`automations delete --json` requires --yes.");
459
+ }
460
+ if (options.yes !== true) {
461
+ await confirmOrThrow(`Delete automation ${id}?`);
462
+ }
463
+ const runtime = getRuntimeOptions(command, options);
464
+ const config = requireConfig(runtime);
465
+ await automationApiFetchText(config, `/api/automation/schedules/${encodeURIComponent(id)}`, { method: "DELETE" });
466
+ if (isJson(command, options)) {
467
+ writeJson({ ok: true, id });
468
+ return;
469
+ }
470
+ console.log(`Deleted automation ${id}.`);
471
+ }
472
+ function parseAutomationRepo(value) {
473
+ if (/^git@/i.test(value) && !/^git@github\.com:/i.test(value)) {
474
+ throw new CliError("user", "repo-url must be owner/name or a GitHub URL.");
475
+ }
476
+ const parsed = parseGithubRepoFullName(value);
477
+ if (!parsed) throw new CliError("user", "repo-url must be owner/name or a GitHub URL.");
478
+ return { owner: parsed.owner, repo: parsed.repo };
479
+ }
480
+ async function automationApiFetch(config, path, init) {
481
+ try {
482
+ return await apiFetch(config, path, init);
483
+ } catch (err) {
484
+ throw mapAutomationApiError(err);
485
+ }
486
+ }
487
+ async function automationApiFetchText(config, path, init) {
488
+ try {
489
+ return await apiFetchText(config, path, init);
490
+ } catch (err) {
491
+ throw mapAutomationApiError(err);
492
+ }
493
+ }
494
+ function mapAutomationApiError(err) {
495
+ if (!(err instanceof ApiError)) {
496
+ return err instanceof CliError ? err : new CliError("server", err instanceof Error ? err.message : String(err));
497
+ }
498
+ const parsed = parseApiErrorBody2(err.body);
499
+ const serverCode = parsed?.error;
500
+ return new CliError(codeForStatus(err.status), parsed?.message || serverCode || err.message, {
501
+ exitCode: err.exitCode,
502
+ hint: serverCode ? AUTOMATION_ERROR_HINTS[serverCode] : void 0,
503
+ requestId: err.requestId
504
+ });
505
+ }
506
+ function parseApiErrorBody2(body) {
507
+ try {
508
+ const parsed = JSON.parse(body);
509
+ if (!parsed || typeof parsed !== "object") return null;
510
+ const record = parsed;
511
+ return {
512
+ error: typeof record.error === "string" ? record.error : void 0,
513
+ message: typeof record.message === "string" ? record.message : void 0
514
+ };
515
+ } catch {
516
+ return null;
517
+ }
518
+ }
519
+ function codeForStatus(status) {
520
+ if (status === 401 || status === 403) return "auth";
521
+ if (status === 404) return "not_found";
522
+ if (status === 409) return "conflict";
523
+ if (status >= 500) return "server";
524
+ return "user";
525
+ }
526
+ function printAutomationRule(rule) {
527
+ console.log(`ID: ${rule.id}`);
528
+ console.log(`Repo: ${rule.repoOwner}/${rule.repoName}`);
529
+ console.log(`Cron: ${rule.normalizedCron}`);
530
+ console.log(`Next fire: ${formatTime(rule.nextFireAt)}`);
531
+ }
532
+ function formatTime(value) {
533
+ return typeof value === "number" ? new Date(value).toISOString() : "none";
534
+ }
535
+
381
536
  // ../../shared/agent/agent-runtime-backend.ts
382
537
  var CODEX_AGENT_RUNTIME_BACKEND = "codex";
383
538
  var CLAUDE_CODE_AGENT_RUNTIME_BACKEND = "claude_code";
@@ -2416,6 +2571,10 @@ async function createCommand(repoUrl, promptArg, options, command) {
2416
2571
  if (repoError) {
2417
2572
  throw new CliError("user", repoError);
2418
2573
  }
2574
+ const baseBranch = options.baseBranch?.trim();
2575
+ if (options.baseBranch !== void 0 && !baseBranch) {
2576
+ throw new CliError("user", "--base-branch must not be empty.");
2577
+ }
2419
2578
  const uploadedFiles = await resolveUploadedFileOptions(options.uploadedFile);
2420
2579
  const idempotencyKey = options.idempotencyKey ?? randomIdempotencyKey();
2421
2580
  const sessionIdempotencyKey = `${idempotencyKey}:session`;
@@ -2424,6 +2583,7 @@ async function createCommand(repoUrl, promptArg, options, command) {
2424
2583
  if (options.model) body.model = options.model;
2425
2584
  if (options.backend) body.agentRuntimeBackend = agentRuntimeBackend;
2426
2585
  if (options.reasoningEffort) body.reasoningEffort = options.reasoningEffort;
2586
+ if (baseBranch) body.baseBranch = baseBranch;
2427
2587
  if (options.cold) body.cold = true;
2428
2588
  if (options.onboarding) body.onboarding = true;
2429
2589
  const sessionData = await apiFetch(config, "/api/sessions", {
@@ -2469,6 +2629,7 @@ async function createCommand(repoUrl, promptArg, options, command) {
2469
2629
  if (options.model) output.model = options.model;
2470
2630
  if (options.backend) output.agentRuntimeBackend = agentRuntimeBackend;
2471
2631
  if (options.reasoningEffort) output.reasoningEffort = options.reasoningEffort;
2632
+ if (baseBranch) output.baseBranch = baseBranch;
2472
2633
  if (options.onboarding) output.onboarding = true;
2473
2634
  if (promptId) output.promptId = promptId;
2474
2635
  writeJson(output);
@@ -2551,17 +2712,6 @@ import { execFileSync } from "child_process";
2551
2712
  import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2552
2713
  import { dirname } from "path";
2553
2714
 
2554
- // ../../shared/github/repo-url.ts
2555
- function parseGithubRepoFullName(value) {
2556
- const shorthand = value.match(/^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/);
2557
- if (shorthand) return { owner: shorthand[1], repo: shorthand[2] };
2558
- const ssh = value.match(/git@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/);
2559
- if (ssh) return { owner: ssh[1], repo: ssh[2] };
2560
- const https = value.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
2561
- if (https) return { owner: https[1], repo: https[2] };
2562
- return null;
2563
- }
2564
-
2565
2715
  // ../../shared/sandbox-layer/parser.ts
2566
2716
  import { isMap, isScalar, isSeq, parseDocument } from "yaml";
2567
2717
 
@@ -4002,7 +4152,7 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
4002
4152
  applyColorEnvironment(getRuntimeOptions(actionCommand));
4003
4153
  });
4004
4154
  function addCreateOptions(cmd) {
4005
- return cmd.argument("<repo-url>", "Repository URL").argument("[prompt]", "Prompt to send, or '-' to read stdin").option("--model <model>", "Model to use").option("--backend <backend>", "Agent runtime backend: codex (default) or claude_code").option("--reasoning-effort <effort>", "Reasoning effort to use for models that support it").option("--prompt-stdin", "Read prompt from stdin").option(
4155
+ return cmd.argument("<repo-url>", "Repository URL").argument("[prompt]", "Prompt to send, or '-' to read stdin").option("--model <model>", "Model to use").option("--backend <backend>", "Agent runtime backend: codex (default) or claude_code").option("--reasoning-effort <effort>", "Reasoning effort to use for models that support it").option("--base-branch <branch>", "Base branch to create the session against (defaults to the repo default branch)").option("--prompt-stdin", "Read prompt from stdin").option(
4006
4156
  "--uploaded-file <path>",
4007
4157
  "Attach a local text file to the prompt as uploadedFiles; repeat for multiple files",
4008
4158
  collectUploadedFileOption
@@ -4129,6 +4279,32 @@ Examples:
4129
4279
  arcanist sessions usage <session-id> --json
4130
4280
  `
4131
4281
  ).action((sessionId, options, command) => usageCommand(sessionId, options, command));
4282
+ var automations = program.command("automations").description("Automation commands");
4283
+ automations.command("create").description("Create a scheduled automation").argument("<repo-url>", "Repository URL").argument("[prompt]", "Prompt to run, or '-' to read stdin").requiredOption("--cron <expr>", "Cron expression").option("--name <name>", "Automation display name").option("--prompt-stdin", "Read prompt from stdin").addHelpText(
4284
+ "after",
4285
+ `
4286
+ Examples:
4287
+ arcanist automations create tryarcanist/arcanist "summarize regressions" --cron "*/15 * * * *"
4288
+ printf "summarize regressions" | arcanist automations create tryarcanist/arcanist --prompt-stdin --cron "*/15 * * * *" --json
4289
+ `
4290
+ ).action((repoUrl, prompt, options, command) => createAutomationCommand(repoUrl, prompt, options, command));
4291
+ automations.command("list").description("List scheduled automations").option("--limit <n>", "Maximum automations to return").option("--cursor <cursor>", "Pagination cursor").option("--all", "Fetch all pages").addHelpText(
4292
+ "after",
4293
+ `
4294
+ Examples:
4295
+ arcanist automations list
4296
+ arcanist automations list --limit 20 --json
4297
+ arcanist automations list --all --json
4298
+ `
4299
+ ).action((options, command) => listAutomationsCommand(options, command));
4300
+ automations.command("delete").description("Delete a scheduled automation").argument("<id>", "Automation ID").option("--yes", "Confirm deletion without prompting").addHelpText(
4301
+ "after",
4302
+ `
4303
+ Examples:
4304
+ arcanist automations delete auto_123
4305
+ arcanist automations delete auto_123 --yes --json
4306
+ `
4307
+ ).action((id, options, command) => deleteAutomationCommand(id, options, command));
4132
4308
  var sandbox = program.command("sandbox").description("Sandbox layer commands");
4133
4309
  sandbox.command("init").description("Create sandbox layer source files").action((options, command) => sandboxInitCommand(options, command));
4134
4310
  sandbox.command("validate").description("Validate local sandbox layer source files").option("--manifest <path>", "Manifest path", ".arcanist/sandbox.yaml").action((options, command) => sandboxValidateCommand(options, command));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.130",
3
+ "version": "0.1.132",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {