@hasna/todos 0.11.84 → 0.11.85

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
@@ -8587,6 +8587,54 @@ async function cloudRegisterAgent(client, input) {
8587
8587
  }
8588
8588
  return raw;
8589
8589
  }
8590
+ async function cloudHeartbeatAgent(client, idOrName) {
8591
+ const raw = await client.transport.post(`/agents/${encodeURIComponent(idOrName)}/heartbeat`, {});
8592
+ if (raw && typeof raw === "object" && "agent" in raw) {
8593
+ return raw.agent;
8594
+ }
8595
+ return raw ?? null;
8596
+ }
8597
+ async function cloudReleaseAgent(client, idOrName, sessionId) {
8598
+ const raw = await client.transport.post(`/agents/${encodeURIComponent(idOrName)}/release`, sessionId ? { session_id: sessionId } : {});
8599
+ const env = raw ?? {};
8600
+ return { agent: env.agent ?? null, released: env.released !== false };
8601
+ }
8602
+ async function cloudLinkCommit(client, taskId, input) {
8603
+ const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/commits`, input);
8604
+ if (raw && typeof raw === "object" && "commit" in raw) {
8605
+ return raw.commit;
8606
+ }
8607
+ return raw;
8608
+ }
8609
+ async function cloudFindCommit(client, sha) {
8610
+ const raw = await client.transport.get(`/commits/${encodeURIComponent(sha)}`);
8611
+ const env = raw ?? {};
8612
+ return env.commit ?? null;
8613
+ }
8614
+ async function cloudLinkRef(client, taskId, input) {
8615
+ const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/refs`, input);
8616
+ if (raw && typeof raw === "object" && "ref" in raw) {
8617
+ return raw.ref;
8618
+ }
8619
+ return raw;
8620
+ }
8621
+ async function cloudFindRefs(client, ref) {
8622
+ const raw = await client.transport.get(`/refs/${encodeURIComponent(ref)}`);
8623
+ const env = raw ?? {};
8624
+ return Array.isArray(env.refs) ? env.refs : [];
8625
+ }
8626
+ async function cloudResolvePlan(client, ref, projectId) {
8627
+ const direct = await client.get("plans", ref).catch(() => null);
8628
+ if (direct) {
8629
+ const env = direct;
8630
+ if (env.plan)
8631
+ return env.plan;
8632
+ if (direct.id)
8633
+ return direct;
8634
+ }
8635
+ const plans = await cloudListPlans(client, projectId);
8636
+ 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;
8637
+ }
8590
8638
  async function cloudLockTask(client, id, agentId) {
8591
8639
  const raw = await client.transport.post(`/tasks/${encodeURIComponent(id)}/lock`, { agent_id: agentId });
8592
8640
  if (raw && typeof raw === "object" && "result" in raw) {
@@ -21094,28 +21142,54 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
21094
21142
  console.log(formatTaskLine(task2));
21095
21143
  }
21096
21144
  });
21097
- program2.command("approve <id>").description("Approve a task that requires approval").action((id) => {
21145
+ program2.command("approve <id>").description("Approve a task that requires approval").action(async (id) => {
21098
21146
  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
- }
21147
+ const approver = globalOpts.agent || "cli";
21113
21148
  try {
21114
- const updated = updateTask(resolvedId, { approved_by: globalOpts.agent || "cli", version: task2.version });
21149
+ const cloud = getTodosCloudClient();
21150
+ if (cloud) {
21151
+ const cloudId = resolveTaskId(id);
21152
+ const task3 = await cloudGetTask(cloud, cloudId);
21153
+ if (!task3) {
21154
+ console.error(chalk2.red(`Task not found: ${id}`));
21155
+ process.exit(1);
21156
+ }
21157
+ if (!task3.requires_approval) {
21158
+ console.log(chalk2.yellow("This task does not require approval."));
21159
+ return;
21160
+ }
21161
+ if (task3.approved_by) {
21162
+ console.log(chalk2.yellow(`Already approved by ${task3.approved_by}.`));
21163
+ return;
21164
+ }
21165
+ const updated2 = await cloudUpdateTask(cloud, cloudId, { approved_by: approver, version: task3.version });
21166
+ if (globalOpts.json) {
21167
+ output(updated2, true);
21168
+ } else {
21169
+ console.log(chalk2.green(`Task approved by ${approver}:`));
21170
+ console.log(formatTaskLine(updated2));
21171
+ }
21172
+ return;
21173
+ }
21174
+ const resolvedId = resolveTaskId(id);
21175
+ const task2 = getTask(resolvedId);
21176
+ if (!task2) {
21177
+ console.error(chalk2.red(`Task not found: ${id}`));
21178
+ process.exit(1);
21179
+ }
21180
+ if (!task2.requires_approval) {
21181
+ console.log(chalk2.yellow("This task does not require approval."));
21182
+ return;
21183
+ }
21184
+ if (task2.approved_by) {
21185
+ console.log(chalk2.yellow(`Already approved by ${task2.approved_by}.`));
21186
+ return;
21187
+ }
21188
+ const updated = updateTask(resolvedId, { approved_by: approver, version: task2.version });
21115
21189
  if (globalOpts.json) {
21116
21190
  output(updated, true);
21117
21191
  } else {
21118
- console.log(chalk2.green(`Task approved by ${globalOpts.agent || "cli"}:`));
21192
+ console.log(chalk2.green(`Task approved by ${approver}:`));
21119
21193
  console.log(formatTaskLine(updated));
21120
21194
  }
21121
21195
  } catch (e) {
@@ -22033,6 +22107,40 @@ function registerPlanTemplateCommands(program2) {
22033
22107
  return;
22034
22108
  }
22035
22109
  if (opts.show) {
22110
+ if (cloud) {
22111
+ const plan2 = await cloudResolvePlan(cloud, opts.show, projectId);
22112
+ if (!plan2) {
22113
+ console.error(chalk3.red(`Plan not found: ${opts.show}`));
22114
+ process.exit(1);
22115
+ }
22116
+ const tasks2 = await cloudListTasks(cloud, { plan_id: plan2.id });
22117
+ if (globalOpts.json) {
22118
+ output({ plan: plan2, tasks: tasks2, artifact: null }, true);
22119
+ return;
22120
+ }
22121
+ console.log(chalk3.bold(`Plan Details:
22122
+ `));
22123
+ console.log(` ${chalk3.dim("ID:")} ${plan2.id}`);
22124
+ if (plan2.slug)
22125
+ console.log(` ${chalk3.dim("Slug:")} ${plan2.slug}`);
22126
+ console.log(` ${chalk3.dim("Name:")} ${plan2.name}`);
22127
+ console.log(` ${chalk3.dim("Status:")} ${chalk3.cyan(plan2.status)}`);
22128
+ if (plan2.description)
22129
+ console.log(` ${chalk3.dim("Desc:")} ${plan2.description}`);
22130
+ if (plan2.project_id)
22131
+ console.log(` ${chalk3.dim("Project:")} ${plan2.project_id}`);
22132
+ console.log(` ${chalk3.dim("Created:")} ${plan2.created_at}`);
22133
+ if (tasks2.length > 0) {
22134
+ console.log(chalk3.bold(`
22135
+ Tasks (${tasks2.length}):`));
22136
+ for (const t of tasks2)
22137
+ console.log(` ${formatTaskLine(t)}`);
22138
+ } else {
22139
+ console.log(chalk3.dim(`
22140
+ No tasks in this plan.`));
22141
+ }
22142
+ return;
22143
+ }
22036
22144
  const db = getDatabase();
22037
22145
  const resolvedId = resolvePlanCliRef(opts.show, projectId);
22038
22146
  const plan = getPlan(resolvedId);
@@ -33132,17 +33240,35 @@ Use ${chalk5.cyan(`--agent ${result.id}`)} on future commands.`);
33132
33240
  console.error(chalk5.red("Agent ID required. Use --agent or pass as argument."));
33133
33241
  process.exit(1);
33134
33242
  }
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`));
33243
+ try {
33244
+ const cloud = getTodosCloudClient();
33245
+ if (cloud) {
33246
+ const a2 = await cloudHeartbeatAgent(cloud, agentId);
33247
+ if (!a2) {
33248
+ console.error(chalk5.red(`Agent not found: ${agentId}`));
33249
+ process.exit(1);
33250
+ }
33251
+ if (globalOpts.json) {
33252
+ console.log(JSON.stringify({ agent_id: a2.id, name: a2.name, last_seen_at: a2.last_seen_at }));
33253
+ } else {
33254
+ console.log(chalk5.green(`\u2665 ${a2.name} (${a2.id.slice(0, 8)}) \u2014 heartbeat sent`));
33255
+ }
33256
+ return;
33257
+ }
33258
+ const { updateAgentActivity: updateAgentActivity2, getAgent: getAgent2, getAgentByName: getAgentByName2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
33259
+ const a = getAgent2(agentId) || getAgentByName2(agentId);
33260
+ if (!a) {
33261
+ console.error(chalk5.red(`Agent not found: ${agentId}`));
33262
+ process.exit(1);
33263
+ }
33264
+ updateAgentActivity2(a.id);
33265
+ if (globalOpts.json) {
33266
+ console.log(JSON.stringify({ agent_id: a.id, name: a.name, last_seen_at: new Date().toISOString() }));
33267
+ } else {
33268
+ console.log(chalk5.green(`\u2665 ${a.name} (${a.id.slice(0, 8)}) \u2014 heartbeat sent`));
33269
+ }
33270
+ } catch (e) {
33271
+ handleError(e);
33146
33272
  }
33147
33273
  });
33148
33274
  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 +33278,43 @@ Use ${chalk5.cyan(`--agent ${result.id}`)} on future commands.`);
33152
33278
  console.error(chalk5.red("Agent ID or name required. Use --agent or pass as argument."));
33153
33279
  process.exit(1);
33154
33280
  }
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.`));
33281
+ try {
33282
+ const cloud = getTodosCloudClient();
33283
+ if (cloud) {
33284
+ const result = await cloudReleaseAgent(cloud, agentId, opts?.sessionId);
33285
+ if (!result.agent) {
33286
+ console.error(chalk5.red(`Agent not found: ${agentId}`));
33287
+ process.exit(1);
33288
+ }
33289
+ if (!result.released) {
33290
+ console.error(chalk5.red("Release denied: session_id does not match agent's current session."));
33291
+ process.exit(1);
33292
+ }
33293
+ if (globalOpts.json) {
33294
+ console.log(JSON.stringify({ agent_id: result.agent.id, name: result.agent.name, released: true }));
33295
+ } else {
33296
+ console.log(chalk5.green(`\u2713 ${result.agent.name} (${result.agent.id}) released \u2014 name is now available.`));
33297
+ }
33298
+ return;
33299
+ }
33300
+ const { getAgent: getAgent2, getAgentByName: getAgentByName2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
33301
+ const a = getAgent2(agentId) || getAgentByName2(agentId);
33302
+ if (!a) {
33303
+ console.error(chalk5.red(`Agent not found: ${agentId}`));
33304
+ process.exit(1);
33305
+ }
33306
+ const released = releaseAgent(a.id, opts?.sessionId);
33307
+ if (!released) {
33308
+ console.error(chalk5.red("Release denied: session_id does not match agent's current session."));
33309
+ process.exit(1);
33310
+ }
33311
+ if (globalOpts.json) {
33312
+ console.log(JSON.stringify({ agent_id: a.id, name: a.name, released: true }));
33313
+ } else {
33314
+ console.log(chalk5.green(`\u2713 ${a.name} (${a.id}) released \u2014 name is now available.`));
33315
+ }
33316
+ } catch (e) {
33317
+ handleError(e);
33170
33318
  }
33171
33319
  });
33172
33320
  program2.command("focus [project]").description("Focus on a project (or clear focus if no project given)").action(async (project) => {
@@ -38697,6 +38845,16 @@ function createPostgresTodosStorageAdapter(options) {
38697
38845
  add: (input, context) => addVerification(input, store, context),
38698
38846
  list: (taskId) => listVerifications(taskId, store)
38699
38847
  },
38848
+ commits: {
38849
+ add: (input, context) => addCommit(input, store, context),
38850
+ list: (taskId) => listCommits(taskId, store),
38851
+ find: (sha) => findCommit(sha, store)
38852
+ },
38853
+ gitRefs: {
38854
+ add: (input, context) => addGitRef(input, store, context),
38855
+ list: (taskId) => listGitRefs(taskId, store),
38856
+ find: (ref) => findGitRefs(ref, store)
38857
+ },
38700
38858
  projects: {
38701
38859
  create: (input, context) => createProject2(input, store, context),
38702
38860
  get: (id) => store.get("projects", id),
@@ -38717,7 +38875,9 @@ function createPostgresTodosStorageAdapter(options) {
38717
38875
  get: (id) => store.get("agents", id),
38718
38876
  getByName: async (name) => (await store.list("agents")).find((agent) => agent.name === name) ?? null,
38719
38877
  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)
38878
+ update: (id, input) => updateAgent2(id, input, store),
38879
+ heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
38880
+ release: (idOrName, sessionId, context) => releaseAgent2(idOrName, sessionId, store, context)
38721
38881
  },
38722
38882
  taskLists: {
38723
38883
  create: (input, context) => createTaskList2(input, store, context),
@@ -39247,6 +39407,53 @@ async function addVerification(input, store, context) {
39247
39407
  async function listVerifications(taskId, store) {
39248
39408
  return (await store.list("verifications")).filter((verification) => verification.task_id === taskId).sort((a, b) => b.run_at.localeCompare(a.run_at));
39249
39409
  }
39410
+ async function addCommit(input, store, context) {
39411
+ if (!await store.get("tasks", input.task_id))
39412
+ throw new Error(`Task not found: ${input.task_id}`);
39413
+ const timestamp = new Date().toISOString();
39414
+ const commit = {
39415
+ id: randomUUID3(),
39416
+ task_id: input.task_id,
39417
+ sha: input.sha,
39418
+ message: input.message ?? null,
39419
+ author: input.author ?? null,
39420
+ files_changed: input.files_changed ?? null,
39421
+ created_at: timestamp
39422
+ };
39423
+ await store.upsert("commits", { ...commit, updated_at: timestamp }, context);
39424
+ return commit;
39425
+ }
39426
+ async function listCommits(taskId, store) {
39427
+ return (await store.list("commits")).filter((commit) => commit.task_id === taskId).sort((a, b) => b.created_at.localeCompare(a.created_at));
39428
+ }
39429
+ async function findCommit(sha, store) {
39430
+ 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));
39431
+ return matches[0] ?? null;
39432
+ }
39433
+ async function addGitRef(input, store, context) {
39434
+ if (!await store.get("tasks", input.task_id))
39435
+ throw new Error(`Task not found: ${input.task_id}`);
39436
+ const timestamp = new Date().toISOString();
39437
+ const gitRef = {
39438
+ id: randomUUID3(),
39439
+ task_id: input.task_id,
39440
+ ref_type: input.ref_type,
39441
+ name: input.name,
39442
+ url: input.url ?? null,
39443
+ provider: input.provider ?? null,
39444
+ metadata: input.metadata ?? {},
39445
+ created_at: timestamp,
39446
+ updated_at: timestamp
39447
+ };
39448
+ await store.upsert("refs", gitRef, context);
39449
+ return gitRef;
39450
+ }
39451
+ async function listGitRefs(taskId, store) {
39452
+ return (await store.list("refs")).filter((ref) => ref.task_id === taskId).sort((a, b) => b.created_at.localeCompare(a.created_at));
39453
+ }
39454
+ async function findGitRefs(ref, store) {
39455
+ return (await store.list("refs")).filter((r) => r.name === ref).sort((a, b) => b.created_at.localeCompare(a.created_at));
39456
+ }
39250
39457
  function toFilterArray(value) {
39251
39458
  return Array.isArray(value) ? value : [value];
39252
39459
  }
@@ -39393,6 +39600,28 @@ async function updateAgent2(id, input, store) {
39393
39600
  last_seen_at: new Date().toISOString()
39394
39601
  });
39395
39602
  }
39603
+ async function resolveAgent(idOrName, store) {
39604
+ const byId = await store.get("agents", idOrName);
39605
+ if (byId)
39606
+ return byId;
39607
+ return (await store.list("agents")).find((agent) => agent.name === idOrName) ?? null;
39608
+ }
39609
+ async function heartbeatAgent(idOrName, store, context) {
39610
+ const agent = await resolveAgent(idOrName, store);
39611
+ if (!agent)
39612
+ return null;
39613
+ return store.upsert("agents", { ...agent, last_seen_at: new Date().toISOString() }, context);
39614
+ }
39615
+ async function releaseAgent2(idOrName, sessionId, store, context) {
39616
+ const agent = await resolveAgent(idOrName, store);
39617
+ if (!agent)
39618
+ return null;
39619
+ if (sessionId && agent.session_id && agent.session_id !== sessionId) {
39620
+ return { agent, released: false };
39621
+ }
39622
+ const updated = await store.upsert("agents", { ...agent, session_id: null, last_seen_at: new Date().toISOString() }, context);
39623
+ return { agent: updated, released: true };
39624
+ }
39396
39625
  async function createTaskList2(input, store, context) {
39397
39626
  const timestamp = new Date().toISOString();
39398
39627
  return store.upsert("task_lists", {
@@ -40277,6 +40506,70 @@ async function handleV1Request(req, url) {
40277
40506
  }
40278
40507
  return error(405, `method ${method} not allowed on /v1/tasks/:id/verifications`);
40279
40508
  }
40509
+ if (action === "commits") {
40510
+ if (!store.commits)
40511
+ return error(501, "commit links are not supported by this storage backend");
40512
+ if (method === "GET") {
40513
+ if (!await store.tasks.get(id))
40514
+ return error(404, "task not found");
40515
+ const commits = await store.commits.list(id);
40516
+ return json2({ commits, count: commits.length });
40517
+ }
40518
+ if (method === "POST") {
40519
+ const body2 = await readJson(req) ?? {};
40520
+ if (typeof body2.sha !== "string" || !body2.sha.trim())
40521
+ return error(400, "sha is required");
40522
+ try {
40523
+ const commit = await store.commits.add({
40524
+ task_id: id,
40525
+ sha: body2.sha,
40526
+ message: body2.message,
40527
+ author: body2.author,
40528
+ files_changed: Array.isArray(body2.files_changed) ? body2.files_changed : undefined
40529
+ }, contextFromPrincipal(principal));
40530
+ return json2({ commit }, 201);
40531
+ } catch (e) {
40532
+ const msg = e.message || "";
40533
+ if (msg.includes("not found"))
40534
+ return error(404, msg);
40535
+ throw e;
40536
+ }
40537
+ }
40538
+ return error(405, `method ${method} not allowed on /v1/tasks/:id/commits`);
40539
+ }
40540
+ if (action === "refs") {
40541
+ if (!store.gitRefs)
40542
+ return error(501, "git ref links are not supported by this storage backend");
40543
+ if (method === "GET") {
40544
+ if (!await store.tasks.get(id))
40545
+ return error(404, "task not found");
40546
+ const refs = await store.gitRefs.list(id);
40547
+ return json2({ refs, count: refs.length });
40548
+ }
40549
+ if (method === "POST") {
40550
+ const body2 = await readJson(req) ?? {};
40551
+ const refType = body2.ref_type === "pull_request" || body2.ref_type === "branch" ? body2.ref_type : "branch";
40552
+ if (typeof body2.name !== "string" || !body2.name.trim())
40553
+ return error(400, "name is required");
40554
+ try {
40555
+ const ref = await store.gitRefs.add({
40556
+ task_id: id,
40557
+ ref_type: refType,
40558
+ name: body2.name,
40559
+ url: body2.url,
40560
+ provider: body2.provider,
40561
+ metadata: body2.metadata
40562
+ }, contextFromPrincipal(principal));
40563
+ return json2({ ref }, 201);
40564
+ } catch (e) {
40565
+ const msg = e.message || "";
40566
+ if (msg.includes("not found"))
40567
+ return error(404, msg);
40568
+ throw e;
40569
+ }
40570
+ }
40571
+ return error(405, `method ${method} not allowed on /v1/tasks/:id/refs`);
40572
+ }
40280
40573
  const body = await readJson(req) ?? {};
40281
40574
  const agentId = body.agent_id || principal.agent || "todos-serve";
40282
40575
  if (action === "start" && method === "POST") {
@@ -40389,6 +40682,30 @@ async function handleV1Request(req, url) {
40389
40682
  }
40390
40683
  return json2({ agent: result }, 201);
40391
40684
  }
40685
+ if (id && action === "heartbeat") {
40686
+ if (method !== "POST")
40687
+ return error(405, `method ${method} not allowed on /v1/agents/:id/heartbeat`);
40688
+ if (typeof store.agents.heartbeat !== "function") {
40689
+ return error(501, "agent heartbeat is not supported by this storage backend");
40690
+ }
40691
+ const agent = await store.agents.heartbeat(id, contextFromPrincipal(principal));
40692
+ return agent ? json2({ agent }) : error(404, "agent not found");
40693
+ }
40694
+ if (id && action === "release") {
40695
+ if (method !== "POST")
40696
+ return error(405, `method ${method} not allowed on /v1/agents/:id/release`);
40697
+ if (typeof store.agents.release !== "function") {
40698
+ return error(501, "agent release is not supported by this storage backend");
40699
+ }
40700
+ const body = await readJson(req) ?? {};
40701
+ const result = await store.agents.release(id, body.session_id, contextFromPrincipal(principal));
40702
+ if (!result)
40703
+ return error(404, "agent not found");
40704
+ if (!result.released) {
40705
+ return error(409, "release denied: session_id does not match agent's current session", { released: false });
40706
+ }
40707
+ return json2({ agent: result.agent, released: true });
40708
+ }
40392
40709
  if (id && method === "GET") {
40393
40710
  const agent = await store.agents.get(id);
40394
40711
  return agent ? json2({ agent }) : error(404, "agent not found");
@@ -40418,6 +40735,22 @@ async function handleV1Request(req, url) {
40418
40735
  const dependencies = await store.dependencies.listAll();
40419
40736
  return json2({ dependencies, count: dependencies.length });
40420
40737
  }
40738
+ if (resource === "commits" && id) {
40739
+ if (method !== "GET")
40740
+ return error(405, `method ${method} not allowed on /v1/commits/:sha`);
40741
+ if (!store.commits)
40742
+ return error(501, "commit links are not supported by this storage backend");
40743
+ const commit = await store.commits.find(id);
40744
+ return json2({ commit: commit ?? null });
40745
+ }
40746
+ if (resource === "refs" && id) {
40747
+ if (method !== "GET")
40748
+ return error(405, `method ${method} not allowed on /v1/refs/:ref`);
40749
+ if (!store.gitRefs)
40750
+ return error(501, "git ref links are not supported by this storage backend");
40751
+ const refs = await store.gitRefs.find(id);
40752
+ return json2({ refs, count: refs.length });
40753
+ }
40421
40754
  if (resource === "next" && !id) {
40422
40755
  if (method !== "GET")
40423
40756
  return error(405, `method ${method} not allowed on /v1/next`);
@@ -45774,7 +46107,7 @@ function resolveRun(parsed, db) {
45774
46107
  }
45775
46108
  return resolution;
45776
46109
  }
45777
- function resolveAgent(parsed, db) {
46110
+ function resolveAgent2(parsed, db) {
45778
46111
  const resolution = blankResolution(parsed);
45779
46112
  const id = resolvePartialId(db, "agents", parsed.target);
45780
46113
  const agent = id ? getAgent(id, db) : getAgentByName(parsed.target, db);
@@ -45811,7 +46144,7 @@ function resolveTask(parsed, db) {
45811
46144
  function resolveUnknown(parsed, workspace, db, maxSymbolMatches) {
45812
46145
  const attempts = [
45813
46146
  () => resolveTask({ ...parsed, kind: "task" }, db),
45814
- () => resolveAgent({ ...parsed, kind: "agent" }, db),
46147
+ () => resolveAgent2({ ...parsed, kind: "agent" }, db),
45815
46148
  () => resolvePlan({ ...parsed, kind: "plan" }, db),
45816
46149
  () => resolveSymbol({ ...parsed, kind: "symbol" }, workspace, maxSymbolMatches)
45817
46150
  ];
@@ -45846,7 +46179,7 @@ function resolveMentions(input, db) {
45846
46179
  case "run":
45847
46180
  return resolveRun(parsed, d);
45848
46181
  case "agent":
45849
- return resolveAgent(parsed, d);
46182
+ return resolveAgent2(parsed, d);
45850
46183
  case "task":
45851
46184
  return resolveTask(parsed, d);
45852
46185
  default:
@@ -55660,7 +55993,7 @@ function resolveProjectId(value, db) {
55660
55993
  return null;
55661
55994
  return resolvePartialId(db, "projects", value) || value;
55662
55995
  }
55663
- function resolveAgent2(agentId, db) {
55996
+ function resolveAgent3(agentId, db) {
55664
55997
  return db.query("SELECT id, name FROM agents WHERE id = ? OR LOWER(name) = LOWER(?)").get(agentId, agentId);
55665
55998
  }
55666
55999
  function agentTaskWhere(agent, options, db) {
@@ -55704,7 +56037,7 @@ function scoreHandoffs(created, withTaskRefs, withBlockers) {
55704
56037
  }
55705
56038
  function getAgentReliabilityScorecard(agentId, options = {}, db) {
55706
56039
  const d = db || getDatabase();
55707
- const agent = resolveAgent2(agentId, d);
56040
+ const agent = resolveAgent3(agentId, d);
55708
56041
  if (!agent)
55709
56042
  return null;
55710
56043
  const generatedAt = now();
@@ -55873,7 +56206,7 @@ function getAgentReliabilityScorecard(agentId, options = {}, db) {
55873
56206
  }
55874
56207
  function listAgentReliabilityScorecards(options = {}, db) {
55875
56208
  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();
56209
+ 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
56210
  const limit = Number.isFinite(options.limit) && options.limit > 0 ? Math.min(Math.floor(options.limit), 500) : 50;
55878
56211
  return agents.map((agent) => getAgentReliabilityScorecard(agent.id, options, d)).filter((scorecard) => Boolean(scorecard)).filter((scorecard) => {
55879
56212
  const signals = scorecard.signals;
@@ -63244,6 +63577,19 @@ ID: ${updated.id}${taskNote}`
63244
63577
  agent_id: exports_external3.string().describe("Your agent ID or name.")
63245
63578
  }, async ({ agent_id }) => {
63246
63579
  try {
63580
+ const cloud = getTodosCloudClient();
63581
+ if (cloud) {
63582
+ const a = await cloudHeartbeatAgent(cloud, agent_id);
63583
+ if (!a) {
63584
+ return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
63585
+ }
63586
+ return {
63587
+ content: [{
63588
+ type: "text",
63589
+ text: `Heartbeat: ${a.name} (${a.id}) \u2014 last_seen_at updated to ${a.last_seen_at}`
63590
+ }]
63591
+ };
63592
+ }
63247
63593
  const agent = getAgent(agent_id) || getAgentByName(agent_id);
63248
63594
  if (!agent) {
63249
63595
  return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
@@ -63266,6 +63612,22 @@ ID: ${updated.id}${taskNote}`
63266
63612
  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
63613
  }, async ({ agent_id, session_id }) => {
63268
63614
  try {
63615
+ const cloud = getTodosCloudClient();
63616
+ if (cloud) {
63617
+ const result = await cloudReleaseAgent(cloud, agent_id, session_id);
63618
+ if (!result.agent) {
63619
+ return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
63620
+ }
63621
+ if (!result.released) {
63622
+ return { content: [{ type: "text", text: `Release denied: session_id does not match agent's current session.` }], isError: true };
63623
+ }
63624
+ return {
63625
+ content: [{
63626
+ type: "text",
63627
+ text: `Agent released: ${result.agent.name} (${result.agent.id}) \u2014 session cleared, name is now available.`
63628
+ }]
63629
+ };
63630
+ }
63269
63631
  const agent = getAgent(agent_id) || getAgentByName(agent_id);
63270
63632
  if (!agent) {
63271
63633
  return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
@@ -71789,38 +72151,68 @@ exit 0
71789
72151
  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
72152
  const globalOpts = program2.opts();
71791
72153
  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;
72154
+ const files = opts.files ? opts.files.split(",").filter(Boolean) : undefined;
72155
+ try {
72156
+ const cloud = getTodosCloudClient();
72157
+ const commit = cloud ? await cloudLinkCommit(cloud, resolvedId, {
72158
+ sha,
72159
+ ...opts.message !== undefined ? { message: opts.message } : {},
72160
+ ...opts.author !== undefined ? { author: opts.author } : {},
72161
+ ...files ? { files_changed: files } : {}
72162
+ }) : (await Promise.resolve().then(() => (init_task_commits(), exports_task_commits))).linkTaskToCommit({
72163
+ task_id: resolvedId,
72164
+ sha,
72165
+ message: opts.message,
72166
+ author: opts.author,
72167
+ files_changed: files
72168
+ });
72169
+ if (globalOpts.json) {
72170
+ output(commit, true);
72171
+ return;
72172
+ }
72173
+ console.log(chalk8.green(`Linked commit ${sha.slice(0, 7)} to task ${taskId}`));
72174
+ } catch (e) {
72175
+ handleError(e);
71803
72176
  }
71804
- console.log(chalk8.green(`Linked commit ${sha.slice(0, 7)} to task ${taskId}`));
71805
72177
  });
71806
72178
  program2.command("find-commit <sha>").description("Find which task explains a git commit SHA").action(async (sha) => {
71807
72179
  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;
72180
+ try {
72181
+ const cloud = getTodosCloudClient();
72182
+ if (cloud) {
72183
+ const commit = await cloudFindCommit(cloud, sha);
72184
+ if (globalOpts.json) {
72185
+ output(commit ? { task_id: commit.task_id, commit } : null, true);
72186
+ return;
72187
+ }
72188
+ if (!commit) {
72189
+ console.log(chalk8.dim(`No task linked to commit ${sha}.`));
72190
+ return;
72191
+ }
72192
+ console.log(`${chalk8.yellow(commit.sha.slice(0, 7))} -> ${chalk8.cyan(commit.task_id)}`);
72193
+ if (commit.message)
72194
+ console.log(chalk8.dim(` ${commit.message}`));
72195
+ return;
72196
+ }
72197
+ const { findTaskByCommit: findTaskByCommit2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
72198
+ const { getTask: getTask3 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
72199
+ const result = findTaskByCommit2(sha);
72200
+ if (globalOpts.json) {
72201
+ output(result, true);
72202
+ return;
72203
+ }
72204
+ if (!result) {
72205
+ console.log(chalk8.dim(`No task linked to commit ${sha}.`));
72206
+ return;
72207
+ }
72208
+ const task2 = getTask3(result.task_id);
72209
+ const taskLabel = task2 ? `${task2.short_id || task2.id.slice(0, 8)} ${task2.title}` : result.task_id;
72210
+ console.log(`${chalk8.yellow(result.commit.sha.slice(0, 7))} -> ${chalk8.cyan(taskLabel)}`);
72211
+ if (result.commit.message)
72212
+ console.log(chalk8.dim(` ${result.commit.message}`));
72213
+ } catch (e) {
72214
+ handleError(e);
71818
72215
  }
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
72216
  });
71825
72217
  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
72218
  const globalOpts = program2.opts();
@@ -71840,38 +72232,70 @@ exit 0
71840
72232
  process.exit(1);
71841
72233
  }
71842
72234
  }
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;
72235
+ try {
72236
+ const cloud = getTodosCloudClient();
72237
+ const gitRef = cloud ? await cloudLinkRef(cloud, resolvedId, {
72238
+ ref_type: refType,
72239
+ name: ref,
72240
+ ...opts.url !== undefined ? { url: opts.url } : {},
72241
+ ...opts.provider !== undefined ? { provider: opts.provider } : {},
72242
+ ...metadata ? { metadata } : {}
72243
+ }) : linkTaskGitRef2({
72244
+ task_id: resolvedId,
72245
+ ref_type: refType,
72246
+ name: ref,
72247
+ url: opts.url,
72248
+ provider: opts.provider,
72249
+ metadata
72250
+ });
72251
+ if (globalOpts.json) {
72252
+ output(gitRef, true);
72253
+ return;
72254
+ }
72255
+ console.log(chalk8.green(`Linked ${gitRef.ref_type} ${gitRef.name} to task ${taskId}`));
72256
+ } catch (e) {
72257
+ handleError(e);
71854
72258
  }
71855
- console.log(chalk8.green(`Linked ${gitRef.ref_type} ${gitRef.name} to task ${taskId}`));
71856
72259
  });
71857
72260
  program2.command("find-ref <ref>").description("Find tasks linked to a git branch or pull request").action(async (ref) => {
71858
72261
  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}`);
72262
+ try {
72263
+ const cloud = getTodosCloudClient();
72264
+ if (cloud) {
72265
+ const refs2 = await cloudFindRefs(cloud, ref);
72266
+ if (globalOpts.json) {
72267
+ output(refs2, true);
72268
+ return;
72269
+ }
72270
+ if (refs2.length === 0) {
72271
+ console.log(chalk8.dim(`No tasks linked to ${ref}.`));
72272
+ return;
72273
+ }
72274
+ for (const gitRef of refs2) {
72275
+ const url = gitRef.url ? chalk8.dim(` ${gitRef.url}`) : "";
72276
+ console.log(`${chalk8.cyan(gitRef.task_id)} <- ${gitRef.ref_type} ${chalk8.yellow(gitRef.name)}${url}`);
72277
+ }
72278
+ return;
72279
+ }
72280
+ const { findTasksByGitRef: findTasksByGitRef2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
72281
+ const { getTask: getTask3 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
72282
+ const refs = findTasksByGitRef2(ref);
72283
+ if (globalOpts.json) {
72284
+ output(refs, true);
72285
+ return;
72286
+ }
72287
+ if (refs.length === 0) {
72288
+ console.log(chalk8.dim(`No tasks linked to ${ref}.`));
72289
+ return;
72290
+ }
72291
+ for (const gitRef of refs) {
72292
+ const task2 = getTask3(gitRef.task_id);
72293
+ const label = task2 ? `${task2.short_id || task2.id.slice(0, 8)} ${task2.title}` : gitRef.task_id;
72294
+ const url = gitRef.url ? chalk8.dim(` ${gitRef.url}`) : "";
72295
+ console.log(`${chalk8.cyan(label)} <- ${gitRef.ref_type} ${chalk8.yellow(gitRef.name)}${url}`);
72296
+ }
72297
+ } catch (e) {
72298
+ handleError(e);
71875
72299
  }
71876
72300
  });
71877
72301
  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) => {