@dalmasonto/taskflow-mcp 1.0.35 → 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.
Files changed (101) hide show
  1. package/README.md +138 -193
  2. package/dist/attachment-download.d.ts +74 -0
  3. package/dist/attachment-download.js +193 -0
  4. package/dist/attachment-download.js.map +1 -0
  5. package/dist/attachments.d.ts +23 -0
  6. package/dist/attachments.js +66 -0
  7. package/dist/attachments.js.map +1 -0
  8. package/dist/client.d.ts +206 -0
  9. package/dist/client.js +279 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/config.d.ts +137 -18
  12. package/dist/config.js +187 -106
  13. package/dist/config.js.map +1 -0
  14. package/dist/connect.d.ts +89 -0
  15. package/dist/connect.js +269 -0
  16. package/dist/connect.js.map +1 -0
  17. package/dist/doctor.d.ts +24 -0
  18. package/dist/doctor.js +120 -0
  19. package/dist/doctor.js.map +1 -0
  20. package/dist/events.d.ts +186 -0
  21. package/dist/events.js +407 -0
  22. package/dist/events.js.map +1 -0
  23. package/dist/index.d.ts +7 -1
  24. package/dist/index.js +121 -218
  25. package/dist/index.js.map +1 -0
  26. package/dist/instructions.d.ts +12 -0
  27. package/dist/instructions.js +114 -0
  28. package/dist/instructions.js.map +1 -0
  29. package/dist/mint.d.ts +62 -0
  30. package/dist/mint.js +135 -0
  31. package/dist/mint.js.map +1 -0
  32. package/dist/mirror.d.ts +68 -0
  33. package/dist/mirror.js +103 -0
  34. package/dist/mirror.js.map +1 -0
  35. package/dist/pane-queue.d.ts +29 -0
  36. package/dist/pane-queue.js +35 -0
  37. package/dist/pane-queue.js.map +1 -0
  38. package/dist/prompts.d.ts +79 -0
  39. package/dist/prompts.js +211 -0
  40. package/dist/prompts.js.map +1 -0
  41. package/dist/resolve.d.ts +72 -0
  42. package/dist/resolve.js +89 -0
  43. package/dist/resolve.js.map +1 -0
  44. package/dist/runtime.d.ts +54 -0
  45. package/dist/runtime.js +321 -0
  46. package/dist/runtime.js.map +1 -0
  47. package/dist/server.d.ts +56 -0
  48. package/dist/server.js +793 -0
  49. package/dist/server.js.map +1 -0
  50. package/dist/session-identifier.d.ts +48 -0
  51. package/dist/session-identifier.js +44 -0
  52. package/dist/session-identifier.js.map +1 -0
  53. package/dist/sessions-store.d.ts +38 -0
  54. package/dist/sessions-store.js +88 -0
  55. package/dist/sessions-store.js.map +1 -0
  56. package/dist/tmux.d.ts +200 -0
  57. package/dist/tmux.js +580 -0
  58. package/dist/tmux.js.map +1 -0
  59. package/hooks/metadata.mjs +99 -0
  60. package/hooks/permission-prompt.mjs +100 -0
  61. package/hooks/taskflow-hook.mjs +499 -0
  62. package/hooks/tool-logging.mjs +63 -0
  63. package/package.json +38 -29
  64. package/dist/agent-registry.d.ts +0 -28
  65. package/dist/agent-registry.js +0 -158
  66. package/dist/db.d.ts +0 -5
  67. package/dist/db.js +0 -220
  68. package/dist/helpers.d.ts +0 -21
  69. package/dist/helpers.js +0 -27
  70. package/dist/resources.d.ts +0 -2
  71. package/dist/resources.js +0 -89
  72. package/dist/retry.d.ts +0 -34
  73. package/dist/retry.js +0 -94
  74. package/dist/sse.d.ts +0 -10
  75. package/dist/sse.js +0 -824
  76. package/dist/tmux-bridge.d.ts +0 -13
  77. package/dist/tmux-bridge.js +0 -217
  78. package/dist/tools/activity.d.ts +0 -39
  79. package/dist/tools/activity.js +0 -152
  80. package/dist/tools/agent-inbox.d.ts +0 -12
  81. package/dist/tools/agent-inbox.js +0 -272
  82. package/dist/tools/agent.d.ts +0 -14
  83. package/dist/tools/agent.js +0 -168
  84. package/dist/tools/analytics.d.ts +0 -21
  85. package/dist/tools/analytics.js +0 -191
  86. package/dist/tools/checkpoint.d.ts +0 -27
  87. package/dist/tools/checkpoint.js +0 -105
  88. package/dist/tools/notifications.d.ts +0 -31
  89. package/dist/tools/notifications.js +0 -59
  90. package/dist/tools/projects.d.ts +0 -55
  91. package/dist/tools/projects.js +0 -112
  92. package/dist/tools/settings.d.ts +0 -19
  93. package/dist/tools/settings.js +0 -73
  94. package/dist/tools/tasks.d.ts +0 -105
  95. package/dist/tools/tasks.js +0 -403
  96. package/dist/tools/terminal.d.ts +0 -4
  97. package/dist/tools/terminal.js +0 -98
  98. package/dist/tools/timer.d.ts +0 -37
  99. package/dist/tools/timer.js +0 -154
  100. package/dist/types.d.ts +0 -83
  101. 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": "1.0.35",
4
- "description": "MCP server for TaskFlowmanage projects, tasks, timers, analytics via AI agents",
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
- "time-tracking",
20
- "sqlite"
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-server"
46
+ "url": "git+https://github.com/dalmasonto/task_flow.git",
47
+ "directory": "mcp"
28
48
  },
29
- "homepage": "https://github.com/dalmasonto/task_flow#mcp-server-ai-agent-tools",
30
- "scripts": {
31
- "build": "tsc",
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.29.0",
40
- "better-sqlite3": "latest",
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/better-sqlite3": "latest",
46
- "@types/node": "latest",
47
- "typescript": "latest",
48
- "vitest": "latest"
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": ">=18"
64
+ "node": ">=20"
56
65
  }
57
66
  }
@@ -1,28 +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
- * Also purges entries that have been disconnected for more than 24 hours. */
23
- export declare function checkAgentLiveness(): void;
24
- /** Get a registered agent by name */
25
- export declare function getAgent(name: string): AgentRow | undefined;
26
- /** List all agents, optionally filtered by status */
27
- export declare function listAgents(status?: string): AgentRow[];
28
- export {};
@@ -1,158 +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
- * Also purges entries that have been disconnected for more than 24 hours. */
134
- export function checkAgentLiveness() {
135
- const db = getDb();
136
- // Mark dead connected agents as disconnected
137
- const liveAgents = db.prepare("SELECT * FROM agent_registry WHERE status = 'connected'").all();
138
- for (const agent of liveAgents) {
139
- if (!isAlive(agent.pid)) {
140
- unregisterAgent(agent.name);
141
- }
142
- }
143
- // Purge entries that have been disconnected for more than 24 hours
144
- const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
145
- db.prepare("DELETE FROM agent_registry WHERE status = 'disconnected' AND disconnected_at IS NOT NULL AND disconnected_at < ?").run(cutoff);
146
- }
147
- /** Get a registered agent by name */
148
- export function getAgent(name) {
149
- return getDb().prepare('SELECT * FROM agent_registry WHERE name = ?').get(name);
150
- }
151
- /** List all agents, optionally filtered by status */
152
- export function listAgents(status) {
153
- const db = getDb();
154
- if (status) {
155
- return db.prepare('SELECT * FROM agent_registry WHERE status = ? ORDER BY connected_at DESC').all(status);
156
- }
157
- return db.prepare('SELECT * FROM agent_registry ORDER BY connected_at DESC').all();
158
- }
package/dist/db.d.ts DELETED
@@ -1,5 +0,0 @@
1
- import Database from 'better-sqlite3';
2
- export declare function resolvePath(p: string): string;
3
- export declare function getDb(): Database.Database;
4
- export declare function initDb(path: string): Database.Database;
5
- export declare function closeDb(): void;
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
- }
@@ -1,2 +0,0 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare function registerResources(server: McpServer): void;