@hasna/todos 0.10.19 → 0.10.21
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/components/Dashboard.d.ts +7 -0
- package/dist/cli/components/Dashboard.d.ts.map +1 -0
- package/dist/cli/index.js +2151 -559
- package/dist/db/agents.d.ts +19 -4
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/db/audit.d.ts +46 -0
- package/dist/db/audit.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +468 -23
- package/dist/lib/burndown.d.ts +18 -0
- package/dist/lib/burndown.d.ts.map +1 -0
- package/dist/lib/extract.d.ts +51 -0
- package/dist/lib/extract.d.ts.map +1 -0
- package/dist/lib/github.d.ts +25 -0
- package/dist/lib/github.d.ts.map +1 -0
- package/dist/mcp/index.js +1014 -187
- package/dist/server/index.js +129 -25
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2346,6 +2346,7 @@ function ensureSchema(db) {
|
|
|
2346
2346
|
ensureColumn("tasks", "spawned_from_session", "TEXT");
|
|
2347
2347
|
ensureColumn("tasks", "assigned_by", "TEXT");
|
|
2348
2348
|
ensureColumn("tasks", "assigned_from_project", "TEXT");
|
|
2349
|
+
ensureColumn("tasks", "started_at", "TEXT");
|
|
2349
2350
|
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
2350
2351
|
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
2351
2352
|
ensureColumn("agents", "reports_to", "TEXT");
|
|
@@ -2463,6 +2464,16 @@ function resolvePartialId(db, table, partialId) {
|
|
|
2463
2464
|
return shortIdRows[0].id;
|
|
2464
2465
|
}
|
|
2465
2466
|
}
|
|
2467
|
+
if (table === "task_lists") {
|
|
2468
|
+
const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
|
|
2469
|
+
if (slugRow)
|
|
2470
|
+
return slugRow.id;
|
|
2471
|
+
}
|
|
2472
|
+
if (table === "projects") {
|
|
2473
|
+
const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
|
|
2474
|
+
if (nameRow)
|
|
2475
|
+
return nameRow.id;
|
|
2476
|
+
}
|
|
2466
2477
|
return null;
|
|
2467
2478
|
}
|
|
2468
2479
|
var LOCK_EXPIRY_MINUTES = 30, MIGRATIONS, _db = null;
|
|
@@ -2941,6 +2952,15 @@ var init_database = __esm(() => {
|
|
|
2941
2952
|
CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
|
|
2942
2953
|
CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
|
|
2943
2954
|
INSERT OR IGNORE INTO _migrations (id) VALUES (33);
|
|
2955
|
+
`,
|
|
2956
|
+
`
|
|
2957
|
+
ALTER TABLE tasks ADD COLUMN started_at TEXT;
|
|
2958
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (34);
|
|
2959
|
+
`,
|
|
2960
|
+
`
|
|
2961
|
+
ALTER TABLE tasks ADD COLUMN task_type TEXT;
|
|
2962
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type);
|
|
2963
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (35);
|
|
2944
2964
|
`
|
|
2945
2965
|
];
|
|
2946
2966
|
});
|
|
@@ -3413,7 +3433,8 @@ var exports_audit = {};
|
|
|
3413
3433
|
__export(exports_audit, {
|
|
3414
3434
|
logTaskChange: () => logTaskChange,
|
|
3415
3435
|
getTaskHistory: () => getTaskHistory,
|
|
3416
|
-
getRecentActivity: () => getRecentActivity
|
|
3436
|
+
getRecentActivity: () => getRecentActivity,
|
|
3437
|
+
getRecap: () => getRecap
|
|
3417
3438
|
});
|
|
3418
3439
|
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
3419
3440
|
const d = db || getDatabase();
|
|
@@ -3431,6 +3452,32 @@ function getRecentActivity(limit = 50, db) {
|
|
|
3431
3452
|
const d = db || getDatabase();
|
|
3432
3453
|
return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
3433
3454
|
}
|
|
3455
|
+
function getRecap(hours = 8, projectId, db) {
|
|
3456
|
+
const d = db || getDatabase();
|
|
3457
|
+
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
3458
|
+
const staleWindow = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
3459
|
+
const pf = projectId ? " AND project_id = ?" : "";
|
|
3460
|
+
const tpf = projectId ? " AND t.project_id = ?" : "";
|
|
3461
|
+
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);
|
|
3462
|
+
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);
|
|
3463
|
+
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();
|
|
3464
|
+
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();
|
|
3465
|
+
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);
|
|
3466
|
+
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);
|
|
3467
|
+
return {
|
|
3468
|
+
hours,
|
|
3469
|
+
since,
|
|
3470
|
+
completed: completed.map((r) => ({
|
|
3471
|
+
...r,
|
|
3472
|
+
duration_minutes: r.started_at && r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.started_at).getTime()) / 60000) : null
|
|
3473
|
+
})),
|
|
3474
|
+
created,
|
|
3475
|
+
in_progress,
|
|
3476
|
+
blocked,
|
|
3477
|
+
stale,
|
|
3478
|
+
agents
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3434
3481
|
var init_audit = __esm(() => {
|
|
3435
3482
|
init_database();
|
|
3436
3483
|
});
|
|
@@ -3773,8 +3820,8 @@ function createTask(input, db) {
|
|
|
3773
3820
|
const title = shortId ? `${shortId}: ${input.title}` : input.title;
|
|
3774
3821
|
const assignedBy = input.assigned_by || input.agent_id;
|
|
3775
3822
|
const assignedFromProject = input.assigned_from_project || null;
|
|
3776
|
-
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)
|
|
3777
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3823
|
+
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)
|
|
3824
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3778
3825
|
id,
|
|
3779
3826
|
shortId,
|
|
3780
3827
|
input.project_id || null,
|
|
@@ -3804,7 +3851,8 @@ function createTask(input, db) {
|
|
|
3804
3851
|
input.reason || null,
|
|
3805
3852
|
input.spawned_from_session || null,
|
|
3806
3853
|
assignedBy || null,
|
|
3807
|
-
assignedFromProject || null
|
|
3854
|
+
assignedFromProject || null,
|
|
3855
|
+
input.task_type || null
|
|
3808
3856
|
]);
|
|
3809
3857
|
if (tags.length > 0) {
|
|
3810
3858
|
insertTaskTags(id, tags, d);
|
|
@@ -3913,6 +3961,15 @@ function listTasks(filter = {}, db) {
|
|
|
3913
3961
|
} else if (filter.has_recurrence === false) {
|
|
3914
3962
|
conditions.push("recurrence_rule IS NULL");
|
|
3915
3963
|
}
|
|
3964
|
+
if (filter.task_type) {
|
|
3965
|
+
if (Array.isArray(filter.task_type)) {
|
|
3966
|
+
conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
|
|
3967
|
+
params.push(...filter.task_type);
|
|
3968
|
+
} else {
|
|
3969
|
+
conditions.push("task_type = ?");
|
|
3970
|
+
params.push(filter.task_type);
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3916
3973
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
3917
3974
|
if (filter.cursor) {
|
|
3918
3975
|
try {
|
|
@@ -4072,6 +4129,10 @@ function updateTask(id, input, db) {
|
|
|
4072
4129
|
sets.push("recurrence_rule = ?");
|
|
4073
4130
|
params.push(input.recurrence_rule);
|
|
4074
4131
|
}
|
|
4132
|
+
if (input.task_type !== undefined) {
|
|
4133
|
+
sets.push("task_type = ?");
|
|
4134
|
+
params.push(input.task_type ?? null);
|
|
4135
|
+
}
|
|
4075
4136
|
params.push(id, input.version);
|
|
4076
4137
|
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
4077
4138
|
if (result.changes === 0) {
|
|
@@ -4141,8 +4202,8 @@ function startTask(id, agentId, db) {
|
|
|
4141
4202
|
}
|
|
4142
4203
|
const cutoff = lockExpiryCutoff();
|
|
4143
4204
|
const timestamp = now();
|
|
4144
|
-
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
4145
|
-
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
4205
|
+
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
|
|
4206
|
+
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
|
|
4146
4207
|
if (result.changes === 0) {
|
|
4147
4208
|
if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
4148
4209
|
throw new LockError(id, task.locked_by);
|
|
@@ -4150,7 +4211,7 @@ function startTask(id, agentId, db) {
|
|
|
4150
4211
|
}
|
|
4151
4212
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
4152
4213
|
dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
|
|
4153
|
-
return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
|
|
4214
|
+
return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
|
|
4154
4215
|
}
|
|
4155
4216
|
function completeTask(id, agentId, db, options) {
|
|
4156
4217
|
const d = db || getDatabase();
|
|
@@ -4801,6 +4862,7 @@ __export(exports_agents, {
|
|
|
4801
4862
|
updateAgentActivity: () => updateAgentActivity,
|
|
4802
4863
|
updateAgent: () => updateAgent,
|
|
4803
4864
|
unarchiveAgent: () => unarchiveAgent,
|
|
4865
|
+
releaseAgent: () => releaseAgent,
|
|
4804
4866
|
registerAgent: () => registerAgent,
|
|
4805
4867
|
matchCapabilities: () => matchCapabilities,
|
|
4806
4868
|
listAgents: () => listAgents,
|
|
@@ -4812,10 +4874,29 @@ __export(exports_agents, {
|
|
|
4812
4874
|
getAgentByName: () => getAgentByName,
|
|
4813
4875
|
getAgent: () => getAgent,
|
|
4814
4876
|
deleteAgent: () => deleteAgent,
|
|
4877
|
+
autoReleaseStaleAgents: () => autoReleaseStaleAgents,
|
|
4815
4878
|
archiveAgent: () => archiveAgent
|
|
4816
4879
|
});
|
|
4880
|
+
function getActiveWindowMs() {
|
|
4881
|
+
const env = process.env["TODOS_AGENT_TIMEOUT_MS"];
|
|
4882
|
+
if (env) {
|
|
4883
|
+
const parsed = parseInt(env, 10);
|
|
4884
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
4885
|
+
return parsed;
|
|
4886
|
+
}
|
|
4887
|
+
return 30 * 60 * 1000;
|
|
4888
|
+
}
|
|
4889
|
+
function autoReleaseStaleAgents(db) {
|
|
4890
|
+
if (process.env["TODOS_AGENT_AUTO_RELEASE"] !== "true")
|
|
4891
|
+
return 0;
|
|
4892
|
+
const d = db || getDatabase();
|
|
4893
|
+
const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
|
|
4894
|
+
const result = d.run("UPDATE agents SET session_id = NULL WHERE status = 'active' AND session_id IS NOT NULL AND last_seen_at < ?", [cutoff]);
|
|
4895
|
+
return result.changes;
|
|
4896
|
+
}
|
|
4817
4897
|
function getAvailableNamesFromPool(pool, db) {
|
|
4818
|
-
|
|
4898
|
+
autoReleaseStaleAgents(db);
|
|
4899
|
+
const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
|
|
4819
4900
|
const activeNames = new Set(db.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
|
|
4820
4901
|
return pool.filter((name) => !activeNames.has(name.toLowerCase()));
|
|
4821
4902
|
}
|
|
@@ -4837,22 +4918,19 @@ function registerAgent(input, db) {
|
|
|
4837
4918
|
const existing = getAgentByName(normalizedName, d);
|
|
4838
4919
|
if (existing) {
|
|
4839
4920
|
const lastSeenMs = new Date(existing.last_seen_at).getTime();
|
|
4840
|
-
const
|
|
4921
|
+
const activeWindowMs = getActiveWindowMs();
|
|
4922
|
+
const isActive = Date.now() - lastSeenMs < activeWindowMs;
|
|
4841
4923
|
const sameSession = input.session_id && existing.session_id && input.session_id === existing.session_id;
|
|
4842
4924
|
const differentSession = input.session_id && existing.session_id && input.session_id !== existing.session_id;
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
working_dir: existing.working_dir,
|
|
4853
|
-
suggestions: suggestions.slice(0, 5),
|
|
4854
|
-
message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session \u2026${existing.session_id?.slice(-4)}, dir: ${existing.working_dir ?? "unknown"}). Cannot reclaim an active agent \u2014 choose a different name, or wait for the session to go stale.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
|
|
4855
|
-
};
|
|
4925
|
+
const callerHasNoSession = !input.session_id;
|
|
4926
|
+
const existingHasActiveSession = existing.session_id && isActive;
|
|
4927
|
+
if (!input.force) {
|
|
4928
|
+
if (isActive && differentSession) {
|
|
4929
|
+
return buildConflictError(existing, lastSeenMs, input.pool, d);
|
|
4930
|
+
}
|
|
4931
|
+
if (callerHasNoSession && existingHasActiveSession) {
|
|
4932
|
+
return buildConflictError(existing, lastSeenMs, input.pool, d);
|
|
4933
|
+
}
|
|
4856
4934
|
}
|
|
4857
4935
|
const updates = ["last_seen_at = ?", "status = 'active'"];
|
|
4858
4936
|
const params = [now()];
|
|
@@ -4897,6 +4975,32 @@ function registerAgent(input, db) {
|
|
|
4897
4975
|
function isAgentConflict(result) {
|
|
4898
4976
|
return result.conflict === true;
|
|
4899
4977
|
}
|
|
4978
|
+
function buildConflictError(existing, lastSeenMs, pool, d) {
|
|
4979
|
+
const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
|
|
4980
|
+
const suggestions = pool ? getAvailableNamesFromPool(pool, d) : [];
|
|
4981
|
+
return {
|
|
4982
|
+
conflict: true,
|
|
4983
|
+
existing_id: existing.id,
|
|
4984
|
+
existing_name: existing.name,
|
|
4985
|
+
last_seen_at: existing.last_seen_at,
|
|
4986
|
+
session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
|
|
4987
|
+
working_dir: existing.working_dir,
|
|
4988
|
+
suggestions: suggestions.slice(0, 5),
|
|
4989
|
+
message: `Agent "${existing.name}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id ? "\u2026" + existing.session_id.slice(-4) : "none"}, dir: ${existing.working_dir ?? "unknown"}). Pass force: true to take over, or choose a different name.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
|
|
4990
|
+
};
|
|
4991
|
+
}
|
|
4992
|
+
function releaseAgent(id, session_id, db) {
|
|
4993
|
+
const d = db || getDatabase();
|
|
4994
|
+
const agent = getAgent(id, d);
|
|
4995
|
+
if (!agent)
|
|
4996
|
+
return false;
|
|
4997
|
+
if (session_id && agent.session_id && agent.session_id !== session_id) {
|
|
4998
|
+
return false;
|
|
4999
|
+
}
|
|
5000
|
+
const epoch = new Date(0).toISOString();
|
|
5001
|
+
d.run("UPDATE agents SET session_id = NULL, last_seen_at = ? WHERE id = ?", [epoch, id]);
|
|
5002
|
+
return true;
|
|
5003
|
+
}
|
|
4900
5004
|
function getAgent(id, db) {
|
|
4901
5005
|
const d = db || getDatabase();
|
|
4902
5006
|
const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
|
|
@@ -4917,6 +5021,7 @@ function listAgents(opts, db) {
|
|
|
4917
5021
|
includeArchived = opts?.include_archived ?? false;
|
|
4918
5022
|
d = db || getDatabase();
|
|
4919
5023
|
}
|
|
5024
|
+
autoReleaseStaleAgents(d);
|
|
4920
5025
|
if (includeArchived) {
|
|
4921
5026
|
return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
|
|
4922
5027
|
}
|
|
@@ -4934,8 +5039,19 @@ function updateAgent(id, input, db) {
|
|
|
4934
5039
|
const sets = ["last_seen_at = ?"];
|
|
4935
5040
|
const params = [now()];
|
|
4936
5041
|
if (input.name !== undefined) {
|
|
5042
|
+
const newName = input.name.trim().toLowerCase();
|
|
5043
|
+
const holder = getAgentByName(newName, d);
|
|
5044
|
+
if (holder && holder.id !== id) {
|
|
5045
|
+
const lastSeenMs = new Date(holder.last_seen_at).getTime();
|
|
5046
|
+
const isActive = Date.now() - lastSeenMs < getActiveWindowMs();
|
|
5047
|
+
if (isActive && holder.status === "active") {
|
|
5048
|
+
throw new Error(`Cannot rename: name "${newName}" is held by active agent ${holder.id} (last seen ${Math.round((Date.now() - lastSeenMs) / 60000)}m ago)`);
|
|
5049
|
+
}
|
|
5050
|
+
const evictedName = `${holder.name}__evicted_${holder.id}`;
|
|
5051
|
+
d.run("UPDATE agents SET name = ? WHERE id = ?", [evictedName, holder.id]);
|
|
5052
|
+
}
|
|
4937
5053
|
sets.push("name = ?");
|
|
4938
|
-
params.push(
|
|
5054
|
+
params.push(newName);
|
|
4939
5055
|
}
|
|
4940
5056
|
if (input.description !== undefined) {
|
|
4941
5057
|
sets.push("description = ?");
|
|
@@ -5032,10 +5148,8 @@ function getCapableAgents(capabilities, opts, db) {
|
|
|
5032
5148
|
})).filter((entry) => entry.score >= minScore).sort((a, b) => b.score - a.score);
|
|
5033
5149
|
return opts?.limit ? scored.slice(0, opts.limit) : scored;
|
|
5034
5150
|
}
|
|
5035
|
-
var AGENT_ACTIVE_WINDOW_MS;
|
|
5036
5151
|
var init_agents = __esm(() => {
|
|
5037
5152
|
init_database();
|
|
5038
|
-
AGENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
|
|
5039
5153
|
});
|
|
5040
5154
|
|
|
5041
5155
|
// src/db/task-lists.ts
|
|
@@ -5867,133 +5981,549 @@ var init_sync = __esm(() => {
|
|
|
5867
5981
|
init_config();
|
|
5868
5982
|
});
|
|
5869
5983
|
|
|
5870
|
-
//
|
|
5871
|
-
var
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
return ZodParsedType.null;
|
|
5894
|
-
}
|
|
5895
|
-
if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") {
|
|
5896
|
-
return ZodParsedType.promise;
|
|
5897
|
-
}
|
|
5898
|
-
if (typeof Map !== "undefined" && data instanceof Map) {
|
|
5899
|
-
return ZodParsedType.map;
|
|
5900
|
-
}
|
|
5901
|
-
if (typeof Set !== "undefined" && data instanceof Set) {
|
|
5902
|
-
return ZodParsedType.set;
|
|
5903
|
-
}
|
|
5904
|
-
if (typeof Date !== "undefined" && data instanceof Date) {
|
|
5905
|
-
return ZodParsedType.date;
|
|
5906
|
-
}
|
|
5907
|
-
return ZodParsedType.object;
|
|
5908
|
-
default:
|
|
5909
|
-
return ZodParsedType.unknown;
|
|
5984
|
+
// src/db/task-files.ts
|
|
5985
|
+
var exports_task_files = {};
|
|
5986
|
+
__export(exports_task_files, {
|
|
5987
|
+
updateTaskFileStatus: () => updateTaskFileStatus,
|
|
5988
|
+
removeTaskFile: () => removeTaskFile,
|
|
5989
|
+
listTaskFiles: () => listTaskFiles,
|
|
5990
|
+
listActiveFiles: () => listActiveFiles,
|
|
5991
|
+
getTaskFile: () => getTaskFile,
|
|
5992
|
+
getFileHeatMap: () => getFileHeatMap,
|
|
5993
|
+
findTasksByFile: () => findTasksByFile,
|
|
5994
|
+
detectFileConflicts: () => detectFileConflicts,
|
|
5995
|
+
bulkFindTasksByFiles: () => bulkFindTasksByFiles,
|
|
5996
|
+
bulkAddTaskFiles: () => bulkAddTaskFiles,
|
|
5997
|
+
addTaskFile: () => addTaskFile
|
|
5998
|
+
});
|
|
5999
|
+
function addTaskFile(input, db) {
|
|
6000
|
+
const d = db || getDatabase();
|
|
6001
|
+
const id = uuid();
|
|
6002
|
+
const timestamp = now();
|
|
6003
|
+
const existing = d.query("SELECT id FROM task_files WHERE task_id = ? AND path = ?").get(input.task_id, input.path);
|
|
6004
|
+
if (existing) {
|
|
6005
|
+
d.run("UPDATE task_files SET status = ?, agent_id = ?, note = ?, updated_at = ? WHERE id = ?", [input.status || "active", input.agent_id || null, input.note || null, timestamp, existing.id]);
|
|
6006
|
+
return getTaskFile(existing.id, d);
|
|
5910
6007
|
}
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
(
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
}
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
(
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
6008
|
+
d.run(`INSERT INTO task_files (id, task_id, path, status, agent_id, note, created_at, updated_at)
|
|
6009
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.path, input.status || "active", input.agent_id || null, input.note || null, timestamp, timestamp]);
|
|
6010
|
+
return getTaskFile(id, d);
|
|
6011
|
+
}
|
|
6012
|
+
function getTaskFile(id, db) {
|
|
6013
|
+
const d = db || getDatabase();
|
|
6014
|
+
return d.query("SELECT * FROM task_files WHERE id = ?").get(id);
|
|
6015
|
+
}
|
|
6016
|
+
function listTaskFiles(taskId, db) {
|
|
6017
|
+
const d = db || getDatabase();
|
|
6018
|
+
return d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY path").all(taskId);
|
|
6019
|
+
}
|
|
6020
|
+
function findTasksByFile(path, db) {
|
|
6021
|
+
const d = db || getDatabase();
|
|
6022
|
+
return d.query("SELECT * FROM task_files WHERE path = ? AND status != 'removed' ORDER BY updated_at DESC").all(path);
|
|
6023
|
+
}
|
|
6024
|
+
function updateTaskFileStatus(taskId, path, status, agentId, db) {
|
|
6025
|
+
const d = db || getDatabase();
|
|
6026
|
+
const timestamp = now();
|
|
6027
|
+
d.run("UPDATE task_files SET status = ?, agent_id = COALESCE(?, agent_id), updated_at = ? WHERE task_id = ? AND path = ?", [status, agentId || null, timestamp, taskId, path]);
|
|
6028
|
+
const row = d.query("SELECT * FROM task_files WHERE task_id = ? AND path = ?").get(taskId, path);
|
|
6029
|
+
return row;
|
|
6030
|
+
}
|
|
6031
|
+
function removeTaskFile(taskId, path, db) {
|
|
6032
|
+
const d = db || getDatabase();
|
|
6033
|
+
const result = d.run("DELETE FROM task_files WHERE task_id = ? AND path = ?", [taskId, path]);
|
|
6034
|
+
return result.changes > 0;
|
|
6035
|
+
}
|
|
6036
|
+
function detectFileConflicts(taskId, paths, db) {
|
|
6037
|
+
const d = db || getDatabase();
|
|
6038
|
+
if (paths.length === 0)
|
|
6039
|
+
return [];
|
|
6040
|
+
const placeholders = paths.map(() => "?").join(", ");
|
|
6041
|
+
const rows = d.query(`
|
|
6042
|
+
SELECT tf.path, tf.agent_id AS conflicting_agent_id, t.id AS conflicting_task_id,
|
|
6043
|
+
t.title AS conflicting_task_title, t.status AS conflicting_task_status
|
|
6044
|
+
FROM task_files tf
|
|
6045
|
+
JOIN tasks t ON tf.task_id = t.id
|
|
6046
|
+
WHERE tf.path IN (${placeholders})
|
|
6047
|
+
AND tf.task_id != ?
|
|
6048
|
+
AND tf.status != 'removed'
|
|
6049
|
+
AND t.status = 'in_progress'
|
|
6050
|
+
ORDER BY tf.updated_at DESC
|
|
6051
|
+
`).all(...paths, taskId);
|
|
6052
|
+
return rows;
|
|
6053
|
+
}
|
|
6054
|
+
function bulkFindTasksByFiles(paths, db) {
|
|
6055
|
+
const d = db || getDatabase();
|
|
6056
|
+
if (paths.length === 0)
|
|
6057
|
+
return [];
|
|
6058
|
+
const placeholders = paths.map(() => "?").join(", ");
|
|
6059
|
+
const rows = d.query(`SELECT tf.*, t.status AS task_status FROM task_files tf
|
|
6060
|
+
JOIN tasks t ON tf.task_id = t.id
|
|
6061
|
+
WHERE tf.path IN (${placeholders}) AND tf.status != 'removed'
|
|
6062
|
+
ORDER BY tf.updated_at DESC`).all(...paths);
|
|
6063
|
+
const byPath = new Map;
|
|
6064
|
+
for (const path of paths)
|
|
6065
|
+
byPath.set(path, []);
|
|
6066
|
+
for (const row of rows) {
|
|
6067
|
+
byPath.get(row.path)?.push(row);
|
|
6068
|
+
}
|
|
6069
|
+
return paths.map((path) => {
|
|
6070
|
+
const tasks = byPath.get(path) ?? [];
|
|
6071
|
+
const inProgressCount = tasks.filter((t) => t.task_status === "in_progress").length;
|
|
6072
|
+
return {
|
|
6073
|
+
path,
|
|
6074
|
+
tasks,
|
|
6075
|
+
has_conflict: inProgressCount > 1,
|
|
6076
|
+
in_progress_count: inProgressCount
|
|
5975
6077
|
};
|
|
5976
|
-
})
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
6078
|
+
});
|
|
6079
|
+
}
|
|
6080
|
+
function listActiveFiles(db) {
|
|
6081
|
+
const d = db || getDatabase();
|
|
6082
|
+
return d.query(`
|
|
6083
|
+
SELECT
|
|
6084
|
+
tf.path,
|
|
6085
|
+
tf.status AS file_status,
|
|
6086
|
+
tf.agent_id AS file_agent_id,
|
|
6087
|
+
tf.note,
|
|
6088
|
+
tf.updated_at,
|
|
6089
|
+
t.id AS task_id,
|
|
6090
|
+
t.short_id AS task_short_id,
|
|
6091
|
+
t.title AS task_title,
|
|
6092
|
+
t.status AS task_status,
|
|
6093
|
+
t.locked_by AS task_locked_by,
|
|
6094
|
+
t.locked_at AS task_locked_at,
|
|
6095
|
+
a.id AS agent_id,
|
|
6096
|
+
a.name AS agent_name
|
|
6097
|
+
FROM task_files tf
|
|
6098
|
+
JOIN tasks t ON tf.task_id = t.id
|
|
6099
|
+
LEFT JOIN agents a ON (tf.agent_id = a.id OR (tf.agent_id IS NULL AND t.assigned_to = a.id))
|
|
6100
|
+
WHERE t.status = 'in_progress'
|
|
6101
|
+
AND tf.status != 'removed'
|
|
6102
|
+
ORDER BY tf.updated_at DESC
|
|
6103
|
+
`).all();
|
|
6104
|
+
}
|
|
6105
|
+
function getFileHeatMap(opts, db) {
|
|
6106
|
+
const d = db || getDatabase();
|
|
6107
|
+
const limit = opts?.limit ?? 20;
|
|
6108
|
+
const minEdits = opts?.min_edits ?? 1;
|
|
6109
|
+
const rows = d.query(`
|
|
6110
|
+
SELECT
|
|
6111
|
+
tf.path,
|
|
6112
|
+
COUNT(*) AS edit_count,
|
|
6113
|
+
COUNT(DISTINCT COALESCE(tf.agent_id, t.assigned_to)) AS unique_agents,
|
|
6114
|
+
GROUP_CONCAT(DISTINCT COALESCE(tf.agent_id, t.assigned_to)) AS agent_ids,
|
|
6115
|
+
MAX(tf.updated_at) AS last_edited_at,
|
|
6116
|
+
SUM(CASE WHEN t.status = 'in_progress' THEN 1 ELSE 0 END) AS active_task_count
|
|
6117
|
+
FROM task_files tf
|
|
6118
|
+
JOIN tasks t ON tf.task_id = t.id
|
|
6119
|
+
WHERE tf.status != 'removed'
|
|
6120
|
+
${opts?.project_id ? `AND t.project_id = '${opts.project_id}'` : ""}
|
|
6121
|
+
GROUP BY tf.path
|
|
6122
|
+
HAVING edit_count >= ${minEdits}
|
|
6123
|
+
ORDER BY edit_count DESC, last_edited_at DESC
|
|
6124
|
+
LIMIT ${limit}
|
|
6125
|
+
`).all();
|
|
6126
|
+
return rows.map((r) => ({
|
|
6127
|
+
path: r.path,
|
|
6128
|
+
edit_count: r.edit_count,
|
|
6129
|
+
unique_agents: r.unique_agents,
|
|
6130
|
+
agent_ids: r.agent_ids ? r.agent_ids.split(",").filter(Boolean) : [],
|
|
6131
|
+
last_edited_at: r.last_edited_at,
|
|
6132
|
+
active_task_count: r.active_task_count
|
|
6133
|
+
}));
|
|
6134
|
+
}
|
|
6135
|
+
function bulkAddTaskFiles(taskId, paths, agentId, db) {
|
|
6136
|
+
const d = db || getDatabase();
|
|
6137
|
+
const results = [];
|
|
6138
|
+
const tx = d.transaction(() => {
|
|
6139
|
+
for (const path of paths) {
|
|
6140
|
+
results.push(addTaskFile({ task_id: taskId, path, agent_id: agentId }, d));
|
|
6141
|
+
}
|
|
6142
|
+
});
|
|
6143
|
+
tx();
|
|
6144
|
+
return results;
|
|
6145
|
+
}
|
|
6146
|
+
var init_task_files = __esm(() => {
|
|
6147
|
+
init_database();
|
|
6148
|
+
});
|
|
6149
|
+
|
|
6150
|
+
// src/db/task-commits.ts
|
|
6151
|
+
var exports_task_commits = {};
|
|
6152
|
+
__export(exports_task_commits, {
|
|
6153
|
+
unlinkTaskCommit: () => unlinkTaskCommit,
|
|
6154
|
+
linkTaskToCommit: () => linkTaskToCommit,
|
|
6155
|
+
getTaskCommits: () => getTaskCommits,
|
|
6156
|
+
findTaskByCommit: () => findTaskByCommit
|
|
6157
|
+
});
|
|
6158
|
+
function rowToCommit(row) {
|
|
6159
|
+
return {
|
|
6160
|
+
...row,
|
|
6161
|
+
files_changed: row.files_changed ? JSON.parse(row.files_changed) : null
|
|
6162
|
+
};
|
|
6163
|
+
}
|
|
6164
|
+
function linkTaskToCommit(input, db) {
|
|
6165
|
+
const d = db || getDatabase();
|
|
6166
|
+
const existing = d.query("SELECT * FROM task_commits WHERE task_id = ? AND sha = ?").get(input.task_id, input.sha);
|
|
6167
|
+
if (existing) {
|
|
6168
|
+
d.run("UPDATE task_commits SET message = COALESCE(?, message), author = COALESCE(?, author), files_changed = COALESCE(?, files_changed), committed_at = COALESCE(?, committed_at) WHERE id = ?", [input.message ?? null, input.author ?? null, input.files_changed ? JSON.stringify(input.files_changed) : null, input.committed_at ?? null, existing.id]);
|
|
6169
|
+
return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(existing.id));
|
|
6170
|
+
}
|
|
6171
|
+
const id = uuid();
|
|
6172
|
+
d.run("INSERT INTO task_commits (id, task_id, sha, message, author, files_changed, committed_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, input.task_id, input.sha, input.message ?? null, input.author ?? null, input.files_changed ? JSON.stringify(input.files_changed) : null, input.committed_at ?? null, now()]);
|
|
6173
|
+
return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(id));
|
|
6174
|
+
}
|
|
6175
|
+
function getTaskCommits(taskId, db) {
|
|
6176
|
+
const d = db || getDatabase();
|
|
6177
|
+
return d.query("SELECT * FROM task_commits WHERE task_id = ? ORDER BY committed_at DESC, created_at DESC").all(taskId).map(rowToCommit);
|
|
6178
|
+
}
|
|
6179
|
+
function findTaskByCommit(sha, db) {
|
|
6180
|
+
const d = db || getDatabase();
|
|
6181
|
+
const row = d.query("SELECT * FROM task_commits WHERE sha = ? OR sha LIKE ? LIMIT 1").get(sha, `${sha}%`);
|
|
6182
|
+
if (!row)
|
|
6183
|
+
return null;
|
|
6184
|
+
return { task_id: row.task_id, commit: rowToCommit(row) };
|
|
6185
|
+
}
|
|
6186
|
+
function unlinkTaskCommit(taskId, sha, db) {
|
|
6187
|
+
const d = db || getDatabase();
|
|
6188
|
+
return d.run("DELETE FROM task_commits WHERE task_id = ? AND (sha = ? OR sha LIKE ?)", [taskId, sha, `${sha}%`]).changes > 0;
|
|
6189
|
+
}
|
|
6190
|
+
var init_task_commits = __esm(() => {
|
|
6191
|
+
init_database();
|
|
6192
|
+
});
|
|
6193
|
+
|
|
6194
|
+
// src/lib/extract.ts
|
|
6195
|
+
var exports_extract = {};
|
|
6196
|
+
__export(exports_extract, {
|
|
6197
|
+
tagToPriority: () => tagToPriority,
|
|
6198
|
+
extractTodos: () => extractTodos,
|
|
6199
|
+
extractFromSource: () => extractFromSource,
|
|
6200
|
+
EXTRACT_TAGS: () => EXTRACT_TAGS
|
|
6201
|
+
});
|
|
6202
|
+
import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
6203
|
+
import { relative, resolve as resolve2, join as join6 } from "path";
|
|
6204
|
+
function tagToPriority(tag) {
|
|
6205
|
+
switch (tag) {
|
|
6206
|
+
case "BUG":
|
|
6207
|
+
case "FIXME":
|
|
6208
|
+
return "high";
|
|
6209
|
+
case "HACK":
|
|
6210
|
+
case "XXX":
|
|
6211
|
+
return "medium";
|
|
6212
|
+
case "TODO":
|
|
6213
|
+
return "medium";
|
|
6214
|
+
case "NOTE":
|
|
6215
|
+
return "low";
|
|
6216
|
+
}
|
|
6217
|
+
}
|
|
6218
|
+
function buildTagRegex(tags) {
|
|
6219
|
+
const tagPattern = tags.join("|");
|
|
6220
|
+
return new RegExp(`(?:^|\\s)(?:\\/\\/|\\/\\*|#|\\*|--|;;|%|<!--|\\{-)\\s*(?:@?)(${tagPattern})\\s*[:(]?\\s*(.*)$`, "i");
|
|
6221
|
+
}
|
|
6222
|
+
function extractFromSource(source, filePath, tags = [...EXTRACT_TAGS]) {
|
|
6223
|
+
const regex = buildTagRegex(tags);
|
|
6224
|
+
const results = [];
|
|
6225
|
+
const lines = source.split(`
|
|
6226
|
+
`);
|
|
6227
|
+
for (let i = 0;i < lines.length; i++) {
|
|
6228
|
+
const line = lines[i];
|
|
6229
|
+
const match = line.match(regex);
|
|
6230
|
+
if (match) {
|
|
6231
|
+
const tag = match[1].toUpperCase();
|
|
6232
|
+
let message = match[2].trim();
|
|
6233
|
+
message = message.replace(/\s*\*\/\s*$/, "").replace(/\s*-->\s*$/, "").replace(/\s*-\}\s*$/, "").trim();
|
|
6234
|
+
if (message) {
|
|
6235
|
+
results.push({
|
|
6236
|
+
tag,
|
|
6237
|
+
message,
|
|
6238
|
+
file: filePath,
|
|
6239
|
+
line: i + 1,
|
|
6240
|
+
raw: line
|
|
6241
|
+
});
|
|
6242
|
+
}
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
return results;
|
|
6246
|
+
}
|
|
6247
|
+
function collectFiles(basePath, extensions) {
|
|
6248
|
+
const stat = statSync2(basePath);
|
|
6249
|
+
if (stat.isFile()) {
|
|
6250
|
+
return [basePath];
|
|
6251
|
+
}
|
|
6252
|
+
const glob = new Bun.Glob("**/*");
|
|
6253
|
+
const files = [];
|
|
6254
|
+
for (const entry of glob.scanSync({ cwd: basePath, onlyFiles: true, dot: false })) {
|
|
6255
|
+
const parts = entry.split("/");
|
|
6256
|
+
if (parts.some((p) => SKIP_DIRS.has(p)))
|
|
6257
|
+
continue;
|
|
6258
|
+
const dotIdx = entry.lastIndexOf(".");
|
|
6259
|
+
if (dotIdx === -1)
|
|
6260
|
+
continue;
|
|
6261
|
+
const ext = entry.slice(dotIdx);
|
|
6262
|
+
if (!extensions.has(ext))
|
|
6263
|
+
continue;
|
|
6264
|
+
files.push(entry);
|
|
6265
|
+
}
|
|
6266
|
+
return files.sort();
|
|
6267
|
+
}
|
|
6268
|
+
function extractTodos(options, db) {
|
|
6269
|
+
const basePath = resolve2(options.path);
|
|
6270
|
+
const tags = options.patterns || [...EXTRACT_TAGS];
|
|
6271
|
+
const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
|
|
6272
|
+
const files = collectFiles(basePath, extensions);
|
|
6273
|
+
const allComments = [];
|
|
6274
|
+
for (const file of files) {
|
|
6275
|
+
const fullPath = statSync2(basePath).isFile() ? basePath : join6(basePath, file);
|
|
6276
|
+
try {
|
|
6277
|
+
const source = readFileSync3(fullPath, "utf-8");
|
|
6278
|
+
const relPath = statSync2(basePath).isFile() ? relative(resolve2(basePath, ".."), fullPath) : file;
|
|
6279
|
+
const comments = extractFromSource(source, relPath, tags);
|
|
6280
|
+
allComments.push(...comments);
|
|
6281
|
+
} catch {}
|
|
6282
|
+
}
|
|
6283
|
+
if (options.dry_run) {
|
|
6284
|
+
return { comments: allComments, tasks: [], skipped: 0 };
|
|
6285
|
+
}
|
|
6286
|
+
const tasks = [];
|
|
6287
|
+
let skipped = 0;
|
|
6288
|
+
const existingTasks = options.project_id ? listTasks({ project_id: options.project_id, tags: ["extracted"] }, db) : listTasks({ tags: ["extracted"] }, db);
|
|
6289
|
+
const existingKeys = new Set;
|
|
6290
|
+
for (const t of existingTasks) {
|
|
6291
|
+
const meta = t.metadata;
|
|
6292
|
+
if (meta?.["source_file"] && meta?.["source_line"]) {
|
|
6293
|
+
existingKeys.add(`${meta["source_file"]}:${meta["source_line"]}`);
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
6296
|
+
for (const comment of allComments) {
|
|
6297
|
+
const dedupKey = `${comment.file}:${comment.line}`;
|
|
6298
|
+
if (existingKeys.has(dedupKey)) {
|
|
6299
|
+
skipped++;
|
|
6300
|
+
continue;
|
|
6301
|
+
}
|
|
6302
|
+
const taskTags = ["extracted", comment.tag.toLowerCase(), ...options.tags || []];
|
|
6303
|
+
const task = createTask({
|
|
6304
|
+
title: `[${comment.tag}] ${comment.message}`,
|
|
6305
|
+
description: `Extracted from code comment in \`${comment.file}\` at line ${comment.line}:
|
|
6306
|
+
\`\`\`
|
|
6307
|
+
${comment.raw.trim()}
|
|
6308
|
+
\`\`\``,
|
|
6309
|
+
priority: tagToPriority(comment.tag),
|
|
6310
|
+
project_id: options.project_id,
|
|
6311
|
+
task_list_id: options.task_list_id,
|
|
6312
|
+
assigned_to: options.assigned_to,
|
|
6313
|
+
agent_id: options.agent_id,
|
|
6314
|
+
tags: taskTags,
|
|
6315
|
+
metadata: {
|
|
6316
|
+
source: "code_comment",
|
|
6317
|
+
comment_type: comment.tag,
|
|
6318
|
+
source_file: comment.file,
|
|
6319
|
+
source_line: comment.line
|
|
6320
|
+
}
|
|
6321
|
+
}, db);
|
|
6322
|
+
addTaskFile({
|
|
6323
|
+
task_id: task.id,
|
|
6324
|
+
path: comment.file,
|
|
6325
|
+
note: `Line ${comment.line}: ${comment.tag} comment`
|
|
6326
|
+
}, db);
|
|
6327
|
+
tasks.push(task);
|
|
6328
|
+
existingKeys.add(dedupKey);
|
|
6329
|
+
}
|
|
6330
|
+
return { comments: allComments, tasks, skipped };
|
|
6331
|
+
}
|
|
6332
|
+
var EXTRACT_TAGS, DEFAULT_EXTENSIONS, SKIP_DIRS;
|
|
6333
|
+
var init_extract = __esm(() => {
|
|
6334
|
+
init_tasks();
|
|
6335
|
+
init_task_files();
|
|
6336
|
+
EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
|
|
6337
|
+
DEFAULT_EXTENSIONS = new Set([
|
|
6338
|
+
".ts",
|
|
6339
|
+
".tsx",
|
|
6340
|
+
".js",
|
|
6341
|
+
".jsx",
|
|
6342
|
+
".mjs",
|
|
6343
|
+
".cjs",
|
|
6344
|
+
".py",
|
|
6345
|
+
".rb",
|
|
6346
|
+
".go",
|
|
6347
|
+
".rs",
|
|
6348
|
+
".c",
|
|
6349
|
+
".cpp",
|
|
6350
|
+
".h",
|
|
6351
|
+
".hpp",
|
|
6352
|
+
".java",
|
|
6353
|
+
".kt",
|
|
6354
|
+
".swift",
|
|
6355
|
+
".cs",
|
|
6356
|
+
".php",
|
|
6357
|
+
".sh",
|
|
6358
|
+
".bash",
|
|
6359
|
+
".zsh",
|
|
6360
|
+
".lua",
|
|
6361
|
+
".sql",
|
|
6362
|
+
".r",
|
|
6363
|
+
".R",
|
|
6364
|
+
".yaml",
|
|
6365
|
+
".yml",
|
|
6366
|
+
".toml",
|
|
6367
|
+
".css",
|
|
6368
|
+
".scss",
|
|
6369
|
+
".less",
|
|
6370
|
+
".vue",
|
|
6371
|
+
".svelte",
|
|
6372
|
+
".ex",
|
|
6373
|
+
".exs",
|
|
6374
|
+
".erl",
|
|
6375
|
+
".hs",
|
|
6376
|
+
".ml",
|
|
6377
|
+
".mli",
|
|
6378
|
+
".clj",
|
|
6379
|
+
".cljs"
|
|
6380
|
+
]);
|
|
6381
|
+
SKIP_DIRS = new Set([
|
|
6382
|
+
"node_modules",
|
|
6383
|
+
".git",
|
|
6384
|
+
"dist",
|
|
6385
|
+
"build",
|
|
6386
|
+
"out",
|
|
6387
|
+
".next",
|
|
6388
|
+
".turbo",
|
|
6389
|
+
"coverage",
|
|
6390
|
+
"__pycache__",
|
|
6391
|
+
".venv",
|
|
6392
|
+
"venv",
|
|
6393
|
+
"vendor",
|
|
6394
|
+
"target",
|
|
6395
|
+
".cache",
|
|
6396
|
+
".parcel-cache"
|
|
6397
|
+
]);
|
|
6398
|
+
});
|
|
6399
|
+
|
|
6400
|
+
// node_modules/zod/v3/helpers/util.js
|
|
6401
|
+
var util, objectUtil, ZodParsedType, getParsedType = (data) => {
|
|
6402
|
+
const t = typeof data;
|
|
6403
|
+
switch (t) {
|
|
6404
|
+
case "undefined":
|
|
6405
|
+
return ZodParsedType.undefined;
|
|
6406
|
+
case "string":
|
|
6407
|
+
return ZodParsedType.string;
|
|
6408
|
+
case "number":
|
|
6409
|
+
return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number;
|
|
6410
|
+
case "boolean":
|
|
6411
|
+
return ZodParsedType.boolean;
|
|
6412
|
+
case "function":
|
|
6413
|
+
return ZodParsedType.function;
|
|
6414
|
+
case "bigint":
|
|
6415
|
+
return ZodParsedType.bigint;
|
|
6416
|
+
case "symbol":
|
|
6417
|
+
return ZodParsedType.symbol;
|
|
6418
|
+
case "object":
|
|
6419
|
+
if (Array.isArray(data)) {
|
|
6420
|
+
return ZodParsedType.array;
|
|
6421
|
+
}
|
|
6422
|
+
if (data === null) {
|
|
6423
|
+
return ZodParsedType.null;
|
|
6424
|
+
}
|
|
6425
|
+
if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") {
|
|
6426
|
+
return ZodParsedType.promise;
|
|
6427
|
+
}
|
|
6428
|
+
if (typeof Map !== "undefined" && data instanceof Map) {
|
|
6429
|
+
return ZodParsedType.map;
|
|
6430
|
+
}
|
|
6431
|
+
if (typeof Set !== "undefined" && data instanceof Set) {
|
|
6432
|
+
return ZodParsedType.set;
|
|
6433
|
+
}
|
|
6434
|
+
if (typeof Date !== "undefined" && data instanceof Date) {
|
|
6435
|
+
return ZodParsedType.date;
|
|
6436
|
+
}
|
|
6437
|
+
return ZodParsedType.object;
|
|
6438
|
+
default:
|
|
6439
|
+
return ZodParsedType.unknown;
|
|
6440
|
+
}
|
|
6441
|
+
};
|
|
6442
|
+
var init_util = __esm(() => {
|
|
6443
|
+
(function(util2) {
|
|
6444
|
+
util2.assertEqual = (_) => {};
|
|
6445
|
+
function assertIs(_arg) {}
|
|
6446
|
+
util2.assertIs = assertIs;
|
|
6447
|
+
function assertNever(_x) {
|
|
6448
|
+
throw new Error;
|
|
6449
|
+
}
|
|
6450
|
+
util2.assertNever = assertNever;
|
|
6451
|
+
util2.arrayToEnum = (items) => {
|
|
6452
|
+
const obj = {};
|
|
6453
|
+
for (const item of items) {
|
|
6454
|
+
obj[item] = item;
|
|
6455
|
+
}
|
|
6456
|
+
return obj;
|
|
6457
|
+
};
|
|
6458
|
+
util2.getValidEnumValues = (obj) => {
|
|
6459
|
+
const validKeys = util2.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number");
|
|
6460
|
+
const filtered = {};
|
|
6461
|
+
for (const k of validKeys) {
|
|
6462
|
+
filtered[k] = obj[k];
|
|
6463
|
+
}
|
|
6464
|
+
return util2.objectValues(filtered);
|
|
6465
|
+
};
|
|
6466
|
+
util2.objectValues = (obj) => {
|
|
6467
|
+
return util2.objectKeys(obj).map(function(e) {
|
|
6468
|
+
return obj[e];
|
|
6469
|
+
});
|
|
6470
|
+
};
|
|
6471
|
+
util2.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object) => {
|
|
6472
|
+
const keys = [];
|
|
6473
|
+
for (const key in object) {
|
|
6474
|
+
if (Object.prototype.hasOwnProperty.call(object, key)) {
|
|
6475
|
+
keys.push(key);
|
|
6476
|
+
}
|
|
6477
|
+
}
|
|
6478
|
+
return keys;
|
|
6479
|
+
};
|
|
6480
|
+
util2.find = (arr, checker) => {
|
|
6481
|
+
for (const item of arr) {
|
|
6482
|
+
if (checker(item))
|
|
6483
|
+
return item;
|
|
6484
|
+
}
|
|
6485
|
+
return;
|
|
6486
|
+
};
|
|
6487
|
+
util2.isInteger = typeof Number.isInteger === "function" ? (val) => Number.isInteger(val) : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val;
|
|
6488
|
+
function joinValues(array, separator = " | ") {
|
|
6489
|
+
return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator);
|
|
6490
|
+
}
|
|
6491
|
+
util2.joinValues = joinValues;
|
|
6492
|
+
util2.jsonStringifyReplacer = (_, value) => {
|
|
6493
|
+
if (typeof value === "bigint") {
|
|
6494
|
+
return value.toString();
|
|
6495
|
+
}
|
|
6496
|
+
return value;
|
|
6497
|
+
};
|
|
6498
|
+
})(util || (util = {}));
|
|
6499
|
+
(function(objectUtil2) {
|
|
6500
|
+
objectUtil2.mergeShapes = (first, second) => {
|
|
6501
|
+
return {
|
|
6502
|
+
...first,
|
|
6503
|
+
...second
|
|
6504
|
+
};
|
|
6505
|
+
};
|
|
6506
|
+
})(objectUtil || (objectUtil = {}));
|
|
6507
|
+
ZodParsedType = util.arrayToEnum([
|
|
6508
|
+
"string",
|
|
6509
|
+
"nan",
|
|
6510
|
+
"number",
|
|
6511
|
+
"integer",
|
|
6512
|
+
"float",
|
|
6513
|
+
"boolean",
|
|
6514
|
+
"date",
|
|
6515
|
+
"bigint",
|
|
6516
|
+
"symbol",
|
|
6517
|
+
"function",
|
|
6518
|
+
"undefined",
|
|
6519
|
+
"null",
|
|
6520
|
+
"array",
|
|
6521
|
+
"object",
|
|
6522
|
+
"unknown",
|
|
6523
|
+
"promise",
|
|
6524
|
+
"void",
|
|
6525
|
+
"never",
|
|
6526
|
+
"map",
|
|
5997
6527
|
"set"
|
|
5998
6528
|
]);
|
|
5999
6529
|
});
|
|
@@ -9833,47 +10363,148 @@ var init_zod = __esm(() => {
|
|
|
9833
10363
|
init_external();
|
|
9834
10364
|
});
|
|
9835
10365
|
|
|
9836
|
-
// src/
|
|
9837
|
-
var
|
|
9838
|
-
__export(
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
|
|
9842
|
-
findTaskByCommit: () => findTaskByCommit
|
|
10366
|
+
// src/lib/github.ts
|
|
10367
|
+
var exports_github = {};
|
|
10368
|
+
__export(exports_github, {
|
|
10369
|
+
parseGitHubUrl: () => parseGitHubUrl,
|
|
10370
|
+
issueToTask: () => issueToTask,
|
|
10371
|
+
fetchGitHubIssue: () => fetchGitHubIssue
|
|
9843
10372
|
});
|
|
9844
|
-
|
|
10373
|
+
import { execSync } from "child_process";
|
|
10374
|
+
function parseGitHubUrl(url) {
|
|
10375
|
+
const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
10376
|
+
if (!match)
|
|
10377
|
+
return null;
|
|
10378
|
+
return { owner: match[1], repo: match[2], number: parseInt(match[3], 10) };
|
|
10379
|
+
}
|
|
10380
|
+
function fetchGitHubIssue(owner, repo, number) {
|
|
10381
|
+
const json = execSync(`gh api repos/${owner}/${repo}/issues/${number}`, { encoding: "utf-8", timeout: 15000 });
|
|
10382
|
+
const data = JSON.parse(json);
|
|
9845
10383
|
return {
|
|
9846
|
-
|
|
9847
|
-
|
|
10384
|
+
number: data.number,
|
|
10385
|
+
title: data.title,
|
|
10386
|
+
body: data.body,
|
|
10387
|
+
labels: (data.labels || []).map((l) => l.name),
|
|
10388
|
+
state: data.state,
|
|
10389
|
+
assignee: data.assignee?.login || null,
|
|
10390
|
+
url: data.html_url
|
|
9848
10391
|
};
|
|
9849
10392
|
}
|
|
9850
|
-
function
|
|
10393
|
+
function issueToTask(issue, opts) {
|
|
10394
|
+
const labelToPriority = {
|
|
10395
|
+
critical: "critical",
|
|
10396
|
+
"priority:critical": "critical",
|
|
10397
|
+
high: "high",
|
|
10398
|
+
"priority:high": "high",
|
|
10399
|
+
urgent: "high",
|
|
10400
|
+
low: "low",
|
|
10401
|
+
"priority:low": "low"
|
|
10402
|
+
};
|
|
10403
|
+
let priority = "medium";
|
|
10404
|
+
for (const label of issue.labels) {
|
|
10405
|
+
const mapped = labelToPriority[label.toLowerCase()];
|
|
10406
|
+
if (mapped) {
|
|
10407
|
+
priority = mapped;
|
|
10408
|
+
break;
|
|
10409
|
+
}
|
|
10410
|
+
}
|
|
10411
|
+
return {
|
|
10412
|
+
title: `[GH#${issue.number}] ${issue.title}`,
|
|
10413
|
+
description: issue.body ? issue.body.slice(0, 4000) : undefined,
|
|
10414
|
+
tags: issue.labels.slice(0, 10),
|
|
10415
|
+
priority,
|
|
10416
|
+
metadata: { github_url: issue.url, github_number: issue.number, github_state: issue.state },
|
|
10417
|
+
project_id: opts?.project_id,
|
|
10418
|
+
task_list_id: opts?.task_list_id,
|
|
10419
|
+
agent_id: opts?.agent_id
|
|
10420
|
+
};
|
|
10421
|
+
}
|
|
10422
|
+
var init_github = () => {};
|
|
10423
|
+
|
|
10424
|
+
// src/lib/burndown.ts
|
|
10425
|
+
var exports_burndown = {};
|
|
10426
|
+
__export(exports_burndown, {
|
|
10427
|
+
getBurndown: () => getBurndown
|
|
10428
|
+
});
|
|
10429
|
+
function getBurndown(opts, db) {
|
|
9851
10430
|
const d = db || getDatabase();
|
|
9852
|
-
const
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
10431
|
+
const conditions = [];
|
|
10432
|
+
const params = [];
|
|
10433
|
+
if (opts.plan_id) {
|
|
10434
|
+
conditions.push("plan_id = ?");
|
|
10435
|
+
params.push(opts.plan_id);
|
|
10436
|
+
}
|
|
10437
|
+
if (opts.project_id) {
|
|
10438
|
+
conditions.push("project_id = ?");
|
|
10439
|
+
params.push(opts.project_id);
|
|
10440
|
+
}
|
|
10441
|
+
if (opts.task_list_id) {
|
|
10442
|
+
conditions.push("task_list_id = ?");
|
|
10443
|
+
params.push(opts.task_list_id);
|
|
10444
|
+
}
|
|
10445
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
10446
|
+
const total = d.query(`SELECT COUNT(*) as c FROM tasks ${where}`).get(...params).c;
|
|
10447
|
+
const completed = d.query(`SELECT COUNT(*) as c FROM tasks ${where}${where ? " AND" : " WHERE"} status = 'completed'`).get(...params).c;
|
|
10448
|
+
const completions = d.query(`SELECT DATE(completed_at) as date, COUNT(*) as count FROM tasks ${where}${where ? " AND" : " WHERE"} status = 'completed' AND completed_at IS NOT NULL GROUP BY DATE(completed_at) ORDER BY date`).all(...params);
|
|
10449
|
+
const firstTask = d.query(`SELECT MIN(created_at) as min_date FROM tasks ${where}`).get(...params);
|
|
10450
|
+
const startDate = firstTask?.min_date ? new Date(firstTask.min_date) : new Date;
|
|
10451
|
+
const endDate = new Date;
|
|
10452
|
+
const days = [];
|
|
10453
|
+
const totalDays = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)));
|
|
10454
|
+
let cumulative = 0;
|
|
10455
|
+
const completionMap = new Map(completions.map((c) => [c.date, c.count]));
|
|
10456
|
+
const current = new Date(startDate);
|
|
10457
|
+
for (let i = 0;i <= totalDays; i++) {
|
|
10458
|
+
const dateStr = current.toISOString().slice(0, 10);
|
|
10459
|
+
cumulative += completionMap.get(dateStr) || 0;
|
|
10460
|
+
days.push({
|
|
10461
|
+
date: dateStr,
|
|
10462
|
+
completed_cumulative: cumulative,
|
|
10463
|
+
ideal: Math.round(total / totalDays * i)
|
|
10464
|
+
});
|
|
10465
|
+
current.setDate(current.getDate() + 1);
|
|
9856
10466
|
}
|
|
9857
|
-
const
|
|
9858
|
-
|
|
9859
|
-
return rowToCommit(d.query("SELECT * FROM task_commits WHERE id = ?").get(id));
|
|
9860
|
-
}
|
|
9861
|
-
function getTaskCommits(taskId, db) {
|
|
9862
|
-
const d = db || getDatabase();
|
|
9863
|
-
return d.query("SELECT * FROM task_commits WHERE task_id = ? ORDER BY committed_at DESC, created_at DESC").all(taskId).map(rowToCommit);
|
|
9864
|
-
}
|
|
9865
|
-
function findTaskByCommit(sha, db) {
|
|
9866
|
-
const d = db || getDatabase();
|
|
9867
|
-
const row = d.query("SELECT * FROM task_commits WHERE sha = ? OR sha LIKE ? LIMIT 1").get(sha, `${sha}%`);
|
|
9868
|
-
if (!row)
|
|
9869
|
-
return null;
|
|
9870
|
-
return { task_id: row.task_id, commit: rowToCommit(row) };
|
|
10467
|
+
const chart = renderBurndownChart(total, days);
|
|
10468
|
+
return { total, completed, remaining: total - completed, days, chart };
|
|
9871
10469
|
}
|
|
9872
|
-
function
|
|
9873
|
-
const
|
|
9874
|
-
|
|
10470
|
+
function renderBurndownChart(total, days) {
|
|
10471
|
+
const height = 12;
|
|
10472
|
+
const width = Math.min(60, days.length);
|
|
10473
|
+
const step = Math.max(1, Math.floor(days.length / width));
|
|
10474
|
+
const sampled = days.filter((_, i) => i % step === 0 || i === days.length - 1).slice(0, width);
|
|
10475
|
+
const lines = [];
|
|
10476
|
+
lines.push(` ${total} \u2524`);
|
|
10477
|
+
for (let row = height - 1;row >= 0; row--) {
|
|
10478
|
+
const threshold = Math.round(total / height * row);
|
|
10479
|
+
let line = "";
|
|
10480
|
+
for (const day of sampled) {
|
|
10481
|
+
const remaining = total - day.completed_cumulative;
|
|
10482
|
+
const idealRemaining = total - day.ideal;
|
|
10483
|
+
if (remaining >= threshold && remaining > threshold - Math.round(total / height)) {
|
|
10484
|
+
line += "\u2588";
|
|
10485
|
+
} else if (idealRemaining >= threshold && idealRemaining > threshold - Math.round(total / height)) {
|
|
10486
|
+
line += "\xB7";
|
|
10487
|
+
} else {
|
|
10488
|
+
line += " ";
|
|
10489
|
+
}
|
|
10490
|
+
}
|
|
10491
|
+
const label = String(threshold).padStart(4);
|
|
10492
|
+
lines.push(`${label} \u2524${line}`);
|
|
10493
|
+
}
|
|
10494
|
+
lines.push(` 0 \u2524${"\u2500".repeat(sampled.length)}`);
|
|
10495
|
+
lines.push(` \u2514${"\u2500".repeat(sampled.length)}`);
|
|
10496
|
+
if (sampled.length > 0) {
|
|
10497
|
+
const first = sampled[0].date.slice(5);
|
|
10498
|
+
const last = sampled[sampled.length - 1].date.slice(5);
|
|
10499
|
+
const pad = sampled.length - first.length - last.length;
|
|
10500
|
+
lines.push(` ${first}${" ".repeat(Math.max(1, pad))}${last}`);
|
|
10501
|
+
}
|
|
10502
|
+
lines.push("");
|
|
10503
|
+
lines.push(` \u2588 actual remaining \xB7 ideal burndown`);
|
|
10504
|
+
return lines.join(`
|
|
10505
|
+
`);
|
|
9875
10506
|
}
|
|
9876
|
-
var
|
|
10507
|
+
var init_burndown = __esm(() => {
|
|
9877
10508
|
init_database();
|
|
9878
10509
|
});
|
|
9879
10510
|
|
|
@@ -9921,281 +10552,115 @@ function buildPrompt(task, agents) {
|
|
|
9921
10552
|
TASK:
|
|
9922
10553
|
Title: ${task.title}
|
|
9923
10554
|
Priority: ${task.priority}
|
|
9924
|
-
Tags: ${task.tags.join(", ") || "none"}
|
|
9925
|
-
Description: ${task.description?.slice(0, 300) || "none"}
|
|
9926
|
-
|
|
9927
|
-
AVAILABLE AGENTS:
|
|
9928
|
-
${agentList}
|
|
9929
|
-
|
|
9930
|
-
Rules:
|
|
9931
|
-
- Match task tags/content to agent capabilities
|
|
9932
|
-
- Prefer agents with fewer active tasks
|
|
9933
|
-
- Prefer agents whose role fits the task (lead for critical, developer for features, qa for testing)
|
|
9934
|
-
- If no clear match, pick the agent with fewest active tasks
|
|
9935
|
-
|
|
9936
|
-
Respond with ONLY a JSON object: {"agent_name": "<name>", "reason": "<one sentence>"}`;
|
|
9937
|
-
}
|
|
9938
|
-
async function callCerebras(prompt, apiKey) {
|
|
9939
|
-
try {
|
|
9940
|
-
const resp = await fetch(CEREBRAS_API_URL, {
|
|
9941
|
-
method: "POST",
|
|
9942
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
9943
|
-
body: JSON.stringify({
|
|
9944
|
-
model: CEREBRAS_MODEL,
|
|
9945
|
-
messages: [{ role: "user", content: prompt }],
|
|
9946
|
-
max_tokens: 150,
|
|
9947
|
-
temperature: 0
|
|
9948
|
-
}),
|
|
9949
|
-
signal: AbortSignal.timeout(1e4)
|
|
9950
|
-
});
|
|
9951
|
-
if (!resp.ok)
|
|
9952
|
-
return null;
|
|
9953
|
-
const data = await resp.json();
|
|
9954
|
-
const content = data?.choices?.[0]?.message?.content?.trim();
|
|
9955
|
-
if (!content)
|
|
9956
|
-
return null;
|
|
9957
|
-
const match = content.match(/\{[^}]+\}/s);
|
|
9958
|
-
if (!match)
|
|
9959
|
-
return null;
|
|
9960
|
-
return JSON.parse(match[0]);
|
|
9961
|
-
} catch {
|
|
9962
|
-
return null;
|
|
9963
|
-
}
|
|
9964
|
-
}
|
|
9965
|
-
async function autoAssignTask(taskId, db) {
|
|
9966
|
-
const d = db || getDatabase();
|
|
9967
|
-
const task = getTask(taskId, d);
|
|
9968
|
-
if (!task)
|
|
9969
|
-
throw new Error(`Task ${taskId} not found`);
|
|
9970
|
-
const agents = listAgents(d).filter((a) => a.status === "active");
|
|
9971
|
-
if (agents.length === 0) {
|
|
9972
|
-
return { task_id: taskId, assigned_to: null, agent_name: null, method: "no_agents" };
|
|
9973
|
-
}
|
|
9974
|
-
const workloads = getAgentWorkloads(d);
|
|
9975
|
-
const apiKey = process.env["CEREBRAS_API_KEY"];
|
|
9976
|
-
let selectedAgent = null;
|
|
9977
|
-
let method = "capability_match";
|
|
9978
|
-
let reason;
|
|
9979
|
-
if (apiKey) {
|
|
9980
|
-
const agentData = agents.map((a) => ({
|
|
9981
|
-
id: a.id,
|
|
9982
|
-
name: a.name,
|
|
9983
|
-
role: a.role || "agent",
|
|
9984
|
-
capabilities: a.capabilities || [],
|
|
9985
|
-
in_progress_tasks: workloads.get(a.id) ?? 0
|
|
9986
|
-
}));
|
|
9987
|
-
const result = await callCerebras(buildPrompt({
|
|
9988
|
-
title: task.title,
|
|
9989
|
-
description: task.description,
|
|
9990
|
-
priority: task.priority,
|
|
9991
|
-
tags: task.tags || []
|
|
9992
|
-
}, agentData), apiKey);
|
|
9993
|
-
if (result?.agent_name) {
|
|
9994
|
-
selectedAgent = agents.find((a) => a.name === result.agent_name) ?? null;
|
|
9995
|
-
if (selectedAgent) {
|
|
9996
|
-
method = "cerebras";
|
|
9997
|
-
reason = result.reason;
|
|
9998
|
-
}
|
|
9999
|
-
}
|
|
10000
|
-
}
|
|
10001
|
-
if (!selectedAgent) {
|
|
10002
|
-
const taskTags = task.tags || [];
|
|
10003
|
-
const capable = getCapableAgents(taskTags, { min_score: 0, limit: 10 }, d);
|
|
10004
|
-
if (capable.length > 0) {
|
|
10005
|
-
const sorted = capable.sort((a, b) => {
|
|
10006
|
-
if (b.score !== a.score)
|
|
10007
|
-
return b.score - a.score;
|
|
10008
|
-
return (workloads.get(a.agent.id) ?? 0) - (workloads.get(b.agent.id) ?? 0);
|
|
10009
|
-
});
|
|
10010
|
-
selectedAgent = sorted[0].agent;
|
|
10011
|
-
reason = `Capability match (score: ${sorted[0].score.toFixed(2)})`;
|
|
10012
|
-
} else {
|
|
10013
|
-
selectedAgent = agents.slice().sort((a, b) => (workloads.get(a.id) ?? 0) - (workloads.get(b.id) ?? 0))[0];
|
|
10014
|
-
reason = `Least busy agent (${workloads.get(selectedAgent.id) ?? 0} active tasks)`;
|
|
10015
|
-
}
|
|
10016
|
-
}
|
|
10017
|
-
if (selectedAgent) {
|
|
10018
|
-
updateTask(taskId, { assigned_to: selectedAgent.id, version: task.version }, d);
|
|
10019
|
-
}
|
|
10020
|
-
return {
|
|
10021
|
-
task_id: taskId,
|
|
10022
|
-
assigned_to: selectedAgent?.id ?? null,
|
|
10023
|
-
agent_name: selectedAgent?.name ?? null,
|
|
10024
|
-
method,
|
|
10025
|
-
reason
|
|
10026
|
-
};
|
|
10027
|
-
}
|
|
10028
|
-
var CEREBRAS_API_URL = "https://api.cerebras.ai/v1/chat/completions", CEREBRAS_MODEL = "llama-3.3-70b";
|
|
10029
|
-
var init_auto_assign = __esm(() => {
|
|
10030
|
-
init_database();
|
|
10031
|
-
init_tasks();
|
|
10032
|
-
init_agents();
|
|
10033
|
-
});
|
|
10034
|
-
|
|
10035
|
-
// src/db/task-files.ts
|
|
10036
|
-
var exports_task_files = {};
|
|
10037
|
-
__export(exports_task_files, {
|
|
10038
|
-
updateTaskFileStatus: () => updateTaskFileStatus,
|
|
10039
|
-
removeTaskFile: () => removeTaskFile,
|
|
10040
|
-
listTaskFiles: () => listTaskFiles,
|
|
10041
|
-
listActiveFiles: () => listActiveFiles,
|
|
10042
|
-
getTaskFile: () => getTaskFile,
|
|
10043
|
-
getFileHeatMap: () => getFileHeatMap,
|
|
10044
|
-
findTasksByFile: () => findTasksByFile,
|
|
10045
|
-
detectFileConflicts: () => detectFileConflicts,
|
|
10046
|
-
bulkFindTasksByFiles: () => bulkFindTasksByFiles,
|
|
10047
|
-
bulkAddTaskFiles: () => bulkAddTaskFiles,
|
|
10048
|
-
addTaskFile: () => addTaskFile
|
|
10049
|
-
});
|
|
10050
|
-
function addTaskFile(input, db) {
|
|
10051
|
-
const d = db || getDatabase();
|
|
10052
|
-
const id = uuid();
|
|
10053
|
-
const timestamp = now();
|
|
10054
|
-
const existing = d.query("SELECT id FROM task_files WHERE task_id = ? AND path = ?").get(input.task_id, input.path);
|
|
10055
|
-
if (existing) {
|
|
10056
|
-
d.run("UPDATE task_files SET status = ?, agent_id = ?, note = ?, updated_at = ? WHERE id = ?", [input.status || "active", input.agent_id || null, input.note || null, timestamp, existing.id]);
|
|
10057
|
-
return getTaskFile(existing.id, d);
|
|
10058
|
-
}
|
|
10059
|
-
d.run(`INSERT INTO task_files (id, task_id, path, status, agent_id, note, created_at, updated_at)
|
|
10060
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.path, input.status || "active", input.agent_id || null, input.note || null, timestamp, timestamp]);
|
|
10061
|
-
return getTaskFile(id, d);
|
|
10062
|
-
}
|
|
10063
|
-
function getTaskFile(id, db) {
|
|
10064
|
-
const d = db || getDatabase();
|
|
10065
|
-
return d.query("SELECT * FROM task_files WHERE id = ?").get(id);
|
|
10066
|
-
}
|
|
10067
|
-
function listTaskFiles(taskId, db) {
|
|
10068
|
-
const d = db || getDatabase();
|
|
10069
|
-
return d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY path").all(taskId);
|
|
10070
|
-
}
|
|
10071
|
-
function findTasksByFile(path, db) {
|
|
10072
|
-
const d = db || getDatabase();
|
|
10073
|
-
return d.query("SELECT * FROM task_files WHERE path = ? AND status != 'removed' ORDER BY updated_at DESC").all(path);
|
|
10074
|
-
}
|
|
10075
|
-
function updateTaskFileStatus(taskId, path, status, agentId, db) {
|
|
10076
|
-
const d = db || getDatabase();
|
|
10077
|
-
const timestamp = now();
|
|
10078
|
-
d.run("UPDATE task_files SET status = ?, agent_id = COALESCE(?, agent_id), updated_at = ? WHERE task_id = ? AND path = ?", [status, agentId || null, timestamp, taskId, path]);
|
|
10079
|
-
const row = d.query("SELECT * FROM task_files WHERE task_id = ? AND path = ?").get(taskId, path);
|
|
10080
|
-
return row;
|
|
10081
|
-
}
|
|
10082
|
-
function removeTaskFile(taskId, path, db) {
|
|
10083
|
-
const d = db || getDatabase();
|
|
10084
|
-
const result = d.run("DELETE FROM task_files WHERE task_id = ? AND path = ?", [taskId, path]);
|
|
10085
|
-
return result.changes > 0;
|
|
10086
|
-
}
|
|
10087
|
-
function detectFileConflicts(taskId, paths, db) {
|
|
10088
|
-
const d = db || getDatabase();
|
|
10089
|
-
if (paths.length === 0)
|
|
10090
|
-
return [];
|
|
10091
|
-
const placeholders = paths.map(() => "?").join(", ");
|
|
10092
|
-
const rows = d.query(`
|
|
10093
|
-
SELECT tf.path, tf.agent_id AS conflicting_agent_id, t.id AS conflicting_task_id,
|
|
10094
|
-
t.title AS conflicting_task_title, t.status AS conflicting_task_status
|
|
10095
|
-
FROM task_files tf
|
|
10096
|
-
JOIN tasks t ON tf.task_id = t.id
|
|
10097
|
-
WHERE tf.path IN (${placeholders})
|
|
10098
|
-
AND tf.task_id != ?
|
|
10099
|
-
AND tf.status != 'removed'
|
|
10100
|
-
AND t.status = 'in_progress'
|
|
10101
|
-
ORDER BY tf.updated_at DESC
|
|
10102
|
-
`).all(...paths, taskId);
|
|
10103
|
-
return rows;
|
|
10104
|
-
}
|
|
10105
|
-
function bulkFindTasksByFiles(paths, db) {
|
|
10106
|
-
const d = db || getDatabase();
|
|
10107
|
-
if (paths.length === 0)
|
|
10108
|
-
return [];
|
|
10109
|
-
const placeholders = paths.map(() => "?").join(", ");
|
|
10110
|
-
const rows = d.query(`SELECT tf.*, t.status AS task_status FROM task_files tf
|
|
10111
|
-
JOIN tasks t ON tf.task_id = t.id
|
|
10112
|
-
WHERE tf.path IN (${placeholders}) AND tf.status != 'removed'
|
|
10113
|
-
ORDER BY tf.updated_at DESC`).all(...paths);
|
|
10114
|
-
const byPath = new Map;
|
|
10115
|
-
for (const path of paths)
|
|
10116
|
-
byPath.set(path, []);
|
|
10117
|
-
for (const row of rows) {
|
|
10118
|
-
byPath.get(row.path)?.push(row);
|
|
10119
|
-
}
|
|
10120
|
-
return paths.map((path) => {
|
|
10121
|
-
const tasks = byPath.get(path) ?? [];
|
|
10122
|
-
const inProgressCount = tasks.filter((t) => t.task_status === "in_progress").length;
|
|
10123
|
-
return {
|
|
10124
|
-
path,
|
|
10125
|
-
tasks,
|
|
10126
|
-
has_conflict: inProgressCount > 1,
|
|
10127
|
-
in_progress_count: inProgressCount
|
|
10128
|
-
};
|
|
10129
|
-
});
|
|
10130
|
-
}
|
|
10131
|
-
function listActiveFiles(db) {
|
|
10132
|
-
const d = db || getDatabase();
|
|
10133
|
-
return d.query(`
|
|
10134
|
-
SELECT
|
|
10135
|
-
tf.path,
|
|
10136
|
-
tf.status AS file_status,
|
|
10137
|
-
tf.agent_id AS file_agent_id,
|
|
10138
|
-
tf.note,
|
|
10139
|
-
tf.updated_at,
|
|
10140
|
-
t.id AS task_id,
|
|
10141
|
-
t.short_id AS task_short_id,
|
|
10142
|
-
t.title AS task_title,
|
|
10143
|
-
t.status AS task_status,
|
|
10144
|
-
t.locked_by AS task_locked_by,
|
|
10145
|
-
t.locked_at AS task_locked_at,
|
|
10146
|
-
a.id AS agent_id,
|
|
10147
|
-
a.name AS agent_name
|
|
10148
|
-
FROM task_files tf
|
|
10149
|
-
JOIN tasks t ON tf.task_id = t.id
|
|
10150
|
-
LEFT JOIN agents a ON (tf.agent_id = a.id OR (tf.agent_id IS NULL AND t.assigned_to = a.id))
|
|
10151
|
-
WHERE t.status = 'in_progress'
|
|
10152
|
-
AND tf.status != 'removed'
|
|
10153
|
-
ORDER BY tf.updated_at DESC
|
|
10154
|
-
`).all();
|
|
10555
|
+
Tags: ${task.tags.join(", ") || "none"}
|
|
10556
|
+
Description: ${task.description?.slice(0, 300) || "none"}
|
|
10557
|
+
|
|
10558
|
+
AVAILABLE AGENTS:
|
|
10559
|
+
${agentList}
|
|
10560
|
+
|
|
10561
|
+
Rules:
|
|
10562
|
+
- Match task tags/content to agent capabilities
|
|
10563
|
+
- Prefer agents with fewer active tasks
|
|
10564
|
+
- Prefer agents whose role fits the task (lead for critical, developer for features, qa for testing)
|
|
10565
|
+
- If no clear match, pick the agent with fewest active tasks
|
|
10566
|
+
|
|
10567
|
+
Respond with ONLY a JSON object: {"agent_name": "<name>", "reason": "<one sentence>"}`;
|
|
10155
10568
|
}
|
|
10156
|
-
function
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
last_edited_at: r.last_edited_at,
|
|
10183
|
-
active_task_count: r.active_task_count
|
|
10184
|
-
}));
|
|
10569
|
+
async function callCerebras(prompt, apiKey) {
|
|
10570
|
+
try {
|
|
10571
|
+
const resp = await fetch(CEREBRAS_API_URL, {
|
|
10572
|
+
method: "POST",
|
|
10573
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
10574
|
+
body: JSON.stringify({
|
|
10575
|
+
model: CEREBRAS_MODEL,
|
|
10576
|
+
messages: [{ role: "user", content: prompt }],
|
|
10577
|
+
max_tokens: 150,
|
|
10578
|
+
temperature: 0
|
|
10579
|
+
}),
|
|
10580
|
+
signal: AbortSignal.timeout(1e4)
|
|
10581
|
+
});
|
|
10582
|
+
if (!resp.ok)
|
|
10583
|
+
return null;
|
|
10584
|
+
const data = await resp.json();
|
|
10585
|
+
const content = data?.choices?.[0]?.message?.content?.trim();
|
|
10586
|
+
if (!content)
|
|
10587
|
+
return null;
|
|
10588
|
+
const match = content.match(/\{[^}]+\}/s);
|
|
10589
|
+
if (!match)
|
|
10590
|
+
return null;
|
|
10591
|
+
return JSON.parse(match[0]);
|
|
10592
|
+
} catch {
|
|
10593
|
+
return null;
|
|
10594
|
+
}
|
|
10185
10595
|
}
|
|
10186
|
-
function
|
|
10596
|
+
async function autoAssignTask(taskId, db) {
|
|
10187
10597
|
const d = db || getDatabase();
|
|
10188
|
-
const
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10598
|
+
const task = getTask(taskId, d);
|
|
10599
|
+
if (!task)
|
|
10600
|
+
throw new Error(`Task ${taskId} not found`);
|
|
10601
|
+
const agents = listAgents(d).filter((a) => a.status === "active");
|
|
10602
|
+
if (agents.length === 0) {
|
|
10603
|
+
return { task_id: taskId, assigned_to: null, agent_name: null, method: "no_agents" };
|
|
10604
|
+
}
|
|
10605
|
+
const workloads = getAgentWorkloads(d);
|
|
10606
|
+
const apiKey = process.env["CEREBRAS_API_KEY"];
|
|
10607
|
+
let selectedAgent = null;
|
|
10608
|
+
let method = "capability_match";
|
|
10609
|
+
let reason;
|
|
10610
|
+
if (apiKey) {
|
|
10611
|
+
const agentData = agents.map((a) => ({
|
|
10612
|
+
id: a.id,
|
|
10613
|
+
name: a.name,
|
|
10614
|
+
role: a.role || "agent",
|
|
10615
|
+
capabilities: a.capabilities || [],
|
|
10616
|
+
in_progress_tasks: workloads.get(a.id) ?? 0
|
|
10617
|
+
}));
|
|
10618
|
+
const result = await callCerebras(buildPrompt({
|
|
10619
|
+
title: task.title,
|
|
10620
|
+
description: task.description,
|
|
10621
|
+
priority: task.priority,
|
|
10622
|
+
tags: task.tags || []
|
|
10623
|
+
}, agentData), apiKey);
|
|
10624
|
+
if (result?.agent_name) {
|
|
10625
|
+
selectedAgent = agents.find((a) => a.name === result.agent_name) ?? null;
|
|
10626
|
+
if (selectedAgent) {
|
|
10627
|
+
method = "cerebras";
|
|
10628
|
+
reason = result.reason;
|
|
10629
|
+
}
|
|
10192
10630
|
}
|
|
10193
|
-
}
|
|
10194
|
-
|
|
10195
|
-
|
|
10631
|
+
}
|
|
10632
|
+
if (!selectedAgent) {
|
|
10633
|
+
const taskTags = task.tags || [];
|
|
10634
|
+
const capable = getCapableAgents(taskTags, { min_score: 0, limit: 10 }, d);
|
|
10635
|
+
if (capable.length > 0) {
|
|
10636
|
+
const sorted = capable.sort((a, b) => {
|
|
10637
|
+
if (b.score !== a.score)
|
|
10638
|
+
return b.score - a.score;
|
|
10639
|
+
return (workloads.get(a.agent.id) ?? 0) - (workloads.get(b.agent.id) ?? 0);
|
|
10640
|
+
});
|
|
10641
|
+
selectedAgent = sorted[0].agent;
|
|
10642
|
+
reason = `Capability match (score: ${sorted[0].score.toFixed(2)})`;
|
|
10643
|
+
} else {
|
|
10644
|
+
selectedAgent = agents.slice().sort((a, b) => (workloads.get(a.id) ?? 0) - (workloads.get(b.id) ?? 0))[0];
|
|
10645
|
+
reason = `Least busy agent (${workloads.get(selectedAgent.id) ?? 0} active tasks)`;
|
|
10646
|
+
}
|
|
10647
|
+
}
|
|
10648
|
+
if (selectedAgent) {
|
|
10649
|
+
updateTask(taskId, { assigned_to: selectedAgent.id, version: task.version }, d);
|
|
10650
|
+
}
|
|
10651
|
+
return {
|
|
10652
|
+
task_id: taskId,
|
|
10653
|
+
assigned_to: selectedAgent?.id ?? null,
|
|
10654
|
+
agent_name: selectedAgent?.name ?? null,
|
|
10655
|
+
method,
|
|
10656
|
+
reason
|
|
10657
|
+
};
|
|
10196
10658
|
}
|
|
10197
|
-
var
|
|
10659
|
+
var CEREBRAS_API_URL = "https://api.cerebras.ai/v1/chat/completions", CEREBRAS_MODEL = "llama-3.3-70b";
|
|
10660
|
+
var init_auto_assign = __esm(() => {
|
|
10198
10661
|
init_database();
|
|
10662
|
+
init_tasks();
|
|
10663
|
+
init_agents();
|
|
10199
10664
|
});
|
|
10200
10665
|
|
|
10201
10666
|
// src/db/file-locks.ts
|
|
@@ -10997,14 +11462,14 @@ __export(exports_mcp, {
|
|
|
10997
11462
|
});
|
|
10998
11463
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10999
11464
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11000
|
-
import { readFileSync as
|
|
11001
|
-
import { join as
|
|
11465
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
11466
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
11002
11467
|
import { fileURLToPath } from "url";
|
|
11003
11468
|
function getMcpVersion() {
|
|
11004
11469
|
try {
|
|
11005
11470
|
const __dir = dirname2(fileURLToPath(import.meta.url));
|
|
11006
|
-
const pkgPath =
|
|
11007
|
-
return JSON.parse(
|
|
11471
|
+
const pkgPath = join7(__dir, "..", "package.json");
|
|
11472
|
+
return JSON.parse(readFileSync4(pkgPath, "utf-8")).version || "0.0.0";
|
|
11008
11473
|
} catch {
|
|
11009
11474
|
return "0.0.0";
|
|
11010
11475
|
}
|
|
@@ -11168,7 +11633,8 @@ var init_mcp = __esm(() => {
|
|
|
11168
11633
|
"get_next_task",
|
|
11169
11634
|
"bootstrap",
|
|
11170
11635
|
"get_tasks_changed_since",
|
|
11171
|
-
"heartbeat"
|
|
11636
|
+
"heartbeat",
|
|
11637
|
+
"release_agent"
|
|
11172
11638
|
]);
|
|
11173
11639
|
STANDARD_EXCLUDED = new Set([
|
|
11174
11640
|
"rename_agent",
|
|
@@ -11206,7 +11672,8 @@ var init_mcp = __esm(() => {
|
|
|
11206
11672
|
spawns_template_id: exports_external.string().optional().describe("Template ID to auto-create as next task when this task is completed (pipeline/handoff chains)"),
|
|
11207
11673
|
reason: exports_external.string().optional().describe("Why this task exists \u2014 context for agents picking it up"),
|
|
11208
11674
|
spawned_from_session: exports_external.string().optional().describe("Session ID that created this task (for tracing task lineage)"),
|
|
11209
|
-
assigned_from_project: exports_external.string().optional().describe("Override: project ID the assigning agent is working from. Auto-detected from agent focus if omitted.")
|
|
11675
|
+
assigned_from_project: exports_external.string().optional().describe("Override: project ID the assigning agent is working from. Auto-detected from agent focus if omitted."),
|
|
11676
|
+
task_type: exports_external.string().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or any custom string")
|
|
11210
11677
|
}, async (params) => {
|
|
11211
11678
|
try {
|
|
11212
11679
|
if (!params.agent_id) {
|
|
@@ -11252,6 +11719,7 @@ var init_mcp = __esm(() => {
|
|
|
11252
11719
|
has_recurrence: exports_external.boolean().optional(),
|
|
11253
11720
|
due_today: exports_external.boolean().optional(),
|
|
11254
11721
|
overdue: exports_external.boolean().optional(),
|
|
11722
|
+
task_type: exports_external.union([exports_external.string(), exports_external.array(exports_external.string())]).optional().describe("Filter by task type: bug, feature, chore, improvement, docs, test, security, or custom"),
|
|
11255
11723
|
limit: exports_external.number().optional(),
|
|
11256
11724
|
offset: exports_external.number().optional(),
|
|
11257
11725
|
summary_only: exports_external.boolean().optional().describe("When true, return only id, short_id, title, status, priority \u2014 minimal tokens for navigation"),
|
|
@@ -11386,11 +11854,17 @@ Checklist (${done}/${task.checklist.length}):`);
|
|
|
11386
11854
|
tags: exports_external.array(exports_external.string()).optional(),
|
|
11387
11855
|
metadata: exports_external.record(exports_external.unknown()).optional(),
|
|
11388
11856
|
plan_id: exports_external.string().optional(),
|
|
11389
|
-
task_list_id: exports_external.string().optional()
|
|
11857
|
+
task_list_id: exports_external.string().optional(),
|
|
11858
|
+
task_type: exports_external.string().nullable().optional().describe("Task type: bug, feature, chore, improvement, docs, test, security, or custom. null to clear.")
|
|
11390
11859
|
}, async ({ id, ...rest }) => {
|
|
11391
11860
|
try {
|
|
11392
11861
|
const resolvedId = resolveId(id);
|
|
11393
|
-
const
|
|
11862
|
+
const resolved = { ...rest };
|
|
11863
|
+
if (resolved.task_list_id)
|
|
11864
|
+
resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
|
|
11865
|
+
if (resolved.plan_id)
|
|
11866
|
+
resolved.plan_id = resolveId(resolved.plan_id, "plans");
|
|
11867
|
+
const task = updateTask(resolvedId, resolved);
|
|
11394
11868
|
return { content: [{ type: "text", text: `updated: ${formatTask(task)}` }] };
|
|
11395
11869
|
} catch (e) {
|
|
11396
11870
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
@@ -12030,11 +12504,12 @@ ${text}` }] };
|
|
|
12030
12504
|
description: exports_external.string().optional(),
|
|
12031
12505
|
capabilities: exports_external.array(exports_external.string()).optional().describe("Agent capabilities/skills for task routing (e.g. ['typescript', 'testing', 'devops'])"),
|
|
12032
12506
|
session_id: exports_external.string().optional().describe("Unique ID for this coding session (e.g. process PID + timestamp, or env var). Used to detect name collisions across sessions. Store it and pass on every register_agent call."),
|
|
12033
|
-
working_dir: exports_external.string().optional().describe("Working directory of this session \u2014 used to look up the project's agent pool and identify who holds the name in a conflict")
|
|
12034
|
-
|
|
12507
|
+
working_dir: exports_external.string().optional().describe("Working directory of this session \u2014 used to look up the project's agent pool and identify who holds the name in a conflict"),
|
|
12508
|
+
force: exports_external.boolean().optional().describe("Force takeover of an active agent's name. Use with caution \u2014 only when you know the previous session is dead.")
|
|
12509
|
+
}, async ({ name, description, capabilities, session_id, working_dir, force }) => {
|
|
12035
12510
|
try {
|
|
12036
12511
|
const pool = getAgentPoolForProject(working_dir);
|
|
12037
|
-
const result = registerAgent({ name, description, capabilities, session_id, working_dir, pool: pool || undefined });
|
|
12512
|
+
const result = registerAgent({ name, description, capabilities, session_id, working_dir, force, pool: pool || undefined });
|
|
12038
12513
|
if (isAgentConflict(result)) {
|
|
12039
12514
|
const suggestLine = result.suggestions && result.suggestions.length > 0 ? `
|
|
12040
12515
|
Available names: ${result.suggestions.join(", ")}` : "";
|
|
@@ -12253,6 +12728,31 @@ ID: ${updated.id}`
|
|
|
12253
12728
|
}
|
|
12254
12729
|
});
|
|
12255
12730
|
}
|
|
12731
|
+
if (shouldRegisterTool("release_agent")) {
|
|
12732
|
+
server.tool("release_agent", "Explicitly release/logout an agent \u2014 clears session binding and makes the name immediately available. Call this when your session ends instead of waiting for the 30-minute stale timeout.", {
|
|
12733
|
+
agent_id: exports_external.string().describe("Your agent ID or name."),
|
|
12734
|
+
session_id: exports_external.string().optional().describe("Your session ID \u2014 if provided, release only succeeds if it matches (prevents other sessions from releasing your agent).")
|
|
12735
|
+
}, async ({ agent_id, session_id }) => {
|
|
12736
|
+
try {
|
|
12737
|
+
const agent = getAgent(agent_id) || getAgentByName(agent_id);
|
|
12738
|
+
if (!agent) {
|
|
12739
|
+
return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
|
|
12740
|
+
}
|
|
12741
|
+
const released = releaseAgent(agent.id, session_id);
|
|
12742
|
+
if (!released) {
|
|
12743
|
+
return { content: [{ type: "text", text: `Release denied: session_id does not match agent's current session.` }], isError: true };
|
|
12744
|
+
}
|
|
12745
|
+
return {
|
|
12746
|
+
content: [{
|
|
12747
|
+
type: "text",
|
|
12748
|
+
text: `Agent released: ${agent.name} (${agent.id}) \u2014 session cleared, name is now available.`
|
|
12749
|
+
}]
|
|
12750
|
+
};
|
|
12751
|
+
} catch (e) {
|
|
12752
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12753
|
+
}
|
|
12754
|
+
});
|
|
12755
|
+
}
|
|
12256
12756
|
if (shouldRegisterTool("create_task_list")) {
|
|
12257
12757
|
server.tool("create_task_list", "Create a task list container for organizing tasks.", {
|
|
12258
12758
|
name: exports_external.string(),
|
|
@@ -12356,56 +12856,228 @@ Slug: ${list.slug}`
|
|
|
12356
12856
|
}
|
|
12357
12857
|
});
|
|
12358
12858
|
}
|
|
12359
|
-
if (shouldRegisterTool("delete_task_list")) {
|
|
12360
|
-
server.tool("delete_task_list", "Delete a task list. Tasks are orphaned, not deleted.", {
|
|
12361
|
-
id: exports_external.string()
|
|
12362
|
-
}, async ({ id }) => {
|
|
12859
|
+
if (shouldRegisterTool("delete_task_list")) {
|
|
12860
|
+
server.tool("delete_task_list", "Delete a task list. Tasks are orphaned, not deleted.", {
|
|
12861
|
+
id: exports_external.string()
|
|
12862
|
+
}, async ({ id }) => {
|
|
12863
|
+
try {
|
|
12864
|
+
const resolvedId = resolveId(id, "task_lists");
|
|
12865
|
+
const deleted = deleteTaskList(resolvedId);
|
|
12866
|
+
return {
|
|
12867
|
+
content: [{
|
|
12868
|
+
type: "text",
|
|
12869
|
+
text: deleted ? `Task list ${id} deleted.` : `Task list ${id} not found.`
|
|
12870
|
+
}]
|
|
12871
|
+
};
|
|
12872
|
+
} catch (e) {
|
|
12873
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12874
|
+
}
|
|
12875
|
+
});
|
|
12876
|
+
}
|
|
12877
|
+
if (shouldRegisterTool("get_task_history")) {
|
|
12878
|
+
server.tool("get_task_history", "Get audit log \u2014 field changes with timestamps and actors.", {
|
|
12879
|
+
task_id: exports_external.string()
|
|
12880
|
+
}, async ({ task_id }) => {
|
|
12881
|
+
try {
|
|
12882
|
+
const resolvedId = resolveId(task_id);
|
|
12883
|
+
const { getTaskHistory: getTaskHistory2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
12884
|
+
const history = getTaskHistory2(resolvedId);
|
|
12885
|
+
if (history.length === 0)
|
|
12886
|
+
return { content: [{ type: "text", text: "No history for this task." }] };
|
|
12887
|
+
const text = history.map((h) => `${h.created_at} | ${h.action}${h.field ? ` ${h.field}` : ""}${h.old_value ? ` from "${h.old_value}"` : ""}${h.new_value ? ` to "${h.new_value}"` : ""}${h.agent_id ? ` by ${h.agent_id}` : ""}`).join(`
|
|
12888
|
+
`);
|
|
12889
|
+
return { content: [{ type: "text", text: `${history.length} change(s):
|
|
12890
|
+
${text}` }] };
|
|
12891
|
+
} catch (e) {
|
|
12892
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12893
|
+
}
|
|
12894
|
+
});
|
|
12895
|
+
}
|
|
12896
|
+
if (shouldRegisterTool("get_recent_activity")) {
|
|
12897
|
+
server.tool("get_recent_activity", "Get recent task changes \u2014 global activity feed.", {
|
|
12898
|
+
limit: exports_external.number().optional()
|
|
12899
|
+
}, async ({ limit }) => {
|
|
12900
|
+
try {
|
|
12901
|
+
const { getRecentActivity: getRecentActivity2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
12902
|
+
const activity = getRecentActivity2(limit || 50);
|
|
12903
|
+
if (activity.length === 0)
|
|
12904
|
+
return { content: [{ type: "text", text: "No recent activity." }] };
|
|
12905
|
+
const text = activity.map((h) => `${h.created_at} | ${h.task_id.slice(0, 8)} | ${h.action}${h.field ? ` ${h.field}` : ""}${h.agent_id ? ` by ${h.agent_id}` : ""}`).join(`
|
|
12906
|
+
`);
|
|
12907
|
+
return { content: [{ type: "text", text: `${activity.length} recent change(s):
|
|
12908
|
+
${text}` }] };
|
|
12909
|
+
} catch (e) {
|
|
12910
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12911
|
+
}
|
|
12912
|
+
});
|
|
12913
|
+
}
|
|
12914
|
+
if (shouldRegisterTool("recap")) {
|
|
12915
|
+
server.tool("recap", "Get a summary of what happened in the last N hours \u2014 completed tasks with duration, new tasks, in-progress work, blockers, stale tasks, and agent activity. Great for session start or standup prep.", {
|
|
12916
|
+
hours: exports_external.number().optional().describe("Look back N hours (default: 8)"),
|
|
12917
|
+
project_id: exports_external.string().optional().describe("Filter to a specific project")
|
|
12918
|
+
}, async ({ hours, project_id }) => {
|
|
12919
|
+
try {
|
|
12920
|
+
const { getRecap: getRecap2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
12921
|
+
const recap = getRecap2(hours || 8, project_id);
|
|
12922
|
+
const lines = [`Recap \u2014 last ${recap.hours}h (since ${recap.since})`];
|
|
12923
|
+
if (recap.completed.length > 0) {
|
|
12924
|
+
lines.push(`
|
|
12925
|
+
Completed (${recap.completed.length}):`);
|
|
12926
|
+
for (const t of recap.completed) {
|
|
12927
|
+
const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
|
|
12928
|
+
lines.push(` \u2713 ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
|
|
12929
|
+
}
|
|
12930
|
+
}
|
|
12931
|
+
if (recap.in_progress.length > 0) {
|
|
12932
|
+
lines.push(`
|
|
12933
|
+
In Progress (${recap.in_progress.length}):`);
|
|
12934
|
+
for (const t of recap.in_progress)
|
|
12935
|
+
lines.push(` \u2192 ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
|
|
12936
|
+
}
|
|
12937
|
+
if (recap.blocked.length > 0) {
|
|
12938
|
+
lines.push(`
|
|
12939
|
+
Blocked (${recap.blocked.length}):`);
|
|
12940
|
+
for (const t of recap.blocked)
|
|
12941
|
+
lines.push(` \u2717 ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
|
|
12942
|
+
}
|
|
12943
|
+
if (recap.stale.length > 0) {
|
|
12944
|
+
lines.push(`
|
|
12945
|
+
Stale (${recap.stale.length}):`);
|
|
12946
|
+
for (const t of recap.stale)
|
|
12947
|
+
lines.push(` ! ${t.short_id || t.id.slice(0, 8)} ${t.title} \u2014 updated ${t.updated_at}`);
|
|
12948
|
+
}
|
|
12949
|
+
if (recap.agents.length > 0) {
|
|
12950
|
+
lines.push(`
|
|
12951
|
+
Agents:`);
|
|
12952
|
+
for (const a of recap.agents)
|
|
12953
|
+
lines.push(` ${a.name}: ${a.completed_count} done, ${a.in_progress_count} active (seen ${a.last_seen_at})`);
|
|
12954
|
+
}
|
|
12955
|
+
lines.push(`
|
|
12956
|
+
Created: ${recap.created.length} new tasks`);
|
|
12957
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
12958
|
+
`) }] };
|
|
12959
|
+
} catch (e) {
|
|
12960
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12961
|
+
}
|
|
12962
|
+
});
|
|
12963
|
+
}
|
|
12964
|
+
if (shouldRegisterTool("standup")) {
|
|
12965
|
+
server.tool("standup", "Generate standup notes \u2014 completed tasks since yesterday grouped by agent, in-progress work, and blockers. Copy-paste ready.", {
|
|
12966
|
+
hours: exports_external.number().optional().describe("Look back N hours (default: 24)"),
|
|
12967
|
+
project_id: exports_external.string().optional()
|
|
12968
|
+
}, async ({ hours, project_id }) => {
|
|
12969
|
+
try {
|
|
12970
|
+
const { getRecap: getRecap2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
12971
|
+
const recap = getRecap2(hours || 24, project_id);
|
|
12972
|
+
const lines = [`Standup \u2014 last ${recap.hours}h`];
|
|
12973
|
+
const byAgent = new Map;
|
|
12974
|
+
for (const t of recap.completed) {
|
|
12975
|
+
const agent = t.assigned_to || "unassigned";
|
|
12976
|
+
if (!byAgent.has(agent))
|
|
12977
|
+
byAgent.set(agent, []);
|
|
12978
|
+
byAgent.get(agent).push(t);
|
|
12979
|
+
}
|
|
12980
|
+
if (byAgent.size > 0) {
|
|
12981
|
+
lines.push(`
|
|
12982
|
+
Done:`);
|
|
12983
|
+
for (const [agent, tasks] of byAgent) {
|
|
12984
|
+
lines.push(` ${agent}:`);
|
|
12985
|
+
for (const t of tasks) {
|
|
12986
|
+
const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
|
|
12987
|
+
lines.push(` \u2713 ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}`);
|
|
12988
|
+
}
|
|
12989
|
+
}
|
|
12990
|
+
} else {
|
|
12991
|
+
lines.push(`
|
|
12992
|
+
Nothing completed.`);
|
|
12993
|
+
}
|
|
12994
|
+
if (recap.in_progress.length > 0) {
|
|
12995
|
+
lines.push(`
|
|
12996
|
+
In Progress:`);
|
|
12997
|
+
for (const t of recap.in_progress)
|
|
12998
|
+
lines.push(` \u2192 ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${t.assigned_to}` : ""}`);
|
|
12999
|
+
}
|
|
13000
|
+
if (recap.blocked.length > 0) {
|
|
13001
|
+
lines.push(`
|
|
13002
|
+
Blocked:`);
|
|
13003
|
+
for (const t of recap.blocked)
|
|
13004
|
+
lines.push(` \u2717 ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
|
|
13005
|
+
}
|
|
13006
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
13007
|
+
`) }] };
|
|
13008
|
+
} catch (e) {
|
|
13009
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13010
|
+
}
|
|
13011
|
+
});
|
|
13012
|
+
}
|
|
13013
|
+
if (shouldRegisterTool("import_github_issue")) {
|
|
13014
|
+
server.tool("import_github_issue", "Import a GitHub issue as a task. Requires gh CLI installed and authenticated.", {
|
|
13015
|
+
url: exports_external.string().describe("GitHub issue URL (e.g. https://github.com/owner/repo/issues/42)"),
|
|
13016
|
+
project_id: exports_external.string().optional(),
|
|
13017
|
+
task_list_id: exports_external.string().optional()
|
|
13018
|
+
}, async ({ url, project_id, task_list_id }) => {
|
|
12363
13019
|
try {
|
|
12364
|
-
const
|
|
12365
|
-
const
|
|
12366
|
-
|
|
12367
|
-
content: [{
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
|
|
12371
|
-
}
|
|
13020
|
+
const { parseGitHubUrl: parseGitHubUrl2, fetchGitHubIssue: fetchGitHubIssue2, issueToTask: issueToTask2 } = await Promise.resolve().then(() => (init_github(), exports_github));
|
|
13021
|
+
const parsed = parseGitHubUrl2(url);
|
|
13022
|
+
if (!parsed)
|
|
13023
|
+
return { content: [{ type: "text", text: "Invalid GitHub issue URL." }], isError: true };
|
|
13024
|
+
const issue = fetchGitHubIssue2(parsed.owner, parsed.repo, parsed.number);
|
|
13025
|
+
const input = issueToTask2(issue, { project_id, task_list_id });
|
|
13026
|
+
const task = createTask(input);
|
|
13027
|
+
return { content: [{ type: "text", text: `Imported GH#${issue.number}: ${issue.title}
|
|
13028
|
+
Task: ${task.short_id || task.id} [${task.priority}]
|
|
13029
|
+
Labels: ${issue.labels.join(", ") || "none"}` }] };
|
|
12372
13030
|
} catch (e) {
|
|
12373
13031
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12374
13032
|
}
|
|
12375
13033
|
});
|
|
12376
13034
|
}
|
|
12377
|
-
if (shouldRegisterTool("
|
|
12378
|
-
server.tool("
|
|
12379
|
-
|
|
12380
|
-
}, async ({
|
|
13035
|
+
if (shouldRegisterTool("blame")) {
|
|
13036
|
+
server.tool("blame", "Show which tasks and agents touched a file \u2014 combines task_files and task_commits data.", {
|
|
13037
|
+
path: exports_external.string().describe("File path to look up")
|
|
13038
|
+
}, async ({ path }) => {
|
|
12381
13039
|
try {
|
|
12382
|
-
const
|
|
12383
|
-
const
|
|
12384
|
-
const
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
`
|
|
12389
|
-
|
|
12390
|
-
|
|
13040
|
+
const { findTasksByFile: findTasksByFile2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
|
|
13041
|
+
const db = getDatabase();
|
|
13042
|
+
const taskFiles = findTasksByFile2(path, db);
|
|
13043
|
+
const commitRows = db.query("SELECT tc.*, t.title, t.short_id FROM task_commits tc JOIN tasks t ON t.id = tc.task_id WHERE tc.files_changed LIKE ? ORDER BY tc.committed_at DESC").all(`%${path}%`);
|
|
13044
|
+
const lines = [`Blame: ${path}`];
|
|
13045
|
+
if (taskFiles.length > 0) {
|
|
13046
|
+
lines.push(`
|
|
13047
|
+
Task File Links (${taskFiles.length}):`);
|
|
13048
|
+
for (const tf of taskFiles) {
|
|
13049
|
+
const task = getTask(tf.task_id, db);
|
|
13050
|
+
lines.push(` ${task?.short_id || tf.task_id.slice(0, 8)} ${task?.title || "?"} \u2014 ${tf.role || "file"}`);
|
|
13051
|
+
}
|
|
13052
|
+
}
|
|
13053
|
+
if (commitRows.length > 0) {
|
|
13054
|
+
lines.push(`
|
|
13055
|
+
Commit Links (${commitRows.length}):`);
|
|
13056
|
+
for (const c of commitRows)
|
|
13057
|
+
lines.push(` ${c.sha?.slice(0, 7)} ${c.short_id || c.task_id.slice(0, 8)} ${c.title || ""} \u2014 ${c.author || ""}`);
|
|
13058
|
+
}
|
|
13059
|
+
if (taskFiles.length === 0 && commitRows.length === 0) {
|
|
13060
|
+
lines.push("No task or commit links found.");
|
|
13061
|
+
}
|
|
13062
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
13063
|
+
`) }] };
|
|
12391
13064
|
} catch (e) {
|
|
12392
13065
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12393
13066
|
}
|
|
12394
13067
|
});
|
|
12395
13068
|
}
|
|
12396
|
-
if (shouldRegisterTool("
|
|
12397
|
-
server.tool("
|
|
12398
|
-
|
|
12399
|
-
|
|
13069
|
+
if (shouldRegisterTool("burndown")) {
|
|
13070
|
+
server.tool("burndown", "ASCII burndown chart showing actual vs ideal progress for a plan, project, or task list.", {
|
|
13071
|
+
plan_id: exports_external.string().optional(),
|
|
13072
|
+
project_id: exports_external.string().optional(),
|
|
13073
|
+
task_list_id: exports_external.string().optional()
|
|
13074
|
+
}, async ({ plan_id, project_id, task_list_id }) => {
|
|
12400
13075
|
try {
|
|
12401
|
-
const {
|
|
12402
|
-
const
|
|
12403
|
-
|
|
12404
|
-
|
|
12405
|
-
|
|
12406
|
-
`);
|
|
12407
|
-
return { content: [{ type: "text", text: `${activity.length} recent change(s):
|
|
12408
|
-
${text}` }] };
|
|
13076
|
+
const { getBurndown: getBurndown2 } = await Promise.resolve().then(() => (init_burndown(), exports_burndown));
|
|
13077
|
+
const data = getBurndown2({ plan_id, project_id, task_list_id });
|
|
13078
|
+
return { content: [{ type: "text", text: `Burndown: ${data.completed}/${data.total} done, ${data.remaining} remaining
|
|
13079
|
+
|
|
13080
|
+
${data.chart}` }] };
|
|
12409
13081
|
} catch (e) {
|
|
12410
13082
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
12411
13083
|
}
|
|
@@ -13216,6 +13888,91 @@ ${checks.map((c) => ` ${c.status === "ok" ? "\u2713" : "\u26A0"} ${c.name}: ${c
|
|
|
13216
13888
|
}
|
|
13217
13889
|
});
|
|
13218
13890
|
}
|
|
13891
|
+
if (shouldRegisterTool("task_context")) {
|
|
13892
|
+
server.tool("task_context", "Full orientation for a specific task \u2014 details, description, dependencies (with blocked status), files, commits, comments, checklist. Use when starting work on a task.", {
|
|
13893
|
+
id: exports_external.string().describe("Task ID, short_id, or partial ID")
|
|
13894
|
+
}, async ({ id }) => {
|
|
13895
|
+
try {
|
|
13896
|
+
const resolvedId = resolveId(id, "tasks");
|
|
13897
|
+
const task = getTaskWithRelations(resolvedId);
|
|
13898
|
+
if (!task)
|
|
13899
|
+
return { content: [{ type: "text", text: `Task not found: ${id}` }], isError: true };
|
|
13900
|
+
const lines = [];
|
|
13901
|
+
const sid = task.short_id || task.id.slice(0, 8);
|
|
13902
|
+
lines.push(`${sid} [${task.status}] [${task.priority}] ${task.title}`);
|
|
13903
|
+
if (task.description)
|
|
13904
|
+
lines.push(`
|
|
13905
|
+
Description:
|
|
13906
|
+
${task.description}`);
|
|
13907
|
+
if (task.assigned_to)
|
|
13908
|
+
lines.push(`Assigned: ${task.assigned_to}`);
|
|
13909
|
+
if (task.started_at)
|
|
13910
|
+
lines.push(`Started: ${task.started_at}`);
|
|
13911
|
+
if (task.completed_at) {
|
|
13912
|
+
lines.push(`Completed: ${task.completed_at}`);
|
|
13913
|
+
if (task.started_at) {
|
|
13914
|
+
const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
|
|
13915
|
+
lines.push(`Duration: ${dur}m`);
|
|
13916
|
+
}
|
|
13917
|
+
}
|
|
13918
|
+
if (task.tags.length > 0)
|
|
13919
|
+
lines.push(`Tags: ${task.tags.join(", ")}`);
|
|
13920
|
+
if (task.dependencies.length > 0) {
|
|
13921
|
+
lines.push(`
|
|
13922
|
+
Depends on (${task.dependencies.length}):`);
|
|
13923
|
+
for (const dep of task.dependencies) {
|
|
13924
|
+
const blocked = dep.status !== "completed" && dep.status !== "cancelled";
|
|
13925
|
+
lines.push(` ${blocked ? "\u2717" : "\u2713"} ${dep.short_id || dep.id.slice(0, 8)} [${dep.status}] ${dep.title}`);
|
|
13926
|
+
}
|
|
13927
|
+
const unfinished = task.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
|
|
13928
|
+
if (unfinished.length > 0)
|
|
13929
|
+
lines.push(`\u26A0 BLOCKED by ${unfinished.length} unfinished dep(s)`);
|
|
13930
|
+
}
|
|
13931
|
+
if (task.blocked_by.length > 0) {
|
|
13932
|
+
lines.push(`
|
|
13933
|
+
Blocks (${task.blocked_by.length}):`);
|
|
13934
|
+
for (const b of task.blocked_by)
|
|
13935
|
+
lines.push(` ${b.short_id || b.id.slice(0, 8)} [${b.status}] ${b.title}`);
|
|
13936
|
+
}
|
|
13937
|
+
if (task.subtasks.length > 0) {
|
|
13938
|
+
lines.push(`
|
|
13939
|
+
Subtasks (${task.subtasks.length}):`);
|
|
13940
|
+
for (const st of task.subtasks)
|
|
13941
|
+
lines.push(` ${st.short_id || st.id.slice(0, 8)} [${st.status}] ${st.title}`);
|
|
13942
|
+
}
|
|
13943
|
+
try {
|
|
13944
|
+
const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
|
|
13945
|
+
const files = listTaskFiles2(task.id);
|
|
13946
|
+
if (files.length > 0) {
|
|
13947
|
+
lines.push(`
|
|
13948
|
+
Files (${files.length}):`);
|
|
13949
|
+
for (const f of files)
|
|
13950
|
+
lines.push(` ${f.role || "file"}: ${f.path}`);
|
|
13951
|
+
}
|
|
13952
|
+
} catch {}
|
|
13953
|
+
try {
|
|
13954
|
+
const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
|
|
13955
|
+
const commits = getTaskCommits2(task.id);
|
|
13956
|
+
if (commits.length > 0) {
|
|
13957
|
+
lines.push(`
|
|
13958
|
+
Commits (${commits.length}):`);
|
|
13959
|
+
for (const c of commits)
|
|
13960
|
+
lines.push(` ${c.commit_hash?.slice(0, 7)} ${c.message || ""}`);
|
|
13961
|
+
}
|
|
13962
|
+
} catch {}
|
|
13963
|
+
if (task.comments.length > 0) {
|
|
13964
|
+
lines.push(`
|
|
13965
|
+
Comments (${task.comments.length}):`);
|
|
13966
|
+
for (const c of task.comments)
|
|
13967
|
+
lines.push(` [${c.agent_id || "?"}] ${c.created_at}: ${c.content}`);
|
|
13968
|
+
}
|
|
13969
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
13970
|
+
`) }] };
|
|
13971
|
+
} catch (e) {
|
|
13972
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13973
|
+
}
|
|
13974
|
+
});
|
|
13975
|
+
}
|
|
13219
13976
|
if (shouldRegisterTool("get_context")) {
|
|
13220
13977
|
server.tool("get_context", "Get a compact task summary for agent prompt injection. Returns formatted text.", {
|
|
13221
13978
|
agent_id: exports_external.string().optional(),
|
|
@@ -13478,6 +14235,7 @@ ${stack_trace.slice(0, 1500)}
|
|
|
13478
14235
|
"delete_agent",
|
|
13479
14236
|
"unarchive_agent",
|
|
13480
14237
|
"heartbeat",
|
|
14238
|
+
"release_agent",
|
|
13481
14239
|
"get_my_tasks",
|
|
13482
14240
|
"get_org_chart",
|
|
13483
14241
|
"set_reports_to",
|
|
@@ -13494,6 +14252,12 @@ ${stack_trace.slice(0, 1500)}
|
|
|
13494
14252
|
"claim_next_task",
|
|
13495
14253
|
"get_task_history",
|
|
13496
14254
|
"get_recent_activity",
|
|
14255
|
+
"recap",
|
|
14256
|
+
"task_context",
|
|
14257
|
+
"standup",
|
|
14258
|
+
"burndown",
|
|
14259
|
+
"blame",
|
|
14260
|
+
"import_github_issue",
|
|
13497
14261
|
"create_webhook",
|
|
13498
14262
|
"list_webhooks",
|
|
13499
14263
|
"delete_webhook",
|
|
@@ -13618,8 +14382,8 @@ ${stack_trace.slice(0, 1500)}
|
|
|
13618
14382
|
suggest_agent_name: `Check available agent names before registering. Shows active agents and, if a pool is configured, which pool names are free.
|
|
13619
14383
|
Params: working_dir(string \u2014 your working directory, used to look up project pool from config)
|
|
13620
14384
|
Example: {working_dir: '/workspace/platform'}`,
|
|
13621
|
-
register_agent: `Register an agent. Any name is allowed \u2014 pool is advisory. Returns CONFLICT if name is held by a recently-active agent.
|
|
13622
|
-
Params: name(string, req), description(string), capabilities(string[]), session_id(string \u2014 unique per session), working_dir(string \u2014 used to determine project pool)
|
|
14385
|
+
register_agent: `Register an agent. Any name is allowed \u2014 pool is advisory. Returns CONFLICT if name is held by a recently-active agent. Use force:true to take over.
|
|
14386
|
+
Params: name(string, req), description(string), capabilities(string[]), session_id(string \u2014 unique per session), working_dir(string \u2014 used to determine project pool), force(boolean \u2014 skip conflict check)
|
|
13623
14387
|
Example: {name: 'my-agent', session_id: 'abc123-1741952000', working_dir: '/workspace/platform'}`,
|
|
13624
14388
|
list_agents: `List all registered agents (active by default). Set include_archived: true to see archived agents.
|
|
13625
14389
|
Params: include_archived(boolean, optional)
|
|
@@ -13639,6 +14403,9 @@ ${stack_trace.slice(0, 1500)}
|
|
|
13639
14403
|
heartbeat: `Update last_seen_at timestamp to signal you're still active. Call periodically during long tasks.
|
|
13640
14404
|
Params: agent_id(string, req \u2014 your agent ID or name)
|
|
13641
14405
|
Example: {agent_id: 'maximus'}`,
|
|
14406
|
+
release_agent: `Explicitly release/logout an agent \u2014 clears session binding and makes name immediately available. Call when session ends.
|
|
14407
|
+
Params: agent_id(string, req), session_id(string \u2014 only releases if matching)
|
|
14408
|
+
Example: {agent_id: 'maximus', session_id: 'my-session-123'}`,
|
|
13642
14409
|
get_my_tasks: `Get all tasks assigned to/created by an agent, with stats (pending/active/done/rate).
|
|
13643
14410
|
Params: agent_name(string, req)
|
|
13644
14411
|
Example: {agent_name: 'maximus'}`,
|
|
@@ -13699,6 +14466,24 @@ ${stack_trace.slice(0, 1500)}
|
|
|
13699
14466
|
get_recent_activity: `Get recent task changes across all tasks \u2014 global activity feed.
|
|
13700
14467
|
Params: limit(number, default:50)
|
|
13701
14468
|
Example: {limit: 20}`,
|
|
14469
|
+
recap: `Summary of what happened in the last N hours \u2014 completed tasks with durations, new tasks, in-progress, blocked, stale, agent activity.
|
|
14470
|
+
Params: hours(number, default:8), project_id(string)
|
|
14471
|
+
Example: {hours: 4}`,
|
|
14472
|
+
task_context: `Full orientation for a specific task \u2014 description, dependencies with blocked status, files, commits, comments, checklist, duration. Use before starting work.
|
|
14473
|
+
Params: id(string, req)
|
|
14474
|
+
Example: {id: 'OPE-00042'}`,
|
|
14475
|
+
standup: `Generate standup notes \u2014 completed tasks grouped by agent, in-progress, blocked. Copy-paste ready.
|
|
14476
|
+
Params: hours(number, default:24), project_id(string)
|
|
14477
|
+
Example: {hours: 24}`,
|
|
14478
|
+
import_github_issue: `Import a GitHub issue as a task. Requires gh CLI.
|
|
14479
|
+
Params: url(string, req), project_id(string), task_list_id(string)
|
|
14480
|
+
Example: {url: 'https://github.com/owner/repo/issues/42'}`,
|
|
14481
|
+
blame: `Show which tasks/agents touched a file \u2014 combines task_files and task_commits.
|
|
14482
|
+
Params: path(string, req)
|
|
14483
|
+
Example: {path: 'src/db/agents.ts'}`,
|
|
14484
|
+
burndown: `ASCII burndown chart \u2014 actual vs ideal progress for a plan, project, or task list.
|
|
14485
|
+
Params: plan_id(string), project_id(string), task_list_id(string)
|
|
14486
|
+
Example: {plan_id: 'abc123'}`,
|
|
13702
14487
|
create_webhook: `Register a webhook for task change events.
|
|
13703
14488
|
Params: url(string, req), events(string[] \u2014 empty=all), secret(string \u2014 HMAC signing)
|
|
13704
14489
|
Example: {url: 'https://example.com/hook', events: ['task.created', 'task.completed']}`,
|
|
@@ -14474,6 +15259,48 @@ ${lines.join(`
|
|
|
14474
15259
|
const lines = entries.map((e) => `#${e.rank} ${e.agent_name.padEnd(15)} score:${(e.composite_score * 100).toFixed(0).padStart(3)}% done:${String(e.tasks_completed).padStart(3)} rate:${(e.completion_rate * 100).toFixed(0)}%`);
|
|
14475
15260
|
return { content: [{ type: "text", text: `Leaderboard:
|
|
14476
15261
|
${lines.join(`
|
|
15262
|
+
`)}` }] };
|
|
15263
|
+
} catch (e) {
|
|
15264
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
15265
|
+
}
|
|
15266
|
+
});
|
|
15267
|
+
}
|
|
15268
|
+
if (shouldRegisterTool("extract_todos")) {
|
|
15269
|
+
server.tool("extract_todos", "Scan source files for TODO/FIXME/HACK/BUG/XXX/NOTE comments and create tasks from them. Deduplicates on re-runs.", {
|
|
15270
|
+
path: exports_external.string().describe("Directory or file path to scan"),
|
|
15271
|
+
project_id: exports_external.string().optional().describe("Project to assign tasks to"),
|
|
15272
|
+
task_list_id: exports_external.string().optional().describe("Task list to add tasks to"),
|
|
15273
|
+
patterns: exports_external.array(exports_external.enum(["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"])).optional().describe("Tags to search for (default: all)"),
|
|
15274
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Extra tags to add to created tasks"),
|
|
15275
|
+
assigned_to: exports_external.string().optional().describe("Agent to assign tasks to"),
|
|
15276
|
+
agent_id: exports_external.string().optional().describe("Agent performing the extraction"),
|
|
15277
|
+
dry_run: exports_external.boolean().optional().describe("If true, return found comments without creating tasks"),
|
|
15278
|
+
extensions: exports_external.array(exports_external.string()).optional().describe("File extensions to scan (e.g. ['ts', 'py'])")
|
|
15279
|
+
}, async (params) => {
|
|
15280
|
+
try {
|
|
15281
|
+
const { extractTodos: extractTodos2 } = (init_extract(), __toCommonJS(exports_extract));
|
|
15282
|
+
const resolved = { ...params };
|
|
15283
|
+
if (resolved["project_id"])
|
|
15284
|
+
resolved["project_id"] = resolveId(resolved["project_id"], "projects");
|
|
15285
|
+
if (resolved["task_list_id"])
|
|
15286
|
+
resolved["task_list_id"] = resolveId(resolved["task_list_id"], "task_lists");
|
|
15287
|
+
const result = extractTodos2(resolved);
|
|
15288
|
+
if (params.dry_run) {
|
|
15289
|
+
const lines = result.comments.map((c) => `[${c.tag}] ${c.message} \u2014 ${c.file}:${c.line}`);
|
|
15290
|
+
return { content: [{ type: "text", text: `Found ${result.comments.length} comment(s):
|
|
15291
|
+
${lines.join(`
|
|
15292
|
+
`)}` }] };
|
|
15293
|
+
}
|
|
15294
|
+
const summary = [
|
|
15295
|
+
`Created ${result.tasks.length} task(s)`,
|
|
15296
|
+
result.skipped > 0 ? `Skipped ${result.skipped} duplicate(s)` : null,
|
|
15297
|
+
`Total comments found: ${result.comments.length}`
|
|
15298
|
+
].filter(Boolean).join(`
|
|
15299
|
+
`);
|
|
15300
|
+
const taskLines = result.tasks.map((t) => formatTask(t));
|
|
15301
|
+
return { content: [{ type: "text", text: `${summary}
|
|
15302
|
+
|
|
15303
|
+
${taskLines.join(`
|
|
14477
15304
|
`)}` }] };
|
|
14478
15305
|
} catch (e) {
|
|
14479
15306
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
@@ -14561,26 +15388,26 @@ __export(exports_serve, {
|
|
|
14561
15388
|
startServer: () => startServer
|
|
14562
15389
|
});
|
|
14563
15390
|
import { existsSync as existsSync6 } from "fs";
|
|
14564
|
-
import { join as
|
|
15391
|
+
import { join as join8, dirname as dirname3, extname, resolve as resolve3, sep } from "path";
|
|
14565
15392
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
14566
15393
|
function resolveDashboardDir() {
|
|
14567
15394
|
const candidates = [];
|
|
14568
15395
|
try {
|
|
14569
15396
|
const scriptDir = dirname3(fileURLToPath2(import.meta.url));
|
|
14570
|
-
candidates.push(
|
|
14571
|
-
candidates.push(
|
|
15397
|
+
candidates.push(join8(scriptDir, "..", "dashboard", "dist"));
|
|
15398
|
+
candidates.push(join8(scriptDir, "..", "..", "dashboard", "dist"));
|
|
14572
15399
|
} catch {}
|
|
14573
15400
|
if (process.argv[1]) {
|
|
14574
15401
|
const mainDir = dirname3(process.argv[1]);
|
|
14575
|
-
candidates.push(
|
|
14576
|
-
candidates.push(
|
|
15402
|
+
candidates.push(join8(mainDir, "..", "dashboard", "dist"));
|
|
15403
|
+
candidates.push(join8(mainDir, "..", "..", "dashboard", "dist"));
|
|
14577
15404
|
}
|
|
14578
|
-
candidates.push(
|
|
15405
|
+
candidates.push(join8(process.cwd(), "dashboard", "dist"));
|
|
14579
15406
|
for (const candidate of candidates) {
|
|
14580
15407
|
if (existsSync6(candidate))
|
|
14581
15408
|
return candidate;
|
|
14582
15409
|
}
|
|
14583
|
-
return
|
|
15410
|
+
return join8(process.cwd(), "dashboard", "dist");
|
|
14584
15411
|
}
|
|
14585
15412
|
function json(data, status = 200, port) {
|
|
14586
15413
|
return new Response(JSON.stringify(data), {
|
|
@@ -15384,9 +16211,9 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
|
|
|
15384
16211
|
}
|
|
15385
16212
|
if (dashboardExists && (method === "GET" || method === "HEAD")) {
|
|
15386
16213
|
if (path !== "/") {
|
|
15387
|
-
const filePath =
|
|
15388
|
-
const resolvedFile =
|
|
15389
|
-
const resolvedBase =
|
|
16214
|
+
const filePath = join8(dashboardDir, path);
|
|
16215
|
+
const resolvedFile = resolve3(filePath);
|
|
16216
|
+
const resolvedBase = resolve3(dashboardDir);
|
|
15390
16217
|
if (!resolvedFile.startsWith(resolvedBase + sep) && resolvedFile !== resolvedBase) {
|
|
15391
16218
|
return json({ error: "Forbidden" }, 403, port);
|
|
15392
16219
|
}
|
|
@@ -15394,7 +16221,7 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
|
|
|
15394
16221
|
if (res2)
|
|
15395
16222
|
return res2;
|
|
15396
16223
|
}
|
|
15397
|
-
const indexPath =
|
|
16224
|
+
const indexPath = join8(dashboardDir, "index.html");
|
|
15398
16225
|
const res = serveStaticFile(indexPath);
|
|
15399
16226
|
if (res)
|
|
15400
16227
|
return res;
|
|
@@ -16483,21 +17310,307 @@ function App({ projectId }) {
|
|
|
16483
17310
|
]
|
|
16484
17311
|
}, undefined, true, undefined, this);
|
|
16485
17312
|
}
|
|
16486
|
-
function renderApp(projectId) {
|
|
16487
|
-
render(/* @__PURE__ */ jsxDEV7(App, {
|
|
16488
|
-
projectId
|
|
16489
|
-
}, undefined, false, undefined, this));
|
|
16490
|
-
}
|
|
16491
|
-
var init_App = __esm(() => {
|
|
16492
|
-
init_Header();
|
|
16493
|
-
init_TaskList();
|
|
16494
|
-
init_TaskDetail();
|
|
16495
|
-
init_TaskForm();
|
|
16496
|
-
init_ProjectList();
|
|
16497
|
-
init_SearchView();
|
|
17313
|
+
function renderApp(projectId) {
|
|
17314
|
+
render(/* @__PURE__ */ jsxDEV7(App, {
|
|
17315
|
+
projectId
|
|
17316
|
+
}, undefined, false, undefined, this));
|
|
17317
|
+
}
|
|
17318
|
+
var init_App = __esm(() => {
|
|
17319
|
+
init_Header();
|
|
17320
|
+
init_TaskList();
|
|
17321
|
+
init_TaskDetail();
|
|
17322
|
+
init_TaskForm();
|
|
17323
|
+
init_ProjectList();
|
|
17324
|
+
init_SearchView();
|
|
17325
|
+
init_tasks();
|
|
17326
|
+
init_projects();
|
|
17327
|
+
init_search();
|
|
17328
|
+
});
|
|
17329
|
+
|
|
17330
|
+
// src/cli/components/Dashboard.tsx
|
|
17331
|
+
var exports_Dashboard = {};
|
|
17332
|
+
__export(exports_Dashboard, {
|
|
17333
|
+
Dashboard: () => Dashboard
|
|
17334
|
+
});
|
|
17335
|
+
import { useState as useState4, useEffect as useEffect2 } from "react";
|
|
17336
|
+
import { Box as Box8, Text as Text7, useApp as useApp2, useInput as useInput4 } from "ink";
|
|
17337
|
+
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
17338
|
+
function AgentStatus({ name, lastSeen, sessionId }) {
|
|
17339
|
+
const ago = Math.round((Date.now() - new Date(lastSeen).getTime()) / 60000);
|
|
17340
|
+
const color = ago < 5 ? "green" : ago < 15 ? "yellow" : "red";
|
|
17341
|
+
const symbol = ago < 5 ? "\u25CF" : ago < 15 ? "\u25D0" : "\u25CB";
|
|
17342
|
+
return /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17343
|
+
children: [
|
|
17344
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17345
|
+
color,
|
|
17346
|
+
children: [
|
|
17347
|
+
symbol,
|
|
17348
|
+
" "
|
|
17349
|
+
]
|
|
17350
|
+
}, undefined, true, undefined, this),
|
|
17351
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17352
|
+
bold: true,
|
|
17353
|
+
children: name
|
|
17354
|
+
}, undefined, false, undefined, this),
|
|
17355
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17356
|
+
dimColor: true,
|
|
17357
|
+
children: [
|
|
17358
|
+
" ",
|
|
17359
|
+
ago,
|
|
17360
|
+
"m ago"
|
|
17361
|
+
]
|
|
17362
|
+
}, undefined, true, undefined, this),
|
|
17363
|
+
sessionId && /* @__PURE__ */ jsxDEV8(Text7, {
|
|
17364
|
+
dimColor: true,
|
|
17365
|
+
children: [
|
|
17366
|
+
" [",
|
|
17367
|
+
sessionId.slice(0, 8),
|
|
17368
|
+
"]"
|
|
17369
|
+
]
|
|
17370
|
+
}, undefined, true, undefined, this)
|
|
17371
|
+
]
|
|
17372
|
+
}, undefined, true, undefined, this);
|
|
17373
|
+
}
|
|
17374
|
+
function Dashboard({ projectId, refreshMs = 2000 }) {
|
|
17375
|
+
const { exit } = useApp2();
|
|
17376
|
+
const [tick, setTick] = useState4(0);
|
|
17377
|
+
const [recap, setRecap] = useState4(null);
|
|
17378
|
+
const [counts, setCounts] = useState4({ pending: 0, in_progress: 0, completed: 0, failed: 0, total: 0 });
|
|
17379
|
+
const [agents, setAgents] = useState4([]);
|
|
17380
|
+
useInput4((input) => {
|
|
17381
|
+
if (input === "q" || input === "Q")
|
|
17382
|
+
exit();
|
|
17383
|
+
});
|
|
17384
|
+
useEffect2(() => {
|
|
17385
|
+
const timer = setInterval(() => setTick((t) => t + 1), refreshMs);
|
|
17386
|
+
return () => clearInterval(timer);
|
|
17387
|
+
}, [refreshMs]);
|
|
17388
|
+
useEffect2(() => {
|
|
17389
|
+
try {
|
|
17390
|
+
const db = getDatabase();
|
|
17391
|
+
const filters = projectId ? { project_id: projectId } : {};
|
|
17392
|
+
const pending = countTasks({ ...filters, status: "pending" }, db);
|
|
17393
|
+
const in_progress = countTasks({ ...filters, status: "in_progress" }, db);
|
|
17394
|
+
const completed = countTasks({ ...filters, status: "completed" }, db);
|
|
17395
|
+
const failed = countTasks({ ...filters, status: "failed" }, db);
|
|
17396
|
+
setCounts({ pending, in_progress, completed, failed, total: pending + in_progress + completed + failed });
|
|
17397
|
+
setAgents(listAgents());
|
|
17398
|
+
setRecap(getRecap(1, projectId, db));
|
|
17399
|
+
} catch {}
|
|
17400
|
+
}, [tick, projectId]);
|
|
17401
|
+
return /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17402
|
+
flexDirection: "column",
|
|
17403
|
+
padding: 1,
|
|
17404
|
+
children: [
|
|
17405
|
+
/* @__PURE__ */ jsxDEV8(Box8, {
|
|
17406
|
+
marginBottom: 1,
|
|
17407
|
+
children: [
|
|
17408
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17409
|
+
bold: true,
|
|
17410
|
+
color: "cyan",
|
|
17411
|
+
children: " todos dashboard "
|
|
17412
|
+
}, undefined, false, undefined, this),
|
|
17413
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17414
|
+
dimColor: true,
|
|
17415
|
+
children: [
|
|
17416
|
+
"| refreshing every ",
|
|
17417
|
+
refreshMs / 1000,
|
|
17418
|
+
"s | press q to quit"
|
|
17419
|
+
]
|
|
17420
|
+
}, undefined, true, undefined, this)
|
|
17421
|
+
]
|
|
17422
|
+
}, undefined, true, undefined, this),
|
|
17423
|
+
/* @__PURE__ */ jsxDEV8(Box8, {
|
|
17424
|
+
marginBottom: 1,
|
|
17425
|
+
children: [
|
|
17426
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17427
|
+
color: "yellow",
|
|
17428
|
+
children: [
|
|
17429
|
+
counts.pending,
|
|
17430
|
+
" pending"
|
|
17431
|
+
]
|
|
17432
|
+
}, undefined, true, undefined, this),
|
|
17433
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17434
|
+
dimColor: true,
|
|
17435
|
+
children: " | "
|
|
17436
|
+
}, undefined, false, undefined, this),
|
|
17437
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17438
|
+
color: "blue",
|
|
17439
|
+
children: [
|
|
17440
|
+
counts.in_progress,
|
|
17441
|
+
" active"
|
|
17442
|
+
]
|
|
17443
|
+
}, undefined, true, undefined, this),
|
|
17444
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17445
|
+
dimColor: true,
|
|
17446
|
+
children: " | "
|
|
17447
|
+
}, undefined, false, undefined, this),
|
|
17448
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17449
|
+
color: "green",
|
|
17450
|
+
children: [
|
|
17451
|
+
counts.completed,
|
|
17452
|
+
" done"
|
|
17453
|
+
]
|
|
17454
|
+
}, undefined, true, undefined, this),
|
|
17455
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17456
|
+
dimColor: true,
|
|
17457
|
+
children: " | "
|
|
17458
|
+
}, undefined, false, undefined, this),
|
|
17459
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17460
|
+
color: "red",
|
|
17461
|
+
children: [
|
|
17462
|
+
counts.failed,
|
|
17463
|
+
" failed"
|
|
17464
|
+
]
|
|
17465
|
+
}, undefined, true, undefined, this),
|
|
17466
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17467
|
+
dimColor: true,
|
|
17468
|
+
children: " | "
|
|
17469
|
+
}, undefined, false, undefined, this),
|
|
17470
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17471
|
+
children: [
|
|
17472
|
+
counts.total,
|
|
17473
|
+
" total"
|
|
17474
|
+
]
|
|
17475
|
+
}, undefined, true, undefined, this)
|
|
17476
|
+
]
|
|
17477
|
+
}, undefined, true, undefined, this),
|
|
17478
|
+
agents.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17479
|
+
flexDirection: "column",
|
|
17480
|
+
marginBottom: 1,
|
|
17481
|
+
children: [
|
|
17482
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17483
|
+
bold: true,
|
|
17484
|
+
children: [
|
|
17485
|
+
"Agents (",
|
|
17486
|
+
agents.length,
|
|
17487
|
+
"):"
|
|
17488
|
+
]
|
|
17489
|
+
}, undefined, true, undefined, this),
|
|
17490
|
+
agents.map((a) => /* @__PURE__ */ jsxDEV8(AgentStatus, {
|
|
17491
|
+
name: a.name,
|
|
17492
|
+
lastSeen: a.last_seen_at,
|
|
17493
|
+
sessionId: a.session_id
|
|
17494
|
+
}, a.id, false, undefined, this))
|
|
17495
|
+
]
|
|
17496
|
+
}, undefined, true, undefined, this),
|
|
17497
|
+
recap && recap.in_progress.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17498
|
+
flexDirection: "column",
|
|
17499
|
+
marginBottom: 1,
|
|
17500
|
+
children: [
|
|
17501
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17502
|
+
bold: true,
|
|
17503
|
+
color: "blue",
|
|
17504
|
+
children: [
|
|
17505
|
+
"In Progress (",
|
|
17506
|
+
recap.in_progress.length,
|
|
17507
|
+
"):"
|
|
17508
|
+
]
|
|
17509
|
+
}, undefined, true, undefined, this),
|
|
17510
|
+
recap.in_progress.slice(0, 8).map((t) => /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17511
|
+
children: [
|
|
17512
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17513
|
+
color: "cyan",
|
|
17514
|
+
children: [
|
|
17515
|
+
t.short_id || t.id.slice(0, 8),
|
|
17516
|
+
" "
|
|
17517
|
+
]
|
|
17518
|
+
}, undefined, true, undefined, this),
|
|
17519
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17520
|
+
children: t.title
|
|
17521
|
+
}, undefined, false, undefined, this),
|
|
17522
|
+
t.assigned_to && /* @__PURE__ */ jsxDEV8(Text7, {
|
|
17523
|
+
dimColor: true,
|
|
17524
|
+
children: [
|
|
17525
|
+
" \u2014 ",
|
|
17526
|
+
t.assigned_to
|
|
17527
|
+
]
|
|
17528
|
+
}, undefined, true, undefined, this)
|
|
17529
|
+
]
|
|
17530
|
+
}, t.id, true, undefined, this))
|
|
17531
|
+
]
|
|
17532
|
+
}, undefined, true, undefined, this),
|
|
17533
|
+
recap && recap.completed.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17534
|
+
flexDirection: "column",
|
|
17535
|
+
marginBottom: 1,
|
|
17536
|
+
children: [
|
|
17537
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17538
|
+
bold: true,
|
|
17539
|
+
color: "green",
|
|
17540
|
+
children: [
|
|
17541
|
+
"Completed (last 1h: ",
|
|
17542
|
+
recap.completed.length,
|
|
17543
|
+
"):"
|
|
17544
|
+
]
|
|
17545
|
+
}, undefined, true, undefined, this),
|
|
17546
|
+
recap.completed.slice(0, 5).map((t) => /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17547
|
+
children: [
|
|
17548
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17549
|
+
color: "green",
|
|
17550
|
+
children: "\u2713 "
|
|
17551
|
+
}, undefined, false, undefined, this),
|
|
17552
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17553
|
+
color: "cyan",
|
|
17554
|
+
children: [
|
|
17555
|
+
t.short_id || t.id.slice(0, 8),
|
|
17556
|
+
" "
|
|
17557
|
+
]
|
|
17558
|
+
}, undefined, true, undefined, this),
|
|
17559
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17560
|
+
children: t.title
|
|
17561
|
+
}, undefined, false, undefined, this),
|
|
17562
|
+
t.duration_minutes != null && /* @__PURE__ */ jsxDEV8(Text7, {
|
|
17563
|
+
dimColor: true,
|
|
17564
|
+
children: [
|
|
17565
|
+
" (",
|
|
17566
|
+
t.duration_minutes,
|
|
17567
|
+
"m)"
|
|
17568
|
+
]
|
|
17569
|
+
}, undefined, true, undefined, this)
|
|
17570
|
+
]
|
|
17571
|
+
}, t.id, true, undefined, this))
|
|
17572
|
+
]
|
|
17573
|
+
}, undefined, true, undefined, this),
|
|
17574
|
+
recap && recap.stale.length > 0 && /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17575
|
+
flexDirection: "column",
|
|
17576
|
+
children: [
|
|
17577
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17578
|
+
bold: true,
|
|
17579
|
+
color: "red",
|
|
17580
|
+
children: [
|
|
17581
|
+
"Stale (",
|
|
17582
|
+
recap.stale.length,
|
|
17583
|
+
"):"
|
|
17584
|
+
]
|
|
17585
|
+
}, undefined, true, undefined, this),
|
|
17586
|
+
recap.stale.slice(0, 3).map((t) => /* @__PURE__ */ jsxDEV8(Box8, {
|
|
17587
|
+
children: [
|
|
17588
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17589
|
+
color: "red",
|
|
17590
|
+
children: "! "
|
|
17591
|
+
}, undefined, false, undefined, this),
|
|
17592
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17593
|
+
color: "cyan",
|
|
17594
|
+
children: [
|
|
17595
|
+
t.short_id || t.id.slice(0, 8),
|
|
17596
|
+
" "
|
|
17597
|
+
]
|
|
17598
|
+
}, undefined, true, undefined, this),
|
|
17599
|
+
/* @__PURE__ */ jsxDEV8(Text7, {
|
|
17600
|
+
children: t.title
|
|
17601
|
+
}, undefined, false, undefined, this)
|
|
17602
|
+
]
|
|
17603
|
+
}, t.id, true, undefined, this))
|
|
17604
|
+
]
|
|
17605
|
+
}, undefined, true, undefined, this)
|
|
17606
|
+
]
|
|
17607
|
+
}, undefined, true, undefined, this);
|
|
17608
|
+
}
|
|
17609
|
+
var init_Dashboard = __esm(() => {
|
|
17610
|
+
init_database();
|
|
17611
|
+
init_agents();
|
|
16498
17612
|
init_tasks();
|
|
16499
|
-
|
|
16500
|
-
init_search();
|
|
17613
|
+
init_audit();
|
|
16501
17614
|
});
|
|
16502
17615
|
|
|
16503
17616
|
// node_modules/commander/esm.mjs
|
|
@@ -16528,14 +17641,14 @@ init_search();
|
|
|
16528
17641
|
init_sync();
|
|
16529
17642
|
init_config();
|
|
16530
17643
|
import chalk from "chalk";
|
|
16531
|
-
import { execSync } from "child_process";
|
|
16532
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as
|
|
16533
|
-
import { basename, dirname as dirname4, join as
|
|
17644
|
+
import { execSync as execSync2 } from "child_process";
|
|
17645
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
17646
|
+
import { basename, dirname as dirname4, join as join9, resolve as resolve4 } from "path";
|
|
16534
17647
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
16535
17648
|
function getPackageVersion() {
|
|
16536
17649
|
try {
|
|
16537
|
-
const pkgPath =
|
|
16538
|
-
return JSON.parse(
|
|
17650
|
+
const pkgPath = join9(dirname4(fileURLToPath3(import.meta.url)), "..", "..", "package.json");
|
|
17651
|
+
return JSON.parse(readFileSync5(pkgPath, "utf-8")).version || "0.0.0";
|
|
16539
17652
|
} catch {
|
|
16540
17653
|
return "0.0.0";
|
|
16541
17654
|
}
|
|
@@ -16567,14 +17680,14 @@ function resolveTaskId(partialId) {
|
|
|
16567
17680
|
}
|
|
16568
17681
|
function detectGitRoot() {
|
|
16569
17682
|
try {
|
|
16570
|
-
return
|
|
17683
|
+
return execSync2("git rev-parse --show-toplevel", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
16571
17684
|
} catch {
|
|
16572
17685
|
return null;
|
|
16573
17686
|
}
|
|
16574
17687
|
}
|
|
16575
17688
|
function autoDetectProject(opts) {
|
|
16576
17689
|
if (opts.project) {
|
|
16577
|
-
return getProjectByPath(
|
|
17690
|
+
return getProjectByPath(resolve4(opts.project)) ?? undefined;
|
|
16578
17691
|
}
|
|
16579
17692
|
if (process.env["TODOS_AUTO_PROJECT"] === "false")
|
|
16580
17693
|
return;
|
|
@@ -16842,8 +17955,15 @@ program2.command("show <id>").description("Show full task details").action((id)
|
|
|
16842
17955
|
console.log(` ${chalk.dim("Tags:")} ${task.tags.join(", ")}`);
|
|
16843
17956
|
console.log(` ${chalk.dim("Version:")} ${task.version}`);
|
|
16844
17957
|
console.log(` ${chalk.dim("Created:")} ${task.created_at}`);
|
|
16845
|
-
if (task.
|
|
17958
|
+
if (task.started_at)
|
|
17959
|
+
console.log(` ${chalk.dim("Started:")} ${task.started_at}`);
|
|
17960
|
+
if (task.completed_at) {
|
|
16846
17961
|
console.log(` ${chalk.dim("Done:")} ${task.completed_at}`);
|
|
17962
|
+
if (task.started_at) {
|
|
17963
|
+
const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
|
|
17964
|
+
console.log(` ${chalk.dim("Duration:")} ${dur}m`);
|
|
17965
|
+
}
|
|
17966
|
+
}
|
|
16847
17967
|
if (task.subtasks.length > 0) {
|
|
16848
17968
|
console.log(chalk.bold(`
|
|
16849
17969
|
Subtasks (${task.subtasks.length}):`));
|
|
@@ -16874,6 +17994,137 @@ program2.command("show <id>").description("Show full task details").action((id)
|
|
|
16874
17994
|
}
|
|
16875
17995
|
}
|
|
16876
17996
|
});
|
|
17997
|
+
program2.command("inspect [id]").description("Full orientation for a task \u2014 details, description, dependencies, blockers, files, commits, comments. If no ID given, shows current in-progress task for --agent.").action((id) => {
|
|
17998
|
+
const globalOpts = program2.opts();
|
|
17999
|
+
let resolvedId = id ? resolveTaskId(id) : null;
|
|
18000
|
+
if (!resolvedId && globalOpts.agent) {
|
|
18001
|
+
const { listTasks: listTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
18002
|
+
const active = listTasks2({ status: "in_progress", assigned_to: globalOpts.agent });
|
|
18003
|
+
if (active.length > 0)
|
|
18004
|
+
resolvedId = active[0].id;
|
|
18005
|
+
}
|
|
18006
|
+
if (!resolvedId) {
|
|
18007
|
+
console.error(chalk.red("No task ID given and no active task found. Pass an ID or use --agent."));
|
|
18008
|
+
process.exit(1);
|
|
18009
|
+
}
|
|
18010
|
+
const task = getTaskWithRelations(resolvedId);
|
|
18011
|
+
if (!task) {
|
|
18012
|
+
console.error(chalk.red(`Task not found: ${id || resolvedId}`));
|
|
18013
|
+
process.exit(1);
|
|
18014
|
+
}
|
|
18015
|
+
if (globalOpts.json) {
|
|
18016
|
+
const { listTaskFiles: listTaskFiles2 } = (init_task_files(), __toCommonJS(exports_task_files));
|
|
18017
|
+
const { getTaskCommits: getTaskCommits2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
|
|
18018
|
+
try {
|
|
18019
|
+
task.files = listTaskFiles2(task.id);
|
|
18020
|
+
} catch {
|
|
18021
|
+
task.files = [];
|
|
18022
|
+
}
|
|
18023
|
+
try {
|
|
18024
|
+
task.commits = getTaskCommits2(task.id);
|
|
18025
|
+
} catch {
|
|
18026
|
+
task.commits = [];
|
|
18027
|
+
}
|
|
18028
|
+
output(task, true);
|
|
18029
|
+
return;
|
|
18030
|
+
}
|
|
18031
|
+
const sid = task.short_id || task.id.slice(0, 8);
|
|
18032
|
+
const statusColor = statusColors4[task.status] || chalk.white;
|
|
18033
|
+
const prioColor = priorityColors2[task.priority] || chalk.white;
|
|
18034
|
+
console.log(chalk.bold(`
|
|
18035
|
+
${chalk.cyan(sid)} ${statusColor(task.status)} ${prioColor(task.priority)} ${task.title}
|
|
18036
|
+
`));
|
|
18037
|
+
if (task.description) {
|
|
18038
|
+
console.log(chalk.dim("Description:"));
|
|
18039
|
+
console.log(` ${task.description}
|
|
18040
|
+
`);
|
|
18041
|
+
}
|
|
18042
|
+
if (task.assigned_to)
|
|
18043
|
+
console.log(` ${chalk.dim("Assigned:")} ${task.assigned_to}`);
|
|
18044
|
+
if (task.locked_by)
|
|
18045
|
+
console.log(` ${chalk.dim("Locked by:")} ${task.locked_by}`);
|
|
18046
|
+
if (task.project_id)
|
|
18047
|
+
console.log(` ${chalk.dim("Project:")} ${task.project_id}`);
|
|
18048
|
+
if (task.plan_id)
|
|
18049
|
+
console.log(` ${chalk.dim("Plan:")} ${task.plan_id}`);
|
|
18050
|
+
if (task.started_at)
|
|
18051
|
+
console.log(` ${chalk.dim("Started:")} ${task.started_at}`);
|
|
18052
|
+
if (task.completed_at) {
|
|
18053
|
+
console.log(` ${chalk.dim("Completed:")} ${task.completed_at}`);
|
|
18054
|
+
if (task.started_at) {
|
|
18055
|
+
const dur = Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 60000);
|
|
18056
|
+
console.log(` ${chalk.dim("Duration:")} ${dur}m`);
|
|
18057
|
+
}
|
|
18058
|
+
}
|
|
18059
|
+
if (task.estimated_minutes)
|
|
18060
|
+
console.log(` ${chalk.dim("Estimate:")} ${task.estimated_minutes}m`);
|
|
18061
|
+
if (task.tags.length > 0)
|
|
18062
|
+
console.log(` ${chalk.dim("Tags:")} ${task.tags.join(", ")}`);
|
|
18063
|
+
const unfinishedDeps = task.dependencies.filter((d) => d.status !== "completed" && d.status !== "cancelled");
|
|
18064
|
+
if (task.dependencies.length > 0) {
|
|
18065
|
+
console.log(chalk.bold(`
|
|
18066
|
+
Depends on (${task.dependencies.length}):`));
|
|
18067
|
+
for (const dep of task.dependencies) {
|
|
18068
|
+
const blocked = dep.status !== "completed" && dep.status !== "cancelled";
|
|
18069
|
+
const icon = blocked ? chalk.red("\u2717") : chalk.green("\u2713");
|
|
18070
|
+
console.log(` ${icon} ${formatTaskLine(dep)}`);
|
|
18071
|
+
}
|
|
18072
|
+
}
|
|
18073
|
+
if (unfinishedDeps.length > 0) {
|
|
18074
|
+
console.log(chalk.red(`
|
|
18075
|
+
BLOCKED by ${unfinishedDeps.length} unfinished dep(s)`));
|
|
18076
|
+
}
|
|
18077
|
+
if (task.blocked_by.length > 0) {
|
|
18078
|
+
console.log(chalk.bold(`
|
|
18079
|
+
Blocks (${task.blocked_by.length}):`));
|
|
18080
|
+
for (const b of task.blocked_by)
|
|
18081
|
+
console.log(` ${formatTaskLine(b)}`);
|
|
18082
|
+
}
|
|
18083
|
+
if (task.subtasks.length > 0) {
|
|
18084
|
+
console.log(chalk.bold(`
|
|
18085
|
+
Subtasks (${task.subtasks.length}):`));
|
|
18086
|
+
for (const st of task.subtasks)
|
|
18087
|
+
console.log(` ${formatTaskLine(st)}`);
|
|
18088
|
+
}
|
|
18089
|
+
try {
|
|
18090
|
+
const { listTaskFiles: listTaskFiles2 } = (init_task_files(), __toCommonJS(exports_task_files));
|
|
18091
|
+
const files = listTaskFiles2(task.id);
|
|
18092
|
+
if (files.length > 0) {
|
|
18093
|
+
console.log(chalk.bold(`
|
|
18094
|
+
Files (${files.length}):`));
|
|
18095
|
+
for (const f of files)
|
|
18096
|
+
console.log(` ${chalk.dim(f.role || "file")} ${f.path}`);
|
|
18097
|
+
}
|
|
18098
|
+
} catch {}
|
|
18099
|
+
try {
|
|
18100
|
+
const { getTaskCommits: getTaskCommits2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
|
|
18101
|
+
const commits = getTaskCommits2(task.id);
|
|
18102
|
+
if (commits.length > 0) {
|
|
18103
|
+
console.log(chalk.bold(`
|
|
18104
|
+
Commits (${commits.length}):`));
|
|
18105
|
+
for (const c of commits)
|
|
18106
|
+
console.log(` ${chalk.yellow(c.commit_hash.slice(0, 7))} ${c.message || ""}`);
|
|
18107
|
+
}
|
|
18108
|
+
} catch {}
|
|
18109
|
+
if (task.comments.length > 0) {
|
|
18110
|
+
console.log(chalk.bold(`
|
|
18111
|
+
Comments (${task.comments.length}):`));
|
|
18112
|
+
for (const c of task.comments) {
|
|
18113
|
+
const agent = c.agent_id ? chalk.cyan(`[${c.agent_id}] `) : "";
|
|
18114
|
+
console.log(` ${agent}${chalk.dim(c.created_at)}: ${c.content}`);
|
|
18115
|
+
}
|
|
18116
|
+
}
|
|
18117
|
+
if (task.checklist && task.checklist.length > 0) {
|
|
18118
|
+
const done = task.checklist.filter((c) => c.checked).length;
|
|
18119
|
+
console.log(chalk.bold(`
|
|
18120
|
+
Checklist (${done}/${task.checklist.length}):`));
|
|
18121
|
+
for (const item of task.checklist) {
|
|
18122
|
+
const icon = item.checked ? chalk.green("\u2611") : chalk.dim("\u2610");
|
|
18123
|
+
console.log(` ${icon} ${item.text || item.title}`);
|
|
18124
|
+
}
|
|
18125
|
+
}
|
|
18126
|
+
console.log();
|
|
18127
|
+
});
|
|
16877
18128
|
program2.command("history <id>").description("Show change history for a task (audit log)").action((id) => {
|
|
16878
18129
|
const globalOpts = program2.opts();
|
|
16879
18130
|
const resolvedId = resolveTaskId(id);
|
|
@@ -17377,7 +18628,7 @@ program2.command("deps <id>").description("Manage task dependencies").option("--
|
|
|
17377
18628
|
program2.command("projects").description("List and manage projects").option("--add <path>", "Register a project by path").option("--name <name>", "Project name (with --add)").option("--task-list-id <id>", "Custom task list ID (with --add)").action((opts) => {
|
|
17378
18629
|
const globalOpts = program2.opts();
|
|
17379
18630
|
if (opts.add) {
|
|
17380
|
-
const projectPath =
|
|
18631
|
+
const projectPath = resolve4(opts.add);
|
|
17381
18632
|
const name = opts.name || basename(projectPath);
|
|
17382
18633
|
const existing = getProjectByPath(projectPath);
|
|
17383
18634
|
let project;
|
|
@@ -17414,6 +18665,55 @@ program2.command("projects").description("List and manage projects").option("--a
|
|
|
17414
18665
|
console.log(`${chalk.dim(p.id.slice(0, 8))} ${chalk.bold(p.name)} ${chalk.dim(p.path)}${taskList}${p.description ? ` - ${p.description}` : ""}`);
|
|
17415
18666
|
}
|
|
17416
18667
|
});
|
|
18668
|
+
program2.command("extract <path>").description("Extract TODO/FIXME/HACK/BUG/XXX/NOTE comments from source files and create tasks").option("--dry-run", "Show extracted comments without creating tasks").option("--pattern <tags>", "Comma-separated tags to look for (default: TODO,FIXME,HACK,XXX,BUG,NOTE)").option("-t, --tags <tags>", "Extra comma-separated tags to add to created tasks").option("--assign <agent>", "Assign extracted tasks to an agent").option("--list <id>", "Task list ID").option("--ext <extensions>", "Comma-separated file extensions to scan (e.g. ts,py,go)").action((scanPath, opts) => {
|
|
18669
|
+
try {
|
|
18670
|
+
const globalOpts = program2.opts();
|
|
18671
|
+
const projectId = autoProject(globalOpts);
|
|
18672
|
+
const { extractTodos: extractTodos2, EXTRACT_TAGS: EXTRACT_TAGS2 } = (init_extract(), __toCommonJS(exports_extract));
|
|
18673
|
+
const patterns = opts.pattern ? opts.pattern.split(",").map((t) => t.trim().toUpperCase()) : undefined;
|
|
18674
|
+
const taskListId = opts.list ? (() => {
|
|
18675
|
+
const db = getDatabase();
|
|
18676
|
+
const id = resolvePartialId(db, "task_lists", opts.list);
|
|
18677
|
+
if (!id) {
|
|
18678
|
+
console.error(chalk.red(`Could not resolve task list ID: ${opts.list}`));
|
|
18679
|
+
process.exit(1);
|
|
18680
|
+
}
|
|
18681
|
+
return id;
|
|
18682
|
+
})() : undefined;
|
|
18683
|
+
const result = extractTodos2({
|
|
18684
|
+
path: resolve4(scanPath),
|
|
18685
|
+
patterns,
|
|
18686
|
+
project_id: projectId,
|
|
18687
|
+
task_list_id: taskListId,
|
|
18688
|
+
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
|
|
18689
|
+
assigned_to: opts.assign,
|
|
18690
|
+
agent_id: globalOpts.agent,
|
|
18691
|
+
dry_run: opts.dryRun,
|
|
18692
|
+
extensions: opts.ext ? opts.ext.split(",").map((e) => e.trim()) : undefined
|
|
18693
|
+
});
|
|
18694
|
+
if (globalOpts.json) {
|
|
18695
|
+
console.log(JSON.stringify(opts.dryRun ? { comments: result.comments } : { tasks_created: result.tasks.length, skipped: result.skipped, comments: result.comments.length }, null, 2));
|
|
18696
|
+
} else if (opts.dryRun) {
|
|
18697
|
+
console.log(chalk.cyan(`Found ${result.comments.length} comment(s):
|
|
18698
|
+
`));
|
|
18699
|
+
for (const c of result.comments) {
|
|
18700
|
+
console.log(` ${chalk.yellow(`[${c.tag}]`)} ${c.message}`);
|
|
18701
|
+
console.log(` ${chalk.gray(`${c.file}:${c.line}`)}`);
|
|
18702
|
+
}
|
|
18703
|
+
} else {
|
|
18704
|
+
console.log(chalk.green(`Created ${result.tasks.length} task(s)`));
|
|
18705
|
+
if (result.skipped > 0) {
|
|
18706
|
+
console.log(chalk.gray(`Skipped ${result.skipped} duplicate(s)`));
|
|
18707
|
+
}
|
|
18708
|
+
console.log(chalk.gray(`Total comments found: ${result.comments.length}`));
|
|
18709
|
+
for (const t of result.tasks) {
|
|
18710
|
+
console.log(formatTaskLine(t));
|
|
18711
|
+
}
|
|
18712
|
+
}
|
|
18713
|
+
} catch (e) {
|
|
18714
|
+
handleError(e);
|
|
18715
|
+
}
|
|
18716
|
+
});
|
|
17417
18717
|
program2.command("export").description("Export tasks").option("-f, --format <format>", "Format: json or md", "json").action((opts) => {
|
|
17418
18718
|
const globalOpts = program2.opts();
|
|
17419
18719
|
const projectId = autoProject(globalOpts);
|
|
@@ -17479,11 +18779,11 @@ var hooks = program2.command("hooks").description("Manage Claude Code hook integ
|
|
|
17479
18779
|
hooks.command("install").description("Install Claude Code hooks for auto-sync").action(() => {
|
|
17480
18780
|
let todosBin = "todos";
|
|
17481
18781
|
try {
|
|
17482
|
-
const p =
|
|
18782
|
+
const p = execSync2("which todos", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
17483
18783
|
if (p)
|
|
17484
18784
|
todosBin = p;
|
|
17485
18785
|
} catch {}
|
|
17486
|
-
const hooksDir =
|
|
18786
|
+
const hooksDir = join9(process.cwd(), ".claude", "hooks");
|
|
17487
18787
|
if (!existsSync7(hooksDir))
|
|
17488
18788
|
mkdirSync3(hooksDir, { recursive: true });
|
|
17489
18789
|
const hookScript = `#!/usr/bin/env bash
|
|
@@ -17509,11 +18809,11 @@ esac
|
|
|
17509
18809
|
|
|
17510
18810
|
exit 0
|
|
17511
18811
|
`;
|
|
17512
|
-
const hookPath =
|
|
18812
|
+
const hookPath = join9(hooksDir, "todos-sync.sh");
|
|
17513
18813
|
writeFileSync3(hookPath, hookScript);
|
|
17514
|
-
|
|
18814
|
+
execSync2(`chmod +x "${hookPath}"`);
|
|
17515
18815
|
console.log(chalk.green(`Hook script created: ${hookPath}`));
|
|
17516
|
-
const settingsPath =
|
|
18816
|
+
const settingsPath = join9(process.cwd(), ".claude", "settings.json");
|
|
17517
18817
|
const settings = readJsonFile2(settingsPath);
|
|
17518
18818
|
if (!settings["hooks"]) {
|
|
17519
18819
|
settings["hooks"] = {};
|
|
@@ -17556,11 +18856,11 @@ program2.command("mcp").description("Start MCP server (stdio)").option("--regist
|
|
|
17556
18856
|
var HOME2 = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
17557
18857
|
function getMcpBinaryPath() {
|
|
17558
18858
|
try {
|
|
17559
|
-
const p =
|
|
18859
|
+
const p = execSync2("which todos-mcp", { encoding: "utf-8" }).trim();
|
|
17560
18860
|
if (p)
|
|
17561
18861
|
return p;
|
|
17562
18862
|
} catch {}
|
|
17563
|
-
const bunBin =
|
|
18863
|
+
const bunBin = join9(HOME2, ".bun", "bin", "todos-mcp");
|
|
17564
18864
|
if (existsSync7(bunBin))
|
|
17565
18865
|
return bunBin;
|
|
17566
18866
|
return "todos-mcp";
|
|
@@ -17569,7 +18869,7 @@ function readJsonFile2(path) {
|
|
|
17569
18869
|
if (!existsSync7(path))
|
|
17570
18870
|
return {};
|
|
17571
18871
|
try {
|
|
17572
|
-
return JSON.parse(
|
|
18872
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
17573
18873
|
} catch {
|
|
17574
18874
|
return {};
|
|
17575
18875
|
}
|
|
@@ -17584,7 +18884,7 @@ function writeJsonFile2(path, data) {
|
|
|
17584
18884
|
function readTomlFile(path) {
|
|
17585
18885
|
if (!existsSync7(path))
|
|
17586
18886
|
return "";
|
|
17587
|
-
return
|
|
18887
|
+
return readFileSync5(path, "utf-8");
|
|
17588
18888
|
}
|
|
17589
18889
|
function writeTomlFile(path, content) {
|
|
17590
18890
|
const dir = dirname4(path);
|
|
@@ -17596,8 +18896,8 @@ function registerClaude(binPath, global) {
|
|
|
17596
18896
|
const scope = global ? "user" : "project";
|
|
17597
18897
|
const cmd = `claude mcp add --transport stdio --scope ${scope} todos -- ${binPath}`;
|
|
17598
18898
|
try {
|
|
17599
|
-
const { execSync:
|
|
17600
|
-
|
|
18899
|
+
const { execSync: execSync3 } = __require("child_process");
|
|
18900
|
+
execSync3(cmd, { stdio: "pipe" });
|
|
17601
18901
|
console.log(chalk.green(`Claude Code (${scope}): registered via 'claude mcp add'`));
|
|
17602
18902
|
} catch {
|
|
17603
18903
|
console.log(chalk.yellow(`Claude Code: could not auto-register. Run this command manually:`));
|
|
@@ -17606,8 +18906,8 @@ function registerClaude(binPath, global) {
|
|
|
17606
18906
|
}
|
|
17607
18907
|
function unregisterClaude(_global) {
|
|
17608
18908
|
try {
|
|
17609
|
-
const { execSync:
|
|
17610
|
-
|
|
18909
|
+
const { execSync: execSync3 } = __require("child_process");
|
|
18910
|
+
execSync3("claude mcp remove todos", { stdio: "pipe" });
|
|
17611
18911
|
console.log(chalk.green(`Claude Code: removed todos MCP server`));
|
|
17612
18912
|
} catch {
|
|
17613
18913
|
console.log(chalk.yellow(`Claude Code: could not auto-remove. Run manually:`));
|
|
@@ -17615,7 +18915,7 @@ function unregisterClaude(_global) {
|
|
|
17615
18915
|
}
|
|
17616
18916
|
}
|
|
17617
18917
|
function registerCodex(binPath) {
|
|
17618
|
-
const configPath =
|
|
18918
|
+
const configPath = join9(HOME2, ".codex", "config.toml");
|
|
17619
18919
|
let content = readTomlFile(configPath);
|
|
17620
18920
|
content = removeTomlBlock(content, "mcp_servers.todos");
|
|
17621
18921
|
const block = `
|
|
@@ -17629,7 +18929,7 @@ args = []
|
|
|
17629
18929
|
console.log(chalk.green(`Codex CLI: registered in ${configPath}`));
|
|
17630
18930
|
}
|
|
17631
18931
|
function unregisterCodex() {
|
|
17632
|
-
const configPath =
|
|
18932
|
+
const configPath = join9(HOME2, ".codex", "config.toml");
|
|
17633
18933
|
let content = readTomlFile(configPath);
|
|
17634
18934
|
if (!content.includes("[mcp_servers.todos]")) {
|
|
17635
18935
|
console.log(chalk.dim(`Codex CLI: todos not found in ${configPath}`));
|
|
@@ -17662,7 +18962,7 @@ function removeTomlBlock(content, blockName) {
|
|
|
17662
18962
|
`);
|
|
17663
18963
|
}
|
|
17664
18964
|
function registerGemini(binPath) {
|
|
17665
|
-
const configPath =
|
|
18965
|
+
const configPath = join9(HOME2, ".gemini", "settings.json");
|
|
17666
18966
|
const config = readJsonFile2(configPath);
|
|
17667
18967
|
if (!config["mcpServers"]) {
|
|
17668
18968
|
config["mcpServers"] = {};
|
|
@@ -17676,7 +18976,7 @@ function registerGemini(binPath) {
|
|
|
17676
18976
|
console.log(chalk.green(`Gemini CLI: registered in ${configPath}`));
|
|
17677
18977
|
}
|
|
17678
18978
|
function unregisterGemini() {
|
|
17679
|
-
const configPath =
|
|
18979
|
+
const configPath = join9(HOME2, ".gemini", "settings.json");
|
|
17680
18980
|
const config = readJsonFile2(configPath);
|
|
17681
18981
|
const servers = config["mcpServers"];
|
|
17682
18982
|
if (!servers || !("todos" in servers)) {
|
|
@@ -17724,6 +19024,115 @@ function unregisterMcp(agent, global) {
|
|
|
17724
19024
|
}
|
|
17725
19025
|
}
|
|
17726
19026
|
}
|
|
19027
|
+
program2.command("import <url>").description("Import a GitHub issue as a task").option("--project <id>", "Project ID").option("--list <id>", "Task list ID").action((url, opts) => {
|
|
19028
|
+
const globalOpts = program2.opts();
|
|
19029
|
+
const { parseGitHubUrl: parseGitHubUrl2, fetchGitHubIssue: fetchGitHubIssue2, issueToTask: issueToTask2 } = (init_github(), __toCommonJS(exports_github));
|
|
19030
|
+
const parsed = parseGitHubUrl2(url);
|
|
19031
|
+
if (!parsed) {
|
|
19032
|
+
console.error(chalk.red("Invalid GitHub issue URL. Expected: https://github.com/owner/repo/issues/123"));
|
|
19033
|
+
process.exit(1);
|
|
19034
|
+
}
|
|
19035
|
+
try {
|
|
19036
|
+
const issue = fetchGitHubIssue2(parsed.owner, parsed.repo, parsed.number);
|
|
19037
|
+
const projectId = opts.project || autoProject(globalOpts) || undefined;
|
|
19038
|
+
const input = issueToTask2(issue, { project_id: projectId, task_list_id: opts.list });
|
|
19039
|
+
const task = createTask(input);
|
|
19040
|
+
if (globalOpts.json) {
|
|
19041
|
+
output(task, true);
|
|
19042
|
+
return;
|
|
19043
|
+
}
|
|
19044
|
+
console.log(chalk.green(`Imported GH#${issue.number}: ${issue.title}`));
|
|
19045
|
+
console.log(` ${chalk.dim("Task ID:")} ${task.short_id || task.id}`);
|
|
19046
|
+
console.log(` ${chalk.dim("Labels:")} ${issue.labels.join(", ") || "none"}`);
|
|
19047
|
+
console.log(` ${chalk.dim("Priority:")} ${task.priority}`);
|
|
19048
|
+
} catch (e) {
|
|
19049
|
+
if (e.message?.includes("gh")) {
|
|
19050
|
+
console.error(chalk.red("GitHub CLI (gh) not found or not authenticated. Install: https://cli.github.com"));
|
|
19051
|
+
} else {
|
|
19052
|
+
console.error(chalk.red(`Import failed: ${e.message}`));
|
|
19053
|
+
}
|
|
19054
|
+
process.exit(1);
|
|
19055
|
+
}
|
|
19056
|
+
});
|
|
19057
|
+
program2.command("link-commit <task-id> <sha>").description("Link a git commit to a task").option("--message <text>", "Commit message").option("--author <name>", "Commit author").option("--files <list>", "Comma-separated list of changed files").action((taskId, sha, opts) => {
|
|
19058
|
+
const globalOpts = program2.opts();
|
|
19059
|
+
const resolvedId = resolveTaskId(taskId);
|
|
19060
|
+
const { linkTaskToCommit: linkTaskToCommit2 } = (init_task_commits(), __toCommonJS(exports_task_commits));
|
|
19061
|
+
const commit = linkTaskToCommit2({
|
|
19062
|
+
task_id: resolvedId,
|
|
19063
|
+
sha,
|
|
19064
|
+
message: opts.message,
|
|
19065
|
+
author: opts.author,
|
|
19066
|
+
files_changed: opts.files ? opts.files.split(",").filter(Boolean) : undefined
|
|
19067
|
+
});
|
|
19068
|
+
if (globalOpts.json) {
|
|
19069
|
+
output(commit, true);
|
|
19070
|
+
return;
|
|
19071
|
+
}
|
|
19072
|
+
console.log(chalk.green(`Linked commit ${sha.slice(0, 7)} to task ${taskId}`));
|
|
19073
|
+
});
|
|
19074
|
+
var hookCmd = program2.command("hook").description("Manage git hooks for auto-linking commits to tasks");
|
|
19075
|
+
hookCmd.command("install").description("Install post-commit hook that auto-links commits to tasks").action(() => {
|
|
19076
|
+
const { execSync: execSync3 } = __require("child_process");
|
|
19077
|
+
try {
|
|
19078
|
+
const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
|
|
19079
|
+
const hookPath = `${gitDir}/hooks/post-commit`;
|
|
19080
|
+
const { existsSync: existsSync8, readFileSync: readFileSync6, writeFileSync: writeFileSync4, chmodSync } = __require("fs");
|
|
19081
|
+
const marker = "# todos-auto-link";
|
|
19082
|
+
if (existsSync8(hookPath)) {
|
|
19083
|
+
const existing = readFileSync6(hookPath, "utf-8");
|
|
19084
|
+
if (existing.includes(marker)) {
|
|
19085
|
+
console.log(chalk.yellow("Hook already installed."));
|
|
19086
|
+
return;
|
|
19087
|
+
}
|
|
19088
|
+
writeFileSync4(hookPath, existing + `
|
|
19089
|
+
${marker}
|
|
19090
|
+
$(dirname "$0")/../../scripts/post-commit-hook.sh
|
|
19091
|
+
`);
|
|
19092
|
+
} else {
|
|
19093
|
+
writeFileSync4(hookPath, `#!/usr/bin/env bash
|
|
19094
|
+
${marker}
|
|
19095
|
+
$(dirname "$0")/../../scripts/post-commit-hook.sh
|
|
19096
|
+
`);
|
|
19097
|
+
chmodSync(hookPath, 493);
|
|
19098
|
+
}
|
|
19099
|
+
console.log(chalk.green("Post-commit hook installed. Commits with task IDs (e.g. OPE-00042) will auto-link."));
|
|
19100
|
+
} catch (e) {
|
|
19101
|
+
console.error(chalk.red("Not in a git repository or hook install failed."));
|
|
19102
|
+
process.exit(1);
|
|
19103
|
+
}
|
|
19104
|
+
});
|
|
19105
|
+
hookCmd.command("uninstall").description("Remove the todos post-commit hook").action(() => {
|
|
19106
|
+
const { execSync: execSync3 } = __require("child_process");
|
|
19107
|
+
try {
|
|
19108
|
+
const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
|
|
19109
|
+
const hookPath = `${gitDir}/hooks/post-commit`;
|
|
19110
|
+
const { existsSync: existsSync8, readFileSync: readFileSync6, writeFileSync: writeFileSync4 } = __require("fs");
|
|
19111
|
+
const marker = "# todos-auto-link";
|
|
19112
|
+
if (!existsSync8(hookPath)) {
|
|
19113
|
+
console.log(chalk.dim("No post-commit hook found."));
|
|
19114
|
+
return;
|
|
19115
|
+
}
|
|
19116
|
+
const content = readFileSync6(hookPath, "utf-8");
|
|
19117
|
+
if (!content.includes(marker)) {
|
|
19118
|
+
console.log(chalk.dim("Hook not managed by todos."));
|
|
19119
|
+
return;
|
|
19120
|
+
}
|
|
19121
|
+
const cleaned = content.split(`
|
|
19122
|
+
`).filter((l) => !l.includes(marker) && !l.includes("post-commit-hook.sh")).join(`
|
|
19123
|
+
`).trim();
|
|
19124
|
+
if (cleaned === "#!/usr/bin/env bash" || cleaned === "") {
|
|
19125
|
+
__require("fs").unlinkSync(hookPath);
|
|
19126
|
+
} else {
|
|
19127
|
+
writeFileSync4(hookPath, cleaned + `
|
|
19128
|
+
`);
|
|
19129
|
+
}
|
|
19130
|
+
console.log(chalk.green("Post-commit hook removed."));
|
|
19131
|
+
} catch (e) {
|
|
19132
|
+
console.error(chalk.red("Not in a git repository or hook removal failed."));
|
|
19133
|
+
process.exit(1);
|
|
19134
|
+
}
|
|
19135
|
+
});
|
|
17727
19136
|
program2.command("init <name>").description("Register an agent and get a short UUID").option("-d, --description <text>", "Agent description").action((name, opts) => {
|
|
17728
19137
|
const globalOpts = program2.opts();
|
|
17729
19138
|
try {
|
|
@@ -17765,6 +19174,30 @@ program2.command("heartbeat [agent]").description("Update last_seen_at to signal
|
|
|
17765
19174
|
console.log(chalk.green(`\u2665 ${a.name} (${a.id.slice(0, 8)}) \u2014 heartbeat sent`));
|
|
17766
19175
|
}
|
|
17767
19176
|
});
|
|
19177
|
+
program2.command("release [agent]").description("Release/logout an agent \u2014 clears session binding so the name is immediately available").option("--session-id <id>", "Only release if session ID matches").action((agent, opts) => {
|
|
19178
|
+
const globalOpts = program2.opts();
|
|
19179
|
+
const agentId = agent || globalOpts.agent;
|
|
19180
|
+
if (!agentId) {
|
|
19181
|
+
console.error(chalk.red("Agent ID or name required. Use --agent or pass as argument."));
|
|
19182
|
+
process.exit(1);
|
|
19183
|
+
}
|
|
19184
|
+
const { getAgent: getAgent2, getAgentByName: getAgentByName2 } = (init_agents(), __toCommonJS(exports_agents));
|
|
19185
|
+
const a = getAgent2(agentId) || getAgentByName2(agentId);
|
|
19186
|
+
if (!a) {
|
|
19187
|
+
console.error(chalk.red(`Agent not found: ${agentId}`));
|
|
19188
|
+
process.exit(1);
|
|
19189
|
+
}
|
|
19190
|
+
const released = releaseAgent(a.id, opts?.sessionId);
|
|
19191
|
+
if (!released) {
|
|
19192
|
+
console.error(chalk.red("Release denied: session_id does not match agent's current session."));
|
|
19193
|
+
process.exit(1);
|
|
19194
|
+
}
|
|
19195
|
+
if (globalOpts.json) {
|
|
19196
|
+
console.log(JSON.stringify({ agent_id: a.id, name: a.name, released: true }));
|
|
19197
|
+
} else {
|
|
19198
|
+
console.log(chalk.green(`\u2713 ${a.name} (${a.id}) released \u2014 name is now available.`));
|
|
19199
|
+
}
|
|
19200
|
+
});
|
|
17768
19201
|
program2.command("focus [project]").description("Focus on a project (or clear focus if no project given)").action((project) => {
|
|
17769
19202
|
const globalOpts = program2.opts();
|
|
17770
19203
|
const agentId = globalOpts.agent;
|
|
@@ -17990,13 +19423,13 @@ Update available: ${currentVersion} \u2192 ${latestVersion}`));
|
|
|
17990
19423
|
}
|
|
17991
19424
|
let useBun = false;
|
|
17992
19425
|
try {
|
|
17993
|
-
|
|
19426
|
+
execSync2("which bun", { stdio: "ignore" });
|
|
17994
19427
|
useBun = true;
|
|
17995
19428
|
} catch {}
|
|
17996
19429
|
const cmd = useBun ? "bun add -g @hasna/todos@latest" : "npm install -g @hasna/todos@latest";
|
|
17997
19430
|
console.log(chalk.dim(`
|
|
17998
19431
|
Running: ${cmd}`));
|
|
17999
|
-
|
|
19432
|
+
execSync2(cmd, { stdio: "inherit" });
|
|
18000
19433
|
console.log(chalk.green(`
|
|
18001
19434
|
Updated to ${latestVersion}!`));
|
|
18002
19435
|
} catch (e) {
|
|
@@ -18005,7 +19438,7 @@ Updated to ${latestVersion}!`));
|
|
|
18005
19438
|
});
|
|
18006
19439
|
program2.command("config").description("View or update configuration").option("--get <key>", "Get a config value").option("--set <key=value>", "Set a config value (e.g. completion_guard.enabled=true)").action((opts) => {
|
|
18007
19440
|
const globalOpts = program2.opts();
|
|
18008
|
-
const configPath =
|
|
19441
|
+
const configPath = join9(process.env["HOME"] || "~", ".todos", "config.json");
|
|
18009
19442
|
if (opts.get) {
|
|
18010
19443
|
const config2 = loadConfig();
|
|
18011
19444
|
const keys = opts.get.split(".");
|
|
@@ -18031,7 +19464,7 @@ program2.command("config").description("View or update configuration").option("-
|
|
|
18031
19464
|
}
|
|
18032
19465
|
let config2 = {};
|
|
18033
19466
|
try {
|
|
18034
|
-
config2 = JSON.parse(
|
|
19467
|
+
config2 = JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
18035
19468
|
} catch {}
|
|
18036
19469
|
const keys = key.split(".");
|
|
18037
19470
|
let obj = config2;
|
|
@@ -18188,6 +19621,51 @@ program2.command("interactive").description("Launch interactive TUI").action(asy
|
|
|
18188
19621
|
const projectId = autoProject(globalOpts);
|
|
18189
19622
|
renderApp2(projectId);
|
|
18190
19623
|
});
|
|
19624
|
+
program2.command("blame <file>").description("Show which tasks/agents touched a file and why \u2014 combines task_files + task_commits").action((filePath) => {
|
|
19625
|
+
const globalOpts = program2.opts();
|
|
19626
|
+
const { findTasksByFile: findTasksByFile2 } = (init_task_files(), __toCommonJS(exports_task_files));
|
|
19627
|
+
const { getTask: getTask2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
19628
|
+
const db = getDatabase();
|
|
19629
|
+
const taskFiles = findTasksByFile2(filePath, db);
|
|
19630
|
+
const commitRows = db.query("SELECT tc.*, t.title, t.short_id FROM task_commits tc JOIN tasks t ON t.id = tc.task_id WHERE tc.files_changed LIKE ? ORDER BY tc.committed_at DESC").all(`%${filePath}%`);
|
|
19631
|
+
if (globalOpts.json) {
|
|
19632
|
+
output({ file: filePath, task_files: taskFiles, commits: commitRows }, true);
|
|
19633
|
+
return;
|
|
19634
|
+
}
|
|
19635
|
+
console.log(chalk.bold(`
|
|
19636
|
+
Blame: ${filePath}
|
|
19637
|
+
`));
|
|
19638
|
+
if (taskFiles.length > 0) {
|
|
19639
|
+
console.log(chalk.bold("Task File Links:"));
|
|
19640
|
+
for (const tf of taskFiles) {
|
|
19641
|
+
const task = getTask2(tf.task_id, db);
|
|
19642
|
+
const title = task ? task.title : "unknown";
|
|
19643
|
+
const sid = task?.short_id || tf.task_id.slice(0, 8);
|
|
19644
|
+
console.log(` ${chalk.cyan(sid)} ${title} \u2014 ${chalk.dim(tf.role || "file")} ${chalk.dim(tf.updated_at)}`);
|
|
19645
|
+
}
|
|
19646
|
+
}
|
|
19647
|
+
if (commitRows.length > 0) {
|
|
19648
|
+
console.log(chalk.bold(`
|
|
19649
|
+
Commit Links (${commitRows.length}):`));
|
|
19650
|
+
for (const c of commitRows) {
|
|
19651
|
+
const sid = c.short_id || c.task_id.slice(0, 8);
|
|
19652
|
+
console.log(` ${chalk.yellow(c.sha?.slice(0, 7) || "?")} ${chalk.cyan(sid)} ${c.title || ""} \u2014 ${chalk.dim(c.author || "")} ${chalk.dim(c.committed_at || "")}`);
|
|
19653
|
+
}
|
|
19654
|
+
}
|
|
19655
|
+
if (taskFiles.length === 0 && commitRows.length === 0) {
|
|
19656
|
+
console.log(chalk.dim("No task or commit links found for this file."));
|
|
19657
|
+
console.log(chalk.dim("Use 'todos hook install' to auto-link future commits."));
|
|
19658
|
+
}
|
|
19659
|
+
console.log();
|
|
19660
|
+
});
|
|
19661
|
+
program2.command("dashboard").description("Live-updating dashboard showing project health, agents, task flow").option("--project <id>", "Filter to project").option("--refresh <ms>", "Refresh interval in ms (default: 2000)", "2000").action(async (opts) => {
|
|
19662
|
+
const { render: render2 } = await import("ink");
|
|
19663
|
+
const React = await import("react");
|
|
19664
|
+
const { Dashboard: Dashboard2 } = await Promise.resolve().then(() => (init_Dashboard(), exports_Dashboard));
|
|
19665
|
+
const globalOpts = program2.opts();
|
|
19666
|
+
const projectId = opts.project || autoProject(globalOpts) || undefined;
|
|
19667
|
+
render2(React.createElement(Dashboard2, { projectId, refreshMs: parseInt(opts.refresh, 10) }));
|
|
19668
|
+
});
|
|
18191
19669
|
program2.command("next").description("Show the best pending task to work on next").option("--agent <id>", "Prefer tasks assigned to this agent").option("--project <id>", "Filter to project").option("--json", "Output as JSON").action(async (opts) => {
|
|
18192
19670
|
const db = getDatabase();
|
|
18193
19671
|
const filters = {};
|
|
@@ -18253,6 +19731,120 @@ Next up:`));
|
|
|
18253
19731
|
console.log(` ${chalk.cyan(t.short_id || t.id.slice(0, 8))} ${chalk.yellow(t.priority)} ${t.title}`);
|
|
18254
19732
|
}
|
|
18255
19733
|
});
|
|
19734
|
+
program2.command("recap").description("Show what happened in the last N hours \u2014 completed tasks, new tasks, agent activity, blockers").option("--hours <n>", "Look back N hours (default: 8)", "8").option("--project <id>", "Filter to project").action((opts) => {
|
|
19735
|
+
const globalOpts = program2.opts();
|
|
19736
|
+
const { getRecap: getRecap2 } = (init_audit(), __toCommonJS(exports_audit));
|
|
19737
|
+
const recap = getRecap2(parseInt(opts.hours, 10), opts.project);
|
|
19738
|
+
if (globalOpts.json) {
|
|
19739
|
+
output(recap, true);
|
|
19740
|
+
return;
|
|
19741
|
+
}
|
|
19742
|
+
console.log(chalk.bold(`
|
|
19743
|
+
Recap \u2014 last ${recap.hours} hours (since ${new Date(recap.since).toLocaleString()})
|
|
19744
|
+
`));
|
|
19745
|
+
if (recap.completed.length > 0) {
|
|
19746
|
+
console.log(chalk.green.bold(`Completed (${recap.completed.length}):`));
|
|
19747
|
+
for (const t of recap.completed) {
|
|
19748
|
+
const id = t.short_id || t.id.slice(0, 8);
|
|
19749
|
+
const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
|
|
19750
|
+
console.log(` ${chalk.green("\u2713")} ${chalk.cyan(id)} ${t.title}${dur}${t.assigned_to ? ` \u2014 ${chalk.dim(t.assigned_to)}` : ""}`);
|
|
19751
|
+
}
|
|
19752
|
+
} else {
|
|
19753
|
+
console.log(chalk.dim("No tasks completed in this period."));
|
|
19754
|
+
}
|
|
19755
|
+
if (recap.in_progress.length > 0) {
|
|
19756
|
+
console.log(chalk.blue.bold(`
|
|
19757
|
+
In Progress (${recap.in_progress.length}):`));
|
|
19758
|
+
for (const t of recap.in_progress) {
|
|
19759
|
+
const id = t.short_id || t.id.slice(0, 8);
|
|
19760
|
+
console.log(` ${chalk.blue("\u2192")} ${chalk.cyan(id)} ${t.title}${t.assigned_to ? ` \u2014 ${chalk.dim(t.assigned_to)}` : ""}`);
|
|
19761
|
+
}
|
|
19762
|
+
}
|
|
19763
|
+
if (recap.blocked.length > 0) {
|
|
19764
|
+
console.log(chalk.red.bold(`
|
|
19765
|
+
Blocked (${recap.blocked.length}):`));
|
|
19766
|
+
for (const t of recap.blocked) {
|
|
19767
|
+
const id = t.short_id || t.id.slice(0, 8);
|
|
19768
|
+
console.log(` ${chalk.red("\u2717")} ${chalk.cyan(id)} ${t.title}`);
|
|
19769
|
+
}
|
|
19770
|
+
}
|
|
19771
|
+
if (recap.stale.length > 0) {
|
|
19772
|
+
console.log(chalk.yellow.bold(`
|
|
19773
|
+
Stale (${recap.stale.length}):`));
|
|
19774
|
+
for (const t of recap.stale) {
|
|
19775
|
+
const id = t.short_id || t.id.slice(0, 8);
|
|
19776
|
+
const ago = Math.round((Date.now() - new Date(t.updated_at).getTime()) / 60000);
|
|
19777
|
+
console.log(` ${chalk.yellow("!")} ${chalk.cyan(id)} ${t.title} \u2014 last update ${ago}m ago`);
|
|
19778
|
+
}
|
|
19779
|
+
}
|
|
19780
|
+
if (recap.created.length > 0) {
|
|
19781
|
+
console.log(chalk.dim.bold(`
|
|
19782
|
+
Created (${recap.created.length}):`));
|
|
19783
|
+
for (const t of recap.created.slice(0, 10)) {
|
|
19784
|
+
const id = t.short_id || t.id.slice(0, 8);
|
|
19785
|
+
console.log(` ${chalk.dim("+")} ${chalk.cyan(id)} ${t.title}`);
|
|
19786
|
+
}
|
|
19787
|
+
if (recap.created.length > 10)
|
|
19788
|
+
console.log(chalk.dim(` ... and ${recap.created.length - 10} more`));
|
|
19789
|
+
}
|
|
19790
|
+
if (recap.agents.length > 0) {
|
|
19791
|
+
console.log(chalk.bold(`
|
|
19792
|
+
Agents:`));
|
|
19793
|
+
for (const a of recap.agents) {
|
|
19794
|
+
const seen = Math.round((Date.now() - new Date(a.last_seen_at).getTime()) / 60000);
|
|
19795
|
+
console.log(` ${a.name}: ${chalk.green(a.completed_count + " done")} | ${chalk.blue(a.in_progress_count + " active")} | last seen ${seen}m ago`);
|
|
19796
|
+
}
|
|
19797
|
+
}
|
|
19798
|
+
console.log();
|
|
19799
|
+
});
|
|
19800
|
+
program2.command("standup").description("Generate standup notes \u2014 completed since yesterday, in progress, blocked. Grouped by agent.").option("--since <date>", "ISO date or 'yesterday' (default: yesterday)").option("--project <id>", "Filter to project").action((opts) => {
|
|
19801
|
+
const globalOpts = program2.opts();
|
|
19802
|
+
const sinceDate = opts.since === "yesterday" || !opts.since ? new Date(Date.now() - 24 * 60 * 60 * 1000) : new Date(opts.since);
|
|
19803
|
+
const hours = Math.max(1, Math.round((Date.now() - sinceDate.getTime()) / (60 * 60 * 1000)));
|
|
19804
|
+
const { getRecap: getRecap2 } = (init_audit(), __toCommonJS(exports_audit));
|
|
19805
|
+
const recap = getRecap2(hours, opts.project);
|
|
19806
|
+
if (globalOpts.json) {
|
|
19807
|
+
output(recap, true);
|
|
19808
|
+
return;
|
|
19809
|
+
}
|
|
19810
|
+
console.log(chalk.bold(`
|
|
19811
|
+
Standup \u2014 since ${sinceDate.toLocaleDateString()}
|
|
19812
|
+
`));
|
|
19813
|
+
const byAgent = new Map;
|
|
19814
|
+
for (const t of recap.completed) {
|
|
19815
|
+
const agent = t.assigned_to || "unassigned";
|
|
19816
|
+
if (!byAgent.has(agent))
|
|
19817
|
+
byAgent.set(agent, []);
|
|
19818
|
+
byAgent.get(agent).push(t);
|
|
19819
|
+
}
|
|
19820
|
+
if (byAgent.size > 0) {
|
|
19821
|
+
console.log(chalk.green.bold("Done:"));
|
|
19822
|
+
for (const [agent, tasks] of byAgent) {
|
|
19823
|
+
console.log(` ${chalk.cyan(agent)}:`);
|
|
19824
|
+
for (const t of tasks) {
|
|
19825
|
+
const dur = t.duration_minutes != null ? ` (${t.duration_minutes}m)` : "";
|
|
19826
|
+
console.log(` ${chalk.green("\u2713")} ${t.short_id || t.id.slice(0, 8)} ${t.title}${dur}`);
|
|
19827
|
+
}
|
|
19828
|
+
}
|
|
19829
|
+
} else {
|
|
19830
|
+
console.log(chalk.dim("Nothing completed."));
|
|
19831
|
+
}
|
|
19832
|
+
if (recap.in_progress.length > 0) {
|
|
19833
|
+
console.log(chalk.blue.bold(`
|
|
19834
|
+
In Progress:`));
|
|
19835
|
+
for (const t of recap.in_progress) {
|
|
19836
|
+
console.log(` ${chalk.blue("\u2192")} ${t.short_id || t.id.slice(0, 8)} ${t.title}${t.assigned_to ? ` \u2014 ${chalk.dim(t.assigned_to)}` : ""}`);
|
|
19837
|
+
}
|
|
19838
|
+
}
|
|
19839
|
+
if (recap.blocked.length > 0) {
|
|
19840
|
+
console.log(chalk.red.bold(`
|
|
19841
|
+
Blocked:`));
|
|
19842
|
+
for (const t of recap.blocked) {
|
|
19843
|
+
console.log(` ${chalk.red("\u2717")} ${t.short_id || t.id.slice(0, 8)} ${t.title}`);
|
|
19844
|
+
}
|
|
19845
|
+
}
|
|
19846
|
+
console.log();
|
|
19847
|
+
});
|
|
18256
19848
|
program2.command("fail <id>").description("Mark a task as failed with optional reason and retry").option("--reason <text>", "Why it failed").option("--agent <id>", "Agent reporting the failure").option("--retry", "Auto-create a retry copy").option("--json", "Output as JSON").action(async (id, opts) => {
|
|
18257
19849
|
const db = getDatabase();
|
|
18258
19850
|
const resolvedId = resolvePartialId(db, "tasks", id);
|
|
@@ -18541,11 +20133,11 @@ program2.command("health").description("Check todos system health \u2014 databas
|
|
|
18541
20133
|
try {
|
|
18542
20134
|
const db = getDatabase();
|
|
18543
20135
|
const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
|
|
18544
|
-
const { statSync:
|
|
20136
|
+
const { statSync: statSync3 } = __require("fs");
|
|
18545
20137
|
const dbPath = process.env["TODOS_DB_PATH"] || __require("path").join(process.env["HOME"] || "~", ".todos", "todos.db");
|
|
18546
20138
|
let size = "unknown";
|
|
18547
20139
|
try {
|
|
18548
|
-
size = `${(
|
|
20140
|
+
size = `${(statSync3(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
|
|
18549
20141
|
} catch {}
|
|
18550
20142
|
checks.push({ name: "Database", ok: true, message: `${row.count} tasks \xB7 ${size} \xB7 ${chalk.dim(dbPath)}` });
|
|
18551
20143
|
} catch (e) {
|