@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 +2 -0
- package/dist/config.js +10 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/dist/sse.d.ts +1 -0
- package/dist/sse.js +69 -13
- package/package.json +2 -1
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
|
-
|
|
2
|
+
import 'dotenv/config';
|
package/dist/index.js
CHANGED
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
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
129
|
+
for (const [name] of promptState) {
|
|
104
130
|
if (!agents.some(a => a.name === name)) {
|
|
105
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
565
|
+
pollAgentTerminals(promptState, contentHashes);
|
|
538
566
|
}
|
|
539
567
|
catch { /* ignore */ }
|
|
540
|
-
},
|
|
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.
|
|
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": {
|