@tryarcanist/cli 0.1.163 → 0.1.164
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 +221 -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,183 @@ 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
|
+
console.log(`Next fire: ${formatTime(rule.nextFireAt)}`);
|
|
1044
|
+
}
|
|
1045
|
+
function formatTime(value) {
|
|
1046
|
+
return typeof value === "number" ? new Date(value).toISOString() : "none";
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1021
1049
|
// ../../shared/utils/timing.ts
|
|
1022
1050
|
function sleep(ms) {
|
|
1023
1051
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -1351,6 +1379,28 @@ function getRawSessionEventKind(event) {
|
|
|
1351
1379
|
return bridgeEventType ?? event.phase;
|
|
1352
1380
|
}
|
|
1353
1381
|
}
|
|
1382
|
+
function getRawSessionEventTimestamp(event) {
|
|
1383
|
+
if (isCanonicalRawSessionEvent(event)) {
|
|
1384
|
+
return Number.isFinite(event.timestampMs) && Number.isFinite(new Date(event.timestampMs).getTime()) ? new Date(event.timestampMs).toISOString() : void 0;
|
|
1385
|
+
}
|
|
1386
|
+
const dataTimestamp = event.data?.timestamp;
|
|
1387
|
+
if (typeof dataTimestamp === "string" && dataTimestamp.length > 0 || typeof dataTimestamp === "number") {
|
|
1388
|
+
const parsed = new Date(dataTimestamp);
|
|
1389
|
+
if (Number.isFinite(parsed.getTime())) return parsed.toISOString();
|
|
1390
|
+
}
|
|
1391
|
+
const topLevelTimestamp = event.timestamp;
|
|
1392
|
+
if (typeof topLevelTimestamp === "string" && topLevelTimestamp.length > 0 || typeof topLevelTimestamp === "number") {
|
|
1393
|
+
const parsed = new Date(topLevelTimestamp);
|
|
1394
|
+
if (Number.isFinite(parsed.getTime())) return parsed.toISOString();
|
|
1395
|
+
}
|
|
1396
|
+
return void 0;
|
|
1397
|
+
}
|
|
1398
|
+
function getRawSessionEventTimestampMs(event) {
|
|
1399
|
+
const iso = getRawSessionEventTimestamp(event);
|
|
1400
|
+
if (!iso) return void 0;
|
|
1401
|
+
const ms = Date.parse(iso);
|
|
1402
|
+
return Number.isFinite(ms) ? ms : void 0;
|
|
1403
|
+
}
|
|
1354
1404
|
function getRawSessionEventData(event) {
|
|
1355
1405
|
if (!isCanonicalRawSessionEvent(event)) {
|
|
1356
1406
|
return isRecord(event.data) ? event.data : void 0;
|
|
@@ -1500,7 +1550,7 @@ function canAppendAcrossNonContentInterstitials(events, state, key, existingIdx)
|
|
|
1500
1550
|
function resolvePromptId(data) {
|
|
1501
1551
|
return typeof data?.promptId === "string" ? data.promptId : void 0;
|
|
1502
1552
|
}
|
|
1503
|
-
function coalesceStreamDelta(events, state, type, streamId, text, promptId) {
|
|
1553
|
+
function coalesceStreamDelta(events, state, type, streamId, text, promptId, timestampMs) {
|
|
1504
1554
|
const key = streamableKey(type, streamId);
|
|
1505
1555
|
const existingText = state.concatByStreamId.get(key) ?? "";
|
|
1506
1556
|
const existingIdx = state.streamableIndexById.get(key);
|
|
@@ -1511,6 +1561,10 @@ function coalesceStreamDelta(events, state, type, streamId, text, promptId) {
|
|
|
1511
1561
|
existing.text += text;
|
|
1512
1562
|
state.concatByStreamId.set(key, existingText + text);
|
|
1513
1563
|
if (!existing.promptId && promptId) existing.promptId = promptId;
|
|
1564
|
+
if (existing.type === "reasoning" && timestampMs !== void 0) {
|
|
1565
|
+
if (existing.startedAtMs === void 0) existing.startedAtMs = timestampMs;
|
|
1566
|
+
existing.endedAtMs = timestampMs;
|
|
1567
|
+
}
|
|
1514
1568
|
return true;
|
|
1515
1569
|
}
|
|
1516
1570
|
if (existingIdx === events.length - 1 || !appendableText) return false;
|
|
@@ -1536,7 +1590,8 @@ function coalesceStreamDelta(events, state, type, streamId, text, promptId) {
|
|
|
1536
1590
|
id: resolveSegmentId(streamId, segmentOrdinal),
|
|
1537
1591
|
...segmentOrdinal > 0 ? { streamId } : {},
|
|
1538
1592
|
text,
|
|
1539
|
-
...promptId ? { promptId } : {}
|
|
1593
|
+
...promptId ? { promptId } : {},
|
|
1594
|
+
...timestampMs !== void 0 ? { startedAtMs: timestampMs, endedAtMs: timestampMs } : {}
|
|
1540
1595
|
});
|
|
1541
1596
|
}
|
|
1542
1597
|
return true;
|
|
@@ -1810,14 +1865,15 @@ function projectPatch(data, index) {
|
|
|
1810
1865
|
...resolvePromptId(data) ? { promptId: resolvePromptId(data) } : {}
|
|
1811
1866
|
};
|
|
1812
1867
|
}
|
|
1813
|
-
function projectStream(type, data, state) {
|
|
1868
|
+
function projectStream(type, data, state, timestampMs) {
|
|
1814
1869
|
coalesceStreamDelta(
|
|
1815
1870
|
state.merged,
|
|
1816
1871
|
state.streams,
|
|
1817
1872
|
type,
|
|
1818
1873
|
resolveEventId(data, type, state.merged.length),
|
|
1819
1874
|
resolveTextValue(data),
|
|
1820
|
-
resolvePromptId(data)
|
|
1875
|
+
resolvePromptId(data),
|
|
1876
|
+
timestampMs
|
|
1821
1877
|
);
|
|
1822
1878
|
}
|
|
1823
1879
|
function projectToolCall(data, state) {
|
|
@@ -1959,7 +2015,7 @@ function flattenSessionEvents(raw) {
|
|
|
1959
2015
|
pushEvent(state, projectRawCodex(data, state.merged.length));
|
|
1960
2016
|
break;
|
|
1961
2017
|
case "reasoning":
|
|
1962
|
-
projectStream("reasoning", data, state);
|
|
2018
|
+
projectStream("reasoning", data, state, getRawSessionEventTimestampMs(normalized.raw));
|
|
1963
2019
|
break;
|
|
1964
2020
|
case "text":
|
|
1965
2021
|
projectStream("text", data, state);
|
|
@@ -4697,11 +4753,15 @@ Examples:
|
|
|
4697
4753
|
`
|
|
4698
4754
|
).action((sessionId, options, command) => usageCommand(sessionId, options, command));
|
|
4699
4755
|
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(
|
|
4756
|
+
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
|
+
"--model <model>",
|
|
4758
|
+
"Model to pin for this automation's sessions (e.g. gpt-5.5, claude-opus-4-8). Defaults to the codex backend default."
|
|
4759
|
+
).option("--prompt-stdin", "Read prompt from stdin").addHelpText(
|
|
4701
4760
|
"after",
|
|
4702
4761
|
`
|
|
4703
4762
|
Examples:
|
|
4704
4763
|
arcanist automations create tryarcanist/arcanist "summarize regressions" --cron "*/15 * * * *"
|
|
4764
|
+
arcanist automations create tryarcanist/arcanist "audit N+1s" --cron "0 13 * * 5" --model gpt-5.5
|
|
4705
4765
|
printf "summarize regressions" | arcanist automations create tryarcanist/arcanist --prompt-stdin --cron "*/15 * * * *" --json
|
|
4706
4766
|
`
|
|
4707
4767
|
).action((repoUrl, prompt, options, command) => createAutomationCommand(repoUrl, prompt, options, command));
|