@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/cloud-router.d.ts +95 -0
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +701 -110
- package/dist/index.js +101 -8
- package/dist/mcp/index.js +303 -2
- package/dist/mcp/tools/agents.d.ts.map +1 -1
- package/dist/release-provenance.json +3 -3
- package/dist/server/index.js +312 -11
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +74 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage.js +95 -2
- package/package.json +1 -1
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
|
|
20941
|
-
|
|
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
|
|
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
|
|
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 ${
|
|
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
|
-
|
|
33136
|
-
|
|
33137
|
-
|
|
33138
|
-
|
|
33139
|
-
|
|
33140
|
-
|
|
33141
|
-
|
|
33142
|
-
|
|
33143
|
-
|
|
33144
|
-
|
|
33145
|
-
|
|
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
|
-
|
|
33156
|
-
|
|
33157
|
-
|
|
33158
|
-
|
|
33159
|
-
|
|
33160
|
-
|
|
33161
|
-
|
|
33162
|
-
|
|
33163
|
-
|
|
33164
|
-
|
|
33165
|
-
|
|
33166
|
-
|
|
33167
|
-
|
|
33168
|
-
|
|
33169
|
-
|
|
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
|
|
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
|
-
() =>
|
|
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
|
|
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
|
|
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 =
|
|
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 ?
|
|
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
|
|
71793
|
-
|
|
71794
|
-
|
|
71795
|
-
|
|
71796
|
-
|
|
71797
|
-
|
|
71798
|
-
|
|
71799
|
-
|
|
71800
|
-
|
|
71801
|
-
|
|
71802
|
-
|
|
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
|
-
|
|
71809
|
-
|
|
71810
|
-
|
|
71811
|
-
|
|
71812
|
-
|
|
71813
|
-
|
|
71814
|
-
|
|
71815
|
-
|
|
71816
|
-
|
|
71817
|
-
|
|
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
|
-
|
|
71844
|
-
|
|
71845
|
-
|
|
71846
|
-
|
|
71847
|
-
|
|
71848
|
-
|
|
71849
|
-
|
|
71850
|
-
|
|
71851
|
-
|
|
71852
|
-
|
|
71853
|
-
|
|
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
|
-
|
|
71860
|
-
|
|
71861
|
-
|
|
71862
|
-
|
|
71863
|
-
|
|
71864
|
-
|
|
71865
|
-
|
|
71866
|
-
|
|
71867
|
-
|
|
71868
|
-
|
|
71869
|
-
|
|
71870
|
-
|
|
71871
|
-
|
|
71872
|
-
|
|
71873
|
-
|
|
71874
|
-
|
|
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) => {
|