capyai 0.3.7 → 0.4.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/AGENTS.md +13 -3
- package/README.md +3 -2
- package/bin/capy.ts +2 -0
- package/package.json +1 -1
- package/skills/capy/SKILL.md +1 -1
- package/src/api.ts +74 -9
- package/src/commands/pool.ts +128 -0
- package/src/commands/setup.ts +2 -0
- package/src/commands/triage.ts +266 -0
- package/src/mcp.ts +152 -4
package/AGENTS.md
CHANGED
|
@@ -81,7 +81,7 @@ Config file locations:
|
|
|
81
81
|
- Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
82
82
|
- Cursor: `.cursor/mcp.json`
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
21 MCP tools with full API parity:
|
|
85
85
|
|
|
86
86
|
| Tool | What it does | Annotations |
|
|
87
87
|
|------|-------------|-------------|
|
|
@@ -98,10 +98,14 @@ Config file locations:
|
|
|
98
98
|
| `capy_threads` | List threads (paginated) | readOnly, idempotent |
|
|
99
99
|
| `capy_thread_messages` | Read thread conversation history | readOnly, idempotent |
|
|
100
100
|
| `capy_diff` | View diff | readOnly |
|
|
101
|
-
| `capy_msg` | Message task/thread | openWorld |
|
|
101
|
+
| `capy_msg` | Message task/thread (supports attachments, model switch) | openWorld |
|
|
102
102
|
| `capy_stop` | Stop task/thread | destructive |
|
|
103
|
-
| `capy_pr` | Create PR | openWorld |
|
|
103
|
+
| `capy_pr` | Create PR (title, description, draft) | openWorld |
|
|
104
104
|
| `capy_models` | List models | readOnly, idempotent |
|
|
105
|
+
| `capy_pool_status` | Warm pool config + VM status | readOnly, idempotent |
|
|
106
|
+
| `capy_pool_update` | Update warm pool config | openWorld |
|
|
107
|
+
| `capy_pool_instances` | List warm pool VMs | readOnly, idempotent |
|
|
108
|
+
| `capy_pool_clear` | Clear/refresh warm pool | destructive |
|
|
105
109
|
|
|
106
110
|
Tools with predictable outputs (`capy_captain`, `capy_build`, `capy_review`, `capy_approve`, `capy_retry`) declare `outputSchema` for typed structured content per the 2025-03-26 MCP spec.
|
|
107
111
|
|
|
@@ -227,6 +231,12 @@ Every command supports `--json` for structured output. Errors always return `{ "
|
|
|
227
231
|
| `capy threads msg <id> "<text>"` | Message a thread |
|
|
228
232
|
| `capy threads stop <id>` | Stop a thread |
|
|
229
233
|
| `capy threads messages <id>` | Read thread conversation history |
|
|
234
|
+
| `capy pool` | Warm pool status |
|
|
235
|
+
| `capy pool set --size=N --age=M` | Update warm pool config |
|
|
236
|
+
| `capy pool test` | Test VM boot |
|
|
237
|
+
| `capy pool instances [status]` | List pool VMs |
|
|
238
|
+
| `capy pool instance <id>` | VM detail + logs |
|
|
239
|
+
| `capy pool clear [--replenish]` | Clear/refresh pool |
|
|
230
240
|
| `capy config [key] [value]` | Get/set config |
|
|
231
241
|
| `capy models` | List available models |
|
|
232
242
|
| `capy tools` | Show all commands + env vars |
|
package/README.md
CHANGED
|
@@ -53,7 +53,8 @@ Every command supports `--json` for machine-readable output.
|
|
|
53
53
|
| `capy diff <id>` | View diff |
|
|
54
54
|
| `capy pr <id>` | Create PR for task |
|
|
55
55
|
| `capy watch <id>` | Cron poll + notify on completion |
|
|
56
|
-
| `capy threads [list\|get\|msg\|stop]` | Manage Captain threads |
|
|
56
|
+
| `capy threads [list\|get\|msg\|stop\|messages]` | Manage Captain threads |
|
|
57
|
+
| `capy pool [status\|set\|test\|instances\|clear]` | Manage warm pool VMs |
|
|
57
58
|
| `capy models` | List available models |
|
|
58
59
|
| `capy config [key] [value]` | Get/set config |
|
|
59
60
|
|
|
@@ -87,7 +88,7 @@ For agents that prefer MCP over CLI:
|
|
|
87
88
|
}
|
|
88
89
|
```
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
21 tools with full API parity, including warm pool management.
|
|
91
92
|
|
|
92
93
|
## Config
|
|
93
94
|
|
package/bin/capy.ts
CHANGED
|
@@ -27,6 +27,7 @@ const main = defineCommand({
|
|
|
27
27
|
models: () => import("../src/commands/setup.js").then(m => m.models),
|
|
28
28
|
tools: () => import("../src/commands/setup.js").then(m => m.tools),
|
|
29
29
|
status: () => import("../src/commands/setup.js").then(m => m.status),
|
|
30
|
+
triage: () => import("../src/commands/triage.js").then(m => m.triage),
|
|
30
31
|
review: () => import("../src/commands/quality.js").then(m => m.review),
|
|
31
32
|
"re-review": () => import("../src/commands/quality.js").then(m => m.reReview),
|
|
32
33
|
approve: () => import("../src/commands/quality.js").then(m => m.approve),
|
|
@@ -36,6 +37,7 @@ const main = defineCommand({
|
|
|
36
37
|
watches: () => import("../src/commands/monitoring.js").then(m => m.watches),
|
|
37
38
|
wait: () => import("../src/commands/monitoring.js").then(m => m.wait),
|
|
38
39
|
_poll: () => import("../src/commands/monitoring.js").then(m => m._poll),
|
|
40
|
+
pool: () => import("../src/commands/pool.js").then(m => m.default),
|
|
39
41
|
init: () => import("../src/commands/setup.js").then(m => m.init),
|
|
40
42
|
config: () => import("../src/commands/setup.js").then(m => m.config),
|
|
41
43
|
},
|
package/package.json
CHANGED
package/skills/capy/SKILL.md
CHANGED
package/src/api.ts
CHANGED
|
@@ -79,21 +79,24 @@ export async function getProject(id?: string): Promise<Project & { createdAt?: s
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// --- Threads ---
|
|
82
|
-
export async function createThread(prompt: string, model?: string, repos?: unknown[]): Promise<Thread> {
|
|
82
|
+
export async function createThread(prompt: string, model?: string, opts: { repos?: unknown[]; attachmentUrls?: string[] } = {}): Promise<Thread> {
|
|
83
83
|
const cfg = config.load();
|
|
84
84
|
return request("POST", "/threads", {
|
|
85
85
|
projectId: cfg.projectId,
|
|
86
86
|
prompt,
|
|
87
87
|
model: model || cfg.defaultModel,
|
|
88
|
-
repos: repos || cfg.repos,
|
|
88
|
+
repos: opts.repos || cfg.repos,
|
|
89
|
+
...(opts.attachmentUrls?.length ? { attachmentUrls: opts.attachmentUrls } : {}),
|
|
89
90
|
});
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
export async function listThreads(opts: { limit?: number; status?: string; cursor?: string } = {}): Promise<ListResponse<Thread>> {
|
|
93
|
+
export async function listThreads(opts: { limit?: number; status?: string; cursor?: string; prNumber?: number; branch?: string } = {}): Promise<ListResponse<Thread>> {
|
|
93
94
|
const cfg = config.load();
|
|
94
95
|
const p = new URLSearchParams({ projectId: cfg.projectId, limit: String(opts.limit || 10) });
|
|
95
96
|
if (opts.status) p.set("status", opts.status);
|
|
96
97
|
if (opts.cursor) p.set("cursor", opts.cursor);
|
|
98
|
+
if (opts.prNumber) p.set("prNumber", String(opts.prNumber));
|
|
99
|
+
if (opts.branch) p.set("branch", opts.branch);
|
|
97
100
|
return request("GET", `/threads?${p}`);
|
|
98
101
|
}
|
|
99
102
|
|
|
@@ -101,8 +104,12 @@ export async function getThread(id: string): Promise<Thread> {
|
|
|
101
104
|
return request("GET", `/threads/${id}`);
|
|
102
105
|
}
|
|
103
106
|
|
|
104
|
-
export async function messageThread(id: string, msg: string): Promise<unknown> {
|
|
105
|
-
return request("POST", `/threads/${id}/message`, {
|
|
107
|
+
export async function messageThread(id: string, msg: string, opts: { model?: string; attachmentUrls?: string[] } = {}): Promise<unknown> {
|
|
108
|
+
return request("POST", `/threads/${id}/message`, {
|
|
109
|
+
message: msg,
|
|
110
|
+
...(opts.model ? { model: opts.model } : {}),
|
|
111
|
+
...(opts.attachmentUrls?.length ? { attachmentUrls: opts.attachmentUrls } : {}),
|
|
112
|
+
});
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
export async function stopThread(id: string): Promise<unknown> {
|
|
@@ -115,7 +122,7 @@ export async function getThreadMessages(id: string, opts: { limit?: number } = {
|
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
// --- Tasks ---
|
|
118
|
-
export async function createTask(prompt: string, model?: string, opts: { title?: string; start?: boolean; labels?: string[] } = {}): Promise<Task> {
|
|
125
|
+
export async function createTask(prompt: string, model?: string, opts: { title?: string; start?: boolean; labels?: string[]; attachmentUrls?: string[] } = {}): Promise<Task> {
|
|
119
126
|
const cfg = config.load();
|
|
120
127
|
return request("POST", "/tasks", {
|
|
121
128
|
projectId: cfg.projectId,
|
|
@@ -125,14 +132,17 @@ export async function createTask(prompt: string, model?: string, opts: { title?:
|
|
|
125
132
|
model: model || cfg.defaultModel,
|
|
126
133
|
start: opts.start !== false,
|
|
127
134
|
...(opts.labels ? { labels: opts.labels } : {}),
|
|
135
|
+
...(opts.attachmentUrls?.length ? { attachmentUrls: opts.attachmentUrls } : {}),
|
|
128
136
|
});
|
|
129
137
|
}
|
|
130
138
|
|
|
131
|
-
export async function listTasks(opts: { limit?: number; status?: string; cursor?: string } = {}): Promise<ListResponse<Task>> {
|
|
139
|
+
export async function listTasks(opts: { limit?: number; status?: string; cursor?: string; prNumber?: number; branch?: string } = {}): Promise<ListResponse<Task>> {
|
|
132
140
|
const cfg = config.load();
|
|
133
141
|
const p = new URLSearchParams({ projectId: cfg.projectId, limit: String(opts.limit || 30) });
|
|
134
142
|
if (opts.status) p.set("status", opts.status);
|
|
135
143
|
if (opts.cursor) p.set("cursor", opts.cursor);
|
|
144
|
+
if (opts.prNumber) p.set("prNumber", String(opts.prNumber));
|
|
145
|
+
if (opts.branch) p.set("branch", opts.branch);
|
|
136
146
|
return request("GET", `/tasks?${p}`);
|
|
137
147
|
}
|
|
138
148
|
|
|
@@ -148,8 +158,11 @@ export async function stopTask(id: string, reason?: string): Promise<Task> {
|
|
|
148
158
|
return request("POST", `/tasks/${id}/stop`, reason ? { reason } : {});
|
|
149
159
|
}
|
|
150
160
|
|
|
151
|
-
export async function messageTask(id: string, msg: string): Promise<unknown> {
|
|
152
|
-
return request("POST", `/tasks/${id}/message`, {
|
|
161
|
+
export async function messageTask(id: string, msg: string, opts: { attachmentUrls?: string[] } = {}): Promise<unknown> {
|
|
162
|
+
return request("POST", `/tasks/${id}/message`, {
|
|
163
|
+
message: msg,
|
|
164
|
+
...(opts.attachmentUrls?.length ? { attachmentUrls: opts.attachmentUrls } : {}),
|
|
165
|
+
});
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
export async function createPR(id: string, opts: Record<string, unknown> = {}): Promise<PullRequestRef & { url?: string; number?: number; title?: string; headRef?: string; baseRef?: string }> {
|
|
@@ -163,3 +176,55 @@ export async function getDiff(id: string, mode = "run"): Promise<DiffData> {
|
|
|
163
176
|
export async function listModels(): Promise<{ models?: Model[] }> {
|
|
164
177
|
return request("GET", "/models");
|
|
165
178
|
}
|
|
179
|
+
|
|
180
|
+
// --- Warm Pool ---
|
|
181
|
+
export interface WarmPoolConfig {
|
|
182
|
+
enabled: boolean;
|
|
183
|
+
targetSize: number;
|
|
184
|
+
maxAgeMinutes: number;
|
|
185
|
+
branch?: string;
|
|
186
|
+
setupCommands?: string[];
|
|
187
|
+
instances?: WarmPoolInstance[];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface WarmPoolInstance {
|
|
191
|
+
id: string;
|
|
192
|
+
status: string;
|
|
193
|
+
createdAt?: string;
|
|
194
|
+
claimedAt?: string;
|
|
195
|
+
logs?: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function getWarmPool(projectId?: string): Promise<WarmPoolConfig> {
|
|
199
|
+
const pid = projectId || config.load().projectId;
|
|
200
|
+
return request("GET", `/projects/${pid}/warm-pool`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function updateWarmPool(update: { enabled?: boolean; targetSize?: number; maxAgeMinutes?: number; branch?: string; setupCommands?: string[] }, projectId?: string): Promise<WarmPoolConfig> {
|
|
204
|
+
const pid = projectId || config.load().projectId;
|
|
205
|
+
return request("PATCH", `/projects/${pid}/warm-pool`, update);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function testWarmPool(projectId?: string): Promise<{ instanceId: string; status: string }> {
|
|
209
|
+
const pid = projectId || config.load().projectId;
|
|
210
|
+
return request("POST", `/projects/${pid}/warm-pool/test`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function listWarmPoolInstances(opts: { status?: string } = {}, projectId?: string): Promise<WarmPoolInstance[]> {
|
|
214
|
+
const pid = projectId || config.load().projectId;
|
|
215
|
+
const p = new URLSearchParams();
|
|
216
|
+
if (opts.status) p.set("status", opts.status);
|
|
217
|
+
const qs = p.toString();
|
|
218
|
+
const data = await request("GET", `/projects/${pid}/warm-pool/instances${qs ? `?${qs}` : ""}`);
|
|
219
|
+
return Array.isArray(data) ? data : data.instances || data.items || [];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function getWarmPoolInstance(instanceId: string, projectId?: string): Promise<WarmPoolInstance> {
|
|
223
|
+
const pid = projectId || config.load().projectId;
|
|
224
|
+
return request("GET", `/projects/${pid}/warm-pool/instances/${instanceId}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function clearWarmPool(opts: { replenish?: boolean } = {}, projectId?: string): Promise<unknown> {
|
|
228
|
+
const pid = projectId || config.load().projectId;
|
|
229
|
+
return request("POST", `/projects/${pid}/warm-pool/clear`, opts.replenish != null ? { replenish: opts.replenish } : {});
|
|
230
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { jsonArg } from "./_shared.js";
|
|
3
|
+
|
|
4
|
+
const status = defineCommand({
|
|
5
|
+
meta: { name: "status", description: "Warm pool config and status" },
|
|
6
|
+
args: { ...jsonArg },
|
|
7
|
+
async run({ args }) {
|
|
8
|
+
const api = await import("../api.js");
|
|
9
|
+
const fmt = await import("../output.js");
|
|
10
|
+
const { log } = await import("@clack/prompts");
|
|
11
|
+
|
|
12
|
+
const pool = await api.getWarmPool();
|
|
13
|
+
if (args.json) { fmt.out(pool); return; }
|
|
14
|
+
|
|
15
|
+
log.info(`Warm pool: ${pool.enabled ? "enabled" : "disabled"}`);
|
|
16
|
+
console.log(` Target size: ${pool.targetSize}`);
|
|
17
|
+
console.log(` Max age: ${pool.maxAgeMinutes}min`);
|
|
18
|
+
if (pool.branch) console.log(` Branch: ${pool.branch}`);
|
|
19
|
+
if (pool.setupCommands?.length) {
|
|
20
|
+
console.log(` Setup: ${pool.setupCommands.length} commands`);
|
|
21
|
+
pool.setupCommands.forEach(c => console.log(` $ ${c}`));
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const set = defineCommand({
|
|
27
|
+
meta: { name: "set", description: "Update warm pool config" },
|
|
28
|
+
args: {
|
|
29
|
+
enabled: { type: "boolean", description: "Enable/disable" },
|
|
30
|
+
size: { type: "string", description: "Target pool size" },
|
|
31
|
+
age: { type: "string", description: "Max VM age in minutes" },
|
|
32
|
+
branch: { type: "string", description: "Branch for pool VMs" },
|
|
33
|
+
...jsonArg,
|
|
34
|
+
},
|
|
35
|
+
async run({ args }) {
|
|
36
|
+
const api = await import("../api.js");
|
|
37
|
+
const fmt = await import("../output.js");
|
|
38
|
+
const { log } = await import("@clack/prompts");
|
|
39
|
+
|
|
40
|
+
const update: Record<string, unknown> = {};
|
|
41
|
+
if (args.enabled !== undefined) update.enabled = args.enabled;
|
|
42
|
+
if (args.size) update.targetSize = parseInt(args.size);
|
|
43
|
+
if (args.age) update.maxAgeMinutes = parseInt(args.age);
|
|
44
|
+
if (args.branch) update.branch = args.branch;
|
|
45
|
+
|
|
46
|
+
const pool = await api.updateWarmPool(update as any);
|
|
47
|
+
if (args.json) { fmt.out(pool); return; }
|
|
48
|
+
log.success(`Updated: size=${pool.targetSize}, age=${pool.maxAgeMinutes}min, enabled=${pool.enabled}`);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const test = defineCommand({
|
|
53
|
+
meta: { name: "test", description: "Test VM boot with setup commands" },
|
|
54
|
+
args: { ...jsonArg },
|
|
55
|
+
async run({ args }) {
|
|
56
|
+
const api = await import("../api.js");
|
|
57
|
+
const fmt = await import("../output.js");
|
|
58
|
+
const { log } = await import("@clack/prompts");
|
|
59
|
+
|
|
60
|
+
const result = await api.testWarmPool();
|
|
61
|
+
if (args.json) { fmt.out(result); return; }
|
|
62
|
+
log.success(`Test VM started: ${result.instanceId} [${result.status}]`);
|
|
63
|
+
log.info(`Check status: capy pool instance ${result.instanceId}`);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const instances = defineCommand({
|
|
68
|
+
meta: { name: "instances", description: "List warm pool VMs", alias: "ls" },
|
|
69
|
+
args: {
|
|
70
|
+
status: { type: "positional", required: false, description: "Filter: ready, provisioning, failed, claimed" },
|
|
71
|
+
...jsonArg,
|
|
72
|
+
},
|
|
73
|
+
async run({ args }) {
|
|
74
|
+
const api = await import("../api.js");
|
|
75
|
+
const fmt = await import("../output.js");
|
|
76
|
+
|
|
77
|
+
const list = await api.listWarmPoolInstances({ status: args.status });
|
|
78
|
+
if (args.json) { fmt.out(list); return; }
|
|
79
|
+
if (!list.length) { console.log("No instances."); return; }
|
|
80
|
+
fmt.table(["ID", "STATUS", "CREATED"], list.map(i => [
|
|
81
|
+
i.id.slice(0, 16), i.status, i.createdAt || "?",
|
|
82
|
+
]));
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const instance = defineCommand({
|
|
87
|
+
meta: { name: "instance", description: "VM instance detail + logs" },
|
|
88
|
+
args: {
|
|
89
|
+
id: { type: "positional", description: "Instance ID", required: true },
|
|
90
|
+
...jsonArg,
|
|
91
|
+
},
|
|
92
|
+
async run({ args }) {
|
|
93
|
+
const api = await import("../api.js");
|
|
94
|
+
const fmt = await import("../output.js");
|
|
95
|
+
const { log } = await import("@clack/prompts");
|
|
96
|
+
|
|
97
|
+
const data = await api.getWarmPoolInstance(args.id);
|
|
98
|
+
if (args.json) { fmt.out(data); return; }
|
|
99
|
+
log.info(`Instance: ${data.id}\nStatus: ${data.status}\nCreated: ${data.createdAt || "?"}`);
|
|
100
|
+
if (data.claimedAt) console.log(`Claimed: ${data.claimedAt}`);
|
|
101
|
+
if (data.logs) {
|
|
102
|
+
console.log(`\nLogs:\n${data.logs}`);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const clear = defineCommand({
|
|
108
|
+
meta: { name: "clear", description: "Clear all warm pool VMs" },
|
|
109
|
+
args: {
|
|
110
|
+
replenish: { type: "boolean", description: "Replenish after clearing", default: false },
|
|
111
|
+
...jsonArg,
|
|
112
|
+
},
|
|
113
|
+
async run({ args }) {
|
|
114
|
+
const api = await import("../api.js");
|
|
115
|
+
const fmt = await import("../output.js");
|
|
116
|
+
const { log } = await import("@clack/prompts");
|
|
117
|
+
|
|
118
|
+
const result = await api.clearWarmPool({ replenish: args.replenish });
|
|
119
|
+
if (args.json) { fmt.out(result); return; }
|
|
120
|
+
log.success(`Pool cleared.${args.replenish ? " Replenishing..." : ""}`);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export default defineCommand({
|
|
125
|
+
meta: { name: "pool", description: "Manage warm pool VMs" },
|
|
126
|
+
default: "status",
|
|
127
|
+
subCommands: { status, set, test, instances, instance, clear },
|
|
128
|
+
});
|
package/src/commands/setup.ts
CHANGED
|
@@ -162,6 +162,7 @@ export const tools = defineCommand({
|
|
|
162
162
|
captain: { args: "<prompt>", desc: "Start Captain thread" },
|
|
163
163
|
build: { args: "<prompt>", desc: "Start Build agent (isolated)" },
|
|
164
164
|
threads: { args: "[list|get|msg|stop]", desc: "Manage threads" },
|
|
165
|
+
triage: { args: "[id,...]", desc: "Actionable triage with diffs + recs" },
|
|
165
166
|
status: { args: "", desc: "Dashboard" },
|
|
166
167
|
list: { args: "[status]", desc: "List tasks" },
|
|
167
168
|
get: { args: "<id>", desc: "Task details" },
|
|
@@ -178,6 +179,7 @@ export const tools = defineCommand({
|
|
|
178
179
|
watch: { args: "<id>", desc: "Poll + notify on completion" },
|
|
179
180
|
unwatch: { args: "<id>", desc: "Stop watching" },
|
|
180
181
|
watches: { args: "", desc: "List watches" },
|
|
182
|
+
pool: { args: "[status|set|test|...]", desc: "Manage warm pool VMs" },
|
|
181
183
|
models: { args: "", desc: "List models" },
|
|
182
184
|
tools: { args: "", desc: "This list" },
|
|
183
185
|
config: { args: "[key] [value]", desc: "Get/set config" },
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { jsonArg } from "./_shared.js";
|
|
3
|
+
|
|
4
|
+
interface TriageTask {
|
|
5
|
+
identifier: string;
|
|
6
|
+
title: string;
|
|
7
|
+
status: string;
|
|
8
|
+
labels: string[];
|
|
9
|
+
category: "merged" | "ready" | "needs_pr" | "stuck" | "backlog" | "in_progress";
|
|
10
|
+
pr?: { number: number; state: string; url?: string };
|
|
11
|
+
diff?: { files: number; additions: number; deletions: number };
|
|
12
|
+
jam?: { model: string; status: string; credits: { llm: number; vm: number } };
|
|
13
|
+
createdAt?: string;
|
|
14
|
+
updatedAt?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TriageResult {
|
|
18
|
+
summary: { total: number; merged: number; ready: number; needs_pr: number; stuck: number; backlog: number; in_progress: number };
|
|
19
|
+
tasks: TriageTask[];
|
|
20
|
+
recommendations: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const triage = defineCommand({
|
|
24
|
+
meta: { name: "triage", description: "Actionable status for all tasks with diffs, PR state, and recommendations" },
|
|
25
|
+
args: {
|
|
26
|
+
ids: { type: "positional", required: false, description: "Specific task IDs (comma-separated or space-separated)" },
|
|
27
|
+
...jsonArg,
|
|
28
|
+
},
|
|
29
|
+
async run({ args }) {
|
|
30
|
+
const api = await import("../api.js");
|
|
31
|
+
const config = await import("../config.js");
|
|
32
|
+
const github = await import("../github.js");
|
|
33
|
+
const fmt = await import("../output.js");
|
|
34
|
+
const { log, spinner } = await import("@clack/prompts");
|
|
35
|
+
|
|
36
|
+
const cfg = config.load();
|
|
37
|
+
|
|
38
|
+
// Get base task list
|
|
39
|
+
let tasks: any[];
|
|
40
|
+
if (args.ids) {
|
|
41
|
+
const ids = args.ids.split(/[,\s]+/).filter(Boolean);
|
|
42
|
+
tasks = await Promise.all(ids.map(id => api.getTask(id)));
|
|
43
|
+
} else {
|
|
44
|
+
const data = await api.listTasks({ limit: 100 });
|
|
45
|
+
tasks = data.items || [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!tasks.length) {
|
|
49
|
+
if (args.json) { fmt.out({ summary: { total: 0 }, tasks: [], recommendations: [] }); return; }
|
|
50
|
+
console.log("No tasks.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fetch detail + diff in parallel for all tasks
|
|
55
|
+
if (!args.json) {
|
|
56
|
+
const s = spinner();
|
|
57
|
+
s.start(`Loading ${tasks.length} tasks (details + diffs)...`);
|
|
58
|
+
var results = await enrichTasks(api, tasks, cfg);
|
|
59
|
+
s.stop(`${tasks.length} tasks loaded`);
|
|
60
|
+
} else {
|
|
61
|
+
var results = await enrichTasks(api, tasks, cfg);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Cross-ref PR state with GitHub
|
|
65
|
+
for (const r of results) {
|
|
66
|
+
if (r.pr && r.pr.state === "closed") {
|
|
67
|
+
const repo = r._raw?.pullRequest?.repoFullName || cfg.repos[0]?.repoFullName;
|
|
68
|
+
if (repo) {
|
|
69
|
+
const ghPR = github.getPR(repo, r.pr.number);
|
|
70
|
+
if (ghPR) r.pr.state = ghPR.state.toLowerCase();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Categorize
|
|
76
|
+
const triaged: TriageTask[] = results.map(r => {
|
|
77
|
+
let category: TriageTask["category"];
|
|
78
|
+
if (r.status === "backlog") {
|
|
79
|
+
category = "backlog";
|
|
80
|
+
} else if (r.status === "in_progress") {
|
|
81
|
+
category = "in_progress";
|
|
82
|
+
} else if (r.pr?.state === "merged") {
|
|
83
|
+
category = "merged";
|
|
84
|
+
} else if (r.pr && r.pr.state === "open") {
|
|
85
|
+
category = "ready";
|
|
86
|
+
} else if (r.diff && r.diff.files > 0 && !r.pr) {
|
|
87
|
+
category = "needs_pr";
|
|
88
|
+
} else if (r.status === "needs_review" && (!r.diff || r.diff.files === 0)) {
|
|
89
|
+
category = "stuck";
|
|
90
|
+
} else if (r.status === "needs_review" && r.pr) {
|
|
91
|
+
category = "ready";
|
|
92
|
+
} else {
|
|
93
|
+
category = "stuck";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
identifier: r.identifier,
|
|
98
|
+
title: r.title,
|
|
99
|
+
status: r.status,
|
|
100
|
+
labels: r.labels || [],
|
|
101
|
+
category,
|
|
102
|
+
...(r.pr ? { pr: { number: r.pr.number, state: r.pr.state, url: r.pr.url } } : {}),
|
|
103
|
+
...(r.diff ? { diff: r.diff } : {}),
|
|
104
|
+
...(r.jam ? { jam: r.jam } : {}),
|
|
105
|
+
createdAt: r.createdAt,
|
|
106
|
+
updatedAt: r.updatedAt,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Sort: in_progress first, then needs_pr, ready, stuck, backlog, merged
|
|
111
|
+
const order: Record<string, number> = { in_progress: 0, needs_pr: 1, ready: 2, stuck: 3, backlog: 4, merged: 5 };
|
|
112
|
+
triaged.sort((a, b) => (order[a.category] ?? 9) - (order[b.category] ?? 9));
|
|
113
|
+
|
|
114
|
+
// Build summary
|
|
115
|
+
const summary = {
|
|
116
|
+
total: triaged.length,
|
|
117
|
+
merged: triaged.filter(t => t.category === "merged").length,
|
|
118
|
+
ready: triaged.filter(t => t.category === "ready").length,
|
|
119
|
+
needs_pr: triaged.filter(t => t.category === "needs_pr").length,
|
|
120
|
+
stuck: triaged.filter(t => t.category === "stuck").length,
|
|
121
|
+
backlog: triaged.filter(t => t.category === "backlog").length,
|
|
122
|
+
in_progress: triaged.filter(t => t.category === "in_progress").length,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Build recommendations
|
|
126
|
+
const recs: string[] = [];
|
|
127
|
+
const needsPr = triaged.filter(t => t.category === "needs_pr");
|
|
128
|
+
const stuck = triaged.filter(t => t.category === "stuck");
|
|
129
|
+
const ready = triaged.filter(t => t.category === "ready");
|
|
130
|
+
const inProgress = triaged.filter(t => t.category === "in_progress");
|
|
131
|
+
|
|
132
|
+
if (needsPr.length) {
|
|
133
|
+
recs.push(`Create PRs: ${needsPr.map(t => t.identifier).join(", ")} (have diffs, no PR)`);
|
|
134
|
+
}
|
|
135
|
+
if (ready.length) {
|
|
136
|
+
recs.push(`Review + approve: ${ready.map(t => t.identifier).join(", ")}`);
|
|
137
|
+
}
|
|
138
|
+
if (stuck.length) {
|
|
139
|
+
// Detect duplicates by similar titles
|
|
140
|
+
const stuckTitles = stuck.map(t => t.title.replace(/^(Implement |PLW-\d+ (BLOCKER|MEDIUM|LOW): )/i, "").slice(0, 40));
|
|
141
|
+
const dupes = stuck.filter((t, i) => {
|
|
142
|
+
const norm = stuckTitles[i];
|
|
143
|
+
return triaged.some(other =>
|
|
144
|
+
other.identifier !== t.identifier &&
|
|
145
|
+
(other.category === "needs_pr" || other.category === "ready" || other.category === "merged") &&
|
|
146
|
+
other.title.replace(/^(Implement |PLW-\d+ (BLOCKER|MEDIUM|LOW): )/i, "").slice(0, 40) === norm
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
if (dupes.length) {
|
|
150
|
+
recs.push(`Stop duplicates: ${dupes.map(t => t.identifier).join(", ")} (no output, duplicates of working tasks)`);
|
|
151
|
+
}
|
|
152
|
+
const realStuck = stuck.filter(t => !dupes.includes(t));
|
|
153
|
+
if (realStuck.length) {
|
|
154
|
+
recs.push(`Retry or stop: ${realStuck.map(t => t.identifier).join(", ")} (no diff produced)`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const result: TriageResult = { summary, tasks: triaged, recommendations: recs };
|
|
159
|
+
|
|
160
|
+
if (args.json) { fmt.out(result); return; }
|
|
161
|
+
|
|
162
|
+
// Human output
|
|
163
|
+
const groups: Record<string, TriageTask[]> = {};
|
|
164
|
+
triaged.forEach(t => { (groups[t.category] = groups[t.category] || []).push(t); });
|
|
165
|
+
|
|
166
|
+
const sectionNames: Record<string, string> = {
|
|
167
|
+
in_progress: "IN PROGRESS",
|
|
168
|
+
needs_pr: "HAS CODE, NEEDS PR",
|
|
169
|
+
ready: "READY TO REVIEW",
|
|
170
|
+
stuck: "STUCK (no output)",
|
|
171
|
+
backlog: "BACKLOG",
|
|
172
|
+
merged: "MERGED",
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
for (const cat of ["in_progress", "needs_pr", "ready", "stuck", "backlog", "merged"]) {
|
|
176
|
+
const items = groups[cat];
|
|
177
|
+
if (!items?.length) continue;
|
|
178
|
+
|
|
179
|
+
fmt.section(`${sectionNames[cat]} (${items.length})`);
|
|
180
|
+
items.forEach(t => {
|
|
181
|
+
let line = ` ${fmt.pad(t.identifier, 8)}`;
|
|
182
|
+
|
|
183
|
+
if (t.pr) {
|
|
184
|
+
line += ` PR#${fmt.pad(String(t.pr.number), 4)} [${fmt.pad(t.pr.state, 6)}]`;
|
|
185
|
+
} else {
|
|
186
|
+
line += ` `;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (t.diff && t.diff.files > 0) {
|
|
190
|
+
line += ` +${fmt.pad(String(t.diff.additions), 5)} -${fmt.pad(String(t.diff.deletions), 5)} ${fmt.pad(t.diff.files + " files", 8)}`;
|
|
191
|
+
} else {
|
|
192
|
+
line += ` `;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (t.jam) {
|
|
196
|
+
line += ` ${fmt.pad(t.jam.model, 12)}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
line += ` ${(t.title || "").slice(0, 45)}`;
|
|
200
|
+
console.log(line);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log();
|
|
205
|
+
log.info(`Summary: ${summary.total} tasks — ${summary.in_progress} active, ${summary.needs_pr} need PR, ${summary.ready} to review, ${summary.stuck} stuck, ${summary.merged} merged`);
|
|
206
|
+
|
|
207
|
+
if (recs.length) {
|
|
208
|
+
console.log();
|
|
209
|
+
log.step("Recommendations");
|
|
210
|
+
recs.forEach((r, i) => console.log(` ${i + 1}. ${r}`));
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
async function enrichTasks(api: typeof import("../api.js"), tasks: any[], cfg: any) {
|
|
216
|
+
// Fetch full details and diffs in parallel batches
|
|
217
|
+
const enriched = await Promise.all(tasks.map(async (task) => {
|
|
218
|
+
const id = task.identifier || task.id;
|
|
219
|
+
let detail: any = task;
|
|
220
|
+
let diff: any = null;
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
// Only fetch detail if list response (no jams field)
|
|
224
|
+
if (!task.jams) {
|
|
225
|
+
detail = await api.getTask(id);
|
|
226
|
+
}
|
|
227
|
+
} catch {}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
diff = await api.getDiff(id);
|
|
231
|
+
} catch {}
|
|
232
|
+
|
|
233
|
+
const lastJam = (detail.jams || []).at(-1);
|
|
234
|
+
const credits = lastJam?.credits;
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
identifier: detail.identifier || id,
|
|
238
|
+
title: detail.title || task.title || "",
|
|
239
|
+
status: detail.status || task.status,
|
|
240
|
+
labels: detail.labels || task.labels || [],
|
|
241
|
+
createdAt: detail.createdAt || task.createdAt,
|
|
242
|
+
updatedAt: detail.updatedAt || task.updatedAt,
|
|
243
|
+
pr: detail.pullRequest?.number ? {
|
|
244
|
+
number: detail.pullRequest.number,
|
|
245
|
+
state: detail.pullRequest.state || "?",
|
|
246
|
+
url: detail.pullRequest.url,
|
|
247
|
+
} : null,
|
|
248
|
+
diff: diff?.stats ? {
|
|
249
|
+
files: diff.stats.files || 0,
|
|
250
|
+
additions: diff.stats.additions || 0,
|
|
251
|
+
deletions: diff.stats.deletions || 0,
|
|
252
|
+
} : null,
|
|
253
|
+
jam: lastJam ? {
|
|
254
|
+
model: lastJam.model || "?",
|
|
255
|
+
status: lastJam.status || "?",
|
|
256
|
+
credits: {
|
|
257
|
+
llm: typeof credits === "object" ? (credits?.llm ?? 0) : (credits || 0),
|
|
258
|
+
vm: typeof credits === "object" ? (credits?.vm ?? 0) : 0,
|
|
259
|
+
},
|
|
260
|
+
} : null,
|
|
261
|
+
_raw: detail,
|
|
262
|
+
};
|
|
263
|
+
}));
|
|
264
|
+
|
|
265
|
+
return enriched;
|
|
266
|
+
}
|
package/src/mcp.ts
CHANGED
|
@@ -200,6 +200,88 @@ server.registerTool("capy_retry", {
|
|
|
200
200
|
} catch (e) { return err(e); }
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
+
server.registerTool("capy_triage", {
|
|
204
|
+
description: "Actionable triage of all tasks. Fetches details + diffs in parallel. Categorizes into: merged, ready, needs_pr, stuck, backlog, in_progress. Includes diff stats, PR state, credit usage, and recommendations.",
|
|
205
|
+
inputSchema: {
|
|
206
|
+
ids: z.array(z.string()).optional().describe("Specific task IDs to triage. Omit for all tasks."),
|
|
207
|
+
},
|
|
208
|
+
annotations: { readOnlyHint: true },
|
|
209
|
+
}, async ({ ids }) => {
|
|
210
|
+
try {
|
|
211
|
+
const github = await import("./github.js");
|
|
212
|
+
const cfg = config.load();
|
|
213
|
+
|
|
214
|
+
let tasks: any[];
|
|
215
|
+
if (ids?.length) {
|
|
216
|
+
tasks = await Promise.all(ids.map(id => api.getTask(id)));
|
|
217
|
+
} else {
|
|
218
|
+
const data = await api.listTasks({ limit: 100 });
|
|
219
|
+
tasks = data.items || [];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const enriched = await Promise.all(tasks.map(async (task: any) => {
|
|
223
|
+
const id = task.identifier || task.id;
|
|
224
|
+
let detail: any = task;
|
|
225
|
+
let diff: any = null;
|
|
226
|
+
try { if (!task.jams) detail = await api.getTask(id); } catch {}
|
|
227
|
+
try { diff = await api.getDiff(id); } catch {}
|
|
228
|
+
|
|
229
|
+
if (detail.pullRequest?.number && detail.pullRequest.state === "closed") {
|
|
230
|
+
const repo = detail.pullRequest.repoFullName || cfg.repos[0]?.repoFullName;
|
|
231
|
+
if (repo) {
|
|
232
|
+
const ghPR = github.getPR(repo, detail.pullRequest.number);
|
|
233
|
+
if (ghPR) detail.pullRequest.state = ghPR.state.toLowerCase();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const lastJam = (detail.jams || []).at(-1);
|
|
238
|
+
const credits = lastJam?.credits;
|
|
239
|
+
const pr = detail.pullRequest?.number ? { number: detail.pullRequest.number, state: detail.pullRequest.state || "?", url: detail.pullRequest.url } : null;
|
|
240
|
+
const diffStats = diff?.stats ? { files: diff.stats.files || 0, additions: diff.stats.additions || 0, deletions: diff.stats.deletions || 0 } : null;
|
|
241
|
+
|
|
242
|
+
let category: string;
|
|
243
|
+
if (detail.status === "backlog") category = "backlog";
|
|
244
|
+
else if (detail.status === "in_progress") category = "in_progress";
|
|
245
|
+
else if (pr?.state === "merged") category = "merged";
|
|
246
|
+
else if (pr && pr.state === "open") category = "ready";
|
|
247
|
+
else if (diffStats && diffStats.files > 0 && !pr) category = "needs_pr";
|
|
248
|
+
else if (detail.status === "needs_review" && (!diffStats || diffStats.files === 0)) category = "stuck";
|
|
249
|
+
else category = "stuck";
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
identifier: detail.identifier || id,
|
|
253
|
+
title: detail.title || "",
|
|
254
|
+
status: detail.status,
|
|
255
|
+
labels: detail.labels || [],
|
|
256
|
+
category,
|
|
257
|
+
pr,
|
|
258
|
+
diff: diffStats,
|
|
259
|
+
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,
|
|
260
|
+
};
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
const summary = {
|
|
264
|
+
total: enriched.length,
|
|
265
|
+
merged: enriched.filter((t: any) => t.category === "merged").length,
|
|
266
|
+
ready: enriched.filter((t: any) => t.category === "ready").length,
|
|
267
|
+
needs_pr: enriched.filter((t: any) => t.category === "needs_pr").length,
|
|
268
|
+
stuck: enriched.filter((t: any) => t.category === "stuck").length,
|
|
269
|
+
backlog: enriched.filter((t: any) => t.category === "backlog").length,
|
|
270
|
+
in_progress: enriched.filter((t: any) => t.category === "in_progress").length,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const recs: string[] = [];
|
|
274
|
+
const needsPr = enriched.filter((t: any) => t.category === "needs_pr");
|
|
275
|
+
const stuck = enriched.filter((t: any) => t.category === "stuck");
|
|
276
|
+
const ready = enriched.filter((t: any) => t.category === "ready");
|
|
277
|
+
if (needsPr.length) recs.push(`Create PRs: ${needsPr.map((t: any) => t.identifier).join(", ")}`);
|
|
278
|
+
if (ready.length) recs.push(`Review + approve: ${ready.map((t: any) => t.identifier).join(", ")}`);
|
|
279
|
+
if (stuck.length) recs.push(`Retry or stop: ${stuck.map((t: any) => t.identifier).join(", ")} (no diff produced)`);
|
|
280
|
+
|
|
281
|
+
return text({ summary, tasks: enriched, recommendations: recs });
|
|
282
|
+
} catch (e) { return err(e); }
|
|
283
|
+
});
|
|
284
|
+
|
|
203
285
|
// --- Status & monitoring ---
|
|
204
286
|
|
|
205
287
|
server.registerTool("capy_status", {
|
|
@@ -278,12 +360,16 @@ server.registerTool("capy_msg", {
|
|
|
278
360
|
inputSchema: {
|
|
279
361
|
id: z.string().describe("Task or thread ID"),
|
|
280
362
|
text: z.string().describe("Message text"),
|
|
363
|
+
model: z.string().optional().describe("Switch model mid-conversation (threads only)"),
|
|
364
|
+
attachmentUrls: z.array(z.string()).optional().describe("URLs to attach"),
|
|
281
365
|
},
|
|
282
366
|
annotations: { openWorldHint: true },
|
|
283
|
-
}, async ({ id, text: msg }) => {
|
|
367
|
+
}, async ({ id, text: msg, model, attachmentUrls }) => {
|
|
284
368
|
try {
|
|
285
369
|
const isThread = isThreadId(id);
|
|
286
|
-
const result = isThread
|
|
370
|
+
const result = isThread
|
|
371
|
+
? await api.messageThread(id, msg, { model, attachmentUrls })
|
|
372
|
+
: await api.messageTask(id, msg, { attachmentUrls });
|
|
287
373
|
return text({ id, sent: true, type: isThread ? "thread" : "task", ...(result && typeof result === "object" ? result as Record<string, unknown> : {}) });
|
|
288
374
|
} catch (e) { return err(e); }
|
|
289
375
|
});
|
|
@@ -308,11 +394,17 @@ server.registerTool("capy_pr", {
|
|
|
308
394
|
inputSchema: {
|
|
309
395
|
id: z.string().describe("Task ID"),
|
|
310
396
|
title: z.string().optional().describe("PR title override"),
|
|
397
|
+
description: z.string().optional().describe("PR body/description"),
|
|
398
|
+
draft: z.boolean().optional().describe("Create as draft PR"),
|
|
311
399
|
},
|
|
312
400
|
annotations: { openWorldHint: true },
|
|
313
|
-
}, async ({ id, title }) => {
|
|
401
|
+
}, async ({ id, title, description, draft }) => {
|
|
314
402
|
try {
|
|
315
|
-
const
|
|
403
|
+
const opts: Record<string, unknown> = {};
|
|
404
|
+
if (title) opts.title = title;
|
|
405
|
+
if (description) opts.description = description;
|
|
406
|
+
if (draft != null) opts.draft = draft;
|
|
407
|
+
const data = await api.createPR(id, opts);
|
|
316
408
|
return text(data);
|
|
317
409
|
} catch (e) { return err(e); }
|
|
318
410
|
});
|
|
@@ -383,5 +475,61 @@ server.registerTool("capy_models", {
|
|
|
383
475
|
} catch (e) { return err(e); }
|
|
384
476
|
});
|
|
385
477
|
|
|
478
|
+
// --- Warm Pool ---
|
|
479
|
+
|
|
480
|
+
server.registerTool("capy_pool_status", {
|
|
481
|
+
description: "Get warm pool config and VM status",
|
|
482
|
+
inputSchema: {},
|
|
483
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
484
|
+
}, async () => {
|
|
485
|
+
try {
|
|
486
|
+
const pool = await api.getWarmPool();
|
|
487
|
+
return text(pool);
|
|
488
|
+
} catch (e) { return err(e); }
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
server.registerTool("capy_pool_update", {
|
|
492
|
+
description: "Update warm pool configuration (VM pre-warming)",
|
|
493
|
+
inputSchema: {
|
|
494
|
+
enabled: z.boolean().optional().describe("Enable/disable warm pool"),
|
|
495
|
+
targetSize: z.number().optional().describe("Number of VMs to keep warm"),
|
|
496
|
+
maxAgeMinutes: z.number().optional().describe("Max VM age before recycling"),
|
|
497
|
+
branch: z.string().optional().describe("Branch for pool VMs"),
|
|
498
|
+
setupCommands: z.array(z.string()).optional().describe("Commands to run on VM boot"),
|
|
499
|
+
},
|
|
500
|
+
annotations: { openWorldHint: true },
|
|
501
|
+
}, async (params) => {
|
|
502
|
+
try {
|
|
503
|
+
const data = await api.updateWarmPool(params);
|
|
504
|
+
return text(data);
|
|
505
|
+
} catch (e) { return err(e); }
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
server.registerTool("capy_pool_instances", {
|
|
509
|
+
description: "List warm pool VM instances",
|
|
510
|
+
inputSchema: {
|
|
511
|
+
status: z.string().optional().describe("Filter: ready, provisioning, failed, claimed"),
|
|
512
|
+
},
|
|
513
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
514
|
+
}, async ({ status }) => {
|
|
515
|
+
try {
|
|
516
|
+
const data = await api.listWarmPoolInstances({ status });
|
|
517
|
+
return text(data);
|
|
518
|
+
} catch (e) { return err(e); }
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
server.registerTool("capy_pool_clear", {
|
|
522
|
+
description: "Clear all warm pool VMs",
|
|
523
|
+
inputSchema: {
|
|
524
|
+
replenish: z.boolean().optional().describe("Replenish pool after clearing"),
|
|
525
|
+
},
|
|
526
|
+
annotations: { destructiveHint: true },
|
|
527
|
+
}, async ({ replenish }) => {
|
|
528
|
+
try {
|
|
529
|
+
const data = await api.clearWarmPool({ replenish });
|
|
530
|
+
return text(data);
|
|
531
|
+
} catch (e) { return err(e); }
|
|
532
|
+
});
|
|
533
|
+
|
|
386
534
|
const transport = new StdioServerTransport();
|
|
387
535
|
await server.connect(transport);
|