@hasna/todos 0.9.21 → 0.9.23

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/README.md CHANGED
@@ -163,20 +163,20 @@ Or start manually via stdio:
163
163
  todos-mcp
164
164
  ```
165
165
 
166
- ### MCP Tools (29)
167
-
168
- | Category | Tools |
169
- |----------|-------|
170
- | **Tasks** | `create_task`, `list_tasks`, `get_task`, `update_task`, `delete_task`, `start_task`, `complete_task` |
171
- | **Locking** | `lock_task`, `unlock_task` |
172
- | **Dependencies** | `add_dependency`, `remove_dependency` |
173
- | **Comments** | `add_comment` |
174
- | **Projects** | `create_project`, `list_projects` |
175
- | **Plans** | `create_plan`, `list_plans`, `get_plan`, `update_plan`, `delete_plan` |
176
- | **Agents** | `register_agent`, `list_agents`, `get_agent` |
177
- | **Task Lists** | `create_task_list`, `list_task_lists`, `get_task_list`, `update_task_list`, `delete_task_list` |
178
- | **Search** | `search_tasks` |
179
- | **Sync** | `sync` |
166
+ ### MCP Tools (40)
167
+
168
+ **Tasks:** `create_task`, `list_tasks`, `get_task`, `update_task`, `delete_task`, `start_task`, `complete_task`, `lock_task`, `unlock_task`, `approve_task`
169
+ **Dependencies:** `add_dependency`, `remove_dependency`
170
+ **Comments:** `add_comment`
171
+ **Projects:** `create_project`, `list_projects`
172
+ **Plans:** `create_plan`, `list_plans`, `get_plan`, `update_plan`, `delete_plan`
173
+ **Agents:** `register_agent`, `list_agents`, `get_agent`
174
+ **Task Lists:** `create_task_list`, `list_task_lists`, `get_task_list`, `update_task_list`, `delete_task_list`
175
+ **Search:** `search_tasks`
176
+ **Sync:** `sync`
177
+ **Audit:** `get_task_history`, `get_recent_activity`
178
+ **Webhooks:** `create_webhook`, `list_webhooks`, `delete_webhook`
179
+ **Templates:** `create_template`, `list_templates`, `create_task_from_template`, `delete_template`
180
180
 
181
181
  ### MCP Resources
182
182
 
@@ -208,28 +208,66 @@ The dashboard auto-refreshes every 30 seconds, supports dark mode, and includes
208
208
 
209
209
  ## REST API
210
210
 
211
- When running `todos serve`, the following REST API is available:
211
+ Start the server with `todos serve` or `todos-serve`. Default port: 19427.
212
+
213
+ ### Tasks
212
214
 
213
215
  | Method | Endpoint | Description |
214
216
  |--------|----------|-------------|
215
- | GET | `/api/stats` | Dashboard statistics |
216
- | GET | `/api/tasks` | List tasks (supports `?status=`, `?project_id=`, `?limit=`) |
217
- | POST | `/api/tasks` | Create a task |
217
+ | GET | `/api/tasks` | List tasks. Query: `?status=`, `?project_id=`, `?limit=` |
218
+ | POST | `/api/tasks` | Create task. Body: `{ title, description?, priority?, project_id?, estimated_minutes?, requires_approval? }` |
218
219
  | GET | `/api/tasks/:id` | Get task details |
219
- | PATCH | `/api/tasks/:id` | Update a task |
220
- | DELETE | `/api/tasks/:id` | Delete a task |
221
- | POST | `/api/tasks/:id/start` | Start a task |
222
- | POST | `/api/tasks/:id/complete` | Complete a task |
223
- | POST | `/api/tasks/bulk` | Bulk operations (start, complete, delete) |
224
- | GET | `/api/tasks/export?format=csv` | Export tasks as CSV |
225
- | GET | `/api/tasks/export?format=json` | Export tasks as JSON |
226
- | GET | `/api/projects` | List projects |
227
- | POST | `/api/projects` | Create a project |
228
- | DELETE | `/api/projects/:id` | Delete a project |
229
- | GET | `/api/agents` | List agents |
230
- | POST | `/api/agents` | Register an agent |
231
- | PATCH | `/api/agents/:id` | Update an agent |
232
- | DELETE | `/api/agents/:id` | Delete an agent |
220
+ | PATCH | `/api/tasks/:id` | Update task. Body: `{ title?, status?, priority?, description?, assigned_to?, tags?, due_at?, estimated_minutes?, requires_approval?, approved_by? }` |
221
+ | DELETE | `/api/tasks/:id` | Delete task |
222
+ | POST | `/api/tasks/:id/start` | Start task (sets in_progress, locks) |
223
+ | POST | `/api/tasks/:id/complete` | Complete task |
224
+ | GET | `/api/tasks/:id/history` | Get task audit log |
225
+ | POST | `/api/tasks/bulk` | Bulk ops. Body: `{ ids: [...], action: "start" | "complete" | "delete" }` |
226
+ | GET | `/api/tasks/export?format=csv` | Export as CSV |
227
+ | GET | `/api/tasks/export?format=json` | Export as JSON |
228
+
229
+ ### Projects
230
+
231
+ | Method | Endpoint | Description |
232
+ |--------|----------|-------------|
233
+ | GET | `/api/projects` | List all projects |
234
+ | POST | `/api/projects` | Create project. Body: `{ name, path, description? }` |
235
+ | DELETE | `/api/projects/:id` | Delete project |
236
+ | POST | `/api/projects/bulk` | Bulk delete. Body: `{ ids: [...], action: "delete" }` |
237
+
238
+ ### Plans
239
+
240
+ | Method | Endpoint | Description |
241
+ |--------|----------|-------------|
242
+ | GET | `/api/plans` | List plans. Query: `?project_id=` |
243
+ | POST | `/api/plans` | Create plan. Body: `{ name, description?, project_id?, task_list_id?, agent_id?, status? }` |
244
+ | GET | `/api/plans/:id` | Get plan with its tasks |
245
+ | PATCH | `/api/plans/:id` | Update plan |
246
+ | DELETE | `/api/plans/:id` | Delete plan |
247
+ | POST | `/api/plans/bulk` | Bulk delete |
248
+
249
+ ### Agents
250
+
251
+ | Method | Endpoint | Description |
252
+ |--------|----------|-------------|
253
+ | GET | `/api/agents` | List all agents |
254
+ | POST | `/api/agents` | Register agent. Body: `{ name, description?, role? }` |
255
+ | PATCH | `/api/agents/:id` | Update agent. Body: `{ name?, description?, role? }` |
256
+ | DELETE | `/api/agents/:id` | Delete agent |
257
+ | POST | `/api/agents/bulk` | Bulk delete |
258
+
259
+ ### Webhooks, Templates, Activity
260
+
261
+ | Method | Endpoint | Description |
262
+ |--------|----------|-------------|
263
+ | GET | `/api/webhooks` | List webhooks |
264
+ | POST | `/api/webhooks` | Create webhook. Body: `{ url, events?, secret? }` |
265
+ | DELETE | `/api/webhooks/:id` | Delete webhook |
266
+ | GET | `/api/templates` | List task templates |
267
+ | POST | `/api/templates` | Create template. Body: `{ name, title_pattern, description?, priority?, tags? }` |
268
+ | DELETE | `/api/templates/:id` | Delete template |
269
+ | GET | `/api/activity` | Recent audit log. Query: `?limit=50` |
270
+ | GET | `/api/stats` | Dashboard statistics |
233
271
 
234
272
  ## Sync
235
273
 
@@ -274,50 +312,48 @@ Claude uses native Claude Code task lists. Other agents use JSON files under `~/
274
312
  }
275
313
  ```
276
314
 
277
- ## CLI Commands
278
-
279
- ### Task Operations
280
-
281
- | Command | Description |
282
- |---------|-------------|
283
- | `todos add <title>` | Create a task (`-p` priority, `--tags`, `--list`, `--plan`, `--assign`, `--parent`) |
284
- | `todos list` | List tasks (`-s` status, `-p` priority, `--list`, `--tags`, `-a` all) |
285
- | `todos show <id>` | Show full task details with relations |
286
- | `todos update <id>` | Update fields (`--title`, `-s`, `-p`, `--tags`, `--list`, `--assign`) |
287
- | `todos start <id>` | Claim task, lock it, set to in_progress |
288
- | `todos done <id>` | Mark task completed, release lock |
289
- | `todos delete <id>` | Delete permanently |
290
- | `todos lock <id>` | Acquire exclusive lock |
291
- | `todos unlock <id>` | Release lock |
292
-
293
- ### Organization
294
-
295
- | Command | Description |
296
- |---------|-------------|
297
- | `todos lists` | List task lists (`--add`, `--delete`, `--slug`, `-d`) |
298
- | `todos plans` | List plans (`--add`, `--show`, `--delete`, `--complete`) |
299
- | `todos projects` | List projects (`--add`, `--name`, `--task-list-id`) |
300
- | `todos deps <id>` | Manage dependencies (`--needs`, `--remove`) |
301
- | `todos comment <id> <text>` | Add a comment to a task |
302
- | `todos search <query>` | Full-text search across tasks |
303
-
304
- ### Agent & System
305
-
306
- | Command | Description |
307
- |---------|-------------|
308
- | `todos init <name>` | Register agent, get short UUID (`-d` description) |
309
- | `todos agents` | List registered agents |
310
- | `todos sync` | Sync with agent task lists |
311
- | `todos mcp` | MCP server (`--register`, `--unregister`) |
312
- | `todos hooks install` | Install Claude Code auto-sync hooks |
313
- | `todos export` | Export tasks (`-f json\|md`) |
314
- | `todos upgrade` | Self-update to latest version |
315
-
316
- ### Global Options
317
-
318
- All commands support: `--project <path>`, `--json`, `--agent <name>`, `--session <id>`.
319
-
320
- Partial IDs work everywhere — use the first 8+ characters of any UUID.
315
+ ## CLI Reference
316
+
317
+ ```bash
318
+ # Task management
319
+ todos add "title" [-d desc] [-p priority] [--tags t1,t2] [--plan id] [--estimated 30] [--approval]
320
+ todos list [-s status] [-p priority] [--assigned agent] [--project-name name] [--agent-name name] [--sort field] [-a]
321
+ todos show <id> # Full task details with relations
322
+ todos update <id> [--title t] [-s status] [-p priority] [--tags t1,t2] [--estimated 30]
323
+ todos done <id> # Complete a task
324
+ todos start <id> # Claim, lock, and start
325
+ todos delete <id>
326
+ todos approve <id> # Approve a task requiring approval
327
+ todos history <id> # Show task audit log
328
+ todos search <query>
329
+ todos bulk <done|start|delete> <id1> <id2> ...
330
+ todos comment <id> <text>
331
+ todos deps <id> --add <dep_id> # Manage dependencies
332
+
333
+ # Plans
334
+ todos plans [--add name] [--show id] [--delete id] [--complete id]
335
+
336
+ # Templates
337
+ todos templates [--add name --title pattern] [--delete id] [--use id]
338
+
339
+ # Projects & Agents
340
+ todos projects [--add name --path /path]
341
+ todos agents
342
+ todos init <name> # Register an agent
343
+ todos lists # Manage task lists
344
+
345
+ # Utilities
346
+ todos count [--json] # Quick stats
347
+ todos watch [-s status] [-i 5] # Live-updating task list
348
+ todos config [--get key] [--set key=value]
349
+ todos export [--format csv|json]
350
+ todos sync [--task-list id] # Sync with Claude Code
351
+ todos serve [--port 19427] # Start web dashboard
352
+ todos interactive # Launch TUI
353
+ todos upgrade # Update to latest version
354
+ ```
355
+
356
+ All output supports `--json` for machine-readable format.
321
357
 
322
358
  ## Library Usage
323
359
 
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];
@@ -6898,6 +6924,39 @@ 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 your assigned tasks and stats. Auto-registers if needed.", {
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
+ });
6901
6960
  server.resource("tasks", "todos://tasks", { description: "All active tasks", mimeType: "application/json" }, async () => {
6902
6961
  const tasks = listTasks({ status: ["pending", "in_progress"] });
6903
6962
  return { contents: [{ uri: "todos://tasks", text: JSON.stringify(tasks, null, 2), mimeType: "application/json" }] };
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.9.21",
3
+ "version": "0.9.23",
4
4
  "description": "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",