@dalmasonto/taskflow-mcp 1.0.5 → 1.0.7

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 CHANGED
@@ -40,7 +40,10 @@ By default, Claude Code will prompt you to approve each MCP tool call. To allow
40
40
  "mcp__taskflow__*"
41
41
  ]
42
42
  },
43
- "enableAllProjectMcpServers": true
43
+ "enableAllProjectMcpServers": true,
44
+ "enabledMcpjsonServers": [
45
+ "taskflow"
46
+ ]
44
47
  }
45
48
  ```
46
49
 
@@ -112,6 +115,34 @@ The `get_agent_instructions` tool tells agents to:
112
115
  5. **Stay in sync** — create tasks for new work items to keep the tracker up to date
113
116
  6. **Read descriptions** — task descriptions contain implementation details and acceptance criteria
114
117
 
118
+ ## Agent Inbox — Remote Communication
119
+
120
+ The Agent Inbox lets agents ask questions that appear in the TaskFlow UI. Users can respond from any device (phone, browser, another machine), and the response is delivered back to the agent's terminal automatically.
121
+
122
+ ### How it works
123
+
124
+ 1. Agent calls `ask_user` with a question, context, and optional quick-tap choices
125
+ 2. Question appears instantly in the TaskFlow UI at `/inbox`
126
+ 3. User responds from the UI — response is injected into the agent's terminal via tmux
127
+ 4. Agent also asks in the terminal normally, so the user can answer from either place
128
+
129
+ ### Setup for auto-injection
130
+
131
+ For responses to be injected directly into the terminal, run Claude Code inside tmux:
132
+
133
+ ```bash
134
+ # Install tmux (one-time)
135
+ sudo apt-get install -y tmux
136
+
137
+ # Start a tmux session and run claude inside it
138
+ tmux new -s agent
139
+ claude
140
+ ```
141
+
142
+ Without tmux, the inbox still works — agents can use `check_response` to poll for answers, or the user can dismiss questions answered in the terminal.
143
+
144
+ See [Terminal Injection Setup](../docs/agent-inbox-terminal-injection.md) for full details, multiple agent setup, and cleanup instructions.
145
+
115
146
  ## Available Tools
116
147
 
117
148
  ### Agent
@@ -168,6 +199,12 @@ The `get_agent_instructions` tool tells agents to:
168
199
  | `mark_all_notifications_read` | Mark all as read |
169
200
  | `clear_notifications` | Delete all notifications |
170
201
 
202
+ ### Agent Inbox
203
+ | Tool | Description |
204
+ |------|-------------|
205
+ | `ask_user` | Post a question to the Agent Inbox for remote response. Returns immediately with message ID |
206
+ | `check_response` | Check if the user has responded to a previously posted question |
207
+
171
208
  ### Settings
172
209
  | Tool | Description |
173
210
  |------|-------------|
@@ -0,0 +1,23 @@
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
+ export declare function registerAgent(options?: {
13
+ customName?: string;
14
+ }): string;
15
+ /** Mark an agent as disconnected */
16
+ export declare function unregisterAgent(name: string): void;
17
+ /** Check all registered agents and mark dead ones as disconnected */
18
+ export declare function checkAgentLiveness(): void;
19
+ /** Get a registered agent by name */
20
+ export declare function getAgent(name: string): AgentRow | undefined;
21
+ /** List all agents, optionally filtered by status */
22
+ export declare function listAgents(status?: string): AgentRow[];
23
+ export {};
@@ -0,0 +1,107 @@
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
+ function generateName(folderName) {
31
+ const db = getDb();
32
+ const existing = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(folderName);
33
+ if (!existing || !isAlive(existing.pid)) {
34
+ return folderName;
35
+ }
36
+ for (let i = 2; i < 100; i++) {
37
+ const candidate = `${folderName}:${i}`;
38
+ const row = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(candidate);
39
+ if (!row || !isAlive(row.pid))
40
+ return candidate;
41
+ }
42
+ return `${folderName}:${Date.now()}`;
43
+ }
44
+ /** Register an agent. Returns the assigned name. */
45
+ export function registerAgent(options) {
46
+ const db = getDb();
47
+ const agentPid = process.ppid;
48
+ const projectPath = process.cwd();
49
+ const folderName = projectPath.split('/').pop() || 'unknown';
50
+ // Check if this PID already has a registration — reuse it
51
+ const existingByPid = db.prepare('SELECT * FROM agent_registry WHERE pid = ? AND status = ?').get(agentPid, 'connected');
52
+ if (existingByPid) {
53
+ // Update tmux pane in case it changed, but keep the same name
54
+ const tmuxPane = detectTmuxPane(agentPid);
55
+ if (tmuxPane !== existingByPid.tmux_pane) {
56
+ db.prepare('UPDATE agent_registry SET tmux_pane = ? WHERE id = ?').run(tmuxPane, existingByPid.id);
57
+ }
58
+ return existingByPid.name;
59
+ }
60
+ // Clean up dead agents first to free up names
61
+ checkAgentLiveness();
62
+ const name = options?.customName || generateName(folderName);
63
+ const tmuxPane = detectTmuxPane(agentPid);
64
+ const ts = new Date().toISOString();
65
+ const existing = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(name);
66
+ if (existing) {
67
+ db.prepare('UPDATE agent_registry SET project_path = ?, pid = ?, tmux_pane = ?, status = ?, connected_at = ?, disconnected_at = NULL WHERE name = ?').run(projectPath, agentPid, tmuxPane, 'connected', ts, name);
68
+ }
69
+ else {
70
+ db.prepare('INSERT INTO agent_registry (name, project_path, pid, tmux_pane, status, connected_at) VALUES (?, ?, ?, ?, ?, ?)').run(name, projectPath, agentPid, tmuxPane, 'connected', ts);
71
+ }
72
+ const row = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(name);
73
+ broadcast('agent_connected', { entity: 'agent', action: 'agent_connected', payload: row });
74
+ logActivity('agent_connected', `Agent "${name}" connected`, { entityType: 'agent' });
75
+ return name;
76
+ }
77
+ /** Mark an agent as disconnected */
78
+ export function unregisterAgent(name) {
79
+ const db = getDb();
80
+ const ts = new Date().toISOString();
81
+ db.prepare("UPDATE agent_registry SET status = 'disconnected', disconnected_at = ? WHERE name = ?").run(ts, name);
82
+ const row = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(name);
83
+ broadcast('agent_disconnected', { entity: 'agent', action: 'agent_disconnected', payload: row });
84
+ logActivity('agent_disconnected', `Agent "${name}" disconnected`, { entityType: 'agent' });
85
+ }
86
+ /** Check all registered agents and mark dead ones as disconnected */
87
+ export function checkAgentLiveness() {
88
+ const db = getDb();
89
+ const liveAgents = db.prepare("SELECT * FROM agent_registry WHERE status = 'connected'").all();
90
+ for (const agent of liveAgents) {
91
+ if (!isAlive(agent.pid)) {
92
+ unregisterAgent(agent.name);
93
+ }
94
+ }
95
+ }
96
+ /** Get a registered agent by name */
97
+ export function getAgent(name) {
98
+ return getDb().prepare('SELECT * FROM agent_registry WHERE name = ?').get(name);
99
+ }
100
+ /** List all agents, optionally filtered by status */
101
+ export function listAgents(status) {
102
+ const db = getDb();
103
+ if (status) {
104
+ return db.prepare('SELECT * FROM agent_registry WHERE status = ? ORDER BY connected_at DESC').all(status);
105
+ }
106
+ return db.prepare('SELECT * FROM agent_registry ORDER BY connected_at DESC').all();
107
+ }
package/dist/db.js CHANGED
@@ -91,6 +91,33 @@ function initSchema(db) {
91
91
  value TEXT NOT NULL
92
92
  );
93
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
+ created_at TEXT NOT NULL,
107
+ answered_at TEXT
108
+ );
109
+
110
+ CREATE TABLE IF NOT EXISTS agent_registry (
111
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
112
+ name TEXT NOT NULL UNIQUE,
113
+ project_path TEXT NOT NULL,
114
+ pid INTEGER NOT NULL,
115
+ tmux_pane TEXT,
116
+ status TEXT NOT NULL DEFAULT 'connected',
117
+ connected_at TEXT NOT NULL,
118
+ disconnected_at TEXT
119
+ );
120
+
94
121
  CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
95
122
  CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id);
96
123
  CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
@@ -99,7 +126,59 @@ function initSchema(db) {
99
126
  CREATE INDEX IF NOT EXISTS idx_activity_logs_action ON activity_logs(action);
100
127
  CREATE INDEX IF NOT EXISTS idx_notifications_read ON notifications(read);
101
128
  CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at);
129
+ CREATE INDEX IF NOT EXISTS idx_agent_messages_status ON agent_messages(status);
130
+ CREATE INDEX IF NOT EXISTS idx_agent_messages_project_id ON agent_messages(project_id);
131
+ CREATE INDEX IF NOT EXISTS idx_agent_registry_status ON agent_registry(status);
132
+ CREATE INDEX IF NOT EXISTS idx_agent_registry_name ON agent_registry(name);
102
133
  `);
134
+ // Migrations — add columns that may be missing on existing databases
135
+ const cols = db.prepare("PRAGMA table_info(agent_messages)").all();
136
+ const colNames = new Set(cols.map(c => c.name));
137
+ if (!colNames.has('agent_pid')) {
138
+ db.exec('ALTER TABLE agent_messages ADD COLUMN agent_pid INTEGER');
139
+ }
140
+ if (!colNames.has('delivered')) {
141
+ db.exec('ALTER TABLE agent_messages ADD COLUMN delivered INTEGER');
142
+ }
143
+ if (!colNames.has('sender_name')) {
144
+ db.exec("ALTER TABLE agent_messages ADD COLUMN sender_name TEXT NOT NULL DEFAULT 'unknown'");
145
+ }
146
+ if (!colNames.has('recipient_name')) {
147
+ db.exec("ALTER TABLE agent_messages ADD COLUMN recipient_name TEXT NOT NULL DEFAULT 'user'");
148
+ }
149
+ // Migration: make agent_messages.project_id nullable if it was NOT NULL
150
+ try {
151
+ const tableInfo = db.prepare("SELECT sql FROM sqlite_master WHERE name = 'agent_messages'").get();
152
+ if (tableInfo?.sql?.includes('project_id INTEGER NOT NULL')) {
153
+ db.exec(`
154
+ CREATE TABLE agent_messages_new (
155
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
156
+ project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
157
+ question TEXT NOT NULL,
158
+ context TEXT,
159
+ choices TEXT,
160
+ response TEXT,
161
+ agent_pid INTEGER,
162
+ delivered INTEGER,
163
+ sender_name TEXT NOT NULL DEFAULT 'unknown',
164
+ recipient_name TEXT NOT NULL DEFAULT 'user',
165
+ status TEXT NOT NULL DEFAULT 'pending',
166
+ created_at TEXT NOT NULL,
167
+ answered_at TEXT
168
+ );
169
+ INSERT INTO agent_messages_new SELECT
170
+ id, project_id, question, context, choices, response, agent_pid, delivered,
171
+ COALESCE(sender_name, 'unknown'), COALESCE(recipient_name, 'user'),
172
+ status, created_at, answered_at
173
+ FROM agent_messages;
174
+ DROP TABLE agent_messages;
175
+ ALTER TABLE agent_messages_new RENAME TO agent_messages;
176
+ CREATE INDEX IF NOT EXISTS idx_agent_messages_status ON agent_messages(status);
177
+ CREATE INDEX IF NOT EXISTS idx_agent_messages_project_id ON agent_messages(project_id);
178
+ `);
179
+ }
180
+ }
181
+ catch { /* table may not exist yet */ }
103
182
  }
104
183
  export function closeDb() {
105
184
  if (db) {
package/dist/index.js CHANGED
@@ -31,8 +31,16 @@ function cleanupOrphanedSessions() {
31
31
  }
32
32
  }
33
33
  cleanupOrphanedSessions();
34
- // Always start the HTTP/SSE server
35
- startSSEServer();
34
+ // Always start the HTTP/SSE server (probes for existing instances, finds fallback port)
35
+ await startSSEServer();
36
+ // Liveness checker — periodically check registered agents and mark dead ones
37
+ setInterval(async () => {
38
+ try {
39
+ const { checkAgentLiveness } = await import('./agent-registry.js');
40
+ checkAgentLiveness();
41
+ }
42
+ catch { /* ignore */ }
43
+ }, 30_000);
36
44
  // Only start MCP stdio transport when not in http-only mode
37
45
  if (!httpOnly) {
38
46
  const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
@@ -45,6 +53,7 @@ if (!httpOnly) {
45
53
  const { registerNotificationTools } = await import('./tools/notifications.js');
46
54
  const { registerSettingsTools } = await import('./tools/settings.js');
47
55
  const { registerAgentTools } = await import('./tools/agent.js');
56
+ const { registerAgentInboxTools } = await import('./tools/agent-inbox.js');
48
57
  const server = new McpServer({
49
58
  name: 'taskflow',
50
59
  version: '1.0.0',
@@ -57,6 +66,82 @@ if (!httpOnly) {
57
66
  registerActivityTools(server);
58
67
  registerNotificationTools(server);
59
68
  registerSettingsTools(server);
69
+ registerAgentInboxTools(server);
60
70
  const transport = new StdioServerTransport();
61
71
  await server.connect(transport);
72
+ const { registerAgent, unregisterAgent } = await import('./agent-registry.js');
73
+ // Auto-register this agent
74
+ const agentName = registerAgent();
75
+ console.error(`[agent] registered as "${agentName}"`);
76
+ // Graceful shutdown — mark agent as disconnected
77
+ const cleanup = () => { try {
78
+ unregisterAgent(agentName);
79
+ }
80
+ catch { } process.exit(0); };
81
+ process.on('SIGINT', cleanup);
82
+ process.on('SIGTERM', cleanup);
83
+ // Background poller: deliver messages to this agent's terminal via tmux
84
+ const POLL_INTERVAL = 3000;
85
+ const agentPid = process.ppid;
86
+ let tmuxTarget = null;
87
+ try {
88
+ const { execSync: exec } = await import('child_process');
89
+ const ptsPath = exec(`readlink /proc/${agentPid}/fd/0`).toString().trim();
90
+ const panes = exec('tmux list-panes -a -F "#{pane_id} #{pane_tty}"').toString().trim().split('\n');
91
+ for (const line of panes) {
92
+ const [paneId, paneTty] = line.split(' ');
93
+ if (paneTty === ptsPath) {
94
+ tmuxTarget = paneId;
95
+ break;
96
+ }
97
+ }
98
+ if (tmuxTarget)
99
+ console.error(`[inject] tmux pane ${tmuxTarget} for agent "${agentName}"`);
100
+ else
101
+ console.error('[inject] agent not in tmux — terminal injection disabled');
102
+ }
103
+ catch {
104
+ console.error('[inject] tmux not available');
105
+ }
106
+ if (tmuxTarget) {
107
+ const { execSync: exec } = await import('child_process');
108
+ const target = tmuxTarget;
109
+ setInterval(() => {
110
+ try {
111
+ const db = getDb();
112
+ // Check for messages addressed to this agent (from user or other agents)
113
+ // AND for answered questions this agent sent (inbox responses)
114
+ // Check by agent name AND by agent_pid (backward compat with old messages)
115
+ const incoming = db.prepare(`SELECT * FROM agent_messages WHERE delivered IS NULL AND (
116
+ (recipient_name = ? AND status = 'pending') OR
117
+ (sender_name = ? AND recipient_name = 'user' AND status = 'answered') OR
118
+ (agent_pid = ? AND recipient_name = 'user' AND status = 'answered')
119
+ )`).all(agentName, agentName, agentPid);
120
+ for (const msg of incoming) {
121
+ db.prepare('UPDATE agent_messages SET delivered = 1 WHERE id = ?').run(msg.id);
122
+ let text;
123
+ if (msg.recipient_name === agentName && msg.sender_name === 'user') {
124
+ text = `[Message from User]: ${msg.question}`;
125
+ }
126
+ else if (msg.recipient_name === agentName && msg.sender_name !== 'user') {
127
+ text = `[Message from ${msg.sender_name}]: ${msg.question}`;
128
+ }
129
+ else if (msg.status === 'answered' && msg.response) {
130
+ text = `[Inbox Response] to "${msg.question.slice(0, 60)}": ${msg.response}`;
131
+ }
132
+ else {
133
+ continue;
134
+ }
135
+ try {
136
+ exec(`tmux send-keys -t ${target} ${JSON.stringify(text)} Enter`, { stdio: 'ignore', timeout: 5000 });
137
+ console.error(`[inject] delivered message ${msg.id} to tmux pane ${target}`);
138
+ }
139
+ catch (err) {
140
+ console.error(`[inject] tmux send-keys failed for message ${msg.id}:`, err);
141
+ }
142
+ }
143
+ }
144
+ catch { /* ignore */ }
145
+ }, POLL_INTERVAL);
146
+ }
62
147
  }
package/dist/sse.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- export declare function startSSEServer(): void;
1
+ export declare function startSSEServer(): Promise<void>;
2
2
  export declare function markSSEActive(): void;
3
3
  /**
4
4
  * Broadcast an SSE event. If this process owns the SSE server, send directly.
5
- * Otherwise, relay via HTTP to the process that does (sidecar on port 3456).
5
+ * Otherwise, relay via HTTP to the process that does.
6
6
  */
7
7
  export declare function broadcast(event: string, data: object): void;
8
+ /** Returns the port the SSE server is actively using */
9
+ export declare function getActivePort(): number;
package/dist/sse.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { createServer } from 'http';
2
2
  import { getDb } from './db.js';
3
3
  import { logActivity } from './helpers.js';
4
+ const SERVICE_ID = 'taskflow-mcp';
5
+ const MAX_PORT_ATTEMPTS = 10;
6
+ const PROBE_TIMEOUT_MS = 2000;
4
7
  const clients = new Set();
5
8
  function resolvePort() {
6
9
  // CLI arg takes priority: --port 4000
@@ -14,7 +17,27 @@ function resolvePort() {
14
17
  }
15
18
  return 3456;
16
19
  }
17
- const PORT = resolvePort();
20
+ const PREFERRED_PORT = resolvePort();
21
+ /** The port that is actually serving SSE — either ours or an existing instance's */
22
+ let activePort = PREFERRED_PORT;
23
+ /** Probe a port to check if a TaskFlow service is already running there */
24
+ async function probeTaskFlow(port) {
25
+ const controller = new AbortController();
26
+ const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
27
+ try {
28
+ const res = await fetch(`http://localhost:${port}/healthz`, { signal: controller.signal });
29
+ if (!res.ok)
30
+ return false;
31
+ const body = await res.json();
32
+ return body.service === SERVICE_ID;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ finally {
38
+ clearTimeout(timeout);
39
+ }
40
+ }
18
41
  function jsonResponse(res, status, data) {
19
42
  res.writeHead(status, { 'Content-Type': 'application/json' });
20
43
  res.end(JSON.stringify(data));
@@ -26,7 +49,7 @@ function readBody(req) {
26
49
  req.on('end', () => resolve(body));
27
50
  });
28
51
  }
29
- export function startSSEServer() {
52
+ export async function startSSEServer() {
30
53
  const server = createServer(async (req, res) => {
31
54
  // CORS headers for all requests
32
55
  res.setHeader('Access-Control-Allow-Origin', '*');
@@ -37,6 +60,11 @@ export function startSSEServer() {
37
60
  res.end();
38
61
  return;
39
62
  }
63
+ // GET /healthz — identity probe so other instances can detect us
64
+ if (req.url === '/healthz' && req.method === 'GET') {
65
+ jsonResponse(res, 200, { service: SERVICE_ID, pid: process.pid });
66
+ return;
67
+ }
40
68
  if (req.url === '/events' && req.method === 'GET') {
41
69
  res.writeHead(200, {
42
70
  'Content-Type': 'text/event-stream',
@@ -58,7 +86,9 @@ export function startSSEServer() {
58
86
  const sessions = db.prepare('SELECT * FROM sessions').all();
59
87
  const settings = db.prepare('SELECT * FROM settings').all();
60
88
  const activityLogs = db.prepare('SELECT * FROM activity_logs ORDER BY created_at DESC LIMIT 200').all();
61
- jsonResponse(res, 200, { tasks, projects, sessions, settings, activityLogs });
89
+ const agentMessages = db.prepare('SELECT * FROM agent_messages ORDER BY created_at DESC LIMIT 100').all();
90
+ const agentRegistry = db.prepare('SELECT * FROM agent_registry ORDER BY connected_at DESC').all();
91
+ jsonResponse(res, 200, { tasks, projects, sessions, settings, activityLogs, agentMessages, agentRegistry });
62
92
  return;
63
93
  }
64
94
  // ─── Mutation endpoints ───────────────────────────────────────────
@@ -69,6 +99,8 @@ export function startSSEServer() {
69
99
  db.exec('DELETE FROM projects');
70
100
  db.exec('DELETE FROM notifications');
71
101
  db.exec('DELETE FROM activity_logs');
102
+ db.exec('DELETE FROM agent_messages');
103
+ db.exec('DELETE FROM agent_registry');
72
104
  logActivity('data_cleared', 'All data cleared via UI', { entityType: 'system' });
73
105
  broadcast('data_cleared', { entity: 'system', action: 'data_cleared', payload: {} });
74
106
  jsonResponse(res, 200, { cleared: true });
@@ -234,18 +266,121 @@ export function startSSEServer() {
234
266
  jsonResponse(res, 200, { relayed: true });
235
267
  return;
236
268
  }
269
+ // POST /api/agent-messages/:id/respond — user responds to an agent question
270
+ const agentRespondMatch = req.url?.match(/^\/api\/agent-messages\/(\d+)\/respond$/);
271
+ if (agentRespondMatch && req.method === 'POST') {
272
+ const db = getDb();
273
+ const id = Number(agentRespondMatch[1]);
274
+ const message = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
275
+ if (!message) {
276
+ jsonResponse(res, 404, { error: 'Message not found' });
277
+ return;
278
+ }
279
+ if (message.status === 'answered') {
280
+ jsonResponse(res, 400, { error: 'Already answered' });
281
+ return;
282
+ }
283
+ const body = JSON.parse(await readBody(req));
284
+ const response = body.response;
285
+ if (!response) {
286
+ jsonResponse(res, 400, { error: 'Response is required' });
287
+ return;
288
+ }
289
+ const ts = new Date().toISOString();
290
+ db.prepare('UPDATE agent_messages SET response = ?, status = ?, answered_at = ? WHERE id = ?')
291
+ .run(response, 'answered', ts, id);
292
+ const updated = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
293
+ broadcast('agent_question_answered', { entity: 'agent_message', action: 'agent_question_answered', payload: updated });
294
+ logActivity('agent_question_answered', `Responded to: ${message.question}`, { entityType: 'agent_message', entityId: id });
295
+ jsonResponse(res, 200, updated);
296
+ return;
297
+ }
298
+ // POST /api/agent-messages/:id/dismiss — dismiss without responding (answered in terminal)
299
+ const agentDismissMatch = req.url?.match(/^\/api\/agent-messages\/(\d+)\/dismiss$/);
300
+ if (agentDismissMatch && req.method === 'POST') {
301
+ const db = getDb();
302
+ const id = Number(agentDismissMatch[1]);
303
+ const message = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
304
+ if (!message) {
305
+ jsonResponse(res, 404, { error: 'Message not found' });
306
+ return;
307
+ }
308
+ if (message.status !== 'pending') {
309
+ jsonResponse(res, 400, { error: 'Message is not pending' });
310
+ return;
311
+ }
312
+ const ts = new Date().toISOString();
313
+ db.prepare('UPDATE agent_messages SET status = ?, answered_at = ? WHERE id = ?')
314
+ .run('dismissed', ts, id);
315
+ const updated = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
316
+ broadcast('agent_question_answered', { entity: 'agent_message', action: 'agent_question_answered', payload: updated });
317
+ jsonResponse(res, 200, updated);
318
+ return;
319
+ }
320
+ // POST /api/agent-messages/send — user sends a message to an agent
321
+ if (req.url === '/api/agent-messages/send' && req.method === 'POST') {
322
+ const db = getDb();
323
+ const body = JSON.parse(await readBody(req));
324
+ const { recipient, message: msgText, projectId } = body;
325
+ if (!recipient || !msgText) {
326
+ jsonResponse(res, 400, { error: 'recipient and message are required' });
327
+ return;
328
+ }
329
+ const ts = new Date().toISOString();
330
+ const result = db.prepare(`INSERT INTO agent_messages (project_id, question, sender_name, recipient_name, status, created_at)
331
+ VALUES (?, ?, 'user', ?, 'pending', ?)`).run(projectId ?? null, msgText, recipient, ts);
332
+ const id = result.lastInsertRowid;
333
+ const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
334
+ broadcast('agent_question', { entity: 'agent_message', action: 'agent_question', payload: msg });
335
+ jsonResponse(res, 200, msg);
336
+ return;
337
+ }
237
338
  res.writeHead(404);
238
339
  res.end('Not Found');
239
340
  });
240
- server.on('error', (err) => {
241
- if (err.code === 'EADDRINUSE') {
242
- // Another MCP instance already owns this port — skip SSE, MCP tools still work
243
- return;
341
+ // Try ports sequentially: probe occupied ports to see if they're ours
342
+ await new Promise((resolve) => {
343
+ let attempt = 0;
344
+ function tryPort(port) {
345
+ if (attempt >= MAX_PORT_ATTEMPTS) {
346
+ console.error(`[SSE] failed to bind after ${MAX_PORT_ATTEMPTS} attempts (ports ${PREFERRED_PORT}–${port - 1})`);
347
+ resolve();
348
+ return;
349
+ }
350
+ server.once('error', async (err) => {
351
+ if (err.code === 'EADDRINUSE') {
352
+ const isTaskFlow = await probeTaskFlow(port);
353
+ if (isTaskFlow) {
354
+ // Another TaskFlow instance owns this port — connect to it instead
355
+ activePort = port;
356
+ console.log(`[SSE] port ${port} owned by another TaskFlow instance (pid probe) — using it`);
357
+ resolve();
358
+ }
359
+ else {
360
+ // Not ours — try next port
361
+ console.log(`[SSE] port ${port} in use by non-TaskFlow service — trying ${port + 1}`);
362
+ attempt++;
363
+ tryPort(port + 1);
364
+ }
365
+ }
366
+ else {
367
+ console.error('[SSE] unexpected server error:', err.message);
368
+ resolve();
369
+ }
370
+ });
371
+ server.listen(port, '0.0.0.0', () => {
372
+ activePort = port;
373
+ markSSEActive();
374
+ if (port !== PREFERRED_PORT) {
375
+ console.log(`[SSE] listening on fallback port ${port} (preferred ${PREFERRED_PORT} was unavailable)`);
376
+ }
377
+ else {
378
+ console.log(`[SSE] listening on port ${port}`);
379
+ }
380
+ resolve();
381
+ });
244
382
  }
245
- // Unexpected error — still don't crash the MCP process
246
- });
247
- server.listen(PORT, '0.0.0.0', () => {
248
- markSSEActive();
383
+ tryPort(PREFERRED_PORT);
249
384
  });
250
385
  }
251
386
  /** Broadcast directly to connected SSE clients in this process */
@@ -262,7 +397,7 @@ export function markSSEActive() {
262
397
  }
263
398
  /**
264
399
  * Broadcast an SSE event. If this process owns the SSE server, send directly.
265
- * Otherwise, relay via HTTP to the process that does (sidecar on port 3456).
400
+ * Otherwise, relay via HTTP to the process that does.
266
401
  */
267
402
  export function broadcast(event, data) {
268
403
  if (sseServerActive && clients.size > 0) {
@@ -271,7 +406,7 @@ export function broadcast(event, data) {
271
406
  else {
272
407
  // Relay to the SSE server owner via HTTP
273
408
  const body = JSON.stringify({ event, data });
274
- fetch(`http://localhost:${PORT}/api/broadcast`, {
409
+ fetch(`http://localhost:${activePort}/api/broadcast`, {
275
410
  method: 'POST',
276
411
  headers: { 'Content-Type': 'application/json' },
277
412
  body,
@@ -280,3 +415,7 @@ export function broadcast(event, data) {
280
415
  });
281
416
  }
282
417
  }
418
+ /** Returns the port the SSE server is actively using */
419
+ export function getActivePort() {
420
+ return activePort;
421
+ }
@@ -18,6 +18,7 @@ export declare function clearActivityLog(): Promise<{
18
18
  export declare function logDebug(params: {
19
19
  message: string;
20
20
  task_id?: number;
21
+ project_id?: number;
21
22
  detail?: string;
22
23
  }): Promise<{
23
24
  content: {
@@ -34,8 +34,11 @@ export async function clearActivityLog() {
34
34
  export async function logDebug(params) {
35
35
  const db = getDb();
36
36
  const now = new Date().toISOString();
37
+ // task_id takes priority; fall back to project_id
38
+ const entityType = params.task_id ? 'task' : params.project_id ? 'project' : null;
39
+ const entityId = params.task_id ?? params.project_id ?? null;
37
40
  const result = db.prepare(`INSERT INTO activity_logs (action, title, detail, entity_type, entity_id, created_at)
38
- VALUES (?, ?, ?, ?, ?, ?)`).run('debug_log', params.message, params.detail ?? null, params.task_id ? 'task' : null, params.task_id ?? null, now);
41
+ VALUES (?, ?, ?, ?, ?, ?)`).run('debug_log', params.message, params.detail ?? null, entityType, entityId, now);
39
42
  const entry = db.prepare('SELECT * FROM activity_logs WHERE id = ?').get(result.lastInsertRowid);
40
43
  broadcastChange('activity', 'activity_logged', entry);
41
44
  return successResponse({ id: result.lastInsertRowid, message: params.message });
@@ -48,9 +51,10 @@ export function registerActivityTools(server) {
48
51
  entity_type: z.string().optional(),
49
52
  }, async (params) => getActivityLog(params));
50
53
  server.tool('clear_activity_log', 'Delete all activity log entries. Use with caution — this is irreversible.', {}, async () => clearActivityLog());
51
- server.tool('log_debug', 'Log a debug entry to the activity log. Use this while debugging to record what you are investigating, what you tried, what you found, and your reasoning. Optionally link to a task. These entries appear in the Activity Pulse in the UI.', {
54
+ server.tool('log_debug', 'Log a debug entry to the activity log. Use this to record your work process what you investigated, commands you ran, decisions you made, and findings. Entries appear in the Activity Pulse and on the project page. Link to a task (task_id) or project (project_id) for context.', {
52
55
  message: z.string().describe('Short summary of what you are doing or found'),
53
- detail: z.string().optional().describe('Longer explanation — stack traces, error messages, hypotheses, what you tried'),
56
+ detail: z.string().optional().describe('Longer explanation — stack traces, error messages, hypotheses, commands run, what you tried'),
54
57
  task_id: z.number().optional().describe('Link this debug log to a specific task'),
58
+ project_id: z.number().optional().describe('Link this debug log to a project (used when no specific task applies)'),
55
59
  }, async (params) => logDebug(params));
56
60
  }
@@ -0,0 +1,5 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /** The name assigned to this agent after registration */
3
+ declare let myAgentName: string | null;
4
+ export { myAgentName };
5
+ export declare function registerAgentInboxTools(server: McpServer): void;
@@ -0,0 +1,103 @@
1
+ import { z } from 'zod';
2
+ import { getDb } from '../db.js';
3
+ import { logActivity, errorResponse, successResponse, now, broadcastChange } from '../helpers.js';
4
+ import { registerAgent as doRegister, getAgent, listAgents } from '../agent-registry.js';
5
+ /** The name assigned to this agent after registration */
6
+ let myAgentName = null;
7
+ /** Get or auto-register the agent name */
8
+ function ensureRegistered() {
9
+ if (!myAgentName) {
10
+ myAgentName = doRegister();
11
+ }
12
+ return myAgentName;
13
+ }
14
+ export { myAgentName };
15
+ export function registerAgentInboxTools(server) {
16
+ server.tool('register_agent', 'Register this agent with a custom name. Optional — agents auto-register on startup using the project folder name. Call this only if you want a specific name.', {
17
+ name: z.string().optional().describe('Custom agent name. If omitted, uses the project folder name.'),
18
+ }, async (params) => {
19
+ myAgentName = doRegister({ customName: params.name });
20
+ return successResponse({ name: myAgentName, message: `Registered as "${myAgentName}"` });
21
+ });
22
+ server.tool('ask_user', 'Post a question to the TaskFlow Agent Inbox for the user to answer remotely. Returns immediately with the message ID. The question appears in the Agent Inbox UI with full context and optional quick-tap choices. After posting, use check_response to retrieve the user\'s answer. Always tell the user you posted a question so they know to check the inbox.', {
23
+ project_id: z.number().describe('Project ID to attach the question to'),
24
+ question: z.string().describe('The question to ask the user'),
25
+ context: z.string().optional().describe('Markdown context — proposals, trade-offs, code snippets shown before the question'),
26
+ choices: z.array(z.string()).optional().describe('Optional quick-tap choices, e.g. ["Yes", "No", "Skip"]'),
27
+ }, async (params) => {
28
+ const db = getDb();
29
+ const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(params.project_id);
30
+ if (!project)
31
+ return errorResponse(`Project ${params.project_id} not found`, 'NOT_FOUND');
32
+ const senderName = ensureRegistered();
33
+ const ts = now();
34
+ const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, choices, sender_name, recipient_name, agent_pid, status, created_at)
35
+ VALUES (?, ?, ?, ?, ?, 'user', ?, 'pending', ?)`).run(params.project_id, params.question, params.context ?? null, params.choices ? JSON.stringify(params.choices) : null, senderName, process.ppid, ts);
36
+ const id = result.lastInsertRowid;
37
+ const message = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
38
+ broadcastChange('agent_message', 'agent_question', message);
39
+ logActivity('agent_question', params.question, { entityType: 'agent_message', entityId: id });
40
+ return successResponse({
41
+ id,
42
+ status: 'pending',
43
+ sender: senderName,
44
+ message: `Question posted to Agent Inbox (id: ${id}). Use check_response(${id}) to retrieve the user's answer.`,
45
+ });
46
+ });
47
+ server.tool('check_response', 'Check if the user has responded to a previously posted agent question. Returns the response if answered, or status "pending" if still waiting.', {
48
+ message_id: z.number().describe('The agent message ID returned by ask_user'),
49
+ }, async (params) => {
50
+ const db = getDb();
51
+ const message = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(params.message_id);
52
+ if (!message)
53
+ return errorResponse(`Message ${params.message_id} not found`, 'NOT_FOUND');
54
+ if (message.status === 'answered') {
55
+ return successResponse({
56
+ id: message.id, status: 'answered', response: message.response,
57
+ question: message.question, answered_at: message.answered_at,
58
+ });
59
+ }
60
+ return successResponse({
61
+ id: message.id, status: 'pending', question: message.question,
62
+ message: 'User has not responded yet. Try again later or continue with other work.',
63
+ });
64
+ });
65
+ server.tool('send_to_agent', 'Send a message to another agent by name. Returns immediately. The recipient agent will receive the message in their terminal (if running in tmux).', {
66
+ recipient: z.string().describe('Name of the target agent (e.g. "backend", "task_flow:2")'),
67
+ message: z.string().describe('The message to send'),
68
+ context: z.string().optional().describe('Optional markdown context'),
69
+ }, async (params) => {
70
+ const db = getDb();
71
+ const senderName = ensureRegistered();
72
+ const recipient = getAgent(params.recipient);
73
+ if (!recipient)
74
+ return errorResponse(`Agent "${params.recipient}" not found`, 'NOT_FOUND');
75
+ const ts = now();
76
+ const result = db.prepare(`INSERT INTO agent_messages (project_id, question, context, sender_name, recipient_name, status, created_at)
77
+ VALUES (NULL, ?, ?, ?, ?, 'pending', ?)`).run(params.message, params.context ?? null, senderName, params.recipient, ts);
78
+ const id = result.lastInsertRowid;
79
+ const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
80
+ broadcastChange('agent_message', 'agent_question', msg);
81
+ return successResponse({ id, sender: senderName, recipient: params.recipient, status: 'pending' });
82
+ });
83
+ server.tool('check_messages', 'Check for incoming messages from users or other agents addressed to this agent.', {}, async () => {
84
+ const db = getDb();
85
+ const name = ensureRegistered();
86
+ const messages = db.prepare(`SELECT * FROM agent_messages WHERE recipient_name = ? AND status = 'pending' ORDER BY created_at ASC`).all(name);
87
+ return successResponse({
88
+ agent: name,
89
+ count: messages.length,
90
+ messages: messages.map(m => ({
91
+ id: m.id, sender: m.sender_name, question: m.question,
92
+ context: m.context, choices: m.choices ? JSON.parse(m.choices) : null,
93
+ created_at: m.created_at,
94
+ })),
95
+ });
96
+ });
97
+ server.tool('list_agents', 'List registered agents with their status, project path, and connection info.', {
98
+ status: z.enum(['connected', 'disconnected']).optional().describe('Filter by status. Omit for all agents.'),
99
+ }, async (params) => {
100
+ const agents = listAgents(params.status);
101
+ return successResponse(agents);
102
+ });
103
+ }
@@ -12,8 +12,8 @@ export async function getAgentInstructions() {
12
12
  const instructions = {
13
13
  role: 'TaskFlow — local-first task & time tracker with MCP integration.',
14
14
  startup: [
15
- 'search_projects to find the current project (try name variants; confirm with user if ambiguous)',
16
- 'list_tasks status="in_progress" and status="blocked"',
15
+ 'Derive the project name from the **folder name** of the current working directory (e.g. `/home/user/projects/my-app` search for "my-app"). Use search_projects with that name. If 2–3 results match, **ask the user** which project to use — never guess.',
16
+ 'list_tasks status="in_progress" and status="blocked" for the confirmed project.',
17
17
  'list_notifications unread_only=true',
18
18
  ],
19
19
  state: {
@@ -24,26 +24,43 @@ export async function getAgentInstructions() {
24
24
  unread: unreadNotifs,
25
25
  },
26
26
  rules: [
27
+ // Project discovery
28
+ 'The project name should be derived from the working directory folder name. Always search_projects first. If no match, create the project using create_project with the folder name. If multiple matches, present them to the user and ask which one to use.',
27
29
  // Task tracking
28
30
  'Proactively create tasks for ALL substantial work — features, bugs, refactors, AND debugging/investigation. Debugging is real work: create a task for it (e.g. "Debug: SSE connection dropping"), start a timer, and track it the same way you would a feature. search_tasks first to avoid duplicates. Link tasks to the confirmed project.',
29
31
  'Timer lifecycle: start_timer → work → stop_timer(final_status). Use "done"/"partial_done"/"blocked". pause_timer when waiting for input. This applies equally to debugging tasks — start a timer before investigating, stop it when resolved or blocked.',
30
32
  'MUST stop_timer with "done" when work is complete. Never leave finished tasks in "in_progress" or "paused". This includes debugging tasks — when the bug is fixed or the investigation concludes, stop the timer.',
33
+ // Bugs & post-build debugging
34
+ 'When a user reports a bug or you discover one while testing/building: FIRST search_tasks for an existing related task. If found (even if "done"), move it back to in_progress (update_task_status) — this reopens the task and auto-starts a timer. Then log_debug on that task to document the new bug. If no related task exists, create a new one (e.g. "Bug: sidebar not rendering in light mode") with tag "bug", start the timer, and begin debugging.',
35
+ 'This also applies to post-build issues: after running a build/test and seeing failures, do NOT silently fix them. Open or reopen a task first, log what failed, then fix. The task becomes the paper trail — the user and future agents can see what broke, what was tried, and how it was resolved.',
31
36
  // Dependencies
32
37
  'Check task dependencies before starting. If any dep is incomplete, set task to "blocked".',
33
38
  'After completing a task, check if blocked tasks depending on it can be unblocked.',
34
39
  // Prioritization
35
40
  'When user is unsure what to work on: list_tasks priority="critical"/"high" status="not_started".',
36
- // Logging
37
- 'ALWAYS log your entire debugging process using log_debug with task_id. Log every stage: issue identification, hypothesis, files read/edited, commands run, errors encountered, fixes attempted, and resolution. This creates a visible trail in the Activity Pulse so users can follow your reasoning in real-time.',
38
- 'log_debug early and often not just at the end. Log when you start investigating, when you find a clue, when you edit a file, and when the fix lands. Use Markdown: headings for stages, code blocks for errors/paths, bold for key findings.',
41
+ // Transparency — CRITICAL
42
+ 'ALWAYS tell the user what commands you are running and why. Before executing a shell command, state: "Running: `<command>`". After significant actions (file edits, installs, config changes), summarize what changed. The user must be able to reconstruct what happened from your messages alone they should never wonder "what did the agent do?".',
43
+ 'When starting work, briefly state the approach: what you plan to do, which files you expect to touch, and what commands you will run. This gives the user a chance to course-correct before you act.',
44
+ // Debug logging — comprehensive
45
+ 'ALWAYS log your work using log_debug. Log with a task_id when working on a specific task, or with project_id for project-level observations. Use log_debug as a running journal — it shows up in the Activity Pulse and on the project page so the user (and future agents) can follow your reasoning.',
46
+ 'What to log: **every stage** of your process. When you start investigating, when you form a hypothesis, when you read/edit a file, the exact commands you run (with output snippets), errors encountered, fixes attempted, and the resolution. Log the **path you took**, not just the destination.',
47
+ 'Log early and often — not just at the end. A debug log entry per significant step (e.g. "Read `sse.ts:57` — found the healthz route exists but is unreachable because..."). Use Markdown: `## headings` for stages, `` `code blocks` `` for errors/paths/commands, **bold** for key findings.',
48
+ 'For project-level notes (architecture decisions, setup gotchas, "how to run the app"), use log_debug with project_id instead of task_id. These appear on the project page and serve as living documentation.',
39
49
  // Formatting
40
50
  'Use Markdown in descriptions — headings, bullets, code blocks, bold. The UI renders it.',
51
+ // Agent Inbox — remote communication
52
+ 'When you need user input (choices, confirmations, clarifications), ALWAYS do BOTH: (1) ask the question normally in the terminal conversation, AND (2) call ask_user to post it to the Agent Inbox. This lets the user respond from either the terminal or the TaskFlow UI remotely. If the user explicitly tells you not to use the inbox (e.g. "don\'t post to inbox"), skip the ask_user call.',
53
+ 'After calling ask_user, continue your work if possible. The user may respond from the UI, and the response will be delivered to your terminal automatically (if running in tmux). You can also call check_response to poll for the answer.',
54
+ // Agent Inbox — formatting
55
+ 'When calling ask_user, write **proper titles** for the question field — clear, concise, well-formed sentences (not random fragments or debug-style text). The question is the headline the user sees first.',
56
+ 'The `context` field in ask_user is rendered as Markdown in the UI. Format it well: use `## headings` to organize sections, `**bold**` for key terms, bullet lists for options/trade-offs, and `\\`code\\`` for file paths or commands. Use real newlines (not literal \\\\n). The context should read like a well-written message, not raw debug output.',
41
57
  ],
42
58
  workflow: 'not_started → in_progress (start_timer) → paused (pause_timer) → done/partial_done/blocked (stop_timer)',
43
59
  tips: [
44
60
  'Filter by tags (list_tasks tag="bug"), search by keyword (search_tasks), get full detail (get_task id).',
45
61
  'get_analytics for time spent & completion rates. Dependencies show in the dependency graph.',
46
62
  'list_tasks/search_tasks return compact summaries. Use get_task(id) to read full descriptions.',
63
+ 'log_debug accepts task_id OR project_id — use project_id for project-wide notes visible on the project page.',
47
64
  ],
48
65
  };
49
66
  return successResponse(instructions);
package/dist/types.d.ts CHANGED
@@ -27,6 +27,17 @@ export declare const NotificationType: z.ZodEnum<{
27
27
  error: "error";
28
28
  }>;
29
29
  export type NotificationType = z.infer<typeof NotificationType>;
30
+ export declare const AgentMessageStatus: z.ZodEnum<{
31
+ pending: "pending";
32
+ answered: "answered";
33
+ dismissed: "dismissed";
34
+ }>;
35
+ export type AgentMessageStatus = z.infer<typeof AgentMessageStatus>;
36
+ export declare const AgentStatus: z.ZodEnum<{
37
+ connected: "connected";
38
+ disconnected: "disconnected";
39
+ }>;
40
+ export type AgentStatus = z.infer<typeof AgentStatus>;
30
41
  export declare const ActivityAction: z.ZodEnum<{
31
42
  task_created: "task_created";
32
43
  task_deleted: "task_deleted";
@@ -51,10 +62,14 @@ export declare const ActivityAction: z.ZodEnum<{
51
62
  tag_added: "tag_added";
52
63
  tag_removed: "tag_removed";
53
64
  debug_log: "debug_log";
65
+ agent_question: "agent_question";
66
+ agent_question_answered: "agent_question_answered";
67
+ agent_connected: "agent_connected";
68
+ agent_disconnected: "agent_disconnected";
54
69
  }>;
55
70
  export type ActivityAction = z.infer<typeof ActivityAction>;
56
71
  export declare const VALID_TRANSITIONS: Record<TaskStatus, TaskStatus[]>;
57
- export type ErrorCode = 'NOT_FOUND' | 'INVALID_TRANSITION' | 'VALIDATION_ERROR' | 'CYCLE_DETECTED' | 'SESSION_ALREADY_ACTIVE' | 'NO_ACTIVE_SESSION';
72
+ export type ErrorCode = 'NOT_FOUND' | 'INVALID_TRANSITION' | 'VALIDATION_ERROR' | 'CYCLE_DETECTED' | 'SESSION_ALREADY_ACTIVE' | 'NO_ACTIVE_SESSION' | 'ALREADY_ANSWERED';
58
73
  export declare const LinkSchema: z.ZodObject<{
59
74
  label: z.ZodString;
60
75
  url: z.ZodString;
package/dist/types.js CHANGED
@@ -5,6 +5,8 @@ export const TaskStatus = z.enum([
5
5
  export const TaskPriority = z.enum(['low', 'medium', 'high', 'critical']);
6
6
  export const ProjectType = z.enum(['active_project', 'project_idea']);
7
7
  export const NotificationType = z.enum(['info', 'success', 'warning', 'error']);
8
+ export const AgentMessageStatus = z.enum(['pending', 'answered', 'dismissed']);
9
+ export const AgentStatus = z.enum(['connected', 'disconnected']);
8
10
  export const ActivityAction = z.enum([
9
11
  'task_created', 'task_deleted', 'task_status_changed', 'task_completed',
10
12
  'task_partial_done', 'timer_started', 'timer_paused', 'timer_stopped',
@@ -12,6 +14,8 @@ export const ActivityAction = z.enum([
12
14
  'tasks_bulk_created', 'settings_saved', 'data_seeded', 'data_cleared',
13
15
  'task_linked', 'task_unlinked', 'dependency_added', 'dependency_removed',
14
16
  'link_added', 'tag_added', 'tag_removed', 'debug_log',
17
+ 'agent_question', 'agent_question_answered',
18
+ 'agent_connected', 'agent_disconnected',
15
19
  ]);
16
20
  export const VALID_TRANSITIONS = {
17
21
  not_started: ['in_progress', 'blocked'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dalmasonto/taskflow-mcp",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "MCP server for TaskFlow — manage projects, tasks, timers, analytics via AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",