@integrity-labs/agt-cli 0.8.9 → 0.9.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/dist/bin/agt.js +2 -2
- package/dist/lib/manager-worker.js +42 -9
- package/dist/lib/manager-worker.js.map +1 -1
- package/mcp/index.js +587 -0
- package/mcp/slack-channel.js +254 -0
- package/package.json +4 -3
package/mcp/index.js
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
// ── Configuration via env vars ──────────────────────────────────────────────
|
|
6
|
+
const AGT_HOST = process.env.AGT_HOST;
|
|
7
|
+
const AGT_API_KEY = process.env.AGT_API_KEY; // Host API key (tlk_...) for token refresh
|
|
8
|
+
const AGT_AGENT_ID = process.env.AGT_AGENT_ID; // Agent UUID
|
|
9
|
+
const AGT_AGENT_CODE_NAME = process.env.AGT_AGENT_CODE_NAME; // Agent code_name (kebab-case)
|
|
10
|
+
// Initial token from provisioning — will be refreshed automatically
|
|
11
|
+
let AGT_TOKEN = process.env.AGT_TOKEN ?? '';
|
|
12
|
+
if (!AGT_HOST || !AGT_AGENT_ID || (!AGT_TOKEN && !AGT_API_KEY)) {
|
|
13
|
+
console.error('augmented-mcp: Missing required env vars. Need AGT_HOST, AGT_AGENT_ID, and AGT_TOKEN or AGT_API_KEY');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
// ── Token refresh ───────────────────────────────────────────────────────────
|
|
17
|
+
let tokenExpiresAt = Date.now() + 50 * 60_000; // assume ~50min remaining on initial token
|
|
18
|
+
async function getToken() {
|
|
19
|
+
// If we have an API key and the token is expiring soon (within 5 min), refresh
|
|
20
|
+
if (AGT_API_KEY && Date.now() > tokenExpiresAt - 5 * 60_000) {
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(`${AGT_HOST}/host/exchange`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25
|
+
body: JSON.stringify({ host_key: AGT_API_KEY }),
|
|
26
|
+
});
|
|
27
|
+
if (res.ok) {
|
|
28
|
+
const data = (await res.json());
|
|
29
|
+
AGT_TOKEN = data.token;
|
|
30
|
+
tokenExpiresAt = new Date(data.expires_at).getTime();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Use existing token if refresh fails
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return AGT_TOKEN;
|
|
38
|
+
}
|
|
39
|
+
// ── API client ──────────────────────────────────────────────────────────────
|
|
40
|
+
async function apiPost(path, body) {
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
43
|
+
const token = await getToken();
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(`${AGT_HOST}${path}`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
Authorization: `Bearer ${token}`,
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify(body),
|
|
52
|
+
signal: controller.signal,
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const text = await res.text().catch(() => res.statusText);
|
|
56
|
+
throw new Error(`API ${path} returned ${res.status}: ${text}`);
|
|
57
|
+
}
|
|
58
|
+
return (await res.json());
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
62
|
+
throw new Error(`API ${path} timed out after 15s`);
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
clearTimeout(timeout);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// ── MCP Server ──────────────────────────────────────────────────────────────
|
|
71
|
+
const server = new McpServer({
|
|
72
|
+
name: 'augmented',
|
|
73
|
+
version: '0.1.0',
|
|
74
|
+
});
|
|
75
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
76
|
+
// KANBAN TOOLS
|
|
77
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
78
|
+
// ── kanban.list ─────────────────────────────────────────────────────────────
|
|
79
|
+
server.tool('kanban.list', 'List kanban board items for this agent. Returns active items and recently completed items (last 7 days).', {}, async () => {
|
|
80
|
+
const data = await apiPost('/host/my-kanban', {
|
|
81
|
+
agent_id: AGT_AGENT_ID,
|
|
82
|
+
});
|
|
83
|
+
if (!data.items.length) {
|
|
84
|
+
return { content: [{ type: 'text', text: 'Board is empty.' }] };
|
|
85
|
+
}
|
|
86
|
+
const grouped = groupByStatus(data.items);
|
|
87
|
+
const lines = [];
|
|
88
|
+
for (const status of ['in_progress', 'today', 'backlog', 'done']) {
|
|
89
|
+
const items = grouped[status];
|
|
90
|
+
if (!items?.length)
|
|
91
|
+
continue;
|
|
92
|
+
lines.push(`\n## ${statusLabel(status)} (${items.length})`);
|
|
93
|
+
for (const item of items) {
|
|
94
|
+
const pri = priorityLabel(item.priority);
|
|
95
|
+
const est = item.estimated_minutes ? ` ~${item.estimated_minutes}min` : '';
|
|
96
|
+
const del = item.deliverable ? ` → ${item.deliverable}` : '';
|
|
97
|
+
const res = item.result ? ` ✓ ${item.result}` : '';
|
|
98
|
+
lines.push(`- [${pri}] ${item.title}${est}${del}${res} (id: ${item.id})`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
102
|
+
});
|
|
103
|
+
// ── kanban.add ──────────────────────────────────────────────────────────────
|
|
104
|
+
server.tool('kanban.add', 'Add a new item to the kanban board.', {
|
|
105
|
+
title: z.string().describe('Item title (max 200 chars)'),
|
|
106
|
+
description: z.string().optional().describe('Detailed description'),
|
|
107
|
+
priority: z
|
|
108
|
+
.number()
|
|
109
|
+
.int()
|
|
110
|
+
.min(1)
|
|
111
|
+
.max(3)
|
|
112
|
+
.optional()
|
|
113
|
+
.describe('Priority: 1=high, 2=medium (default), 3=low'),
|
|
114
|
+
status: z
|
|
115
|
+
.enum(['backlog', 'today', 'in_progress'])
|
|
116
|
+
.optional()
|
|
117
|
+
.describe('Initial status (default: today)'),
|
|
118
|
+
estimated_minutes: z.number().int().optional().describe('Estimated time in minutes'),
|
|
119
|
+
deliverable: z.string().optional().describe('Expected output/deliverable'),
|
|
120
|
+
source: z
|
|
121
|
+
.enum(['cron', 'chat', 'manual', 'integration'])
|
|
122
|
+
.optional()
|
|
123
|
+
.describe('Source of the item (default: manual)'),
|
|
124
|
+
source_integration: z.string().optional().describe('Integration name (e.g., "linear", "github") when source is "integration"'),
|
|
125
|
+
source_external_id: z.string().optional().describe('ID in the external system (e.g., "ENG-123")'),
|
|
126
|
+
source_url: z.string().optional().describe('Deep link URL to the external source'),
|
|
127
|
+
}, async (params) => {
|
|
128
|
+
const data = await apiPost('/host/kanban', {
|
|
129
|
+
agent_id: AGT_AGENT_ID,
|
|
130
|
+
add: [
|
|
131
|
+
{
|
|
132
|
+
title: params.title,
|
|
133
|
+
description: params.description,
|
|
134
|
+
priority: params.priority ?? 2,
|
|
135
|
+
status: params.status ?? 'today',
|
|
136
|
+
estimated_minutes: params.estimated_minutes,
|
|
137
|
+
deliverable: params.deliverable,
|
|
138
|
+
source: params.source ?? 'manual',
|
|
139
|
+
source_integration: params.source_integration,
|
|
140
|
+
source_external_id: params.source_external_id,
|
|
141
|
+
source_url: params.source_url,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: 'text',
|
|
149
|
+
text: data.ok
|
|
150
|
+
? `Added "${params.title}" to board (status: ${params.status ?? 'today'}).`
|
|
151
|
+
: 'Failed to add item.',
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
// ── kanban.move ─────────────────────────────────────────────────────────────
|
|
157
|
+
server.tool('kanban.move', 'Move a kanban item to a different status column.', {
|
|
158
|
+
id: z.string().optional().describe('Item UUID (preferred)'),
|
|
159
|
+
title: z.string().optional().describe('Item title for fuzzy match (if no id)'),
|
|
160
|
+
status: z
|
|
161
|
+
.enum(['backlog', 'today', 'in_progress', 'done'])
|
|
162
|
+
.describe('Target status'),
|
|
163
|
+
notes: z.string().optional().describe('Progress notes'),
|
|
164
|
+
}, async (params) => {
|
|
165
|
+
if (!params.id && !params.title) {
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: 'text', text: 'Error: provide either id or title.' }],
|
|
168
|
+
isError: true,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const data = await apiPost('/host/kanban', {
|
|
172
|
+
agent_id: AGT_AGENT_ID,
|
|
173
|
+
update: [
|
|
174
|
+
{
|
|
175
|
+
id: params.id,
|
|
176
|
+
title: params.title,
|
|
177
|
+
status: params.status,
|
|
178
|
+
notes: params.notes,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
const label = params.id ?? params.title;
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
{
|
|
186
|
+
type: 'text',
|
|
187
|
+
text: data.updated > 0
|
|
188
|
+
? `Moved "${label}" → ${params.status}.`
|
|
189
|
+
: `Item "${label}" not found.`,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
// ── kanban.update ───────────────────────────────────────────────────────────
|
|
195
|
+
server.tool('kanban.update', 'Update notes or result on a kanban item without changing its status.', {
|
|
196
|
+
id: z.string().optional().describe('Item UUID (preferred)'),
|
|
197
|
+
title: z.string().optional().describe('Item title for fuzzy match (if no id)'),
|
|
198
|
+
notes: z.string().optional().describe('Progress notes to append'),
|
|
199
|
+
result: z.string().optional().describe('Result/output produced'),
|
|
200
|
+
}, async (params) => {
|
|
201
|
+
if (!params.id && !params.title) {
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: 'text', text: 'Error: provide either id or title.' }],
|
|
204
|
+
isError: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// We need a status for the API — fetch current status first
|
|
208
|
+
const board = await apiPost('/host/my-kanban', {
|
|
209
|
+
agent_id: AGT_AGENT_ID,
|
|
210
|
+
});
|
|
211
|
+
const match = board.items.find((i) => i.id === params.id || i.title === params.title);
|
|
212
|
+
if (!match) {
|
|
213
|
+
return {
|
|
214
|
+
content: [
|
|
215
|
+
{ type: 'text', text: `Item "${params.id ?? params.title}" not found.` },
|
|
216
|
+
],
|
|
217
|
+
isError: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const data = await apiPost('/host/kanban', {
|
|
221
|
+
agent_id: AGT_AGENT_ID,
|
|
222
|
+
update: [
|
|
223
|
+
{
|
|
224
|
+
id: match.id,
|
|
225
|
+
status: match.status,
|
|
226
|
+
notes: params.notes,
|
|
227
|
+
result: params.result,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
content: [
|
|
233
|
+
{
|
|
234
|
+
type: 'text',
|
|
235
|
+
text: data.updated > 0 ? `Updated "${match.title}".` : 'Update failed.',
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
// ── kanban.done ─────────────────────────────────────────────────────────────
|
|
241
|
+
server.tool('kanban.done', 'Mark a kanban item as done with an optional result.', {
|
|
242
|
+
id: z.string().optional().describe('Item UUID (preferred)'),
|
|
243
|
+
title: z.string().optional().describe('Item title for fuzzy match (if no id)'),
|
|
244
|
+
result: z.string().optional().describe('What was produced/delivered'),
|
|
245
|
+
notes: z.string().optional().describe('Completion notes'),
|
|
246
|
+
}, async (params) => {
|
|
247
|
+
if (!params.id && !params.title) {
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: 'text', text: 'Error: provide either id or title.' }],
|
|
250
|
+
isError: true,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const data = await apiPost('/host/kanban', {
|
|
254
|
+
agent_id: AGT_AGENT_ID,
|
|
255
|
+
update: [
|
|
256
|
+
{
|
|
257
|
+
id: params.id,
|
|
258
|
+
title: params.title,
|
|
259
|
+
status: 'done',
|
|
260
|
+
result: params.result,
|
|
261
|
+
notes: params.notes,
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
});
|
|
265
|
+
const label = params.id ?? params.title;
|
|
266
|
+
return {
|
|
267
|
+
content: [
|
|
268
|
+
{
|
|
269
|
+
type: 'text',
|
|
270
|
+
text: data.updated > 0
|
|
271
|
+
? `Marked "${label}" as done.${params.result ? ` Result: ${params.result}` : ''}`
|
|
272
|
+
: `Item "${label}" not found.`,
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
278
|
+
// STATUS TOOLS
|
|
279
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
280
|
+
// ── status.standup ──────────────────────────────────────────────────────────
|
|
281
|
+
server.tool('status.standup', 'Submit a daily standup update with yesterday, today, and blockers.', {
|
|
282
|
+
yesterday: z.string().describe('What was accomplished yesterday/last session'),
|
|
283
|
+
today: z.string().describe('What is planned for today/this session'),
|
|
284
|
+
blockers: z.string().optional().describe('Any blockers or issues (default: none)'),
|
|
285
|
+
}, async (params) => {
|
|
286
|
+
if (!AGT_AGENT_CODE_NAME) {
|
|
287
|
+
return {
|
|
288
|
+
content: [{ type: 'text', text: 'Error: AGT_AGENT_CODE_NAME not configured.' }],
|
|
289
|
+
isError: true,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const data = await apiPost('/host/agent-status', {
|
|
293
|
+
agent_code_name: AGT_AGENT_CODE_NAME,
|
|
294
|
+
standup: {
|
|
295
|
+
yesterday: params.yesterday,
|
|
296
|
+
today: params.today,
|
|
297
|
+
blockers: params.blockers ?? 'None',
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
content: [
|
|
302
|
+
{
|
|
303
|
+
type: 'text',
|
|
304
|
+
text: data.ok
|
|
305
|
+
? 'Standup submitted successfully.'
|
|
306
|
+
: 'Failed to submit standup.',
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
// ── status.update ───────────────────────────────────────────────────────────
|
|
312
|
+
server.tool('status.update', 'Report current task progress or status update.', {
|
|
313
|
+
current_tasks: z.string().describe('Summary of what you are currently working on'),
|
|
314
|
+
}, async (params) => {
|
|
315
|
+
if (!AGT_AGENT_CODE_NAME) {
|
|
316
|
+
return {
|
|
317
|
+
content: [{ type: 'text', text: 'Error: AGT_AGENT_CODE_NAME not configured.' }],
|
|
318
|
+
isError: true,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const data = await apiPost('/host/agent-status', {
|
|
322
|
+
agent_code_name: AGT_AGENT_CODE_NAME,
|
|
323
|
+
current_tasks: params.current_tasks,
|
|
324
|
+
});
|
|
325
|
+
return {
|
|
326
|
+
content: [
|
|
327
|
+
{
|
|
328
|
+
type: 'text',
|
|
329
|
+
text: data.ok
|
|
330
|
+
? 'Status updated.'
|
|
331
|
+
: 'Failed to update status.',
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
337
|
+
// DRIFT & TOKEN TOOLS
|
|
338
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
339
|
+
// ── drift.report ────────────────────────────────────────────────────────────
|
|
340
|
+
server.tool('drift.report', 'Report configuration drift detected in local agent files (CHARTER.md, TOOLS.md, etc.). Compares local file hashes against API-side versions and reports any differences.', {
|
|
341
|
+
drifted_files: z
|
|
342
|
+
.array(z.string())
|
|
343
|
+
.describe('List of filenames that have drifted from provisioned state (e.g. ["CHARTER.md", "TOOLS.md"])'),
|
|
344
|
+
local_hashes: z
|
|
345
|
+
.record(z.string())
|
|
346
|
+
.optional()
|
|
347
|
+
.describe('Map of filename to SHA-256 hash of local content'),
|
|
348
|
+
}, async (params) => {
|
|
349
|
+
const data = await apiPost('/host/drift', {
|
|
350
|
+
agent_id: AGT_AGENT_ID,
|
|
351
|
+
drifted_files: params.drifted_files,
|
|
352
|
+
local_hashes: params.local_hashes ?? {},
|
|
353
|
+
});
|
|
354
|
+
return {
|
|
355
|
+
content: [
|
|
356
|
+
{
|
|
357
|
+
type: 'text',
|
|
358
|
+
text: data.ok
|
|
359
|
+
? `Drift reported for ${params.drifted_files.length} file(s): ${params.drifted_files.join(', ')}`
|
|
360
|
+
: 'Failed to report drift.',
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
// ── token.refresh ───────────────────────────────────────────────────────────
|
|
366
|
+
server.tool('token.refresh', 'Request fresh OAuth tokens for an integration. Returns the new access token directly — no file-based handoff needed.', {
|
|
367
|
+
integration_id: z
|
|
368
|
+
.string()
|
|
369
|
+
.optional()
|
|
370
|
+
.describe('Specific integration UUID to refresh. If omitted, refreshes all expiring tokens.'),
|
|
371
|
+
}, async (params) => {
|
|
372
|
+
// Fetch all integrations for this agent
|
|
373
|
+
const integrations = await apiPost('/host/agent-integrations', {
|
|
374
|
+
agent_id: AGT_AGENT_ID,
|
|
375
|
+
});
|
|
376
|
+
// Filter to OAuth integrations; when no specific ID, only refresh expiring tokens
|
|
377
|
+
const TEN_MINUTES_MS = 10 * 60_000;
|
|
378
|
+
const oauthIntegrations = integrations.integrations.filter((i) => {
|
|
379
|
+
if (i.auth_type !== 'oauth2')
|
|
380
|
+
return false;
|
|
381
|
+
if (params.integration_id)
|
|
382
|
+
return i.id === params.integration_id;
|
|
383
|
+
// No specific ID — only refresh tokens expiring within 10 minutes
|
|
384
|
+
if (!i.credentials.token_expires_at)
|
|
385
|
+
return true;
|
|
386
|
+
return new Date(i.credentials.token_expires_at).getTime() - Date.now() < TEN_MINUTES_MS;
|
|
387
|
+
});
|
|
388
|
+
if (oauthIntegrations.length === 0) {
|
|
389
|
+
return {
|
|
390
|
+
content: [
|
|
391
|
+
{
|
|
392
|
+
type: 'text',
|
|
393
|
+
text: params.integration_id
|
|
394
|
+
? `No OAuth integration found with ID ${params.integration_id}.`
|
|
395
|
+
: 'No expiring OAuth tokens found for this agent.',
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const results = [];
|
|
401
|
+
for (const integration of oauthIntegrations) {
|
|
402
|
+
try {
|
|
403
|
+
const refreshed = await apiPost(`/integrations/oauth/${integration.id}/refresh`, {});
|
|
404
|
+
if (refreshed.ok && refreshed.access_token) {
|
|
405
|
+
results.push({
|
|
406
|
+
definition_id: integration.definition_id,
|
|
407
|
+
access_token: refreshed.access_token,
|
|
408
|
+
expires_at: refreshed.expires_at,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
results.push({ definition_id: integration.definition_id, error: true });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// Don't leak raw error messages into agent context
|
|
417
|
+
results.push({ definition_id: integration.definition_id, error: true });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Return tokens directly for programmatic consumption
|
|
421
|
+
const summary = results.map((r) => r.access_token
|
|
422
|
+
? `${r.definition_id}: refreshed (expires ${r.expires_at ?? 'unknown'})`
|
|
423
|
+
: `${r.definition_id}: refresh failed`);
|
|
424
|
+
return {
|
|
425
|
+
content: [
|
|
426
|
+
{
|
|
427
|
+
type: 'text',
|
|
428
|
+
text: JSON.stringify({
|
|
429
|
+
refreshed: results.filter((r) => r.access_token).map((r) => ({
|
|
430
|
+
definition_id: r.definition_id,
|
|
431
|
+
access_token: r.access_token,
|
|
432
|
+
expires_at: r.expires_at,
|
|
433
|
+
})),
|
|
434
|
+
failed: results.filter((r) => r.error).map((r) => r.definition_id),
|
|
435
|
+
summary: summary.join('; '),
|
|
436
|
+
}),
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
};
|
|
440
|
+
});
|
|
441
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
442
|
+
// ACPX TOOLS — Agent-to-agent communication via ACP
|
|
443
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
444
|
+
import { spawn } from 'node:child_process';
|
|
445
|
+
async function runAcpxCommand(args, cwd) {
|
|
446
|
+
return new Promise((resolve) => {
|
|
447
|
+
const child = spawn('acpx', args, {
|
|
448
|
+
cwd: cwd ?? process.cwd(),
|
|
449
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
450
|
+
});
|
|
451
|
+
let stdout = '';
|
|
452
|
+
let stderr = '';
|
|
453
|
+
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
454
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
455
|
+
child.on('close', (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
456
|
+
child.on('error', (err) => resolve({ exitCode: 1, stdout: '', stderr: err.message }));
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
// ── acpx.prompt ─────────────────────────────────────────────────────────────
|
|
460
|
+
server.tool('acpx.prompt', 'Send a prompt to a coding agent (claude, codex, openclaw) via ACP. Uses persistent sessions scoped to the working directory.', {
|
|
461
|
+
agent: z.enum(['claude', 'codex', 'openclaw']).describe('Coding agent to use'),
|
|
462
|
+
prompt: z.string().describe('The prompt/instruction to send'),
|
|
463
|
+
session_name: z.string().optional().describe('Named session for parallel workstreams (default: uses directory-scoped session)'),
|
|
464
|
+
approve_all: z.boolean().optional().describe('Auto-approve all tool permissions (default: false)'),
|
|
465
|
+
}, async (params) => {
|
|
466
|
+
const args = [];
|
|
467
|
+
if (params.approve_all)
|
|
468
|
+
args.push('--approve-all');
|
|
469
|
+
args.push('--format', 'json');
|
|
470
|
+
args.push(params.agent, 'prompt', params.prompt);
|
|
471
|
+
if (params.session_name)
|
|
472
|
+
args.push('-s', params.session_name);
|
|
473
|
+
const result = await runAcpxCommand(args);
|
|
474
|
+
if (result.exitCode !== 0) {
|
|
475
|
+
return {
|
|
476
|
+
content: [{ type: 'text', text: `acpx prompt failed (exit ${result.exitCode}): ${result.stderr || result.stdout}` }],
|
|
477
|
+
isError: true,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
return { content: [{ type: 'text', text: result.stdout || 'Prompt completed (no output).' }] };
|
|
481
|
+
});
|
|
482
|
+
// ── acpx.exec ───────────────────────────────────────────────────────────────
|
|
483
|
+
server.tool('acpx.exec', 'Run a one-shot task on a coding agent. Does not reuse sessions — fire and forget.', {
|
|
484
|
+
agent: z.enum(['claude', 'codex', 'openclaw']).describe('Coding agent to use'),
|
|
485
|
+
prompt: z.string().describe('The task to execute'),
|
|
486
|
+
}, async (params) => {
|
|
487
|
+
const args = ['--format', 'json', params.agent, 'exec', params.prompt];
|
|
488
|
+
const result = await runAcpxCommand(args);
|
|
489
|
+
if (result.exitCode !== 0) {
|
|
490
|
+
return {
|
|
491
|
+
content: [{ type: 'text', text: `acpx exec failed (exit ${result.exitCode}): ${result.stderr || result.stdout}` }],
|
|
492
|
+
isError: true,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
return { content: [{ type: 'text', text: result.stdout || 'Exec completed (no output).' }] };
|
|
496
|
+
});
|
|
497
|
+
// ── acpx.spawn ──────────────────────────────────────────────────────────────
|
|
498
|
+
server.tool('acpx.spawn', 'Ensure an ACP session exists for a coding agent. Creates one if needed, reuses existing.', {
|
|
499
|
+
agent: z.enum(['claude', 'codex', 'openclaw']).describe('Coding agent to use'),
|
|
500
|
+
session_name: z.string().optional().describe('Named session for parallel workstreams'),
|
|
501
|
+
}, async (params) => {
|
|
502
|
+
const args = ['--format', 'json', params.agent, 'sessions', 'ensure'];
|
|
503
|
+
if (params.session_name)
|
|
504
|
+
args.push('-s', params.session_name);
|
|
505
|
+
const result = await runAcpxCommand(args);
|
|
506
|
+
if (result.exitCode !== 0) {
|
|
507
|
+
return {
|
|
508
|
+
content: [{ type: 'text', text: `acpx spawn failed: ${result.stderr || result.stdout}` }],
|
|
509
|
+
isError: true,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
return { content: [{ type: 'text', text: result.stdout || 'Session ready.' }] };
|
|
513
|
+
});
|
|
514
|
+
// ── acpx.cancel ─────────────────────────────────────────────────────────────
|
|
515
|
+
server.tool('acpx.cancel', 'Send a cooperative cancel to the running ACP session. Does not destroy state.', {
|
|
516
|
+
agent: z.enum(['claude', 'codex', 'openclaw']).describe('Coding agent to cancel'),
|
|
517
|
+
session_name: z.string().optional().describe('Named session to cancel'),
|
|
518
|
+
}, async (params) => {
|
|
519
|
+
const args = [params.agent, 'cancel'];
|
|
520
|
+
if (params.session_name)
|
|
521
|
+
args.push('-s', params.session_name);
|
|
522
|
+
const result = await runAcpxCommand(args);
|
|
523
|
+
return {
|
|
524
|
+
content: [{
|
|
525
|
+
type: 'text',
|
|
526
|
+
text: result.exitCode === 0 ? 'Cancel sent.' : `Cancel failed: ${result.stderr || result.stdout}`,
|
|
527
|
+
}],
|
|
528
|
+
...(result.exitCode !== 0 ? { isError: true } : {}),
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
// ── acpx.status ─────────────────────────────────────────────────────────────
|
|
532
|
+
server.tool('acpx.status', 'Check the status of the current ACP session (running, idle, dead).', {
|
|
533
|
+
agent: z.enum(['claude', 'codex', 'openclaw']).describe('Coding agent to check'),
|
|
534
|
+
session_name: z.string().optional().describe('Named session to check'),
|
|
535
|
+
}, async (params) => {
|
|
536
|
+
const args = ['--format', 'json', params.agent, 'status'];
|
|
537
|
+
if (params.session_name)
|
|
538
|
+
args.push('-s', params.session_name);
|
|
539
|
+
const result = await runAcpxCommand(args);
|
|
540
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr || 'No session found.' }] };
|
|
541
|
+
});
|
|
542
|
+
// ── acpx.close ──────────────────────────────────────────────────────────────
|
|
543
|
+
server.tool('acpx.close', 'Soft-close an ACP session. Terminates the process but preserves history.', {
|
|
544
|
+
agent: z.enum(['claude', 'codex', 'openclaw']).describe('Coding agent session to close'),
|
|
545
|
+
session_name: z.string().optional().describe('Named session to close'),
|
|
546
|
+
}, async (params) => {
|
|
547
|
+
const args = [params.agent, 'sessions', 'close'];
|
|
548
|
+
if (params.session_name)
|
|
549
|
+
args.push(params.session_name);
|
|
550
|
+
const result = await runAcpxCommand(args);
|
|
551
|
+
return {
|
|
552
|
+
content: [{
|
|
553
|
+
type: 'text',
|
|
554
|
+
text: result.exitCode === 0 ? 'Session closed.' : `Close failed: ${result.stderr || result.stdout}`,
|
|
555
|
+
}],
|
|
556
|
+
...(result.exitCode !== 0 ? { isError: true } : {}),
|
|
557
|
+
};
|
|
558
|
+
});
|
|
559
|
+
function groupByStatus(items) {
|
|
560
|
+
const groups = {};
|
|
561
|
+
for (const item of items) {
|
|
562
|
+
(groups[item.status] ??= []).push(item);
|
|
563
|
+
}
|
|
564
|
+
return groups;
|
|
565
|
+
}
|
|
566
|
+
function statusLabel(status) {
|
|
567
|
+
const labels = {
|
|
568
|
+
backlog: 'Backlog',
|
|
569
|
+
today: 'Today',
|
|
570
|
+
in_progress: 'In Progress',
|
|
571
|
+
done: 'Done',
|
|
572
|
+
};
|
|
573
|
+
return labels[status] ?? status;
|
|
574
|
+
}
|
|
575
|
+
function priorityLabel(priority) {
|
|
576
|
+
return priority === 1 ? 'HIGH' : priority === 3 ? 'LOW' : 'MED';
|
|
577
|
+
}
|
|
578
|
+
// ── Start server ────────────────────────────────────────────────────────────
|
|
579
|
+
async function main() {
|
|
580
|
+
const transport = new StdioServerTransport();
|
|
581
|
+
await server.connect(transport);
|
|
582
|
+
}
|
|
583
|
+
main().catch((err) => {
|
|
584
|
+
console.error('augmented-mcp: Fatal error:', err);
|
|
585
|
+
process.exit(1);
|
|
586
|
+
});
|
|
587
|
+
//# sourceMappingURL=index.js.map
|