@dalmasonto/taskflow-mcp 1.0.17 → 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;
@@ -71,17 +73,42 @@ function detectPrompt(content) {
71
73
  const detected = PROMPT_PATTERNS.some(p => p.test(tail));
72
74
  return { detected, hints };
73
75
  }
74
- function pollAgentPrompts(prevState) {
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) {
75
91
  const db = getDb();
76
92
  const agents = db.prepare("SELECT name, tmux_pane, status, pid FROM agent_registry WHERE status = 'connected' AND tmux_pane IS NOT NULL").all();
77
93
  for (const agent of agents) {
78
94
  try {
79
- const output = execFileSync('tmux', ['capture-pane', '-p', '-S', '-', '-t', agent.tmux_pane], { timeout: 5000 }).toString();
80
- const { detected, hints } = detectPrompt(output);
81
- const wasAwaiting = prevState.get(agent.name) ?? false;
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;
82
110
  if (detected && !wasAwaiting) {
83
- // Transition: not awaiting → awaiting — fire notification + broadcast
84
- prevState.set(agent.name, true);
111
+ promptState.set(agent.name, true);
85
112
  const ts = new Date().toISOString();
86
113
  const hintsText = hints.length > 0 ? ` (${hints.join(' · ')})` : '';
87
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);
@@ -90,8 +117,7 @@ function pollAgentPrompts(prevState) {
90
117
  broadcast('agent_awaiting_input', { entity: 'agent', action: 'agent_awaiting_input', payload: { name: agent.name, hints, awaiting: true } });
91
118
  }
92
119
  else if (!detected && wasAwaiting) {
93
- // Transition: awaiting → resolved
94
- prevState.set(agent.name, false);
120
+ promptState.set(agent.name, false);
95
121
  broadcast('agent_input_resolved', { entity: 'agent', action: 'agent_input_resolved', payload: { name: agent.name, awaiting: false } });
96
122
  }
97
123
  }
@@ -100,9 +126,10 @@ function pollAgentPrompts(prevState) {
100
126
  }
101
127
  }
102
128
  // Clean up agents that disconnected
103
- for (const [name] of prevState) {
129
+ for (const [name] of promptState) {
104
130
  if (!agents.some(a => a.name === name)) {
105
- prevState.delete(name);
131
+ promptState.delete(name);
132
+ contentHashes.delete(name);
106
133
  }
107
134
  }
108
135
  }
@@ -530,20 +557,37 @@ export async function startSSEServer() {
530
557
  }
531
558
  }
532
559
  }, 30_000);
533
- // Prompt detection — poll connected agents for input-required state
560
+ // Terminal capture & prompt detection — single capture loop for all agents
534
561
  const promptState = new Map();
562
+ const contentHashes = new Map();
535
563
  setInterval(() => {
536
564
  try {
537
- pollAgentPrompts(promptState);
565
+ pollAgentTerminals(promptState, contentHashes);
538
566
  }
539
567
  catch { /* ignore */ }
540
- }, 5_000);
568
+ }, 3_000);
541
569
  if (port !== PREFERRED_PORT) {
542
570
  console.log(`[SSE] listening on fallback port ${port} (preferred ${PREFERRED_PORT} was unavailable)`);
543
571
  }
544
572
  else {
545
573
  console.log(`[SSE] listening on port ${port}`);
546
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
+ }
547
591
  resolve();
548
592
  });
549
593
  }
@@ -567,6 +611,7 @@ export function markSSEActive() {
567
611
  /**
568
612
  * Broadcast an SSE event. If this process owns the SSE server, send directly.
569
613
  * Otherwise, relay via HTTP to the process that does.
614
+ * Also pushes to the remote relay server if configured.
570
615
  */
571
616
  export function broadcast(event, data) {
572
617
  if (sseServerActive && clients.size > 0) {
@@ -583,6 +628,17 @@ export function broadcast(event, data) {
583
628
  console.error(`[SSE] broadcast relay to port ${activePort} failed: ${err.message}`);
584
629
  });
585
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
+ }
586
642
  }
587
643
  /** Returns the port the SSE server is actively using */
588
644
  export function getActivePort() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dalmasonto/taskflow-mcp",
3
- "version": "1.0.17",
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": {