@inteeka/task-cli 0.1.0 → 0.1.2

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 CHANGED
@@ -635,15 +635,15 @@ function registerLogin(program2) {
635
635
  noBrowser: !opts.browser,
636
636
  silent: cfg.silent
637
637
  });
638
- const access = await apiCall("GET", "/api/v1/cli/access");
639
- if (!access.ok || !access.data) {
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 (!access.data.has_access) {
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: access.data.email
658
+ email: access2.data.email
659
659
  });
660
660
  }
661
- process.stdout.write(`${c.ok("\u2713")} Signed in as ${c.bold(access.data.email)}
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 = access.data.projects.length;
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 access = await apiCallOrThrow("GET", "/api/v1/cli/access");
704
- process.stdout.write(`${c.bold(access.email || creds.email || access.user_id)}
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: ${access.projects.length} authorised
714
+ process.stdout.write(` Projects: ${access2.projects.length} authorised
715
715
  `);
716
- for (const p of access.projects) {
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 access = await apiCallOrThrow("GET", "/api/v1/cli/access");
790
- if (access.projects.length === 0) {
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
- let chosen = access.projects.find(
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 access = await apiCallOrThrow("GET", "/api/v1/cli/access");
857
- if (access.projects.length === 0) {
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 = access.projects.map((p) => [
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 writeFile4 } from "fs/promises";
1111
+ import { mkdir as mkdir4, writeFile as writeFile5 } from "fs/promises";
1069
1112
  import { homedir as homedir3 } from "os";
1070
- import { join as join4 } from "path";
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 = join4(homedir3(), ".cache", "task", "runs");
1170
+ const dir = join5(homedir3(), ".cache", "task", "runs");
1128
1171
  await mkdir4(dir, { recursive: true });
1129
- outputLogPath = join4(dir, `${args.runId}.log`);
1130
- await writeFile4(outputLogPath, "");
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 found.\n"));
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/scheduled-task.ts
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 readFile4, writeFile as writeFile5, unlink as unlink3, readdir } from "fs/promises";
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 join5 } from "path";
1512
- import { execFileSync as execFileSync5, spawn as spawn2 } from "child_process";
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 = join5(homedir4(), "Library", "LaunchAgents");
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 join5(PLIST_DIR, `${LABEL_PREFIX}${id}.plist`);
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(join5(homedir4(), ".cache", "task", "launchd-stdout.log"))}</string>`,
2220
+ ` <string>${escapeXml(join6(homedir4(), ".cache", "task", "launchd-stdout.log"))}</string>`,
1657
2221
  ` <key>StandardErrorPath</key>`,
1658
- ` <string>${escapeXml(join5(homedir4(), ".cache", "task", "launchd-stderr.log"))}</string>`,
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 writeFile5(path, buildPlist(entry));
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 readFile4(join5(PLIST_DIR, file), "utf8");
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 = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
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 readFile4(path, "utf8");
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 writeFile5(path, xml);
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 writeFile5(path, xml);
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 spawn3 } from "child_process";
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 = spawn3("crontab", ["-"], { stdio: ["pipe", "inherit", "inherit"] });
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 = spawn3(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
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 spawn4 } from "child_process";
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 = spawn4(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
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 readFile5, writeFile as writeFile6 } from "fs/promises";
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 join6 } from "path";
2121
- var REGISTRY_PATH = join6(homedir5(), ".config", "task", "schedules.json");
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 readFile5(REGISTRY_PATH, "utf8");
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 writeFile6(REGISTRY_PATH, JSON.stringify(rows, null, 2));
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 = randomUUID2();
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 readFile6 } from "fs/promises";
2945
+ import { readFile as readFile7 } from "fs/promises";
2382
2946
  import { homedir as homedir6 } from "os";
2383
- import { join as join7 } from "path";
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 = join7(homedir6(), ".cache", "task", "runs", `${id}.log`);
2976
+ const localPath = join8(homedir6(), ".cache", "task", "runs", `${id}.log`);
2413
2977
  try {
2414
- const text = await readFile6(localPath, "utf8");
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 request4 } from "undici";
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 request4(apiUrl, {
3077
+ const res = await request5(apiUrl, {
2514
3078
  method: "GET",
2515
3079
  headersTimeout: 5e3,
2516
3080
  bodyTimeout: 5e3
@@ -2561,7 +3125,7 @@ function checkBinary(name, command) {
2561
3125
  }
2562
3126
 
2563
3127
  // src/commands/version.ts
2564
- var CLI_VERSION = "0.1.0";
3128
+ var CLI_VERSION = true ? "0.1.2" : "0.0.0-dev";
2565
3129
  function registerVersion(program2) {
2566
3130
  program2.command("version").description("Print the CLI version").action(() => {
2567
3131
  process.stdout.write(CLI_VERSION + "\n");
@@ -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);