@inteeka/task-cli 0.2.32 → 0.2.34

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
@@ -1763,8 +1763,8 @@ async function runAgent(args) {
1763
1763
  stdoutBuffer = "";
1764
1764
  }
1765
1765
  logHandle?.end();
1766
- const exitCode = code ?? 0;
1767
- resolve2({ exitCode, ok: exitCode === 0, outputLogPath, stderrTail: stderrBuffer });
1766
+ const exitCode2 = code ?? 0;
1767
+ resolve2({ exitCode: exitCode2, ok: exitCode2 === 0, outputLogPath, stderrTail: stderrBuffer });
1768
1768
  });
1769
1769
  });
1770
1770
  }
@@ -1952,7 +1952,7 @@ async function runPrReview(args) {
1952
1952
  const logHandle = createWriteStream(logPath, { flags: "a" });
1953
1953
  let stdoutBuffer = "";
1954
1954
  let stderrTail = "";
1955
- const exitCode = await new Promise((resolve2, reject) => {
1955
+ const exitCode2 = await new Promise((resolve2, reject) => {
1956
1956
  const child = spawn2(claude, cliArgs, {
1957
1957
  cwd: args.cwd,
1958
1958
  stdio: ["ignore", "pipe", "pipe"],
@@ -1985,10 +1985,10 @@ async function runPrReview(args) {
1985
1985
  resolve2(code ?? 0);
1986
1986
  });
1987
1987
  });
1988
- if (exitCode !== 0) {
1988
+ if (exitCode2 !== 0) {
1989
1989
  throw new PrReviewError(
1990
1990
  "nonzero_exit",
1991
- `claude review subprocess exited with code ${exitCode}`,
1991
+ `claude review subprocess exited with code ${exitCode2}`,
1992
1992
  stderrTail
1993
1993
  );
1994
1994
  }
@@ -4704,9 +4704,587 @@ function clampInt(raw, min, max, fallback) {
4704
4704
  return Math.min(v, max);
4705
4705
  }
4706
4706
 
4707
- // src/commands/slack-import.ts
4708
- import { spawn as spawn6 } from "child_process";
4707
+ // src/commands/seo-feedback.ts
4708
+ import { randomUUID as randomUUID3 } from "crypto";
4709
+ import ora3 from "ora";
4710
+
4711
+ // src/seo-feedback/api.ts
4709
4712
  import { request as request5 } from "undici";
4713
+ async function jsonRequest2(url, init) {
4714
+ const res = await request5(url, {
4715
+ method: init.method,
4716
+ headers: init.headers,
4717
+ body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
4718
+ bodyTimeout: 12e4,
4719
+ headersTimeout: 12e4
4720
+ });
4721
+ let body;
4722
+ try {
4723
+ body = await res.body.json();
4724
+ } catch {
4725
+ body = void 0;
4726
+ }
4727
+ const nonce = res.headers["x-prepare-nonce"];
4728
+ const nonceStr = typeof nonce === "string" ? nonce : Array.isArray(nonce) ? nonce[0] ?? null : null;
4729
+ if (res.statusCode >= 200 && res.statusCode < 300) {
4730
+ const env = body;
4731
+ return { ok: true, status: res.statusCode, data: env?.data ?? body, nonce: nonceStr };
4732
+ }
4733
+ const errBody = body;
4734
+ return {
4735
+ ok: false,
4736
+ status: res.statusCode,
4737
+ code: errBody?.error?.code ?? `HTTP_${res.statusCode}`,
4738
+ message: errBody?.error?.message ?? `Request failed with status ${res.statusCode}`
4739
+ };
4740
+ }
4741
+ var FRESH_CRED_CACHE_MS2 = 5e3;
4742
+ var SeoFeedbackApi = class {
4743
+ initialCreds;
4744
+ cachedCreds = null;
4745
+ cachedAt = 0;
4746
+ apiUrl;
4747
+ constructor(opts) {
4748
+ this.initialCreds = opts.creds;
4749
+ this.cachedCreds = opts.creds;
4750
+ this.cachedAt = Date.now();
4751
+ this.apiUrl = opts.apiUrl;
4752
+ }
4753
+ async getFreshCreds() {
4754
+ if (this.cachedCreds && Date.now() - this.cachedAt < FRESH_CRED_CACHE_MS2) {
4755
+ return this.cachedCreds;
4756
+ }
4757
+ const onDisk = await readCredentials();
4758
+ const base = onDisk ?? this.cachedCreds ?? this.initialCreds;
4759
+ const fresh = await ensureFreshAccessToken(base);
4760
+ this.cachedCreds = fresh;
4761
+ this.cachedAt = Date.now();
4762
+ return fresh;
4763
+ }
4764
+ async userHeaders() {
4765
+ const creds = await this.getFreshCreds();
4766
+ return {
4767
+ "Content-Type": "application/json",
4768
+ Authorization: `Bearer ${creds.access_token}`,
4769
+ "User-Agent": "task-cli/seo-feedback"
4770
+ };
4771
+ }
4772
+ skillHeaders(skillToken, extra = {}) {
4773
+ return {
4774
+ "Content-Type": "application/json",
4775
+ Authorization: `Bearer ${skillToken}`,
4776
+ "User-Agent": "task-cli/seo-feedback",
4777
+ ...extra
4778
+ };
4779
+ }
4780
+ async issueSkillToken(args) {
4781
+ const result = await jsonRequest2(
4782
+ `${this.apiUrl}/api/v1/cli/issue-skill-token`,
4783
+ {
4784
+ method: "POST",
4785
+ headers: await this.userHeaders(),
4786
+ body: {
4787
+ project_id: args.project_id,
4788
+ scope: "seo_feedback_sync",
4789
+ max_submits: args.max_submits,
4790
+ ttl_minutes: args.ttl_minutes ?? 30
4791
+ }
4792
+ }
4793
+ );
4794
+ if (!result.ok) {
4795
+ await handleUserAuthFailure2(result.code, result.status);
4796
+ throw new CliError(exitCode(result.code, result.status), `${result.code}: ${result.message}`);
4797
+ }
4798
+ return result.data;
4799
+ }
4800
+ async prepare(skillToken, batchSize, idempotencyKey) {
4801
+ const result = await jsonRequest2(
4802
+ `${this.apiUrl}/api/v1/cli/seo-feedback-sync/prepare`,
4803
+ {
4804
+ method: "POST",
4805
+ headers: this.skillHeaders(skillToken, { "Idempotency-Key": idempotencyKey }),
4806
+ body: { batch_size: batchSize }
4807
+ }
4808
+ );
4809
+ if (!result.ok) {
4810
+ throw new CliError(exitCode(result.code, result.status), `${result.code}: ${result.message}`);
4811
+ }
4812
+ if (!result.data.prepare_nonce && result.nonce) {
4813
+ result.data.prepare_nonce = result.nonce;
4814
+ }
4815
+ return result.data;
4816
+ }
4817
+ async submit(args) {
4818
+ const result = await jsonRequest2(
4819
+ `${this.apiUrl}/api/v1/cli/seo-feedback-sync/submit`,
4820
+ {
4821
+ method: "POST",
4822
+ headers: this.skillHeaders(args.skillToken, { "X-Prepare-Nonce": args.nonce }),
4823
+ body: {
4824
+ ticket_id: args.ticketId,
4825
+ report: args.report,
4826
+ input_tokens: args.inputTokens,
4827
+ output_tokens: args.outputTokens,
4828
+ model: args.model
4829
+ }
4830
+ }
4831
+ );
4832
+ if (result.ok) return { status: "ready" };
4833
+ if (result.code === "CLAIM_MISMATCH" || result.code === "BAD_STATUS" || result.code === "WRONG_SCOPE" || result.code === "SCAN_CONTEXT_FAILED" || result.code === "FINALISE_RACE") {
4834
+ return { status: "skip", reason: result.code };
4835
+ }
4836
+ throw new CliError(exitCode(result.code, result.status), `${result.code}: ${result.message}`);
4837
+ }
4838
+ async abort(skillToken, ticketIds) {
4839
+ if (ticketIds.length === 0) return;
4840
+ await jsonRequest2(`${this.apiUrl}/api/v1/cli/seo-feedback-sync/abort`, {
4841
+ method: "POST",
4842
+ headers: this.skillHeaders(skillToken),
4843
+ body: { ticket_ids: ticketIds }
4844
+ }).catch(() => void 0);
4845
+ }
4846
+ async runSummary(skillToken, summary) {
4847
+ await jsonRequest2(`${this.apiUrl}/api/v1/cli/seo-feedback-sync/run-summary`, {
4848
+ method: "POST",
4849
+ headers: this.skillHeaders(skillToken),
4850
+ body: summary
4851
+ }).catch(() => void 0);
4852
+ }
4853
+ };
4854
+ async function handleUserAuthFailure2(code, status) {
4855
+ if (status === 401 && (code === "UNAUTHORIZED" || code === "TOKEN_EXPIRED")) {
4856
+ await clearCredentials();
4857
+ throw new CliError(
4858
+ CLI_EXIT_CODES.UNAUTHORISED,
4859
+ "Your CLI session is no longer valid",
4860
+ "Run 'task login' to authenticate again."
4861
+ );
4862
+ }
4863
+ if (status === 403 && code === "CLI_ACCESS_REVOKED") {
4864
+ await clearCredentials();
4865
+ throw new CliError(
4866
+ CLI_EXIT_CODES.UNAUTHORISED,
4867
+ "CLI access has been revoked",
4868
+ "Ask a project admin to re-grant access from the Agentic CLI page."
4869
+ );
4870
+ }
4871
+ }
4872
+ function exitCode(code, status) {
4873
+ if (status === 401 || status === 403) return CLI_EXIT_CODES.UNAUTHORISED;
4874
+ if (code === "TIER_LIMIT_EXCEEDED") return CLI_EXIT_CODES.MISCONFIGURATION;
4875
+ if (status >= 500) return CLI_EXIT_CODES.NETWORK_UNREACHABLE;
4876
+ return CLI_EXIT_CODES.GENERIC_ERROR;
4877
+ }
4878
+
4879
+ // src/seo-feedback/llm.ts
4880
+ import { spawn as spawn6 } from "child_process";
4881
+ import { mkdir as mkdir10, writeFile as writeFile11 } from "fs/promises";
4882
+ import { homedir as homedir7 } from "os";
4883
+ import { join as join12 } from "path";
4884
+ var SEO_FEEDBACK_JSON_SCHEMA = {
4885
+ type: "object",
4886
+ required: [
4887
+ "overall_score",
4888
+ "headline",
4889
+ "overall_assessment",
4890
+ "score_interpretation",
4891
+ "strengths",
4892
+ "prioritised_actions",
4893
+ "quick_wins",
4894
+ "summary",
4895
+ "generated_for_url"
4896
+ ],
4897
+ additionalProperties: false,
4898
+ properties: {
4899
+ overall_score: { type: "integer", minimum: 0, maximum: 100 },
4900
+ headline: { type: "string", minLength: 1, maxLength: 200 },
4901
+ overall_assessment: { type: "string", minLength: 1, maxLength: 1500 },
4902
+ score_interpretation: { type: "string", minLength: 1, maxLength: 800 },
4903
+ strengths: {
4904
+ type: "array",
4905
+ maxItems: 8,
4906
+ items: { type: "string", minLength: 1, maxLength: 300 }
4907
+ },
4908
+ prioritised_actions: {
4909
+ type: "array",
4910
+ maxItems: 12,
4911
+ items: {
4912
+ type: "object",
4913
+ required: ["title", "detail", "priority", "impact", "effort"],
4914
+ additionalProperties: false,
4915
+ properties: {
4916
+ title: { type: "string", minLength: 1, maxLength: 160 },
4917
+ detail: { type: "string", minLength: 1, maxLength: 1200 },
4918
+ priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
4919
+ impact: { type: "string", minLength: 1, maxLength: 400 },
4920
+ effort: { type: "string", enum: ["quick", "moderate", "involved"] }
4921
+ }
4922
+ }
4923
+ },
4924
+ quick_wins: {
4925
+ type: "array",
4926
+ maxItems: 6,
4927
+ items: { type: "string", minLength: 1, maxLength: 300 }
4928
+ },
4929
+ summary: { type: "string", minLength: 1, maxLength: 600 },
4930
+ generated_for_url: { type: "string", minLength: 1, maxLength: 2e3 }
4931
+ }
4932
+ };
4933
+ var SeoFeedbackLlmError = class extends Error {
4934
+ constructor(reason, message, debugLogPath) {
4935
+ super(message);
4936
+ this.reason = reason;
4937
+ if (debugLogPath !== void 0) this.debugLogPath = debugLogPath;
4938
+ }
4939
+ debugLogPath;
4940
+ };
4941
+ var DEBUG2 = process.env["TASK_SCAN_DEBUG"] === "1";
4942
+ async function generateSeoFeedbackJson(args) {
4943
+ const claude = args.claudePath ?? "claude";
4944
+ const userPrompt = [
4945
+ args.userMessage,
4946
+ "",
4947
+ "Return JSON only matching the supplied schema. Do not include explanatory prose, markdown fences, or commentary."
4948
+ ].join("\n");
4949
+ const cliArgs = [
4950
+ "--print",
4951
+ "--output-format",
4952
+ "json",
4953
+ "--tools",
4954
+ "",
4955
+ "--system-prompt",
4956
+ args.systemPrompt,
4957
+ "--model",
4958
+ args.modelId,
4959
+ "--json-schema",
4960
+ JSON.stringify(SEO_FEEDBACK_JSON_SCHEMA)
4961
+ ];
4962
+ return new Promise((resolve2, reject) => {
4963
+ let child;
4964
+ try {
4965
+ child = spawn6(claude, cliArgs, { stdio: ["pipe", "pipe", "pipe"], signal: args.signal });
4966
+ } catch (err) {
4967
+ reject(
4968
+ new SeoFeedbackLlmError(
4969
+ "spawn_failed",
4970
+ `Could not invoke claude: ${err.message}`
4971
+ )
4972
+ );
4973
+ return;
4974
+ }
4975
+ let stdoutBuf = "";
4976
+ let stderrBuf = "";
4977
+ child.stdout?.on("data", (c2) => stdoutBuf += c2.toString("utf8"));
4978
+ child.stderr?.on("data", (c2) => stderrBuf += c2.toString("utf8"));
4979
+ child.on("error", (err) => reject(new SeoFeedbackLlmError("spawn_failed", err.message)));
4980
+ child.on("close", async (code, signal) => {
4981
+ if (signal === "SIGTERM" || signal === "SIGKILL") {
4982
+ reject(new SeoFeedbackLlmError("aborted", "claude was aborted"));
4983
+ return;
4984
+ }
4985
+ if (detectAuthFailure2(stdoutBuf)) {
4986
+ const dump = await maybeDumpDebug2(args.ticketId, stdoutBuf, stderrBuf);
4987
+ reject(
4988
+ new SeoFeedbackLlmError(
4989
+ "non_zero_exit",
4990
+ `Claude is not logged in. Run \`claude /login\` once on this machine, then re-run \`task seo-feedback\`.`,
4991
+ dump ?? void 0
4992
+ )
4993
+ );
4994
+ return;
4995
+ }
4996
+ if (code !== 0) {
4997
+ const dump = await maybeDumpDebug2(args.ticketId, stdoutBuf, stderrBuf);
4998
+ reject(
4999
+ new SeoFeedbackLlmError(
5000
+ "non_zero_exit",
5001
+ `claude exited with code ${code}: ${stderrBuf.trim().slice(0, 600)}`,
5002
+ dump ?? void 0
5003
+ )
5004
+ );
5005
+ return;
5006
+ }
5007
+ const structuredFromEnvelope = extractStructuredOutput2(stdoutBuf);
5008
+ const innerText = extractEnvelopeText2(stdoutBuf);
5009
+ const parsed = structuredFromEnvelope ?? parseStructuredJson2(innerText);
5010
+ if (!parsed) {
5011
+ const dump = await maybeDumpDebug2(args.ticketId, stdoutBuf, stderrBuf);
5012
+ reject(
5013
+ new SeoFeedbackLlmError(
5014
+ "no_json",
5015
+ `No JSON object in claude output${dump ? ` (raw output saved to ${dump})` : ""}`,
5016
+ dump ?? void 0
5017
+ )
5018
+ );
5019
+ return;
5020
+ }
5021
+ const tokens = readEnvelopeTokens2(stdoutBuf, userPrompt, innerText);
5022
+ resolve2({
5023
+ report: parsed,
5024
+ rawText: stdoutBuf,
5025
+ inputTokens: tokens.input,
5026
+ outputTokens: tokens.output
5027
+ });
5028
+ });
5029
+ child.stdin?.write(userPrompt);
5030
+ child.stdin?.end();
5031
+ });
5032
+ }
5033
+ function detectAuthFailure2(raw) {
5034
+ const trimmed = raw.trim();
5035
+ if (!trimmed) return false;
5036
+ try {
5037
+ const env = JSON.parse(trimmed);
5038
+ if (env.is_error === true && typeof env.result === "string") {
5039
+ const msg = env.result.toLowerCase();
5040
+ return msg.includes("not logged in") || msg.includes("please run /login") || msg.includes("please log in");
5041
+ }
5042
+ } catch {
5043
+ }
5044
+ return false;
5045
+ }
5046
+ function extractStructuredOutput2(raw) {
5047
+ const trimmed = raw.trim();
5048
+ if (!trimmed) return null;
5049
+ try {
5050
+ const env = JSON.parse(trimmed);
5051
+ const so = env.structured_output;
5052
+ if (so && typeof so === "object") return so;
5053
+ } catch {
5054
+ }
5055
+ return null;
5056
+ }
5057
+ function extractEnvelopeText2(raw) {
5058
+ const trimmed = raw.trim();
5059
+ if (!trimmed) return raw;
5060
+ try {
5061
+ const env = JSON.parse(trimmed);
5062
+ if (typeof env.result === "string") return env.result;
5063
+ } catch {
5064
+ }
5065
+ return raw;
5066
+ }
5067
+ function readEnvelopeTokens2(raw, userPrompt, innerText) {
5068
+ try {
5069
+ const env = JSON.parse(raw.trim());
5070
+ const inTok = env.input_tokens ?? env.usage?.input_tokens;
5071
+ const outTok = env.output_tokens ?? env.usage?.output_tokens;
5072
+ if (typeof inTok === "number" && typeof outTok === "number") {
5073
+ return { input: inTok, output: outTok };
5074
+ }
5075
+ } catch {
5076
+ }
5077
+ return {
5078
+ input: Math.max(1, Math.round(userPrompt.length / 4)),
5079
+ output: Math.max(1, Math.round(innerText.length / 4))
5080
+ };
5081
+ }
5082
+ async function maybeDumpDebug2(ticketId, stdout, stderr) {
5083
+ if (!DEBUG2 && stdout.length === 0 && stderr.length === 0) return null;
5084
+ try {
5085
+ const dir = join12(homedir7(), ".cache", "task", "seo-feedback-debug");
5086
+ await mkdir10(dir, { recursive: true });
5087
+ const path = join12(dir, `${ticketId}-${Date.now()}.log`);
5088
+ await writeFile11(
5089
+ path,
5090
+ ["## ticket_id", ticketId, "", "## stdout", stdout, "", "## stderr", stderr].join("\n")
5091
+ );
5092
+ return path;
5093
+ } catch {
5094
+ return null;
5095
+ }
5096
+ }
5097
+ function parseStructuredJson2(raw) {
5098
+ const trimmed = raw.trim();
5099
+ if (!trimmed) return null;
5100
+ try {
5101
+ const direct = JSON.parse(trimmed);
5102
+ if (direct && typeof direct === "object") return direct;
5103
+ } catch {
5104
+ }
5105
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
5106
+ if (fenced && fenced[1]) {
5107
+ try {
5108
+ const obj = JSON.parse(fenced[1].trim());
5109
+ if (obj && typeof obj === "object") return obj;
5110
+ } catch {
5111
+ }
5112
+ }
5113
+ const start = trimmed.indexOf("{");
5114
+ if (start === -1) return null;
5115
+ let depth = 0;
5116
+ let inString = false;
5117
+ let escape = false;
5118
+ for (let i = start; i < trimmed.length; i++) {
5119
+ const ch = trimmed[i];
5120
+ if (inString) {
5121
+ if (escape) escape = false;
5122
+ else if (ch === "\\") escape = true;
5123
+ else if (ch === '"') inString = false;
5124
+ continue;
5125
+ }
5126
+ if (ch === '"') {
5127
+ inString = true;
5128
+ continue;
5129
+ }
5130
+ if (ch === "{") depth += 1;
5131
+ else if (ch === "}") {
5132
+ depth -= 1;
5133
+ if (depth === 0) {
5134
+ try {
5135
+ const obj = JSON.parse(trimmed.slice(start, i + 1));
5136
+ if (obj && typeof obj === "object") return obj;
5137
+ } catch {
5138
+ return null;
5139
+ }
5140
+ }
5141
+ }
5142
+ }
5143
+ return null;
5144
+ }
5145
+
5146
+ // src/commands/seo-feedback.ts
5147
+ function registerSeoFeedback(program2) {
5148
+ program2.command("seo-feedback").description("Generate AI Feedback SEO reports for queued tickets in the linked project").option("--project <id>", "Project id (default: the linked project from .task/config.json)").option("--max <n>", "Max submissions", "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) => {
5149
+ await runSeoFeedback(opts);
5150
+ });
5151
+ }
5152
+ async function runSeoFeedback(opts) {
5153
+ let creds = await readCredentials();
5154
+ if (!creds) {
5155
+ throw new CliError(
5156
+ CLI_EXIT_CODES.MISCONFIGURATION,
5157
+ "Not signed in",
5158
+ "Run 'task login' to authenticate."
5159
+ );
5160
+ }
5161
+ creds = await ensureFreshAccessToken(creds);
5162
+ const localCfg = await readLocalConfig();
5163
+ const linkedProject = await readProjectConfig(findRepoRoot());
5164
+ const apiUrl = (opts.apiUrl ?? process.env["TASK_API_URL"] ?? creds.api_url ?? localCfg.api_url ?? linkedProject?.api_url ?? "http://localhost:3400").replace(/\/$/, "");
5165
+ const projectId = opts.project ?? linkedProject?.project_id ?? null;
5166
+ if (!projectId) {
5167
+ throw new CliError(
5168
+ CLI_EXIT_CODES.MISCONFIGURATION,
5169
+ "No project to scan",
5170
+ "Run `task link` inside the repo, or pass --project <id>."
5171
+ );
5172
+ }
5173
+ const max = clampInt2(opts.max, 1, 500, 50);
5174
+ const batchSize = clampInt2(opts.batch, 1, 10, 5);
5175
+ const silent = !!opts.silent || localCfg.silent;
5176
+ const claudePath = localCfg.claude_path ?? void 0;
5177
+ const api = new SeoFeedbackApi({ apiUrl, creds });
5178
+ const issued = await api.issueSkillToken({ project_id: projectId, max_submits: max });
5179
+ const skillToken = issued.token;
5180
+ let prepared = 0;
5181
+ let submitted = 0;
5182
+ let failed = 0;
5183
+ let skipped = 0;
5184
+ const startedAt = Date.now();
5185
+ const inFlight = /* @__PURE__ */ new Set();
5186
+ let fatal = null;
5187
+ try {
5188
+ while (submitted < max) {
5189
+ let batch;
5190
+ try {
5191
+ batch = await api.prepare(skillToken, batchSize, randomUUID3());
5192
+ } catch (err) {
5193
+ fatal = err;
5194
+ break;
5195
+ }
5196
+ if (batch.tickets.length === 0) break;
5197
+ prepared += batch.tickets.length;
5198
+ const nonce = batch.prepare_nonce;
5199
+ for (const ticket of batch.tickets) {
5200
+ inFlight.add(ticket.ticket_id);
5201
+ const spinner = silent ? null : ora3(`Analysing ${ticket.ticket_id.slice(0, 8)}\u2026`).start();
5202
+ try {
5203
+ const gen = await safeGenerate2(ticket, claudePath);
5204
+ if (!gen.ok) {
5205
+ skipped += 1;
5206
+ spinner?.warn(`${ticket.ticket_id.slice(0, 8)} skipped (${gen.reason})`);
5207
+ await api.abort(skillToken, [ticket.ticket_id]).catch(() => void 0);
5208
+ inFlight.delete(ticket.ticket_id);
5209
+ continue;
5210
+ }
5211
+ const result = await api.submit({
5212
+ skillToken,
5213
+ nonce,
5214
+ ticketId: ticket.ticket_id,
5215
+ report: gen.report,
5216
+ inputTokens: gen.inputTokens,
5217
+ outputTokens: gen.outputTokens,
5218
+ model: ticket.model_id
5219
+ });
5220
+ if (result.status === "skip") {
5221
+ skipped += 1;
5222
+ spinner?.warn(`${ticket.ticket_id.slice(0, 8)} skipped (${result.reason})`);
5223
+ } else {
5224
+ submitted += 1;
5225
+ spinner?.succeed(`${ticket.ticket_id.slice(0, 8)} report ready`);
5226
+ }
5227
+ inFlight.delete(ticket.ticket_id);
5228
+ if (submitted >= max) break;
5229
+ } catch (err) {
5230
+ failed += 1;
5231
+ spinner?.fail(`${ticket.ticket_id.slice(0, 8)} ${err.message.slice(0, 200)}`);
5232
+ if (err instanceof CliError && err.code === CLI_EXIT_CODES.UNAUTHORISED) {
5233
+ fatal = err;
5234
+ break;
5235
+ }
5236
+ }
5237
+ }
5238
+ if (fatal) break;
5239
+ }
5240
+ } finally {
5241
+ const leftover = Array.from(inFlight);
5242
+ if (leftover.length > 0) await api.abort(skillToken, leftover).catch(() => void 0);
5243
+ await api.runSummary(skillToken, {
5244
+ prepared,
5245
+ submitted,
5246
+ denylist_hits: 0,
5247
+ failed,
5248
+ duration_ms: Date.now() - startedAt
5249
+ }).catch(() => void 0);
5250
+ }
5251
+ process.stdout.write(
5252
+ `${c.bold("\nAI Feedback")} \u2014 ${c.ok(String(submitted))} report(s) ready, ${c.err(String(failed))} failed, ${c.dim(String(skipped) + " skipped")}.
5253
+ `
5254
+ );
5255
+ if (fatal) throw fatal;
5256
+ }
5257
+ async function safeGenerate2(ticket, claudePath) {
5258
+ try {
5259
+ const out = await generateSeoFeedbackJson({
5260
+ systemPrompt: ticket.system_prompt,
5261
+ userMessage: ticket.user_message,
5262
+ modelId: ticket.model_id,
5263
+ ticketId: ticket.ticket_id,
5264
+ ...claudePath ? { claudePath } : {}
5265
+ });
5266
+ return {
5267
+ ok: true,
5268
+ report: out.report,
5269
+ inputTokens: out.inputTokens,
5270
+ outputTokens: out.outputTokens
5271
+ };
5272
+ } catch (err) {
5273
+ if (err instanceof SeoFeedbackLlmError) {
5274
+ return { ok: false, reason: `${err.reason}: ${err.message.slice(0, 150)}` };
5275
+ }
5276
+ throw err;
5277
+ }
5278
+ }
5279
+ function clampInt2(raw, min, max, fallback) {
5280
+ const v = parseInt(raw, 10);
5281
+ if (!Number.isFinite(v) || v < min) return fallback;
5282
+ return Math.min(v, max);
5283
+ }
5284
+
5285
+ // src/commands/slack-import.ts
5286
+ import { spawn as spawn7 } from "child_process";
5287
+ import { request as request6 } from "undici";
4710
5288
  var BULLET_TYPES = ["bug", "feature", "task", "question", "improvement"];
4711
5289
  var BULLET_PRIORITIES = ["critical", "high", "medium", "low", "none"];
4712
5290
  var CLASSIFICATIONS = ["code", "physical"];
@@ -4842,7 +5420,7 @@ async function generateBullets(args) {
4842
5420
  return new Promise((resolve2, reject) => {
4843
5421
  let child;
4844
5422
  try {
4845
- child = spawn6(claude, cliArgs, { stdio: ["pipe", "pipe", "pipe"] });
5423
+ child = spawn7(claude, cliArgs, { stdio: ["pipe", "pipe", "pipe"] });
4846
5424
  } catch (err) {
4847
5425
  reject(new Error(`Could not invoke claude: ${err.message}`));
4848
5426
  return;
@@ -4917,7 +5495,7 @@ function extractStructured(raw) {
4917
5495
  return { ok: false, error: "claude returned no parseable JSON" };
4918
5496
  }
4919
5497
  async function patchStatus(url, bearer, body) {
4920
- const res = await request5(url, {
5498
+ const res = await request6(url, {
4921
5499
  method: "PATCH",
4922
5500
  headers: {
4923
5501
  "Content-Type": "application/json",
@@ -4938,8 +5516,8 @@ async function patchStatus(url, bearer, body) {
4938
5516
  }
4939
5517
 
4940
5518
  // src/commands/fast-track.ts
4941
- import { randomUUID as randomUUID3 } from "crypto";
4942
- import ora3 from "ora";
5519
+ import { randomUUID as randomUUID4 } from "crypto";
5520
+ import ora4 from "ora";
4943
5521
  function registerFastTrack(program2) {
4944
5522
  program2.command("fast-track").description(
4945
5523
  "End-to-end: scan + auto-approve + work on the next CLI-eligible ticket(s) in the linked project \u2014 no admin review step"
@@ -5041,13 +5619,13 @@ async function fastTrackOneTicket(args) {
5041
5619
  max_submits: 1
5042
5620
  });
5043
5621
  const skillToken = issued.token;
5044
- const prepared = await api.prepare(skillToken, 1, randomUUID3());
5622
+ const prepared = await api.prepare(skillToken, 1, randomUUID4());
5045
5623
  if (prepared.tickets.length === 0) {
5046
5624
  return { kind: "no_eligible" };
5047
5625
  }
5048
5626
  const ticket = prepared.tickets[0];
5049
5627
  const nonce = prepared.prepare_nonce;
5050
- const spinner = silent ? null : ora3(`#${ticket.sequence_number} ${ticket.title.slice(0, 60)} \u2014 scanning`).start();
5628
+ const spinner = silent ? null : ora4(`#${ticket.sequence_number} ${ticket.title.slice(0, 60)} \u2014 scanning`).start();
5051
5629
  let generated;
5052
5630
  try {
5053
5631
  generated = await generateFixPromptJson({
@@ -5340,16 +5918,16 @@ ${c.err("\u2717 pr-test failed")}: ${err.message}
5340
5918
  }
5341
5919
 
5342
5920
  // src/commands/scheduled-task.ts
5343
- import { randomUUID as randomUUID4 } from "crypto";
5921
+ import { randomUUID as randomUUID5 } from "crypto";
5344
5922
 
5345
5923
  // src/scheduler/index.ts
5346
5924
  import { platform as platform2 } from "os";
5347
5925
 
5348
5926
  // src/scheduler/launchd.ts
5349
- import { mkdir as mkdir10, readFile as readFile5, writeFile as writeFile11, unlink as unlink4, readdir as readdir2 } from "fs/promises";
5350
- import { homedir as homedir7 } from "os";
5351
- import { join as join12 } from "path";
5352
- import { execFileSync as execFileSync9, spawn as spawn7 } from "child_process";
5927
+ import { mkdir as mkdir11, readFile as readFile5, writeFile as writeFile12, unlink as unlink4, readdir as readdir2 } from "fs/promises";
5928
+ import { homedir as homedir8 } from "os";
5929
+ import { join as join13 } from "path";
5930
+ import { execFileSync as execFileSync9, spawn as spawn8 } from "child_process";
5353
5931
 
5354
5932
  // src/scheduler/cron-translate.ts
5355
5933
  function translateToLaunchd(cron) {
@@ -5450,14 +6028,14 @@ function expandField(field, min, max) {
5450
6028
  }
5451
6029
 
5452
6030
  // src/scheduler/launchd.ts
5453
- var PLIST_DIR = join12(homedir7(), "Library", "LaunchAgents");
6031
+ var PLIST_DIR = join13(homedir8(), "Library", "LaunchAgents");
5454
6032
  var LABEL_PREFIX = "com.inteeka.task.cli.";
5455
6033
  var SAFE_ID_RE = /^[0-9a-zA-Z._-]+$/;
5456
6034
  function plistPath(id) {
5457
6035
  if (!SAFE_ID_RE.test(id) || id.includes("..")) {
5458
6036
  throw new Error(`Refusing to compute plist path for unsafe id: ${id}`);
5459
6037
  }
5460
- return join12(PLIST_DIR, `${LABEL_PREFIX}${id}.plist`);
6038
+ return join13(PLIST_DIR, `${LABEL_PREFIX}${id}.plist`);
5461
6039
  }
5462
6040
  function buildPlist(entry) {
5463
6041
  const calendars = translateToLaunchd(entry.cron);
@@ -5493,9 +6071,9 @@ ${fields}
5493
6071
  ` <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>`,
5494
6072
  ` </dict>`,
5495
6073
  ` <key>StandardOutPath</key>`,
5496
- ` <string>${escapeXml(join12(homedir7(), ".cache", "task", "launchd-stdout.log"))}</string>`,
6074
+ ` <string>${escapeXml(join13(homedir8(), ".cache", "task", "launchd-stdout.log"))}</string>`,
5497
6075
  ` <key>StandardErrorPath</key>`,
5498
- ` <string>${escapeXml(join12(homedir7(), ".cache", "task", "launchd-stderr.log"))}</string>`,
6076
+ ` <string>${escapeXml(join13(homedir8(), ".cache", "task", "launchd-stderr.log"))}</string>`,
5499
6077
  !entry.enabled ? ` <key>Disabled</key>
5500
6078
  <true/>` : "",
5501
6079
  "</dict>",
@@ -5513,9 +6091,9 @@ function bootstrapDomain() {
5513
6091
  }
5514
6092
  var launchdAdapter = {
5515
6093
  async upsert(entry) {
5516
- await mkdir10(PLIST_DIR, { recursive: true });
6094
+ await mkdir11(PLIST_DIR, { recursive: true });
5517
6095
  const path = plistPath(entry.id);
5518
- await writeFile11(path, buildPlist(entry));
6096
+ await writeFile12(path, buildPlist(entry));
5519
6097
  try {
5520
6098
  execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
5521
6099
  } catch {
@@ -5544,7 +6122,7 @@ var launchdAdapter = {
5544
6122
  for (const file of ours) {
5545
6123
  const id = file.slice(LABEL_PREFIX.length, -".plist".length);
5546
6124
  try {
5547
- const xml = await readFile5(join12(PLIST_DIR, file), "utf8");
6125
+ const xml = await readFile5(join13(PLIST_DIR, file), "utf8");
5548
6126
  const cron = xml.match(/<key>StartCalendarInterval<\/key>[\s\S]*?<\/array>/)?.[0] ?? "";
5549
6127
  const command = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/)?.[1] ?? "";
5550
6128
  const disabled = /<key>Disabled<\/key>\s*<true\/>/.test(xml);
@@ -5567,7 +6145,7 @@ var launchdAdapter = {
5567
6145
  return new Promise((resolve2) => {
5568
6146
  const args = entry.command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [entry.command];
5569
6147
  const cmd = args.shift() ?? entry.command;
5570
- const child = spawn7(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
6148
+ const child = spawn8(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
5571
6149
  let stdoutTail = "";
5572
6150
  let stderrTail = "";
5573
6151
  child.stdout?.on("data", (chunk) => {
@@ -5590,7 +6168,7 @@ var launchdAdapter = {
5590
6168
  }
5591
6169
  if (enabled) {
5592
6170
  xml = xml.replace(/\s*<key>Disabled<\/key>\s*<true\/>/, "");
5593
- await writeFile11(path, xml);
6171
+ await writeFile12(path, xml);
5594
6172
  try {
5595
6173
  execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
5596
6174
  } catch {
@@ -5602,7 +6180,7 @@ var launchdAdapter = {
5602
6180
  "</dict>\n</plist>",
5603
6181
  " <key>Disabled</key>\n <true/>\n</dict>\n</plist>"
5604
6182
  );
5605
- await writeFile11(path, xml);
6183
+ await writeFile12(path, xml);
5606
6184
  }
5607
6185
  try {
5608
6186
  execFileSync9("launchctl", ["bootout", bootstrapDomain(), path], { stdio: "ignore" });
@@ -5613,7 +6191,7 @@ var launchdAdapter = {
5613
6191
  };
5614
6192
 
5615
6193
  // src/scheduler/cron.ts
5616
- import { execFileSync as execFileSync10, spawn as spawn8 } from "child_process";
6194
+ import { execFileSync as execFileSync10, spawn as spawn9 } from "child_process";
5617
6195
 
5618
6196
  // src/scheduler/safe-command.ts
5619
6197
  var FORBIDDEN = /[;&|`$()<>\\]/;
@@ -5674,7 +6252,7 @@ function readCrontab() {
5674
6252
  }
5675
6253
  }
5676
6254
  function writeCrontab(text) {
5677
- const child = spawn8("crontab", ["-"], { stdio: ["pipe", "inherit", "inherit"] });
6255
+ const child = spawn9("crontab", ["-"], { stdio: ["pipe", "inherit", "inherit"] });
5678
6256
  child.stdin.write(text);
5679
6257
  child.stdin.end();
5680
6258
  }
@@ -5755,7 +6333,7 @@ var cronAdapter = {
5755
6333
  return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
5756
6334
  }
5757
6335
  return new Promise((resolve2) => {
5758
- const child = spawn8(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
6336
+ const child = spawn9(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
5759
6337
  let stdoutTail = "";
5760
6338
  let stderrTail = "";
5761
6339
  child.stdout?.on(
@@ -5783,7 +6361,7 @@ var cronAdapter = {
5783
6361
  };
5784
6362
 
5785
6363
  // src/scheduler/windows.ts
5786
- import { execFileSync as execFileSync11, spawn as spawn9 } from "child_process";
6364
+ import { execFileSync as execFileSync11, spawn as spawn10 } from "child_process";
5787
6365
  var TASK_PREFIX = "TaskCLI_";
5788
6366
  function taskName(id) {
5789
6367
  return `${TASK_PREFIX}${id.replace(/[^A-Za-z0-9_-]/g, "_")}`;
@@ -5896,7 +6474,7 @@ var windowsAdapter = {
5896
6474
  return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
5897
6475
  }
5898
6476
  return new Promise((resolve2) => {
5899
- const child = spawn9(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
6477
+ const child = spawn10(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
5900
6478
  let stdoutTail = "";
5901
6479
  let stderrTail = "";
5902
6480
  child.stdout?.on(
@@ -5955,10 +6533,10 @@ var unsupportedAdapter = {
5955
6533
  };
5956
6534
 
5957
6535
  // src/scheduler/registry.ts
5958
- import { mkdir as mkdir11, readFile as readFile6, writeFile as writeFile12 } from "fs/promises";
5959
- import { homedir as homedir8 } from "os";
5960
- import { dirname as dirname5, join as join13 } from "path";
5961
- var REGISTRY_PATH = join13(homedir8(), ".config", "task", "schedules.json");
6536
+ import { mkdir as mkdir12, readFile as readFile6, writeFile as writeFile13 } from "fs/promises";
6537
+ import { homedir as homedir9 } from "os";
6538
+ import { dirname as dirname5, join as join14 } from "path";
6539
+ var REGISTRY_PATH = join14(homedir9(), ".config", "task", "schedules.json");
5962
6540
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5963
6541
  function looksLikeRegistryRow(value) {
5964
6542
  if (!value || typeof value !== "object") return false;
@@ -5978,8 +6556,8 @@ async function readRegistry() {
5978
6556
  }
5979
6557
  }
5980
6558
  async function writeRegistry(rows) {
5981
- await mkdir11(dirname5(REGISTRY_PATH), { recursive: true });
5982
- await writeFile12(REGISTRY_PATH, JSON.stringify(rows, null, 2));
6559
+ await mkdir12(dirname5(REGISTRY_PATH), { recursive: true });
6560
+ await writeFile13(REGISTRY_PATH, JSON.stringify(rows, null, 2));
5983
6561
  }
5984
6562
  async function upsertRegistry(row) {
5985
6563
  if (!UUID_RE.test(row.id)) {
@@ -6067,7 +6645,7 @@ function registerScheduledTask(program2) {
6067
6645
  const max = Math.min(100, Math.max(1, parseInt(opts.max, 10) || 5));
6068
6646
  const command = opts.command ?? `task work --auto --silent --max ${max}`;
6069
6647
  const { hostId, hostLabel } = getHostInfo();
6070
- const id = randomUUID4();
6648
+ const id = randomUUID5();
6071
6649
  const created = await apiCall("POST", "/api/v1/cli/schedules", {
6072
6650
  body: {
6073
6651
  name,
@@ -6219,8 +6797,8 @@ function stripAnsi(s) {
6219
6797
 
6220
6798
  // src/commands/runs.ts
6221
6799
  import { readFile as readFile7 } from "fs/promises";
6222
- import { homedir as homedir9 } from "os";
6223
- import { join as join14 } from "path";
6800
+ import { homedir as homedir10 } from "os";
6801
+ import { join as join15 } from "path";
6224
6802
  function registerRuns(program2) {
6225
6803
  const cmd = program2.command("runs").description("Inspect agentic CLI run history");
6226
6804
  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) => {
@@ -6249,7 +6827,7 @@ function registerRuns(program2) {
6249
6827
  process.stdout.write(JSON.stringify(row, null, 2) + "\n");
6250
6828
  });
6251
6829
  cmd.command("logs <id>").description("Show captured agent output for a run, if available").action(async (id) => {
6252
- const localPath = join14(homedir9(), ".cache", "task", "runs", `${id}.log`);
6830
+ const localPath = join15(homedir10(), ".cache", "task", "runs", `${id}.log`);
6253
6831
  try {
6254
6832
  const text = await readFile7(localPath, "utf8");
6255
6833
  process.stdout.write(text);
@@ -6323,9 +6901,9 @@ function registerConfig(program2) {
6323
6901
  // src/commands/doctor.ts
6324
6902
  import { execFileSync as execFileSync12 } from "child_process";
6325
6903
  import { existsSync } from "fs";
6326
- import { readFile as readFile8, writeFile as writeFile13 } from "fs/promises";
6327
- import { isAbsolute, join as join15 } from "path";
6328
- import { request as request6 } from "undici";
6904
+ import { readFile as readFile8, writeFile as writeFile14 } from "fs/promises";
6905
+ import { isAbsolute, join as join16 } from "path";
6906
+ import { request as request7 } from "undici";
6329
6907
  var ALLOWED_TEST_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun", "node", "npx"]);
6330
6908
  var PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun"]);
6331
6909
  var DEFAULT_TEST_COMMAND = "pnpm typecheck";
@@ -6371,7 +6949,7 @@ function registerDoctor(program2) {
6371
6949
  });
6372
6950
  const apiUrl = creds?.api_url ?? cfg.api_url;
6373
6951
  try {
6374
- const res = await request6(apiUrl, {
6952
+ const res = await request7(apiUrl, {
6375
6953
  method: "GET",
6376
6954
  headersTimeout: 5e3,
6377
6955
  bodyTimeout: 5e3
@@ -6544,11 +7122,11 @@ async function checkPrePushTest(root, configuredCommand, fix) {
6544
7122
  };
6545
7123
  }
6546
7124
  const { scriptName, subdir } = resolveTestTarget(argv);
6547
- const targetDir = subdir ? join15(root, subdir) : root;
7125
+ const targetDir = subdir ? join16(root, subdir) : root;
6548
7126
  const where = subdir ? `${subdir}/` : "repo root";
6549
7127
  const inSubdir = subdir ? ` in ${subdir}/` : "";
6550
7128
  if (PACKAGE_MANAGERS.has(exe)) {
6551
- const nodeModules = join15(targetDir, "node_modules");
7129
+ const nodeModules = join16(targetDir, "node_modules");
6552
7130
  if (!existsSync(nodeModules)) {
6553
7131
  if (fix) {
6554
7132
  try {
@@ -6583,7 +7161,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
6583
7161
  detail: PACKAGE_MANAGERS.has(exe) ? `${command} (dependencies present; script not statically verifiable)` : `${command} (non-script executable, not statically verifiable)`
6584
7162
  };
6585
7163
  }
6586
- const pkgPath = join15(targetDir, "package.json");
7164
+ const pkgPath = join16(targetDir, "package.json");
6587
7165
  let pkgRaw;
6588
7166
  try {
6589
7167
  pkgRaw = await readFile8(pkgPath, "utf8");
@@ -6619,7 +7197,7 @@ async function checkPrePushTest(root, configuredCommand, fix) {
6619
7197
  pkg.scripts = { ...scripts, typecheck: "tsc --noEmit" };
6620
7198
  const indent = detectIndent(pkgRaw);
6621
7199
  const trailingNewline = pkgRaw.endsWith("\n") ? "\n" : "";
6622
- await writeFile13(pkgPath, JSON.stringify(pkg, null, indent) + trailingNewline);
7200
+ await writeFile14(pkgPath, JSON.stringify(pkg, null, indent) + trailingNewline);
6623
7201
  return {
6624
7202
  name: "pre-push test",
6625
7203
  ok: true,
@@ -6696,7 +7274,7 @@ function checkBinary(name, command) {
6696
7274
  }
6697
7275
 
6698
7276
  // src/commands/version.ts
6699
- var CLI_VERSION = true ? "0.2.32" : "0.0.0-dev";
7277
+ var CLI_VERSION = true ? "0.2.34" : "0.0.0-dev";
6700
7278
  function registerVersion(program2) {
6701
7279
  program2.command("version").description("Print the CLI version").action(() => {
6702
7280
  process.stdout.write(CLI_VERSION + "\n");
@@ -6723,6 +7301,7 @@ registerMultiWork(program);
6723
7301
  registerResume(program);
6724
7302
  registerReset(program);
6725
7303
  registerScan(program);
7304
+ registerSeoFeedback(program);
6726
7305
  registerSlackImport(program);
6727
7306
  registerFastTrack(program);
6728
7307
  registerPrTest(program);