opencode-magi 0.0.0-dev-20260519011027
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/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/commands.js +18 -0
- package/dist/config/load.js +62 -0
- package/dist/config/output.js +16 -0
- package/dist/config/resolve.js +113 -0
- package/dist/config/validate.js +580 -0
- package/dist/config/worktree.js +13 -0
- package/dist/github/commands.js +398 -0
- package/dist/github/retry.js +44 -0
- package/dist/index.js +540 -0
- package/dist/orchestrator/abort.js +9 -0
- package/dist/orchestrator/ci.js +568 -0
- package/dist/orchestrator/findings.js +66 -0
- package/dist/orchestrator/majority.js +48 -0
- package/dist/orchestrator/merge.js +836 -0
- package/dist/orchestrator/model.js +202 -0
- package/dist/orchestrator/pool.js +15 -0
- package/dist/orchestrator/report.js +168 -0
- package/dist/orchestrator/review.js +791 -0
- package/dist/orchestrator/run-manager.js +1670 -0
- package/dist/orchestrator/safety.js +44 -0
- package/dist/permissions/common.json +24 -0
- package/dist/permissions/editor.json +7 -0
- package/dist/prompts/compose.js +298 -0
- package/dist/prompts/contracts.js +189 -0
- package/dist/prompts/output.js +260 -0
- package/dist/prompts/templates/ci-classification-after-edit.md +16 -0
- package/dist/prompts/templates/ci-classification.md +9 -0
- package/dist/prompts/templates/close-reconsideration.md +6 -0
- package/dist/prompts/templates/edit.md +9 -0
- package/dist/prompts/templates/finding-validation.md +7 -0
- package/dist/prompts/templates/rereview-close-reconsideration.md +6 -0
- package/dist/prompts/templates/rereview.md +16 -0
- package/dist/prompts/templates/review.md +7 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
- package/schema.json +206 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { exec as nodeExec } from "node:child_process";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { MAGI_COMMANDS } from "./commands";
|
|
8
|
+
import { loadConfig, mergeMagiConfig } from "./config/load";
|
|
9
|
+
import { outputBaseDirs } from "./config/output";
|
|
10
|
+
import { worktreeBaseDirs } from "./config/worktree";
|
|
11
|
+
import { resolveRepository } from "./config/resolve";
|
|
12
|
+
import { validateConfig } from "./config/validate";
|
|
13
|
+
import { withGitHubApiRetry } from "./github/retry";
|
|
14
|
+
import { mapPool } from "./orchestrator/pool";
|
|
15
|
+
import { MagiRunManager } from "./orchestrator/run-manager";
|
|
16
|
+
const execAsync = promisify(nodeExec);
|
|
17
|
+
const GLOBAL_CONFIG_PATH = join(homedir(), ".config", "opencode", "magi.json");
|
|
18
|
+
const PROJECT_CONFIG_PATH = join(".opencode", "magi.json");
|
|
19
|
+
function createExec(defaultCwd) {
|
|
20
|
+
return async (command, options) => {
|
|
21
|
+
const { stdout } = await execAsync(command, {
|
|
22
|
+
cwd: options?.cwd ?? defaultCwd,
|
|
23
|
+
env: { ...process.env, ...options?.env },
|
|
24
|
+
maxBuffer: 1024 * 1024 * 20,
|
|
25
|
+
signal: options?.signal,
|
|
26
|
+
});
|
|
27
|
+
return stdout;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function responseData(result) {
|
|
31
|
+
if (!result || typeof result !== "object")
|
|
32
|
+
return result;
|
|
33
|
+
return result.data ?? result;
|
|
34
|
+
}
|
|
35
|
+
function extractModelCatalog(result) {
|
|
36
|
+
const data = responseData(result);
|
|
37
|
+
if (!data || typeof data !== "object")
|
|
38
|
+
return undefined;
|
|
39
|
+
const providers = data.providers ??
|
|
40
|
+
data.all;
|
|
41
|
+
if (!Array.isArray(providers))
|
|
42
|
+
return undefined;
|
|
43
|
+
const catalog = {};
|
|
44
|
+
for (const provider of providers) {
|
|
45
|
+
if (!provider || typeof provider !== "object")
|
|
46
|
+
continue;
|
|
47
|
+
const id = provider.id;
|
|
48
|
+
const models = provider.models;
|
|
49
|
+
if (typeof id !== "string" || !models || typeof models !== "object")
|
|
50
|
+
continue;
|
|
51
|
+
catalog[id] = Object.keys(models);
|
|
52
|
+
}
|
|
53
|
+
return catalog;
|
|
54
|
+
}
|
|
55
|
+
function parsePrToken(value) {
|
|
56
|
+
const trimmed = value.trim();
|
|
57
|
+
const pullUrl = trimmed.match(/(?:^|\/)pull\/(\d+)(?:[/?#].*)?$/);
|
|
58
|
+
const raw = pullUrl?.[1] ?? trimmed.replace(/^#/, "");
|
|
59
|
+
const pr = Number.parseInt(raw, 10);
|
|
60
|
+
if (!Number.isInteger(pr) || pr <= 0 || String(pr) !== raw) {
|
|
61
|
+
throw new Error("Specify one or more PR numbers or PR URLs.");
|
|
62
|
+
}
|
|
63
|
+
return pr;
|
|
64
|
+
}
|
|
65
|
+
export function parsePrs(value) {
|
|
66
|
+
const prs = value
|
|
67
|
+
.split(/[\s,]+/)
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.map(parsePrToken);
|
|
70
|
+
if (!prs.length)
|
|
71
|
+
throw new Error("Specify one or more PR numbers or PR URLs.");
|
|
72
|
+
return prs;
|
|
73
|
+
}
|
|
74
|
+
export function parseRunArguments(value, dryRun = false) {
|
|
75
|
+
const tokens = value.split(/[\s,]+/).filter(Boolean);
|
|
76
|
+
const prTokens = tokens.filter((token) => {
|
|
77
|
+
if (token === "--dry-run") {
|
|
78
|
+
dryRun = true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
return { dryRun, prs: parsePrs(prTokens.join(" ")) };
|
|
84
|
+
}
|
|
85
|
+
function parseOptionalPr(value) {
|
|
86
|
+
if (!value?.trim())
|
|
87
|
+
return undefined;
|
|
88
|
+
return parsePrToken(value);
|
|
89
|
+
}
|
|
90
|
+
function clearFlag(value) {
|
|
91
|
+
return typeof value === "boolean" ? value : undefined;
|
|
92
|
+
}
|
|
93
|
+
function clearToolFlag(value) {
|
|
94
|
+
if (value === true || value === "true")
|
|
95
|
+
return true;
|
|
96
|
+
if (value === "false")
|
|
97
|
+
return false;
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
function hasBlankSelector(args) {
|
|
101
|
+
return !args.runId?.trim() && !args.pr?.trim();
|
|
102
|
+
}
|
|
103
|
+
function hasDefaultedFalseClearFlags(args) {
|
|
104
|
+
return (hasBlankSelector(args) &&
|
|
105
|
+
args.branch === "false" &&
|
|
106
|
+
args.output === "false" &&
|
|
107
|
+
args.session === "false" &&
|
|
108
|
+
args.worktree === "false");
|
|
109
|
+
}
|
|
110
|
+
function parseQuestionAnswers(value) {
|
|
111
|
+
const trimmed = value.trim();
|
|
112
|
+
if (!trimmed)
|
|
113
|
+
throw new Error("Specify at least one answer.");
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(trimmed);
|
|
116
|
+
if (Array.isArray(parsed) &&
|
|
117
|
+
parsed.length &&
|
|
118
|
+
parsed.every((item) => typeof item === "string")) {
|
|
119
|
+
return parsed;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Plain text answers are accepted below.
|
|
124
|
+
}
|
|
125
|
+
return [trimmed];
|
|
126
|
+
}
|
|
127
|
+
function prMarkdownLink(repository, pr) {
|
|
128
|
+
const host = repository.github.host || "github.com";
|
|
129
|
+
const url = `https://${host}/${repository.github.owner}/${repository.github.repo}/pull/${pr}`;
|
|
130
|
+
return `[#${pr}](${url})`;
|
|
131
|
+
}
|
|
132
|
+
function isPlainObject(value) {
|
|
133
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
134
|
+
}
|
|
135
|
+
async function readConfigFile(path, target) {
|
|
136
|
+
try {
|
|
137
|
+
const config = JSON.parse(await readFile(path, "utf8"));
|
|
138
|
+
if (!isPlainObject(config)) {
|
|
139
|
+
return {
|
|
140
|
+
error: `${target} config must be a JSON object: ${path}`,
|
|
141
|
+
exists: true,
|
|
142
|
+
path,
|
|
143
|
+
target,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return { config, exists: true, path, target };
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error.code === "ENOENT") {
|
|
150
|
+
return { exists: false, path, target };
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
error: `${target} config is invalid JSON at ${path}: ${error.message}`,
|
|
154
|
+
exists: true,
|
|
155
|
+
path,
|
|
156
|
+
target,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function formatConfigStatus(status) {
|
|
161
|
+
if (!status.exists)
|
|
162
|
+
return `- ${status.target}: missing (${status.path})`;
|
|
163
|
+
if (status.error)
|
|
164
|
+
return `- ${status.target}: invalid (${status.path})`;
|
|
165
|
+
return `- ${status.target}: found (${status.path})`;
|
|
166
|
+
}
|
|
167
|
+
export async function validateMagiConfigFiles(directory, options = {}) {
|
|
168
|
+
const projectPath = join(directory, PROJECT_CONFIG_PATH);
|
|
169
|
+
const statuses = await Promise.all([
|
|
170
|
+
readConfigFile(GLOBAL_CONFIG_PATH, "global"),
|
|
171
|
+
readConfigFile(projectPath, "project"),
|
|
172
|
+
]);
|
|
173
|
+
const existing = statuses.filter((status) => status.exists);
|
|
174
|
+
const hasProjectConfig = statuses.some((status) => status.target === "project" && status.exists);
|
|
175
|
+
const errors = statuses
|
|
176
|
+
.map((status) => status.error)
|
|
177
|
+
.filter((error) => Boolean(error));
|
|
178
|
+
const warnings = [];
|
|
179
|
+
let loadedFrom = "none";
|
|
180
|
+
if (!existing.length) {
|
|
181
|
+
errors.push(`No Magi config found. Expected ${GLOBAL_CONFIG_PATH} or ${projectPath}.`);
|
|
182
|
+
}
|
|
183
|
+
if (existing.length && !errors.length) {
|
|
184
|
+
const merged = existing.reduce((config, status) => mergeMagiConfig(config, status.config ?? {}), {});
|
|
185
|
+
const mergedConfig = merged;
|
|
186
|
+
const validation = await validateConfig(mergedConfig, {
|
|
187
|
+
checkAuth: options.checkAuth ?? true,
|
|
188
|
+
directory,
|
|
189
|
+
exec: options.exec
|
|
190
|
+
? withGitHubApiRetry(options.exec, mergedConfig.github?.apiRetryAttempts ?? 3)
|
|
191
|
+
: undefined,
|
|
192
|
+
modelCatalog: options.modelCatalog,
|
|
193
|
+
requireGithub: hasProjectConfig && Boolean(mergedConfig.agents?.reviewers),
|
|
194
|
+
});
|
|
195
|
+
loadedFrom = existing.map((status) => status.path).join(", ");
|
|
196
|
+
errors.push(...validation.errors);
|
|
197
|
+
warnings.push(...validation.warnings);
|
|
198
|
+
}
|
|
199
|
+
return [
|
|
200
|
+
`Magi config validation: ${errors.length ? "failed" : "passed"}`,
|
|
201
|
+
"",
|
|
202
|
+
"Config files:",
|
|
203
|
+
...statuses.map(formatConfigStatus),
|
|
204
|
+
"",
|
|
205
|
+
"Effective config:",
|
|
206
|
+
`- loaded from: ${loadedFrom}`,
|
|
207
|
+
`- auth checks: ${(options.checkAuth ?? true) ? "enabled" : "disabled"}`,
|
|
208
|
+
"",
|
|
209
|
+
"Errors:",
|
|
210
|
+
...(errors.length ? errors.map((error) => `- ${error}`) : ["- None"]),
|
|
211
|
+
"",
|
|
212
|
+
"Warnings:",
|
|
213
|
+
...(warnings.length
|
|
214
|
+
? warnings.map((warning) => `- ${warning}`)
|
|
215
|
+
: ["- None"]),
|
|
216
|
+
].join("\n");
|
|
217
|
+
}
|
|
218
|
+
export const MagiPlugin = async ({ client, directory }) => {
|
|
219
|
+
const exec = createExec(directory);
|
|
220
|
+
const modelClient = client;
|
|
221
|
+
const catalogClient = client;
|
|
222
|
+
let modelCatalogPromise;
|
|
223
|
+
const sessionOptions = new Map();
|
|
224
|
+
const runManager = new MagiRunManager({
|
|
225
|
+
client: modelClient,
|
|
226
|
+
directory,
|
|
227
|
+
exec,
|
|
228
|
+
setSessionOptions: (sessionId, options) => {
|
|
229
|
+
if (Object.keys(options).length)
|
|
230
|
+
sessionOptions.set(sessionId, options);
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
async function configuredOutputDir() {
|
|
234
|
+
return loadConfig(directory)
|
|
235
|
+
.then((loaded) => outputBaseDirs(directory, loaded.config))
|
|
236
|
+
.catch(() => undefined);
|
|
237
|
+
}
|
|
238
|
+
async function modelCatalog() {
|
|
239
|
+
modelCatalogPromise ??= catalogClient.config
|
|
240
|
+
?.providers({ query: { directory } })
|
|
241
|
+
.then(extractModelCatalog)
|
|
242
|
+
.catch(() => catalogClient.provider
|
|
243
|
+
?.list({ query: { directory } })
|
|
244
|
+
.then(extractModelCatalog));
|
|
245
|
+
return modelCatalogPromise;
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
"chat.params": async (input, output) => {
|
|
249
|
+
const options = sessionOptions.get(input.sessionID);
|
|
250
|
+
if (options)
|
|
251
|
+
Object.assign(output.options, options);
|
|
252
|
+
},
|
|
253
|
+
"permission.ask": async (input, output) => {
|
|
254
|
+
const permissionInput = input;
|
|
255
|
+
const permissionOutput = output;
|
|
256
|
+
const sessionId = permissionInput.sessionID ?? permissionInput.sessionId;
|
|
257
|
+
if (typeof sessionId === "string" && runManager.hasSession(sessionId)) {
|
|
258
|
+
permissionOutput.status = permissionOutput.status ?? "ask";
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
event: async (input) => {
|
|
262
|
+
await runManager.handleEvent(input);
|
|
263
|
+
},
|
|
264
|
+
config: async (config) => {
|
|
265
|
+
config.command = { ...config.command, ...MAGI_COMMANDS };
|
|
266
|
+
},
|
|
267
|
+
tool: {
|
|
268
|
+
magi_merge: tool({
|
|
269
|
+
description: "Start background Magi merge runs for one or more GitHub pull requests with configured Magi agents.",
|
|
270
|
+
args: {
|
|
271
|
+
prs: tool.schema.string(),
|
|
272
|
+
dryRun: tool.schema.boolean().optional(),
|
|
273
|
+
},
|
|
274
|
+
async execute(args, context) {
|
|
275
|
+
const parsed = parseRunArguments(args.prs, args.dryRun ?? false);
|
|
276
|
+
const loaded = await loadConfig(directory);
|
|
277
|
+
const retryingExec = withGitHubApiRetry(exec, loaded.config.github?.apiRetryAttempts ?? 3);
|
|
278
|
+
const validation = await validateConfig(loaded.config, {
|
|
279
|
+
checkAuth: true,
|
|
280
|
+
directory,
|
|
281
|
+
exec: retryingExec,
|
|
282
|
+
modelCatalog: await modelCatalog(),
|
|
283
|
+
requireEditor: true,
|
|
284
|
+
});
|
|
285
|
+
if (!validation.ok)
|
|
286
|
+
return JSON.stringify(validation, null, 2);
|
|
287
|
+
const repository = resolveRepository(loaded.config);
|
|
288
|
+
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startMerge({
|
|
289
|
+
config: loaded.config,
|
|
290
|
+
dryRun: parsed.dryRun,
|
|
291
|
+
repository,
|
|
292
|
+
pr,
|
|
293
|
+
parentSessionId: context.sessionID,
|
|
294
|
+
signal: context.abort,
|
|
295
|
+
}), { signal: context.abort });
|
|
296
|
+
return states
|
|
297
|
+
.map((state) => `Started reviewing ${prMarkdownLink(repository, state.pr)}.`)
|
|
298
|
+
.join("\n");
|
|
299
|
+
},
|
|
300
|
+
}),
|
|
301
|
+
magi_review: tool({
|
|
302
|
+
description: "Start background Magi review runs for one or more GitHub pull requests and post the reviews.",
|
|
303
|
+
args: {
|
|
304
|
+
prs: tool.schema.string(),
|
|
305
|
+
dryRun: tool.schema.boolean().optional(),
|
|
306
|
+
},
|
|
307
|
+
async execute(args, context) {
|
|
308
|
+
const parsed = parseRunArguments(args.prs, args.dryRun ?? false);
|
|
309
|
+
const loaded = await loadConfig(directory);
|
|
310
|
+
const retryingExec = withGitHubApiRetry(exec, loaded.config.github?.apiRetryAttempts ?? 3);
|
|
311
|
+
const validation = await validateConfig(loaded.config, {
|
|
312
|
+
checkAuth: true,
|
|
313
|
+
directory,
|
|
314
|
+
exec: retryingExec,
|
|
315
|
+
modelCatalog: await modelCatalog(),
|
|
316
|
+
});
|
|
317
|
+
if (!validation.ok)
|
|
318
|
+
return JSON.stringify(validation, null, 2);
|
|
319
|
+
const repository = resolveRepository(loaded.config);
|
|
320
|
+
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startReview({
|
|
321
|
+
config: loaded.config,
|
|
322
|
+
dryRun: parsed.dryRun,
|
|
323
|
+
repository,
|
|
324
|
+
pr,
|
|
325
|
+
parentSessionId: context.sessionID,
|
|
326
|
+
signal: context.abort,
|
|
327
|
+
}), { signal: context.abort });
|
|
328
|
+
return states
|
|
329
|
+
.map((state) => `Started reviewing ${prMarkdownLink(repository, state.pr)}.`)
|
|
330
|
+
.join("\n");
|
|
331
|
+
},
|
|
332
|
+
}),
|
|
333
|
+
magi_status: tool({
|
|
334
|
+
description: "Show Magi background run status. Optionally filter by runId or PR and wait for completion.",
|
|
335
|
+
args: {
|
|
336
|
+
runId: tool.schema.string().optional(),
|
|
337
|
+
pr: tool.schema.string().optional(),
|
|
338
|
+
block: tool.schema.boolean().optional(),
|
|
339
|
+
timeoutSeconds: tool.schema.number().optional(),
|
|
340
|
+
verbose: tool.schema.boolean().optional(),
|
|
341
|
+
},
|
|
342
|
+
async execute(args) {
|
|
343
|
+
const states = await runManager.status({
|
|
344
|
+
block: args.block,
|
|
345
|
+
outputDir: await configuredOutputDir(),
|
|
346
|
+
pr: parseOptionalPr(args.pr),
|
|
347
|
+
runId: args.runId,
|
|
348
|
+
timeoutMs: args.timeoutSeconds == null
|
|
349
|
+
? undefined
|
|
350
|
+
: args.timeoutSeconds * 1_000,
|
|
351
|
+
});
|
|
352
|
+
return runManager.formatStatesWithReports(states, {
|
|
353
|
+
verbose: args.verbose ?? false,
|
|
354
|
+
});
|
|
355
|
+
},
|
|
356
|
+
}),
|
|
357
|
+
magi_output: tool({
|
|
358
|
+
description: "Show artifacts and details for a Magi background run by runId or PR, optionally for a single reviewer.",
|
|
359
|
+
args: {
|
|
360
|
+
runId: tool.schema.string().optional(),
|
|
361
|
+
pr: tool.schema.string().optional(),
|
|
362
|
+
reviewer: tool.schema.string().optional(),
|
|
363
|
+
},
|
|
364
|
+
async execute(args) {
|
|
365
|
+
if (!args.runId && !args.pr)
|
|
366
|
+
return "Specify runId or pr.";
|
|
367
|
+
const outputDir = await configuredOutputDir();
|
|
368
|
+
if (outputDir)
|
|
369
|
+
await runManager.status({ outputDir });
|
|
370
|
+
return runManager.output({
|
|
371
|
+
outputDir,
|
|
372
|
+
pr: parseOptionalPr(args.pr),
|
|
373
|
+
reviewer: args.reviewer,
|
|
374
|
+
runId: args.runId,
|
|
375
|
+
});
|
|
376
|
+
},
|
|
377
|
+
}),
|
|
378
|
+
magi_cancel: tool({
|
|
379
|
+
description: "Cancel a Magi background run by runId or PR.",
|
|
380
|
+
args: {
|
|
381
|
+
runId: tool.schema.string().optional(),
|
|
382
|
+
pr: tool.schema.string().optional(),
|
|
383
|
+
},
|
|
384
|
+
async execute(args) {
|
|
385
|
+
if (!args.runId && !args.pr)
|
|
386
|
+
return "Specify runId or pr.";
|
|
387
|
+
const outputDir = await configuredOutputDir();
|
|
388
|
+
if (outputDir)
|
|
389
|
+
await runManager.status({ outputDir });
|
|
390
|
+
const pr = parseOptionalPr(args.pr);
|
|
391
|
+
const state = await runManager.cancel({
|
|
392
|
+
outputDir,
|
|
393
|
+
pr,
|
|
394
|
+
runId: args.runId,
|
|
395
|
+
});
|
|
396
|
+
if (!state) {
|
|
397
|
+
return args.runId
|
|
398
|
+
? `Magi run not found: ${args.runId}`
|
|
399
|
+
: `Magi run not found for PR #${pr}`;
|
|
400
|
+
}
|
|
401
|
+
return runManager.formatStates([state]);
|
|
402
|
+
},
|
|
403
|
+
}),
|
|
404
|
+
magi_clear: tool({
|
|
405
|
+
description: "Clear all inactive Magi runs by deleting configured sessions, worktrees, branches, and output artifacts.",
|
|
406
|
+
args: {
|
|
407
|
+
runId: tool.schema.string().optional(),
|
|
408
|
+
pr: tool.schema.string().optional(),
|
|
409
|
+
branch: tool.schema.enum(["true", "false"]).optional(),
|
|
410
|
+
output: tool.schema.enum(["true", "false"]).optional(),
|
|
411
|
+
session: tool.schema.enum(["true", "false"]).optional(),
|
|
412
|
+
worktree: tool.schema.enum(["true", "false"]).optional(),
|
|
413
|
+
},
|
|
414
|
+
async execute(args) {
|
|
415
|
+
const loaded = await loadConfig(directory).catch(() => undefined);
|
|
416
|
+
const clear = loaded?.config.clear;
|
|
417
|
+
const useConfiguredDefaults = hasDefaultedFalseClearFlags(args);
|
|
418
|
+
const options = {
|
|
419
|
+
branch: (useConfiguredDefaults
|
|
420
|
+
? undefined
|
|
421
|
+
: clearToolFlag(args.branch)) ?? clearFlag(clear?.branch),
|
|
422
|
+
output: (useConfiguredDefaults
|
|
423
|
+
? undefined
|
|
424
|
+
: clearToolFlag(args.output)) ?? clearFlag(clear?.output),
|
|
425
|
+
session: (useConfiguredDefaults
|
|
426
|
+
? undefined
|
|
427
|
+
: clearToolFlag(args.session)) ?? clearFlag(clear?.session),
|
|
428
|
+
worktree: (useConfiguredDefaults
|
|
429
|
+
? undefined
|
|
430
|
+
: clearToolFlag(args.worktree)) ?? clearFlag(clear?.worktree),
|
|
431
|
+
};
|
|
432
|
+
return runManager.clear({
|
|
433
|
+
options,
|
|
434
|
+
outputDir: loaded
|
|
435
|
+
? outputBaseDirs(directory, loaded.config)
|
|
436
|
+
: undefined,
|
|
437
|
+
pr: parseOptionalPr(args.pr),
|
|
438
|
+
runId: args.runId,
|
|
439
|
+
worktreeDir: loaded
|
|
440
|
+
? worktreeBaseDirs(directory, loaded.config)
|
|
441
|
+
: undefined,
|
|
442
|
+
});
|
|
443
|
+
},
|
|
444
|
+
}),
|
|
445
|
+
magi_permission_reply: tool({
|
|
446
|
+
description: "Reply to a pending Magi child-agent permission request by runId or PR.",
|
|
447
|
+
args: {
|
|
448
|
+
runId: tool.schema.string().optional(),
|
|
449
|
+
pr: tool.schema.string().optional(),
|
|
450
|
+
agent: tool.schema.string().optional(),
|
|
451
|
+
reviewer: tool.schema.string().optional(),
|
|
452
|
+
requestId: tool.schema.string().optional(),
|
|
453
|
+
reply: tool.schema.string(),
|
|
454
|
+
},
|
|
455
|
+
async execute(args) {
|
|
456
|
+
if (!args.runId && !args.pr)
|
|
457
|
+
return "Specify runId or pr.";
|
|
458
|
+
if (!["always", "once", "reject"].includes(args.reply)) {
|
|
459
|
+
return "reply must be once, always, or reject.";
|
|
460
|
+
}
|
|
461
|
+
const outputDir = await configuredOutputDir();
|
|
462
|
+
if (outputDir)
|
|
463
|
+
await runManager.status({ outputDir });
|
|
464
|
+
return runManager.replyPermission({
|
|
465
|
+
agent: args.agent ?? args.reviewer,
|
|
466
|
+
outputDir,
|
|
467
|
+
pr: parseOptionalPr(args.pr),
|
|
468
|
+
reply: args.reply,
|
|
469
|
+
requestId: args.requestId,
|
|
470
|
+
runId: args.runId,
|
|
471
|
+
});
|
|
472
|
+
},
|
|
473
|
+
}),
|
|
474
|
+
magi_question_reply: tool({
|
|
475
|
+
description: "Reply to a pending Magi child-agent question request by runId or PR.",
|
|
476
|
+
args: {
|
|
477
|
+
runId: tool.schema.string().optional(),
|
|
478
|
+
pr: tool.schema.string().optional(),
|
|
479
|
+
agent: tool.schema.string().optional(),
|
|
480
|
+
reviewer: tool.schema.string().optional(),
|
|
481
|
+
requestId: tool.schema.string().optional(),
|
|
482
|
+
answers: tool.schema.string(),
|
|
483
|
+
},
|
|
484
|
+
async execute(args) {
|
|
485
|
+
if (!args.runId && !args.pr)
|
|
486
|
+
return "Specify runId or pr.";
|
|
487
|
+
const outputDir = await configuredOutputDir();
|
|
488
|
+
if (outputDir)
|
|
489
|
+
await runManager.status({ outputDir });
|
|
490
|
+
return runManager.replyQuestion({
|
|
491
|
+
agent: args.agent ?? args.reviewer,
|
|
492
|
+
answers: parseQuestionAnswers(args.answers),
|
|
493
|
+
outputDir,
|
|
494
|
+
pr: parseOptionalPr(args.pr),
|
|
495
|
+
requestId: args.requestId,
|
|
496
|
+
runId: args.runId,
|
|
497
|
+
});
|
|
498
|
+
},
|
|
499
|
+
}),
|
|
500
|
+
magi_question_reject: tool({
|
|
501
|
+
description: "Reject a pending Magi child-agent question request by runId or PR.",
|
|
502
|
+
args: {
|
|
503
|
+
runId: tool.schema.string().optional(),
|
|
504
|
+
pr: tool.schema.string().optional(),
|
|
505
|
+
agent: tool.schema.string().optional(),
|
|
506
|
+
reviewer: tool.schema.string().optional(),
|
|
507
|
+
requestId: tool.schema.string().optional(),
|
|
508
|
+
},
|
|
509
|
+
async execute(args) {
|
|
510
|
+
if (!args.runId && !args.pr)
|
|
511
|
+
return "Specify runId or pr.";
|
|
512
|
+
const outputDir = await configuredOutputDir();
|
|
513
|
+
if (outputDir)
|
|
514
|
+
await runManager.status({ outputDir });
|
|
515
|
+
return runManager.rejectQuestion({
|
|
516
|
+
agent: args.agent ?? args.reviewer,
|
|
517
|
+
outputDir,
|
|
518
|
+
pr: parseOptionalPr(args.pr),
|
|
519
|
+
requestId: args.requestId,
|
|
520
|
+
runId: args.runId,
|
|
521
|
+
});
|
|
522
|
+
},
|
|
523
|
+
}),
|
|
524
|
+
magi_validate: tool({
|
|
525
|
+
description: "Validate global and project Magi config presence, merged settings, reviewer rules, model IDs, and GitHub authentication.",
|
|
526
|
+
args: {
|
|
527
|
+
checkAuth: tool.schema.boolean().optional(),
|
|
528
|
+
},
|
|
529
|
+
async execute(args) {
|
|
530
|
+
return validateMagiConfigFiles(directory, {
|
|
531
|
+
checkAuth: args.checkAuth ?? true,
|
|
532
|
+
exec,
|
|
533
|
+
modelCatalog: await modelCatalog(),
|
|
534
|
+
});
|
|
535
|
+
},
|
|
536
|
+
}),
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
};
|
|
540
|
+
export default MagiPlugin;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function throwIfAborted(signal) {
|
|
2
|
+
signal?.throwIfAborted();
|
|
3
|
+
}
|
|
4
|
+
export function withAbortSignal(exec, signal) {
|
|
5
|
+
return async (command, options) => {
|
|
6
|
+
throwIfAborted(signal);
|
|
7
|
+
return exec(command, { ...options, signal: options?.signal ?? signal });
|
|
8
|
+
};
|
|
9
|
+
}
|