@tryarcanist/cli 0.1.163 → 0.1.165
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 +3 -0
- package/dist/index.js +222 -161
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -272,10 +272,13 @@ Creates a scheduled automation for a GitHub repo. Create/delete require a write-
|
|
|
272
272
|
|
|
273
273
|
```bash
|
|
274
274
|
arcanist automations create your-org/your-repo "summarize new regressions" --cron "*/15 * * * *"
|
|
275
|
+
arcanist automations create your-org/your-repo "audit N+1s" --cron "0 13 * * 5" --model gpt-5.5
|
|
275
276
|
printf "summarize new regressions" | arcanist automations create your-org/your-repo --prompt-stdin --cron "*/15 * * * *" --json
|
|
276
277
|
arcanist automations create https://github.com/your-org/your-repo - --cron "0 14 * * 1-5" --name "Weekday summary"
|
|
277
278
|
```
|
|
278
279
|
|
|
280
|
+
Use `--model <model>` to pin the model for sessions started by the automation. Backend is derived from the model; non-codex backends such as Claude or opencode are limited to Arcanist team businesses. When `--model` is omitted the codex backend default is used.
|
|
281
|
+
|
|
279
282
|
JSON mode prints the created rule. Human-readable mode prints the rule ID, repo, normalized cron, and next fire time.
|
|
280
283
|
|
|
281
284
|
### `arcanist automations list`
|
package/dist/index.js
CHANGED
|
@@ -495,161 +495,6 @@ async function whoamiCommand(options, command) {
|
|
|
495
495
|
});
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
-
// ../../shared/github/repo-url.ts
|
|
499
|
-
function parseGithubRepoFullName(value) {
|
|
500
|
-
const shorthand = value.match(/^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/);
|
|
501
|
-
if (shorthand) return { owner: shorthand[1], repo: shorthand[2] };
|
|
502
|
-
const ssh = value.match(/git@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
503
|
-
if (ssh) return { owner: ssh[1], repo: ssh[2] };
|
|
504
|
-
const https = value.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
505
|
-
if (https) return { owner: https[1], repo: https[2] };
|
|
506
|
-
return null;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// src/commands/automations.ts
|
|
510
|
-
var AUTOMATION_ERROR_HINTS = {
|
|
511
|
-
invalid_cron: "Use a standard five-field cron expression, for example: */15 * * * *",
|
|
512
|
-
repo_not_available: "Check that the token owner has GitHub access and Arcanist is installed for that repo.",
|
|
513
|
-
duplicate_rule: "An enabled automation already exists for this repo, cron, and prompt.",
|
|
514
|
-
rule_cap_reached: "Delete or disable an existing automation before creating another one. The cap is 20 enabled rules.",
|
|
515
|
-
installation_unresolved: "Reconnect or reinstall the GitHub integration for that repository, then retry."
|
|
516
|
-
};
|
|
517
|
-
async function createAutomationCommand(repoUrl, promptArg, options, command) {
|
|
518
|
-
const repo = parseAutomationRepo(repoUrl);
|
|
519
|
-
const prompt = await resolvePromptInput(promptArg, options);
|
|
520
|
-
const runtime = getRuntimeOptions(command, options);
|
|
521
|
-
const config = requireConfig(runtime);
|
|
522
|
-
const body = {
|
|
523
|
-
repoOwner: repo.owner,
|
|
524
|
-
repoName: repo.repo,
|
|
525
|
-
cron: options.cron,
|
|
526
|
-
prompt
|
|
527
|
-
};
|
|
528
|
-
if (options.name !== void 0) body.name = options.name;
|
|
529
|
-
const payload = await automationApiFetch(config, "/api/automation/schedules", {
|
|
530
|
-
method: "POST",
|
|
531
|
-
body: JSON.stringify(body)
|
|
532
|
-
});
|
|
533
|
-
if (isJson(command, options)) {
|
|
534
|
-
writeJson(payload.data);
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
printAutomationRule(payload.data);
|
|
538
|
-
}
|
|
539
|
-
async function listAutomationsCommand(options, command) {
|
|
540
|
-
const runtime = getRuntimeOptions(command, options);
|
|
541
|
-
const config = requireConfig(runtime);
|
|
542
|
-
const items = [];
|
|
543
|
-
let cursor = options.cursor;
|
|
544
|
-
let nextCursor = null;
|
|
545
|
-
do {
|
|
546
|
-
const query = new URLSearchParams();
|
|
547
|
-
if (options.limit) query.set("limit", options.limit);
|
|
548
|
-
if (cursor) query.set("cursor", cursor);
|
|
549
|
-
const payload = await automationApiFetch(
|
|
550
|
-
config,
|
|
551
|
-
`/api/automation/schedules${query.size ? `?${query.toString()}` : ""}`
|
|
552
|
-
);
|
|
553
|
-
items.push(...payload.data.items);
|
|
554
|
-
nextCursor = payload.data.nextCursor;
|
|
555
|
-
cursor = nextCursor ?? void 0;
|
|
556
|
-
} while (options.all === true && nextCursor);
|
|
557
|
-
const output = { items, nextCursor: options.all === true ? null : nextCursor };
|
|
558
|
-
if (isJson(command, options)) {
|
|
559
|
-
writeJson(output);
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
if (items.length === 0) {
|
|
563
|
-
console.log("No automations found.");
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
for (const rule of items) {
|
|
567
|
-
console.log(
|
|
568
|
-
`${rule.id} ${rule.repoOwner}/${rule.repoName} ${rule.normalizedCron} ${String(rule.enabled)} ${formatTime(rule.nextFireAt)}`
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
if (output.nextCursor) console.log(`Next cursor: ${output.nextCursor}`);
|
|
572
|
-
}
|
|
573
|
-
async function deleteAutomationCommand(id, options, command) {
|
|
574
|
-
if (isJson(command, options) && options.yes !== true) {
|
|
575
|
-
throw new CliError("user", "`automations delete --json` requires --yes.");
|
|
576
|
-
}
|
|
577
|
-
if (options.yes !== true) {
|
|
578
|
-
await confirmOrThrow(`Delete automation ${id}?`);
|
|
579
|
-
}
|
|
580
|
-
const runtime = getRuntimeOptions(command, options);
|
|
581
|
-
const config = requireConfig(runtime);
|
|
582
|
-
await automationApiFetchText(config, `/api/automation/schedules/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
583
|
-
if (isJson(command, options)) {
|
|
584
|
-
writeJson({ ok: true, id });
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
console.log(`Deleted automation ${id}.`);
|
|
588
|
-
}
|
|
589
|
-
function parseAutomationRepo(value) {
|
|
590
|
-
if (/^git@/i.test(value) && !/^git@github\.com:/i.test(value)) {
|
|
591
|
-
throw new CliError("user", "repo-url must be owner/name or a GitHub URL.");
|
|
592
|
-
}
|
|
593
|
-
const parsed = parseGithubRepoFullName(value);
|
|
594
|
-
if (!parsed) throw new CliError("user", "repo-url must be owner/name or a GitHub URL.");
|
|
595
|
-
return { owner: parsed.owner, repo: parsed.repo };
|
|
596
|
-
}
|
|
597
|
-
async function automationApiFetch(config, path, init) {
|
|
598
|
-
try {
|
|
599
|
-
return await apiFetch(config, path, init);
|
|
600
|
-
} catch (err) {
|
|
601
|
-
throw mapAutomationApiError(err);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
async function automationApiFetchText(config, path, init) {
|
|
605
|
-
try {
|
|
606
|
-
return await apiFetchText(config, path, init);
|
|
607
|
-
} catch (err) {
|
|
608
|
-
throw mapAutomationApiError(err);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
function mapAutomationApiError(err) {
|
|
612
|
-
if (!(err instanceof ApiError)) {
|
|
613
|
-
return err instanceof CliError ? err : new CliError("server", err instanceof Error ? err.message : String(err));
|
|
614
|
-
}
|
|
615
|
-
const parsed = parseApiErrorBody2(err.body);
|
|
616
|
-
const serverCode = parsed?.error;
|
|
617
|
-
return new CliError(codeForStatus(err.status), parsed?.message || serverCode || err.message, {
|
|
618
|
-
exitCode: err.exitCode,
|
|
619
|
-
hint: serverCode ? AUTOMATION_ERROR_HINTS[serverCode] : void 0,
|
|
620
|
-
requestId: err.requestId
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
function parseApiErrorBody2(body) {
|
|
624
|
-
try {
|
|
625
|
-
const parsed = JSON.parse(body);
|
|
626
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
627
|
-
const record = parsed;
|
|
628
|
-
return {
|
|
629
|
-
error: typeof record.error === "string" ? record.error : void 0,
|
|
630
|
-
message: typeof record.message === "string" ? record.message : void 0
|
|
631
|
-
};
|
|
632
|
-
} catch {
|
|
633
|
-
return null;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
function codeForStatus(status) {
|
|
637
|
-
if (status === 401 || status === 403) return "auth";
|
|
638
|
-
if (status === 404) return "not_found";
|
|
639
|
-
if (status === 409) return "conflict";
|
|
640
|
-
if (status >= 500) return "server";
|
|
641
|
-
return "user";
|
|
642
|
-
}
|
|
643
|
-
function printAutomationRule(rule) {
|
|
644
|
-
console.log(`ID: ${rule.id}`);
|
|
645
|
-
console.log(`Repo: ${rule.repoOwner}/${rule.repoName}`);
|
|
646
|
-
console.log(`Cron: ${rule.normalizedCron}`);
|
|
647
|
-
console.log(`Next fire: ${formatTime(rule.nextFireAt)}`);
|
|
648
|
-
}
|
|
649
|
-
function formatTime(value) {
|
|
650
|
-
return typeof value === "number" ? new Date(value).toISOString() : "none";
|
|
651
|
-
}
|
|
652
|
-
|
|
653
498
|
// ../../shared/agent/agent-runtime-backend.ts
|
|
654
499
|
var CODEX_AGENT_RUNTIME_BACKEND = "codex";
|
|
655
500
|
var CLAUDE_CODE_AGENT_RUNTIME_BACKEND = "claude_code";
|
|
@@ -953,12 +798,18 @@ function extractModelId(raw) {
|
|
|
953
798
|
if (!value) return void 0;
|
|
954
799
|
return splitModelIdentifier(value)?.modelID;
|
|
955
800
|
}
|
|
801
|
+
function getAgentRuntimeBackendForModel(modelId) {
|
|
802
|
+
return getBackendsForModel(modelId)?.[0];
|
|
803
|
+
}
|
|
956
804
|
function getSessionStartModelIdsForBackend(backend) {
|
|
957
805
|
return SESSION_START_MODEL_IDS_BY_BACKEND[backend];
|
|
958
806
|
}
|
|
959
807
|
function isSessionStartModelAllowedForBackend(modelId, backend) {
|
|
960
808
|
return VALID_SESSION_START_MODEL_IDS_BY_BACKEND[backend].has(modelId);
|
|
961
809
|
}
|
|
810
|
+
function getBackendsForModel(modelId) {
|
|
811
|
+
return MODEL_DEFINITIONS_BY_ID[modelId]?.backends;
|
|
812
|
+
}
|
|
962
813
|
function getProviderForModel(modelId) {
|
|
963
814
|
return MODEL_PROVIDERS[modelId] ?? "openai";
|
|
964
815
|
}
|
|
@@ -1018,6 +869,184 @@ var CODEX_SUBSCRIPTION_SESSION_START_MODEL_PROVIDER_GROUPS = buildModelProviderG
|
|
|
1018
869
|
)
|
|
1019
870
|
);
|
|
1020
871
|
|
|
872
|
+
// ../../shared/github/repo-url.ts
|
|
873
|
+
function parseGithubRepoFullName(value) {
|
|
874
|
+
const shorthand = value.match(/^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/);
|
|
875
|
+
if (shorthand) return { owner: shorthand[1], repo: shorthand[2] };
|
|
876
|
+
const ssh = value.match(/git@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
877
|
+
if (ssh) return { owner: ssh[1], repo: ssh[2] };
|
|
878
|
+
const https = value.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
879
|
+
if (https) return { owner: https[1], repo: https[2] };
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/commands/automations.ts
|
|
884
|
+
var AUTOMATION_ERROR_HINTS = {
|
|
885
|
+
invalid_cron: "Use a standard five-field cron expression, for example: */15 * * * *",
|
|
886
|
+
invalid_model: "Model is unknown or not selectable for its backend. Run `arcanist automations create --help` for allowed models.",
|
|
887
|
+
repo_not_available: "Check that the token owner has GitHub access and Arcanist is installed for that repo.",
|
|
888
|
+
duplicate_rule: "An enabled automation already exists for this repo, cron, and prompt.",
|
|
889
|
+
rule_cap_reached: "Delete or disable an existing automation before creating another one. The cap is 20 enabled rules.",
|
|
890
|
+
installation_unresolved: "Reconnect or reinstall the GitHub integration for that repository, then retry.",
|
|
891
|
+
model_not_available: "That model's backend (Claude/opencode) is limited to Arcanist team businesses; use the codex default or a codex model."
|
|
892
|
+
};
|
|
893
|
+
async function createAutomationCommand(repoUrl, promptArg, options, command) {
|
|
894
|
+
const repo = parseAutomationRepo(repoUrl);
|
|
895
|
+
const prompt = await resolvePromptInput(promptArg, options);
|
|
896
|
+
const runtime = getRuntimeOptions(command, options);
|
|
897
|
+
const config = requireConfig(runtime);
|
|
898
|
+
let normalizedModel;
|
|
899
|
+
if (options.model !== void 0) {
|
|
900
|
+
normalizedModel = extractModelId(options.model);
|
|
901
|
+
const backend = resolveAutomationModelBackend(options.model, normalizedModel);
|
|
902
|
+
if (normalizedModel === void 0 || !isSessionStartModelAllowedForBackend(normalizedModel, backend)) {
|
|
903
|
+
const allowed = getSessionStartModelIdsForBackend(backend).join(", ");
|
|
904
|
+
throw new CliError("user", `Model '${options.model}' is not selectable. Allowed: ${allowed}.`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
const body = {
|
|
908
|
+
repoOwner: repo.owner,
|
|
909
|
+
repoName: repo.repo,
|
|
910
|
+
cron: options.cron,
|
|
911
|
+
prompt
|
|
912
|
+
};
|
|
913
|
+
if (options.name !== void 0) body.name = options.name;
|
|
914
|
+
if (options.model !== void 0) body.modelId = normalizedModel;
|
|
915
|
+
const payload = await automationApiFetch(config, "/api/automation/schedules", {
|
|
916
|
+
method: "POST",
|
|
917
|
+
body: JSON.stringify(body)
|
|
918
|
+
});
|
|
919
|
+
if (isJson(command, options)) {
|
|
920
|
+
writeJson(payload.data);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
printAutomationRule(payload.data);
|
|
924
|
+
}
|
|
925
|
+
async function listAutomationsCommand(options, command) {
|
|
926
|
+
const runtime = getRuntimeOptions(command, options);
|
|
927
|
+
const config = requireConfig(runtime);
|
|
928
|
+
const items = [];
|
|
929
|
+
let cursor = options.cursor;
|
|
930
|
+
let nextCursor = null;
|
|
931
|
+
do {
|
|
932
|
+
const query = new URLSearchParams();
|
|
933
|
+
if (options.limit) query.set("limit", options.limit);
|
|
934
|
+
if (cursor) query.set("cursor", cursor);
|
|
935
|
+
const payload = await automationApiFetch(
|
|
936
|
+
config,
|
|
937
|
+
`/api/automation/schedules${query.size ? `?${query.toString()}` : ""}`
|
|
938
|
+
);
|
|
939
|
+
items.push(...payload.data.items);
|
|
940
|
+
nextCursor = payload.data.nextCursor;
|
|
941
|
+
cursor = nextCursor ?? void 0;
|
|
942
|
+
} while (options.all === true && nextCursor);
|
|
943
|
+
const output = { items, nextCursor: options.all === true ? null : nextCursor };
|
|
944
|
+
if (isJson(command, options)) {
|
|
945
|
+
writeJson(output);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (items.length === 0) {
|
|
949
|
+
console.log("No automations found.");
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
for (const rule of items) {
|
|
953
|
+
console.log(
|
|
954
|
+
`${rule.id} ${rule.repoOwner}/${rule.repoName} ${rule.normalizedCron} ${String(rule.enabled)} ${formatTime(rule.nextFireAt)}`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
if (output.nextCursor) console.log(`Next cursor: ${output.nextCursor}`);
|
|
958
|
+
}
|
|
959
|
+
async function deleteAutomationCommand(id, options, command) {
|
|
960
|
+
if (isJson(command, options) && options.yes !== true) {
|
|
961
|
+
throw new CliError("user", "`automations delete --json` requires --yes.");
|
|
962
|
+
}
|
|
963
|
+
if (options.yes !== true) {
|
|
964
|
+
await confirmOrThrow(`Delete automation ${id}?`);
|
|
965
|
+
}
|
|
966
|
+
const runtime = getRuntimeOptions(command, options);
|
|
967
|
+
const config = requireConfig(runtime);
|
|
968
|
+
await automationApiFetchText(config, `/api/automation/schedules/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
969
|
+
if (isJson(command, options)) {
|
|
970
|
+
writeJson({ ok: true, id });
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
console.log(`Deleted automation ${id}.`);
|
|
974
|
+
}
|
|
975
|
+
function parseAutomationRepo(value) {
|
|
976
|
+
if (/^git@/i.test(value) && !/^git@github\.com:/i.test(value)) {
|
|
977
|
+
throw new CliError("user", "repo-url must be owner/name or a GitHub URL.");
|
|
978
|
+
}
|
|
979
|
+
const parsed = parseGithubRepoFullName(value);
|
|
980
|
+
if (!parsed) throw new CliError("user", "repo-url must be owner/name or a GitHub URL.");
|
|
981
|
+
return { owner: parsed.owner, repo: parsed.repo };
|
|
982
|
+
}
|
|
983
|
+
function resolveAutomationModelBackend(rawModel, normalizedModel) {
|
|
984
|
+
if (normalizedModel) {
|
|
985
|
+
const backend = getAgentRuntimeBackendForModel(normalizedModel);
|
|
986
|
+
if (backend) return backend;
|
|
987
|
+
}
|
|
988
|
+
const provider = rawModel.match(/^([a-z]+)[/:]/i)?.[1]?.toLowerCase();
|
|
989
|
+
if (provider === "anthropic") return CLAUDE_CODE_AGENT_RUNTIME_BACKEND;
|
|
990
|
+
if (provider === "baseten") return OPENCODE_AGENT_RUNTIME_BACKEND;
|
|
991
|
+
return CODEX_AGENT_RUNTIME_BACKEND;
|
|
992
|
+
}
|
|
993
|
+
async function automationApiFetch(config, path, init) {
|
|
994
|
+
try {
|
|
995
|
+
return await apiFetch(config, path, init);
|
|
996
|
+
} catch (err) {
|
|
997
|
+
throw mapAutomationApiError(err);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
async function automationApiFetchText(config, path, init) {
|
|
1001
|
+
try {
|
|
1002
|
+
return await apiFetchText(config, path, init);
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
throw mapAutomationApiError(err);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function mapAutomationApiError(err) {
|
|
1008
|
+
if (!(err instanceof ApiError)) {
|
|
1009
|
+
return err instanceof CliError ? err : new CliError("server", err instanceof Error ? err.message : String(err));
|
|
1010
|
+
}
|
|
1011
|
+
const parsed = parseApiErrorBody2(err.body);
|
|
1012
|
+
const serverCode = parsed?.error;
|
|
1013
|
+
return new CliError(codeForStatus(err.status), parsed?.message || serverCode || err.message, {
|
|
1014
|
+
exitCode: err.exitCode,
|
|
1015
|
+
hint: serverCode ? AUTOMATION_ERROR_HINTS[serverCode] : void 0,
|
|
1016
|
+
requestId: err.requestId
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
function parseApiErrorBody2(body) {
|
|
1020
|
+
try {
|
|
1021
|
+
const parsed = JSON.parse(body);
|
|
1022
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
1023
|
+
const record = parsed;
|
|
1024
|
+
return {
|
|
1025
|
+
error: typeof record.error === "string" ? record.error : void 0,
|
|
1026
|
+
message: typeof record.message === "string" ? record.message : void 0
|
|
1027
|
+
};
|
|
1028
|
+
} catch {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function codeForStatus(status) {
|
|
1033
|
+
if (status === 401 || status === 403) return "auth";
|
|
1034
|
+
if (status === 404) return "not_found";
|
|
1035
|
+
if (status === 409) return "conflict";
|
|
1036
|
+
if (status >= 500) return "server";
|
|
1037
|
+
return "user";
|
|
1038
|
+
}
|
|
1039
|
+
function printAutomationRule(rule) {
|
|
1040
|
+
console.log(`ID: ${rule.id}`);
|
|
1041
|
+
console.log(`Repo: ${rule.repoOwner}/${rule.repoName}`);
|
|
1042
|
+
console.log(`Cron: ${rule.normalizedCron}`);
|
|
1043
|
+
if (rule.modelId) console.log(`Model: ${rule.modelId}`);
|
|
1044
|
+
console.log(`Next fire: ${formatTime(rule.nextFireAt)}`);
|
|
1045
|
+
}
|
|
1046
|
+
function formatTime(value) {
|
|
1047
|
+
return typeof value === "number" ? new Date(value).toISOString() : "none";
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1021
1050
|
// ../../shared/utils/timing.ts
|
|
1022
1051
|
function sleep(ms) {
|
|
1023
1052
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -1351,6 +1380,28 @@ function getRawSessionEventKind(event) {
|
|
|
1351
1380
|
return bridgeEventType ?? event.phase;
|
|
1352
1381
|
}
|
|
1353
1382
|
}
|
|
1383
|
+
function getRawSessionEventTimestamp(event) {
|
|
1384
|
+
if (isCanonicalRawSessionEvent(event)) {
|
|
1385
|
+
return Number.isFinite(event.timestampMs) && Number.isFinite(new Date(event.timestampMs).getTime()) ? new Date(event.timestampMs).toISOString() : void 0;
|
|
1386
|
+
}
|
|
1387
|
+
const dataTimestamp = event.data?.timestamp;
|
|
1388
|
+
if (typeof dataTimestamp === "string" && dataTimestamp.length > 0 || typeof dataTimestamp === "number") {
|
|
1389
|
+
const parsed = new Date(dataTimestamp);
|
|
1390
|
+
if (Number.isFinite(parsed.getTime())) return parsed.toISOString();
|
|
1391
|
+
}
|
|
1392
|
+
const topLevelTimestamp = event.timestamp;
|
|
1393
|
+
if (typeof topLevelTimestamp === "string" && topLevelTimestamp.length > 0 || typeof topLevelTimestamp === "number") {
|
|
1394
|
+
const parsed = new Date(topLevelTimestamp);
|
|
1395
|
+
if (Number.isFinite(parsed.getTime())) return parsed.toISOString();
|
|
1396
|
+
}
|
|
1397
|
+
return void 0;
|
|
1398
|
+
}
|
|
1399
|
+
function getRawSessionEventTimestampMs(event) {
|
|
1400
|
+
const iso = getRawSessionEventTimestamp(event);
|
|
1401
|
+
if (!iso) return void 0;
|
|
1402
|
+
const ms = Date.parse(iso);
|
|
1403
|
+
return Number.isFinite(ms) ? ms : void 0;
|
|
1404
|
+
}
|
|
1354
1405
|
function getRawSessionEventData(event) {
|
|
1355
1406
|
if (!isCanonicalRawSessionEvent(event)) {
|
|
1356
1407
|
return isRecord(event.data) ? event.data : void 0;
|
|
@@ -1500,7 +1551,7 @@ function canAppendAcrossNonContentInterstitials(events, state, key, existingIdx)
|
|
|
1500
1551
|
function resolvePromptId(data) {
|
|
1501
1552
|
return typeof data?.promptId === "string" ? data.promptId : void 0;
|
|
1502
1553
|
}
|
|
1503
|
-
function coalesceStreamDelta(events, state, type, streamId, text, promptId) {
|
|
1554
|
+
function coalesceStreamDelta(events, state, type, streamId, text, promptId, timestampMs) {
|
|
1504
1555
|
const key = streamableKey(type, streamId);
|
|
1505
1556
|
const existingText = state.concatByStreamId.get(key) ?? "";
|
|
1506
1557
|
const existingIdx = state.streamableIndexById.get(key);
|
|
@@ -1511,6 +1562,10 @@ function coalesceStreamDelta(events, state, type, streamId, text, promptId) {
|
|
|
1511
1562
|
existing.text += text;
|
|
1512
1563
|
state.concatByStreamId.set(key, existingText + text);
|
|
1513
1564
|
if (!existing.promptId && promptId) existing.promptId = promptId;
|
|
1565
|
+
if (existing.type === "reasoning" && timestampMs !== void 0) {
|
|
1566
|
+
if (existing.startedAtMs === void 0) existing.startedAtMs = timestampMs;
|
|
1567
|
+
existing.endedAtMs = timestampMs;
|
|
1568
|
+
}
|
|
1514
1569
|
return true;
|
|
1515
1570
|
}
|
|
1516
1571
|
if (existingIdx === events.length - 1 || !appendableText) return false;
|
|
@@ -1536,7 +1591,8 @@ function coalesceStreamDelta(events, state, type, streamId, text, promptId) {
|
|
|
1536
1591
|
id: resolveSegmentId(streamId, segmentOrdinal),
|
|
1537
1592
|
...segmentOrdinal > 0 ? { streamId } : {},
|
|
1538
1593
|
text,
|
|
1539
|
-
...promptId ? { promptId } : {}
|
|
1594
|
+
...promptId ? { promptId } : {},
|
|
1595
|
+
...timestampMs !== void 0 ? { startedAtMs: timestampMs, endedAtMs: timestampMs } : {}
|
|
1540
1596
|
});
|
|
1541
1597
|
}
|
|
1542
1598
|
return true;
|
|
@@ -1810,14 +1866,15 @@ function projectPatch(data, index) {
|
|
|
1810
1866
|
...resolvePromptId(data) ? { promptId: resolvePromptId(data) } : {}
|
|
1811
1867
|
};
|
|
1812
1868
|
}
|
|
1813
|
-
function projectStream(type, data, state) {
|
|
1869
|
+
function projectStream(type, data, state, timestampMs) {
|
|
1814
1870
|
coalesceStreamDelta(
|
|
1815
1871
|
state.merged,
|
|
1816
1872
|
state.streams,
|
|
1817
1873
|
type,
|
|
1818
1874
|
resolveEventId(data, type, state.merged.length),
|
|
1819
1875
|
resolveTextValue(data),
|
|
1820
|
-
resolvePromptId(data)
|
|
1876
|
+
resolvePromptId(data),
|
|
1877
|
+
timestampMs
|
|
1821
1878
|
);
|
|
1822
1879
|
}
|
|
1823
1880
|
function projectToolCall(data, state) {
|
|
@@ -1959,7 +2016,7 @@ function flattenSessionEvents(raw) {
|
|
|
1959
2016
|
pushEvent(state, projectRawCodex(data, state.merged.length));
|
|
1960
2017
|
break;
|
|
1961
2018
|
case "reasoning":
|
|
1962
|
-
projectStream("reasoning", data, state);
|
|
2019
|
+
projectStream("reasoning", data, state, getRawSessionEventTimestampMs(normalized.raw));
|
|
1963
2020
|
break;
|
|
1964
2021
|
case "text":
|
|
1965
2022
|
projectStream("text", data, state);
|
|
@@ -4697,11 +4754,15 @@ Examples:
|
|
|
4697
4754
|
`
|
|
4698
4755
|
).action((sessionId, options, command) => usageCommand(sessionId, options, command));
|
|
4699
4756
|
var automations = program.command("automations").description("Automation commands");
|
|
4700
|
-
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(
|
|
4757
|
+
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(
|
|
4758
|
+
"--model <model>",
|
|
4759
|
+
"Model to pin for this automation's sessions (e.g. gpt-5.5, claude-opus-4-8). Defaults to the codex backend default."
|
|
4760
|
+
).option("--prompt-stdin", "Read prompt from stdin").addHelpText(
|
|
4701
4761
|
"after",
|
|
4702
4762
|
`
|
|
4703
4763
|
Examples:
|
|
4704
4764
|
arcanist automations create tryarcanist/arcanist "summarize regressions" --cron "*/15 * * * *"
|
|
4765
|
+
arcanist automations create tryarcanist/arcanist "audit N+1s" --cron "0 13 * * 5" --model gpt-5.5
|
|
4705
4766
|
printf "summarize regressions" | arcanist automations create tryarcanist/arcanist --prompt-stdin --cron "*/15 * * * *" --json
|
|
4706
4767
|
`
|
|
4707
4768
|
).action((repoUrl, prompt, options, command) => createAutomationCommand(repoUrl, prompt, options, command));
|