@dalmasonto/taskflow-mcp 1.0.35 → 2.1.0
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 +138 -193
- package/dist/attachment-download.d.ts +74 -0
- package/dist/attachment-download.js +193 -0
- package/dist/attachment-download.js.map +1 -0
- package/dist/attachments.d.ts +23 -0
- package/dist/attachments.js +66 -0
- package/dist/attachments.js.map +1 -0
- package/dist/client.d.ts +206 -0
- package/dist/client.js +301 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +137 -18
- package/dist/config.js +187 -106
- package/dist/config.js.map +1 -0
- package/dist/connect.d.ts +89 -0
- package/dist/connect.js +269 -0
- package/dist/connect.js.map +1 -0
- package/dist/doctor.d.ts +24 -0
- package/dist/doctor.js +120 -0
- package/dist/doctor.js.map +1 -0
- package/dist/events.d.ts +208 -0
- package/dist/events.js +454 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +121 -218
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.js +114 -0
- package/dist/instructions.js.map +1 -0
- package/dist/mint.d.ts +62 -0
- package/dist/mint.js +135 -0
- package/dist/mint.js.map +1 -0
- package/dist/mirror.d.ts +68 -0
- package/dist/mirror.js +103 -0
- package/dist/mirror.js.map +1 -0
- package/dist/pane-queue.d.ts +29 -0
- package/dist/pane-queue.js +35 -0
- package/dist/pane-queue.js.map +1 -0
- package/dist/prompts.d.ts +79 -0
- package/dist/prompts.js +211 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resolve.d.ts +72 -0
- package/dist/resolve.js +89 -0
- package/dist/resolve.js.map +1 -0
- package/dist/runtime.d.ts +54 -0
- package/dist/runtime.js +339 -0
- package/dist/runtime.js.map +1 -0
- package/dist/server.d.ts +56 -0
- package/dist/server.js +793 -0
- package/dist/server.js.map +1 -0
- package/dist/session-identifier.d.ts +48 -0
- package/dist/session-identifier.js +44 -0
- package/dist/session-identifier.js.map +1 -0
- package/dist/sessions-store.d.ts +38 -0
- package/dist/sessions-store.js +88 -0
- package/dist/sessions-store.js.map +1 -0
- package/dist/tmux.d.ts +200 -0
- package/dist/tmux.js +580 -0
- package/dist/tmux.js.map +1 -0
- package/hooks/metadata.mjs +99 -0
- package/hooks/permission-prompt.mjs +100 -0
- package/hooks/taskflow-hook.mjs +499 -0
- package/hooks/tool-logging.mjs +63 -0
- package/package.json +38 -29
- package/dist/agent-registry.d.ts +0 -28
- package/dist/agent-registry.js +0 -158
- package/dist/db.d.ts +0 -5
- package/dist/db.js +0 -220
- package/dist/helpers.d.ts +0 -21
- package/dist/helpers.js +0 -27
- package/dist/resources.d.ts +0 -2
- package/dist/resources.js +0 -89
- package/dist/retry.d.ts +0 -34
- package/dist/retry.js +0 -94
- package/dist/sse.d.ts +0 -10
- package/dist/sse.js +0 -824
- package/dist/tmux-bridge.d.ts +0 -13
- package/dist/tmux-bridge.js +0 -217
- package/dist/tools/activity.d.ts +0 -39
- package/dist/tools/activity.js +0 -152
- package/dist/tools/agent-inbox.d.ts +0 -12
- package/dist/tools/agent-inbox.js +0 -272
- package/dist/tools/agent.d.ts +0 -14
- package/dist/tools/agent.js +0 -168
- package/dist/tools/analytics.d.ts +0 -21
- package/dist/tools/analytics.js +0 -191
- package/dist/tools/checkpoint.d.ts +0 -27
- package/dist/tools/checkpoint.js +0 -105
- package/dist/tools/notifications.d.ts +0 -31
- package/dist/tools/notifications.js +0 -59
- package/dist/tools/projects.d.ts +0 -55
- package/dist/tools/projects.js +0 -112
- package/dist/tools/settings.d.ts +0 -19
- package/dist/tools/settings.js +0 -73
- package/dist/tools/tasks.d.ts +0 -105
- package/dist/tools/tasks.js +0 -403
- package/dist/tools/terminal.d.ts +0 -4
- package/dist/tools/terminal.js +0 -98
- package/dist/tools/timer.d.ts +0 -37
- package/dist/tools/timer.js +0 -154
- package/dist/types.d.ts +0 -83
- package/dist/types.js +0 -30
package/dist/tmux-bridge.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
interface BridgeOptions {
|
|
2
|
-
/** Returns the current agent name — must be a getter so renames are picked up */
|
|
3
|
-
getAgentName: () => string;
|
|
4
|
-
agentPid: number;
|
|
5
|
-
/** Null when not running inside tmux — falls back to stderr delivery */
|
|
6
|
-
tmuxPane: string | null;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Start the tmux bridge: SSE listener for instant message delivery.
|
|
10
|
-
* Returns a cleanup function for graceful shutdown.
|
|
11
|
-
*/
|
|
12
|
-
export declare function startTmuxBridge(options: BridgeOptions): () => void;
|
|
13
|
-
export {};
|
package/dist/tmux-bridge.js
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'child_process';
|
|
2
|
-
import { appendFileSync, mkdirSync } from 'fs';
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { getDb } from './db.js';
|
|
6
|
-
import { getActivePort } from './sse.js';
|
|
7
|
-
import http from 'http';
|
|
8
|
-
const LOG_FILE = join(homedir(), '.taskflow', 'bridge.log');
|
|
9
|
-
try {
|
|
10
|
-
mkdirSync(join(homedir(), '.taskflow'), { recursive: true });
|
|
11
|
-
}
|
|
12
|
-
catch { }
|
|
13
|
-
function blogLog(msg) {
|
|
14
|
-
const line = `${new Date().toISOString()} ${msg}\n`;
|
|
15
|
-
console.error(`[bridge] ${msg}`);
|
|
16
|
-
try {
|
|
17
|
-
appendFileSync(LOG_FILE, line);
|
|
18
|
-
}
|
|
19
|
-
catch { }
|
|
20
|
-
}
|
|
21
|
-
// ─── SSE Listener (replaces the 3s poller) ───────────────────────────
|
|
22
|
-
function startSSEListener(options) {
|
|
23
|
-
const port = getActivePort();
|
|
24
|
-
function connect() {
|
|
25
|
-
const req = http.get(`http://localhost:${port}/events`, (res) => {
|
|
26
|
-
let buffer = '';
|
|
27
|
-
let eventType = ''; // persists across chunks — SSE fields may split across TCP segments
|
|
28
|
-
res.on('data', (chunk) => {
|
|
29
|
-
buffer += chunk.toString();
|
|
30
|
-
const lines = buffer.split('\n');
|
|
31
|
-
buffer = lines.pop() || '';
|
|
32
|
-
for (const line of lines) {
|
|
33
|
-
if (line.startsWith('event: ')) {
|
|
34
|
-
eventType = line.slice(7).trim();
|
|
35
|
-
}
|
|
36
|
-
else if (line.startsWith('data: ') && eventType) {
|
|
37
|
-
try {
|
|
38
|
-
const data = JSON.parse(line.slice(6));
|
|
39
|
-
handleSSEEvent(eventType, data, options);
|
|
40
|
-
}
|
|
41
|
-
catch { /* malformed JSON */ }
|
|
42
|
-
eventType = '';
|
|
43
|
-
}
|
|
44
|
-
else if (line === '') {
|
|
45
|
-
// SSE event delimiter — reset state for next event
|
|
46
|
-
eventType = '';
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
res.on('end', () => {
|
|
51
|
-
blogLog('SSE connection closed, reconnecting in 3s...');
|
|
52
|
-
setTimeout(connect, 3000);
|
|
53
|
-
});
|
|
54
|
-
res.on('error', () => {
|
|
55
|
-
blogLog('SSE connection error, reconnecting in 3s...');
|
|
56
|
-
setTimeout(connect, 3000);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
req.on('error', () => {
|
|
60
|
-
blogLog('SSE connect failed, retrying in 3s...');
|
|
61
|
-
setTimeout(connect, 3000);
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
// Initial sweep: deliver any undelivered messages from before SSE connected
|
|
65
|
-
deliverUndelivered(options);
|
|
66
|
-
connect();
|
|
67
|
-
}
|
|
68
|
-
function handleSSEEvent(event, data, options) {
|
|
69
|
-
const { getAgentName, agentPid, tmuxPane } = options;
|
|
70
|
-
const agentName = getAgentName();
|
|
71
|
-
const payload = data.payload;
|
|
72
|
-
if (!payload)
|
|
73
|
-
return;
|
|
74
|
-
if (event === 'agent_question') {
|
|
75
|
-
const recipient = payload.recipient_name;
|
|
76
|
-
const sender = payload.sender_name;
|
|
77
|
-
const status = payload.status;
|
|
78
|
-
const id = payload.id;
|
|
79
|
-
const delivered = payload.delivered;
|
|
80
|
-
if (recipient !== agentName || status !== 'pending' || delivered === 1)
|
|
81
|
-
return;
|
|
82
|
-
let text;
|
|
83
|
-
if (sender === 'user') {
|
|
84
|
-
text = `[Message from User]: ${payload.question}`;
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
text = `[Message from ${sender}]: ${payload.question}`;
|
|
88
|
-
}
|
|
89
|
-
injectAndMarkDelivered(id, text, tmuxPane);
|
|
90
|
-
}
|
|
91
|
-
if (event === 'agent_question_answered') {
|
|
92
|
-
const sender = payload.sender_name;
|
|
93
|
-
const recipient = payload.recipient_name;
|
|
94
|
-
const status = payload.status;
|
|
95
|
-
const id = payload.id;
|
|
96
|
-
const delivered = payload.delivered;
|
|
97
|
-
const agentPidField = payload.agent_pid;
|
|
98
|
-
const isOurs = sender === agentName || agentPidField === agentPid;
|
|
99
|
-
if (!isOurs || status !== 'answered' || delivered === 1)
|
|
100
|
-
return;
|
|
101
|
-
const question = (payload.question || '').slice(0, 60);
|
|
102
|
-
const response = payload.response;
|
|
103
|
-
const text = `[Inbox Response] to "${question}": ${response}`;
|
|
104
|
-
injectAndMarkDelivered(id, text, tmuxPane);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// TIOCSTI Python script — injects each character of `text` + newline into the
|
|
108
|
-
// controlling terminal's input queue via ioctl(TIOCSTI). Passed as a CLI arg
|
|
109
|
-
// (not inline) so special characters in the message are never shell-interpreted.
|
|
110
|
-
const TIOCSTI_SCRIPT = `
|
|
111
|
-
import fcntl, sys
|
|
112
|
-
TIOCSTI = 0x5412
|
|
113
|
-
with open('/dev/tty', 'rb+', buffering=0) as tty:
|
|
114
|
-
for c in (sys.argv[1] + '\\n').encode():
|
|
115
|
-
fcntl.ioctl(tty, TIOCSTI, bytes([c]))
|
|
116
|
-
`.trim();
|
|
117
|
-
function injectViaTiocsti(text) {
|
|
118
|
-
try {
|
|
119
|
-
execFileSync('python3', ['-c', TIOCSTI_SCRIPT, text], { timeout: 5000 });
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
// Capture stderr from the Python process (where the real error lives)
|
|
124
|
-
const stderr = err?.stderr?.toString().trim() || '';
|
|
125
|
-
const stdout = err?.stdout?.toString().trim() || '';
|
|
126
|
-
const msg = err?.message || String(err);
|
|
127
|
-
return [msg, stderr, stdout].filter(Boolean).join(' | ');
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
function injectAndMarkDelivered(id, text, tmuxPane) {
|
|
131
|
-
const db = getDb();
|
|
132
|
-
db.prepare('UPDATE agent_messages SET delivered = 1 WHERE id = ?').run(id);
|
|
133
|
-
if (tmuxPane) {
|
|
134
|
-
try {
|
|
135
|
-
// Send text literally (-l) so special chars are safe and tmux doesn't interpret them
|
|
136
|
-
execFileSync('tmux', ['send-keys', '-t', tmuxPane, '-l', text], { stdio: 'ignore', timeout: 5000 });
|
|
137
|
-
// Delay before Enter — gives the CLI time to fully process the bracketed paste.
|
|
138
|
-
// Without this, Codex (and similar TUIs) may only show partial text because
|
|
139
|
-
// the Enter arrives inside the paste bracket and gets swallowed.
|
|
140
|
-
setTimeout(() => {
|
|
141
|
-
try {
|
|
142
|
-
execFileSync('tmux', ['send-keys', '-t', tmuxPane, 'Enter'], { stdio: 'ignore', timeout: 5000 });
|
|
143
|
-
}
|
|
144
|
-
catch { /* pane may have closed */ }
|
|
145
|
-
// Second Enter after another delay — catches CLIs that need an extra nudge
|
|
146
|
-
// after bracketed paste ends (e.g. long messages that trigger paste mode)
|
|
147
|
-
setTimeout(() => {
|
|
148
|
-
try {
|
|
149
|
-
execFileSync('tmux', ['send-keys', '-t', tmuxPane, 'Enter'], { stdio: 'ignore', timeout: 5000 });
|
|
150
|
-
}
|
|
151
|
-
catch { /* pane may have closed */ }
|
|
152
|
-
}, 500);
|
|
153
|
-
}, 300);
|
|
154
|
-
blogLog(`delivered message ${id} to tmux pane ${tmuxPane}`);
|
|
155
|
-
}
|
|
156
|
-
catch (err) {
|
|
157
|
-
blogLog(`tmux send-keys failed for message ${id}: ${err}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
// Non-tmux: try TIOCSTI to inject text into the controlling terminal's input
|
|
162
|
-
// queue. TIOCSTI (0x5412) works when the caller shares the controlling terminal
|
|
163
|
-
// with the target process — which is true here since the MCP server is a child
|
|
164
|
-
// of Claude Code and inherits its tty session.
|
|
165
|
-
// Falls back to stderr if python3 is unavailable or the ioctl is denied.
|
|
166
|
-
const tiocError = injectViaTiocsti(text);
|
|
167
|
-
if (tiocError !== null) {
|
|
168
|
-
blogLog(`TIOCSTI failed for message ${id}: ${tiocError}`);
|
|
169
|
-
// Last-resort visual: at least make the message visible in the terminal output.
|
|
170
|
-
// The tool-response piggyback in index.ts will also deliver it on the next call.
|
|
171
|
-
process.stderr.write(`\n\x1b[33m[INBOX #${id}]: ${text}\x1b[0m\n`);
|
|
172
|
-
blogLog(`message ${id} visible in stderr, will appear in next tool response`);
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
blogLog(`delivered message ${id} via TIOCSTI`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
function deliverUndelivered(options) {
|
|
180
|
-
const { getAgentName, agentPid, tmuxPane } = options;
|
|
181
|
-
const agentName = getAgentName();
|
|
182
|
-
const db = getDb();
|
|
183
|
-
const incoming = db.prepare(`SELECT * FROM agent_messages WHERE delivered IS NULL AND (
|
|
184
|
-
(recipient_name = ? AND status = 'pending') OR
|
|
185
|
-
(sender_name = ? AND status = 'answered') OR
|
|
186
|
-
(agent_pid = ? AND status = 'answered')
|
|
187
|
-
)`).all(agentName, agentName, agentPid);
|
|
188
|
-
for (const msg of incoming) {
|
|
189
|
-
let text;
|
|
190
|
-
if (msg.recipient_name === agentName && msg.sender_name === 'user') {
|
|
191
|
-
text = `[Message from User]: ${msg.question}`;
|
|
192
|
-
}
|
|
193
|
-
else if (msg.recipient_name === agentName && msg.sender_name !== 'user') {
|
|
194
|
-
text = `[Message from ${msg.sender_name}]: ${msg.question}`;
|
|
195
|
-
}
|
|
196
|
-
else if (msg.status === 'answered' && msg.response) {
|
|
197
|
-
text = `[Inbox Response] to "${msg.question.slice(0, 60)}": ${msg.response}`;
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
injectAndMarkDelivered(msg.id, text, tmuxPane);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
// ─── Public API ──────────────────────────────────────────────────────
|
|
206
|
-
/**
|
|
207
|
-
* Start the tmux bridge: SSE listener for instant message delivery.
|
|
208
|
-
* Returns a cleanup function for graceful shutdown.
|
|
209
|
-
*/
|
|
210
|
-
export function startTmuxBridge(options) {
|
|
211
|
-
startSSEListener(options);
|
|
212
|
-
const pane = options.tmuxPane;
|
|
213
|
-
blogLog(`bridge active for agent "${options.getAgentName()}"${pane ? ` on tmux pane ${pane}` : ' (TIOCSTI mode — no tmux pane)'}`);
|
|
214
|
-
return () => {
|
|
215
|
-
// SSE connection will close when process exits
|
|
216
|
-
};
|
|
217
|
-
}
|
package/dist/tools/activity.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
export declare function getActivityLog(params: {
|
|
3
|
-
limit?: number;
|
|
4
|
-
action?: string;
|
|
5
|
-
entity_type?: string;
|
|
6
|
-
}): Promise<{
|
|
7
|
-
content: {
|
|
8
|
-
type: "text";
|
|
9
|
-
text: string;
|
|
10
|
-
}[];
|
|
11
|
-
}>;
|
|
12
|
-
export declare function clearActivityLog(): Promise<{
|
|
13
|
-
content: {
|
|
14
|
-
type: "text";
|
|
15
|
-
text: string;
|
|
16
|
-
}[];
|
|
17
|
-
}>;
|
|
18
|
-
export declare function compactActivityLog(params: {
|
|
19
|
-
preserve_recent?: number;
|
|
20
|
-
max_entries?: number;
|
|
21
|
-
dry_run?: boolean;
|
|
22
|
-
}): Promise<{
|
|
23
|
-
content: {
|
|
24
|
-
type: "text";
|
|
25
|
-
text: string;
|
|
26
|
-
}[];
|
|
27
|
-
}>;
|
|
28
|
-
export declare function logDebug(params: {
|
|
29
|
-
message: string;
|
|
30
|
-
task_id?: number;
|
|
31
|
-
project_id?: number;
|
|
32
|
-
detail?: string;
|
|
33
|
-
}): Promise<{
|
|
34
|
-
content: {
|
|
35
|
-
type: "text";
|
|
36
|
-
text: string;
|
|
37
|
-
}[];
|
|
38
|
-
}>;
|
|
39
|
-
export declare function registerActivityTools(server: McpServer): void;
|
package/dist/tools/activity.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { getDb } from '../db.js';
|
|
3
|
-
import { successResponse, broadcastChange } from '../helpers.js';
|
|
4
|
-
// ─── exported handler functions ───────────────────────────────────────
|
|
5
|
-
export async function getActivityLog(params) {
|
|
6
|
-
const db = getDb();
|
|
7
|
-
const limit = params.limit ?? 50;
|
|
8
|
-
const conditions = [];
|
|
9
|
-
const values = [];
|
|
10
|
-
if (params.action) {
|
|
11
|
-
conditions.push('action = ?');
|
|
12
|
-
values.push(params.action);
|
|
13
|
-
}
|
|
14
|
-
if (params.entity_type) {
|
|
15
|
-
conditions.push('entity_type = ?');
|
|
16
|
-
values.push(params.entity_type);
|
|
17
|
-
}
|
|
18
|
-
let sql = 'SELECT * FROM activity_logs';
|
|
19
|
-
if (conditions.length > 0) {
|
|
20
|
-
sql += ' WHERE ' + conditions.join(' AND ');
|
|
21
|
-
}
|
|
22
|
-
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
23
|
-
values.push(limit);
|
|
24
|
-
const rows = db.prepare(sql).all(...values);
|
|
25
|
-
return successResponse(rows);
|
|
26
|
-
}
|
|
27
|
-
export async function clearActivityLog() {
|
|
28
|
-
const db = getDb();
|
|
29
|
-
const countRow = db.prepare('SELECT COUNT(*) AS count FROM activity_logs').get();
|
|
30
|
-
db.prepare('DELETE FROM activity_logs').run();
|
|
31
|
-
broadcastChange('activity', 'activity_cleared', {});
|
|
32
|
-
return successResponse({ deleted: countRow.count, message: 'Activity log cleared' });
|
|
33
|
-
}
|
|
34
|
-
// ─── compaction ──────────────────────────────────────────────────────
|
|
35
|
-
const DEFAULT_PRESERVE_RECENT = 50;
|
|
36
|
-
const DEFAULT_MAX_LOG_ENTRIES = 500;
|
|
37
|
-
/** Actions that are always preserved in full (never compacted) */
|
|
38
|
-
const MILESTONE_ACTIONS = new Set([
|
|
39
|
-
'task_created', 'task_completed', 'task_partial_done',
|
|
40
|
-
'project_created', 'project_deleted',
|
|
41
|
-
'agent_connected', 'agent_disconnected', 'agent_renamed',
|
|
42
|
-
]);
|
|
43
|
-
export async function compactActivityLog(params) {
|
|
44
|
-
const db = getDb();
|
|
45
|
-
const preserveRecent = params.preserve_recent ?? DEFAULT_PRESERVE_RECENT;
|
|
46
|
-
const maxEntries = params.max_entries ?? DEFAULT_MAX_LOG_ENTRIES;
|
|
47
|
-
const totalCount = db.prepare('SELECT COUNT(*) AS count FROM activity_logs').get().count;
|
|
48
|
-
if (totalCount <= maxEntries) {
|
|
49
|
-
return successResponse({
|
|
50
|
-
compacted: false,
|
|
51
|
-
total_entries: totalCount,
|
|
52
|
-
message: `Log has ${totalCount} entries (threshold: ${maxEntries}) — no compaction needed.`,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
// Get all entries sorted by date
|
|
56
|
-
const allEntries = db.prepare('SELECT * FROM activity_logs ORDER BY created_at ASC').all();
|
|
57
|
-
// Split into: entries to compact vs entries to preserve
|
|
58
|
-
const cutoffIndex = allEntries.length - preserveRecent;
|
|
59
|
-
const toCompact = allEntries.slice(0, cutoffIndex);
|
|
60
|
-
const toPreserve = allEntries.slice(cutoffIndex);
|
|
61
|
-
if (toCompact.length === 0) {
|
|
62
|
-
return successResponse({
|
|
63
|
-
compacted: false,
|
|
64
|
-
total_entries: totalCount,
|
|
65
|
-
message: 'All entries are within the preserve window — nothing to compact.',
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
// Generate summary of compacted entries
|
|
69
|
-
const actionCounts = {};
|
|
70
|
-
const milestones = [];
|
|
71
|
-
const timeRange = {
|
|
72
|
-
start: toCompact[0].created_at,
|
|
73
|
-
end: toCompact[toCompact.length - 1].created_at,
|
|
74
|
-
};
|
|
75
|
-
for (const entry of toCompact) {
|
|
76
|
-
actionCounts[entry.action] = (actionCounts[entry.action] || 0) + 1;
|
|
77
|
-
if (MILESTONE_ACTIONS.has(entry.action)) {
|
|
78
|
-
milestones.push(`[${entry.created_at.slice(0, 16)}] ${entry.action}: ${entry.title}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const summaryDetail = [
|
|
82
|
-
`## Compaction Summary`,
|
|
83
|
-
`**Period:** ${timeRange.start} → ${timeRange.end}`,
|
|
84
|
-
`**Entries compacted:** ${toCompact.length}`,
|
|
85
|
-
``,
|
|
86
|
-
`### Action Counts`,
|
|
87
|
-
...Object.entries(actionCounts)
|
|
88
|
-
.sort((a, b) => b[1] - a[1])
|
|
89
|
-
.map(([action, count]) => `- \`${action}\`: ${count}`),
|
|
90
|
-
``,
|
|
91
|
-
`### Key Milestones`,
|
|
92
|
-
...(milestones.length > 0
|
|
93
|
-
? milestones.slice(-20).map(m => `- ${m}`)
|
|
94
|
-
: ['- (none)']),
|
|
95
|
-
].join('\n');
|
|
96
|
-
if (params.dry_run) {
|
|
97
|
-
return successResponse({
|
|
98
|
-
compacted: false,
|
|
99
|
-
dry_run: true,
|
|
100
|
-
would_compact: toCompact.length,
|
|
101
|
-
would_preserve: toPreserve.length,
|
|
102
|
-
summary_preview: summaryDetail,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
// Delete compacted entries and insert summary
|
|
106
|
-
const compactIds = toCompact.map(e => e.id);
|
|
107
|
-
const placeholders = compactIds.map(() => '?').join(',');
|
|
108
|
-
db.prepare(`DELETE FROM activity_logs WHERE id IN (${placeholders})`).run(...compactIds);
|
|
109
|
-
const now = new Date().toISOString();
|
|
110
|
-
db.prepare(`INSERT INTO activity_logs (action, title, detail, entity_type, entity_id, created_at)
|
|
111
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run('compaction_summary', `Compacted ${toCompact.length} activity log entries (${timeRange.start.slice(0, 10)} → ${timeRange.end.slice(0, 10)})`, summaryDetail, null, null, now);
|
|
112
|
-
broadcastChange('activity', 'activity_compacted', { compacted: toCompact.length, preserved: toPreserve.length });
|
|
113
|
-
return successResponse({
|
|
114
|
-
compacted: true,
|
|
115
|
-
entries_removed: toCompact.length,
|
|
116
|
-
entries_preserved: toPreserve.length,
|
|
117
|
-
summary_inserted: true,
|
|
118
|
-
new_total: toPreserve.length + 1,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
export async function logDebug(params) {
|
|
122
|
-
const db = getDb();
|
|
123
|
-
const now = new Date().toISOString();
|
|
124
|
-
// task_id takes priority; fall back to project_id
|
|
125
|
-
const entityType = params.task_id ? 'task' : params.project_id ? 'project' : null;
|
|
126
|
-
const entityId = params.task_id ?? params.project_id ?? null;
|
|
127
|
-
const result = db.prepare(`INSERT INTO activity_logs (action, title, detail, entity_type, entity_id, created_at)
|
|
128
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run('debug_log', params.message, params.detail ?? null, entityType, entityId, now);
|
|
129
|
-
const entry = db.prepare('SELECT * FROM activity_logs WHERE id = ?').get(result.lastInsertRowid);
|
|
130
|
-
broadcastChange('activity', 'activity_logged', entry);
|
|
131
|
-
return successResponse({ id: result.lastInsertRowid, message: params.message });
|
|
132
|
-
}
|
|
133
|
-
// ─── MCP registration ─────────────────────────────────────────────────
|
|
134
|
-
export function registerActivityTools(server) {
|
|
135
|
-
server.tool('get_activity_log', 'Retrieve recent activity log entries. Shows what has changed — task completions, timer events, status transitions. Filter by action or entity type.', {
|
|
136
|
-
limit: z.number().optional(),
|
|
137
|
-
action: z.string().optional(),
|
|
138
|
-
entity_type: z.string().optional(),
|
|
139
|
-
}, { readOnlyHint: true }, async (params) => getActivityLog(params));
|
|
140
|
-
server.tool('clear_activity_log', 'Delete all activity log entries. Use with caution — this is irreversible.', {}, { destructiveHint: true }, async () => clearActivityLog());
|
|
141
|
-
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.', {
|
|
142
|
-
message: z.string().describe('Short summary of what you are doing or found'),
|
|
143
|
-
detail: z.string().optional().describe('Longer explanation — stack traces, error messages, hypotheses, commands run, what you tried'),
|
|
144
|
-
task_id: z.number().optional().describe('Link this debug log to a specific task'),
|
|
145
|
-
project_id: z.number().optional().describe('Link this debug log to a project (used when no specific task applies)'),
|
|
146
|
-
}, { readOnlyHint: false }, async (params) => logDebug(params));
|
|
147
|
-
server.tool('compact_activity_log', 'Compact old activity log entries into a summary. Keeps the most recent entries (default 50) and summarizes the rest. Use dry_run=true to preview what would be compacted. Runs automatically when log exceeds max_entries threshold.', {
|
|
148
|
-
preserve_recent: z.number().optional().describe('Number of recent entries to keep verbatim (default: 50)'),
|
|
149
|
-
max_entries: z.number().optional().describe('Only compact if log exceeds this count (default: 500)'),
|
|
150
|
-
dry_run: z.boolean().optional().describe('If true, show what would be compacted without doing it'),
|
|
151
|
-
}, { readOnlyHint: false }, async (params) => compactActivityLog(params));
|
|
152
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
/** 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;
|
|
9
|
-
export { myAgentName };
|
|
10
|
-
/** Get the current agent name — used by tmux bridge to track renames */
|
|
11
|
-
export declare function getAgentName(): string;
|
|
12
|
-
export declare function registerAgentInboxTools(server: McpServer): void;
|