@recursiv/mcp 0.1.1
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 +135 -0
- package/dist/__tests__/scopes.test.d.ts +2 -0
- package/dist/__tests__/scopes.test.d.ts.map +1 -0
- package/dist/__tests__/scopes.test.js +85 -0
- package/dist/__tests__/scopes.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/approval-gate.d.ts +45 -0
- package/dist/lib/approval-gate.d.ts.map +1 -0
- package/dist/lib/approval-gate.js +143 -0
- package/dist/lib/approval-gate.js.map +1 -0
- package/dist/scopes.d.ts +99 -0
- package/dist/scopes.d.ts.map +1 -0
- package/dist/scopes.js +122 -0
- package/dist/scopes.js.map +1 -0
- package/dist/tools/__tests__/agents.test.d.ts +2 -0
- package/dist/tools/__tests__/agents.test.d.ts.map +1 -0
- package/dist/tools/__tests__/agents.test.js +248 -0
- package/dist/tools/__tests__/agents.test.js.map +1 -0
- package/dist/tools/__tests__/devtools.test.d.ts +2 -0
- package/dist/tools/__tests__/devtools.test.d.ts.map +1 -0
- package/dist/tools/__tests__/devtools.test.js +206 -0
- package/dist/tools/__tests__/devtools.test.js.map +1 -0
- package/dist/tools/__tests__/dispatcher.test.d.ts +2 -0
- package/dist/tools/__tests__/dispatcher.test.d.ts.map +1 -0
- package/dist/tools/__tests__/dispatcher.test.js +178 -0
- package/dist/tools/__tests__/dispatcher.test.js.map +1 -0
- package/dist/tools/__tests__/memory.test.d.ts +2 -0
- package/dist/tools/__tests__/memory.test.d.ts.map +1 -0
- package/dist/tools/__tests__/memory.test.js +151 -0
- package/dist/tools/__tests__/memory.test.js.map +1 -0
- package/dist/tools/__tests__/projects.test.d.ts +2 -0
- package/dist/tools/__tests__/projects.test.d.ts.map +1 -0
- package/dist/tools/__tests__/projects.test.js +168 -0
- package/dist/tools/__tests__/projects.test.js.map +1 -0
- package/dist/tools/__tests__/sandbox.test.d.ts +2 -0
- package/dist/tools/__tests__/sandbox.test.d.ts.map +1 -0
- package/dist/tools/__tests__/sandbox.test.js +113 -0
- package/dist/tools/__tests__/sandbox.test.js.map +1 -0
- package/dist/tools/__tests__/social.test.d.ts +2 -0
- package/dist/tools/__tests__/social.test.d.ts.map +1 -0
- package/dist/tools/__tests__/social.test.js +127 -0
- package/dist/tools/__tests__/social.test.js.map +1 -0
- package/dist/tools/__tests__/swarms.test.d.ts +2 -0
- package/dist/tools/__tests__/swarms.test.d.ts.map +1 -0
- package/dist/tools/__tests__/swarms.test.js +320 -0
- package/dist/tools/__tests__/swarms.test.js.map +1 -0
- package/dist/tools/__tests__/templates.test.d.ts +2 -0
- package/dist/tools/__tests__/templates.test.d.ts.map +1 -0
- package/dist/tools/__tests__/templates.test.js +176 -0
- package/dist/tools/__tests__/templates.test.js.map +1 -0
- package/dist/tools/agents.d.ts +4 -0
- package/dist/tools/agents.d.ts.map +1 -0
- package/dist/tools/agents.js +170 -0
- package/dist/tools/agents.js.map +1 -0
- package/dist/tools/devtools.d.ts +4 -0
- package/dist/tools/devtools.d.ts.map +1 -0
- package/dist/tools/devtools.js +511 -0
- package/dist/tools/devtools.js.map +1 -0
- package/dist/tools/dispatcher.d.ts +4 -0
- package/dist/tools/dispatcher.d.ts.map +1 -0
- package/dist/tools/dispatcher.js +647 -0
- package/dist/tools/dispatcher.js.map +1 -0
- package/dist/tools/memory.d.ts +4 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +92 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/platform.d.ts +4 -0
- package/dist/tools/platform.d.ts.map +1 -0
- package/dist/tools/platform.js +359 -0
- package/dist/tools/platform.js.map +1 -0
- package/dist/tools/projects.d.ts +4 -0
- package/dist/tools/projects.d.ts.map +1 -0
- package/dist/tools/projects.js +79 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/rate-limiter.d.ts +53 -0
- package/dist/tools/rate-limiter.d.ts.map +1 -0
- package/dist/tools/rate-limiter.js +87 -0
- package/dist/tools/rate-limiter.js.map +1 -0
- package/dist/tools/remaining.d.ts +8 -0
- package/dist/tools/remaining.d.ts.map +1 -0
- package/dist/tools/remaining.js +283 -0
- package/dist/tools/remaining.js.map +1 -0
- package/dist/tools/sandbox.d.ts +4 -0
- package/dist/tools/sandbox.d.ts.map +1 -0
- package/dist/tools/sandbox.js +35 -0
- package/dist/tools/sandbox.js.map +1 -0
- package/dist/tools/social.d.ts +11 -0
- package/dist/tools/social.d.ts.map +1 -0
- package/dist/tools/social.js +136 -0
- package/dist/tools/social.js.map +1 -0
- package/dist/tools/swarms.d.ts +4 -0
- package/dist/tools/swarms.d.ts.map +1 -0
- package/dist/tools/swarms.js +184 -0
- package/dist/tools/swarms.js.map +1 -0
- package/dist/tools/templates.d.ts +4 -0
- package/dist/tools/templates.d.ts.map +1 -0
- package/dist/tools/templates.js +102 -0
- package/dist/tools/templates.js.map +1 -0
- package/dist/tools/utils.d.ts +9 -0
- package/dist/tools/utils.d.ts.map +1 -0
- package/dist/tools/utils.js +8 -0
- package/dist/tools/utils.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Scopes } from '../scopes.js';
|
|
3
|
+
import { fmtErr } from './utils.js';
|
|
4
|
+
// Env-based default so users don't need to pass org every call
|
|
5
|
+
const defaultOrgId = process.env.RECURSIV_ORGANIZATION_ID;
|
|
6
|
+
export function registerDispatcherTools(registry, client) {
|
|
7
|
+
registry.tool('list_tasks', 'List tasks from the dispatcher with optional filters. Automatically scoped to the configured organization.', [Scopes.COMMANDS_READ], {
|
|
8
|
+
status: z.string().optional().describe('Filter by status (pending, in_progress, claimed, done). "claimed" returns in_progress tasks with an active claim.'),
|
|
9
|
+
milestone: z.string().optional().describe('Filter by milestone'),
|
|
10
|
+
layer: z.string().optional().describe('Filter by layer (core, plugin, ops)'),
|
|
11
|
+
owner: z.string().optional().describe('Filter by owner'),
|
|
12
|
+
organization_id: z.string().optional().describe('Organization UUID (defaults to RECURSIV_ORGANIZATION_ID env var)'),
|
|
13
|
+
limit: z.number().optional().describe('Max results (default 20)'),
|
|
14
|
+
}, async (params) => {
|
|
15
|
+
try {
|
|
16
|
+
const result = await client.dispatcher.tasks({
|
|
17
|
+
...params,
|
|
18
|
+
organization_id: params.organization_id || defaultOrgId,
|
|
19
|
+
});
|
|
20
|
+
const allTasks = result.data;
|
|
21
|
+
const limit = params.limit ?? 20;
|
|
22
|
+
const tasks = allTasks.slice(0, limit);
|
|
23
|
+
if (tasks.length === 0) {
|
|
24
|
+
return { content: [{ type: 'text', text: 'No tasks found matching filters.' }] };
|
|
25
|
+
}
|
|
26
|
+
const lines = tasks.map((t) => `[${t.status}] ${t.id} — ${t.title} (score: ${t.score ?? 'N/A'}, effort: ${t.effort ?? 'N/A'}${t.owner ? `, owner: ${t.owner}` : ''})`);
|
|
27
|
+
const header = `Showing ${tasks.length} of ${allTasks.length} tasks${allTasks.length > limit ? ` (use limit param to see more)` : ''}:`;
|
|
28
|
+
return { content: [{ type: 'text', text: `${header}\n${lines.join('\n')}` }] };
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return fmtErr('Error listing tasks', e);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
registry.tool('claim_next_task', 'Atomically claim the highest-priority available task. Scoped to the configured organization.', [Scopes.COMMANDS_WRITE], {
|
|
35
|
+
agent: z.string().describe('Agent identifier (e.g., "claude-terminal-1")'),
|
|
36
|
+
owner: z.string().optional().describe('Owner filter (e.g., "bill")'),
|
|
37
|
+
}, async (params) => {
|
|
38
|
+
try {
|
|
39
|
+
const result = await client.dispatcher.claimNext(params);
|
|
40
|
+
const claim = result.data;
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: 'text', text: `Claimed task: ${claim.task.title}\nTask ID: ${claim.task_id}\nClaim ID: ${claim.claim_id}\nAgent: ${claim.agent}\n\nRemember to send heartbeats every 5 minutes.` }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
if (e.status === 409) {
|
|
47
|
+
return { content: [{ type: 'text', text: 'No available tasks to claim. All tasks are either claimed, blocked, or done.' }] };
|
|
48
|
+
}
|
|
49
|
+
return fmtErr('Error claiming task', e);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
registry.tool('claim_task', 'Claim a specific task by ID', [Scopes.COMMANDS_WRITE], {
|
|
53
|
+
task_id: z.string().describe('Task ID to claim'),
|
|
54
|
+
agent: z.string().describe('Agent identifier'),
|
|
55
|
+
}, async (params) => {
|
|
56
|
+
try {
|
|
57
|
+
const result = await client.dispatcher.claim(params.task_id, { agent: params.agent });
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: 'text', text: `Claimed task: ${result.data.task.title}\nTask ID: ${result.data.task_id}\nClaim ID: ${result.data.claim_id}` }],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
if (e.status === 409) {
|
|
64
|
+
return { content: [{ type: 'text', text: `Task ${params.task_id} is already claimed by another agent.` }] };
|
|
65
|
+
}
|
|
66
|
+
return fmtErr('Error claiming task', e);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
registry.tool('heartbeat_task', 'Send heartbeat for an active task claim. Must be called every 5 minutes to keep the claim alive.', [Scopes.COMMANDS_WRITE], {
|
|
70
|
+
task_id: z.string().describe('Task ID'),
|
|
71
|
+
agent: z.string().describe('Agent identifier'),
|
|
72
|
+
}, async (params) => {
|
|
73
|
+
try {
|
|
74
|
+
await client.dispatcher.heartbeat(params.task_id, { agent: params.agent });
|
|
75
|
+
return { content: [{ type: 'text', text: `Heartbeat sent for task ${params.task_id}.` }] };
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
return fmtErr('Error sending heartbeat', e);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
registry.tool('complete_task', 'Mark a task as done', [Scopes.COMMANDS_WRITE], {
|
|
82
|
+
task_id: z.string().describe('Task ID to complete'),
|
|
83
|
+
notes: z.string().optional().describe('Completion notes'),
|
|
84
|
+
}, async (params) => {
|
|
85
|
+
try {
|
|
86
|
+
await client.dispatcher.complete(params.task_id, { notes: params.notes });
|
|
87
|
+
return { content: [{ type: 'text', text: `Task ${params.task_id} marked as done.` }] };
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
return fmtErr('Error completing task', e);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
registry.tool('release_task', 'Release a claim on a task, making it available for other agents', [Scopes.COMMANDS_WRITE], {
|
|
94
|
+
task_id: z.string().describe('Task ID to release'),
|
|
95
|
+
agent: z.string().optional().describe('Agent identifier'),
|
|
96
|
+
notes: z.string().optional().describe('Reason for release'),
|
|
97
|
+
release_reason: z.string().optional().describe('Structured reason: completed, blocked, timeout, reassigned, out_of_scope, needs_human, audit_failed'),
|
|
98
|
+
context_handoff: z.string().optional().describe('What you learned, where you left off, what the next agent needs to know'),
|
|
99
|
+
pr_urls: z.array(z.string()).optional().describe('PR URLs produced during this claim'),
|
|
100
|
+
commits: z.array(z.string()).optional().describe('Commit SHAs touched during this claim'),
|
|
101
|
+
}, async (params) => {
|
|
102
|
+
try {
|
|
103
|
+
await client.dispatcher.release(params.task_id, {
|
|
104
|
+
agent: params.agent,
|
|
105
|
+
notes: params.notes,
|
|
106
|
+
release_reason: params.release_reason,
|
|
107
|
+
context_handoff: params.context_handoff,
|
|
108
|
+
pr_urls: params.pr_urls,
|
|
109
|
+
commits: params.commits,
|
|
110
|
+
});
|
|
111
|
+
return { content: [{ type: 'text', text: `Released claim on task ${params.task_id}.` }] };
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
return fmtErr('Error releasing task', e);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
registry.tool('create_task', 'Create a new task in the dispatcher. Automatically scoped to the configured organization.', [Scopes.COMMANDS_WRITE], {
|
|
118
|
+
title: z.string().describe('Task title'),
|
|
119
|
+
description: z.string().optional().describe('Task description'),
|
|
120
|
+
milestone: z.string().optional().describe('Milestone (e.g., "v1-launch")'),
|
|
121
|
+
effort: z.number().optional().describe('Effort estimate (1-10)'),
|
|
122
|
+
layer: z.string().optional().describe('Layer (core, plugin, ops)'),
|
|
123
|
+
owner: z.string().optional().describe('Assigned owner'),
|
|
124
|
+
organization_id: z.string().optional().describe('Organization UUID (defaults to RECURSIV_ORGANIZATION_ID env var)'),
|
|
125
|
+
project_id: z.string().optional().describe('Project UUID to scope the task to a specific project'),
|
|
126
|
+
severity: z.number().min(0).max(5).optional().describe('Bug/security severity 0-5 (5 = critical). Drives priority scoring.'),
|
|
127
|
+
urgency: z.number().min(0).max(5).optional().describe('Time urgency 0-5 (5 = immediate). Drives priority scoring.'),
|
|
128
|
+
signal: z.number().min(0).max(5).optional().describe('User/market signal strength 0-5. Drives priority scoring.'),
|
|
129
|
+
ui_impact: z.number().min(0).max(5).optional().describe('UI/UX impact 0-5 (5 = core user-facing experience). Drives priority scoring.'),
|
|
130
|
+
created_by: z.string().optional().describe('Agent name or user who created this task'),
|
|
131
|
+
created_via: z.string().optional().describe('How this task was created: mcp, api, sync, auto-audit, manual'),
|
|
132
|
+
}, async (params) => {
|
|
133
|
+
try {
|
|
134
|
+
// Generate a slug-based ID (REST endpoint requires it)
|
|
135
|
+
const slug = params.title
|
|
136
|
+
.toLowerCase()
|
|
137
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
138
|
+
.replace(/^-|-$/g, '')
|
|
139
|
+
.slice(0, 80);
|
|
140
|
+
const suffix = Math.random().toString(36).slice(2, 10);
|
|
141
|
+
const id = `proj-${slug}-${suffix}`;
|
|
142
|
+
const result = await client.dispatcher.create({
|
|
143
|
+
id,
|
|
144
|
+
title: params.title,
|
|
145
|
+
notes: params.description,
|
|
146
|
+
effort: params.effort,
|
|
147
|
+
layer: params.layer,
|
|
148
|
+
owner: params.owner,
|
|
149
|
+
milestone: params.milestone,
|
|
150
|
+
organization_id: params.organization_id || defaultOrgId,
|
|
151
|
+
project_id: params.project_id,
|
|
152
|
+
severity: params.severity,
|
|
153
|
+
urgency_score: params.urgency,
|
|
154
|
+
signal_score: params.signal,
|
|
155
|
+
ui_impact: params.ui_impact,
|
|
156
|
+
created_by: params.created_by,
|
|
157
|
+
created_via: params.created_via || 'mcp',
|
|
158
|
+
});
|
|
159
|
+
const task = result.data;
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: 'text', text: `Created task: ${task.title}\nID: ${task.id}\nScore: ${task.score ?? 'N/A'}` }],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
return fmtErr('Error creating task', e);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
registry.tool('update_task', 'Update an existing task in the dispatcher. Can modify title, status, scoring fields, and more.', [Scopes.COMMANDS_WRITE], {
|
|
169
|
+
task_id: z.string().describe('Task ID to update'),
|
|
170
|
+
title: z.string().optional().describe('New task title'),
|
|
171
|
+
notes: z.string().optional().describe('Task notes'),
|
|
172
|
+
status: z.string().optional().describe('Task status (pending, in_progress, blocked, done, archived)'),
|
|
173
|
+
milestone: z.string().optional().describe('Milestone (e.g., "R1", "v1-launch")'),
|
|
174
|
+
effort: z.number().optional().describe('Effort estimate (1-10)'),
|
|
175
|
+
layer: z.string().optional().describe('Layer (core, plugin, ops)'),
|
|
176
|
+
owner: z.string().optional().describe('Assigned owner'),
|
|
177
|
+
proximity: z.number().min(1).max(10).optional().describe('How close to shippable (1-10). Drives priority scoring.'),
|
|
178
|
+
severity: z.number().min(0).max(5).optional().describe('Bug/security severity 0-5 (5 = critical). Drives priority scoring.'),
|
|
179
|
+
urgency: z.number().min(0).max(5).optional().describe('Time urgency 0-5 (5 = immediate). Drives priority scoring.'),
|
|
180
|
+
signal: z.number().min(0).max(5).optional().describe('User/market signal strength 0-5. Drives priority scoring.'),
|
|
181
|
+
ui_impact: z.number().min(0).max(5).optional().describe('UI/UX impact 0-5 (5 = core user-facing experience). Drives priority scoring.'),
|
|
182
|
+
score_override: z.number().optional().describe('Manual score override (bypasses formula calculation)'),
|
|
183
|
+
}, async (params) => {
|
|
184
|
+
try {
|
|
185
|
+
const result = await client.dispatcher.update(params.task_id, {
|
|
186
|
+
title: params.title,
|
|
187
|
+
notes: params.notes,
|
|
188
|
+
status: params.status,
|
|
189
|
+
milestone: params.milestone,
|
|
190
|
+
effort: params.effort,
|
|
191
|
+
layer: params.layer,
|
|
192
|
+
owner: params.owner,
|
|
193
|
+
proximity: params.proximity,
|
|
194
|
+
severity: params.severity,
|
|
195
|
+
urgency_score: params.urgency,
|
|
196
|
+
signal_score: params.signal,
|
|
197
|
+
ui_impact: params.ui_impact,
|
|
198
|
+
score_override: params.score_override,
|
|
199
|
+
});
|
|
200
|
+
const task = result.data;
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: 'text', text: `Updated task: ${task.title}\nID: ${task.id}\nStatus: ${task.status}\nScore: ${task.score ?? 'N/A'}` }],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
return fmtErr('Error updating task', e);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
registry.tool('get_dispatcher_stats', 'Get dispatcher statistics: task counts by status, owner, layer, and active claims. Scoped to the configured organization.', [Scopes.COMMANDS_READ], {}, async () => {
|
|
210
|
+
try {
|
|
211
|
+
const result = await client.dispatcher.stats();
|
|
212
|
+
const { tasks, byOwner, byLayer, activeClaims } = result.data;
|
|
213
|
+
const lines = [
|
|
214
|
+
`Total tasks: ${tasks.total}`,
|
|
215
|
+
`Pending: ${tasks.pending}`,
|
|
216
|
+
`In progress: ${tasks.in_progress}`,
|
|
217
|
+
`Done: ${tasks.done}`,
|
|
218
|
+
`Blocked: ${tasks.blocked}`,
|
|
219
|
+
`Archived: ${tasks.archived}`,
|
|
220
|
+
`Stale: ${tasks.stale}`,
|
|
221
|
+
`Active claims: ${activeClaims}`,
|
|
222
|
+
'',
|
|
223
|
+
'By layer:',
|
|
224
|
+
...byLayer.map((r) => ` ${r.layer}: ${r.count} (${r.done} done)`),
|
|
225
|
+
'',
|
|
226
|
+
'By owner:',
|
|
227
|
+
...byOwner.map((r) => ` ${r.owner}: ${r.count} (${r.done} done)`),
|
|
228
|
+
];
|
|
229
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
return fmtErr('Error getting stats', e);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
registry.tool('list_active_claims', 'List all active task claims — shows which agents are working on which tasks, with heartbeat status.', [Scopes.COMMANDS_READ], {}, async () => {
|
|
236
|
+
try {
|
|
237
|
+
const result = await client.dispatcher.activeClaims();
|
|
238
|
+
const claims = result.data;
|
|
239
|
+
if (!claims || claims.length === 0) {
|
|
240
|
+
return { content: [{ type: 'text', text: 'No active claims.' }] };
|
|
241
|
+
}
|
|
242
|
+
const lines = claims.map((c) => {
|
|
243
|
+
const heartbeatAge = c.lastHeartbeat || c.last_heartbeat
|
|
244
|
+
? Math.round((Date.now() - new Date(c.lastHeartbeat || c.last_heartbeat).getTime()) / 60000)
|
|
245
|
+
: null;
|
|
246
|
+
const staleWarning = heartbeatAge != null && heartbeatAge > 10 ? ' [STALE]' : '';
|
|
247
|
+
return `${c.agent} → ${c.taskId || c.task_id} (claimed ${c.claimedAt || c.claimed_at}, heartbeat ${heartbeatAge ?? '?'}m ago${staleWarning})`;
|
|
248
|
+
});
|
|
249
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
return fmtErr('Error listing claims', e);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
registry.tool('list_signals', 'List signals from the dispatcher signal log. Shows what external data (PostHog, Stripe, GitHub, social) has been collected for tasks.', [Scopes.COMMANDS_READ], {
|
|
256
|
+
task_id: z.string().optional().describe('Filter signals for a specific task'),
|
|
257
|
+
source: z.string().optional().describe('Filter by signal source (posthog, stripe, github, social, human)'),
|
|
258
|
+
limit: z.number().optional().describe('Max results (default 100)'),
|
|
259
|
+
organization_id: z.string().optional().describe('Organization UUID (defaults to RECURSIV_ORGANIZATION_ID env var)'),
|
|
260
|
+
}, async (params) => {
|
|
261
|
+
try {
|
|
262
|
+
const result = await client.dispatcher.signals({
|
|
263
|
+
...params,
|
|
264
|
+
organization_id: params.organization_id || defaultOrgId,
|
|
265
|
+
});
|
|
266
|
+
const signals = result.data;
|
|
267
|
+
if (!signals || signals.length === 0) {
|
|
268
|
+
return { content: [{ type: 'text', text: 'No signals found.' }] };
|
|
269
|
+
}
|
|
270
|
+
const lines = signals.map((s) => `[${s.signal_source}] ${s.signal_type}: ${s.signal_value} → task ${s.task_id} (${s.created_at})`);
|
|
271
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
272
|
+
}
|
|
273
|
+
catch (e) {
|
|
274
|
+
return fmtErr('Error listing signals', e);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
registry.tool('list_outcomes', 'List outcome checks — measured results for completed tasks (before/after metrics).', [Scopes.COMMANDS_READ], {
|
|
278
|
+
task_id: z.string().optional().describe('Filter outcomes for a specific task'),
|
|
279
|
+
limit: z.number().optional().describe('Max results (default 100)'),
|
|
280
|
+
organization_id: z.string().optional().describe('Organization UUID (defaults to RECURSIV_ORGANIZATION_ID env var)'),
|
|
281
|
+
}, async (params) => {
|
|
282
|
+
try {
|
|
283
|
+
const result = await client.dispatcher.outcomes({
|
|
284
|
+
...params,
|
|
285
|
+
organization_id: params.organization_id || defaultOrgId,
|
|
286
|
+
});
|
|
287
|
+
const outcomes = result.data;
|
|
288
|
+
if (!outcomes || outcomes.length === 0) {
|
|
289
|
+
return { content: [{ type: 'text', text: 'No outcomes found.' }] };
|
|
290
|
+
}
|
|
291
|
+
const lines = outcomes.map((o) => `[${o.status}] ${o.metric_name}: ${o.before_value ?? '?'} → ${o.after_value} (task ${o.task_id}, ${o.created_at})`);
|
|
292
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
293
|
+
}
|
|
294
|
+
catch (e) {
|
|
295
|
+
return fmtErr('Error listing outcomes', e);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
registry.tool('list_discoveries', 'List tasks in the discovery queue — agent-discovered work items needing human review.', [Scopes.COMMANDS_READ], {
|
|
299
|
+
max_confidence: z.number().optional().describe('Max confidence threshold (1-10, default 5)'),
|
|
300
|
+
limit: z.number().optional().describe('Max results (default 50)'),
|
|
301
|
+
organization_id: z.string().optional().describe('Organization UUID (defaults to RECURSIV_ORGANIZATION_ID env var)'),
|
|
302
|
+
}, async (params) => {
|
|
303
|
+
try {
|
|
304
|
+
const result = await client.dispatcher.discoveries({
|
|
305
|
+
maxConfidence: params.max_confidence,
|
|
306
|
+
limit: params.limit,
|
|
307
|
+
organization_id: params.organization_id || defaultOrgId,
|
|
308
|
+
});
|
|
309
|
+
const tasks = result.data;
|
|
310
|
+
if (!tasks || tasks.length === 0) {
|
|
311
|
+
return { content: [{ type: 'text', text: 'No discoveries in queue.' }] };
|
|
312
|
+
}
|
|
313
|
+
const lines = tasks.map((t) => `${t.id} — ${t.title} (score: ${t.score ?? 'N/A'}, effort: ${t.effort ?? 'N/A'})`);
|
|
314
|
+
return { content: [{ type: 'text', text: `Discovery queue (${tasks.length} items):\n${lines.join('\n')}` }] };
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
return fmtErr('Error listing discoveries', e);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
registry.tool('get_pause_state', 'Check if dispatcher claiming is currently paused.', [Scopes.COMMANDS_READ], {
|
|
321
|
+
organization_id: z.string().optional().describe('Organization UUID (defaults to RECURSIV_ORGANIZATION_ID env var)'),
|
|
322
|
+
}, async (params) => {
|
|
323
|
+
try {
|
|
324
|
+
const result = await client.dispatcher.pauseState({
|
|
325
|
+
organization_id: params.organization_id || defaultOrgId,
|
|
326
|
+
});
|
|
327
|
+
const state = result.data;
|
|
328
|
+
if (!state.pausedUntil) {
|
|
329
|
+
return { content: [{ type: 'text', text: 'Dispatcher is NOT paused. Claiming is active.' }] };
|
|
330
|
+
}
|
|
331
|
+
return { content: [{ type: 'text', text: `Dispatcher is PAUSED until ${state.pausedUntil}${state.reason ? ` — Reason: ${state.reason}` : ''}` }] };
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
return fmtErr('Error getting pause state', e);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
registry.tool('pause_dispatcher', 'Pause dispatcher claiming for a duration. No agents can claim tasks while paused.', [Scopes.COMMANDS_WRITE], {
|
|
338
|
+
pause_minutes: z.number().optional().describe('Pause duration in minutes (default: 60)'),
|
|
339
|
+
until: z.string().optional().describe('ISO 8601 datetime to pause until (alternative to pause_minutes)'),
|
|
340
|
+
reason: z.string().optional().describe('Reason for pausing'),
|
|
341
|
+
organization_id: z.string().optional().describe('Organization UUID (defaults to RECURSIV_ORGANIZATION_ID env var)'),
|
|
342
|
+
}, async (params) => {
|
|
343
|
+
try {
|
|
344
|
+
const result = await client.dispatcher.pause({
|
|
345
|
+
pauseMs: params.until ? undefined : (params.pause_minutes || 60) * 60_000,
|
|
346
|
+
until: params.until,
|
|
347
|
+
reason: params.reason,
|
|
348
|
+
organization_id: params.organization_id || defaultOrgId,
|
|
349
|
+
});
|
|
350
|
+
const state = result.data;
|
|
351
|
+
return { content: [{ type: 'text', text: `Dispatcher paused until ${state.pausedUntil}${state.reason ? ` — ${state.reason}` : ''}` }] };
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
return fmtErr('Error pausing dispatcher', e);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
registry.tool('resume_dispatcher', 'Resume dispatcher claiming (clear pause). Agents can claim tasks again.', [Scopes.COMMANDS_WRITE], {
|
|
358
|
+
organization_id: z.string().optional().describe('Organization UUID (defaults to RECURSIV_ORGANIZATION_ID env var)'),
|
|
359
|
+
}, async (params) => {
|
|
360
|
+
try {
|
|
361
|
+
await client.dispatcher.resume({
|
|
362
|
+
organization_id: params.organization_id || defaultOrgId,
|
|
363
|
+
});
|
|
364
|
+
return { content: [{ type: 'text', text: 'Dispatcher resumed. Claiming is active.' }] };
|
|
365
|
+
}
|
|
366
|
+
catch (e) {
|
|
367
|
+
return fmtErr('Error resuming dispatcher', e);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
registry.tool('list_stale_tasks', 'List tasks with no updates in N days. Helps identify abandoned or forgotten work.', [Scopes.COMMANDS_READ], {
|
|
371
|
+
days: z.number().optional().describe('Days threshold (default 60)'),
|
|
372
|
+
organization_id: z.string().optional().describe('Organization UUID (defaults to RECURSIV_ORGANIZATION_ID env var)'),
|
|
373
|
+
}, async (params) => {
|
|
374
|
+
try {
|
|
375
|
+
const result = await client.dispatcher.staleItems({
|
|
376
|
+
days: params.days,
|
|
377
|
+
organization_id: params.organization_id || defaultOrgId,
|
|
378
|
+
});
|
|
379
|
+
const tasks = result.data;
|
|
380
|
+
if (!tasks || tasks.length === 0) {
|
|
381
|
+
return { content: [{ type: 'text', text: `No stale tasks (>${params.days || 60} days without update).` }] };
|
|
382
|
+
}
|
|
383
|
+
const lines = tasks.map((t) => `[${t.status}] ${t.id} — ${t.title} (last updated: ${t.updated_at})`);
|
|
384
|
+
return { content: [{ type: 'text', text: `Stale tasks (${tasks.length}):\n${lines.join('\n')}` }] };
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
return fmtErr('Error listing stale tasks', e);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
registry.tool('list_stuck_tasks', 'List in-progress tasks stuck for N days — claimed but no heartbeat or completion.', [Scopes.COMMANDS_READ], {
|
|
391
|
+
days: z.number().optional().describe('Days threshold (default 7)'),
|
|
392
|
+
organization_id: z.string().optional().describe('Organization UUID (defaults to RECURSIV_ORGANIZATION_ID env var)'),
|
|
393
|
+
}, async (params) => {
|
|
394
|
+
try {
|
|
395
|
+
const result = await client.dispatcher.stuckItems({
|
|
396
|
+
days: params.days,
|
|
397
|
+
organization_id: params.organization_id || defaultOrgId,
|
|
398
|
+
});
|
|
399
|
+
const tasks = result.data;
|
|
400
|
+
if (!tasks || tasks.length === 0) {
|
|
401
|
+
return { content: [{ type: 'text', text: `No stuck tasks (>${params.days || 7} days in progress).` }] };
|
|
402
|
+
}
|
|
403
|
+
const lines = tasks.map((t) => `[${t.status}] ${t.id} — ${t.title} (owner: ${t.owner ?? 'unassigned'}, updated: ${t.updated_at})`);
|
|
404
|
+
return { content: [{ type: 'text', text: `Stuck tasks (${tasks.length}):\n${lines.join('\n')}` }] };
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
return fmtErr('Error listing stuck tasks', e);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
registry.tool('get_claim', 'Get the claim status for a specific task — shows if it is claimed, by whom, and heartbeat age.', [Scopes.COMMANDS_READ], {
|
|
411
|
+
task_id: z.string().describe('Task ID to check'),
|
|
412
|
+
}, async (params) => {
|
|
413
|
+
try {
|
|
414
|
+
const result = await client.dispatcher.getClaim(params.task_id);
|
|
415
|
+
const claim = result.data;
|
|
416
|
+
if (!claim) {
|
|
417
|
+
return { content: [{ type: 'text', text: `Task ${params.task_id} has no active or historical claims.` }] };
|
|
418
|
+
}
|
|
419
|
+
const c = claim;
|
|
420
|
+
const status = c.status;
|
|
421
|
+
const heartbeatAge = c.lastHeartbeat || c.last_heartbeat
|
|
422
|
+
? Math.round((Date.now() - new Date(c.lastHeartbeat || c.last_heartbeat).getTime()) / 60000)
|
|
423
|
+
: null;
|
|
424
|
+
const lines = [
|
|
425
|
+
`Task: ${c.taskId || c.task_id}`,
|
|
426
|
+
`Status: ${status}`,
|
|
427
|
+
`Agent: ${c.agent}`,
|
|
428
|
+
`Claimed at: ${c.claimedAt || c.claimed_at}`,
|
|
429
|
+
heartbeatAge != null ? `Last heartbeat: ${heartbeatAge}m ago` : null,
|
|
430
|
+
c.releasedAt || c.released_at ? `Released at: ${c.releasedAt || c.released_at}` : null,
|
|
431
|
+
c.completedAt || c.completed_at ? `Completed at: ${c.completedAt || c.completed_at}` : null,
|
|
432
|
+
c.notes ? `Notes: ${c.notes}` : null,
|
|
433
|
+
].filter(Boolean);
|
|
434
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
435
|
+
}
|
|
436
|
+
catch (e) {
|
|
437
|
+
return fmtErr('Error getting claim', e);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
registry.tool('get_task', 'Get a single task by ID — full details including score, status, owner, description.', [Scopes.COMMANDS_READ], {
|
|
441
|
+
task_id: z.string().describe('Task ID to retrieve'),
|
|
442
|
+
}, async (params) => {
|
|
443
|
+
try {
|
|
444
|
+
const result = await client.dispatcher.getTask(params.task_id);
|
|
445
|
+
const t = result.data;
|
|
446
|
+
const lines = [
|
|
447
|
+
`ID: ${t.id}`,
|
|
448
|
+
`Title: ${t.title}`,
|
|
449
|
+
`Status: ${t.status}`,
|
|
450
|
+
`Score: ${t.score ?? 'N/A'}`,
|
|
451
|
+
`Effort: ${t.effort ?? 'N/A'}`,
|
|
452
|
+
`Layer: ${t.layer ?? 'N/A'}`,
|
|
453
|
+
`Owner: ${t.owner ?? 'unassigned'}`,
|
|
454
|
+
`Milestone: ${t.milestone ?? 'N/A'}`,
|
|
455
|
+
t.description ? `Description: ${t.description}` : null,
|
|
456
|
+
`Created: ${t.created_at}`,
|
|
457
|
+
`Updated: ${t.updated_at}`,
|
|
458
|
+
].filter(Boolean);
|
|
459
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
460
|
+
}
|
|
461
|
+
catch (e) {
|
|
462
|
+
if (e.status === 404) {
|
|
463
|
+
return { content: [{ type: 'text', text: `Task ${params.task_id} not found.` }] };
|
|
464
|
+
}
|
|
465
|
+
return fmtErr('Error getting task', e);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
registry.tool('list_task_activity', 'Get the activity log for a task — shows claim/release/complete events with timestamps and agents.', [Scopes.COMMANDS_READ], {
|
|
469
|
+
task_id: z.string().describe('Task ID to get activity for'),
|
|
470
|
+
limit: z.number().optional().describe('Max results (default 50, max 200)'),
|
|
471
|
+
}, async (params) => {
|
|
472
|
+
try {
|
|
473
|
+
const result = await client.dispatcher.activity(params.task_id, {
|
|
474
|
+
limit: params.limit,
|
|
475
|
+
});
|
|
476
|
+
const entries = result.data;
|
|
477
|
+
if (!entries || entries.length === 0) {
|
|
478
|
+
return { content: [{ type: 'text', text: `No activity recorded for task ${params.task_id}.` }] };
|
|
479
|
+
}
|
|
480
|
+
const lines = entries.map((e) => `[${e.created_at}] ${e.event_type}${e.agent ? ` by ${e.agent}` : ''}${e.detail ? ` — ${e.detail}` : ''}`);
|
|
481
|
+
return { content: [{ type: 'text', text: `Activity for ${params.task_id} (${entries.length} events):\n${lines.join('\n')}` }] };
|
|
482
|
+
}
|
|
483
|
+
catch (e) {
|
|
484
|
+
return fmtErr('Error getting task activity', e);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
registry.tool('archive_task', 'Archive (soft-delete) a task. Sets status to archived. Cannot be undone easily.', [Scopes.COMMANDS_WRITE], {
|
|
488
|
+
task_id: z.string().describe('Task ID to archive'),
|
|
489
|
+
}, async (params) => {
|
|
490
|
+
try {
|
|
491
|
+
const result = await client.dispatcher.archive(params.task_id);
|
|
492
|
+
return { content: [{ type: 'text', text: `Archived task: ${result.data.title} (${params.task_id})` }] };
|
|
493
|
+
}
|
|
494
|
+
catch (e) {
|
|
495
|
+
if (e.status === 404) {
|
|
496
|
+
return { content: [{ type: 'text', text: `Task ${params.task_id} not found.` }] };
|
|
497
|
+
}
|
|
498
|
+
return fmtErr('Error archiving task', e);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
registry.tool('discover_task', 'Submit a discovered task for human review. Use when an agent finds work that needs to be done but should be triaged first.', [Scopes.COMMANDS_WRITE], {
|
|
502
|
+
title: z.string().describe('Task title (min 3 chars)'),
|
|
503
|
+
discovered_by: z.string().describe('Agent name or task ID that discovered this work'),
|
|
504
|
+
notes: z.string().optional().describe('Details about what was discovered and why it matters'),
|
|
505
|
+
layer: z.string().optional().describe('Layer (core, plugin, ops)'),
|
|
506
|
+
effort: z.number().optional().describe('Estimated effort (1-10)'),
|
|
507
|
+
task_type: z.string().optional().describe('Task type (bug, feature, refactor, content, infra)'),
|
|
508
|
+
}, async (params) => {
|
|
509
|
+
try {
|
|
510
|
+
const result = await client.dispatcher.discover({
|
|
511
|
+
...params,
|
|
512
|
+
organization_id: defaultOrgId,
|
|
513
|
+
});
|
|
514
|
+
const task = result.data;
|
|
515
|
+
return {
|
|
516
|
+
content: [{ type: 'text', text: `Discovery submitted: ${task.title}\nID: ${task.id}\nScore: ${task.score ?? 'N/A'}\n\nTask is in the discovery queue for human review.` }],
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
catch (e) {
|
|
520
|
+
return fmtErr('Error submitting discovery', e);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
registry.tool('record_outcome', 'Record a measured outcome for a completed task — before/after metrics to track impact.', [Scopes.COMMANDS_WRITE], {
|
|
524
|
+
task_id: z.string().describe('Task ID to record outcome for'),
|
|
525
|
+
metric_name: z.string().describe('Metric being measured (e.g., "page_load_ms", "error_rate", "signup_conversion")'),
|
|
526
|
+
after_value: z.string().describe('Measured value after the task was completed'),
|
|
527
|
+
before_value: z.string().optional().describe('Measured value before the task (if known)'),
|
|
528
|
+
notes: z.string().optional().describe('Additional context about the measurement'),
|
|
529
|
+
}, async (params) => {
|
|
530
|
+
try {
|
|
531
|
+
const result = await client.dispatcher.recordOutcome(params.task_id, {
|
|
532
|
+
metric_name: params.metric_name,
|
|
533
|
+
after_value: params.after_value,
|
|
534
|
+
before_value: params.before_value,
|
|
535
|
+
notes: params.notes,
|
|
536
|
+
});
|
|
537
|
+
const o = result.data;
|
|
538
|
+
return {
|
|
539
|
+
content: [{ type: 'text', text: `Outcome recorded for task ${params.task_id}:\n${o.metric_name}: ${o.before_value ?? '?'} → ${o.after_value}${o.notes ? `\nNotes: ${o.notes}` : ''}` }],
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
catch (e) {
|
|
543
|
+
return fmtErr('Error recording outcome', e);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
registry.tool('register_webhook', 'Register a webhook URL to receive dispatcher events (task_claimed, task_done, task_released, etc.). Returns the signing secret — save it, it is only shown once.', [Scopes.COMMANDS_WRITE], {
|
|
547
|
+
url: z.string().url().refine((u) => u.startsWith('https://'), { message: 'URL must use HTTPS' }).describe('HTTPS URL to receive POST events'),
|
|
548
|
+
event_types: z.array(z.string()).optional().describe('Event types to subscribe to (empty = all). Options: task_claimed, task_released, task_done, task_created, task_updated, task_archived, heartbeat'),
|
|
549
|
+
description: z.string().optional().describe('Human-readable label for this webhook'),
|
|
550
|
+
}, async (params) => {
|
|
551
|
+
try {
|
|
552
|
+
const result = await client.dispatcher.createWebhook({
|
|
553
|
+
url: params.url,
|
|
554
|
+
event_types: params.event_types,
|
|
555
|
+
description: params.description,
|
|
556
|
+
});
|
|
557
|
+
const sub = result.data;
|
|
558
|
+
return {
|
|
559
|
+
content: [{ type: 'text', text: `Webhook registered!\nID: ${sub.id}\nURL: ${sub.url}\nEvents: ${sub.event_types.length > 0 ? sub.event_types.join(', ') : 'all'}\n\nSAVE THIS SECRET (shown only once):\n${sub.secret}\n\nUse it to verify X-Recursiv-Signature header (HMAC-SHA256).` }],
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
catch (e) {
|
|
563
|
+
return fmtErr('Error registering webhook', e);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
registry.tool('list_webhooks', 'List registered webhook subscriptions. Secrets are redacted.', [Scopes.COMMANDS_READ], {}, async () => {
|
|
567
|
+
try {
|
|
568
|
+
const result = await client.dispatcher.listWebhooks();
|
|
569
|
+
const subs = result.data;
|
|
570
|
+
if (!subs || subs.length === 0) {
|
|
571
|
+
return { content: [{ type: 'text', text: 'No webhooks registered.' }] };
|
|
572
|
+
}
|
|
573
|
+
const lines = subs.map((s) => `${s.id} → ${s.url} (events: ${s.event_types?.length > 0 ? s.event_types.join(', ') : 'all'}, active: ${s.active}${s.description ? `, ${s.description}` : ''})`);
|
|
574
|
+
return { content: [{ type: 'text', text: `Webhooks (${subs.length}):\n${lines.join('\n')}` }] };
|
|
575
|
+
}
|
|
576
|
+
catch (e) {
|
|
577
|
+
return fmtErr('Error listing webhooks', e);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
registry.tool('update_webhook', 'Update a webhook subscription (URL, event types, description, or active status).', [Scopes.COMMANDS_WRITE], {
|
|
581
|
+
webhook_id: z.string().describe('Webhook subscription ID to update'),
|
|
582
|
+
url: z.string().url().optional().describe('New HTTPS URL'),
|
|
583
|
+
event_types: z.array(z.string()).optional().describe('New event types filter (empty array = all events)'),
|
|
584
|
+
description: z.string().optional().describe('Updated description'),
|
|
585
|
+
active: z.boolean().optional().describe('Enable or disable the webhook'),
|
|
586
|
+
}, async (params) => {
|
|
587
|
+
try {
|
|
588
|
+
const { webhook_id, ...input } = params;
|
|
589
|
+
const result = await client.dispatcher.updateWebhook(webhook_id, input);
|
|
590
|
+
const sub = result.data;
|
|
591
|
+
return {
|
|
592
|
+
content: [{ type: 'text', text: `Webhook ${sub.id} updated.\nURL: ${sub.url}\nActive: ${sub.active}\nEvents: ${sub.event_types.length > 0 ? sub.event_types.join(', ') : 'all'}` }],
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
catch (e) {
|
|
596
|
+
return fmtErr('Error updating webhook', e);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
registry.tool('delete_webhook', 'Delete a webhook subscription. Stops all future event deliveries to this URL.', [Scopes.COMMANDS_WRITE], {
|
|
600
|
+
webhook_id: z.string().describe('Webhook subscription ID to delete'),
|
|
601
|
+
}, async (params) => {
|
|
602
|
+
try {
|
|
603
|
+
await client.dispatcher.deleteWebhook(params.webhook_id);
|
|
604
|
+
return { content: [{ type: 'text', text: `Webhook ${params.webhook_id} deleted.` }] };
|
|
605
|
+
}
|
|
606
|
+
catch (e) {
|
|
607
|
+
if (e.status === 404) {
|
|
608
|
+
return { content: [{ type: 'text', text: `Webhook ${params.webhook_id} not found.` }] };
|
|
609
|
+
}
|
|
610
|
+
return fmtErr('Error deleting webhook', e);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
registry.tool('test_webhook', 'Send a test event to a webhook subscription to verify it is reachable and properly configured.', [Scopes.COMMANDS_WRITE], {
|
|
614
|
+
webhook_id: z.string().describe('Webhook subscription ID to test'),
|
|
615
|
+
}, async (params) => {
|
|
616
|
+
try {
|
|
617
|
+
const result = await client.dispatcher.testWebhook(params.webhook_id);
|
|
618
|
+
const test = result.data;
|
|
619
|
+
if (test.success) {
|
|
620
|
+
return { content: [{ type: 'text', text: `Webhook test successful! HTTP ${test.status}` }] };
|
|
621
|
+
}
|
|
622
|
+
return { content: [{ type: 'text', text: `Webhook test failed. ${test.error || `HTTP ${test.status}`}` }] };
|
|
623
|
+
}
|
|
624
|
+
catch (e) {
|
|
625
|
+
return fmtErr('Error testing webhook', e);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
// ── Webhook Delivery Log ──
|
|
629
|
+
registry.tool('webhook_deliveries', 'Get recent delivery log for a webhook subscription. Shows status, HTTP response, attempts, and errors.', [Scopes.COMMANDS_READ], {
|
|
630
|
+
webhook_id: z.string().describe('Webhook subscription ID'),
|
|
631
|
+
limit: z.number().optional().default(20).describe('Max deliveries to return (default 20, max 200)'),
|
|
632
|
+
}, async (params) => {
|
|
633
|
+
try {
|
|
634
|
+
const result = await client.dispatcher.webhookDeliveries(params.webhook_id, { limit: params.limit });
|
|
635
|
+
const deliveries = result.data;
|
|
636
|
+
if (!deliveries || deliveries.length === 0) {
|
|
637
|
+
return { content: [{ type: 'text', text: 'No deliveries found for this webhook.' }] };
|
|
638
|
+
}
|
|
639
|
+
const lines = deliveries.map((d) => `${d.event_type} — ${d.status} (HTTP ${d.response_status || '?'}) — ${d.attempts}/${d.max_attempts} attempts — ${d.created_at}${d.error ? ` — Error: ${d.error}` : ''}`);
|
|
640
|
+
return { content: [{ type: 'text', text: `Deliveries (${deliveries.length}):\n${lines.join('\n')}` }] };
|
|
641
|
+
}
|
|
642
|
+
catch (e) {
|
|
643
|
+
return fmtErr('Error fetching webhook deliveries', e);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
//# sourceMappingURL=dispatcher.js.map
|