capyai 0.4.0 → 0.5.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/AGENTS.md +9 -121
- package/README.md +3 -1
- package/bin/capy.ts +2 -0
- package/package.json +1 -1
- package/skills/capy/SKILL.md +235 -64
- package/src/api.ts +45 -18
- package/src/commands/_shared.ts +4 -0
- package/src/commands/diff-pr.ts +6 -1
- package/src/commands/monitoring.ts +16 -10
- package/src/commands/quality.ts +5 -6
- package/src/commands/setup.ts +46 -0
- package/src/commands/tasks.ts +4 -2
- package/src/commands/threads.ts +7 -3
- package/src/commands/triage.ts +271 -0
- package/src/config.ts +1 -3
- package/src/github.ts +1 -1
- package/src/mcp.ts +145 -22
- package/src/types.ts +0 -2
- package/src/watch.ts +4 -4
package/src/mcp.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import * as api from "./api.js";
|
|
6
6
|
import { CapyError } from "./api.js";
|
|
7
7
|
import * as config from "./config.js";
|
|
8
|
+
import { isThreadId } from "./commands/_shared.js";
|
|
8
9
|
|
|
9
10
|
const require = createRequire(import.meta.url);
|
|
10
11
|
const { version } = require("../package.json");
|
|
@@ -26,10 +27,6 @@ function structured(data: Record<string, unknown>) {
|
|
|
26
27
|
return { structuredContent: data, content: [{ type: "text" as const, text: JSON.stringify(data) }] };
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
function isThreadId(id: string): boolean {
|
|
30
|
-
return id.length > 20 || (id.length > 10 && !id.match(/^[A-Z]+-\d+$/));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
30
|
// --- Orchestration ---
|
|
34
31
|
|
|
35
32
|
server.registerTool("capy_captain", {
|
|
@@ -89,12 +86,17 @@ server.registerTool("capy_wait", {
|
|
|
89
86
|
: new Set(["needs_review", "archived", "completed", "failed"]);
|
|
90
87
|
|
|
91
88
|
const start = Date.now();
|
|
89
|
+
let lastData: any = null;
|
|
92
90
|
while (Date.now() - start < timeoutMs) {
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
try {
|
|
92
|
+
lastData = isThread ? await api.getThread(id) : await api.getTask(id);
|
|
93
|
+
if (terminal.has(lastData.status)) return text(lastData);
|
|
94
|
+
} catch {
|
|
95
|
+
// transient error during poll, keep trying
|
|
96
|
+
}
|
|
95
97
|
await new Promise(r => setTimeout(r, intervalMs));
|
|
96
98
|
}
|
|
97
|
-
return { content: [{ type: "text" as const, text: JSON.stringify({ error: { code: "timeout", message: `Timed out after ${timeout || 300}s
|
|
99
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ error: { code: "timeout", message: `Timed out after ${timeout || 300}s`, lastStatus: lastData?.status || "unknown" } }) }], isError: true as const };
|
|
98
100
|
} catch (e) { return err(e); }
|
|
99
101
|
});
|
|
100
102
|
|
|
@@ -103,15 +105,6 @@ server.registerTool("capy_review", {
|
|
|
103
105
|
inputSchema: {
|
|
104
106
|
id: z.string().describe("Task ID"),
|
|
105
107
|
},
|
|
106
|
-
outputSchema: {
|
|
107
|
-
task: z.string(),
|
|
108
|
-
quality: z.object({
|
|
109
|
-
pass: z.boolean(),
|
|
110
|
-
passed: z.number(),
|
|
111
|
-
total: z.number(),
|
|
112
|
-
summary: z.string(),
|
|
113
|
-
}),
|
|
114
|
-
},
|
|
115
108
|
annotations: { readOnlyHint: true },
|
|
116
109
|
}, async ({ id }) => {
|
|
117
110
|
try {
|
|
@@ -134,6 +127,12 @@ server.registerTool("capy_approve", {
|
|
|
134
127
|
outputSchema: {
|
|
135
128
|
task: z.string(),
|
|
136
129
|
approved: z.boolean(),
|
|
130
|
+
quality: z.object({
|
|
131
|
+
pass: z.boolean(),
|
|
132
|
+
passed: z.number(),
|
|
133
|
+
total: z.number(),
|
|
134
|
+
summary: z.string(),
|
|
135
|
+
}),
|
|
137
136
|
},
|
|
138
137
|
annotations: { openWorldHint: true },
|
|
139
138
|
}, async ({ id, force }) => {
|
|
@@ -146,13 +145,12 @@ server.registerTool("capy_approve", {
|
|
|
146
145
|
|
|
147
146
|
if (approved && cfg.approveCommand) {
|
|
148
147
|
try {
|
|
149
|
-
const {
|
|
150
|
-
const
|
|
148
|
+
const { execSync } = await import("node:child_process");
|
|
149
|
+
const expanded = cfg.approveCommand
|
|
151
150
|
.replace("{task}", task.identifier || task.id)
|
|
152
|
-
.replace("{title}", task.title || "")
|
|
153
|
-
.replace("{pr}", String(task.pullRequest?.number || ""))
|
|
154
|
-
|
|
155
|
-
execFileSync(parts[0], parts.slice(1), { encoding: "utf8", timeout: 15000, stdio: "pipe" });
|
|
151
|
+
.replace("{title}", (task.title || "").replace(/'/g, "'\\''"))
|
|
152
|
+
.replace("{pr}", String(task.pullRequest?.number || ""));
|
|
153
|
+
execSync(expanded, { timeout: 15000, stdio: "pipe" });
|
|
156
154
|
} catch {}
|
|
157
155
|
}
|
|
158
156
|
|
|
@@ -200,6 +198,91 @@ server.registerTool("capy_retry", {
|
|
|
200
198
|
} catch (e) { return err(e); }
|
|
201
199
|
});
|
|
202
200
|
|
|
201
|
+
server.registerTool("capy_triage", {
|
|
202
|
+
description: "Actionable triage of all tasks. Categorizes into: merged, ready, needs_pr, stuck, backlog, in_progress. Use brief=true for fast mode (skips diff fetching, ~2x faster).",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
ids: z.array(z.string()).optional().describe("Specific task IDs to triage. Omit for all tasks."),
|
|
205
|
+
brief: z.boolean().optional().describe("Skip diff fetching for speed. Categories based on status + PR state only."),
|
|
206
|
+
},
|
|
207
|
+
annotations: { readOnlyHint: true },
|
|
208
|
+
}, async ({ ids, brief }) => {
|
|
209
|
+
try {
|
|
210
|
+
const github = await import("./github.js");
|
|
211
|
+
const cfg = config.load();
|
|
212
|
+
|
|
213
|
+
let tasks: any[];
|
|
214
|
+
if (ids?.length) {
|
|
215
|
+
tasks = await Promise.all(ids.map(id => api.getTask(id)));
|
|
216
|
+
} else {
|
|
217
|
+
const data = await api.listTasks({ limit: 100 });
|
|
218
|
+
tasks = data.items || [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const enriched = await Promise.all(tasks.map(async (task: any) => {
|
|
222
|
+
const id = task.identifier || task.id;
|
|
223
|
+
let detail: any = task;
|
|
224
|
+
let diff: any = null;
|
|
225
|
+
try { if (!task.jams) detail = await api.getTask(id); } catch {}
|
|
226
|
+
if (!brief) { try { diff = await api.getDiff(id); } catch {} }
|
|
227
|
+
|
|
228
|
+
if (detail.pullRequest?.number && detail.pullRequest.state === "closed") {
|
|
229
|
+
const repo = detail.pullRequest.repoFullName || cfg.repos[0]?.repoFullName;
|
|
230
|
+
if (repo) {
|
|
231
|
+
const ghPR = github.getPR(repo, detail.pullRequest.number);
|
|
232
|
+
if (ghPR) detail.pullRequest.state = ghPR.state.toLowerCase();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const lastJam = (detail.jams || []).at(-1);
|
|
237
|
+
const credits = lastJam?.credits;
|
|
238
|
+
const pr = detail.pullRequest?.number ? { number: detail.pullRequest.number, state: detail.pullRequest.state || "?", url: detail.pullRequest.url } : null;
|
|
239
|
+
const diffStats = diff?.stats ? { files: diff.stats.files || 0, additions: diff.stats.additions || 0, deletions: diff.stats.deletions || 0 } : null;
|
|
240
|
+
|
|
241
|
+
let category: string;
|
|
242
|
+
if (detail.status === "backlog") category = "backlog";
|
|
243
|
+
else if (detail.status === "in_progress") category = "in_progress";
|
|
244
|
+
else if (pr?.state === "merged") category = "merged";
|
|
245
|
+
else if (pr && pr.state === "open") category = "ready";
|
|
246
|
+
else if (diffStats && diffStats.files > 0 && !pr) category = "needs_pr";
|
|
247
|
+
else if (detail.status === "needs_review" && !pr && (!diffStats || diffStats.files === 0)) category = brief ? "needs_pr" : "stuck";
|
|
248
|
+
else if (detail.status === "needs_review" && pr) category = "ready";
|
|
249
|
+
else if (detail.status === "needs_review" && !pr) category = "needs_pr";
|
|
250
|
+
else category = "stuck";
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
identifier: detail.identifier || id,
|
|
254
|
+
title: detail.title || "",
|
|
255
|
+
status: detail.status,
|
|
256
|
+
labels: detail.labels || [],
|
|
257
|
+
category,
|
|
258
|
+
pr,
|
|
259
|
+
diff: diffStats,
|
|
260
|
+
jam: lastJam ? { model: lastJam.model || "?", status: lastJam.status || "?", credits: { llm: typeof credits === "object" ? (credits?.llm ?? 0) : (credits || 0), vm: typeof credits === "object" ? (credits?.vm ?? 0) : 0 } } : null,
|
|
261
|
+
};
|
|
262
|
+
}));
|
|
263
|
+
|
|
264
|
+
const summary = {
|
|
265
|
+
total: enriched.length,
|
|
266
|
+
merged: enriched.filter((t: any) => t.category === "merged").length,
|
|
267
|
+
ready: enriched.filter((t: any) => t.category === "ready").length,
|
|
268
|
+
needs_pr: enriched.filter((t: any) => t.category === "needs_pr").length,
|
|
269
|
+
stuck: enriched.filter((t: any) => t.category === "stuck").length,
|
|
270
|
+
backlog: enriched.filter((t: any) => t.category === "backlog").length,
|
|
271
|
+
in_progress: enriched.filter((t: any) => t.category === "in_progress").length,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const recs: string[] = [];
|
|
275
|
+
const needsPr = enriched.filter((t: any) => t.category === "needs_pr");
|
|
276
|
+
const stuck = enriched.filter((t: any) => t.category === "stuck");
|
|
277
|
+
const ready = enriched.filter((t: any) => t.category === "ready");
|
|
278
|
+
if (needsPr.length) recs.push(`Create PRs: ${needsPr.map((t: any) => t.identifier).join(", ")}`);
|
|
279
|
+
if (ready.length) recs.push(`Review + approve: ${ready.map((t: any) => t.identifier).join(", ")}`);
|
|
280
|
+
if (stuck.length) recs.push(`Retry or stop: ${stuck.map((t: any) => t.identifier).join(", ")} (no diff produced)`);
|
|
281
|
+
|
|
282
|
+
return text({ summary, tasks: enriched, recommendations: recs });
|
|
283
|
+
} catch (e) { return err(e); }
|
|
284
|
+
});
|
|
285
|
+
|
|
203
286
|
// --- Status & monitoring ---
|
|
204
287
|
|
|
205
288
|
server.registerTool("capy_status", {
|
|
@@ -382,6 +465,35 @@ server.registerTool("capy_re_review", {
|
|
|
382
465
|
} catch (e) { return err(e); }
|
|
383
466
|
});
|
|
384
467
|
|
|
468
|
+
server.registerTool("capy_projects", {
|
|
469
|
+
description: "List all projects accessible with the current API key",
|
|
470
|
+
inputSchema: {},
|
|
471
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
472
|
+
}, async () => {
|
|
473
|
+
try {
|
|
474
|
+
const data = await api.listProjectsAuth();
|
|
475
|
+
return text(data);
|
|
476
|
+
} catch (e) { return err(e); }
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
server.registerTool("capy_project", {
|
|
480
|
+
description: "Get project details (repos, task code, config)",
|
|
481
|
+
inputSchema: {
|
|
482
|
+
id: z.string().optional().describe("Project ID (defaults to current project)"),
|
|
483
|
+
},
|
|
484
|
+
outputSchema: {
|
|
485
|
+
id: z.string(),
|
|
486
|
+
name: z.string(),
|
|
487
|
+
taskCode: z.string(),
|
|
488
|
+
},
|
|
489
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
490
|
+
}, async ({ id }) => {
|
|
491
|
+
try {
|
|
492
|
+
const data = await api.getProject(id);
|
|
493
|
+
return structured({ id: data.id, name: data.name, taskCode: data.taskCode, repos: data.repos as unknown as Record<string, unknown>, createdAt: data.createdAt, updatedAt: data.updatedAt });
|
|
494
|
+
} catch (e) { return err(e); }
|
|
495
|
+
});
|
|
496
|
+
|
|
385
497
|
server.registerTool("capy_models", {
|
|
386
498
|
description: "List available AI models",
|
|
387
499
|
inputSchema: {},
|
|
@@ -423,6 +535,17 @@ server.registerTool("capy_pool_update", {
|
|
|
423
535
|
} catch (e) { return err(e); }
|
|
424
536
|
});
|
|
425
537
|
|
|
538
|
+
server.registerTool("capy_pool_test", {
|
|
539
|
+
description: "Test warm pool VM boot with setup commands",
|
|
540
|
+
inputSchema: {},
|
|
541
|
+
annotations: { openWorldHint: true },
|
|
542
|
+
}, async () => {
|
|
543
|
+
try {
|
|
544
|
+
const data = await api.testWarmPool();
|
|
545
|
+
return text(data);
|
|
546
|
+
} catch (e) { return err(e); }
|
|
547
|
+
});
|
|
548
|
+
|
|
426
549
|
server.registerTool("capy_pool_instances", {
|
|
427
550
|
description: "List warm pool VM instances",
|
|
428
551
|
inputSchema: {
|
package/src/types.ts
CHANGED
package/src/watch.ts
CHANGED
|
@@ -51,11 +51,11 @@ export function list(): WatchEntry[] {
|
|
|
51
51
|
|
|
52
52
|
export function notify(text: string): boolean {
|
|
53
53
|
const cfg = config.load();
|
|
54
|
-
const cmd = cfg.notifyCommand
|
|
54
|
+
const cmd = cfg.notifyCommand;
|
|
55
|
+
if (!cmd) return false;
|
|
55
56
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
encoding: "utf8", timeout: 15000, stdio: "pipe",
|
|
57
|
+
execSync(cmd.replace("{text}", text.replace(/'/g, "'\\''")), {
|
|
58
|
+
timeout: 15000, stdio: "pipe",
|
|
59
59
|
});
|
|
60
60
|
return true;
|
|
61
61
|
} catch { return false; }
|