@dalmasonto/taskflow-mcp 1.0.33 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -193
- package/dist/attachment-download.d.ts +74 -0
- package/dist/attachment-download.js +193 -0
- package/dist/attachment-download.js.map +1 -0
- package/dist/attachments.d.ts +23 -0
- package/dist/attachments.js +66 -0
- package/dist/attachments.js.map +1 -0
- package/dist/client.d.ts +206 -0
- package/dist/client.js +279 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +137 -18
- package/dist/config.js +187 -106
- package/dist/config.js.map +1 -0
- package/dist/connect.d.ts +89 -0
- package/dist/connect.js +269 -0
- package/dist/connect.js.map +1 -0
- package/dist/doctor.d.ts +24 -0
- package/dist/doctor.js +120 -0
- package/dist/doctor.js.map +1 -0
- package/dist/events.d.ts +186 -0
- package/dist/events.js +407 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +124 -184
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.js +114 -0
- package/dist/instructions.js.map +1 -0
- package/dist/mint.d.ts +62 -0
- package/dist/mint.js +135 -0
- package/dist/mint.js.map +1 -0
- package/dist/mirror.d.ts +68 -0
- package/dist/mirror.js +103 -0
- package/dist/mirror.js.map +1 -0
- package/dist/pane-queue.d.ts +29 -0
- package/dist/pane-queue.js +35 -0
- package/dist/pane-queue.js.map +1 -0
- package/dist/prompts.d.ts +79 -0
- package/dist/prompts.js +211 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resolve.d.ts +72 -0
- package/dist/resolve.js +89 -0
- package/dist/resolve.js.map +1 -0
- package/dist/runtime.d.ts +54 -0
- package/dist/runtime.js +321 -0
- package/dist/runtime.js.map +1 -0
- package/dist/server.d.ts +56 -0
- package/dist/server.js +793 -0
- package/dist/server.js.map +1 -0
- package/dist/session-identifier.d.ts +48 -0
- package/dist/session-identifier.js +44 -0
- package/dist/session-identifier.js.map +1 -0
- package/dist/sessions-store.d.ts +38 -0
- package/dist/sessions-store.js +88 -0
- package/dist/sessions-store.js.map +1 -0
- package/dist/tmux.d.ts +200 -0
- package/dist/tmux.js +580 -0
- package/dist/tmux.js.map +1 -0
- package/hooks/metadata.mjs +99 -0
- package/hooks/permission-prompt.mjs +100 -0
- package/hooks/taskflow-hook.mjs +499 -0
- package/hooks/tool-logging.mjs +63 -0
- package/package.json +38 -29
- package/dist/agent-registry.d.ts +0 -27
- package/dist/agent-registry.js +0 -153
- package/dist/db.d.ts +0 -5
- package/dist/db.js +0 -220
- package/dist/helpers.d.ts +0 -21
- package/dist/helpers.js +0 -27
- package/dist/resources.d.ts +0 -2
- package/dist/resources.js +0 -89
- package/dist/sse.d.ts +0 -10
- package/dist/sse.js +0 -766
- package/dist/tmux-bridge.d.ts +0 -12
- package/dist/tmux-bridge.js +0 -157
- package/dist/tools/activity.d.ts +0 -39
- package/dist/tools/activity.js +0 -152
- package/dist/tools/agent-inbox.d.ts +0 -12
- package/dist/tools/agent-inbox.js +0 -257
- package/dist/tools/agent.d.ts +0 -14
- package/dist/tools/agent.js +0 -168
- package/dist/tools/analytics.d.ts +0 -21
- package/dist/tools/analytics.js +0 -191
- package/dist/tools/checkpoint.d.ts +0 -27
- package/dist/tools/checkpoint.js +0 -105
- package/dist/tools/notifications.d.ts +0 -31
- package/dist/tools/notifications.js +0 -59
- package/dist/tools/projects.d.ts +0 -55
- package/dist/tools/projects.js +0 -112
- package/dist/tools/settings.d.ts +0 -19
- package/dist/tools/settings.js +0 -73
- package/dist/tools/tasks.d.ts +0 -105
- package/dist/tools/tasks.js +0 -403
- package/dist/tools/terminal.d.ts +0 -4
- package/dist/tools/terminal.js +0 -98
- package/dist/tools/timer.d.ts +0 -37
- package/dist/tools/timer.js +0 -154
- package/dist/types.d.ts +0 -82
- package/dist/types.js +0 -30
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tool calls are worth an activity row (#56).
|
|
3
|
+
*
|
|
4
|
+
* The PostToolUse hook logged every tool call. Measured on project 2: 5713
|
|
5
|
+
* activity rows in 5 days (~1140/day), of which `tool:Bash` was 2728,
|
|
6
|
+
* `tool:Edit` 707 and `tool:Read` 584. Roughly 70% was read-only exploration —
|
|
7
|
+
* rows nobody reads back, but which every client paging the feed still carries,
|
|
8
|
+
* and which crowd genuine notes out of view.
|
|
9
|
+
*
|
|
10
|
+
* The activity feed is a JOURNAL: what an agent did, for a human reconstructing
|
|
11
|
+
* it later. "Read a file" is not that. "Edited a file", "changed a task status",
|
|
12
|
+
* "sent a message" are.
|
|
13
|
+
*
|
|
14
|
+
* A SKIP list, not an allowlist: an unrecognised tool is more likely to matter
|
|
15
|
+
* than not, and one extra row costs far less than silently losing something that
|
|
16
|
+
* did. New tools default to being recorded.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately dependency-free (like metadata.mjs) so the hook needs no build.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/// Read-only or high-volume-low-signal. Bash is the largest single writer and
|
|
22
|
+
/// the most debatable entry: it is often meaningful (running tests, committing),
|
|
23
|
+
/// but at 2728 rows in five days it drowns the feed it is meant to inform. Move
|
|
24
|
+
/// it out of this list if the journal starts feeling too thin.
|
|
25
|
+
const SKIP_TOOLS = new Set([
|
|
26
|
+
"Read",
|
|
27
|
+
"Grep",
|
|
28
|
+
"Glob",
|
|
29
|
+
"Bash",
|
|
30
|
+
"WebFetch",
|
|
31
|
+
"WebSearch",
|
|
32
|
+
"NotebookRead",
|
|
33
|
+
"TodoWrite",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/// #57: TaskFlow's own tools already write a SEMANTIC row from the backend —
|
|
37
|
+
/// `status_changed` carrying `#57 "title" · in_progress -> partial_done`, `note`
|
|
38
|
+
/// carrying the note itself. The hook's parallel row describes the same event
|
|
39
|
+
/// with strictly less: body "completed", no task link, the id buried in JSON.
|
|
40
|
+
///
|
|
41
|
+
/// Measured on project 2: 594 rows, 10% of the feed, 257 KB. The two worst are
|
|
42
|
+
/// pure duplication of content that exists in full elsewhere —
|
|
43
|
+
/// send_message (112 KB) embeds whole message bodies that ARE the message
|
|
44
|
+
/// table, log_activity (86 KB) embeds the note body of the row it just created.
|
|
45
|
+
///
|
|
46
|
+
/// Scoped to these prefixes on purpose: another MCP server's tools are somebody
|
|
47
|
+
/// else's events, and nothing writes a semantic row for them. The prefix is the
|
|
48
|
+
/// user's server key in .mcp.json — "taskflow" is the documented name, and
|
|
49
|
+
/// "taskflow_v2" survives from configs written before the 2.0.0 rename.
|
|
50
|
+
const TASKFLOW_TOOL_PREFIXES = ["mcp__taskflow__", "mcp__taskflow_v2__"];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether this tool call should be recorded as activity.
|
|
54
|
+
*
|
|
55
|
+
* @param {string | undefined | null} toolName
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
export function shouldLogTool(toolName) {
|
|
59
|
+
if (!toolName) return false;
|
|
60
|
+
if (TASKFLOW_TOOL_PREFIXES.some((p) => toolName.startsWith(p))) return false;
|
|
61
|
+
// Exact match only — a tool merely CONTAINING "Read" is not the Read tool.
|
|
62
|
+
return !SKIP_TOOLS.has(toolName);
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,57 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dalmasonto/taskflow-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MCP server
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "TaskFlow MCP server + Claude Code hooks — multi-agent task board, realtime chat, reviews, terminal streaming, and activity logging for AI coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
6
|
"bin": {
|
|
8
|
-
"taskflow-mcp": "dist/index.js"
|
|
7
|
+
"taskflow-mcp": "dist/index.js",
|
|
8
|
+
"taskflow-hook": "hooks/taskflow-hook.mjs"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"hooks",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"//build": "chmod is not decoration: `bin` is exec'd by name, and tsc emits 0644. npm sets the bit at install time, so a rebuild over an EXISTING dist inherits it and looks fine — but a clean build (rm -rf dist) emits a fresh, non-executable file and every launch then fails with EACCES.",
|
|
18
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
19
|
+
"dev": "tsx src/index.ts",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"smoke": "node scripts/smoke.mjs",
|
|
25
|
+
"prepublishOnly": "npm run test && npm run build"
|
|
9
26
|
},
|
|
10
27
|
"keywords": [
|
|
11
28
|
"mcp",
|
|
12
29
|
"model-context-protocol",
|
|
30
|
+
"taskflow",
|
|
13
31
|
"task-manager",
|
|
14
32
|
"ai-agent",
|
|
33
|
+
"multi-agent",
|
|
15
34
|
"claude",
|
|
35
|
+
"claude-code",
|
|
16
36
|
"cursor",
|
|
17
37
|
"windsurf",
|
|
18
38
|
"productivity",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
39
|
+
"kanban",
|
|
40
|
+
"realtime"
|
|
21
41
|
],
|
|
22
42
|
"author": "dalmasonto",
|
|
23
43
|
"license": "MIT",
|
|
24
44
|
"repository": {
|
|
25
45
|
"type": "git",
|
|
26
|
-
"url": "https://github.com/dalmasonto/task_flow",
|
|
27
|
-
"directory": "mcp
|
|
46
|
+
"url": "git+https://github.com/dalmasonto/task_flow.git",
|
|
47
|
+
"directory": "mcp"
|
|
28
48
|
},
|
|
29
|
-
"homepage": "https://github.com/dalmasonto/task_flow#
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"dev": "tsc --watch",
|
|
33
|
-
"start": "node dist/index.js",
|
|
34
|
-
"prepublishOnly": "npm run build",
|
|
35
|
-
"test": "vitest run",
|
|
36
|
-
"test:watch": "vitest"
|
|
49
|
+
"homepage": "https://github.com/dalmasonto/task_flow#readme",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/dalmasonto/task_flow/issues"
|
|
37
52
|
},
|
|
38
53
|
"dependencies": {
|
|
39
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
40
|
-
"
|
|
41
|
-
"dotenv": "^17.4.1",
|
|
42
|
-
"zod": "latest"
|
|
54
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
55
|
+
"zod": "^4.4.3"
|
|
43
56
|
},
|
|
44
57
|
"devDependencies": {
|
|
45
|
-
"@types/
|
|
46
|
-
"
|
|
47
|
-
"typescript": "
|
|
48
|
-
"vitest": "
|
|
58
|
+
"@types/node": "^24.10.1",
|
|
59
|
+
"tsx": "^4.23.4",
|
|
60
|
+
"typescript": "^6.0.2",
|
|
61
|
+
"vitest": "^4.1.10"
|
|
49
62
|
},
|
|
50
|
-
"files": [
|
|
51
|
-
"dist",
|
|
52
|
-
"README.md"
|
|
53
|
-
],
|
|
54
63
|
"engines": {
|
|
55
|
-
"node": ">=
|
|
64
|
+
"node": ">=20"
|
|
56
65
|
}
|
|
57
66
|
}
|
package/dist/agent-registry.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
interface AgentRow {
|
|
2
|
-
id: number;
|
|
3
|
-
name: string;
|
|
4
|
-
project_path: string;
|
|
5
|
-
pid: number;
|
|
6
|
-
tmux_pane: string | null;
|
|
7
|
-
status: string;
|
|
8
|
-
connected_at: string;
|
|
9
|
-
disconnected_at: string | null;
|
|
10
|
-
}
|
|
11
|
-
/** Register an agent. Returns the assigned name.
|
|
12
|
-
* Uses project_path as the stable identifier — same directory reuses
|
|
13
|
-
* disconnected entries, preserving message history across sessions.
|
|
14
|
-
* Multiple concurrent agents in the same project get suffixed names.
|
|
15
|
-
*/
|
|
16
|
-
export declare function registerAgent(options?: {
|
|
17
|
-
customName?: string;
|
|
18
|
-
}): string;
|
|
19
|
-
/** Mark an agent as disconnected */
|
|
20
|
-
export declare function unregisterAgent(name: string): void;
|
|
21
|
-
/** Check all registered agents and mark dead ones as disconnected */
|
|
22
|
-
export declare function checkAgentLiveness(): void;
|
|
23
|
-
/** Get a registered agent by name */
|
|
24
|
-
export declare function getAgent(name: string): AgentRow | undefined;
|
|
25
|
-
/** List all agents, optionally filtered by status */
|
|
26
|
-
export declare function listAgents(status?: string): AgentRow[];
|
|
27
|
-
export {};
|
package/dist/agent-registry.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import { getDb } from './db.js';
|
|
3
|
-
import { broadcast } from './sse.js';
|
|
4
|
-
import { logActivity } from './helpers.js';
|
|
5
|
-
/** Check if a process is still alive */
|
|
6
|
-
function isAlive(pid) {
|
|
7
|
-
try {
|
|
8
|
-
process.kill(pid, 0);
|
|
9
|
-
return true;
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
/** Detect the tmux pane for a given PID */
|
|
16
|
-
function detectTmuxPane(pid) {
|
|
17
|
-
try {
|
|
18
|
-
const ptsPath = execSync(`readlink /proc/${pid}/fd/0`).toString().trim();
|
|
19
|
-
const panes = execSync('tmux list-panes -a -F "#{pane_id} #{pane_tty}"').toString().trim().split('\n');
|
|
20
|
-
for (const line of panes) {
|
|
21
|
-
const [paneId, paneTty] = line.split(' ');
|
|
22
|
-
if (paneTty === ptsPath)
|
|
23
|
-
return paneId;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
catch { /* tmux not available */ }
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
/** Generate a unique agent name from the project folder, auto-suffixing on collision.
|
|
30
|
-
* A name is available if no row exists or the existing row's process is dead. */
|
|
31
|
-
function generateName(folderName) {
|
|
32
|
-
const db = getDb();
|
|
33
|
-
const isAvailable = (name) => {
|
|
34
|
-
const row = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(name);
|
|
35
|
-
if (!row)
|
|
36
|
-
return true;
|
|
37
|
-
return !isAlive(row.pid);
|
|
38
|
-
};
|
|
39
|
-
if (isAvailable(folderName))
|
|
40
|
-
return folderName;
|
|
41
|
-
for (let i = 2; i < 100; i++) {
|
|
42
|
-
const candidate = `${folderName}:${i}`;
|
|
43
|
-
if (isAvailable(candidate))
|
|
44
|
-
return candidate;
|
|
45
|
-
}
|
|
46
|
-
return `${folderName}:${Date.now()}`;
|
|
47
|
-
}
|
|
48
|
-
/** Register an agent. Returns the assigned name.
|
|
49
|
-
* Uses project_path as the stable identifier — same directory reuses
|
|
50
|
-
* disconnected entries, preserving message history across sessions.
|
|
51
|
-
* Multiple concurrent agents in the same project get suffixed names.
|
|
52
|
-
*/
|
|
53
|
-
export function registerAgent(options) {
|
|
54
|
-
const db = getDb();
|
|
55
|
-
const agentPid = process.ppid;
|
|
56
|
-
const projectPath = process.cwd();
|
|
57
|
-
const folderName = projectPath.split('/').pop() || 'unknown';
|
|
58
|
-
const tmuxPane = detectTmuxPane(agentPid);
|
|
59
|
-
const ts = new Date().toISOString();
|
|
60
|
-
// Clean up dead agents first so we can reuse their entries
|
|
61
|
-
checkAgentLiveness();
|
|
62
|
-
// Find all entries for this project path
|
|
63
|
-
const entries = db.prepare('SELECT * FROM agent_registry WHERE project_path = ? ORDER BY connected_at DESC').all(projectPath);
|
|
64
|
-
// Priority 0: Same PID already connected — this agent is renaming itself
|
|
65
|
-
// (e.g. auto-registered as folder name, now calling register_agent with a custom name)
|
|
66
|
-
const samePid = entries.find(e => e.status === 'connected' && e.pid === agentPid);
|
|
67
|
-
if (samePid && options?.customName && options.customName !== samePid.name) {
|
|
68
|
-
const oldName = samePid.name;
|
|
69
|
-
const newName = options.customName;
|
|
70
|
-
// If the target name already exists as another row (disconnected from a previous session),
|
|
71
|
-
// delete it to free the UNIQUE name. Messages stay — they're linked by name string, not FK.
|
|
72
|
-
const existing = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(newName);
|
|
73
|
-
if (existing && existing.id !== samePid.id) {
|
|
74
|
-
db.prepare('DELETE FROM agent_registry WHERE id = ?').run(existing.id);
|
|
75
|
-
}
|
|
76
|
-
db.prepare('UPDATE agent_registry SET name = ?, tmux_pane = ?, connected_at = ? WHERE id = ?').run(newName, tmuxPane, ts, samePid.id);
|
|
77
|
-
const row = db.prepare('SELECT * FROM agent_registry WHERE id = ?').get(samePid.id);
|
|
78
|
-
broadcast('agent_renamed', { entity: 'agent', action: 'agent_renamed', payload: { ...row, oldName } });
|
|
79
|
-
logActivity('agent_renamed', `Agent "${oldName}" renamed to "${newName}"`, { entityType: 'agent' });
|
|
80
|
-
return newName;
|
|
81
|
-
}
|
|
82
|
-
// If same PID is already connected with the right name (or no custom name), just return it
|
|
83
|
-
if (samePid) {
|
|
84
|
-
return samePid.name;
|
|
85
|
-
}
|
|
86
|
-
// Reconnection priority for disconnected entries:
|
|
87
|
-
// 1. Custom name matches a disconnected entry exactly — the name IS the stable identity.
|
|
88
|
-
// This is how users resume a specific agent: register_agent({ name: "sentinmail_coder" })
|
|
89
|
-
// 2. Same PID — exact session resume (e.g. MCP server restarted, same shell)
|
|
90
|
-
// 3. Only ONE disconnected entry — unambiguous, safe to reuse
|
|
91
|
-
// When multiple disconnected entries exist and no match is found,
|
|
92
|
-
// we create a fresh entry rather than guessing wrong and mixing up histories.
|
|
93
|
-
const allDisconnected = entries.filter(e => e.status === 'disconnected');
|
|
94
|
-
const disconnected = (options?.customName ? allDisconnected.find(e => e.name === options.customName) : undefined)
|
|
95
|
-
|| allDisconnected.find(e => e.pid === agentPid)
|
|
96
|
-
|| (allDisconnected.length === 1 ? allDisconnected[0] : undefined);
|
|
97
|
-
if (disconnected) {
|
|
98
|
-
const newName = options?.customName || disconnected.name;
|
|
99
|
-
db.prepare('UPDATE agent_registry SET name = ?, pid = ?, tmux_pane = ?, status = ?, connected_at = ?, disconnected_at = NULL WHERE id = ?').run(newName, agentPid, tmuxPane, 'connected', ts, disconnected.id);
|
|
100
|
-
const row = db.prepare('SELECT * FROM agent_registry WHERE id = ?').get(disconnected.id);
|
|
101
|
-
broadcast('agent_connected', { entity: 'agent', action: 'agent_connected', payload: row });
|
|
102
|
-
logActivity('agent_connected', `Agent "${newName}" reconnected`, { entityType: 'agent' });
|
|
103
|
-
return newName;
|
|
104
|
-
}
|
|
105
|
-
// Priority 3: No reusable disconnected entry — this is a new or concurrent agent
|
|
106
|
-
// Generate a suffixed name to avoid collision with live agents
|
|
107
|
-
const baseName = options?.customName || folderName;
|
|
108
|
-
const name = entries.length === 0 ? baseName : generateName(baseName);
|
|
109
|
-
// Upsert: if a dead entry with this name exists (from another project path),
|
|
110
|
-
// reuse it in-place to preserve its message history. Otherwise insert fresh.
|
|
111
|
-
const stale = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(name);
|
|
112
|
-
if (stale) {
|
|
113
|
-
db.prepare('UPDATE agent_registry SET project_path = ?, pid = ?, tmux_pane = ?, status = ?, connected_at = ?, disconnected_at = NULL WHERE id = ?').run(projectPath, agentPid, tmuxPane, 'connected', ts, stale.id);
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
db.prepare('INSERT INTO agent_registry (name, project_path, pid, tmux_pane, status, connected_at) VALUES (?, ?, ?, ?, ?, ?)').run(name, projectPath, agentPid, tmuxPane, 'connected', ts);
|
|
117
|
-
}
|
|
118
|
-
const row = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(name);
|
|
119
|
-
broadcast('agent_connected', { entity: 'agent', action: 'agent_connected', payload: row });
|
|
120
|
-
logActivity('agent_connected', `Agent "${name}" connected`, { entityType: 'agent' });
|
|
121
|
-
return name;
|
|
122
|
-
}
|
|
123
|
-
/** Mark an agent as disconnected */
|
|
124
|
-
export function unregisterAgent(name) {
|
|
125
|
-
const db = getDb();
|
|
126
|
-
const ts = new Date().toISOString();
|
|
127
|
-
db.prepare("UPDATE agent_registry SET status = 'disconnected', disconnected_at = ? WHERE name = ?").run(ts, name);
|
|
128
|
-
const row = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(name);
|
|
129
|
-
broadcast('agent_disconnected', { entity: 'agent', action: 'agent_disconnected', payload: row });
|
|
130
|
-
logActivity('agent_disconnected', `Agent "${name}" disconnected`, { entityType: 'agent' });
|
|
131
|
-
}
|
|
132
|
-
/** Check all registered agents and mark dead ones as disconnected */
|
|
133
|
-
export function checkAgentLiveness() {
|
|
134
|
-
const db = getDb();
|
|
135
|
-
const liveAgents = db.prepare("SELECT * FROM agent_registry WHERE status = 'connected'").all();
|
|
136
|
-
for (const agent of liveAgents) {
|
|
137
|
-
if (!isAlive(agent.pid)) {
|
|
138
|
-
unregisterAgent(agent.name);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
/** Get a registered agent by name */
|
|
143
|
-
export function getAgent(name) {
|
|
144
|
-
return getDb().prepare('SELECT * FROM agent_registry WHERE name = ?').get(name);
|
|
145
|
-
}
|
|
146
|
-
/** List all agents, optionally filtered by status */
|
|
147
|
-
export function listAgents(status) {
|
|
148
|
-
const db = getDb();
|
|
149
|
-
if (status) {
|
|
150
|
-
return db.prepare('SELECT * FROM agent_registry WHERE status = ? ORDER BY connected_at DESC').all(status);
|
|
151
|
-
}
|
|
152
|
-
return db.prepare('SELECT * FROM agent_registry ORDER BY connected_at DESC').all();
|
|
153
|
-
}
|
package/dist/db.d.ts
DELETED
package/dist/db.js
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import Database from 'better-sqlite3';
|
|
2
|
-
import { mkdirSync } from 'fs';
|
|
3
|
-
import { dirname, resolve } from 'path';
|
|
4
|
-
import { homedir } from 'os';
|
|
5
|
-
import { getConfig } from './config.js';
|
|
6
|
-
let db = null;
|
|
7
|
-
// Expands ~/path to $HOME/path. Only handles ~/ prefix, not ~user/ paths.
|
|
8
|
-
export function resolvePath(p) {
|
|
9
|
-
if (p.startsWith('~/') || p === '~') {
|
|
10
|
-
return resolve(homedir(), p.slice(2));
|
|
11
|
-
}
|
|
12
|
-
return resolve(p);
|
|
13
|
-
}
|
|
14
|
-
export function getDb() {
|
|
15
|
-
if (db)
|
|
16
|
-
return db;
|
|
17
|
-
return initDb(getConfig().databasePath);
|
|
18
|
-
}
|
|
19
|
-
// For testing: initialize with a specific path (use ':memory:' for tests)
|
|
20
|
-
export function initDb(path) {
|
|
21
|
-
if (db) {
|
|
22
|
-
db.close();
|
|
23
|
-
db = null;
|
|
24
|
-
}
|
|
25
|
-
const dbPath = path === ':memory:' ? ':memory:' : resolvePath(path);
|
|
26
|
-
if (path !== ':memory:')
|
|
27
|
-
mkdirSync(dirname(dbPath), { recursive: true });
|
|
28
|
-
db = new Database(dbPath);
|
|
29
|
-
db.pragma('journal_mode = WAL');
|
|
30
|
-
db.pragma('foreign_keys = ON');
|
|
31
|
-
db.pragma('busy_timeout = 5000');
|
|
32
|
-
initSchema(db);
|
|
33
|
-
return db;
|
|
34
|
-
}
|
|
35
|
-
function initSchema(db) {
|
|
36
|
-
db.exec(`
|
|
37
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
38
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
-
name TEXT NOT NULL,
|
|
40
|
-
color TEXT NOT NULL DEFAULT '#de8eff',
|
|
41
|
-
type TEXT NOT NULL DEFAULT 'active_project',
|
|
42
|
-
description TEXT,
|
|
43
|
-
created_at TEXT NOT NULL,
|
|
44
|
-
updated_at TEXT NOT NULL
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
CREATE TABLE IF NOT EXISTS tasks (
|
|
48
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
49
|
-
title TEXT NOT NULL,
|
|
50
|
-
description TEXT,
|
|
51
|
-
status TEXT NOT NULL DEFAULT 'not_started',
|
|
52
|
-
priority TEXT NOT NULL DEFAULT 'medium',
|
|
53
|
-
project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL,
|
|
54
|
-
dependencies TEXT NOT NULL DEFAULT '[]',
|
|
55
|
-
links TEXT NOT NULL DEFAULT '[]',
|
|
56
|
-
tags TEXT NOT NULL DEFAULT '[]',
|
|
57
|
-
due_date TEXT,
|
|
58
|
-
estimated_time INTEGER,
|
|
59
|
-
created_at TEXT NOT NULL,
|
|
60
|
-
updated_at TEXT NOT NULL
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
64
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
-
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
66
|
-
start TEXT NOT NULL,
|
|
67
|
-
end TEXT
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
CREATE TABLE IF NOT EXISTS activity_logs (
|
|
71
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
72
|
-
action TEXT NOT NULL,
|
|
73
|
-
title TEXT NOT NULL,
|
|
74
|
-
detail TEXT,
|
|
75
|
-
entity_type TEXT,
|
|
76
|
-
entity_id INTEGER,
|
|
77
|
-
created_at TEXT NOT NULL
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
CREATE TABLE IF NOT EXISTS notifications (
|
|
81
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
82
|
-
title TEXT NOT NULL,
|
|
83
|
-
message TEXT NOT NULL,
|
|
84
|
-
type TEXT NOT NULL DEFAULT 'info',
|
|
85
|
-
read INTEGER NOT NULL DEFAULT 0,
|
|
86
|
-
created_at TEXT NOT NULL
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
CREATE TABLE IF NOT EXISTS settings (
|
|
90
|
-
key TEXT PRIMARY KEY,
|
|
91
|
-
value TEXT NOT NULL
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
CREATE TABLE IF NOT EXISTS agent_messages (
|
|
95
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
96
|
-
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
|
|
97
|
-
question TEXT NOT NULL,
|
|
98
|
-
context TEXT,
|
|
99
|
-
choices TEXT,
|
|
100
|
-
response TEXT,
|
|
101
|
-
agent_pid INTEGER,
|
|
102
|
-
delivered INTEGER,
|
|
103
|
-
sender_name TEXT NOT NULL DEFAULT 'unknown',
|
|
104
|
-
recipient_name TEXT NOT NULL DEFAULT 'user',
|
|
105
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
106
|
-
source TEXT NOT NULL DEFAULT 'mcp',
|
|
107
|
-
created_at TEXT NOT NULL,
|
|
108
|
-
broadcast_id TEXT,
|
|
109
|
-
answered_at TEXT
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
CREATE TABLE IF NOT EXISTS agent_registry (
|
|
113
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
114
|
-
name TEXT NOT NULL UNIQUE,
|
|
115
|
-
project_path TEXT NOT NULL,
|
|
116
|
-
pid INTEGER NOT NULL,
|
|
117
|
-
tmux_pane TEXT,
|
|
118
|
-
status TEXT NOT NULL DEFAULT 'connected',
|
|
119
|
-
connected_at TEXT NOT NULL,
|
|
120
|
-
disconnected_at TEXT
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
124
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id);
|
|
125
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
|
|
126
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_task_id ON sessions(task_id);
|
|
127
|
-
CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at);
|
|
128
|
-
CREATE INDEX IF NOT EXISTS idx_activity_logs_action ON activity_logs(action);
|
|
129
|
-
CREATE INDEX IF NOT EXISTS idx_notifications_read ON notifications(read);
|
|
130
|
-
CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at);
|
|
131
|
-
CREATE INDEX IF NOT EXISTS idx_agent_messages_status ON agent_messages(status);
|
|
132
|
-
CREATE INDEX IF NOT EXISTS idx_agent_messages_project_id ON agent_messages(project_id);
|
|
133
|
-
CREATE INDEX IF NOT EXISTS idx_agent_registry_status ON agent_registry(status);
|
|
134
|
-
CREATE INDEX IF NOT EXISTS idx_agent_registry_name ON agent_registry(name);
|
|
135
|
-
CREATE INDEX IF NOT EXISTS idx_agent_registry_project_path ON agent_registry(project_path);
|
|
136
|
-
|
|
137
|
-
CREATE TABLE IF NOT EXISTS tool_executions (
|
|
138
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
139
|
-
tool_name TEXT NOT NULL,
|
|
140
|
-
duration_ms INTEGER NOT NULL,
|
|
141
|
-
success INTEGER NOT NULL DEFAULT 1,
|
|
142
|
-
error_message TEXT,
|
|
143
|
-
created_at TEXT NOT NULL
|
|
144
|
-
);
|
|
145
|
-
CREATE INDEX IF NOT EXISTS idx_tool_executions_tool_name ON tool_executions(tool_name);
|
|
146
|
-
CREATE INDEX IF NOT EXISTS idx_tool_executions_created_at ON tool_executions(created_at);
|
|
147
|
-
`);
|
|
148
|
-
// Migrations — add columns that may be missing on existing databases
|
|
149
|
-
const cols = db.prepare("PRAGMA table_info(agent_messages)").all();
|
|
150
|
-
const colNames = new Set(cols.map(c => c.name));
|
|
151
|
-
if (!colNames.has('agent_pid')) {
|
|
152
|
-
db.exec('ALTER TABLE agent_messages ADD COLUMN agent_pid INTEGER');
|
|
153
|
-
}
|
|
154
|
-
if (!colNames.has('delivered')) {
|
|
155
|
-
db.exec('ALTER TABLE agent_messages ADD COLUMN delivered INTEGER');
|
|
156
|
-
}
|
|
157
|
-
if (!colNames.has('sender_name')) {
|
|
158
|
-
db.exec("ALTER TABLE agent_messages ADD COLUMN sender_name TEXT NOT NULL DEFAULT 'unknown'");
|
|
159
|
-
}
|
|
160
|
-
if (!colNames.has('recipient_name')) {
|
|
161
|
-
db.exec("ALTER TABLE agent_messages ADD COLUMN recipient_name TEXT NOT NULL DEFAULT 'user'");
|
|
162
|
-
}
|
|
163
|
-
if (!colNames.has('source')) {
|
|
164
|
-
db.exec("ALTER TABLE agent_messages ADD COLUMN source TEXT NOT NULL DEFAULT 'mcp'");
|
|
165
|
-
}
|
|
166
|
-
if (!colNames.has('broadcast_id')) {
|
|
167
|
-
db.exec('ALTER TABLE agent_messages ADD COLUMN broadcast_id TEXT');
|
|
168
|
-
}
|
|
169
|
-
// Always ensure the index exists (safe for both fresh and migrated databases)
|
|
170
|
-
db.exec('CREATE INDEX IF NOT EXISTS idx_agent_messages_broadcast_id ON agent_messages(broadcast_id)');
|
|
171
|
-
// Migration: make agent_messages.project_id nullable if it was NOT NULL
|
|
172
|
-
try {
|
|
173
|
-
const tableInfo = db.prepare("SELECT sql FROM sqlite_master WHERE name = 'agent_messages'").get();
|
|
174
|
-
if (tableInfo?.sql?.includes('project_id INTEGER NOT NULL')) {
|
|
175
|
-
db.exec(`
|
|
176
|
-
CREATE TABLE agent_messages_new (
|
|
177
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
178
|
-
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
|
|
179
|
-
question TEXT NOT NULL,
|
|
180
|
-
context TEXT,
|
|
181
|
-
choices TEXT,
|
|
182
|
-
response TEXT,
|
|
183
|
-
agent_pid INTEGER,
|
|
184
|
-
delivered INTEGER,
|
|
185
|
-
sender_name TEXT NOT NULL DEFAULT 'unknown',
|
|
186
|
-
recipient_name TEXT NOT NULL DEFAULT 'user',
|
|
187
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
188
|
-
source TEXT NOT NULL DEFAULT 'mcp',
|
|
189
|
-
created_at TEXT NOT NULL,
|
|
190
|
-
answered_at TEXT
|
|
191
|
-
);
|
|
192
|
-
INSERT INTO agent_messages_new SELECT
|
|
193
|
-
id, project_id, question, context, choices, response, agent_pid, delivered,
|
|
194
|
-
COALESCE(sender_name, 'unknown'), COALESCE(recipient_name, 'user'),
|
|
195
|
-
status, COALESCE(source, 'mcp'), created_at, answered_at
|
|
196
|
-
FROM agent_messages;
|
|
197
|
-
DROP TABLE agent_messages;
|
|
198
|
-
ALTER TABLE agent_messages_new RENAME TO agent_messages;
|
|
199
|
-
CREATE INDEX IF NOT EXISTS idx_agent_messages_status ON agent_messages(status);
|
|
200
|
-
CREATE INDEX IF NOT EXISTS idx_agent_messages_project_id ON agent_messages(project_id);
|
|
201
|
-
`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
catch { /* table may not exist yet */ }
|
|
205
|
-
// Migration: add allowed_tools and denied_tools to tasks
|
|
206
|
-
const taskCols = db.prepare("PRAGMA table_info(tasks)").all();
|
|
207
|
-
const taskColNames = new Set(taskCols.map(c => c.name));
|
|
208
|
-
if (!taskColNames.has('allowed_tools')) {
|
|
209
|
-
db.exec("ALTER TABLE tasks ADD COLUMN allowed_tools TEXT");
|
|
210
|
-
}
|
|
211
|
-
if (!taskColNames.has('denied_tools')) {
|
|
212
|
-
db.exec("ALTER TABLE tasks ADD COLUMN denied_tools TEXT");
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
export function closeDb() {
|
|
216
|
-
if (db) {
|
|
217
|
-
db.close();
|
|
218
|
-
db = null;
|
|
219
|
-
}
|
|
220
|
-
}
|
package/dist/helpers.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { ActivityAction, ErrorCode } from './types.js';
|
|
2
|
-
export declare function logActivity(action: ActivityAction, title: string, options?: {
|
|
3
|
-
detail?: string;
|
|
4
|
-
entityType?: string;
|
|
5
|
-
entityId?: number;
|
|
6
|
-
}): void;
|
|
7
|
-
export declare function errorResponse(error: string, code: ErrorCode): {
|
|
8
|
-
isError: true;
|
|
9
|
-
content: {
|
|
10
|
-
type: "text";
|
|
11
|
-
text: string;
|
|
12
|
-
}[];
|
|
13
|
-
};
|
|
14
|
-
export declare function successResponse(data: unknown): {
|
|
15
|
-
content: {
|
|
16
|
-
type: "text";
|
|
17
|
-
text: string;
|
|
18
|
-
}[];
|
|
19
|
-
};
|
|
20
|
-
export declare function now(): string;
|
|
21
|
-
export declare function broadcastChange(entity: string, action: string, payload: unknown): void;
|
package/dist/helpers.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { getDb } from './db.js';
|
|
2
|
-
import { broadcast } from './sse.js';
|
|
3
|
-
export function logActivity(action, title, options) {
|
|
4
|
-
const db = getDb();
|
|
5
|
-
const ts = new Date().toISOString();
|
|
6
|
-
const result = db.prepare(`INSERT INTO activity_logs (action, title, detail, entity_type, entity_id, created_at)
|
|
7
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(action, title, options?.detail ?? null, options?.entityType ?? null, options?.entityId ?? null, ts);
|
|
8
|
-
const entry = db.prepare('SELECT * FROM activity_logs WHERE id = ?').get(result.lastInsertRowid);
|
|
9
|
-
broadcast('activity_logged', { entity: 'activity', action: 'activity_logged', payload: entry });
|
|
10
|
-
}
|
|
11
|
-
export function errorResponse(error, code) {
|
|
12
|
-
return {
|
|
13
|
-
isError: true,
|
|
14
|
-
content: [{ type: 'text', text: JSON.stringify({ error, code }) }],
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
export function successResponse(data) {
|
|
18
|
-
return {
|
|
19
|
-
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
export function now() {
|
|
23
|
-
return new Date().toISOString();
|
|
24
|
-
}
|
|
25
|
-
export function broadcastChange(entity, action, payload) {
|
|
26
|
-
broadcast(action, { entity, action, payload });
|
|
27
|
-
}
|
package/dist/resources.d.ts
DELETED