@inteeka/task-cli 0.1.0 → 0.1.1
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/dist/cli.js +644 -79
- package/dist/cli.js.map +1 -1
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -635,15 +635,15 @@ function registerLogin(program2) {
|
|
|
635
635
|
noBrowser: !opts.browser,
|
|
636
636
|
silent: cfg.silent
|
|
637
637
|
});
|
|
638
|
-
const
|
|
639
|
-
if (!
|
|
638
|
+
const access2 = await apiCall("GET", "/api/v1/cli/access");
|
|
639
|
+
if (!access2.ok || !access2.data) {
|
|
640
640
|
await clearCredentials();
|
|
641
641
|
throw new CliError(
|
|
642
642
|
CLI_EXIT_CODES.UNAUTHORISED,
|
|
643
643
|
"Authentication succeeded but /cli/access did not return a result"
|
|
644
644
|
);
|
|
645
645
|
}
|
|
646
|
-
if (!
|
|
646
|
+
if (!access2.data.has_access) {
|
|
647
647
|
await clearCredentials();
|
|
648
648
|
throw new CliError(
|
|
649
649
|
CLI_EXIT_CODES.UNAUTHORISED,
|
|
@@ -655,14 +655,14 @@ function registerLogin(program2) {
|
|
|
655
655
|
if (stored) {
|
|
656
656
|
await writeCredentials({
|
|
657
657
|
...stored,
|
|
658
|
-
email:
|
|
658
|
+
email: access2.data.email
|
|
659
659
|
});
|
|
660
660
|
}
|
|
661
|
-
process.stdout.write(`${c.ok("\u2713")} Signed in as ${c.bold(
|
|
661
|
+
process.stdout.write(`${c.ok("\u2713")} Signed in as ${c.bold(access2.data.email)}
|
|
662
662
|
`);
|
|
663
663
|
process.stdout.write(` Session: ${c.dim(result.sessionId)}
|
|
664
664
|
`);
|
|
665
|
-
const projectCount =
|
|
665
|
+
const projectCount = access2.data.projects.length;
|
|
666
666
|
process.stdout.write(
|
|
667
667
|
` ${projectCount} project${projectCount === 1 ? "" : "s"} authorised. Run ${c.cyan("task projects")} to list them.
|
|
668
668
|
`
|
|
@@ -700,8 +700,8 @@ function registerWhoami(program2) {
|
|
|
700
700
|
`);
|
|
701
701
|
return;
|
|
702
702
|
}
|
|
703
|
-
const
|
|
704
|
-
process.stdout.write(`${c.bold(
|
|
703
|
+
const access2 = await apiCallOrThrow("GET", "/api/v1/cli/access");
|
|
704
|
+
process.stdout.write(`${c.bold(access2.email || creds.email || access2.user_id)}
|
|
705
705
|
`);
|
|
706
706
|
process.stdout.write(` API: ${creds.api_url}
|
|
707
707
|
`);
|
|
@@ -711,9 +711,9 @@ function registerWhoami(program2) {
|
|
|
711
711
|
`);
|
|
712
712
|
process.stdout.write(` Refresh expires: ${creds.refresh_expires_at}
|
|
713
713
|
`);
|
|
714
|
-
process.stdout.write(` Projects: ${
|
|
714
|
+
process.stdout.write(` Projects: ${access2.projects.length} authorised
|
|
715
715
|
`);
|
|
716
|
-
for (const p of
|
|
716
|
+
for (const p of access2.projects) {
|
|
717
717
|
process.stdout.write(
|
|
718
718
|
` \u2022 ${c.bold(p.name)} ${c.dim(`(${p.organisation_slug}/${p.slug})`)} \u2014 ${p.cli_eligible_count} eligible
|
|
719
719
|
`
|
|
@@ -734,6 +734,9 @@ function registerAuthRefresh(program2) {
|
|
|
734
734
|
}
|
|
735
735
|
|
|
736
736
|
// src/commands/link.ts
|
|
737
|
+
import { readFile as readFile4, writeFile as writeFile4, appendFile, access } from "fs/promises";
|
|
738
|
+
import { constants as fsConstants } from "fs";
|
|
739
|
+
import { join as join4 } from "path";
|
|
737
740
|
import inquirer from "inquirer";
|
|
738
741
|
|
|
739
742
|
// src/config/project.ts
|
|
@@ -777,7 +780,7 @@ async function clearProjectConfig(repoRoot) {
|
|
|
777
780
|
|
|
778
781
|
// src/commands/link.ts
|
|
779
782
|
function registerLink(program2) {
|
|
780
|
-
program2.command("link").description("Link the current repo to a project").option("--org <slug>", "Org slug").option("--project <slug>", "Project slug").action(async (opts) => {
|
|
783
|
+
program2.command("link").description("Link the current repo to a project").option("--org <slug>", "Org slug \u2014 must be combined with --project").option("--project <slug>", "Project slug \u2014 must be combined with --org").action(async (opts) => {
|
|
781
784
|
const creds = await readCredentials();
|
|
782
785
|
if (!creds) {
|
|
783
786
|
throw new CliError(
|
|
@@ -786,40 +789,15 @@ function registerLink(program2) {
|
|
|
786
789
|
"Run 'task login' first."
|
|
787
790
|
);
|
|
788
791
|
}
|
|
789
|
-
const
|
|
790
|
-
if (
|
|
792
|
+
const accessResp = await apiCallOrThrow("GET", "/api/v1/cli/access");
|
|
793
|
+
if (accessResp.projects.length === 0) {
|
|
791
794
|
throw new CliError(
|
|
792
795
|
CLI_EXIT_CODES.UNAUTHORISED,
|
|
793
796
|
"No projects authorised for your account",
|
|
794
797
|
"Ask an admin to grant CLI access on the Agentic CLI page."
|
|
795
798
|
);
|
|
796
799
|
}
|
|
797
|
-
|
|
798
|
-
(p) => (opts.org ? p.organisation_slug === opts.org : true) && (opts.project ? p.slug === opts.project : true)
|
|
799
|
-
);
|
|
800
|
-
if (!chosen) {
|
|
801
|
-
if (opts.org || opts.project) {
|
|
802
|
-
throw new CliError(
|
|
803
|
-
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
804
|
-
"No matching project found among your authorised projects"
|
|
805
|
-
);
|
|
806
|
-
}
|
|
807
|
-
const answer = await inquirer.prompt([
|
|
808
|
-
{
|
|
809
|
-
type: "list",
|
|
810
|
-
name: "projectId",
|
|
811
|
-
message: "Select a project to link this repo to:",
|
|
812
|
-
choices: access.projects.map((p) => ({
|
|
813
|
-
name: `${p.name} (${p.organisation_slug}/${p.slug}) \u2014 ${p.cli_eligible_count} eligible tickets`,
|
|
814
|
-
value: p.id
|
|
815
|
-
}))
|
|
816
|
-
}
|
|
817
|
-
]);
|
|
818
|
-
chosen = access.projects.find((p) => p.id === answer.projectId);
|
|
819
|
-
}
|
|
820
|
-
if (!chosen) {
|
|
821
|
-
throw new CliError(CLI_EXIT_CODES.GENERIC_ERROR, "No project selected");
|
|
822
|
-
}
|
|
800
|
+
const chosen = await resolveProject(accessResp.projects, opts);
|
|
823
801
|
const repoRoot = findRepoRoot();
|
|
824
802
|
await writeProjectConfig(
|
|
825
803
|
{
|
|
@@ -833,12 +811,77 @@ function registerLink(program2) {
|
|
|
833
811
|
},
|
|
834
812
|
repoRoot
|
|
835
813
|
);
|
|
814
|
+
const gitignoreOutcome = await ensureGitignored(repoRoot);
|
|
836
815
|
process.stdout.write(
|
|
837
816
|
`${c.ok("\u2713")} Linked ${c.bold(repoRoot)} \u2192 ${c.bold(`${chosen.organisation_slug}/${chosen.slug}`)}
|
|
838
817
|
`
|
|
839
818
|
);
|
|
819
|
+
if (gitignoreOutcome === "added") {
|
|
820
|
+
process.stdout.write(`${c.dim(" Added")} ${c.cyan(".task/")} ${c.dim("to .gitignore")}
|
|
821
|
+
`);
|
|
822
|
+
} else if (gitignoreOutcome === "created") {
|
|
823
|
+
process.stdout.write(
|
|
824
|
+
`${c.dim(" Created")} ${c.cyan(".gitignore")} ${c.dim("with")} ${c.cyan(".task/")}
|
|
825
|
+
`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
840
828
|
});
|
|
841
829
|
}
|
|
830
|
+
async function resolveProject(projects, opts) {
|
|
831
|
+
if (opts.org && !opts.project || !opts.org && opts.project) {
|
|
832
|
+
throw new CliError(
|
|
833
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
834
|
+
"--org and --project must be supplied together",
|
|
835
|
+
"Either pass both flags or run `task link` with no flags to pick interactively."
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
if (opts.org && opts.project) {
|
|
839
|
+
const match = projects.find((p) => p.organisation_slug === opts.org && p.slug === opts.project);
|
|
840
|
+
if (!match) {
|
|
841
|
+
throw new CliError(
|
|
842
|
+
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
843
|
+
`No project ${opts.org}/${opts.project} among your authorised projects`,
|
|
844
|
+
"Run `task projects` to see what you have access to."
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
return match;
|
|
848
|
+
}
|
|
849
|
+
const answer = await inquirer.prompt([
|
|
850
|
+
{
|
|
851
|
+
type: "list",
|
|
852
|
+
name: "projectId",
|
|
853
|
+
message: "Select a project to link this repo to:",
|
|
854
|
+
choices: projects.map((p) => ({
|
|
855
|
+
name: `${p.name} (${p.organisation_slug}/${p.slug}) \u2014 ${p.cli_eligible_count} eligible tickets`,
|
|
856
|
+
value: p.id
|
|
857
|
+
}))
|
|
858
|
+
}
|
|
859
|
+
]);
|
|
860
|
+
const picked = projects.find((p) => p.id === answer.projectId);
|
|
861
|
+
if (!picked) {
|
|
862
|
+
throw new CliError(CLI_EXIT_CODES.GENERIC_ERROR, "No project selected");
|
|
863
|
+
}
|
|
864
|
+
return picked;
|
|
865
|
+
}
|
|
866
|
+
async function ensureGitignored(repoRoot) {
|
|
867
|
+
const gitignorePath = join4(repoRoot, ".gitignore");
|
|
868
|
+
let existing = null;
|
|
869
|
+
try {
|
|
870
|
+
await access(gitignorePath, fsConstants.F_OK);
|
|
871
|
+
existing = await readFile4(gitignorePath, "utf8");
|
|
872
|
+
} catch {
|
|
873
|
+
}
|
|
874
|
+
const PATTERNS = [".task/", ".task", "/.task/", "/.task"];
|
|
875
|
+
if (existing !== null) {
|
|
876
|
+
const lines = existing.split("\n").map((l) => l.trim());
|
|
877
|
+
if (lines.some((l) => PATTERNS.includes(l))) return "noop";
|
|
878
|
+
const block = (existing.endsWith("\n") ? "" : "\n") + "\n# task CLI link config\n.task/\n";
|
|
879
|
+
await appendFile(gitignorePath, block);
|
|
880
|
+
return "added";
|
|
881
|
+
}
|
|
882
|
+
await writeFile4(gitignorePath, "# task CLI link config\n.task/\n");
|
|
883
|
+
return "created";
|
|
884
|
+
}
|
|
842
885
|
|
|
843
886
|
// src/commands/unlink.ts
|
|
844
887
|
function registerUnlink(program2) {
|
|
@@ -853,14 +896,14 @@ function registerUnlink(program2) {
|
|
|
853
896
|
// src/commands/projects.ts
|
|
854
897
|
function registerProjects(program2) {
|
|
855
898
|
program2.command("projects").description("List projects the CLI is authorised for").action(async () => {
|
|
856
|
-
const
|
|
857
|
-
if (
|
|
899
|
+
const access2 = await apiCallOrThrow("GET", "/api/v1/cli/access");
|
|
900
|
+
if (access2.projects.length === 0) {
|
|
858
901
|
process.stdout.write(`${c.dim("No projects authorised.")}
|
|
859
902
|
`);
|
|
860
903
|
return;
|
|
861
904
|
}
|
|
862
905
|
const headers = ["NAME", "ORG", "SLUG", "ELIGIBLE", "PROTECTED"];
|
|
863
|
-
const rows =
|
|
906
|
+
const rows = access2.projects.map((p) => [
|
|
864
907
|
p.name,
|
|
865
908
|
p.organisation_slug,
|
|
866
909
|
p.slug,
|
|
@@ -1065,9 +1108,9 @@ import inquirer2 from "inquirer";
|
|
|
1065
1108
|
|
|
1066
1109
|
// src/agent/agent-service.ts
|
|
1067
1110
|
import { spawn } from "child_process";
|
|
1068
|
-
import { mkdir as mkdir4, writeFile as
|
|
1111
|
+
import { mkdir as mkdir4, writeFile as writeFile5 } from "fs/promises";
|
|
1069
1112
|
import { homedir as homedir3 } from "os";
|
|
1070
|
-
import { join as
|
|
1113
|
+
import { join as join5 } from "path";
|
|
1071
1114
|
|
|
1072
1115
|
// src/agent/allowed-tools.ts
|
|
1073
1116
|
var ALLOWED_TOOLS = CLI_ALLOWED_TOOLS;
|
|
@@ -1124,10 +1167,10 @@ async function runAgent(args) {
|
|
|
1124
1167
|
let outputLogPath = null;
|
|
1125
1168
|
let logHandle = null;
|
|
1126
1169
|
if (args.silent) {
|
|
1127
|
-
const dir =
|
|
1170
|
+
const dir = join5(homedir3(), ".cache", "task", "runs");
|
|
1128
1171
|
await mkdir4(dir, { recursive: true });
|
|
1129
|
-
outputLogPath =
|
|
1130
|
-
await
|
|
1172
|
+
outputLogPath = join5(dir, `${args.runId}.log`);
|
|
1173
|
+
await writeFile5(outputLogPath, "");
|
|
1131
1174
|
const { createWriteStream } = await import("fs");
|
|
1132
1175
|
logHandle = createWriteStream(outputLogPath, { flags: "a" });
|
|
1133
1176
|
}
|
|
@@ -1316,7 +1359,11 @@ async function runWork(ticketId, opts) {
|
|
|
1316
1359
|
const targetId = nextTicketId ?? (opts.auto || opts.next ? await pickNextEligible(project.project_id) : await promptForTicket(project.project_id));
|
|
1317
1360
|
if (!targetId) {
|
|
1318
1361
|
if (processed === 0 && !silent) {
|
|
1319
|
-
process.stdout.write(c.dim("No eligible tickets
|
|
1362
|
+
process.stdout.write(c.dim("No CLI-eligible tickets in this project.\n"));
|
|
1363
|
+
process.stdout.write(
|
|
1364
|
+
`${c.dim(" Toggle")} ${c.bold("Allow the agentic CLI to work on this ticket")} ${c.dim("on a ticket from the dashboard, then run")} ${c.cyan("task scan")} ${c.dim("to seed AI fix prompts the agent can act on.")}
|
|
1365
|
+
`
|
|
1366
|
+
);
|
|
1320
1367
|
}
|
|
1321
1368
|
return;
|
|
1322
1369
|
}
|
|
@@ -1499,17 +1546,534 @@ async function promptForTicket(projectId) {
|
|
|
1499
1546
|
return answer.ticketId;
|
|
1500
1547
|
}
|
|
1501
1548
|
|
|
1502
|
-
// src/commands/
|
|
1549
|
+
// src/commands/scan.ts
|
|
1503
1550
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1551
|
+
import ora2 from "ora";
|
|
1552
|
+
|
|
1553
|
+
// src/scan/api.ts
|
|
1554
|
+
import { request as request4 } from "undici";
|
|
1555
|
+
async function jsonRequest(url, init) {
|
|
1556
|
+
const res = await request4(url, {
|
|
1557
|
+
method: init.method,
|
|
1558
|
+
headers: init.headers,
|
|
1559
|
+
body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
|
|
1560
|
+
bodyTimeout: 6e4,
|
|
1561
|
+
headersTimeout: 6e4
|
|
1562
|
+
});
|
|
1563
|
+
let body;
|
|
1564
|
+
try {
|
|
1565
|
+
body = await res.body.json();
|
|
1566
|
+
} catch {
|
|
1567
|
+
body = void 0;
|
|
1568
|
+
}
|
|
1569
|
+
const nonce = res.headers["x-prepare-nonce"];
|
|
1570
|
+
const nonceStr = typeof nonce === "string" ? nonce : Array.isArray(nonce) ? nonce[0] ?? null : null;
|
|
1571
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
1572
|
+
const env = body;
|
|
1573
|
+
return { ok: true, status: res.statusCode, data: env?.data ?? body, nonce: nonceStr };
|
|
1574
|
+
}
|
|
1575
|
+
const errBody = body;
|
|
1576
|
+
return {
|
|
1577
|
+
ok: false,
|
|
1578
|
+
status: res.statusCode,
|
|
1579
|
+
code: errBody?.error?.code ?? `HTTP_${res.statusCode}`,
|
|
1580
|
+
message: errBody?.error?.message ?? `Request failed with status ${res.statusCode}`
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
var AutopilotApi = class {
|
|
1584
|
+
constructor(opts) {
|
|
1585
|
+
this.opts = opts;
|
|
1586
|
+
}
|
|
1587
|
+
adminHeaders() {
|
|
1588
|
+
return {
|
|
1589
|
+
"Content-Type": "application/json",
|
|
1590
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
1591
|
+
"X-Actor-Email": this.opts.actorEmail,
|
|
1592
|
+
"User-Agent": "task-cli/scan"
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
skillHeaders(skillToken, extra = {}) {
|
|
1596
|
+
return {
|
|
1597
|
+
"Content-Type": "application/json",
|
|
1598
|
+
Authorization: `Bearer ${skillToken}`,
|
|
1599
|
+
"User-Agent": "task-cli/scan",
|
|
1600
|
+
...extra
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
async listEligibleProjects() {
|
|
1604
|
+
const url = `${this.opts.apiUrl}/api/v1/cli/projects`;
|
|
1605
|
+
const result = await jsonRequest(url, {
|
|
1606
|
+
method: "GET",
|
|
1607
|
+
headers: this.adminHeaders()
|
|
1608
|
+
});
|
|
1609
|
+
if (!result.ok) {
|
|
1610
|
+
throw new CliError(
|
|
1611
|
+
autopilotExitCode(result.code, result.status),
|
|
1612
|
+
`${result.code}: ${result.message}`
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
return result.data ?? [];
|
|
1616
|
+
}
|
|
1617
|
+
async issueSkillToken(args) {
|
|
1618
|
+
const url = `${this.opts.apiUrl}/api/v1/cli/issue-skill-token`;
|
|
1619
|
+
const result = await jsonRequest(url, {
|
|
1620
|
+
method: "POST",
|
|
1621
|
+
headers: this.adminHeaders(),
|
|
1622
|
+
body: {
|
|
1623
|
+
project_id: args.project_id,
|
|
1624
|
+
scope: "fix_prompt_sync",
|
|
1625
|
+
max_submits: args.max_submits,
|
|
1626
|
+
ttl_minutes: args.ttl_minutes ?? 30
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
if (!result.ok) {
|
|
1630
|
+
throw new CliError(
|
|
1631
|
+
autopilotExitCode(result.code, result.status),
|
|
1632
|
+
`${result.code}: ${result.message}`
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
return result.data;
|
|
1636
|
+
}
|
|
1637
|
+
async prepare(skillToken, batchSize, idempotencyKey) {
|
|
1638
|
+
const url = `${this.opts.apiUrl}/api/v1/cli/fix-prompt-sync/prepare`;
|
|
1639
|
+
const result = await jsonRequest(url, {
|
|
1640
|
+
method: "POST",
|
|
1641
|
+
headers: this.skillHeaders(skillToken, { "Idempotency-Key": idempotencyKey }),
|
|
1642
|
+
body: { batch_size: batchSize }
|
|
1643
|
+
});
|
|
1644
|
+
if (!result.ok) {
|
|
1645
|
+
throw new CliError(
|
|
1646
|
+
autopilotExitCode(result.code, result.status),
|
|
1647
|
+
`${result.code}: ${result.message}`
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
if (!result.data.prepare_nonce && result.nonce) {
|
|
1651
|
+
result.data.prepare_nonce = result.nonce;
|
|
1652
|
+
}
|
|
1653
|
+
return result.data;
|
|
1654
|
+
}
|
|
1655
|
+
async submit(args) {
|
|
1656
|
+
const url = `${this.opts.apiUrl}/api/v1/cli/fix-prompt-sync/submit`;
|
|
1657
|
+
const result = await jsonRequest(url, {
|
|
1658
|
+
method: "POST",
|
|
1659
|
+
headers: this.skillHeaders(args.skillToken, { "X-Prepare-Nonce": args.nonce }),
|
|
1660
|
+
body: {
|
|
1661
|
+
ticket_id: args.ticketId,
|
|
1662
|
+
structured: args.structured,
|
|
1663
|
+
input_tokens: args.inputTokens,
|
|
1664
|
+
output_tokens: args.outputTokens,
|
|
1665
|
+
model: args.model
|
|
1666
|
+
}
|
|
1667
|
+
});
|
|
1668
|
+
if (result.ok) {
|
|
1669
|
+
const status = result.data.ai_fix_status === "needs_review" ? "needs_review" : "ready";
|
|
1670
|
+
return { status, denylistHit: result.data.denylist_hit === true };
|
|
1671
|
+
}
|
|
1672
|
+
if (result.code === "CLAIM_MISMATCH" || result.code === "BAD_STATUS" || result.code === "WRONG_SCOPE" || result.code === "OUTPUT_VALIDATION_FAILED") {
|
|
1673
|
+
return { status: "skip", reason: result.code };
|
|
1674
|
+
}
|
|
1675
|
+
throw new CliError(
|
|
1676
|
+
autopilotExitCode(result.code, result.status),
|
|
1677
|
+
`${result.code}: ${result.message}`
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
async abort(skillToken, ticketIds) {
|
|
1681
|
+
if (ticketIds.length === 0) return;
|
|
1682
|
+
const url = `${this.opts.apiUrl}/api/v1/cli/fix-prompt-sync/abort`;
|
|
1683
|
+
await jsonRequest(url, {
|
|
1684
|
+
method: "POST",
|
|
1685
|
+
headers: this.skillHeaders(skillToken),
|
|
1686
|
+
body: { ticket_ids: ticketIds }
|
|
1687
|
+
}).catch(() => void 0);
|
|
1688
|
+
}
|
|
1689
|
+
async runSummary(skillToken, summary) {
|
|
1690
|
+
const url = `${this.opts.apiUrl}/api/v1/cli/fix-prompt-sync/run-summary`;
|
|
1691
|
+
await jsonRequest(url, {
|
|
1692
|
+
method: "POST",
|
|
1693
|
+
headers: this.skillHeaders(skillToken),
|
|
1694
|
+
body: summary
|
|
1695
|
+
}).catch(() => void 0);
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
function autopilotExitCode(code, status) {
|
|
1699
|
+
if (status === 401 || status === 403) return CLI_EXIT_CODES.UNAUTHORISED;
|
|
1700
|
+
if (code === "TIER_LIMIT_EXCEEDED" || code === "NO_GIT_INTEGRATION") {
|
|
1701
|
+
return CLI_EXIT_CODES.MISCONFIGURATION;
|
|
1702
|
+
}
|
|
1703
|
+
if (status >= 500) return CLI_EXIT_CODES.NETWORK_UNREACHABLE;
|
|
1704
|
+
return CLI_EXIT_CODES.GENERIC_ERROR;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// src/scan/llm.ts
|
|
1708
|
+
import { spawn as spawn2 } from "child_process";
|
|
1709
|
+
var LlmGenerationError = class extends Error {
|
|
1710
|
+
constructor(reason, message) {
|
|
1711
|
+
super(message);
|
|
1712
|
+
this.reason = reason;
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
async function generateFixPromptJson(args) {
|
|
1716
|
+
const claude = args.claudePath ?? "claude";
|
|
1717
|
+
const userPrompt = [
|
|
1718
|
+
args.repoOverviewBlock,
|
|
1719
|
+
"",
|
|
1720
|
+
args.ticketBlock,
|
|
1721
|
+
"",
|
|
1722
|
+
`Return JSON only, matching this shape: ${args.outputSchemaHint}`,
|
|
1723
|
+
"Do not include explanatory prose, markdown fences, or code commentary \u2014 just the JSON object."
|
|
1724
|
+
].join("\n");
|
|
1725
|
+
const cliArgs = [
|
|
1726
|
+
"--print",
|
|
1727
|
+
"--allowedTools",
|
|
1728
|
+
"",
|
|
1729
|
+
"--system-prompt",
|
|
1730
|
+
args.systemPrompt,
|
|
1731
|
+
"--model",
|
|
1732
|
+
args.modelId,
|
|
1733
|
+
"--max-turns",
|
|
1734
|
+
"1"
|
|
1735
|
+
];
|
|
1736
|
+
return new Promise((resolve2, reject) => {
|
|
1737
|
+
let child;
|
|
1738
|
+
try {
|
|
1739
|
+
child = spawn2(claude, cliArgs, {
|
|
1740
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1741
|
+
signal: args.signal
|
|
1742
|
+
});
|
|
1743
|
+
} catch (err) {
|
|
1744
|
+
reject(
|
|
1745
|
+
new LlmGenerationError(
|
|
1746
|
+
"spawn_failed",
|
|
1747
|
+
`Could not invoke claude: ${err.message}`
|
|
1748
|
+
)
|
|
1749
|
+
);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
let stdoutBuf = "";
|
|
1753
|
+
let stderrBuf = "";
|
|
1754
|
+
child.stdout?.on("data", (c2) => stdoutBuf += c2.toString("utf8"));
|
|
1755
|
+
child.stderr?.on("data", (c2) => stderrBuf += c2.toString("utf8"));
|
|
1756
|
+
child.on("error", (err) => {
|
|
1757
|
+
reject(new LlmGenerationError("spawn_failed", err.message));
|
|
1758
|
+
});
|
|
1759
|
+
child.on("close", (code, signal) => {
|
|
1760
|
+
if (signal === "SIGTERM" || signal === "SIGKILL") {
|
|
1761
|
+
reject(new LlmGenerationError("aborted", "claude was aborted"));
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
if (code !== 0) {
|
|
1765
|
+
reject(
|
|
1766
|
+
new LlmGenerationError(
|
|
1767
|
+
"non_zero_exit",
|
|
1768
|
+
`claude exited with code ${code}: ${stderrBuf.trim().slice(0, 1e3)}`
|
|
1769
|
+
)
|
|
1770
|
+
);
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
const parsed = parseStructuredJson(stdoutBuf);
|
|
1774
|
+
if (!parsed) {
|
|
1775
|
+
reject(new LlmGenerationError("no_json", "No JSON object found in claude output"));
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
const tokens = estimateTokens(userPrompt, stdoutBuf);
|
|
1779
|
+
resolve2({
|
|
1780
|
+
structured: parsed,
|
|
1781
|
+
rawText: stdoutBuf,
|
|
1782
|
+
inputTokens: tokens.input,
|
|
1783
|
+
outputTokens: tokens.output
|
|
1784
|
+
});
|
|
1785
|
+
});
|
|
1786
|
+
child.stdin?.write(userPrompt);
|
|
1787
|
+
child.stdin?.end();
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
function parseStructuredJson(raw) {
|
|
1791
|
+
const trimmed = raw.trim();
|
|
1792
|
+
if (!trimmed) return null;
|
|
1793
|
+
try {
|
|
1794
|
+
const direct = JSON.parse(trimmed);
|
|
1795
|
+
if (direct && typeof direct === "object") return direct;
|
|
1796
|
+
} catch {
|
|
1797
|
+
}
|
|
1798
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
1799
|
+
if (fenced && fenced[1]) {
|
|
1800
|
+
try {
|
|
1801
|
+
const obj = JSON.parse(fenced[1].trim());
|
|
1802
|
+
if (obj && typeof obj === "object") return obj;
|
|
1803
|
+
} catch {
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
const start = trimmed.indexOf("{");
|
|
1807
|
+
if (start === -1) return null;
|
|
1808
|
+
let depth = 0;
|
|
1809
|
+
let inString = false;
|
|
1810
|
+
let escape = false;
|
|
1811
|
+
for (let i = start; i < trimmed.length; i++) {
|
|
1812
|
+
const ch = trimmed[i];
|
|
1813
|
+
if (inString) {
|
|
1814
|
+
if (escape) {
|
|
1815
|
+
escape = false;
|
|
1816
|
+
} else if (ch === "\\") {
|
|
1817
|
+
escape = true;
|
|
1818
|
+
} else if (ch === '"') {
|
|
1819
|
+
inString = false;
|
|
1820
|
+
}
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
if (ch === '"') {
|
|
1824
|
+
inString = true;
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
if (ch === "{") depth += 1;
|
|
1828
|
+
else if (ch === "}") {
|
|
1829
|
+
depth -= 1;
|
|
1830
|
+
if (depth === 0) {
|
|
1831
|
+
const slice = trimmed.slice(start, i + 1);
|
|
1832
|
+
try {
|
|
1833
|
+
const obj = JSON.parse(slice);
|
|
1834
|
+
if (obj && typeof obj === "object") return obj;
|
|
1835
|
+
} catch {
|
|
1836
|
+
return null;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
return null;
|
|
1842
|
+
}
|
|
1843
|
+
function estimateTokens(input, output) {
|
|
1844
|
+
return {
|
|
1845
|
+
input: Math.max(1, Math.round(input.length / 4)),
|
|
1846
|
+
output: Math.max(1, Math.round(output.length / 4))
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// src/commands/scan.ts
|
|
1851
|
+
function registerScan(program2) {
|
|
1852
|
+
program2.command("scan").description(
|
|
1853
|
+
"Drive the AI fix-prompt autopilot loop locally \u2014 same flow as the /task-autopilot skill, run by the CLI binary"
|
|
1854
|
+
).option("--project <slugOrId>", "Restrict to one project (default: every visible project)").option("--max <n>", "Max submissions per project token", "50").option("--batch <n>", "Tickets per /prepare batch (1-10)", "5").option("--api-url <url>", "Override TASK_API_URL").option("--silent", "Suppress per-ticket progress chrome").action(async (opts) => {
|
|
1855
|
+
await runScan(opts);
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
async function runScan(opts) {
|
|
1859
|
+
const apiKey = process.env["TASK_API_KEY"];
|
|
1860
|
+
if (!apiKey || apiKey.length < 32) {
|
|
1861
|
+
throw new CliError(
|
|
1862
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
1863
|
+
"TASK_API_KEY is missing or shorter than 32 chars",
|
|
1864
|
+
"Set TASK_API_KEY in your environment. The autopilot loop authenticates with the shared admin secret, not the per-user CLI bearer."
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
const actorEmail = process.env["TASK_API_KEY_OWNER_EMAIL"];
|
|
1868
|
+
if (!actorEmail || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(actorEmail)) {
|
|
1869
|
+
throw new CliError(
|
|
1870
|
+
CLI_EXIT_CODES.MISCONFIGURATION,
|
|
1871
|
+
"TASK_API_KEY_OWNER_EMAIL is not set or not a valid email",
|
|
1872
|
+
"Set TASK_API_KEY_OWNER_EMAIL=<you@example.com>. The server records this on every audit row."
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
const localCfg = await readLocalConfig();
|
|
1876
|
+
const apiUrl = (opts.apiUrl ?? process.env["TASK_API_URL"] ?? localCfg.api_url ?? "http://localhost:3400").replace(/\/$/, "");
|
|
1877
|
+
const max = clampInt(opts.max, 1, 500, 50);
|
|
1878
|
+
const batchSize = clampInt(opts.batch, 1, 10, 5);
|
|
1879
|
+
const silent = !!opts.silent || localCfg.silent;
|
|
1880
|
+
const api = new AutopilotApi({ apiUrl, apiKey, actorEmail });
|
|
1881
|
+
if (!silent) process.stdout.write(`${c.dim("Discovering eligible projects\u2026")}
|
|
1882
|
+
`);
|
|
1883
|
+
const all = await api.listEligibleProjects();
|
|
1884
|
+
if (all.length === 0) {
|
|
1885
|
+
process.stdout.write(c.dim("No CLI-eligible tickets across any visible project.\n"));
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
const projects = opts.project ? all.filter(
|
|
1889
|
+
(p) => p.project_id === opts.project || p.project_slug === opts.project || `${p.organisation_slug}/${p.project_slug}` === opts.project
|
|
1890
|
+
) : all;
|
|
1891
|
+
if (projects.length === 0) {
|
|
1892
|
+
throw new CliError(
|
|
1893
|
+
CLI_EXIT_CODES.GENERIC_ERROR,
|
|
1894
|
+
`Project "${opts.project}" not found among eligible projects`
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
const aggregates = [];
|
|
1898
|
+
const claudePath = localCfg.claude_path ?? void 0;
|
|
1899
|
+
let interrupted = false;
|
|
1900
|
+
const onSigint = () => {
|
|
1901
|
+
interrupted = true;
|
|
1902
|
+
};
|
|
1903
|
+
process.on("SIGINT", onSigint);
|
|
1904
|
+
try {
|
|
1905
|
+
for (const proj of projects) {
|
|
1906
|
+
if (interrupted) break;
|
|
1907
|
+
const agg = await scanProject({
|
|
1908
|
+
api,
|
|
1909
|
+
project: proj,
|
|
1910
|
+
maxSubmits: max,
|
|
1911
|
+
batchSize,
|
|
1912
|
+
silent,
|
|
1913
|
+
claudePath,
|
|
1914
|
+
isInterrupted: () => interrupted
|
|
1915
|
+
});
|
|
1916
|
+
aggregates.push(agg);
|
|
1917
|
+
}
|
|
1918
|
+
} finally {
|
|
1919
|
+
process.off("SIGINT", onSigint);
|
|
1920
|
+
}
|
|
1921
|
+
const totals = aggregates.reduce(
|
|
1922
|
+
(acc, a) => ({
|
|
1923
|
+
prepared: acc.prepared + a.prepared,
|
|
1924
|
+
submitted: acc.submitted + a.submitted,
|
|
1925
|
+
denylist_hits: acc.denylist_hits + a.denylist_hits,
|
|
1926
|
+
failed: acc.failed + a.failed,
|
|
1927
|
+
skipped: acc.skipped + a.skipped
|
|
1928
|
+
}),
|
|
1929
|
+
{ prepared: 0, submitted: 0, denylist_hits: 0, failed: 0, skipped: 0 }
|
|
1930
|
+
);
|
|
1931
|
+
process.stdout.write(
|
|
1932
|
+
`${c.bold("\nSubmitted")} ${c.ok(String(totals.submitted))} ${c.bold("prompts")} (${c.warn(String(totals.denylist_hits))} flagged for review, ${c.err(String(totals.failed))} failed, ${c.dim(String(totals.skipped) + " skipped")}). Run summary recorded.
|
|
1933
|
+
`
|
|
1934
|
+
);
|
|
1935
|
+
if (interrupted) {
|
|
1936
|
+
process.stdout.write(
|
|
1937
|
+
`${c.warn("Run was interrupted")}; any claimed-but-unfinalised tickets were released.
|
|
1938
|
+
`
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
async function scanProject(args) {
|
|
1943
|
+
const { api, project, maxSubmits, batchSize, silent, claudePath } = args;
|
|
1944
|
+
const agg = {
|
|
1945
|
+
project_id: project.project_id,
|
|
1946
|
+
project_slug: project.project_slug,
|
|
1947
|
+
organisation_slug: project.organisation_slug,
|
|
1948
|
+
prepared: 0,
|
|
1949
|
+
submitted: 0,
|
|
1950
|
+
denylist_hits: 0,
|
|
1951
|
+
failed: 0,
|
|
1952
|
+
skipped: 0
|
|
1953
|
+
};
|
|
1954
|
+
const startedAt = Date.now();
|
|
1955
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
1956
|
+
if (!silent) {
|
|
1957
|
+
process.stdout.write(
|
|
1958
|
+
`
|
|
1959
|
+
${c.bold(`${project.organisation_slug}/${project.project_slug}`)} ${c.dim(`(${project.eligible_count} eligible)`)}
|
|
1960
|
+
`
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
const issued = await api.issueSkillToken({
|
|
1964
|
+
project_id: project.project_id,
|
|
1965
|
+
max_submits: maxSubmits
|
|
1966
|
+
});
|
|
1967
|
+
const skillToken = issued.token;
|
|
1968
|
+
let fatal = null;
|
|
1969
|
+
try {
|
|
1970
|
+
while (!args.isInterrupted()) {
|
|
1971
|
+
let prepared;
|
|
1972
|
+
try {
|
|
1973
|
+
prepared = await api.prepare(skillToken, batchSize, randomUUID2());
|
|
1974
|
+
} catch (err) {
|
|
1975
|
+
fatal = err;
|
|
1976
|
+
break;
|
|
1977
|
+
}
|
|
1978
|
+
if (prepared.tickets.length === 0) break;
|
|
1979
|
+
agg.prepared += prepared.tickets.length;
|
|
1980
|
+
const nonce = prepared.prepare_nonce;
|
|
1981
|
+
for (const ticket of prepared.tickets) {
|
|
1982
|
+
if (args.isInterrupted()) break;
|
|
1983
|
+
inFlight.add(ticket.ticket_id);
|
|
1984
|
+
const spinner = silent ? null : ora2(`#${ticket.sequence_number} ${ticket.title.slice(0, 60)}`).start();
|
|
1985
|
+
try {
|
|
1986
|
+
const generated = await safeGenerate(ticket, claudePath);
|
|
1987
|
+
if (!generated) {
|
|
1988
|
+
agg.skipped += 1;
|
|
1989
|
+
spinner?.warn(
|
|
1990
|
+
`#${ticket.sequence_number} skipped (no JSON from claude) \u2014 calling abort`
|
|
1991
|
+
);
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
const result = await api.submit({
|
|
1995
|
+
skillToken,
|
|
1996
|
+
nonce,
|
|
1997
|
+
ticketId: ticket.ticket_id,
|
|
1998
|
+
structured: generated.structured,
|
|
1999
|
+
inputTokens: generated.inputTokens,
|
|
2000
|
+
outputTokens: generated.outputTokens,
|
|
2001
|
+
model: ticket.model_id
|
|
2002
|
+
});
|
|
2003
|
+
if (result.status === "skip") {
|
|
2004
|
+
agg.skipped += 1;
|
|
2005
|
+
spinner?.warn(`#${ticket.sequence_number} skipped (${result.reason})`);
|
|
2006
|
+
} else if (result.status === "needs_review") {
|
|
2007
|
+
agg.submitted += 1;
|
|
2008
|
+
agg.denylist_hits += 1;
|
|
2009
|
+
spinner?.warn(`#${ticket.sequence_number} flagged for review`);
|
|
2010
|
+
} else {
|
|
2011
|
+
agg.submitted += 1;
|
|
2012
|
+
spinner?.succeed(`#${ticket.sequence_number} ready`);
|
|
2013
|
+
}
|
|
2014
|
+
inFlight.delete(ticket.ticket_id);
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
agg.failed += 1;
|
|
2017
|
+
spinner?.fail(`#${ticket.sequence_number} ${err.message.slice(0, 200)}`);
|
|
2018
|
+
if (err instanceof CliError && err.code === CLI_EXIT_CODES.UNAUTHORISED) {
|
|
2019
|
+
fatal = err;
|
|
2020
|
+
break;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
if (fatal) break;
|
|
2025
|
+
}
|
|
2026
|
+
} finally {
|
|
2027
|
+
const stillClaimed = Array.from(inFlight);
|
|
2028
|
+
if (stillClaimed.length > 0) {
|
|
2029
|
+
await api.abort(skillToken, stillClaimed).catch(() => void 0);
|
|
2030
|
+
}
|
|
2031
|
+
await api.runSummary(skillToken, {
|
|
2032
|
+
prepared: agg.prepared,
|
|
2033
|
+
submitted: agg.submitted,
|
|
2034
|
+
denylist_hits: agg.denylist_hits,
|
|
2035
|
+
failed: agg.failed,
|
|
2036
|
+
duration_ms: Date.now() - startedAt
|
|
2037
|
+
}).catch(() => void 0);
|
|
2038
|
+
}
|
|
2039
|
+
if (fatal) throw fatal;
|
|
2040
|
+
return agg;
|
|
2041
|
+
}
|
|
2042
|
+
async function safeGenerate(ticket, claudePath) {
|
|
2043
|
+
try {
|
|
2044
|
+
const out = await generateFixPromptJson({
|
|
2045
|
+
systemPrompt: ticket.system_prompt,
|
|
2046
|
+
repoOverviewBlock: ticket.repo_overview_block,
|
|
2047
|
+
ticketBlock: ticket.ticket_block,
|
|
2048
|
+
outputSchemaHint: ticket.output_schema_hint,
|
|
2049
|
+
modelId: ticket.model_id,
|
|
2050
|
+
...claudePath ? { claudePath } : {}
|
|
2051
|
+
});
|
|
2052
|
+
return out;
|
|
2053
|
+
} catch (err) {
|
|
2054
|
+
if (err instanceof LlmGenerationError) {
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
throw err;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
function clampInt(raw, min, max, fallback) {
|
|
2061
|
+
const v = parseInt(raw, 10);
|
|
2062
|
+
if (!Number.isFinite(v) || v < min) return fallback;
|
|
2063
|
+
return Math.min(v, max);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// src/commands/scheduled-task.ts
|
|
2067
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1504
2068
|
|
|
1505
2069
|
// src/scheduler/index.ts
|
|
1506
2070
|
import { platform as platform2 } from "os";
|
|
1507
2071
|
|
|
1508
2072
|
// src/scheduler/launchd.ts
|
|
1509
|
-
import { mkdir as mkdir5, readFile as
|
|
2073
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile6, unlink as unlink3, readdir } from "fs/promises";
|
|
1510
2074
|
import { homedir as homedir4 } from "os";
|
|
1511
|
-
import { join as
|
|
1512
|
-
import { execFileSync as execFileSync5, spawn as
|
|
2075
|
+
import { join as join6 } from "path";
|
|
2076
|
+
import { execFileSync as execFileSync5, spawn as spawn3 } from "child_process";
|
|
1513
2077
|
|
|
1514
2078
|
// src/scheduler/cron-translate.ts
|
|
1515
2079
|
function translateToLaunchd(cron) {
|
|
@@ -1610,14 +2174,14 @@ function expandField(field, min, max) {
|
|
|
1610
2174
|
}
|
|
1611
2175
|
|
|
1612
2176
|
// src/scheduler/launchd.ts
|
|
1613
|
-
var PLIST_DIR =
|
|
2177
|
+
var PLIST_DIR = join6(homedir4(), "Library", "LaunchAgents");
|
|
1614
2178
|
var LABEL_PREFIX = "com.inteeka.task.cli.";
|
|
1615
2179
|
var SAFE_ID_RE = /^[0-9a-zA-Z._-]+$/;
|
|
1616
2180
|
function plistPath(id) {
|
|
1617
2181
|
if (!SAFE_ID_RE.test(id) || id.includes("..")) {
|
|
1618
2182
|
throw new Error(`Refusing to compute plist path for unsafe id: ${id}`);
|
|
1619
2183
|
}
|
|
1620
|
-
return
|
|
2184
|
+
return join6(PLIST_DIR, `${LABEL_PREFIX}${id}.plist`);
|
|
1621
2185
|
}
|
|
1622
2186
|
function buildPlist(entry) {
|
|
1623
2187
|
const calendars = translateToLaunchd(entry.cron);
|
|
@@ -1653,9 +2217,9 @@ ${fields}
|
|
|
1653
2217
|
` <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>`,
|
|
1654
2218
|
` </dict>`,
|
|
1655
2219
|
` <key>StandardOutPath</key>`,
|
|
1656
|
-
` <string>${escapeXml(
|
|
2220
|
+
` <string>${escapeXml(join6(homedir4(), ".cache", "task", "launchd-stdout.log"))}</string>`,
|
|
1657
2221
|
` <key>StandardErrorPath</key>`,
|
|
1658
|
-
` <string>${escapeXml(
|
|
2222
|
+
` <string>${escapeXml(join6(homedir4(), ".cache", "task", "launchd-stderr.log"))}</string>`,
|
|
1659
2223
|
!entry.enabled ? ` <key>Disabled</key>
|
|
1660
2224
|
<true/>` : "",
|
|
1661
2225
|
"</dict>",
|
|
@@ -1675,7 +2239,7 @@ var launchdAdapter = {
|
|
|
1675
2239
|
async upsert(entry) {
|
|
1676
2240
|
await mkdir5(PLIST_DIR, { recursive: true });
|
|
1677
2241
|
const path = plistPath(entry.id);
|
|
1678
|
-
await
|
|
2242
|
+
await writeFile6(path, buildPlist(entry));
|
|
1679
2243
|
try {
|
|
1680
2244
|
execFileSync5("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
1681
2245
|
} catch {
|
|
@@ -1704,7 +2268,7 @@ var launchdAdapter = {
|
|
|
1704
2268
|
for (const file of ours) {
|
|
1705
2269
|
const id = file.slice(LABEL_PREFIX.length, -".plist".length);
|
|
1706
2270
|
try {
|
|
1707
|
-
const xml = await
|
|
2271
|
+
const xml = await readFile5(join6(PLIST_DIR, file), "utf8");
|
|
1708
2272
|
const cron = xml.match(/<key>StartCalendarInterval<\/key>[\s\S]*?<\/array>/)?.[0] ?? "";
|
|
1709
2273
|
const command = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/)?.[1] ?? "";
|
|
1710
2274
|
const disabled = /<key>Disabled<\/key>\s*<true\/>/.test(xml);
|
|
@@ -1727,7 +2291,7 @@ var launchdAdapter = {
|
|
|
1727
2291
|
return new Promise((resolve2) => {
|
|
1728
2292
|
const args = entry.command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [entry.command];
|
|
1729
2293
|
const cmd = args.shift() ?? entry.command;
|
|
1730
|
-
const child =
|
|
2294
|
+
const child = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1731
2295
|
let stdoutTail = "";
|
|
1732
2296
|
let stderrTail = "";
|
|
1733
2297
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -1744,13 +2308,13 @@ var launchdAdapter = {
|
|
|
1744
2308
|
const path = plistPath(id);
|
|
1745
2309
|
let xml;
|
|
1746
2310
|
try {
|
|
1747
|
-
xml = await
|
|
2311
|
+
xml = await readFile5(path, "utf8");
|
|
1748
2312
|
} catch {
|
|
1749
2313
|
return;
|
|
1750
2314
|
}
|
|
1751
2315
|
if (enabled) {
|
|
1752
2316
|
xml = xml.replace(/\s*<key>Disabled<\/key>\s*<true\/>/, "");
|
|
1753
|
-
await
|
|
2317
|
+
await writeFile6(path, xml);
|
|
1754
2318
|
try {
|
|
1755
2319
|
execFileSync5("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
1756
2320
|
} catch {
|
|
@@ -1762,7 +2326,7 @@ var launchdAdapter = {
|
|
|
1762
2326
|
"</dict>\n</plist>",
|
|
1763
2327
|
" <key>Disabled</key>\n <true/>\n</dict>\n</plist>"
|
|
1764
2328
|
);
|
|
1765
|
-
await
|
|
2329
|
+
await writeFile6(path, xml);
|
|
1766
2330
|
}
|
|
1767
2331
|
try {
|
|
1768
2332
|
execFileSync5("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
|
|
@@ -1773,7 +2337,7 @@ var launchdAdapter = {
|
|
|
1773
2337
|
};
|
|
1774
2338
|
|
|
1775
2339
|
// src/scheduler/cron.ts
|
|
1776
|
-
import { execFileSync as execFileSync6, spawn as
|
|
2340
|
+
import { execFileSync as execFileSync6, spawn as spawn4 } from "child_process";
|
|
1777
2341
|
|
|
1778
2342
|
// src/scheduler/safe-command.ts
|
|
1779
2343
|
var FORBIDDEN = /[;&|`$()<>\\]/;
|
|
@@ -1834,7 +2398,7 @@ function readCrontab() {
|
|
|
1834
2398
|
}
|
|
1835
2399
|
}
|
|
1836
2400
|
function writeCrontab(text) {
|
|
1837
|
-
const child =
|
|
2401
|
+
const child = spawn4("crontab", ["-"], { stdio: ["pipe", "inherit", "inherit"] });
|
|
1838
2402
|
child.stdin.write(text);
|
|
1839
2403
|
child.stdin.end();
|
|
1840
2404
|
}
|
|
@@ -1915,7 +2479,7 @@ var cronAdapter = {
|
|
|
1915
2479
|
return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
|
|
1916
2480
|
}
|
|
1917
2481
|
return new Promise((resolve2) => {
|
|
1918
|
-
const child =
|
|
2482
|
+
const child = spawn4(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1919
2483
|
let stdoutTail = "";
|
|
1920
2484
|
let stderrTail = "";
|
|
1921
2485
|
child.stdout?.on(
|
|
@@ -1943,7 +2507,7 @@ var cronAdapter = {
|
|
|
1943
2507
|
};
|
|
1944
2508
|
|
|
1945
2509
|
// src/scheduler/windows.ts
|
|
1946
|
-
import { execFileSync as execFileSync7, spawn as
|
|
2510
|
+
import { execFileSync as execFileSync7, spawn as spawn5 } from "child_process";
|
|
1947
2511
|
var TASK_PREFIX = "TaskCLI_";
|
|
1948
2512
|
function taskName(id) {
|
|
1949
2513
|
return `${TASK_PREFIX}${id.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
@@ -2056,7 +2620,7 @@ var windowsAdapter = {
|
|
|
2056
2620
|
return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
|
|
2057
2621
|
}
|
|
2058
2622
|
return new Promise((resolve2) => {
|
|
2059
|
-
const child =
|
|
2623
|
+
const child = spawn5(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2060
2624
|
let stdoutTail = "";
|
|
2061
2625
|
let stderrTail = "";
|
|
2062
2626
|
child.stdout?.on(
|
|
@@ -2115,10 +2679,10 @@ var unsupportedAdapter = {
|
|
|
2115
2679
|
};
|
|
2116
2680
|
|
|
2117
2681
|
// src/scheduler/registry.ts
|
|
2118
|
-
import { mkdir as mkdir6, readFile as
|
|
2682
|
+
import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
|
|
2119
2683
|
import { homedir as homedir5 } from "os";
|
|
2120
|
-
import { dirname as dirname4, join as
|
|
2121
|
-
var REGISTRY_PATH =
|
|
2684
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
2685
|
+
var REGISTRY_PATH = join7(homedir5(), ".config", "task", "schedules.json");
|
|
2122
2686
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2123
2687
|
function looksLikeRegistryRow(value) {
|
|
2124
2688
|
if (!value || typeof value !== "object") return false;
|
|
@@ -2127,7 +2691,7 @@ function looksLikeRegistryRow(value) {
|
|
|
2127
2691
|
}
|
|
2128
2692
|
async function readRegistry() {
|
|
2129
2693
|
try {
|
|
2130
|
-
const raw = await
|
|
2694
|
+
const raw = await readFile6(REGISTRY_PATH, "utf8");
|
|
2131
2695
|
const parsed = JSON.parse(raw);
|
|
2132
2696
|
if (!Array.isArray(parsed)) return [];
|
|
2133
2697
|
return parsed.filter(looksLikeRegistryRow);
|
|
@@ -2139,7 +2703,7 @@ async function readRegistry() {
|
|
|
2139
2703
|
}
|
|
2140
2704
|
async function writeRegistry(rows) {
|
|
2141
2705
|
await mkdir6(dirname4(REGISTRY_PATH), { recursive: true });
|
|
2142
|
-
await
|
|
2706
|
+
await writeFile7(REGISTRY_PATH, JSON.stringify(rows, null, 2));
|
|
2143
2707
|
}
|
|
2144
2708
|
async function upsertRegistry(row) {
|
|
2145
2709
|
if (!UUID_RE.test(row.id)) {
|
|
@@ -2227,7 +2791,7 @@ function registerScheduledTask(program2) {
|
|
|
2227
2791
|
const max = Math.min(100, Math.max(1, parseInt(opts.max, 10) || 5));
|
|
2228
2792
|
const command = opts.command ?? `task work --auto --silent --max ${max}`;
|
|
2229
2793
|
const { hostId, hostLabel } = getHostInfo();
|
|
2230
|
-
const id =
|
|
2794
|
+
const id = randomUUID3();
|
|
2231
2795
|
const created = await apiCall("POST", "/api/v1/cli/schedules", {
|
|
2232
2796
|
body: {
|
|
2233
2797
|
name,
|
|
@@ -2378,9 +2942,9 @@ function stripAnsi(s) {
|
|
|
2378
2942
|
}
|
|
2379
2943
|
|
|
2380
2944
|
// src/commands/runs.ts
|
|
2381
|
-
import { readFile as
|
|
2945
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2382
2946
|
import { homedir as homedir6 } from "os";
|
|
2383
|
-
import { join as
|
|
2947
|
+
import { join as join8 } from "path";
|
|
2384
2948
|
function registerRuns(program2) {
|
|
2385
2949
|
const cmd = program2.command("runs").description("Inspect agentic CLI run history");
|
|
2386
2950
|
cmd.command("list").description("List recent runs").option("--limit <n>", "Max rows", "50").option("--ticket <id>", "Filter by ticket").option("--schedule <id>", "Filter by schedule").action(async (opts) => {
|
|
@@ -2409,9 +2973,9 @@ function registerRuns(program2) {
|
|
|
2409
2973
|
process.stdout.write(JSON.stringify(row, null, 2) + "\n");
|
|
2410
2974
|
});
|
|
2411
2975
|
cmd.command("logs <id>").description("Show captured agent output for a run, if available").action(async (id) => {
|
|
2412
|
-
const localPath =
|
|
2976
|
+
const localPath = join8(homedir6(), ".cache", "task", "runs", `${id}.log`);
|
|
2413
2977
|
try {
|
|
2414
|
-
const text = await
|
|
2978
|
+
const text = await readFile7(localPath, "utf8");
|
|
2415
2979
|
process.stdout.write(text);
|
|
2416
2980
|
return;
|
|
2417
2981
|
} catch {
|
|
@@ -2482,7 +3046,7 @@ function registerConfig(program2) {
|
|
|
2482
3046
|
|
|
2483
3047
|
// src/commands/doctor.ts
|
|
2484
3048
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
2485
|
-
import { request as
|
|
3049
|
+
import { request as request5 } from "undici";
|
|
2486
3050
|
function registerDoctor(program2) {
|
|
2487
3051
|
program2.command("doctor").description("Diagnose your CLI setup").action(async () => {
|
|
2488
3052
|
const checks = [];
|
|
@@ -2510,7 +3074,7 @@ function registerDoctor(program2) {
|
|
|
2510
3074
|
});
|
|
2511
3075
|
const apiUrl = creds?.api_url ?? cfg.api_url;
|
|
2512
3076
|
try {
|
|
2513
|
-
const res = await
|
|
3077
|
+
const res = await request5(apiUrl, {
|
|
2514
3078
|
method: "GET",
|
|
2515
3079
|
headersTimeout: 5e3,
|
|
2516
3080
|
bodyTimeout: 5e3
|
|
@@ -2584,6 +3148,7 @@ registerStatus(program);
|
|
|
2584
3148
|
registerTickets(program);
|
|
2585
3149
|
registerTicket(program);
|
|
2586
3150
|
registerWork(program);
|
|
3151
|
+
registerScan(program);
|
|
2587
3152
|
registerScheduledTask(program);
|
|
2588
3153
|
registerRuns(program);
|
|
2589
3154
|
registerConfig(program);
|