@hasna/todos 0.11.27 → 0.11.29

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/mcp/index.js CHANGED
@@ -4,37 +4,27 @@ var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- function __accessProp(key) {
8
- return this[key];
9
- }
7
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
10
8
  var __toCommonJS = (from) => {
11
- var entry = (__moduleCache ??= new WeakMap).get(from), desc;
9
+ var entry = __moduleCache.get(from), desc;
12
10
  if (entry)
13
11
  return entry;
14
12
  entry = __defProp({}, "__esModule", { value: true });
15
- if (from && typeof from === "object" || typeof from === "function") {
16
- for (var key of __getOwnPropNames(from))
17
- if (!__hasOwnProp.call(entry, key))
18
- __defProp(entry, key, {
19
- get: __accessProp.bind(from, key),
20
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
21
- });
22
- }
13
+ if (from && typeof from === "object" || typeof from === "function")
14
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
15
+ get: () => from[key],
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ }));
23
18
  __moduleCache.set(from, entry);
24
19
  return entry;
25
20
  };
26
- var __moduleCache;
27
- var __returnValue = (v) => v;
28
- function __exportSetter(name, newValue) {
29
- this[name] = __returnValue.bind(null, newValue);
30
- }
31
21
  var __export = (target, all) => {
32
22
  for (var name in all)
33
23
  __defProp(target, name, {
34
24
  get: all[name],
35
25
  enumerable: true,
36
26
  configurable: true,
37
- set: __exportSetter.bind(all, name)
27
+ set: (newValue) => all[name] = () => newValue
38
28
  });
39
29
  };
40
30
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -384,6 +374,32 @@ function ensureSchema(db) {
384
374
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_synced ON tasks(synced_at)");
385
375
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_machine ON projects(machine_id)");
386
376
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_machine ON agents(machine_id)");
377
+ ensureTable("task_time_logs", `
378
+ CREATE TABLE task_time_logs (
379
+ id TEXT PRIMARY KEY,
380
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
381
+ agent_id TEXT,
382
+ started_at TEXT,
383
+ ended_at TEXT,
384
+ minutes INTEGER NOT NULL,
385
+ notes TEXT,
386
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
387
+ )`);
388
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_task ON task_time_logs(task_id)");
389
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_agent ON task_time_logs(agent_id)");
390
+ ensureColumn("tasks", "actual_minutes", "INTEGER");
391
+ ensureTable("task_watchers", `
392
+ CREATE TABLE task_watchers (
393
+ id TEXT PRIMARY KEY,
394
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
395
+ agent_id TEXT NOT NULL,
396
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
397
+ UNIQUE(task_id, agent_id)
398
+ )`);
399
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_task ON task_watchers(task_id)");
400
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_agent ON task_watchers(agent_id)");
401
+ ensureColumn("task_dependencies", "external_project_id", "TEXT");
402
+ ensureColumn("task_dependencies", "external_task_id", "TEXT");
387
403
  }
388
404
  function backfillTaskTags(db) {
389
405
  try {
@@ -1111,6 +1127,39 @@ var init_schema = __esm(() => {
1111
1127
  CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id, machine_id) WHERE short_id IS NOT NULL AND machine_id IS NOT NULL;
1112
1128
  CREATE INDEX IF NOT EXISTS idx_tasks_short_id_lookup ON tasks(short_id) WHERE short_id IS NOT NULL;
1113
1129
  INSERT OR IGNORE INTO _migrations (id) VALUES (44);
1130
+ `,
1131
+ `
1132
+ CREATE TABLE IF NOT EXISTS task_time_logs (
1133
+ id TEXT PRIMARY KEY,
1134
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1135
+ agent_id TEXT,
1136
+ started_at TEXT,
1137
+ ended_at TEXT,
1138
+ minutes INTEGER NOT NULL,
1139
+ notes TEXT,
1140
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1141
+ );
1142
+ CREATE INDEX IF NOT EXISTS idx_task_time_logs_task ON task_time_logs(task_id);
1143
+ CREATE INDEX IF NOT EXISTS idx_task_time_logs_agent ON task_time_logs(agent_id);
1144
+ ALTER TABLE tasks ADD COLUMN actual_minutes INTEGER;
1145
+ INSERT OR IGNORE INTO _migrations (id) VALUES (45);
1146
+ `,
1147
+ `
1148
+ CREATE TABLE IF NOT EXISTS task_watchers (
1149
+ id TEXT PRIMARY KEY,
1150
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1151
+ agent_id TEXT NOT NULL,
1152
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1153
+ UNIQUE(task_id, agent_id)
1154
+ );
1155
+ CREATE INDEX IF NOT EXISTS idx_task_watchers_task ON task_watchers(task_id);
1156
+ CREATE INDEX IF NOT EXISTS idx_task_watchers_agent ON task_watchers(agent_id);
1157
+ INSERT OR IGNORE INTO _migrations (id) VALUES (46);
1158
+ `,
1159
+ `
1160
+ ALTER TABLE task_dependencies ADD COLUMN external_project_id TEXT;
1161
+ ALTER TABLE task_dependencies ADD COLUMN external_task_id TEXT;
1162
+ INSERT OR IGNORE INTO _migrations (id) VALUES (47);
1114
1163
  `
1115
1164
  ];
1116
1165
  });
@@ -1341,7 +1390,7 @@ var init_database = __esm(() => {
1341
1390
  init_machines();
1342
1391
  });
1343
1392
 
1344
- // node_modules/@hasna/cloud/dist/index.js
1393
+ // ../../../../node_modules/@hasna/cloud/dist/index.js
1345
1394
  var exports_dist = {};
1346
1395
  __export(exports_dist, {
1347
1396
  translateSql: () => translateSql,
@@ -1436,11 +1485,11 @@ import { join as join5 } from "path";
1436
1485
  import { join as join6, dirname as dirname2 } from "path";
1437
1486
  import { existsSync as existsSync6, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
1438
1487
  import { homedir as homedir5, platform } from "os";
1439
- function __accessProp2(key) {
1488
+ function __accessProp(key) {
1440
1489
  return this[key];
1441
1490
  }
1442
- function __exportSetter2(name, newValue) {
1443
- this[name] = __returnValue2.bind(null, newValue);
1491
+ function __exportSetter(name, newValue) {
1492
+ this[name] = __returnValue.bind(null, newValue);
1444
1493
  }
1445
1494
  function translateSql(sql, dialect) {
1446
1495
  if (dialect === "sqlite")
@@ -3897,7 +3946,7 @@ async function ensureAllPgDatabases() {
3897
3946
  }
3898
3947
  return results;
3899
3948
  }
3900
- function registerCloudTools(server, serviceName) {
3949
+ function registerCloudTools(server, serviceName, opts = {}) {
3901
3950
  server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
3902
3951
  const config = getCloudConfig();
3903
3952
  const lines = [
@@ -3930,8 +3979,13 @@ function registerCloudTools(server, serviceName) {
3930
3979
  isError: true
3931
3980
  };
3932
3981
  }
3933
- const local = new SqliteAdapter(getDbPath2(serviceName));
3982
+ const local = new SqliteAdapter(opts.dbPath ?? getDbPath2(serviceName));
3934
3983
  const cloud = new PgAdapterAsync(getConnectionString(serviceName));
3984
+ if (opts.migrations?.length) {
3985
+ for (const sql of opts.migrations) {
3986
+ await cloud.run(sql);
3987
+ }
3988
+ }
3935
3989
  const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables(local);
3936
3990
  const results = await syncPush(local, cloud, { tables: tableList });
3937
3991
  local.close();
@@ -3953,7 +4007,7 @@ function registerCloudTools(server, serviceName) {
3953
4007
  isError: true
3954
4008
  };
3955
4009
  }
3956
- const local = new SqliteAdapter(getDbPath2(serviceName));
4010
+ const local = new SqliteAdapter(opts.dbPath ?? getDbPath2(serviceName));
3957
4011
  const cloud = new PgAdapterAsync(getConnectionString(serviceName));
3958
4012
  let tableList;
3959
4013
  if (tablesStr) {
@@ -4089,19 +4143,19 @@ var __create, __getProtoOf, __defProp2, __getOwnPropNames2, __hasOwnProp2, __toE
4089
4143
  for (let key of __getOwnPropNames2(mod))
4090
4144
  if (!__hasOwnProp2.call(to, key))
4091
4145
  __defProp2(to, key, {
4092
- get: __accessProp2.bind(mod, key),
4146
+ get: __accessProp.bind(mod, key),
4093
4147
  enumerable: true
4094
4148
  });
4095
4149
  if (canCache)
4096
4150
  cache.set(mod, to);
4097
4151
  return to;
4098
- }, __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports), __returnValue2 = (v) => v, __export2 = (target, all) => {
4152
+ }, __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports), __returnValue = (v) => v, __export2 = (target, all) => {
4099
4153
  for (var name in all)
4100
4154
  __defProp2(target, name, {
4101
4155
  get: all[name],
4102
4156
  enumerable: true,
4103
4157
  configurable: true,
4104
- set: __exportSetter2.bind(all, name)
4158
+ set: __exportSetter.bind(all, name)
4105
4159
  });
4106
4160
  }, __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res), __require, require_postgres_array, require_arrayParser, require_postgres_date, require_mutable, require_postgres_interval, require_postgres_bytea, require_textParsers, require_pg_int8, require_binaryParsers, require_builtins, require_pg_types, require_defaults, require_utils, require_utils_legacy, require_utils_webcrypto, require_utils2, require_cert_signatures, require_sasl, require_type_overrides, require_pg_connection_string, require_connection_parameters, require_result, require_query, require_messages, require_buffer_writer, require_serializer, require_buffer_reader, require_parser, require_dist, require_empty, require_stream, require_connection, require_split2, require_helper, require_lib, require_client, require_pg_pool, require_query2, require_client2, require_lib2, import_lib, Client, Pool, Connection, types2, Query, DatabaseError, escapeIdentifier, escapeLiteral, Result, TypeOverrides, defaults, esm_default, init_esm, init_adapter, util3, objectUtil2, ZodParsedType2, getParsedType2 = (data) => {
4107
4161
  const t = typeof data;
@@ -13018,6 +13072,7 @@ var init_recurrence = __esm(() => {
13018
13072
  // src/db/webhooks.ts
13019
13073
  var exports_webhooks = {};
13020
13074
  __export(exports_webhooks, {
13075
+ validateWebhookUrl: () => validateWebhookUrl,
13021
13076
  listWebhooks: () => listWebhooks,
13022
13077
  listDeliveries: () => listDeliveries,
13023
13078
  getWebhook: () => getWebhook,
@@ -13025,6 +13080,40 @@ __export(exports_webhooks, {
13025
13080
  deleteWebhook: () => deleteWebhook,
13026
13081
  createWebhook: () => createWebhook
13027
13082
  });
13083
+ function validateWebhookUrl(urlString) {
13084
+ try {
13085
+ const url = new URL(urlString);
13086
+ if (url.protocol !== "https:") {
13087
+ throw new Error("Webhook URLs must use HTTPS");
13088
+ }
13089
+ const hostname2 = url.hostname.toLowerCase();
13090
+ if (hostname2 === "localhost" || hostname2 === "127.0.0.1" || hostname2 === "::1" || hostname2 === "0.0.0.0") {
13091
+ throw new Error("Webhook URLs cannot target localhost");
13092
+ }
13093
+ if (hostname2 === "169.254.169.254" || hostname2.startsWith("169.254.")) {
13094
+ throw new Error("Webhook URLs cannot target cloud metadata endpoints");
13095
+ }
13096
+ const privateRanges = [
13097
+ /^10\./,
13098
+ /^172\.(1[6-9]|2\d|3[01])\./,
13099
+ /^192\.168\./,
13100
+ /^127\./,
13101
+ /^169\.254\./,
13102
+ /^fc00:/i,
13103
+ /^fe80:/i
13104
+ ];
13105
+ for (const range of privateRanges) {
13106
+ if (range.test(hostname2)) {
13107
+ throw new Error("Webhook URLs cannot target private IP ranges");
13108
+ }
13109
+ }
13110
+ } catch (e) {
13111
+ if (e instanceof Error && e.message.startsWith("Webhook URLs")) {
13112
+ throw e;
13113
+ }
13114
+ throw new Error(`Invalid webhook URL: ${urlString}`);
13115
+ }
13116
+ }
13028
13117
  function rowToWebhook(row) {
13029
13118
  return {
13030
13119
  ...row,
@@ -13038,6 +13127,7 @@ function rowToWebhook(row) {
13038
13127
  }
13039
13128
  function createWebhook(input, db) {
13040
13129
  const d = db || getDatabase();
13130
+ validateWebhookUrl(input.url);
13041
13131
  const id = uuid();
13042
13132
  d.run(`INSERT INTO webhooks (id, url, events, secret, project_id, task_list_id, agent_id, task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
13043
13133
  id,
@@ -13636,7 +13726,9 @@ var init_checklists = __esm(() => {
13636
13726
  // src/db/tasks.ts
13637
13727
  var exports_tasks = {};
13638
13728
  __export(exports_tasks, {
13729
+ watchTask: () => watchTask,
13639
13730
  updateTask: () => updateTask,
13731
+ unwatchTask: () => unwatchTask,
13640
13732
  unlockTask: () => unlockTask,
13641
13733
  unarchiveTask: () => unarchiveTask,
13642
13734
  stealTask: () => stealTask,
@@ -13645,12 +13737,17 @@ __export(exports_tasks, {
13645
13737
  setTaskPriority: () => setTaskPriority,
13646
13738
  removeDependency: () => removeDependency,
13647
13739
  redistributeStaleTasks: () => redistributeStaleTasks,
13740
+ notifyWatchers: () => notifyWatchers,
13648
13741
  moveTask: () => moveTask,
13742
+ logTime: () => logTime,
13649
13743
  logCost: () => logCost,
13650
13744
  lockTask: () => lockTask,
13651
13745
  listTasks: () => listTasks,
13746
+ getTimeReport: () => getTimeReport,
13747
+ getTimeLogs: () => getTimeLogs,
13652
13748
  getTasksChangedSince: () => getTasksChangedSince,
13653
13749
  getTaskWithRelations: () => getTaskWithRelations,
13750
+ getTaskWatchers: () => getTaskWatchers,
13654
13751
  getTaskStats: () => getTaskStats,
13655
13752
  getTaskGraph: () => getTaskGraph,
13656
13753
  getTaskDependents: () => getTaskDependents,
@@ -14833,6 +14930,58 @@ function getOverdueTasks(projectId, db) {
14833
14930
  const rows = d.query(query).all(...params);
14834
14931
  return rows.map(rowToTask);
14835
14932
  }
14933
+ function logTime(input, db) {
14934
+ const d = db || getDatabase();
14935
+ const id = uuid();
14936
+ const ts = now();
14937
+ d.run(`INSERT INTO task_time_logs (id, task_id, agent_id, minutes, started_at, ended_at, notes, created_at)
14938
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.agent_id || null, input.minutes, input.started_at || null, input.ended_at || null, input.notes || null, ts]);
14939
+ return { id, task_id: input.task_id, agent_id: input.agent_id || null, minutes: input.minutes, started_at: input.started_at || null, ended_at: input.ended_at || null, notes: input.notes || null, created_at: ts };
14940
+ }
14941
+ function getTimeLogs(taskId, db) {
14942
+ const d = db || getDatabase();
14943
+ return d.query(`SELECT * FROM task_time_logs WHERE task_id = ? ORDER BY created_at DESC`).all(taskId);
14944
+ }
14945
+ function getTimeReport(opts, db) {
14946
+ const d = db || getDatabase();
14947
+ let query = `SELECT t.id as task_id, t.title, t.estimated_minutes, t.actual_minutes FROM tasks t WHERE t.status = 'completed'`;
14948
+ const params = [];
14949
+ if (opts?.project_id) {
14950
+ query += ` AND t.project_id = ?`;
14951
+ params.push(opts.project_id);
14952
+ }
14953
+ if (opts?.agent_id) {
14954
+ query += ` AND t.assigned_to = ?`;
14955
+ params.push(opts.agent_id);
14956
+ }
14957
+ if (opts?.since) {
14958
+ query += ` AND t.completed_at >= ?`;
14959
+ params.push(opts.since);
14960
+ }
14961
+ const rows = d.query(query).all(...params);
14962
+ return rows.map((row) => ({ ...row, time_logs: getTimeLogs(row.task_id, d) }));
14963
+ }
14964
+ function watchTask(taskId, agentId, db) {
14965
+ const d = db || getDatabase();
14966
+ const id = uuid();
14967
+ const ts = now();
14968
+ d.run(`INSERT OR IGNORE INTO task_watchers (id, task_id, agent_id, created_at) VALUES (?, ?, ?, ?)`, [id, taskId, agentId, ts]);
14969
+ const existing = d.query(`SELECT * FROM task_watchers WHERE task_id = ? AND agent_id = ?`).get(taskId, agentId);
14970
+ return existing;
14971
+ }
14972
+ function unwatchTask(taskId, agentId, db) {
14973
+ const d = db || getDatabase();
14974
+ const result = d.run(`DELETE FROM task_watchers WHERE task_id = ? AND agent_id = ?`, [taskId, agentId]);
14975
+ return result.changes > 0;
14976
+ }
14977
+ function getTaskWatchers(taskId, db) {
14978
+ const d = db || getDatabase();
14979
+ return d.query(`SELECT * FROM task_watchers WHERE task_id = ?`).all(taskId);
14980
+ }
14981
+ function notifyWatchers(taskId, event, data, db) {
14982
+ const watchers = getTaskWatchers(taskId, db);
14983
+ dispatchWebhook(`task.watcher.${event}`, { task_id: taskId, watchers: watchers.map((w) => w.agent_id), ...data }, db).catch(() => {});
14984
+ }
14836
14985
  var init_tasks = __esm(() => {
14837
14986
  init_types2();
14838
14987
  init_database();
@@ -14919,6 +15068,9 @@ function registerAgent(input, db) {
14919
15068
  if (callerHasNoSession && existingHasActiveSession) {
14920
15069
  return buildConflictError(existing, lastSeenMs, input.pool, d);
14921
15070
  }
15071
+ if (input.project_id && existing.session_id && isActive && existing.active_project_id && existing.active_project_id !== input.project_id) {
15072
+ return buildConflictError(existing, lastSeenMs, input.pool, d);
15073
+ }
14922
15074
  }
14923
15075
  const updates = ["last_seen_at = ?", "status = 'active'"];
14924
15076
  const params = [now()];
@@ -14934,14 +15086,18 @@ function registerAgent(input, db) {
14934
15086
  updates.push("description = ?");
14935
15087
  params.push(input.description);
14936
15088
  }
15089
+ if (input.project_id && input.session_id) {
15090
+ updates.push("active_project_id = ?");
15091
+ params.push(input.project_id);
15092
+ }
14937
15093
  params.push(existing.id);
14938
15094
  d.run(`UPDATE agents SET ${updates.join(", ")} WHERE id = ?`, params);
14939
15095
  return getAgent(existing.id, d);
14940
15096
  }
14941
15097
  const id = shortUuid();
14942
15098
  const timestamp = now();
14943
- d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir)
14944
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
15099
+ d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir, active_project_id)
15100
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
14945
15101
  id,
14946
15102
  normalizedName,
14947
15103
  input.description || null,
@@ -14956,7 +15112,8 @@ function registerAgent(input, db) {
14956
15112
  timestamp,
14957
15113
  timestamp,
14958
15114
  input.session_id || null,
14959
- input.working_dir || null
15115
+ input.working_dir || null,
15116
+ input.project_id && input.session_id ? input.project_id : null
14960
15117
  ]);
14961
15118
  return getAgent(id, d);
14962
15119
  }
@@ -17825,6 +17982,39 @@ var init_pg_migrations = __esm(() => {
17825
17982
  CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id, machine_id) WHERE short_id IS NOT NULL AND machine_id IS NOT NULL;
17826
17983
  CREATE INDEX IF NOT EXISTS idx_tasks_short_id_lookup ON tasks(short_id) WHERE short_id IS NOT NULL;
17827
17984
  INSERT INTO _migrations (id) VALUES (40) ON CONFLICT DO NOTHING;
17985
+ `,
17986
+ `
17987
+ CREATE TABLE IF NOT EXISTS task_time_logs (
17988
+ id TEXT PRIMARY KEY,
17989
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
17990
+ agent_id TEXT,
17991
+ started_at TIMESTAMPTZ,
17992
+ ended_at TIMESTAMPTZ,
17993
+ minutes INTEGER NOT NULL,
17994
+ notes TEXT,
17995
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
17996
+ );
17997
+ CREATE INDEX IF NOT EXISTS idx_task_time_logs_task ON task_time_logs(task_id);
17998
+ CREATE INDEX IF NOT EXISTS idx_task_time_logs_agent ON task_time_logs(agent_id);
17999
+ ALTER TABLE tasks ADD COLUMN actual_minutes INTEGER;
18000
+ INSERT INTO _migrations (id) VALUES (41) ON CONFLICT DO NOTHING;
18001
+ `,
18002
+ `
18003
+ CREATE TABLE IF NOT EXISTS task_watchers (
18004
+ id TEXT PRIMARY KEY,
18005
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
18006
+ agent_id TEXT NOT NULL,
18007
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
18008
+ UNIQUE(task_id, agent_id)
18009
+ );
18010
+ CREATE INDEX IF NOT EXISTS idx_task_watchers_task ON task_watchers(task_id);
18011
+ CREATE INDEX IF NOT EXISTS idx_task_watchers_agent ON task_watchers(agent_id);
18012
+ INSERT INTO _migrations (id) VALUES (42) ON CONFLICT DO NOTHING;
18013
+ `,
18014
+ `
18015
+ ALTER TABLE task_dependencies ADD COLUMN external_project_id TEXT;
18016
+ ALTER TABLE task_dependencies ADD COLUMN external_task_id TEXT;
18017
+ INSERT INTO _migrations (id) VALUES (43) ON CONFLICT DO NOTHING;
17828
18018
  `
17829
18019
  ];
17830
18020
  });
@@ -23404,7 +23594,8 @@ function registerWebhookTools(server, { shouldRegisterTool, formatError }) {
23404
23594
  task_id: exports_external.string().optional().describe("Only fire for events on this specific task")
23405
23595
  }, async (params) => {
23406
23596
  try {
23407
- const { createWebhook: createWebhook2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
23597
+ const { createWebhook: createWebhook2, validateWebhookUrl: validateWebhookUrl2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
23598
+ validateWebhookUrl2(params.url);
23408
23599
  const wh = createWebhook2(params);
23409
23600
  const scope = [wh.project_id && `project:${wh.project_id}`, wh.task_list_id && `list:${wh.task_list_id}`, wh.agent_id && `agent:${wh.agent_id}`, wh.task_id && `task:${wh.task_id}`].filter(Boolean).join(", ");
23410
23601
  return { content: [{ type: "text", text: `Webhook created: ${wh.id.slice(0, 8)} | ${wh.url} | events: ${wh.events.length === 0 ? "all" : wh.events.join(",")}${scope ? ` | scope: ${scope}` : ""}` }] };
@@ -25688,7 +25879,7 @@ Blocks:
25688
25879
  }
25689
25880
  if (shouldRegisterTool("bulk_create_tasks")) {
25690
25881
  server.tool("bulk_create_tasks", "Create multiple tasks atomically with dependency support.", {
25691
- tasks: exports_external.array(exports_external.object({
25882
+ tasks: exports_external.preprocess((val) => typeof val === "string" ? JSON.parse(val) : val, exports_external.array(exports_external.object({
25692
25883
  temp_id: exports_external.string().optional(),
25693
25884
  title: exports_external.string(),
25694
25885
  description: exports_external.string().optional(),
@@ -25702,7 +25893,7 @@ if (shouldRegisterTool("bulk_create_tasks")) {
25702
25893
  tags: exports_external.array(exports_external.string()).optional(),
25703
25894
  estimated_minutes: exports_external.number().optional(),
25704
25895
  depends_on_temp_ids: exports_external.array(exports_external.string()).optional()
25705
- })),
25896
+ }))),
25706
25897
  project_id: exports_external.string().optional(),
25707
25898
  plan_id: exports_external.string().optional(),
25708
25899
  task_list_id: exports_external.string().optional()
@@ -27438,6 +27629,143 @@ if (shouldRegisterTool("score_task")) {
27438
27629
  }
27439
27630
  });
27440
27631
  }
27632
+ if (shouldRegisterTool("log_time")) {
27633
+ server.tool("log_time", "Log time spent on a task.", {
27634
+ task_id: exports_external.string().describe("Task ID to log time against"),
27635
+ minutes: exports_external.number().min(1).describe("Minutes spent"),
27636
+ agent_id: exports_external.string().optional().describe("Agent logging the time"),
27637
+ started_at: exports_external.string().optional().describe("ISO timestamp when work started"),
27638
+ ended_at: exports_external.string().optional().describe("ISO timestamp when work ended"),
27639
+ notes: exports_external.string().optional().describe("Notes about what was done")
27640
+ }, async ({ task_id, minutes, agent_id, started_at, ended_at, notes }) => {
27641
+ try {
27642
+ const { logTime: logTime2 } = (init_tasks(), __toCommonJS(exports_tasks));
27643
+ logTime2({ task_id: resolveId(task_id), minutes, agent_id, started_at, ended_at, notes });
27644
+ return { content: [{ type: "text", text: `Logged ${minutes} min on task ${task_id.slice(0, 8)}` }] };
27645
+ } catch (e) {
27646
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
27647
+ }
27648
+ });
27649
+ }
27650
+ if (shouldRegisterTool("get_time_report")) {
27651
+ server.tool("get_time_report", "Get time tracking report: actual vs estimated minutes for completed tasks.", {
27652
+ project_id: exports_external.string().optional().describe("Filter by project"),
27653
+ agent_id: exports_external.string().optional().describe("Filter by assignee"),
27654
+ since: exports_external.string().optional().describe("ISO date \u2014 only tasks completed after this date")
27655
+ }, async ({ project_id, agent_id, since }) => {
27656
+ try {
27657
+ const { getTimeReport: getTimeReport2 } = (init_tasks(), __toCommonJS(exports_tasks));
27658
+ const report = getTimeReport2({ project_id: project_id ? resolveId(project_id, "projects") : undefined, agent_id, since });
27659
+ if (report.length === 0)
27660
+ return { content: [{ type: "text", text: "No completed tasks found." }] };
27661
+ const lines = report.map((r) => {
27662
+ const est = r.estimated_minutes ?? "?";
27663
+ const actual = r.actual_minutes ?? "?";
27664
+ const diff = r.estimated_minutes != null && r.actual_minutes != null ? ` (${r.actual_minutes - r.estimated_minutes >= 0 ? "+" : ""}${r.actual_minutes - r.estimated_minutes})` : "";
27665
+ return `${r.title.slice(0, 50)}: estimated ${est}min, actual ${actual}min${diff}`;
27666
+ });
27667
+ return { content: [{ type: "text", text: `Time Report:
27668
+ ${lines.join(`
27669
+ `)}` }] };
27670
+ } catch (e) {
27671
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
27672
+ }
27673
+ });
27674
+ }
27675
+ if (shouldRegisterTool("watch_task")) {
27676
+ server.tool("watch_task", "Subscribe to notifications for a task.", {
27677
+ task_id: exports_external.string().describe("Task ID to watch"),
27678
+ agent_id: exports_external.string().optional().describe("Agent subscribing (defaults to context agent)")
27679
+ }, async ({ task_id, agent_id }) => {
27680
+ try {
27681
+ const { watchTask: watchTask2 } = (init_tasks(), __toCommonJS(exports_tasks));
27682
+ watchTask2(resolveId(task_id), agent_id || "");
27683
+ return { content: [{ type: "text", text: `Now watching task ${task_id.slice(0, 8)}` }] };
27684
+ } catch (e) {
27685
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
27686
+ }
27687
+ });
27688
+ }
27689
+ if (shouldRegisterTool("unwatch_task")) {
27690
+ server.tool("unwatch_task", "Unsubscribe from notifications for a task.", {
27691
+ task_id: exports_external.string().describe("Task ID to unwatch"),
27692
+ agent_id: exports_external.string().optional().describe("Agent unsubscribing (defaults to context agent)")
27693
+ }, async ({ task_id, agent_id }) => {
27694
+ try {
27695
+ const { unwatchTask: unwatchTask2 } = (init_tasks(), __toCommonJS(exports_tasks));
27696
+ unwatchTask2(resolveId(task_id), agent_id || "");
27697
+ return { content: [{ type: "text", text: `Stopped watching task ${task_id.slice(0, 8)}` }] };
27698
+ } catch (e) {
27699
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
27700
+ }
27701
+ });
27702
+ }
27703
+ if (shouldRegisterTool("get_task_watchers")) {
27704
+ server.tool("get_task_watchers", "List agents watching a task.", {
27705
+ task_id: exports_external.string().describe("Task ID")
27706
+ }, async ({ task_id }) => {
27707
+ try {
27708
+ const { getTaskWatchers: getTaskWatchers2 } = (init_tasks(), __toCommonJS(exports_tasks));
27709
+ const watchers = getTaskWatchers2(resolveId(task_id));
27710
+ if (watchers.length === 0)
27711
+ return { content: [{ type: "text", text: "No watchers." }] };
27712
+ return { content: [{ type: "text", text: `Watching (${watchers.length}): ${watchers.map((w) => w.agent_id).join(", ")}` }] };
27713
+ } catch (e) {
27714
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
27715
+ }
27716
+ });
27717
+ }
27718
+ if (shouldRegisterTool("todos_retro")) {
27719
+ server.tool("todos_retro", "Post-completion retrospective: stats on completed tasks, low-confidence completions, avg time vs estimate, and patterns.", {
27720
+ project_id: exports_external.string().optional().describe("Filter by project"),
27721
+ plan_id: exports_external.string().optional().describe("Filter by plan"),
27722
+ task_list_id: exports_external.string().optional().describe("Filter by task list"),
27723
+ since: exports_external.string().optional().describe("ISO date \u2014 only tasks completed after this date"),
27724
+ agent_id: exports_external.string().optional().describe("Filter by assignee")
27725
+ }, async ({ project_id, plan_id, task_list_id, since, agent_id }) => {
27726
+ try {
27727
+ const { listTasks: listTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
27728
+ const { patrolTasks: patrolTasks2 } = (init_patrol(), __toCommonJS(exports_patrol));
27729
+ const completed = listTasks2({ status: "completed", project_id, plan_id, task_list_id, assigned_to: agent_id, limit: 500 }, undefined);
27730
+ const filtered = since ? completed.filter((t) => t.completed_at && t.completed_at >= since) : completed;
27731
+ const total = filtered.length;
27732
+ const lowConf = filtered.filter((t) => t.confidence != null && t.confidence < 0.7).length;
27733
+ const withEstimate = filtered.filter((t) => t.estimated_minutes != null && t.actual_minutes != null);
27734
+ const avgDiff = withEstimate.length > 0 ? withEstimate.reduce((acc, t) => acc + (t.actual_minutes - t.estimated_minutes), 0) / withEstimate.length : 0;
27735
+ const patrolResult = patrolTasks2({ project_id: project_id ? resolveId(project_id, "projects") : undefined });
27736
+ const stuck = patrolResult.issues.filter((i) => i.type === "stuck").length;
27737
+ const lines = [
27738
+ `Retro (${total} completed tasks${since ? ` since ${since}` : ""})`,
27739
+ `Low confidence: ${lowConf}/${total}`,
27740
+ `Avg time vs estimate: ${avgDiff >= 0 ? "+" : ""}${avgDiff.toFixed(1)}min`,
27741
+ `Currently stuck: ${stuck}`
27742
+ ];
27743
+ return { content: [{ type: "text", text: lines.join(`
27744
+ `) }] };
27745
+ } catch (e) {
27746
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
27747
+ }
27748
+ });
27749
+ }
27750
+ if (shouldRegisterTool("todos_inbox")) {
27751
+ server.tool("todos_inbox", "Get unassigned tasks (GTD inbox) \u2014 tasks with no assignee, not yet started.", {
27752
+ project_id: exports_external.string().optional().describe("Filter by project"),
27753
+ limit: exports_external.number().optional().describe("Max results (default: 20)")
27754
+ }, async ({ project_id, limit }) => {
27755
+ try {
27756
+ const { listTasks: listTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
27757
+ const tasks = listTasks2({ status: "pending", project_id, assigned_to: "", limit: limit || 20 }, undefined);
27758
+ if (tasks.length === 0)
27759
+ return { content: [{ type: "text", text: "Inbox is empty." }] };
27760
+ const lines = tasks.map((t) => `[${t.priority}] ${t.title.slice(0, 60)} (${t.id.slice(0, 8)})`);
27761
+ return { content: [{ type: "text", text: `Inbox (${tasks.length}):
27762
+ ${lines.join(`
27763
+ `)}` }] };
27764
+ } catch (e) {
27765
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
27766
+ }
27767
+ });
27768
+ }
27441
27769
  if (shouldRegisterTool("get_agent_metrics")) {
27442
27770
  server.tool("get_agent_metrics", "Get performance metrics for an agent: completion rate, speed, confidence, review scores.", {
27443
27771
  agent_id: exports_external.string().describe("Agent ID or name"),
@@ -1 +1 @@
1
- {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGzE,KAAK,OAAO,GAAG;IACb,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC;CACrC,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,kBAAkB,EAAE,WAAW,EAAE,EAAE,OAAO,GAAG,IAAI,CAwD1G"}
1
+ {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGzE,KAAK,OAAO,GAAG;IACb,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC;CACrC,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,kBAAkB,EAAE,WAAW,EAAE,EAAE,OAAO,GAAG,IAAI,CAyD1G"}