@hasna/todos 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +355 -39
- package/dist/index.d.ts +2 -0
- package/dist/index.js +126 -1
- package/dist/mcp/index.js +34 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2332,6 +2332,36 @@ var init_database = __esm(() => {
|
|
|
2332
2332
|
);
|
|
2333
2333
|
CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash);
|
|
2334
2334
|
INSERT OR IGNORE INTO _migrations (id) VALUES (5);
|
|
2335
|
+
`,
|
|
2336
|
+
`
|
|
2337
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
2338
|
+
id TEXT PRIMARY KEY,
|
|
2339
|
+
entity_type TEXT NOT NULL CHECK(entity_type IN ('task', 'plan', 'project', 'api_key', 'comment')),
|
|
2340
|
+
entity_id TEXT NOT NULL,
|
|
2341
|
+
action TEXT NOT NULL CHECK(action IN ('create', 'update', 'delete', 'start', 'complete', 'lock', 'unlock')),
|
|
2342
|
+
actor TEXT,
|
|
2343
|
+
changes TEXT,
|
|
2344
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2345
|
+
);
|
|
2346
|
+
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log(entity_type, entity_id);
|
|
2347
|
+
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log(created_at);
|
|
2348
|
+
|
|
2349
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
2350
|
+
id TEXT PRIMARY KEY,
|
|
2351
|
+
url TEXT NOT NULL,
|
|
2352
|
+
events TEXT NOT NULL DEFAULT '[]',
|
|
2353
|
+
secret TEXT,
|
|
2354
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
2355
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2356
|
+
);
|
|
2357
|
+
|
|
2358
|
+
CREATE TABLE IF NOT EXISTS rate_limits (
|
|
2359
|
+
key TEXT PRIMARY KEY,
|
|
2360
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
2361
|
+
window_start TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2362
|
+
);
|
|
2363
|
+
|
|
2364
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (6);
|
|
2335
2365
|
`
|
|
2336
2366
|
];
|
|
2337
2367
|
});
|
|
@@ -2539,9 +2569,12 @@ function listTasks(filter = {}, db) {
|
|
|
2539
2569
|
params.push(filter.plan_id);
|
|
2540
2570
|
}
|
|
2541
2571
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2572
|
+
const limitVal = filter.limit || 100;
|
|
2573
|
+
const offsetVal = filter.offset || 0;
|
|
2574
|
+
params.push(limitVal, offsetVal);
|
|
2542
2575
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY
|
|
2543
2576
|
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
2544
|
-
created_at DESC
|
|
2577
|
+
created_at DESC LIMIT ? OFFSET ?`).all(...params);
|
|
2545
2578
|
return rows.map(rowToTask);
|
|
2546
2579
|
}
|
|
2547
2580
|
function updateTask(id, input, db) {
|
|
@@ -8082,6 +8115,163 @@ var init_api_keys = __esm(() => {
|
|
|
8082
8115
|
init_database();
|
|
8083
8116
|
});
|
|
8084
8117
|
|
|
8118
|
+
// src/db/audit.ts
|
|
8119
|
+
function rowToEntry(row) {
|
|
8120
|
+
return {
|
|
8121
|
+
...row,
|
|
8122
|
+
changes: row.changes ? JSON.parse(row.changes) : null
|
|
8123
|
+
};
|
|
8124
|
+
}
|
|
8125
|
+
function logAudit(entityType, entityId, action, actor, changes, db) {
|
|
8126
|
+
const d = db || getDatabase();
|
|
8127
|
+
d.run(`INSERT INTO audit_log (id, entity_type, entity_id, action, actor, changes, created_at)
|
|
8128
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [uuid(), entityType, entityId, action, actor || null, changes ? JSON.stringify(changes) : null, now()]);
|
|
8129
|
+
}
|
|
8130
|
+
function getAuditLog(entityType, entityId, limit = 50, offset = 0, db) {
|
|
8131
|
+
const d = db || getDatabase();
|
|
8132
|
+
const conditions = [];
|
|
8133
|
+
const params = [];
|
|
8134
|
+
if (entityType) {
|
|
8135
|
+
conditions.push("entity_type = ?");
|
|
8136
|
+
params.push(entityType);
|
|
8137
|
+
}
|
|
8138
|
+
if (entityId) {
|
|
8139
|
+
conditions.push("entity_id = ?");
|
|
8140
|
+
params.push(entityId);
|
|
8141
|
+
}
|
|
8142
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
8143
|
+
params.push(limit, offset);
|
|
8144
|
+
const rows = d.query(`SELECT * FROM audit_log ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params);
|
|
8145
|
+
return rows.map(rowToEntry);
|
|
8146
|
+
}
|
|
8147
|
+
var init_audit = __esm(() => {
|
|
8148
|
+
init_database();
|
|
8149
|
+
});
|
|
8150
|
+
|
|
8151
|
+
// src/db/webhooks.ts
|
|
8152
|
+
function rowToWebhook(row) {
|
|
8153
|
+
return {
|
|
8154
|
+
...row,
|
|
8155
|
+
events: JSON.parse(row.events),
|
|
8156
|
+
active: row.active === 1
|
|
8157
|
+
};
|
|
8158
|
+
}
|
|
8159
|
+
function createWebhook(input, db) {
|
|
8160
|
+
const d = db || getDatabase();
|
|
8161
|
+
const id = uuid();
|
|
8162
|
+
d.run(`INSERT INTO webhooks (id, url, events, secret, created_at)
|
|
8163
|
+
VALUES (?, ?, ?, ?, ?)`, [id, input.url, JSON.stringify(input.events || []), input.secret || null, now()]);
|
|
8164
|
+
return getWebhook(id, d);
|
|
8165
|
+
}
|
|
8166
|
+
function getWebhook(id, db) {
|
|
8167
|
+
const d = db || getDatabase();
|
|
8168
|
+
const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
|
|
8169
|
+
return row ? rowToWebhook(row) : null;
|
|
8170
|
+
}
|
|
8171
|
+
function listWebhooks(db) {
|
|
8172
|
+
const d = db || getDatabase();
|
|
8173
|
+
return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
|
|
8174
|
+
}
|
|
8175
|
+
function deleteWebhook(id, db) {
|
|
8176
|
+
const d = db || getDatabase();
|
|
8177
|
+
return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
|
|
8178
|
+
}
|
|
8179
|
+
async function dispatchWebhooks(event, payload, db) {
|
|
8180
|
+
const d = db || getDatabase();
|
|
8181
|
+
const rows = d.query("SELECT * FROM webhooks WHERE active = 1").all();
|
|
8182
|
+
const webhooks = rows.map(rowToWebhook).filter((w) => w.events.length === 0 || w.events.includes(event));
|
|
8183
|
+
for (const webhook of webhooks) {
|
|
8184
|
+
try {
|
|
8185
|
+
const headers = { "Content-Type": "application/json" };
|
|
8186
|
+
if (webhook.secret) {
|
|
8187
|
+
const encoder = new TextEncoder;
|
|
8188
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(webhook.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
8189
|
+
const body = JSON.stringify({ event, data: payload, timestamp: now() });
|
|
8190
|
+
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
8191
|
+
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
8192
|
+
fetch(webhook.url, {
|
|
8193
|
+
method: "POST",
|
|
8194
|
+
headers,
|
|
8195
|
+
body
|
|
8196
|
+
}).catch(() => {});
|
|
8197
|
+
} else {
|
|
8198
|
+
fetch(webhook.url, {
|
|
8199
|
+
method: "POST",
|
|
8200
|
+
headers,
|
|
8201
|
+
body: JSON.stringify({ event, data: payload, timestamp: now() })
|
|
8202
|
+
}).catch(() => {});
|
|
8203
|
+
}
|
|
8204
|
+
} catch {}
|
|
8205
|
+
}
|
|
8206
|
+
}
|
|
8207
|
+
var init_webhooks = __esm(() => {
|
|
8208
|
+
init_database();
|
|
8209
|
+
});
|
|
8210
|
+
|
|
8211
|
+
// src/lib/rate-limit.ts
|
|
8212
|
+
function cleanup() {
|
|
8213
|
+
const now2 = Date.now();
|
|
8214
|
+
if (now2 - lastCleanup < CLEANUP_INTERVAL)
|
|
8215
|
+
return;
|
|
8216
|
+
lastCleanup = now2;
|
|
8217
|
+
for (const [key, entry] of windows) {
|
|
8218
|
+
if (entry.resetAt < now2)
|
|
8219
|
+
windows.delete(key);
|
|
8220
|
+
}
|
|
8221
|
+
}
|
|
8222
|
+
function checkRateLimit(key, maxRequests, windowMs) {
|
|
8223
|
+
cleanup();
|
|
8224
|
+
const now2 = Date.now();
|
|
8225
|
+
const entry = windows.get(key);
|
|
8226
|
+
if (!entry || entry.resetAt < now2) {
|
|
8227
|
+
windows.set(key, { count: 1, resetAt: now2 + windowMs });
|
|
8228
|
+
return { allowed: true, remaining: maxRequests - 1, resetAt: now2 + windowMs };
|
|
8229
|
+
}
|
|
8230
|
+
entry.count++;
|
|
8231
|
+
if (entry.count > maxRequests) {
|
|
8232
|
+
return { allowed: false, remaining: 0, resetAt: entry.resetAt };
|
|
8233
|
+
}
|
|
8234
|
+
return { allowed: true, remaining: maxRequests - entry.count, resetAt: entry.resetAt };
|
|
8235
|
+
}
|
|
8236
|
+
var windows, CLEANUP_INTERVAL = 60000, lastCleanup;
|
|
8237
|
+
var init_rate_limit = __esm(() => {
|
|
8238
|
+
windows = new Map;
|
|
8239
|
+
lastCleanup = Date.now();
|
|
8240
|
+
});
|
|
8241
|
+
|
|
8242
|
+
// src/lib/env.ts
|
|
8243
|
+
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
|
|
8244
|
+
import { join as join6 } from "path";
|
|
8245
|
+
function loadEnv() {
|
|
8246
|
+
const paths = [
|
|
8247
|
+
join6(process.cwd(), ".env"),
|
|
8248
|
+
join6(process.cwd(), ".env.local")
|
|
8249
|
+
];
|
|
8250
|
+
for (const path of paths) {
|
|
8251
|
+
if (!existsSync6(path))
|
|
8252
|
+
continue;
|
|
8253
|
+
const content = readFileSync2(path, "utf-8");
|
|
8254
|
+
for (const line of content.split(`
|
|
8255
|
+
`)) {
|
|
8256
|
+
const trimmed = line.trim();
|
|
8257
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
8258
|
+
continue;
|
|
8259
|
+
const eqIdx = trimmed.indexOf("=");
|
|
8260
|
+
if (eqIdx === -1)
|
|
8261
|
+
continue;
|
|
8262
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
8263
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
8264
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
8265
|
+
value = value.slice(1, -1);
|
|
8266
|
+
}
|
|
8267
|
+
if (!process.env[key]) {
|
|
8268
|
+
process.env[key] = value;
|
|
8269
|
+
}
|
|
8270
|
+
}
|
|
8271
|
+
}
|
|
8272
|
+
}
|
|
8273
|
+
var init_env = () => {};
|
|
8274
|
+
|
|
8085
8275
|
// src/server/serve.ts
|
|
8086
8276
|
var exports_serve = {};
|
|
8087
8277
|
__export(exports_serve, {
|
|
@@ -8089,27 +8279,27 @@ __export(exports_serve, {
|
|
|
8089
8279
|
createFetchHandler: () => createFetchHandler
|
|
8090
8280
|
});
|
|
8091
8281
|
import { execSync } from "child_process";
|
|
8092
|
-
import { existsSync as
|
|
8093
|
-
import { join as
|
|
8282
|
+
import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
|
|
8283
|
+
import { join as join7, dirname as dirname2, extname } from "path";
|
|
8094
8284
|
import { fileURLToPath } from "url";
|
|
8095
8285
|
function resolveDashboardDir() {
|
|
8096
8286
|
const candidates = [];
|
|
8097
8287
|
try {
|
|
8098
8288
|
const scriptDir = dirname2(fileURLToPath(import.meta.url));
|
|
8099
|
-
candidates.push(
|
|
8100
|
-
candidates.push(
|
|
8289
|
+
candidates.push(join7(scriptDir, "..", "dashboard", "dist"));
|
|
8290
|
+
candidates.push(join7(scriptDir, "..", "..", "dashboard", "dist"));
|
|
8101
8291
|
} catch {}
|
|
8102
8292
|
if (process.argv[1]) {
|
|
8103
8293
|
const mainDir = dirname2(process.argv[1]);
|
|
8104
|
-
candidates.push(
|
|
8105
|
-
candidates.push(
|
|
8294
|
+
candidates.push(join7(mainDir, "..", "dashboard", "dist"));
|
|
8295
|
+
candidates.push(join7(mainDir, "..", "..", "dashboard", "dist"));
|
|
8106
8296
|
}
|
|
8107
|
-
candidates.push(
|
|
8297
|
+
candidates.push(join7(process.cwd(), "dashboard", "dist"));
|
|
8108
8298
|
for (const candidate of candidates) {
|
|
8109
|
-
if (
|
|
8299
|
+
if (existsSync7(candidate))
|
|
8110
8300
|
return candidate;
|
|
8111
8301
|
}
|
|
8112
|
-
return
|
|
8302
|
+
return join7(process.cwd(), "dashboard", "dist");
|
|
8113
8303
|
}
|
|
8114
8304
|
function randomPort() {
|
|
8115
8305
|
return 20000 + Math.floor(Math.random() * 20000);
|
|
@@ -8126,8 +8316,8 @@ function json(data, status = 200, port) {
|
|
|
8126
8316
|
}
|
|
8127
8317
|
function getPackageVersion() {
|
|
8128
8318
|
try {
|
|
8129
|
-
const pkgPath =
|
|
8130
|
-
return JSON.parse(
|
|
8319
|
+
const pkgPath = join7(dirname2(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
8320
|
+
return JSON.parse(readFileSync3(pkgPath, "utf-8")).version || "0.0.0";
|
|
8131
8321
|
} catch {
|
|
8132
8322
|
return "0.0.0";
|
|
8133
8323
|
}
|
|
@@ -8140,7 +8330,7 @@ async function parseJsonBody(req) {
|
|
|
8140
8330
|
}
|
|
8141
8331
|
}
|
|
8142
8332
|
function serveStaticFile(filePath) {
|
|
8143
|
-
if (!
|
|
8333
|
+
if (!existsSync7(filePath))
|
|
8144
8334
|
return null;
|
|
8145
8335
|
const ext = extname(filePath);
|
|
8146
8336
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
@@ -8149,8 +8339,9 @@ function serveStaticFile(filePath) {
|
|
|
8149
8339
|
});
|
|
8150
8340
|
}
|
|
8151
8341
|
function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
8342
|
+
loadEnv();
|
|
8152
8343
|
const dir = dashboardDir || resolveDashboardDir();
|
|
8153
|
-
const hasDashboard = dashboardExists ??
|
|
8344
|
+
const hasDashboard = dashboardExists ?? existsSync7(dir);
|
|
8154
8345
|
return async (req) => {
|
|
8155
8346
|
const url = new URL(req.url);
|
|
8156
8347
|
let path = url.pathname;
|
|
@@ -8173,18 +8364,42 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8173
8364
|
}
|
|
8174
8365
|
}
|
|
8175
8366
|
}
|
|
8367
|
+
if (path.startsWith("/api/")) {
|
|
8368
|
+
const rateLimitKey = req.headers.get("authorization") || req.headers.get("x-forwarded-for") || "anonymous";
|
|
8369
|
+
const rateResult = checkRateLimit(rateLimitKey, 100, 60000);
|
|
8370
|
+
if (!rateResult.allowed) {
|
|
8371
|
+
return new Response(JSON.stringify({ error: "Rate limit exceeded. Try again later." }), {
|
|
8372
|
+
status: 429,
|
|
8373
|
+
headers: {
|
|
8374
|
+
"Content-Type": "application/json",
|
|
8375
|
+
"Retry-After": String(Math.ceil((rateResult.resetAt - Date.now()) / 1000)),
|
|
8376
|
+
"X-RateLimit-Remaining": "0",
|
|
8377
|
+
...SECURITY_HEADERS
|
|
8378
|
+
}
|
|
8379
|
+
});
|
|
8380
|
+
}
|
|
8381
|
+
}
|
|
8176
8382
|
if (path === "/api/tasks" && method === "GET") {
|
|
8177
8383
|
try {
|
|
8178
8384
|
const filter = {};
|
|
8179
8385
|
const status = url.searchParams.get("status");
|
|
8180
8386
|
const priority = url.searchParams.get("priority");
|
|
8181
8387
|
const projectId = url.searchParams.get("project_id");
|
|
8388
|
+
const planId = url.searchParams.get("plan_id");
|
|
8182
8389
|
if (status)
|
|
8183
8390
|
filter.status = status;
|
|
8184
8391
|
if (priority)
|
|
8185
8392
|
filter.priority = priority;
|
|
8186
8393
|
if (projectId)
|
|
8187
8394
|
filter.project_id = projectId;
|
|
8395
|
+
if (planId)
|
|
8396
|
+
filter.plan_id = planId;
|
|
8397
|
+
const limit = parseInt(url.searchParams.get("limit") || "100", 10);
|
|
8398
|
+
const offset = parseInt(url.searchParams.get("offset") || "0", 10);
|
|
8399
|
+
if (limit)
|
|
8400
|
+
filter.limit = Math.min(limit, 500);
|
|
8401
|
+
if (offset)
|
|
8402
|
+
filter.offset = offset;
|
|
8188
8403
|
const tasks = listTasks(filter);
|
|
8189
8404
|
const projectCache = new Map;
|
|
8190
8405
|
const planCache = new Map;
|
|
@@ -8262,6 +8477,8 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8262
8477
|
agent_id: parsed.data.agent_id,
|
|
8263
8478
|
status: parsed.data.status
|
|
8264
8479
|
});
|
|
8480
|
+
logAudit("task", task.id, "create", parsed.data.agent_id);
|
|
8481
|
+
dispatchWebhooks("task.created", task);
|
|
8265
8482
|
return json(task, 201, port);
|
|
8266
8483
|
} catch (e) {
|
|
8267
8484
|
return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
|
|
@@ -8295,6 +8512,8 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8295
8512
|
tags: parsed.data.tags,
|
|
8296
8513
|
metadata: parsed.data.metadata
|
|
8297
8514
|
});
|
|
8515
|
+
logAudit("task", id, "update", undefined, parsed.data);
|
|
8516
|
+
dispatchWebhooks("task.updated", task);
|
|
8298
8517
|
return json(task, 200, port);
|
|
8299
8518
|
} catch (e) {
|
|
8300
8519
|
const status = e instanceof Error && e.name === "VersionConflictError" ? 409 : 500;
|
|
@@ -8308,6 +8527,8 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8308
8527
|
const deleted = deleteTask(id);
|
|
8309
8528
|
if (!deleted)
|
|
8310
8529
|
return json({ error: "Task not found" }, 404, port);
|
|
8530
|
+
logAudit("task", id, "delete");
|
|
8531
|
+
dispatchWebhooks("task.deleted", { id });
|
|
8311
8532
|
return json({ deleted: true }, 200, port);
|
|
8312
8533
|
} catch (e) {
|
|
8313
8534
|
return json({ error: e instanceof Error ? e.message : "Failed to delete task" }, 500, port);
|
|
@@ -8325,6 +8546,7 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8325
8546
|
return json({ error: "Invalid request body" }, 400, port);
|
|
8326
8547
|
const agentId = parsed.data.agent_id || "dashboard";
|
|
8327
8548
|
const task = startTask(id, agentId);
|
|
8549
|
+
logAudit("task", id, "start", agentId);
|
|
8328
8550
|
return json(task, 200, port);
|
|
8329
8551
|
} catch (e) {
|
|
8330
8552
|
const status = e instanceof Error && e.name === "TaskNotFoundError" ? 404 : e instanceof Error && e.name === "LockError" ? 409 : 500;
|
|
@@ -8343,6 +8565,7 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8343
8565
|
return json({ error: "Invalid request body" }, 400, port);
|
|
8344
8566
|
const agentId = parsed.data.agent_id;
|
|
8345
8567
|
const task = completeTask(id, agentId);
|
|
8568
|
+
logAudit("task", id, "complete", agentId);
|
|
8346
8569
|
return json(task, 200, port);
|
|
8347
8570
|
} catch (e) {
|
|
8348
8571
|
const status = e instanceof Error && e.name === "TaskNotFoundError" ? 404 : e instanceof Error && e.name === "LockError" ? 409 : 500;
|
|
@@ -8416,6 +8639,7 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8416
8639
|
description: parsed.data.description,
|
|
8417
8640
|
task_list_id: parsed.data.task_list_id
|
|
8418
8641
|
});
|
|
8642
|
+
logAudit("project", project.id, "create");
|
|
8419
8643
|
return json(project, 201, port);
|
|
8420
8644
|
} catch (e) {
|
|
8421
8645
|
return json({ error: e instanceof Error ? e.message : "Failed to create project" }, 500, port);
|
|
@@ -8497,6 +8721,8 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8497
8721
|
return json({ error: "Invalid request body" }, 400, port);
|
|
8498
8722
|
}
|
|
8499
8723
|
const plan = createPlan(parsed.data);
|
|
8724
|
+
logAudit("plan", plan.id, "create");
|
|
8725
|
+
dispatchWebhooks("plan.created", plan);
|
|
8500
8726
|
return json(plan, 201, port);
|
|
8501
8727
|
} catch (e) {
|
|
8502
8728
|
return json({ error: e instanceof Error ? e.message : "Failed to create plan" }, 500, port);
|
|
@@ -8529,6 +8755,7 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8529
8755
|
return json({ error: "Invalid request body" }, 400, port);
|
|
8530
8756
|
}
|
|
8531
8757
|
const plan = updatePlan(id, parsed.data);
|
|
8758
|
+
logAudit("plan", id, "update", undefined, parsed.data);
|
|
8532
8759
|
return json(plan, 200, port);
|
|
8533
8760
|
} catch (e) {
|
|
8534
8761
|
const status = e instanceof Error && e.name === "PlanNotFoundError" ? 404 : 500;
|
|
@@ -8542,11 +8769,45 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8542
8769
|
const deleted = deletePlan(id);
|
|
8543
8770
|
if (!deleted)
|
|
8544
8771
|
return json({ error: "Plan not found" }, 404, port);
|
|
8772
|
+
logAudit("plan", id, "delete");
|
|
8545
8773
|
return json({ deleted: true }, 200, port);
|
|
8546
8774
|
} catch (e) {
|
|
8547
8775
|
return json({ error: e instanceof Error ? e.message : "Failed to delete plan" }, 500, port);
|
|
8548
8776
|
}
|
|
8549
8777
|
}
|
|
8778
|
+
const projectGetMatch = path.match(/^\/api\/projects\/([^/]+)$/);
|
|
8779
|
+
if (projectGetMatch && method === "GET") {
|
|
8780
|
+
try {
|
|
8781
|
+
const id = projectGetMatch[1];
|
|
8782
|
+
const project = getProject(id);
|
|
8783
|
+
if (!project)
|
|
8784
|
+
return json({ error: "Project not found" }, 404, port);
|
|
8785
|
+
return json(project, 200, port);
|
|
8786
|
+
} catch (e) {
|
|
8787
|
+
return json({ error: e instanceof Error ? e.message : "Failed to get project" }, 500, port);
|
|
8788
|
+
}
|
|
8789
|
+
}
|
|
8790
|
+
const projectPatchMatch = path.match(/^\/api\/projects\/([^/]+)$/);
|
|
8791
|
+
if (projectPatchMatch && method === "PATCH") {
|
|
8792
|
+
try {
|
|
8793
|
+
const id = projectPatchMatch[1];
|
|
8794
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
8795
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
8796
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
8797
|
+
const body = await parseJsonBody(req);
|
|
8798
|
+
if (!body)
|
|
8799
|
+
return json({ error: "Invalid JSON" }, 400, port);
|
|
8800
|
+
const project = updateProject(id, {
|
|
8801
|
+
name: typeof body.name === "string" ? body.name : undefined,
|
|
8802
|
+
description: typeof body.description === "string" ? body.description : body.description === null ? "" : undefined,
|
|
8803
|
+
task_list_id: typeof body.task_list_id === "string" ? body.task_list_id : body.task_list_id === null ? "" : undefined
|
|
8804
|
+
});
|
|
8805
|
+
return json(project, 200, port);
|
|
8806
|
+
} catch (e) {
|
|
8807
|
+
const status = e instanceof Error && e.name === "ProjectNotFoundError" ? 404 : 500;
|
|
8808
|
+
return json({ error: e instanceof Error ? e.message : "Failed to update project" }, status, port);
|
|
8809
|
+
}
|
|
8810
|
+
}
|
|
8550
8811
|
const projectDeleteMatch = path.match(/^\/api\/projects\/([^/]+)$/);
|
|
8551
8812
|
if (projectDeleteMatch && method === "DELETE") {
|
|
8552
8813
|
try {
|
|
@@ -8583,6 +8844,7 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8583
8844
|
return json({ error: "Invalid request body" }, 400, port);
|
|
8584
8845
|
}
|
|
8585
8846
|
const apiKey = await createApiKey(parsed.data);
|
|
8847
|
+
logAudit("api_key", apiKey.id, "create");
|
|
8586
8848
|
return json(apiKey, 201, port);
|
|
8587
8849
|
} catch (e) {
|
|
8588
8850
|
return json({ error: e instanceof Error ? e.message : "Failed to create API key" }, 500, port);
|
|
@@ -8608,6 +8870,56 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8608
8870
|
return json({ error: e instanceof Error ? e.message : "Failed to check auth status" }, 500, port);
|
|
8609
8871
|
}
|
|
8610
8872
|
}
|
|
8873
|
+
if (path === "/api/audit" && method === "GET") {
|
|
8874
|
+
try {
|
|
8875
|
+
const entityType = url.searchParams.get("entity_type") || undefined;
|
|
8876
|
+
const entityId = url.searchParams.get("entity_id") || undefined;
|
|
8877
|
+
const limit = parseInt(url.searchParams.get("limit") || "50", 10);
|
|
8878
|
+
const offset = parseInt(url.searchParams.get("offset") || "0", 10);
|
|
8879
|
+
const entries = getAuditLog(entityType, entityId, Math.min(limit, 200), offset);
|
|
8880
|
+
return json(entries, 200, port);
|
|
8881
|
+
} catch (e) {
|
|
8882
|
+
return json({ error: e instanceof Error ? e.message : "Failed to get audit log" }, 500, port);
|
|
8883
|
+
}
|
|
8884
|
+
}
|
|
8885
|
+
if (path === "/api/webhooks" && method === "GET") {
|
|
8886
|
+
try {
|
|
8887
|
+
const webhooks = listWebhooks();
|
|
8888
|
+
return json(webhooks, 200, port);
|
|
8889
|
+
} catch (e) {
|
|
8890
|
+
return json({ error: e instanceof Error ? e.message : "Failed to list webhooks" }, 500, port);
|
|
8891
|
+
}
|
|
8892
|
+
}
|
|
8893
|
+
if (path === "/api/webhooks" && method === "POST") {
|
|
8894
|
+
try {
|
|
8895
|
+
const body = await parseJsonBody(req);
|
|
8896
|
+
if (!body)
|
|
8897
|
+
return json({ error: "Invalid JSON" }, 400, port);
|
|
8898
|
+
if (!body.url || typeof body.url !== "string") {
|
|
8899
|
+
return json({ error: "Missing required field: url" }, 400, port);
|
|
8900
|
+
}
|
|
8901
|
+
const webhook = createWebhook({
|
|
8902
|
+
url: body.url,
|
|
8903
|
+
events: Array.isArray(body.events) ? body.events : undefined,
|
|
8904
|
+
secret: typeof body.secret === "string" ? body.secret : undefined
|
|
8905
|
+
});
|
|
8906
|
+
return json(webhook, 201, port);
|
|
8907
|
+
} catch (e) {
|
|
8908
|
+
return json({ error: e instanceof Error ? e.message : "Failed to create webhook" }, 500, port);
|
|
8909
|
+
}
|
|
8910
|
+
}
|
|
8911
|
+
const webhookDeleteMatch = path.match(/^\/api\/webhooks\/([^/]+)$/);
|
|
8912
|
+
if (webhookDeleteMatch && method === "DELETE") {
|
|
8913
|
+
try {
|
|
8914
|
+
const id = webhookDeleteMatch[1];
|
|
8915
|
+
const deleted = deleteWebhook(id);
|
|
8916
|
+
if (!deleted)
|
|
8917
|
+
return json({ error: "Webhook not found" }, 404, port);
|
|
8918
|
+
return json({ deleted: true }, 200, port);
|
|
8919
|
+
} catch (e) {
|
|
8920
|
+
return json({ error: e instanceof Error ? e.message : "Failed to delete webhook" }, 500, port);
|
|
8921
|
+
}
|
|
8922
|
+
}
|
|
8611
8923
|
if (method === "OPTIONS") {
|
|
8612
8924
|
return new Response(null, {
|
|
8613
8925
|
headers: {
|
|
@@ -8619,12 +8931,12 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8619
8931
|
}
|
|
8620
8932
|
if (hasDashboard && (method === "GET" || method === "HEAD")) {
|
|
8621
8933
|
if (path !== "/") {
|
|
8622
|
-
const filePath =
|
|
8934
|
+
const filePath = join7(dir, path);
|
|
8623
8935
|
const res2 = serveStaticFile(filePath);
|
|
8624
8936
|
if (res2)
|
|
8625
8937
|
return res2;
|
|
8626
8938
|
}
|
|
8627
|
-
const indexPath =
|
|
8939
|
+
const indexPath = join7(dir, "index.html");
|
|
8628
8940
|
const res = serveStaticFile(indexPath);
|
|
8629
8941
|
if (res)
|
|
8630
8942
|
return res;
|
|
@@ -8635,7 +8947,7 @@ function createFetchHandler(getPort, dashboardDir, dashboardExists) {
|
|
|
8635
8947
|
async function startServer(port, options) {
|
|
8636
8948
|
const shouldOpen = options?.open ?? true;
|
|
8637
8949
|
const dashboardDir = resolveDashboardDir();
|
|
8638
|
-
const dashboardExists =
|
|
8950
|
+
const dashboardExists = existsSync7(dashboardDir);
|
|
8639
8951
|
if (!dashboardExists) {
|
|
8640
8952
|
console.error(`
|
|
8641
8953
|
Dashboard not found at: ${dashboardDir}`);
|
|
@@ -8699,6 +9011,10 @@ var init_serve = __esm(() => {
|
|
|
8699
9011
|
init_comments();
|
|
8700
9012
|
init_api_keys();
|
|
8701
9013
|
init_search();
|
|
9014
|
+
init_audit();
|
|
9015
|
+
init_webhooks();
|
|
9016
|
+
init_rate_limit();
|
|
9017
|
+
init_env();
|
|
8702
9018
|
MIME_TYPES = {
|
|
8703
9019
|
".html": "text/html; charset=utf-8",
|
|
8704
9020
|
".js": "application/javascript",
|
|
@@ -9854,13 +10170,13 @@ init_sync();
|
|
|
9854
10170
|
init_config();
|
|
9855
10171
|
import chalk from "chalk";
|
|
9856
10172
|
import { execSync as execSync2 } from "child_process";
|
|
9857
|
-
import { existsSync as
|
|
9858
|
-
import { basename, dirname as dirname3, join as
|
|
10173
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
10174
|
+
import { basename, dirname as dirname3, join as join8, resolve as resolve2 } from "path";
|
|
9859
10175
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9860
10176
|
function getPackageVersion2() {
|
|
9861
10177
|
try {
|
|
9862
|
-
const pkgPath =
|
|
9863
|
-
return JSON.parse(
|
|
10178
|
+
const pkgPath = join8(dirname3(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
|
|
10179
|
+
return JSON.parse(readFileSync4(pkgPath, "utf-8")).version || "0.0.0";
|
|
9864
10180
|
} catch {
|
|
9865
10181
|
return "0.0.0";
|
|
9866
10182
|
}
|
|
@@ -10476,8 +10792,8 @@ hooks.command("install").description("Install Claude Code hooks for auto-sync").
|
|
|
10476
10792
|
if (p)
|
|
10477
10793
|
todosBin = p;
|
|
10478
10794
|
} catch {}
|
|
10479
|
-
const hooksDir =
|
|
10480
|
-
if (!
|
|
10795
|
+
const hooksDir = join8(process.cwd(), ".claude", "hooks");
|
|
10796
|
+
if (!existsSync8(hooksDir))
|
|
10481
10797
|
mkdirSync3(hooksDir, { recursive: true });
|
|
10482
10798
|
const hookScript = `#!/usr/bin/env bash
|
|
10483
10799
|
# Auto-generated by: todos hooks install
|
|
@@ -10502,11 +10818,11 @@ esac
|
|
|
10502
10818
|
|
|
10503
10819
|
exit 0
|
|
10504
10820
|
`;
|
|
10505
|
-
const hookPath =
|
|
10821
|
+
const hookPath = join8(hooksDir, "todos-sync.sh");
|
|
10506
10822
|
writeFileSync2(hookPath, hookScript);
|
|
10507
10823
|
execSync2(`chmod +x "${hookPath}"`);
|
|
10508
10824
|
console.log(chalk.green(`Hook script created: ${hookPath}`));
|
|
10509
|
-
const settingsPath =
|
|
10825
|
+
const settingsPath = join8(process.cwd(), ".claude", "settings.json");
|
|
10510
10826
|
const settings = readJsonFile2(settingsPath);
|
|
10511
10827
|
if (!settings["hooks"]) {
|
|
10512
10828
|
settings["hooks"] = {};
|
|
@@ -10553,40 +10869,40 @@ function getMcpBinaryPath() {
|
|
|
10553
10869
|
if (p)
|
|
10554
10870
|
return p;
|
|
10555
10871
|
} catch {}
|
|
10556
|
-
const bunBin =
|
|
10557
|
-
if (
|
|
10872
|
+
const bunBin = join8(HOME2, ".bun", "bin", "todos-mcp");
|
|
10873
|
+
if (existsSync8(bunBin))
|
|
10558
10874
|
return bunBin;
|
|
10559
10875
|
return "todos-mcp";
|
|
10560
10876
|
}
|
|
10561
10877
|
function readJsonFile2(path) {
|
|
10562
|
-
if (!
|
|
10878
|
+
if (!existsSync8(path))
|
|
10563
10879
|
return {};
|
|
10564
10880
|
try {
|
|
10565
|
-
return JSON.parse(
|
|
10881
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
10566
10882
|
} catch {
|
|
10567
10883
|
return {};
|
|
10568
10884
|
}
|
|
10569
10885
|
}
|
|
10570
10886
|
function writeJsonFile2(path, data) {
|
|
10571
10887
|
const dir = dirname3(path);
|
|
10572
|
-
if (!
|
|
10888
|
+
if (!existsSync8(dir))
|
|
10573
10889
|
mkdirSync3(dir, { recursive: true });
|
|
10574
10890
|
writeFileSync2(path, JSON.stringify(data, null, 2) + `
|
|
10575
10891
|
`);
|
|
10576
10892
|
}
|
|
10577
10893
|
function readTomlFile(path) {
|
|
10578
|
-
if (!
|
|
10894
|
+
if (!existsSync8(path))
|
|
10579
10895
|
return "";
|
|
10580
|
-
return
|
|
10896
|
+
return readFileSync4(path, "utf-8");
|
|
10581
10897
|
}
|
|
10582
10898
|
function writeTomlFile(path, content) {
|
|
10583
10899
|
const dir = dirname3(path);
|
|
10584
|
-
if (!
|
|
10900
|
+
if (!existsSync8(dir))
|
|
10585
10901
|
mkdirSync3(dir, { recursive: true });
|
|
10586
10902
|
writeFileSync2(path, content);
|
|
10587
10903
|
}
|
|
10588
10904
|
function registerClaude(binPath, global) {
|
|
10589
|
-
const configPath = global ?
|
|
10905
|
+
const configPath = global ? join8(HOME2, ".claude", ".mcp.json") : join8(process.cwd(), ".mcp.json");
|
|
10590
10906
|
const config = readJsonFile2(configPath);
|
|
10591
10907
|
if (!config["mcpServers"]) {
|
|
10592
10908
|
config["mcpServers"] = {};
|
|
@@ -10601,7 +10917,7 @@ function registerClaude(binPath, global) {
|
|
|
10601
10917
|
console.log(chalk.green(`Claude Code (${scope}): registered in ${configPath}`));
|
|
10602
10918
|
}
|
|
10603
10919
|
function unregisterClaude(global) {
|
|
10604
|
-
const configPath = global ?
|
|
10920
|
+
const configPath = global ? join8(HOME2, ".claude", ".mcp.json") : join8(process.cwd(), ".mcp.json");
|
|
10605
10921
|
const config = readJsonFile2(configPath);
|
|
10606
10922
|
const servers = config["mcpServers"];
|
|
10607
10923
|
if (!servers || !("todos" in servers)) {
|
|
@@ -10614,7 +10930,7 @@ function unregisterClaude(global) {
|
|
|
10614
10930
|
console.log(chalk.green(`Claude Code (${scope}): unregistered from ${configPath}`));
|
|
10615
10931
|
}
|
|
10616
10932
|
function registerCodex(binPath) {
|
|
10617
|
-
const configPath =
|
|
10933
|
+
const configPath = join8(HOME2, ".codex", "config.toml");
|
|
10618
10934
|
let content = readTomlFile(configPath);
|
|
10619
10935
|
content = removeTomlBlock(content, "mcp_servers.todos");
|
|
10620
10936
|
const block = `
|
|
@@ -10628,7 +10944,7 @@ args = []
|
|
|
10628
10944
|
console.log(chalk.green(`Codex CLI: registered in ${configPath}`));
|
|
10629
10945
|
}
|
|
10630
10946
|
function unregisterCodex() {
|
|
10631
|
-
const configPath =
|
|
10947
|
+
const configPath = join8(HOME2, ".codex", "config.toml");
|
|
10632
10948
|
let content = readTomlFile(configPath);
|
|
10633
10949
|
if (!content.includes("[mcp_servers.todos]")) {
|
|
10634
10950
|
console.log(chalk.dim(`Codex CLI: todos not found in ${configPath}`));
|
|
@@ -10661,7 +10977,7 @@ function removeTomlBlock(content, blockName) {
|
|
|
10661
10977
|
`);
|
|
10662
10978
|
}
|
|
10663
10979
|
function registerGemini(binPath) {
|
|
10664
|
-
const configPath =
|
|
10980
|
+
const configPath = join8(HOME2, ".gemini", "settings.json");
|
|
10665
10981
|
const config = readJsonFile2(configPath);
|
|
10666
10982
|
if (!config["mcpServers"]) {
|
|
10667
10983
|
config["mcpServers"] = {};
|
|
@@ -10675,7 +10991,7 @@ function registerGemini(binPath) {
|
|
|
10675
10991
|
console.log(chalk.green(`Gemini CLI: registered in ${configPath}`));
|
|
10676
10992
|
}
|
|
10677
10993
|
function unregisterGemini() {
|
|
10678
|
-
const configPath =
|
|
10994
|
+
const configPath = join8(HOME2, ".gemini", "settings.json");
|
|
10679
10995
|
const config = readJsonFile2(configPath);
|
|
10680
10996
|
const servers = config["mcpServers"];
|
|
10681
10997
|
if (!servers || !("todos" in servers)) {
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -177,6 +177,36 @@ var MIGRATIONS = [
|
|
|
177
177
|
);
|
|
178
178
|
CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash);
|
|
179
179
|
INSERT OR IGNORE INTO _migrations (id) VALUES (5);
|
|
180
|
+
`,
|
|
181
|
+
`
|
|
182
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
183
|
+
id TEXT PRIMARY KEY,
|
|
184
|
+
entity_type TEXT NOT NULL CHECK(entity_type IN ('task', 'plan', 'project', 'api_key', 'comment')),
|
|
185
|
+
entity_id TEXT NOT NULL,
|
|
186
|
+
action TEXT NOT NULL CHECK(action IN ('create', 'update', 'delete', 'start', 'complete', 'lock', 'unlock')),
|
|
187
|
+
actor TEXT,
|
|
188
|
+
changes TEXT,
|
|
189
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
190
|
+
);
|
|
191
|
+
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log(entity_type, entity_id);
|
|
192
|
+
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log(created_at);
|
|
193
|
+
|
|
194
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
195
|
+
id TEXT PRIMARY KEY,
|
|
196
|
+
url TEXT NOT NULL,
|
|
197
|
+
events TEXT NOT NULL DEFAULT '[]',
|
|
198
|
+
secret TEXT,
|
|
199
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
200
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
CREATE TABLE IF NOT EXISTS rate_limits (
|
|
204
|
+
key TEXT PRIMARY KEY,
|
|
205
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
206
|
+
window_start TEXT NOT NULL DEFAULT (datetime('now'))
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (6);
|
|
180
210
|
`
|
|
181
211
|
];
|
|
182
212
|
var _db = null;
|
|
@@ -499,9 +529,12 @@ function listTasks(filter = {}, db) {
|
|
|
499
529
|
params.push(filter.plan_id);
|
|
500
530
|
}
|
|
501
531
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
532
|
+
const limitVal = filter.limit || 100;
|
|
533
|
+
const offsetVal = filter.offset || 0;
|
|
534
|
+
params.push(limitVal, offsetVal);
|
|
502
535
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY
|
|
503
536
|
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
504
|
-
created_at DESC
|
|
537
|
+
created_at DESC LIMIT ? OFFSET ?`).all(...params);
|
|
505
538
|
return rows.map(rowToTask);
|
|
506
539
|
}
|
|
507
540
|
function updateTask(id, input, db) {
|
|
@@ -1530,6 +1563,91 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
|
|
|
1530
1563
|
}
|
|
1531
1564
|
return { pushed, pulled, errors };
|
|
1532
1565
|
}
|
|
1566
|
+
// src/db/audit.ts
|
|
1567
|
+
function rowToEntry(row) {
|
|
1568
|
+
return {
|
|
1569
|
+
...row,
|
|
1570
|
+
changes: row.changes ? JSON.parse(row.changes) : null
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
function logAudit(entityType, entityId, action, actor, changes, db) {
|
|
1574
|
+
const d = db || getDatabase();
|
|
1575
|
+
d.run(`INSERT INTO audit_log (id, entity_type, entity_id, action, actor, changes, created_at)
|
|
1576
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [uuid(), entityType, entityId, action, actor || null, changes ? JSON.stringify(changes) : null, now()]);
|
|
1577
|
+
}
|
|
1578
|
+
function getAuditLog(entityType, entityId, limit = 50, offset = 0, db) {
|
|
1579
|
+
const d = db || getDatabase();
|
|
1580
|
+
const conditions = [];
|
|
1581
|
+
const params = [];
|
|
1582
|
+
if (entityType) {
|
|
1583
|
+
conditions.push("entity_type = ?");
|
|
1584
|
+
params.push(entityType);
|
|
1585
|
+
}
|
|
1586
|
+
if (entityId) {
|
|
1587
|
+
conditions.push("entity_id = ?");
|
|
1588
|
+
params.push(entityId);
|
|
1589
|
+
}
|
|
1590
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1591
|
+
params.push(limit, offset);
|
|
1592
|
+
const rows = d.query(`SELECT * FROM audit_log ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params);
|
|
1593
|
+
return rows.map(rowToEntry);
|
|
1594
|
+
}
|
|
1595
|
+
// src/db/webhooks.ts
|
|
1596
|
+
function rowToWebhook(row) {
|
|
1597
|
+
return {
|
|
1598
|
+
...row,
|
|
1599
|
+
events: JSON.parse(row.events),
|
|
1600
|
+
active: row.active === 1
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
function createWebhook(input, db) {
|
|
1604
|
+
const d = db || getDatabase();
|
|
1605
|
+
const id = uuid();
|
|
1606
|
+
d.run(`INSERT INTO webhooks (id, url, events, secret, created_at)
|
|
1607
|
+
VALUES (?, ?, ?, ?, ?)`, [id, input.url, JSON.stringify(input.events || []), input.secret || null, now()]);
|
|
1608
|
+
return getWebhook(id, d);
|
|
1609
|
+
}
|
|
1610
|
+
function getWebhook(id, db) {
|
|
1611
|
+
const d = db || getDatabase();
|
|
1612
|
+
const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
|
|
1613
|
+
return row ? rowToWebhook(row) : null;
|
|
1614
|
+
}
|
|
1615
|
+
function listWebhooks(db) {
|
|
1616
|
+
const d = db || getDatabase();
|
|
1617
|
+
return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
|
|
1618
|
+
}
|
|
1619
|
+
function deleteWebhook(id, db) {
|
|
1620
|
+
const d = db || getDatabase();
|
|
1621
|
+
return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
|
|
1622
|
+
}
|
|
1623
|
+
async function dispatchWebhooks(event, payload, db) {
|
|
1624
|
+
const d = db || getDatabase();
|
|
1625
|
+
const rows = d.query("SELECT * FROM webhooks WHERE active = 1").all();
|
|
1626
|
+
const webhooks = rows.map(rowToWebhook).filter((w) => w.events.length === 0 || w.events.includes(event));
|
|
1627
|
+
for (const webhook of webhooks) {
|
|
1628
|
+
try {
|
|
1629
|
+
const headers = { "Content-Type": "application/json" };
|
|
1630
|
+
if (webhook.secret) {
|
|
1631
|
+
const encoder = new TextEncoder;
|
|
1632
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(webhook.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
1633
|
+
const body = JSON.stringify({ event, data: payload, timestamp: now() });
|
|
1634
|
+
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
1635
|
+
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1636
|
+
fetch(webhook.url, {
|
|
1637
|
+
method: "POST",
|
|
1638
|
+
headers,
|
|
1639
|
+
body
|
|
1640
|
+
}).catch(() => {});
|
|
1641
|
+
} else {
|
|
1642
|
+
fetch(webhook.url, {
|
|
1643
|
+
method: "POST",
|
|
1644
|
+
headers,
|
|
1645
|
+
body: JSON.stringify({ event, data: payload, timestamp: now() })
|
|
1646
|
+
}).catch(() => {});
|
|
1647
|
+
}
|
|
1648
|
+
} catch {}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1533
1651
|
export {
|
|
1534
1652
|
validateApiKey,
|
|
1535
1653
|
updateTask,
|
|
@@ -1545,8 +1663,10 @@ export {
|
|
|
1545
1663
|
resolvePartialId,
|
|
1546
1664
|
resetDatabase,
|
|
1547
1665
|
removeDependency,
|
|
1666
|
+
logAudit,
|
|
1548
1667
|
lockTask,
|
|
1549
1668
|
loadConfig,
|
|
1669
|
+
listWebhooks,
|
|
1550
1670
|
listTasks,
|
|
1551
1671
|
listSessions,
|
|
1552
1672
|
listProjects,
|
|
@@ -1554,6 +1674,7 @@ export {
|
|
|
1554
1674
|
listComments,
|
|
1555
1675
|
listApiKeys,
|
|
1556
1676
|
hasAnyApiKeys,
|
|
1677
|
+
getWebhook,
|
|
1557
1678
|
getTaskWithRelations,
|
|
1558
1679
|
getTaskDependents,
|
|
1559
1680
|
getTaskDependencies,
|
|
@@ -1564,7 +1685,10 @@ export {
|
|
|
1564
1685
|
getPlan,
|
|
1565
1686
|
getDatabase,
|
|
1566
1687
|
getComment,
|
|
1688
|
+
getAuditLog,
|
|
1567
1689
|
ensureProject,
|
|
1690
|
+
dispatchWebhooks,
|
|
1691
|
+
deleteWebhook,
|
|
1568
1692
|
deleteTask,
|
|
1569
1693
|
deleteSession,
|
|
1570
1694
|
deleteProject,
|
|
@@ -1572,6 +1696,7 @@ export {
|
|
|
1572
1696
|
deleteComment,
|
|
1573
1697
|
deleteApiKey,
|
|
1574
1698
|
defaultSyncAgents,
|
|
1699
|
+
createWebhook,
|
|
1575
1700
|
createTask,
|
|
1576
1701
|
createSession,
|
|
1577
1702
|
createProject,
|
package/dist/mcp/index.js
CHANGED
|
@@ -4219,6 +4219,36 @@ var MIGRATIONS = [
|
|
|
4219
4219
|
);
|
|
4220
4220
|
CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash);
|
|
4221
4221
|
INSERT OR IGNORE INTO _migrations (id) VALUES (5);
|
|
4222
|
+
`,
|
|
4223
|
+
`
|
|
4224
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
4225
|
+
id TEXT PRIMARY KEY,
|
|
4226
|
+
entity_type TEXT NOT NULL CHECK(entity_type IN ('task', 'plan', 'project', 'api_key', 'comment')),
|
|
4227
|
+
entity_id TEXT NOT NULL,
|
|
4228
|
+
action TEXT NOT NULL CHECK(action IN ('create', 'update', 'delete', 'start', 'complete', 'lock', 'unlock')),
|
|
4229
|
+
actor TEXT,
|
|
4230
|
+
changes TEXT,
|
|
4231
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4232
|
+
);
|
|
4233
|
+
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log(entity_type, entity_id);
|
|
4234
|
+
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log(created_at);
|
|
4235
|
+
|
|
4236
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
4237
|
+
id TEXT PRIMARY KEY,
|
|
4238
|
+
url TEXT NOT NULL,
|
|
4239
|
+
events TEXT NOT NULL DEFAULT '[]',
|
|
4240
|
+
secret TEXT,
|
|
4241
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
4242
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4243
|
+
);
|
|
4244
|
+
|
|
4245
|
+
CREATE TABLE IF NOT EXISTS rate_limits (
|
|
4246
|
+
key TEXT PRIMARY KEY,
|
|
4247
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
4248
|
+
window_start TEXT NOT NULL DEFAULT (datetime('now'))
|
|
4249
|
+
);
|
|
4250
|
+
|
|
4251
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (6);
|
|
4222
4252
|
`
|
|
4223
4253
|
];
|
|
4224
4254
|
var _db = null;
|
|
@@ -4455,9 +4485,12 @@ function listTasks(filter = {}, db) {
|
|
|
4455
4485
|
params.push(filter.plan_id);
|
|
4456
4486
|
}
|
|
4457
4487
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4488
|
+
const limitVal = filter.limit || 100;
|
|
4489
|
+
const offsetVal = filter.offset || 0;
|
|
4490
|
+
params.push(limitVal, offsetVal);
|
|
4458
4491
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY
|
|
4459
4492
|
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
4460
|
-
created_at DESC
|
|
4493
|
+
created_at DESC LIMIT ? OFFSET ?`).all(...params);
|
|
4461
4494
|
return rows.map(rowToTask);
|
|
4462
4495
|
}
|
|
4463
4496
|
function updateTask(id, input, db) {
|