@hasna/todos 0.9.22 → 0.9.24
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 +158 -1
- package/dist/index.js +24 -1
- package/dist/mcp/index.js +148 -16
- package/dist/server/index.js +125 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -3205,8 +3205,26 @@ function deleteTask(id, db) {
|
|
|
3205
3205
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
3206
3206
|
return result.changes > 0;
|
|
3207
3207
|
}
|
|
3208
|
+
function getBlockingDeps(id, db) {
|
|
3209
|
+
const d = db || getDatabase();
|
|
3210
|
+
const deps = getTaskDependencies(id, d);
|
|
3211
|
+
if (deps.length === 0)
|
|
3212
|
+
return [];
|
|
3213
|
+
const blocking = [];
|
|
3214
|
+
for (const dep of deps) {
|
|
3215
|
+
const task = getTask(dep.depends_on, d);
|
|
3216
|
+
if (task && task.status !== "completed")
|
|
3217
|
+
blocking.push(task);
|
|
3218
|
+
}
|
|
3219
|
+
return blocking;
|
|
3220
|
+
}
|
|
3208
3221
|
function startTask(id, agentId, db) {
|
|
3209
3222
|
const d = db || getDatabase();
|
|
3223
|
+
const blocking = getBlockingDeps(id, d);
|
|
3224
|
+
if (blocking.length > 0) {
|
|
3225
|
+
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
3226
|
+
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
3227
|
+
}
|
|
3210
3228
|
const cutoff = lockExpiryCutoff();
|
|
3211
3229
|
const timestamp = now();
|
|
3212
3230
|
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
@@ -3222,7 +3240,7 @@ function startTask(id, agentId, db) {
|
|
|
3222
3240
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
3223
3241
|
return getTask(id, d);
|
|
3224
3242
|
}
|
|
3225
|
-
function completeTask(id, agentId, db) {
|
|
3243
|
+
function completeTask(id, agentId, db, evidence) {
|
|
3226
3244
|
const d = db || getDatabase();
|
|
3227
3245
|
const task = getTask(id, d);
|
|
3228
3246
|
if (!task)
|
|
@@ -3231,6 +3249,10 @@ function completeTask(id, agentId, db) {
|
|
|
3231
3249
|
throw new LockError(id, task.locked_by);
|
|
3232
3250
|
}
|
|
3233
3251
|
checkCompletionGuard(task, agentId || null, d);
|
|
3252
|
+
if (evidence) {
|
|
3253
|
+
const meta = { ...task.metadata, _evidence: evidence };
|
|
3254
|
+
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta), id]);
|
|
3255
|
+
}
|
|
3234
3256
|
const timestamp = now();
|
|
3235
3257
|
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
|
|
3236
3258
|
WHERE id = ?`, [timestamp, timestamp, id]);
|
|
@@ -3293,6 +3315,10 @@ function removeDependency(taskId, dependsOn, db) {
|
|
|
3293
3315
|
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
3294
3316
|
return result.changes > 0;
|
|
3295
3317
|
}
|
|
3318
|
+
function getTaskDependencies(taskId, db) {
|
|
3319
|
+
const d = db || getDatabase();
|
|
3320
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
3321
|
+
}
|
|
3296
3322
|
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
3297
3323
|
const visited = new Set;
|
|
3298
3324
|
const queue = [dependsOn];
|
|
@@ -9132,6 +9158,39 @@ ${task.id.slice(0, 8)} | ${task.priority} | ${task.title}` }] };
|
|
|
9132
9158
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
9133
9159
|
}
|
|
9134
9160
|
});
|
|
9161
|
+
server.tool("get_my_tasks", "Get your assigned tasks and stats. Auto-registers if needed.", {
|
|
9162
|
+
agent_name: exports_external.string().describe("Your agent name")
|
|
9163
|
+
}, async ({ agent_name }) => {
|
|
9164
|
+
try {
|
|
9165
|
+
const agent = registerAgent({ name: agent_name });
|
|
9166
|
+
const tasks = listTasks({});
|
|
9167
|
+
const myTasks = tasks.filter((t) => t.assigned_to === agent_name || t.assigned_to === agent.id || t.agent_id === agent.id || t.agent_id === agent_name);
|
|
9168
|
+
const pending = myTasks.filter((t) => t.status === "pending");
|
|
9169
|
+
const inProgress = myTasks.filter((t) => t.status === "in_progress");
|
|
9170
|
+
const completed = myTasks.filter((t) => t.status === "completed");
|
|
9171
|
+
const rate = myTasks.length > 0 ? Math.round(completed.length / myTasks.length * 100) : 0;
|
|
9172
|
+
const lines = [
|
|
9173
|
+
`Agent: ${agent.name} (${agent.id})`,
|
|
9174
|
+
`Tasks: ${myTasks.length} total, ${pending.length} pending, ${inProgress.length} active, ${completed.length} done (${rate}%)`
|
|
9175
|
+
];
|
|
9176
|
+
if (pending.length > 0) {
|
|
9177
|
+
lines.push(`
|
|
9178
|
+
Pending:`);
|
|
9179
|
+
for (const t of pending.slice(0, 10))
|
|
9180
|
+
lines.push(` [${t.priority}] ${t.id.slice(0, 8)} | ${t.title}`);
|
|
9181
|
+
}
|
|
9182
|
+
if (inProgress.length > 0) {
|
|
9183
|
+
lines.push(`
|
|
9184
|
+
In Progress:`);
|
|
9185
|
+
for (const t of inProgress)
|
|
9186
|
+
lines.push(` ${t.id.slice(0, 8)} | ${t.title}`);
|
|
9187
|
+
}
|
|
9188
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
9189
|
+
`) }] };
|
|
9190
|
+
} catch (e) {
|
|
9191
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
9192
|
+
}
|
|
9193
|
+
});
|
|
9135
9194
|
server.resource("tasks", "todos://tasks", { description: "All active tasks", mimeType: "application/json" }, async () => {
|
|
9136
9195
|
const tasks = listTasks({ status: ["pending", "in_progress"] });
|
|
9137
9196
|
return { contents: [{ uri: "todos://tasks", text: JSON.stringify(tasks, null, 2), mimeType: "application/json" }] };
|
|
@@ -9225,6 +9284,19 @@ function taskToSummary(task) {
|
|
|
9225
9284
|
async function startServer(port, options) {
|
|
9226
9285
|
const shouldOpen = options?.open ?? true;
|
|
9227
9286
|
getDatabase();
|
|
9287
|
+
const sseClients = new Set;
|
|
9288
|
+
function broadcastEvent(event) {
|
|
9289
|
+
const data = JSON.stringify({ ...event, timestamp: new Date().toISOString() });
|
|
9290
|
+
for (const controller of sseClients) {
|
|
9291
|
+
try {
|
|
9292
|
+
controller.enqueue(`data: ${data}
|
|
9293
|
+
|
|
9294
|
+
`);
|
|
9295
|
+
} catch {
|
|
9296
|
+
sseClients.delete(controller);
|
|
9297
|
+
}
|
|
9298
|
+
}
|
|
9299
|
+
}
|
|
9228
9300
|
const dashboardDir = resolveDashboardDir();
|
|
9229
9301
|
const dashboardExists = existsSync6(dashboardDir);
|
|
9230
9302
|
if (!dashboardExists) {
|
|
@@ -9250,6 +9322,27 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
9250
9322
|
}
|
|
9251
9323
|
});
|
|
9252
9324
|
}
|
|
9325
|
+
if (path === "/api/events" && method === "GET") {
|
|
9326
|
+
const stream = new ReadableStream({
|
|
9327
|
+
start(controller) {
|
|
9328
|
+
sseClients.add(controller);
|
|
9329
|
+
controller.enqueue(`data: ${JSON.stringify({ type: "connected", timestamp: new Date().toISOString() })}
|
|
9330
|
+
|
|
9331
|
+
`);
|
|
9332
|
+
},
|
|
9333
|
+
cancel(controller) {
|
|
9334
|
+
sseClients.delete(controller);
|
|
9335
|
+
}
|
|
9336
|
+
});
|
|
9337
|
+
return new Response(stream, {
|
|
9338
|
+
headers: {
|
|
9339
|
+
"Content-Type": "text/event-stream",
|
|
9340
|
+
"Cache-Control": "no-cache",
|
|
9341
|
+
Connection: "keep-alive",
|
|
9342
|
+
"Access-Control-Allow-Origin": `http://localhost:${port}`
|
|
9343
|
+
}
|
|
9344
|
+
});
|
|
9345
|
+
}
|
|
9253
9346
|
if (path === "/api/stats" && method === "GET") {
|
|
9254
9347
|
const all = listTasks({ limit: 1e4 });
|
|
9255
9348
|
const projects = listProjects();
|
|
@@ -9287,6 +9380,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
9287
9380
|
priority: body.priority,
|
|
9288
9381
|
project_id: body.project_id
|
|
9289
9382
|
});
|
|
9383
|
+
broadcastEvent({ type: "task", task_id: task.id, action: "created", agent_id: task.agent_id });
|
|
9290
9384
|
return json(taskToSummary(task), 201, port);
|
|
9291
9385
|
} catch (e) {
|
|
9292
9386
|
return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
|
|
@@ -9389,6 +9483,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
9389
9483
|
const id = startMatch[1];
|
|
9390
9484
|
try {
|
|
9391
9485
|
const task = startTask(id, "dashboard");
|
|
9486
|
+
broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard" });
|
|
9392
9487
|
return json(taskToSummary(task), 200, port);
|
|
9393
9488
|
} catch (e) {
|
|
9394
9489
|
return json({ error: e instanceof Error ? e.message : "Failed to start task" }, 500, port);
|
|
@@ -9399,6 +9494,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
9399
9494
|
const id = completeMatch[1];
|
|
9400
9495
|
try {
|
|
9401
9496
|
const task = completeTask(id, "dashboard");
|
|
9497
|
+
broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard" });
|
|
9402
9498
|
return json(taskToSummary(task), 200, port);
|
|
9403
9499
|
} catch (e) {
|
|
9404
9500
|
return json({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500, port);
|
|
@@ -9407,6 +9503,67 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
9407
9503
|
if (path === "/api/projects" && method === "GET") {
|
|
9408
9504
|
return json(listProjects(), 200, port);
|
|
9409
9505
|
}
|
|
9506
|
+
if (path === "/api/agents/me" && method === "GET") {
|
|
9507
|
+
const name = url.searchParams.get("name");
|
|
9508
|
+
if (!name)
|
|
9509
|
+
return json({ error: "Missing name param" }, 400, port);
|
|
9510
|
+
const { registerAgent: registerAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
9511
|
+
const agent = registerAgent2({ name });
|
|
9512
|
+
const tasks = listTasks({ assigned_to: name });
|
|
9513
|
+
const agentIdTasks = listTasks({ agent_id: agent.id });
|
|
9514
|
+
const allTasks = [...tasks, ...agentIdTasks.filter((t) => !tasks.some((tt) => tt.id === t.id))];
|
|
9515
|
+
const pending = allTasks.filter((t) => t.status === "pending");
|
|
9516
|
+
const inProgress = allTasks.filter((t) => t.status === "in_progress");
|
|
9517
|
+
const completed = allTasks.filter((t) => t.status === "completed");
|
|
9518
|
+
return json({
|
|
9519
|
+
agent,
|
|
9520
|
+
pending_tasks: pending.map(taskToSummary),
|
|
9521
|
+
in_progress_tasks: inProgress.map(taskToSummary),
|
|
9522
|
+
stats: {
|
|
9523
|
+
total: allTasks.length,
|
|
9524
|
+
pending: pending.length,
|
|
9525
|
+
in_progress: inProgress.length,
|
|
9526
|
+
completed: completed.length,
|
|
9527
|
+
completion_rate: allTasks.length > 0 ? Math.round(completed.length / allTasks.length * 100) : 0
|
|
9528
|
+
}
|
|
9529
|
+
}, 200, port);
|
|
9530
|
+
}
|
|
9531
|
+
const queueMatch = path.match(/^\/api\/agents\/([^/]+)\/queue$/);
|
|
9532
|
+
if (queueMatch && method === "GET") {
|
|
9533
|
+
const agentId = decodeURIComponent(queueMatch[1]);
|
|
9534
|
+
const pending = listTasks({ status: "pending" });
|
|
9535
|
+
const queue = pending.filter((t) => t.assigned_to === agentId || t.agent_id === agentId || !t.assigned_to && !t.locked_by);
|
|
9536
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
9537
|
+
queue.sort((a, b) => (order[a.priority] ?? 4) - (order[b.priority] ?? 4) || new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
9538
|
+
return json(queue.map(taskToSummary), 200, port);
|
|
9539
|
+
}
|
|
9540
|
+
if (path === "/api/tasks/claim" && method === "POST") {
|
|
9541
|
+
try {
|
|
9542
|
+
const body = await req.json();
|
|
9543
|
+
const agentId = body.agent_id || "anonymous";
|
|
9544
|
+
const pending = listTasks({ status: "pending", project_id: body.project_id });
|
|
9545
|
+
const available = pending.filter((t) => !t.locked_by);
|
|
9546
|
+
if (available.length === 0)
|
|
9547
|
+
return json({ task: null }, 200, port);
|
|
9548
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
9549
|
+
available.sort((a, b) => (order[a.priority] ?? 4) - (order[b.priority] ?? 4));
|
|
9550
|
+
const target = available[0];
|
|
9551
|
+
try {
|
|
9552
|
+
const claimed = startTask(target.id, agentId);
|
|
9553
|
+
return json({ task: taskToSummary(claimed) }, 200, port);
|
|
9554
|
+
} catch (e) {
|
|
9555
|
+
const next = available[1] || null;
|
|
9556
|
+
return json({
|
|
9557
|
+
task: null,
|
|
9558
|
+
locked_by: target.locked_by,
|
|
9559
|
+
locked_since: target.locked_at,
|
|
9560
|
+
suggested_task: next ? taskToSummary(next) : null
|
|
9561
|
+
}, 200, port);
|
|
9562
|
+
}
|
|
9563
|
+
} catch (e) {
|
|
9564
|
+
return json({ error: e instanceof Error ? e.message : "Failed to claim" }, 500, port);
|
|
9565
|
+
}
|
|
9566
|
+
}
|
|
9410
9567
|
if (path === "/api/agents" && method === "GET") {
|
|
9411
9568
|
return json(listAgents(), 200, port);
|
|
9412
9569
|
}
|
package/dist/index.js
CHANGED
|
@@ -1122,8 +1122,26 @@ function deleteTask(id, db) {
|
|
|
1122
1122
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
1123
1123
|
return result.changes > 0;
|
|
1124
1124
|
}
|
|
1125
|
+
function getBlockingDeps(id, db) {
|
|
1126
|
+
const d = db || getDatabase();
|
|
1127
|
+
const deps = getTaskDependencies(id, d);
|
|
1128
|
+
if (deps.length === 0)
|
|
1129
|
+
return [];
|
|
1130
|
+
const blocking = [];
|
|
1131
|
+
for (const dep of deps) {
|
|
1132
|
+
const task = getTask(dep.depends_on, d);
|
|
1133
|
+
if (task && task.status !== "completed")
|
|
1134
|
+
blocking.push(task);
|
|
1135
|
+
}
|
|
1136
|
+
return blocking;
|
|
1137
|
+
}
|
|
1125
1138
|
function startTask(id, agentId, db) {
|
|
1126
1139
|
const d = db || getDatabase();
|
|
1140
|
+
const blocking = getBlockingDeps(id, d);
|
|
1141
|
+
if (blocking.length > 0) {
|
|
1142
|
+
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
1143
|
+
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
1144
|
+
}
|
|
1127
1145
|
const cutoff = lockExpiryCutoff();
|
|
1128
1146
|
const timestamp = now();
|
|
1129
1147
|
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
@@ -1139,7 +1157,7 @@ function startTask(id, agentId, db) {
|
|
|
1139
1157
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
1140
1158
|
return getTask(id, d);
|
|
1141
1159
|
}
|
|
1142
|
-
function completeTask(id, agentId, db) {
|
|
1160
|
+
function completeTask(id, agentId, db, evidence) {
|
|
1143
1161
|
const d = db || getDatabase();
|
|
1144
1162
|
const task = getTask(id, d);
|
|
1145
1163
|
if (!task)
|
|
@@ -1148,6 +1166,10 @@ function completeTask(id, agentId, db) {
|
|
|
1148
1166
|
throw new LockError(id, task.locked_by);
|
|
1149
1167
|
}
|
|
1150
1168
|
checkCompletionGuard(task, agentId || null, d);
|
|
1169
|
+
if (evidence) {
|
|
1170
|
+
const meta = { ...task.metadata, _evidence: evidence };
|
|
1171
|
+
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta), id]);
|
|
1172
|
+
}
|
|
1151
1173
|
const timestamp = now();
|
|
1152
1174
|
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
|
|
1153
1175
|
WHERE id = ?`, [timestamp, timestamp, id]);
|
|
@@ -2233,6 +2255,7 @@ export {
|
|
|
2233
2255
|
getDatabase,
|
|
2234
2256
|
getCompletionGuardConfig,
|
|
2235
2257
|
getComment,
|
|
2258
|
+
getBlockingDeps,
|
|
2236
2259
|
getAgentByName,
|
|
2237
2260
|
getAgent,
|
|
2238
2261
|
ensureTaskList,
|
package/dist/mcp/index.js
CHANGED
|
@@ -5161,8 +5161,26 @@ function deleteTask(id, db) {
|
|
|
5161
5161
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
5162
5162
|
return result.changes > 0;
|
|
5163
5163
|
}
|
|
5164
|
+
function getBlockingDeps(id, db) {
|
|
5165
|
+
const d = db || getDatabase();
|
|
5166
|
+
const deps = getTaskDependencies(id, d);
|
|
5167
|
+
if (deps.length === 0)
|
|
5168
|
+
return [];
|
|
5169
|
+
const blocking = [];
|
|
5170
|
+
for (const dep of deps) {
|
|
5171
|
+
const task = getTask(dep.depends_on, d);
|
|
5172
|
+
if (task && task.status !== "completed")
|
|
5173
|
+
blocking.push(task);
|
|
5174
|
+
}
|
|
5175
|
+
return blocking;
|
|
5176
|
+
}
|
|
5164
5177
|
function startTask(id, agentId, db) {
|
|
5165
5178
|
const d = db || getDatabase();
|
|
5179
|
+
const blocking = getBlockingDeps(id, d);
|
|
5180
|
+
if (blocking.length > 0) {
|
|
5181
|
+
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
5182
|
+
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
5183
|
+
}
|
|
5166
5184
|
const cutoff = lockExpiryCutoff();
|
|
5167
5185
|
const timestamp = now();
|
|
5168
5186
|
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
@@ -5178,7 +5196,7 @@ function startTask(id, agentId, db) {
|
|
|
5178
5196
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
5179
5197
|
return getTask(id, d);
|
|
5180
5198
|
}
|
|
5181
|
-
function completeTask(id, agentId, db) {
|
|
5199
|
+
function completeTask(id, agentId, db, evidence) {
|
|
5182
5200
|
const d = db || getDatabase();
|
|
5183
5201
|
const task = getTask(id, d);
|
|
5184
5202
|
if (!task)
|
|
@@ -5187,6 +5205,10 @@ function completeTask(id, agentId, db) {
|
|
|
5187
5205
|
throw new LockError(id, task.locked_by);
|
|
5188
5206
|
}
|
|
5189
5207
|
checkCompletionGuard(task, agentId || null, d);
|
|
5208
|
+
if (evidence) {
|
|
5209
|
+
const meta = { ...task.metadata, _evidence: evidence };
|
|
5210
|
+
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta), id]);
|
|
5211
|
+
}
|
|
5190
5212
|
const timestamp = now();
|
|
5191
5213
|
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
|
|
5192
5214
|
WHERE id = ?`, [timestamp, timestamp, id]);
|
|
@@ -5249,6 +5271,10 @@ function removeDependency(taskId, dependsOn, db) {
|
|
|
5249
5271
|
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
5250
5272
|
return result.changes > 0;
|
|
5251
5273
|
}
|
|
5274
|
+
function getTaskDependencies(taskId, db) {
|
|
5275
|
+
const d = db || getDatabase();
|
|
5276
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
5277
|
+
}
|
|
5252
5278
|
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
5253
5279
|
const visited = new Set;
|
|
5254
5280
|
const queue = [dependsOn];
|
|
@@ -6170,7 +6196,7 @@ ${text}` }] };
|
|
|
6170
6196
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6171
6197
|
}
|
|
6172
6198
|
});
|
|
6173
|
-
server.tool("get_task", "Get full task details
|
|
6199
|
+
server.tool("get_task", "Get full task details with relations", {
|
|
6174
6200
|
id: exports_external.string().describe("Task ID (full or partial)")
|
|
6175
6201
|
}, async ({ id }) => {
|
|
6176
6202
|
try {
|
|
@@ -6218,7 +6244,7 @@ Parent: ${task.parent.id.slice(0, 8)} | ${task.parent.title}`);
|
|
|
6218
6244
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6219
6245
|
}
|
|
6220
6246
|
});
|
|
6221
|
-
server.tool("update_task", "Update task fields
|
|
6247
|
+
server.tool("update_task", "Update task fields. Version required for optimistic locking.", {
|
|
6222
6248
|
id: exports_external.string().describe("Task ID (full or partial)"),
|
|
6223
6249
|
version: exports_external.number().describe("Current version (for optimistic locking)"),
|
|
6224
6250
|
title: exports_external.string().optional().describe("New title"),
|
|
@@ -6256,7 +6282,7 @@ server.tool("delete_task", "Delete a task permanently", {
|
|
|
6256
6282
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6257
6283
|
}
|
|
6258
6284
|
});
|
|
6259
|
-
server.tool("start_task", "Claim
|
|
6285
|
+
server.tool("start_task", "Claim, lock, and set task status to in_progress.", {
|
|
6260
6286
|
id: exports_external.string().describe("Task ID (full or partial)"),
|
|
6261
6287
|
agent_id: exports_external.string().describe("Agent claiming the task")
|
|
6262
6288
|
}, async ({ id, agent_id }) => {
|
|
@@ -6269,7 +6295,7 @@ ${formatTask(task)}` }] };
|
|
|
6269
6295
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6270
6296
|
}
|
|
6271
6297
|
});
|
|
6272
|
-
server.tool("complete_task", "Mark
|
|
6298
|
+
server.tool("complete_task", "Mark task completed and release lock.", {
|
|
6273
6299
|
id: exports_external.string().describe("Task ID (full or partial)"),
|
|
6274
6300
|
agent_id: exports_external.string().optional().describe("Agent completing the task")
|
|
6275
6301
|
}, async ({ id, agent_id }) => {
|
|
@@ -6309,7 +6335,7 @@ server.tool("unlock_task", "Release exclusive lock on a task", {
|
|
|
6309
6335
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6310
6336
|
}
|
|
6311
6337
|
});
|
|
6312
|
-
server.tool("add_dependency", "Add a dependency
|
|
6338
|
+
server.tool("add_dependency", "Add a dependency: task_id depends on depends_on.", {
|
|
6313
6339
|
task_id: exports_external.string().describe("Task that depends on another"),
|
|
6314
6340
|
depends_on: exports_external.string().describe("Task that must complete first")
|
|
6315
6341
|
}, async ({ task_id, depends_on }) => {
|
|
@@ -6500,7 +6526,7 @@ server.tool("delete_plan", "Delete a plan", {
|
|
|
6500
6526
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6501
6527
|
}
|
|
6502
6528
|
});
|
|
6503
|
-
server.tool("search_tasks", "Full-text search across task titles, descriptions,
|
|
6529
|
+
server.tool("search_tasks", "Full-text search across task titles, descriptions, tags.", {
|
|
6504
6530
|
query: exports_external.string().describe("Search query"),
|
|
6505
6531
|
project_id: exports_external.string().optional().describe("Limit to project"),
|
|
6506
6532
|
task_list_id: exports_external.string().optional().describe("Filter by task list")
|
|
@@ -6520,7 +6546,7 @@ ${text}` }] };
|
|
|
6520
6546
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6521
6547
|
}
|
|
6522
6548
|
});
|
|
6523
|
-
server.tool("sync", "Sync tasks with an agent task list
|
|
6549
|
+
server.tool("sync", "Sync tasks with an agent task list.", {
|
|
6524
6550
|
task_list_id: exports_external.string().optional().describe("Task list ID (required for Claude)"),
|
|
6525
6551
|
agent: exports_external.string().optional().describe("Agent/provider name (default: claude)"),
|
|
6526
6552
|
all_agents: exports_external.boolean().optional().describe("Sync across all configured agents"),
|
|
@@ -6568,7 +6594,7 @@ server.tool("sync", "Sync tasks with an agent task list (Claude uses native task
|
|
|
6568
6594
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6569
6595
|
}
|
|
6570
6596
|
});
|
|
6571
|
-
server.tool("register_agent", "Register an agent
|
|
6597
|
+
server.tool("register_agent", "Register an agent (idempotent by name).", {
|
|
6572
6598
|
name: exports_external.string().describe("Agent name"),
|
|
6573
6599
|
description: exports_external.string().optional().describe("Agent description")
|
|
6574
6600
|
}, async ({ name, description }) => {
|
|
@@ -6728,7 +6754,7 @@ Slug: ${list.slug}`
|
|
|
6728
6754
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6729
6755
|
}
|
|
6730
6756
|
});
|
|
6731
|
-
server.tool("delete_task_list", "Delete a task list. Tasks
|
|
6757
|
+
server.tool("delete_task_list", "Delete a task list. Tasks lose association but keep data.", {
|
|
6732
6758
|
id: exports_external.string().describe("Task list ID (full or partial)")
|
|
6733
6759
|
}, async ({ id }) => {
|
|
6734
6760
|
try {
|
|
@@ -6744,7 +6770,7 @@ server.tool("delete_task_list", "Delete a task list. Tasks in this list keep the
|
|
|
6744
6770
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6745
6771
|
}
|
|
6746
6772
|
});
|
|
6747
|
-
server.tool("get_task_history", "Get
|
|
6773
|
+
server.tool("get_task_history", "Get audit log for a task.", {
|
|
6748
6774
|
task_id: exports_external.string().describe("Task ID (full or partial)")
|
|
6749
6775
|
}, async ({ task_id }) => {
|
|
6750
6776
|
try {
|
|
@@ -6761,7 +6787,7 @@ ${text}` }] };
|
|
|
6761
6787
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6762
6788
|
}
|
|
6763
6789
|
});
|
|
6764
|
-
server.tool("get_recent_activity", "Get recent task changes across all tasks
|
|
6790
|
+
server.tool("get_recent_activity", "Get recent task changes across all tasks.", {
|
|
6765
6791
|
limit: exports_external.number().optional().describe("Max entries (default 50)")
|
|
6766
6792
|
}, async ({ limit }) => {
|
|
6767
6793
|
try {
|
|
@@ -6777,7 +6803,7 @@ ${text}` }] };
|
|
|
6777
6803
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6778
6804
|
}
|
|
6779
6805
|
});
|
|
6780
|
-
server.tool("create_webhook", "Register a webhook
|
|
6806
|
+
server.tool("create_webhook", "Register a webhook to receive task change events.", {
|
|
6781
6807
|
url: exports_external.string().describe("Webhook URL"),
|
|
6782
6808
|
events: exports_external.array(exports_external.string()).optional().describe("Event types to subscribe to (empty = all)"),
|
|
6783
6809
|
secret: exports_external.string().optional().describe("HMAC secret for signature verification")
|
|
@@ -6815,7 +6841,7 @@ server.tool("delete_webhook", "Delete a webhook", {
|
|
|
6815
6841
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6816
6842
|
}
|
|
6817
6843
|
});
|
|
6818
|
-
server.tool("create_template", "Create a reusable task template", {
|
|
6844
|
+
server.tool("create_template", "Create a reusable task template.", {
|
|
6819
6845
|
name: exports_external.string().describe("Template name"),
|
|
6820
6846
|
title_pattern: exports_external.string().describe("Title pattern for tasks created from this template"),
|
|
6821
6847
|
description: exports_external.string().optional().describe("Default description"),
|
|
@@ -6846,7 +6872,7 @@ ${text}` }] };
|
|
|
6846
6872
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6847
6873
|
}
|
|
6848
6874
|
});
|
|
6849
|
-
server.tool("create_task_from_template", "Create a task from a template with optional overrides", {
|
|
6875
|
+
server.tool("create_task_from_template", "Create a task from a template with optional overrides.", {
|
|
6850
6876
|
template_id: exports_external.string().describe("Template ID"),
|
|
6851
6877
|
title: exports_external.string().optional().describe("Override title"),
|
|
6852
6878
|
description: exports_external.string().optional().describe("Override description"),
|
|
@@ -6879,7 +6905,7 @@ server.tool("delete_template", "Delete a task template", { id: exports_external.
|
|
|
6879
6905
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6880
6906
|
}
|
|
6881
6907
|
});
|
|
6882
|
-
server.tool("approve_task", "Approve a task that requires approval
|
|
6908
|
+
server.tool("approve_task", "Approve a task that requires approval.", {
|
|
6883
6909
|
id: exports_external.string().describe("Task ID (full or partial)"),
|
|
6884
6910
|
agent_id: exports_external.string().optional().describe("Agent approving the task")
|
|
6885
6911
|
}, async ({ id, agent_id }) => {
|
|
@@ -6898,6 +6924,112 @@ server.tool("approve_task", "Approve a task that requires approval before comple
|
|
|
6898
6924
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6899
6925
|
}
|
|
6900
6926
|
});
|
|
6927
|
+
server.tool("get_my_tasks", "Get assigned tasks and stats for an agent.", {
|
|
6928
|
+
agent_name: exports_external.string().describe("Your agent name")
|
|
6929
|
+
}, async ({ agent_name }) => {
|
|
6930
|
+
try {
|
|
6931
|
+
const agent = registerAgent({ name: agent_name });
|
|
6932
|
+
const tasks = listTasks({});
|
|
6933
|
+
const myTasks = tasks.filter((t) => t.assigned_to === agent_name || t.assigned_to === agent.id || t.agent_id === agent.id || t.agent_id === agent_name);
|
|
6934
|
+
const pending = myTasks.filter((t) => t.status === "pending");
|
|
6935
|
+
const inProgress = myTasks.filter((t) => t.status === "in_progress");
|
|
6936
|
+
const completed = myTasks.filter((t) => t.status === "completed");
|
|
6937
|
+
const rate = myTasks.length > 0 ? Math.round(completed.length / myTasks.length * 100) : 0;
|
|
6938
|
+
const lines = [
|
|
6939
|
+
`Agent: ${agent.name} (${agent.id})`,
|
|
6940
|
+
`Tasks: ${myTasks.length} total, ${pending.length} pending, ${inProgress.length} active, ${completed.length} done (${rate}%)`
|
|
6941
|
+
];
|
|
6942
|
+
if (pending.length > 0) {
|
|
6943
|
+
lines.push(`
|
|
6944
|
+
Pending:`);
|
|
6945
|
+
for (const t of pending.slice(0, 10))
|
|
6946
|
+
lines.push(` [${t.priority}] ${t.id.slice(0, 8)} | ${t.title}`);
|
|
6947
|
+
}
|
|
6948
|
+
if (inProgress.length > 0) {
|
|
6949
|
+
lines.push(`
|
|
6950
|
+
In Progress:`);
|
|
6951
|
+
for (const t of inProgress)
|
|
6952
|
+
lines.push(` ${t.id.slice(0, 8)} | ${t.title}`);
|
|
6953
|
+
}
|
|
6954
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
6955
|
+
`) }] };
|
|
6956
|
+
} catch (e) {
|
|
6957
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
6958
|
+
}
|
|
6959
|
+
});
|
|
6960
|
+
server.tool("search_tools", "List tool names matching a query.", { query: exports_external.string().optional().describe("Keyword to filter tools") }, async ({ query }) => {
|
|
6961
|
+
const all = [
|
|
6962
|
+
"create_task",
|
|
6963
|
+
"list_tasks",
|
|
6964
|
+
"get_task",
|
|
6965
|
+
"update_task",
|
|
6966
|
+
"delete_task",
|
|
6967
|
+
"start_task",
|
|
6968
|
+
"complete_task",
|
|
6969
|
+
"lock_task",
|
|
6970
|
+
"unlock_task",
|
|
6971
|
+
"approve_task",
|
|
6972
|
+
"add_dependency",
|
|
6973
|
+
"remove_dependency",
|
|
6974
|
+
"add_comment",
|
|
6975
|
+
"create_project",
|
|
6976
|
+
"list_projects",
|
|
6977
|
+
"create_plan",
|
|
6978
|
+
"list_plans",
|
|
6979
|
+
"get_plan",
|
|
6980
|
+
"update_plan",
|
|
6981
|
+
"delete_plan",
|
|
6982
|
+
"register_agent",
|
|
6983
|
+
"list_agents",
|
|
6984
|
+
"get_agent",
|
|
6985
|
+
"get_my_tasks",
|
|
6986
|
+
"create_task_list",
|
|
6987
|
+
"list_task_lists",
|
|
6988
|
+
"get_task_list",
|
|
6989
|
+
"update_task_list",
|
|
6990
|
+
"delete_task_list",
|
|
6991
|
+
"search_tasks",
|
|
6992
|
+
"sync",
|
|
6993
|
+
"get_task_history",
|
|
6994
|
+
"get_recent_activity",
|
|
6995
|
+
"create_webhook",
|
|
6996
|
+
"list_webhooks",
|
|
6997
|
+
"delete_webhook",
|
|
6998
|
+
"create_template",
|
|
6999
|
+
"list_templates",
|
|
7000
|
+
"create_task_from_template",
|
|
7001
|
+
"delete_template",
|
|
7002
|
+
"search_tools",
|
|
7003
|
+
"describe_tools"
|
|
7004
|
+
];
|
|
7005
|
+
const q = query?.toLowerCase();
|
|
7006
|
+
const matches = q ? all.filter((n) => n.includes(q)) : all;
|
|
7007
|
+
return { content: [{ type: "text", text: matches.join(", ") }] };
|
|
7008
|
+
});
|
|
7009
|
+
server.tool("describe_tools", "Get descriptions for specific tools by name.", { names: exports_external.array(exports_external.string()).describe("Tool names from search_tools") }, async ({ names }) => {
|
|
7010
|
+
const descriptions = {
|
|
7011
|
+
create_task: "Create a task. Params: title(req), description, priority, project_id, plan_id, tags, assigned_to, estimated_minutes, requires_approval",
|
|
7012
|
+
list_tasks: "List tasks. Params: status, priority, project_id, plan_id, assigned_to, tags, limit",
|
|
7013
|
+
get_task: "Get full task details. Params: id",
|
|
7014
|
+
update_task: "Update task fields. Params: id, version(req), title, description, status, priority, tags, assigned_to, due_at",
|
|
7015
|
+
delete_task: "Delete a task. Params: id",
|
|
7016
|
+
start_task: "Claim, lock, and start a task. Params: id",
|
|
7017
|
+
complete_task: "Mark task completed. Params: id, agent_id",
|
|
7018
|
+
approve_task: "Approve task requiring approval. Params: id, agent_id",
|
|
7019
|
+
create_plan: "Create a plan. Params: name, description, project_id, task_list_id, agent_id, status",
|
|
7020
|
+
list_plans: "List plans. Params: project_id",
|
|
7021
|
+
get_plan: "Get plan with tasks. Params: id",
|
|
7022
|
+
search_tasks: "Full-text search tasks. Params: query, project_id, task_list_id",
|
|
7023
|
+
get_my_tasks: "Get your tasks and stats. Params: agent_name",
|
|
7024
|
+
get_task_history: "Get task audit log. Params: task_id",
|
|
7025
|
+
get_recent_activity: "Recent changes across all tasks. Params: limit",
|
|
7026
|
+
create_template: "Create task template. Params: name, title_pattern, description, priority, tags",
|
|
7027
|
+
create_task_from_template: "Create task from template. Params: template_id, title, priority, assigned_to"
|
|
7028
|
+
};
|
|
7029
|
+
const result = names.map((n) => `${n}: ${descriptions[n] || "See tool schema"}`).join(`
|
|
7030
|
+
`);
|
|
7031
|
+
return { content: [{ type: "text", text: result }] };
|
|
7032
|
+
});
|
|
6901
7033
|
server.resource("tasks", "todos://tasks", { description: "All active tasks", mimeType: "application/json" }, async () => {
|
|
6902
7034
|
const tasks = listTasks({ status: ["pending", "in_progress"] });
|
|
6903
7035
|
return { contents: [{ uri: "todos://tasks", text: JSON.stringify(tasks, null, 2), mimeType: "application/json" }] };
|
package/dist/server/index.js
CHANGED
|
@@ -1240,8 +1240,26 @@ function deleteTask(id, db) {
|
|
|
1240
1240
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
1241
1241
|
return result.changes > 0;
|
|
1242
1242
|
}
|
|
1243
|
+
function getBlockingDeps(id, db) {
|
|
1244
|
+
const d = db || getDatabase();
|
|
1245
|
+
const deps = getTaskDependencies(id, d);
|
|
1246
|
+
if (deps.length === 0)
|
|
1247
|
+
return [];
|
|
1248
|
+
const blocking = [];
|
|
1249
|
+
for (const dep of deps) {
|
|
1250
|
+
const task = getTask(dep.depends_on, d);
|
|
1251
|
+
if (task && task.status !== "completed")
|
|
1252
|
+
blocking.push(task);
|
|
1253
|
+
}
|
|
1254
|
+
return blocking;
|
|
1255
|
+
}
|
|
1243
1256
|
function startTask(id, agentId, db) {
|
|
1244
1257
|
const d = db || getDatabase();
|
|
1258
|
+
const blocking = getBlockingDeps(id, d);
|
|
1259
|
+
if (blocking.length > 0) {
|
|
1260
|
+
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
1261
|
+
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
1262
|
+
}
|
|
1245
1263
|
const cutoff = lockExpiryCutoff();
|
|
1246
1264
|
const timestamp = now();
|
|
1247
1265
|
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
@@ -1257,7 +1275,7 @@ function startTask(id, agentId, db) {
|
|
|
1257
1275
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
1258
1276
|
return getTask(id, d);
|
|
1259
1277
|
}
|
|
1260
|
-
function completeTask(id, agentId, db) {
|
|
1278
|
+
function completeTask(id, agentId, db, evidence) {
|
|
1261
1279
|
const d = db || getDatabase();
|
|
1262
1280
|
const task = getTask(id, d);
|
|
1263
1281
|
if (!task)
|
|
@@ -1266,12 +1284,20 @@ function completeTask(id, agentId, db) {
|
|
|
1266
1284
|
throw new LockError(id, task.locked_by);
|
|
1267
1285
|
}
|
|
1268
1286
|
checkCompletionGuard(task, agentId || null, d);
|
|
1287
|
+
if (evidence) {
|
|
1288
|
+
const meta = { ...task.metadata, _evidence: evidence };
|
|
1289
|
+
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta), id]);
|
|
1290
|
+
}
|
|
1269
1291
|
const timestamp = now();
|
|
1270
1292
|
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
|
|
1271
1293
|
WHERE id = ?`, [timestamp, timestamp, id]);
|
|
1272
1294
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
1273
1295
|
return getTask(id, d);
|
|
1274
1296
|
}
|
|
1297
|
+
function getTaskDependencies(taskId, db) {
|
|
1298
|
+
const d = db || getDatabase();
|
|
1299
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
1300
|
+
}
|
|
1275
1301
|
|
|
1276
1302
|
// src/server/serve.ts
|
|
1277
1303
|
init_projects();
|
|
@@ -1428,6 +1454,19 @@ function taskToSummary(task) {
|
|
|
1428
1454
|
async function startServer(port, options) {
|
|
1429
1455
|
const shouldOpen = options?.open ?? true;
|
|
1430
1456
|
getDatabase();
|
|
1457
|
+
const sseClients = new Set;
|
|
1458
|
+
function broadcastEvent(event) {
|
|
1459
|
+
const data = JSON.stringify({ ...event, timestamp: new Date().toISOString() });
|
|
1460
|
+
for (const controller of sseClients) {
|
|
1461
|
+
try {
|
|
1462
|
+
controller.enqueue(`data: ${data}
|
|
1463
|
+
|
|
1464
|
+
`);
|
|
1465
|
+
} catch {
|
|
1466
|
+
sseClients.delete(controller);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1431
1470
|
const dashboardDir = resolveDashboardDir();
|
|
1432
1471
|
const dashboardExists = existsSync4(dashboardDir);
|
|
1433
1472
|
if (!dashboardExists) {
|
|
@@ -1453,6 +1492,27 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
1453
1492
|
}
|
|
1454
1493
|
});
|
|
1455
1494
|
}
|
|
1495
|
+
if (path === "/api/events" && method === "GET") {
|
|
1496
|
+
const stream = new ReadableStream({
|
|
1497
|
+
start(controller) {
|
|
1498
|
+
sseClients.add(controller);
|
|
1499
|
+
controller.enqueue(`data: ${JSON.stringify({ type: "connected", timestamp: new Date().toISOString() })}
|
|
1500
|
+
|
|
1501
|
+
`);
|
|
1502
|
+
},
|
|
1503
|
+
cancel(controller) {
|
|
1504
|
+
sseClients.delete(controller);
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
return new Response(stream, {
|
|
1508
|
+
headers: {
|
|
1509
|
+
"Content-Type": "text/event-stream",
|
|
1510
|
+
"Cache-Control": "no-cache",
|
|
1511
|
+
Connection: "keep-alive",
|
|
1512
|
+
"Access-Control-Allow-Origin": `http://localhost:${port}`
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1456
1516
|
if (path === "/api/stats" && method === "GET") {
|
|
1457
1517
|
const all = listTasks({ limit: 1e4 });
|
|
1458
1518
|
const projects = listProjects();
|
|
@@ -1490,6 +1550,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
1490
1550
|
priority: body.priority,
|
|
1491
1551
|
project_id: body.project_id
|
|
1492
1552
|
});
|
|
1553
|
+
broadcastEvent({ type: "task", task_id: task.id, action: "created", agent_id: task.agent_id });
|
|
1493
1554
|
return json(taskToSummary(task), 201, port);
|
|
1494
1555
|
} catch (e) {
|
|
1495
1556
|
return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
|
|
@@ -1592,6 +1653,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
1592
1653
|
const id = startMatch[1];
|
|
1593
1654
|
try {
|
|
1594
1655
|
const task = startTask(id, "dashboard");
|
|
1656
|
+
broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard" });
|
|
1595
1657
|
return json(taskToSummary(task), 200, port);
|
|
1596
1658
|
} catch (e) {
|
|
1597
1659
|
return json({ error: e instanceof Error ? e.message : "Failed to start task" }, 500, port);
|
|
@@ -1602,6 +1664,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
1602
1664
|
const id = completeMatch[1];
|
|
1603
1665
|
try {
|
|
1604
1666
|
const task = completeTask(id, "dashboard");
|
|
1667
|
+
broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard" });
|
|
1605
1668
|
return json(taskToSummary(task), 200, port);
|
|
1606
1669
|
} catch (e) {
|
|
1607
1670
|
return json({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500, port);
|
|
@@ -1610,6 +1673,67 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
1610
1673
|
if (path === "/api/projects" && method === "GET") {
|
|
1611
1674
|
return json(listProjects(), 200, port);
|
|
1612
1675
|
}
|
|
1676
|
+
if (path === "/api/agents/me" && method === "GET") {
|
|
1677
|
+
const name = url.searchParams.get("name");
|
|
1678
|
+
if (!name)
|
|
1679
|
+
return json({ error: "Missing name param" }, 400, port);
|
|
1680
|
+
const { registerAgent: registerAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
1681
|
+
const agent = registerAgent2({ name });
|
|
1682
|
+
const tasks = listTasks({ assigned_to: name });
|
|
1683
|
+
const agentIdTasks = listTasks({ agent_id: agent.id });
|
|
1684
|
+
const allTasks = [...tasks, ...agentIdTasks.filter((t) => !tasks.some((tt) => tt.id === t.id))];
|
|
1685
|
+
const pending = allTasks.filter((t) => t.status === "pending");
|
|
1686
|
+
const inProgress = allTasks.filter((t) => t.status === "in_progress");
|
|
1687
|
+
const completed = allTasks.filter((t) => t.status === "completed");
|
|
1688
|
+
return json({
|
|
1689
|
+
agent,
|
|
1690
|
+
pending_tasks: pending.map(taskToSummary),
|
|
1691
|
+
in_progress_tasks: inProgress.map(taskToSummary),
|
|
1692
|
+
stats: {
|
|
1693
|
+
total: allTasks.length,
|
|
1694
|
+
pending: pending.length,
|
|
1695
|
+
in_progress: inProgress.length,
|
|
1696
|
+
completed: completed.length,
|
|
1697
|
+
completion_rate: allTasks.length > 0 ? Math.round(completed.length / allTasks.length * 100) : 0
|
|
1698
|
+
}
|
|
1699
|
+
}, 200, port);
|
|
1700
|
+
}
|
|
1701
|
+
const queueMatch = path.match(/^\/api\/agents\/([^/]+)\/queue$/);
|
|
1702
|
+
if (queueMatch && method === "GET") {
|
|
1703
|
+
const agentId = decodeURIComponent(queueMatch[1]);
|
|
1704
|
+
const pending = listTasks({ status: "pending" });
|
|
1705
|
+
const queue = pending.filter((t) => t.assigned_to === agentId || t.agent_id === agentId || !t.assigned_to && !t.locked_by);
|
|
1706
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
1707
|
+
queue.sort((a, b) => (order[a.priority] ?? 4) - (order[b.priority] ?? 4) || new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
1708
|
+
return json(queue.map(taskToSummary), 200, port);
|
|
1709
|
+
}
|
|
1710
|
+
if (path === "/api/tasks/claim" && method === "POST") {
|
|
1711
|
+
try {
|
|
1712
|
+
const body = await req.json();
|
|
1713
|
+
const agentId = body.agent_id || "anonymous";
|
|
1714
|
+
const pending = listTasks({ status: "pending", project_id: body.project_id });
|
|
1715
|
+
const available = pending.filter((t) => !t.locked_by);
|
|
1716
|
+
if (available.length === 0)
|
|
1717
|
+
return json({ task: null }, 200, port);
|
|
1718
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
1719
|
+
available.sort((a, b) => (order[a.priority] ?? 4) - (order[b.priority] ?? 4));
|
|
1720
|
+
const target = available[0];
|
|
1721
|
+
try {
|
|
1722
|
+
const claimed = startTask(target.id, agentId);
|
|
1723
|
+
return json({ task: taskToSummary(claimed) }, 200, port);
|
|
1724
|
+
} catch (e) {
|
|
1725
|
+
const next = available[1] || null;
|
|
1726
|
+
return json({
|
|
1727
|
+
task: null,
|
|
1728
|
+
locked_by: target.locked_by,
|
|
1729
|
+
locked_since: target.locked_at,
|
|
1730
|
+
suggested_task: next ? taskToSummary(next) : null
|
|
1731
|
+
}, 200, port);
|
|
1732
|
+
}
|
|
1733
|
+
} catch (e) {
|
|
1734
|
+
return json({ error: e instanceof Error ? e.message : "Failed to claim" }, 500, port);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1613
1737
|
if (path === "/api/agents" && method === "GET") {
|
|
1614
1738
|
return json(listAgents(), 200, port);
|
|
1615
1739
|
}
|