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/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
- const data = isThread ? await api.getThread(id) : await api.getTask(id);
94
- if (terminal.has(data.status)) return text(data);
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` } }) }], isError: true as const };
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 { execFileSync } = await import("node:child_process");
150
- const parts = cfg.approveCommand
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
- .split(/\s+/);
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
@@ -1,8 +1,6 @@
1
1
  export interface QualityConfig {
2
- minReviewScore: number;
3
2
  requireCI: boolean;
4
3
  requireTests: boolean;
5
- requireLinearLink: boolean;
6
4
  reviewProvider: string;
7
5
  }
8
6
 
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 || "openclaw system event --text {text} --mode now";
54
+ const cmd = cfg.notifyCommand;
55
+ if (!cmd) return false;
55
56
  try {
56
- const parts = cmd.replace("{text}", text).split(/\s+/);
57
- execFileSync(parts[0], parts.slice(1), {
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; }