@dalmasonto/taskflow-mcp 1.0.6 → 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 +38 -1
- package/dist/agent-registry.d.ts +23 -0
- package/dist/agent-registry.js +107 -0
- package/dist/db.js +79 -0
- package/dist/index.js +85 -0
- package/dist/sse.js +74 -1
- package/dist/tools/activity.d.ts +1 -0
- package/dist/tools/activity.js +7 -3
- package/dist/tools/agent-inbox.d.ts +5 -0
- package/dist/tools/agent-inbox.js +103 -0
- package/dist/tools/agent.js +22 -5
- package/dist/types.d.ts +16 -1
- package/dist/types.js +4 -0
- package/package.json +1 -1
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
|
@@ -33,6 +33,14 @@ function cleanupOrphanedSessions() {
|
|
|
33
33
|
cleanupOrphanedSessions();
|
|
34
34
|
// Always start the HTTP/SSE server (probes for existing instances, finds fallback port)
|
|
35
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.js
CHANGED
|
@@ -86,7 +86,9 @@ export async function startSSEServer() {
|
|
|
86
86
|
const sessions = db.prepare('SELECT * FROM sessions').all();
|
|
87
87
|
const settings = db.prepare('SELECT * FROM settings').all();
|
|
88
88
|
const activityLogs = db.prepare('SELECT * FROM activity_logs ORDER BY created_at DESC LIMIT 200').all();
|
|
89
|
-
|
|
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 });
|
|
90
92
|
return;
|
|
91
93
|
}
|
|
92
94
|
// ─── Mutation endpoints ───────────────────────────────────────────
|
|
@@ -97,6 +99,8 @@ export async function startSSEServer() {
|
|
|
97
99
|
db.exec('DELETE FROM projects');
|
|
98
100
|
db.exec('DELETE FROM notifications');
|
|
99
101
|
db.exec('DELETE FROM activity_logs');
|
|
102
|
+
db.exec('DELETE FROM agent_messages');
|
|
103
|
+
db.exec('DELETE FROM agent_registry');
|
|
100
104
|
logActivity('data_cleared', 'All data cleared via UI', { entityType: 'system' });
|
|
101
105
|
broadcast('data_cleared', { entity: 'system', action: 'data_cleared', payload: {} });
|
|
102
106
|
jsonResponse(res, 200, { cleared: true });
|
|
@@ -262,6 +266,75 @@ export async function startSSEServer() {
|
|
|
262
266
|
jsonResponse(res, 200, { relayed: true });
|
|
263
267
|
return;
|
|
264
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
|
+
}
|
|
265
338
|
res.writeHead(404);
|
|
266
339
|
res.end('Not Found');
|
|
267
340
|
});
|
package/dist/tools/activity.d.ts
CHANGED
package/dist/tools/activity.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
+
}
|
package/dist/tools/agent.js
CHANGED
|
@@ -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
|
-
'
|
|
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
|
-
//
|
|
37
|
-
'ALWAYS
|
|
38
|
-
'
|
|
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'],
|