capyai 0.2.1 → 0.3.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/README.md +1 -1
- package/bin/capy.js +12 -53
- package/bin/capy.ts +36 -56
- package/package.json +8 -6
- package/src/commands/_shared.ts +32 -0
- package/src/commands/agents.ts +48 -0
- package/src/commands/diff-pr.ts +46 -0
- package/src/commands/monitoring.ts +95 -0
- package/src/commands/quality.ts +313 -0
- package/src/commands/setup.ts +257 -0
- package/src/commands/tasks.ts +103 -0
- package/src/commands/threads.ts +95 -0
- package/src/{format.ts → output.ts} +8 -9
- package/dist/capy.js +0 -1619
- package/src/cli.ts +0 -722
- /package/src/{quality.ts → quality-engine.ts} +0 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { modelArgs, jsonArg, resolveModel } from "./_shared.js";
|
|
3
|
+
|
|
4
|
+
export const review = defineCommand({
|
|
5
|
+
meta: { name: "review", description: "Quality gates check" },
|
|
6
|
+
args: {
|
|
7
|
+
id: { type: "positional", description: "Task ID", required: true },
|
|
8
|
+
...jsonArg,
|
|
9
|
+
},
|
|
10
|
+
async run({ args }) {
|
|
11
|
+
const api = await import("../api.js");
|
|
12
|
+
const config = await import("../config.js");
|
|
13
|
+
const github = await import("../github.js");
|
|
14
|
+
const quality = await import("../quality-engine.js");
|
|
15
|
+
const greptileApi = await import("../greptile.js");
|
|
16
|
+
const fmt = await import("../output.js");
|
|
17
|
+
const { log } = await import("@clack/prompts");
|
|
18
|
+
|
|
19
|
+
const task = await api.getTask(args.id);
|
|
20
|
+
const cfg = config.load();
|
|
21
|
+
const reviewProvider = cfg.quality?.reviewProvider || "greptile";
|
|
22
|
+
|
|
23
|
+
if (!task.pullRequest?.number) {
|
|
24
|
+
if (args.json) { fmt.out({ error: "no_pr", task: task.identifier }); return; }
|
|
25
|
+
log.warn(`${task.identifier}: No PR. Create one first: capy pr ${task.identifier}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const repo = task.pullRequest.repoFullName || cfg.repos[0]?.repoFullName || "";
|
|
30
|
+
const prNum = task.pullRequest.number;
|
|
31
|
+
const defaultBranch = cfg.repos.find(r => r.repoFullName === repo)?.branch || "main";
|
|
32
|
+
|
|
33
|
+
let diffStats = null;
|
|
34
|
+
try {
|
|
35
|
+
const d = await api.getDiff(args.id);
|
|
36
|
+
diffStats = d.stats || null;
|
|
37
|
+
} catch {}
|
|
38
|
+
|
|
39
|
+
const q = await quality.check(task);
|
|
40
|
+
|
|
41
|
+
let unaddressed: Awaited<ReturnType<typeof greptileApi.getUnaddressedIssues>> = [];
|
|
42
|
+
const hasGreptileKey = !!(cfg.greptileApiKey || process.env.GREPTILE_API_KEY);
|
|
43
|
+
|
|
44
|
+
if ((reviewProvider === "greptile" || reviewProvider === "both") && hasGreptileKey) {
|
|
45
|
+
unaddressed = await greptileApi.getUnaddressedIssues(repo, prNum, defaultBranch);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (args.json) {
|
|
49
|
+
fmt.out({
|
|
50
|
+
task: task.identifier,
|
|
51
|
+
quality: q,
|
|
52
|
+
unaddressed,
|
|
53
|
+
reviewProvider,
|
|
54
|
+
diff: diffStats ? { files: diffStats.files || 0, additions: diffStats.additions || 0, deletions: diffStats.deletions || 0 } : null,
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const prOpen = q.gates.find(g => g.name === "pr_open");
|
|
60
|
+
log.info(`Review: ${task.identifier} \u2014 ${task.title}`);
|
|
61
|
+
console.log(`PR: #${prNum} [${prOpen?.detail || task.pullRequest?.state || "?"}]`);
|
|
62
|
+
if (diffStats) console.log(`Diff: +${diffStats.additions || 0} -${diffStats.deletions || 0} in ${diffStats.files || 0} files`);
|
|
63
|
+
console.log(`Review: ${reviewProvider}\n`);
|
|
64
|
+
|
|
65
|
+
q.gates.forEach(g => {
|
|
66
|
+
const icon = g.pass ? "\u2713" : "\u2717";
|
|
67
|
+
console.log(` ${icon} ${g.name}: ${g.detail}`);
|
|
68
|
+
if (g.name === "ci" && g.failing?.length) {
|
|
69
|
+
g.failing.forEach(f => console.log(` \u2717 ${f.name} (${f.conclusion || f.status})`));
|
|
70
|
+
}
|
|
71
|
+
if (g.name === "ci" && g.pending?.length) {
|
|
72
|
+
g.pending.forEach(f => console.log(` ... ${f.name} (${f.status})`));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (unaddressed.length > 0) {
|
|
77
|
+
log.warn(`Unaddressed Greptile issues (${unaddressed.length}):`);
|
|
78
|
+
unaddressed.forEach(u => {
|
|
79
|
+
console.log(` ${u.file}:${u.line} ${u.body}`);
|
|
80
|
+
if (u.hasSuggestion) console.log(` ^ has suggested fix`);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (q.pass) {
|
|
85
|
+
log.success(q.summary);
|
|
86
|
+
} else {
|
|
87
|
+
log.warn(q.summary);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const greptileGate = q.gates.find(g => g.name === "greptile");
|
|
91
|
+
if (greptileGate && !greptileGate.pass) {
|
|
92
|
+
if (greptileGate.detail.includes("processing")) {
|
|
93
|
+
log.info(`Greptile is still processing. Wait a minute, then: capy review ${task.identifier}`);
|
|
94
|
+
} else {
|
|
95
|
+
log.info(`Fix the unaddressed issues, push, and Greptile will auto-re-review.\nThen: capy review ${task.identifier}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export const reReview = defineCommand({
|
|
102
|
+
meta: { name: "re-review", description: "Trigger Greptile re-review", alias: "rereview" },
|
|
103
|
+
args: {
|
|
104
|
+
id: { type: "positional", description: "Task ID", required: true },
|
|
105
|
+
...jsonArg,
|
|
106
|
+
},
|
|
107
|
+
async run({ args }) {
|
|
108
|
+
const api = await import("../api.js");
|
|
109
|
+
const config = await import("../config.js");
|
|
110
|
+
const greptileApi = await import("../greptile.js");
|
|
111
|
+
const fmt = await import("../output.js");
|
|
112
|
+
const { log } = await import("@clack/prompts");
|
|
113
|
+
|
|
114
|
+
const cfg = config.load();
|
|
115
|
+
const reviewProvider = cfg.quality?.reviewProvider || "greptile";
|
|
116
|
+
|
|
117
|
+
if (reviewProvider !== "greptile" && reviewProvider !== "both") {
|
|
118
|
+
console.error(`capy: re-review requires Greptile provider (current: ${reviewProvider})`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!cfg.greptileApiKey && !process.env.GREPTILE_API_KEY) {
|
|
123
|
+
console.error("capy: GREPTILE_API_KEY not set. Run: capy config greptileApiKey <key>");
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const task = await api.getTask(args.id);
|
|
128
|
+
if (!task.pullRequest?.number) {
|
|
129
|
+
console.error(`${task.identifier}: No PR. Create one first: capy pr ${task.identifier}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const repo = task.pullRequest.repoFullName || cfg.repos[0]?.repoFullName || "";
|
|
134
|
+
const prNum = task.pullRequest.number;
|
|
135
|
+
const defaultBranch = cfg.repos.find(r => r.repoFullName === repo)?.branch || "main";
|
|
136
|
+
|
|
137
|
+
log.info(`Triggering fresh Greptile review for PR#${prNum}...`);
|
|
138
|
+
console.log("(Note: Greptile auto-reviews on every push via triggerOnUpdates. This is a manual override.)");
|
|
139
|
+
const result = await greptileApi.freshReview(repo, prNum, defaultBranch);
|
|
140
|
+
|
|
141
|
+
if (args.json) { fmt.out(result); return; }
|
|
142
|
+
|
|
143
|
+
if (result) {
|
|
144
|
+
if (result.status === "COMPLETED") log.success("Review completed.");
|
|
145
|
+
else if (result.status === "FAILED") log.error("Review failed. Check the PR state.");
|
|
146
|
+
else log.info(`Review status: ${result.status || "unknown"}`);
|
|
147
|
+
} else {
|
|
148
|
+
log.info("Review triggered. Check back shortly or run: capy review " + task.identifier);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const unaddressed = await greptileApi.getUnaddressedIssues(repo, prNum, defaultBranch);
|
|
152
|
+
if (unaddressed.length > 0) {
|
|
153
|
+
log.warn(`Unaddressed issues: ${unaddressed.length}`);
|
|
154
|
+
unaddressed.forEach(u => console.log(` ${u.file}:${u.line} ${u.body}`));
|
|
155
|
+
} else {
|
|
156
|
+
log.success("All issues addressed.");
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export const approve = defineCommand({
|
|
162
|
+
meta: { name: "approve", description: "Approve if gates pass" },
|
|
163
|
+
args: {
|
|
164
|
+
id: { type: "positional", description: "Task ID", required: true },
|
|
165
|
+
force: { type: "boolean", description: "Override failing gates", default: false },
|
|
166
|
+
...jsonArg,
|
|
167
|
+
},
|
|
168
|
+
async run({ args }) {
|
|
169
|
+
const api = await import("../api.js");
|
|
170
|
+
const config = await import("../config.js");
|
|
171
|
+
const quality = await import("../quality-engine.js");
|
|
172
|
+
const fmt = await import("../output.js");
|
|
173
|
+
const { log } = await import("@clack/prompts");
|
|
174
|
+
|
|
175
|
+
const task = await api.getTask(args.id);
|
|
176
|
+
const cfg = config.load();
|
|
177
|
+
const q = await quality.check(task);
|
|
178
|
+
|
|
179
|
+
if (args.json) { fmt.out({ task: task.identifier, quality: q, approved: q.pass || args.force }); return; }
|
|
180
|
+
|
|
181
|
+
log.info(`${task.identifier} \u2014 ${task.title}\n`);
|
|
182
|
+
q.gates.forEach(g => {
|
|
183
|
+
const icon = g.pass ? "\u2713" : "\u2717";
|
|
184
|
+
console.log(` ${icon} ${g.name}: ${g.detail}`);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (q.pass) log.success(q.summary);
|
|
188
|
+
else log.warn(q.summary);
|
|
189
|
+
|
|
190
|
+
if (!q.pass && !args.force) {
|
|
191
|
+
log.error("Blocked. Fix the failing gates or use --force to override.");
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (q.pass || args.force) {
|
|
196
|
+
log.success(`Approved.${args.force && !q.pass ? " (forced)" : ""}`);
|
|
197
|
+
const approveCmd = cfg.approveCommand;
|
|
198
|
+
if (approveCmd) {
|
|
199
|
+
try {
|
|
200
|
+
const { execSync } = await import("node:child_process");
|
|
201
|
+
const expanded = approveCmd
|
|
202
|
+
.replace("{task}", task.identifier || task.id)
|
|
203
|
+
.replace("{title}", task.title || "")
|
|
204
|
+
.replace("{pr}", String(task.pullRequest?.number || ""));
|
|
205
|
+
execSync(expanded, { encoding: "utf8", timeout: 15000, stdio: "pipe" });
|
|
206
|
+
log.info("Post-approve hook ran.");
|
|
207
|
+
} catch {}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
export const retry = defineCommand({
|
|
214
|
+
meta: { name: "retry", description: "Retry with failure context" },
|
|
215
|
+
args: {
|
|
216
|
+
id: { type: "positional", description: "Task ID", required: true },
|
|
217
|
+
fix: { type: "string", description: "Specific fix instructions" },
|
|
218
|
+
...modelArgs,
|
|
219
|
+
...jsonArg,
|
|
220
|
+
},
|
|
221
|
+
async run({ args }) {
|
|
222
|
+
const api = await import("../api.js");
|
|
223
|
+
const config = await import("../config.js");
|
|
224
|
+
const github = await import("../github.js");
|
|
225
|
+
const greptileApi = await import("../greptile.js");
|
|
226
|
+
const fmt = await import("../output.js");
|
|
227
|
+
const { log } = await import("@clack/prompts");
|
|
228
|
+
|
|
229
|
+
const task = await api.getTask(args.id);
|
|
230
|
+
const cfg = config.load();
|
|
231
|
+
|
|
232
|
+
let context = `Previous attempt: ${task.identifier} "${task.title}" [${task.status}]\n`;
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const d = await api.getDiff(args.id);
|
|
236
|
+
if (d.stats?.files && d.stats.files > 0) {
|
|
237
|
+
context += `\nPrevious diff: +${d.stats.additions} -${d.stats.deletions} in ${d.stats.files} files\n`;
|
|
238
|
+
context += `Files changed: ${(d.files || []).map(f => f.path).join(", ")}\n`;
|
|
239
|
+
} else {
|
|
240
|
+
context += `\nPrevious diff: empty (agent produced no changes)\n`;
|
|
241
|
+
}
|
|
242
|
+
} catch { context += "\nPrevious diff: unavailable\n"; }
|
|
243
|
+
|
|
244
|
+
if (task.pullRequest?.number) {
|
|
245
|
+
const repo = task.pullRequest.repoFullName || cfg.repos[0]?.repoFullName || "";
|
|
246
|
+
const prNum = task.pullRequest.number;
|
|
247
|
+
const defaultBranch = cfg.repos.find(r => r.repoFullName === repo)?.branch || "main";
|
|
248
|
+
const reviewComments = github.getPRReviewComments(repo, prNum);
|
|
249
|
+
const ci = github.getCIStatus(repo, prNum);
|
|
250
|
+
|
|
251
|
+
const reviewProvider = cfg.quality?.reviewProvider || "greptile";
|
|
252
|
+
const hasGreptileKey = !!(cfg.greptileApiKey || process.env.GREPTILE_API_KEY);
|
|
253
|
+
|
|
254
|
+
if (reviewProvider === "greptile" && hasGreptileKey) {
|
|
255
|
+
const unaddressed = await greptileApi.getUnaddressedIssues(repo, prNum, defaultBranch);
|
|
256
|
+
if (unaddressed.length > 0) {
|
|
257
|
+
context += `\nUnaddressed Greptile issues (${unaddressed.length}):\n`;
|
|
258
|
+
unaddressed.forEach(u => {
|
|
259
|
+
context += ` ${u.file}:${u.line}: ${u.body}\n`;
|
|
260
|
+
if (u.suggestedCode) context += ` Suggested fix: ${u.suggestedCode.slice(0, 200)}\n`;
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
context += `\nGreptile: all issues addressed\n`;
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
const issueComments = github.getPRIssueComments(repo, prNum);
|
|
267
|
+
const greptileReview = github.parseGreptileReview(issueComments);
|
|
268
|
+
if (greptileReview) {
|
|
269
|
+
context += `\nGreptile review: ${greptileReview.score}/5 (stale, may not reflect latest)\n`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (ci && !ci.allGreen) {
|
|
274
|
+
context += `\nCI failures: ${ci.failing.map(f => f.name).join(", ")}\n`;
|
|
275
|
+
}
|
|
276
|
+
if (reviewComments.length) {
|
|
277
|
+
context += `\nReview comments (${reviewComments.length}):\n`;
|
|
278
|
+
reviewComments.slice(0, 5).forEach((c: any) => {
|
|
279
|
+
context += ` ${c.path}:${c.line || "?"}: ${(c.body || "").slice(0, 150)}\n`;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const originalPrompt = task.prompt || task.title;
|
|
285
|
+
let retryPrompt = `RETRY: This is a retry of a previous attempt that had issues.\n\n`;
|
|
286
|
+
retryPrompt += `Original task: ${originalPrompt}\n\n`;
|
|
287
|
+
retryPrompt += `--- CONTEXT FROM PREVIOUS ATTEMPT ---\n${context}\n`;
|
|
288
|
+
|
|
289
|
+
if (args.fix) {
|
|
290
|
+
retryPrompt += `--- SPECIFIC FIX REQUESTED ---\n${args.fix}\n\n`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
retryPrompt += `--- INSTRUCTIONS ---\n`;
|
|
294
|
+
retryPrompt += `Fix the issues from the previous attempt. Do not repeat the same mistakes.\n`;
|
|
295
|
+
retryPrompt += `Include tests. Run tests before completing. Verify CI will pass.\n`;
|
|
296
|
+
|
|
297
|
+
if (args.json) {
|
|
298
|
+
fmt.out({ originalTask: task.identifier, retryPrompt, context });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (task.status === "in_progress") {
|
|
303
|
+
await api.stopTask(args.id, "Retrying with fixes");
|
|
304
|
+
log.info(`Stopped ${task.identifier}.`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const model = resolveModel(args) || cfg.defaultModel;
|
|
308
|
+
const data = await api.createThread(retryPrompt, model);
|
|
309
|
+
log.success(`Retry started: https://capy.ai/project/${cfg.projectId}/captain/${data.id}`);
|
|
310
|
+
log.info(`Thread: ${data.id} Model: ${model}`);
|
|
311
|
+
log.info(`Context included: ${context.split("\n").length} lines from previous attempt.`);
|
|
312
|
+
},
|
|
313
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { jsonArg } from "./_shared.js";
|
|
3
|
+
|
|
4
|
+
export const init = defineCommand({
|
|
5
|
+
meta: { name: "init", description: "Interactive setup wizard" },
|
|
6
|
+
args: {},
|
|
7
|
+
async run() {
|
|
8
|
+
const p = await import("@clack/prompts");
|
|
9
|
+
const config = await import("../config.js");
|
|
10
|
+
|
|
11
|
+
p.intro("capy setup");
|
|
12
|
+
|
|
13
|
+
const cfg = config.load();
|
|
14
|
+
const result = await p.group({
|
|
15
|
+
apiKey: () => p.password({
|
|
16
|
+
message: "Capy API key",
|
|
17
|
+
mask: "*",
|
|
18
|
+
validate: (v) => { if (!v) return "API key is required"; },
|
|
19
|
+
}),
|
|
20
|
+
projectId: () => p.text({
|
|
21
|
+
message: "Project ID",
|
|
22
|
+
initialValue: cfg.projectId || "",
|
|
23
|
+
validate: (v) => { if (!v) return "Project ID is required"; },
|
|
24
|
+
}),
|
|
25
|
+
repos: () => p.text({
|
|
26
|
+
message: "Repos (owner/repo:branch, comma-separated)",
|
|
27
|
+
initialValue: cfg.repos.map(r => `${r.repoFullName}:${r.branch}`).join(", ") || "",
|
|
28
|
+
placeholder: "owner/repo:main",
|
|
29
|
+
}),
|
|
30
|
+
defaultModel: () => p.text({
|
|
31
|
+
message: "Default model",
|
|
32
|
+
initialValue: cfg.defaultModel,
|
|
33
|
+
}),
|
|
34
|
+
reviewProvider: () => p.select({
|
|
35
|
+
message: "Review provider",
|
|
36
|
+
initialValue: cfg.quality?.reviewProvider || "greptile",
|
|
37
|
+
options: [
|
|
38
|
+
{ value: "greptile", label: "Greptile", hint: "AI code review via Greptile API" },
|
|
39
|
+
{ value: "capy", label: "Capy", hint: "GitHub unresolved review threads" },
|
|
40
|
+
{ value: "both", label: "Both", hint: "Strictest: Greptile + GitHub threads" },
|
|
41
|
+
{ value: "none", label: "None", hint: "Skip review gates" },
|
|
42
|
+
],
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (p.isCancel(result)) {
|
|
47
|
+
p.cancel("Setup cancelled.");
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
cfg.apiKey = result.apiKey;
|
|
52
|
+
cfg.projectId = result.projectId;
|
|
53
|
+
cfg.repos = (result.repos || "").split(",").filter(Boolean).map(s => {
|
|
54
|
+
const [repo, branch] = s.trim().split(":");
|
|
55
|
+
return { repoFullName: repo, branch: branch || "main" };
|
|
56
|
+
});
|
|
57
|
+
cfg.defaultModel = result.defaultModel;
|
|
58
|
+
cfg.quality.reviewProvider = result.reviewProvider as string;
|
|
59
|
+
|
|
60
|
+
config.save(cfg);
|
|
61
|
+
p.outro(`Config saved to ${config.CONFIG_PATH}`);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const config = defineCommand({
|
|
66
|
+
meta: { name: "config", description: "Get/set config" },
|
|
67
|
+
args: {
|
|
68
|
+
key: { type: "positional", description: "Config key (dot notation)" },
|
|
69
|
+
value: { type: "positional", description: "Value to set" },
|
|
70
|
+
...jsonArg,
|
|
71
|
+
},
|
|
72
|
+
async run({ args }) {
|
|
73
|
+
const configMod = await import("../config.js");
|
|
74
|
+
const fmt = await import("../output.js");
|
|
75
|
+
|
|
76
|
+
if (!args.key) {
|
|
77
|
+
fmt.out(configMod.load());
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!args.value) {
|
|
81
|
+
const val = configMod.get(args.key);
|
|
82
|
+
if (val === undefined) {
|
|
83
|
+
console.error(`capy: unknown config key "${args.key}"`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
if (args.json || typeof val === "object") {
|
|
87
|
+
fmt.out(args.json ? { [args.key]: val } : val);
|
|
88
|
+
} else {
|
|
89
|
+
console.log(String(val));
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
configMod.set(args.key, args.value);
|
|
94
|
+
console.log(`Set ${args.key} = ${configMod.get(args.key)}`);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export const models = defineCommand({
|
|
99
|
+
meta: { name: "models", description: "List available models" },
|
|
100
|
+
args: { ...jsonArg },
|
|
101
|
+
async run({ args }) {
|
|
102
|
+
const api = await import("../api.js");
|
|
103
|
+
const fmt = await import("../output.js");
|
|
104
|
+
|
|
105
|
+
const data = await api.listModels();
|
|
106
|
+
if (args.json) { fmt.out(data.models || []); return; }
|
|
107
|
+
if (data.models) {
|
|
108
|
+
fmt.table(["MODEL", "PROVIDER", "CAPTAIN"], data.models.map(m => [
|
|
109
|
+
m.id, m.provider || "?", m.captainEligible ? "yes" : "no",
|
|
110
|
+
]));
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const tools = defineCommand({
|
|
116
|
+
meta: { name: "tools", description: "All commands + env vars", alias: "commands" },
|
|
117
|
+
args: { ...jsonArg },
|
|
118
|
+
async run({ args }) {
|
|
119
|
+
const configMod = await import("../config.js");
|
|
120
|
+
const fmt = await import("../output.js");
|
|
121
|
+
const { log } = await import("@clack/prompts");
|
|
122
|
+
|
|
123
|
+
const all: Record<string, { args: string; desc: string }> = {
|
|
124
|
+
captain: { args: "<prompt>", desc: "Start Captain thread" },
|
|
125
|
+
build: { args: "<prompt>", desc: "Start Build agent (isolated)" },
|
|
126
|
+
threads: { args: "[list|get|msg|stop]", desc: "Manage threads" },
|
|
127
|
+
status: { args: "", desc: "Dashboard" },
|
|
128
|
+
list: { args: "[status]", desc: "List tasks" },
|
|
129
|
+
get: { args: "<id>", desc: "Task details" },
|
|
130
|
+
start: { args: "<id>", desc: "Start task" },
|
|
131
|
+
stop: { args: "<id> [reason]", desc: "Stop task" },
|
|
132
|
+
msg: { args: "<id> <text>", desc: "Message task" },
|
|
133
|
+
diff: { args: "<id>", desc: "View diff" },
|
|
134
|
+
pr: { args: "<id> [title]", desc: "Create PR" },
|
|
135
|
+
review: { args: "<id>", desc: "Quality gates check" },
|
|
136
|
+
"re-review":{ args: "<id>", desc: "Trigger Greptile re-review" },
|
|
137
|
+
approve: { args: "<id>", desc: "Approve if gates pass" },
|
|
138
|
+
retry: { args: "<id> [--fix=...]", desc: "Retry with failure context" },
|
|
139
|
+
watch: { args: "<id>", desc: "Poll + notify on completion" },
|
|
140
|
+
unwatch: { args: "<id>", desc: "Stop watching" },
|
|
141
|
+
watches: { args: "", desc: "List watches" },
|
|
142
|
+
models: { args: "", desc: "List models" },
|
|
143
|
+
tools: { args: "", desc: "This list" },
|
|
144
|
+
config: { args: "[key] [value]", desc: "Get/set config" },
|
|
145
|
+
init: { args: "", desc: "Interactive setup" },
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (args.json) { fmt.out(all); return; }
|
|
149
|
+
|
|
150
|
+
const cfg = configMod.load();
|
|
151
|
+
log.step("Available commands");
|
|
152
|
+
for (const [name, t] of Object.entries(all)) {
|
|
153
|
+
console.log(` ${fmt.pad(name, 14)} ${fmt.pad(t.args, 24)} ${t.desc}`);
|
|
154
|
+
}
|
|
155
|
+
console.log(`\nConfig: ${configMod.CONFIG_PATH}`);
|
|
156
|
+
console.log(`Review provider: ${cfg.quality?.reviewProvider || "greptile"}`);
|
|
157
|
+
console.log(`Default model: ${cfg.defaultModel}`);
|
|
158
|
+
console.log(`Repos: ${(cfg.repos || []).map(r => r.repoFullName).join(", ") || "none"}`);
|
|
159
|
+
|
|
160
|
+
const envVars: [string, string][] = [
|
|
161
|
+
["CAPY_API_KEY", "API key (overrides config)"],
|
|
162
|
+
["CAPY_PROJECT_ID", "Project ID (overrides config)"],
|
|
163
|
+
["CAPY_SERVER", "API server URL"],
|
|
164
|
+
["CAPY_ENV_FILE", "Path to .env file"],
|
|
165
|
+
["GREPTILE_API_KEY", "Greptile API key"],
|
|
166
|
+
];
|
|
167
|
+
log.step("Environment variables");
|
|
168
|
+
envVars.forEach(([k, v]) => console.log(` ${fmt.pad(k, 20)} ${v}`));
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
export const status = defineCommand({
|
|
173
|
+
meta: { name: "status", description: "Dashboard", alias: "dashboard" },
|
|
174
|
+
args: { ...jsonArg },
|
|
175
|
+
async run({ args }) {
|
|
176
|
+
const api = await import("../api.js");
|
|
177
|
+
const config = await import("../config.js");
|
|
178
|
+
const github = await import("../github.js");
|
|
179
|
+
const w = await import("../watch.js");
|
|
180
|
+
const fmt = await import("../output.js");
|
|
181
|
+
const { log, spinner } = await import("@clack/prompts");
|
|
182
|
+
|
|
183
|
+
const cfg = config.load();
|
|
184
|
+
|
|
185
|
+
let threads: any, tasks: any;
|
|
186
|
+
if (!args.json) {
|
|
187
|
+
const s = spinner();
|
|
188
|
+
s.start("Loading dashboard...");
|
|
189
|
+
[threads, tasks] = await Promise.all([api.listThreads({ limit: 10 }), api.listTasks({ limit: 30 })]);
|
|
190
|
+
s.stop("Dashboard loaded");
|
|
191
|
+
} else {
|
|
192
|
+
[threads, tasks] = await Promise.all([api.listThreads({ limit: 10 }), api.listTasks({ limit: 30 })]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (args.json) {
|
|
196
|
+
fmt.out({
|
|
197
|
+
threads: threads.items || [],
|
|
198
|
+
tasks: tasks.items || [],
|
|
199
|
+
watches: w.list(),
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const active = (threads.items || []).filter(t => t.status === "active");
|
|
205
|
+
if (active.length) {
|
|
206
|
+
fmt.section("ACTIVE THREADS");
|
|
207
|
+
active.forEach(t => console.log(` ${t.id.slice(0, 14)} ${(t.title || "(untitled)").slice(0, 50)} [active]`));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const allTasks = tasks.items || [];
|
|
211
|
+
const buckets: Record<string, typeof allTasks> = {};
|
|
212
|
+
allTasks.forEach(t => { (buckets[t.status] = buckets[t.status] || []).push(t); });
|
|
213
|
+
|
|
214
|
+
if (buckets.in_progress?.length) {
|
|
215
|
+
fmt.section("IN PROGRESS");
|
|
216
|
+
buckets.in_progress.forEach(t => {
|
|
217
|
+
const j = (t.jams || []).at(-1);
|
|
218
|
+
const stuck = j && j.status === "idle" && (!j.credits || (typeof j.credits === "object" && j.credits.llm === 0 && j.credits.vm === 0));
|
|
219
|
+
console.log(` ${fmt.pad(t.identifier, 10)} ${fmt.pad((t.title || "").slice(0, 48), 50)}${stuck ? " !! STUCK" : ""}`);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (buckets.needs_review?.length) {
|
|
224
|
+
fmt.section("NEEDS REVIEW");
|
|
225
|
+
buckets.needs_review.forEach(t => {
|
|
226
|
+
let prInfo = "no PR";
|
|
227
|
+
if (t.pullRequest?.number) {
|
|
228
|
+
const repo = t.pullRequest.repoFullName || cfg.repos[0]?.repoFullName || "";
|
|
229
|
+
const prData = github.getPR(repo, t.pullRequest.number);
|
|
230
|
+
const state = prData ? prData.state : t.pullRequest.state || "?";
|
|
231
|
+
const ci = github.getCIStatus(repo, t.pullRequest.number, prData);
|
|
232
|
+
const ciStr = ci ? (ci.allGreen ? "CI pass" : ci.noChecks ? "no CI" : "CI FAIL") : "?";
|
|
233
|
+
prInfo = `PR#${t.pullRequest.number} [${state}] ${ciStr}`;
|
|
234
|
+
}
|
|
235
|
+
console.log(` ${fmt.pad(t.identifier, 10)} ${fmt.pad((t.title || "").slice(0, 42), 44)} ${prInfo}`);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (buckets.backlog?.length) {
|
|
240
|
+
fmt.section(`BACKLOG (${buckets.backlog.length})`);
|
|
241
|
+
buckets.backlog.forEach(t => console.log(` ${fmt.pad(t.identifier, 10)} ${(t.title || "").slice(0, 60)}`));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const watchEntries = w.list();
|
|
245
|
+
if (watchEntries.length) {
|
|
246
|
+
fmt.section(`ACTIVE WATCHES (${watchEntries.length})`);
|
|
247
|
+
watchEntries.forEach(e => console.log(` ${fmt.pad(e.id.slice(0, 18), 20)} type=${e.type} every ${e.intervalMin}min`));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const stuckCount = (buckets.in_progress || []).filter(t => {
|
|
251
|
+
const j = (t.jams || []).at(-1);
|
|
252
|
+
return j && j.status === "idle" && (!j.credits || (typeof j.credits === "object" && j.credits.llm === 0 && j.credits.vm === 0));
|
|
253
|
+
}).length;
|
|
254
|
+
|
|
255
|
+
log.info(`Summary: ${allTasks.length} tasks, ${(buckets.in_progress || []).length} active, ${(buckets.needs_review || []).length} review, ${stuckCount} stuck`);
|
|
256
|
+
},
|
|
257
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { modelArgs, jsonArg, resolveModel } from "./_shared.js";
|
|
3
|
+
|
|
4
|
+
export const list = defineCommand({
|
|
5
|
+
meta: { name: "list", description: "List tasks", alias: "ls" },
|
|
6
|
+
args: {
|
|
7
|
+
status: { type: "positional", description: "Filter by status" },
|
|
8
|
+
...jsonArg,
|
|
9
|
+
},
|
|
10
|
+
async run({ args }) {
|
|
11
|
+
const api = await import("../api.js");
|
|
12
|
+
const fmt = await import("../output.js");
|
|
13
|
+
|
|
14
|
+
const data = await api.listTasks({ status: args.status });
|
|
15
|
+
if (args.json) { fmt.out(data.items || []); return; }
|
|
16
|
+
if (!data.items?.length) { console.log("No tasks."); return; }
|
|
17
|
+
fmt.table(["ID", "STATUS", "TITLE", "PR"], data.items.map(t => [
|
|
18
|
+
t.identifier,
|
|
19
|
+
t.status,
|
|
20
|
+
(t.title || "").slice(0, 45),
|
|
21
|
+
t.pullRequest ? `PR#${t.pullRequest.number} [${t.pullRequest.state}]` : "\u2014",
|
|
22
|
+
]));
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const get = defineCommand({
|
|
27
|
+
meta: { name: "get", description: "Task details", alias: "show" },
|
|
28
|
+
args: {
|
|
29
|
+
id: { type: "positional", description: "Task ID", required: true },
|
|
30
|
+
...jsonArg,
|
|
31
|
+
},
|
|
32
|
+
async run({ args }) {
|
|
33
|
+
const api = await import("../api.js");
|
|
34
|
+
const fmt = await import("../output.js");
|
|
35
|
+
const { log } = await import("@clack/prompts");
|
|
36
|
+
|
|
37
|
+
const data = await api.getTask(args.id);
|
|
38
|
+
if (args.json) { fmt.out(data); return; }
|
|
39
|
+
log.info(`Task: ${data.identifier} \u2014 ${data.title}\nStatus: ${data.status}\nCreated: ${data.createdAt}`);
|
|
40
|
+
if (data.pullRequest) {
|
|
41
|
+
console.log(`PR: ${data.pullRequest.url || `#${data.pullRequest.number}`} [${data.pullRequest.state}]`);
|
|
42
|
+
}
|
|
43
|
+
if (data.jams?.length) {
|
|
44
|
+
log.step(`Jams (${data.jams.length})`);
|
|
45
|
+
data.jams.forEach((j, i) => {
|
|
46
|
+
console.log(` ${i+1}. model=${j.model || "?"} status=${j.status || "?"} credits=${fmt.credits(j.credits)}`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const start = defineCommand({
|
|
53
|
+
meta: { name: "start", description: "Start a task" },
|
|
54
|
+
args: {
|
|
55
|
+
id: { type: "positional", description: "Task ID", required: true },
|
|
56
|
+
...modelArgs,
|
|
57
|
+
...jsonArg,
|
|
58
|
+
},
|
|
59
|
+
async run({ args }) {
|
|
60
|
+
const api = await import("../api.js");
|
|
61
|
+
const config = await import("../config.js");
|
|
62
|
+
const fmt = await import("../output.js");
|
|
63
|
+
|
|
64
|
+
const model = resolveModel(args) || config.load().defaultModel;
|
|
65
|
+
const data = await api.startTask(args.id, model);
|
|
66
|
+
if (args.json) { fmt.out(data); return; }
|
|
67
|
+
console.log(`Started ${data.identifier || args.id} \u2192 ${data.status}`);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export const stop = defineCommand({
|
|
72
|
+
meta: { name: "stop", description: "Stop a task", alias: "kill" },
|
|
73
|
+
args: {
|
|
74
|
+
id: { type: "positional", description: "Task ID", required: true },
|
|
75
|
+
reason: { type: "positional", description: "Stop reason" },
|
|
76
|
+
...jsonArg,
|
|
77
|
+
},
|
|
78
|
+
async run({ args }) {
|
|
79
|
+
const api = await import("../api.js");
|
|
80
|
+
const fmt = await import("../output.js");
|
|
81
|
+
|
|
82
|
+
const data = await api.stopTask(args.id, args.reason);
|
|
83
|
+
if (args.json) { fmt.out(data); return; }
|
|
84
|
+
console.log(`Stopped ${data.identifier || args.id} \u2192 ${data.status}`);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const msg = defineCommand({
|
|
89
|
+
meta: { name: "msg", description: "Message a task", alias: "message" },
|
|
90
|
+
args: {
|
|
91
|
+
id: { type: "positional", description: "Task ID", required: true },
|
|
92
|
+
text: { type: "positional", description: "Message text", required: true },
|
|
93
|
+
...jsonArg,
|
|
94
|
+
},
|
|
95
|
+
async run({ args }) {
|
|
96
|
+
const api = await import("../api.js");
|
|
97
|
+
const fmt = await import("../output.js");
|
|
98
|
+
|
|
99
|
+
await api.messageTask(args.id, args.text);
|
|
100
|
+
if (args.json) { fmt.out({ id: args.id, message: args.text, status: "sent" }); return; }
|
|
101
|
+
console.log("Message sent.");
|
|
102
|
+
},
|
|
103
|
+
});
|