@hasna/todos 0.11.84 → 0.11.86

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/index.js CHANGED
@@ -8571,6 +8571,21 @@ async function cloudAddComment(client, taskId, input) {
8571
8571
  }
8572
8572
  return raw;
8573
8573
  }
8574
+ async function cloudTaskHistory(client, taskId) {
8575
+ const raw = await client.transport.get(`/tasks/${encodeURIComponent(taskId)}/history`);
8576
+ const envelope = raw ?? {};
8577
+ if (Array.isArray(envelope.history))
8578
+ return envelope.history;
8579
+ return Array.isArray(raw) ? raw : [];
8580
+ }
8581
+ async function cloudUpsertTaskByFingerprint(client, input) {
8582
+ const raw = await client.transport.post("/tasks/upsert", input);
8583
+ const envelope = raw ?? {};
8584
+ return {
8585
+ task: unwrapTask(envelope.task ?? raw),
8586
+ created: Boolean(envelope.created)
8587
+ };
8588
+ }
8574
8589
  async function cloudCountTasks(client, filter = {}) {
8575
8590
  const { limit: _drop, offset: _o, ...rest } = filter;
8576
8591
  const res = await client.list("tasks", { query: { ...toListQuery(rest), limit: 1 } });
@@ -8587,6 +8602,54 @@ async function cloudRegisterAgent(client, input) {
8587
8602
  }
8588
8603
  return raw;
8589
8604
  }
8605
+ async function cloudHeartbeatAgent(client, idOrName) {
8606
+ const raw = await client.transport.post(`/agents/${encodeURIComponent(idOrName)}/heartbeat`, {});
8607
+ if (raw && typeof raw === "object" && "agent" in raw) {
8608
+ return raw.agent;
8609
+ }
8610
+ return raw ?? null;
8611
+ }
8612
+ async function cloudReleaseAgent(client, idOrName, sessionId) {
8613
+ const raw = await client.transport.post(`/agents/${encodeURIComponent(idOrName)}/release`, sessionId ? { session_id: sessionId } : {});
8614
+ const env = raw ?? {};
8615
+ return { agent: env.agent ?? null, released: env.released !== false };
8616
+ }
8617
+ async function cloudLinkCommit(client, taskId, input) {
8618
+ const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/commits`, input);
8619
+ if (raw && typeof raw === "object" && "commit" in raw) {
8620
+ return raw.commit;
8621
+ }
8622
+ return raw;
8623
+ }
8624
+ async function cloudFindCommit(client, sha) {
8625
+ const raw = await client.transport.get(`/commits/${encodeURIComponent(sha)}`);
8626
+ const env = raw ?? {};
8627
+ return env.commit ?? null;
8628
+ }
8629
+ async function cloudLinkRef(client, taskId, input) {
8630
+ const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/refs`, input);
8631
+ if (raw && typeof raw === "object" && "ref" in raw) {
8632
+ return raw.ref;
8633
+ }
8634
+ return raw;
8635
+ }
8636
+ async function cloudFindRefs(client, ref) {
8637
+ const raw = await client.transport.get(`/refs/${encodeURIComponent(ref)}`);
8638
+ const env = raw ?? {};
8639
+ return Array.isArray(env.refs) ? env.refs : [];
8640
+ }
8641
+ async function cloudResolvePlan(client, ref, projectId) {
8642
+ const direct = await client.get("plans", ref).catch(() => null);
8643
+ if (direct) {
8644
+ const env = direct;
8645
+ if (env.plan)
8646
+ return env.plan;
8647
+ if (direct.id)
8648
+ return direct;
8649
+ }
8650
+ const plans = await cloudListPlans(client, projectId);
8651
+ return plans.find((p) => p.id === ref) ?? plans.find((p) => p.slug === ref) ?? plans.find((p) => p.name === ref) ?? plans.find((p) => p.id.startsWith(ref)) ?? null;
8652
+ }
8590
8653
  async function cloudLockTask(client, id, agentId) {
8591
8654
  const raw = await client.transport.post(`/tasks/${encodeURIComponent(id)}/lock`, { agent_id: agentId });
8592
8655
  if (raw && typeof raw === "object" && "result" in raw) {
@@ -20430,7 +20493,7 @@ function registerTaskCommands(program2) {
20430
20493
  }
20431
20494
  });
20432
20495
  const task = program2.command("task").description("Task subcommands for deterministic automation");
20433
- task.command("upsert").description("Create or update a task by stable metadata fingerprint").requiredOption("--fingerprint <key>", "Stable dedupe fingerprint").requiredOption("--title <text>", "Task title").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("-s, --status <status>", "Task status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--metadata-json <json>", "JSON object merged into task metadata").option("--working-dir <path>", "Working directory to store on create/update").option("--project <id>", "Assign to project by ID, slug, or path").option("--assign <agent>", "Assign to agent").option("--expectation-id <id>", "Expectation metadata ID").option("--expectation-fingerprint <key>", "Expectation metadata fingerprint").option("--evidence-paths <paths>", "Comma-separated evidence paths").option("--origin-loop-id <id>", "Origin loop ID").option("--origin-run-id <id>", "Origin run ID").option("--expected <json-or-text>", "Expected value metadata").option("--observed <json-or-text>", "Observed value metadata").option("--acceptance <json-or-text>", "Acceptance metadata").action((opts) => {
20496
+ task.command("upsert").description("Create or update a task by stable metadata fingerprint").requiredOption("--fingerprint <key>", "Stable dedupe fingerprint").requiredOption("--title <text>", "Task title").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("-s, --status <status>", "Task status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--metadata-json <json>", "JSON object merged into task metadata").option("--working-dir <path>", "Working directory to store on create/update").option("--project <id>", "Assign to project by ID, slug, or path").option("--assign <agent>", "Assign to agent").option("--expectation-id <id>", "Expectation metadata ID").option("--expectation-fingerprint <key>", "Expectation metadata fingerprint").option("--evidence-paths <paths>", "Comma-separated evidence paths").option("--origin-loop-id <id>", "Origin loop ID").option("--origin-run-id <id>", "Origin run ID").option("--expected <json-or-text>", "Expected value metadata").option("--observed <json-or-text>", "Observed value metadata").option("--acceptance <json-or-text>", "Acceptance metadata").action(async (opts) => {
20434
20497
  const globalOpts = program2.opts();
20435
20498
  opts.tags = opts.tags || opts.tag;
20436
20499
  opts.list = opts.list || opts.taskList;
@@ -20445,6 +20508,34 @@ function registerTaskCommands(program2) {
20445
20508
  }
20446
20509
  return id;
20447
20510
  })() : undefined;
20511
+ const cloud = getTodosCloudClient();
20512
+ if (cloud) {
20513
+ let cloudResult;
20514
+ try {
20515
+ cloudResult = await cloudUpsertTaskByFingerprint(cloud, {
20516
+ fingerprint: opts.fingerprint,
20517
+ title: opts.title,
20518
+ description: opts.description,
20519
+ priority: parsePriority(opts.priority),
20520
+ status: parseStatus(opts.status),
20521
+ task_list_id: taskListId,
20522
+ tags: parseTags(opts.tags),
20523
+ metadata: buildExpectationMetadata(opts),
20524
+ working_dir: opts.workingDir ? resolve9(opts.workingDir) : process.cwd(),
20525
+ project_id: projectId,
20526
+ assigned_to: opts.assign
20527
+ });
20528
+ } catch (e) {
20529
+ handleError(e);
20530
+ }
20531
+ if (globalOpts.json) {
20532
+ output(cloudResult, true);
20533
+ } else {
20534
+ console.log(chalk2.green(cloudResult.created ? "Task created:" : "Task updated:"));
20535
+ console.log(formatTaskLine(cloudResult.task));
20536
+ }
20537
+ return;
20538
+ }
20448
20539
  let result;
20449
20540
  try {
20450
20541
  result = upsertTaskByFingerprint({
@@ -20937,8 +21028,18 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
20937
21028
  program2.command("history <id>").description("Show change history for a task (audit log)").action(async (id) => {
20938
21029
  const globalOpts = program2.opts();
20939
21030
  const resolvedId = resolveTaskId(id);
20940
- const { getTaskHistory: getTaskHistory2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
20941
- const history = getTaskHistory2(resolvedId);
21031
+ const cloud = getTodosCloudClient();
21032
+ let history;
21033
+ if (cloud) {
21034
+ try {
21035
+ history = await cloudTaskHistory(cloud, resolvedId);
21036
+ } catch (e) {
21037
+ handleError(e);
21038
+ }
21039
+ } else {
21040
+ const { getTaskHistory: getTaskHistory2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
21041
+ history = getTaskHistory2(resolvedId);
21042
+ }
20942
21043
  if (globalOpts.json) {
20943
21044
  output(history, true);
20944
21045
  return;
@@ -21094,28 +21195,54 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21094
21195
  console.log(formatTaskLine(task2));
21095
21196
  }
21096
21197
  });
21097
- program2.command("approve <id>").description("Approve a task that requires approval").action((id) => {
21198
+ program2.command("approve <id>").description("Approve a task that requires approval").action(async (id) => {
21098
21199
  const globalOpts = program2.opts();
21099
- const resolvedId = resolveTaskId(id);
21100
- const task2 = getTask(resolvedId);
21101
- if (!task2) {
21102
- console.error(chalk2.red(`Task not found: ${id}`));
21103
- process.exit(1);
21104
- }
21105
- if (!task2.requires_approval) {
21106
- console.log(chalk2.yellow("This task does not require approval."));
21107
- return;
21108
- }
21109
- if (task2.approved_by) {
21110
- console.log(chalk2.yellow(`Already approved by ${task2.approved_by}.`));
21111
- return;
21112
- }
21200
+ const approver = globalOpts.agent || "cli";
21113
21201
  try {
21114
- const updated = updateTask(resolvedId, { approved_by: globalOpts.agent || "cli", version: task2.version });
21202
+ const cloud = getTodosCloudClient();
21203
+ if (cloud) {
21204
+ const cloudId = resolveTaskId(id);
21205
+ const task3 = await cloudGetTask(cloud, cloudId);
21206
+ if (!task3) {
21207
+ console.error(chalk2.red(`Task not found: ${id}`));
21208
+ process.exit(1);
21209
+ }
21210
+ if (!task3.requires_approval) {
21211
+ console.log(chalk2.yellow("This task does not require approval."));
21212
+ return;
21213
+ }
21214
+ if (task3.approved_by) {
21215
+ console.log(chalk2.yellow(`Already approved by ${task3.approved_by}.`));
21216
+ return;
21217
+ }
21218
+ const updated2 = await cloudUpdateTask(cloud, cloudId, { approved_by: approver, version: task3.version });
21219
+ if (globalOpts.json) {
21220
+ output(updated2, true);
21221
+ } else {
21222
+ console.log(chalk2.green(`Task approved by ${approver}:`));
21223
+ console.log(formatTaskLine(updated2));
21224
+ }
21225
+ return;
21226
+ }
21227
+ const resolvedId = resolveTaskId(id);
21228
+ const task2 = getTask(resolvedId);
21229
+ if (!task2) {
21230
+ console.error(chalk2.red(`Task not found: ${id}`));
21231
+ process.exit(1);
21232
+ }
21233
+ if (!task2.requires_approval) {
21234
+ console.log(chalk2.yellow("This task does not require approval."));
21235
+ return;
21236
+ }
21237
+ if (task2.approved_by) {
21238
+ console.log(chalk2.yellow(`Already approved by ${task2.approved_by}.`));
21239
+ return;
21240
+ }
21241
+ const updated = updateTask(resolvedId, { approved_by: approver, version: task2.version });
21115
21242
  if (globalOpts.json) {
21116
21243
  output(updated, true);
21117
21244
  } else {
21118
- console.log(chalk2.green(`Task approved by ${globalOpts.agent || "cli"}:`));
21245
+ console.log(chalk2.green(`Task approved by ${approver}:`));
21119
21246
  console.log(formatTaskLine(updated));
21120
21247
  }
21121
21248
  } catch (e) {
@@ -21218,7 +21345,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21218
21345
  process.exit(1);
21219
21346
  }
21220
21347
  });
21221
- program2.command("bulk <action> <ids...>").description("Bulk operation on multiple tasks (done, start, delete, plan)").option("--plan <id>", "Plan ID for the plan/move-plan action").option("--clear-plan", "Remove plan assignment for the plan/move-plan action").action((action, ids, opts) => {
21348
+ program2.command("bulk <action> <ids...>").description("Bulk operation on multiple tasks (done, start, delete, plan)").option("--plan <id>", "Plan ID for the plan/move-plan action").option("--clear-plan", "Remove plan assignment for the plan/move-plan action").action(async (action, ids, opts) => {
21222
21349
  const globalOpts = program2.opts();
21223
21350
  const results = [];
21224
21351
  const isPlanAction = action === "plan" || action === "move-plan";
@@ -21227,6 +21354,45 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21227
21354
  process.exit(1);
21228
21355
  }
21229
21356
  const planId = isPlanAction ? opts.plan ? resolvePlanId(opts.plan) : null : undefined;
21357
+ const knownActions = new Set(["done", "complete", "start", "delete", "plan", "move-plan"]);
21358
+ if (!knownActions.has(action)) {
21359
+ console.error(chalk2.red(`Unknown action: ${action}. Use: done, start, delete, plan`));
21360
+ process.exit(1);
21361
+ }
21362
+ const cloud = getTodosCloudClient();
21363
+ if (cloud) {
21364
+ for (const rawId of ids) {
21365
+ try {
21366
+ const resolvedId = resolveTaskId(rawId);
21367
+ if (action === "done" || action === "complete") {
21368
+ await cloudTaskAction(cloud, resolvedId, "complete", { agent_id: globalOpts.agent });
21369
+ } else if (action === "start") {
21370
+ await cloudTaskAction(cloud, resolvedId, "start", { agent_id: globalOpts.agent || "cli" });
21371
+ } else if (action === "delete") {
21372
+ await cloudDeleteTask(cloud, resolvedId);
21373
+ } else {
21374
+ const current = await cloudGetTask(cloud, resolvedId);
21375
+ if (!current)
21376
+ throw new Error(`Task not found: ${rawId}`);
21377
+ await cloudUpdateTask(cloud, resolvedId, { version: current.version, plan_id: planId });
21378
+ }
21379
+ results.push({ id: resolvedId, success: true });
21380
+ } catch (e) {
21381
+ results.push({ id: rawId, success: false, error: e instanceof Error ? e.message : String(e) });
21382
+ }
21383
+ }
21384
+ const succeededCloud = results.filter((r) => r.success).length;
21385
+ const failedCloud = results.filter((r) => !r.success).length;
21386
+ if (globalOpts.json) {
21387
+ output({ results, succeeded: succeededCloud, failed: failedCloud }, true);
21388
+ } else {
21389
+ console.log(chalk2.green(`${action}: ${succeededCloud} succeeded, ${failedCloud} failed`));
21390
+ for (const r of results.filter((r2) => !r2.success)) {
21391
+ console.log(chalk2.red(` ${r.id}: ${r.error}`));
21392
+ }
21393
+ }
21394
+ return;
21395
+ }
21230
21396
  for (const rawId of ids) {
21231
21397
  try {
21232
21398
  const resolvedId = resolveTaskId(rawId);
@@ -22033,6 +22199,40 @@ function registerPlanTemplateCommands(program2) {
22033
22199
  return;
22034
22200
  }
22035
22201
  if (opts.show) {
22202
+ if (cloud) {
22203
+ const plan2 = await cloudResolvePlan(cloud, opts.show, projectId);
22204
+ if (!plan2) {
22205
+ console.error(chalk3.red(`Plan not found: ${opts.show}`));
22206
+ process.exit(1);
22207
+ }
22208
+ const tasks2 = await cloudListTasks(cloud, { plan_id: plan2.id });
22209
+ if (globalOpts.json) {
22210
+ output({ plan: plan2, tasks: tasks2, artifact: null }, true);
22211
+ return;
22212
+ }
22213
+ console.log(chalk3.bold(`Plan Details:
22214
+ `));
22215
+ console.log(` ${chalk3.dim("ID:")} ${plan2.id}`);
22216
+ if (plan2.slug)
22217
+ console.log(` ${chalk3.dim("Slug:")} ${plan2.slug}`);
22218
+ console.log(` ${chalk3.dim("Name:")} ${plan2.name}`);
22219
+ console.log(` ${chalk3.dim("Status:")} ${chalk3.cyan(plan2.status)}`);
22220
+ if (plan2.description)
22221
+ console.log(` ${chalk3.dim("Desc:")} ${plan2.description}`);
22222
+ if (plan2.project_id)
22223
+ console.log(` ${chalk3.dim("Project:")} ${plan2.project_id}`);
22224
+ console.log(` ${chalk3.dim("Created:")} ${plan2.created_at}`);
22225
+ if (tasks2.length > 0) {
22226
+ console.log(chalk3.bold(`
22227
+ Tasks (${tasks2.length}):`));
22228
+ for (const t of tasks2)
22229
+ console.log(` ${formatTaskLine(t)}`);
22230
+ } else {
22231
+ console.log(chalk3.dim(`
22232
+ No tasks in this plan.`));
22233
+ }
22234
+ return;
22235
+ }
22036
22236
  const db = getDatabase();
22037
22237
  const resolvedId = resolvePlanCliRef(opts.show, projectId);
22038
22238
  const plan = getPlan(resolvedId);
@@ -33132,17 +33332,35 @@ Use ${chalk5.cyan(`--agent ${result.id}`)} on future commands.`);
33132
33332
  console.error(chalk5.red("Agent ID required. Use --agent or pass as argument."));
33133
33333
  process.exit(1);
33134
33334
  }
33135
- const { updateAgentActivity: updateAgentActivity2, getAgent: getAgent2, getAgentByName: getAgentByName2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
33136
- const a = getAgent2(agentId) || getAgentByName2(agentId);
33137
- if (!a) {
33138
- console.error(chalk5.red(`Agent not found: ${agentId}`));
33139
- process.exit(1);
33140
- }
33141
- updateAgentActivity2(a.id);
33142
- if (globalOpts.json) {
33143
- console.log(JSON.stringify({ agent_id: a.id, name: a.name, last_seen_at: new Date().toISOString() }));
33144
- } else {
33145
- console.log(chalk5.green(`\u2665 ${a.name} (${a.id.slice(0, 8)}) \u2014 heartbeat sent`));
33335
+ try {
33336
+ const cloud = getTodosCloudClient();
33337
+ if (cloud) {
33338
+ const a2 = await cloudHeartbeatAgent(cloud, agentId);
33339
+ if (!a2) {
33340
+ console.error(chalk5.red(`Agent not found: ${agentId}`));
33341
+ process.exit(1);
33342
+ }
33343
+ if (globalOpts.json) {
33344
+ console.log(JSON.stringify({ agent_id: a2.id, name: a2.name, last_seen_at: a2.last_seen_at }));
33345
+ } else {
33346
+ console.log(chalk5.green(`\u2665 ${a2.name} (${a2.id.slice(0, 8)}) \u2014 heartbeat sent`));
33347
+ }
33348
+ return;
33349
+ }
33350
+ const { updateAgentActivity: updateAgentActivity2, getAgent: getAgent2, getAgentByName: getAgentByName2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
33351
+ const a = getAgent2(agentId) || getAgentByName2(agentId);
33352
+ if (!a) {
33353
+ console.error(chalk5.red(`Agent not found: ${agentId}`));
33354
+ process.exit(1);
33355
+ }
33356
+ updateAgentActivity2(a.id);
33357
+ if (globalOpts.json) {
33358
+ console.log(JSON.stringify({ agent_id: a.id, name: a.name, last_seen_at: new Date().toISOString() }));
33359
+ } else {
33360
+ console.log(chalk5.green(`\u2665 ${a.name} (${a.id.slice(0, 8)}) \u2014 heartbeat sent`));
33361
+ }
33362
+ } catch (e) {
33363
+ handleError(e);
33146
33364
  }
33147
33365
  });
33148
33366
  program2.command("release [agent]").description("Release/logout an agent \u2014 clears session binding so the name is immediately available").option("--session-id <id>", "Only release if session ID matches").action(async (agent, opts) => {
@@ -33152,21 +33370,43 @@ Use ${chalk5.cyan(`--agent ${result.id}`)} on future commands.`);
33152
33370
  console.error(chalk5.red("Agent ID or name required. Use --agent or pass as argument."));
33153
33371
  process.exit(1);
33154
33372
  }
33155
- const { getAgent: getAgent2, getAgentByName: getAgentByName2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
33156
- const a = getAgent2(agentId) || getAgentByName2(agentId);
33157
- if (!a) {
33158
- console.error(chalk5.red(`Agent not found: ${agentId}`));
33159
- process.exit(1);
33160
- }
33161
- const released = releaseAgent(a.id, opts?.sessionId);
33162
- if (!released) {
33163
- console.error(chalk5.red("Release denied: session_id does not match agent's current session."));
33164
- process.exit(1);
33165
- }
33166
- if (globalOpts.json) {
33167
- console.log(JSON.stringify({ agent_id: a.id, name: a.name, released: true }));
33168
- } else {
33169
- console.log(chalk5.green(`\u2713 ${a.name} (${a.id}) released \u2014 name is now available.`));
33373
+ try {
33374
+ const cloud = getTodosCloudClient();
33375
+ if (cloud) {
33376
+ const result = await cloudReleaseAgent(cloud, agentId, opts?.sessionId);
33377
+ if (!result.agent) {
33378
+ console.error(chalk5.red(`Agent not found: ${agentId}`));
33379
+ process.exit(1);
33380
+ }
33381
+ if (!result.released) {
33382
+ console.error(chalk5.red("Release denied: session_id does not match agent's current session."));
33383
+ process.exit(1);
33384
+ }
33385
+ if (globalOpts.json) {
33386
+ console.log(JSON.stringify({ agent_id: result.agent.id, name: result.agent.name, released: true }));
33387
+ } else {
33388
+ console.log(chalk5.green(`\u2713 ${result.agent.name} (${result.agent.id}) released \u2014 name is now available.`));
33389
+ }
33390
+ return;
33391
+ }
33392
+ const { getAgent: getAgent2, getAgentByName: getAgentByName2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
33393
+ const a = getAgent2(agentId) || getAgentByName2(agentId);
33394
+ if (!a) {
33395
+ console.error(chalk5.red(`Agent not found: ${agentId}`));
33396
+ process.exit(1);
33397
+ }
33398
+ const released = releaseAgent(a.id, opts?.sessionId);
33399
+ if (!released) {
33400
+ console.error(chalk5.red("Release denied: session_id does not match agent's current session."));
33401
+ process.exit(1);
33402
+ }
33403
+ if (globalOpts.json) {
33404
+ console.log(JSON.stringify({ agent_id: a.id, name: a.name, released: true }));
33405
+ } else {
33406
+ console.log(chalk5.green(`\u2713 ${a.name} (${a.id}) released \u2014 name is now available.`));
33407
+ }
33408
+ } catch (e) {
33409
+ handleError(e);
33170
33410
  }
33171
33411
  });
33172
33412
  program2.command("focus [project]").description("Focus on a project (or clear focus if no project given)").action(async (project) => {
@@ -38685,7 +38925,8 @@ function createPostgresTodosStorageAdapter(options) {
38685
38925
  getActiveWork: (filters) => getActiveWork2(filters, store),
38686
38926
  getChangedSince: (since, filters) => getChangedSince(since, filters, store),
38687
38927
  lock: (id, agentId) => lockTask2(id, agentId, store),
38688
- unlock: (id, agentId) => unlockTask2(id, agentId, store)
38928
+ unlock: (id, agentId) => unlockTask2(id, agentId, store),
38929
+ getByFingerprint: (fingerprint) => store.getTaskByFingerprint(fingerprint)
38689
38930
  },
38690
38931
  dependencies: {
38691
38932
  add: (taskId, dependsOn, context) => addDependency2(taskId, dependsOn, store, context),
@@ -38697,6 +38938,16 @@ function createPostgresTodosStorageAdapter(options) {
38697
38938
  add: (input, context) => addVerification(input, store, context),
38698
38939
  list: (taskId) => listVerifications(taskId, store)
38699
38940
  },
38941
+ commits: {
38942
+ add: (input, context) => addCommit(input, store, context),
38943
+ list: (taskId) => listCommits(taskId, store),
38944
+ find: (sha) => findCommit(sha, store)
38945
+ },
38946
+ gitRefs: {
38947
+ add: (input, context) => addGitRef(input, store, context),
38948
+ list: (taskId) => listGitRefs(taskId, store),
38949
+ find: (ref) => findGitRefs(ref, store)
38950
+ },
38700
38951
  projects: {
38701
38952
  create: (input, context) => createProject2(input, store, context),
38702
38953
  get: (id) => store.get("projects", id),
@@ -38717,7 +38968,9 @@ function createPostgresTodosStorageAdapter(options) {
38717
38968
  get: (id) => store.get("agents", id),
38718
38969
  getByName: async (name) => (await store.list("agents")).find((agent) => agent.name === name) ?? null,
38719
38970
  list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
38720
- update: (id, input) => updateAgent2(id, input, store)
38971
+ update: (id, input) => updateAgent2(id, input, store),
38972
+ heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
38973
+ release: (idOrName, sessionId, context) => releaseAgent2(idOrName, sessionId, store, context)
38721
38974
  },
38722
38975
  taskLists: {
38723
38976
  create: (input, context) => createTaskList2(input, store, context),
@@ -38862,6 +39115,17 @@ class PostgresJsonRecordStore {
38862
39115
  const result = await this.options.client.query(sql, params);
38863
39116
  return result.rows.map((row) => payloadRecord2(row.payload));
38864
39117
  }
39118
+ async getTaskByFingerprint(fingerprint) {
39119
+ await this.ensureSchema();
39120
+ const sql = `/* todos:task-by-fingerprint */ SELECT payload FROM ${this.tableName}
39121
+ WHERE service = $1 AND object_type = $2 AND deleted_at IS NULL
39122
+ AND payload->'metadata'->>'fingerprint' = $3
39123
+ ORDER BY payload->>'created_at' ASC
39124
+ LIMIT 1`;
39125
+ const result = await this.options.client.query(sql, [this.service, "tasks", fingerprint]);
39126
+ const row = result.rows[0];
39127
+ return row ? payloadRecord2(row.payload) : null;
39128
+ }
38865
39129
  async countTasks(filter) {
38866
39130
  await this.ensureSchema();
38867
39131
  const { where, params } = this.buildTaskFilterSql(filter);
@@ -39247,6 +39511,53 @@ async function addVerification(input, store, context) {
39247
39511
  async function listVerifications(taskId, store) {
39248
39512
  return (await store.list("verifications")).filter((verification) => verification.task_id === taskId).sort((a, b) => b.run_at.localeCompare(a.run_at));
39249
39513
  }
39514
+ async function addCommit(input, store, context) {
39515
+ if (!await store.get("tasks", input.task_id))
39516
+ throw new Error(`Task not found: ${input.task_id}`);
39517
+ const timestamp = new Date().toISOString();
39518
+ const commit = {
39519
+ id: randomUUID3(),
39520
+ task_id: input.task_id,
39521
+ sha: input.sha,
39522
+ message: input.message ?? null,
39523
+ author: input.author ?? null,
39524
+ files_changed: input.files_changed ?? null,
39525
+ created_at: timestamp
39526
+ };
39527
+ await store.upsert("commits", { ...commit, updated_at: timestamp }, context);
39528
+ return commit;
39529
+ }
39530
+ async function listCommits(taskId, store) {
39531
+ return (await store.list("commits")).filter((commit) => commit.task_id === taskId).sort((a, b) => b.created_at.localeCompare(a.created_at));
39532
+ }
39533
+ async function findCommit(sha, store) {
39534
+ const matches = (await store.list("commits")).filter((commit) => commit.sha === sha || commit.sha.startsWith(sha) || sha.startsWith(commit.sha)).sort((a, b) => b.created_at.localeCompare(a.created_at));
39535
+ return matches[0] ?? null;
39536
+ }
39537
+ async function addGitRef(input, store, context) {
39538
+ if (!await store.get("tasks", input.task_id))
39539
+ throw new Error(`Task not found: ${input.task_id}`);
39540
+ const timestamp = new Date().toISOString();
39541
+ const gitRef = {
39542
+ id: randomUUID3(),
39543
+ task_id: input.task_id,
39544
+ ref_type: input.ref_type,
39545
+ name: input.name,
39546
+ url: input.url ?? null,
39547
+ provider: input.provider ?? null,
39548
+ metadata: input.metadata ?? {},
39549
+ created_at: timestamp,
39550
+ updated_at: timestamp
39551
+ };
39552
+ await store.upsert("refs", gitRef, context);
39553
+ return gitRef;
39554
+ }
39555
+ async function listGitRefs(taskId, store) {
39556
+ return (await store.list("refs")).filter((ref) => ref.task_id === taskId).sort((a, b) => b.created_at.localeCompare(a.created_at));
39557
+ }
39558
+ async function findGitRefs(ref, store) {
39559
+ return (await store.list("refs")).filter((r) => r.name === ref).sort((a, b) => b.created_at.localeCompare(a.created_at));
39560
+ }
39250
39561
  function toFilterArray(value) {
39251
39562
  return Array.isArray(value) ? value : [value];
39252
39563
  }
@@ -39393,6 +39704,28 @@ async function updateAgent2(id, input, store) {
39393
39704
  last_seen_at: new Date().toISOString()
39394
39705
  });
39395
39706
  }
39707
+ async function resolveAgent(idOrName, store) {
39708
+ const byId = await store.get("agents", idOrName);
39709
+ if (byId)
39710
+ return byId;
39711
+ return (await store.list("agents")).find((agent) => agent.name === idOrName) ?? null;
39712
+ }
39713
+ async function heartbeatAgent(idOrName, store, context) {
39714
+ const agent = await resolveAgent(idOrName, store);
39715
+ if (!agent)
39716
+ return null;
39717
+ return store.upsert("agents", { ...agent, last_seen_at: new Date().toISOString() }, context);
39718
+ }
39719
+ async function releaseAgent2(idOrName, sessionId, store, context) {
39720
+ const agent = await resolveAgent(idOrName, store);
39721
+ if (!agent)
39722
+ return null;
39723
+ if (sessionId && agent.session_id && agent.session_id !== sessionId) {
39724
+ return { agent, released: false };
39725
+ }
39726
+ const updated = await store.upsert("agents", { ...agent, session_id: null, last_seen_at: new Date().toISOString() }, context);
39727
+ return { agent: updated, released: true };
39728
+ }
39396
39729
  async function createTaskList2(input, store, context) {
39397
39730
  const timestamp = new Date().toISOString();
39398
39731
  return store.upsert("task_lists", {
@@ -40141,6 +40474,61 @@ async function handleV1Request(req, url) {
40141
40474
  missing
40142
40475
  });
40143
40476
  }
40477
+ if (id === "upsert" && !action) {
40478
+ if (method !== "POST")
40479
+ return error(405, `method ${method} not allowed on /v1/tasks/upsert`);
40480
+ if (typeof store.tasks.getByFingerprint !== "function") {
40481
+ return error(501, "fingerprint upsert is not supported by this storage backend");
40482
+ }
40483
+ const body = await readJson(req) ?? {};
40484
+ const fingerprint = typeof body.fingerprint === "string" ? body.fingerprint.trim() : "";
40485
+ if (!fingerprint)
40486
+ return error(400, "fingerprint is required");
40487
+ if (typeof body.title !== "string" || !body.title.trim())
40488
+ return error(400, "title is required");
40489
+ const existing = await store.tasks.getByFingerprint(fingerprint);
40490
+ const metadata = {
40491
+ ...existing?.metadata ?? {},
40492
+ ...body.metadata ?? {},
40493
+ fingerprint
40494
+ };
40495
+ const fields = { metadata };
40496
+ for (const key of [
40497
+ "title",
40498
+ "description",
40499
+ "priority",
40500
+ "status",
40501
+ "project_id",
40502
+ "assigned_to",
40503
+ "working_dir",
40504
+ "plan_id",
40505
+ "task_list_id",
40506
+ "tags",
40507
+ "due_at",
40508
+ "estimated_minutes",
40509
+ "sla_minutes",
40510
+ "requires_approval",
40511
+ "recurrence_rule",
40512
+ "task_type"
40513
+ ]) {
40514
+ const bag = body;
40515
+ if (bag[key] !== undefined)
40516
+ fields[key] = bag[key];
40517
+ }
40518
+ if (!existing) {
40519
+ const task = await store.tasks.create({ ...fields, title: body.title }, contextFromPrincipal(principal, body));
40520
+ return json2({ task, created: true }, 201);
40521
+ }
40522
+ try {
40523
+ const task = await store.tasks.update(existing.id, { ...fields, version: existing.version }, contextFromPrincipal(principal, body));
40524
+ return json2({ task, created: false });
40525
+ } catch (e) {
40526
+ const msg = e.message || "";
40527
+ if (msg.includes("version conflict"))
40528
+ return error(409, msg);
40529
+ throw e;
40530
+ }
40531
+ }
40144
40532
  if (!id) {
40145
40533
  if (method === "GET") {
40146
40534
  const filter = {
@@ -40194,6 +40582,14 @@ async function handleV1Request(req, url) {
40194
40582
  }
40195
40583
  return error(405, `method ${method} not allowed on /v1/tasks/:id/comments`);
40196
40584
  }
40585
+ if (action === "history") {
40586
+ if (method !== "GET")
40587
+ return error(405, `method ${method} not allowed on /v1/tasks/:id/history`);
40588
+ if (!await store.tasks.get(id))
40589
+ return error(404, "task not found");
40590
+ const history = await store.audit.getTaskHistory(id);
40591
+ return json2({ history, count: history.length });
40592
+ }
40197
40593
  if (action === "lock" || action === "unlock") {
40198
40594
  if (method !== "POST")
40199
40595
  return error(405, `method ${method} not allowed on /v1/tasks/:id/${action}`);
@@ -40277,6 +40673,70 @@ async function handleV1Request(req, url) {
40277
40673
  }
40278
40674
  return error(405, `method ${method} not allowed on /v1/tasks/:id/verifications`);
40279
40675
  }
40676
+ if (action === "commits") {
40677
+ if (!store.commits)
40678
+ return error(501, "commit links are not supported by this storage backend");
40679
+ if (method === "GET") {
40680
+ if (!await store.tasks.get(id))
40681
+ return error(404, "task not found");
40682
+ const commits = await store.commits.list(id);
40683
+ return json2({ commits, count: commits.length });
40684
+ }
40685
+ if (method === "POST") {
40686
+ const body2 = await readJson(req) ?? {};
40687
+ if (typeof body2.sha !== "string" || !body2.sha.trim())
40688
+ return error(400, "sha is required");
40689
+ try {
40690
+ const commit = await store.commits.add({
40691
+ task_id: id,
40692
+ sha: body2.sha,
40693
+ message: body2.message,
40694
+ author: body2.author,
40695
+ files_changed: Array.isArray(body2.files_changed) ? body2.files_changed : undefined
40696
+ }, contextFromPrincipal(principal));
40697
+ return json2({ commit }, 201);
40698
+ } catch (e) {
40699
+ const msg = e.message || "";
40700
+ if (msg.includes("not found"))
40701
+ return error(404, msg);
40702
+ throw e;
40703
+ }
40704
+ }
40705
+ return error(405, `method ${method} not allowed on /v1/tasks/:id/commits`);
40706
+ }
40707
+ if (action === "refs") {
40708
+ if (!store.gitRefs)
40709
+ return error(501, "git ref links are not supported by this storage backend");
40710
+ if (method === "GET") {
40711
+ if (!await store.tasks.get(id))
40712
+ return error(404, "task not found");
40713
+ const refs = await store.gitRefs.list(id);
40714
+ return json2({ refs, count: refs.length });
40715
+ }
40716
+ if (method === "POST") {
40717
+ const body2 = await readJson(req) ?? {};
40718
+ const refType = body2.ref_type === "pull_request" || body2.ref_type === "branch" ? body2.ref_type : "branch";
40719
+ if (typeof body2.name !== "string" || !body2.name.trim())
40720
+ return error(400, "name is required");
40721
+ try {
40722
+ const ref = await store.gitRefs.add({
40723
+ task_id: id,
40724
+ ref_type: refType,
40725
+ name: body2.name,
40726
+ url: body2.url,
40727
+ provider: body2.provider,
40728
+ metadata: body2.metadata
40729
+ }, contextFromPrincipal(principal));
40730
+ return json2({ ref }, 201);
40731
+ } catch (e) {
40732
+ const msg = e.message || "";
40733
+ if (msg.includes("not found"))
40734
+ return error(404, msg);
40735
+ throw e;
40736
+ }
40737
+ }
40738
+ return error(405, `method ${method} not allowed on /v1/tasks/:id/refs`);
40739
+ }
40280
40740
  const body = await readJson(req) ?? {};
40281
40741
  const agentId = body.agent_id || principal.agent || "todos-serve";
40282
40742
  if (action === "start" && method === "POST") {
@@ -40389,6 +40849,30 @@ async function handleV1Request(req, url) {
40389
40849
  }
40390
40850
  return json2({ agent: result }, 201);
40391
40851
  }
40852
+ if (id && action === "heartbeat") {
40853
+ if (method !== "POST")
40854
+ return error(405, `method ${method} not allowed on /v1/agents/:id/heartbeat`);
40855
+ if (typeof store.agents.heartbeat !== "function") {
40856
+ return error(501, "agent heartbeat is not supported by this storage backend");
40857
+ }
40858
+ const agent = await store.agents.heartbeat(id, contextFromPrincipal(principal));
40859
+ return agent ? json2({ agent }) : error(404, "agent not found");
40860
+ }
40861
+ if (id && action === "release") {
40862
+ if (method !== "POST")
40863
+ return error(405, `method ${method} not allowed on /v1/agents/:id/release`);
40864
+ if (typeof store.agents.release !== "function") {
40865
+ return error(501, "agent release is not supported by this storage backend");
40866
+ }
40867
+ const body = await readJson(req) ?? {};
40868
+ const result = await store.agents.release(id, body.session_id, contextFromPrincipal(principal));
40869
+ if (!result)
40870
+ return error(404, "agent not found");
40871
+ if (!result.released) {
40872
+ return error(409, "release denied: session_id does not match agent's current session", { released: false });
40873
+ }
40874
+ return json2({ agent: result.agent, released: true });
40875
+ }
40392
40876
  if (id && method === "GET") {
40393
40877
  const agent = await store.agents.get(id);
40394
40878
  return agent ? json2({ agent }) : error(404, "agent not found");
@@ -40418,6 +40902,22 @@ async function handleV1Request(req, url) {
40418
40902
  const dependencies = await store.dependencies.listAll();
40419
40903
  return json2({ dependencies, count: dependencies.length });
40420
40904
  }
40905
+ if (resource === "commits" && id) {
40906
+ if (method !== "GET")
40907
+ return error(405, `method ${method} not allowed on /v1/commits/:sha`);
40908
+ if (!store.commits)
40909
+ return error(501, "commit links are not supported by this storage backend");
40910
+ const commit = await store.commits.find(id);
40911
+ return json2({ commit: commit ?? null });
40912
+ }
40913
+ if (resource === "refs" && id) {
40914
+ if (method !== "GET")
40915
+ return error(405, `method ${method} not allowed on /v1/refs/:ref`);
40916
+ if (!store.gitRefs)
40917
+ return error(501, "git ref links are not supported by this storage backend");
40918
+ const refs = await store.gitRefs.find(id);
40919
+ return json2({ refs, count: refs.length });
40920
+ }
40421
40921
  if (resource === "next" && !id) {
40422
40922
  if (method !== "GET")
40423
40923
  return error(405, `method ${method} not allowed on /v1/next`);
@@ -45774,7 +46274,7 @@ function resolveRun(parsed, db) {
45774
46274
  }
45775
46275
  return resolution;
45776
46276
  }
45777
- function resolveAgent(parsed, db) {
46277
+ function resolveAgent2(parsed, db) {
45778
46278
  const resolution = blankResolution(parsed);
45779
46279
  const id = resolvePartialId(db, "agents", parsed.target);
45780
46280
  const agent = id ? getAgent(id, db) : getAgentByName(parsed.target, db);
@@ -45811,7 +46311,7 @@ function resolveTask(parsed, db) {
45811
46311
  function resolveUnknown(parsed, workspace, db, maxSymbolMatches) {
45812
46312
  const attempts = [
45813
46313
  () => resolveTask({ ...parsed, kind: "task" }, db),
45814
- () => resolveAgent({ ...parsed, kind: "agent" }, db),
46314
+ () => resolveAgent2({ ...parsed, kind: "agent" }, db),
45815
46315
  () => resolvePlan({ ...parsed, kind: "plan" }, db),
45816
46316
  () => resolveSymbol({ ...parsed, kind: "symbol" }, workspace, maxSymbolMatches)
45817
46317
  ];
@@ -45846,7 +46346,7 @@ function resolveMentions(input, db) {
45846
46346
  case "run":
45847
46347
  return resolveRun(parsed, d);
45848
46348
  case "agent":
45849
- return resolveAgent(parsed, d);
46349
+ return resolveAgent2(parsed, d);
45850
46350
  case "task":
45851
46351
  return resolveTask(parsed, d);
45852
46352
  default:
@@ -55660,7 +56160,7 @@ function resolveProjectId(value, db) {
55660
56160
  return null;
55661
56161
  return resolvePartialId(db, "projects", value) || value;
55662
56162
  }
55663
- function resolveAgent2(agentId, db) {
56163
+ function resolveAgent3(agentId, db) {
55664
56164
  return db.query("SELECT id, name FROM agents WHERE id = ? OR LOWER(name) = LOWER(?)").get(agentId, agentId);
55665
56165
  }
55666
56166
  function agentTaskWhere(agent, options, db) {
@@ -55704,7 +56204,7 @@ function scoreHandoffs(created, withTaskRefs, withBlockers) {
55704
56204
  }
55705
56205
  function getAgentReliabilityScorecard(agentId, options = {}, db) {
55706
56206
  const d = db || getDatabase();
55707
- const agent = resolveAgent2(agentId, d);
56207
+ const agent = resolveAgent3(agentId, d);
55708
56208
  if (!agent)
55709
56209
  return null;
55710
56210
  const generatedAt = now();
@@ -55873,7 +56373,7 @@ function getAgentReliabilityScorecard(agentId, options = {}, db) {
55873
56373
  }
55874
56374
  function listAgentReliabilityScorecards(options = {}, db) {
55875
56375
  const d = db || getDatabase();
55876
- const agents = options.agent_id ? resolveAgent2(options.agent_id, d) ? [resolveAgent2(options.agent_id, d)] : [] : d.query("SELECT id, name FROM agents ORDER BY name").all();
56376
+ const agents = options.agent_id ? resolveAgent3(options.agent_id, d) ? [resolveAgent3(options.agent_id, d)] : [] : d.query("SELECT id, name FROM agents ORDER BY name").all();
55877
56377
  const limit = Number.isFinite(options.limit) && options.limit > 0 ? Math.min(Math.floor(options.limit), 500) : 50;
55878
56378
  return agents.map((agent) => getAgentReliabilityScorecard(agent.id, options, d)).filter((scorecard) => Boolean(scorecard)).filter((scorecard) => {
55879
56379
  const signals = scorecard.signals;
@@ -63244,6 +63744,19 @@ ID: ${updated.id}${taskNote}`
63244
63744
  agent_id: exports_external3.string().describe("Your agent ID or name.")
63245
63745
  }, async ({ agent_id }) => {
63246
63746
  try {
63747
+ const cloud = getTodosCloudClient();
63748
+ if (cloud) {
63749
+ const a = await cloudHeartbeatAgent(cloud, agent_id);
63750
+ if (!a) {
63751
+ return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
63752
+ }
63753
+ return {
63754
+ content: [{
63755
+ type: "text",
63756
+ text: `Heartbeat: ${a.name} (${a.id}) \u2014 last_seen_at updated to ${a.last_seen_at}`
63757
+ }]
63758
+ };
63759
+ }
63247
63760
  const agent = getAgent(agent_id) || getAgentByName(agent_id);
63248
63761
  if (!agent) {
63249
63762
  return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
@@ -63266,6 +63779,22 @@ ID: ${updated.id}${taskNote}`
63266
63779
  session_id: exports_external3.string().optional().describe("Your session ID \u2014 if provided, release only succeeds if it matches (prevents other sessions from releasing your agent).")
63267
63780
  }, async ({ agent_id, session_id }) => {
63268
63781
  try {
63782
+ const cloud = getTodosCloudClient();
63783
+ if (cloud) {
63784
+ const result = await cloudReleaseAgent(cloud, agent_id, session_id);
63785
+ if (!result.agent) {
63786
+ return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
63787
+ }
63788
+ if (!result.released) {
63789
+ return { content: [{ type: "text", text: `Release denied: session_id does not match agent's current session.` }], isError: true };
63790
+ }
63791
+ return {
63792
+ content: [{
63793
+ type: "text",
63794
+ text: `Agent released: ${result.agent.name} (${result.agent.id}) \u2014 session cleared, name is now available.`
63795
+ }]
63796
+ };
63797
+ }
63269
63798
  const agent = getAgent(agent_id) || getAgentByName(agent_id);
63270
63799
  if (!agent) {
63271
63800
  return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
@@ -71789,38 +72318,68 @@ exit 0
71789
72318
  program2.command("link-commit <task-id> <sha>").description("Link a git commit to a task").option("--message <text>", "Commit message").option("--author <name>", "Commit author").option("--files <list>", "Comma-separated list of changed files").action(async (taskId, sha, opts) => {
71790
72319
  const globalOpts = program2.opts();
71791
72320
  const resolvedId = resolveTaskId(taskId);
71792
- const { linkTaskToCommit: linkTaskToCommit2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
71793
- const commit = linkTaskToCommit2({
71794
- task_id: resolvedId,
71795
- sha,
71796
- message: opts.message,
71797
- author: opts.author,
71798
- files_changed: opts.files ? opts.files.split(",").filter(Boolean) : undefined
71799
- });
71800
- if (globalOpts.json) {
71801
- output(commit, true);
71802
- return;
72321
+ const files = opts.files ? opts.files.split(",").filter(Boolean) : undefined;
72322
+ try {
72323
+ const cloud = getTodosCloudClient();
72324
+ const commit = cloud ? await cloudLinkCommit(cloud, resolvedId, {
72325
+ sha,
72326
+ ...opts.message !== undefined ? { message: opts.message } : {},
72327
+ ...opts.author !== undefined ? { author: opts.author } : {},
72328
+ ...files ? { files_changed: files } : {}
72329
+ }) : (await Promise.resolve().then(() => (init_task_commits(), exports_task_commits))).linkTaskToCommit({
72330
+ task_id: resolvedId,
72331
+ sha,
72332
+ message: opts.message,
72333
+ author: opts.author,
72334
+ files_changed: files
72335
+ });
72336
+ if (globalOpts.json) {
72337
+ output(commit, true);
72338
+ return;
72339
+ }
72340
+ console.log(chalk8.green(`Linked commit ${sha.slice(0, 7)} to task ${taskId}`));
72341
+ } catch (e) {
72342
+ handleError(e);
71803
72343
  }
71804
- console.log(chalk8.green(`Linked commit ${sha.slice(0, 7)} to task ${taskId}`));
71805
72344
  });
71806
72345
  program2.command("find-commit <sha>").description("Find which task explains a git commit SHA").action(async (sha) => {
71807
72346
  const globalOpts = program2.opts();
71808
- const { findTaskByCommit: findTaskByCommit2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
71809
- const { getTask: getTask3 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
71810
- const result = findTaskByCommit2(sha);
71811
- if (globalOpts.json) {
71812
- output(result, true);
71813
- return;
71814
- }
71815
- if (!result) {
71816
- console.log(chalk8.dim(`No task linked to commit ${sha}.`));
71817
- return;
72347
+ try {
72348
+ const cloud = getTodosCloudClient();
72349
+ if (cloud) {
72350
+ const commit = await cloudFindCommit(cloud, sha);
72351
+ if (globalOpts.json) {
72352
+ output(commit ? { task_id: commit.task_id, commit } : null, true);
72353
+ return;
72354
+ }
72355
+ if (!commit) {
72356
+ console.log(chalk8.dim(`No task linked to commit ${sha}.`));
72357
+ return;
72358
+ }
72359
+ console.log(`${chalk8.yellow(commit.sha.slice(0, 7))} -> ${chalk8.cyan(commit.task_id)}`);
72360
+ if (commit.message)
72361
+ console.log(chalk8.dim(` ${commit.message}`));
72362
+ return;
72363
+ }
72364
+ const { findTaskByCommit: findTaskByCommit2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
72365
+ const { getTask: getTask3 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
72366
+ const result = findTaskByCommit2(sha);
72367
+ if (globalOpts.json) {
72368
+ output(result, true);
72369
+ return;
72370
+ }
72371
+ if (!result) {
72372
+ console.log(chalk8.dim(`No task linked to commit ${sha}.`));
72373
+ return;
72374
+ }
72375
+ const task2 = getTask3(result.task_id);
72376
+ const taskLabel = task2 ? `${task2.short_id || task2.id.slice(0, 8)} ${task2.title}` : result.task_id;
72377
+ console.log(`${chalk8.yellow(result.commit.sha.slice(0, 7))} -> ${chalk8.cyan(taskLabel)}`);
72378
+ if (result.commit.message)
72379
+ console.log(chalk8.dim(` ${result.commit.message}`));
72380
+ } catch (e) {
72381
+ handleError(e);
71818
72382
  }
71819
- const task2 = getTask3(result.task_id);
71820
- const taskLabel = task2 ? `${task2.short_id || task2.id.slice(0, 8)} ${task2.title}` : result.task_id;
71821
- console.log(`${chalk8.yellow(result.commit.sha.slice(0, 7))} -> ${chalk8.cyan(taskLabel)}`);
71822
- if (result.commit.message)
71823
- console.log(chalk8.dim(` ${result.commit.message}`));
71824
72383
  });
71825
72384
  program2.command("link-ref <task-id> <ref>").description("Link a git branch or pull request to a task").option("--type <type>", "Ref type: branch or pull_request", "branch").option("--url <url>", "Remote URL for the branch or pull request").option("--provider <name>", "Provider name, e.g. git or github").option("--metadata <json>", "Additional JSON metadata").action(async (taskId, ref, opts) => {
71826
72385
  const globalOpts = program2.opts();
@@ -71840,38 +72399,70 @@ exit 0
71840
72399
  process.exit(1);
71841
72400
  }
71842
72401
  }
71843
- const gitRef = linkTaskGitRef2({
71844
- task_id: resolvedId,
71845
- ref_type: refType,
71846
- name: ref,
71847
- url: opts.url,
71848
- provider: opts.provider,
71849
- metadata
71850
- });
71851
- if (globalOpts.json) {
71852
- output(gitRef, true);
71853
- return;
72402
+ try {
72403
+ const cloud = getTodosCloudClient();
72404
+ const gitRef = cloud ? await cloudLinkRef(cloud, resolvedId, {
72405
+ ref_type: refType,
72406
+ name: ref,
72407
+ ...opts.url !== undefined ? { url: opts.url } : {},
72408
+ ...opts.provider !== undefined ? { provider: opts.provider } : {},
72409
+ ...metadata ? { metadata } : {}
72410
+ }) : linkTaskGitRef2({
72411
+ task_id: resolvedId,
72412
+ ref_type: refType,
72413
+ name: ref,
72414
+ url: opts.url,
72415
+ provider: opts.provider,
72416
+ metadata
72417
+ });
72418
+ if (globalOpts.json) {
72419
+ output(gitRef, true);
72420
+ return;
72421
+ }
72422
+ console.log(chalk8.green(`Linked ${gitRef.ref_type} ${gitRef.name} to task ${taskId}`));
72423
+ } catch (e) {
72424
+ handleError(e);
71854
72425
  }
71855
- console.log(chalk8.green(`Linked ${gitRef.ref_type} ${gitRef.name} to task ${taskId}`));
71856
72426
  });
71857
72427
  program2.command("find-ref <ref>").description("Find tasks linked to a git branch or pull request").action(async (ref) => {
71858
72428
  const globalOpts = program2.opts();
71859
- const { findTasksByGitRef: findTasksByGitRef2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
71860
- const { getTask: getTask3 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
71861
- const refs = findTasksByGitRef2(ref);
71862
- if (globalOpts.json) {
71863
- output(refs, true);
71864
- return;
71865
- }
71866
- if (refs.length === 0) {
71867
- console.log(chalk8.dim(`No tasks linked to ${ref}.`));
71868
- return;
71869
- }
71870
- for (const gitRef of refs) {
71871
- const task2 = getTask3(gitRef.task_id);
71872
- const label = task2 ? `${task2.short_id || task2.id.slice(0, 8)} ${task2.title}` : gitRef.task_id;
71873
- const url = gitRef.url ? chalk8.dim(` ${gitRef.url}`) : "";
71874
- console.log(`${chalk8.cyan(label)} <- ${gitRef.ref_type} ${chalk8.yellow(gitRef.name)}${url}`);
72429
+ try {
72430
+ const cloud = getTodosCloudClient();
72431
+ if (cloud) {
72432
+ const refs2 = await cloudFindRefs(cloud, ref);
72433
+ if (globalOpts.json) {
72434
+ output(refs2, true);
72435
+ return;
72436
+ }
72437
+ if (refs2.length === 0) {
72438
+ console.log(chalk8.dim(`No tasks linked to ${ref}.`));
72439
+ return;
72440
+ }
72441
+ for (const gitRef of refs2) {
72442
+ const url = gitRef.url ? chalk8.dim(` ${gitRef.url}`) : "";
72443
+ console.log(`${chalk8.cyan(gitRef.task_id)} <- ${gitRef.ref_type} ${chalk8.yellow(gitRef.name)}${url}`);
72444
+ }
72445
+ return;
72446
+ }
72447
+ const { findTasksByGitRef: findTasksByGitRef2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
72448
+ const { getTask: getTask3 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
72449
+ const refs = findTasksByGitRef2(ref);
72450
+ if (globalOpts.json) {
72451
+ output(refs, true);
72452
+ return;
72453
+ }
72454
+ if (refs.length === 0) {
72455
+ console.log(chalk8.dim(`No tasks linked to ${ref}.`));
72456
+ return;
72457
+ }
72458
+ for (const gitRef of refs) {
72459
+ const task2 = getTask3(gitRef.task_id);
72460
+ const label = task2 ? `${task2.short_id || task2.id.slice(0, 8)} ${task2.title}` : gitRef.task_id;
72461
+ const url = gitRef.url ? chalk8.dim(` ${gitRef.url}`) : "";
72462
+ console.log(`${chalk8.cyan(label)} <- ${gitRef.ref_type} ${chalk8.yellow(gitRef.name)}${url}`);
72463
+ }
72464
+ } catch (e) {
72465
+ handleError(e);
71875
72466
  }
71876
72467
  });
71877
72468
  program2.command("branch-plan [task-id]").description("Create a local branch-safe work plan from task or plan files").requiredOption("--branch <name>", "Branch name to plan").option("--base <name>", "Base branch", "main").option("--plan <id>", "Plan ID scope instead of a single task").option("--path <list>", "Comma-separated extra paths expected for this branch").option("--root <path>", "Git root to inspect", process.cwd()).option("--no-git-status", "Skip local git status checks").action(async (taskId, opts) => {