@inteeka/task-cli 0.2.24 → 0.2.26

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
@@ -3997,6 +3997,239 @@ function clampInt(raw, min, max, fallback) {
3997
3997
  return Math.min(v, max);
3998
3998
  }
3999
3999
 
4000
+ // src/commands/slack-import.ts
4001
+ import { spawn as spawn5 } from "child_process";
4002
+ import { request as request5 } from "undici";
4003
+ var BULLET_TYPES = ["bug", "feature", "task", "question", "improvement"];
4004
+ var BULLET_PRIORITIES = ["critical", "high", "medium", "low", "none"];
4005
+ var CLASSIFICATIONS = ["code", "physical"];
4006
+ var BULLETS_SCHEMA = {
4007
+ type: "object",
4008
+ required: ["bullets"],
4009
+ additionalProperties: false,
4010
+ properties: {
4011
+ bullets: {
4012
+ type: "array",
4013
+ maxItems: 200,
4014
+ items: {
4015
+ type: "object",
4016
+ required: ["title", "description", "type", "priority", "classification"],
4017
+ additionalProperties: false,
4018
+ properties: {
4019
+ title: { type: "string", minLength: 1, maxLength: 500 },
4020
+ description: { type: "string", minLength: 1, maxLength: 8e3 },
4021
+ type: { type: "string", enum: BULLET_TYPES },
4022
+ priority: { type: "string", enum: BULLET_PRIORITIES },
4023
+ classification: { type: "string", enum: CLASSIFICATIONS },
4024
+ reason: { type: "string", maxLength: 500 }
4025
+ }
4026
+ }
4027
+ }
4028
+ }
4029
+ };
4030
+ var SYSTEM_PROMPT = `You convert raw Slack call notes into a list of actionable tickets, one per concrete action item.
4031
+
4032
+ For each bullet decide:
4033
+ - classification = "code" if the item requires changes to the team's software (bug fixes, feature work, refactors, deploys, infra/config changes that a developer would do).
4034
+ - classification = "physical" if it is a human action that doesn't touch the codebase (sending an email, calling a vendor, booking a meeting, writing a non-code doc, scheduling someone's time, follow-ups, status updates).
4035
+
4036
+ When in doubt, prefer "physical". A false-positive "code" ticket wastes AI budget and pollutes the dev queue. A false-positive "physical" ticket still gets tracked and a human can re-classify.
4037
+
4038
+ For each ticket also choose:
4039
+ - type: bug | feature | task | question | improvement (task is the safe default)
4040
+ - priority: critical | high | medium | low | none (medium is the safe default)
4041
+
4042
+ The "reason" field is a brief 1-sentence explanation of WHY you classified the bullet this way \u2014 useful for the team reviewing the import. Keep it under 500 chars.
4043
+
4044
+ Drop greetings, social chatter, recaps that aren't actionable, and anything that is purely informational. Each bullet must be a concrete action someone has to do.
4045
+
4046
+ Return JSON only, matching the supplied schema. No prose, no markdown fences, no commentary.`;
4047
+ var SLACK_IMPORT_MODEL = "claude-sonnet-4-6";
4048
+ function sanitiseNotes(raw) {
4049
+ return raw.replace(/ignore\s+(all\s+)?previous\s+instructions/gi, "[REDACTED]").replace(/system\s*:\s*/gi, "[REDACTED]");
4050
+ }
4051
+ function registerSlackImport(program2) {
4052
+ program2.command("slack-import").description(
4053
+ "Internal: parse Slack call notes (from stdin) into classified bullets and PATCH them back to a slack_imports row. Spawned by the listener \u2014 not for direct human use."
4054
+ ).requiredOption("--import-id <uuid>", "Slack-import row id (from the webhook payload)").requiredOption("--organisation-id <uuid>", "Organisation id of the import").requiredOption("--project-id <uuid>", "Project id of the import").requiredOption("--update-url <url>", "Absolute callback URL for PATCH status updates").option(
4055
+ "--notes-stdin",
4056
+ "Read raw notes from stdin (default; the listener passes them this way)"
4057
+ ).option("--notes <text>", "Inline notes for local testing (mutually exclusive with stdin)").option("--claude-path <path>", "Override the claude binary path").action(async (opts) => {
4058
+ await runSlackImport(opts);
4059
+ });
4060
+ }
4061
+ async function runSlackImport(opts) {
4062
+ let creds = await readCredentials();
4063
+ if (!creds) {
4064
+ throw new CliError(
4065
+ CLI_EXIT_CODES.MISCONFIGURATION,
4066
+ "Not signed in",
4067
+ "Run 'task login' on this host so the listener can PATCH back."
4068
+ );
4069
+ }
4070
+ creds = await ensureFreshAccessToken(creds);
4071
+ await patchStatus(opts.updateUrl, creds.access_token, { status: "processing" }).catch((err) => {
4072
+ process.stderr.write(`[slack-import] processing PATCH failed: ${err.message}
4073
+ `);
4074
+ });
4075
+ const rawNotes = await readNotes(opts);
4076
+ if (!rawNotes.trim()) {
4077
+ await patchStatus(opts.updateUrl, creds.access_token, {
4078
+ status: "failed",
4079
+ error_message: "No notes provided on stdin"
4080
+ });
4081
+ throw new CliError(CLI_EXIT_CODES.GENERIC_ERROR, "No notes provided");
4082
+ }
4083
+ let bullets;
4084
+ try {
4085
+ bullets = await generateBullets({
4086
+ notes: sanitiseNotes(rawNotes),
4087
+ claudePath: opts.claudePath
4088
+ });
4089
+ } catch (err) {
4090
+ const msg = err instanceof Error ? err.message : String(err);
4091
+ await patchStatus(opts.updateUrl, creds.access_token, {
4092
+ status: "failed",
4093
+ error_message: msg.slice(0, 2e3)
4094
+ }).catch(() => void 0);
4095
+ throw new CliError(CLI_EXIT_CODES.GENERIC_ERROR, `Bullet generation failed: ${msg}`);
4096
+ }
4097
+ const result = await patchStatus(opts.updateUrl, creds.access_token, {
4098
+ status: "completed",
4099
+ bullets
4100
+ });
4101
+ process.stdout.write(
4102
+ `${c.ok("\u2713")} slack-import ${opts.importId.slice(0, 8)}\u2026 \u2014 ${bullets.length} bullets (${result.code_count ?? 0} code, ${result.physical_count ?? 0} physical)
4103
+ `
4104
+ );
4105
+ }
4106
+ async function readNotes(opts) {
4107
+ if (opts.notes !== void 0) return opts.notes;
4108
+ const chunks = [];
4109
+ let total = 0;
4110
+ for await (const chunk of process.stdin) {
4111
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4112
+ total += buf.length;
4113
+ if (total > 6e4) {
4114
+ throw new CliError(CLI_EXIT_CODES.GENERIC_ERROR, "Notes exceed 60KB");
4115
+ }
4116
+ chunks.push(buf);
4117
+ }
4118
+ return Buffer.concat(chunks).toString("utf8");
4119
+ }
4120
+ async function generateBullets(args) {
4121
+ const claude = args.claudePath ?? "claude";
4122
+ const cliArgs = [
4123
+ "--print",
4124
+ "--output-format",
4125
+ "json",
4126
+ "--tools",
4127
+ "",
4128
+ "--system-prompt",
4129
+ SYSTEM_PROMPT,
4130
+ "--model",
4131
+ SLACK_IMPORT_MODEL,
4132
+ "--json-schema",
4133
+ JSON.stringify(BULLETS_SCHEMA)
4134
+ ];
4135
+ return new Promise((resolve2, reject) => {
4136
+ let child;
4137
+ try {
4138
+ child = spawn5(claude, cliArgs, { stdio: ["pipe", "pipe", "pipe"] });
4139
+ } catch (err) {
4140
+ reject(new Error(`Could not invoke claude: ${err.message}`));
4141
+ return;
4142
+ }
4143
+ let stdoutBuf = "";
4144
+ let stderrBuf = "";
4145
+ child.stdout?.on("data", (b) => stdoutBuf += b.toString("utf8"));
4146
+ child.stderr?.on("data", (b) => stderrBuf += b.toString("utf8"));
4147
+ child.on("error", (err) => reject(err));
4148
+ child.on("close", (code) => {
4149
+ if (code !== 0) {
4150
+ reject(
4151
+ new Error(
4152
+ `claude exited ${code}${stderrBuf.trim() ? ": " + stderrBuf.trim().slice(0, 500) : ""}`
4153
+ )
4154
+ );
4155
+ return;
4156
+ }
4157
+ const extracted = extractStructured(stdoutBuf);
4158
+ if (!extracted.ok) {
4159
+ reject(new Error(extracted.error));
4160
+ return;
4161
+ }
4162
+ const list = extracted.value.bullets;
4163
+ if (!Array.isArray(list)) {
4164
+ reject(new Error("claude returned no bullets array"));
4165
+ return;
4166
+ }
4167
+ const out = [];
4168
+ for (const b of list) {
4169
+ const cls = b["classification"];
4170
+ if (cls !== "code" && cls !== "physical") continue;
4171
+ const title = typeof b["title"] === "string" ? b["title"].slice(0, 500) : "";
4172
+ const description = typeof b["description"] === "string" ? b["description"].slice(0, 8e3) : "";
4173
+ if (!title || !description) continue;
4174
+ const type2 = BULLET_TYPES.includes(b["type"]) ? b["type"] : "task";
4175
+ const priority = BULLET_PRIORITIES.includes(b["priority"]) ? b["priority"] : "medium";
4176
+ const item = { title, description, type: type2, priority, classification: cls };
4177
+ if (typeof b["reason"] === "string") item.reason = b["reason"].slice(0, 500);
4178
+ out.push(item);
4179
+ }
4180
+ resolve2(out);
4181
+ });
4182
+ child.stdin?.write(args.notes);
4183
+ child.stdin?.end();
4184
+ });
4185
+ }
4186
+ function extractStructured(raw) {
4187
+ const trimmed = raw.trim();
4188
+ if (!trimmed) return { ok: false, error: "claude returned empty stdout" };
4189
+ try {
4190
+ const env = JSON.parse(trimmed);
4191
+ if (env.is_error === true) {
4192
+ const code = typeof env.error_code === "string" ? env.error_code : "unknown";
4193
+ const result = typeof env.result === "string" ? env.result.slice(0, 400) : "";
4194
+ return { ok: false, error: `claude is_error (${code}): ${result || "no detail"}` };
4195
+ }
4196
+ if (env.structured_output && typeof env.structured_output === "object") {
4197
+ return { ok: true, value: env.structured_output };
4198
+ }
4199
+ if (typeof env.result === "string") {
4200
+ const m = env.result.match(/\{[\s\S]*\}/);
4201
+ if (m) return { ok: true, value: JSON.parse(m[0]) };
4202
+ }
4203
+ } catch {
4204
+ }
4205
+ try {
4206
+ const m = trimmed.match(/\{[\s\S]*\}/);
4207
+ if (m) return { ok: true, value: JSON.parse(m[0]) };
4208
+ } catch {
4209
+ }
4210
+ return { ok: false, error: "claude returned no parseable JSON" };
4211
+ }
4212
+ async function patchStatus(url, bearer, body) {
4213
+ const res = await request5(url, {
4214
+ method: "PATCH",
4215
+ headers: {
4216
+ "Content-Type": "application/json",
4217
+ Authorization: `Bearer ${bearer}`
4218
+ },
4219
+ body: JSON.stringify(body)
4220
+ });
4221
+ const text = await res.body.text();
4222
+ if (res.statusCode >= 400) {
4223
+ throw new Error(`PATCH ${url} \u2192 ${res.statusCode}: ${text.slice(0, 400)}`);
4224
+ }
4225
+ try {
4226
+ const parsed = JSON.parse(text);
4227
+ return parsed.data ?? {};
4228
+ } catch {
4229
+ return {};
4230
+ }
4231
+ }
4232
+
4000
4233
  // src/commands/fast-track.ts
4001
4234
  import { randomUUID as randomUUID3 } from "crypto";
4002
4235
  import ora3 from "ora";
@@ -4390,7 +4623,7 @@ import { platform as platform2 } from "os";
4390
4623
  import { mkdir as mkdir9, readFile as readFile5, writeFile as writeFile10, unlink as unlink4, readdir as readdir2 } from "fs/promises";
4391
4624
  import { homedir as homedir6 } from "os";
4392
4625
  import { join as join11 } from "path";
4393
- import { execFileSync as execFileSync9, spawn as spawn5 } from "child_process";
4626
+ import { execFileSync as execFileSync9, spawn as spawn6 } from "child_process";
4394
4627
 
4395
4628
  // src/scheduler/cron-translate.ts
4396
4629
  function translateToLaunchd(cron) {
@@ -4608,7 +4841,7 @@ var launchdAdapter = {
4608
4841
  return new Promise((resolve2) => {
4609
4842
  const args = entry.command.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [entry.command];
4610
4843
  const cmd = args.shift() ?? entry.command;
4611
- const child = spawn5(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
4844
+ const child = spawn6(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
4612
4845
  let stdoutTail = "";
4613
4846
  let stderrTail = "";
4614
4847
  child.stdout?.on("data", (chunk) => {
@@ -4654,7 +4887,7 @@ var launchdAdapter = {
4654
4887
  };
4655
4888
 
4656
4889
  // src/scheduler/cron.ts
4657
- import { execFileSync as execFileSync10, spawn as spawn6 } from "child_process";
4890
+ import { execFileSync as execFileSync10, spawn as spawn7 } from "child_process";
4658
4891
 
4659
4892
  // src/scheduler/safe-command.ts
4660
4893
  var FORBIDDEN = /[;&|`$()<>\\]/;
@@ -4715,7 +4948,7 @@ function readCrontab() {
4715
4948
  }
4716
4949
  }
4717
4950
  function writeCrontab(text) {
4718
- const child = spawn6("crontab", ["-"], { stdio: ["pipe", "inherit", "inherit"] });
4951
+ const child = spawn7("crontab", ["-"], { stdio: ["pipe", "inherit", "inherit"] });
4719
4952
  child.stdin.write(text);
4720
4953
  child.stdin.end();
4721
4954
  }
@@ -4796,7 +5029,7 @@ var cronAdapter = {
4796
5029
  return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
4797
5030
  }
4798
5031
  return new Promise((resolve2) => {
4799
- const child = spawn6(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
5032
+ const child = spawn7(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
4800
5033
  let stdoutTail = "";
4801
5034
  let stderrTail = "";
4802
5035
  child.stdout?.on(
@@ -4824,7 +5057,7 @@ var cronAdapter = {
4824
5057
  };
4825
5058
 
4826
5059
  // src/scheduler/windows.ts
4827
- import { execFileSync as execFileSync11, spawn as spawn7 } from "child_process";
5060
+ import { execFileSync as execFileSync11, spawn as spawn8 } from "child_process";
4828
5061
  var TASK_PREFIX = "TaskCLI_";
4829
5062
  function taskName(id) {
4830
5063
  return `${TASK_PREFIX}${id.replace(/[^A-Za-z0-9_-]/g, "_")}`;
@@ -4937,7 +5170,7 @@ var windowsAdapter = {
4937
5170
  return Promise.resolve({ exitCode: 1, stdoutTail: "", stderrTail: `rejected: ${reason}` });
4938
5171
  }
4939
5172
  return new Promise((resolve2) => {
4940
- const child = spawn7(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
5173
+ const child = spawn8(parsed.bin, parsed.args, { stdio: ["ignore", "pipe", "pipe"] });
4941
5174
  let stdoutTail = "";
4942
5175
  let stderrTail = "";
4943
5176
  child.stdout?.on(
@@ -5365,7 +5598,7 @@ function registerConfig(program2) {
5365
5598
  import { execFileSync as execFileSync12 } from "child_process";
5366
5599
  import { readFile as readFile8, writeFile as writeFile12 } from "fs/promises";
5367
5600
  import { join as join14 } from "path";
5368
- import { request as request5 } from "undici";
5601
+ import { request as request6 } from "undici";
5369
5602
  var ALLOWED_TEST_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun", "node", "npx"]);
5370
5603
  var DEFAULT_TEST_COMMAND = "pnpm typecheck";
5371
5604
  function registerDoctor(program2) {
@@ -5406,7 +5639,7 @@ function registerDoctor(program2) {
5406
5639
  });
5407
5640
  const apiUrl = creds?.api_url ?? cfg.api_url;
5408
5641
  try {
5409
- const res = await request5(apiUrl, {
5642
+ const res = await request6(apiUrl, {
5410
5643
  method: "GET",
5411
5644
  headersTimeout: 5e3,
5412
5645
  bodyTimeout: 5e3
@@ -5663,7 +5896,7 @@ function checkBinary(name, command) {
5663
5896
  }
5664
5897
 
5665
5898
  // src/commands/version.ts
5666
- var CLI_VERSION = true ? "0.2.24" : "0.0.0-dev";
5899
+ var CLI_VERSION = true ? "0.2.26" : "0.0.0-dev";
5667
5900
  function registerVersion(program2) {
5668
5901
  program2.command("version").description("Print the CLI version").action(() => {
5669
5902
  process.stdout.write(CLI_VERSION + "\n");
@@ -5690,6 +5923,7 @@ registerMultiWork(program);
5690
5923
  registerResume(program);
5691
5924
  registerReset(program);
5692
5925
  registerScan(program);
5926
+ registerSlackImport(program);
5693
5927
  registerFastTrack(program);
5694
5928
  registerPrTest(program);
5695
5929
  registerScheduledTask(program);