@hasna/conversations 0.2.47 → 0.2.49

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.
Files changed (42) hide show
  1. package/LICENSE +5 -15
  2. package/README.md +14 -1
  3. package/bin/hook.js +94 -0
  4. package/bin/index.js +1878 -50
  5. package/bin/mcp.js +1828 -38
  6. package/dashboard/dist/assets/index-DhHQq3wL.css +1 -0
  7. package/dashboard/dist/index.html +2 -2
  8. package/dist/cli/brains.test.d.ts +1 -0
  9. package/dist/cli/commands/analytics.test.d.ts +1 -0
  10. package/dist/cli/commands/messaging.test.d.ts +1 -0
  11. package/dist/cli/commands/spaces.test.d.ts +1 -0
  12. package/dist/cli/commands/tmux.test.d.ts +1 -0
  13. package/dist/hooks/blocker-hook.test.d.ts +1 -0
  14. package/dist/index.d.ts +6 -2
  15. package/dist/index.js +916 -15
  16. package/dist/index.test.d.ts +1 -0
  17. package/dist/lib/gatherer.test.d.ts +1 -0
  18. package/dist/lib/model-config.test.d.ts +1 -0
  19. package/dist/lib/names.test.d.ts +1 -0
  20. package/dist/lib/pg-migrations.test.d.ts +1 -0
  21. package/dist/lib/tasks.d.ts +78 -0
  22. package/dist/lib/tasks.test.d.ts +1 -0
  23. package/dist/lib/terminal-markdown.test.d.ts +1 -0
  24. package/dist/lib/webhooks-management.test.d.ts +1 -0
  25. package/dist/lib/webhooks.d.ts +46 -1
  26. package/dist/mcp/http.d.ts +16 -0
  27. package/dist/mcp/http.test.d.ts +1 -0
  28. package/dist/mcp/index.d.ts +3 -1
  29. package/dist/mcp/telegram-channel.test.d.ts +1 -0
  30. package/dist/mcp/tools/advanced.test.d.ts +1 -0
  31. package/dist/mcp/tools/agents.test.d.ts +1 -0
  32. package/dist/mcp/tools/messaging.test.d.ts +1 -0
  33. package/dist/mcp/tools/projects.test.d.ts +1 -0
  34. package/dist/mcp/tools/spaces.test.d.ts +1 -0
  35. package/dist/mcp/tools/tasks.d.ts +6 -0
  36. package/dist/mcp/tools/tasks.test.d.ts +1 -0
  37. package/dist/mcp/tools/webhooks.d.ts +6 -0
  38. package/dist/mcp/tools/webhooks.test.d.ts +1 -0
  39. package/dist/types.d.ts +120 -0
  40. package/package.json +3 -2
  41. package/dashboard/dist/assets/index-CF_GDtNp.css +0 -1
  42. /package/dashboard/dist/assets/{index-Bw0wMcXE.js → index-UKgLYJ49.js} +0 -0
package/bin/mcp.js CHANGED
@@ -7948,9 +7948,9 @@ async function syncTransfer(source, target, options, _direction) {
7948
7948
  const batch = rows.slice(offset, offset + batchSize);
7949
7949
  try {
7950
7950
  if (isAsyncAdapter(target)) {
7951
- await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch);
7951
+ await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch, columns.includes(conflictColumn) ? conflictColumn : undefined);
7952
7952
  } else {
7953
- batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch);
7953
+ batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch, columns.includes(conflictColumn) ? conflictColumn : undefined);
7954
7954
  }
7955
7955
  result.rowsWritten += batch.length;
7956
7956
  } catch (err) {
@@ -7997,7 +7997,7 @@ async function syncTransfer(source, target, options, _direction) {
7997
7997
  }
7998
7998
  return results;
7999
7999
  }
8000
- async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
8000
+ async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch, conflictColumn) {
8001
8001
  if (batch.length === 0)
8002
8002
  return;
8003
8003
  const colList = columns.map((c) => `"${c}"`).join(", ");
@@ -8007,20 +8007,22 @@ async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, ba
8007
8007
  }).join(", ");
8008
8008
  const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
8009
8009
  const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
8010
+ const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
8010
8011
  const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
8011
- ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
8012
+ ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}${whereClause}`;
8012
8013
  const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
8013
8014
  await target.run(sql, ...params);
8014
8015
  }
8015
- function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch) {
8016
+ function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch, conflictColumn) {
8016
8017
  if (batch.length === 0)
8017
8018
  return;
8018
8019
  const colList = columns.map((c) => `"${c}"`).join(", ");
8019
8020
  const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
8020
8021
  const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
8021
8022
  const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
8023
+ const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
8022
8024
  const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
8023
- ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
8025
+ ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}${whereClause}`;
8024
8026
  const params = batch.flatMap((row) => columns.map((c) => coerceForSqlite(row[c])));
8025
8027
  target.run(sql, ...params);
8026
8028
  }
@@ -9090,7 +9092,7 @@ async function ensureAllPgDatabases() {
9090
9092
  }
9091
9093
  return results;
9092
9094
  }
9093
- function registerCloudTools(server, serviceName) {
9095
+ function registerCloudTools(server, serviceName, opts = {}) {
9094
9096
  server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
9095
9097
  const config2 = getCloudConfig();
9096
9098
  const lines = [
@@ -9123,8 +9125,13 @@ function registerCloudTools(server, serviceName) {
9123
9125
  isError: true
9124
9126
  };
9125
9127
  }
9126
- const local = new SqliteAdapter(getDbPath(serviceName));
9128
+ const local = new SqliteAdapter(opts.dbPath ?? getDbPath(serviceName));
9127
9129
  const cloud = new PgAdapterAsync(getConnectionString(serviceName));
9130
+ if (opts.migrations?.length) {
9131
+ for (const sql of opts.migrations) {
9132
+ await cloud.run(sql);
9133
+ }
9134
+ }
9128
9135
  const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables(local);
9129
9136
  const results = await syncPush(local, cloud, { tables: tableList });
9130
9137
  local.close();
@@ -9146,7 +9153,7 @@ function registerCloudTools(server, serviceName) {
9146
9153
  isError: true
9147
9154
  };
9148
9155
  }
9149
- const local = new SqliteAdapter(getDbPath(serviceName));
9156
+ const local = new SqliteAdapter(opts.dbPath ?? getDbPath(serviceName));
9150
9157
  const cloud = new PgAdapterAsync(getConnectionString(serviceName));
9151
9158
  let tableList;
9152
9159
  if (tablesStr) {
@@ -18100,6 +18107,100 @@ function getDb() {
18100
18107
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
18101
18108
  )
18102
18109
  `);
18110
+ db.exec(`
18111
+ CREATE TABLE IF NOT EXISTS tasks (
18112
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18113
+ uuid TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))),
18114
+ subject TEXT NOT NULL,
18115
+ description TEXT,
18116
+ status TEXT NOT NULL DEFAULT 'pending',
18117
+ priority TEXT NOT NULL DEFAULT 'medium',
18118
+ assignee TEXT,
18119
+ reporter TEXT NOT NULL,
18120
+ project_id TEXT,
18121
+ space TEXT,
18122
+ parent_id INTEGER REFERENCES tasks(id),
18123
+ depends_on TEXT,
18124
+ tags TEXT,
18125
+ metadata TEXT,
18126
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
18127
+ started_at TEXT,
18128
+ completed_at TEXT,
18129
+ cancelled_at TEXT,
18130
+ due_at TEXT
18131
+ )
18132
+ `);
18133
+ db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_uuid ON tasks(uuid)");
18134
+ db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)");
18135
+ db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(assignee)");
18136
+ db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_reporter ON tasks(reporter)");
18137
+ db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id)");
18138
+ db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_space ON tasks(space)");
18139
+ db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id)");
18140
+ db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority)");
18141
+ db.exec(`
18142
+ CREATE TABLE IF NOT EXISTS task_comments (
18143
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18144
+ task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
18145
+ agent TEXT NOT NULL,
18146
+ content TEXT NOT NULL,
18147
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
18148
+ )
18149
+ `);
18150
+ db.exec("CREATE INDEX IF NOT EXISTS idx_task_comments_task ON task_comments(task_id)");
18151
+ db.exec(`
18152
+ CREATE TABLE IF NOT EXISTS task_activity (
18153
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18154
+ task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
18155
+ agent TEXT NOT NULL,
18156
+ action TEXT NOT NULL,
18157
+ detail TEXT,
18158
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
18159
+ )
18160
+ `);
18161
+ db.exec("CREATE INDEX IF NOT EXISTS idx_task_activity_task ON task_activity(task_id)");
18162
+ db.exec("CREATE INDEX IF NOT EXISTS idx_task_activity_agent ON task_activity(agent)");
18163
+ db.exec(`
18164
+ CREATE TABLE IF NOT EXISTS task_dependencies (
18165
+ task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
18166
+ depends_on_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
18167
+ PRIMARY KEY (task_id, depends_on_id)
18168
+ )
18169
+ `);
18170
+ db.exec("CREATE INDEX IF NOT EXISTS idx_task_deps_depends ON task_dependencies(depends_on_id)");
18171
+ const hasTasksFts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='tasks_fts'").get();
18172
+ if (!hasTasksFts) {
18173
+ db.exec(`
18174
+ CREATE VIRTUAL TABLE tasks_fts USING fts5(
18175
+ subject, description, tags
18176
+ )
18177
+ `);
18178
+ db.exec(`
18179
+ INSERT INTO tasks_fts(rowid, subject, description, tags)
18180
+ SELECT id, COALESCE(subject, ''), COALESCE(description, ''),
18181
+ COALESCE(REPLACE(REPLACE(REPLACE(tags, '[', ''), ']', ''), '"', ''), '')
18182
+ FROM tasks
18183
+ `);
18184
+ db.exec(`
18185
+ CREATE TRIGGER IF NOT EXISTS tasks_fts_insert AFTER INSERT ON tasks BEGIN
18186
+ INSERT INTO tasks_fts(rowid, subject, description, tags)
18187
+ VALUES (new.id, COALESCE(new.subject, ''), COALESCE(new.description, ''),
18188
+ COALESCE(REPLACE(REPLACE(REPLACE(new.tags, '[', ''), ']', ''), '"', ''), ''));
18189
+ END
18190
+ `);
18191
+ db.exec(`
18192
+ CREATE TRIGGER IF NOT EXISTS tasks_fts_delete AFTER DELETE ON tasks BEGIN
18193
+ DELETE FROM tasks_fts WHERE rowid = old.id;
18194
+ END
18195
+ `);
18196
+ db.exec(`
18197
+ CREATE TRIGGER IF NOT EXISTS tasks_fts_update AFTER UPDATE ON tasks BEGIN
18198
+ INSERT OR REPLACE INTO tasks_fts(rowid, subject, description, tags)
18199
+ VALUES (new.id, COALESCE(new.subject, ''), COALESCE(new.description, ''),
18200
+ COALESCE(REPLACE(REPLACE(REPLACE(new.tags, '[', ''), ']', ''), '"', ''), ''));
18201
+ END
18202
+ `);
18203
+ }
18103
18204
  return db;
18104
18205
  }
18105
18206
  function closeDb() {
@@ -18474,7 +18575,7 @@ __export(exports_identity, {
18474
18575
  getAutoName: () => getAutoName,
18475
18576
  _resetAutoName: () => _resetAutoName
18476
18577
  });
18477
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
18578
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync8 } from "fs";
18478
18579
  import { join as join11, dirname as dirname3 } from "path";
18479
18580
  function isNameTaken(name) {
18480
18581
  try {
@@ -18506,8 +18607,8 @@ function getAutoName() {
18506
18607
  }
18507
18608
  cachedAutoName = name;
18508
18609
  try {
18509
- mkdirSync7(dirname3(AGENT_ID_FILE), { recursive: true });
18510
- writeFileSync3(AGENT_ID_FILE, name + `
18610
+ mkdirSync8(dirname3(AGENT_ID_FILE), { recursive: true });
18611
+ writeFileSync5(AGENT_ID_FILE, name + `
18511
18612
  `, "utf-8");
18512
18613
  } catch {}
18513
18614
  return name;
@@ -18533,8 +18634,8 @@ function requireIdentity(explicit) {
18533
18634
  function updateCachedAutoName(newName) {
18534
18635
  cachedAutoName = newName;
18535
18636
  try {
18536
- mkdirSync7(dirname3(AGENT_ID_FILE), { recursive: true });
18537
- writeFileSync3(AGENT_ID_FILE, newName + `
18637
+ mkdirSync8(dirname3(AGENT_ID_FILE), { recursive: true });
18638
+ writeFileSync5(AGENT_ID_FILE, newName + `
18538
18639
  `, "utf-8");
18539
18640
  } catch {}
18540
18641
  }
@@ -36290,6 +36391,7 @@ config(en_default2());
36290
36391
 
36291
36392
  // node_modules/@modelcontextprotocol/sdk/dist/esm/types.js
36292
36393
  var LATEST_PROTOCOL_VERSION = "2025-11-25";
36394
+ var DEFAULT_NEGOTIATED_PROTOCOL_VERSION = "2025-03-26";
36293
36395
  var SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05", "2024-10-07"];
36294
36396
  var RELATED_TASK_META_KEY = "io.modelcontextprotocol/related-task";
36295
36397
  var JSONRPC_VERSION = "2.0";
@@ -36462,6 +36564,7 @@ var InitializeRequestSchema = RequestSchema.extend({
36462
36564
  method: literal("initialize"),
36463
36565
  params: InitializeRequestParamsSchema
36464
36566
  });
36567
+ var isInitializeRequest = (value) => InitializeRequestSchema.safeParse(value).success;
36465
36568
  var ServerCapabilitiesSchema = object2({
36466
36569
  experimental: record(string2(), AssertObjectSchema).optional(),
36467
36570
  logging: AssertObjectSchema.optional(),
@@ -40810,12 +40913,12 @@ function setPresenceProject(agent, projectId) {
40810
40913
  // src/lib/messages.ts
40811
40914
  init_db();
40812
40915
  import { randomUUID } from "crypto";
40813
- import { mkdirSync as mkdirSync6, copyFileSync as copyFileSync3, statSync as statSync2, existsSync as existsSync9, realpathSync } from "fs";
40916
+ import { mkdirSync as mkdirSync7, copyFileSync as copyFileSync3, statSync as statSync2, existsSync as existsSync10, realpathSync } from "fs";
40814
40917
  import { join as join10, basename, resolve } from "path";
40815
40918
 
40816
40919
  // src/lib/webhooks.ts
40817
40920
  init_db();
40818
- import { readFileSync as readFileSync3 } from "fs";
40921
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
40819
40922
  import { join as join9 } from "path";
40820
40923
  import dns from "dns";
40821
40924
  import net from "net";
@@ -40920,6 +41023,27 @@ function fireWebhooks(msg) {
40920
41023
  });
40921
41024
  }
40922
41025
  }
41026
+ function fireTaskWebhooks(event) {
41027
+ const config2 = loadConfig();
41028
+ if (!config2.webhooks || config2.webhooks.length === 0)
41029
+ return;
41030
+ const taskWebhooks = config2.webhooks.filter((w) => w.events.includes("task"));
41031
+ if (taskWebhooks.length === 0)
41032
+ return;
41033
+ for (const webhook of taskWebhooks) {
41034
+ if (webhook.agent && event.agent !== webhook.agent)
41035
+ continue;
41036
+ validateWebhookUrl(webhook.url).then((valid) => {
41037
+ if (!valid)
41038
+ return;
41039
+ fetch(webhook.url, {
41040
+ method: "POST",
41041
+ headers: { "Content-Type": "application/json" },
41042
+ body: JSON.stringify(event)
41043
+ }).catch(() => {});
41044
+ });
41045
+ }
41046
+ }
40923
41047
 
40924
41048
  // src/lib/messages.ts
40925
41049
  function compactMessage(msg) {
@@ -40964,7 +41088,7 @@ function getAttachmentsDir() {
40964
41088
  }
40965
41089
  function validateAttachment(sourcePath, name) {
40966
41090
  const absolute = resolve(sourcePath);
40967
- if (!existsSync9(absolute)) {
41091
+ if (!existsSync10(absolute)) {
40968
41092
  throw new Error(`Attachment source not found: ${sourcePath}`);
40969
41093
  }
40970
41094
  const real = realpathSync(absolute);
@@ -41046,7 +41170,7 @@ function sendMessage(opts) {
41046
41170
  const message = parseMessage(row);
41047
41171
  if (opts.attachments && opts.attachments.length > 0) {
41048
41172
  const attachmentsDir = join10(getAttachmentsDir(), String(message.id));
41049
- mkdirSync6(attachmentsDir, { recursive: true });
41173
+ mkdirSync7(attachmentsDir, { recursive: true });
41050
41174
  const attachmentInfos = [];
41051
41175
  for (const att of opts.attachments) {
41052
41176
  const { safeSource, safeName } = validateAttachment(att.source_path, att.name);
@@ -44518,7 +44642,28 @@ function registerAdvancedTools(server, pkgVersion) {
44518
44642
  "remove_agent",
44519
44643
  "rename_agent",
44520
44644
  "search_tools",
44521
- "describe_tools"
44645
+ "describe_tools",
44646
+ "create_task",
44647
+ "get_task",
44648
+ "list_tasks",
44649
+ "start_task",
44650
+ "complete_task",
44651
+ "cancel_task",
44652
+ "block_task",
44653
+ "unblock_task",
44654
+ "reopen_task",
44655
+ "assign_task",
44656
+ "set_task_priority",
44657
+ "delete_task",
44658
+ "add_comment",
44659
+ "get_comments",
44660
+ "get_subtasks",
44661
+ "get_task_tree",
44662
+ "add_dependency",
44663
+ "remove_dependency",
44664
+ "get_dependencies",
44665
+ "get_dependents",
44666
+ "get_task_activity"
44522
44667
  ];
44523
44668
  const q = query?.toLowerCase();
44524
44669
  const matches = q ? all.filter((n) => n.includes(q)) : all;
@@ -44599,7 +44744,28 @@ function registerAdvancedTools(server, pkgVersion) {
44599
44744
  remove_agent: "Remove agent from presence list. Optional: from?, agent?(defaults to self)",
44600
44745
  rename_agent: "Rename agent in presence list. Required: new_name. Optional: from?",
44601
44746
  search_tools: "Search tool names by keyword. Optional: query?",
44602
- describe_tools: "Get full descriptions for tools. Required: names(array of tool names)"
44747
+ describe_tools: "Get full descriptions for tools. Required: names(array of tool names)",
44748
+ create_task: "Create a new task. Required: subject, reporter. Optional: description?, assignee?, priority?(low|medium|high|critical), project_id?, space?, parent_id?(subtask), depends_on?(array of task ids), tags?(array), metadata?(JSON), due_at?(ISO date)",
44749
+ get_task: "Get a task by id or uuid. Returns enriched TaskInfo with subtask_count, comment_count, dependency_count, blocker_info. Required: id? or uuid?",
44750
+ list_tasks: "List tasks with filters. Optional: status?(pending|in_progress|completed|cancelled|blocked), assignee?, reporter?, project_id?, space?, parent_id?(null for top-level), priority?, tag?, limit?(default 50), offset?, include_archived?",
44751
+ start_task: "Mark task in_progress. Fails if any dependency not completed. Required: id. Optional: agent?",
44752
+ complete_task: "Mark task completed. Auto-unblocks dependent tasks with all deps met. Required: id. Optional: agent?, evidence?",
44753
+ cancel_task: "Cancel a task with optional reason. Required: id. Optional: agent?, reason?",
44754
+ block_task: "Manually block a task. Required: id. Optional: agent?, reason?",
44755
+ unblock_task: "Unblock a task to pending if all deps completed, stays blocked otherwise. Required: id. Optional: agent?",
44756
+ reopen_task: "Reopen completed/cancelled task back to pending. Re-checks dependencies. Required: id. Optional: agent?",
44757
+ assign_task: "Assign a task to an agent. Required: id, assignee. Optional: agent?",
44758
+ set_task_priority: "Change task priority. Required: id, priority(low|medium|high|critical). Optional: agent?",
44759
+ delete_task: "Delete a task. Fails if subtasks exist. Required: id. Optional: agent?",
44760
+ add_comment: "Add a comment to a task. Required: task_id, content. Optional: agent?",
44761
+ get_comments: "Get all comments on a task ordered by creation time. Required: task_id",
44762
+ get_subtasks: "Get direct children (subtasks) of a parent task. Required: parent_id",
44763
+ get_task_tree: "Get task with full subtask tree (recursive, max depth 5). Required: parent_id. Optional: max_depth?",
44764
+ add_dependency: "Add dependency: task_id depends on depends_on_id. Prevents circular deps. Auto-blocks if dep not completed. Required: task_id, depends_on_id",
44765
+ remove_dependency: "Remove a dependency. Required: task_id, depends_on_id",
44766
+ get_dependencies: "Get tasks this task depends on (must complete first). Required: task_id",
44767
+ get_dependents: "Get tasks that depend on this task (blocked by this). Required: task_id",
44768
+ get_task_activity: "Get activity log: status changes, comments, dep changes. Required: task_id. Optional: limit?(default 50)"
44603
44769
  };
44604
44770
  const result = names.map((n) => `${n}: ${descriptions[n] || "See tool schema"}`).join(`
44605
44771
  `);
@@ -44665,7 +44831,9 @@ function registerCloudSyncTools(server) {
44665
44831
  `Service: conversations`,
44666
44832
  `RDS Host: ${config2.rds.host || "(not configured)"}`
44667
44833
  ];
44668
- if (config2.rds.host && config2.rds.username) {
44834
+ if (config2.mode === "local") {
44835
+ lines.push("PostgreSQL: skipped in local mode");
44836
+ } else if (config2.rds.host && config2.rds.username) {
44669
44837
  try {
44670
44838
  const pg = new PgAdapterAsync2(getConnectionString2("postgres"));
44671
44839
  await pg.get("SELECT 1 as ok");
@@ -45135,10 +45303,1593 @@ function registerTmuxTools(server) {
45135
45303
  };
45136
45304
  });
45137
45305
  }
45306
+
45307
+ // src/lib/tasks.ts
45308
+ init_db();
45309
+ import { randomUUID as randomUUID3 } from "crypto";
45310
+ function parseTask(row) {
45311
+ let dependsOn = null;
45312
+ if (row.depends_on) {
45313
+ try {
45314
+ dependsOn = JSON.parse(row.depends_on);
45315
+ } catch {
45316
+ dependsOn = null;
45317
+ }
45318
+ }
45319
+ let tags = null;
45320
+ if (row.tags) {
45321
+ try {
45322
+ tags = JSON.parse(row.tags);
45323
+ } catch {
45324
+ tags = null;
45325
+ }
45326
+ }
45327
+ let metadata = null;
45328
+ if (row.metadata) {
45329
+ try {
45330
+ metadata = JSON.parse(row.metadata);
45331
+ } catch {
45332
+ metadata = null;
45333
+ }
45334
+ }
45335
+ return {
45336
+ id: row.id,
45337
+ uuid: row.uuid,
45338
+ subject: row.subject,
45339
+ description: row.description || null,
45340
+ status: row.status,
45341
+ priority: row.priority,
45342
+ assignee: row.assignee || null,
45343
+ reporter: row.reporter,
45344
+ project_id: row.project_id || null,
45345
+ space: row.space || null,
45346
+ parent_id: row.parent_id || null,
45347
+ depends_on: dependsOn,
45348
+ tags,
45349
+ metadata,
45350
+ created_at: row.created_at,
45351
+ started_at: row.started_at || null,
45352
+ completed_at: row.completed_at || null,
45353
+ cancelled_at: row.cancelled_at || null,
45354
+ due_at: row.due_at || null
45355
+ };
45356
+ }
45357
+ function logActivity(taskId, agent, action, detail) {
45358
+ const db2 = getDb();
45359
+ db2.prepare("INSERT INTO task_activity (task_id, agent, action, detail) VALUES (?, ?, ?, ?)").run(taskId, agent, action, detail || null);
45360
+ }
45361
+ function emitTaskEvent(task, action, agent, oldStatus, detail) {
45362
+ fireTaskWebhooks({
45363
+ task_id: task.id,
45364
+ task_uuid: task.uuid,
45365
+ subject: task.subject,
45366
+ action,
45367
+ old_status: oldStatus,
45368
+ new_status: task.status,
45369
+ agent,
45370
+ detail,
45371
+ priority: task.priority,
45372
+ assignee: task.assignee,
45373
+ project_id: task.project_id,
45374
+ created_at: task.created_at
45375
+ });
45376
+ }
45377
+ function createTask(opts) {
45378
+ const db2 = getDb();
45379
+ const uuid3 = randomUUID3().replace(/-/g, "");
45380
+ const priority = opts.priority || "medium";
45381
+ const description = opts.description || null;
45382
+ const assignee = opts.assignee || null;
45383
+ const project_id = opts.project_id || null;
45384
+ const space = opts.space || null;
45385
+ const parent_id = opts.parent_id || null;
45386
+ const tags = opts.tags ? JSON.stringify(opts.tags) : null;
45387
+ const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
45388
+ const due_at = opts.due_at || null;
45389
+ const row = db2.prepare(`
45390
+ INSERT INTO tasks (uuid, subject, description, reporter, assignee, priority, project_id, space, parent_id, tags, metadata, due_at)
45391
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
45392
+ RETURNING *
45393
+ `).get(uuid3, opts.subject, description, opts.reporter, assignee, priority, project_id, space, parent_id, tags, metadata, due_at);
45394
+ const task = parseTask(row);
45395
+ if (opts.depends_on && opts.depends_on.length > 0) {
45396
+ const depIds = opts.depends_on;
45397
+ const insertDep = db2.prepare("INSERT INTO task_dependencies (task_id, depends_on_id) VALUES (?, ?)");
45398
+ const depIdsResolved = [];
45399
+ for (const depId of depIds) {
45400
+ const exists = db2.prepare("SELECT id, status FROM tasks WHERE id = ?").get(depId);
45401
+ if (!exists)
45402
+ throw new Error(`Dependency task #${depId} not found`);
45403
+ insertDep.run(task.id, depId);
45404
+ depIdsResolved.push(depId);
45405
+ }
45406
+ db2.prepare("UPDATE tasks SET depends_on = ? WHERE id = ?").run(JSON.stringify(depIdsResolved), task.id);
45407
+ const incompleteDeps = db2.prepare("SELECT depends_on_id FROM task_dependencies WHERE task_id = ? AND depends_on_id IN (SELECT id FROM tasks WHERE status != 'completed')").all(task.id);
45408
+ if (incompleteDeps.length > 0) {
45409
+ db2.prepare("UPDATE tasks SET status = 'blocked' WHERE id = ?").run(task.id);
45410
+ }
45411
+ }
45412
+ logActivity(task.id, opts.reporter, "created");
45413
+ const created = parseTask(db2.prepare("SELECT * FROM tasks WHERE id = ?").get(task.id));
45414
+ fireTaskWebhooks({
45415
+ task_id: created.id,
45416
+ task_uuid: created.uuid,
45417
+ subject: created.subject,
45418
+ action: "created",
45419
+ new_status: created.status,
45420
+ agent: opts.reporter,
45421
+ priority: created.priority,
45422
+ assignee: created.assignee,
45423
+ project_id: created.project_id,
45424
+ created_at: created.created_at
45425
+ });
45426
+ return created;
45427
+ }
45428
+ function getTask(idOrUuid) {
45429
+ const db2 = getDb();
45430
+ let row = null;
45431
+ if (typeof idOrUuid === "number") {
45432
+ row = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(idOrUuid);
45433
+ } else {
45434
+ row = db2.prepare("SELECT * FROM tasks WHERE uuid = ?").get(idOrUuid);
45435
+ }
45436
+ if (!row)
45437
+ return null;
45438
+ return enrichTask(row);
45439
+ }
45440
+ function listTasks(opts = {}) {
45441
+ const db2 = getDb();
45442
+ const conditions = [];
45443
+ const params = [];
45444
+ if (opts.status) {
45445
+ conditions.push("t.status = ?");
45446
+ params.push(opts.status);
45447
+ }
45448
+ if (opts.assignee) {
45449
+ conditions.push("t.assignee = ?");
45450
+ params.push(opts.assignee);
45451
+ }
45452
+ if (opts.reporter) {
45453
+ conditions.push("t.reporter = ?");
45454
+ params.push(opts.reporter);
45455
+ }
45456
+ if (opts.project_id) {
45457
+ conditions.push("t.project_id = ?");
45458
+ params.push(opts.project_id);
45459
+ }
45460
+ if (opts.space) {
45461
+ conditions.push("t.space = ?");
45462
+ params.push(opts.space);
45463
+ }
45464
+ if (opts.priority) {
45465
+ conditions.push("t.priority = ?");
45466
+ params.push(opts.priority);
45467
+ }
45468
+ if (opts.tag) {
45469
+ conditions.push("t.tags LIKE ?");
45470
+ params.push(`%"${opts.tag}"%`);
45471
+ }
45472
+ if (opts.tags && opts.tags.length > 0) {
45473
+ for (const tag of opts.tags) {
45474
+ conditions.push("t.tags LIKE ?");
45475
+ params.push(`%"${tag}"%`);
45476
+ }
45477
+ }
45478
+ if (opts.metadata && Object.keys(opts.metadata).length > 0) {
45479
+ for (const [key, value] of Object.entries(opts.metadata)) {
45480
+ if (typeof value === "string") {
45481
+ conditions.push(`t.metadata LIKE ?`);
45482
+ params.push(`%"${key}":"${value}"%`);
45483
+ } else if (typeof value === "number" || typeof value === "boolean") {
45484
+ conditions.push(`t.metadata LIKE ?`);
45485
+ params.push(`%"${key}":${value}%`);
45486
+ } else {
45487
+ conditions.push(`t.metadata LIKE ?`);
45488
+ params.push(`%"${key}"%`);
45489
+ }
45490
+ }
45491
+ }
45492
+ if (opts.parent_id === null) {
45493
+ conditions.push("t.parent_id IS NULL");
45494
+ } else if (typeof opts.parent_id === "number") {
45495
+ conditions.push("t.parent_id = ?");
45496
+ params.push(opts.parent_id);
45497
+ }
45498
+ if (!opts.include_archived) {
45499
+ conditions.push("t.status != 'cancelled'");
45500
+ }
45501
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
45502
+ const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 50;
45503
+ const offset = Number.isFinite(opts.offset) && opts.offset >= 0 ? Math.floor(opts.offset) : 0;
45504
+ const rows = db2.prepare(`
45505
+ SELECT t.* FROM tasks t
45506
+ ${where}
45507
+ ORDER BY
45508
+ CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
45509
+ t.created_at DESC
45510
+ LIMIT ? OFFSET ?
45511
+ `).all(...params, limit, offset);
45512
+ return rows.map(enrichTask);
45513
+ }
45514
+ function startTask(id, agent) {
45515
+ const db2 = getDb();
45516
+ const task = resolveTask(id);
45517
+ if (!task)
45518
+ return null;
45519
+ const incompleteDeps = db2.prepare(`
45520
+ SELECT td.depends_on_id, t.subject, t.status
45521
+ FROM task_dependencies td
45522
+ JOIN tasks t ON t.id = td.depends_on_id
45523
+ WHERE td.task_id = ? AND t.status != 'completed'
45524
+ `).all(task.id);
45525
+ if (incompleteDeps.length > 0) {
45526
+ throw new Error(`Cannot start: blocked by ${incompleteDeps.length} incomplete task(s): ${incompleteDeps.map((d) => `#${d.depends_on_id} "${d.subject}" (${d.status})`).join(", ")}`);
45527
+ }
45528
+ const now = new Date().toISOString();
45529
+ const oldStatus = task.status;
45530
+ db2.prepare("UPDATE tasks SET status = 'in_progress', started_at = ? WHERE id = ?").run(now, task.id);
45531
+ logActivity(task.id, agent || task.reporter, "started");
45532
+ const updated = getTaskById(task.id);
45533
+ if (updated)
45534
+ emitTaskEvent(updated, "started", agent || task.reporter, oldStatus);
45535
+ return updated;
45536
+ }
45537
+ function completeTask(id, agent, opts) {
45538
+ const db2 = getDb();
45539
+ const task = resolveTask(id);
45540
+ if (!task)
45541
+ return null;
45542
+ const now = new Date().toISOString();
45543
+ const oldStatus = task.status;
45544
+ db2.prepare("UPDATE tasks SET status = 'completed', completed_at = ? WHERE id = ?").run(now, task.id);
45545
+ logActivity(task.id, agent || task.reporter, "completed", opts?.evidence);
45546
+ unblockDependents(task.id);
45547
+ const updated = getTaskById(task.id);
45548
+ if (updated)
45549
+ emitTaskEvent(updated, "completed", agent || task.reporter, oldStatus, opts?.evidence);
45550
+ return updated;
45551
+ }
45552
+ function cancelTask(id, agent, opts) {
45553
+ const db2 = getDb();
45554
+ const task = resolveTask(id);
45555
+ if (!task)
45556
+ return null;
45557
+ const now = new Date().toISOString();
45558
+ const oldStatus = task.status;
45559
+ db2.prepare("UPDATE tasks SET status = 'cancelled', cancelled_at = ? WHERE id = ?").run(now, task.id);
45560
+ logActivity(task.id, agent || task.reporter, "cancelled", opts?.reason);
45561
+ const updated = getTaskById(task.id);
45562
+ if (updated)
45563
+ emitTaskEvent(updated, "cancelled", agent || task.reporter, oldStatus, opts?.reason);
45564
+ return updated;
45565
+ }
45566
+ function blockTask(id, agent, opts) {
45567
+ const db2 = getDb();
45568
+ const task = resolveTask(id);
45569
+ if (!task)
45570
+ return null;
45571
+ const oldStatus = task.status;
45572
+ db2.prepare("UPDATE tasks SET status = 'blocked' WHERE id = ?").run(task.id);
45573
+ logActivity(task.id, agent || task.reporter, "blocked", opts?.reason);
45574
+ const updated = getTaskById(task.id);
45575
+ if (updated)
45576
+ emitTaskEvent(updated, "blocked", agent || task.reporter, oldStatus, opts?.reason);
45577
+ return updated;
45578
+ }
45579
+ function unblockTask(id, agent) {
45580
+ const db2 = getDb();
45581
+ const task = resolveTask(id);
45582
+ if (!task)
45583
+ return null;
45584
+ const incompleteDeps = db2.prepare(`
45585
+ SELECT 1 FROM task_dependencies td
45586
+ JOIN tasks t ON t.id = td.depends_on_id
45587
+ WHERE td.task_id = ? AND t.status != 'completed'
45588
+ LIMIT 1
45589
+ `).get(task.id);
45590
+ const oldStatus = task.status;
45591
+ const newStatus = incompleteDeps ? "blocked" : "pending";
45592
+ db2.prepare("UPDATE tasks SET status = ? WHERE id = ?").run(newStatus, task.id);
45593
+ logActivity(task.id, agent || task.reporter, "unblocked");
45594
+ const updated = getTaskById(task.id);
45595
+ if (updated)
45596
+ emitTaskEvent(updated, "unblocked", agent || task.reporter, oldStatus);
45597
+ return updated;
45598
+ }
45599
+ function reopenTask(id, agent) {
45600
+ const db2 = getDb();
45601
+ const task = resolveTask(id);
45602
+ if (!task)
45603
+ return null;
45604
+ const oldStatus = task.status;
45605
+ db2.prepare("UPDATE tasks SET status = 'pending', completed_at = NULL, cancelled_at = NULL WHERE id = ?").run(task.id);
45606
+ logActivity(task.id, agent || task.reporter, "reopened");
45607
+ const incompleteDeps = db2.prepare(`
45608
+ SELECT 1 FROM task_dependencies td
45609
+ JOIN tasks t ON t.id = td.depends_on_id
45610
+ WHERE td.task_id = ? AND t.status != 'completed'
45611
+ LIMIT 1
45612
+ `).get(task.id);
45613
+ const updated = getTaskById(task.id);
45614
+ if (updated)
45615
+ emitTaskEvent(updated, "reopened", agent || task.reporter, oldStatus);
45616
+ return updated;
45617
+ }
45618
+ function assignTask(id, assignee, agent) {
45619
+ const db2 = getDb();
45620
+ const task = resolveTask(id);
45621
+ if (!task)
45622
+ return null;
45623
+ db2.prepare("UPDATE tasks SET assignee = ? WHERE id = ?").run(assignee, task.id);
45624
+ logActivity(task.id, agent || task.reporter, "assigned", assignee);
45625
+ const updated = getTaskById(task.id);
45626
+ if (updated)
45627
+ emitTaskEvent(updated, "assigned", agent || task.reporter, task.status);
45628
+ return updated;
45629
+ }
45630
+ function setTaskPriority(id, priority, agent) {
45631
+ const db2 = getDb();
45632
+ const task = resolveTask(id);
45633
+ if (!task)
45634
+ return null;
45635
+ const oldPriority = task.priority;
45636
+ db2.prepare("UPDATE tasks SET priority = ? WHERE id = ?").run(priority, task.id);
45637
+ logActivity(task.id, agent || task.reporter, "priority_changed", `${oldPriority} -> ${priority}`);
45638
+ const updated = getTaskById(task.id);
45639
+ if (updated)
45640
+ emitTaskEvent(updated, "priority_changed", agent || task.reporter, task.status, `${oldPriority} -> ${priority}`);
45641
+ return updated;
45642
+ }
45643
+ function addComment(taskId, agent, content) {
45644
+ const db2 = getDb();
45645
+ const task = resolveTask(taskId);
45646
+ if (!task)
45647
+ throw new Error(`Task not found: ${taskId}`);
45648
+ const row = db2.prepare("INSERT INTO task_comments (task_id, agent, content) VALUES (?, ?, ?) RETURNING *").get(task.id, agent, content);
45649
+ logActivity(task.id, agent, "comment", content.length > 200 ? content.slice(0, 200) + "\u2026" : content);
45650
+ return {
45651
+ id: row.id,
45652
+ task_id: row.task_id,
45653
+ agent: row.agent,
45654
+ content: row.content,
45655
+ created_at: row.created_at
45656
+ };
45657
+ }
45658
+ function getComments(taskId) {
45659
+ const db2 = getDb();
45660
+ const task = resolveTask(taskId);
45661
+ if (!task)
45662
+ return [];
45663
+ return db2.prepare("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at ASC, id ASC").all(task.id);
45664
+ }
45665
+ function getSubtasks(parentId) {
45666
+ const db2 = getDb();
45667
+ const parent = resolveTask(parentId);
45668
+ if (!parent)
45669
+ return [];
45670
+ const rows = db2.prepare("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at ASC, id ASC").all(parent.id);
45671
+ return rows.map(enrichTask);
45672
+ }
45673
+ function getTaskTree(parentId, maxDepth = 5) {
45674
+ const root = getTask(typeof parentId === "number" ? parentId : parentId);
45675
+ if (!root)
45676
+ throw new Error(`Task not found: ${parentId}`);
45677
+ const buildTree = (task, depth) => {
45678
+ if (depth >= maxDepth)
45679
+ return { ...task, children: [] };
45680
+ const children = getSubtasks(task.id);
45681
+ return { ...task, children: children.map((c) => buildTree(c, depth + 1)) };
45682
+ };
45683
+ return buildTree(root, 0);
45684
+ }
45685
+ function addDependency(taskId, dependsOnId) {
45686
+ const db2 = getDb();
45687
+ const task = resolveTask(taskId);
45688
+ const dep = resolveTask(dependsOnId);
45689
+ if (!task)
45690
+ throw new Error(`Task not found: ${taskId}`);
45691
+ if (!dep)
45692
+ throw new Error(`Dependency task not found: ${dependsOnId}`);
45693
+ if (task.id === dep.id)
45694
+ throw new Error("A task cannot depend on itself");
45695
+ if (isCircularDependency(task.id, dep.id)) {
45696
+ throw new Error(`Circular dependency detected: task #${task.id} -> #${dep.id}`);
45697
+ }
45698
+ db2.prepare("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on_id) VALUES (?, ?)").run(task.id, dep.id);
45699
+ const deps = db2.prepare("SELECT depends_on_id FROM task_dependencies WHERE task_id = ?").all(task.id);
45700
+ db2.prepare("UPDATE tasks SET depends_on = ? WHERE id = ?").run(JSON.stringify(deps.map((d) => d.depends_on_id)), task.id);
45701
+ if (dep.status !== "completed") {
45702
+ db2.prepare("UPDATE tasks SET status = 'blocked' WHERE id = ?").run(task.id);
45703
+ }
45704
+ logActivity(task.id, "", "dependency_added", `depends on #${dep.id}`);
45705
+ }
45706
+ function removeDependency(taskId, dependsOnId) {
45707
+ const db2 = getDb();
45708
+ const task = resolveTask(taskId);
45709
+ if (!task)
45710
+ throw new Error(`Task not found: ${taskId}`);
45711
+ db2.prepare("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on_id = ?").run(task.id, dependsOnId);
45712
+ const deps = db2.prepare("SELECT depends_on_id FROM task_dependencies WHERE task_id = ?").all(task.id);
45713
+ db2.prepare("UPDATE tasks SET depends_on = ? WHERE id = ?").run(JSON.stringify(deps.map((d) => d.depends_on_id)), task.id);
45714
+ logActivity(task.id, "", "dependency_removed", `no longer depends on #${dependsOnId}`);
45715
+ }
45716
+ function getDependencies(taskId) {
45717
+ const db2 = getDb();
45718
+ const task = resolveTask(taskId);
45719
+ if (!task)
45720
+ return [];
45721
+ return db2.prepare(`
45722
+ SELECT t.* FROM tasks t
45723
+ INNER JOIN task_dependencies td ON td.depends_on_id = t.id
45724
+ WHERE td.task_id = ?
45725
+ ORDER BY t.created_at ASC
45726
+ `).all(task.id).map(parseTask);
45727
+ }
45728
+ function getDependents(taskId) {
45729
+ const db2 = getDb();
45730
+ const task = resolveTask(taskId);
45731
+ if (!task)
45732
+ return [];
45733
+ return db2.prepare(`
45734
+ SELECT t.* FROM tasks t
45735
+ INNER JOIN task_dependencies td ON td.task_id = t.id
45736
+ WHERE td.depends_on_id = ?
45737
+ ORDER BY t.created_at ASC
45738
+ `).all(task.id).map(parseTask);
45739
+ }
45740
+ function getTaskActivity(taskId, limit = 50) {
45741
+ const db2 = getDb();
45742
+ const task = resolveTask(taskId);
45743
+ if (!task)
45744
+ return [];
45745
+ const safeLimit = Math.max(1, Math.min(Math.floor(limit), 1000));
45746
+ return db2.prepare(`SELECT * FROM task_activity WHERE task_id = ? ORDER BY created_at DESC LIMIT ${safeLimit}`).all(task.id);
45747
+ }
45748
+ function deleteTask(id, agent) {
45749
+ const db2 = getDb();
45750
+ const task = resolveTask(id);
45751
+ if (!task)
45752
+ return false;
45753
+ const subtaskCount = db2.prepare("SELECT COUNT(*) as c FROM tasks WHERE parent_id = ?").get(task.id).c;
45754
+ if (subtaskCount > 0) {
45755
+ throw new Error(`Cannot delete: ${subtaskCount} subtask(s) still reference this task`);
45756
+ }
45757
+ logActivity(task.id, agent || "", "deleted");
45758
+ db2.prepare("DELETE FROM tasks WHERE id = ?").run(task.id);
45759
+ return true;
45760
+ }
45761
+ function searchTasks(opts) {
45762
+ const db2 = getDb();
45763
+ const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
45764
+ const sortByRelevance = opts.sort !== "recent";
45765
+ const query = opts.query.trim();
45766
+ const terms = query.split(/\s+/).filter(Boolean);
45767
+ const ftsAvailable = db2.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='tasks_fts'").get();
45768
+ if (ftsAvailable && terms.length > 0) {
45769
+ try {
45770
+ let ftsQuery;
45771
+ if (query.startsWith('"') && query.endsWith('"')) {
45772
+ ftsQuery = query;
45773
+ } else {
45774
+ ftsQuery = terms.map((w) => `"${w.replace(/"/g, '""')}"`).join(" ");
45775
+ }
45776
+ const ftsRows = db2.prepare(`SELECT rowid, rank, snippet(tasks_fts, 0, '**', '**', '...', 10) as snippet
45777
+ FROM tasks_fts WHERE tasks_fts MATCH ? ORDER BY rank LIMIT ${limit * 3}`).all(ftsQuery);
45778
+ if (ftsRows.length === 0) {} else {
45779
+ const ids = ftsRows.map((r) => r.rowid);
45780
+ const placeholders = ids.map(() => "?").join(",");
45781
+ const rows2 = db2.prepare(`SELECT * FROM tasks WHERE id IN (${placeholders})`).all(...ids);
45782
+ const taskMap = new Map;
45783
+ for (const row of rows2)
45784
+ taskMap.set(row.id, row);
45785
+ const rankMap = new Map(ftsRows.map((r) => [r.rowid, { rank: r.rank, snippet: r.snippet }]));
45786
+ const sorted = sortByRelevance ? [...ftsRows].sort((a, b) => a.rank - b.rank) : [...ftsRows].sort((a, b) => {
45787
+ const aTask = taskMap.get(a.rowid);
45788
+ const bTask = taskMap.get(b.rowid);
45789
+ return (bTask?.created_at || "").localeCompare(aTask?.created_at || "");
45790
+ });
45791
+ const results = [];
45792
+ const maxRank = Math.abs(sorted[0].rank) || 1;
45793
+ for (const fts of sorted) {
45794
+ const row = taskMap.get(fts.rowid);
45795
+ if (!row)
45796
+ continue;
45797
+ const task = enrichTask(row);
45798
+ if (opts.status && task.status !== opts.status)
45799
+ continue;
45800
+ if (opts.assignee && task.assignee !== opts.assignee)
45801
+ continue;
45802
+ if (opts.project_id && task.project_id !== opts.project_id)
45803
+ continue;
45804
+ if (opts.space && task.space !== opts.space)
45805
+ continue;
45806
+ if (opts.priority && task.priority !== opts.priority)
45807
+ continue;
45808
+ if (!opts.include_archived && task.status === "cancelled")
45809
+ continue;
45810
+ results.push({
45811
+ ...task,
45812
+ snippet: fts.snippet || null,
45813
+ relevance_score: Math.round((1 - Math.abs(fts.rank) / maxRank) * 100)
45814
+ });
45815
+ if (results.length >= limit)
45816
+ break;
45817
+ }
45818
+ return results;
45819
+ }
45820
+ } catch {}
45821
+ }
45822
+ if (terms.length === 0)
45823
+ return [];
45824
+ const params = [];
45825
+ const conditions = [];
45826
+ for (const term of terms) {
45827
+ conditions.push("(LOWER(t.subject) LIKE ? OR LOWER(t.description) LIKE ? OR LOWER(t.tags) LIKE ?)");
45828
+ const likeTerm = `%${term}%`;
45829
+ params.push(likeTerm, likeTerm, likeTerm);
45830
+ }
45831
+ if (opts.status) {
45832
+ conditions.push("t.status = ?");
45833
+ params.push(opts.status);
45834
+ }
45835
+ if (opts.assignee) {
45836
+ conditions.push("t.assignee = ?");
45837
+ params.push(opts.assignee);
45838
+ }
45839
+ if (opts.project_id) {
45840
+ conditions.push("t.project_id = ?");
45841
+ params.push(opts.project_id);
45842
+ }
45843
+ if (opts.space) {
45844
+ conditions.push("t.space = ?");
45845
+ params.push(opts.space);
45846
+ }
45847
+ if (opts.priority) {
45848
+ conditions.push("t.priority = ?");
45849
+ params.push(opts.priority);
45850
+ }
45851
+ if (!opts.include_archived) {
45852
+ conditions.push("t.status != 'cancelled'");
45853
+ }
45854
+ const orderClause = sortByRelevance ? "ORDER BY CASE t.priority WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 WHEN 'low' THEN 4 END, t.created_at DESC" : "ORDER BY t.created_at DESC";
45855
+ const rows = db2.prepare(`SELECT t.* FROM tasks t WHERE ${conditions.join(" AND ")} ${orderClause} LIMIT ${limit}`).all(...params);
45856
+ return rows.map((row) => {
45857
+ const task = enrichTask(row);
45858
+ const subject = row.subject.toLowerCase();
45859
+ const matchCount = terms.filter((t) => subject.includes(t)).length;
45860
+ return {
45861
+ ...task,
45862
+ snippet: null,
45863
+ relevance_score: Math.round(matchCount / terms.length * 100)
45864
+ };
45865
+ });
45866
+ }
45867
+ function getDueTasks(opts = {}) {
45868
+ const db2 = getDb();
45869
+ const windowHours = opts.window_hours ?? 24;
45870
+ const now = new Date;
45871
+ const deadline = new Date(now.getTime() + windowHours * 60 * 60 * 1000);
45872
+ const rows = db2.prepare(`
45873
+ SELECT t.* FROM tasks t
45874
+ WHERE t.due_at IS NOT NULL
45875
+ AND t.due_at <= ?
45876
+ AND t.status NOT IN ('completed', 'cancelled')
45877
+ ORDER BY t.due_at ASC
45878
+ `).all(deadline.toISOString());
45879
+ return rows.map((row) => {
45880
+ const task = enrichTask(row);
45881
+ const dueAt = new Date(task.due_at);
45882
+ const hoursUntilDue = (dueAt.getTime() - now.getTime()) / (1000 * 60 * 60);
45883
+ let urgency;
45884
+ if (hoursUntilDue < 0)
45885
+ urgency = "overdue";
45886
+ else if (hoursUntilDue <= 24)
45887
+ urgency = "due_today";
45888
+ else
45889
+ urgency = "due_soon";
45890
+ return { task, due_in_hours: Math.round(hoursUntilDue * 10) / 10, urgency };
45891
+ });
45892
+ }
45893
+ function getTaskSummary(idOrUuid) {
45894
+ const db2 = getDb();
45895
+ const task = getTask(idOrUuid);
45896
+ if (!task)
45897
+ return null;
45898
+ const subtasks = db2.prepare("SELECT status FROM tasks WHERE parent_id = ?").all(task.id);
45899
+ const totalSubtasks = subtasks.length;
45900
+ const completedSubtasks = subtasks.filter((s) => s.status === "completed").length;
45901
+ const depRows = db2.prepare("SELECT td.depends_on_id, t.status FROM task_dependencies td JOIN tasks t ON t.id = td.depends_on_id WHERE td.task_id = ?").all(task.id);
45902
+ const totalDeps = depRows.length;
45903
+ const completedDeps = depRows.filter((d) => d.status === "completed").length;
45904
+ const commentCount = db2.prepare("SELECT COUNT(*) as c FROM task_comments WHERE task_id = ?").get(task.id).c;
45905
+ const items = totalSubtasks + totalDeps;
45906
+ const completed = completedSubtasks + completedDeps;
45907
+ const completionPct = items > 0 ? Math.round(completed / items * 100) : task.status === "completed" ? 100 : 0;
45908
+ const activity = db2.prepare("SELECT action, agent, detail, created_at FROM task_activity WHERE task_id = ? ORDER BY id DESC LIMIT 10").all(task.id);
45909
+ const blockerInfo = db2.prepare("SELECT td.depends_on_id as task_id, t.subject, t.status FROM task_dependencies td JOIN tasks t ON t.id = td.depends_on_id WHERE td.task_id = ? AND t.status != 'completed'").all(task.id);
45910
+ const dependentRows = db2.prepare("SELECT td.task_id, t.subject, t.status FROM task_dependencies td JOIN tasks t ON t.id = td.task_id WHERE td.depends_on_id = ?").all(task.id);
45911
+ return {
45912
+ task,
45913
+ progress: {
45914
+ total_subtasks: totalSubtasks,
45915
+ completed_subtasks: completedSubtasks,
45916
+ total_dependencies: totalDeps,
45917
+ completed_dependencies: completedDeps,
45918
+ comment_count: commentCount,
45919
+ completion_pct: completionPct
45920
+ },
45921
+ recent_activity: activity,
45922
+ blockers: blockerInfo,
45923
+ dependents: dependentRows
45924
+ };
45925
+ }
45926
+ function enrichTask(row) {
45927
+ const db2 = getDb();
45928
+ const task = parseTask(row);
45929
+ const subtaskCount = db2.prepare("SELECT COUNT(*) as c FROM tasks WHERE parent_id = ?").get(task.id).c;
45930
+ const commentCount = db2.prepare("SELECT COUNT(*) as c FROM task_comments WHERE task_id = ?").get(task.id).c;
45931
+ const depRows = db2.prepare("SELECT depends_on_id FROM task_dependencies WHERE task_id = ?").all(task.id);
45932
+ const depCount = depRows.length;
45933
+ let blockerInfo = [];
45934
+ if (depRows.length > 0) {
45935
+ blockerInfo = depRows.map((d) => {
45936
+ const dep = db2.prepare("SELECT id, subject, status FROM tasks WHERE id = ?").get(d.depends_on_id);
45937
+ return dep ? { task_id: dep.id, subject: dep.subject, status: dep.status } : null;
45938
+ }).filter(Boolean);
45939
+ }
45940
+ return { ...task, subtask_count: subtaskCount, comment_count: commentCount, dependency_count: depCount, blocker_info: blockerInfo };
45941
+ }
45942
+ function resolveTask(idOrUuid) {
45943
+ const db2 = getDb();
45944
+ if (typeof idOrUuid === "number") {
45945
+ const row2 = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(idOrUuid);
45946
+ return row2 ? parseTask(row2) : null;
45947
+ }
45948
+ const row = db2.prepare("SELECT * FROM tasks WHERE uuid = ?").get(idOrUuid);
45949
+ return row ? parseTask(row) : null;
45950
+ }
45951
+ function getTaskById(id) {
45952
+ const db2 = getDb();
45953
+ const row = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
45954
+ return row ? parseTask(row) : null;
45955
+ }
45956
+ function unblockDependents(completedTaskId) {
45957
+ const db2 = getDb();
45958
+ const dependents = db2.prepare(`
45959
+ SELECT td.task_id, t.status FROM task_dependencies td
45960
+ JOIN tasks t ON t.id = td.task_id
45961
+ WHERE td.depends_on_id = ?
45962
+ `).all(completedTaskId);
45963
+ for (const dep of dependents) {
45964
+ if (dep.status === "blocked") {
45965
+ const incompleteCount = db2.prepare(`
45966
+ SELECT COUNT(*) as c FROM task_dependencies td
45967
+ JOIN tasks t ON t.id = td.depends_on_id
45968
+ WHERE td.task_id = ? AND t.status != 'completed'
45969
+ `).get(dep.task_id).c;
45970
+ if (incompleteCount === 0) {
45971
+ db2.prepare("UPDATE tasks SET status = 'pending' WHERE id = ?").run(dep.task_id);
45972
+ logActivity(dep.task_id, "", "auto_unblocked", `dependency #${completedTaskId} completed`);
45973
+ const task = getTaskById(dep.task_id);
45974
+ if (task)
45975
+ emitTaskEvent(task, "auto_unblocked", "system", "blocked", `dependency #${completedTaskId} completed`);
45976
+ }
45977
+ }
45978
+ }
45979
+ }
45980
+ function isCircularDependency(taskId, dependsOnId) {
45981
+ const db2 = getDb();
45982
+ const visited = new Set;
45983
+ let current = dependsOnId;
45984
+ let depth = 0;
45985
+ while (current !== undefined && depth < 20) {
45986
+ if (current === taskId)
45987
+ return true;
45988
+ if (visited.has(current))
45989
+ break;
45990
+ visited.add(current);
45991
+ const parents = db2.prepare("SELECT depends_on_id FROM task_dependencies WHERE task_id = ?").all(current);
45992
+ current = parents.length > 0 ? parents[0].depends_on_id : undefined;
45993
+ depth++;
45994
+ }
45995
+ return false;
45996
+ }
45997
+
45998
+ // src/mcp/tools/tasks.ts
45999
+ init_identity();
46000
+ function registerTaskTools(server) {
46001
+ server.registerTool("create_task", {
46002
+ description: "Create a new task with optional assignee, priority, parent (subtask), dependencies, tags, and metadata.",
46003
+ inputSchema: {
46004
+ subject: exports_external.string(),
46005
+ description: exports_external.string().optional(),
46006
+ reporter: exports_external.string().optional(),
46007
+ assignee: exports_external.string().optional(),
46008
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
46009
+ project_id: exports_external.string().optional(),
46010
+ space: exports_external.string().optional(),
46011
+ parent_id: exports_external.coerce.number().optional(),
46012
+ depends_on: exports_external.array(exports_external.coerce.number()).optional(),
46013
+ tags: exports_external.array(exports_external.string()).optional(),
46014
+ metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
46015
+ due_at: exports_external.string().optional()
46016
+ }
46017
+ }, async (args) => {
46018
+ if (!args.reporter) {
46019
+ try {
46020
+ args.reporter = resolveIdentity(undefined);
46021
+ } catch {
46022
+ args.reporter = "unknown";
46023
+ }
46024
+ }
46025
+ const task = createTask({
46026
+ subject: args.subject,
46027
+ description: args.description,
46028
+ reporter: args.reporter,
46029
+ assignee: args.assignee,
46030
+ priority: args.priority,
46031
+ project_id: args.project_id,
46032
+ space: args.space,
46033
+ parent_id: args.parent_id,
46034
+ depends_on: args.depends_on,
46035
+ tags: args.tags,
46036
+ metadata: args.metadata,
46037
+ due_at: args.due_at
46038
+ });
46039
+ return { content: [{ type: "text", text: JSON.stringify(task) }] };
46040
+ });
46041
+ server.registerTool("get_task", {
46042
+ description: "Get a task by id or uuid. Returns enriched TaskInfo with subtask count, comment count, dependency count, and blocker info.",
46043
+ inputSchema: {
46044
+ id: exports_external.coerce.number().optional(),
46045
+ uuid: exports_external.string().optional()
46046
+ }
46047
+ }, async (args) => {
46048
+ const lookup = args.id ?? args.uuid;
46049
+ if (!lookup)
46050
+ return { content: [{ type: "text", text: "id or uuid required" }], isError: true };
46051
+ const task = getTask(lookup);
46052
+ if (!task)
46053
+ return { content: [{ type: "text", text: `Task not found: ${lookup}` }], isError: true };
46054
+ return { content: [{ type: "text", text: JSON.stringify(task) }] };
46055
+ });
46056
+ server.registerTool("list_tasks", {
46057
+ description: "List tasks with optional filters. Default: 50 tasks, sorted by priority then date. Use 'tags' for AND-matching multiple tags. Use 'metadata' to filter by metadata key/value pairs.",
46058
+ inputSchema: {
46059
+ status: exports_external.enum(["pending", "in_progress", "completed", "cancelled", "blocked"]).optional(),
46060
+ assignee: exports_external.string().optional(),
46061
+ reporter: exports_external.string().optional(),
46062
+ project_id: exports_external.string().optional(),
46063
+ space: exports_external.string().optional(),
46064
+ parent_id: exports_external.coerce.number().nullable().optional(),
46065
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
46066
+ tag: exports_external.string().optional(),
46067
+ tags: exports_external.array(exports_external.string()).optional(),
46068
+ metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
46069
+ limit: exports_external.coerce.number().optional(),
46070
+ offset: exports_external.coerce.number().optional(),
46071
+ include_archived: exports_external.coerce.boolean().optional()
46072
+ }
46073
+ }, async (args) => {
46074
+ const tasks = listTasks(args);
46075
+ return { content: [{ type: "text", text: JSON.stringify({ tasks, count: tasks.length }) }] };
46076
+ });
46077
+ server.registerTool("start_task", {
46078
+ description: "Mark a task as in_progress. Fails if any dependency is not completed.",
46079
+ inputSchema: {
46080
+ id: exports_external.coerce.number(),
46081
+ agent: exports_external.string().optional()
46082
+ }
46083
+ }, async (args) => {
46084
+ const agent = args.agent ? args.agent : resolveIdentity(undefined);
46085
+ const task = startTask(args.id, agent);
46086
+ if (!task)
46087
+ return { content: [{ type: "text", text: `Task not found: ${args.id}` }], isError: true };
46088
+ return { content: [{ type: "text", text: JSON.stringify(task) }] };
46089
+ });
46090
+ server.registerTool("complete_task", {
46091
+ description: "Mark a task as completed. Auto-unblocks any dependent tasks that now have all dependencies completed.",
46092
+ inputSchema: {
46093
+ id: exports_external.coerce.number(),
46094
+ agent: exports_external.string().optional(),
46095
+ evidence: exports_external.string().optional()
46096
+ }
46097
+ }, async (args) => {
46098
+ const agent = args.agent ? args.agent : resolveIdentity(undefined);
46099
+ const task = completeTask(args.id, agent, args.evidence ? { evidence: args.evidence } : undefined);
46100
+ if (!task)
46101
+ return { content: [{ type: "text", text: `Task not found: ${args.id}` }], isError: true };
46102
+ return { content: [{ type: "text", text: JSON.stringify(task) }] };
46103
+ });
46104
+ server.registerTool("cancel_task", {
46105
+ description: "Cancel a task with optional reason.",
46106
+ inputSchema: {
46107
+ id: exports_external.coerce.number(),
46108
+ agent: exports_external.string().optional(),
46109
+ reason: exports_external.string().optional()
46110
+ }
46111
+ }, async (args) => {
46112
+ const agent = args.agent ? args.agent : resolveIdentity(undefined);
46113
+ const task = cancelTask(args.id, agent, args.reason ? { reason: args.reason } : undefined);
46114
+ if (!task)
46115
+ return { content: [{ type: "text", text: `Task not found: ${args.id}` }], isError: true };
46116
+ return { content: [{ type: "text", text: JSON.stringify(task) }] };
46117
+ });
46118
+ server.registerTool("block_task", {
46119
+ description: "Manually block a task with optional reason.",
46120
+ inputSchema: {
46121
+ id: exports_external.coerce.number(),
46122
+ agent: exports_external.string().optional(),
46123
+ reason: exports_external.string().optional()
46124
+ }
46125
+ }, async (args) => {
46126
+ const agent = args.agent ? args.agent : resolveIdentity(undefined);
46127
+ const task = blockTask(args.id, agent, args.reason ? { reason: args.reason } : undefined);
46128
+ if (!task)
46129
+ return { content: [{ type: "text", text: `Task not found: ${args.id}` }], isError: true };
46130
+ return { content: [{ type: "text", text: JSON.stringify(task) }] };
46131
+ });
46132
+ server.registerTool("unblock_task", {
46133
+ description: "Unblock a task. Sets to 'pending' if all dependencies are completed, otherwise stays 'blocked'.",
46134
+ inputSchema: {
46135
+ id: exports_external.coerce.number(),
46136
+ agent: exports_external.string().optional()
46137
+ }
46138
+ }, async (args) => {
46139
+ const agent = args.agent ? args.agent : resolveIdentity(undefined);
46140
+ const task = unblockTask(args.id, agent);
46141
+ if (!task)
46142
+ return { content: [{ type: "text", text: `Task not found: ${args.id}` }], isError: true };
46143
+ return { content: [{ type: "text", text: JSON.stringify(task) }] };
46144
+ });
46145
+ server.registerTool("reopen_task", {
46146
+ description: "Reopen a completed or cancelled task back to pending. Re-checks dependencies.",
46147
+ inputSchema: {
46148
+ id: exports_external.coerce.number(),
46149
+ agent: exports_external.string().optional()
46150
+ }
46151
+ }, async (args) => {
46152
+ const agent = args.agent ? args.agent : resolveIdentity(undefined);
46153
+ const task = reopenTask(args.id, agent);
46154
+ if (!task)
46155
+ return { content: [{ type: "text", text: `Task not found: ${args.id}` }], isError: true };
46156
+ return { content: [{ type: "text", text: JSON.stringify(task) }] };
46157
+ });
46158
+ server.registerTool("assign_task", {
46159
+ description: "Assign a task to an agent.",
46160
+ inputSchema: {
46161
+ id: exports_external.coerce.number(),
46162
+ assignee: exports_external.string(),
46163
+ agent: exports_external.string().optional()
46164
+ }
46165
+ }, async (args) => {
46166
+ const agent = args.agent ? args.agent : resolveIdentity(undefined);
46167
+ const task = assignTask(args.id, args.assignee, agent);
46168
+ if (!task)
46169
+ return { content: [{ type: "text", text: `Task not found: ${args.id}` }], isError: true };
46170
+ return { content: [{ type: "text", text: JSON.stringify(task) }] };
46171
+ });
46172
+ server.registerTool("set_task_priority", {
46173
+ description: "Change a task's priority: low, medium, high, critical.",
46174
+ inputSchema: {
46175
+ id: exports_external.coerce.number(),
46176
+ priority: exports_external.enum(["low", "medium", "high", "critical"]),
46177
+ agent: exports_external.string().optional()
46178
+ }
46179
+ }, async (args) => {
46180
+ const agent = args.agent ? args.agent : resolveIdentity(undefined);
46181
+ const task = setTaskPriority(args.id, args.priority, agent);
46182
+ if (!task)
46183
+ return { content: [{ type: "text", text: `Task not found: ${args.id}` }], isError: true };
46184
+ return { content: [{ type: "text", text: JSON.stringify(task) }] };
46185
+ });
46186
+ server.registerTool("delete_task", {
46187
+ description: "Delete a task. Fails if subtasks still reference it.",
46188
+ inputSchema: {
46189
+ id: exports_external.coerce.number(),
46190
+ agent: exports_external.string().optional()
46191
+ }
46192
+ }, async (args) => {
46193
+ const agent = args.agent ? args.agent : resolveIdentity(undefined);
46194
+ const deleted = deleteTask(args.id, agent);
46195
+ return { content: [{ type: "text", text: JSON.stringify({ deleted, id: args.id }) }] };
46196
+ });
46197
+ server.registerTool("add_comment", {
46198
+ description: "Add a comment to a task.",
46199
+ inputSchema: {
46200
+ task_id: exports_external.coerce.number(),
46201
+ content: exports_external.string(),
46202
+ agent: exports_external.string().optional()
46203
+ }
46204
+ }, async (args) => {
46205
+ const agent = args.agent ? args.agent : resolveIdentity(undefined);
46206
+ const comment = addComment(args.task_id, agent, args.content);
46207
+ return { content: [{ type: "text", text: JSON.stringify(comment) }] };
46208
+ });
46209
+ server.registerTool("get_comments", {
46210
+ description: "Get all comments on a task, ordered by creation time.",
46211
+ inputSchema: {
46212
+ task_id: exports_external.coerce.number()
46213
+ }
46214
+ }, async (args) => {
46215
+ const comments = getComments(args.task_id);
46216
+ return { content: [{ type: "text", text: JSON.stringify({ comments, count: comments.length }) }] };
46217
+ });
46218
+ server.registerTool("get_subtasks", {
46219
+ description: "Get direct children (subtasks) of a parent task.",
46220
+ inputSchema: {
46221
+ parent_id: exports_external.coerce.number()
46222
+ }
46223
+ }, async (args) => {
46224
+ const subtasks = getSubtasks(args.parent_id);
46225
+ return { content: [{ type: "text", text: JSON.stringify({ subtasks, count: subtasks.length }) }] };
46226
+ });
46227
+ server.registerTool("get_task_tree", {
46228
+ description: "Get a task with its full subtask tree (recursive, max depth 5).",
46229
+ inputSchema: {
46230
+ parent_id: exports_external.coerce.number(),
46231
+ max_depth: exports_external.coerce.number().optional()
46232
+ }
46233
+ }, async (args) => {
46234
+ const tree = getTaskTree(args.parent_id, args.max_depth ?? 5);
46235
+ return { content: [{ type: "text", text: JSON.stringify(tree) }] };
46236
+ });
46237
+ server.registerTool("add_dependency", {
46238
+ description: "Add a dependency: task_id depends on depends_on_id. Prevents circular dependencies. Auto-blocks if dependency not completed.",
46239
+ inputSchema: {
46240
+ task_id: exports_external.coerce.number(),
46241
+ depends_on_id: exports_external.coerce.number()
46242
+ }
46243
+ }, async (args) => {
46244
+ addDependency(args.task_id, args.depends_on_id);
46245
+ return { content: [{ type: "text", text: `Task #${args.task_id} now depends on #${args.depends_on_id}` }] };
46246
+ });
46247
+ server.registerTool("remove_dependency", {
46248
+ description: "Remove a dependency between two tasks.",
46249
+ inputSchema: {
46250
+ task_id: exports_external.coerce.number(),
46251
+ depends_on_id: exports_external.coerce.number()
46252
+ }
46253
+ }, async (args) => {
46254
+ removeDependency(args.task_id, args.depends_on_id);
46255
+ return { content: [{ type: "text", text: `Removed dependency: #${args.task_id} no longer depends on #${args.depends_on_id}` }] };
46256
+ });
46257
+ server.registerTool("get_dependencies", {
46258
+ description: "Get tasks that this task depends on (what must be completed first).",
46259
+ inputSchema: {
46260
+ task_id: exports_external.coerce.number()
46261
+ }
46262
+ }, async (args) => {
46263
+ const deps = getDependencies(args.task_id);
46264
+ return { content: [{ type: "text", text: JSON.stringify({ dependencies: deps, count: deps.length }) }] };
46265
+ });
46266
+ server.registerTool("get_dependents", {
46267
+ description: "Get tasks that depend on this task (what is blocked by this).",
46268
+ inputSchema: {
46269
+ task_id: exports_external.coerce.number()
46270
+ }
46271
+ }, async (args) => {
46272
+ const deps = getDependents(args.task_id);
46273
+ return { content: [{ type: "text", text: JSON.stringify({ dependents: deps, count: deps.length }) }] };
46274
+ });
46275
+ server.registerTool("get_task_activity", {
46276
+ description: "Get activity log for a task: status changes, comments, dependency changes.",
46277
+ inputSchema: {
46278
+ task_id: exports_external.coerce.number(),
46279
+ limit: exports_external.coerce.number().optional()
46280
+ }
46281
+ }, async (args) => {
46282
+ const activity = getTaskActivity(args.task_id, args.limit ?? 50);
46283
+ return { content: [{ type: "text", text: JSON.stringify({ activity, count: activity.length }) }] };
46284
+ });
46285
+ server.registerTool("get_due_tasks", {
46286
+ description: "Get tasks with approaching or past due dates. Returns tasks that are overdue, due today, or due within the specified window (default 24h). Ordered by due_at ascending. Excludes completed and cancelled tasks.",
46287
+ inputSchema: {
46288
+ window_hours: exports_external.coerce.number().optional()
46289
+ }
46290
+ }, async (args) => {
46291
+ const due = getDueTasks({ window_hours: args.window_hours });
46292
+ return { content: [{ type: "text", text: JSON.stringify({ tasks: due, count: due.length }) }] };
46293
+ });
46294
+ server.registerTool("get_task_summary", {
46295
+ description: "Get a structured summary of a task including progress metrics, recent activity, blockers, and dependents. Returns subtask progress, dependency progress, completion percentage, and recent activity log.",
46296
+ inputSchema: {
46297
+ id: exports_external.coerce.number().optional(),
46298
+ uuid: exports_external.string().optional()
46299
+ }
46300
+ }, async (args) => {
46301
+ const lookup = args.id ?? args.uuid;
46302
+ if (!lookup)
46303
+ return { content: [{ type: "text", text: "id or uuid required" }], isError: true };
46304
+ const summary = getTaskSummary(lookup);
46305
+ if (!summary)
46306
+ return { content: [{ type: "text", text: `Task not found: ${lookup}` }], isError: true };
46307
+ return { content: [{ type: "text", text: JSON.stringify(summary) }] };
46308
+ });
46309
+ server.registerTool("search_tasks", {
46310
+ description: "Search tasks using full-text search on subject, description, and tags. Supports phrase queries (quoted) and prefix matching. Optional filters: status, assignee, project_id, space, priority. Use sort='relevance' (default) or 'recent'.",
46311
+ inputSchema: {
46312
+ query: exports_external.string(),
46313
+ status: exports_external.enum(["pending", "in_progress", "completed", "cancelled", "blocked"]).optional(),
46314
+ assignee: exports_external.string().optional(),
46315
+ project_id: exports_external.string().optional(),
46316
+ space: exports_external.string().optional(),
46317
+ priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
46318
+ limit: exports_external.coerce.number().optional(),
46319
+ sort: exports_external.enum(["relevance", "recent"]).optional(),
46320
+ include_archived: exports_external.coerce.boolean().optional()
46321
+ }
46322
+ }, async (args) => {
46323
+ const results = searchTasks({ query: args.query, ...args });
46324
+ return { content: [{ type: "text", text: JSON.stringify({ tasks: results, count: results.length }) }] };
46325
+ });
46326
+ }
46327
+
46328
+ // node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js
46329
+ class WebStandardStreamableHTTPServerTransport {
46330
+ constructor(options = {}) {
46331
+ this._started = false;
46332
+ this._hasHandledRequest = false;
46333
+ this._streamMapping = new Map;
46334
+ this._requestToStreamMapping = new Map;
46335
+ this._requestResponseMap = new Map;
46336
+ this._initialized = false;
46337
+ this._enableJsonResponse = false;
46338
+ this._standaloneSseStreamId = "_GET_stream";
46339
+ this.sessionIdGenerator = options.sessionIdGenerator;
46340
+ this._enableJsonResponse = options.enableJsonResponse ?? false;
46341
+ this._eventStore = options.eventStore;
46342
+ this._onsessioninitialized = options.onsessioninitialized;
46343
+ this._onsessionclosed = options.onsessionclosed;
46344
+ this._allowedHosts = options.allowedHosts;
46345
+ this._allowedOrigins = options.allowedOrigins;
46346
+ this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false;
46347
+ this._retryInterval = options.retryInterval;
46348
+ }
46349
+ async start() {
46350
+ if (this._started) {
46351
+ throw new Error("Transport already started");
46352
+ }
46353
+ this._started = true;
46354
+ }
46355
+ createJsonErrorResponse(status, code, message, options) {
46356
+ const error48 = { code, message };
46357
+ if (options?.data !== undefined) {
46358
+ error48.data = options.data;
46359
+ }
46360
+ return new Response(JSON.stringify({
46361
+ jsonrpc: "2.0",
46362
+ error: error48,
46363
+ id: null
46364
+ }), {
46365
+ status,
46366
+ headers: {
46367
+ "Content-Type": "application/json",
46368
+ ...options?.headers
46369
+ }
46370
+ });
46371
+ }
46372
+ validateRequestHeaders(req) {
46373
+ if (!this._enableDnsRebindingProtection) {
46374
+ return;
46375
+ }
46376
+ if (this._allowedHosts && this._allowedHosts.length > 0) {
46377
+ const hostHeader = req.headers.get("host");
46378
+ if (!hostHeader || !this._allowedHosts.includes(hostHeader)) {
46379
+ const error48 = `Invalid Host header: ${hostHeader}`;
46380
+ this.onerror?.(new Error(error48));
46381
+ return this.createJsonErrorResponse(403, -32000, error48);
46382
+ }
46383
+ }
46384
+ if (this._allowedOrigins && this._allowedOrigins.length > 0) {
46385
+ const originHeader = req.headers.get("origin");
46386
+ if (originHeader && !this._allowedOrigins.includes(originHeader)) {
46387
+ const error48 = `Invalid Origin header: ${originHeader}`;
46388
+ this.onerror?.(new Error(error48));
46389
+ return this.createJsonErrorResponse(403, -32000, error48);
46390
+ }
46391
+ }
46392
+ return;
46393
+ }
46394
+ async handleRequest(req, options) {
46395
+ if (!this.sessionIdGenerator && this._hasHandledRequest) {
46396
+ throw new Error("Stateless transport cannot be reused across requests. Create a new transport per request.");
46397
+ }
46398
+ this._hasHandledRequest = true;
46399
+ const validationError = this.validateRequestHeaders(req);
46400
+ if (validationError) {
46401
+ return validationError;
46402
+ }
46403
+ switch (req.method) {
46404
+ case "POST":
46405
+ return this.handlePostRequest(req, options);
46406
+ case "GET":
46407
+ return this.handleGetRequest(req);
46408
+ case "DELETE":
46409
+ return this.handleDeleteRequest(req);
46410
+ default:
46411
+ return this.handleUnsupportedRequest();
46412
+ }
46413
+ }
46414
+ async writePrimingEvent(controller, encoder, streamId, protocolVersion) {
46415
+ if (!this._eventStore) {
46416
+ return;
46417
+ }
46418
+ if (protocolVersion < "2025-11-25") {
46419
+ return;
46420
+ }
46421
+ const primingEventId = await this._eventStore.storeEvent(streamId, {});
46422
+ let primingEvent = `id: ${primingEventId}
46423
+ data:
46424
+
46425
+ `;
46426
+ if (this._retryInterval !== undefined) {
46427
+ primingEvent = `id: ${primingEventId}
46428
+ retry: ${this._retryInterval}
46429
+ data:
46430
+
46431
+ `;
46432
+ }
46433
+ controller.enqueue(encoder.encode(primingEvent));
46434
+ }
46435
+ async handleGetRequest(req) {
46436
+ const acceptHeader = req.headers.get("accept");
46437
+ if (!acceptHeader?.includes("text/event-stream")) {
46438
+ return this.createJsonErrorResponse(406, -32000, "Not Acceptable: Client must accept text/event-stream");
46439
+ }
46440
+ const sessionError = this.validateSession(req);
46441
+ if (sessionError) {
46442
+ return sessionError;
46443
+ }
46444
+ const protocolError = this.validateProtocolVersion(req);
46445
+ if (protocolError) {
46446
+ return protocolError;
46447
+ }
46448
+ if (this._eventStore) {
46449
+ const lastEventId = req.headers.get("last-event-id");
46450
+ if (lastEventId) {
46451
+ return this.replayEvents(lastEventId);
46452
+ }
46453
+ }
46454
+ if (this._streamMapping.get(this._standaloneSseStreamId) !== undefined) {
46455
+ return this.createJsonErrorResponse(409, -32000, "Conflict: Only one SSE stream is allowed per session");
46456
+ }
46457
+ const encoder = new TextEncoder;
46458
+ let streamController;
46459
+ const readable = new ReadableStream({
46460
+ start: (controller) => {
46461
+ streamController = controller;
46462
+ },
46463
+ cancel: () => {
46464
+ this._streamMapping.delete(this._standaloneSseStreamId);
46465
+ }
46466
+ });
46467
+ const headers = {
46468
+ "Content-Type": "text/event-stream",
46469
+ "Cache-Control": "no-cache, no-transform",
46470
+ Connection: "keep-alive"
46471
+ };
46472
+ if (this.sessionId !== undefined) {
46473
+ headers["mcp-session-id"] = this.sessionId;
46474
+ }
46475
+ this._streamMapping.set(this._standaloneSseStreamId, {
46476
+ controller: streamController,
46477
+ encoder,
46478
+ cleanup: () => {
46479
+ this._streamMapping.delete(this._standaloneSseStreamId);
46480
+ try {
46481
+ streamController.close();
46482
+ } catch {}
46483
+ }
46484
+ });
46485
+ return new Response(readable, { headers });
46486
+ }
46487
+ async replayEvents(lastEventId) {
46488
+ if (!this._eventStore) {
46489
+ return this.createJsonErrorResponse(400, -32000, "Event store not configured");
46490
+ }
46491
+ try {
46492
+ let streamId;
46493
+ if (this._eventStore.getStreamIdForEventId) {
46494
+ streamId = await this._eventStore.getStreamIdForEventId(lastEventId);
46495
+ if (!streamId) {
46496
+ return this.createJsonErrorResponse(400, -32000, "Invalid event ID format");
46497
+ }
46498
+ if (this._streamMapping.get(streamId) !== undefined) {
46499
+ return this.createJsonErrorResponse(409, -32000, "Conflict: Stream already has an active connection");
46500
+ }
46501
+ }
46502
+ const headers = {
46503
+ "Content-Type": "text/event-stream",
46504
+ "Cache-Control": "no-cache, no-transform",
46505
+ Connection: "keep-alive"
46506
+ };
46507
+ if (this.sessionId !== undefined) {
46508
+ headers["mcp-session-id"] = this.sessionId;
46509
+ }
46510
+ const encoder = new TextEncoder;
46511
+ let streamController;
46512
+ const readable = new ReadableStream({
46513
+ start: (controller) => {
46514
+ streamController = controller;
46515
+ },
46516
+ cancel: () => {}
46517
+ });
46518
+ const replayedStreamId = await this._eventStore.replayEventsAfter(lastEventId, {
46519
+ send: async (eventId, message) => {
46520
+ const success2 = this.writeSSEEvent(streamController, encoder, message, eventId);
46521
+ if (!success2) {
46522
+ this.onerror?.(new Error("Failed replay events"));
46523
+ try {
46524
+ streamController.close();
46525
+ } catch {}
46526
+ }
46527
+ }
46528
+ });
46529
+ this._streamMapping.set(replayedStreamId, {
46530
+ controller: streamController,
46531
+ encoder,
46532
+ cleanup: () => {
46533
+ this._streamMapping.delete(replayedStreamId);
46534
+ try {
46535
+ streamController.close();
46536
+ } catch {}
46537
+ }
46538
+ });
46539
+ return new Response(readable, { headers });
46540
+ } catch (error48) {
46541
+ this.onerror?.(error48);
46542
+ return this.createJsonErrorResponse(500, -32000, "Error replaying events");
46543
+ }
46544
+ }
46545
+ writeSSEEvent(controller, encoder, message, eventId) {
46546
+ try {
46547
+ let eventData = `event: message
46548
+ `;
46549
+ if (eventId) {
46550
+ eventData += `id: ${eventId}
46551
+ `;
46552
+ }
46553
+ eventData += `data: ${JSON.stringify(message)}
46554
+
46555
+ `;
46556
+ controller.enqueue(encoder.encode(eventData));
46557
+ return true;
46558
+ } catch {
46559
+ return false;
46560
+ }
46561
+ }
46562
+ handleUnsupportedRequest() {
46563
+ return new Response(JSON.stringify({
46564
+ jsonrpc: "2.0",
46565
+ error: {
46566
+ code: -32000,
46567
+ message: "Method not allowed."
46568
+ },
46569
+ id: null
46570
+ }), {
46571
+ status: 405,
46572
+ headers: {
46573
+ Allow: "GET, POST, DELETE",
46574
+ "Content-Type": "application/json"
46575
+ }
46576
+ });
46577
+ }
46578
+ async handlePostRequest(req, options) {
46579
+ try {
46580
+ const acceptHeader = req.headers.get("accept");
46581
+ if (!acceptHeader?.includes("application/json") || !acceptHeader.includes("text/event-stream")) {
46582
+ return this.createJsonErrorResponse(406, -32000, "Not Acceptable: Client must accept both application/json and text/event-stream");
46583
+ }
46584
+ const ct = req.headers.get("content-type");
46585
+ if (!ct || !ct.includes("application/json")) {
46586
+ return this.createJsonErrorResponse(415, -32000, "Unsupported Media Type: Content-Type must be application/json");
46587
+ }
46588
+ const requestInfo = {
46589
+ headers: Object.fromEntries(req.headers.entries())
46590
+ };
46591
+ let rawMessage;
46592
+ if (options?.parsedBody !== undefined) {
46593
+ rawMessage = options.parsedBody;
46594
+ } else {
46595
+ try {
46596
+ rawMessage = await req.json();
46597
+ } catch {
46598
+ return this.createJsonErrorResponse(400, -32700, "Parse error: Invalid JSON");
46599
+ }
46600
+ }
46601
+ let messages;
46602
+ try {
46603
+ if (Array.isArray(rawMessage)) {
46604
+ messages = rawMessage.map((msg) => JSONRPCMessageSchema.parse(msg));
46605
+ } else {
46606
+ messages = [JSONRPCMessageSchema.parse(rawMessage)];
46607
+ }
46608
+ } catch {
46609
+ return this.createJsonErrorResponse(400, -32700, "Parse error: Invalid JSON-RPC message");
46610
+ }
46611
+ const isInitializationRequest = messages.some(isInitializeRequest);
46612
+ if (isInitializationRequest) {
46613
+ if (this._initialized && this.sessionId !== undefined) {
46614
+ return this.createJsonErrorResponse(400, -32600, "Invalid Request: Server already initialized");
46615
+ }
46616
+ if (messages.length > 1) {
46617
+ return this.createJsonErrorResponse(400, -32600, "Invalid Request: Only one initialization request is allowed");
46618
+ }
46619
+ this.sessionId = this.sessionIdGenerator?.();
46620
+ this._initialized = true;
46621
+ if (this.sessionId && this._onsessioninitialized) {
46622
+ await Promise.resolve(this._onsessioninitialized(this.sessionId));
46623
+ }
46624
+ }
46625
+ if (!isInitializationRequest) {
46626
+ const sessionError = this.validateSession(req);
46627
+ if (sessionError) {
46628
+ return sessionError;
46629
+ }
46630
+ const protocolError = this.validateProtocolVersion(req);
46631
+ if (protocolError) {
46632
+ return protocolError;
46633
+ }
46634
+ }
46635
+ const hasRequests = messages.some(isJSONRPCRequest);
46636
+ if (!hasRequests) {
46637
+ for (const message of messages) {
46638
+ this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo });
46639
+ }
46640
+ return new Response(null, { status: 202 });
46641
+ }
46642
+ const streamId = crypto.randomUUID();
46643
+ const initRequest = messages.find((m) => isInitializeRequest(m));
46644
+ const clientProtocolVersion = initRequest ? initRequest.params.protocolVersion : req.headers.get("mcp-protocol-version") ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
46645
+ if (this._enableJsonResponse) {
46646
+ return new Promise((resolve2) => {
46647
+ this._streamMapping.set(streamId, {
46648
+ resolveJson: resolve2,
46649
+ cleanup: () => {
46650
+ this._streamMapping.delete(streamId);
46651
+ }
46652
+ });
46653
+ for (const message of messages) {
46654
+ if (isJSONRPCRequest(message)) {
46655
+ this._requestToStreamMapping.set(message.id, streamId);
46656
+ }
46657
+ }
46658
+ for (const message of messages) {
46659
+ this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo });
46660
+ }
46661
+ });
46662
+ }
46663
+ const encoder = new TextEncoder;
46664
+ let streamController;
46665
+ const readable = new ReadableStream({
46666
+ start: (controller) => {
46667
+ streamController = controller;
46668
+ },
46669
+ cancel: () => {
46670
+ this._streamMapping.delete(streamId);
46671
+ }
46672
+ });
46673
+ const headers = {
46674
+ "Content-Type": "text/event-stream",
46675
+ "Cache-Control": "no-cache",
46676
+ Connection: "keep-alive"
46677
+ };
46678
+ if (this.sessionId !== undefined) {
46679
+ headers["mcp-session-id"] = this.sessionId;
46680
+ }
46681
+ for (const message of messages) {
46682
+ if (isJSONRPCRequest(message)) {
46683
+ this._streamMapping.set(streamId, {
46684
+ controller: streamController,
46685
+ encoder,
46686
+ cleanup: () => {
46687
+ this._streamMapping.delete(streamId);
46688
+ try {
46689
+ streamController.close();
46690
+ } catch {}
46691
+ }
46692
+ });
46693
+ this._requestToStreamMapping.set(message.id, streamId);
46694
+ }
46695
+ }
46696
+ await this.writePrimingEvent(streamController, encoder, streamId, clientProtocolVersion);
46697
+ for (const message of messages) {
46698
+ let closeSSEStream;
46699
+ let closeStandaloneSSEStream;
46700
+ if (isJSONRPCRequest(message) && this._eventStore && clientProtocolVersion >= "2025-11-25") {
46701
+ closeSSEStream = () => {
46702
+ this.closeSSEStream(message.id);
46703
+ };
46704
+ closeStandaloneSSEStream = () => {
46705
+ this.closeStandaloneSSEStream();
46706
+ };
46707
+ }
46708
+ this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo, closeSSEStream, closeStandaloneSSEStream });
46709
+ }
46710
+ return new Response(readable, { status: 200, headers });
46711
+ } catch (error48) {
46712
+ this.onerror?.(error48);
46713
+ return this.createJsonErrorResponse(400, -32700, "Parse error", { data: String(error48) });
46714
+ }
46715
+ }
46716
+ async handleDeleteRequest(req) {
46717
+ const sessionError = this.validateSession(req);
46718
+ if (sessionError) {
46719
+ return sessionError;
46720
+ }
46721
+ const protocolError = this.validateProtocolVersion(req);
46722
+ if (protocolError) {
46723
+ return protocolError;
46724
+ }
46725
+ await Promise.resolve(this._onsessionclosed?.(this.sessionId));
46726
+ await this.close();
46727
+ return new Response(null, { status: 200 });
46728
+ }
46729
+ validateSession(req) {
46730
+ if (this.sessionIdGenerator === undefined) {
46731
+ return;
46732
+ }
46733
+ if (!this._initialized) {
46734
+ return this.createJsonErrorResponse(400, -32000, "Bad Request: Server not initialized");
46735
+ }
46736
+ const sessionId = req.headers.get("mcp-session-id");
46737
+ if (!sessionId) {
46738
+ return this.createJsonErrorResponse(400, -32000, "Bad Request: Mcp-Session-Id header is required");
46739
+ }
46740
+ if (sessionId !== this.sessionId) {
46741
+ return this.createJsonErrorResponse(404, -32001, "Session not found");
46742
+ }
46743
+ return;
46744
+ }
46745
+ validateProtocolVersion(req) {
46746
+ const protocolVersion = req.headers.get("mcp-protocol-version");
46747
+ if (protocolVersion !== null && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
46748
+ return this.createJsonErrorResponse(400, -32000, `Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")})`);
46749
+ }
46750
+ return;
46751
+ }
46752
+ async close() {
46753
+ this._streamMapping.forEach(({ cleanup }) => {
46754
+ cleanup();
46755
+ });
46756
+ this._streamMapping.clear();
46757
+ this._requestResponseMap.clear();
46758
+ this.onclose?.();
46759
+ }
46760
+ closeSSEStream(requestId) {
46761
+ const streamId = this._requestToStreamMapping.get(requestId);
46762
+ if (!streamId)
46763
+ return;
46764
+ const stream = this._streamMapping.get(streamId);
46765
+ if (stream) {
46766
+ stream.cleanup();
46767
+ }
46768
+ }
46769
+ closeStandaloneSSEStream() {
46770
+ const stream = this._streamMapping.get(this._standaloneSseStreamId);
46771
+ if (stream) {
46772
+ stream.cleanup();
46773
+ }
46774
+ }
46775
+ async send(message, options) {
46776
+ let requestId = options?.relatedRequestId;
46777
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
46778
+ requestId = message.id;
46779
+ }
46780
+ if (requestId === undefined) {
46781
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
46782
+ throw new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request");
46783
+ }
46784
+ let eventId;
46785
+ if (this._eventStore) {
46786
+ eventId = await this._eventStore.storeEvent(this._standaloneSseStreamId, message);
46787
+ }
46788
+ const standaloneSse = this._streamMapping.get(this._standaloneSseStreamId);
46789
+ if (standaloneSse === undefined) {
46790
+ return;
46791
+ }
46792
+ if (standaloneSse.controller && standaloneSse.encoder) {
46793
+ this.writeSSEEvent(standaloneSse.controller, standaloneSse.encoder, message, eventId);
46794
+ }
46795
+ return;
46796
+ }
46797
+ const streamId = this._requestToStreamMapping.get(requestId);
46798
+ if (!streamId) {
46799
+ throw new Error(`No connection established for request ID: ${String(requestId)}`);
46800
+ }
46801
+ const stream = this._streamMapping.get(streamId);
46802
+ if (!this._enableJsonResponse && stream?.controller && stream?.encoder) {
46803
+ let eventId;
46804
+ if (this._eventStore) {
46805
+ eventId = await this._eventStore.storeEvent(streamId, message);
46806
+ }
46807
+ this.writeSSEEvent(stream.controller, stream.encoder, message, eventId);
46808
+ }
46809
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
46810
+ this._requestResponseMap.set(requestId, message);
46811
+ const relatedIds = Array.from(this._requestToStreamMapping.entries()).filter(([_, sid]) => sid === streamId).map(([id]) => id);
46812
+ const allResponsesReady = relatedIds.every((id) => this._requestResponseMap.has(id));
46813
+ if (allResponsesReady) {
46814
+ if (!stream) {
46815
+ throw new Error(`No connection established for request ID: ${String(requestId)}`);
46816
+ }
46817
+ if (this._enableJsonResponse && stream.resolveJson) {
46818
+ const headers = {
46819
+ "Content-Type": "application/json"
46820
+ };
46821
+ if (this.sessionId !== undefined) {
46822
+ headers["mcp-session-id"] = this.sessionId;
46823
+ }
46824
+ const responses = relatedIds.map((id) => this._requestResponseMap.get(id));
46825
+ if (responses.length === 1) {
46826
+ stream.resolveJson(new Response(JSON.stringify(responses[0]), { status: 200, headers }));
46827
+ } else {
46828
+ stream.resolveJson(new Response(JSON.stringify(responses), { status: 200, headers }));
46829
+ }
46830
+ } else {
46831
+ stream.cleanup();
46832
+ }
46833
+ for (const id of relatedIds) {
46834
+ this._requestResponseMap.delete(id);
46835
+ this._requestToStreamMapping.delete(id);
46836
+ }
46837
+ }
46838
+ }
46839
+ }
46840
+ }
46841
+
46842
+ // src/mcp/http.ts
46843
+ var DEFAULT_MCP_HTTP_PORT = 8811;
46844
+ var MCP_HTTP_HOST = "127.0.0.1";
46845
+ var MCP_SERVICE_NAME = "conversations";
46846
+ function isHttpMode(args) {
46847
+ return args.includes("--http") || process.env.MCP_HTTP === "1";
46848
+ }
46849
+ function resolveMcpHttpPort(args) {
46850
+ const portIdx = args.indexOf("--port");
46851
+ if (portIdx >= 0 && args[portIdx + 1]) {
46852
+ return Number(args[portIdx + 1]);
46853
+ }
46854
+ const envPort = process.env.MCP_HTTP_PORT;
46855
+ if (envPort)
46856
+ return Number(envPort);
46857
+ return DEFAULT_MCP_HTTP_PORT;
46858
+ }
46859
+ function healthPayload(name = MCP_SERVICE_NAME) {
46860
+ return { status: "ok", name };
46861
+ }
46862
+ async function handleMcpRequest(req, buildServer) {
46863
+ const transport = new WebStandardStreamableHTTPServerTransport({
46864
+ sessionIdGenerator: undefined
46865
+ });
46866
+ const server = buildServer();
46867
+ await server.connect(transport);
46868
+ return transport.handleRequest(req);
46869
+ }
46870
+ function startMcpHttpServer(options) {
46871
+ const { name, port, buildServer } = options;
46872
+ const server = Bun.serve({
46873
+ hostname: MCP_HTTP_HOST,
46874
+ port,
46875
+ async fetch(req) {
46876
+ const url2 = new URL(req.url);
46877
+ if (url2.pathname === "/health" && req.method === "GET") {
46878
+ return Response.json(healthPayload(name));
46879
+ }
46880
+ if (url2.pathname === "/mcp") {
46881
+ return handleMcpRequest(req, buildServer);
46882
+ }
46883
+ return new Response("Not Found", { status: 404 });
46884
+ }
46885
+ });
46886
+ console.error(`${name}-mcp HTTP listening on http://${MCP_HTTP_HOST}:${port}/mcp`);
46887
+ return server;
46888
+ }
45138
46889
  // package.json
45139
46890
  var package_default = {
45140
46891
  name: "@hasna/conversations",
45141
- version: "0.2.47",
46892
+ version: "0.2.49",
45142
46893
  description: "Real-time CLI messaging for AI agents",
45143
46894
  type: "module",
45144
46895
  bin: {
@@ -45189,7 +46940,7 @@ var package_default = {
45189
46940
  typescript: "^5"
45190
46941
  },
45191
46942
  dependencies: {
45192
- "@hasna/cloud": "^0.1.24",
46943
+ "@hasna/cloud": "^0.1.30",
45193
46944
  "@modelcontextprotocol/sdk": "^1.26.0",
45194
46945
  chalk: "^5.3.0",
45195
46946
  commander: "^12.1.0",
@@ -45197,6 +46948,7 @@ var package_default = {
45197
46948
  "ink-select-input": "^6.0.0",
45198
46949
  "ink-spinner": "^5.0.0",
45199
46950
  "ink-text-input": "^6.0.0",
46951
+ pg: "^8.20.0",
45200
46952
  react: "^18.2.0",
45201
46953
  zod: "^4.3.6"
45202
46954
  },
@@ -45218,10 +46970,6 @@ var package_default = {
45218
46970
  };
45219
46971
 
45220
46972
  // src/mcp/index.ts
45221
- var server = new McpServer({
45222
- name: "conversations",
45223
- version: package_default.version
45224
- });
45225
46973
  var agentFocus = new Map;
45226
46974
  function getAgentFocus(agentId) {
45227
46975
  if (agentFocus.has(agentId))
@@ -45235,27 +46983,69 @@ function resolveProjectId(explicitProjectId, agentId) {
45235
46983
  const focused = getAgentFocus(agentId);
45236
46984
  return focused ?? undefined;
45237
46985
  }
45238
- registerMessagingTools(server, resolveProjectId);
45239
- registerSpaceTools(server);
45240
- registerProjectTools(server);
45241
- registerAgentTools(server, agentFocus, getAgentFocus);
45242
- registerAdvancedTools(server, package_default.version);
45243
- registerTmuxTools(server);
45244
- registerChannelBridge(server);
45245
- registerTelegramChannel(server);
46986
+ function buildServer(forHttp = false) {
46987
+ const srv = new McpServer({
46988
+ name: "conversations",
46989
+ version: package_default.version
46990
+ });
46991
+ registerMessagingTools(srv, resolveProjectId);
46992
+ registerSpaceTools(srv);
46993
+ registerProjectTools(srv);
46994
+ registerAgentTools(srv, agentFocus, getAgentFocus);
46995
+ registerAdvancedTools(srv, package_default.version);
46996
+ registerTaskTools(srv);
46997
+ registerTmuxTools(srv);
46998
+ registerCloudSyncTools(srv);
46999
+ if (!forHttp) {
47000
+ registerChannelBridge(srv);
47001
+ registerTelegramChannel(srv);
47002
+ }
47003
+ return srv;
47004
+ }
47005
+ var server = buildServer();
45246
47006
  async function startMcpServer() {
45247
47007
  const transport = new StdioServerTransport;
45248
- registerCloudSyncTools(server);
45249
47008
  await server.connect(transport);
45250
47009
  }
45251
47010
  var isDirectRun = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("mcp.js") || process.argv[1]?.endsWith("mcp.ts");
47011
+ async function main() {
47012
+ const args = process.argv.slice(2);
47013
+ if (args.includes("--help") || args.includes("-h")) {
47014
+ console.log(`conversations-mcp \u2014 MCP server for @hasna/conversations v${package_default.version}
47015
+
47016
+ Usage:
47017
+ conversations-mcp stdio transport (default)
47018
+ conversations-mcp --http Streamable HTTP on 127.0.0.1:8811
47019
+ conversations-mcp --http --port <n>
47020
+
47021
+ Environment:
47022
+ MCP_HTTP=1 Enable HTTP mode
47023
+ MCP_HTTP_PORT=<n> Override default port (8811)
47024
+ `);
47025
+ return;
47026
+ }
47027
+ if (args.includes("--version") || args.includes("-V")) {
47028
+ console.log(package_default.version);
47029
+ return;
47030
+ }
47031
+ if (isHttpMode(args)) {
47032
+ startMcpHttpServer({
47033
+ name: "conversations",
47034
+ port: resolveMcpHttpPort(args),
47035
+ buildServer: () => buildServer(true)
47036
+ });
47037
+ return;
47038
+ }
47039
+ await startMcpServer();
47040
+ }
45252
47041
  if (isDirectRun) {
45253
- startMcpServer().catch((error48) => {
47042
+ main().catch((error48) => {
45254
47043
  console.error("MCP server error:", error48);
45255
47044
  process.exit(1);
45256
47045
  });
45257
47046
  }
45258
47047
  export {
45259
47048
  startMcpServer,
45260
- server
47049
+ server,
47050
+ buildServer
45261
47051
  };