@hasna/conversations 0.2.47 → 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/dist/index.js
CHANGED
|
@@ -9929,6 +9929,100 @@ function getDb() {
|
|
|
9929
9929
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
9930
9930
|
)
|
|
9931
9931
|
`);
|
|
9932
|
+
db.exec(`
|
|
9933
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
9934
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9935
|
+
uuid TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))),
|
|
9936
|
+
subject TEXT NOT NULL,
|
|
9937
|
+
description TEXT,
|
|
9938
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
9939
|
+
priority TEXT NOT NULL DEFAULT 'medium',
|
|
9940
|
+
assignee TEXT,
|
|
9941
|
+
reporter TEXT NOT NULL,
|
|
9942
|
+
project_id TEXT,
|
|
9943
|
+
space TEXT,
|
|
9944
|
+
parent_id INTEGER REFERENCES tasks(id),
|
|
9945
|
+
depends_on TEXT,
|
|
9946
|
+
tags TEXT,
|
|
9947
|
+
metadata TEXT,
|
|
9948
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
|
|
9949
|
+
started_at TEXT,
|
|
9950
|
+
completed_at TEXT,
|
|
9951
|
+
cancelled_at TEXT,
|
|
9952
|
+
due_at TEXT
|
|
9953
|
+
)
|
|
9954
|
+
`);
|
|
9955
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_uuid ON tasks(uuid)");
|
|
9956
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)");
|
|
9957
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(assignee)");
|
|
9958
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_reporter ON tasks(reporter)");
|
|
9959
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id)");
|
|
9960
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_space ON tasks(space)");
|
|
9961
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id)");
|
|
9962
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority)");
|
|
9963
|
+
db.exec(`
|
|
9964
|
+
CREATE TABLE IF NOT EXISTS task_comments (
|
|
9965
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9966
|
+
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
9967
|
+
agent TEXT NOT NULL,
|
|
9968
|
+
content TEXT NOT NULL,
|
|
9969
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
|
|
9970
|
+
)
|
|
9971
|
+
`);
|
|
9972
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_task_comments_task ON task_comments(task_id)");
|
|
9973
|
+
db.exec(`
|
|
9974
|
+
CREATE TABLE IF NOT EXISTS task_activity (
|
|
9975
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9976
|
+
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
9977
|
+
agent TEXT NOT NULL,
|
|
9978
|
+
action TEXT NOT NULL,
|
|
9979
|
+
detail TEXT,
|
|
9980
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now'))
|
|
9981
|
+
)
|
|
9982
|
+
`);
|
|
9983
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_task_activity_task ON task_activity(task_id)");
|
|
9984
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_task_activity_agent ON task_activity(agent)");
|
|
9985
|
+
db.exec(`
|
|
9986
|
+
CREATE TABLE IF NOT EXISTS task_dependencies (
|
|
9987
|
+
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
9988
|
+
depends_on_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
9989
|
+
PRIMARY KEY (task_id, depends_on_id)
|
|
9990
|
+
)
|
|
9991
|
+
`);
|
|
9992
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_task_deps_depends ON task_dependencies(depends_on_id)");
|
|
9993
|
+
const hasTasksFts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='tasks_fts'").get();
|
|
9994
|
+
if (!hasTasksFts) {
|
|
9995
|
+
db.exec(`
|
|
9996
|
+
CREATE VIRTUAL TABLE tasks_fts USING fts5(
|
|
9997
|
+
subject, description, tags
|
|
9998
|
+
)
|
|
9999
|
+
`);
|
|
10000
|
+
db.exec(`
|
|
10001
|
+
INSERT INTO tasks_fts(rowid, subject, description, tags)
|
|
10002
|
+
SELECT id, COALESCE(subject, ''), COALESCE(description, ''),
|
|
10003
|
+
COALESCE(REPLACE(REPLACE(REPLACE(tags, '[', ''), ']', ''), '"', ''), '')
|
|
10004
|
+
FROM tasks
|
|
10005
|
+
`);
|
|
10006
|
+
db.exec(`
|
|
10007
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_insert AFTER INSERT ON tasks BEGIN
|
|
10008
|
+
INSERT INTO tasks_fts(rowid, subject, description, tags)
|
|
10009
|
+
VALUES (new.id, COALESCE(new.subject, ''), COALESCE(new.description, ''),
|
|
10010
|
+
COALESCE(REPLACE(REPLACE(REPLACE(new.tags, '[', ''), ']', ''), '"', ''), ''));
|
|
10011
|
+
END
|
|
10012
|
+
`);
|
|
10013
|
+
db.exec(`
|
|
10014
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_delete AFTER DELETE ON tasks BEGIN
|
|
10015
|
+
DELETE FROM tasks_fts WHERE rowid = old.id;
|
|
10016
|
+
END
|
|
10017
|
+
`);
|
|
10018
|
+
db.exec(`
|
|
10019
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_update AFTER UPDATE ON tasks BEGIN
|
|
10020
|
+
INSERT OR REPLACE INTO tasks_fts(rowid, subject, description, tags)
|
|
10021
|
+
VALUES (new.id, COALESCE(new.subject, ''), COALESCE(new.description, ''),
|
|
10022
|
+
COALESCE(REPLACE(REPLACE(REPLACE(new.tags, '[', ''), ']', ''), '"', ''), ''));
|
|
10023
|
+
END
|
|
10024
|
+
`);
|
|
10025
|
+
}
|
|
9932
10026
|
return db;
|
|
9933
10027
|
}
|
|
9934
10028
|
function closeDb() {
|
|
@@ -11756,18 +11850,22 @@ var require_react = __commonJS((exports, module) => {
|
|
|
11756
11850
|
// src/lib/messages.ts
|
|
11757
11851
|
init_db();
|
|
11758
11852
|
import { randomUUID } from "crypto";
|
|
11759
|
-
import { mkdirSync as
|
|
11853
|
+
import { mkdirSync as mkdirSync6, copyFileSync as copyFileSync3, statSync as statSync2, existsSync as existsSync7, realpathSync } from "fs";
|
|
11760
11854
|
import { join as join8, basename, resolve } from "path";
|
|
11761
11855
|
|
|
11762
11856
|
// src/lib/webhooks.ts
|
|
11763
11857
|
init_db();
|
|
11764
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
11858
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync5 } from "fs";
|
|
11765
11859
|
import { join as join7 } from "path";
|
|
11766
11860
|
import dns from "dns";
|
|
11767
11861
|
import net from "net";
|
|
11768
11862
|
var cachedConfig = null;
|
|
11769
11863
|
var configLoadedAt = 0;
|
|
11770
11864
|
var CONFIG_CACHE_MS = 1e4;
|
|
11865
|
+
function _resetConfigCache() {
|
|
11866
|
+
cachedConfig = null;
|
|
11867
|
+
configLoadedAt = 0;
|
|
11868
|
+
}
|
|
11771
11869
|
function getConfigPath2() {
|
|
11772
11870
|
return process.env.CONVERSATIONS_CONFIG_PATH || join7(getDataDir2(), "config.json");
|
|
11773
11871
|
}
|
|
@@ -11866,6 +11964,87 @@ function fireWebhooks(msg) {
|
|
|
11866
11964
|
});
|
|
11867
11965
|
}
|
|
11868
11966
|
}
|
|
11967
|
+
function fireTaskWebhooks(event) {
|
|
11968
|
+
const config = loadConfig();
|
|
11969
|
+
if (!config.webhooks || config.webhooks.length === 0)
|
|
11970
|
+
return;
|
|
11971
|
+
const taskWebhooks = config.webhooks.filter((w) => w.events.includes("task"));
|
|
11972
|
+
if (taskWebhooks.length === 0)
|
|
11973
|
+
return;
|
|
11974
|
+
for (const webhook of taskWebhooks) {
|
|
11975
|
+
if (webhook.agent && event.agent !== webhook.agent)
|
|
11976
|
+
continue;
|
|
11977
|
+
validateWebhookUrl(webhook.url).then((valid) => {
|
|
11978
|
+
if (!valid)
|
|
11979
|
+
return;
|
|
11980
|
+
fetch(webhook.url, {
|
|
11981
|
+
method: "POST",
|
|
11982
|
+
headers: { "Content-Type": "application/json" },
|
|
11983
|
+
body: JSON.stringify(event)
|
|
11984
|
+
}).catch(() => {});
|
|
11985
|
+
});
|
|
11986
|
+
}
|
|
11987
|
+
}
|
|
11988
|
+
function listWebhooks() {
|
|
11989
|
+
const config = loadConfig();
|
|
11990
|
+
return config.webhooks || [];
|
|
11991
|
+
}
|
|
11992
|
+
async function addWebhook(url, events, agent) {
|
|
11993
|
+
if (!url || !events || events.length === 0) {
|
|
11994
|
+
return { success: false, error: "url and events are required" };
|
|
11995
|
+
}
|
|
11996
|
+
const validEvents = ["dm", "blocker", "space", "mention", "task"];
|
|
11997
|
+
for (const event of events) {
|
|
11998
|
+
if (!validEvents.includes(event)) {
|
|
11999
|
+
return { success: false, error: `Invalid event "${event}". Valid events: ${validEvents.join(", ")}` };
|
|
12000
|
+
}
|
|
12001
|
+
}
|
|
12002
|
+
try {
|
|
12003
|
+
new URL(url);
|
|
12004
|
+
} catch {
|
|
12005
|
+
return { success: false, error: `Invalid URL: ${url}` };
|
|
12006
|
+
}
|
|
12007
|
+
const webhook = { url, events, ...agent ? { agent } : {} };
|
|
12008
|
+
const configPath = getConfigPath2();
|
|
12009
|
+
const configDir = configPath.substring(0, configPath.lastIndexOf("/"));
|
|
12010
|
+
if (!existsSync6(configDir)) {
|
|
12011
|
+
mkdirSync5(configDir, { recursive: true });
|
|
12012
|
+
}
|
|
12013
|
+
let config = {};
|
|
12014
|
+
try {
|
|
12015
|
+
const raw = readFileSync2(configPath, "utf-8");
|
|
12016
|
+
config = JSON.parse(raw);
|
|
12017
|
+
} catch {}
|
|
12018
|
+
if (!config.webhooks)
|
|
12019
|
+
config.webhooks = [];
|
|
12020
|
+
if (config.webhooks.some((w) => w.url === url && w.agent === agent && JSON.stringify(w.events.sort()) === JSON.stringify(events.sort()))) {
|
|
12021
|
+
return { success: false, error: "Webhook already exists" };
|
|
12022
|
+
}
|
|
12023
|
+
config.webhooks.push(webhook);
|
|
12024
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
12025
|
+
_resetConfigCache();
|
|
12026
|
+
return { success: true, webhook, index: config.webhooks.length - 1 };
|
|
12027
|
+
}
|
|
12028
|
+
function removeWebhook(index) {
|
|
12029
|
+
const configPath = getConfigPath2();
|
|
12030
|
+
let config = {};
|
|
12031
|
+
try {
|
|
12032
|
+
const raw = readFileSync2(configPath, "utf-8");
|
|
12033
|
+
config = JSON.parse(raw);
|
|
12034
|
+
} catch {
|
|
12035
|
+
return { success: false, error: "No config file found" };
|
|
12036
|
+
}
|
|
12037
|
+
if (!config.webhooks || config.webhooks.length === 0) {
|
|
12038
|
+
return { success: false, error: "No webhooks configured" };
|
|
12039
|
+
}
|
|
12040
|
+
if (index < 0 || index >= config.webhooks.length) {
|
|
12041
|
+
return { success: false, error: `Invalid index: ${index}. Valid range: 0-${config.webhooks.length - 1}` };
|
|
12042
|
+
}
|
|
12043
|
+
const removed = config.webhooks.splice(index, 1)[0];
|
|
12044
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
12045
|
+
_resetConfigCache();
|
|
12046
|
+
return { success: true, removed };
|
|
12047
|
+
}
|
|
11869
12048
|
|
|
11870
12049
|
// src/lib/messages.ts
|
|
11871
12050
|
function compactMessage(msg) {
|
|
@@ -11910,7 +12089,7 @@ function getAttachmentsDir() {
|
|
|
11910
12089
|
}
|
|
11911
12090
|
function validateAttachment(sourcePath, name) {
|
|
11912
12091
|
const absolute = resolve(sourcePath);
|
|
11913
|
-
if (!
|
|
12092
|
+
if (!existsSync7(absolute)) {
|
|
11914
12093
|
throw new Error(`Attachment source not found: ${sourcePath}`);
|
|
11915
12094
|
}
|
|
11916
12095
|
const real = realpathSync(absolute);
|
|
@@ -11992,7 +12171,7 @@ function sendMessage(opts) {
|
|
|
11992
12171
|
const message = parseMessage(row);
|
|
11993
12172
|
if (opts.attachments && opts.attachments.length > 0) {
|
|
11994
12173
|
const attachmentsDir = join8(getAttachmentsDir(), String(message.id));
|
|
11995
|
-
|
|
12174
|
+
mkdirSync6(attachmentsDir, { recursive: true });
|
|
11996
12175
|
const attachmentInfos = [];
|
|
11997
12176
|
for (const att of opts.attachments) {
|
|
11998
12177
|
const { safeSource, safeName } = validateAttachment(att.source_path, att.name);
|
|
@@ -12987,7 +13166,7 @@ function useSpaceMessages(spaceName) {
|
|
|
12987
13166
|
return messages;
|
|
12988
13167
|
}
|
|
12989
13168
|
// src/lib/identity.ts
|
|
12990
|
-
import { readFileSync as readFileSync3, writeFileSync as
|
|
13169
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
|
|
12991
13170
|
import { join as join9, dirname as dirname3 } from "path";
|
|
12992
13171
|
|
|
12993
13172
|
// src/lib/names.ts
|
|
@@ -13373,8 +13552,8 @@ function getAutoName() {
|
|
|
13373
13552
|
}
|
|
13374
13553
|
cachedAutoName = name;
|
|
13375
13554
|
try {
|
|
13376
|
-
|
|
13377
|
-
|
|
13555
|
+
mkdirSync7(dirname3(AGENT_ID_FILE), { recursive: true });
|
|
13556
|
+
writeFileSync3(AGENT_ID_FILE, name + `
|
|
13378
13557
|
`, "utf-8");
|
|
13379
13558
|
} catch {}
|
|
13380
13559
|
return name;
|
|
@@ -14333,25 +14512,29 @@ var gatherTrainingData = async (options = {}) => {
|
|
|
14333
14512
|
};
|
|
14334
14513
|
// src/lib/model-config.ts
|
|
14335
14514
|
init_db();
|
|
14336
|
-
import { readFileSync as readFileSync4, writeFileSync as
|
|
14515
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync8, existsSync as existsSync8 } from "fs";
|
|
14337
14516
|
import { join as join10 } from "path";
|
|
14338
14517
|
var DEFAULT_MODEL = "gpt-4o-mini";
|
|
14339
|
-
|
|
14340
|
-
|
|
14518
|
+
function getConfigPath3() {
|
|
14519
|
+
return process.env.CONVERSATIONS_CONFIG_PATH || join10(getDataDir2(), "config.json");
|
|
14520
|
+
}
|
|
14341
14521
|
function readConfig() {
|
|
14342
|
-
|
|
14522
|
+
const path = getConfigPath3();
|
|
14523
|
+
if (!existsSync8(path))
|
|
14343
14524
|
return {};
|
|
14344
14525
|
try {
|
|
14345
|
-
return JSON.parse(readFileSync4(
|
|
14526
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
14346
14527
|
} catch {
|
|
14347
14528
|
return {};
|
|
14348
14529
|
}
|
|
14349
14530
|
}
|
|
14350
14531
|
function writeConfig(config) {
|
|
14351
|
-
|
|
14352
|
-
|
|
14532
|
+
const path = getConfigPath3();
|
|
14533
|
+
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
14534
|
+
if (!existsSync8(dir)) {
|
|
14535
|
+
mkdirSync8(dir, { recursive: true });
|
|
14353
14536
|
}
|
|
14354
|
-
|
|
14537
|
+
writeFileSync4(path, JSON.stringify(config, null, 2), "utf-8");
|
|
14355
14538
|
}
|
|
14356
14539
|
function getActiveModel() {
|
|
14357
14540
|
const config = readConfig();
|
|
@@ -14367,6 +14550,696 @@ function clearActiveModel() {
|
|
|
14367
14550
|
delete config.activeModel;
|
|
14368
14551
|
writeConfig(config);
|
|
14369
14552
|
}
|
|
14553
|
+
// src/lib/tasks.ts
|
|
14554
|
+
init_db();
|
|
14555
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
14556
|
+
function parseTask(row) {
|
|
14557
|
+
let dependsOn = null;
|
|
14558
|
+
if (row.depends_on) {
|
|
14559
|
+
try {
|
|
14560
|
+
dependsOn = JSON.parse(row.depends_on);
|
|
14561
|
+
} catch {
|
|
14562
|
+
dependsOn = null;
|
|
14563
|
+
}
|
|
14564
|
+
}
|
|
14565
|
+
let tags = null;
|
|
14566
|
+
if (row.tags) {
|
|
14567
|
+
try {
|
|
14568
|
+
tags = JSON.parse(row.tags);
|
|
14569
|
+
} catch {
|
|
14570
|
+
tags = null;
|
|
14571
|
+
}
|
|
14572
|
+
}
|
|
14573
|
+
let metadata = null;
|
|
14574
|
+
if (row.metadata) {
|
|
14575
|
+
try {
|
|
14576
|
+
metadata = JSON.parse(row.metadata);
|
|
14577
|
+
} catch {
|
|
14578
|
+
metadata = null;
|
|
14579
|
+
}
|
|
14580
|
+
}
|
|
14581
|
+
return {
|
|
14582
|
+
id: row.id,
|
|
14583
|
+
uuid: row.uuid,
|
|
14584
|
+
subject: row.subject,
|
|
14585
|
+
description: row.description || null,
|
|
14586
|
+
status: row.status,
|
|
14587
|
+
priority: row.priority,
|
|
14588
|
+
assignee: row.assignee || null,
|
|
14589
|
+
reporter: row.reporter,
|
|
14590
|
+
project_id: row.project_id || null,
|
|
14591
|
+
space: row.space || null,
|
|
14592
|
+
parent_id: row.parent_id || null,
|
|
14593
|
+
depends_on: dependsOn,
|
|
14594
|
+
tags,
|
|
14595
|
+
metadata,
|
|
14596
|
+
created_at: row.created_at,
|
|
14597
|
+
started_at: row.started_at || null,
|
|
14598
|
+
completed_at: row.completed_at || null,
|
|
14599
|
+
cancelled_at: row.cancelled_at || null,
|
|
14600
|
+
due_at: row.due_at || null
|
|
14601
|
+
};
|
|
14602
|
+
}
|
|
14603
|
+
function logActivity(taskId, agent, action, detail) {
|
|
14604
|
+
const db2 = getDb();
|
|
14605
|
+
db2.prepare("INSERT INTO task_activity (task_id, agent, action, detail) VALUES (?, ?, ?, ?)").run(taskId, agent, action, detail || null);
|
|
14606
|
+
}
|
|
14607
|
+
function emitTaskEvent(task, action, agent, oldStatus, detail) {
|
|
14608
|
+
fireTaskWebhooks({
|
|
14609
|
+
task_id: task.id,
|
|
14610
|
+
task_uuid: task.uuid,
|
|
14611
|
+
subject: task.subject,
|
|
14612
|
+
action,
|
|
14613
|
+
old_status: oldStatus,
|
|
14614
|
+
new_status: task.status,
|
|
14615
|
+
agent,
|
|
14616
|
+
detail,
|
|
14617
|
+
priority: task.priority,
|
|
14618
|
+
assignee: task.assignee,
|
|
14619
|
+
project_id: task.project_id,
|
|
14620
|
+
created_at: task.created_at
|
|
14621
|
+
});
|
|
14622
|
+
}
|
|
14623
|
+
function createTask(opts) {
|
|
14624
|
+
const db2 = getDb();
|
|
14625
|
+
const uuid = randomUUID3().replace(/-/g, "");
|
|
14626
|
+
const priority = opts.priority || "medium";
|
|
14627
|
+
const description = opts.description || null;
|
|
14628
|
+
const assignee = opts.assignee || null;
|
|
14629
|
+
const project_id = opts.project_id || null;
|
|
14630
|
+
const space = opts.space || null;
|
|
14631
|
+
const parent_id = opts.parent_id || null;
|
|
14632
|
+
const tags = opts.tags ? JSON.stringify(opts.tags) : null;
|
|
14633
|
+
const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
|
|
14634
|
+
const due_at = opts.due_at || null;
|
|
14635
|
+
const row = db2.prepare(`
|
|
14636
|
+
INSERT INTO tasks (uuid, subject, description, reporter, assignee, priority, project_id, space, parent_id, tags, metadata, due_at)
|
|
14637
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
14638
|
+
RETURNING *
|
|
14639
|
+
`).get(uuid, opts.subject, description, opts.reporter, assignee, priority, project_id, space, parent_id, tags, metadata, due_at);
|
|
14640
|
+
const task = parseTask(row);
|
|
14641
|
+
if (opts.depends_on && opts.depends_on.length > 0) {
|
|
14642
|
+
const depIds = opts.depends_on;
|
|
14643
|
+
const insertDep = db2.prepare("INSERT INTO task_dependencies (task_id, depends_on_id) VALUES (?, ?)");
|
|
14644
|
+
const depIdsResolved = [];
|
|
14645
|
+
for (const depId of depIds) {
|
|
14646
|
+
const exists = db2.prepare("SELECT id, status FROM tasks WHERE id = ?").get(depId);
|
|
14647
|
+
if (!exists)
|
|
14648
|
+
throw new Error(`Dependency task #${depId} not found`);
|
|
14649
|
+
insertDep.run(task.id, depId);
|
|
14650
|
+
depIdsResolved.push(depId);
|
|
14651
|
+
}
|
|
14652
|
+
db2.prepare("UPDATE tasks SET depends_on = ? WHERE id = ?").run(JSON.stringify(depIdsResolved), task.id);
|
|
14653
|
+
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);
|
|
14654
|
+
if (incompleteDeps.length > 0) {
|
|
14655
|
+
db2.prepare("UPDATE tasks SET status = 'blocked' WHERE id = ?").run(task.id);
|
|
14656
|
+
}
|
|
14657
|
+
}
|
|
14658
|
+
logActivity(task.id, opts.reporter, "created");
|
|
14659
|
+
const created = parseTask(db2.prepare("SELECT * FROM tasks WHERE id = ?").get(task.id));
|
|
14660
|
+
fireTaskWebhooks({
|
|
14661
|
+
task_id: created.id,
|
|
14662
|
+
task_uuid: created.uuid,
|
|
14663
|
+
subject: created.subject,
|
|
14664
|
+
action: "created",
|
|
14665
|
+
new_status: created.status,
|
|
14666
|
+
agent: opts.reporter,
|
|
14667
|
+
priority: created.priority,
|
|
14668
|
+
assignee: created.assignee,
|
|
14669
|
+
project_id: created.project_id,
|
|
14670
|
+
created_at: created.created_at
|
|
14671
|
+
});
|
|
14672
|
+
return created;
|
|
14673
|
+
}
|
|
14674
|
+
function getTask(idOrUuid) {
|
|
14675
|
+
const db2 = getDb();
|
|
14676
|
+
let row = null;
|
|
14677
|
+
if (typeof idOrUuid === "number") {
|
|
14678
|
+
row = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(idOrUuid);
|
|
14679
|
+
} else {
|
|
14680
|
+
row = db2.prepare("SELECT * FROM tasks WHERE uuid = ?").get(idOrUuid);
|
|
14681
|
+
}
|
|
14682
|
+
if (!row)
|
|
14683
|
+
return null;
|
|
14684
|
+
return enrichTask(row);
|
|
14685
|
+
}
|
|
14686
|
+
function listTasks(opts = {}) {
|
|
14687
|
+
const db2 = getDb();
|
|
14688
|
+
const conditions = [];
|
|
14689
|
+
const params = [];
|
|
14690
|
+
if (opts.status) {
|
|
14691
|
+
conditions.push("t.status = ?");
|
|
14692
|
+
params.push(opts.status);
|
|
14693
|
+
}
|
|
14694
|
+
if (opts.assignee) {
|
|
14695
|
+
conditions.push("t.assignee = ?");
|
|
14696
|
+
params.push(opts.assignee);
|
|
14697
|
+
}
|
|
14698
|
+
if (opts.reporter) {
|
|
14699
|
+
conditions.push("t.reporter = ?");
|
|
14700
|
+
params.push(opts.reporter);
|
|
14701
|
+
}
|
|
14702
|
+
if (opts.project_id) {
|
|
14703
|
+
conditions.push("t.project_id = ?");
|
|
14704
|
+
params.push(opts.project_id);
|
|
14705
|
+
}
|
|
14706
|
+
if (opts.space) {
|
|
14707
|
+
conditions.push("t.space = ?");
|
|
14708
|
+
params.push(opts.space);
|
|
14709
|
+
}
|
|
14710
|
+
if (opts.priority) {
|
|
14711
|
+
conditions.push("t.priority = ?");
|
|
14712
|
+
params.push(opts.priority);
|
|
14713
|
+
}
|
|
14714
|
+
if (opts.tag) {
|
|
14715
|
+
conditions.push("t.tags LIKE ?");
|
|
14716
|
+
params.push(`%"${opts.tag}"%`);
|
|
14717
|
+
}
|
|
14718
|
+
if (opts.tags && opts.tags.length > 0) {
|
|
14719
|
+
for (const tag of opts.tags) {
|
|
14720
|
+
conditions.push("t.tags LIKE ?");
|
|
14721
|
+
params.push(`%"${tag}"%`);
|
|
14722
|
+
}
|
|
14723
|
+
}
|
|
14724
|
+
if (opts.metadata && Object.keys(opts.metadata).length > 0) {
|
|
14725
|
+
for (const [key, value] of Object.entries(opts.metadata)) {
|
|
14726
|
+
if (typeof value === "string") {
|
|
14727
|
+
conditions.push(`t.metadata LIKE ?`);
|
|
14728
|
+
params.push(`%"${key}":"${value}"%`);
|
|
14729
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
14730
|
+
conditions.push(`t.metadata LIKE ?`);
|
|
14731
|
+
params.push(`%"${key}":${value}%`);
|
|
14732
|
+
} else {
|
|
14733
|
+
conditions.push(`t.metadata LIKE ?`);
|
|
14734
|
+
params.push(`%"${key}"%`);
|
|
14735
|
+
}
|
|
14736
|
+
}
|
|
14737
|
+
}
|
|
14738
|
+
if (opts.parent_id === null) {
|
|
14739
|
+
conditions.push("t.parent_id IS NULL");
|
|
14740
|
+
} else if (typeof opts.parent_id === "number") {
|
|
14741
|
+
conditions.push("t.parent_id = ?");
|
|
14742
|
+
params.push(opts.parent_id);
|
|
14743
|
+
}
|
|
14744
|
+
if (!opts.include_archived) {
|
|
14745
|
+
conditions.push("t.status != 'cancelled'");
|
|
14746
|
+
}
|
|
14747
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
14748
|
+
const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 50;
|
|
14749
|
+
const offset = Number.isFinite(opts.offset) && opts.offset >= 0 ? Math.floor(opts.offset) : 0;
|
|
14750
|
+
const rows = db2.prepare(`
|
|
14751
|
+
SELECT t.* FROM tasks t
|
|
14752
|
+
${where}
|
|
14753
|
+
ORDER BY
|
|
14754
|
+
CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
14755
|
+
t.created_at DESC
|
|
14756
|
+
LIMIT ? OFFSET ?
|
|
14757
|
+
`).all(...params, limit, offset);
|
|
14758
|
+
return rows.map(enrichTask);
|
|
14759
|
+
}
|
|
14760
|
+
function startTask(id, agent) {
|
|
14761
|
+
const db2 = getDb();
|
|
14762
|
+
const task = resolveTask(id);
|
|
14763
|
+
if (!task)
|
|
14764
|
+
return null;
|
|
14765
|
+
const incompleteDeps = db2.prepare(`
|
|
14766
|
+
SELECT td.depends_on_id, t.subject, t.status
|
|
14767
|
+
FROM task_dependencies td
|
|
14768
|
+
JOIN tasks t ON t.id = td.depends_on_id
|
|
14769
|
+
WHERE td.task_id = ? AND t.status != 'completed'
|
|
14770
|
+
`).all(task.id);
|
|
14771
|
+
if (incompleteDeps.length > 0) {
|
|
14772
|
+
throw new Error(`Cannot start: blocked by ${incompleteDeps.length} incomplete task(s): ${incompleteDeps.map((d) => `#${d.depends_on_id} "${d.subject}" (${d.status})`).join(", ")}`);
|
|
14773
|
+
}
|
|
14774
|
+
const now = new Date().toISOString();
|
|
14775
|
+
const oldStatus = task.status;
|
|
14776
|
+
db2.prepare("UPDATE tasks SET status = 'in_progress', started_at = ? WHERE id = ?").run(now, task.id);
|
|
14777
|
+
logActivity(task.id, agent || task.reporter, "started");
|
|
14778
|
+
const updated = getTaskById(task.id);
|
|
14779
|
+
if (updated)
|
|
14780
|
+
emitTaskEvent(updated, "started", agent || task.reporter, oldStatus);
|
|
14781
|
+
return updated;
|
|
14782
|
+
}
|
|
14783
|
+
function completeTask(id, agent, opts) {
|
|
14784
|
+
const db2 = getDb();
|
|
14785
|
+
const task = resolveTask(id);
|
|
14786
|
+
if (!task)
|
|
14787
|
+
return null;
|
|
14788
|
+
const now = new Date().toISOString();
|
|
14789
|
+
const oldStatus = task.status;
|
|
14790
|
+
db2.prepare("UPDATE tasks SET status = 'completed', completed_at = ? WHERE id = ?").run(now, task.id);
|
|
14791
|
+
logActivity(task.id, agent || task.reporter, "completed", opts?.evidence);
|
|
14792
|
+
unblockDependents(task.id);
|
|
14793
|
+
const updated = getTaskById(task.id);
|
|
14794
|
+
if (updated)
|
|
14795
|
+
emitTaskEvent(updated, "completed", agent || task.reporter, oldStatus, opts?.evidence);
|
|
14796
|
+
return updated;
|
|
14797
|
+
}
|
|
14798
|
+
function cancelTask(id, agent, opts) {
|
|
14799
|
+
const db2 = getDb();
|
|
14800
|
+
const task = resolveTask(id);
|
|
14801
|
+
if (!task)
|
|
14802
|
+
return null;
|
|
14803
|
+
const now = new Date().toISOString();
|
|
14804
|
+
const oldStatus = task.status;
|
|
14805
|
+
db2.prepare("UPDATE tasks SET status = 'cancelled', cancelled_at = ? WHERE id = ?").run(now, task.id);
|
|
14806
|
+
logActivity(task.id, agent || task.reporter, "cancelled", opts?.reason);
|
|
14807
|
+
const updated = getTaskById(task.id);
|
|
14808
|
+
if (updated)
|
|
14809
|
+
emitTaskEvent(updated, "cancelled", agent || task.reporter, oldStatus, opts?.reason);
|
|
14810
|
+
return updated;
|
|
14811
|
+
}
|
|
14812
|
+
function blockTask(id, agent, opts) {
|
|
14813
|
+
const db2 = getDb();
|
|
14814
|
+
const task = resolveTask(id);
|
|
14815
|
+
if (!task)
|
|
14816
|
+
return null;
|
|
14817
|
+
const oldStatus = task.status;
|
|
14818
|
+
db2.prepare("UPDATE tasks SET status = 'blocked' WHERE id = ?").run(task.id);
|
|
14819
|
+
logActivity(task.id, agent || task.reporter, "blocked", opts?.reason);
|
|
14820
|
+
const updated = getTaskById(task.id);
|
|
14821
|
+
if (updated)
|
|
14822
|
+
emitTaskEvent(updated, "blocked", agent || task.reporter, oldStatus, opts?.reason);
|
|
14823
|
+
return updated;
|
|
14824
|
+
}
|
|
14825
|
+
function unblockTask(id, agent) {
|
|
14826
|
+
const db2 = getDb();
|
|
14827
|
+
const task = resolveTask(id);
|
|
14828
|
+
if (!task)
|
|
14829
|
+
return null;
|
|
14830
|
+
const incompleteDeps = db2.prepare(`
|
|
14831
|
+
SELECT 1 FROM task_dependencies td
|
|
14832
|
+
JOIN tasks t ON t.id = td.depends_on_id
|
|
14833
|
+
WHERE td.task_id = ? AND t.status != 'completed'
|
|
14834
|
+
LIMIT 1
|
|
14835
|
+
`).get(task.id);
|
|
14836
|
+
const oldStatus = task.status;
|
|
14837
|
+
const newStatus = incompleteDeps ? "blocked" : "pending";
|
|
14838
|
+
db2.prepare("UPDATE tasks SET status = ? WHERE id = ?").run(newStatus, task.id);
|
|
14839
|
+
logActivity(task.id, agent || task.reporter, "unblocked");
|
|
14840
|
+
const updated = getTaskById(task.id);
|
|
14841
|
+
if (updated)
|
|
14842
|
+
emitTaskEvent(updated, "unblocked", agent || task.reporter, oldStatus);
|
|
14843
|
+
return updated;
|
|
14844
|
+
}
|
|
14845
|
+
function reopenTask(id, agent) {
|
|
14846
|
+
const db2 = getDb();
|
|
14847
|
+
const task = resolveTask(id);
|
|
14848
|
+
if (!task)
|
|
14849
|
+
return null;
|
|
14850
|
+
const oldStatus = task.status;
|
|
14851
|
+
db2.prepare("UPDATE tasks SET status = 'pending', completed_at = NULL, cancelled_at = NULL WHERE id = ?").run(task.id);
|
|
14852
|
+
logActivity(task.id, agent || task.reporter, "reopened");
|
|
14853
|
+
const incompleteDeps = db2.prepare(`
|
|
14854
|
+
SELECT 1 FROM task_dependencies td
|
|
14855
|
+
JOIN tasks t ON t.id = td.depends_on_id
|
|
14856
|
+
WHERE td.task_id = ? AND t.status != 'completed'
|
|
14857
|
+
LIMIT 1
|
|
14858
|
+
`).get(task.id);
|
|
14859
|
+
const updated = getTaskById(task.id);
|
|
14860
|
+
if (updated)
|
|
14861
|
+
emitTaskEvent(updated, "reopened", agent || task.reporter, oldStatus);
|
|
14862
|
+
return updated;
|
|
14863
|
+
}
|
|
14864
|
+
function assignTask(id, assignee, agent) {
|
|
14865
|
+
const db2 = getDb();
|
|
14866
|
+
const task = resolveTask(id);
|
|
14867
|
+
if (!task)
|
|
14868
|
+
return null;
|
|
14869
|
+
db2.prepare("UPDATE tasks SET assignee = ? WHERE id = ?").run(assignee, task.id);
|
|
14870
|
+
logActivity(task.id, agent || task.reporter, "assigned", assignee);
|
|
14871
|
+
const updated = getTaskById(task.id);
|
|
14872
|
+
if (updated)
|
|
14873
|
+
emitTaskEvent(updated, "assigned", agent || task.reporter, task.status);
|
|
14874
|
+
return updated;
|
|
14875
|
+
}
|
|
14876
|
+
function setTaskPriority(id, priority, agent) {
|
|
14877
|
+
const db2 = getDb();
|
|
14878
|
+
const task = resolveTask(id);
|
|
14879
|
+
if (!task)
|
|
14880
|
+
return null;
|
|
14881
|
+
const oldPriority = task.priority;
|
|
14882
|
+
db2.prepare("UPDATE tasks SET priority = ? WHERE id = ?").run(priority, task.id);
|
|
14883
|
+
logActivity(task.id, agent || task.reporter, "priority_changed", `${oldPriority} -> ${priority}`);
|
|
14884
|
+
const updated = getTaskById(task.id);
|
|
14885
|
+
if (updated)
|
|
14886
|
+
emitTaskEvent(updated, "priority_changed", agent || task.reporter, task.status, `${oldPriority} -> ${priority}`);
|
|
14887
|
+
return updated;
|
|
14888
|
+
}
|
|
14889
|
+
function addComment(taskId, agent, content) {
|
|
14890
|
+
const db2 = getDb();
|
|
14891
|
+
const task = resolveTask(taskId);
|
|
14892
|
+
if (!task)
|
|
14893
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
14894
|
+
const row = db2.prepare("INSERT INTO task_comments (task_id, agent, content) VALUES (?, ?, ?) RETURNING *").get(task.id, agent, content);
|
|
14895
|
+
logActivity(task.id, agent, "comment", content.length > 200 ? content.slice(0, 200) + "\u2026" : content);
|
|
14896
|
+
return {
|
|
14897
|
+
id: row.id,
|
|
14898
|
+
task_id: row.task_id,
|
|
14899
|
+
agent: row.agent,
|
|
14900
|
+
content: row.content,
|
|
14901
|
+
created_at: row.created_at
|
|
14902
|
+
};
|
|
14903
|
+
}
|
|
14904
|
+
function getComments(taskId) {
|
|
14905
|
+
const db2 = getDb();
|
|
14906
|
+
const task = resolveTask(taskId);
|
|
14907
|
+
if (!task)
|
|
14908
|
+
return [];
|
|
14909
|
+
return db2.prepare("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at ASC, id ASC").all(task.id);
|
|
14910
|
+
}
|
|
14911
|
+
function getSubtasks(parentId) {
|
|
14912
|
+
const db2 = getDb();
|
|
14913
|
+
const parent = resolveTask(parentId);
|
|
14914
|
+
if (!parent)
|
|
14915
|
+
return [];
|
|
14916
|
+
const rows = db2.prepare("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at ASC, id ASC").all(parent.id);
|
|
14917
|
+
return rows.map(enrichTask);
|
|
14918
|
+
}
|
|
14919
|
+
function getTaskTree(parentId, maxDepth = 5) {
|
|
14920
|
+
const root = getTask(typeof parentId === "number" ? parentId : parentId);
|
|
14921
|
+
if (!root)
|
|
14922
|
+
throw new Error(`Task not found: ${parentId}`);
|
|
14923
|
+
const buildTree = (task, depth) => {
|
|
14924
|
+
if (depth >= maxDepth)
|
|
14925
|
+
return { ...task, children: [] };
|
|
14926
|
+
const children = getSubtasks(task.id);
|
|
14927
|
+
return { ...task, children: children.map((c) => buildTree(c, depth + 1)) };
|
|
14928
|
+
};
|
|
14929
|
+
return buildTree(root, 0);
|
|
14930
|
+
}
|
|
14931
|
+
function addDependency(taskId, dependsOnId) {
|
|
14932
|
+
const db2 = getDb();
|
|
14933
|
+
const task = resolveTask(taskId);
|
|
14934
|
+
const dep = resolveTask(dependsOnId);
|
|
14935
|
+
if (!task)
|
|
14936
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
14937
|
+
if (!dep)
|
|
14938
|
+
throw new Error(`Dependency task not found: ${dependsOnId}`);
|
|
14939
|
+
if (task.id === dep.id)
|
|
14940
|
+
throw new Error("A task cannot depend on itself");
|
|
14941
|
+
if (isCircularDependency(task.id, dep.id)) {
|
|
14942
|
+
throw new Error(`Circular dependency detected: task #${task.id} -> #${dep.id}`);
|
|
14943
|
+
}
|
|
14944
|
+
db2.prepare("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on_id) VALUES (?, ?)").run(task.id, dep.id);
|
|
14945
|
+
const deps = db2.prepare("SELECT depends_on_id FROM task_dependencies WHERE task_id = ?").all(task.id);
|
|
14946
|
+
db2.prepare("UPDATE tasks SET depends_on = ? WHERE id = ?").run(JSON.stringify(deps.map((d) => d.depends_on_id)), task.id);
|
|
14947
|
+
if (dep.status !== "completed") {
|
|
14948
|
+
db2.prepare("UPDATE tasks SET status = 'blocked' WHERE id = ?").run(task.id);
|
|
14949
|
+
}
|
|
14950
|
+
logActivity(task.id, "", "dependency_added", `depends on #${dep.id}`);
|
|
14951
|
+
}
|
|
14952
|
+
function removeDependency(taskId, dependsOnId) {
|
|
14953
|
+
const db2 = getDb();
|
|
14954
|
+
const task = resolveTask(taskId);
|
|
14955
|
+
if (!task)
|
|
14956
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
14957
|
+
db2.prepare("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on_id = ?").run(task.id, dependsOnId);
|
|
14958
|
+
const deps = db2.prepare("SELECT depends_on_id FROM task_dependencies WHERE task_id = ?").all(task.id);
|
|
14959
|
+
db2.prepare("UPDATE tasks SET depends_on = ? WHERE id = ?").run(JSON.stringify(deps.map((d) => d.depends_on_id)), task.id);
|
|
14960
|
+
logActivity(task.id, "", "dependency_removed", `no longer depends on #${dependsOnId}`);
|
|
14961
|
+
}
|
|
14962
|
+
function getDependencies(taskId) {
|
|
14963
|
+
const db2 = getDb();
|
|
14964
|
+
const task = resolveTask(taskId);
|
|
14965
|
+
if (!task)
|
|
14966
|
+
return [];
|
|
14967
|
+
return db2.prepare(`
|
|
14968
|
+
SELECT t.* FROM tasks t
|
|
14969
|
+
INNER JOIN task_dependencies td ON td.depends_on_id = t.id
|
|
14970
|
+
WHERE td.task_id = ?
|
|
14971
|
+
ORDER BY t.created_at ASC
|
|
14972
|
+
`).all(task.id).map(parseTask);
|
|
14973
|
+
}
|
|
14974
|
+
function getDependents(taskId) {
|
|
14975
|
+
const db2 = getDb();
|
|
14976
|
+
const task = resolveTask(taskId);
|
|
14977
|
+
if (!task)
|
|
14978
|
+
return [];
|
|
14979
|
+
return db2.prepare(`
|
|
14980
|
+
SELECT t.* FROM tasks t
|
|
14981
|
+
INNER JOIN task_dependencies td ON td.task_id = t.id
|
|
14982
|
+
WHERE td.depends_on_id = ?
|
|
14983
|
+
ORDER BY t.created_at ASC
|
|
14984
|
+
`).all(task.id).map(parseTask);
|
|
14985
|
+
}
|
|
14986
|
+
function getTaskActivity(taskId, limit = 50) {
|
|
14987
|
+
const db2 = getDb();
|
|
14988
|
+
const task = resolveTask(taskId);
|
|
14989
|
+
if (!task)
|
|
14990
|
+
return [];
|
|
14991
|
+
const safeLimit = Math.max(1, Math.min(Math.floor(limit), 1000));
|
|
14992
|
+
return db2.prepare(`SELECT * FROM task_activity WHERE task_id = ? ORDER BY created_at DESC LIMIT ${safeLimit}`).all(task.id);
|
|
14993
|
+
}
|
|
14994
|
+
function deleteTask(id, agent) {
|
|
14995
|
+
const db2 = getDb();
|
|
14996
|
+
const task = resolveTask(id);
|
|
14997
|
+
if (!task)
|
|
14998
|
+
return false;
|
|
14999
|
+
const subtaskCount = db2.prepare("SELECT COUNT(*) as c FROM tasks WHERE parent_id = ?").get(task.id).c;
|
|
15000
|
+
if (subtaskCount > 0) {
|
|
15001
|
+
throw new Error(`Cannot delete: ${subtaskCount} subtask(s) still reference this task`);
|
|
15002
|
+
}
|
|
15003
|
+
logActivity(task.id, agent || "", "deleted");
|
|
15004
|
+
db2.prepare("DELETE FROM tasks WHERE id = ?").run(task.id);
|
|
15005
|
+
return true;
|
|
15006
|
+
}
|
|
15007
|
+
function searchTasks(opts) {
|
|
15008
|
+
const db2 = getDb();
|
|
15009
|
+
const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 20;
|
|
15010
|
+
const sortByRelevance = opts.sort !== "recent";
|
|
15011
|
+
const query = opts.query.trim();
|
|
15012
|
+
const terms = query.split(/\s+/).filter(Boolean);
|
|
15013
|
+
const ftsAvailable = db2.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='tasks_fts'").get();
|
|
15014
|
+
if (ftsAvailable && terms.length > 0) {
|
|
15015
|
+
try {
|
|
15016
|
+
let ftsQuery;
|
|
15017
|
+
if (query.startsWith('"') && query.endsWith('"')) {
|
|
15018
|
+
ftsQuery = query;
|
|
15019
|
+
} else {
|
|
15020
|
+
ftsQuery = terms.map((w) => `"${w.replace(/"/g, '""')}"`).join(" ");
|
|
15021
|
+
}
|
|
15022
|
+
const ftsRows = db2.prepare(`SELECT rowid, rank, snippet(tasks_fts, 0, '**', '**', '...', 10) as snippet
|
|
15023
|
+
FROM tasks_fts WHERE tasks_fts MATCH ? ORDER BY rank LIMIT ${limit * 3}`).all(ftsQuery);
|
|
15024
|
+
if (ftsRows.length === 0) {} else {
|
|
15025
|
+
const ids = ftsRows.map((r) => r.rowid);
|
|
15026
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
15027
|
+
const rows2 = db2.prepare(`SELECT * FROM tasks WHERE id IN (${placeholders})`).all(...ids);
|
|
15028
|
+
const taskMap = new Map;
|
|
15029
|
+
for (const row of rows2)
|
|
15030
|
+
taskMap.set(row.id, row);
|
|
15031
|
+
const rankMap = new Map(ftsRows.map((r) => [r.rowid, { rank: r.rank, snippet: r.snippet }]));
|
|
15032
|
+
const sorted = sortByRelevance ? [...ftsRows].sort((a, b) => a.rank - b.rank) : [...ftsRows].sort((a, b) => {
|
|
15033
|
+
const aTask = taskMap.get(a.rowid);
|
|
15034
|
+
const bTask = taskMap.get(b.rowid);
|
|
15035
|
+
return (bTask?.created_at || "").localeCompare(aTask?.created_at || "");
|
|
15036
|
+
});
|
|
15037
|
+
const results = [];
|
|
15038
|
+
const maxRank = Math.abs(sorted[0].rank) || 1;
|
|
15039
|
+
for (const fts of sorted) {
|
|
15040
|
+
const row = taskMap.get(fts.rowid);
|
|
15041
|
+
if (!row)
|
|
15042
|
+
continue;
|
|
15043
|
+
const task = enrichTask(row);
|
|
15044
|
+
if (opts.status && task.status !== opts.status)
|
|
15045
|
+
continue;
|
|
15046
|
+
if (opts.assignee && task.assignee !== opts.assignee)
|
|
15047
|
+
continue;
|
|
15048
|
+
if (opts.project_id && task.project_id !== opts.project_id)
|
|
15049
|
+
continue;
|
|
15050
|
+
if (opts.space && task.space !== opts.space)
|
|
15051
|
+
continue;
|
|
15052
|
+
if (opts.priority && task.priority !== opts.priority)
|
|
15053
|
+
continue;
|
|
15054
|
+
if (!opts.include_archived && task.status === "cancelled")
|
|
15055
|
+
continue;
|
|
15056
|
+
results.push({
|
|
15057
|
+
...task,
|
|
15058
|
+
snippet: fts.snippet || null,
|
|
15059
|
+
relevance_score: Math.round((1 - Math.abs(fts.rank) / maxRank) * 100)
|
|
15060
|
+
});
|
|
15061
|
+
if (results.length >= limit)
|
|
15062
|
+
break;
|
|
15063
|
+
}
|
|
15064
|
+
return results;
|
|
15065
|
+
}
|
|
15066
|
+
} catch {}
|
|
15067
|
+
}
|
|
15068
|
+
if (terms.length === 0)
|
|
15069
|
+
return [];
|
|
15070
|
+
const params = [];
|
|
15071
|
+
const conditions = [];
|
|
15072
|
+
for (const term of terms) {
|
|
15073
|
+
conditions.push("(LOWER(t.subject) LIKE ? OR LOWER(t.description) LIKE ? OR LOWER(t.tags) LIKE ?)");
|
|
15074
|
+
const likeTerm = `%${term}%`;
|
|
15075
|
+
params.push(likeTerm, likeTerm, likeTerm);
|
|
15076
|
+
}
|
|
15077
|
+
if (opts.status) {
|
|
15078
|
+
conditions.push("t.status = ?");
|
|
15079
|
+
params.push(opts.status);
|
|
15080
|
+
}
|
|
15081
|
+
if (opts.assignee) {
|
|
15082
|
+
conditions.push("t.assignee = ?");
|
|
15083
|
+
params.push(opts.assignee);
|
|
15084
|
+
}
|
|
15085
|
+
if (opts.project_id) {
|
|
15086
|
+
conditions.push("t.project_id = ?");
|
|
15087
|
+
params.push(opts.project_id);
|
|
15088
|
+
}
|
|
15089
|
+
if (opts.space) {
|
|
15090
|
+
conditions.push("t.space = ?");
|
|
15091
|
+
params.push(opts.space);
|
|
15092
|
+
}
|
|
15093
|
+
if (opts.priority) {
|
|
15094
|
+
conditions.push("t.priority = ?");
|
|
15095
|
+
params.push(opts.priority);
|
|
15096
|
+
}
|
|
15097
|
+
if (!opts.include_archived) {
|
|
15098
|
+
conditions.push("t.status != 'cancelled'");
|
|
15099
|
+
}
|
|
15100
|
+
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";
|
|
15101
|
+
const rows = db2.prepare(`SELECT t.* FROM tasks t WHERE ${conditions.join(" AND ")} ${orderClause} LIMIT ${limit}`).all(...params);
|
|
15102
|
+
return rows.map((row) => {
|
|
15103
|
+
const task = enrichTask(row);
|
|
15104
|
+
const subject = row.subject.toLowerCase();
|
|
15105
|
+
const matchCount = terms.filter((t) => subject.includes(t)).length;
|
|
15106
|
+
return {
|
|
15107
|
+
...task,
|
|
15108
|
+
snippet: null,
|
|
15109
|
+
relevance_score: Math.round(matchCount / terms.length * 100)
|
|
15110
|
+
};
|
|
15111
|
+
});
|
|
15112
|
+
}
|
|
15113
|
+
function getDueTasks(opts = {}) {
|
|
15114
|
+
const db2 = getDb();
|
|
15115
|
+
const windowHours = opts.window_hours ?? 24;
|
|
15116
|
+
const now = new Date;
|
|
15117
|
+
const deadline = new Date(now.getTime() + windowHours * 60 * 60 * 1000);
|
|
15118
|
+
const rows = db2.prepare(`
|
|
15119
|
+
SELECT t.* FROM tasks t
|
|
15120
|
+
WHERE t.due_at IS NOT NULL
|
|
15121
|
+
AND t.due_at <= ?
|
|
15122
|
+
AND t.status NOT IN ('completed', 'cancelled')
|
|
15123
|
+
ORDER BY t.due_at ASC
|
|
15124
|
+
`).all(deadline.toISOString());
|
|
15125
|
+
return rows.map((row) => {
|
|
15126
|
+
const task = enrichTask(row);
|
|
15127
|
+
const dueAt = new Date(task.due_at);
|
|
15128
|
+
const hoursUntilDue = (dueAt.getTime() - now.getTime()) / (1000 * 60 * 60);
|
|
15129
|
+
let urgency;
|
|
15130
|
+
if (hoursUntilDue < 0)
|
|
15131
|
+
urgency = "overdue";
|
|
15132
|
+
else if (hoursUntilDue <= 24)
|
|
15133
|
+
urgency = "due_today";
|
|
15134
|
+
else
|
|
15135
|
+
urgency = "due_soon";
|
|
15136
|
+
return { task, due_in_hours: Math.round(hoursUntilDue * 10) / 10, urgency };
|
|
15137
|
+
});
|
|
15138
|
+
}
|
|
15139
|
+
function getTaskSummary(idOrUuid) {
|
|
15140
|
+
const db2 = getDb();
|
|
15141
|
+
const task = getTask(idOrUuid);
|
|
15142
|
+
if (!task)
|
|
15143
|
+
return null;
|
|
15144
|
+
const subtasks = db2.prepare("SELECT status FROM tasks WHERE parent_id = ?").all(task.id);
|
|
15145
|
+
const totalSubtasks = subtasks.length;
|
|
15146
|
+
const completedSubtasks = subtasks.filter((s) => s.status === "completed").length;
|
|
15147
|
+
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);
|
|
15148
|
+
const totalDeps = depRows.length;
|
|
15149
|
+
const completedDeps = depRows.filter((d) => d.status === "completed").length;
|
|
15150
|
+
const commentCount = db2.prepare("SELECT COUNT(*) as c FROM task_comments WHERE task_id = ?").get(task.id).c;
|
|
15151
|
+
const items = totalSubtasks + totalDeps;
|
|
15152
|
+
const completed = completedSubtasks + completedDeps;
|
|
15153
|
+
const completionPct = items > 0 ? Math.round(completed / items * 100) : task.status === "completed" ? 100 : 0;
|
|
15154
|
+
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);
|
|
15155
|
+
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);
|
|
15156
|
+
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);
|
|
15157
|
+
return {
|
|
15158
|
+
task,
|
|
15159
|
+
progress: {
|
|
15160
|
+
total_subtasks: totalSubtasks,
|
|
15161
|
+
completed_subtasks: completedSubtasks,
|
|
15162
|
+
total_dependencies: totalDeps,
|
|
15163
|
+
completed_dependencies: completedDeps,
|
|
15164
|
+
comment_count: commentCount,
|
|
15165
|
+
completion_pct: completionPct
|
|
15166
|
+
},
|
|
15167
|
+
recent_activity: activity,
|
|
15168
|
+
blockers: blockerInfo,
|
|
15169
|
+
dependents: dependentRows
|
|
15170
|
+
};
|
|
15171
|
+
}
|
|
15172
|
+
function enrichTask(row) {
|
|
15173
|
+
const db2 = getDb();
|
|
15174
|
+
const task = parseTask(row);
|
|
15175
|
+
const subtaskCount = db2.prepare("SELECT COUNT(*) as c FROM tasks WHERE parent_id = ?").get(task.id).c;
|
|
15176
|
+
const commentCount = db2.prepare("SELECT COUNT(*) as c FROM task_comments WHERE task_id = ?").get(task.id).c;
|
|
15177
|
+
const depRows = db2.prepare("SELECT depends_on_id FROM task_dependencies WHERE task_id = ?").all(task.id);
|
|
15178
|
+
const depCount = depRows.length;
|
|
15179
|
+
let blockerInfo = [];
|
|
15180
|
+
if (depRows.length > 0) {
|
|
15181
|
+
blockerInfo = depRows.map((d) => {
|
|
15182
|
+
const dep = db2.prepare("SELECT id, subject, status FROM tasks WHERE id = ?").get(d.depends_on_id);
|
|
15183
|
+
return dep ? { task_id: dep.id, subject: dep.subject, status: dep.status } : null;
|
|
15184
|
+
}).filter(Boolean);
|
|
15185
|
+
}
|
|
15186
|
+
return { ...task, subtask_count: subtaskCount, comment_count: commentCount, dependency_count: depCount, blocker_info: blockerInfo };
|
|
15187
|
+
}
|
|
15188
|
+
function resolveTask(idOrUuid) {
|
|
15189
|
+
const db2 = getDb();
|
|
15190
|
+
if (typeof idOrUuid === "number") {
|
|
15191
|
+
const row2 = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(idOrUuid);
|
|
15192
|
+
return row2 ? parseTask(row2) : null;
|
|
15193
|
+
}
|
|
15194
|
+
const row = db2.prepare("SELECT * FROM tasks WHERE uuid = ?").get(idOrUuid);
|
|
15195
|
+
return row ? parseTask(row) : null;
|
|
15196
|
+
}
|
|
15197
|
+
function getTaskById(id) {
|
|
15198
|
+
const db2 = getDb();
|
|
15199
|
+
const row = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
15200
|
+
return row ? parseTask(row) : null;
|
|
15201
|
+
}
|
|
15202
|
+
function unblockDependents(completedTaskId) {
|
|
15203
|
+
const db2 = getDb();
|
|
15204
|
+
const dependents = db2.prepare(`
|
|
15205
|
+
SELECT td.task_id, t.status FROM task_dependencies td
|
|
15206
|
+
JOIN tasks t ON t.id = td.task_id
|
|
15207
|
+
WHERE td.depends_on_id = ?
|
|
15208
|
+
`).all(completedTaskId);
|
|
15209
|
+
for (const dep of dependents) {
|
|
15210
|
+
if (dep.status === "blocked") {
|
|
15211
|
+
const incompleteCount = db2.prepare(`
|
|
15212
|
+
SELECT COUNT(*) as c FROM task_dependencies td
|
|
15213
|
+
JOIN tasks t ON t.id = td.depends_on_id
|
|
15214
|
+
WHERE td.task_id = ? AND t.status != 'completed'
|
|
15215
|
+
`).get(dep.task_id).c;
|
|
15216
|
+
if (incompleteCount === 0) {
|
|
15217
|
+
db2.prepare("UPDATE tasks SET status = 'pending' WHERE id = ?").run(dep.task_id);
|
|
15218
|
+
logActivity(dep.task_id, "", "auto_unblocked", `dependency #${completedTaskId} completed`);
|
|
15219
|
+
const task = getTaskById(dep.task_id);
|
|
15220
|
+
if (task)
|
|
15221
|
+
emitTaskEvent(task, "auto_unblocked", "system", "blocked", `dependency #${completedTaskId} completed`);
|
|
15222
|
+
}
|
|
15223
|
+
}
|
|
15224
|
+
}
|
|
15225
|
+
}
|
|
15226
|
+
function isCircularDependency(taskId, dependsOnId) {
|
|
15227
|
+
const db2 = getDb();
|
|
15228
|
+
const visited = new Set;
|
|
15229
|
+
let current = dependsOnId;
|
|
15230
|
+
let depth = 0;
|
|
15231
|
+
while (current !== undefined && depth < 20) {
|
|
15232
|
+
if (current === taskId)
|
|
15233
|
+
return true;
|
|
15234
|
+
if (visited.has(current))
|
|
15235
|
+
break;
|
|
15236
|
+
visited.add(current);
|
|
15237
|
+
const parents = db2.prepare("SELECT depends_on_id FROM task_dependencies WHERE task_id = ?").all(current);
|
|
15238
|
+
current = parents.length > 0 ? parents[0].depends_on_id : undefined;
|
|
15239
|
+
depth++;
|
|
15240
|
+
}
|
|
15241
|
+
return false;
|
|
15242
|
+
}
|
|
14370
15243
|
export {
|
|
14371
15244
|
useSpaceMessages,
|
|
14372
15245
|
useMessages,
|
|
@@ -14374,18 +15247,25 @@ export {
|
|
|
14374
15247
|
updateProject,
|
|
14375
15248
|
unsubscribeFromSpaceNotifications,
|
|
14376
15249
|
unpinMessage,
|
|
15250
|
+
unblockTask,
|
|
14377
15251
|
unarchiveSpace,
|
|
14378
15252
|
tryBulkAcquireLock,
|
|
14379
15253
|
subscribeToSpaceNotifications,
|
|
15254
|
+
startTask,
|
|
14380
15255
|
startPolling,
|
|
15256
|
+
setTaskPriority,
|
|
14381
15257
|
setActiveModel,
|
|
14382
15258
|
sendMessage,
|
|
15259
|
+
searchTasks,
|
|
14383
15260
|
searchMessages,
|
|
14384
15261
|
resolveIdentity,
|
|
14385
15262
|
requireIdentity,
|
|
15263
|
+
reopenTask,
|
|
14386
15264
|
renameAgent,
|
|
15265
|
+
removeWebhook,
|
|
14387
15266
|
removeReaction,
|
|
14388
15267
|
removePresence,
|
|
15268
|
+
removeDependency,
|
|
14389
15269
|
releaseStaleAgentLocks,
|
|
14390
15270
|
releaseLock,
|
|
14391
15271
|
registerAgent,
|
|
@@ -14398,6 +15278,8 @@ export {
|
|
|
14398
15278
|
markRead,
|
|
14399
15279
|
markAllSpaceNotificationsRead,
|
|
14400
15280
|
markAllRead,
|
|
15281
|
+
listWebhooks,
|
|
15282
|
+
listTasks,
|
|
14401
15283
|
listSpaces,
|
|
14402
15284
|
listSpaceNotificationSubscriptions,
|
|
14403
15285
|
listSessions,
|
|
@@ -14414,6 +15296,11 @@ export {
|
|
|
14414
15296
|
getUnreadBlockers,
|
|
14415
15297
|
getTrendingTopics,
|
|
14416
15298
|
getThreadReplies,
|
|
15299
|
+
getTaskTree,
|
|
15300
|
+
getTaskSummary,
|
|
15301
|
+
getTaskActivity,
|
|
15302
|
+
getTask,
|
|
15303
|
+
getSubtasks,
|
|
14417
15304
|
getSubscribedSpaces,
|
|
14418
15305
|
getSpaceTopics,
|
|
14419
15306
|
getSpaceMembers,
|
|
@@ -14431,28 +15318,42 @@ export {
|
|
|
14431
15318
|
getPinnedMessages,
|
|
14432
15319
|
getMessageById,
|
|
14433
15320
|
getGraphStats,
|
|
15321
|
+
getDueTasks,
|
|
15322
|
+
getDependents,
|
|
15323
|
+
getDependencies,
|
|
14434
15324
|
getDbPath2 as getDbPath,
|
|
14435
15325
|
getDb,
|
|
14436
15326
|
getConversationSummary,
|
|
15327
|
+
getComments,
|
|
14437
15328
|
getAgentNetwork,
|
|
14438
15329
|
getActiveModel,
|
|
14439
15330
|
gatherTrainingData,
|
|
14440
15331
|
fireWebhooks,
|
|
15332
|
+
fireTaskWebhooks,
|
|
14441
15333
|
extractTopics,
|
|
14442
15334
|
exportMessages,
|
|
14443
15335
|
editMessage,
|
|
15336
|
+
deleteTask,
|
|
14444
15337
|
deleteProject,
|
|
14445
15338
|
deleteMessage,
|
|
15339
|
+
createTask,
|
|
14446
15340
|
createSpace,
|
|
14447
15341
|
createProject,
|
|
14448
15342
|
computeHotness,
|
|
15343
|
+
completeTask,
|
|
14449
15344
|
closeDb,
|
|
14450
15345
|
clearActiveModel,
|
|
14451
15346
|
cleanExpiredLocks,
|
|
14452
15347
|
checkLock,
|
|
15348
|
+
cancelTask,
|
|
14453
15349
|
buildMessagePreview,
|
|
14454
15350
|
buildGraph,
|
|
15351
|
+
blockTask,
|
|
15352
|
+
assignTask,
|
|
14455
15353
|
archiveSpace,
|
|
15354
|
+
addWebhook,
|
|
14456
15355
|
addReaction,
|
|
15356
|
+
addDependency,
|
|
15357
|
+
addComment,
|
|
14457
15358
|
acquireLock
|
|
14458
15359
|
};
|