@dalmasonto/taskflow-mcp 1.0.16 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.d.ts CHANGED
@@ -5,6 +5,8 @@ export interface TaskFlowConfig {
5
5
  logLevel: 'debug' | 'info' | 'warn' | 'error';
6
6
  agentLivenessInterval: number;
7
7
  maxPortAttempts: number;
8
+ relayUrl: string;
9
+ relayPushToken: string;
8
10
  }
9
11
  /** Keys that live in the config file (not SQLite) */
10
12
  export declare const SERVER_CONFIG_KEYS: Set<string>;
package/dist/config.js CHANGED
@@ -9,6 +9,8 @@ export const SERVER_CONFIG_KEYS = new Set([
9
9
  'logLevel',
10
10
  'agentLivenessInterval',
11
11
  'maxPortAttempts',
12
+ 'relayUrl',
13
+ 'relayPushToken',
12
14
  ]);
13
15
  // ─── defaults ────────────────────────────────────────────────────────
14
16
  const DEFAULTS = {
@@ -18,6 +20,8 @@ const DEFAULTS = {
18
20
  logLevel: 'info',
19
21
  agentLivenessInterval: 30_000,
20
22
  maxPortAttempts: 10,
23
+ relayUrl: '',
24
+ relayPushToken: '',
21
25
  };
22
26
  // ─── config file path ────────────────────────────────────────────────
23
27
  const CONFIG_PATH = resolve(homedir(), '.taskflow_config.json');
@@ -57,6 +61,12 @@ function applyCliAndEnvOverrides(config) {
57
61
  if (process.env.TASKFLOW_LOG_LEVEL) {
58
62
  config.logLevel = process.env.TASKFLOW_LOG_LEVEL;
59
63
  }
64
+ if (process.env.TASKFLOW_RELAY_URL) {
65
+ config.relayUrl = process.env.TASKFLOW_RELAY_URL;
66
+ }
67
+ if (process.env.TASKFLOW_RELAY_PUSH_TOKEN) {
68
+ config.relayPushToken = process.env.TASKFLOW_RELAY_PUSH_TOKEN;
69
+ }
60
70
  // CLI args take highest priority
61
71
  const portArgIdx = process.argv.indexOf('--port');
62
72
  if (portArgIdx !== -1 && process.argv[portArgIdx + 1]) {
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import 'dotenv/config';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import 'dotenv/config';
2
3
  import { getConfig } from './config.js';
3
4
  import { startSSEServer } from './sse.js';
4
5
  import { getDb } from './db.js';
package/dist/sse.d.ts CHANGED
@@ -3,6 +3,7 @@ export declare function markSSEActive(): void;
3
3
  /**
4
4
  * Broadcast an SSE event. If this process owns the SSE server, send directly.
5
5
  * Otherwise, relay via HTTP to the process that does.
6
+ * Also pushes to the remote relay server if configured.
6
7
  */
7
8
  export declare function broadcast(event: string, data: object): void;
8
9
  /** Returns the port the SSE server is actively using */
package/dist/sse.js CHANGED
@@ -5,6 +5,8 @@ import { logActivity } from './helpers.js';
5
5
  import { getConfig } from './config.js';
6
6
  const SERVICE_ID = 'taskflow-mcp';
7
7
  const PROBE_TIMEOUT_MS = 2000;
8
+ // ─── Relay upstream config (from config file, env vars, or .env) ────
9
+ const { relayUrl: RELAY_URL, relayPushToken: RELAY_PUSH_TOKEN } = getConfig();
8
10
  const clients = new Set();
9
11
  // ─── Event buffer for Last-Event-ID replay ──────────────────────────
10
12
  const EVENT_BUFFER_SIZE = 200;
@@ -51,6 +53,86 @@ function readBody(req) {
51
53
  req.on('end', () => resolve(body));
52
54
  });
53
55
  }
56
+ const PROMPT_PATTERNS = [
57
+ /Esc to cancel/i,
58
+ /Tab to amend/i,
59
+ /\(y\/n\)/i,
60
+ /\(yes\/no\)/i,
61
+ /Do you want to/i,
62
+ /Allow .+ for this/i,
63
+ ];
64
+ function detectPrompt(content) {
65
+ const tail = content.split('\n').slice(-25).join('\n');
66
+ const hints = [];
67
+ if (/Esc to cancel/i.test(tail))
68
+ hints.push('Esc to cancel');
69
+ if (/Tab to amend/i.test(tail))
70
+ hints.push('Tab to amend');
71
+ if (/shift\+tab/i.test(tail))
72
+ hints.push('Shift+Tab');
73
+ const detected = PROMPT_PATTERNS.some(p => p.test(tail));
74
+ return { detected, hints };
75
+ }
76
+ /** Fast string hash (djb2) for content change detection */
77
+ function hashContent(str) {
78
+ let hash = 5381;
79
+ for (let i = 0; i < str.length; i++) {
80
+ hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
81
+ }
82
+ return hash;
83
+ }
84
+ /**
85
+ * Unified terminal poller — single capture per agent, broadcasts:
86
+ * - terminal_capture: full pane content for UI rendering (only when changed)
87
+ * - agent_awaiting_input / agent_input_resolved: prompt state transitions
88
+ * - notification_created: when an agent first needs input
89
+ */
90
+ function pollAgentTerminals(promptState, contentHashes) {
91
+ const db = getDb();
92
+ const agents = db.prepare("SELECT name, tmux_pane, status, pid FROM agent_registry WHERE status = 'connected' AND tmux_pane IS NOT NULL").all();
93
+ for (const agent of agents) {
94
+ try {
95
+ const content = execFileSync('tmux', ['capture-pane', '-p', '-S', '-', '-t', agent.tmux_pane], { timeout: 5000 }).toString();
96
+ // Only broadcast if content actually changed
97
+ const hash = hashContent(content);
98
+ const prevHash = contentHashes.get(agent.name);
99
+ if (hash !== prevHash) {
100
+ contentHashes.set(agent.name, hash);
101
+ broadcast('terminal_capture', {
102
+ entity: 'terminal',
103
+ action: 'terminal_capture',
104
+ payload: { name: agent.name, pane: agent.tmux_pane, content },
105
+ });
106
+ }
107
+ // Prompt detection with state transitions (always check, even if content unchanged hash-wise)
108
+ const { detected, hints } = detectPrompt(content);
109
+ const wasAwaiting = promptState.get(agent.name) ?? false;
110
+ if (detected && !wasAwaiting) {
111
+ promptState.set(agent.name, true);
112
+ const ts = new Date().toISOString();
113
+ const hintsText = hints.length > 0 ? ` (${hints.join(' · ')})` : '';
114
+ db.prepare('INSERT INTO notifications (title, message, type, created_at) VALUES (?, ?, ?, ?)').run(`Agent "${agent.name}" needs input`, `A permission prompt is waiting for your response${hintsText}`, 'warning', ts);
115
+ const notification = db.prepare('SELECT * FROM notifications ORDER BY id DESC LIMIT 1').get();
116
+ broadcast('notification_created', { entity: 'notification', action: 'notification_created', payload: notification });
117
+ broadcast('agent_awaiting_input', { entity: 'agent', action: 'agent_awaiting_input', payload: { name: agent.name, hints, awaiting: true } });
118
+ }
119
+ else if (!detected && wasAwaiting) {
120
+ promptState.set(agent.name, false);
121
+ broadcast('agent_input_resolved', { entity: 'agent', action: 'agent_input_resolved', payload: { name: agent.name, awaiting: false } });
122
+ }
123
+ }
124
+ catch {
125
+ // tmux capture failed — agent pane may be gone
126
+ }
127
+ }
128
+ // Clean up agents that disconnected
129
+ for (const [name] of promptState) {
130
+ if (!agents.some(a => a.name === name)) {
131
+ promptState.delete(name);
132
+ contentHashes.delete(name);
133
+ }
134
+ }
135
+ }
54
136
  export async function startSSEServer() {
55
137
  const server = createServer(async (req, res) => {
56
138
  // CORS headers for all requests
@@ -475,12 +557,37 @@ export async function startSSEServer() {
475
557
  }
476
558
  }
477
559
  }, 30_000);
560
+ // Terminal capture & prompt detection — single capture loop for all agents
561
+ const promptState = new Map();
562
+ const contentHashes = new Map();
563
+ setInterval(() => {
564
+ try {
565
+ pollAgentTerminals(promptState, contentHashes);
566
+ }
567
+ catch { /* ignore */ }
568
+ }, 3_000);
478
569
  if (port !== PREFERRED_PORT) {
479
570
  console.log(`[SSE] listening on fallback port ${port} (preferred ${PREFERRED_PORT} was unavailable)`);
480
571
  }
481
572
  else {
482
573
  console.log(`[SSE] listening on port ${port}`);
483
574
  }
575
+ // Register with remote relay so it knows where to proxy commands
576
+ if (RELAY_URL && RELAY_PUSH_TOKEN) {
577
+ const localUrl = `http://${cfg.host}:${port}`;
578
+ fetch(`${RELAY_URL}/register`, {
579
+ method: 'POST',
580
+ headers: {
581
+ 'Content-Type': 'application/json',
582
+ 'Authorization': `Bearer ${RELAY_PUSH_TOKEN}`,
583
+ },
584
+ body: JSON.stringify({ url: localUrl }),
585
+ }).then(() => {
586
+ console.log(`[SSE] registered with relay at ${RELAY_URL}`);
587
+ }).catch((err) => {
588
+ console.error(`[SSE] relay registration failed: ${err.message}`);
589
+ });
590
+ }
484
591
  resolve();
485
592
  });
486
593
  }
@@ -504,6 +611,7 @@ export function markSSEActive() {
504
611
  /**
505
612
  * Broadcast an SSE event. If this process owns the SSE server, send directly.
506
613
  * Otherwise, relay via HTTP to the process that does.
614
+ * Also pushes to the remote relay server if configured.
507
615
  */
508
616
  export function broadcast(event, data) {
509
617
  if (sseServerActive && clients.size > 0) {
@@ -520,6 +628,17 @@ export function broadcast(event, data) {
520
628
  console.error(`[SSE] broadcast relay to port ${activePort} failed: ${err.message}`);
521
629
  });
522
630
  }
631
+ // Push to remote relay server (fire-and-forget)
632
+ if (RELAY_URL && RELAY_PUSH_TOKEN) {
633
+ fetch(`${RELAY_URL}/push`, {
634
+ method: 'POST',
635
+ headers: {
636
+ 'Content-Type': 'application/json',
637
+ 'Authorization': `Bearer ${RELAY_PUSH_TOKEN}`,
638
+ },
639
+ body: JSON.stringify({ event, data }),
640
+ }).catch(() => { }); // silent — relay may be down
641
+ }
523
642
  }
524
643
  /** Returns the port the SSE server is actively using */
525
644
  export function getActivePort() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dalmasonto/taskflow-mcp",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
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",
@@ -38,6 +38,7 @@
38
38
  "dependencies": {
39
39
  "@modelcontextprotocol/sdk": "latest",
40
40
  "better-sqlite3": "latest",
41
+ "dotenv": "^17.4.1",
41
42
  "zod": "latest"
42
43
  },
43
44
  "devDependencies": {