@hasna/todos 0.11.29 → 0.11.31
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/commands/machines.d.ts +3 -0
- package/dist/cli/commands/machines.d.ts.map +1 -0
- package/dist/cli/index.js +26812 -28466
- 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/machines.d.ts +30 -1
- package/dist/db/machines.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 +2400 -1791
- package/dist/lib/github.d.ts +1 -1
- package/dist/lib/github.d.ts.map +1 -1
- package/dist/lib/logger.d.ts +17 -0
- package/dist/lib/logger.d.ts.map +1 -0
- 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 +5423 -9138
- 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/machines.d.ts +8 -0
- package/dist/mcp/tools/machines.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 +1968 -2804
- 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 +83 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -2
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,15 +769,465 @@ 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
|
+
ssh_address TEXT, is_primary INTEGER NOT NULL DEFAULT 0,
|
|
992
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
993
|
+
archived_at TEXT,
|
|
994
|
+
metadata TEXT DEFAULT '{}',
|
|
995
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
996
|
+
)`);
|
|
997
|
+
ensureColumn("machines", "ssh_address", "TEXT");
|
|
998
|
+
ensureColumn("machines", "is_primary", "INTEGER NOT NULL DEFAULT 0");
|
|
999
|
+
ensureColumn("machines", "archived_at", "TEXT");
|
|
1000
|
+
ensureColumn("projects", "task_list_id", "TEXT");
|
|
1001
|
+
ensureColumn("projects", "task_prefix", "TEXT");
|
|
1002
|
+
ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
|
|
1003
|
+
ensureColumn("tasks", "plan_id", "TEXT REFERENCES plans(id) ON DELETE SET NULL");
|
|
1004
|
+
ensureColumn("tasks", "task_list_id", "TEXT REFERENCES task_lists(id) ON DELETE SET NULL");
|
|
1005
|
+
ensureColumn("tasks", "short_id", "TEXT");
|
|
1006
|
+
ensureColumn("tasks", "due_at", "TEXT");
|
|
1007
|
+
ensureColumn("tasks", "estimated_minutes", "INTEGER");
|
|
1008
|
+
ensureColumn("tasks", "requires_approval", "INTEGER NOT NULL DEFAULT 0");
|
|
1009
|
+
ensureColumn("tasks", "approved_by", "TEXT");
|
|
1010
|
+
ensureColumn("tasks", "approved_at", "TEXT");
|
|
1011
|
+
ensureColumn("tasks", "recurrence_rule", "TEXT");
|
|
1012
|
+
ensureColumn("tasks", "recurrence_parent_id", "TEXT REFERENCES tasks(id) ON DELETE SET NULL");
|
|
1013
|
+
ensureColumn("tasks", "confidence", "REAL");
|
|
1014
|
+
ensureColumn("tasks", "reason", "TEXT");
|
|
1015
|
+
ensureColumn("tasks", "spawned_from_session", "TEXT");
|
|
1016
|
+
ensureColumn("tasks", "assigned_by", "TEXT");
|
|
1017
|
+
ensureColumn("tasks", "assigned_from_project", "TEXT");
|
|
1018
|
+
ensureColumn("tasks", "started_at", "TEXT");
|
|
1019
|
+
ensureColumn("tasks", "task_type", "TEXT");
|
|
1020
|
+
ensureColumn("tasks", "cost_tokens", "INTEGER DEFAULT 0");
|
|
1021
|
+
ensureColumn("tasks", "cost_usd", "REAL DEFAULT 0");
|
|
1022
|
+
ensureColumn("tasks", "delegated_from", "TEXT");
|
|
1023
|
+
ensureColumn("tasks", "delegation_depth", "INTEGER DEFAULT 0");
|
|
1024
|
+
ensureColumn("tasks", "retry_count", "INTEGER DEFAULT 0");
|
|
1025
|
+
ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
|
|
1026
|
+
ensureColumn("tasks", "retry_after", "TEXT");
|
|
1027
|
+
ensureColumn("tasks", "sla_minutes", "INTEGER");
|
|
1028
|
+
ensureColumn("tasks", "archived_at", "TEXT");
|
|
1029
|
+
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
1030
|
+
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
1031
|
+
ensureColumn("agents", "reports_to", "TEXT");
|
|
1032
|
+
ensureColumn("agents", "title", "TEXT");
|
|
1033
|
+
ensureColumn("agents", "level", "TEXT");
|
|
1034
|
+
ensureColumn("agents", "org_id", "TEXT");
|
|
1035
|
+
ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
|
|
1036
|
+
ensureColumn("projects", "org_id", "TEXT");
|
|
1037
|
+
ensureColumn("plans", "task_list_id", "TEXT");
|
|
1038
|
+
ensureColumn("plans", "agent_id", "TEXT");
|
|
1039
|
+
ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
|
|
1040
|
+
ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
|
|
1041
|
+
ensureColumn("template_tasks", "condition", "TEXT");
|
|
1042
|
+
ensureColumn("template_tasks", "include_template_id", "TEXT");
|
|
1043
|
+
ensureTable("template_versions", `
|
|
1044
|
+
CREATE TABLE template_versions (
|
|
1045
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
1046
|
+
template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
|
|
1047
|
+
version INTEGER NOT NULL,
|
|
1048
|
+
snapshot TEXT NOT NULL,
|
|
1049
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1050
|
+
)`);
|
|
1051
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id)");
|
|
1052
|
+
ensureTable("dispatches", `
|
|
1053
|
+
CREATE TABLE dispatches (
|
|
1054
|
+
id TEXT PRIMARY KEY,
|
|
1055
|
+
title TEXT,
|
|
1056
|
+
target_window TEXT NOT NULL,
|
|
1057
|
+
task_ids TEXT NOT NULL DEFAULT '[]',
|
|
1058
|
+
task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL,
|
|
1059
|
+
message TEXT,
|
|
1060
|
+
delay_ms INTEGER,
|
|
1061
|
+
scheduled_at TEXT,
|
|
1062
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'sent', 'failed', 'cancelled')),
|
|
1063
|
+
error TEXT,
|
|
1064
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1065
|
+
sent_at TEXT
|
|
1066
|
+
)`);
|
|
1067
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_status ON dispatches(status)");
|
|
1068
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_scheduled ON dispatches(scheduled_at)");
|
|
1069
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatches_task_list ON dispatches(task_list_id)");
|
|
1070
|
+
ensureTable("dispatch_logs", `
|
|
1071
|
+
CREATE TABLE dispatch_logs (
|
|
1072
|
+
id TEXT PRIMARY KEY,
|
|
1073
|
+
dispatch_id TEXT NOT NULL REFERENCES dispatches(id) ON DELETE CASCADE,
|
|
1074
|
+
target_window TEXT NOT NULL,
|
|
1075
|
+
message TEXT NOT NULL,
|
|
1076
|
+
delay_ms INTEGER NOT NULL,
|
|
1077
|
+
status TEXT NOT NULL CHECK(status IN ('sent', 'failed')),
|
|
1078
|
+
error TEXT,
|
|
1079
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1080
|
+
)`);
|
|
1081
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_dispatch_logs_dispatch ON dispatch_logs(dispatch_id)");
|
|
1082
|
+
ensureColumn("webhooks", "project_id", "TEXT");
|
|
1083
|
+
ensureColumn("webhooks", "task_list_id", "TEXT");
|
|
1084
|
+
ensureColumn("webhooks", "agent_id", "TEXT");
|
|
1085
|
+
ensureColumn("webhooks", "task_id", "TEXT");
|
|
1086
|
+
ensureTable("webhook_deliveries", `
|
|
1087
|
+
CREATE TABLE webhook_deliveries (
|
|
1088
|
+
id TEXT PRIMARY KEY,
|
|
1089
|
+
webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
|
|
1090
|
+
event TEXT NOT NULL,
|
|
1091
|
+
payload TEXT NOT NULL,
|
|
1092
|
+
status_code INTEGER,
|
|
1093
|
+
response TEXT,
|
|
1094
|
+
attempt INTEGER NOT NULL DEFAULT 1,
|
|
1095
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1096
|
+
)`);
|
|
1097
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id)");
|
|
1098
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
|
|
1099
|
+
ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
|
|
1100
|
+
ensureColumn("task_comments", "progress_pct", "INTEGER");
|
|
1101
|
+
ensureColumn("projects", "machine_id", "TEXT");
|
|
1102
|
+
ensureColumn("projects", "synced_at", "TEXT");
|
|
1103
|
+
ensureColumn("tasks", "machine_id", "TEXT");
|
|
1104
|
+
ensureColumn("tasks", "synced_at", "TEXT");
|
|
1105
|
+
ensureColumn("agents", "machine_id", "TEXT");
|
|
1106
|
+
ensureColumn("agents", "synced_at", "TEXT");
|
|
1107
|
+
ensureColumn("task_lists", "machine_id", "TEXT");
|
|
1108
|
+
ensureColumn("task_lists", "synced_at", "TEXT");
|
|
1109
|
+
ensureColumn("plans", "machine_id", "TEXT");
|
|
1110
|
+
ensureColumn("plans", "synced_at", "TEXT");
|
|
1111
|
+
ensureColumn("task_comments", "machine_id", "TEXT");
|
|
1112
|
+
ensureColumn("task_comments", "synced_at", "TEXT");
|
|
1113
|
+
ensureColumn("sessions", "machine_id", "TEXT");
|
|
1114
|
+
ensureColumn("sessions", "synced_at", "TEXT");
|
|
1115
|
+
ensureColumn("task_history", "machine_id", "TEXT");
|
|
1116
|
+
ensureColumn("webhooks", "machine_id", "TEXT");
|
|
1117
|
+
ensureColumn("webhooks", "synced_at", "TEXT");
|
|
1118
|
+
ensureColumn("task_templates", "machine_id", "TEXT");
|
|
1119
|
+
ensureColumn("task_templates", "synced_at", "TEXT");
|
|
1120
|
+
ensureColumn("orgs", "machine_id", "TEXT");
|
|
1121
|
+
ensureColumn("orgs", "synced_at", "TEXT");
|
|
1122
|
+
ensureColumn("handoffs", "machine_id", "TEXT");
|
|
1123
|
+
ensureColumn("handoffs", "synced_at", "TEXT");
|
|
1124
|
+
ensureColumn("task_checklists", "machine_id", "TEXT");
|
|
1125
|
+
ensureColumn("project_sources", "machine_id", "TEXT");
|
|
1126
|
+
ensureColumn("project_sources", "synced_at", "TEXT");
|
|
1127
|
+
ensureColumn("task_files", "machine_id", "TEXT");
|
|
1128
|
+
ensureColumn("task_relationships", "machine_id", "TEXT");
|
|
1129
|
+
ensureColumn("kg_edges", "machine_id", "TEXT");
|
|
1130
|
+
ensureColumn("project_agent_roles", "machine_id", "TEXT");
|
|
1131
|
+
ensureColumn("dispatches", "machine_id", "TEXT");
|
|
1132
|
+
ensureColumn("dispatches", "synced_at", "TEXT");
|
|
1133
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
|
|
1134
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id)");
|
|
1135
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at)");
|
|
1136
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL");
|
|
1137
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name)");
|
|
1138
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_project ON task_lists(project_id)");
|
|
1139
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug)");
|
|
1140
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag)");
|
|
1141
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id)");
|
|
1142
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
|
|
1143
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
|
|
1144
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
|
|
1145
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
|
|
1146
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
|
|
1147
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(task_id)");
|
|
1148
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id)");
|
|
1149
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL");
|
|
1150
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_checklists_task ON task_checklists(task_id)");
|
|
1151
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id)");
|
|
1152
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type)");
|
|
1153
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by)");
|
|
1154
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id)");
|
|
1155
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id)");
|
|
1156
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type)");
|
|
1157
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id)");
|
|
1158
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type)");
|
|
1159
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type)");
|
|
1160
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type)");
|
|
1161
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_machine ON tasks(machine_id)");
|
|
1162
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_synced ON tasks(synced_at)");
|
|
1163
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_projects_machine ON projects(machine_id)");
|
|
1164
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_agents_machine ON agents(machine_id)");
|
|
1165
|
+
ensureTable("task_time_logs", `
|
|
1166
|
+
CREATE TABLE task_time_logs (
|
|
1167
|
+
id TEXT PRIMARY KEY,
|
|
1168
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1169
|
+
agent_id TEXT,
|
|
1170
|
+
started_at TEXT,
|
|
1171
|
+
ended_at TEXT,
|
|
1172
|
+
minutes INTEGER NOT NULL,
|
|
1173
|
+
notes TEXT,
|
|
1174
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1175
|
+
)`);
|
|
1176
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_task ON task_time_logs(task_id)");
|
|
1177
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_agent ON task_time_logs(agent_id)");
|
|
1178
|
+
ensureColumn("tasks", "actual_minutes", "INTEGER");
|
|
1179
|
+
ensureTable("task_watchers", `
|
|
1180
|
+
CREATE TABLE task_watchers (
|
|
1181
|
+
id TEXT PRIMARY KEY,
|
|
1182
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1183
|
+
agent_id TEXT NOT NULL,
|
|
1184
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1185
|
+
UNIQUE(task_id, agent_id)
|
|
1186
|
+
)`);
|
|
1187
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_task ON task_watchers(task_id)");
|
|
1188
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_watchers_agent ON task_watchers(agent_id)");
|
|
1189
|
+
ensureColumn("task_dependencies", "external_project_id", "TEXT");
|
|
1190
|
+
ensureColumn("task_dependencies", "external_task_id", "TEXT");
|
|
1191
|
+
}
|
|
1192
|
+
function backfillTaskTags(db) {
|
|
1193
|
+
try {
|
|
1194
|
+
const count = db.query("SELECT COUNT(*) as count FROM task_tags").get();
|
|
1195
|
+
if (count && count.count > 0)
|
|
1196
|
+
return;
|
|
1197
|
+
} catch {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
try {
|
|
1201
|
+
const rows = db.query("SELECT id, tags FROM tasks WHERE tags IS NOT NULL AND tags != '[]'").all();
|
|
1202
|
+
if (rows.length === 0)
|
|
1203
|
+
return;
|
|
1204
|
+
const insert = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
1205
|
+
for (const row of rows) {
|
|
1206
|
+
if (!row.tags)
|
|
1207
|
+
continue;
|
|
1208
|
+
let tags = [];
|
|
1209
|
+
try {
|
|
1210
|
+
tags = JSON.parse(row.tags);
|
|
1211
|
+
} catch {
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
for (const tag of tags) {
|
|
1215
|
+
if (tag)
|
|
1216
|
+
insert.run(row.id, tag);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
} catch {}
|
|
1220
|
+
}
|
|
1221
|
+
var init_schema = __esm(() => {
|
|
1222
|
+
init_migrations();
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1149
1225
|
// src/db/machines.ts
|
|
1150
1226
|
import { hostname as osHostname, platform as osPlatform } from "os";
|
|
1151
1227
|
function rowToMachine(row) {
|
|
1152
1228
|
return {
|
|
1153
1229
|
...row,
|
|
1230
|
+
is_primary: !!row.is_primary,
|
|
1154
1231
|
metadata: row.metadata ? JSON.parse(row.metadata) : {}
|
|
1155
1232
|
};
|
|
1156
1233
|
}
|
|
@@ -1167,7 +1244,7 @@ function getOrCreateLocalMachine(db) {
|
|
|
1167
1244
|
const id = uuid();
|
|
1168
1245
|
const ts = now();
|
|
1169
1246
|
d.run("INSERT INTO machines (id, name, hostname, platform, last_seen_at, metadata, created_at) VALUES (?, ?, ?, ?, ?, '{}', ?)", [id, name, host, plat, ts, ts]);
|
|
1170
|
-
return { id, name, hostname: host, platform: plat, last_seen_at: ts, metadata: {}, created_at: ts };
|
|
1247
|
+
return { id, name, hostname: host, platform: plat, ssh_address: null, is_primary: false, last_seen_at: ts, archived_at: null, metadata: {}, created_at: ts };
|
|
1171
1248
|
}
|
|
1172
1249
|
function getMachineId(db) {
|
|
1173
1250
|
if (_machineId)
|
|
@@ -1189,13 +1266,23 @@ function getMachineByName(name, db) {
|
|
|
1189
1266
|
const row = d.query("SELECT * FROM machines WHERE name = ?").get(name);
|
|
1190
1267
|
return row ? rowToMachine(row) : null;
|
|
1191
1268
|
}
|
|
1192
|
-
function listMachines(db) {
|
|
1269
|
+
function listMachines(db, includeArchived = false) {
|
|
1193
1270
|
const d = db || getDatabase();
|
|
1194
|
-
const
|
|
1271
|
+
const query = includeArchived ? "SELECT * FROM machines ORDER BY last_seen_at DESC" : "SELECT * FROM machines WHERE archived_at IS NULL ORDER BY last_seen_at DESC";
|
|
1272
|
+
const rows = d.query(query).all();
|
|
1195
1273
|
return rows.map(rowToMachine);
|
|
1196
1274
|
}
|
|
1197
1275
|
function deleteMachine(id, db) {
|
|
1198
1276
|
const d = db || getDatabase();
|
|
1277
|
+
const row = d.query("SELECT * FROM machines WHERE id = ?").get(id);
|
|
1278
|
+
if (!row)
|
|
1279
|
+
return false;
|
|
1280
|
+
if (row.is_primary)
|
|
1281
|
+
throw new Error("Cannot delete the primary machine");
|
|
1282
|
+
const activeCount = d.query("SELECT COUNT(*) as cnt FROM tasks WHERE machine_id = ? AND status IN ('pending', 'in_progress')").get(id);
|
|
1283
|
+
if (activeCount.cnt > 0) {
|
|
1284
|
+
throw new Error(`Cannot delete machine with ${activeCount.cnt} active/pending tasks`);
|
|
1285
|
+
}
|
|
1199
1286
|
const result = d.run("DELETE FROM machines WHERE id = ?", [id]);
|
|
1200
1287
|
return result.changes > 0;
|
|
1201
1288
|
}
|
|
@@ -1238,6 +1325,19 @@ var init_machines = __esm(() => {
|
|
|
1238
1325
|
});
|
|
1239
1326
|
|
|
1240
1327
|
// src/db/database.ts
|
|
1328
|
+
var exports_database = {};
|
|
1329
|
+
__export(exports_database, {
|
|
1330
|
+
uuid: () => uuid,
|
|
1331
|
+
resolvePartialId: () => resolvePartialId,
|
|
1332
|
+
resetDatabase: () => resetDatabase,
|
|
1333
|
+
now: () => now,
|
|
1334
|
+
lockExpiryCutoff: () => lockExpiryCutoff,
|
|
1335
|
+
isLockExpired: () => isLockExpired,
|
|
1336
|
+
getDatabase: () => getDatabase,
|
|
1337
|
+
closeDatabase: () => closeDatabase,
|
|
1338
|
+
clearExpiredLocks: () => clearExpiredLocks,
|
|
1339
|
+
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
1340
|
+
});
|
|
1241
1341
|
import { Database } from "bun:sqlite";
|
|
1242
1342
|
import { existsSync, mkdirSync } from "fs";
|
|
1243
1343
|
import { dirname, join, resolve } from "path";
|
|
@@ -1347,6 +1447,9 @@ function clearExpiredLocks(db) {
|
|
|
1347
1447
|
db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
|
|
1348
1448
|
}
|
|
1349
1449
|
function resolvePartialId(db, table, partialId) {
|
|
1450
|
+
if (!ALLOWED_TABLES.has(table)) {
|
|
1451
|
+
throw new Error(`Invalid table name: ${table}`);
|
|
1452
|
+
}
|
|
1350
1453
|
if (partialId.length >= 36) {
|
|
1351
1454
|
const row = db.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
|
|
1352
1455
|
return row?.id ?? null;
|
|
@@ -1376,10 +1479,11 @@ function resolvePartialId(db, table, partialId) {
|
|
|
1376
1479
|
}
|
|
1377
1480
|
return null;
|
|
1378
1481
|
}
|
|
1379
|
-
var LOCK_EXPIRY_MINUTES = 30, _db = null;
|
|
1482
|
+
var LOCK_EXPIRY_MINUTES = 30, _db = null, ALLOWED_TABLES;
|
|
1380
1483
|
var init_database = __esm(() => {
|
|
1381
1484
|
init_schema();
|
|
1382
1485
|
init_machines();
|
|
1486
|
+
ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates"]);
|
|
1383
1487
|
});
|
|
1384
1488
|
|
|
1385
1489
|
// src/types/index.ts
|
|
@@ -1612,231 +1716,572 @@ function getDueDispatches(db) {
|
|
|
1612
1716
|
ORDER BY created_at ASC`).all(now());
|
|
1613
1717
|
return rows.map(rowToDispatch);
|
|
1614
1718
|
}
|
|
1615
|
-
var init_dispatches = __esm(() => {
|
|
1616
|
-
init_database();
|
|
1617
|
-
init_types();
|
|
1618
|
-
});
|
|
1719
|
+
var init_dispatches = __esm(() => {
|
|
1720
|
+
init_database();
|
|
1721
|
+
init_types();
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
// src/sdk/types.ts
|
|
1725
|
+
class TodosAPIError extends Error {
|
|
1726
|
+
status;
|
|
1727
|
+
statusText;
|
|
1728
|
+
body;
|
|
1729
|
+
constructor(message, status, statusText, body) {
|
|
1730
|
+
super(message);
|
|
1731
|
+
this.status = status;
|
|
1732
|
+
this.statusText = statusText;
|
|
1733
|
+
this.body = body;
|
|
1734
|
+
this.name = "TodosAPIError";
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
class TodosNotFoundError extends TodosAPIError {
|
|
1739
|
+
constructor(message, body) {
|
|
1740
|
+
super(message, 404, "Not Found", body);
|
|
1741
|
+
this.name = "TodosNotFoundError";
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
class TodosConflictError extends TodosAPIError {
|
|
1746
|
+
constructor(message, body) {
|
|
1747
|
+
super(message, 409, "Conflict", body);
|
|
1748
|
+
this.name = "TodosConflictError";
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
class TodosUnauthorizedError extends TodosAPIError {
|
|
1753
|
+
constructor(message, body) {
|
|
1754
|
+
super(message, 401, "Unauthorized", body);
|
|
1755
|
+
this.name = "TodosUnauthorizedError";
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
class TodosRateLimitError extends TodosAPIError {
|
|
1760
|
+
retryAfter;
|
|
1761
|
+
constructor(message, retryAfter, body) {
|
|
1762
|
+
super(message, 429, "Too Many Requests", body);
|
|
1763
|
+
this.retryAfter = retryAfter;
|
|
1764
|
+
this.name = "TodosRateLimitError";
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
class TodosTimeoutError extends Error {
|
|
1769
|
+
ms;
|
|
1770
|
+
constructor(ms) {
|
|
1771
|
+
super(`Request timed out after ${ms}ms`);
|
|
1772
|
+
this.ms = ms;
|
|
1773
|
+
this.name = "TodosTimeoutError";
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/sdk/client.ts
|
|
1778
|
+
function buildQuery(params) {
|
|
1779
|
+
const search = new URLSearchParams;
|
|
1780
|
+
for (const [k, v] of Object.entries(params)) {
|
|
1781
|
+
if (v !== undefined)
|
|
1782
|
+
search.set(k, String(v));
|
|
1783
|
+
}
|
|
1784
|
+
const s = search.toString();
|
|
1785
|
+
return s ? `?${s}` : "";
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
class TasksResource {
|
|
1789
|
+
client;
|
|
1790
|
+
constructor(client) {
|
|
1791
|
+
this.client = client;
|
|
1792
|
+
}
|
|
1793
|
+
async list(options) {
|
|
1794
|
+
return this.client._get("/api/tasks", buildQuery(options || {}));
|
|
1795
|
+
}
|
|
1796
|
+
async get(id) {
|
|
1797
|
+
return this.client._get(`/api/tasks/${id}`);
|
|
1798
|
+
}
|
|
1799
|
+
async getWithRelations(id) {
|
|
1800
|
+
return this.client._get(`/api/tasks/${id}`);
|
|
1801
|
+
}
|
|
1802
|
+
async create(data) {
|
|
1803
|
+
return this.client._post("/api/tasks", data);
|
|
1804
|
+
}
|
|
1805
|
+
async update(id, data) {
|
|
1806
|
+
return this.client._patch(`/api/tasks/${id}`, data);
|
|
1807
|
+
}
|
|
1808
|
+
async delete(id) {
|
|
1809
|
+
return this.client._delete(`/api/tasks/${id}`);
|
|
1810
|
+
}
|
|
1811
|
+
async start(id, agentId) {
|
|
1812
|
+
return this.client._post(`/api/tasks/${id}/start`, { agent_id: agentId });
|
|
1813
|
+
}
|
|
1814
|
+
async complete(id, agentId) {
|
|
1815
|
+
return this.client._post(`/api/tasks/${id}/complete`, { agent_id: agentId });
|
|
1816
|
+
}
|
|
1817
|
+
async fail(id, options) {
|
|
1818
|
+
return this.client._post(`/api/tasks/${id}/fail`, options || {});
|
|
1819
|
+
}
|
|
1820
|
+
async logProgress(taskId, message, pctComplete, agentId) {
|
|
1821
|
+
return this.client._post(`/api/tasks/${taskId}/progress`, {
|
|
1822
|
+
message,
|
|
1823
|
+
pct_complete: pctComplete,
|
|
1824
|
+
agent_id: agentId
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
async getProgress(id) {
|
|
1828
|
+
return this.client._get(`/api/tasks/${id}/progress`);
|
|
1829
|
+
}
|
|
1830
|
+
async getHistory(id) {
|
|
1831
|
+
return this.client._get(`/api/tasks/${id}/history`);
|
|
1832
|
+
}
|
|
1833
|
+
async getAttachments(id) {
|
|
1834
|
+
return this.client._get(`/api/tasks/${id}/attachments`);
|
|
1835
|
+
}
|
|
1836
|
+
async status(options) {
|
|
1837
|
+
return this.client._get("/api/tasks/status", buildQuery(options || {}));
|
|
1838
|
+
}
|
|
1839
|
+
async next(options) {
|
|
1840
|
+
return this.client._get("/api/tasks/next", buildQuery(options || {}));
|
|
1841
|
+
}
|
|
1842
|
+
async active(projectId) {
|
|
1843
|
+
return this.client._get("/api/tasks/active", projectId ? `?project_id=${projectId}` : "");
|
|
1844
|
+
}
|
|
1845
|
+
async stale(options) {
|
|
1846
|
+
return this.client._get("/api/tasks/stale", buildQuery(options || {}));
|
|
1847
|
+
}
|
|
1848
|
+
async changedSince(since, projectId) {
|
|
1849
|
+
return this.client._get("/api/tasks/changed", buildQuery({ since, project_id: projectId }));
|
|
1850
|
+
}
|
|
1851
|
+
async context(options) {
|
|
1852
|
+
const q = buildQuery({
|
|
1853
|
+
agent_id: options?.agentId,
|
|
1854
|
+
project_id: options?.projectId,
|
|
1855
|
+
format: options?.format
|
|
1856
|
+
});
|
|
1857
|
+
const url = `${this.client.baseUrl}/api/tasks/context${q}`;
|
|
1858
|
+
const res = await this.client._fetchRaw(url);
|
|
1859
|
+
if (!res.ok)
|
|
1860
|
+
return options?.format === "json" ? {} : "";
|
|
1861
|
+
if (options?.format === "json")
|
|
1862
|
+
return res.json();
|
|
1863
|
+
return res.text();
|
|
1864
|
+
}
|
|
1865
|
+
async export(options) {
|
|
1866
|
+
const q = buildQuery(options || {});
|
|
1867
|
+
const url = `${this.client.baseUrl}/api/tasks/export${q}`;
|
|
1868
|
+
const res = await this.client._fetchRaw(url);
|
|
1869
|
+
if (!res.ok)
|
|
1870
|
+
throw new TodosAPIError("Export failed", res.status, res.statusText, null);
|
|
1871
|
+
if (options?.format === "csv")
|
|
1872
|
+
return res.text();
|
|
1873
|
+
return res.json();
|
|
1874
|
+
}
|
|
1875
|
+
async bulk(ids, action) {
|
|
1876
|
+
return this.client._post("/api/tasks/bulk", { ids, action });
|
|
1877
|
+
}
|
|
1878
|
+
async claim(agentId, projectId) {
|
|
1879
|
+
return this.client._post("/api/tasks/claim", { agent_id: agentId, project_id: projectId });
|
|
1880
|
+
}
|
|
1881
|
+
async* subscribe(options = {}) {
|
|
1882
|
+
const q = buildQuery({
|
|
1883
|
+
agent_id: options.agentId,
|
|
1884
|
+
project_id: options.projectId,
|
|
1885
|
+
events: options.events?.join(",")
|
|
1886
|
+
});
|
|
1887
|
+
const url = `${this.client.baseUrl}/api/tasks/stream${q}`;
|
|
1888
|
+
const resp = await fetch(url);
|
|
1889
|
+
if (!resp.ok || !resp.body)
|
|
1890
|
+
throw new Error(`SSE connection failed: ${resp.status}`);
|
|
1891
|
+
const reader = resp.body.getReader();
|
|
1892
|
+
const decoder = new TextDecoder;
|
|
1893
|
+
let buffer = "";
|
|
1894
|
+
try {
|
|
1895
|
+
while (true) {
|
|
1896
|
+
const { done, value } = await reader.read();
|
|
1897
|
+
if (done)
|
|
1898
|
+
break;
|
|
1899
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1900
|
+
const lines = buffer.split(`
|
|
1901
|
+
`);
|
|
1902
|
+
buffer = lines.pop() || "";
|
|
1903
|
+
for (const line of lines) {
|
|
1904
|
+
if (line.startsWith("data: ")) {
|
|
1905
|
+
try {
|
|
1906
|
+
const data = JSON.parse(line.slice(6));
|
|
1907
|
+
if (data.type !== "connected")
|
|
1908
|
+
yield data;
|
|
1909
|
+
} catch {}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
} finally {
|
|
1914
|
+
reader.releaseLock();
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
class AgentsResource {
|
|
1920
|
+
client;
|
|
1921
|
+
constructor(client) {
|
|
1922
|
+
this.client = client;
|
|
1923
|
+
}
|
|
1924
|
+
async list(options) {
|
|
1925
|
+
return this.client._get("/api/agents", buildQuery(options || {}));
|
|
1926
|
+
}
|
|
1927
|
+
async register(data) {
|
|
1928
|
+
return this.client._post("/api/agents", data);
|
|
1929
|
+
}
|
|
1930
|
+
async fullRegister(data) {
|
|
1931
|
+
return this.client._post("/api/agents", data);
|
|
1932
|
+
}
|
|
1933
|
+
async update(id, data) {
|
|
1934
|
+
return this.client._patch(`/api/agents/${id}`, data);
|
|
1935
|
+
}
|
|
1936
|
+
async delete(id) {
|
|
1937
|
+
return this.client._delete(`/api/agents/${id}`);
|
|
1938
|
+
}
|
|
1939
|
+
async bulkDelete(ids) {
|
|
1940
|
+
return this.client._post("/api/agents/bulk", { ids, action: "delete" });
|
|
1941
|
+
}
|
|
1942
|
+
async me(name) {
|
|
1943
|
+
return this.client._get("/api/agents/me", `?name=${encodeURIComponent(name)}`);
|
|
1944
|
+
}
|
|
1945
|
+
async queue(agentId) {
|
|
1946
|
+
return this.client._get(`/api/agents/${encodeURIComponent(agentId)}/queue`);
|
|
1947
|
+
}
|
|
1948
|
+
async team(agentId) {
|
|
1949
|
+
return this.client._get(`/api/agents/${encodeURIComponent(agentId)}/team`);
|
|
1950
|
+
}
|
|
1951
|
+
async orgChart() {
|
|
1952
|
+
return this.client._get("/api/org");
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
class ProjectsResource {
|
|
1957
|
+
client;
|
|
1958
|
+
constructor(client) {
|
|
1959
|
+
this.client = client;
|
|
1960
|
+
}
|
|
1961
|
+
async list(options) {
|
|
1962
|
+
return this.client._get("/api/projects", buildQuery(options || {}));
|
|
1963
|
+
}
|
|
1964
|
+
async create(data) {
|
|
1965
|
+
return this.client._post("/api/projects", data);
|
|
1966
|
+
}
|
|
1967
|
+
async delete(id) {
|
|
1968
|
+
return this.client._delete(`/api/projects/${id}`);
|
|
1969
|
+
}
|
|
1970
|
+
async bulkDelete(ids) {
|
|
1971
|
+
return this.client._post("/api/projects/bulk", { ids, action: "delete" });
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
class PlansResource {
|
|
1976
|
+
client;
|
|
1977
|
+
constructor(client) {
|
|
1978
|
+
this.client = client;
|
|
1979
|
+
}
|
|
1980
|
+
async list(projectId) {
|
|
1981
|
+
return this.client._get("/api/plans", projectId ? `?project_id=${projectId}` : "");
|
|
1982
|
+
}
|
|
1983
|
+
async create(data) {
|
|
1984
|
+
return this.client._post("/api/plans", data);
|
|
1985
|
+
}
|
|
1986
|
+
async get(id) {
|
|
1987
|
+
return this.client._get(`/api/plans/${id}`);
|
|
1988
|
+
}
|
|
1989
|
+
async update(id, data) {
|
|
1990
|
+
return this.client._patch(`/api/plans/${id}`, data);
|
|
1991
|
+
}
|
|
1992
|
+
async delete(id) {
|
|
1993
|
+
return this.client._delete(`/api/plans/${id}`);
|
|
1994
|
+
}
|
|
1995
|
+
async bulkDelete(ids) {
|
|
1996
|
+
return this.client._post("/api/plans/bulk", { ids, action: "delete" });
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
class OrgsResource {
|
|
2001
|
+
client;
|
|
2002
|
+
constructor(client) {
|
|
2003
|
+
this.client = client;
|
|
2004
|
+
}
|
|
2005
|
+
async list() {
|
|
2006
|
+
return this.client._get("/api/orgs");
|
|
2007
|
+
}
|
|
2008
|
+
async create(data) {
|
|
2009
|
+
return this.client._post("/api/orgs", data);
|
|
2010
|
+
}
|
|
2011
|
+
async update(id, data) {
|
|
2012
|
+
return this.client._patch(`/api/orgs/${id}`, data);
|
|
2013
|
+
}
|
|
2014
|
+
async delete(id) {
|
|
2015
|
+
return this.client._delete(`/api/orgs/${id}`);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
class WebhooksResource {
|
|
2020
|
+
client;
|
|
2021
|
+
constructor(client) {
|
|
2022
|
+
this.client = client;
|
|
2023
|
+
}
|
|
2024
|
+
async list() {
|
|
2025
|
+
return this.client._get("/api/webhooks");
|
|
2026
|
+
}
|
|
2027
|
+
async create(data) {
|
|
2028
|
+
return this.client._post("/api/webhooks", data);
|
|
2029
|
+
}
|
|
2030
|
+
async delete(id) {
|
|
2031
|
+
return this.client._delete(`/api/webhooks/${id}`);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
class TemplatesResource {
|
|
2036
|
+
client;
|
|
2037
|
+
constructor(client) {
|
|
2038
|
+
this.client = client;
|
|
2039
|
+
}
|
|
2040
|
+
async list() {
|
|
2041
|
+
return this.client._get("/api/templates");
|
|
2042
|
+
}
|
|
2043
|
+
async create(data) {
|
|
2044
|
+
return this.client._post("/api/templates", data);
|
|
2045
|
+
}
|
|
2046
|
+
async delete(id) {
|
|
2047
|
+
return this.client._delete(`/api/templates/${id}`);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
1619
2050
|
|
|
1620
|
-
// src/sdk.ts
|
|
1621
2051
|
class TodosClient {
|
|
1622
2052
|
baseUrl;
|
|
1623
2053
|
timeout;
|
|
2054
|
+
apiKey;
|
|
2055
|
+
maxRetries;
|
|
2056
|
+
retryDelay;
|
|
2057
|
+
tasks;
|
|
2058
|
+
agents;
|
|
2059
|
+
projects;
|
|
2060
|
+
plans;
|
|
2061
|
+
orgs;
|
|
2062
|
+
webhooks;
|
|
2063
|
+
templates;
|
|
1624
2064
|
constructor(options = {}) {
|
|
1625
2065
|
this.baseUrl = options.baseUrl || process.env["TODOS_URL"] || "http://localhost:19427";
|
|
1626
|
-
this.
|
|
2066
|
+
this.baseUrl = this.baseUrl.replace(/\/+$/, "");
|
|
2067
|
+
this.timeout = options.timeout ?? 1e4;
|
|
2068
|
+
this.apiKey = options.apiKey || process.env["TODOS_API_KEY"] || null;
|
|
2069
|
+
this.maxRetries = options.maxRetries ?? 0;
|
|
2070
|
+
this.retryDelay = options.retryDelay ?? 1000;
|
|
2071
|
+
this.tasks = new TasksResource(this);
|
|
2072
|
+
this.agents = new AgentsResource(this);
|
|
2073
|
+
this.projects = new ProjectsResource(this);
|
|
2074
|
+
this.plans = new PlansResource(this);
|
|
2075
|
+
this.orgs = new OrgsResource(this);
|
|
2076
|
+
this.webhooks = new WebhooksResource(this);
|
|
2077
|
+
this.templates = new TemplatesResource(this);
|
|
2078
|
+
}
|
|
2079
|
+
static fromEnv(apiKey) {
|
|
2080
|
+
return new TodosClient({ apiKey });
|
|
2081
|
+
}
|
|
2082
|
+
async _fetchRaw(url, init) {
|
|
2083
|
+
const controller = new AbortController;
|
|
2084
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
2085
|
+
try {
|
|
2086
|
+
const headers = this._buildHeaders(init?.headers);
|
|
2087
|
+
return await fetch(url, { ...init, headers, signal: controller.signal });
|
|
2088
|
+
} finally {
|
|
2089
|
+
clearTimeout(timer);
|
|
2090
|
+
}
|
|
1627
2091
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
2092
|
+
_buildHeaders(existing) {
|
|
2093
|
+
const headers = { "Content-Type": "application/json" };
|
|
2094
|
+
if (this.apiKey)
|
|
2095
|
+
headers["x-api-key"] = this.apiKey;
|
|
2096
|
+
if (existing) {
|
|
2097
|
+
if (existing instanceof Headers) {
|
|
2098
|
+
existing.forEach((v, k) => {
|
|
2099
|
+
headers[k] = v;
|
|
2100
|
+
});
|
|
2101
|
+
} else if (Array.isArray(existing)) {
|
|
2102
|
+
for (const [k, v] of existing)
|
|
2103
|
+
headers[k] = v;
|
|
2104
|
+
} else {
|
|
2105
|
+
Object.assign(headers, existing);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
return headers;
|
|
2109
|
+
}
|
|
2110
|
+
async _fetchWithRetry(path, init) {
|
|
2111
|
+
let lastError = null;
|
|
2112
|
+
const maxAttempts = this.maxRetries + 1;
|
|
2113
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
2114
|
+
try {
|
|
2115
|
+
return await this._fetch(path, init);
|
|
2116
|
+
} catch (e) {
|
|
2117
|
+
lastError = e;
|
|
2118
|
+
if (e instanceof TodosAPIError && e.status < 500 && e.status !== 429)
|
|
2119
|
+
throw e;
|
|
2120
|
+
if (e instanceof TodosUnauthorizedError || e instanceof TodosNotFoundError || e instanceof TodosConflictError)
|
|
2121
|
+
throw e;
|
|
2122
|
+
if (attempt < maxAttempts - 1) {
|
|
2123
|
+
const delay = this.retryDelay * Math.pow(2, attempt);
|
|
2124
|
+
if (e instanceof TodosRateLimitError) {
|
|
2125
|
+
await this._sleep(e.retryAfter * 1000);
|
|
2126
|
+
} else {
|
|
2127
|
+
await this._sleep(delay);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
throw lastError || new Error("Request failed after retries");
|
|
1630
2133
|
}
|
|
1631
|
-
async
|
|
2134
|
+
async _fetch(path, init) {
|
|
1632
2135
|
const controller = new AbortController;
|
|
1633
2136
|
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
1634
2137
|
try {
|
|
1635
|
-
const
|
|
2138
|
+
const url = `${this.baseUrl}${path}`;
|
|
2139
|
+
const headers = this._buildHeaders(init?.headers);
|
|
2140
|
+
const res = await fetch(url, { ...init, headers, signal: controller.signal });
|
|
1636
2141
|
if (!res.ok) {
|
|
1637
|
-
const
|
|
1638
|
-
|
|
2142
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
2143
|
+
const message = body.error || `HTTP ${res.status}: ${res.statusText}`;
|
|
2144
|
+
if (res.status === 401)
|
|
2145
|
+
throw new TodosUnauthorizedError(message, body);
|
|
2146
|
+
if (res.status === 404)
|
|
2147
|
+
throw new TodosNotFoundError(message, body);
|
|
2148
|
+
if (res.status === 409)
|
|
2149
|
+
throw new TodosConflictError(message, body);
|
|
2150
|
+
if (res.status === 429) {
|
|
2151
|
+
const retryAfter = parseInt(res.headers.get("retry-after") || "60", 10);
|
|
2152
|
+
throw new TodosRateLimitError(message, retryAfter, body);
|
|
2153
|
+
}
|
|
2154
|
+
throw new TodosAPIError(message, res.status, res.statusText, body);
|
|
1639
2155
|
}
|
|
2156
|
+
const contentLength = res.headers.get("content-length");
|
|
2157
|
+
if (contentLength === "0" || contentLength === "4")
|
|
2158
|
+
return null;
|
|
1640
2159
|
return res.json();
|
|
2160
|
+
} catch (e) {
|
|
2161
|
+
if (e instanceof DOMException && e.name === "AbortError") {
|
|
2162
|
+
throw new TodosTimeoutError(this.timeout);
|
|
2163
|
+
}
|
|
2164
|
+
throw e;
|
|
1641
2165
|
} finally {
|
|
1642
2166
|
clearTimeout(timer);
|
|
1643
2167
|
}
|
|
1644
2168
|
}
|
|
2169
|
+
async _get(path, query = "") {
|
|
2170
|
+
return this._fetchWithRetry(`${path}${query}`);
|
|
2171
|
+
}
|
|
2172
|
+
async _post(path, body) {
|
|
2173
|
+
return this._fetchWithRetry(path, {
|
|
2174
|
+
method: "POST",
|
|
2175
|
+
body: body ? JSON.stringify(body) : undefined
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
async _patch(path, body) {
|
|
2179
|
+
return this._fetchWithRetry(path, {
|
|
2180
|
+
method: "PATCH",
|
|
2181
|
+
body: JSON.stringify(body)
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
async _delete(path) {
|
|
2185
|
+
return this._fetchWithRetry(path, { method: "DELETE" });
|
|
2186
|
+
}
|
|
2187
|
+
_sleep(ms) {
|
|
2188
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2189
|
+
}
|
|
1645
2190
|
async getHealth() {
|
|
1646
|
-
return this.
|
|
2191
|
+
return this._get("/api/health");
|
|
1647
2192
|
}
|
|
1648
2193
|
async isAlive() {
|
|
1649
2194
|
try {
|
|
1650
|
-
await this.
|
|
2195
|
+
await this._get("/api/stats");
|
|
1651
2196
|
return true;
|
|
1652
2197
|
} catch {
|
|
1653
2198
|
return false;
|
|
1654
2199
|
}
|
|
1655
2200
|
}
|
|
1656
|
-
async
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
2201
|
+
async getStats() {
|
|
2202
|
+
return this._get("/api/stats");
|
|
2203
|
+
}
|
|
2204
|
+
async getReport(options) {
|
|
2205
|
+
return this._get("/api/report", buildQuery({ days: options?.days, project_id: options?.projectId }));
|
|
2206
|
+
}
|
|
2207
|
+
async doctor() {
|
|
2208
|
+
return this._get("/api/doctor");
|
|
2209
|
+
}
|
|
2210
|
+
async activity(limit) {
|
|
2211
|
+
return this._get("/api/activity", limit ? `?limit=${limit}` : "");
|
|
1663
2212
|
}
|
|
1664
2213
|
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}`);
|
|
2214
|
+
return this.tasks.list(filter);
|
|
1671
2215
|
}
|
|
1672
2216
|
async getTask(id) {
|
|
1673
|
-
return this.
|
|
2217
|
+
return this.tasks.get(id);
|
|
1674
2218
|
}
|
|
1675
|
-
async
|
|
1676
|
-
return this.
|
|
2219
|
+
async createTask(data) {
|
|
2220
|
+
return this.tasks.create(data);
|
|
1677
2221
|
}
|
|
1678
|
-
async
|
|
1679
|
-
return this.
|
|
2222
|
+
async updateTask(id, data) {
|
|
2223
|
+
return this.tasks.update(id, data);
|
|
1680
2224
|
}
|
|
1681
|
-
async
|
|
1682
|
-
|
|
2225
|
+
async deleteTask(id) {
|
|
2226
|
+
await this.tasks.delete(id);
|
|
1683
2227
|
}
|
|
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
|
-
});
|
|
2228
|
+
async startTask(id, agentId) {
|
|
2229
|
+
return this.tasks.start(id, agentId);
|
|
1690
2230
|
}
|
|
1691
2231
|
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
|
-
});
|
|
2232
|
+
return this.tasks.complete(id, agentId);
|
|
1704
2233
|
}
|
|
1705
|
-
async
|
|
1706
|
-
return this.
|
|
1707
|
-
method: "PATCH",
|
|
1708
|
-
headers: { "Content-Type": "application/json" },
|
|
1709
|
-
body: JSON.stringify(data)
|
|
1710
|
-
});
|
|
2234
|
+
async failTask(id, options = {}) {
|
|
2235
|
+
return this.tasks.fail(id, options);
|
|
1711
2236
|
}
|
|
1712
|
-
async
|
|
1713
|
-
|
|
2237
|
+
async logProgress(taskId, message, pctComplete, agentId) {
|
|
2238
|
+
return this.tasks.logProgress(taskId, message, pctComplete, agentId);
|
|
1714
2239
|
}
|
|
1715
|
-
async
|
|
1716
|
-
return this.
|
|
2240
|
+
async getStatus(projectId, agentId) {
|
|
2241
|
+
return this.tasks.status({ project_id: projectId, agent_id: agentId });
|
|
1717
2242
|
}
|
|
1718
2243
|
async getActiveWork(projectId) {
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
params.set("project_id", projectId);
|
|
1722
|
-
return this.fetch(`/api/tasks/active?${params}`);
|
|
2244
|
+
const res = await this.tasks.active(projectId);
|
|
2245
|
+
return res.active;
|
|
1723
2246
|
}
|
|
1724
2247
|
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
|
-
});
|
|
2248
|
+
return this.tasks.changedSince(since, projectId);
|
|
1736
2249
|
}
|
|
1737
|
-
async
|
|
1738
|
-
return this.
|
|
1739
|
-
method: "POST",
|
|
1740
|
-
headers: { "Content-Type": "application/json" },
|
|
1741
|
-
body: JSON.stringify(options)
|
|
1742
|
-
});
|
|
2250
|
+
async getStaleTasks(minutes, projectId) {
|
|
2251
|
+
return this.tasks.stale({ minutes, project_id: projectId });
|
|
1743
2252
|
}
|
|
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
|
-
});
|
|
2253
|
+
async getContext(options = {}) {
|
|
2254
|
+
return this.tasks.context(options);
|
|
1750
2255
|
}
|
|
1751
2256
|
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}`);
|
|
2257
|
+
return this.tasks.export(filter);
|
|
1761
2258
|
}
|
|
1762
|
-
async
|
|
1763
|
-
return this.
|
|
2259
|
+
async claimNextTask(agentId, projectId) {
|
|
2260
|
+
return this.tasks.claim(agentId, projectId);
|
|
1764
2261
|
}
|
|
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
|
-
}
|
|
2262
|
+
async getTaskHistory(id) {
|
|
2263
|
+
return this.tasks.getHistory(id);
|
|
2264
|
+
}
|
|
2265
|
+
async getTaskAttachments(id) {
|
|
2266
|
+
return this.tasks.getAttachments(id);
|
|
1788
2267
|
}
|
|
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}`);
|
|
2268
|
+
async getTaskProgress(id) {
|
|
2269
|
+
return this.tasks.getProgress(id);
|
|
1796
2270
|
}
|
|
1797
2271
|
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
|
-
}
|
|
2272
|
+
yield* this.tasks.subscribe(options);
|
|
2273
|
+
}
|
|
2274
|
+
async getProjects() {
|
|
2275
|
+
return this.projects.list();
|
|
1830
2276
|
}
|
|
1831
2277
|
}
|
|
1832
2278
|
function createClient(options) {
|
|
1833
2279
|
return new TodosClient(options);
|
|
1834
2280
|
}
|
|
1835
|
-
|
|
1836
2281
|
// src/index.ts
|
|
1837
2282
|
init_database();
|
|
1838
2283
|
|
|
1839
|
-
// src/db/
|
|
2284
|
+
// src/db/task-crud.ts
|
|
1840
2285
|
init_types();
|
|
1841
2286
|
init_database();
|
|
1842
2287
|
|
|
@@ -2207,184 +2652,111 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
|
|
|
2207
2652
|
const remaining = Math.ceil(config.min_work_seconds - elapsedSeconds);
|
|
2208
2653
|
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
2654
|
}
|
|
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);
|
|
2655
|
+
}
|
|
2656
|
+
if (agent && config.max_completions_per_window && config.window_minutes) {
|
|
2657
|
+
const windowStart = new Date(Date.now() - config.window_minutes * 60 * 1000).toISOString();
|
|
2658
|
+
const result = db.query(`SELECT COUNT(*) as count FROM tasks
|
|
2659
|
+
WHERE completed_at > ? AND (assigned_to = ? OR agent_id = ?)`).get(windowStart, agent, agent);
|
|
2660
|
+
if (result.count >= config.max_completions_per_window) {
|
|
2661
|
+
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
2662
|
}
|
|
2352
|
-
return next.toISOString();
|
|
2353
2663
|
}
|
|
2354
|
-
if (
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
daysToAdd = diff;
|
|
2664
|
+
if (agent && config.cooldown_seconds) {
|
|
2665
|
+
const result = db.query(`SELECT MAX(completed_at) as last_completed FROM tasks
|
|
2666
|
+
WHERE completed_at IS NOT NULL AND (assigned_to = ? OR agent_id = ?) AND id != ?`).get(agent, agent, task.id);
|
|
2667
|
+
if (result.last_completed) {
|
|
2668
|
+
const elapsedSeconds = (Date.now() - new Date(result.last_completed).getTime()) / 1000;
|
|
2669
|
+
if (elapsedSeconds < config.cooldown_seconds) {
|
|
2670
|
+
const remaining = Math.ceil(config.cooldown_seconds - elapsedSeconds);
|
|
2671
|
+
throw new CompletionGuardError(`Cooldown: last completion was ${Math.floor(elapsedSeconds)}s ago. Wait ${remaining}s between completions.`, remaining);
|
|
2672
|
+
}
|
|
2364
2673
|
}
|
|
2365
|
-
const next = new Date(base);
|
|
2366
|
-
next.setDate(next.getDate() + daysToAdd);
|
|
2367
|
-
return next.toISOString();
|
|
2368
2674
|
}
|
|
2369
|
-
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// src/db/audit.ts
|
|
2678
|
+
init_database();
|
|
2679
|
+
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
2680
|
+
const d = db || getDatabase();
|
|
2681
|
+
const id = uuid();
|
|
2682
|
+
const timestamp = now();
|
|
2683
|
+
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
|
|
2684
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
|
|
2685
|
+
return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
|
|
2686
|
+
}
|
|
2687
|
+
function getTaskHistory(taskId, db) {
|
|
2688
|
+
const d = db || getDatabase();
|
|
2689
|
+
return d.query("SELECT * FROM task_history WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
|
|
2690
|
+
}
|
|
2691
|
+
function getRecentActivity(limit = 50, db) {
|
|
2692
|
+
const d = db || getDatabase();
|
|
2693
|
+
return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
2694
|
+
}
|
|
2695
|
+
function getRecap(hours = 8, projectId, db) {
|
|
2696
|
+
const d = db || getDatabase();
|
|
2697
|
+
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
2698
|
+
const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
2699
|
+
const pf = projectId ? " AND project_id = ?" : "";
|
|
2700
|
+
const tpf = projectId ? " AND t.project_id = ?" : "";
|
|
2701
|
+
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);
|
|
2702
|
+
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);
|
|
2703
|
+
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();
|
|
2704
|
+
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();
|
|
2705
|
+
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);
|
|
2706
|
+
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);
|
|
2707
|
+
return {
|
|
2708
|
+
hours,
|
|
2709
|
+
since,
|
|
2710
|
+
completed: completed.map((r) => ({
|
|
2711
|
+
...r,
|
|
2712
|
+
duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
|
|
2713
|
+
})),
|
|
2714
|
+
created,
|
|
2715
|
+
in_progress,
|
|
2716
|
+
blocked,
|
|
2717
|
+
stale,
|
|
2718
|
+
agents
|
|
2719
|
+
};
|
|
2370
2720
|
}
|
|
2371
2721
|
|
|
2372
2722
|
// src/db/webhooks.ts
|
|
2373
2723
|
init_database();
|
|
2374
2724
|
var MAX_RETRY_ATTEMPTS = 3;
|
|
2375
2725
|
var RETRY_BASE_DELAY_MS = 1000;
|
|
2726
|
+
function isPrivateOrInternal(ip) {
|
|
2727
|
+
const parts = ip.split(".").map(Number);
|
|
2728
|
+
if (parts.length !== 4)
|
|
2729
|
+
return true;
|
|
2730
|
+
const a = parts[0];
|
|
2731
|
+
const b = parts[1];
|
|
2732
|
+
if (parts.some((p) => isNaN(p) || p < 0 || p > 255))
|
|
2733
|
+
return true;
|
|
2734
|
+
if (a === 10)
|
|
2735
|
+
return true;
|
|
2736
|
+
if (a === 127)
|
|
2737
|
+
return true;
|
|
2738
|
+
if (a === 169 && b === 254)
|
|
2739
|
+
return true;
|
|
2740
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
2741
|
+
return true;
|
|
2742
|
+
if (a === 192 && b === 168)
|
|
2743
|
+
return true;
|
|
2744
|
+
if (a === 0)
|
|
2745
|
+
return true;
|
|
2746
|
+
return false;
|
|
2747
|
+
}
|
|
2376
2748
|
function validateWebhookUrl(urlString) {
|
|
2377
2749
|
try {
|
|
2378
2750
|
const url = new URL(urlString);
|
|
2379
2751
|
if (url.protocol !== "https:") {
|
|
2380
|
-
|
|
2752
|
+
return { valid: false, error: "Webhook URLs must use HTTPS" };
|
|
2381
2753
|
}
|
|
2382
2754
|
const hostname = url.hostname.toLowerCase();
|
|
2383
2755
|
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0") {
|
|
2384
|
-
|
|
2756
|
+
return { valid: false, error: "Webhook URLs cannot target localhost" };
|
|
2385
2757
|
}
|
|
2386
2758
|
if (hostname === "169.254.169.254" || hostname.startsWith("169.254.")) {
|
|
2387
|
-
|
|
2759
|
+
return { valid: false, error: "Webhook URLs cannot target cloud metadata endpoints" };
|
|
2388
2760
|
}
|
|
2389
2761
|
const privateRanges = [
|
|
2390
2762
|
/^10\./,
|
|
@@ -2397,1008 +2769,1308 @@ function validateWebhookUrl(urlString) {
|
|
|
2397
2769
|
];
|
|
2398
2770
|
for (const range of privateRanges) {
|
|
2399
2771
|
if (range.test(hostname)) {
|
|
2400
|
-
|
|
2772
|
+
return { valid: false, error: "Webhook URLs cannot target private IP ranges" };
|
|
2401
2773
|
}
|
|
2402
2774
|
}
|
|
2775
|
+
return { valid: true };
|
|
2403
2776
|
} catch (e) {
|
|
2404
2777
|
if (e instanceof Error && e.message.startsWith("Webhook URLs")) {
|
|
2405
|
-
|
|
2778
|
+
return { valid: false, error: e.message };
|
|
2779
|
+
}
|
|
2780
|
+
return { valid: false, error: `Invalid webhook URL: ${urlString}` };
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
async function resolveAndCheckIp(hostname) {
|
|
2784
|
+
try {
|
|
2785
|
+
const resolved = await Bun.dns.lookup(hostname);
|
|
2786
|
+
if (!resolved)
|
|
2787
|
+
return { allowed: false, error: `Could not resolve hostname: ${hostname}` };
|
|
2788
|
+
const addresses = Array.isArray(resolved) ? resolved : [resolved];
|
|
2789
|
+
for (const addr of addresses) {
|
|
2790
|
+
const ip = typeof addr === "string" ? addr : addr.address;
|
|
2791
|
+
if (isPrivateOrInternal(ip)) {
|
|
2792
|
+
return { allowed: false, error: `Hostname ${hostname} resolves to blocked address ${ip}` };
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
const first = addresses[0];
|
|
2796
|
+
return { allowed: true, ip: typeof first === "string" ? first : first?.address ?? "" };
|
|
2797
|
+
} catch {
|
|
2798
|
+
return { allowed: true, ip: "" };
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
var activeDeliveries = 0;
|
|
2802
|
+
var MAX_CONCURRENT_DELIVERIES = 20;
|
|
2803
|
+
function rowToWebhook(row) {
|
|
2804
|
+
return {
|
|
2805
|
+
...row,
|
|
2806
|
+
events: JSON.parse(row.events || "[]"),
|
|
2807
|
+
active: !!row.active,
|
|
2808
|
+
project_id: row.project_id || null,
|
|
2809
|
+
task_list_id: row.task_list_id || null,
|
|
2810
|
+
agent_id: row.agent_id || null,
|
|
2811
|
+
task_id: row.task_id || null
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
function createWebhook(input, db) {
|
|
2815
|
+
const urlValidation = validateWebhookUrl(input.url);
|
|
2816
|
+
if (!urlValidation.valid) {
|
|
2817
|
+
throw new Error(`Invalid webhook URL: ${urlValidation.error}`);
|
|
2818
|
+
}
|
|
2819
|
+
const d = db || getDatabase();
|
|
2820
|
+
const id = uuid();
|
|
2821
|
+
d.run(`INSERT INTO webhooks (id, url, events, secret, project_id, task_list_id, agent_id, task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2822
|
+
id,
|
|
2823
|
+
input.url,
|
|
2824
|
+
JSON.stringify(input.events || []),
|
|
2825
|
+
input.secret || null,
|
|
2826
|
+
input.project_id || null,
|
|
2827
|
+
input.task_list_id || null,
|
|
2828
|
+
input.agent_id || null,
|
|
2829
|
+
input.task_id || null,
|
|
2830
|
+
now()
|
|
2831
|
+
]);
|
|
2832
|
+
return getWebhook(id, d);
|
|
2833
|
+
}
|
|
2834
|
+
function getWebhook(id, db) {
|
|
2835
|
+
const d = db || getDatabase();
|
|
2836
|
+
const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
|
|
2837
|
+
return row ? rowToWebhook(row) : null;
|
|
2838
|
+
}
|
|
2839
|
+
function listWebhooks(db) {
|
|
2840
|
+
const d = db || getDatabase();
|
|
2841
|
+
return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
|
|
2842
|
+
}
|
|
2843
|
+
function deleteWebhook(id, db) {
|
|
2844
|
+
const d = db || getDatabase();
|
|
2845
|
+
return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
|
|
2846
|
+
}
|
|
2847
|
+
function listDeliveries(webhookId, limit = 50, db) {
|
|
2848
|
+
const d = db || getDatabase();
|
|
2849
|
+
if (webhookId) {
|
|
2850
|
+
return d.query("SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY created_at DESC LIMIT ?").all(webhookId, limit);
|
|
2851
|
+
}
|
|
2852
|
+
return d.query("SELECT * FROM webhook_deliveries ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
2853
|
+
}
|
|
2854
|
+
function logDelivery(d, webhookId, event, payload, statusCode, response, attempt) {
|
|
2855
|
+
const id = uuid();
|
|
2856
|
+
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()]);
|
|
2857
|
+
}
|
|
2858
|
+
function matchesScope(wh, payload) {
|
|
2859
|
+
if (wh.project_id && payload.project_id !== wh.project_id)
|
|
2860
|
+
return false;
|
|
2861
|
+
if (wh.task_list_id && payload.task_list_id !== wh.task_list_id)
|
|
2862
|
+
return false;
|
|
2863
|
+
if (wh.agent_id && payload.agent_id !== wh.agent_id && payload.assigned_to !== wh.agent_id)
|
|
2864
|
+
return false;
|
|
2865
|
+
if (wh.task_id && payload.id !== wh.task_id)
|
|
2866
|
+
return false;
|
|
2867
|
+
return true;
|
|
2868
|
+
}
|
|
2869
|
+
async function deliverWebhook(wh, event, body, attempt, db) {
|
|
2870
|
+
try {
|
|
2871
|
+
const url = new URL(wh.url);
|
|
2872
|
+
const hostname = url.hostname.toLowerCase();
|
|
2873
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
|
|
2874
|
+
logDelivery(db, wh.id, event, body, null, "Blocked: webhook URL points to localhost", attempt);
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
const ipCheck = await resolveAndCheckIp(hostname);
|
|
2878
|
+
if (!ipCheck.allowed) {
|
|
2879
|
+
logDelivery(db, wh.id, event, body, null, `Blocked: ${ipCheck.error}`, attempt);
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
} catch {
|
|
2883
|
+
logDelivery(db, wh.id, event, body, null, `Invalid URL at delivery time: ${wh.url}`, attempt);
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
if (activeDeliveries >= MAX_CONCURRENT_DELIVERIES) {
|
|
2887
|
+
logDelivery(db, wh.id, event, body, null, "Dropped: too many concurrent deliveries", attempt);
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
activeDeliveries++;
|
|
2891
|
+
try {
|
|
2892
|
+
const headers = { "Content-Type": "application/json" };
|
|
2893
|
+
if (wh.secret) {
|
|
2894
|
+
const encoder = new TextEncoder;
|
|
2895
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
2896
|
+
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
2897
|
+
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2898
|
+
}
|
|
2899
|
+
const resp = await fetch(wh.url, { method: "POST", headers, body });
|
|
2900
|
+
const respText = await resp.text().catch(() => "");
|
|
2901
|
+
logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
|
|
2902
|
+
if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
|
|
2903
|
+
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
2904
|
+
setTimeout(() => {
|
|
2905
|
+
deliverWebhook(wh, event, body, attempt + 1, db).catch((retryErr) => {
|
|
2906
|
+
console.error(`[webhook] Retry failed for webhook ${wh.id}:`, retryErr);
|
|
2907
|
+
});
|
|
2908
|
+
}, delay);
|
|
2909
|
+
}
|
|
2910
|
+
} catch (err) {
|
|
2911
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2912
|
+
logDelivery(db, wh.id, event, body, null, errorMsg.slice(0, 1000), attempt);
|
|
2913
|
+
console.error(`[webhook] Delivery failed for webhook ${wh.id} (attempt ${attempt}):`, errorMsg);
|
|
2914
|
+
if (attempt < MAX_RETRY_ATTEMPTS) {
|
|
2915
|
+
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
2916
|
+
setTimeout(() => {
|
|
2917
|
+
deliverWebhook(wh, event, body, attempt + 1, db).catch((retryErr) => {
|
|
2918
|
+
console.error(`[webhook] Retry failed for webhook ${wh.id}:`, retryErr);
|
|
2919
|
+
});
|
|
2920
|
+
}, delay);
|
|
2406
2921
|
}
|
|
2407
|
-
|
|
2922
|
+
} finally {
|
|
2923
|
+
activeDeliveries--;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
async function dispatchWebhook(event, payload, db) {
|
|
2927
|
+
const d = db || getDatabase();
|
|
2928
|
+
const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
|
|
2929
|
+
const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
|
|
2930
|
+
for (const wh of webhooks) {
|
|
2931
|
+
if (!matchesScope(wh, payloadObj))
|
|
2932
|
+
continue;
|
|
2933
|
+
const body = JSON.stringify({ event, payload, timestamp: now() });
|
|
2934
|
+
deliverWebhook(wh, event, body, 1, d).catch((err) => {
|
|
2935
|
+
console.error(`[webhook] Dispatch failed for webhook ${wh.id}:`, err);
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
// src/db/checklists.ts
|
|
2941
|
+
init_database();
|
|
2942
|
+
function rowToItem(row) {
|
|
2943
|
+
return { ...row, checked: !!row.checked };
|
|
2944
|
+
}
|
|
2945
|
+
function getChecklist(taskId, db) {
|
|
2946
|
+
const d = db || getDatabase();
|
|
2947
|
+
const rows = d.query("SELECT * FROM task_checklists WHERE task_id = ? ORDER BY position, created_at").all(taskId);
|
|
2948
|
+
return rows.map(rowToItem);
|
|
2949
|
+
}
|
|
2950
|
+
function addChecklistItem(input, db) {
|
|
2951
|
+
const d = db || getDatabase();
|
|
2952
|
+
const id = uuid();
|
|
2953
|
+
const timestamp = now();
|
|
2954
|
+
let position = input.position;
|
|
2955
|
+
if (position === undefined) {
|
|
2956
|
+
const maxRow = d.query("SELECT MAX(position) as max_pos FROM task_checklists WHERE task_id = ?").get(input.task_id);
|
|
2957
|
+
position = (maxRow?.max_pos ?? -1) + 1;
|
|
2408
2958
|
}
|
|
2959
|
+
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]);
|
|
2960
|
+
return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
|
|
2961
|
+
}
|
|
2962
|
+
function checkChecklistItem(id, checked, db) {
|
|
2963
|
+
const d = db || getDatabase();
|
|
2964
|
+
const timestamp = now();
|
|
2965
|
+
const result = d.run("UPDATE task_checklists SET checked = ?, updated_at = ? WHERE id = ?", [checked ? 1 : 0, timestamp, id]);
|
|
2966
|
+
if (result.changes === 0)
|
|
2967
|
+
return null;
|
|
2968
|
+
return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
|
|
2969
|
+
}
|
|
2970
|
+
function updateChecklistItemText(id, text, db) {
|
|
2971
|
+
const d = db || getDatabase();
|
|
2972
|
+
const timestamp = now();
|
|
2973
|
+
const result = d.run("UPDATE task_checklists SET text = ?, updated_at = ? WHERE id = ?", [text, timestamp, id]);
|
|
2974
|
+
if (result.changes === 0)
|
|
2975
|
+
return null;
|
|
2976
|
+
return rowToItem(d.query("SELECT * FROM task_checklists WHERE id = ?").get(id));
|
|
2977
|
+
}
|
|
2978
|
+
function removeChecklistItem(id, db) {
|
|
2979
|
+
const d = db || getDatabase();
|
|
2980
|
+
const result = d.run("DELETE FROM task_checklists WHERE id = ?", [id]);
|
|
2981
|
+
return result.changes > 0;
|
|
2982
|
+
}
|
|
2983
|
+
function clearChecklist(taskId, db) {
|
|
2984
|
+
const d = db || getDatabase();
|
|
2985
|
+
const result = d.run("DELETE FROM task_checklists WHERE task_id = ?", [taskId]);
|
|
2986
|
+
return result.changes;
|
|
2987
|
+
}
|
|
2988
|
+
function getChecklistStats(taskId, db) {
|
|
2989
|
+
const d = db || getDatabase();
|
|
2990
|
+
const row = d.query("SELECT COUNT(*) as total, SUM(checked) as checked FROM task_checklists WHERE task_id = ?").get(taskId);
|
|
2991
|
+
return { total: row?.total ?? 0, checked: row?.checked ?? 0 };
|
|
2409
2992
|
}
|
|
2410
|
-
|
|
2993
|
+
|
|
2994
|
+
// src/db/task-crud.ts
|
|
2995
|
+
function rowToTask(row) {
|
|
2411
2996
|
return {
|
|
2412
2997
|
...row,
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
task_id: row.task_id || null
|
|
2998
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
2999
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
3000
|
+
status: row.status,
|
|
3001
|
+
priority: row.priority,
|
|
3002
|
+
requires_approval: !!row.requires_approval
|
|
2419
3003
|
};
|
|
2420
3004
|
}
|
|
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);
|
|
3005
|
+
function insertTaskTags(taskId, tags, db) {
|
|
3006
|
+
if (tags.length === 0)
|
|
3007
|
+
return;
|
|
3008
|
+
const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
3009
|
+
for (const tag of tags) {
|
|
3010
|
+
if (tag)
|
|
3011
|
+
stmt.run(taskId, tag);
|
|
3012
|
+
}
|
|
2437
3013
|
}
|
|
2438
|
-
function
|
|
3014
|
+
function replaceTaskTags(taskId, tags, db) {
|
|
3015
|
+
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
3016
|
+
insertTaskTags(taskId, tags, db);
|
|
3017
|
+
}
|
|
3018
|
+
function createTask(input, db) {
|
|
2439
3019
|
const d = db || getDatabase();
|
|
2440
|
-
const
|
|
2441
|
-
|
|
3020
|
+
const timestamp = now();
|
|
3021
|
+
const tags = input.tags || [];
|
|
3022
|
+
const assignedBy = input.assigned_by || input.agent_id;
|
|
3023
|
+
const assignedFromProject = input.assigned_from_project || null;
|
|
3024
|
+
let id = uuid();
|
|
3025
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
3026
|
+
try {
|
|
3027
|
+
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)
|
|
3028
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3029
|
+
id,
|
|
3030
|
+
null,
|
|
3031
|
+
input.project_id || null,
|
|
3032
|
+
input.parent_id || null,
|
|
3033
|
+
input.plan_id || null,
|
|
3034
|
+
input.task_list_id || null,
|
|
3035
|
+
input.title,
|
|
3036
|
+
input.description || null,
|
|
3037
|
+
input.status || "pending",
|
|
3038
|
+
input.priority || "medium",
|
|
3039
|
+
input.agent_id || null,
|
|
3040
|
+
input.assigned_to || null,
|
|
3041
|
+
input.session_id || null,
|
|
3042
|
+
input.working_dir || null,
|
|
3043
|
+
JSON.stringify(tags),
|
|
3044
|
+
JSON.stringify(input.metadata || {}),
|
|
3045
|
+
timestamp,
|
|
3046
|
+
timestamp,
|
|
3047
|
+
input.due_at || null,
|
|
3048
|
+
input.estimated_minutes || null,
|
|
3049
|
+
input.requires_approval ? 1 : 0,
|
|
3050
|
+
null,
|
|
3051
|
+
null,
|
|
3052
|
+
input.recurrence_rule || null,
|
|
3053
|
+
input.recurrence_parent_id || null,
|
|
3054
|
+
input.spawns_template_id || null,
|
|
3055
|
+
input.reason || null,
|
|
3056
|
+
input.spawned_from_session || null,
|
|
3057
|
+
assignedBy || null,
|
|
3058
|
+
assignedFromProject || null,
|
|
3059
|
+
input.task_type || null
|
|
3060
|
+
]);
|
|
3061
|
+
break;
|
|
3062
|
+
} catch (e) {
|
|
3063
|
+
if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
|
|
3064
|
+
id = uuid();
|
|
3065
|
+
continue;
|
|
3066
|
+
}
|
|
3067
|
+
throw e;
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
if (tags.length > 0) {
|
|
3071
|
+
insertTaskTags(id, tags, d);
|
|
3072
|
+
}
|
|
3073
|
+
const task = getTask(id, d);
|
|
3074
|
+
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(() => {});
|
|
3075
|
+
return task;
|
|
2442
3076
|
}
|
|
2443
|
-
function
|
|
3077
|
+
function getTask(id, db) {
|
|
2444
3078
|
const d = db || getDatabase();
|
|
2445
|
-
|
|
3079
|
+
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
3080
|
+
if (!row)
|
|
3081
|
+
return null;
|
|
3082
|
+
return rowToTask(row);
|
|
2446
3083
|
}
|
|
2447
|
-
function
|
|
3084
|
+
function getTaskWithRelations(id, db) {
|
|
2448
3085
|
const d = db || getDatabase();
|
|
2449
|
-
|
|
3086
|
+
const task = getTask(id, d);
|
|
3087
|
+
if (!task)
|
|
3088
|
+
return null;
|
|
3089
|
+
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
3090
|
+
const subtasks = subtaskRows.map(rowToTask);
|
|
3091
|
+
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
3092
|
+
JOIN task_dependencies td ON td.depends_on = t.id
|
|
3093
|
+
WHERE td.task_id = ?`).all(id);
|
|
3094
|
+
const dependencies = depRows.map(rowToTask);
|
|
3095
|
+
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
3096
|
+
JOIN task_dependencies td ON td.task_id = t.id
|
|
3097
|
+
WHERE td.depends_on = ?`).all(id);
|
|
3098
|
+
const blocked_by = blockedByRows.map(rowToTask);
|
|
3099
|
+
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
3100
|
+
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
3101
|
+
const checklist = getChecklist(id, d);
|
|
3102
|
+
return {
|
|
3103
|
+
...task,
|
|
3104
|
+
subtasks,
|
|
3105
|
+
dependencies,
|
|
3106
|
+
blocked_by,
|
|
3107
|
+
comments,
|
|
3108
|
+
parent,
|
|
3109
|
+
checklist
|
|
3110
|
+
};
|
|
2450
3111
|
}
|
|
2451
|
-
function
|
|
3112
|
+
function listTasks(filter = {}, db) {
|
|
2452
3113
|
const d = db || getDatabase();
|
|
2453
|
-
|
|
2454
|
-
|
|
3114
|
+
const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
|
|
3115
|
+
clearExpiredLocks2(d);
|
|
3116
|
+
const conditions = [];
|
|
3117
|
+
const params = [];
|
|
3118
|
+
if (filter.project_id) {
|
|
3119
|
+
conditions.push("project_id = ?");
|
|
3120
|
+
params.push(filter.project_id);
|
|
2455
3121
|
}
|
|
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("");
|
|
3122
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
3123
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
3124
|
+
params.push(...filter.ids);
|
|
3125
|
+
}
|
|
3126
|
+
if (filter.parent_id !== undefined) {
|
|
3127
|
+
if (filter.parent_id === null) {
|
|
3128
|
+
conditions.push("parent_id IS NULL");
|
|
3129
|
+
} else {
|
|
3130
|
+
conditions.push("parent_id = ?");
|
|
3131
|
+
params.push(filter.parent_id);
|
|
2481
3132
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
3133
|
+
}
|
|
3134
|
+
if (filter.status) {
|
|
3135
|
+
if (Array.isArray(filter.status)) {
|
|
3136
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
3137
|
+
params.push(...filter.status);
|
|
3138
|
+
} else {
|
|
3139
|
+
conditions.push("status = ?");
|
|
3140
|
+
params.push(filter.status);
|
|
2490
3141
|
}
|
|
2491
|
-
}
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
3142
|
+
}
|
|
3143
|
+
if (filter.priority) {
|
|
3144
|
+
if (Array.isArray(filter.priority)) {
|
|
3145
|
+
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
3146
|
+
params.push(...filter.priority);
|
|
3147
|
+
} else {
|
|
3148
|
+
conditions.push("priority = ?");
|
|
3149
|
+
params.push(filter.priority);
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
if (filter.assigned_to) {
|
|
3153
|
+
conditions.push("assigned_to = ?");
|
|
3154
|
+
params.push(filter.assigned_to);
|
|
3155
|
+
}
|
|
3156
|
+
if (filter.agent_id) {
|
|
3157
|
+
conditions.push("agent_id = ?");
|
|
3158
|
+
params.push(filter.agent_id);
|
|
3159
|
+
}
|
|
3160
|
+
if (filter.session_id) {
|
|
3161
|
+
conditions.push("session_id = ?");
|
|
3162
|
+
params.push(filter.session_id);
|
|
3163
|
+
}
|
|
3164
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
3165
|
+
const placeholders = filter.tags.map(() => "?").join(",");
|
|
3166
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
3167
|
+
params.push(...filter.tags);
|
|
3168
|
+
}
|
|
3169
|
+
if (filter.plan_id) {
|
|
3170
|
+
conditions.push("plan_id = ?");
|
|
3171
|
+
params.push(filter.plan_id);
|
|
3172
|
+
}
|
|
3173
|
+
if (filter.task_list_id) {
|
|
3174
|
+
conditions.push("task_list_id = ?");
|
|
3175
|
+
params.push(filter.task_list_id);
|
|
3176
|
+
}
|
|
3177
|
+
if (filter.has_recurrence === true) {
|
|
3178
|
+
conditions.push("recurrence_rule IS NOT NULL");
|
|
3179
|
+
} else if (filter.has_recurrence === false) {
|
|
3180
|
+
conditions.push("recurrence_rule IS NULL");
|
|
3181
|
+
}
|
|
3182
|
+
if (filter.task_type) {
|
|
3183
|
+
if (Array.isArray(filter.task_type)) {
|
|
3184
|
+
conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
|
|
3185
|
+
params.push(...filter.task_type);
|
|
3186
|
+
} else {
|
|
3187
|
+
conditions.push("task_type = ?");
|
|
3188
|
+
params.push(filter.task_type);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
3192
|
+
if (filter.cursor) {
|
|
3193
|
+
try {
|
|
3194
|
+
const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
|
|
3195
|
+
conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
|
|
3196
|
+
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
3197
|
+
} catch {}
|
|
3198
|
+
}
|
|
3199
|
+
if (!filter.include_archived) {
|
|
3200
|
+
conditions.push("archived_at IS NULL");
|
|
3201
|
+
}
|
|
3202
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3203
|
+
let limitClause = "";
|
|
3204
|
+
if (filter.limit) {
|
|
3205
|
+
limitClause = " LIMIT ?";
|
|
3206
|
+
params.push(filter.limit);
|
|
3207
|
+
if (!filter.cursor && filter.offset) {
|
|
3208
|
+
limitClause += " OFFSET ?";
|
|
3209
|
+
params.push(filter.offset);
|
|
2499
3210
|
}
|
|
2500
3211
|
}
|
|
3212
|
+
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
3213
|
+
return rows.map(rowToTask);
|
|
2501
3214
|
}
|
|
2502
|
-
|
|
3215
|
+
function countTasks(filter = {}, db) {
|
|
2503
3216
|
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(() => {});
|
|
3217
|
+
const conditions = [];
|
|
3218
|
+
const params = [];
|
|
3219
|
+
if (filter.project_id) {
|
|
3220
|
+
conditions.push("project_id = ?");
|
|
3221
|
+
params.push(filter.project_id);
|
|
2511
3222
|
}
|
|
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);
|
|
3223
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
3224
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
3225
|
+
params.push(...filter.ids);
|
|
2559
3226
|
}
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
if (
|
|
2578
|
-
|
|
2579
|
-
|
|
3227
|
+
if (filter.parent_id !== undefined) {
|
|
3228
|
+
if (filter.parent_id === null) {
|
|
3229
|
+
conditions.push("parent_id IS NULL");
|
|
3230
|
+
} else {
|
|
3231
|
+
conditions.push("parent_id = ?");
|
|
3232
|
+
params.push(filter.parent_id);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
if (filter.status) {
|
|
3236
|
+
if (Array.isArray(filter.status)) {
|
|
3237
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
3238
|
+
params.push(...filter.status);
|
|
3239
|
+
} else {
|
|
3240
|
+
conditions.push("status = ?");
|
|
3241
|
+
params.push(filter.status);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
if (filter.priority) {
|
|
3245
|
+
if (Array.isArray(filter.priority)) {
|
|
3246
|
+
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
3247
|
+
params.push(...filter.priority);
|
|
3248
|
+
} else {
|
|
3249
|
+
conditions.push("priority = ?");
|
|
3250
|
+
params.push(filter.priority);
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
if (filter.assigned_to) {
|
|
3254
|
+
conditions.push("assigned_to = ?");
|
|
3255
|
+
params.push(filter.assigned_to);
|
|
3256
|
+
}
|
|
3257
|
+
if (filter.agent_id) {
|
|
3258
|
+
conditions.push("agent_id = ?");
|
|
3259
|
+
params.push(filter.agent_id);
|
|
3260
|
+
}
|
|
3261
|
+
if (filter.session_id) {
|
|
3262
|
+
conditions.push("session_id = ?");
|
|
3263
|
+
params.push(filter.session_id);
|
|
3264
|
+
}
|
|
3265
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
3266
|
+
const placeholders = filter.tags.map(() => "?").join(",");
|
|
3267
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
3268
|
+
params.push(...filter.tags);
|
|
3269
|
+
}
|
|
3270
|
+
if (filter.plan_id) {
|
|
3271
|
+
conditions.push("plan_id = ?");
|
|
3272
|
+
params.push(filter.plan_id);
|
|
3273
|
+
}
|
|
3274
|
+
if (filter.task_list_id) {
|
|
3275
|
+
conditions.push("task_list_id = ?");
|
|
3276
|
+
params.push(filter.task_list_id);
|
|
3277
|
+
}
|
|
3278
|
+
if (!filter.include_archived) {
|
|
3279
|
+
conditions.push("archived_at IS NULL");
|
|
3280
|
+
}
|
|
3281
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3282
|
+
const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
3283
|
+
return row.count;
|
|
2580
3284
|
}
|
|
2581
|
-
function
|
|
3285
|
+
function updateTask(id, input, db) {
|
|
2582
3286
|
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);
|
|
3287
|
+
const task = getTask(id, d);
|
|
3288
|
+
if (!task)
|
|
3289
|
+
throw new TaskNotFoundError(id);
|
|
3290
|
+
if (task.version !== input.version) {
|
|
3291
|
+
throw new VersionConflictError(id, input.version, task.version);
|
|
2607
3292
|
}
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
3293
|
+
const sets = ["version = version + 1", "updated_at = ?"];
|
|
3294
|
+
const params = [now()];
|
|
3295
|
+
if (input.title !== undefined) {
|
|
3296
|
+
sets.push("title = ?");
|
|
3297
|
+
params.push(input.title);
|
|
2611
3298
|
}
|
|
2612
|
-
if (
|
|
3299
|
+
if (input.description !== undefined) {
|
|
2613
3300
|
sets.push("description = ?");
|
|
2614
|
-
|
|
3301
|
+
params.push(input.description);
|
|
2615
3302
|
}
|
|
2616
|
-
if (
|
|
3303
|
+
if (input.status !== undefined) {
|
|
3304
|
+
if (input.status === "completed") {
|
|
3305
|
+
checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
|
|
3306
|
+
}
|
|
3307
|
+
sets.push("status = ?");
|
|
3308
|
+
params.push(input.status);
|
|
3309
|
+
if (input.status === "completed") {
|
|
3310
|
+
sets.push("completed_at = ?");
|
|
3311
|
+
params.push(now());
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
if (input.priority !== undefined) {
|
|
2617
3315
|
sets.push("priority = ?");
|
|
2618
|
-
|
|
3316
|
+
params.push(input.priority);
|
|
2619
3317
|
}
|
|
2620
|
-
if (
|
|
3318
|
+
if (input.assigned_to !== undefined) {
|
|
3319
|
+
sets.push("assigned_to = ?");
|
|
3320
|
+
params.push(input.assigned_to);
|
|
3321
|
+
}
|
|
3322
|
+
if (input.tags !== undefined) {
|
|
2621
3323
|
sets.push("tags = ?");
|
|
2622
|
-
|
|
3324
|
+
params.push(JSON.stringify(input.tags));
|
|
3325
|
+
}
|
|
3326
|
+
if (input.metadata !== undefined) {
|
|
3327
|
+
sets.push("metadata = ?");
|
|
3328
|
+
params.push(JSON.stringify(input.metadata));
|
|
3329
|
+
}
|
|
3330
|
+
if (input.plan_id !== undefined) {
|
|
3331
|
+
sets.push("plan_id = ?");
|
|
3332
|
+
params.push(input.plan_id);
|
|
3333
|
+
}
|
|
3334
|
+
if (input.task_list_id !== undefined) {
|
|
3335
|
+
sets.push("task_list_id = ?");
|
|
3336
|
+
params.push(input.task_list_id);
|
|
3337
|
+
}
|
|
3338
|
+
if (input.due_at !== undefined) {
|
|
3339
|
+
sets.push("due_at = ?");
|
|
3340
|
+
params.push(input.due_at);
|
|
3341
|
+
}
|
|
3342
|
+
if (input.estimated_minutes !== undefined) {
|
|
3343
|
+
sets.push("estimated_minutes = ?");
|
|
3344
|
+
params.push(input.estimated_minutes);
|
|
3345
|
+
}
|
|
3346
|
+
if (input.requires_approval !== undefined) {
|
|
3347
|
+
sets.push("requires_approval = ?");
|
|
3348
|
+
params.push(input.requires_approval ? 1 : 0);
|
|
3349
|
+
}
|
|
3350
|
+
if (input.approved_by !== undefined) {
|
|
3351
|
+
sets.push("approved_by = ?");
|
|
3352
|
+
params.push(input.approved_by);
|
|
3353
|
+
sets.push("approved_at = ?");
|
|
3354
|
+
params.push(now());
|
|
3355
|
+
}
|
|
3356
|
+
if (input.recurrence_rule !== undefined) {
|
|
3357
|
+
sets.push("recurrence_rule = ?");
|
|
3358
|
+
params.push(input.recurrence_rule);
|
|
3359
|
+
}
|
|
3360
|
+
if (input.task_type !== undefined) {
|
|
3361
|
+
sets.push("task_type = ?");
|
|
3362
|
+
params.push(input.task_type ?? null);
|
|
2623
3363
|
}
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
3364
|
+
params.push(id, input.version);
|
|
3365
|
+
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
3366
|
+
if (result.changes === 0) {
|
|
3367
|
+
const current = getTask(id, d);
|
|
3368
|
+
throw new VersionConflictError(id, input.version, current?.version ?? -1);
|
|
2627
3369
|
}
|
|
2628
|
-
if (
|
|
2629
|
-
|
|
2630
|
-
values.push(updates.project_id);
|
|
3370
|
+
if (input.tags !== undefined) {
|
|
3371
|
+
replaceTaskTags(id, input.tags, d);
|
|
2631
3372
|
}
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
3373
|
+
const agentId = task.assigned_to || task.agent_id || null;
|
|
3374
|
+
if (input.status !== undefined && input.status !== task.status)
|
|
3375
|
+
logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
|
|
3376
|
+
if (input.priority !== undefined && input.priority !== task.priority)
|
|
3377
|
+
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
3378
|
+
if (input.title !== undefined && input.title !== task.title)
|
|
3379
|
+
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
3380
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
3381
|
+
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
3382
|
+
if (input.approved_by !== undefined)
|
|
3383
|
+
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
3384
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
3385
|
+
dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
|
|
2635
3386
|
}
|
|
2636
|
-
if (
|
|
2637
|
-
|
|
2638
|
-
values.push(JSON.stringify(updates.metadata));
|
|
3387
|
+
if (input.status !== undefined && input.status !== task.status) {
|
|
3388
|
+
dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
|
|
2639
3389
|
}
|
|
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
3390
|
return {
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
3391
|
+
...task,
|
|
3392
|
+
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
3393
|
+
tags: input.tags ?? task.tags,
|
|
3394
|
+
metadata: input.metadata ?? task.metadata,
|
|
3395
|
+
version: task.version + 1,
|
|
3396
|
+
updated_at: now(),
|
|
3397
|
+
completed_at: input.status === "completed" ? now() : task.completed_at,
|
|
3398
|
+
requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
|
|
3399
|
+
approved_by: input.approved_by ?? task.approved_by,
|
|
3400
|
+
approved_at: input.approved_by ? now() : task.approved_at
|
|
2658
3401
|
};
|
|
2659
3402
|
}
|
|
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) {
|
|
3403
|
+
function deleteTask(id, db) {
|
|
2693
3404
|
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 };
|
|
3405
|
+
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
3406
|
+
return result.changes > 0;
|
|
2700
3407
|
}
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
3408
|
+
// src/db/task-lifecycle.ts
|
|
3409
|
+
init_types();
|
|
3410
|
+
init_database();
|
|
3411
|
+
|
|
3412
|
+
// src/lib/recurrence.ts
|
|
3413
|
+
var DAY_NAMES = {
|
|
3414
|
+
sunday: 0,
|
|
3415
|
+
sun: 0,
|
|
3416
|
+
monday: 1,
|
|
3417
|
+
mon: 1,
|
|
3418
|
+
tuesday: 2,
|
|
3419
|
+
tue: 2,
|
|
3420
|
+
wednesday: 3,
|
|
3421
|
+
wed: 3,
|
|
3422
|
+
thursday: 4,
|
|
3423
|
+
thu: 4,
|
|
3424
|
+
friday: 5,
|
|
3425
|
+
fri: 5,
|
|
3426
|
+
saturday: 6,
|
|
3427
|
+
sat: 6
|
|
3428
|
+
};
|
|
3429
|
+
function parseRecurrenceRule(rule) {
|
|
3430
|
+
const normalized = rule.trim().toLowerCase();
|
|
3431
|
+
if (normalized === "every weekday" || normalized === "every weekdays") {
|
|
3432
|
+
return { type: "specific_days", days: [1, 2, 3, 4, 5] };
|
|
3433
|
+
}
|
|
3434
|
+
if (normalized === "every day" || normalized === "daily") {
|
|
3435
|
+
return { type: "interval", interval: 1, unit: "day" };
|
|
3436
|
+
}
|
|
3437
|
+
if (normalized === "every week" || normalized === "weekly") {
|
|
3438
|
+
return { type: "interval", interval: 1, unit: "week" };
|
|
3439
|
+
}
|
|
3440
|
+
if (normalized === "every month" || normalized === "monthly") {
|
|
3441
|
+
return { type: "interval", interval: 1, unit: "month" };
|
|
3442
|
+
}
|
|
3443
|
+
const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
|
|
3444
|
+
if (intervalMatch) {
|
|
3445
|
+
return {
|
|
3446
|
+
type: "interval",
|
|
3447
|
+
interval: parseInt(intervalMatch[1], 10),
|
|
3448
|
+
unit: intervalMatch[2]
|
|
3449
|
+
};
|
|
3450
|
+
}
|
|
3451
|
+
const daysMatch = normalized.match(/^every\s+(.+)$/);
|
|
3452
|
+
if (daysMatch) {
|
|
3453
|
+
const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
|
|
3454
|
+
const days = [];
|
|
3455
|
+
for (const part of dayParts) {
|
|
3456
|
+
const dayNum = DAY_NAMES[part];
|
|
3457
|
+
if (dayNum !== undefined) {
|
|
3458
|
+
days.push(dayNum);
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
if (days.length > 0) {
|
|
3462
|
+
return { type: "specific_days", days: days.sort((a, b) => a - b) };
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
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
3466
|
}
|
|
2709
|
-
function
|
|
2710
|
-
|
|
3467
|
+
function isValidRecurrenceRule(rule) {
|
|
3468
|
+
try {
|
|
3469
|
+
parseRecurrenceRule(rule);
|
|
2711
3470
|
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;
|
|
3471
|
+
} catch {
|
|
3472
|
+
return false;
|
|
2724
3473
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
3474
|
+
}
|
|
3475
|
+
function nextOccurrence(rule, from) {
|
|
3476
|
+
const parsed = parseRecurrenceRule(rule);
|
|
3477
|
+
const base = from || new Date;
|
|
3478
|
+
if (parsed.type === "interval") {
|
|
3479
|
+
const next = new Date(base);
|
|
3480
|
+
if (parsed.unit === "day") {
|
|
3481
|
+
next.setDate(next.getDate() + parsed.interval);
|
|
3482
|
+
} else if (parsed.unit === "week") {
|
|
3483
|
+
next.setDate(next.getDate() + parsed.interval * 7);
|
|
3484
|
+
} else if (parsed.unit === "month") {
|
|
3485
|
+
next.setMonth(next.getMonth() + parsed.interval);
|
|
3486
|
+
}
|
|
3487
|
+
return next.toISOString();
|
|
2730
3488
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
const
|
|
2734
|
-
|
|
2735
|
-
|
|
3489
|
+
if (parsed.type === "specific_days") {
|
|
3490
|
+
const currentDay = base.getDay();
|
|
3491
|
+
const days = parsed.days;
|
|
3492
|
+
let daysToAdd = Infinity;
|
|
3493
|
+
for (const day of days) {
|
|
3494
|
+
let diff = day - currentDay;
|
|
3495
|
+
if (diff <= 0)
|
|
3496
|
+
diff += 7;
|
|
3497
|
+
if (diff < daysToAdd)
|
|
3498
|
+
daysToAdd = diff;
|
|
3499
|
+
}
|
|
3500
|
+
const next = new Date(base);
|
|
3501
|
+
next.setDate(next.getDate() + daysToAdd);
|
|
3502
|
+
return next.toISOString();
|
|
2736
3503
|
}
|
|
2737
|
-
|
|
3504
|
+
throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
|
|
2738
3505
|
}
|
|
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
|
-
}))
|
|
3506
|
+
|
|
3507
|
+
// src/db/templates.ts
|
|
3508
|
+
init_database();
|
|
3509
|
+
function rowToTemplate(row) {
|
|
3510
|
+
return {
|
|
3511
|
+
...row,
|
|
3512
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
3513
|
+
variables: JSON.parse(row.variables || "[]"),
|
|
3514
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
3515
|
+
priority: row.priority || "medium",
|
|
3516
|
+
version: row.version ?? 1
|
|
2766
3517
|
};
|
|
2767
3518
|
}
|
|
2768
|
-
function
|
|
3519
|
+
function rowToTemplateTask(row) {
|
|
3520
|
+
return {
|
|
3521
|
+
...row,
|
|
3522
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
3523
|
+
depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
|
|
3524
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
3525
|
+
priority: row.priority || "medium",
|
|
3526
|
+
condition: row.condition ?? null,
|
|
3527
|
+
include_template_id: row.include_template_id ?? null
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3530
|
+
function resolveTemplateId(id, d) {
|
|
3531
|
+
return resolvePartialId(d, "task_templates", id);
|
|
3532
|
+
}
|
|
3533
|
+
function createTemplate(input, db) {
|
|
2769
3534
|
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);
|
|
3535
|
+
const id = uuid();
|
|
3536
|
+
d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
|
|
3537
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3538
|
+
id,
|
|
3539
|
+
input.name,
|
|
3540
|
+
input.title_pattern,
|
|
3541
|
+
input.description || null,
|
|
3542
|
+
input.priority || "medium",
|
|
3543
|
+
JSON.stringify(input.tags || []),
|
|
3544
|
+
JSON.stringify(input.variables || []),
|
|
3545
|
+
input.project_id || null,
|
|
3546
|
+
input.plan_id || null,
|
|
3547
|
+
JSON.stringify(input.metadata || {}),
|
|
3548
|
+
now()
|
|
3549
|
+
]);
|
|
3550
|
+
if (input.tasks && input.tasks.length > 0) {
|
|
3551
|
+
addTemplateTasks(id, input.tasks, d);
|
|
3552
|
+
}
|
|
3553
|
+
return getTemplate(id, d);
|
|
2793
3554
|
}
|
|
2794
|
-
function
|
|
3555
|
+
function getTemplate(id, db) {
|
|
2795
3556
|
const d = db || getDatabase();
|
|
2796
3557
|
const resolved = resolveTemplateId(id, d);
|
|
2797
3558
|
if (!resolved)
|
|
2798
3559
|
return null;
|
|
2799
|
-
const row = d.query("SELECT * FROM
|
|
2800
|
-
return row
|
|
3560
|
+
const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
|
|
3561
|
+
return row ? rowToTemplate(row) : null;
|
|
2801
3562
|
}
|
|
2802
|
-
function
|
|
3563
|
+
function listTemplates(db) {
|
|
3564
|
+
const d = db || getDatabase();
|
|
3565
|
+
return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
|
|
3566
|
+
}
|
|
3567
|
+
function deleteTemplate(id, db) {
|
|
2803
3568
|
const d = db || getDatabase();
|
|
2804
3569
|
const resolved = resolveTemplateId(id, d);
|
|
2805
3570
|
if (!resolved)
|
|
2806
|
-
return
|
|
2807
|
-
return d.
|
|
3571
|
+
return false;
|
|
3572
|
+
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
2808
3573
|
}
|
|
2809
|
-
function
|
|
2810
|
-
const
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
3574
|
+
function updateTemplate(id, updates, db) {
|
|
3575
|
+
const d = db || getDatabase();
|
|
3576
|
+
const resolved = resolveTemplateId(id, d);
|
|
3577
|
+
if (!resolved)
|
|
3578
|
+
return null;
|
|
3579
|
+
const current = getTemplateWithTasks(resolved, d);
|
|
3580
|
+
if (current) {
|
|
3581
|
+
const snapshot = JSON.stringify({
|
|
3582
|
+
name: current.name,
|
|
3583
|
+
title_pattern: current.title_pattern,
|
|
3584
|
+
description: current.description,
|
|
3585
|
+
priority: current.priority,
|
|
3586
|
+
tags: current.tags,
|
|
3587
|
+
variables: current.variables,
|
|
3588
|
+
project_id: current.project_id,
|
|
3589
|
+
plan_id: current.plan_id,
|
|
3590
|
+
metadata: current.metadata,
|
|
3591
|
+
tasks: current.tasks
|
|
3592
|
+
});
|
|
3593
|
+
d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
|
|
2815
3594
|
}
|
|
2816
|
-
const
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
3595
|
+
const sets = ["version = version + 1"];
|
|
3596
|
+
const values = [];
|
|
3597
|
+
if (updates.name !== undefined) {
|
|
3598
|
+
sets.push("name = ?");
|
|
3599
|
+
values.push(updates.name);
|
|
2821
3600
|
}
|
|
2822
|
-
if (
|
|
2823
|
-
|
|
3601
|
+
if (updates.title_pattern !== undefined) {
|
|
3602
|
+
sets.push("title_pattern = ?");
|
|
3603
|
+
values.push(updates.title_pattern);
|
|
2824
3604
|
}
|
|
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);
|
|
3605
|
+
if (updates.description !== undefined) {
|
|
3606
|
+
sets.push("description = ?");
|
|
3607
|
+
values.push(updates.description);
|
|
2831
3608
|
}
|
|
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}`);
|
|
3609
|
+
if (updates.priority !== undefined) {
|
|
3610
|
+
sets.push("priority = ?");
|
|
3611
|
+
values.push(updates.priority);
|
|
2842
3612
|
}
|
|
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];
|
|
3613
|
+
if (updates.tags !== undefined) {
|
|
3614
|
+
sets.push("tags = ?");
|
|
3615
|
+
values.push(JSON.stringify(updates.tags));
|
|
2849
3616
|
}
|
|
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);
|
|
3617
|
+
if (updates.variables !== undefined) {
|
|
3618
|
+
sets.push("variables = ?");
|
|
3619
|
+
values.push(JSON.stringify(updates.variables));
|
|
2885
3620
|
}
|
|
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
|
-
}
|
|
3621
|
+
if (updates.project_id !== undefined) {
|
|
3622
|
+
sets.push("project_id = ?");
|
|
3623
|
+
values.push(updates.project_id);
|
|
2901
3624
|
}
|
|
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
|
-
}
|
|
3625
|
+
if (updates.plan_id !== undefined) {
|
|
3626
|
+
sets.push("plan_id = ?");
|
|
3627
|
+
values.push(updates.plan_id);
|
|
2935
3628
|
}
|
|
3629
|
+
if (updates.metadata !== undefined) {
|
|
3630
|
+
sets.push("metadata = ?");
|
|
3631
|
+
values.push(JSON.stringify(updates.metadata));
|
|
3632
|
+
}
|
|
3633
|
+
values.push(resolved);
|
|
3634
|
+
d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
|
|
3635
|
+
return getTemplate(resolved, d);
|
|
3636
|
+
}
|
|
3637
|
+
function taskFromTemplate(templateId, overrides = {}, db) {
|
|
3638
|
+
const t = getTemplate(templateId, db);
|
|
3639
|
+
if (!t)
|
|
3640
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
3641
|
+
const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
|
|
2936
3642
|
return {
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
3643
|
+
title: cleanOverrides.title || t.title_pattern,
|
|
3644
|
+
description: cleanOverrides.description ?? t.description ?? undefined,
|
|
3645
|
+
priority: cleanOverrides.priority ?? t.priority,
|
|
3646
|
+
tags: cleanOverrides.tags ?? t.tags,
|
|
3647
|
+
project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
|
|
3648
|
+
plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
|
|
3649
|
+
metadata: cleanOverrides.metadata ?? t.metadata,
|
|
3650
|
+
...cleanOverrides
|
|
2943
3651
|
};
|
|
2944
3652
|
}
|
|
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) {
|
|
3653
|
+
function addTemplateTasks(templateId, tasks, db) {
|
|
2957
3654
|
const d = db || getDatabase();
|
|
2958
|
-
const
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3655
|
+
const template = getTemplate(templateId, d);
|
|
3656
|
+
if (!template)
|
|
3657
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
3658
|
+
d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
|
|
3659
|
+
const results = [];
|
|
3660
|
+
for (let i = 0;i < tasks.length; i++) {
|
|
3661
|
+
const task = tasks[i];
|
|
3662
|
+
const id = uuid();
|
|
3663
|
+
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)
|
|
3664
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3665
|
+
id,
|
|
3666
|
+
templateId,
|
|
3667
|
+
i,
|
|
3668
|
+
task.title_pattern,
|
|
3669
|
+
task.description || null,
|
|
3670
|
+
task.priority || "medium",
|
|
3671
|
+
JSON.stringify(task.tags || []),
|
|
3672
|
+
task.task_type || null,
|
|
3673
|
+
task.condition || null,
|
|
3674
|
+
task.include_template_id || null,
|
|
3675
|
+
JSON.stringify(task.depends_on || []),
|
|
3676
|
+
JSON.stringify(task.metadata || {}),
|
|
3677
|
+
now()
|
|
3678
|
+
]);
|
|
3679
|
+
const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
|
|
3680
|
+
if (row)
|
|
3681
|
+
results.push(rowToTemplateTask(row));
|
|
2964
3682
|
}
|
|
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));
|
|
3683
|
+
return results;
|
|
2975
3684
|
}
|
|
2976
|
-
function
|
|
3685
|
+
function getTemplateWithTasks(id, db) {
|
|
2977
3686
|
const d = db || getDatabase();
|
|
2978
|
-
const
|
|
2979
|
-
|
|
2980
|
-
if (result.changes === 0)
|
|
3687
|
+
const template = getTemplate(id, d);
|
|
3688
|
+
if (!template)
|
|
2981
3689
|
return null;
|
|
2982
|
-
|
|
3690
|
+
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
|
|
3691
|
+
const tasks = rows.map(rowToTemplateTask);
|
|
3692
|
+
return { ...template, tasks };
|
|
2983
3693
|
}
|
|
2984
|
-
function
|
|
3694
|
+
function getTemplateTasks(templateId, db) {
|
|
2985
3695
|
const d = db || getDatabase();
|
|
2986
|
-
const
|
|
2987
|
-
|
|
3696
|
+
const resolved = resolveTemplateId(templateId, d);
|
|
3697
|
+
if (!resolved)
|
|
3698
|
+
return [];
|
|
3699
|
+
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
|
|
3700
|
+
return rows.map(rowToTemplateTask);
|
|
2988
3701
|
}
|
|
2989
|
-
function
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3702
|
+
function evaluateCondition(condition, variables) {
|
|
3703
|
+
if (!condition || condition.trim() === "")
|
|
3704
|
+
return true;
|
|
3705
|
+
const trimmed = condition.trim();
|
|
3706
|
+
const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
3707
|
+
if (eqMatch) {
|
|
3708
|
+
const varName = eqMatch[1];
|
|
3709
|
+
const expected = eqMatch[2].trim();
|
|
3710
|
+
return (variables[varName] ?? "") === expected;
|
|
3711
|
+
}
|
|
3712
|
+
const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
3713
|
+
if (neqMatch) {
|
|
3714
|
+
const varName = neqMatch[1];
|
|
3715
|
+
const expected = neqMatch[2].trim();
|
|
3716
|
+
return (variables[varName] ?? "") !== expected;
|
|
3717
|
+
}
|
|
3718
|
+
const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
|
|
3719
|
+
if (falsyMatch) {
|
|
3720
|
+
const varName = falsyMatch[1];
|
|
3721
|
+
const val = variables[varName];
|
|
3722
|
+
return !val || val === "" || val === "false";
|
|
3723
|
+
}
|
|
3724
|
+
const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
|
|
3725
|
+
if (truthyMatch) {
|
|
3726
|
+
const varName = truthyMatch[1];
|
|
3727
|
+
const val = variables[varName];
|
|
3728
|
+
return !!val && val !== "" && val !== "false";
|
|
3729
|
+
}
|
|
3730
|
+
return true;
|
|
2993
3731
|
}
|
|
2994
|
-
function
|
|
3732
|
+
function exportTemplate(id, db) {
|
|
2995
3733
|
const d = db || getDatabase();
|
|
2996
|
-
const
|
|
2997
|
-
|
|
2998
|
-
}
|
|
2999
|
-
|
|
3000
|
-
// src/db/tasks.ts
|
|
3001
|
-
function rowToTask(row) {
|
|
3734
|
+
const template = getTemplateWithTasks(id, d);
|
|
3735
|
+
if (!template)
|
|
3736
|
+
throw new Error(`Template not found: ${id}`);
|
|
3002
3737
|
return {
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3738
|
+
name: template.name,
|
|
3739
|
+
title_pattern: template.title_pattern,
|
|
3740
|
+
description: template.description,
|
|
3741
|
+
priority: template.priority,
|
|
3742
|
+
tags: template.tags,
|
|
3743
|
+
variables: template.variables,
|
|
3744
|
+
project_id: template.project_id,
|
|
3745
|
+
plan_id: template.plan_id,
|
|
3746
|
+
metadata: template.metadata,
|
|
3747
|
+
tasks: template.tasks.map((t) => ({
|
|
3748
|
+
position: t.position,
|
|
3749
|
+
title_pattern: t.title_pattern,
|
|
3750
|
+
description: t.description,
|
|
3751
|
+
priority: t.priority,
|
|
3752
|
+
tags: t.tags,
|
|
3753
|
+
task_type: t.task_type,
|
|
3754
|
+
condition: t.condition,
|
|
3755
|
+
include_template_id: t.include_template_id,
|
|
3756
|
+
depends_on_positions: t.depends_on_positions,
|
|
3757
|
+
metadata: t.metadata
|
|
3758
|
+
}))
|
|
3009
3759
|
};
|
|
3010
3760
|
}
|
|
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) {
|
|
3761
|
+
function importTemplate(json, db) {
|
|
3025
3762
|
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;
|
|
3763
|
+
const taskInputs = (json.tasks || []).map((t) => ({
|
|
3764
|
+
title_pattern: t.title_pattern,
|
|
3765
|
+
description: t.description ?? undefined,
|
|
3766
|
+
priority: t.priority,
|
|
3767
|
+
tags: t.tags,
|
|
3768
|
+
task_type: t.task_type ?? undefined,
|
|
3769
|
+
condition: t.condition ?? undefined,
|
|
3770
|
+
include_template_id: t.include_template_id ?? undefined,
|
|
3771
|
+
depends_on: t.depends_on_positions,
|
|
3772
|
+
metadata: t.metadata
|
|
3773
|
+
}));
|
|
3774
|
+
return createTemplate({
|
|
3775
|
+
name: json.name,
|
|
3776
|
+
title_pattern: json.title_pattern,
|
|
3777
|
+
description: json.description ?? undefined,
|
|
3778
|
+
priority: json.priority,
|
|
3779
|
+
tags: json.tags,
|
|
3780
|
+
variables: json.variables,
|
|
3781
|
+
project_id: json.project_id ?? undefined,
|
|
3782
|
+
plan_id: json.plan_id ?? undefined,
|
|
3783
|
+
metadata: json.metadata,
|
|
3784
|
+
tasks: taskInputs
|
|
3785
|
+
}, d);
|
|
3082
3786
|
}
|
|
3083
|
-
function
|
|
3787
|
+
function getTemplateVersion(id, version, db) {
|
|
3084
3788
|
const d = db || getDatabase();
|
|
3085
|
-
const
|
|
3086
|
-
if (!
|
|
3789
|
+
const resolved = resolveTemplateId(id, d);
|
|
3790
|
+
if (!resolved)
|
|
3087
3791
|
return null;
|
|
3088
|
-
|
|
3792
|
+
const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
|
|
3793
|
+
return row || null;
|
|
3089
3794
|
}
|
|
3090
|
-
function
|
|
3795
|
+
function listTemplateVersions(id, db) {
|
|
3091
3796
|
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
|
-
};
|
|
3797
|
+
const resolved = resolveTemplateId(id, d);
|
|
3798
|
+
if (!resolved)
|
|
3799
|
+
return [];
|
|
3800
|
+
return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
|
|
3117
3801
|
}
|
|
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);
|
|
3802
|
+
function resolveVariables(templateVars, provided) {
|
|
3803
|
+
const merged = { ...provided };
|
|
3804
|
+
for (const v of templateVars) {
|
|
3805
|
+
if (merged[v.name] === undefined && v.default !== undefined) {
|
|
3806
|
+
merged[v.name] = v.default;
|
|
3142
3807
|
}
|
|
3143
3808
|
}
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
} else {
|
|
3149
|
-
conditions.push("priority = ?");
|
|
3150
|
-
params.push(filter.priority);
|
|
3809
|
+
const missing = [];
|
|
3810
|
+
for (const v of templateVars) {
|
|
3811
|
+
if (v.required && merged[v.name] === undefined) {
|
|
3812
|
+
missing.push(v.name);
|
|
3151
3813
|
}
|
|
3152
3814
|
}
|
|
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);
|
|
3815
|
+
if (missing.length > 0) {
|
|
3816
|
+
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
3169
3817
|
}
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3818
|
+
return merged;
|
|
3819
|
+
}
|
|
3820
|
+
function substituteVars(text, variables) {
|
|
3821
|
+
let result = text;
|
|
3822
|
+
for (const [key, val] of Object.entries(variables)) {
|
|
3823
|
+
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
|
|
3173
3824
|
}
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3825
|
+
return result;
|
|
3826
|
+
}
|
|
3827
|
+
function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
|
|
3828
|
+
const d = db || getDatabase();
|
|
3829
|
+
const template = getTemplateWithTasks(templateId, d);
|
|
3830
|
+
if (!template)
|
|
3831
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
3832
|
+
const visited = _visitedTemplateIds || new Set;
|
|
3833
|
+
if (visited.has(template.id)) {
|
|
3834
|
+
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
3177
3835
|
}
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3836
|
+
visited.add(template.id);
|
|
3837
|
+
const resolved = resolveVariables(template.variables, variables);
|
|
3838
|
+
if (template.tasks.length === 0) {
|
|
3839
|
+
const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
|
|
3840
|
+
const task = createTask(input, d);
|
|
3841
|
+
return [task];
|
|
3182
3842
|
}
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3843
|
+
const createdTasks = [];
|
|
3844
|
+
const positionToId = new Map;
|
|
3845
|
+
const skippedPositions = new Set;
|
|
3846
|
+
for (const tt of template.tasks) {
|
|
3847
|
+
if (tt.include_template_id) {
|
|
3848
|
+
const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
|
|
3849
|
+
createdTasks.push(...includedTasks);
|
|
3850
|
+
if (includedTasks.length > 0) {
|
|
3851
|
+
positionToId.set(tt.position, includedTasks[0].id);
|
|
3852
|
+
} else {
|
|
3853
|
+
skippedPositions.add(tt.position);
|
|
3854
|
+
}
|
|
3855
|
+
continue;
|
|
3190
3856
|
}
|
|
3857
|
+
if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
|
|
3858
|
+
skippedPositions.add(tt.position);
|
|
3859
|
+
continue;
|
|
3860
|
+
}
|
|
3861
|
+
let title = tt.title_pattern;
|
|
3862
|
+
let desc = tt.description;
|
|
3863
|
+
title = substituteVars(title, resolved);
|
|
3864
|
+
if (desc)
|
|
3865
|
+
desc = substituteVars(desc, resolved);
|
|
3866
|
+
const task = createTask({
|
|
3867
|
+
title,
|
|
3868
|
+
description: desc ?? undefined,
|
|
3869
|
+
priority: tt.priority,
|
|
3870
|
+
tags: tt.tags,
|
|
3871
|
+
task_type: tt.task_type ?? undefined,
|
|
3872
|
+
project_id: projectId,
|
|
3873
|
+
task_list_id: taskListId,
|
|
3874
|
+
metadata: tt.metadata
|
|
3875
|
+
}, d);
|
|
3876
|
+
createdTasks.push(task);
|
|
3877
|
+
positionToId.set(tt.position, task.id);
|
|
3191
3878
|
}
|
|
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);
|
|
3879
|
+
for (const tt of template.tasks) {
|
|
3880
|
+
if (skippedPositions.has(tt.position))
|
|
3881
|
+
continue;
|
|
3882
|
+
if (tt.include_template_id)
|
|
3883
|
+
continue;
|
|
3884
|
+
const deps = tt.depends_on_positions;
|
|
3885
|
+
for (const depPos of deps) {
|
|
3886
|
+
if (skippedPositions.has(depPos))
|
|
3887
|
+
continue;
|
|
3888
|
+
const taskId = positionToId.get(tt.position);
|
|
3889
|
+
const depId = positionToId.get(depPos);
|
|
3890
|
+
if (taskId && depId) {
|
|
3891
|
+
addDependency(taskId, depId, d);
|
|
3892
|
+
}
|
|
3211
3893
|
}
|
|
3212
3894
|
}
|
|
3213
|
-
|
|
3214
|
-
return rows.map(rowToTask);
|
|
3895
|
+
return createdTasks;
|
|
3215
3896
|
}
|
|
3216
|
-
function
|
|
3897
|
+
function previewTemplate(templateId, variables, db) {
|
|
3217
3898
|
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);
|
|
3899
|
+
const template = getTemplateWithTasks(templateId, d);
|
|
3900
|
+
if (!template)
|
|
3901
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
3902
|
+
const resolved = resolveVariables(template.variables, variables);
|
|
3903
|
+
const tasks = [];
|
|
3904
|
+
if (template.tasks.length === 0) {
|
|
3905
|
+
tasks.push({
|
|
3906
|
+
position: 0,
|
|
3907
|
+
title: substituteVars(template.title_pattern, resolved),
|
|
3908
|
+
description: template.description ? substituteVars(template.description, resolved) : null,
|
|
3909
|
+
priority: template.priority,
|
|
3910
|
+
tags: template.tags,
|
|
3911
|
+
task_type: null,
|
|
3912
|
+
depends_on_positions: []
|
|
3913
|
+
});
|
|
3914
|
+
} else {
|
|
3915
|
+
for (const tt of template.tasks) {
|
|
3916
|
+
if (tt.condition && !evaluateCondition(tt.condition, resolved))
|
|
3917
|
+
continue;
|
|
3918
|
+
tasks.push({
|
|
3919
|
+
position: tt.position,
|
|
3920
|
+
title: substituteVars(tt.title_pattern, resolved),
|
|
3921
|
+
description: tt.description ? substituteVars(tt.description, resolved) : null,
|
|
3922
|
+
priority: tt.priority,
|
|
3923
|
+
tags: tt.tags,
|
|
3924
|
+
task_type: tt.task_type,
|
|
3925
|
+
depends_on_positions: tt.depends_on_positions
|
|
3926
|
+
});
|
|
3248
3927
|
}
|
|
3249
3928
|
}
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
}
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3929
|
+
return {
|
|
3930
|
+
template_id: template.id,
|
|
3931
|
+
template_name: template.name,
|
|
3932
|
+
description: template.description,
|
|
3933
|
+
variables: template.variables,
|
|
3934
|
+
resolved_variables: resolved,
|
|
3935
|
+
tasks
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
// src/db/task-graph.ts
|
|
3940
|
+
init_types();
|
|
3941
|
+
init_database();
|
|
3942
|
+
function addDependency(taskId, dependsOn, db) {
|
|
3943
|
+
const d = db || getDatabase();
|
|
3944
|
+
if (!getTask(taskId, d))
|
|
3945
|
+
throw new TaskNotFoundError(taskId);
|
|
3946
|
+
if (!getTask(dependsOn, d))
|
|
3947
|
+
throw new TaskNotFoundError(dependsOn);
|
|
3948
|
+
if (wouldCreateCycle(taskId, dependsOn, d)) {
|
|
3949
|
+
throw new DependencyCycleError(taskId, dependsOn);
|
|
3261
3950
|
}
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3951
|
+
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
|
|
3952
|
+
}
|
|
3953
|
+
function removeDependency(taskId, dependsOn, db) {
|
|
3954
|
+
const d = db || getDatabase();
|
|
3955
|
+
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
3956
|
+
return result.changes > 0;
|
|
3957
|
+
}
|
|
3958
|
+
function getTaskDependencies(taskId, db) {
|
|
3959
|
+
const d = db || getDatabase();
|
|
3960
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
3961
|
+
}
|
|
3962
|
+
function getTaskDependents(taskId, db) {
|
|
3963
|
+
const d = db || getDatabase();
|
|
3964
|
+
return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
|
|
3965
|
+
}
|
|
3966
|
+
function cloneTask(taskId, overrides, db) {
|
|
3967
|
+
const d = db || getDatabase();
|
|
3968
|
+
const source = getTask(taskId, d);
|
|
3969
|
+
if (!source)
|
|
3970
|
+
throw new TaskNotFoundError(taskId);
|
|
3971
|
+
const input = {
|
|
3972
|
+
title: overrides?.title ?? source.title,
|
|
3973
|
+
description: overrides?.description ?? source.description ?? undefined,
|
|
3974
|
+
priority: overrides?.priority ?? source.priority,
|
|
3975
|
+
project_id: overrides?.project_id ?? source.project_id ?? undefined,
|
|
3976
|
+
parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
|
|
3977
|
+
plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
|
|
3978
|
+
task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
|
|
3979
|
+
status: overrides?.status ?? "pending",
|
|
3980
|
+
agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
|
|
3981
|
+
assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
|
|
3982
|
+
tags: overrides?.tags ?? source.tags,
|
|
3983
|
+
metadata: overrides?.metadata ?? source.metadata,
|
|
3984
|
+
estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
|
|
3985
|
+
recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
|
|
3986
|
+
};
|
|
3987
|
+
return createTask(input, d);
|
|
3988
|
+
}
|
|
3989
|
+
function getTaskGraph(taskId, direction = "both", db) {
|
|
3990
|
+
const d = db || getDatabase();
|
|
3991
|
+
const task = getTask(taskId, d);
|
|
3992
|
+
if (!task)
|
|
3993
|
+
throw new TaskNotFoundError(taskId);
|
|
3994
|
+
function toNode(t) {
|
|
3995
|
+
const deps = getTaskDependencies(t.id, d);
|
|
3996
|
+
const hasUnfinishedDeps = deps.some((dep) => {
|
|
3997
|
+
const depTask = getTask(dep.depends_on, d);
|
|
3998
|
+
return depTask && depTask.status !== "completed";
|
|
3999
|
+
});
|
|
4000
|
+
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
3266
4001
|
}
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
4002
|
+
function buildUp(id, visited) {
|
|
4003
|
+
if (visited.has(id))
|
|
4004
|
+
return [];
|
|
4005
|
+
visited.add(id);
|
|
4006
|
+
const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
|
|
4007
|
+
return deps.map((dep) => {
|
|
4008
|
+
const depTask = getTask(dep.depends_on, d);
|
|
4009
|
+
if (!depTask)
|
|
4010
|
+
return null;
|
|
4011
|
+
return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
|
|
4012
|
+
}).filter(Boolean);
|
|
3270
4013
|
}
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
4014
|
+
function buildDown(id, visited) {
|
|
4015
|
+
if (visited.has(id))
|
|
4016
|
+
return [];
|
|
4017
|
+
visited.add(id);
|
|
4018
|
+
const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
|
|
4019
|
+
return dependents.map((dep) => {
|
|
4020
|
+
const depTask = getTask(dep.task_id, d);
|
|
4021
|
+
if (!depTask)
|
|
4022
|
+
return null;
|
|
4023
|
+
return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
|
|
4024
|
+
}).filter(Boolean);
|
|
3274
4025
|
}
|
|
3275
|
-
const
|
|
3276
|
-
const
|
|
3277
|
-
|
|
4026
|
+
const rootNode = toNode(task);
|
|
4027
|
+
const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
|
|
4028
|
+
const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
|
|
4029
|
+
return { task: rootNode, depends_on, blocks };
|
|
3278
4030
|
}
|
|
3279
|
-
function
|
|
4031
|
+
function moveTask(taskId, target, db) {
|
|
3280
4032
|
const d = db || getDatabase();
|
|
3281
|
-
const task = getTask(
|
|
4033
|
+
const task = getTask(taskId, d);
|
|
3282
4034
|
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 = ?"];
|
|
4035
|
+
throw new TaskNotFoundError(taskId);
|
|
4036
|
+
const sets = ["updated_at = ?", "version = version + 1"];
|
|
3288
4037
|
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) {
|
|
4038
|
+
if (target.task_list_id !== undefined) {
|
|
3329
4039
|
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(() => {});
|
|
4040
|
+
params.push(target.task_list_id);
|
|
3380
4041
|
}
|
|
3381
|
-
if (
|
|
3382
|
-
|
|
4042
|
+
if (target.project_id !== undefined) {
|
|
4043
|
+
sets.push("project_id = ?");
|
|
4044
|
+
params.push(target.project_id);
|
|
3383
4045
|
}
|
|
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
|
-
};
|
|
4046
|
+
if (target.plan_id !== undefined) {
|
|
4047
|
+
sets.push("plan_id = ?");
|
|
4048
|
+
params.push(target.plan_id);
|
|
4049
|
+
}
|
|
4050
|
+
params.push(taskId);
|
|
4051
|
+
d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
4052
|
+
return getTask(taskId, d);
|
|
3396
4053
|
}
|
|
3397
|
-
function
|
|
3398
|
-
const
|
|
3399
|
-
const
|
|
3400
|
-
|
|
4054
|
+
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
4055
|
+
const visited = new Set;
|
|
4056
|
+
const queue = [dependsOn];
|
|
4057
|
+
while (queue.length > 0) {
|
|
4058
|
+
const current = queue.shift();
|
|
4059
|
+
if (current === taskId)
|
|
4060
|
+
return true;
|
|
4061
|
+
if (visited.has(current))
|
|
4062
|
+
continue;
|
|
4063
|
+
visited.add(current);
|
|
4064
|
+
const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
|
|
4065
|
+
for (const dep of deps) {
|
|
4066
|
+
queue.push(dep.depends_on);
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
return false;
|
|
3401
4070
|
}
|
|
4071
|
+
|
|
4072
|
+
// src/db/task-lifecycle.ts
|
|
4073
|
+
var MAX_SPAWN_DEPTH = 10;
|
|
3402
4074
|
function getBlockingDeps(id, db) {
|
|
3403
4075
|
const d = db || getDatabase();
|
|
3404
4076
|
const deps = getTaskDependencies(id, d);
|
|
@@ -3453,14 +4125,21 @@ function completeTask(id, agentId, db, options) {
|
|
|
3453
4125
|
completionMeta._completion = { confidence: options.confidence };
|
|
3454
4126
|
}
|
|
3455
4127
|
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
4128
|
const timestamp = now();
|
|
3461
4129
|
const confidence = options?.confidence !== undefined ? options.confidence : null;
|
|
3462
|
-
|
|
3463
|
-
|
|
4130
|
+
const tx = d.transaction(() => {
|
|
4131
|
+
if (hasMeta) {
|
|
4132
|
+
const meta2 = { ...task.metadata, ...completionMeta };
|
|
4133
|
+
const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
|
|
4134
|
+
if (metaResult.changes === 0) {
|
|
4135
|
+
const current = getTask(id, d);
|
|
4136
|
+
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
4140
|
+
WHERE id = ?`, [timestamp, confidence, timestamp, id]);
|
|
4141
|
+
});
|
|
4142
|
+
tx();
|
|
3464
4143
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
3465
4144
|
dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
|
|
3466
4145
|
let spawnedTask = null;
|
|
@@ -3469,15 +4148,21 @@ function completeTask(id, agentId, db, options) {
|
|
|
3469
4148
|
}
|
|
3470
4149
|
let spawnedFromTemplate = null;
|
|
3471
4150
|
if (task.spawns_template_id) {
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
4151
|
+
const spawnDepth = task.metadata?._spawn_depth || 0;
|
|
4152
|
+
if (spawnDepth >= MAX_SPAWN_DEPTH) {
|
|
4153
|
+
console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
|
|
4154
|
+
} else {
|
|
4155
|
+
try {
|
|
4156
|
+
const input = taskFromTemplate(task.spawns_template_id, {
|
|
4157
|
+
project_id: task.project_id ?? undefined,
|
|
4158
|
+
plan_id: task.plan_id ?? undefined,
|
|
4159
|
+
task_list_id: task.task_list_id ?? undefined,
|
|
4160
|
+
assigned_to: task.assigned_to ?? undefined
|
|
4161
|
+
}, d);
|
|
4162
|
+
input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
|
|
4163
|
+
spawnedFromTemplate = createTask(input, d);
|
|
4164
|
+
} catch {}
|
|
4165
|
+
}
|
|
3481
4166
|
}
|
|
3482
4167
|
const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
|
|
3483
4168
|
if (spawnedTask) {
|
|
@@ -3542,141 +4227,6 @@ function unlockTask(id, agentId, db) {
|
|
|
3542
4227
|
WHERE id = ?`, [timestamp, id]);
|
|
3543
4228
|
return true;
|
|
3544
4229
|
}
|
|
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
4230
|
function claimNextTask(agentId, filters, db) {
|
|
3681
4231
|
const d = db || getDatabase();
|
|
3682
4232
|
const tx = d.transaction(() => {
|
|
@@ -3849,10 +4399,6 @@ function getStaleTasks(staleMinutes = 30, filters, db) {
|
|
|
3849
4399
|
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
|
|
3850
4400
|
return rows.map(rowToTask);
|
|
3851
4401
|
}
|
|
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
4402
|
function stealTask(agentId, opts, db) {
|
|
3857
4403
|
const d = db || getDatabase();
|
|
3858
4404
|
const staleMinutes = opts?.stale_minutes ?? 30;
|
|
@@ -3883,6 +4429,32 @@ function claimOrSteal(agentId, filters, db) {
|
|
|
3883
4429
|
});
|
|
3884
4430
|
return tx();
|
|
3885
4431
|
}
|
|
4432
|
+
function spawnNextRecurrence(completedTask, db) {
|
|
4433
|
+
const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
|
|
4434
|
+
let title = completedTask.title;
|
|
4435
|
+
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
4436
|
+
title = title.slice(completedTask.short_id.length + 2);
|
|
4437
|
+
}
|
|
4438
|
+
const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
|
|
4439
|
+
return createTask({
|
|
4440
|
+
title,
|
|
4441
|
+
description: completedTask.description ?? undefined,
|
|
4442
|
+
priority: completedTask.priority,
|
|
4443
|
+
project_id: completedTask.project_id ?? undefined,
|
|
4444
|
+
task_list_id: completedTask.task_list_id ?? undefined,
|
|
4445
|
+
plan_id: completedTask.plan_id ?? undefined,
|
|
4446
|
+
assigned_to: completedTask.assigned_to ?? undefined,
|
|
4447
|
+
tags: completedTask.tags,
|
|
4448
|
+
metadata: completedTask.metadata,
|
|
4449
|
+
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
4450
|
+
recurrence_rule: completedTask.recurrence_rule,
|
|
4451
|
+
recurrence_parent_id: recurrenceParentId,
|
|
4452
|
+
due_at: dueAt
|
|
4453
|
+
}, db);
|
|
4454
|
+
}
|
|
4455
|
+
// src/db/task-status.ts
|
|
4456
|
+
init_types();
|
|
4457
|
+
init_database();
|
|
3886
4458
|
function getStatus(filters, agentId, options, db) {
|
|
3887
4459
|
const d = db || getDatabase();
|
|
3888
4460
|
const pending = countTasks({ ...filters, status: "pending" }, d);
|
|
@@ -4011,23 +4583,6 @@ function redistributeStaleTasks(agentId, options, db) {
|
|
|
4011
4583
|
const claimed = released.length > 0 ? claimNextTask(agentId, options?.project_id ? { project_id: options.project_id } : undefined, d) : null;
|
|
4012
4584
|
return { released, claimed };
|
|
4013
4585
|
}
|
|
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
4586
|
function getTaskStats(filters, db) {
|
|
4032
4587
|
const d = db || getDatabase();
|
|
4033
4588
|
const conditions = [];
|
|
@@ -4062,6 +4617,8 @@ function getTaskStats(filters, db) {
|
|
|
4062
4617
|
const completion_rate = totalRow.count > 0 ? Math.round(completed / totalRow.count * 100) : 0;
|
|
4063
4618
|
return { total: totalRow.count, by_status, by_priority, completion_rate, by_agent };
|
|
4064
4619
|
}
|
|
4620
|
+
// src/db/task-relations.ts
|
|
4621
|
+
init_database();
|
|
4065
4622
|
function bulkCreateTasks(inputs, db) {
|
|
4066
4623
|
const d = db || getDatabase();
|
|
4067
4624
|
const tempIdToRealId = new Map;
|
|
@@ -4155,6 +4712,10 @@ function getOverdueTasks(projectId, db) {
|
|
|
4155
4712
|
const rows = d.query(query).all(...params);
|
|
4156
4713
|
return rows.map(rowToTask);
|
|
4157
4714
|
}
|
|
4715
|
+
function logCost(taskId, tokens, usd, db) {
|
|
4716
|
+
const d = db || getDatabase();
|
|
4717
|
+
d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
|
|
4718
|
+
}
|
|
4158
4719
|
// src/db/plans.ts
|
|
4159
4720
|
init_types();
|
|
4160
4721
|
init_database();
|
|
@@ -6266,7 +6827,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
|
|
|
6266
6827
|
}
|
|
6267
6828
|
return { pushed, pulled, errors };
|
|
6268
6829
|
}
|
|
6269
|
-
//
|
|
6830
|
+
// node_modules/@hasna/cloud/dist/index.js
|
|
6270
6831
|
import { createRequire } from "module";
|
|
6271
6832
|
import { Database as Database2 } from "bun:sqlite";
|
|
6272
6833
|
import {
|
|
@@ -6290,9 +6851,9 @@ import { homedir as homedir5, platform } from "os";
|
|
|
6290
6851
|
var __create = Object.create;
|
|
6291
6852
|
var __getProtoOf = Object.getPrototypeOf;
|
|
6292
6853
|
var __defProp2 = Object.defineProperty;
|
|
6293
|
-
var
|
|
6294
|
-
var
|
|
6295
|
-
function
|
|
6854
|
+
var __getOwnPropNames2 = Object.getOwnPropertyNames;
|
|
6855
|
+
var __hasOwnProp2 = Object.prototype.hasOwnProperty;
|
|
6856
|
+
function __accessProp2(key) {
|
|
6296
6857
|
return this[key];
|
|
6297
6858
|
}
|
|
6298
6859
|
var __toESMCache_node;
|
|
@@ -6307,10 +6868,10 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
6307
6868
|
}
|
|
6308
6869
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
6309
6870
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target;
|
|
6310
|
-
for (let key of
|
|
6311
|
-
if (!
|
|
6871
|
+
for (let key of __getOwnPropNames2(mod))
|
|
6872
|
+
if (!__hasOwnProp2.call(to, key))
|
|
6312
6873
|
__defProp2(to, key, {
|
|
6313
|
-
get:
|
|
6874
|
+
get: __accessProp2.bind(mod, key),
|
|
6314
6875
|
enumerable: true
|
|
6315
6876
|
});
|
|
6316
6877
|
if (canCache)
|
|
@@ -6318,9 +6879,9 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
6318
6879
|
return to;
|
|
6319
6880
|
};
|
|
6320
6881
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
6321
|
-
var
|
|
6322
|
-
function
|
|
6323
|
-
this[name] =
|
|
6882
|
+
var __returnValue2 = (v) => v;
|
|
6883
|
+
function __exportSetter2(name, newValue) {
|
|
6884
|
+
this[name] = __returnValue2.bind(null, newValue);
|
|
6324
6885
|
}
|
|
6325
6886
|
var __export2 = (target, all) => {
|
|
6326
6887
|
for (var name in all)
|
|
@@ -6328,7 +6889,7 @@ var __export2 = (target, all) => {
|
|
|
6328
6889
|
get: all[name],
|
|
6329
6890
|
enumerable: true,
|
|
6330
6891
|
configurable: true,
|
|
6331
|
-
set:
|
|
6892
|
+
set: __exportSetter2.bind(all, name)
|
|
6332
6893
|
});
|
|
6333
6894
|
};
|
|
6334
6895
|
var __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -16459,6 +17020,45 @@ var PG_MIGRATIONS = [
|
|
|
16459
17020
|
ALTER TABLE task_dependencies ADD COLUMN external_project_id TEXT;
|
|
16460
17021
|
ALTER TABLE task_dependencies ADD COLUMN external_task_id TEXT;
|
|
16461
17022
|
INSERT INTO _migrations (id) VALUES (43) ON CONFLICT DO NOTHING;
|
|
17023
|
+
`,
|
|
17024
|
+
`
|
|
17025
|
+
CREATE TABLE IF NOT EXISTS task_checkpoints (
|
|
17026
|
+
id TEXT PRIMARY KEY,
|
|
17027
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
17028
|
+
agent_id TEXT,
|
|
17029
|
+
step TEXT NOT NULL,
|
|
17030
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed', 'skipped')),
|
|
17031
|
+
data TEXT DEFAULT '{}',
|
|
17032
|
+
error TEXT,
|
|
17033
|
+
attempt INTEGER NOT NULL DEFAULT 1,
|
|
17034
|
+
max_attempts INTEGER NOT NULL DEFAULT 1,
|
|
17035
|
+
started_at TIMESTAMPTZ,
|
|
17036
|
+
completed_at TIMESTAMPTZ,
|
|
17037
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
17038
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
17039
|
+
);
|
|
17040
|
+
CREATE INDEX IF NOT EXISTS idx_task_checkpoints_task ON task_checkpoints(task_id);
|
|
17041
|
+
CREATE INDEX IF NOT EXISTS idx_task_checkpoints_status ON task_checkpoints(status);
|
|
17042
|
+
|
|
17043
|
+
CREATE TABLE IF NOT EXISTS task_heartbeats (
|
|
17044
|
+
id TEXT PRIMARY KEY,
|
|
17045
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
17046
|
+
agent_id TEXT,
|
|
17047
|
+
step TEXT,
|
|
17048
|
+
message TEXT,
|
|
17049
|
+
progress REAL CHECK(progress >= 0 AND progress <= 1),
|
|
17050
|
+
meta TEXT DEFAULT '{}',
|
|
17051
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
17052
|
+
);
|
|
17053
|
+
CREATE INDEX IF NOT EXISTS idx_task_heartbeats_task ON task_heartbeats(task_id);
|
|
17054
|
+
CREATE INDEX IF NOT EXISTS idx_task_heartbeats_agent ON task_heartbeats(agent_id);
|
|
17055
|
+
|
|
17056
|
+
ALTER TABLE tasks ADD COLUMN runner_id TEXT;
|
|
17057
|
+
ALTER TABLE tasks ADD COLUMN runner_started_at TIMESTAMPTZ;
|
|
17058
|
+
ALTER TABLE tasks ADD COLUMN runner_completed_at TIMESTAMPTZ;
|
|
17059
|
+
ALTER TABLE tasks ADD COLUMN current_step TEXT;
|
|
17060
|
+
ALTER TABLE tasks ADD COLUMN total_steps INTEGER;
|
|
17061
|
+
INSERT INTO _migrations (id) VALUES (44) ON CONFLICT DO NOTHING;
|
|
16462
17062
|
`
|
|
16463
17063
|
];
|
|
16464
17064
|
|
|
@@ -16772,15 +17372,24 @@ function renderBurndownChart(total, days) {
|
|
|
16772
17372
|
`);
|
|
16773
17373
|
}
|
|
16774
17374
|
// src/lib/github.ts
|
|
16775
|
-
import {
|
|
17375
|
+
import { execFileSync } from "child_process";
|
|
16776
17376
|
function parseGitHubUrl(url) {
|
|
16777
|
-
const match = url.match(/github\.com\/([
|
|
17377
|
+
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
17378
|
if (!match)
|
|
16779
17379
|
return null;
|
|
16780
17380
|
return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
|
|
16781
17381
|
}
|
|
17382
|
+
function isSafeOwnerRepo(value) {
|
|
17383
|
+
return /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value) && value.length <= 39;
|
|
17384
|
+
}
|
|
16782
17385
|
function fetchGitHubIssue(owner, repo, number) {
|
|
16783
|
-
|
|
17386
|
+
if (!isSafeOwnerRepo(owner))
|
|
17387
|
+
throw new Error(`Invalid GitHub owner: ${owner}`);
|
|
17388
|
+
if (!isSafeOwnerRepo(repo))
|
|
17389
|
+
throw new Error(`Invalid GitHub repo: ${repo}`);
|
|
17390
|
+
if (!Number.isInteger(number) || number < 1)
|
|
17391
|
+
throw new Error(`Invalid issue number: ${number}`);
|
|
17392
|
+
const json = execFileSync("gh", ["api", `repos/${owner}/${repo}/issues/${number}`], { encoding: "utf-8", timeout: 15000 });
|
|
16784
17393
|
const data = JSON.parse(json);
|
|
16785
17394
|
return {
|
|
16786
17395
|
number: data.number,
|