@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 +244 -10
- package/dist/cli.js.map +1 -1
- package/package.json +3 -3
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
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);
|