@inteeka/task-cli 0.2.15 → 0.2.17

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
@@ -247,7 +247,7 @@ import open from "open";
247
247
  import ora from "ora";
248
248
 
249
249
  // src/config/credentials.ts
250
- import { mkdir, readFile, writeFile, unlink, chmod, stat } from "fs/promises";
250
+ import { mkdir, readFile, writeFile, unlink, chmod, stat, rename } from "fs/promises";
251
251
  import { homedir } from "os";
252
252
  import { dirname, join } from "path";
253
253
  var CONFIG_DIR = join(homedir(), ".config", "task");
@@ -289,8 +289,10 @@ async function readCredentials() {
289
289
  }
290
290
  async function writeCredentials(creds) {
291
291
  await ensureDir(dirname(CREDENTIALS_PATH));
292
- await writeFile(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
293
- await chmod(CREDENTIALS_PATH, 384);
292
+ const tmpPath = `${CREDENTIALS_PATH}.${process.pid}.tmp`;
293
+ await writeFile(tmpPath, JSON.stringify(creds, null, 2), { mode: 384 });
294
+ await chmod(tmpPath, 384);
295
+ await rename(tmpPath, CREDENTIALS_PATH);
294
296
  }
295
297
  async function clearCredentials() {
296
298
  try {
@@ -1732,7 +1734,7 @@ async function runProjectTest(args) {
1732
1734
  }
1733
1735
 
1734
1736
  // src/util/progress.ts
1735
- import { mkdir as mkdir6, writeFile as writeFile7, rename, unlink as unlink3, readdir, stat as stat2 } from "fs/promises";
1737
+ import { mkdir as mkdir6, writeFile as writeFile7, rename as rename2, unlink as unlink3, readdir, stat as stat2 } from "fs/promises";
1736
1738
  import { tmpdir } from "os";
1737
1739
  import { join as join7 } from "path";
1738
1740
  var PROGRESS_DIR = join7(tmpdir(), "task-progress");
@@ -1788,7 +1790,7 @@ var ProgressWriter = class {
1788
1790
  const tmp = `${this.path}.tmp`;
1789
1791
  try {
1790
1792
  await writeFile7(tmp, body, { encoding: "utf8", mode: 384 });
1791
- await rename(tmp, this.path);
1793
+ await rename2(tmp, this.path);
1792
1794
  } catch {
1793
1795
  await unlink3(tmp).catch(() => {
1794
1796
  });
@@ -3050,14 +3052,17 @@ async function jsonRequest(url, init) {
3050
3052
  };
3051
3053
  }
3052
3054
  var AutopilotApi = class {
3055
+ creds;
3053
3056
  constructor(opts) {
3054
- this.opts = opts;
3057
+ this.creds = opts.creds;
3058
+ this.apiUrl = opts.apiUrl;
3055
3059
  }
3056
- adminHeaders() {
3060
+ apiUrl;
3061
+ async userHeaders() {
3062
+ this.creds = await ensureFreshAccessToken(this.creds);
3057
3063
  return {
3058
3064
  "Content-Type": "application/json",
3059
- Authorization: `Bearer ${this.opts.apiKey}`,
3060
- "X-Actor-Email": this.opts.actorEmail,
3065
+ Authorization: `Bearer ${this.creds.access_token}`,
3061
3066
  "User-Agent": "task-cli/scan"
3062
3067
  };
3063
3068
  }
@@ -3070,12 +3075,13 @@ var AutopilotApi = class {
3070
3075
  };
3071
3076
  }
3072
3077
  async listEligibleProjects() {
3073
- const url = `${this.opts.apiUrl}/api/v1/cli/projects`;
3078
+ const url = `${this.apiUrl}/api/v1/cli/projects`;
3074
3079
  const result = await jsonRequest(url, {
3075
3080
  method: "GET",
3076
- headers: this.adminHeaders()
3081
+ headers: await this.userHeaders()
3077
3082
  });
3078
3083
  if (!result.ok) {
3084
+ await handleUserAuthFailure(result.code, result.status);
3079
3085
  throw new CliError(
3080
3086
  autopilotExitCode(result.code, result.status),
3081
3087
  `${result.code}: ${result.message}`
@@ -3084,10 +3090,10 @@ var AutopilotApi = class {
3084
3090
  return result.data ?? [];
3085
3091
  }
3086
3092
  async issueSkillToken(args) {
3087
- const url = `${this.opts.apiUrl}/api/v1/cli/issue-skill-token`;
3093
+ const url = `${this.apiUrl}/api/v1/cli/issue-skill-token`;
3088
3094
  const result = await jsonRequest(url, {
3089
3095
  method: "POST",
3090
- headers: this.adminHeaders(),
3096
+ headers: await this.userHeaders(),
3091
3097
  body: {
3092
3098
  project_id: args.project_id,
3093
3099
  scope: "fix_prompt_sync",
@@ -3096,6 +3102,7 @@ var AutopilotApi = class {
3096
3102
  }
3097
3103
  });
3098
3104
  if (!result.ok) {
3105
+ await handleUserAuthFailure(result.code, result.status);
3099
3106
  throw new CliError(
3100
3107
  autopilotExitCode(result.code, result.status),
3101
3108
  `${result.code}: ${result.message}`
@@ -3104,7 +3111,7 @@ var AutopilotApi = class {
3104
3111
  return result.data;
3105
3112
  }
3106
3113
  async prepare(skillToken, batchSize, idempotencyKey) {
3107
- const url = `${this.opts.apiUrl}/api/v1/cli/fix-prompt-sync/prepare`;
3114
+ const url = `${this.apiUrl}/api/v1/cli/fix-prompt-sync/prepare`;
3108
3115
  const result = await jsonRequest(url, {
3109
3116
  method: "POST",
3110
3117
  headers: this.skillHeaders(skillToken, { "Idempotency-Key": idempotencyKey }),
@@ -3122,7 +3129,7 @@ var AutopilotApi = class {
3122
3129
  return result.data;
3123
3130
  }
3124
3131
  async submit(args) {
3125
- const url = `${this.opts.apiUrl}/api/v1/cli/fix-prompt-sync/submit`;
3132
+ const url = `${this.apiUrl}/api/v1/cli/fix-prompt-sync/submit`;
3126
3133
  const result = await jsonRequest(url, {
3127
3134
  method: "POST",
3128
3135
  headers: this.skillHeaders(args.skillToken, { "X-Prepare-Nonce": args.nonce }),
@@ -3148,7 +3155,7 @@ var AutopilotApi = class {
3148
3155
  }
3149
3156
  async abort(skillToken, ticketIds) {
3150
3157
  if (ticketIds.length === 0) return;
3151
- const url = `${this.opts.apiUrl}/api/v1/cli/fix-prompt-sync/abort`;
3158
+ const url = `${this.apiUrl}/api/v1/cli/fix-prompt-sync/abort`;
3152
3159
  await jsonRequest(url, {
3153
3160
  method: "POST",
3154
3161
  headers: this.skillHeaders(skillToken),
@@ -3156,7 +3163,7 @@ var AutopilotApi = class {
3156
3163
  }).catch(() => void 0);
3157
3164
  }
3158
3165
  async runSummary(skillToken, summary) {
3159
- const url = `${this.opts.apiUrl}/api/v1/cli/fix-prompt-sync/run-summary`;
3166
+ const url = `${this.apiUrl}/api/v1/cli/fix-prompt-sync/run-summary`;
3160
3167
  await jsonRequest(url, {
3161
3168
  method: "POST",
3162
3169
  headers: this.skillHeaders(skillToken),
@@ -3164,6 +3171,24 @@ var AutopilotApi = class {
3164
3171
  }).catch(() => void 0);
3165
3172
  }
3166
3173
  };
3174
+ async function handleUserAuthFailure(code, status) {
3175
+ if (status === 401 && (code === "UNAUTHORIZED" || code === "TOKEN_EXPIRED")) {
3176
+ await clearCredentials();
3177
+ throw new CliError(
3178
+ CLI_EXIT_CODES.UNAUTHORISED,
3179
+ "Your CLI session is no longer valid",
3180
+ "Run 'task login' to authenticate again."
3181
+ );
3182
+ }
3183
+ if (status === 403 && code === "CLI_ACCESS_REVOKED") {
3184
+ await clearCredentials();
3185
+ throw new CliError(
3186
+ CLI_EXIT_CODES.UNAUTHORISED,
3187
+ "CLI access has been revoked",
3188
+ "Ask a project admin to re-grant access from the Agentic CLI page."
3189
+ );
3190
+ }
3191
+ }
3167
3192
  function autopilotExitCode(code, status) {
3168
3193
  if (status === 401 || status === 403) return CLI_EXIT_CODES.UNAUTHORISED;
3169
3194
  if (code === "TIER_LIMIT_EXCEEDED" || code === "NO_GIT_INTEGRATION") {
@@ -3467,7 +3492,7 @@ function registerScan(program2) {
3467
3492
  "Restrict to one project (default: the linked project from .task/config.json, falling back to every visible project if the repo is not linked)"
3468
3493
  ).option(
3469
3494
  "--all-projects",
3470
- "Override the linked-project default and scan every CLI-eligible project the admin token can see"
3495
+ "Override the linked-project default and scan every CLI-eligible project the signed-in user has cli_access on"
3471
3496
  ).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) => {
3472
3497
  await runScan(opts);
3473
3498
  });
@@ -3489,25 +3514,18 @@ async function runScan(opts) {
3489
3514
  }
3490
3515
  }
3491
3516
  async function runScanImpl(opts, progress) {
3492
- const apiKey = process.env["TASK_API_KEY"];
3493
- if (!apiKey || apiKey.length < 32) {
3494
- throw new CliError(
3495
- CLI_EXIT_CODES.MISCONFIGURATION,
3496
- "TASK_API_KEY is missing or shorter than 32 chars",
3497
- "Set TASK_API_KEY in your environment. The autopilot loop authenticates with the shared admin secret, not the per-user CLI bearer."
3498
- );
3499
- }
3500
- const actorEmail = process.env["TASK_API_KEY_OWNER_EMAIL"];
3501
- if (!actorEmail || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(actorEmail)) {
3517
+ let creds = await readCredentials();
3518
+ if (!creds) {
3502
3519
  throw new CliError(
3503
3520
  CLI_EXIT_CODES.MISCONFIGURATION,
3504
- "TASK_API_KEY_OWNER_EMAIL is not set or not a valid email",
3505
- "Set TASK_API_KEY_OWNER_EMAIL=<you@example.com>. The server records this on every audit row."
3521
+ "Not signed in",
3522
+ "Run 'task login' to authenticate. The scan autopilot uses the same per-user OAuth bearer as every other CLI command."
3506
3523
  );
3507
3524
  }
3525
+ creds = await ensureFreshAccessToken(creds);
3508
3526
  const localCfg = await readLocalConfig();
3509
3527
  const linkedProject = await readProjectConfig(findRepoRoot());
3510
- const apiUrl = (opts.apiUrl ?? process.env["TASK_API_URL"] ?? localCfg.api_url ?? linkedProject?.api_url ?? "http://localhost:3400").replace(/\/$/, "");
3528
+ const apiUrl = (opts.apiUrl ?? process.env["TASK_API_URL"] ?? creds.api_url ?? localCfg.api_url ?? linkedProject?.api_url ?? "http://localhost:3400").replace(/\/$/, "");
3511
3529
  const max = clampInt(opts.max, 1, 500, 50);
3512
3530
  const batchSize = clampInt(opts.batch, 1, 10, 5);
3513
3531
  const silent = !!opts.silent || localCfg.silent;
@@ -3526,7 +3544,7 @@ async function runScanImpl(opts, progress) {
3526
3544
  projectFilter = linkedProject.project_id;
3527
3545
  filterSource = "link";
3528
3546
  }
3529
- const api = new AutopilotApi({ apiUrl, apiKey, actorEmail });
3547
+ const api = new AutopilotApi({ apiUrl, creds });
3530
3548
  if (!silent) process.stdout.write(`${c.dim("Discovering eligible projects\u2026")}
3531
3549
  `);
3532
3550
  const all = await api.listEligibleProjects();
@@ -3542,7 +3560,7 @@ async function runScanImpl(opts, progress) {
3542
3560
  throw new CliError(
3543
3561
  CLI_EXIT_CODES.GENERIC_ERROR,
3544
3562
  `Linked project ${linkedProject.organisation_slug}/${linkedProject.project_slug} has no CLI-eligible tickets right now`,
3545
- "Mark a ticket as CLI-eligible from the dashboard, or pass --all-projects to scan everything the admin token can see."
3563
+ "Mark a ticket as CLI-eligible from the dashboard, or pass --all-projects to scan every project you have cli_access on."
3546
3564
  );
3547
3565
  }
3548
3566
  throw new CliError(
@@ -4868,26 +4886,37 @@ import { request as request5 } from "undici";
4868
4886
  var ALLOWED_TEST_EXECUTABLES = /* @__PURE__ */ new Set(["pnpm", "npm", "yarn", "bun", "node", "npx"]);
4869
4887
  var DEFAULT_TEST_COMMAND = "pnpm typecheck";
4870
4888
  function registerDoctor(program2) {
4871
- program2.command("doctor").description("Diagnose your CLI setup").option("--fix", "attempt to auto-remediate fixable problems (e.g. add a typecheck script)").action(async (opts) => {
4889
+ program2.command("doctor").description(
4890
+ "Diagnose your CLI setup \u2014 Identity (who you are signed in as) first, then Setup checks"
4891
+ ).option("--fix", "attempt to auto-remediate fixable problems (e.g. add a typecheck script)").action(async (opts) => {
4872
4892
  const checks = [];
4873
4893
  const creds = await readCredentials();
4894
+ let accessLite = null;
4895
+ if (creds) {
4896
+ accessLite = await fetchAccessLite();
4897
+ }
4898
+ const authDetail = creds ? renderAuthDetail(creds.email, creds.access_expires_at, accessLite) : "not signed in \u2014 run 'task login'";
4874
4899
  checks.push({
4900
+ group: "identity",
4875
4901
  name: "auth",
4876
- ok: !!creds,
4877
- detail: creds ? `signed in as ${creds.email ?? "(unknown)"}, expires ${creds.access_expires_at}` : "not signed in \u2014 run 'task login'"
4902
+ ok: !!creds && (accessLite?.has_access ?? true),
4903
+ detail: authDetail,
4904
+ remediation: !creds ? "run 'task login' to authenticate" : accessLite && !accessLite.has_access ? "Your account has no project with cli_access \u2014 ask an admin to grant access on the Agentic CLI page." : void 0
4878
4905
  });
4879
4906
  const root = findRepoRoot();
4880
4907
  const project = await readProjectConfig(root);
4881
4908
  checks.push({
4909
+ group: "identity",
4882
4910
  name: "project link",
4883
4911
  ok: !!project,
4884
4912
  detail: project ? `${project.organisation_slug}/${project.project_slug}` : "no link \u2014 run 'task link'"
4885
4913
  });
4886
4914
  const cfg = await readLocalConfig();
4887
- checks.push(checkBinary("claude", cfg.claude_path ?? "claude"));
4888
- checks.push(checkBinary("git", "git"));
4915
+ checks.push({ group: "setup", ...checkBinary("claude", cfg.claude_path ?? "claude") });
4916
+ checks.push({ group: "setup", ...checkBinary("git", "git") });
4889
4917
  const { kind } = getSchedulerAdapter();
4890
4918
  checks.push({
4919
+ group: "setup",
4891
4920
  name: "scheduler",
4892
4921
  ok: kind !== "unsupported",
4893
4922
  detail: kind === "unsupported" ? "unsupported platform" : kind
@@ -4901,12 +4930,14 @@ function registerDoctor(program2) {
4901
4930
  });
4902
4931
  await res.body.dump();
4903
4932
  checks.push({
4933
+ group: "setup",
4904
4934
  name: "api reachable",
4905
4935
  ok: true,
4906
4936
  detail: `${apiUrl} (HTTP ${res.statusCode})`
4907
4937
  });
4908
4938
  } catch (err) {
4909
4939
  checks.push({
4940
+ group: "setup",
4910
4941
  name: "api reachable",
4911
4942
  ok: false,
4912
4943
  detail: `${apiUrl}: ${err.message}`
@@ -4917,6 +4948,7 @@ function registerDoctor(program2) {
4917
4948
  try {
4918
4949
  const exists = remoteBranchExists(root, baseBranch);
4919
4950
  checks.push({
4951
+ group: "setup",
4920
4952
  name: "base branch on origin",
4921
4953
  ok: exists,
4922
4954
  detail: exists ? `origin/${baseBranch} reachable` : `origin has no branch "${baseBranch}"`,
@@ -4924,6 +4956,7 @@ function registerDoctor(program2) {
4924
4956
  });
4925
4957
  } catch (err) {
4926
4958
  checks.push({
4959
+ group: "setup",
4927
4960
  name: "base branch on origin",
4928
4961
  ok: false,
4929
4962
  detail: err instanceof CliError ? err.message : `could not check origin: ${err.message}`,
@@ -4937,41 +4970,57 @@ function registerDoctor(program2) {
4937
4970
  encoding: "utf8"
4938
4971
  }).trim();
4939
4972
  checks.push({
4973
+ group: "setup",
4940
4974
  name: "working tree",
4941
4975
  ok: dirty.length === 0,
4942
4976
  detail: dirty.length === 0 ? "clean" : "has uncommitted changes"
4943
4977
  });
4944
4978
  } catch {
4945
- checks.push({ name: "working tree", ok: false, detail: "not in a git repo" });
4979
+ checks.push({
4980
+ group: "setup",
4981
+ name: "working tree",
4982
+ ok: false,
4983
+ detail: "not in a git repo"
4984
+ });
4946
4985
  }
4947
4986
  const testCheck = await checkPrePushTest(
4948
4987
  root,
4949
4988
  project?.cli_test_command ?? null,
4950
4989
  opts.fix === true
4951
4990
  );
4952
- checks.push(testCheck);
4991
+ checks.push({ group: "setup", ...testCheck });
4953
4992
  let inFlight = [];
4954
4993
  if (creds && project) {
4955
4994
  inFlight = await listInFlightTickets(project.project_id, root);
4956
4995
  checks.push({
4996
+ group: "setup",
4957
4997
  name: "in-flight tickets",
4958
4998
  ok: true,
4959
4999
  detail: inFlight.length === 0 ? "none" : `${inFlight.length} ticket(s) waiting to be resumed`
4960
5000
  });
4961
5001
  }
4962
5002
  let allOk = true;
4963
- for (const check of checks) {
4964
- const sym = check.ok ? c.ok("\u2713") : c.err("\u2717");
4965
- process.stdout.write(`${sym} ${check.name.padEnd(20)} ${c.dim(check.detail)}
5003
+ const renderGroup = (label, group) => {
5004
+ const groupChecks = checks.filter((ch) => ch.group === group);
5005
+ if (groupChecks.length === 0) return;
5006
+ process.stdout.write(`${c.bold(label)}
5007
+ `);
5008
+ for (const check of groupChecks) {
5009
+ const sym = check.ok ? c.ok("\u2713") : c.err("\u2717");
5010
+ process.stdout.write(`${sym} ${check.name.padEnd(20)} ${c.dim(check.detail)}
4966
5011
  `);
4967
- if (!check.ok) {
4968
- allOk = false;
4969
- if (check.remediation) {
4970
- process.stdout.write(` ${c.dim("\u2192 " + check.remediation)}
5012
+ if (!check.ok) {
5013
+ allOk = false;
5014
+ if (check.remediation) {
5015
+ process.stdout.write(` ${c.dim("\u2192 " + check.remediation)}
4971
5016
  `);
5017
+ }
4972
5018
  }
4973
5019
  }
4974
- }
5020
+ };
5021
+ renderGroup("Identity", "identity");
5022
+ process.stdout.write("\n");
5023
+ renderGroup("Setup", "setup");
4975
5024
  for (const t of inFlight) {
4976
5025
  const status = t.branchPresent ? c.ok("local branch present") : c.err("local branch missing");
4977
5026
  process.stdout.write(
@@ -4982,6 +5031,24 @@ function registerDoctor(program2) {
4982
5031
  if (!allOk) process.exit(1);
4983
5032
  });
4984
5033
  }
5034
+ async function fetchAccessLite() {
5035
+ try {
5036
+ return await apiCallOrThrow("GET", "/api/v1/cli/access");
5037
+ } catch {
5038
+ return null;
5039
+ }
5040
+ }
5041
+ function renderAuthDetail(email, expiresAt, access2) {
5042
+ const who = `signed in as ${email ?? "(unknown)"}`;
5043
+ const expiry = `expires ${expiresAt}`;
5044
+ if (!access2) return `${who}, ${expiry}`;
5045
+ if (access2.projects.length === 0) {
5046
+ return `${who}, ${expiry}, no cli_access projects`;
5047
+ }
5048
+ const sample = access2.projects.slice(0, 3).map((p) => `${p.organisation_slug}/${p.slug}`).join(", ");
5049
+ const more = access2.projects.length > 3 ? `, +${access2.projects.length - 3} more` : "";
5050
+ return `${who}, ${expiry}, ${access2.projects.length} project${access2.projects.length === 1 ? "" : "s"} (${sample}${more})`;
5051
+ }
4985
5052
  async function listInFlightTickets(projectId, cwd) {
4986
5053
  const result = await apiCall(
4987
5054
  "GET",
@@ -5113,7 +5180,7 @@ function checkBinary(name, command) {
5113
5180
  }
5114
5181
 
5115
5182
  // src/commands/version.ts
5116
- var CLI_VERSION = true ? "0.2.15" : "0.0.0-dev";
5183
+ var CLI_VERSION = true ? "0.2.17" : "0.0.0-dev";
5117
5184
  function registerVersion(program2) {
5118
5185
  program2.command("version").description("Print the CLI version").action(() => {
5119
5186
  process.stdout.write(CLI_VERSION + "\n");