@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.
- package/README.md +36 -1
- package/dist/index.js +188 -12
- 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));
|