@hasna/conversations 0.2.46 → 0.2.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +5 -15
- package/README.md +14 -1
- package/bin/hook.js +94 -0
- package/bin/index.js +1860 -50
- package/bin/mcp.js +1810 -38
- package/dashboard/dist/assets/index-DhHQq3wL.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cli/brains.test.d.ts +1 -0
- package/dist/cli/commands/analytics.test.d.ts +1 -0
- package/dist/cli/commands/messaging.test.d.ts +1 -0
- package/dist/cli/commands/spaces.test.d.ts +1 -0
- package/dist/cli/commands/tmux.test.d.ts +1 -0
- package/dist/hooks/blocker-hook.test.d.ts +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +916 -15
- package/dist/index.test.d.ts +1 -0
- package/dist/lib/gatherer.test.d.ts +1 -0
- package/dist/lib/model-config.test.d.ts +1 -0
- package/dist/lib/names.test.d.ts +1 -0
- package/dist/lib/pg-migrations.test.d.ts +1 -0
- package/dist/lib/tasks.d.ts +78 -0
- package/dist/lib/tasks.test.d.ts +1 -0
- package/dist/lib/terminal-markdown.test.d.ts +1 -0
- package/dist/lib/webhooks-management.test.d.ts +1 -0
- package/dist/lib/webhooks.d.ts +46 -1
- package/dist/mcp/http.d.ts +16 -0
- package/dist/mcp/http.test.d.ts +1 -0
- package/dist/mcp/index.d.ts +3 -1
- package/dist/mcp/telegram-channel.test.d.ts +1 -0
- package/dist/mcp/tools/advanced.test.d.ts +1 -0
- package/dist/mcp/tools/agents.test.d.ts +1 -0
- package/dist/mcp/tools/messaging.test.d.ts +1 -0
- package/dist/mcp/tools/projects.test.d.ts +1 -0
- package/dist/mcp/tools/spaces.test.d.ts +1 -0
- package/dist/mcp/tools/tasks.d.ts +6 -0
- package/dist/mcp/tools/tasks.test.d.ts +1 -0
- package/dist/mcp/tools/webhooks.d.ts +6 -0
- package/dist/mcp/tools/webhooks.test.d.ts +1 -0
- package/dist/types.d.ts +120 -0
- package/package.json +3 -2
- package/dashboard/dist/assets/index-CF_GDtNp.css +0 -1
- /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
|
|
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
|
-
|
|
18510
|
-
|
|
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
|
-
|
|
18537
|
-
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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.
|
|
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.
|
|
46892
|
+
version: "0.2.48",
|
|
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.
|
|
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,51 @@ function resolveProjectId(explicitProjectId, agentId) {
|
|
|
45235
46983
|
const focused = getAgentFocus(agentId);
|
|
45236
46984
|
return focused ?? undefined;
|
|
45237
46985
|
}
|
|
45238
|
-
|
|
45239
|
-
|
|
45240
|
-
|
|
45241
|
-
|
|
45242
|
-
|
|
45243
|
-
|
|
45244
|
-
|
|
45245
|
-
|
|
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 (isHttpMode(args)) {
|
|
47014
|
+
startMcpHttpServer({
|
|
47015
|
+
name: "conversations",
|
|
47016
|
+
port: resolveMcpHttpPort(args),
|
|
47017
|
+
buildServer: () => buildServer(true)
|
|
47018
|
+
});
|
|
47019
|
+
return;
|
|
47020
|
+
}
|
|
47021
|
+
await startMcpServer();
|
|
47022
|
+
}
|
|
45252
47023
|
if (isDirectRun) {
|
|
45253
|
-
|
|
47024
|
+
main().catch((error48) => {
|
|
45254
47025
|
console.error("MCP server error:", error48);
|
|
45255
47026
|
process.exit(1);
|
|
45256
47027
|
});
|
|
45257
47028
|
}
|
|
45258
47029
|
export {
|
|
45259
47030
|
startMcpServer,
|
|
45260
|
-
server
|
|
47031
|
+
server,
|
|
47032
|
+
buildServer
|
|
45261
47033
|
};
|