@dalmasonto/taskflow-mcp 1.0.18 → 1.0.19

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.
@@ -57,8 +57,12 @@ export function registerAgent(options) {
57
57
  checkAgentLiveness();
58
58
  // Find all entries for this project path
59
59
  const entries = db.prepare('SELECT * FROM agent_registry WHERE project_path = ? ORDER BY connected_at DESC').all(projectPath);
60
- // Priority 1: Reuse a disconnected entry (preserves history)
61
- const disconnected = entries.find(e => e.status === 'disconnected');
60
+ // Priority 1: Reuse a disconnected entry that had the SAME PID (exact session resume)
61
+ // Priority 2: Reuse any disconnected entry for this path (new session, same project)
62
+ // This prevents a reconnecting agent from stealing another agent's identity
63
+ // when two sessions share the same project directory.
64
+ const disconnected = entries.find(e => e.status === 'disconnected' && e.pid === agentPid)
65
+ || entries.find(e => e.status === 'disconnected');
62
66
  if (disconnected) {
63
67
  const newName = options?.customName || disconnected.name;
64
68
  const renamed = newName !== disconnected.name;
package/dist/config.js CHANGED
@@ -24,7 +24,14 @@ const DEFAULTS = {
24
24
  relayPushToken: '',
25
25
  };
26
26
  // ─── config file path ────────────────────────────────────────────────
27
- const CONFIG_PATH = resolve(homedir(), '.taskflow_config.json');
27
+ // Config lives alongside the database in ~/.taskflow/
28
+ // Falls back to legacy ~/.taskflow_config.json for backward compat
29
+ import { existsSync } from 'fs';
30
+ const NEW_CONFIG_PATH = resolve(homedir(), '.taskflow', 'config.json');
31
+ const LEGACY_CONFIG_PATH = resolve(homedir(), '.taskflow_config.json');
32
+ const CONFIG_PATH = existsSync(NEW_CONFIG_PATH) ? NEW_CONFIG_PATH
33
+ : existsSync(LEGACY_CONFIG_PATH) ? LEGACY_CONFIG_PATH
34
+ : NEW_CONFIG_PATH; // default to new path for fresh installs
28
35
  // ─── loader ──────────────────────────────────────────────────────────
29
36
  function loadFromFile() {
30
37
  try {
package/dist/index.js CHANGED
@@ -140,8 +140,10 @@ if (!httpOnly) {
140
140
  const transport = new StdioServerTransport();
141
141
  await server.connect(transport);
142
142
  const { registerAgent, unregisterAgent } = await import('./agent-registry.js');
143
- // Auto-register this agent
143
+ const { setAgentName } = await import('./tools/agent-inbox.js');
144
+ // Auto-register this agent and sync name to agent-inbox tools
144
145
  const agentName = registerAgent();
146
+ setAgentName(agentName);
145
147
  const agentPid = process.ppid;
146
148
  console.error(`[agent] registered as "${agentName}"`);
147
149
  let cleanup = () => { try {
package/dist/sse.js CHANGED
@@ -3,6 +3,7 @@ import { execFileSync } from 'child_process';
3
3
  import { getDb } from './db.js';
4
4
  import { logActivity } from './helpers.js';
5
5
  import { getConfig } from './config.js';
6
+ import { validateKeys } from './tools/terminal.js';
6
7
  const SERVICE_ID = 'taskflow-mcp';
7
8
  const PROBE_TIMEOUT_MS = 2000;
8
9
  // ─── Relay upstream config (from config file, env vars, or .env) ────
@@ -53,24 +54,32 @@ function readBody(req) {
53
54
  req.on('end', () => resolve(body));
54
55
  });
55
56
  }
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
- ];
57
+ /**
58
+ * Detect an active Claude Code permission prompt.
59
+ *
60
+ * The definitive marker is "Esc to cancel" which only appears in the
61
+ * footer of active prompts and disappears once the user responds.
62
+ * We check only the last 10 lines of the VISIBLE screen (not full
63
+ * scrollback) to avoid matching old, already-answered prompts.
64
+ *
65
+ * Generic patterns like "Do you want to" and "(y/n)" are intentionally
66
+ * excluded — Claude writes these in prose responses too, causing false flags.
67
+ */
64
68
  function detectPrompt(content) {
65
- const tail = content.split('\n').slice(-25).join('\n');
69
+ // Only check last 10 lines — the prompt footer is always at the bottom
70
+ const lines = content.split('\n');
71
+ const tail = lines.slice(-10).join('\n');
66
72
  const hints = [];
67
- if (/Esc to cancel/i.test(tail))
73
+ const hasEsc = /Esc to cancel/i.test(tail);
74
+ const hasTab = /Tab to amend/i.test(tail);
75
+ if (hasEsc)
68
76
  hints.push('Esc to cancel');
69
- if (/Tab to amend/i.test(tail))
77
+ if (hasTab)
70
78
  hints.push('Tab to amend');
71
79
  if (/shift\+tab/i.test(tail))
72
80
  hints.push('Shift+Tab');
73
- const detected = PROMPT_PATTERNS.some(p => p.test(tail));
81
+ // Only flag as detected if we see the actual prompt footer
82
+ const detected = hasEsc || hasTab;
74
83
  return { detected, hints };
75
84
  }
76
85
  /** Fast string hash (djb2) for content change detection */
@@ -466,6 +475,10 @@ export async function startSSEServer() {
466
475
  jsonResponse(res, 400, { error: `Agent "${agentName}" has no tmux pane` });
467
476
  return;
468
477
  }
478
+ if (agent.status !== 'connected') {
479
+ jsonResponse(res, 403, { error: `Agent "${agentName}" is disconnected — sending keys to a bare shell is blocked for security` });
480
+ return;
481
+ }
469
482
  let body;
470
483
  try {
471
484
  body = JSON.parse(await readBody(req));
@@ -481,6 +494,12 @@ export async function startSSEServer() {
481
494
  jsonResponse(res, 400, { error: 'keys must be a string' });
482
495
  return;
483
496
  }
497
+ // Block shell escape patterns and oversized payloads
498
+ const violation = validateKeys(keys);
499
+ if (violation) {
500
+ jsonResponse(res, 403, { error: violation });
501
+ return;
502
+ }
484
503
  try {
485
504
  const pane = agent.tmux_pane;
486
505
  if (literal) {
@@ -582,8 +601,14 @@ export async function startSSEServer() {
582
601
  'Authorization': `Bearer ${RELAY_PUSH_TOKEN}`,
583
602
  },
584
603
  body: JSON.stringify({ url: localUrl }),
585
- }).then(() => {
586
- console.log(`[SSE] registered with relay at ${RELAY_URL}`);
604
+ }).then(async (res) => {
605
+ if (res.ok) {
606
+ console.log(`[SSE] registered with relay at ${RELAY_URL}`);
607
+ }
608
+ else {
609
+ const body = await res.text().catch(() => '');
610
+ console.error(`[SSE] relay registration rejected: ${res.status} ${body}`);
611
+ }
587
612
  }).catch((err) => {
588
613
  console.error(`[SSE] relay registration failed: ${err.message}`);
589
614
  });
@@ -1,5 +1,10 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  /** The name assigned to this agent after registration */
3
3
  declare let myAgentName: string | null;
4
+ /** Set the agent name from external registration (index.ts).
5
+ * This prevents double-registration when index.ts registers at startup
6
+ * and ensureRegistered() would register again on first tool call.
7
+ */
8
+ export declare function setAgentName(name: string): void;
4
9
  export { myAgentName };
5
10
  export declare function registerAgentInboxTools(server: McpServer): void;
@@ -4,11 +4,28 @@ import { logActivity, errorResponse, successResponse, now, broadcastChange } fro
4
4
  import { registerAgent as doRegister, getAgent, listAgents } from '../agent-registry.js';
5
5
  /** The name assigned to this agent after registration */
6
6
  let myAgentName = null;
7
- /** Get or auto-register the agent name */
7
+ /** Set the agent name from external registration (index.ts).
8
+ * This prevents double-registration when index.ts registers at startup
9
+ * and ensureRegistered() would register again on first tool call.
10
+ */
11
+ export function setAgentName(name) {
12
+ myAgentName = name;
13
+ }
14
+ /** Get or auto-register the agent name.
15
+ * Verifies the cached registration is still valid — if the liveness
16
+ * checker disconnected us while sleeping, re-register to reclaim identity.
17
+ */
8
18
  function ensureRegistered() {
9
- if (!myAgentName) {
10
- myAgentName = doRegister();
19
+ if (myAgentName) {
20
+ // Verify our registration is still active
21
+ const agent = getAgent(myAgentName);
22
+ if (agent && agent.status === 'connected' && agent.pid === process.ppid) {
23
+ return myAgentName;
24
+ }
25
+ // Stale — re-register
26
+ myAgentName = null;
11
27
  }
28
+ myAgentName = doRegister();
12
29
  return myAgentName;
13
30
  }
14
31
  export { myAgentName };
@@ -1,2 +1,4 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /** Validate keys before sending — blocks shell escape patterns */
3
+ export declare function validateKeys(keys: string): string | null;
2
4
  export declare function registerTerminalTools(server: McpServer): void;
@@ -2,13 +2,34 @@ import { z } from 'zod';
2
2
  import { execFileSync } from 'child_process';
3
3
  import { getDb } from '../db.js';
4
4
  import { logActivity, errorResponse, successResponse } from '../helpers.js';
5
- function getAgentPane(agentName) {
5
+ const MAX_KEYS_LENGTH = 200;
6
+ // Patterns that should never be sent via remote key injection
7
+ const BLOCKED_PATTERNS = [
8
+ /^\s*!/, // Claude Code shell escape (! command)
9
+ /;\s*!/, // shell escape after semicolon
10
+ ];
11
+ /** Validate keys before sending — blocks shell escape patterns */
12
+ export function validateKeys(keys) {
13
+ if (keys.length > MAX_KEYS_LENGTH) {
14
+ return `Keys too long (${keys.length} chars, max ${MAX_KEYS_LENGTH}) — potential injection`;
15
+ }
16
+ for (const pattern of BLOCKED_PATTERNS) {
17
+ if (pattern.test(keys)) {
18
+ return 'Blocked: shell escape pattern detected (! prefix). Use the terminal directly for shell commands.';
19
+ }
20
+ }
21
+ return null; // valid
22
+ }
23
+ function getAgentPane(agentName, requireConnected = false) {
6
24
  const db = getDb();
7
25
  const agent = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(agentName);
8
26
  if (!agent)
9
27
  return { error: errorResponse(`Agent "${agentName}" not found`, 'NOT_FOUND') };
10
28
  if (!agent.tmux_pane)
11
29
  return { error: errorResponse(`Agent "${agentName}" has no tmux pane`, 'VALIDATION_ERROR') };
30
+ if (requireConnected && agent.status !== 'connected') {
31
+ return { error: errorResponse(`Agent "${agentName}" is disconnected — sending keys to a bare shell is blocked for security`, 'VALIDATION_ERROR') };
32
+ }
12
33
  return { agent };
13
34
  }
14
35
  export function registerTerminalTools(server) {
@@ -36,9 +57,13 @@ export function registerTerminalTools(server) {
36
57
  enter: z.boolean().optional().describe('Whether to press Enter after the keys (default: true)'),
37
58
  literal: z.boolean().optional().describe('Send as literal text with -l flag (default: true). Set false for tmux key names like Escape, Up, Down, Left, Right, BTab'),
38
59
  }, { destructiveHint: true }, async (params) => {
39
- const result = getAgentPane(params.agent_name);
60
+ const result = getAgentPane(params.agent_name, true);
40
61
  if (result.error)
41
62
  return result.error;
63
+ // Validate keys for injection attacks
64
+ const violation = validateKeys(params.keys);
65
+ if (violation)
66
+ return errorResponse(violation, 'VALIDATION_ERROR');
42
67
  const sendEnter = params.enter !== false;
43
68
  const isLiteral = params.literal !== false;
44
69
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dalmasonto/taskflow-mcp",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
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",