@hasna/todos 0.11.29 → 0.11.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +25237 -27283
- package/dist/db/checkpoints.d.ts +55 -0
- package/dist/db/checkpoints.d.ts.map +1 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/schema.d.ts +0 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts +13 -0
- package/dist/db/task-crud.d.ts.map +1 -0
- package/dist/db/task-graph.d.ts +27 -0
- package/dist/db/task-graph.d.ts.map +1 -0
- package/dist/db/task-lifecycle.d.ts +80 -0
- package/dist/db/task-lifecycle.d.ts.map +1 -0
- package/dist/db/task-relations.d.ts +86 -0
- package/dist/db/task-relations.d.ts.map +1 -0
- package/dist/db/task-status.d.ts +65 -0
- package/dist/db/task-status.d.ts.map +1 -0
- package/dist/db/tasks.d.ts +10 -255
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/db/webhooks.d.ts +6 -1
- package/dist/db/webhooks.d.ts.map +1 -1
- package/dist/index.js +2381 -1788
- package/dist/lib/github.d.ts +1 -1
- package/dist/lib/github.d.ts.map +1 -1
- package/dist/lib/north-star.d.ts +33 -0
- package/dist/lib/north-star.d.ts.map +1 -0
- package/dist/lib/task-runner.d.ts +101 -0
- package/dist/lib/task-runner.d.ts.map +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +21955 -25937
- package/dist/mcp/tools/cloud.d.ts.map +1 -1
- package/dist/mcp/tools/code-tools.d.ts +15 -0
- package/dist/mcp/tools/code-tools.d.ts.map +1 -0
- package/dist/mcp/tools/task-adv-tools.d.ts +20 -0
- package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -0
- package/dist/mcp/tools/task-auto-tools.d.ts +20 -0
- package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -0
- package/dist/mcp/tools/task-crud.d.ts +20 -0
- package/dist/mcp/tools/task-crud.d.ts.map +1 -0
- package/dist/mcp/tools/task-meta-tools.d.ts +12 -0
- package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -0
- package/dist/mcp/tools/task-project-tools.d.ts +20 -0
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -0
- package/dist/mcp/tools/task-rel-tools.d.ts +19 -0
- package/dist/mcp/tools/task-rel-tools.d.ts.map +1 -0
- package/dist/mcp/tools/task-resources.d.ts +13 -0
- package/dist/mcp/tools/task-resources.d.ts.map +1 -0
- package/dist/mcp/tools/task-workflow-tools.d.ts +20 -0
- package/dist/mcp/tools/task-workflow-tools.d.ts.map +1 -0
- package/dist/sdk/client.d.ts +361 -0
- package/dist/sdk/client.d.ts.map +1 -0
- package/dist/sdk/index.d.ts +14 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/types.d.ts +167 -0
- package/dist/sdk/types.d.ts.map +1 -0
- package/dist/sdk.d.ts +15 -182
- package/dist/sdk.d.ts.map +1 -1
- package/dist/server/index.js +1961 -2803
- package/dist/server/routes.d.ts +85 -0
- package/dist/server/routes.d.ts.map +1 -0
- package/dist/server/serve.d.ts +29 -0
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +77 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,419 +1,46 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
function __accessProp(key) {
|
|
7
|
+
return this[key];
|
|
8
|
+
}
|
|
9
|
+
var __toCommonJS = (from) => {
|
|
10
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
11
|
+
if (entry)
|
|
12
|
+
return entry;
|
|
13
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (var key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(entry, key))
|
|
17
|
+
__defProp(entry, key, {
|
|
18
|
+
get: __accessProp.bind(from, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
__moduleCache.set(from, entry);
|
|
23
|
+
return entry;
|
|
24
|
+
};
|
|
25
|
+
var __moduleCache;
|
|
26
|
+
var __returnValue = (v) => v;
|
|
27
|
+
function __exportSetter(name, newValue) {
|
|
28
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
29
|
+
}
|
|
3
30
|
var __export = (target, all) => {
|
|
4
31
|
for (var name in all)
|
|
5
32
|
__defProp(target, name, {
|
|
6
33
|
get: all[name],
|
|
7
34
|
enumerable: true,
|
|
8
35
|
configurable: true,
|
|
9
|
-
set: (
|
|
36
|
+
set: __exportSetter.bind(all, name)
|
|
10
37
|
});
|
|
11
38
|
};
|
|
12
39
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
13
40
|
|
|
14
|
-
// src/db/
|
|
15
|
-
function runMigrations(db) {
|
|
16
|
-
try {
|
|
17
|
-
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
18
|
-
const currentLevel = result?.max_id ?? 0;
|
|
19
|
-
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
20
|
-
try {
|
|
21
|
-
db.exec(MIGRATIONS[i]);
|
|
22
|
-
} catch {}
|
|
23
|
-
}
|
|
24
|
-
} catch {
|
|
25
|
-
for (const migration of MIGRATIONS) {
|
|
26
|
-
try {
|
|
27
|
-
db.exec(migration);
|
|
28
|
-
} catch {}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
ensureSchema(db);
|
|
32
|
-
}
|
|
33
|
-
function ensureSchema(db) {
|
|
34
|
-
const ensureColumn = (table, column, type) => {
|
|
35
|
-
try {
|
|
36
|
-
db.query(`SELECT ${column} FROM ${table} LIMIT 0`).get();
|
|
37
|
-
} catch {
|
|
38
|
-
try {
|
|
39
|
-
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
40
|
-
} catch {}
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
const ensureTable = (name, sql) => {
|
|
44
|
-
try {
|
|
45
|
-
const exists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(name);
|
|
46
|
-
if (!exists)
|
|
47
|
-
db.exec(sql);
|
|
48
|
-
} catch {}
|
|
49
|
-
};
|
|
50
|
-
const ensureIndex = (sql) => {
|
|
51
|
-
try {
|
|
52
|
-
db.exec(sql);
|
|
53
|
-
} catch {}
|
|
54
|
-
};
|
|
55
|
-
ensureTable("orgs", `
|
|
56
|
-
CREATE TABLE orgs (
|
|
57
|
-
id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
|
|
58
|
-
metadata TEXT DEFAULT '{}',
|
|
59
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
60
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
61
|
-
)`);
|
|
62
|
-
ensureTable("agents", `
|
|
63
|
-
CREATE TABLE agents (
|
|
64
|
-
id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
|
|
65
|
-
role TEXT DEFAULT 'agent', permissions TEXT DEFAULT '["*"]',
|
|
66
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived')),
|
|
67
|
-
metadata TEXT DEFAULT '{}',
|
|
68
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
69
|
-
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
70
|
-
)`);
|
|
71
|
-
ensureTable("task_lists", `
|
|
72
|
-
CREATE TABLE task_lists (
|
|
73
|
-
id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
74
|
-
slug TEXT NOT NULL, name TEXT NOT NULL, description TEXT,
|
|
75
|
-
metadata TEXT DEFAULT '{}',
|
|
76
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
77
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
78
|
-
UNIQUE(project_id, slug)
|
|
79
|
-
)`);
|
|
80
|
-
ensureTable("plans", `
|
|
81
|
-
CREATE TABLE plans (
|
|
82
|
-
id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
83
|
-
task_list_id TEXT, agent_id TEXT,
|
|
84
|
-
name TEXT NOT NULL, description TEXT,
|
|
85
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
86
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
87
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
88
|
-
)`);
|
|
89
|
-
ensureTable("task_tags", `
|
|
90
|
-
CREATE TABLE task_tags (
|
|
91
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
92
|
-
tag TEXT NOT NULL, PRIMARY KEY (task_id, tag)
|
|
93
|
-
)`);
|
|
94
|
-
ensureTable("task_history", `
|
|
95
|
-
CREATE TABLE task_history (
|
|
96
|
-
id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
97
|
-
action TEXT NOT NULL, field TEXT, old_value TEXT, new_value TEXT, agent_id TEXT,
|
|
98
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
99
|
-
)`);
|
|
100
|
-
ensureTable("webhooks", `
|
|
101
|
-
CREATE TABLE webhooks (
|
|
102
|
-
id TEXT PRIMARY KEY, url TEXT NOT NULL, events TEXT NOT NULL DEFAULT '[]',
|
|
103
|
-
secret TEXT, active INTEGER NOT NULL DEFAULT 1,
|
|
104
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
105
|
-
)`);
|
|
106
|
-
ensureTable("task_templates", `
|
|
107
|
-
CREATE TABLE task_templates (
|
|
108
|
-
id TEXT PRIMARY KEY, name TEXT NOT NULL, title_pattern TEXT NOT NULL,
|
|
109
|
-
description TEXT, priority TEXT DEFAULT 'medium', tags TEXT DEFAULT '[]',
|
|
110
|
-
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
111
|
-
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
112
|
-
metadata TEXT DEFAULT '{}',
|
|
113
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
114
|
-
)`);
|
|
115
|
-
ensureTable("template_tasks", `
|
|
116
|
-
CREATE TABLE template_tasks (
|
|
117
|
-
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
118
|
-
template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
|
|
119
|
-
position INTEGER NOT NULL,
|
|
120
|
-
title_pattern TEXT NOT NULL,
|
|
121
|
-
description TEXT,
|
|
122
|
-
priority TEXT DEFAULT 'medium',
|
|
123
|
-
tags TEXT DEFAULT '[]',
|
|
124
|
-
task_type TEXT,
|
|
125
|
-
depends_on_positions TEXT DEFAULT '[]',
|
|
126
|
-
metadata TEXT DEFAULT '{}',
|
|
127
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
128
|
-
)`);
|
|
129
|
-
ensureTable("task_checklists", `
|
|
130
|
-
CREATE TABLE task_checklists (
|
|
131
|
-
id TEXT PRIMARY KEY,
|
|
132
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
133
|
-
position INTEGER NOT NULL DEFAULT 0,
|
|
134
|
-
text TEXT NOT NULL,
|
|
135
|
-
checked INTEGER NOT NULL DEFAULT 0,
|
|
136
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
137
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
138
|
-
)`);
|
|
139
|
-
ensureTable("project_sources", `
|
|
140
|
-
CREATE TABLE project_sources (
|
|
141
|
-
id TEXT PRIMARY KEY,
|
|
142
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
143
|
-
type TEXT NOT NULL,
|
|
144
|
-
name TEXT NOT NULL,
|
|
145
|
-
uri TEXT NOT NULL,
|
|
146
|
-
description TEXT,
|
|
147
|
-
metadata TEXT DEFAULT '{}',
|
|
148
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
149
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
150
|
-
)`);
|
|
151
|
-
ensureTable("task_relationships", `
|
|
152
|
-
CREATE TABLE task_relationships (
|
|
153
|
-
id TEXT PRIMARY KEY,
|
|
154
|
-
source_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
155
|
-
target_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
156
|
-
relationship_type TEXT NOT NULL,
|
|
157
|
-
metadata TEXT DEFAULT '{}',
|
|
158
|
-
created_by TEXT,
|
|
159
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
160
|
-
CHECK (source_task_id != target_task_id)
|
|
161
|
-
)`);
|
|
162
|
-
ensureTable("kg_edges", `
|
|
163
|
-
CREATE TABLE kg_edges (
|
|
164
|
-
id TEXT PRIMARY KEY,
|
|
165
|
-
source_id TEXT NOT NULL,
|
|
166
|
-
source_type TEXT NOT NULL,
|
|
167
|
-
target_id TEXT NOT NULL,
|
|
168
|
-
target_type TEXT NOT NULL,
|
|
169
|
-
relation_type TEXT NOT NULL,
|
|
170
|
-
weight REAL NOT NULL DEFAULT 1.0,
|
|
171
|
-
metadata TEXT DEFAULT '{}',
|
|
172
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
173
|
-
UNIQUE(source_id, source_type, target_id, target_type, relation_type)
|
|
174
|
-
)`);
|
|
175
|
-
ensureTable("project_machine_paths", `
|
|
176
|
-
CREATE TABLE project_machine_paths (
|
|
177
|
-
id TEXT PRIMARY KEY,
|
|
178
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
179
|
-
machine_id TEXT NOT NULL,
|
|
180
|
-
path TEXT NOT NULL,
|
|
181
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
182
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
183
|
-
UNIQUE(project_id, machine_id)
|
|
184
|
-
)`);
|
|
185
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
|
|
186
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
|
|
187
|
-
ensureTable("machines", `
|
|
188
|
-
CREATE TABLE machines (
|
|
189
|
-
id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, hostname TEXT, platform TEXT,
|
|
190
|
-
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
191
|
-
metadata TEXT DEFAULT '{}',
|
|
192
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
193
|
-
)`);
|
|
194
|
-
ensureColumn("projects", "task_list_id", "TEXT");
|
|
195
|
-
ensureColumn("projects", "task_prefix", "TEXT");
|
|
196
|
-
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
197
|
-
ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
|
|
198
|
-
ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
|
|
199
|
-
ensureColumn("tasks", "short_id", "TEXT");
|
|
200
|
-
ensureColumn("tasks", "due_at", "TEXT");
|
|
201
|
-
ensureColumn("tasks", "estimated_minutes", "INTEGER");
|
|
202
|
-
ensureColumn("tasks", "requires_approval", "INTEGER NOT NULL DEFAULT 0");
|
|
203
|
-
ensureColumn("tasks", "approved_by", "TEXT");
|
|
204
|
-
ensureColumn("tasks", "approved_at", "TEXT");
|
|
205
|
-
ensureColumn("tasks", "recurrence_rule", "TEXT");
|
|
206
|
-
ensureColumn("tasks", "recurrence_parent_id", "TEXT REFERENCES tasks(id) ON DELETE SET NULL");
|
|
207
|
-
ensureColumn("tasks", "confidence", "REAL");
|
|
208
|
-
ensureColumn("tasks", "reason", "TEXT");
|
|
209
|
-
ensureColumn("tasks", "spawned_from_session", "TEXT");
|
|
210
|
-
ensureColumn("tasks", "assigned_by", "TEXT");
|
|
211
|
-
ensureColumn("tasks", "assigned_from_project", "TEXT");
|
|
212
|
-
ensureColumn("tasks", "started_at", "TEXT");
|
|
213
|
-
ensureColumn("tasks", "task_type", "TEXT");
|
|
214
|
-
ensureColumn("tasks", "cost_tokens", "INTEGER DEFAULT 0");
|
|
215
|
-
ensureColumn("tasks", "cost_usd", "REAL DEFAULT 0");
|
|
216
|
-
ensureColumn("tasks", "delegated_from", "TEXT");
|
|
217
|
-
ensureColumn("tasks", "delegation_depth", "INTEGER DEFAULT 0");
|
|
218
|
-
ensureColumn("tasks", "retry_count", "INTEGER DEFAULT 0");
|
|
219
|
-
ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
|
|
220
|
-
ensureColumn("tasks", "retry_after", "TEXT");
|
|
221
|
-
ensureColumn("tasks", "sla_minutes", "INTEGER");
|
|
222
|
-
ensureColumn("tasks", "archived_at", "TEXT");
|
|
223
|
-
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
224
|
-
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
225
|
-
ensureColumn("agents", "reports_to", "TEXT");
|
|
226
|
-
ensureColumn("agents", "title", "TEXT");
|
|
227
|
-
ensureColumn("agents", "level", "TEXT");
|
|
228
|
-
ensureColumn("agents", "org_id", "TEXT");
|
|
229
|
-
ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
|
|
230
|
-
ensureColumn("projects", "org_id", "TEXT");
|
|
231
|
-
ensureColumn("plans", "task_list_id", "TEXT");
|
|
232
|
-
ensureColumn("plans", "agent_id", "TEXT");
|
|
233
|
-
ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
|
|
234
|
-
ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
|
|
235
|
-
ensureColumn("template_tasks", "condition", "TEXT");
|
|
236
|
-
ensureColumn("template_tasks", "include_template_id", "TEXT");
|
|
237
|
-
ensureTable("template_versions", `
|
|
238
|
-
CREATE TABLE template_versions (
|
|
239
|
-
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
240
|
-
template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
|
|
241
|
-
version INTEGER NOT NULL,
|
|
242
|
-
snapshot TEXT NOT NULL,
|
|
243
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
244
|
-
)`);
|
|
245
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id)");
|
|
246
|
-
ensureTable("dispatches", `
|
|
247
|
-
CREATE TABLE dispatches (
|
|
248
|
-
id TEXT PRIMARY KEY,
|
|
249
|
-
title TEXT,
|
|
250
|
-
target_window TEXT NOT NULL,
|
|
251
|
-
task_ids TEXT NOT NULL DEFAULT '[]',
|
|
252
|
-
task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL,
|
|
253
|
-
message TEXT,
|
|
254
|
-
delay_ms INTEGER,
|
|
255
|
-
scheduled_at TEXT,
|
|
256
|
-
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'sent', 'failed', 'cancelled')),
|
|
257
|
-
error TEXT,
|
|
258
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
259
|
-
sent_at TEXT
|
|
260
|
-
)`);
|
|
261
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_status ON dispatches(status)");
|
|
262
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_scheduled ON dispatches(scheduled_at)");
|
|
263
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_task_list ON dispatches(task_list_id)");
|
|
264
|
-
ensureTable("dispatch_logs", `
|
|
265
|
-
CREATE TABLE dispatch_logs (
|
|
266
|
-
id TEXT PRIMARY KEY,
|
|
267
|
-
dispatch_id TEXT NOT NULL REFERENCES dispatches(id) ON DELETE CASCADE,
|
|
268
|
-
target_window TEXT NOT NULL,
|
|
269
|
-
message TEXT NOT NULL,
|
|
270
|
-
delay_ms INTEGER NOT NULL,
|
|
271
|
-
status TEXT NOT NULL CHECK(status IN ('sent', 'failed')),
|
|
272
|
-
error TEXT,
|
|
273
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
274
|
-
)`);
|
|
275
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatch_logs_dispatch ON dispatch_logs(dispatch_id)");
|
|
276
|
-
ensureColumn("webhooks", "project_id", "TEXT");
|
|
277
|
-
ensureColumn("webhooks", "task_list_id", "TEXT");
|
|
278
|
-
ensureColumn("webhooks", "agent_id", "TEXT");
|
|
279
|
-
ensureColumn("webhooks", "task_id", "TEXT");
|
|
280
|
-
ensureTable("webhook_deliveries", `
|
|
281
|
-
CREATE TABLE webhook_deliveries (
|
|
282
|
-
id TEXT PRIMARY KEY,
|
|
283
|
-
webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
|
|
284
|
-
event TEXT NOT NULL,
|
|
285
|
-
payload TEXT NOT NULL,
|
|
286
|
-
status_code INTEGER,
|
|
287
|
-
response TEXT,
|
|
288
|
-
attempt INTEGER NOT NULL DEFAULT 1,
|
|
289
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
290
|
-
)`);
|
|
291
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id)");
|
|
292
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
|
|
293
|
-
ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
|
|
294
|
-
ensureColumn("task_comments", "progress_pct", "INTEGER");
|
|
295
|
-
ensureColumn("projects", "machine_id", "TEXT");
|
|
296
|
-
ensureColumn("projects", "synced_at", "TEXT");
|
|
297
|
-
ensureColumn("tasks", "machine_id", "TEXT");
|
|
298
|
-
ensureColumn("tasks", "synced_at", "TEXT");
|
|
299
|
-
ensureColumn("agents", "machine_id", "TEXT");
|
|
300
|
-
ensureColumn("agents", "synced_at", "TEXT");
|
|
301
|
-
ensureColumn("task_lists", "machine_id", "TEXT");
|
|
302
|
-
ensureColumn("task_lists", "synced_at", "TEXT");
|
|
303
|
-
ensureColumn("plans", "machine_id", "TEXT");
|
|
304
|
-
ensureColumn("plans", "synced_at", "TEXT");
|
|
305
|
-
ensureColumn("task_comments", "machine_id", "TEXT");
|
|
306
|
-
ensureColumn("task_comments", "synced_at", "TEXT");
|
|
307
|
-
ensureColumn("sessions", "machine_id", "TEXT");
|
|
308
|
-
ensureColumn("sessions", "synced_at", "TEXT");
|
|
309
|
-
ensureColumn("task_history", "machine_id", "TEXT");
|
|
310
|
-
ensureColumn("webhooks", "machine_id", "TEXT");
|
|
311
|
-
ensureColumn("webhooks", "synced_at", "TEXT");
|
|
312
|
-
ensureColumn("task_templates", "machine_id", "TEXT");
|
|
313
|
-
ensureColumn("task_templates", "synced_at", "TEXT");
|
|
314
|
-
ensureColumn("orgs", "machine_id", "TEXT");
|
|
315
|
-
ensureColumn("orgs", "synced_at", "TEXT");
|
|
316
|
-
ensureColumn("handoffs", "machine_id", "TEXT");
|
|
317
|
-
ensureColumn("handoffs", "synced_at", "TEXT");
|
|
318
|
-
ensureColumn("task_checklists", "machine_id", "TEXT");
|
|
319
|
-
ensureColumn("project_sources", "machine_id", "TEXT");
|
|
320
|
-
ensureColumn("project_sources", "synced_at", "TEXT");
|
|
321
|
-
ensureColumn("task_files", "machine_id", "TEXT");
|
|
322
|
-
ensureColumn("task_relationships", "machine_id", "TEXT");
|
|
323
|
-
ensureColumn("kg_edges", "machine_id", "TEXT");
|
|
324
|
-
ensureColumn("project_agent_roles", "machine_id", "TEXT");
|
|
325
|
-
ensureColumn("dispatches", "machine_id", "TEXT");
|
|
326
|
-
ensureColumn("dispatches", "synced_at", "TEXT");
|
|
327
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
|
|
328
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id)");
|
|
329
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at)");
|
|
330
|
-
ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL");
|
|
331
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name)");
|
|
332
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_project ON task_lists(project_id)");
|
|
333
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug)");
|
|
334
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag)");
|
|
335
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id)");
|
|
336
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
|
|
337
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
|
|
338
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
|
|
339
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
|
|
340
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
|
|
341
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(agent_id)");
|
|
342
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id)");
|
|
343
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL");
|
|
344
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_checklists_task ON task_checklists(task_id)");
|
|
345
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id)");
|
|
346
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type)");
|
|
347
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by)");
|
|
348
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id)");
|
|
349
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id)");
|
|
350
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type)");
|
|
351
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id)");
|
|
352
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type)");
|
|
353
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type)");
|
|
354
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type)");
|
|
355
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_machine ON tasks(machine_id)");
|
|
356
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_synced ON tasks(synced_at)");
|
|
357
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_machine ON projects(machine_id)");
|
|
358
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_machine ON agents(machine_id)");
|
|
359
|
-
ensureTable("task_time_logs", `
|
|
360
|
-
CREATE TABLE task_time_logs (
|
|
361
|
-
id TEXT PRIMARY KEY,
|
|
362
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
363
|
-
agent_id TEXT,
|
|
364
|
-
started_at TEXT,
|
|
365
|
-
ended_at TEXT,
|
|
366
|
-
minutes INTEGER NOT NULL,
|
|
367
|
-
notes TEXT,
|
|
368
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
369
|
-
)`);
|
|
370
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_task ON task_time_logs(task_id)");
|
|
371
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_agent ON task_time_logs(agent_id)");
|
|
372
|
-
ensureColumn("tasks", "actual_minutes", "INTEGER");
|
|
373
|
-
ensureTable("task_watchers", `
|
|
374
|
-
CREATE TABLE task_watchers (
|
|
375
|
-
id TEXT PRIMARY KEY,
|
|
376
|
-
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
377
|
-
agent_id TEXT NOT NULL,
|
|
378
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
379
|
-
UNIQUE(task_id, agent_id)
|
|
380
|
-
)`);
|
|
381
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_task ON task_watchers(task_id)");
|
|
382
|
-
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_agent ON task_watchers(agent_id)");
|
|
383
|
-
ensureColumn("task_dependencies", "external_project_id", "TEXT");
|
|
384
|
-
ensureColumn("task_dependencies", "external_task_id", "TEXT");
|
|
385
|
-
}
|
|
386
|
-
function backfillTaskTags(db) {
|
|
387
|
-
try {
|
|
388
|
-
const count = db.query("SELECT COUNT(*) as count FROM task_tags").get();
|
|
389
|
-
if (count && count.count > 0)
|
|
390
|
-
return;
|
|
391
|
-
} catch {
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
try {
|
|
395
|
-
const rows = db.query("SELECT id, tags FROM tasks WHERE tags IS NOT NULL AND tags != '[]'").all();
|
|
396
|
-
if (rows.length === 0)
|
|
397
|
-
return;
|
|
398
|
-
const insert = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
399
|
-
for (const row of rows) {
|
|
400
|
-
if (!row.tags)
|
|
401
|
-
continue;
|
|
402
|
-
let tags = [];
|
|
403
|
-
try {
|
|
404
|
-
tags = JSON.parse(row.tags);
|
|
405
|
-
} catch {
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
for (const tag of tags) {
|
|
409
|
-
if (tag)
|
|
410
|
-
insert.run(row.id, tag);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
} catch {}
|
|
414
|
-
}
|
|
41
|
+
// src/db/migrations.ts
|
|
415
42
|
var MIGRATIONS;
|
|
416
|
-
var
|
|
43
|
+
var init_migrations = __esm(() => {
|
|
417
44
|
MIGRATIONS = [
|
|
418
45
|
`
|
|
419
46
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -557,7 +184,7 @@ var init_schema = __esm(() => {
|
|
|
557
184
|
ALTER TABLE projects ADD COLUMN task_counter INTEGER NOT NULL DEFAULT 0;
|
|
558
185
|
|
|
559
186
|
ALTER TABLE tasks ADD COLUMN short_id TEXT;
|
|
560
|
-
CREATE
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL;
|
|
561
188
|
|
|
562
189
|
INSERT OR IGNORE INTO _migrations (id) VALUES (6);
|
|
563
190
|
`,
|
|
@@ -1142,10 +769,454 @@ var init_schema = __esm(() => {
|
|
|
1142
769
|
ALTER TABLE task_dependencies ADD COLUMN external_project_id TEXT;
|
|
1143
770
|
ALTER TABLE task_dependencies ADD COLUMN external_task_id TEXT;
|
|
1144
771
|
INSERT OR IGNORE INTO _migrations (id) VALUES (47);
|
|
772
|
+
`,
|
|
773
|
+
`
|
|
774
|
+
CREATE TABLE IF NOT EXISTS task_checkpoints (
|
|
775
|
+
id TEXT PRIMARY KEY,
|
|
776
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
777
|
+
agent_id TEXT,
|
|
778
|
+
step TEXT NOT NULL,
|
|
779
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed', 'skipped')),
|
|
780
|
+
data TEXT DEFAULT '{}',
|
|
781
|
+
error TEXT,
|
|
782
|
+
attempt INTEGER NOT NULL DEFAULT 1,
|
|
783
|
+
max_attempts INTEGER NOT NULL DEFAULT 1,
|
|
784
|
+
started_at TEXT,
|
|
785
|
+
completed_at TEXT,
|
|
786
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
787
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
788
|
+
);
|
|
789
|
+
CREATE INDEX IF NOT EXISTS idx_task_checkpoints_task ON task_checkpoints(task_id);
|
|
790
|
+
CREATE INDEX IF NOT EXISTS idx_task_checkpoints_status ON task_checkpoints(status);
|
|
791
|
+
|
|
792
|
+
CREATE TABLE IF NOT EXISTS task_heartbeats (
|
|
793
|
+
id TEXT PRIMARY KEY,
|
|
794
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
795
|
+
agent_id TEXT,
|
|
796
|
+
step TEXT,
|
|
797
|
+
message TEXT,
|
|
798
|
+
progress REAL CHECK(progress >= 0 AND progress <= 1),
|
|
799
|
+
meta TEXT DEFAULT '{}',
|
|
800
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
801
|
+
);
|
|
802
|
+
CREATE INDEX IF NOT EXISTS idx_task_heartbeats_task ON task_heartbeats(task_id);
|
|
803
|
+
CREATE INDEX IF NOT EXISTS idx_task_heartbeats_agent ON task_heartbeats(agent_id);
|
|
804
|
+
|
|
805
|
+
ALTER TABLE tasks ADD COLUMN runner_id TEXT;
|
|
806
|
+
ALTER TABLE tasks ADD COLUMN runner_started_at TEXT;
|
|
807
|
+
ALTER TABLE tasks ADD COLUMN runner_completed_at TEXT;
|
|
808
|
+
ALTER TABLE tasks ADD COLUMN current_step TEXT;
|
|
809
|
+
ALTER TABLE tasks ADD COLUMN total_steps INTEGER;
|
|
810
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (48);
|
|
1145
811
|
`
|
|
1146
812
|
];
|
|
1147
813
|
});
|
|
1148
814
|
|
|
815
|
+
// src/db/schema.ts
|
|
816
|
+
function runMigrations(db) {
|
|
817
|
+
try {
|
|
818
|
+
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
819
|
+
const currentLevel = result?.max_id ?? 0;
|
|
820
|
+
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
821
|
+
try {
|
|
822
|
+
db.exec(MIGRATIONS[i]);
|
|
823
|
+
} catch {}
|
|
824
|
+
}
|
|
825
|
+
} catch {
|
|
826
|
+
for (const migration of MIGRATIONS) {
|
|
827
|
+
try {
|
|
828
|
+
db.exec(migration);
|
|
829
|
+
} catch {}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
ensureSchema(db);
|
|
833
|
+
}
|
|
834
|
+
function ensureSchema(db) {
|
|
835
|
+
const ensureColumn = (table, column, type) => {
|
|
836
|
+
try {
|
|
837
|
+
db.query(`SELECT ${column} FROM ${table} LIMIT 0`).get();
|
|
838
|
+
} catch {
|
|
839
|
+
try {
|
|
840
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
841
|
+
} catch {}
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
const ensureTable = (name, sql) => {
|
|
845
|
+
try {
|
|
846
|
+
const exists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(name);
|
|
847
|
+
if (!exists)
|
|
848
|
+
db.exec(sql);
|
|
849
|
+
} catch {}
|
|
850
|
+
};
|
|
851
|
+
const ensureIndex = (sql) => {
|
|
852
|
+
try {
|
|
853
|
+
db.exec(sql);
|
|
854
|
+
} catch {}
|
|
855
|
+
};
|
|
856
|
+
ensureTable("orgs", `
|
|
857
|
+
CREATE TABLE orgs (
|
|
858
|
+
id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
|
|
859
|
+
metadata TEXT DEFAULT '{}',
|
|
860
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
861
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
862
|
+
)`);
|
|
863
|
+
ensureTable("agents", `
|
|
864
|
+
CREATE TABLE agents (
|
|
865
|
+
id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
|
|
866
|
+
role TEXT DEFAULT 'agent', permissions TEXT DEFAULT '["*"]',
|
|
867
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived')),
|
|
868
|
+
metadata TEXT DEFAULT '{}',
|
|
869
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
870
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
871
|
+
)`);
|
|
872
|
+
ensureTable("task_lists", `
|
|
873
|
+
CREATE TABLE task_lists (
|
|
874
|
+
id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
875
|
+
slug TEXT NOT NULL, name TEXT NOT NULL, description TEXT,
|
|
876
|
+
metadata TEXT DEFAULT '{}',
|
|
877
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
878
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
879
|
+
UNIQUE(project_id, slug)
|
|
880
|
+
)`);
|
|
881
|
+
ensureTable("plans", `
|
|
882
|
+
CREATE TABLE plans (
|
|
883
|
+
id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
884
|
+
task_list_id TEXT, agent_id TEXT,
|
|
885
|
+
name TEXT NOT NULL, description TEXT,
|
|
886
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
887
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
888
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
889
|
+
)`);
|
|
890
|
+
ensureTable("task_tags", `
|
|
891
|
+
CREATE TABLE task_tags (
|
|
892
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
893
|
+
tag TEXT NOT NULL, PRIMARY KEY (task_id, tag)
|
|
894
|
+
)`);
|
|
895
|
+
ensureTable("task_history", `
|
|
896
|
+
CREATE TABLE task_history (
|
|
897
|
+
id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
898
|
+
action TEXT NOT NULL, field TEXT, old_value TEXT, new_value TEXT, agent_id TEXT,
|
|
899
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
900
|
+
)`);
|
|
901
|
+
ensureTable("webhooks", `
|
|
902
|
+
CREATE TABLE webhooks (
|
|
903
|
+
id TEXT PRIMARY KEY, url TEXT NOT NULL, events TEXT NOT NULL DEFAULT '[]',
|
|
904
|
+
secret TEXT, active INTEGER NOT NULL DEFAULT 1,
|
|
905
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
906
|
+
)`);
|
|
907
|
+
ensureTable("task_templates", `
|
|
908
|
+
CREATE TABLE task_templates (
|
|
909
|
+
id TEXT PRIMARY KEY, name TEXT NOT NULL, title_pattern TEXT NOT NULL,
|
|
910
|
+
description TEXT, priority TEXT DEFAULT 'medium', tags TEXT DEFAULT '[]',
|
|
911
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
912
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
913
|
+
metadata TEXT DEFAULT '{}',
|
|
914
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
915
|
+
)`);
|
|
916
|
+
ensureTable("template_tasks", `
|
|
917
|
+
CREATE TABLE template_tasks (
|
|
918
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
919
|
+
template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
|
|
920
|
+
position INTEGER NOT NULL,
|
|
921
|
+
title_pattern TEXT NOT NULL,
|
|
922
|
+
description TEXT,
|
|
923
|
+
priority TEXT DEFAULT 'medium',
|
|
924
|
+
tags TEXT DEFAULT '[]',
|
|
925
|
+
task_type TEXT,
|
|
926
|
+
depends_on_positions TEXT DEFAULT '[]',
|
|
927
|
+
metadata TEXT DEFAULT '{}',
|
|
928
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
929
|
+
)`);
|
|
930
|
+
ensureTable("task_checklists", `
|
|
931
|
+
CREATE TABLE task_checklists (
|
|
932
|
+
id TEXT PRIMARY KEY,
|
|
933
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
934
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
935
|
+
text TEXT NOT NULL,
|
|
936
|
+
checked INTEGER NOT NULL DEFAULT 0,
|
|
937
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
938
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
939
|
+
)`);
|
|
940
|
+
ensureTable("project_sources", `
|
|
941
|
+
CREATE TABLE project_sources (
|
|
942
|
+
id TEXT PRIMARY KEY,
|
|
943
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
944
|
+
type TEXT NOT NULL,
|
|
945
|
+
name TEXT NOT NULL,
|
|
946
|
+
uri TEXT NOT NULL,
|
|
947
|
+
description TEXT,
|
|
948
|
+
metadata TEXT DEFAULT '{}',
|
|
949
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
950
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
951
|
+
)`);
|
|
952
|
+
ensureTable("task_relationships", `
|
|
953
|
+
CREATE TABLE task_relationships (
|
|
954
|
+
id TEXT PRIMARY KEY,
|
|
955
|
+
source_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
956
|
+
target_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
957
|
+
relationship_type TEXT NOT NULL,
|
|
958
|
+
metadata TEXT DEFAULT '{}',
|
|
959
|
+
created_by TEXT,
|
|
960
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
961
|
+
CHECK (source_task_id != target_task_id)
|
|
962
|
+
)`);
|
|
963
|
+
ensureTable("kg_edges", `
|
|
964
|
+
CREATE TABLE kg_edges (
|
|
965
|
+
id TEXT PRIMARY KEY,
|
|
966
|
+
source_id TEXT NOT NULL,
|
|
967
|
+
source_type TEXT NOT NULL,
|
|
968
|
+
target_id TEXT NOT NULL,
|
|
969
|
+
target_type TEXT NOT NULL,
|
|
970
|
+
relation_type TEXT NOT NULL,
|
|
971
|
+
weight REAL NOT NULL DEFAULT 1.0,
|
|
972
|
+
metadata TEXT DEFAULT '{}',
|
|
973
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
974
|
+
UNIQUE(source_id, source_type, target_id, target_type, relation_type)
|
|
975
|
+
)`);
|
|
976
|
+
ensureTable("project_machine_paths", `
|
|
977
|
+
CREATE TABLE project_machine_paths (
|
|
978
|
+
id TEXT PRIMARY KEY,
|
|
979
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
980
|
+
machine_id TEXT NOT NULL,
|
|
981
|
+
path TEXT NOT NULL,
|
|
982
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
983
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
984
|
+
UNIQUE(project_id, machine_id)
|
|
985
|
+
)`);
|
|
986
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
|
|
987
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
|
|
988
|
+
ensureTable("machines", `
|
|
989
|
+
CREATE TABLE machines (
|
|
990
|
+
id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, hostname TEXT, platform TEXT,
|
|
991
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
992
|
+
metadata TEXT DEFAULT '{}',
|
|
993
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
994
|
+
)`);
|
|
995
|
+
ensureColumn("projects", "task_list_id", "TEXT");
|
|
996
|
+
ensureColumn("projects", "task_prefix", "TEXT");
|
|
997
|
+
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
998
|
+
ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
|
|
999
|
+
ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
|
|
1000
|
+
ensureColumn("tasks", "short_id", "TEXT");
|
|
1001
|
+
ensureColumn("tasks", "due_at", "TEXT");
|
|
1002
|
+
ensureColumn("tasks", "estimated_minutes", "INTEGER");
|
|
1003
|
+
ensureColumn("tasks", "requires_approval", "INTEGER NOT NULL DEFAULT 0");
|
|
1004
|
+
ensureColumn("tasks", "approved_by", "TEXT");
|
|
1005
|
+
ensureColumn("tasks", "approved_at", "TEXT");
|
|
1006
|
+
ensureColumn("tasks", "recurrence_rule", "TEXT");
|
|
1007
|
+
ensureColumn("tasks", "recurrence_parent_id", "TEXT REFERENCES tasks(id) ON DELETE SET NULL");
|
|
1008
|
+
ensureColumn("tasks", "confidence", "REAL");
|
|
1009
|
+
ensureColumn("tasks", "reason", "TEXT");
|
|
1010
|
+
ensureColumn("tasks", "spawned_from_session", "TEXT");
|
|
1011
|
+
ensureColumn("tasks", "assigned_by", "TEXT");
|
|
1012
|
+
ensureColumn("tasks", "assigned_from_project", "TEXT");
|
|
1013
|
+
ensureColumn("tasks", "started_at", "TEXT");
|
|
1014
|
+
ensureColumn("tasks", "task_type", "TEXT");
|
|
1015
|
+
ensureColumn("tasks", "cost_tokens", "INTEGER DEFAULT 0");
|
|
1016
|
+
ensureColumn("tasks", "cost_usd", "REAL DEFAULT 0");
|
|
1017
|
+
ensureColumn("tasks", "delegated_from", "TEXT");
|
|
1018
|
+
ensureColumn("tasks", "delegation_depth", "INTEGER DEFAULT 0");
|
|
1019
|
+
ensureColumn("tasks", "retry_count", "INTEGER DEFAULT 0");
|
|
1020
|
+
ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
|
|
1021
|
+
ensureColumn("tasks", "retry_after", "TEXT");
|
|
1022
|
+
ensureColumn("tasks", "sla_minutes", "INTEGER");
|
|
1023
|
+
ensureColumn("tasks", "archived_at", "TEXT");
|
|
1024
|
+
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
1025
|
+
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
1026
|
+
ensureColumn("agents", "reports_to", "TEXT");
|
|
1027
|
+
ensureColumn("agents", "title", "TEXT");
|
|
1028
|
+
ensureColumn("agents", "level", "TEXT");
|
|
1029
|
+
ensureColumn("agents", "org_id", "TEXT");
|
|
1030
|
+
ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
|
|
1031
|
+
ensureColumn("projects", "org_id", "TEXT");
|
|
1032
|
+
ensureColumn("plans", "task_list_id", "TEXT");
|
|
1033
|
+
ensureColumn("plans", "agent_id", "TEXT");
|
|
1034
|
+
ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
|
|
1035
|
+
ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
|
|
1036
|
+
ensureColumn("template_tasks", "condition", "TEXT");
|
|
1037
|
+
ensureColumn("template_tasks", "include_template_id", "TEXT");
|
|
1038
|
+
ensureTable("template_versions", `
|
|
1039
|
+
CREATE TABLE template_versions (
|
|
1040
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
1041
|
+
template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
|
|
1042
|
+
version INTEGER NOT NULL,
|
|
1043
|
+
snapshot TEXT NOT NULL,
|
|
1044
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1045
|
+
)`);
|
|
1046
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id)");
|
|
1047
|
+
ensureTable("dispatches", `
|
|
1048
|
+
CREATE TABLE dispatches (
|
|
1049
|
+
id TEXT PRIMARY KEY,
|
|
1050
|
+
title TEXT,
|
|
1051
|
+
target_window TEXT NOT NULL,
|
|
1052
|
+
task_ids TEXT NOT NULL DEFAULT '[]',
|
|
1053
|
+
task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL,
|
|
1054
|
+
message TEXT,
|
|
1055
|
+
delay_ms INTEGER,
|
|
1056
|
+
scheduled_at TEXT,
|
|
1057
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'sent', 'failed', 'cancelled')),
|
|
1058
|
+
error TEXT,
|
|
1059
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1060
|
+
sent_at TEXT
|
|
1061
|
+
)`);
|
|
1062
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_status ON dispatches(status)");
|
|
1063
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_scheduled ON dispatches(scheduled_at)");
|
|
1064
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_task_list ON dispatches(task_list_id)");
|
|
1065
|
+
ensureTable("dispatch_logs", `
|
|
1066
|
+
CREATE TABLE dispatch_logs (
|
|
1067
|
+
id TEXT PRIMARY KEY,
|
|
1068
|
+
dispatch_id TEXT NOT NULL REFERENCES dispatches(id) ON DELETE CASCADE,
|
|
1069
|
+
target_window TEXT NOT NULL,
|
|
1070
|
+
message TEXT NOT NULL,
|
|
1071
|
+
delay_ms INTEGER NOT NULL,
|
|
1072
|
+
status TEXT NOT NULL CHECK(status IN ('sent', 'failed')),
|
|
1073
|
+
error TEXT,
|
|
1074
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1075
|
+
)`);
|
|
1076
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatch_logs_dispatch ON dispatch_logs(dispatch_id)");
|
|
1077
|
+
ensureColumn("webhooks", "project_id", "TEXT");
|
|
1078
|
+
ensureColumn("webhooks", "task_list_id", "TEXT");
|
|
1079
|
+
ensureColumn("webhooks", "agent_id", "TEXT");
|
|
1080
|
+
ensureColumn("webhooks", "task_id", "TEXT");
|
|
1081
|
+
ensureTable("webhook_deliveries", `
|
|
1082
|
+
CREATE TABLE webhook_deliveries (
|
|
1083
|
+
id TEXT PRIMARY KEY,
|
|
1084
|
+
webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
|
|
1085
|
+
event TEXT NOT NULL,
|
|
1086
|
+
payload TEXT NOT NULL,
|
|
1087
|
+
status_code INTEGER,
|
|
1088
|
+
response TEXT,
|
|
1089
|
+
attempt INTEGER NOT NULL DEFAULT 1,
|
|
1090
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1091
|
+
)`);
|
|
1092
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id)");
|
|
1093
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
|
|
1094
|
+
ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
|
|
1095
|
+
ensureColumn("task_comments", "progress_pct", "INTEGER");
|
|
1096
|
+
ensureColumn("projects", "machine_id", "TEXT");
|
|
1097
|
+
ensureColumn("projects", "synced_at", "TEXT");
|
|
1098
|
+
ensureColumn("tasks", "machine_id", "TEXT");
|
|
1099
|
+
ensureColumn("tasks", "synced_at", "TEXT");
|
|
1100
|
+
ensureColumn("agents", "machine_id", "TEXT");
|
|
1101
|
+
ensureColumn("agents", "synced_at", "TEXT");
|
|
1102
|
+
ensureColumn("task_lists", "machine_id", "TEXT");
|
|
1103
|
+
ensureColumn("task_lists", "synced_at", "TEXT");
|
|
1104
|
+
ensureColumn("plans", "machine_id", "TEXT");
|
|
1105
|
+
ensureColumn("plans", "synced_at", "TEXT");
|
|
1106
|
+
ensureColumn("task_comments", "machine_id", "TEXT");
|
|
1107
|
+
ensureColumn("task_comments", "synced_at", "TEXT");
|
|
1108
|
+
ensureColumn("sessions", "machine_id", "TEXT");
|
|
1109
|
+
ensureColumn("sessions", "synced_at", "TEXT");
|
|
1110
|
+
ensureColumn("task_history", "machine_id", "TEXT");
|
|
1111
|
+
ensureColumn("webhooks", "machine_id", "TEXT");
|
|
1112
|
+
ensureColumn("webhooks", "synced_at", "TEXT");
|
|
1113
|
+
ensureColumn("task_templates", "machine_id", "TEXT");
|
|
1114
|
+
ensureColumn("task_templates", "synced_at", "TEXT");
|
|
1115
|
+
ensureColumn("orgs", "machine_id", "TEXT");
|
|
1116
|
+
ensureColumn("orgs", "synced_at", "TEXT");
|
|
1117
|
+
ensureColumn("handoffs", "machine_id", "TEXT");
|
|
1118
|
+
ensureColumn("handoffs", "synced_at", "TEXT");
|
|
1119
|
+
ensureColumn("task_checklists", "machine_id", "TEXT");
|
|
1120
|
+
ensureColumn("project_sources", "machine_id", "TEXT");
|
|
1121
|
+
ensureColumn("project_sources", "synced_at", "TEXT");
|
|
1122
|
+
ensureColumn("task_files", "machine_id", "TEXT");
|
|
1123
|
+
ensureColumn("task_relationships", "machine_id", "TEXT");
|
|
1124
|
+
ensureColumn("kg_edges", "machine_id", "TEXT");
|
|
1125
|
+
ensureColumn("project_agent_roles", "machine_id", "TEXT");
|
|
1126
|
+
ensureColumn("dispatches", "machine_id", "TEXT");
|
|
1127
|
+
ensureColumn("dispatches", "synced_at", "TEXT");
|
|
1128
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
|
|
1129
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id)");
|
|
1130
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at)");
|
|
1131
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL");
|
|
1132
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name)");
|
|
1133
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_project ON task_lists(project_id)");
|
|
1134
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug)");
|
|
1135
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag)");
|
|
1136
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id)");
|
|
1137
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
|
|
1138
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
|
|
1139
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
|
|
1140
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
|
|
1141
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
|
|
1142
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(task_id)");
|
|
1143
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id)");
|
|
1144
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL");
|
|
1145
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_checklists_task ON task_checklists(task_id)");
|
|
1146
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id)");
|
|
1147
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type)");
|
|
1148
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by)");
|
|
1149
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id)");
|
|
1150
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id)");
|
|
1151
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type)");
|
|
1152
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id)");
|
|
1153
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type)");
|
|
1154
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type)");
|
|
1155
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type)");
|
|
1156
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_machine ON tasks(machine_id)");
|
|
1157
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_synced ON tasks(synced_at)");
|
|
1158
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_machine ON projects(machine_id)");
|
|
1159
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_machine ON agents(machine_id)");
|
|
1160
|
+
ensureTable("task_time_logs", `
|
|
1161
|
+
CREATE TABLE task_time_logs (
|
|
1162
|
+
id TEXT PRIMARY KEY,
|
|
1163
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1164
|
+
agent_id TEXT,
|
|
1165
|
+
started_at TEXT,
|
|
1166
|
+
ended_at TEXT,
|
|
1167
|
+
minutes INTEGER NOT NULL,
|
|
1168
|
+
notes TEXT,
|
|
1169
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1170
|
+
)`);
|
|
1171
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_task ON task_time_logs(task_id)");
|
|
1172
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_agent ON task_time_logs(agent_id)");
|
|
1173
|
+
ensureColumn("tasks", "actual_minutes", "INTEGER");
|
|
1174
|
+
ensureTable("task_watchers", `
|
|
1175
|
+
CREATE TABLE task_watchers (
|
|
1176
|
+
id TEXT PRIMARY KEY,
|
|
1177
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1178
|
+
agent_id TEXT NOT NULL,
|
|
1179
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1180
|
+
UNIQUE(task_id, agent_id)
|
|
1181
|
+
)`);
|
|
1182
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_task ON task_watchers(task_id)");
|
|
1183
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_agent ON task_watchers(agent_id)");
|
|
1184
|
+
ensureColumn("task_dependencies", "external_project_id", "TEXT");
|
|
1185
|
+
ensureColumn("task_dependencies", "external_task_id", "TEXT");
|
|
1186
|
+
}
|
|
1187
|
+
function backfillTaskTags(db) {
|
|
1188
|
+
try {
|
|
1189
|
+
const count = db.query("SELECT COUNT(*) as count FROM task_tags").get();
|
|
1190
|
+
if (count && count.count > 0)
|
|
1191
|
+
return;
|
|
1192
|
+
} catch {
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
try {
|
|
1196
|
+
const rows = db.query("SELECT id, tags FROM tasks WHERE tags IS NOT NULL AND tags != '[]'").all();
|
|
1197
|
+
if (rows.length === 0)
|
|
1198
|
+
return;
|
|
1199
|
+
const insert = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
1200
|
+
for (const row of rows) {
|
|
1201
|
+
if (!row.tags)
|
|
1202
|
+
continue;
|
|
1203
|
+
let tags = [];
|
|
1204
|
+
try {
|
|
1205
|
+
tags = JSON.parse(row.tags);
|
|
1206
|
+
} catch {
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
for (const tag of tags) {
|
|
1210
|
+
if (tag)
|
|
1211
|
+
insert.run(row.id, tag);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
} catch {}
|
|
1215
|
+
}
|
|
1216
|
+
var init_schema = __esm(() => {
|
|
1217
|
+
init_migrations();
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1149
1220
|
// src/db/machines.ts
|
|
1150
1221
|
import { hostname as osHostname, platform as osPlatform } from "os";
|
|
1151
1222
|
function rowToMachine(row) {
|
|
@@ -1238,6 +1309,19 @@ var init_machines = __esm(() => {
|
|
|
1238
1309
|
});
|
|
1239
1310
|
|
|
1240
1311
|
// src/db/database.ts
|
|
1312
|
+
var exports_database = {};
|
|
1313
|
+
__export(exports_database, {
|
|
1314
|
+
uuid: () => uuid,
|
|
1315
|
+
resolvePartialId: () => resolvePartialId,
|
|
1316
|
+
resetDatabase: () => resetDatabase,
|
|
1317
|
+
now: () => now,
|
|
1318
|
+
lockExpiryCutoff: () => lockExpiryCutoff,
|
|
1319
|
+
isLockExpired: () => isLockExpired,
|
|
1320
|
+
getDatabase: () => getDatabase,
|
|
1321
|
+
closeDatabase: () => closeDatabase,
|
|
1322
|
+
clearExpiredLocks: () => clearExpiredLocks,
|
|
1323
|
+
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
1324
|
+
});
|
|
1241
1325
|
import { Database } from "bun:sqlite";
|
|
1242
1326
|
import { existsSync, mkdirSync } from "fs";
|
|
1243
1327
|
import { dirname, join, resolve } from "path";
|
|
@@ -1347,6 +1431,9 @@ function clearExpiredLocks(db) {
|
|
|
1347
1431
|
db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
|
|
1348
1432
|
}
|
|
1349
1433
|
function resolvePartialId(db, table, partialId) {
|
|
1434
|
+
if (!ALLOWED_TABLES.has(table)) {
|
|
1435
|
+
throw new Error(`Invalid table name: ${table}`);
|
|
1436
|
+
}
|
|
1350
1437
|
if (partialId.length >= 36) {
|
|
1351
1438
|
const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
|
|
1352
1439
|
return row?.id ?? null;
|
|
@@ -1376,10 +1463,11 @@ function resolvePartialId(db, table, partialId) {
|
|
|
1376
1463
|
}
|
|
1377
1464
|
return null;
|
|
1378
1465
|
}
|
|
1379
|
-
var LOCK_EXPIRY_MINUTES = 30, _db = null;
|
|
1466
|
+
var LOCK_EXPIRY_MINUTES = 30, _db = null, ALLOWED_TABLES;
|
|
1380
1467
|
var init_database = __esm(() => {
|
|
1381
1468
|
init_schema();
|
|
1382
1469
|
init_machines();
|
|
1470
|
+
ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates"]);
|
|
1383
1471
|
});
|
|
1384
1472
|
|
|
1385
1473
|
// src/types/index.ts
|
|
@@ -1612,231 +1700,572 @@ function getDueDispatches(db) {
|
|
|
1612
1700
|
ORDER BY created_at ASC`).all(now());
|
|
1613
1701
|
return rows.map(rowToDispatch);
|
|
1614
1702
|
}
|
|
1615
|
-
var init_dispatches = __esm(() => {
|
|
1616
|
-
init_database();
|
|
1617
|
-
init_types();
|
|
1618
|
-
});
|
|
1703
|
+
var init_dispatches = __esm(() => {
|
|
1704
|
+
init_database();
|
|
1705
|
+
init_types();
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
// src/sdk/types.ts
|
|
1709
|
+
class TodosAPIError extends Error {
|
|
1710
|
+
status;
|
|
1711
|
+
statusText;
|
|
1712
|
+
body;
|
|
1713
|
+
constructor(message, status, statusText, body) {
|
|
1714
|
+
super(message);
|
|
1715
|
+
this.status = status;
|
|
1716
|
+
this.statusText = statusText;
|
|
1717
|
+
this.body = body;
|
|
1718
|
+
this.name = "TodosAPIError";
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
class TodosNotFoundError extends TodosAPIError {
|
|
1723
|
+
constructor(message, body) {
|
|
1724
|
+
super(message, 404, "Not Found", body);
|
|
1725
|
+
this.name = "TodosNotFoundError";
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
class TodosConflictError extends TodosAPIError {
|
|
1730
|
+
constructor(message, body) {
|
|
1731
|
+
super(message, 409, "Conflict", body);
|
|
1732
|
+
this.name = "TodosConflictError";
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
class TodosUnauthorizedError extends TodosAPIError {
|
|
1737
|
+
constructor(message, body) {
|
|
1738
|
+
super(message, 401, "Unauthorized", body);
|
|
1739
|
+
this.name = "TodosUnauthorizedError";
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
class TodosRateLimitError extends TodosAPIError {
|
|
1744
|
+
retryAfter;
|
|
1745
|
+
constructor(message, retryAfter, body) {
|
|
1746
|
+
super(message, 429, "Too Many Requests", body);
|
|
1747
|
+
this.retryAfter = retryAfter;
|
|
1748
|
+
this.name = "TodosRateLimitError";
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
class TodosTimeoutError extends Error {
|
|
1753
|
+
ms;
|
|
1754
|
+
constructor(ms) {
|
|
1755
|
+
super(`Request timed out after ${ms}ms`);
|
|
1756
|
+
this.ms = ms;
|
|
1757
|
+
this.name = "TodosTimeoutError";
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// src/sdk/client.ts
|
|
1762
|
+
function buildQuery(params) {
|
|
1763
|
+
const search = new URLSearchParams;
|
|
1764
|
+
for (const [k, v] of Object.entries(params)) {
|
|
1765
|
+
if (v !== undefined)
|
|
1766
|
+
search.set(k, String(v));
|
|
1767
|
+
}
|
|
1768
|
+
const s = search.toString();
|
|
1769
|
+
return s ? `?${s}` : "";
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
class TasksResource {
|
|
1773
|
+
client;
|
|
1774
|
+
constructor(client) {
|
|
1775
|
+
this.client = client;
|
|
1776
|
+
}
|
|
1777
|
+
async list(options) {
|
|
1778
|
+
return this.client._get("/api/tasks", buildQuery(options || {}));
|
|
1779
|
+
}
|
|
1780
|
+
async get(id) {
|
|
1781
|
+
return this.client._get(`/api/tasks/${id}`);
|
|
1782
|
+
}
|
|
1783
|
+
async getWithRelations(id) {
|
|
1784
|
+
return this.client._get(`/api/tasks/${id}`);
|
|
1785
|
+
}
|
|
1786
|
+
async create(data) {
|
|
1787
|
+
return this.client._post("/api/tasks", data);
|
|
1788
|
+
}
|
|
1789
|
+
async update(id, data) {
|
|
1790
|
+
return this.client._patch(`/api/tasks/${id}`, data);
|
|
1791
|
+
}
|
|
1792
|
+
async delete(id) {
|
|
1793
|
+
return this.client._delete(`/api/tasks/${id}`);
|
|
1794
|
+
}
|
|
1795
|
+
async start(id, agentId) {
|
|
1796
|
+
return this.client._post(`/api/tasks/${id}/start`, { agent_id: agentId });
|
|
1797
|
+
}
|
|
1798
|
+
async complete(id, agentId) {
|
|
1799
|
+
return this.client._post(`/api/tasks/${id}/complete`, { agent_id: agentId });
|
|
1800
|
+
}
|
|
1801
|
+
async fail(id, options) {
|
|
1802
|
+
return this.client._post(`/api/tasks/${id}/fail`, options || {});
|
|
1803
|
+
}
|
|
1804
|
+
async logProgress(taskId, message, pctComplete, agentId) {
|
|
1805
|
+
return this.client._post(`/api/tasks/${taskId}/progress`, {
|
|
1806
|
+
message,
|
|
1807
|
+
pct_complete: pctComplete,
|
|
1808
|
+
agent_id: agentId
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
async getProgress(id) {
|
|
1812
|
+
return this.client._get(`/api/tasks/${id}/progress`);
|
|
1813
|
+
}
|
|
1814
|
+
async getHistory(id) {
|
|
1815
|
+
return this.client._get(`/api/tasks/${id}/history`);
|
|
1816
|
+
}
|
|
1817
|
+
async getAttachments(id) {
|
|
1818
|
+
return this.client._get(`/api/tasks/${id}/attachments`);
|
|
1819
|
+
}
|
|
1820
|
+
async status(options) {
|
|
1821
|
+
return this.client._get("/api/tasks/status", buildQuery(options || {}));
|
|
1822
|
+
}
|
|
1823
|
+
async next(options) {
|
|
1824
|
+
return this.client._get("/api/tasks/next", buildQuery(options || {}));
|
|
1825
|
+
}
|
|
1826
|
+
async active(projectId) {
|
|
1827
|
+
return this.client._get("/api/tasks/active", projectId ? `?project_id=${projectId}` : "");
|
|
1828
|
+
}
|
|
1829
|
+
async stale(options) {
|
|
1830
|
+
return this.client._get("/api/tasks/stale", buildQuery(options || {}));
|
|
1831
|
+
}
|
|
1832
|
+
async changedSince(since, projectId) {
|
|
1833
|
+
return this.client._get("/api/tasks/changed", buildQuery({ since, project_id: projectId }));
|
|
1834
|
+
}
|
|
1835
|
+
async context(options) {
|
|
1836
|
+
const q = buildQuery({
|
|
1837
|
+
agent_id: options?.agentId,
|
|
1838
|
+
project_id: options?.projectId,
|
|
1839
|
+
format: options?.format
|
|
1840
|
+
});
|
|
1841
|
+
const url = `${this.client.baseUrl}/api/tasks/context${q}`;
|
|
1842
|
+
const res = await this.client._fetchRaw(url);
|
|
1843
|
+
if (!res.ok)
|
|
1844
|
+
return options?.format === "json" ? {} : "";
|
|
1845
|
+
if (options?.format === "json")
|
|
1846
|
+
return res.json();
|
|
1847
|
+
return res.text();
|
|
1848
|
+
}
|
|
1849
|
+
async export(options) {
|
|
1850
|
+
const q = buildQuery(options || {});
|
|
1851
|
+
const url = `${this.client.baseUrl}/api/tasks/export${q}`;
|
|
1852
|
+
const res = await this.client._fetchRaw(url);
|
|
1853
|
+
if (!res.ok)
|
|
1854
|
+
throw new TodosAPIError("Export failed", res.status, res.statusText, null);
|
|
1855
|
+
if (options?.format === "csv")
|
|
1856
|
+
return res.text();
|
|
1857
|
+
return res.json();
|
|
1858
|
+
}
|
|
1859
|
+
async bulk(ids, action) {
|
|
1860
|
+
return this.client._post("/api/tasks/bulk", { ids, action });
|
|
1861
|
+
}
|
|
1862
|
+
async claim(agentId, projectId) {
|
|
1863
|
+
return this.client._post("/api/tasks/claim", { agent_id: agentId, project_id: projectId });
|
|
1864
|
+
}
|
|
1865
|
+
async* subscribe(options = {}) {
|
|
1866
|
+
const q = buildQuery({
|
|
1867
|
+
agent_id: options.agentId,
|
|
1868
|
+
project_id: options.projectId,
|
|
1869
|
+
events: options.events?.join(",")
|
|
1870
|
+
});
|
|
1871
|
+
const url = `${this.client.baseUrl}/api/tasks/stream${q}`;
|
|
1872
|
+
const resp = await fetch(url);
|
|
1873
|
+
if (!resp.ok || !resp.body)
|
|
1874
|
+
throw new Error(`SSE connection failed: ${resp.status}`);
|
|
1875
|
+
const reader = resp.body.getReader();
|
|
1876
|
+
const decoder = new TextDecoder;
|
|
1877
|
+
let buffer = "";
|
|
1878
|
+
try {
|
|
1879
|
+
while (true) {
|
|
1880
|
+
const { done, value } = await reader.read();
|
|
1881
|
+
if (done)
|
|
1882
|
+
break;
|
|
1883
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1884
|
+
const lines = buffer.split(`
|
|
1885
|
+
`);
|
|
1886
|
+
buffer = lines.pop() || "";
|
|
1887
|
+
for (const line of lines) {
|
|
1888
|
+
if (line.startsWith("data: ")) {
|
|
1889
|
+
try {
|
|
1890
|
+
const data = JSON.parse(line.slice(6));
|
|
1891
|
+
if (data.type !== "connected")
|
|
1892
|
+
yield data;
|
|
1893
|
+
} catch {}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
} finally {
|
|
1898
|
+
reader.releaseLock();
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
class AgentsResource {
|
|
1904
|
+
client;
|
|
1905
|
+
constructor(client) {
|
|
1906
|
+
this.client = client;
|
|
1907
|
+
}
|
|
1908
|
+
async list(options) {
|
|
1909
|
+
return this.client._get("/api/agents", buildQuery(options || {}));
|
|
1910
|
+
}
|
|
1911
|
+
async register(data) {
|
|
1912
|
+
return this.client._post("/api/agents", data);
|
|
1913
|
+
}
|
|
1914
|
+
async fullRegister(data) {
|
|
1915
|
+
return this.client._post("/api/agents", data);
|
|
1916
|
+
}
|
|
1917
|
+
async update(id, data) {
|
|
1918
|
+
return this.client._patch(`/api/agents/${id}`, data);
|
|
1919
|
+
}
|
|
1920
|
+
async delete(id) {
|
|
1921
|
+
return this.client._delete(`/api/agents/${id}`);
|
|
1922
|
+
}
|
|
1923
|
+
async bulkDelete(ids) {
|
|
1924
|
+
return this.client._post("/api/agents/bulk", { ids, action: "delete" });
|
|
1925
|
+
}
|
|
1926
|
+
async me(name) {
|
|
1927
|
+
return this.client._get("/api/agents/me", `?name=${encodeURIComponent(name)}`);
|
|
1928
|
+
}
|
|
1929
|
+
async queue(agentId) {
|
|
1930
|
+
return this.client._get(`/api/agents/${encodeURIComponent(agentId)}/queue`);
|
|
1931
|
+
}
|
|
1932
|
+
async team(agentId) {
|
|
1933
|
+
return this.client._get(`/api/agents/${encodeURIComponent(agentId)}/team`);
|
|
1934
|
+
}
|
|
1935
|
+
async orgChart() {
|
|
1936
|
+
return this.client._get("/api/org");
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
class ProjectsResource {
|
|
1941
|
+
client;
|
|
1942
|
+
constructor(client) {
|
|
1943
|
+
this.client = client;
|
|
1944
|
+
}
|
|
1945
|
+
async list(options) {
|
|
1946
|
+
return this.client._get("/api/projects", buildQuery(options || {}));
|
|
1947
|
+
}
|
|
1948
|
+
async create(data) {
|
|
1949
|
+
return this.client._post("/api/projects", data);
|
|
1950
|
+
}
|
|
1951
|
+
async delete(id) {
|
|
1952
|
+
return this.client._delete(`/api/projects/${id}`);
|
|
1953
|
+
}
|
|
1954
|
+
async bulkDelete(ids) {
|
|
1955
|
+
return this.client._post("/api/projects/bulk", { ids, action: "delete" });
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
class PlansResource {
|
|
1960
|
+
client;
|
|
1961
|
+
constructor(client) {
|
|
1962
|
+
this.client = client;
|
|
1963
|
+
}
|
|
1964
|
+
async list(projectId) {
|
|
1965
|
+
return this.client._get("/api/plans", projectId ? `?project_id=${projectId}` : "");
|
|
1966
|
+
}
|
|
1967
|
+
async create(data) {
|
|
1968
|
+
return this.client._post("/api/plans", data);
|
|
1969
|
+
}
|
|
1970
|
+
async get(id) {
|
|
1971
|
+
return this.client._get(`/api/plans/${id}`);
|
|
1972
|
+
}
|
|
1973
|
+
async update(id, data) {
|
|
1974
|
+
return this.client._patch(`/api/plans/${id}`, data);
|
|
1975
|
+
}
|
|
1976
|
+
async delete(id) {
|
|
1977
|
+
return this.client._delete(`/api/plans/${id}`);
|
|
1978
|
+
}
|
|
1979
|
+
async bulkDelete(ids) {
|
|
1980
|
+
return this.client._post("/api/plans/bulk", { ids, action: "delete" });
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
class OrgsResource {
|
|
1985
|
+
client;
|
|
1986
|
+
constructor(client) {
|
|
1987
|
+
this.client = client;
|
|
1988
|
+
}
|
|
1989
|
+
async list() {
|
|
1990
|
+
return this.client._get("/api/orgs");
|
|
1991
|
+
}
|
|
1992
|
+
async create(data) {
|
|
1993
|
+
return this.client._post("/api/orgs", data);
|
|
1994
|
+
}
|
|
1995
|
+
async update(id, data) {
|
|
1996
|
+
return this.client._patch(`/api/orgs/${id}`, data);
|
|
1997
|
+
}
|
|
1998
|
+
async delete(id) {
|
|
1999
|
+
return this.client._delete(`/api/orgs/${id}`);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
class WebhooksResource {
|
|
2004
|
+
client;
|
|
2005
|
+
constructor(client) {
|
|
2006
|
+
this.client = client;
|
|
2007
|
+
}
|
|
2008
|
+
async list() {
|
|
2009
|
+
return this.client._get("/api/webhooks");
|
|
2010
|
+
}
|
|
2011
|
+
async create(data) {
|
|
2012
|
+
return this.client._post("/api/webhooks", data);
|
|
2013
|
+
}
|
|
2014
|
+
async delete(id) {
|
|
2015
|
+
return this.client._delete(`/api/webhooks/${id}`);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
class TemplatesResource {
|
|
2020
|
+
client;
|
|
2021
|
+
constructor(client) {
|
|
2022
|
+
this.client = client;
|
|
2023
|
+
}
|
|
2024
|
+
async list() {
|
|
2025
|
+
return this.client._get("/api/templates");
|
|
2026
|
+
}
|
|
2027
|
+
async create(data) {
|
|
2028
|
+
return this.client._post("/api/templates", data);
|
|
2029
|
+
}
|
|
2030
|
+
async delete(id) {
|
|
2031
|
+
return this.client._delete(`/api/templates/${id}`);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
1619
2034
|
|
|
1620
|
-
// src/sdk.ts
|
|
1621
2035
|
class TodosClient {
|
|
1622
2036
|
baseUrl;
|
|
1623
2037
|
timeout;
|
|
2038
|
+
apiKey;
|
|
2039
|
+
maxRetries;
|
|
2040
|
+
retryDelay;
|
|
2041
|
+
tasks;
|
|
2042
|
+
agents;
|
|
2043
|
+
projects;
|
|
2044
|
+
plans;
|
|
2045
|
+
orgs;
|
|
2046
|
+
webhooks;
|
|
2047
|
+
templates;
|
|
1624
2048
|
constructor(options = {}) {
|
|
1625
2049
|
this.baseUrl = options.baseUrl || process.env["TODOS_URL"] || "http://localhost:19427";
|
|
1626
|
-
this.
|
|
2050
|
+
this.baseUrl = this.baseUrl.replace(/\/+$/, "");
|
|
2051
|
+
this.timeout = options.timeout ?? 1e4;
|
|
2052
|
+
this.apiKey = options.apiKey || process.env["TODOS_API_KEY"] || null;
|
|
2053
|
+
this.maxRetries = options.maxRetries ?? 0;
|
|
2054
|
+
this.retryDelay = options.retryDelay ?? 1000;
|
|
2055
|
+
this.tasks = new TasksResource(this);
|
|
2056
|
+
this.agents = new AgentsResource(this);
|
|
2057
|
+
this.projects = new ProjectsResource(this);
|
|
2058
|
+
this.plans = new PlansResource(this);
|
|
2059
|
+
this.orgs = new OrgsResource(this);
|
|
2060
|
+
this.webhooks = new WebhooksResource(this);
|
|
2061
|
+
this.templates = new TemplatesResource(this);
|
|
2062
|
+
}
|
|
2063
|
+
static fromEnv(apiKey) {
|
|
2064
|
+
return new TodosClient({ apiKey });
|
|
2065
|
+
}
|
|
2066
|
+
async _fetchRaw(url, init) {
|
|
2067
|
+
const controller = new AbortController;
|
|
2068
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
2069
|
+
try {
|
|
2070
|
+
const headers = this._buildHeaders(init?.headers);
|
|
2071
|
+
return await fetch(url, { ...init, headers, signal: controller.signal });
|
|
2072
|
+
} finally {
|
|
2073
|
+
clearTimeout(timer);
|
|
2074
|
+
}
|
|
1627
2075
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
2076
|
+
_buildHeaders(existing) {
|
|
2077
|
+
const headers = { "Content-Type": "application/json" };
|
|
2078
|
+
if (this.apiKey)
|
|
2079
|
+
headers["x-api-key"] = this.apiKey;
|
|
2080
|
+
if (existing) {
|
|
2081
|
+
if (existing instanceof Headers) {
|
|
2082
|
+
existing.forEach((v, k) => {
|
|
2083
|
+
headers[k] = v;
|
|
2084
|
+
});
|
|
2085
|
+
} else if (Array.isArray(existing)) {
|
|
2086
|
+
for (const [k, v] of existing)
|
|
2087
|
+
headers[k] = v;
|
|
2088
|
+
} else {
|
|
2089
|
+
Object.assign(headers, existing);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return headers;
|
|
2093
|
+
}
|
|
2094
|
+
async _fetchWithRetry(path, init) {
|
|
2095
|
+
let lastError = null;
|
|
2096
|
+
const maxAttempts = this.maxRetries + 1;
|
|
2097
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
2098
|
+
try {
|
|
2099
|
+
return await this._fetch(path, init);
|
|
2100
|
+
} catch (e) {
|
|
2101
|
+
lastError = e;
|
|
2102
|
+
if (e instanceof TodosAPIError && e.status < 500 && e.status !== 429)
|
|
2103
|
+
throw e;
|
|
2104
|
+
if (e instanceof TodosUnauthorizedError || e instanceof TodosNotFoundError || e instanceof TodosConflictError)
|
|
2105
|
+
throw e;
|
|
2106
|
+
if (attempt < maxAttempts - 1) {
|
|
2107
|
+
const delay = this.retryDelay * Math.pow(2, attempt);
|
|
2108
|
+
if (e instanceof TodosRateLimitError) {
|
|
2109
|
+
await this._sleep(e.retryAfter * 1000);
|
|
2110
|
+
} else {
|
|
2111
|
+
await this._sleep(delay);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
throw lastError || new Error("Request failed after retries");
|
|
1630
2117
|
}
|
|
1631
|
-
async
|
|
2118
|
+
async _fetch(path, init) {
|
|
1632
2119
|
const controller = new AbortController;
|
|
1633
2120
|
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
1634
2121
|
try {
|
|
1635
|
-
const
|
|
2122
|
+
const url = `${this.baseUrl}${path}`;
|
|
2123
|
+
const headers = this._buildHeaders(init?.headers);
|
|
2124
|
+
const res = await fetch(url, { ...init, headers, signal: controller.signal });
|
|
1636
2125
|
if (!res.ok) {
|
|
1637
|
-
const
|
|
1638
|
-
|
|
2126
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
2127
|
+
const message = body.error || `HTTP ${res.status}: ${res.statusText}`;
|
|
2128
|
+
if (res.status === 401)
|
|
2129
|
+
throw new TodosUnauthorizedError(message, body);
|
|
2130
|
+
if (res.status === 404)
|
|
2131
|
+
throw new TodosNotFoundError(message, body);
|
|
2132
|
+
if (res.status === 409)
|
|
2133
|
+
throw new TodosConflictError(message, body);
|
|
2134
|
+
if (res.status === 429) {
|
|
2135
|
+
const retryAfter = parseInt(res.headers.get("retry-after") || "60", 10);
|
|
2136
|
+
throw new TodosRateLimitError(message, retryAfter, body);
|
|
2137
|
+
}
|
|
2138
|
+
throw new TodosAPIError(message, res.status, res.statusText, body);
|
|
1639
2139
|
}
|
|
2140
|
+
const contentLength = res.headers.get("content-length");
|
|
2141
|
+
if (contentLength === "0" || contentLength === "4")
|
|
2142
|
+
return null;
|
|
1640
2143
|
return res.json();
|
|
2144
|
+
} catch (e) {
|
|
2145
|
+
if (e instanceof DOMException && e.name === "AbortError") {
|
|
2146
|
+
throw new TodosTimeoutError(this.timeout);
|
|
2147
|
+
}
|
|
2148
|
+
throw e;
|
|
1641
2149
|
} finally {
|
|
1642
2150
|
clearTimeout(timer);
|
|
1643
2151
|
}
|
|
1644
2152
|
}
|
|
2153
|
+
async _get(path, query = "") {
|
|
2154
|
+
return this._fetchWithRetry(`${path}${query}`);
|
|
2155
|
+
}
|
|
2156
|
+
async _post(path, body) {
|
|
2157
|
+
return this._fetchWithRetry(path, {
|
|
2158
|
+
method: "POST",
|
|
2159
|
+
body: body ? JSON.stringify(body) : undefined
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
async _patch(path, body) {
|
|
2163
|
+
return this._fetchWithRetry(path, {
|
|
2164
|
+
method: "PATCH",
|
|
2165
|
+
body: JSON.stringify(body)
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
async _delete(path) {
|
|
2169
|
+
return this._fetchWithRetry(path, { method: "DELETE" });
|
|
2170
|
+
}
|
|
2171
|
+
_sleep(ms) {
|
|
2172
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2173
|
+
}
|
|
1645
2174
|
async getHealth() {
|
|
1646
|
-
return this.
|
|
2175
|
+
return this._get("/api/health");
|
|
1647
2176
|
}
|
|
1648
2177
|
async isAlive() {
|
|
1649
2178
|
try {
|
|
1650
|
-
await this.
|
|
2179
|
+
await this._get("/api/stats");
|
|
1651
2180
|
return true;
|
|
1652
2181
|
} catch {
|
|
1653
2182
|
return false;
|
|
1654
2183
|
}
|
|
1655
2184
|
}
|
|
1656
|
-
async
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
2185
|
+
async getStats() {
|
|
2186
|
+
return this._get("/api/stats");
|
|
2187
|
+
}
|
|
2188
|
+
async getReport(options) {
|
|
2189
|
+
return this._get("/api/report", buildQuery({ days: options?.days, project_id: options?.projectId }));
|
|
2190
|
+
}
|
|
2191
|
+
async doctor() {
|
|
2192
|
+
return this._get("/api/doctor");
|
|
2193
|
+
}
|
|
2194
|
+
async activity(limit) {
|
|
2195
|
+
return this._get("/api/activity", limit ? `?limit=${limit}` : "");
|
|
1663
2196
|
}
|
|
1664
2197
|
async listTasks(filter = {}) {
|
|
1665
|
-
|
|
1666
|
-
for (const [k, v] of Object.entries(filter)) {
|
|
1667
|
-
if (v !== undefined)
|
|
1668
|
-
params.set(k, String(v));
|
|
1669
|
-
}
|
|
1670
|
-
return this.fetch(`/api/tasks?${params}`);
|
|
2198
|
+
return this.tasks.list(filter);
|
|
1671
2199
|
}
|
|
1672
2200
|
async getTask(id) {
|
|
1673
|
-
return this.
|
|
2201
|
+
return this.tasks.get(id);
|
|
1674
2202
|
}
|
|
1675
|
-
async
|
|
1676
|
-
return this.
|
|
2203
|
+
async createTask(data) {
|
|
2204
|
+
return this.tasks.create(data);
|
|
1677
2205
|
}
|
|
1678
|
-
async
|
|
1679
|
-
return this.
|
|
2206
|
+
async updateTask(id, data) {
|
|
2207
|
+
return this.tasks.update(id, data);
|
|
1680
2208
|
}
|
|
1681
|
-
async
|
|
1682
|
-
|
|
2209
|
+
async deleteTask(id) {
|
|
2210
|
+
await this.tasks.delete(id);
|
|
1683
2211
|
}
|
|
1684
|
-
async
|
|
1685
|
-
return this.
|
|
1686
|
-
method: "POST",
|
|
1687
|
-
headers: { "Content-Type": "application/json" },
|
|
1688
|
-
body: JSON.stringify({ agent_id: agentId, project_id: projectId })
|
|
1689
|
-
});
|
|
2212
|
+
async startTask(id, agentId) {
|
|
2213
|
+
return this.tasks.start(id, agentId);
|
|
1690
2214
|
}
|
|
1691
2215
|
async completeTask(id, agentId) {
|
|
1692
|
-
return this.
|
|
1693
|
-
method: "POST",
|
|
1694
|
-
headers: { "Content-Type": "application/json" },
|
|
1695
|
-
body: JSON.stringify({ agent_id: agentId })
|
|
1696
|
-
});
|
|
1697
|
-
}
|
|
1698
|
-
async createTask(data) {
|
|
1699
|
-
return this.fetch("/api/tasks", {
|
|
1700
|
-
method: "POST",
|
|
1701
|
-
headers: { "Content-Type": "application/json" },
|
|
1702
|
-
body: JSON.stringify(data)
|
|
1703
|
-
});
|
|
2216
|
+
return this.tasks.complete(id, agentId);
|
|
1704
2217
|
}
|
|
1705
|
-
async
|
|
1706
|
-
return this.
|
|
1707
|
-
method: "PATCH",
|
|
1708
|
-
headers: { "Content-Type": "application/json" },
|
|
1709
|
-
body: JSON.stringify(data)
|
|
1710
|
-
});
|
|
2218
|
+
async failTask(id, options = {}) {
|
|
2219
|
+
return this.tasks.fail(id, options);
|
|
1711
2220
|
}
|
|
1712
|
-
async
|
|
1713
|
-
|
|
2221
|
+
async logProgress(taskId, message, pctComplete, agentId) {
|
|
2222
|
+
return this.tasks.logProgress(taskId, message, pctComplete, agentId);
|
|
1714
2223
|
}
|
|
1715
|
-
async
|
|
1716
|
-
return this.
|
|
2224
|
+
async getStatus(projectId, agentId) {
|
|
2225
|
+
return this.tasks.status({ project_id: projectId, agent_id: agentId });
|
|
1717
2226
|
}
|
|
1718
2227
|
async getActiveWork(projectId) {
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
params.set("project_id", projectId);
|
|
1722
|
-
return this.fetch(`/api/tasks/active?${params}`);
|
|
2228
|
+
const res = await this.tasks.active(projectId);
|
|
2229
|
+
return res.active;
|
|
1723
2230
|
}
|
|
1724
2231
|
async getTasksChangedSince(since, projectId) {
|
|
1725
|
-
|
|
1726
|
-
if (projectId)
|
|
1727
|
-
params.set("project_id", projectId);
|
|
1728
|
-
return this.fetch(`/api/tasks/changed?${params}`);
|
|
1729
|
-
}
|
|
1730
|
-
async startTask(id, agentId) {
|
|
1731
|
-
return this.fetch(`/api/tasks/${id}/start`, {
|
|
1732
|
-
method: "POST",
|
|
1733
|
-
headers: { "Content-Type": "application/json" },
|
|
1734
|
-
body: JSON.stringify({ agent_id: agentId })
|
|
1735
|
-
});
|
|
2232
|
+
return this.tasks.changedSince(since, projectId);
|
|
1736
2233
|
}
|
|
1737
|
-
async
|
|
1738
|
-
return this.
|
|
1739
|
-
method: "POST",
|
|
1740
|
-
headers: { "Content-Type": "application/json" },
|
|
1741
|
-
body: JSON.stringify(options)
|
|
1742
|
-
});
|
|
2234
|
+
async getStaleTasks(minutes, projectId) {
|
|
2235
|
+
return this.tasks.stale({ minutes, project_id: projectId });
|
|
1743
2236
|
}
|
|
1744
|
-
async
|
|
1745
|
-
return this.
|
|
1746
|
-
method: "POST",
|
|
1747
|
-
headers: { "Content-Type": "application/json" },
|
|
1748
|
-
body: JSON.stringify({ message, pct_complete: pctComplete, agent_id: agentId })
|
|
1749
|
-
});
|
|
2237
|
+
async getContext(options = {}) {
|
|
2238
|
+
return this.tasks.context(options);
|
|
1750
2239
|
}
|
|
1751
2240
|
async exportTasks(filter = {}) {
|
|
1752
|
-
|
|
1753
|
-
if (filter.status)
|
|
1754
|
-
params.set("status", filter.status);
|
|
1755
|
-
if (filter.project_id)
|
|
1756
|
-
params.set("project_id", filter.project_id);
|
|
1757
|
-
const fmt = filter.format || "json";
|
|
1758
|
-
if (fmt === "csv")
|
|
1759
|
-
params.set("format", "csv");
|
|
1760
|
-
return this.fetch(`/api/tasks/export?${params}`);
|
|
2241
|
+
return this.tasks.export(filter);
|
|
1761
2242
|
}
|
|
1762
|
-
async
|
|
1763
|
-
return this.
|
|
2243
|
+
async claimNextTask(agentId, projectId) {
|
|
2244
|
+
return this.tasks.claim(agentId, projectId);
|
|
1764
2245
|
}
|
|
1765
|
-
async
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
params.set("project_id", options.projectId);
|
|
1771
|
-
if (options.format)
|
|
1772
|
-
params.set("format", options.format);
|
|
1773
|
-
const url = `${this.baseUrl}/api/tasks/context?${params}`;
|
|
1774
|
-
const controller = new AbortController;
|
|
1775
|
-
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
1776
|
-
try {
|
|
1777
|
-
const res = await fetch(url, { signal: controller.signal });
|
|
1778
|
-
if (!res.ok)
|
|
1779
|
-
return options.format === "json" ? {} : "";
|
|
1780
|
-
if (options.format === "json")
|
|
1781
|
-
return res.json();
|
|
1782
|
-
return res.text();
|
|
1783
|
-
} catch {
|
|
1784
|
-
return options.format === "json" ? {} : "";
|
|
1785
|
-
} finally {
|
|
1786
|
-
clearTimeout(timer);
|
|
1787
|
-
}
|
|
2246
|
+
async getTaskHistory(id) {
|
|
2247
|
+
return this.tasks.getHistory(id);
|
|
2248
|
+
}
|
|
2249
|
+
async getTaskAttachments(id) {
|
|
2250
|
+
return this.tasks.getAttachments(id);
|
|
1788
2251
|
}
|
|
1789
|
-
async
|
|
1790
|
-
|
|
1791
|
-
if (options.days)
|
|
1792
|
-
params.set("days", String(options.days));
|
|
1793
|
-
if (options.projectId)
|
|
1794
|
-
params.set("project_id", options.projectId);
|
|
1795
|
-
return this.fetch(`/api/report?${params}`);
|
|
2252
|
+
async getTaskProgress(id) {
|
|
2253
|
+
return this.tasks.getProgress(id);
|
|
1796
2254
|
}
|
|
1797
2255
|
async* subscribeToStream(options = {}) {
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
params.set("project_id", options.projectId);
|
|
1803
|
-
if (options.events)
|
|
1804
|
-
params.set("events", options.events.join(","));
|
|
1805
|
-
const url = `${this.baseUrl}/api/tasks/stream?${params}`;
|
|
1806
|
-
const resp = await fetch(url);
|
|
1807
|
-
if (!resp.ok || !resp.body)
|
|
1808
|
-
throw new Error(`SSE connection failed: ${resp.status}`);
|
|
1809
|
-
const reader = resp.body.getReader();
|
|
1810
|
-
const decoder = new TextDecoder;
|
|
1811
|
-
let buffer = "";
|
|
1812
|
-
while (true) {
|
|
1813
|
-
const { done, value } = await reader.read();
|
|
1814
|
-
if (done)
|
|
1815
|
-
break;
|
|
1816
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1817
|
-
const lines = buffer.split(`
|
|
1818
|
-
`);
|
|
1819
|
-
buffer = lines.pop() || "";
|
|
1820
|
-
for (const line of lines) {
|
|
1821
|
-
if (line.startsWith("data: ")) {
|
|
1822
|
-
try {
|
|
1823
|
-
const data = JSON.parse(line.slice(6));
|
|
1824
|
-
if (data.type !== "connected")
|
|
1825
|
-
yield data;
|
|
1826
|
-
} catch {}
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
2256
|
+
yield* this.tasks.subscribe(options);
|
|
2257
|
+
}
|
|
2258
|
+
async getProjects() {
|
|
2259
|
+
return this.projects.list();
|
|
1830
2260
|
}
|
|
1831
2261
|
}
|
|
1832
2262
|
function createClient(options) {
|
|
1833
2263
|
return new TodosClient(options);
|
|
1834
2264
|
}
|
|
1835
|
-
|
|
1836
2265
|
// src/index.ts
|
|
1837
2266
|
init_database();
|
|
1838
2267
|
|
|
1839
|
-
// src/db/
|
|
2268
|
+
// src/db/task-crud.ts
|
|
1840
2269
|
init_types();
|
|
1841
2270
|
init_database();
|
|
1842
2271
|
|
|
@@ -2207,184 +2636,111 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
|
|
|
2207
2636
|
const remaining = Math.ceil(config.min_work_seconds - elapsedSeconds);
|
|
2208
2637
|
throw new CompletionGuardError(`Too fast: task was started ${Math.floor(elapsedSeconds)}s ago. Minimum work duration is ${config.min_work_seconds}s. Wait ${remaining}s.`, remaining);
|
|
2209
2638
|
}
|
|
2210
|
-
}
|
|
2211
|
-
if (agent && config.max_completions_per_window && config.window_minutes) {
|
|
2212
|
-
const windowStart = new Date(Date.now() - config.window_minutes * 60 * 1000).toISOString();
|
|
2213
|
-
const result = db.query(`SELECT COUNT(*) as count FROM tasks
|
|
2214
|
-
WHERE completed_at > ? AND (assigned_to = ? OR agent_id = ?)`).get(windowStart, agent, agent);
|
|
2215
|
-
if (result.count >= config.max_completions_per_window) {
|
|
2216
|
-
throw new CompletionGuardError(`Rate limit: ${result.count} tasks completed in the last ${config.window_minutes} minutes (max ${config.max_completions_per_window}). Slow down.`);
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
if (agent && config.cooldown_seconds) {
|
|
2220
|
-
const result = db.query(`SELECT MAX(completed_at) as last_completed FROM tasks
|
|
2221
|
-
WHERE completed_at IS NOT NULL AND (assigned_to = ? OR agent_id = ?) AND id != ?`).get(agent, agent, task.id);
|
|
2222
|
-
if (result.last_completed) {
|
|
2223
|
-
const elapsedSeconds = (Date.now() - new Date(result.last_completed).getTime()) / 1000;
|
|
2224
|
-
if (elapsedSeconds < config.cooldown_seconds) {
|
|
2225
|
-
const remaining = Math.ceil(config.cooldown_seconds - elapsedSeconds);
|
|
2226
|
-
throw new CompletionGuardError(`Cooldown: last completion was ${Math.floor(elapsedSeconds)}s ago. Wait ${remaining}s between completions.`, remaining);
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
|
|
2232
|
-
// src/db/audit.ts
|
|
2233
|
-
init_database();
|
|
2234
|
-
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
2235
|
-
const d = db || getDatabase();
|
|
2236
|
-
const id = uuid();
|
|
2237
|
-
const timestamp = now();
|
|
2238
|
-
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
|
|
2239
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
|
|
2240
|
-
return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
|
|
2241
|
-
}
|
|
2242
|
-
function getTaskHistory(taskId, db) {
|
|
2243
|
-
const d = db || getDatabase();
|
|
2244
|
-
return d.query("SELECT * FROM task_history WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
|
|
2245
|
-
}
|
|
2246
|
-
function getRecentActivity(limit = 50, db) {
|
|
2247
|
-
const d = db || getDatabase();
|
|
2248
|
-
return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
2249
|
-
}
|
|
2250
|
-
function getRecap(hours = 8, projectId, db) {
|
|
2251
|
-
const d = db || getDatabase();
|
|
2252
|
-
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
2253
|
-
const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
2254
|
-
const pf = projectId ? " AND project_id = ?" : "";
|
|
2255
|
-
const tpf = projectId ? " AND t.project_id = ?" : "";
|
|
2256
|
-
const completed = projectId ? d.query(`SELECT id, short_id, title, assigned_to, completed_at, started_at FROM tasks WHERE status = 'completed' AND completed_at > ?${pf} ORDER BY completed_at DESC`).all(since, projectId) : d.query(`SELECT id, short_id, title, assigned_to, completed_at, started_at FROM tasks WHERE status = 'completed' AND completed_at > ? ORDER BY completed_at DESC`).all(since);
|
|
2257
|
-
const created = projectId ? d.query(`SELECT id, short_id, title, agent_id, created_at FROM tasks WHERE created_at > ?${pf} ORDER BY created_at DESC`).all(since, projectId) : d.query(`SELECT id, short_id, title, agent_id, created_at FROM tasks WHERE created_at > ? ORDER BY created_at DESC`).all(since);
|
|
2258
|
-
const in_progress = projectId ? d.query(`SELECT id, short_id, title, assigned_to, started_at FROM tasks WHERE status = 'in_progress' AND project_id = ? ORDER BY updated_at DESC`).all(projectId) : d.query(`SELECT id, short_id, title, assigned_to, started_at FROM tasks WHERE status = 'in_progress' ORDER BY updated_at DESC`).all();
|
|
2259
|
-
const blocked = projectId ? d.query(`SELECT DISTINCT t.id, t.short_id, t.title, t.assigned_to FROM tasks t JOIN task_dependencies td ON td.task_id = t.id JOIN tasks dep ON dep.id = td.depends_on AND dep.status NOT IN ('completed','cancelled') WHERE t.status = 'pending'${tpf}`).all(projectId) : d.query(`SELECT DISTINCT t.id, t.short_id, t.title, t.assigned_to FROM tasks t JOIN task_dependencies td ON td.task_id = t.id JOIN tasks dep ON dep.id = td.depends_on AND dep.status NOT IN ('completed','cancelled') WHERE t.status = 'pending'`).all();
|
|
2260
|
-
const stale = projectId ? d.query(`SELECT id, short_id, title, assigned_to, updated_at FROM tasks WHERE status = 'in_progress' AND updated_at < ? AND project_id = ? ORDER BY updated_at ASC`).all(staleWindow, projectId) : d.query(`SELECT id, short_id, title, assigned_to, updated_at FROM tasks WHERE status = 'in_progress' AND updated_at < ? ORDER BY updated_at ASC`).all(staleWindow);
|
|
2261
|
-
const agents = projectId ? d.query(`SELECT a.name, a.last_seen_at, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'completed' AND t.completed_at > ?${tpf}) as completed_count, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'in_progress'${tpf}) as in_progress_count FROM agents a WHERE a.status = 'active' AND a.last_seen_at > ? ORDER BY completed_count DESC`).all(since, projectId, projectId, since) : d.query(`SELECT a.name, a.last_seen_at, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'completed' AND t.completed_at > ?) as completed_count, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'in_progress') as in_progress_count FROM agents a WHERE a.status = 'active' AND a.last_seen_at > ? ORDER BY completed_count DESC`).all(since, since);
|
|
2262
|
-
return {
|
|
2263
|
-
hours,
|
|
2264
|
-
since,
|
|
2265
|
-
completed: completed.map((r) => ({
|
|
2266
|
-
...r,
|
|
2267
|
-
duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
|
|
2268
|
-
})),
|
|
2269
|
-
created,
|
|
2270
|
-
in_progress,
|
|
2271
|
-
blocked,
|
|
2272
|
-
stale,
|
|
2273
|
-
agents
|
|
2274
|
-
};
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
// src/lib/recurrence.ts
|
|
2278
|
-
var DAY_NAMES = {
|
|
2279
|
-
sunday: 0,
|
|
2280
|
-
sun: 0,
|
|
2281
|
-
monday: 1,
|
|
2282
|
-
mon: 1,
|
|
2283
|
-
tuesday: 2,
|
|
2284
|
-
tue: 2,
|
|
2285
|
-
wednesday: 3,
|
|
2286
|
-
wed: 3,
|
|
2287
|
-
thursday: 4,
|
|
2288
|
-
thu: 4,
|
|
2289
|
-
friday: 5,
|
|
2290
|
-
fri: 5,
|
|
2291
|
-
saturday: 6,
|
|
2292
|
-
sat: 6
|
|
2293
|
-
};
|
|
2294
|
-
function parseRecurrenceRule(rule) {
|
|
2295
|
-
const normalized = rule.trim().toLowerCase();
|
|
2296
|
-
if (normalized === "every weekday" || normalized === "every weekdays") {
|
|
2297
|
-
return { type: "specific_days", days: [1, 2, 3, 4, 5] };
|
|
2298
|
-
}
|
|
2299
|
-
if (normalized === "every day" || normalized === "daily") {
|
|
2300
|
-
return { type: "interval", interval: 1, unit: "day" };
|
|
2301
|
-
}
|
|
2302
|
-
if (normalized === "every week" || normalized === "weekly") {
|
|
2303
|
-
return { type: "interval", interval: 1, unit: "week" };
|
|
2304
|
-
}
|
|
2305
|
-
if (normalized === "every month" || normalized === "monthly") {
|
|
2306
|
-
return { type: "interval", interval: 1, unit: "month" };
|
|
2307
|
-
}
|
|
2308
|
-
const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
|
|
2309
|
-
if (intervalMatch) {
|
|
2310
|
-
return {
|
|
2311
|
-
type: "interval",
|
|
2312
|
-
interval: parseInt(intervalMatch[1], 10),
|
|
2313
|
-
unit: intervalMatch[2]
|
|
2314
|
-
};
|
|
2315
|
-
}
|
|
2316
|
-
const daysMatch = normalized.match(/^every\s+(.+)$/);
|
|
2317
|
-
if (daysMatch) {
|
|
2318
|
-
const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
|
|
2319
|
-
const days = [];
|
|
2320
|
-
for (const part of dayParts) {
|
|
2321
|
-
const dayNum = DAY_NAMES[part];
|
|
2322
|
-
if (dayNum !== undefined) {
|
|
2323
|
-
days.push(dayNum);
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
if (days.length > 0) {
|
|
2327
|
-
return { type: "specific_days", days: days.sort((a, b) => a - b) };
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
|
-
throw new Error(`Invalid recurrence rule: "${rule}". Supported formats: "every day", "every weekday", "every week", "every 2 weeks", "every month", "every N days/weeks/months", "every monday", "every mon,wed,fri"`);
|
|
2331
|
-
}
|
|
2332
|
-
function isValidRecurrenceRule(rule) {
|
|
2333
|
-
try {
|
|
2334
|
-
parseRecurrenceRule(rule);
|
|
2335
|
-
return true;
|
|
2336
|
-
} catch {
|
|
2337
|
-
return false;
|
|
2338
|
-
}
|
|
2339
|
-
}
|
|
2340
|
-
function nextOccurrence(rule, from) {
|
|
2341
|
-
const parsed = parseRecurrenceRule(rule);
|
|
2342
|
-
const base = from || new Date;
|
|
2343
|
-
if (parsed.type === "interval") {
|
|
2344
|
-
const next = new Date(base);
|
|
2345
|
-
if (parsed.unit === "day") {
|
|
2346
|
-
next.setDate(next.getDate() + parsed.interval);
|
|
2347
|
-
} else if (parsed.unit === "week") {
|
|
2348
|
-
next.setDate(next.getDate() + parsed.interval * 7);
|
|
2349
|
-
} else if (parsed.unit === "month") {
|
|
2350
|
-
next.setMonth(next.getMonth() + parsed.interval);
|
|
2639
|
+
}
|
|
2640
|
+
if (agent && config.max_completions_per_window && config.window_minutes) {
|
|
2641
|
+
const windowStart = new Date(Date.now() - config.window_minutes * 60 * 1000).toISOString();
|
|
2642
|
+
const result = db.query(`SELECT COUNT(*) as count FROM tasks
|
|
2643
|
+
WHERE completed_at > ? AND (assigned_to = ? OR agent_id = ?)`).get(windowStart, agent, agent);
|
|
2644
|
+
if (result.count >= config.max_completions_per_window) {
|
|
2645
|
+
throw new CompletionGuardError(`Rate limit: ${result.count} tasks completed in the last ${config.window_minutes} minutes (max ${config.max_completions_per_window}). Slow down.`);
|
|
2351
2646
|
}
|
|
2352
|
-
return next.toISOString();
|
|
2353
2647
|
}
|
|
2354
|
-
if (
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
daysToAdd = diff;
|
|
2648
|
+
if (agent && config.cooldown_seconds) {
|
|
2649
|
+
const result = db.query(`SELECT MAX(completed_at) as last_completed FROM tasks
|
|
2650
|
+
WHERE completed_at IS NOT NULL AND (assigned_to = ? OR agent_id = ?) AND id != ?`).get(agent, agent, task.id);
|
|
2651
|
+
if (result.last_completed) {
|
|
2652
|
+
const elapsedSeconds = (Date.now() - new Date(result.last_completed).getTime()) / 1000;
|
|
2653
|
+
if (elapsedSeconds < config.cooldown_seconds) {
|
|
2654
|
+
const remaining = Math.ceil(config.cooldown_seconds - elapsedSeconds);
|
|
2655
|
+
throw new CompletionGuardError(`Cooldown: last completion was ${Math.floor(elapsedSeconds)}s ago. Wait ${remaining}s between completions.`, remaining);
|
|
2656
|
+
}
|
|
2364
2657
|
}
|
|
2365
|
-
const next = new Date(base);
|
|
2366
|
-
next.setDate(next.getDate() + daysToAdd);
|
|
2367
|
-
return next.toISOString();
|
|
2368
2658
|
}
|
|
2369
|
-
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
// src/db/audit.ts
|
|
2662
|
+
init_database();
|
|
2663
|
+
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
2664
|
+
const d = db || getDatabase();
|
|
2665
|
+
const id = uuid();
|
|
2666
|
+
const timestamp = now();
|
|
2667
|
+
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
|
|
2668
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
|
|
2669
|
+
return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
|
|
2670
|
+
}
|
|
2671
|
+
function getTaskHistory(taskId, db) {
|
|
2672
|
+
const d = db || getDatabase();
|
|
2673
|
+
return d.query("SELECT * FROM task_history WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
|
|
2674
|
+
}
|
|
2675
|
+
function getRecentActivity(limit = 50, db) {
|
|
2676
|
+
const d = db || getDatabase();
|
|
2677
|
+
return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
2678
|
+
}
|
|
2679
|
+
function getRecap(hours = 8, projectId, db) {
|
|
2680
|
+
const d = db || getDatabase();
|
|
2681
|
+
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
2682
|
+
const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
2683
|
+
const pf = projectId ? " AND project_id = ?" : "";
|
|
2684
|
+
const tpf = projectId ? " AND t.project_id = ?" : "";
|
|
2685
|
+
const completed = projectId ? d.query(`SELECT id, short_id, title, assigned_to, completed_at, started_at FROM tasks WHERE status = 'completed' AND completed_at > ?${pf} ORDER BY completed_at DESC`).all(since, projectId) : d.query(`SELECT id, short_id, title, assigned_to, completed_at, started_at FROM tasks WHERE status = 'completed' AND completed_at > ? ORDER BY completed_at DESC`).all(since);
|
|
2686
|
+
const created = projectId ? d.query(`SELECT id, short_id, title, agent_id, created_at FROM tasks WHERE created_at > ?${pf} ORDER BY created_at DESC`).all(since, projectId) : d.query(`SELECT id, short_id, title, agent_id, created_at FROM tasks WHERE created_at > ? ORDER BY created_at DESC`).all(since);
|
|
2687
|
+
const in_progress = projectId ? d.query(`SELECT id, short_id, title, assigned_to, started_at FROM tasks WHERE status = 'in_progress' AND project_id = ? ORDER BY updated_at DESC`).all(projectId) : d.query(`SELECT id, short_id, title, assigned_to, started_at FROM tasks WHERE status = 'in_progress' ORDER BY updated_at DESC`).all();
|
|
2688
|
+
const blocked = projectId ? d.query(`SELECT DISTINCT t.id, t.short_id, t.title, t.assigned_to FROM tasks t JOIN task_dependencies td ON td.task_id = t.id JOIN tasks dep ON dep.id = td.depends_on AND dep.status NOT IN ('completed','cancelled') WHERE t.status = 'pending'${tpf}`).all(projectId) : d.query(`SELECT DISTINCT t.id, t.short_id, t.title, t.assigned_to FROM tasks t JOIN task_dependencies td ON td.task_id = t.id JOIN tasks dep ON dep.id = td.depends_on AND dep.status NOT IN ('completed','cancelled') WHERE t.status = 'pending'`).all();
|
|
2689
|
+
const stale = projectId ? d.query(`SELECT id, short_id, title, assigned_to, updated_at FROM tasks WHERE status = 'in_progress' AND updated_at < ? AND project_id = ? ORDER BY updated_at ASC`).all(staleWindow, projectId) : d.query(`SELECT id, short_id, title, assigned_to, updated_at FROM tasks WHERE status = 'in_progress' AND updated_at < ? ORDER BY updated_at ASC`).all(staleWindow);
|
|
2690
|
+
const agents = projectId ? d.query(`SELECT a.name, a.last_seen_at, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'completed' AND t.completed_at > ?${tpf}) as completed_count, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'in_progress'${tpf}) as in_progress_count FROM agents a WHERE a.status = 'active' AND a.last_seen_at > ? ORDER BY completed_count DESC`).all(since, projectId, projectId, since) : d.query(`SELECT a.name, a.last_seen_at, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'completed' AND t.completed_at > ?) as completed_count, (SELECT COUNT(*) FROM tasks t WHERE (t.assigned_to = a.id OR t.agent_id = a.id) AND t.status = 'in_progress') as in_progress_count FROM agents a WHERE a.status = 'active' AND a.last_seen_at > ? ORDER BY completed_count DESC`).all(since, since);
|
|
2691
|
+
return {
|
|
2692
|
+
hours,
|
|
2693
|
+
since,
|
|
2694
|
+
completed: completed.map((r) => ({
|
|
2695
|
+
...r,
|
|
2696
|
+
duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
|
|
2697
|
+
})),
|
|
2698
|
+
created,
|
|
2699
|
+
in_progress,
|
|
2700
|
+
blocked,
|
|
2701
|
+
stale,
|
|
2702
|
+
agents
|
|
2703
|
+
};
|
|
2370
2704
|
}
|
|
2371
2705
|
|
|
2372
2706
|
// src/db/webhooks.ts
|
|
2373
2707
|
init_database();
|
|
2374
2708
|
var MAX_RETRY_ATTEMPTS = 3;
|
|
2375
2709
|
var RETRY_BASE_DELAY_MS = 1000;
|
|
2710
|
+
function isPrivateOrInternal(ip) {
|
|
2711
|
+
const parts = ip.split(".").map(Number);
|
|
2712
|
+
if (parts.length !== 4)
|
|
2713
|
+
return true;
|
|
2714
|
+
const a = parts[0];
|
|
2715
|
+
const b = parts[1];
|
|
2716
|
+
if (parts.some((p) => isNaN(p) || p < 0 || p > 255))
|
|
2717
|
+
return true;
|
|
2718
|
+
if (a === 10)
|
|
2719
|
+
return true;
|
|
2720
|
+
if (a === 127)
|
|
2721
|
+
return true;
|
|
2722
|
+
if (a === 169 && b === 254)
|
|
2723
|
+
return true;
|
|
2724
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
2725
|
+
return true;
|
|
2726
|
+
if (a === 192 && b === 168)
|
|
2727
|
+
return true;
|
|
2728
|
+
if (a === 0)
|
|
2729
|
+
return true;
|
|
2730
|
+
return false;
|
|
2731
|
+
}
|
|
2376
2732
|
function validateWebhookUrl(urlString) {
|
|
2377
2733
|
try {
|
|
2378
2734
|
const url = new URL(urlString);
|
|
2379
2735
|
if (url.protocol !== "https:") {
|
|
2380
|
-
|
|
2736
|
+
return { valid: false, error: "Webhook URLs must use HTTPS" };
|
|
2381
2737
|
}
|
|
2382
2738
|
const hostname = url.hostname.toLowerCase();
|
|
2383
2739
|
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0") {
|
|
2384
|
-
|
|
2740
|
+
return { valid: false, error: "Webhook URLs cannot target localhost" };
|
|
2385
2741
|
}
|
|
2386
2742
|
if (hostname === "169.254.169.254" || hostname.startsWith("169.254.")) {
|
|
2387
|
-
|
|
2743
|
+
return { valid: false, error: "Webhook URLs cannot target cloud metadata endpoints" };
|
|
2388
2744
|
}
|
|
2389
2745
|
const privateRanges = [
|
|
2390
2746
|
/^10\./,
|
|
@@ -2397,1008 +2753,1308 @@ function validateWebhookUrl(urlString) {
|
|
|
2397
2753
|
];
|
|
2398
2754
|
for (const range of privateRanges) {
|
|
2399
2755
|
if (range.test(hostname)) {
|
|
2400
|
-
|
|
2756
|
+
return { valid: false, error: "Webhook URLs cannot target private IP ranges" };
|
|
2401
2757
|
}
|
|
2402
2758
|
}
|
|
2759
|
+
return { valid: true };
|
|
2403
2760
|
} catch (e) {
|
|
2404
2761
|
if (e instanceof Error && e.message.startsWith("Webhook URLs")) {
|
|
2405
|
-
|
|
2762
|
+
return { valid: false, error: e.message };
|
|
2763
|
+
}
|
|
2764
|
+
return { valid: false, error: `Invalid webhook URL: ${urlString}` };
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
async function resolveAndCheckIp(hostname) {
|
|
2768
|
+
try {
|
|
2769
|
+
const resolved = await Bun.dns.lookup(hostname);
|
|
2770
|
+
if (!resolved)
|
|
2771
|
+
return { allowed: false, error: `Could not resolve hostname: ${hostname}` };
|
|
2772
|
+
const addresses = Array.isArray(resolved) ? resolved : [resolved];
|
|
2773
|
+
for (const addr of addresses) {
|
|
2774
|
+
const ip = typeof addr === "string" ? addr : addr.address;
|
|
2775
|
+
if (isPrivateOrInternal(ip)) {
|
|
2776
|
+
return { allowed: false, error: `Hostname ${hostname} resolves to blocked address ${ip}` };
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
const first = addresses[0];
|
|
2780
|
+
return { allowed: true, ip: typeof first === "string" ? first : first?.address ?? "" };
|
|
2781
|
+
} catch {
|
|
2782
|
+
return { allowed: true, ip: "" };
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
var activeDeliveries = 0;
|
|
2786
|
+
var MAX_CONCURRENT_DELIVERIES = 20;
|
|
2787
|
+
function rowToWebhook(row) {
|
|
2788
|
+
return {
|
|
2789
|
+
...row,
|
|
2790
|
+
events: JSON.parse(row.events || "[]"),
|
|
2791
|
+
active: !!row.active,
|
|
2792
|
+
project_id: row.project_id || null,
|
|
2793
|
+
task_list_id: row.task_list_id || null,
|
|
2794
|
+
agent_id: row.agent_id || null,
|
|
2795
|
+
task_id: row.task_id || null
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
function createWebhook(input, db) {
|
|
2799
|
+
const urlValidation = validateWebhookUrl(input.url);
|
|
2800
|
+
if (!urlValidation.valid) {
|
|
2801
|
+
throw new Error(`Invalid webhook URL: ${urlValidation.error}`);
|
|
2802
|
+
}
|
|
2803
|
+
const d = db || getDatabase();
|
|
2804
|
+
const id = uuid();
|
|
2805
|
+
d.run(`INSERT INTO webhooks (id, url, events, secret, project_id, task_list_id, agent_id, task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2806
|
+
id,
|
|
2807
|
+
input.url,
|
|
2808
|
+
JSON.stringify(input.events || []),
|
|
2809
|
+
input.secret || null,
|
|
2810
|
+
input.project_id || null,
|
|
2811
|
+
input.task_list_id || null,
|
|
2812
|
+
input.agent_id || null,
|
|
2813
|
+
input.task_id || null,
|
|
2814
|
+
now()
|
|
2815
|
+
]);
|
|
2816
|
+
return getWebhook(id, d);
|
|
2817
|
+
}
|
|
2818
|
+
function getWebhook(id, db) {
|
|
2819
|
+
const d = db || getDatabase();
|
|
2820
|
+
const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
|
|
2821
|
+
return row ? rowToWebhook(row) : null;
|
|
2822
|
+
}
|
|
2823
|
+
function listWebhooks(db) {
|
|
2824
|
+
const d = db || getDatabase();
|
|
2825
|
+
return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
|
|
2826
|
+
}
|
|
2827
|
+
function deleteWebhook(id, db) {
|
|
2828
|
+
const d = db || getDatabase();
|
|
2829
|
+
return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
|
|
2830
|
+
}
|
|
2831
|
+
function listDeliveries(webhookId, limit = 50, db) {
|
|
2832
|
+
const d = db || getDatabase();
|
|
2833
|
+
if (webhookId) {
|
|
2834
|
+
return d.query("SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY created_at DESC LIMIT ?").all(webhookId, limit);
|
|
2835
|
+
}
|
|
2836
|
+
return d.query("SELECT * FROM webhook_deliveries ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
2837
|
+
}
|
|
2838
|
+
function logDelivery(d, webhookId, event, payload, statusCode, response, attempt) {
|
|
2839
|
+
const id = uuid();
|
|
2840
|
+
d.run(`INSERT INTO webhook_deliveries (id, webhook_id, event, payload, status_code, response, attempt, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, webhookId, event, payload, statusCode, response, attempt, now()]);
|
|
2841
|
+
}
|
|
2842
|
+
function matchesScope(wh, payload) {
|
|
2843
|
+
if (wh.project_id && payload.project_id !== wh.project_id)
|
|
2844
|
+
return false;
|
|
2845
|
+
if (wh.task_list_id && payload.task_list_id !== wh.task_list_id)
|
|
2846
|
+
return false;
|
|
2847
|
+
if (wh.agent_id && payload.agent_id !== wh.agent_id && payload.assigned_to !== wh.agent_id)
|
|
2848
|
+
return false;
|
|
2849
|
+
if (wh.task_id && payload.id !== wh.task_id)
|
|
2850
|
+
return false;
|
|
2851
|
+
return true;
|
|
2852
|
+
}
|
|
2853
|
+
async function deliverWebhook(wh, event, body, attempt, db) {
|
|
2854
|
+
try {
|
|
2855
|
+
const url = new URL(wh.url);
|
|
2856
|
+
const hostname = url.hostname.toLowerCase();
|
|
2857
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
|
|
2858
|
+
logDelivery(db, wh.id, event, body, null, "Blocked: webhook URL points to localhost", attempt);
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
const ipCheck = await resolveAndCheckIp(hostname);
|
|
2862
|
+
if (!ipCheck.allowed) {
|
|
2863
|
+
logDelivery(db, wh.id, event, body, null, `Blocked: ${ipCheck.error}`, attempt);
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
} catch {
|
|
2867
|
+
logDelivery(db, wh.id, event, body, null, `Invalid URL at delivery time: ${wh.url}`, attempt);
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
if (activeDeliveries >= MAX_CONCURRENT_DELIVERIES) {
|
|
2871
|
+
logDelivery(db, wh.id, event, body, null, "Dropped: too many concurrent deliveries", attempt);
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
activeDeliveries++;
|
|
2875
|
+
try {
|
|
2876
|
+
const headers = { "Content-Type": "application/json" };
|
|
2877
|
+
if (wh.secret) {
|
|
2878
|
+
const encoder = new TextEncoder;
|
|
2879
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
2880
|
+
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
2881
|
+
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2882
|
+
}
|
|
2883
|
+
const resp = await fetch(wh.url, { method: "POST", headers, body });
|
|
2884
|
+
const respText = await resp.text().catch(() => "");
|
|
2885
|
+
logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
|
|
2886
|
+
if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
|
|
2887
|
+
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
2888
|
+
setTimeout(() => {
|
|
2889
|
+
deliverWebhook(wh, event, body, attempt + 1, db).catch((retryErr) => {
|
|
2890
|
+
console.error(`[webhook] Retry failed for webhook ${wh.id}:`, retryErr);
|
|
2891
|
+
});
|
|
2892
|
+
}, delay);
|
|
2893
|
+
}
|
|
2894
|
+
} catch (err) {
|
|
2895
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2896
|
+
logDelivery(db, wh.id, event, body, null, errorMsg.slice(0, 1000), attempt);
|
|
2897
|
+
console.error(`[webhook] Delivery failed for webhook ${wh.id} (attempt ${attempt}):`, errorMsg);
|
|
2898
|
+
if (attempt < MAX_RETRY_ATTEMPTS) {
|
|
2899
|
+
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
2900
|
+
setTimeout(() => {
|
|
2901
|
+
deliverWebhook(wh, event, body, attempt + 1, db).catch((retryErr) => {
|
|
2902
|
+
console.error(`[webhook] Retry failed for webhook ${wh.id}:`, retryErr);
|
|
2903
|
+
});
|
|
2904
|
+
}, delay);
|
|
2406
2905
|
}
|
|
2407
|
-
|
|
2906
|
+
} finally {
|
|
2907
|
+
activeDeliveries--;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
async function dispatchWebhook(event, payload, db) {
|
|
2911
|
+
const d = db || getDatabase();
|
|
2912
|
+
const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
|
|
2913
|
+
const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
|
|
2914
|
+
for (const wh of webhooks) {
|
|
2915
|
+
if (!matchesScope(wh, payloadObj))
|
|
2916
|
+
continue;
|
|
2917
|
+
const body = JSON.stringify({ event, payload, timestamp: now() });
|
|
2918
|
+
deliverWebhook(wh, event, body, 1, d).catch((err) => {
|
|
2919
|
+
console.error(`[webhook] Dispatch failed for webhook ${wh.id}:`, err);
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
// src/db/checklists.ts
|
|
2925
|
+
init_database();
|
|
2926
|
+
function rowToItem(row) {
|
|
2927
|
+
return { ...row, checked: !!row.checked };
|
|
2928
|
+
}
|
|
2929
|
+
function getChecklist(taskId, db) {
|
|
2930
|
+
const d = db || getDatabase();
|
|
2931
|
+
const rows = d.query("SELECT * FROM task_checklists WHERE task_id = ? ORDER BY position, created_at").all(taskId);
|
|
2932
|
+
return rows.map(rowToItem);
|
|
2933
|
+
}
|
|
2934
|
+
function addChecklistItem(input, db) {
|
|
2935
|
+
const d = db || getDatabase();
|
|
2936
|
+
const id = uuid();
|
|
2937
|
+
const timestamp = now();
|
|
2938
|
+
let position = input.position;
|
|
2939
|
+
if (position === undefined) {
|
|
2940
|
+
const maxRow = d.query("SELECT MAX(position) as max_pos FROM task_checklists WHERE task_id = ?").get(input.task_id);
|
|
2941
|
+
position = (maxRow?.max_pos ?? -1) + 1;
|
|
2408
2942
|
}
|
|
2943
|
+
d.run("INSERT INTO task_checklists (id, task_id, position, text, checked, created_at, updated_at) VALUES (?, ?, ?, ?, 0, ?, ?)", [id, input.task_id, position, input.text, timestamp, timestamp]);
|
|
2944
|
+
return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
|
|
2945
|
+
}
|
|
2946
|
+
function checkChecklistItem(id, checked, db) {
|
|
2947
|
+
const d = db || getDatabase();
|
|
2948
|
+
const timestamp = now();
|
|
2949
|
+
const result = d.run("UPDATE task_checklists SET checked = ?, updated_at = ? WHERE id = ?", [checked ? 1 : 0, timestamp, id]);
|
|
2950
|
+
if (result.changes === 0)
|
|
2951
|
+
return null;
|
|
2952
|
+
return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
|
|
2953
|
+
}
|
|
2954
|
+
function updateChecklistItemText(id, text, db) {
|
|
2955
|
+
const d = db || getDatabase();
|
|
2956
|
+
const timestamp = now();
|
|
2957
|
+
const result = d.run("UPDATE task_checklists SET text = ?, updated_at = ? WHERE id = ?", [text, timestamp, id]);
|
|
2958
|
+
if (result.changes === 0)
|
|
2959
|
+
return null;
|
|
2960
|
+
return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
|
|
2961
|
+
}
|
|
2962
|
+
function removeChecklistItem(id, db) {
|
|
2963
|
+
const d = db || getDatabase();
|
|
2964
|
+
const result = d.run("DELETE FROM task_checklists WHERE id = ?", [id]);
|
|
2965
|
+
return result.changes > 0;
|
|
2966
|
+
}
|
|
2967
|
+
function clearChecklist(taskId, db) {
|
|
2968
|
+
const d = db || getDatabase();
|
|
2969
|
+
const result = d.run("DELETE FROM task_checklists WHERE task_id = ?", [taskId]);
|
|
2970
|
+
return result.changes;
|
|
2971
|
+
}
|
|
2972
|
+
function getChecklistStats(taskId, db) {
|
|
2973
|
+
const d = db || getDatabase();
|
|
2974
|
+
const row = d.query("SELECT COUNT(*) as total, SUM(checked) as checked FROM task_checklists WHERE task_id = ?").get(taskId);
|
|
2975
|
+
return { total: row?.total ?? 0, checked: row?.checked ?? 0 };
|
|
2409
2976
|
}
|
|
2410
|
-
|
|
2977
|
+
|
|
2978
|
+
// src/db/task-crud.ts
|
|
2979
|
+
function rowToTask(row) {
|
|
2411
2980
|
return {
|
|
2412
2981
|
...row,
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
task_id: row.task_id || null
|
|
2982
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
2983
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
2984
|
+
status: row.status,
|
|
2985
|
+
priority: row.priority,
|
|
2986
|
+
requires_approval: !!row.requires_approval
|
|
2419
2987
|
};
|
|
2420
2988
|
}
|
|
2421
|
-
function
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
const
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
input.secret || null,
|
|
2430
|
-
input.project_id || null,
|
|
2431
|
-
input.task_list_id || null,
|
|
2432
|
-
input.agent_id || null,
|
|
2433
|
-
input.task_id || null,
|
|
2434
|
-
now()
|
|
2435
|
-
]);
|
|
2436
|
-
return getWebhook(id, d);
|
|
2989
|
+
function insertTaskTags(taskId, tags, db) {
|
|
2990
|
+
if (tags.length === 0)
|
|
2991
|
+
return;
|
|
2992
|
+
const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
2993
|
+
for (const tag of tags) {
|
|
2994
|
+
if (tag)
|
|
2995
|
+
stmt.run(taskId, tag);
|
|
2996
|
+
}
|
|
2437
2997
|
}
|
|
2438
|
-
function
|
|
2998
|
+
function replaceTaskTags(taskId, tags, db) {
|
|
2999
|
+
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
3000
|
+
insertTaskTags(taskId, tags, db);
|
|
3001
|
+
}
|
|
3002
|
+
function createTask(input, db) {
|
|
2439
3003
|
const d = db || getDatabase();
|
|
2440
|
-
const
|
|
2441
|
-
|
|
3004
|
+
const timestamp = now();
|
|
3005
|
+
const tags = input.tags || [];
|
|
3006
|
+
const assignedBy = input.assigned_by || input.agent_id;
|
|
3007
|
+
const assignedFromProject = input.assigned_from_project || null;
|
|
3008
|
+
let id = uuid();
|
|
3009
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
3010
|
+
try {
|
|
3011
|
+
d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type)
|
|
3012
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3013
|
+
id,
|
|
3014
|
+
null,
|
|
3015
|
+
input.project_id || null,
|
|
3016
|
+
input.parent_id || null,
|
|
3017
|
+
input.plan_id || null,
|
|
3018
|
+
input.task_list_id || null,
|
|
3019
|
+
input.title,
|
|
3020
|
+
input.description || null,
|
|
3021
|
+
input.status || "pending",
|
|
3022
|
+
input.priority || "medium",
|
|
3023
|
+
input.agent_id || null,
|
|
3024
|
+
input.assigned_to || null,
|
|
3025
|
+
input.session_id || null,
|
|
3026
|
+
input.working_dir || null,
|
|
3027
|
+
JSON.stringify(tags),
|
|
3028
|
+
JSON.stringify(input.metadata || {}),
|
|
3029
|
+
timestamp,
|
|
3030
|
+
timestamp,
|
|
3031
|
+
input.due_at || null,
|
|
3032
|
+
input.estimated_minutes || null,
|
|
3033
|
+
input.requires_approval ? 1 : 0,
|
|
3034
|
+
null,
|
|
3035
|
+
null,
|
|
3036
|
+
input.recurrence_rule || null,
|
|
3037
|
+
input.recurrence_parent_id || null,
|
|
3038
|
+
input.spawns_template_id || null,
|
|
3039
|
+
input.reason || null,
|
|
3040
|
+
input.spawned_from_session || null,
|
|
3041
|
+
assignedBy || null,
|
|
3042
|
+
assignedFromProject || null,
|
|
3043
|
+
input.task_type || null
|
|
3044
|
+
]);
|
|
3045
|
+
break;
|
|
3046
|
+
} catch (e) {
|
|
3047
|
+
if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
|
|
3048
|
+
id = uuid();
|
|
3049
|
+
continue;
|
|
3050
|
+
}
|
|
3051
|
+
throw e;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
if (tags.length > 0) {
|
|
3055
|
+
insertTaskTags(id, tags, d);
|
|
3056
|
+
}
|
|
3057
|
+
const task = getTask(id, d);
|
|
3058
|
+
dispatchWebhook("task.created", { id: task.id, short_id: task.short_id, title: task.title, status: task.status, priority: task.priority, project_id: task.project_id, assigned_to: task.assigned_to }, d).catch(() => {});
|
|
3059
|
+
return task;
|
|
2442
3060
|
}
|
|
2443
|
-
function
|
|
3061
|
+
function getTask(id, db) {
|
|
2444
3062
|
const d = db || getDatabase();
|
|
2445
|
-
|
|
3063
|
+
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
3064
|
+
if (!row)
|
|
3065
|
+
return null;
|
|
3066
|
+
return rowToTask(row);
|
|
2446
3067
|
}
|
|
2447
|
-
function
|
|
3068
|
+
function getTaskWithRelations(id, db) {
|
|
2448
3069
|
const d = db || getDatabase();
|
|
2449
|
-
|
|
3070
|
+
const task = getTask(id, d);
|
|
3071
|
+
if (!task)
|
|
3072
|
+
return null;
|
|
3073
|
+
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
3074
|
+
const subtasks = subtaskRows.map(rowToTask);
|
|
3075
|
+
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
3076
|
+
JOIN task_dependencies td ON td.depends_on = t.id
|
|
3077
|
+
WHERE td.task_id = ?`).all(id);
|
|
3078
|
+
const dependencies = depRows.map(rowToTask);
|
|
3079
|
+
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
3080
|
+
JOIN task_dependencies td ON td.task_id = t.id
|
|
3081
|
+
WHERE td.depends_on = ?`).all(id);
|
|
3082
|
+
const blocked_by = blockedByRows.map(rowToTask);
|
|
3083
|
+
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
3084
|
+
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
3085
|
+
const checklist = getChecklist(id, d);
|
|
3086
|
+
return {
|
|
3087
|
+
...task,
|
|
3088
|
+
subtasks,
|
|
3089
|
+
dependencies,
|
|
3090
|
+
blocked_by,
|
|
3091
|
+
comments,
|
|
3092
|
+
parent,
|
|
3093
|
+
checklist
|
|
3094
|
+
};
|
|
2450
3095
|
}
|
|
2451
|
-
function
|
|
3096
|
+
function listTasks(filter = {}, db) {
|
|
2452
3097
|
const d = db || getDatabase();
|
|
2453
|
-
|
|
2454
|
-
|
|
3098
|
+
const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
|
|
3099
|
+
clearExpiredLocks2(d);
|
|
3100
|
+
const conditions = [];
|
|
3101
|
+
const params = [];
|
|
3102
|
+
if (filter.project_id) {
|
|
3103
|
+
conditions.push("project_id = ?");
|
|
3104
|
+
params.push(filter.project_id);
|
|
2455
3105
|
}
|
|
2456
|
-
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
return false;
|
|
2467
|
-
if (wh.agent_id && payload.agent_id !== wh.agent_id && payload.assigned_to !== wh.agent_id)
|
|
2468
|
-
return false;
|
|
2469
|
-
if (wh.task_id && payload.id !== wh.task_id)
|
|
2470
|
-
return false;
|
|
2471
|
-
return true;
|
|
2472
|
-
}
|
|
2473
|
-
async function deliverWebhook(wh, event, body, attempt, db) {
|
|
2474
|
-
try {
|
|
2475
|
-
const headers = { "Content-Type": "application/json" };
|
|
2476
|
-
if (wh.secret) {
|
|
2477
|
-
const encoder = new TextEncoder;
|
|
2478
|
-
const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
2479
|
-
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
2480
|
-
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3106
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
3107
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
3108
|
+
params.push(...filter.ids);
|
|
3109
|
+
}
|
|
3110
|
+
if (filter.parent_id !== undefined) {
|
|
3111
|
+
if (filter.parent_id === null) {
|
|
3112
|
+
conditions.push("parent_id IS NULL");
|
|
3113
|
+
} else {
|
|
3114
|
+
conditions.push("parent_id = ?");
|
|
3115
|
+
params.push(filter.parent_id);
|
|
2481
3116
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
3117
|
+
}
|
|
3118
|
+
if (filter.status) {
|
|
3119
|
+
if (Array.isArray(filter.status)) {
|
|
3120
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
3121
|
+
params.push(...filter.status);
|
|
3122
|
+
} else {
|
|
3123
|
+
conditions.push("status = ?");
|
|
3124
|
+
params.push(filter.status);
|
|
2490
3125
|
}
|
|
2491
|
-
}
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
3126
|
+
}
|
|
3127
|
+
if (filter.priority) {
|
|
3128
|
+
if (Array.isArray(filter.priority)) {
|
|
3129
|
+
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
3130
|
+
params.push(...filter.priority);
|
|
3131
|
+
} else {
|
|
3132
|
+
conditions.push("priority = ?");
|
|
3133
|
+
params.push(filter.priority);
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
if (filter.assigned_to) {
|
|
3137
|
+
conditions.push("assigned_to = ?");
|
|
3138
|
+
params.push(filter.assigned_to);
|
|
3139
|
+
}
|
|
3140
|
+
if (filter.agent_id) {
|
|
3141
|
+
conditions.push("agent_id = ?");
|
|
3142
|
+
params.push(filter.agent_id);
|
|
3143
|
+
}
|
|
3144
|
+
if (filter.session_id) {
|
|
3145
|
+
conditions.push("session_id = ?");
|
|
3146
|
+
params.push(filter.session_id);
|
|
3147
|
+
}
|
|
3148
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
3149
|
+
const placeholders = filter.tags.map(() => "?").join(",");
|
|
3150
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
3151
|
+
params.push(...filter.tags);
|
|
3152
|
+
}
|
|
3153
|
+
if (filter.plan_id) {
|
|
3154
|
+
conditions.push("plan_id = ?");
|
|
3155
|
+
params.push(filter.plan_id);
|
|
3156
|
+
}
|
|
3157
|
+
if (filter.task_list_id) {
|
|
3158
|
+
conditions.push("task_list_id = ?");
|
|
3159
|
+
params.push(filter.task_list_id);
|
|
3160
|
+
}
|
|
3161
|
+
if (filter.has_recurrence === true) {
|
|
3162
|
+
conditions.push("recurrence_rule IS NOT NULL");
|
|
3163
|
+
} else if (filter.has_recurrence === false) {
|
|
3164
|
+
conditions.push("recurrence_rule IS NULL");
|
|
3165
|
+
}
|
|
3166
|
+
if (filter.task_type) {
|
|
3167
|
+
if (Array.isArray(filter.task_type)) {
|
|
3168
|
+
conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
|
|
3169
|
+
params.push(...filter.task_type);
|
|
3170
|
+
} else {
|
|
3171
|
+
conditions.push("task_type = ?");
|
|
3172
|
+
params.push(filter.task_type);
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
3176
|
+
if (filter.cursor) {
|
|
3177
|
+
try {
|
|
3178
|
+
const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
|
|
3179
|
+
conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
|
|
3180
|
+
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
3181
|
+
} catch {}
|
|
3182
|
+
}
|
|
3183
|
+
if (!filter.include_archived) {
|
|
3184
|
+
conditions.push("archived_at IS NULL");
|
|
3185
|
+
}
|
|
3186
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3187
|
+
let limitClause = "";
|
|
3188
|
+
if (filter.limit) {
|
|
3189
|
+
limitClause = " LIMIT ?";
|
|
3190
|
+
params.push(filter.limit);
|
|
3191
|
+
if (!filter.cursor && filter.offset) {
|
|
3192
|
+
limitClause += " OFFSET ?";
|
|
3193
|
+
params.push(filter.offset);
|
|
2499
3194
|
}
|
|
2500
3195
|
}
|
|
3196
|
+
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
3197
|
+
return rows.map(rowToTask);
|
|
2501
3198
|
}
|
|
2502
|
-
|
|
3199
|
+
function countTasks(filter = {}, db) {
|
|
2503
3200
|
const d = db || getDatabase();
|
|
2504
|
-
const
|
|
2505
|
-
const
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
const body = JSON.stringify({ event, payload, timestamp: now() });
|
|
2510
|
-
deliverWebhook(wh, event, body, 1, d).catch(() => {});
|
|
3201
|
+
const conditions = [];
|
|
3202
|
+
const params = [];
|
|
3203
|
+
if (filter.project_id) {
|
|
3204
|
+
conditions.push("project_id = ?");
|
|
3205
|
+
params.push(filter.project_id);
|
|
2511
3206
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
init_database();
|
|
2516
|
-
function rowToTemplate(row) {
|
|
2517
|
-
return {
|
|
2518
|
-
...row,
|
|
2519
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
2520
|
-
variables: JSON.parse(row.variables || "[]"),
|
|
2521
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
2522
|
-
priority: row.priority || "medium",
|
|
2523
|
-
version: row.version ?? 1
|
|
2524
|
-
};
|
|
2525
|
-
}
|
|
2526
|
-
function rowToTemplateTask(row) {
|
|
2527
|
-
return {
|
|
2528
|
-
...row,
|
|
2529
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
2530
|
-
depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
|
|
2531
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
2532
|
-
priority: row.priority || "medium",
|
|
2533
|
-
condition: row.condition ?? null,
|
|
2534
|
-
include_template_id: row.include_template_id ?? null
|
|
2535
|
-
};
|
|
2536
|
-
}
|
|
2537
|
-
function resolveTemplateId(id, d) {
|
|
2538
|
-
return resolvePartialId(d, "task_templates", id);
|
|
2539
|
-
}
|
|
2540
|
-
function createTemplate(input, db) {
|
|
2541
|
-
const d = db || getDatabase();
|
|
2542
|
-
const id = uuid();
|
|
2543
|
-
d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
|
|
2544
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2545
|
-
id,
|
|
2546
|
-
input.name,
|
|
2547
|
-
input.title_pattern,
|
|
2548
|
-
input.description || null,
|
|
2549
|
-
input.priority || "medium",
|
|
2550
|
-
JSON.stringify(input.tags || []),
|
|
2551
|
-
JSON.stringify(input.variables || []),
|
|
2552
|
-
input.project_id || null,
|
|
2553
|
-
input.plan_id || null,
|
|
2554
|
-
JSON.stringify(input.metadata || {}),
|
|
2555
|
-
now()
|
|
2556
|
-
]);
|
|
2557
|
-
if (input.tasks && input.tasks.length > 0) {
|
|
2558
|
-
addTemplateTasks(id, input.tasks, d);
|
|
3207
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
3208
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
3209
|
+
params.push(...filter.ids);
|
|
2559
3210
|
}
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
if (
|
|
2578
|
-
|
|
2579
|
-
|
|
3211
|
+
if (filter.parent_id !== undefined) {
|
|
3212
|
+
if (filter.parent_id === null) {
|
|
3213
|
+
conditions.push("parent_id IS NULL");
|
|
3214
|
+
} else {
|
|
3215
|
+
conditions.push("parent_id = ?");
|
|
3216
|
+
params.push(filter.parent_id);
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
if (filter.status) {
|
|
3220
|
+
if (Array.isArray(filter.status)) {
|
|
3221
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
3222
|
+
params.push(...filter.status);
|
|
3223
|
+
} else {
|
|
3224
|
+
conditions.push("status = ?");
|
|
3225
|
+
params.push(filter.status);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
if (filter.priority) {
|
|
3229
|
+
if (Array.isArray(filter.priority)) {
|
|
3230
|
+
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
3231
|
+
params.push(...filter.priority);
|
|
3232
|
+
} else {
|
|
3233
|
+
conditions.push("priority = ?");
|
|
3234
|
+
params.push(filter.priority);
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
if (filter.assigned_to) {
|
|
3238
|
+
conditions.push("assigned_to = ?");
|
|
3239
|
+
params.push(filter.assigned_to);
|
|
3240
|
+
}
|
|
3241
|
+
if (filter.agent_id) {
|
|
3242
|
+
conditions.push("agent_id = ?");
|
|
3243
|
+
params.push(filter.agent_id);
|
|
3244
|
+
}
|
|
3245
|
+
if (filter.session_id) {
|
|
3246
|
+
conditions.push("session_id = ?");
|
|
3247
|
+
params.push(filter.session_id);
|
|
3248
|
+
}
|
|
3249
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
3250
|
+
const placeholders = filter.tags.map(() => "?").join(",");
|
|
3251
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
3252
|
+
params.push(...filter.tags);
|
|
3253
|
+
}
|
|
3254
|
+
if (filter.plan_id) {
|
|
3255
|
+
conditions.push("plan_id = ?");
|
|
3256
|
+
params.push(filter.plan_id);
|
|
3257
|
+
}
|
|
3258
|
+
if (filter.task_list_id) {
|
|
3259
|
+
conditions.push("task_list_id = ?");
|
|
3260
|
+
params.push(filter.task_list_id);
|
|
3261
|
+
}
|
|
3262
|
+
if (!filter.include_archived) {
|
|
3263
|
+
conditions.push("archived_at IS NULL");
|
|
3264
|
+
}
|
|
3265
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3266
|
+
const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
3267
|
+
return row.count;
|
|
2580
3268
|
}
|
|
2581
|
-
function
|
|
3269
|
+
function updateTask(id, input, db) {
|
|
2582
3270
|
const d = db || getDatabase();
|
|
2583
|
-
const
|
|
2584
|
-
if (!
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
const snapshot = JSON.stringify({
|
|
2589
|
-
name: current.name,
|
|
2590
|
-
title_pattern: current.title_pattern,
|
|
2591
|
-
description: current.description,
|
|
2592
|
-
priority: current.priority,
|
|
2593
|
-
tags: current.tags,
|
|
2594
|
-
variables: current.variables,
|
|
2595
|
-
project_id: current.project_id,
|
|
2596
|
-
plan_id: current.plan_id,
|
|
2597
|
-
metadata: current.metadata,
|
|
2598
|
-
tasks: current.tasks
|
|
2599
|
-
});
|
|
2600
|
-
d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
|
|
2601
|
-
}
|
|
2602
|
-
const sets = ["version = version + 1"];
|
|
2603
|
-
const values = [];
|
|
2604
|
-
if (updates.name !== undefined) {
|
|
2605
|
-
sets.push("name = ?");
|
|
2606
|
-
values.push(updates.name);
|
|
3271
|
+
const task = getTask(id, d);
|
|
3272
|
+
if (!task)
|
|
3273
|
+
throw new TaskNotFoundError(id);
|
|
3274
|
+
if (task.version !== input.version) {
|
|
3275
|
+
throw new VersionConflictError(id, input.version, task.version);
|
|
2607
3276
|
}
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
3277
|
+
const sets = ["version = version + 1", "updated_at = ?"];
|
|
3278
|
+
const params = [now()];
|
|
3279
|
+
if (input.title !== undefined) {
|
|
3280
|
+
sets.push("title = ?");
|
|
3281
|
+
params.push(input.title);
|
|
2611
3282
|
}
|
|
2612
|
-
if (
|
|
3283
|
+
if (input.description !== undefined) {
|
|
2613
3284
|
sets.push("description = ?");
|
|
2614
|
-
|
|
3285
|
+
params.push(input.description);
|
|
2615
3286
|
}
|
|
2616
|
-
if (
|
|
3287
|
+
if (input.status !== undefined) {
|
|
3288
|
+
if (input.status === "completed") {
|
|
3289
|
+
checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
|
|
3290
|
+
}
|
|
3291
|
+
sets.push("status = ?");
|
|
3292
|
+
params.push(input.status);
|
|
3293
|
+
if (input.status === "completed") {
|
|
3294
|
+
sets.push("completed_at = ?");
|
|
3295
|
+
params.push(now());
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
if (input.priority !== undefined) {
|
|
2617
3299
|
sets.push("priority = ?");
|
|
2618
|
-
|
|
3300
|
+
params.push(input.priority);
|
|
2619
3301
|
}
|
|
2620
|
-
if (
|
|
3302
|
+
if (input.assigned_to !== undefined) {
|
|
3303
|
+
sets.push("assigned_to = ?");
|
|
3304
|
+
params.push(input.assigned_to);
|
|
3305
|
+
}
|
|
3306
|
+
if (input.tags !== undefined) {
|
|
2621
3307
|
sets.push("tags = ?");
|
|
2622
|
-
|
|
3308
|
+
params.push(JSON.stringify(input.tags));
|
|
3309
|
+
}
|
|
3310
|
+
if (input.metadata !== undefined) {
|
|
3311
|
+
sets.push("metadata = ?");
|
|
3312
|
+
params.push(JSON.stringify(input.metadata));
|
|
3313
|
+
}
|
|
3314
|
+
if (input.plan_id !== undefined) {
|
|
3315
|
+
sets.push("plan_id = ?");
|
|
3316
|
+
params.push(input.plan_id);
|
|
3317
|
+
}
|
|
3318
|
+
if (input.task_list_id !== undefined) {
|
|
3319
|
+
sets.push("task_list_id = ?");
|
|
3320
|
+
params.push(input.task_list_id);
|
|
3321
|
+
}
|
|
3322
|
+
if (input.due_at !== undefined) {
|
|
3323
|
+
sets.push("due_at = ?");
|
|
3324
|
+
params.push(input.due_at);
|
|
3325
|
+
}
|
|
3326
|
+
if (input.estimated_minutes !== undefined) {
|
|
3327
|
+
sets.push("estimated_minutes = ?");
|
|
3328
|
+
params.push(input.estimated_minutes);
|
|
3329
|
+
}
|
|
3330
|
+
if (input.requires_approval !== undefined) {
|
|
3331
|
+
sets.push("requires_approval = ?");
|
|
3332
|
+
params.push(input.requires_approval ? 1 : 0);
|
|
3333
|
+
}
|
|
3334
|
+
if (input.approved_by !== undefined) {
|
|
3335
|
+
sets.push("approved_by = ?");
|
|
3336
|
+
params.push(input.approved_by);
|
|
3337
|
+
sets.push("approved_at = ?");
|
|
3338
|
+
params.push(now());
|
|
3339
|
+
}
|
|
3340
|
+
if (input.recurrence_rule !== undefined) {
|
|
3341
|
+
sets.push("recurrence_rule = ?");
|
|
3342
|
+
params.push(input.recurrence_rule);
|
|
3343
|
+
}
|
|
3344
|
+
if (input.task_type !== undefined) {
|
|
3345
|
+
sets.push("task_type = ?");
|
|
3346
|
+
params.push(input.task_type ?? null);
|
|
2623
3347
|
}
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
3348
|
+
params.push(id, input.version);
|
|
3349
|
+
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
3350
|
+
if (result.changes === 0) {
|
|
3351
|
+
const current = getTask(id, d);
|
|
3352
|
+
throw new VersionConflictError(id, input.version, current?.version ?? -1);
|
|
2627
3353
|
}
|
|
2628
|
-
if (
|
|
2629
|
-
|
|
2630
|
-
values.push(updates.project_id);
|
|
3354
|
+
if (input.tags !== undefined) {
|
|
3355
|
+
replaceTaskTags(id, input.tags, d);
|
|
2631
3356
|
}
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
3357
|
+
const agentId = task.assigned_to || task.agent_id || null;
|
|
3358
|
+
if (input.status !== undefined && input.status !== task.status)
|
|
3359
|
+
logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
|
|
3360
|
+
if (input.priority !== undefined && input.priority !== task.priority)
|
|
3361
|
+
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
3362
|
+
if (input.title !== undefined && input.title !== task.title)
|
|
3363
|
+
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
3364
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
3365
|
+
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
3366
|
+
if (input.approved_by !== undefined)
|
|
3367
|
+
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
3368
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
3369
|
+
dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
|
|
2635
3370
|
}
|
|
2636
|
-
if (
|
|
2637
|
-
|
|
2638
|
-
values.push(JSON.stringify(updates.metadata));
|
|
3371
|
+
if (input.status !== undefined && input.status !== task.status) {
|
|
3372
|
+
dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
|
|
2639
3373
|
}
|
|
2640
|
-
values.push(resolved);
|
|
2641
|
-
d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
|
|
2642
|
-
return getTemplate(resolved, d);
|
|
2643
|
-
}
|
|
2644
|
-
function taskFromTemplate(templateId, overrides = {}, db) {
|
|
2645
|
-
const t = getTemplate(templateId, db);
|
|
2646
|
-
if (!t)
|
|
2647
|
-
throw new Error(`Template not found: ${templateId}`);
|
|
2648
|
-
const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
|
|
2649
3374
|
return {
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
3375
|
+
...task,
|
|
3376
|
+
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
3377
|
+
tags: input.tags ?? task.tags,
|
|
3378
|
+
metadata: input.metadata ?? task.metadata,
|
|
3379
|
+
version: task.version + 1,
|
|
3380
|
+
updated_at: now(),
|
|
3381
|
+
completed_at: input.status === "completed" ? now() : task.completed_at,
|
|
3382
|
+
requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
|
|
3383
|
+
approved_by: input.approved_by ?? task.approved_by,
|
|
3384
|
+
approved_at: input.approved_by ? now() : task.approved_at
|
|
2658
3385
|
};
|
|
2659
3386
|
}
|
|
2660
|
-
function
|
|
2661
|
-
const d = db || getDatabase();
|
|
2662
|
-
const template = getTemplate(templateId, d);
|
|
2663
|
-
if (!template)
|
|
2664
|
-
throw new Error(`Template not found: ${templateId}`);
|
|
2665
|
-
d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
|
|
2666
|
-
const results = [];
|
|
2667
|
-
for (let i = 0;i < tasks.length; i++) {
|
|
2668
|
-
const task = tasks[i];
|
|
2669
|
-
const id = uuid();
|
|
2670
|
-
d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, condition, include_template_id, depends_on_positions, metadata, created_at)
|
|
2671
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2672
|
-
id,
|
|
2673
|
-
templateId,
|
|
2674
|
-
i,
|
|
2675
|
-
task.title_pattern,
|
|
2676
|
-
task.description || null,
|
|
2677
|
-
task.priority || "medium",
|
|
2678
|
-
JSON.stringify(task.tags || []),
|
|
2679
|
-
task.task_type || null,
|
|
2680
|
-
task.condition || null,
|
|
2681
|
-
task.include_template_id || null,
|
|
2682
|
-
JSON.stringify(task.depends_on || []),
|
|
2683
|
-
JSON.stringify(task.metadata || {}),
|
|
2684
|
-
now()
|
|
2685
|
-
]);
|
|
2686
|
-
const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
|
|
2687
|
-
if (row)
|
|
2688
|
-
results.push(rowToTemplateTask(row));
|
|
2689
|
-
}
|
|
2690
|
-
return results;
|
|
2691
|
-
}
|
|
2692
|
-
function getTemplateWithTasks(id, db) {
|
|
3387
|
+
function deleteTask(id, db) {
|
|
2693
3388
|
const d = db || getDatabase();
|
|
2694
|
-
const
|
|
2695
|
-
|
|
2696
|
-
return null;
|
|
2697
|
-
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
|
|
2698
|
-
const tasks = rows.map(rowToTemplateTask);
|
|
2699
|
-
return { ...template, tasks };
|
|
3389
|
+
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
3390
|
+
return result.changes > 0;
|
|
2700
3391
|
}
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
3392
|
+
// src/db/task-lifecycle.ts
|
|
3393
|
+
init_types();
|
|
3394
|
+
init_database();
|
|
3395
|
+
|
|
3396
|
+
// src/lib/recurrence.ts
|
|
3397
|
+
var DAY_NAMES = {
|
|
3398
|
+
sunday: 0,
|
|
3399
|
+
sun: 0,
|
|
3400
|
+
monday: 1,
|
|
3401
|
+
mon: 1,
|
|
3402
|
+
tuesday: 2,
|
|
3403
|
+
tue: 2,
|
|
3404
|
+
wednesday: 3,
|
|
3405
|
+
wed: 3,
|
|
3406
|
+
thursday: 4,
|
|
3407
|
+
thu: 4,
|
|
3408
|
+
friday: 5,
|
|
3409
|
+
fri: 5,
|
|
3410
|
+
saturday: 6,
|
|
3411
|
+
sat: 6
|
|
3412
|
+
};
|
|
3413
|
+
function parseRecurrenceRule(rule) {
|
|
3414
|
+
const normalized = rule.trim().toLowerCase();
|
|
3415
|
+
if (normalized === "every weekday" || normalized === "every weekdays") {
|
|
3416
|
+
return { type: "specific_days", days: [1, 2, 3, 4, 5] };
|
|
3417
|
+
}
|
|
3418
|
+
if (normalized === "every day" || normalized === "daily") {
|
|
3419
|
+
return { type: "interval", interval: 1, unit: "day" };
|
|
3420
|
+
}
|
|
3421
|
+
if (normalized === "every week" || normalized === "weekly") {
|
|
3422
|
+
return { type: "interval", interval: 1, unit: "week" };
|
|
3423
|
+
}
|
|
3424
|
+
if (normalized === "every month" || normalized === "monthly") {
|
|
3425
|
+
return { type: "interval", interval: 1, unit: "month" };
|
|
3426
|
+
}
|
|
3427
|
+
const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
|
|
3428
|
+
if (intervalMatch) {
|
|
3429
|
+
return {
|
|
3430
|
+
type: "interval",
|
|
3431
|
+
interval: parseInt(intervalMatch[1], 10),
|
|
3432
|
+
unit: intervalMatch[2]
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
const daysMatch = normalized.match(/^every\s+(.+)$/);
|
|
3436
|
+
if (daysMatch) {
|
|
3437
|
+
const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
|
|
3438
|
+
const days = [];
|
|
3439
|
+
for (const part of dayParts) {
|
|
3440
|
+
const dayNum = DAY_NAMES[part];
|
|
3441
|
+
if (dayNum !== undefined) {
|
|
3442
|
+
days.push(dayNum);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
if (days.length > 0) {
|
|
3446
|
+
return { type: "specific_days", days: days.sort((a, b) => a - b) };
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
throw new Error(`Invalid recurrence rule: "${rule}". Supported formats: "every day", "every weekday", "every week", "every 2 weeks", "every month", "every N days/weeks/months", "every monday", "every mon,wed,fri"`);
|
|
2708
3450
|
}
|
|
2709
|
-
function
|
|
2710
|
-
|
|
3451
|
+
function isValidRecurrenceRule(rule) {
|
|
3452
|
+
try {
|
|
3453
|
+
parseRecurrenceRule(rule);
|
|
2711
3454
|
return true;
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
if (eqMatch) {
|
|
2715
|
-
const varName = eqMatch[1];
|
|
2716
|
-
const expected = eqMatch[2].trim();
|
|
2717
|
-
return (variables[varName] ?? "") === expected;
|
|
2718
|
-
}
|
|
2719
|
-
const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
2720
|
-
if (neqMatch) {
|
|
2721
|
-
const varName = neqMatch[1];
|
|
2722
|
-
const expected = neqMatch[2].trim();
|
|
2723
|
-
return (variables[varName] ?? "") !== expected;
|
|
3455
|
+
} catch {
|
|
3456
|
+
return false;
|
|
2724
3457
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
3458
|
+
}
|
|
3459
|
+
function nextOccurrence(rule, from) {
|
|
3460
|
+
const parsed = parseRecurrenceRule(rule);
|
|
3461
|
+
const base = from || new Date;
|
|
3462
|
+
if (parsed.type === "interval") {
|
|
3463
|
+
const next = new Date(base);
|
|
3464
|
+
if (parsed.unit === "day") {
|
|
3465
|
+
next.setDate(next.getDate() + parsed.interval);
|
|
3466
|
+
} else if (parsed.unit === "week") {
|
|
3467
|
+
next.setDate(next.getDate() + parsed.interval * 7);
|
|
3468
|
+
} else if (parsed.unit === "month") {
|
|
3469
|
+
next.setMonth(next.getMonth() + parsed.interval);
|
|
3470
|
+
}
|
|
3471
|
+
return next.toISOString();
|
|
2730
3472
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
const
|
|
2734
|
-
|
|
2735
|
-
|
|
3473
|
+
if (parsed.type === "specific_days") {
|
|
3474
|
+
const currentDay = base.getDay();
|
|
3475
|
+
const days = parsed.days;
|
|
3476
|
+
let daysToAdd = Infinity;
|
|
3477
|
+
for (const day of days) {
|
|
3478
|
+
let diff = day - currentDay;
|
|
3479
|
+
if (diff <= 0)
|
|
3480
|
+
diff += 7;
|
|
3481
|
+
if (diff < daysToAdd)
|
|
3482
|
+
daysToAdd = diff;
|
|
3483
|
+
}
|
|
3484
|
+
const next = new Date(base);
|
|
3485
|
+
next.setDate(next.getDate() + daysToAdd);
|
|
3486
|
+
return next.toISOString();
|
|
2736
3487
|
}
|
|
2737
|
-
|
|
3488
|
+
throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
|
|
2738
3489
|
}
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
priority:
|
|
2749
|
-
|
|
2750
|
-
variables: template.variables,
|
|
2751
|
-
project_id: template.project_id,
|
|
2752
|
-
plan_id: template.plan_id,
|
|
2753
|
-
metadata: template.metadata,
|
|
2754
|
-
tasks: template.tasks.map((t) => ({
|
|
2755
|
-
position: t.position,
|
|
2756
|
-
title_pattern: t.title_pattern,
|
|
2757
|
-
description: t.description,
|
|
2758
|
-
priority: t.priority,
|
|
2759
|
-
tags: t.tags,
|
|
2760
|
-
task_type: t.task_type,
|
|
2761
|
-
condition: t.condition,
|
|
2762
|
-
include_template_id: t.include_template_id,
|
|
2763
|
-
depends_on_positions: t.depends_on_positions,
|
|
2764
|
-
metadata: t.metadata
|
|
2765
|
-
}))
|
|
3490
|
+
|
|
3491
|
+
// src/db/templates.ts
|
|
3492
|
+
init_database();
|
|
3493
|
+
function rowToTemplate(row) {
|
|
3494
|
+
return {
|
|
3495
|
+
...row,
|
|
3496
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
3497
|
+
variables: JSON.parse(row.variables || "[]"),
|
|
3498
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
3499
|
+
priority: row.priority || "medium",
|
|
3500
|
+
version: row.version ?? 1
|
|
2766
3501
|
};
|
|
2767
3502
|
}
|
|
2768
|
-
function
|
|
3503
|
+
function rowToTemplateTask(row) {
|
|
3504
|
+
return {
|
|
3505
|
+
...row,
|
|
3506
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
3507
|
+
depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
|
|
3508
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
3509
|
+
priority: row.priority || "medium",
|
|
3510
|
+
condition: row.condition ?? null,
|
|
3511
|
+
include_template_id: row.include_template_id ?? null
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
function resolveTemplateId(id, d) {
|
|
3515
|
+
return resolvePartialId(d, "task_templates", id);
|
|
3516
|
+
}
|
|
3517
|
+
function createTemplate(input, db) {
|
|
2769
3518
|
const d = db || getDatabase();
|
|
2770
|
-
const
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
plan_id: json.plan_id ?? undefined,
|
|
2790
|
-
metadata: json.metadata,
|
|
2791
|
-
tasks: taskInputs
|
|
2792
|
-
}, d);
|
|
3519
|
+
const id = uuid();
|
|
3520
|
+
d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
|
|
3521
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3522
|
+
id,
|
|
3523
|
+
input.name,
|
|
3524
|
+
input.title_pattern,
|
|
3525
|
+
input.description || null,
|
|
3526
|
+
input.priority || "medium",
|
|
3527
|
+
JSON.stringify(input.tags || []),
|
|
3528
|
+
JSON.stringify(input.variables || []),
|
|
3529
|
+
input.project_id || null,
|
|
3530
|
+
input.plan_id || null,
|
|
3531
|
+
JSON.stringify(input.metadata || {}),
|
|
3532
|
+
now()
|
|
3533
|
+
]);
|
|
3534
|
+
if (input.tasks && input.tasks.length > 0) {
|
|
3535
|
+
addTemplateTasks(id, input.tasks, d);
|
|
3536
|
+
}
|
|
3537
|
+
return getTemplate(id, d);
|
|
2793
3538
|
}
|
|
2794
|
-
function
|
|
3539
|
+
function getTemplate(id, db) {
|
|
2795
3540
|
const d = db || getDatabase();
|
|
2796
3541
|
const resolved = resolveTemplateId(id, d);
|
|
2797
3542
|
if (!resolved)
|
|
2798
3543
|
return null;
|
|
2799
|
-
const row = d.query("SELECT * FROM
|
|
2800
|
-
return row
|
|
3544
|
+
const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
|
|
3545
|
+
return row ? rowToTemplate(row) : null;
|
|
2801
3546
|
}
|
|
2802
|
-
function
|
|
3547
|
+
function listTemplates(db) {
|
|
3548
|
+
const d = db || getDatabase();
|
|
3549
|
+
return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
|
|
3550
|
+
}
|
|
3551
|
+
function deleteTemplate(id, db) {
|
|
2803
3552
|
const d = db || getDatabase();
|
|
2804
3553
|
const resolved = resolveTemplateId(id, d);
|
|
2805
3554
|
if (!resolved)
|
|
2806
|
-
return
|
|
2807
|
-
return d.
|
|
3555
|
+
return false;
|
|
3556
|
+
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
2808
3557
|
}
|
|
2809
|
-
function
|
|
2810
|
-
const
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
3558
|
+
function updateTemplate(id, updates, db) {
|
|
3559
|
+
const d = db || getDatabase();
|
|
3560
|
+
const resolved = resolveTemplateId(id, d);
|
|
3561
|
+
if (!resolved)
|
|
3562
|
+
return null;
|
|
3563
|
+
const current = getTemplateWithTasks(resolved, d);
|
|
3564
|
+
if (current) {
|
|
3565
|
+
const snapshot = JSON.stringify({
|
|
3566
|
+
name: current.name,
|
|
3567
|
+
title_pattern: current.title_pattern,
|
|
3568
|
+
description: current.description,
|
|
3569
|
+
priority: current.priority,
|
|
3570
|
+
tags: current.tags,
|
|
3571
|
+
variables: current.variables,
|
|
3572
|
+
project_id: current.project_id,
|
|
3573
|
+
plan_id: current.plan_id,
|
|
3574
|
+
metadata: current.metadata,
|
|
3575
|
+
tasks: current.tasks
|
|
3576
|
+
});
|
|
3577
|
+
d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
|
|
2815
3578
|
}
|
|
2816
|
-
const
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
3579
|
+
const sets = ["version = version + 1"];
|
|
3580
|
+
const values = [];
|
|
3581
|
+
if (updates.name !== undefined) {
|
|
3582
|
+
sets.push("name = ?");
|
|
3583
|
+
values.push(updates.name);
|
|
2821
3584
|
}
|
|
2822
|
-
if (
|
|
2823
|
-
|
|
3585
|
+
if (updates.title_pattern !== undefined) {
|
|
3586
|
+
sets.push("title_pattern = ?");
|
|
3587
|
+
values.push(updates.title_pattern);
|
|
2824
3588
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
let result = text;
|
|
2829
|
-
for (const [key, val] of Object.entries(variables)) {
|
|
2830
|
-
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
|
|
3589
|
+
if (updates.description !== undefined) {
|
|
3590
|
+
sets.push("description = ?");
|
|
3591
|
+
values.push(updates.description);
|
|
2831
3592
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
const d = db || getDatabase();
|
|
2836
|
-
const template = getTemplateWithTasks(templateId, d);
|
|
2837
|
-
if (!template)
|
|
2838
|
-
throw new Error(`Template not found: ${templateId}`);
|
|
2839
|
-
const visited = _visitedTemplateIds || new Set;
|
|
2840
|
-
if (visited.has(template.id)) {
|
|
2841
|
-
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
3593
|
+
if (updates.priority !== undefined) {
|
|
3594
|
+
sets.push("priority = ?");
|
|
3595
|
+
values.push(updates.priority);
|
|
2842
3596
|
}
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
|
|
2847
|
-
const task = createTask(input, d);
|
|
2848
|
-
return [task];
|
|
3597
|
+
if (updates.tags !== undefined) {
|
|
3598
|
+
sets.push("tags = ?");
|
|
3599
|
+
values.push(JSON.stringify(updates.tags));
|
|
2849
3600
|
}
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
for (const tt of template.tasks) {
|
|
2854
|
-
if (tt.include_template_id) {
|
|
2855
|
-
const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
|
|
2856
|
-
createdTasks.push(...includedTasks);
|
|
2857
|
-
if (includedTasks.length > 0) {
|
|
2858
|
-
positionToId.set(tt.position, includedTasks[0].id);
|
|
2859
|
-
} else {
|
|
2860
|
-
skippedPositions.add(tt.position);
|
|
2861
|
-
}
|
|
2862
|
-
continue;
|
|
2863
|
-
}
|
|
2864
|
-
if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
|
|
2865
|
-
skippedPositions.add(tt.position);
|
|
2866
|
-
continue;
|
|
2867
|
-
}
|
|
2868
|
-
let title = tt.title_pattern;
|
|
2869
|
-
let desc = tt.description;
|
|
2870
|
-
title = substituteVars(title, resolved);
|
|
2871
|
-
if (desc)
|
|
2872
|
-
desc = substituteVars(desc, resolved);
|
|
2873
|
-
const task = createTask({
|
|
2874
|
-
title,
|
|
2875
|
-
description: desc ?? undefined,
|
|
2876
|
-
priority: tt.priority,
|
|
2877
|
-
tags: tt.tags,
|
|
2878
|
-
task_type: tt.task_type ?? undefined,
|
|
2879
|
-
project_id: projectId,
|
|
2880
|
-
task_list_id: taskListId,
|
|
2881
|
-
metadata: tt.metadata
|
|
2882
|
-
}, d);
|
|
2883
|
-
createdTasks.push(task);
|
|
2884
|
-
positionToId.set(tt.position, task.id);
|
|
3601
|
+
if (updates.variables !== undefined) {
|
|
3602
|
+
sets.push("variables = ?");
|
|
3603
|
+
values.push(JSON.stringify(updates.variables));
|
|
2885
3604
|
}
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
if (tt.include_template_id)
|
|
2890
|
-
continue;
|
|
2891
|
-
const deps = tt.depends_on_positions;
|
|
2892
|
-
for (const depPos of deps) {
|
|
2893
|
-
if (skippedPositions.has(depPos))
|
|
2894
|
-
continue;
|
|
2895
|
-
const taskId = positionToId.get(tt.position);
|
|
2896
|
-
const depId = positionToId.get(depPos);
|
|
2897
|
-
if (taskId && depId) {
|
|
2898
|
-
addDependency(taskId, depId, d);
|
|
2899
|
-
}
|
|
2900
|
-
}
|
|
3605
|
+
if (updates.project_id !== undefined) {
|
|
3606
|
+
sets.push("project_id = ?");
|
|
3607
|
+
values.push(updates.project_id);
|
|
2901
3608
|
}
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
const d = db || getDatabase();
|
|
2906
|
-
const template = getTemplateWithTasks(templateId, d);
|
|
2907
|
-
if (!template)
|
|
2908
|
-
throw new Error(`Template not found: ${templateId}`);
|
|
2909
|
-
const resolved = resolveVariables(template.variables, variables);
|
|
2910
|
-
const tasks = [];
|
|
2911
|
-
if (template.tasks.length === 0) {
|
|
2912
|
-
tasks.push({
|
|
2913
|
-
position: 0,
|
|
2914
|
-
title: substituteVars(template.title_pattern, resolved),
|
|
2915
|
-
description: template.description ? substituteVars(template.description, resolved) : null,
|
|
2916
|
-
priority: template.priority,
|
|
2917
|
-
tags: template.tags,
|
|
2918
|
-
task_type: null,
|
|
2919
|
-
depends_on_positions: []
|
|
2920
|
-
});
|
|
2921
|
-
} else {
|
|
2922
|
-
for (const tt of template.tasks) {
|
|
2923
|
-
if (tt.condition && !evaluateCondition(tt.condition, resolved))
|
|
2924
|
-
continue;
|
|
2925
|
-
tasks.push({
|
|
2926
|
-
position: tt.position,
|
|
2927
|
-
title: substituteVars(tt.title_pattern, resolved),
|
|
2928
|
-
description: tt.description ? substituteVars(tt.description, resolved) : null,
|
|
2929
|
-
priority: tt.priority,
|
|
2930
|
-
tags: tt.tags,
|
|
2931
|
-
task_type: tt.task_type,
|
|
2932
|
-
depends_on_positions: tt.depends_on_positions
|
|
2933
|
-
});
|
|
2934
|
-
}
|
|
3609
|
+
if (updates.plan_id !== undefined) {
|
|
3610
|
+
sets.push("plan_id = ?");
|
|
3611
|
+
values.push(updates.plan_id);
|
|
2935
3612
|
}
|
|
3613
|
+
if (updates.metadata !== undefined) {
|
|
3614
|
+
sets.push("metadata = ?");
|
|
3615
|
+
values.push(JSON.stringify(updates.metadata));
|
|
3616
|
+
}
|
|
3617
|
+
values.push(resolved);
|
|
3618
|
+
d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
|
|
3619
|
+
return getTemplate(resolved, d);
|
|
3620
|
+
}
|
|
3621
|
+
function taskFromTemplate(templateId, overrides = {}, db) {
|
|
3622
|
+
const t = getTemplate(templateId, db);
|
|
3623
|
+
if (!t)
|
|
3624
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
3625
|
+
const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
|
|
2936
3626
|
return {
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
3627
|
+
title: cleanOverrides.title || t.title_pattern,
|
|
3628
|
+
description: cleanOverrides.description ?? t.description ?? undefined,
|
|
3629
|
+
priority: cleanOverrides.priority ?? t.priority,
|
|
3630
|
+
tags: cleanOverrides.tags ?? t.tags,
|
|
3631
|
+
project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
|
|
3632
|
+
plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
|
|
3633
|
+
metadata: cleanOverrides.metadata ?? t.metadata,
|
|
3634
|
+
...cleanOverrides
|
|
2943
3635
|
};
|
|
2944
3636
|
}
|
|
2945
|
-
|
|
2946
|
-
// src/db/checklists.ts
|
|
2947
|
-
init_database();
|
|
2948
|
-
function rowToItem(row) {
|
|
2949
|
-
return { ...row, checked: !!row.checked };
|
|
2950
|
-
}
|
|
2951
|
-
function getChecklist(taskId, db) {
|
|
2952
|
-
const d = db || getDatabase();
|
|
2953
|
-
const rows = d.query("SELECT * FROM task_checklists WHERE task_id = ? ORDER BY position, created_at").all(taskId);
|
|
2954
|
-
return rows.map(rowToItem);
|
|
2955
|
-
}
|
|
2956
|
-
function addChecklistItem(input, db) {
|
|
3637
|
+
function addTemplateTasks(templateId, tasks, db) {
|
|
2957
3638
|
const d = db || getDatabase();
|
|
2958
|
-
const
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3639
|
+
const template = getTemplate(templateId, d);
|
|
3640
|
+
if (!template)
|
|
3641
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
3642
|
+
d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
|
|
3643
|
+
const results = [];
|
|
3644
|
+
for (let i = 0;i < tasks.length; i++) {
|
|
3645
|
+
const task = tasks[i];
|
|
3646
|
+
const id = uuid();
|
|
3647
|
+
d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, condition, include_template_id, depends_on_positions, metadata, created_at)
|
|
3648
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3649
|
+
id,
|
|
3650
|
+
templateId,
|
|
3651
|
+
i,
|
|
3652
|
+
task.title_pattern,
|
|
3653
|
+
task.description || null,
|
|
3654
|
+
task.priority || "medium",
|
|
3655
|
+
JSON.stringify(task.tags || []),
|
|
3656
|
+
task.task_type || null,
|
|
3657
|
+
task.condition || null,
|
|
3658
|
+
task.include_template_id || null,
|
|
3659
|
+
JSON.stringify(task.depends_on || []),
|
|
3660
|
+
JSON.stringify(task.metadata || {}),
|
|
3661
|
+
now()
|
|
3662
|
+
]);
|
|
3663
|
+
const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
|
|
3664
|
+
if (row)
|
|
3665
|
+
results.push(rowToTemplateTask(row));
|
|
2964
3666
|
}
|
|
2965
|
-
|
|
2966
|
-
return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
|
|
2967
|
-
}
|
|
2968
|
-
function checkChecklistItem(id, checked, db) {
|
|
2969
|
-
const d = db || getDatabase();
|
|
2970
|
-
const timestamp = now();
|
|
2971
|
-
const result = d.run("UPDATE task_checklists SET checked = ?, updated_at = ? WHERE id = ?", [checked ? 1 : 0, timestamp, id]);
|
|
2972
|
-
if (result.changes === 0)
|
|
2973
|
-
return null;
|
|
2974
|
-
return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
|
|
3667
|
+
return results;
|
|
2975
3668
|
}
|
|
2976
|
-
function
|
|
3669
|
+
function getTemplateWithTasks(id, db) {
|
|
2977
3670
|
const d = db || getDatabase();
|
|
2978
|
-
const
|
|
2979
|
-
|
|
2980
|
-
if (result.changes === 0)
|
|
3671
|
+
const template = getTemplate(id, d);
|
|
3672
|
+
if (!template)
|
|
2981
3673
|
return null;
|
|
2982
|
-
|
|
3674
|
+
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
|
|
3675
|
+
const tasks = rows.map(rowToTemplateTask);
|
|
3676
|
+
return { ...template, tasks };
|
|
2983
3677
|
}
|
|
2984
|
-
function
|
|
3678
|
+
function getTemplateTasks(templateId, db) {
|
|
2985
3679
|
const d = db || getDatabase();
|
|
2986
|
-
const
|
|
2987
|
-
|
|
3680
|
+
const resolved = resolveTemplateId(templateId, d);
|
|
3681
|
+
if (!resolved)
|
|
3682
|
+
return [];
|
|
3683
|
+
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
|
|
3684
|
+
return rows.map(rowToTemplateTask);
|
|
2988
3685
|
}
|
|
2989
|
-
function
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3686
|
+
function evaluateCondition(condition, variables) {
|
|
3687
|
+
if (!condition || condition.trim() === "")
|
|
3688
|
+
return true;
|
|
3689
|
+
const trimmed = condition.trim();
|
|
3690
|
+
const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
3691
|
+
if (eqMatch) {
|
|
3692
|
+
const varName = eqMatch[1];
|
|
3693
|
+
const expected = eqMatch[2].trim();
|
|
3694
|
+
return (variables[varName] ?? "") === expected;
|
|
3695
|
+
}
|
|
3696
|
+
const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
3697
|
+
if (neqMatch) {
|
|
3698
|
+
const varName = neqMatch[1];
|
|
3699
|
+
const expected = neqMatch[2].trim();
|
|
3700
|
+
return (variables[varName] ?? "") !== expected;
|
|
3701
|
+
}
|
|
3702
|
+
const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
|
|
3703
|
+
if (falsyMatch) {
|
|
3704
|
+
const varName = falsyMatch[1];
|
|
3705
|
+
const val = variables[varName];
|
|
3706
|
+
return !val || val === "" || val === "false";
|
|
3707
|
+
}
|
|
3708
|
+
const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
|
|
3709
|
+
if (truthyMatch) {
|
|
3710
|
+
const varName = truthyMatch[1];
|
|
3711
|
+
const val = variables[varName];
|
|
3712
|
+
return !!val && val !== "" && val !== "false";
|
|
3713
|
+
}
|
|
3714
|
+
return true;
|
|
2993
3715
|
}
|
|
2994
|
-
function
|
|
3716
|
+
function exportTemplate(id, db) {
|
|
2995
3717
|
const d = db || getDatabase();
|
|
2996
|
-
const
|
|
2997
|
-
|
|
2998
|
-
}
|
|
2999
|
-
|
|
3000
|
-
// src/db/tasks.ts
|
|
3001
|
-
function rowToTask(row) {
|
|
3718
|
+
const template = getTemplateWithTasks(id, d);
|
|
3719
|
+
if (!template)
|
|
3720
|
+
throw new Error(`Template not found: ${id}`);
|
|
3002
3721
|
return {
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3722
|
+
name: template.name,
|
|
3723
|
+
title_pattern: template.title_pattern,
|
|
3724
|
+
description: template.description,
|
|
3725
|
+
priority: template.priority,
|
|
3726
|
+
tags: template.tags,
|
|
3727
|
+
variables: template.variables,
|
|
3728
|
+
project_id: template.project_id,
|
|
3729
|
+
plan_id: template.plan_id,
|
|
3730
|
+
metadata: template.metadata,
|
|
3731
|
+
tasks: template.tasks.map((t) => ({
|
|
3732
|
+
position: t.position,
|
|
3733
|
+
title_pattern: t.title_pattern,
|
|
3734
|
+
description: t.description,
|
|
3735
|
+
priority: t.priority,
|
|
3736
|
+
tags: t.tags,
|
|
3737
|
+
task_type: t.task_type,
|
|
3738
|
+
condition: t.condition,
|
|
3739
|
+
include_template_id: t.include_template_id,
|
|
3740
|
+
depends_on_positions: t.depends_on_positions,
|
|
3741
|
+
metadata: t.metadata
|
|
3742
|
+
}))
|
|
3009
3743
|
};
|
|
3010
3744
|
}
|
|
3011
|
-
function
|
|
3012
|
-
if (tags.length === 0)
|
|
3013
|
-
return;
|
|
3014
|
-
const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
3015
|
-
for (const tag of tags) {
|
|
3016
|
-
if (tag)
|
|
3017
|
-
stmt.run(taskId, tag);
|
|
3018
|
-
}
|
|
3019
|
-
}
|
|
3020
|
-
function replaceTaskTags(taskId, tags, db) {
|
|
3021
|
-
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
3022
|
-
insertTaskTags(taskId, tags, db);
|
|
3023
|
-
}
|
|
3024
|
-
function createTask(input, db) {
|
|
3745
|
+
function importTemplate(json, db) {
|
|
3025
3746
|
const d = db || getDatabase();
|
|
3026
|
-
const
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
JSON.stringify(tags),
|
|
3050
|
-
JSON.stringify(input.metadata || {}),
|
|
3051
|
-
timestamp,
|
|
3052
|
-
timestamp,
|
|
3053
|
-
input.due_at || null,
|
|
3054
|
-
input.estimated_minutes || null,
|
|
3055
|
-
input.requires_approval ? 1 : 0,
|
|
3056
|
-
null,
|
|
3057
|
-
null,
|
|
3058
|
-
input.recurrence_rule || null,
|
|
3059
|
-
input.recurrence_parent_id || null,
|
|
3060
|
-
input.spawns_template_id || null,
|
|
3061
|
-
input.reason || null,
|
|
3062
|
-
input.spawned_from_session || null,
|
|
3063
|
-
assignedBy || null,
|
|
3064
|
-
assignedFromProject || null,
|
|
3065
|
-
input.task_type || null
|
|
3066
|
-
]);
|
|
3067
|
-
break;
|
|
3068
|
-
} catch (e) {
|
|
3069
|
-
if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
|
|
3070
|
-
id = uuid();
|
|
3071
|
-
continue;
|
|
3072
|
-
}
|
|
3073
|
-
throw e;
|
|
3074
|
-
}
|
|
3075
|
-
}
|
|
3076
|
-
if (tags.length > 0) {
|
|
3077
|
-
insertTaskTags(id, tags, d);
|
|
3078
|
-
}
|
|
3079
|
-
const task = getTask(id, d);
|
|
3080
|
-
dispatchWebhook("task.created", { id: task.id, short_id: task.short_id, title: task.title, status: task.status, priority: task.priority, project_id: task.project_id, assigned_to: task.assigned_to }, d).catch(() => {});
|
|
3081
|
-
return task;
|
|
3747
|
+
const taskInputs = (json.tasks || []).map((t) => ({
|
|
3748
|
+
title_pattern: t.title_pattern,
|
|
3749
|
+
description: t.description ?? undefined,
|
|
3750
|
+
priority: t.priority,
|
|
3751
|
+
tags: t.tags,
|
|
3752
|
+
task_type: t.task_type ?? undefined,
|
|
3753
|
+
condition: t.condition ?? undefined,
|
|
3754
|
+
include_template_id: t.include_template_id ?? undefined,
|
|
3755
|
+
depends_on: t.depends_on_positions,
|
|
3756
|
+
metadata: t.metadata
|
|
3757
|
+
}));
|
|
3758
|
+
return createTemplate({
|
|
3759
|
+
name: json.name,
|
|
3760
|
+
title_pattern: json.title_pattern,
|
|
3761
|
+
description: json.description ?? undefined,
|
|
3762
|
+
priority: json.priority,
|
|
3763
|
+
tags: json.tags,
|
|
3764
|
+
variables: json.variables,
|
|
3765
|
+
project_id: json.project_id ?? undefined,
|
|
3766
|
+
plan_id: json.plan_id ?? undefined,
|
|
3767
|
+
metadata: json.metadata,
|
|
3768
|
+
tasks: taskInputs
|
|
3769
|
+
}, d);
|
|
3082
3770
|
}
|
|
3083
|
-
function
|
|
3771
|
+
function getTemplateVersion(id, version, db) {
|
|
3084
3772
|
const d = db || getDatabase();
|
|
3085
|
-
const
|
|
3086
|
-
if (!
|
|
3773
|
+
const resolved = resolveTemplateId(id, d);
|
|
3774
|
+
if (!resolved)
|
|
3087
3775
|
return null;
|
|
3088
|
-
|
|
3776
|
+
const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
|
|
3777
|
+
return row || null;
|
|
3089
3778
|
}
|
|
3090
|
-
function
|
|
3779
|
+
function listTemplateVersions(id, db) {
|
|
3091
3780
|
const d = db || getDatabase();
|
|
3092
|
-
const
|
|
3093
|
-
if (!
|
|
3094
|
-
return
|
|
3095
|
-
|
|
3096
|
-
const subtasks = subtaskRows.map(rowToTask);
|
|
3097
|
-
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
3098
|
-
JOIN task_dependencies td ON td.depends_on = t.id
|
|
3099
|
-
WHERE td.task_id = ?`).all(id);
|
|
3100
|
-
const dependencies = depRows.map(rowToTask);
|
|
3101
|
-
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
3102
|
-
JOIN task_dependencies td ON td.task_id = t.id
|
|
3103
|
-
WHERE td.depends_on = ?`).all(id);
|
|
3104
|
-
const blocked_by = blockedByRows.map(rowToTask);
|
|
3105
|
-
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
3106
|
-
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
3107
|
-
const checklist = getChecklist(id, d);
|
|
3108
|
-
return {
|
|
3109
|
-
...task,
|
|
3110
|
-
subtasks,
|
|
3111
|
-
dependencies,
|
|
3112
|
-
blocked_by,
|
|
3113
|
-
comments,
|
|
3114
|
-
parent,
|
|
3115
|
-
checklist
|
|
3116
|
-
};
|
|
3781
|
+
const resolved = resolveTemplateId(id, d);
|
|
3782
|
+
if (!resolved)
|
|
3783
|
+
return [];
|
|
3784
|
+
return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
|
|
3117
3785
|
}
|
|
3118
|
-
function
|
|
3119
|
-
const
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
if (filter.project_id) {
|
|
3124
|
-
conditions.push("project_id = ?");
|
|
3125
|
-
params.push(filter.project_id);
|
|
3126
|
-
}
|
|
3127
|
-
if (filter.parent_id !== undefined) {
|
|
3128
|
-
if (filter.parent_id === null) {
|
|
3129
|
-
conditions.push("parent_id IS NULL");
|
|
3130
|
-
} else {
|
|
3131
|
-
conditions.push("parent_id = ?");
|
|
3132
|
-
params.push(filter.parent_id);
|
|
3133
|
-
}
|
|
3134
|
-
}
|
|
3135
|
-
if (filter.status) {
|
|
3136
|
-
if (Array.isArray(filter.status)) {
|
|
3137
|
-
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
3138
|
-
params.push(...filter.status);
|
|
3139
|
-
} else {
|
|
3140
|
-
conditions.push("status = ?");
|
|
3141
|
-
params.push(filter.status);
|
|
3786
|
+
function resolveVariables(templateVars, provided) {
|
|
3787
|
+
const merged = { ...provided };
|
|
3788
|
+
for (const v of templateVars) {
|
|
3789
|
+
if (merged[v.name] === undefined && v.default !== undefined) {
|
|
3790
|
+
merged[v.name] = v.default;
|
|
3142
3791
|
}
|
|
3143
3792
|
}
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
} else {
|
|
3149
|
-
conditions.push("priority = ?");
|
|
3150
|
-
params.push(filter.priority);
|
|
3793
|
+
const missing = [];
|
|
3794
|
+
for (const v of templateVars) {
|
|
3795
|
+
if (v.required && merged[v.name] === undefined) {
|
|
3796
|
+
missing.push(v.name);
|
|
3151
3797
|
}
|
|
3152
3798
|
}
|
|
3153
|
-
if (
|
|
3154
|
-
|
|
3155
|
-
params.push(filter.assigned_to);
|
|
3156
|
-
}
|
|
3157
|
-
if (filter.agent_id) {
|
|
3158
|
-
conditions.push("agent_id = ?");
|
|
3159
|
-
params.push(filter.agent_id);
|
|
3160
|
-
}
|
|
3161
|
-
if (filter.session_id) {
|
|
3162
|
-
conditions.push("session_id = ?");
|
|
3163
|
-
params.push(filter.session_id);
|
|
3164
|
-
}
|
|
3165
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
3166
|
-
const placeholders = filter.tags.map(() => "?").join(",");
|
|
3167
|
-
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
3168
|
-
params.push(...filter.tags);
|
|
3799
|
+
if (missing.length > 0) {
|
|
3800
|
+
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
3169
3801
|
}
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3802
|
+
return merged;
|
|
3803
|
+
}
|
|
3804
|
+
function substituteVars(text, variables) {
|
|
3805
|
+
let result = text;
|
|
3806
|
+
for (const [key, val] of Object.entries(variables)) {
|
|
3807
|
+
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
|
|
3173
3808
|
}
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3809
|
+
return result;
|
|
3810
|
+
}
|
|
3811
|
+
function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
|
|
3812
|
+
const d = db || getDatabase();
|
|
3813
|
+
const template = getTemplateWithTasks(templateId, d);
|
|
3814
|
+
if (!template)
|
|
3815
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
3816
|
+
const visited = _visitedTemplateIds || new Set;
|
|
3817
|
+
if (visited.has(template.id)) {
|
|
3818
|
+
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
3177
3819
|
}
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3820
|
+
visited.add(template.id);
|
|
3821
|
+
const resolved = resolveVariables(template.variables, variables);
|
|
3822
|
+
if (template.tasks.length === 0) {
|
|
3823
|
+
const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
|
|
3824
|
+
const task = createTask(input, d);
|
|
3825
|
+
return [task];
|
|
3182
3826
|
}
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3827
|
+
const createdTasks = [];
|
|
3828
|
+
const positionToId = new Map;
|
|
3829
|
+
const skippedPositions = new Set;
|
|
3830
|
+
for (const tt of template.tasks) {
|
|
3831
|
+
if (tt.include_template_id) {
|
|
3832
|
+
const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
|
|
3833
|
+
createdTasks.push(...includedTasks);
|
|
3834
|
+
if (includedTasks.length > 0) {
|
|
3835
|
+
positionToId.set(tt.position, includedTasks[0].id);
|
|
3836
|
+
} else {
|
|
3837
|
+
skippedPositions.add(tt.position);
|
|
3838
|
+
}
|
|
3839
|
+
continue;
|
|
3190
3840
|
}
|
|
3841
|
+
if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
|
|
3842
|
+
skippedPositions.add(tt.position);
|
|
3843
|
+
continue;
|
|
3844
|
+
}
|
|
3845
|
+
let title = tt.title_pattern;
|
|
3846
|
+
let desc = tt.description;
|
|
3847
|
+
title = substituteVars(title, resolved);
|
|
3848
|
+
if (desc)
|
|
3849
|
+
desc = substituteVars(desc, resolved);
|
|
3850
|
+
const task = createTask({
|
|
3851
|
+
title,
|
|
3852
|
+
description: desc ?? undefined,
|
|
3853
|
+
priority: tt.priority,
|
|
3854
|
+
tags: tt.tags,
|
|
3855
|
+
task_type: tt.task_type ?? undefined,
|
|
3856
|
+
project_id: projectId,
|
|
3857
|
+
task_list_id: taskListId,
|
|
3858
|
+
metadata: tt.metadata
|
|
3859
|
+
}, d);
|
|
3860
|
+
createdTasks.push(task);
|
|
3861
|
+
positionToId.set(tt.position, task.id);
|
|
3191
3862
|
}
|
|
3192
|
-
const
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
limitClause = " LIMIT ?";
|
|
3207
|
-
params.push(filter.limit);
|
|
3208
|
-
if (!filter.cursor && filter.offset) {
|
|
3209
|
-
limitClause += " OFFSET ?";
|
|
3210
|
-
params.push(filter.offset);
|
|
3863
|
+
for (const tt of template.tasks) {
|
|
3864
|
+
if (skippedPositions.has(tt.position))
|
|
3865
|
+
continue;
|
|
3866
|
+
if (tt.include_template_id)
|
|
3867
|
+
continue;
|
|
3868
|
+
const deps = tt.depends_on_positions;
|
|
3869
|
+
for (const depPos of deps) {
|
|
3870
|
+
if (skippedPositions.has(depPos))
|
|
3871
|
+
continue;
|
|
3872
|
+
const taskId = positionToId.get(tt.position);
|
|
3873
|
+
const depId = positionToId.get(depPos);
|
|
3874
|
+
if (taskId && depId) {
|
|
3875
|
+
addDependency(taskId, depId, d);
|
|
3876
|
+
}
|
|
3211
3877
|
}
|
|
3212
3878
|
}
|
|
3213
|
-
|
|
3214
|
-
return rows.map(rowToTask);
|
|
3879
|
+
return createdTasks;
|
|
3215
3880
|
}
|
|
3216
|
-
function
|
|
3881
|
+
function previewTemplate(templateId, variables, db) {
|
|
3217
3882
|
const d = db || getDatabase();
|
|
3218
|
-
const
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
conditions.push("priority = ?");
|
|
3247
|
-
params.push(filter.priority);
|
|
3883
|
+
const template = getTemplateWithTasks(templateId, d);
|
|
3884
|
+
if (!template)
|
|
3885
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
3886
|
+
const resolved = resolveVariables(template.variables, variables);
|
|
3887
|
+
const tasks = [];
|
|
3888
|
+
if (template.tasks.length === 0) {
|
|
3889
|
+
tasks.push({
|
|
3890
|
+
position: 0,
|
|
3891
|
+
title: substituteVars(template.title_pattern, resolved),
|
|
3892
|
+
description: template.description ? substituteVars(template.description, resolved) : null,
|
|
3893
|
+
priority: template.priority,
|
|
3894
|
+
tags: template.tags,
|
|
3895
|
+
task_type: null,
|
|
3896
|
+
depends_on_positions: []
|
|
3897
|
+
});
|
|
3898
|
+
} else {
|
|
3899
|
+
for (const tt of template.tasks) {
|
|
3900
|
+
if (tt.condition && !evaluateCondition(tt.condition, resolved))
|
|
3901
|
+
continue;
|
|
3902
|
+
tasks.push({
|
|
3903
|
+
position: tt.position,
|
|
3904
|
+
title: substituteVars(tt.title_pattern, resolved),
|
|
3905
|
+
description: tt.description ? substituteVars(tt.description, resolved) : null,
|
|
3906
|
+
priority: tt.priority,
|
|
3907
|
+
tags: tt.tags,
|
|
3908
|
+
task_type: tt.task_type,
|
|
3909
|
+
depends_on_positions: tt.depends_on_positions
|
|
3910
|
+
});
|
|
3248
3911
|
}
|
|
3249
3912
|
}
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
}
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3913
|
+
return {
|
|
3914
|
+
template_id: template.id,
|
|
3915
|
+
template_name: template.name,
|
|
3916
|
+
description: template.description,
|
|
3917
|
+
variables: template.variables,
|
|
3918
|
+
resolved_variables: resolved,
|
|
3919
|
+
tasks
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
// src/db/task-graph.ts
|
|
3924
|
+
init_types();
|
|
3925
|
+
init_database();
|
|
3926
|
+
function addDependency(taskId, dependsOn, db) {
|
|
3927
|
+
const d = db || getDatabase();
|
|
3928
|
+
if (!getTask(taskId, d))
|
|
3929
|
+
throw new TaskNotFoundError(taskId);
|
|
3930
|
+
if (!getTask(dependsOn, d))
|
|
3931
|
+
throw new TaskNotFoundError(dependsOn);
|
|
3932
|
+
if (wouldCreateCycle(taskId, dependsOn, d)) {
|
|
3933
|
+
throw new DependencyCycleError(taskId, dependsOn);
|
|
3261
3934
|
}
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3935
|
+
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
|
|
3936
|
+
}
|
|
3937
|
+
function removeDependency(taskId, dependsOn, db) {
|
|
3938
|
+
const d = db || getDatabase();
|
|
3939
|
+
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
3940
|
+
return result.changes > 0;
|
|
3941
|
+
}
|
|
3942
|
+
function getTaskDependencies(taskId, db) {
|
|
3943
|
+
const d = db || getDatabase();
|
|
3944
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
3945
|
+
}
|
|
3946
|
+
function getTaskDependents(taskId, db) {
|
|
3947
|
+
const d = db || getDatabase();
|
|
3948
|
+
return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
|
|
3949
|
+
}
|
|
3950
|
+
function cloneTask(taskId, overrides, db) {
|
|
3951
|
+
const d = db || getDatabase();
|
|
3952
|
+
const source = getTask(taskId, d);
|
|
3953
|
+
if (!source)
|
|
3954
|
+
throw new TaskNotFoundError(taskId);
|
|
3955
|
+
const input = {
|
|
3956
|
+
title: overrides?.title ?? source.title,
|
|
3957
|
+
description: overrides?.description ?? source.description ?? undefined,
|
|
3958
|
+
priority: overrides?.priority ?? source.priority,
|
|
3959
|
+
project_id: overrides?.project_id ?? source.project_id ?? undefined,
|
|
3960
|
+
parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
|
|
3961
|
+
plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
|
|
3962
|
+
task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
|
|
3963
|
+
status: overrides?.status ?? "pending",
|
|
3964
|
+
agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
|
|
3965
|
+
assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
|
|
3966
|
+
tags: overrides?.tags ?? source.tags,
|
|
3967
|
+
metadata: overrides?.metadata ?? source.metadata,
|
|
3968
|
+
estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
|
|
3969
|
+
recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
|
|
3970
|
+
};
|
|
3971
|
+
return createTask(input, d);
|
|
3972
|
+
}
|
|
3973
|
+
function getTaskGraph(taskId, direction = "both", db) {
|
|
3974
|
+
const d = db || getDatabase();
|
|
3975
|
+
const task = getTask(taskId, d);
|
|
3976
|
+
if (!task)
|
|
3977
|
+
throw new TaskNotFoundError(taskId);
|
|
3978
|
+
function toNode(t) {
|
|
3979
|
+
const deps = getTaskDependencies(t.id, d);
|
|
3980
|
+
const hasUnfinishedDeps = deps.some((dep) => {
|
|
3981
|
+
const depTask = getTask(dep.depends_on, d);
|
|
3982
|
+
return depTask && depTask.status !== "completed";
|
|
3983
|
+
});
|
|
3984
|
+
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
3266
3985
|
}
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3986
|
+
function buildUp(id, visited) {
|
|
3987
|
+
if (visited.has(id))
|
|
3988
|
+
return [];
|
|
3989
|
+
visited.add(id);
|
|
3990
|
+
const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
|
|
3991
|
+
return deps.map((dep) => {
|
|
3992
|
+
const depTask = getTask(dep.depends_on, d);
|
|
3993
|
+
if (!depTask)
|
|
3994
|
+
return null;
|
|
3995
|
+
return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
|
|
3996
|
+
}).filter(Boolean);
|
|
3270
3997
|
}
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3998
|
+
function buildDown(id, visited) {
|
|
3999
|
+
if (visited.has(id))
|
|
4000
|
+
return [];
|
|
4001
|
+
visited.add(id);
|
|
4002
|
+
const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
|
|
4003
|
+
return dependents.map((dep) => {
|
|
4004
|
+
const depTask = getTask(dep.task_id, d);
|
|
4005
|
+
if (!depTask)
|
|
4006
|
+
return null;
|
|
4007
|
+
return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
|
|
4008
|
+
}).filter(Boolean);
|
|
3274
4009
|
}
|
|
3275
|
-
const
|
|
3276
|
-
const
|
|
3277
|
-
|
|
4010
|
+
const rootNode = toNode(task);
|
|
4011
|
+
const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
|
|
4012
|
+
const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
|
|
4013
|
+
return { task: rootNode, depends_on, blocks };
|
|
3278
4014
|
}
|
|
3279
|
-
function
|
|
4015
|
+
function moveTask(taskId, target, db) {
|
|
3280
4016
|
const d = db || getDatabase();
|
|
3281
|
-
const task = getTask(
|
|
4017
|
+
const task = getTask(taskId, d);
|
|
3282
4018
|
if (!task)
|
|
3283
|
-
throw new TaskNotFoundError(
|
|
3284
|
-
|
|
3285
|
-
throw new VersionConflictError(id, input.version, task.version);
|
|
3286
|
-
}
|
|
3287
|
-
const sets = ["version = version + 1", "updated_at = ?"];
|
|
4019
|
+
throw new TaskNotFoundError(taskId);
|
|
4020
|
+
const sets = ["updated_at = ?", "version = version + 1"];
|
|
3288
4021
|
const params = [now()];
|
|
3289
|
-
if (
|
|
3290
|
-
sets.push("title = ?");
|
|
3291
|
-
params.push(input.title);
|
|
3292
|
-
}
|
|
3293
|
-
if (input.description !== undefined) {
|
|
3294
|
-
sets.push("description = ?");
|
|
3295
|
-
params.push(input.description);
|
|
3296
|
-
}
|
|
3297
|
-
if (input.status !== undefined) {
|
|
3298
|
-
if (input.status === "completed") {
|
|
3299
|
-
checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
|
|
3300
|
-
}
|
|
3301
|
-
sets.push("status = ?");
|
|
3302
|
-
params.push(input.status);
|
|
3303
|
-
if (input.status === "completed") {
|
|
3304
|
-
sets.push("completed_at = ?");
|
|
3305
|
-
params.push(now());
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
|
-
if (input.priority !== undefined) {
|
|
3309
|
-
sets.push("priority = ?");
|
|
3310
|
-
params.push(input.priority);
|
|
3311
|
-
}
|
|
3312
|
-
if (input.assigned_to !== undefined) {
|
|
3313
|
-
sets.push("assigned_to = ?");
|
|
3314
|
-
params.push(input.assigned_to);
|
|
3315
|
-
}
|
|
3316
|
-
if (input.tags !== undefined) {
|
|
3317
|
-
sets.push("tags = ?");
|
|
3318
|
-
params.push(JSON.stringify(input.tags));
|
|
3319
|
-
}
|
|
3320
|
-
if (input.metadata !== undefined) {
|
|
3321
|
-
sets.push("metadata = ?");
|
|
3322
|
-
params.push(JSON.stringify(input.metadata));
|
|
3323
|
-
}
|
|
3324
|
-
if (input.plan_id !== undefined) {
|
|
3325
|
-
sets.push("plan_id = ?");
|
|
3326
|
-
params.push(input.plan_id);
|
|
3327
|
-
}
|
|
3328
|
-
if (input.task_list_id !== undefined) {
|
|
4022
|
+
if (target.task_list_id !== undefined) {
|
|
3329
4023
|
sets.push("task_list_id = ?");
|
|
3330
|
-
params.push(
|
|
3331
|
-
}
|
|
3332
|
-
if (input.due_at !== undefined) {
|
|
3333
|
-
sets.push("due_at = ?");
|
|
3334
|
-
params.push(input.due_at);
|
|
3335
|
-
}
|
|
3336
|
-
if (input.estimated_minutes !== undefined) {
|
|
3337
|
-
sets.push("estimated_minutes = ?");
|
|
3338
|
-
params.push(input.estimated_minutes);
|
|
3339
|
-
}
|
|
3340
|
-
if (input.requires_approval !== undefined) {
|
|
3341
|
-
sets.push("requires_approval = ?");
|
|
3342
|
-
params.push(input.requires_approval ? 1 : 0);
|
|
3343
|
-
}
|
|
3344
|
-
if (input.approved_by !== undefined) {
|
|
3345
|
-
sets.push("approved_by = ?");
|
|
3346
|
-
params.push(input.approved_by);
|
|
3347
|
-
sets.push("approved_at = ?");
|
|
3348
|
-
params.push(now());
|
|
3349
|
-
}
|
|
3350
|
-
if (input.recurrence_rule !== undefined) {
|
|
3351
|
-
sets.push("recurrence_rule = ?");
|
|
3352
|
-
params.push(input.recurrence_rule);
|
|
3353
|
-
}
|
|
3354
|
-
if (input.task_type !== undefined) {
|
|
3355
|
-
sets.push("task_type = ?");
|
|
3356
|
-
params.push(input.task_type ?? null);
|
|
3357
|
-
}
|
|
3358
|
-
params.push(id, input.version);
|
|
3359
|
-
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
3360
|
-
if (result.changes === 0) {
|
|
3361
|
-
const current = getTask(id, d);
|
|
3362
|
-
throw new VersionConflictError(id, input.version, current?.version ?? -1);
|
|
3363
|
-
}
|
|
3364
|
-
if (input.tags !== undefined) {
|
|
3365
|
-
replaceTaskTags(id, input.tags, d);
|
|
3366
|
-
}
|
|
3367
|
-
const agentId = task.assigned_to || task.agent_id || null;
|
|
3368
|
-
if (input.status !== undefined && input.status !== task.status)
|
|
3369
|
-
logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
|
|
3370
|
-
if (input.priority !== undefined && input.priority !== task.priority)
|
|
3371
|
-
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
3372
|
-
if (input.title !== undefined && input.title !== task.title)
|
|
3373
|
-
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
3374
|
-
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
3375
|
-
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
3376
|
-
if (input.approved_by !== undefined)
|
|
3377
|
-
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
3378
|
-
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
3379
|
-
dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
|
|
4024
|
+
params.push(target.task_list_id);
|
|
3380
4025
|
}
|
|
3381
|
-
if (
|
|
3382
|
-
|
|
4026
|
+
if (target.project_id !== undefined) {
|
|
4027
|
+
sets.push("project_id = ?");
|
|
4028
|
+
params.push(target.project_id);
|
|
3383
4029
|
}
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
completed_at: input.status === "completed" ? now() : task.completed_at,
|
|
3392
|
-
requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
|
|
3393
|
-
approved_by: input.approved_by ?? task.approved_by,
|
|
3394
|
-
approved_at: input.approved_by ? now() : task.approved_at
|
|
3395
|
-
};
|
|
4030
|
+
if (target.plan_id !== undefined) {
|
|
4031
|
+
sets.push("plan_id = ?");
|
|
4032
|
+
params.push(target.plan_id);
|
|
4033
|
+
}
|
|
4034
|
+
params.push(taskId);
|
|
4035
|
+
d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
4036
|
+
return getTask(taskId, d);
|
|
3396
4037
|
}
|
|
3397
|
-
function
|
|
3398
|
-
const
|
|
3399
|
-
const
|
|
3400
|
-
|
|
4038
|
+
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
4039
|
+
const visited = new Set;
|
|
4040
|
+
const queue = [dependsOn];
|
|
4041
|
+
while (queue.length > 0) {
|
|
4042
|
+
const current = queue.shift();
|
|
4043
|
+
if (current === taskId)
|
|
4044
|
+
return true;
|
|
4045
|
+
if (visited.has(current))
|
|
4046
|
+
continue;
|
|
4047
|
+
visited.add(current);
|
|
4048
|
+
const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
|
|
4049
|
+
for (const dep of deps) {
|
|
4050
|
+
queue.push(dep.depends_on);
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
return false;
|
|
3401
4054
|
}
|
|
4055
|
+
|
|
4056
|
+
// src/db/task-lifecycle.ts
|
|
4057
|
+
var MAX_SPAWN_DEPTH = 10;
|
|
3402
4058
|
function getBlockingDeps(id, db) {
|
|
3403
4059
|
const d = db || getDatabase();
|
|
3404
4060
|
const deps = getTaskDependencies(id, d);
|
|
@@ -3453,14 +4109,21 @@ function completeTask(id, agentId, db, options) {
|
|
|
3453
4109
|
completionMeta._completion = { confidence: options.confidence };
|
|
3454
4110
|
}
|
|
3455
4111
|
const hasMeta = Object.keys(completionMeta).length > 0;
|
|
3456
|
-
if (hasMeta) {
|
|
3457
|
-
const meta2 = { ...task.metadata, ...completionMeta };
|
|
3458
|
-
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
|
|
3459
|
-
}
|
|
3460
4112
|
const timestamp = now();
|
|
3461
4113
|
const confidence = options?.confidence !== undefined ? options.confidence : null;
|
|
3462
|
-
|
|
3463
|
-
|
|
4114
|
+
const tx = d.transaction(() => {
|
|
4115
|
+
if (hasMeta) {
|
|
4116
|
+
const meta2 = { ...task.metadata, ...completionMeta };
|
|
4117
|
+
const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
|
|
4118
|
+
if (metaResult.changes === 0) {
|
|
4119
|
+
const current = getTask(id, d);
|
|
4120
|
+
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
4124
|
+
WHERE id = ?`, [timestamp, confidence, timestamp, id]);
|
|
4125
|
+
});
|
|
4126
|
+
tx();
|
|
3464
4127
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
3465
4128
|
dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
|
|
3466
4129
|
let spawnedTask = null;
|
|
@@ -3469,15 +4132,21 @@ function completeTask(id, agentId, db, options) {
|
|
|
3469
4132
|
}
|
|
3470
4133
|
let spawnedFromTemplate = null;
|
|
3471
4134
|
if (task.spawns_template_id) {
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
4135
|
+
const spawnDepth = task.metadata?._spawn_depth || 0;
|
|
4136
|
+
if (spawnDepth >= MAX_SPAWN_DEPTH) {
|
|
4137
|
+
console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
|
|
4138
|
+
} else {
|
|
4139
|
+
try {
|
|
4140
|
+
const input = taskFromTemplate(task.spawns_template_id, {
|
|
4141
|
+
project_id: task.project_id ?? undefined,
|
|
4142
|
+
plan_id: task.plan_id ?? undefined,
|
|
4143
|
+
task_list_id: task.task_list_id ?? undefined,
|
|
4144
|
+
assigned_to: task.assigned_to ?? undefined
|
|
4145
|
+
}, d);
|
|
4146
|
+
input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
|
|
4147
|
+
spawnedFromTemplate = createTask(input, d);
|
|
4148
|
+
} catch {}
|
|
4149
|
+
}
|
|
3481
4150
|
}
|
|
3482
4151
|
const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
|
|
3483
4152
|
if (spawnedTask) {
|
|
@@ -3542,141 +4211,6 @@ function unlockTask(id, agentId, db) {
|
|
|
3542
4211
|
WHERE id = ?`, [timestamp, id]);
|
|
3543
4212
|
return true;
|
|
3544
4213
|
}
|
|
3545
|
-
function addDependency(taskId, dependsOn, db) {
|
|
3546
|
-
const d = db || getDatabase();
|
|
3547
|
-
if (!getTask(taskId, d))
|
|
3548
|
-
throw new TaskNotFoundError(taskId);
|
|
3549
|
-
if (!getTask(dependsOn, d))
|
|
3550
|
-
throw new TaskNotFoundError(dependsOn);
|
|
3551
|
-
if (wouldCreateCycle(taskId, dependsOn, d)) {
|
|
3552
|
-
throw new DependencyCycleError(taskId, dependsOn);
|
|
3553
|
-
}
|
|
3554
|
-
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
|
|
3555
|
-
}
|
|
3556
|
-
function removeDependency(taskId, dependsOn, db) {
|
|
3557
|
-
const d = db || getDatabase();
|
|
3558
|
-
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
3559
|
-
return result.changes > 0;
|
|
3560
|
-
}
|
|
3561
|
-
function getTaskDependencies(taskId, db) {
|
|
3562
|
-
const d = db || getDatabase();
|
|
3563
|
-
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
3564
|
-
}
|
|
3565
|
-
function getTaskDependents(taskId, db) {
|
|
3566
|
-
const d = db || getDatabase();
|
|
3567
|
-
return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
|
|
3568
|
-
}
|
|
3569
|
-
function cloneTask(taskId, overrides, db) {
|
|
3570
|
-
const d = db || getDatabase();
|
|
3571
|
-
const source = getTask(taskId, d);
|
|
3572
|
-
if (!source)
|
|
3573
|
-
throw new TaskNotFoundError(taskId);
|
|
3574
|
-
const input = {
|
|
3575
|
-
title: overrides?.title ?? source.title,
|
|
3576
|
-
description: overrides?.description ?? source.description ?? undefined,
|
|
3577
|
-
priority: overrides?.priority ?? source.priority,
|
|
3578
|
-
project_id: overrides?.project_id ?? source.project_id ?? undefined,
|
|
3579
|
-
parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
|
|
3580
|
-
plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
|
|
3581
|
-
task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
|
|
3582
|
-
status: overrides?.status ?? "pending",
|
|
3583
|
-
agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
|
|
3584
|
-
assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
|
|
3585
|
-
tags: overrides?.tags ?? source.tags,
|
|
3586
|
-
metadata: overrides?.metadata ?? source.metadata,
|
|
3587
|
-
estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
|
|
3588
|
-
recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
|
|
3589
|
-
};
|
|
3590
|
-
return createTask(input, d);
|
|
3591
|
-
}
|
|
3592
|
-
function getTaskGraph(taskId, direction = "both", db) {
|
|
3593
|
-
const d = db || getDatabase();
|
|
3594
|
-
const task = getTask(taskId, d);
|
|
3595
|
-
if (!task)
|
|
3596
|
-
throw new TaskNotFoundError(taskId);
|
|
3597
|
-
function toNode(t) {
|
|
3598
|
-
const deps = getTaskDependencies(t.id, d);
|
|
3599
|
-
const hasUnfinishedDeps = deps.some((dep) => {
|
|
3600
|
-
const depTask = getTask(dep.depends_on, d);
|
|
3601
|
-
return depTask && depTask.status !== "completed";
|
|
3602
|
-
});
|
|
3603
|
-
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
3604
|
-
}
|
|
3605
|
-
function buildUp(id, visited) {
|
|
3606
|
-
if (visited.has(id))
|
|
3607
|
-
return [];
|
|
3608
|
-
visited.add(id);
|
|
3609
|
-
const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
|
|
3610
|
-
return deps.map((dep) => {
|
|
3611
|
-
const depTask = getTask(dep.depends_on, d);
|
|
3612
|
-
if (!depTask)
|
|
3613
|
-
return null;
|
|
3614
|
-
return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
|
|
3615
|
-
}).filter(Boolean);
|
|
3616
|
-
}
|
|
3617
|
-
function buildDown(id, visited) {
|
|
3618
|
-
if (visited.has(id))
|
|
3619
|
-
return [];
|
|
3620
|
-
visited.add(id);
|
|
3621
|
-
const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
|
|
3622
|
-
return dependents.map((dep) => {
|
|
3623
|
-
const depTask = getTask(dep.task_id, d);
|
|
3624
|
-
if (!depTask)
|
|
3625
|
-
return null;
|
|
3626
|
-
return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
|
|
3627
|
-
}).filter(Boolean);
|
|
3628
|
-
}
|
|
3629
|
-
const rootNode = toNode(task);
|
|
3630
|
-
const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
|
|
3631
|
-
const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
|
|
3632
|
-
return { task: rootNode, depends_on, blocks };
|
|
3633
|
-
}
|
|
3634
|
-
function moveTask(taskId, target, db) {
|
|
3635
|
-
const d = db || getDatabase();
|
|
3636
|
-
const task = getTask(taskId, d);
|
|
3637
|
-
if (!task)
|
|
3638
|
-
throw new TaskNotFoundError(taskId);
|
|
3639
|
-
const sets = ["updated_at = ?", "version = version + 1"];
|
|
3640
|
-
const params = [now()];
|
|
3641
|
-
if (target.task_list_id !== undefined) {
|
|
3642
|
-
sets.push("task_list_id = ?");
|
|
3643
|
-
params.push(target.task_list_id);
|
|
3644
|
-
}
|
|
3645
|
-
if (target.project_id !== undefined) {
|
|
3646
|
-
sets.push("project_id = ?");
|
|
3647
|
-
params.push(target.project_id);
|
|
3648
|
-
}
|
|
3649
|
-
if (target.plan_id !== undefined) {
|
|
3650
|
-
sets.push("plan_id = ?");
|
|
3651
|
-
params.push(target.plan_id);
|
|
3652
|
-
}
|
|
3653
|
-
params.push(taskId);
|
|
3654
|
-
d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
3655
|
-
return getTask(taskId, d);
|
|
3656
|
-
}
|
|
3657
|
-
function spawnNextRecurrence(completedTask, db) {
|
|
3658
|
-
const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
|
|
3659
|
-
let title = completedTask.title;
|
|
3660
|
-
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
3661
|
-
title = title.slice(completedTask.short_id.length + 2);
|
|
3662
|
-
}
|
|
3663
|
-
const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
|
|
3664
|
-
return createTask({
|
|
3665
|
-
title,
|
|
3666
|
-
description: completedTask.description ?? undefined,
|
|
3667
|
-
priority: completedTask.priority,
|
|
3668
|
-
project_id: completedTask.project_id ?? undefined,
|
|
3669
|
-
task_list_id: completedTask.task_list_id ?? undefined,
|
|
3670
|
-
plan_id: completedTask.plan_id ?? undefined,
|
|
3671
|
-
assigned_to: completedTask.assigned_to ?? undefined,
|
|
3672
|
-
tags: completedTask.tags,
|
|
3673
|
-
metadata: completedTask.metadata,
|
|
3674
|
-
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
3675
|
-
recurrence_rule: completedTask.recurrence_rule,
|
|
3676
|
-
recurrence_parent_id: recurrenceParentId,
|
|
3677
|
-
due_at: dueAt
|
|
3678
|
-
}, db);
|
|
3679
|
-
}
|
|
3680
4214
|
function claimNextTask(agentId, filters, db) {
|
|
3681
4215
|
const d = db || getDatabase();
|
|
3682
4216
|
const tx = d.transaction(() => {
|
|
@@ -3849,10 +4383,6 @@ function getStaleTasks(staleMinutes = 30, filters, db) {
|
|
|
3849
4383
|
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
|
|
3850
4384
|
return rows.map(rowToTask);
|
|
3851
4385
|
}
|
|
3852
|
-
function logCost(taskId, tokens, usd, db) {
|
|
3853
|
-
const d = db || getDatabase();
|
|
3854
|
-
d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
|
|
3855
|
-
}
|
|
3856
4386
|
function stealTask(agentId, opts, db) {
|
|
3857
4387
|
const d = db || getDatabase();
|
|
3858
4388
|
const staleMinutes = opts?.stale_minutes ?? 30;
|
|
@@ -3883,6 +4413,32 @@ function claimOrSteal(agentId, filters, db) {
|
|
|
3883
4413
|
});
|
|
3884
4414
|
return tx();
|
|
3885
4415
|
}
|
|
4416
|
+
function spawnNextRecurrence(completedTask, db) {
|
|
4417
|
+
const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
|
|
4418
|
+
let title = completedTask.title;
|
|
4419
|
+
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
4420
|
+
title = title.slice(completedTask.short_id.length + 2);
|
|
4421
|
+
}
|
|
4422
|
+
const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
|
|
4423
|
+
return createTask({
|
|
4424
|
+
title,
|
|
4425
|
+
description: completedTask.description ?? undefined,
|
|
4426
|
+
priority: completedTask.priority,
|
|
4427
|
+
project_id: completedTask.project_id ?? undefined,
|
|
4428
|
+
task_list_id: completedTask.task_list_id ?? undefined,
|
|
4429
|
+
plan_id: completedTask.plan_id ?? undefined,
|
|
4430
|
+
assigned_to: completedTask.assigned_to ?? undefined,
|
|
4431
|
+
tags: completedTask.tags,
|
|
4432
|
+
metadata: completedTask.metadata,
|
|
4433
|
+
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
4434
|
+
recurrence_rule: completedTask.recurrence_rule,
|
|
4435
|
+
recurrence_parent_id: recurrenceParentId,
|
|
4436
|
+
due_at: dueAt
|
|
4437
|
+
}, db);
|
|
4438
|
+
}
|
|
4439
|
+
// src/db/task-status.ts
|
|
4440
|
+
init_types();
|
|
4441
|
+
init_database();
|
|
3886
4442
|
function getStatus(filters, agentId, options, db) {
|
|
3887
4443
|
const d = db || getDatabase();
|
|
3888
4444
|
const pending = countTasks({ ...filters, status: "pending" }, d);
|
|
@@ -4011,23 +4567,6 @@ function redistributeStaleTasks(agentId, options, db) {
|
|
|
4011
4567
|
const claimed = released.length > 0 ? claimNextTask(agentId, options?.project_id ? { project_id: options.project_id } : undefined, d) : null;
|
|
4012
4568
|
return { released, claimed };
|
|
4013
4569
|
}
|
|
4014
|
-
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
4015
|
-
const visited = new Set;
|
|
4016
|
-
const queue = [dependsOn];
|
|
4017
|
-
while (queue.length > 0) {
|
|
4018
|
-
const current = queue.shift();
|
|
4019
|
-
if (current === taskId)
|
|
4020
|
-
return true;
|
|
4021
|
-
if (visited.has(current))
|
|
4022
|
-
continue;
|
|
4023
|
-
visited.add(current);
|
|
4024
|
-
const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
|
|
4025
|
-
for (const dep of deps) {
|
|
4026
|
-
queue.push(dep.depends_on);
|
|
4027
|
-
}
|
|
4028
|
-
}
|
|
4029
|
-
return false;
|
|
4030
|
-
}
|
|
4031
4570
|
function getTaskStats(filters, db) {
|
|
4032
4571
|
const d = db || getDatabase();
|
|
4033
4572
|
const conditions = [];
|
|
@@ -4062,6 +4601,8 @@ function getTaskStats(filters, db) {
|
|
|
4062
4601
|
const completion_rate = totalRow.count > 0 ? Math.round(completed / totalRow.count * 100) : 0;
|
|
4063
4602
|
return { total: totalRow.count, by_status, by_priority, completion_rate, by_agent };
|
|
4064
4603
|
}
|
|
4604
|
+
// src/db/task-relations.ts
|
|
4605
|
+
init_database();
|
|
4065
4606
|
function bulkCreateTasks(inputs, db) {
|
|
4066
4607
|
const d = db || getDatabase();
|
|
4067
4608
|
const tempIdToRealId = new Map;
|
|
@@ -4155,6 +4696,10 @@ function getOverdueTasks(projectId, db) {
|
|
|
4155
4696
|
const rows = d.query(query).all(...params);
|
|
4156
4697
|
return rows.map(rowToTask);
|
|
4157
4698
|
}
|
|
4699
|
+
function logCost(taskId, tokens, usd, db) {
|
|
4700
|
+
const d = db || getDatabase();
|
|
4701
|
+
d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
|
|
4702
|
+
}
|
|
4158
4703
|
// src/db/plans.ts
|
|
4159
4704
|
init_types();
|
|
4160
4705
|
init_database();
|
|
@@ -6266,7 +6811,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
|
|
|
6266
6811
|
}
|
|
6267
6812
|
return { pushed, pulled, errors };
|
|
6268
6813
|
}
|
|
6269
|
-
//
|
|
6814
|
+
// node_modules/@hasna/cloud/dist/index.js
|
|
6270
6815
|
import { createRequire } from "module";
|
|
6271
6816
|
import { Database as Database2 } from "bun:sqlite";
|
|
6272
6817
|
import {
|
|
@@ -6290,9 +6835,9 @@ import { homedir as homedir5, platform } from "os";
|
|
|
6290
6835
|
var __create = Object.create;
|
|
6291
6836
|
var __getProtoOf = Object.getPrototypeOf;
|
|
6292
6837
|
var __defProp2 = Object.defineProperty;
|
|
6293
|
-
var
|
|
6294
|
-
var
|
|
6295
|
-
function
|
|
6838
|
+
var __getOwnPropNames2 = Object.getOwnPropertyNames;
|
|
6839
|
+
var __hasOwnProp2 = Object.prototype.hasOwnProperty;
|
|
6840
|
+
function __accessProp2(key) {
|
|
6296
6841
|
return this[key];
|
|
6297
6842
|
}
|
|
6298
6843
|
var __toESMCache_node;
|
|
@@ -6307,10 +6852,10 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
6307
6852
|
}
|
|
6308
6853
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
6309
6854
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target;
|
|
6310
|
-
for (let key of
|
|
6311
|
-
if (!
|
|
6855
|
+
for (let key of __getOwnPropNames2(mod))
|
|
6856
|
+
if (!__hasOwnProp2.call(to, key))
|
|
6312
6857
|
__defProp2(to, key, {
|
|
6313
|
-
get:
|
|
6858
|
+
get: __accessProp2.bind(mod, key),
|
|
6314
6859
|
enumerable: true
|
|
6315
6860
|
});
|
|
6316
6861
|
if (canCache)
|
|
@@ -6318,9 +6863,9 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
6318
6863
|
return to;
|
|
6319
6864
|
};
|
|
6320
6865
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
6321
|
-
var
|
|
6322
|
-
function
|
|
6323
|
-
this[name] =
|
|
6866
|
+
var __returnValue2 = (v) => v;
|
|
6867
|
+
function __exportSetter2(name, newValue) {
|
|
6868
|
+
this[name] = __returnValue2.bind(null, newValue);
|
|
6324
6869
|
}
|
|
6325
6870
|
var __export2 = (target, all) => {
|
|
6326
6871
|
for (var name in all)
|
|
@@ -6328,7 +6873,7 @@ var __export2 = (target, all) => {
|
|
|
6328
6873
|
get: all[name],
|
|
6329
6874
|
enumerable: true,
|
|
6330
6875
|
configurable: true,
|
|
6331
|
-
set:
|
|
6876
|
+
set: __exportSetter2.bind(all, name)
|
|
6332
6877
|
});
|
|
6333
6878
|
};
|
|
6334
6879
|
var __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -16459,6 +17004,45 @@ var PG_MIGRATIONS = [
|
|
|
16459
17004
|
ALTER TABLE task_dependencies ADD COLUMN external_project_id TEXT;
|
|
16460
17005
|
ALTER TABLE task_dependencies ADD COLUMN external_task_id TEXT;
|
|
16461
17006
|
INSERT INTO _migrations (id) VALUES (43) ON CONFLICT DO NOTHING;
|
|
17007
|
+
`,
|
|
17008
|
+
`
|
|
17009
|
+
CREATE TABLE IF NOT EXISTS task_checkpoints (
|
|
17010
|
+
id TEXT PRIMARY KEY,
|
|
17011
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
17012
|
+
agent_id TEXT,
|
|
17013
|
+
step TEXT NOT NULL,
|
|
17014
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed', 'skipped')),
|
|
17015
|
+
data TEXT DEFAULT '{}',
|
|
17016
|
+
error TEXT,
|
|
17017
|
+
attempt INTEGER NOT NULL DEFAULT 1,
|
|
17018
|
+
max_attempts INTEGER NOT NULL DEFAULT 1,
|
|
17019
|
+
started_at TIMESTAMPTZ,
|
|
17020
|
+
completed_at TIMESTAMPTZ,
|
|
17021
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
17022
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
17023
|
+
);
|
|
17024
|
+
CREATE INDEX IF NOT EXISTS idx_task_checkpoints_task ON task_checkpoints(task_id);
|
|
17025
|
+
CREATE INDEX IF NOT EXISTS idx_task_checkpoints_status ON task_checkpoints(status);
|
|
17026
|
+
|
|
17027
|
+
CREATE TABLE IF NOT EXISTS task_heartbeats (
|
|
17028
|
+
id TEXT PRIMARY KEY,
|
|
17029
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
17030
|
+
agent_id TEXT,
|
|
17031
|
+
step TEXT,
|
|
17032
|
+
message TEXT,
|
|
17033
|
+
progress REAL CHECK(progress >= 0 AND progress <= 1),
|
|
17034
|
+
meta TEXT DEFAULT '{}',
|
|
17035
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
17036
|
+
);
|
|
17037
|
+
CREATE INDEX IF NOT EXISTS idx_task_heartbeats_task ON task_heartbeats(task_id);
|
|
17038
|
+
CREATE INDEX IF NOT EXISTS idx_task_heartbeats_agent ON task_heartbeats(agent_id);
|
|
17039
|
+
|
|
17040
|
+
ALTER TABLE tasks ADD COLUMN runner_id TEXT;
|
|
17041
|
+
ALTER TABLE tasks ADD COLUMN runner_started_at TIMESTAMPTZ;
|
|
17042
|
+
ALTER TABLE tasks ADD COLUMN runner_completed_at TIMESTAMPTZ;
|
|
17043
|
+
ALTER TABLE tasks ADD COLUMN current_step TEXT;
|
|
17044
|
+
ALTER TABLE tasks ADD COLUMN total_steps INTEGER;
|
|
17045
|
+
INSERT INTO _migrations (id) VALUES (44) ON CONFLICT DO NOTHING;
|
|
16462
17046
|
`
|
|
16463
17047
|
];
|
|
16464
17048
|
|
|
@@ -16772,15 +17356,24 @@ function renderBurndownChart(total, days) {
|
|
|
16772
17356
|
`);
|
|
16773
17357
|
}
|
|
16774
17358
|
// src/lib/github.ts
|
|
16775
|
-
import {
|
|
17359
|
+
import { execFileSync } from "child_process";
|
|
16776
17360
|
function parseGitHubUrl(url) {
|
|
16777
|
-
const match = url.match(/github\.com\/([
|
|
17361
|
+
const match = url.match(/github\.com\/([a-zA-Z0-9](?:[a-zA-Z0-9._-]{0,38}[a-zA-Z0-9])?)\/([a-zA-Z0-9._-]+)\/issues\/(\d+)/);
|
|
16778
17362
|
if (!match)
|
|
16779
17363
|
return null;
|
|
16780
17364
|
return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
|
|
16781
17365
|
}
|
|
17366
|
+
function isSafeOwnerRepo(value) {
|
|
17367
|
+
return /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value) && value.length <= 39;
|
|
17368
|
+
}
|
|
16782
17369
|
function fetchGitHubIssue(owner, repo, number) {
|
|
16783
|
-
|
|
17370
|
+
if (!isSafeOwnerRepo(owner))
|
|
17371
|
+
throw new Error(`Invalid GitHub owner: ${owner}`);
|
|
17372
|
+
if (!isSafeOwnerRepo(repo))
|
|
17373
|
+
throw new Error(`Invalid GitHub repo: ${repo}`);
|
|
17374
|
+
if (!Number.isInteger(number) || number < 1)
|
|
17375
|
+
throw new Error(`Invalid issue number: ${number}`);
|
|
17376
|
+
const json = execFileSync("gh", ["api", `repos/${owner}/${repo}/issues/${number}`], { encoding: "utf-8", timeout: 15000 });
|
|
16784
17377
|
const data = JSON.parse(json);
|
|
16785
17378
|
return {
|
|
16786
17379
|
number: data.number,
|