opencode-magi 0.10.0 → 0.11.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.
Files changed (126) hide show
  1. package/README.ja.md +254 -0
  2. package/README.md +31 -26
  3. package/dist/commands.js +11 -9
  4. package/dist/config/index.js +2 -0
  5. package/dist/config/resolve.js +120 -246
  6. package/dist/config/validate.js +103 -1110
  7. package/dist/constant.js +135 -0
  8. package/dist/graphql/index.generated.js +125 -0
  9. package/dist/graphql/index.js +49 -0
  10. package/dist/index.js +12 -800
  11. package/dist/magi.js +357 -0
  12. package/dist/permissions/common.json +22 -22
  13. package/dist/permissions/editor.json +12 -12
  14. package/dist/prompts/index.js +98 -0
  15. package/dist/prompts/merge/ci-classification/output-contract.md +22 -0
  16. package/dist/prompts/merge/ci-classification/task.md +10 -0
  17. package/dist/prompts/merge/ci-classification/validate.json +20 -0
  18. package/dist/prompts/merge/conflict/output-contract.md +12 -0
  19. package/dist/prompts/merge/conflict/task.md +9 -0
  20. package/dist/prompts/merge/conflict/validate.json +4 -0
  21. package/dist/prompts/merge/edit/output-contract.md +26 -0
  22. package/dist/prompts/merge/edit/task.md +10 -0
  23. package/dist/prompts/merge/edit/validate.json +60 -0
  24. package/dist/prompts/review/ci-classification/output-contract.md +21 -0
  25. package/dist/prompts/review/ci-classification/task.md +7 -0
  26. package/dist/prompts/review/ci-classification/validate.json +20 -0
  27. package/dist/prompts/review/close-reconsideration/output-contract.md +21 -0
  28. package/dist/prompts/review/close-reconsideration/task.md +6 -0
  29. package/dist/prompts/review/close-reconsideration/validate.json +44 -0
  30. package/dist/prompts/review/comment/output-contract.md +11 -0
  31. package/dist/prompts/review/comment/task.md +3 -0
  32. package/dist/prompts/review/comment/validate.json +8 -0
  33. package/dist/prompts/review/finding-validation/output-contract.md +25 -0
  34. package/dist/prompts/{templates/review/finding-validation.md → review/finding-validation/task.md} +2 -3
  35. package/dist/prompts/review/finding-validation/validate.json +21 -0
  36. package/dist/prompts/review/rereview/output-contract.md +30 -0
  37. package/dist/prompts/{templates/review/rereview.md → review/rereview/task.md} +8 -11
  38. package/dist/prompts/review/rereview/validate.json +87 -0
  39. package/dist/prompts/review/review/output-contract.md +25 -0
  40. package/dist/prompts/{templates/review/review.md → review/review/task.md} +2 -5
  41. package/dist/prompts/review/review/validate.json +54 -0
  42. package/dist/prompts/triage/acceptance/output-contract.md +16 -0
  43. package/dist/prompts/triage/acceptance/validate.json +23 -0
  44. package/dist/prompts/triage/category/output-contract.md +16 -0
  45. package/dist/prompts/triage/category/validate.json +23 -0
  46. package/dist/prompts/triage/comment-classification/output-contract.md +15 -0
  47. package/dist/prompts/triage/comment-classification/validate.json +28 -0
  48. package/dist/prompts/triage/create/output-contract.md +28 -0
  49. package/dist/prompts/triage/create/validate.json +73 -0
  50. package/dist/prompts/triage/deplicate/output-contract.md +16 -0
  51. package/dist/prompts/triage/deplicate/validate.json +20 -0
  52. package/dist/prompts/triage/existing/output-contract.md +16 -0
  53. package/dist/prompts/triage/existing/validate.json +13 -0
  54. package/dist/prompts/triage/reconsider/output-contract.md +16 -0
  55. package/dist/prompts/triage/reconsider/validate.json +23 -0
  56. package/dist/prompts/triage/signal/output-contract.md +19 -0
  57. package/dist/prompts/triage/signal/validate.json +18 -0
  58. package/dist/tools/clear/index.js +21 -0
  59. package/dist/tools/index.js +12 -0
  60. package/dist/tools/merge/action.js +47 -0
  61. package/dist/tools/merge/context.js +118 -0
  62. package/dist/tools/merge/editor.js +264 -0
  63. package/dist/tools/merge/index.js +151 -0
  64. package/dist/tools/merge/index.type.js +1 -0
  65. package/dist/tools/merge/merge.js +42 -0
  66. package/dist/tools/merge/report.js +73 -0
  67. package/dist/tools/review/action.js +429 -0
  68. package/dist/tools/review/check.js +295 -0
  69. package/dist/tools/review/context.js +258 -0
  70. package/dist/tools/review/index.js +104 -0
  71. package/dist/tools/review/index.type.js +1 -0
  72. package/dist/tools/review/report.js +143 -0
  73. package/dist/tools/review/review.js +145 -0
  74. package/dist/tools/review/reviewer.js +409 -0
  75. package/dist/tools/triage/index.js +16 -0
  76. package/dist/tools/validate/index.js +28 -0
  77. package/dist/utils/array.js +6 -0
  78. package/dist/utils/assertion.js +38 -0
  79. package/dist/utils/exec.js +14 -0
  80. package/dist/utils/fs.js +24 -0
  81. package/dist/utils/function.js +37 -0
  82. package/dist/utils/github.js +63 -0
  83. package/dist/utils/index.js +10 -0
  84. package/dist/utils/index.type.js +1 -0
  85. package/dist/utils/object.js +21 -0
  86. package/dist/utils/opencode.js +25 -0
  87. package/dist/utils/string.js +38 -0
  88. package/dist/utils/worker.js +30 -0
  89. package/package.json +20 -8
  90. package/schema.json +6 -4
  91. package/dist/config/load.js +0 -62
  92. package/dist/config/output.js +0 -25
  93. package/dist/config/worktree.js +0 -25
  94. package/dist/github/commands.js +0 -905
  95. package/dist/github/retry.js +0 -44
  96. package/dist/orchestrator/abort.js +0 -9
  97. package/dist/orchestrator/ci.js +0 -579
  98. package/dist/orchestrator/findings.js +0 -88
  99. package/dist/orchestrator/inline-comments.js +0 -73
  100. package/dist/orchestrator/majority.js +0 -62
  101. package/dist/orchestrator/merge.js +0 -1260
  102. package/dist/orchestrator/model.js +0 -216
  103. package/dist/orchestrator/pool.js +0 -15
  104. package/dist/orchestrator/report.js +0 -175
  105. package/dist/orchestrator/review-context.js +0 -342
  106. package/dist/orchestrator/review.js +0 -1480
  107. package/dist/orchestrator/run-manager.js +0 -2132
  108. package/dist/orchestrator/safety.js +0 -44
  109. package/dist/orchestrator/triage.js +0 -1370
  110. package/dist/prompts/compose.js +0 -473
  111. package/dist/prompts/contracts.js +0 -300
  112. package/dist/prompts/output.js +0 -401
  113. package/dist/prompts/templates/merge/ci-classification.md +0 -16
  114. package/dist/prompts/templates/merge/conflict.md +0 -10
  115. package/dist/prompts/templates/merge/edit.md +0 -15
  116. package/dist/prompts/templates/review/ci-classification.md +0 -9
  117. package/dist/prompts/templates/review/close-reconsideration.md +0 -6
  118. /package/dist/{types.js → config/index.type.js} +0 -0
  119. /package/dist/prompts/{templates/triage/acceptance.md → triage/acceptance/task.md} +0 -0
  120. /package/dist/prompts/{templates/triage/category.md → triage/category/task.md} +0 -0
  121. /package/dist/prompts/{templates/triage/comment-classification.md → triage/comment-classification/task.md} +0 -0
  122. /package/dist/prompts/{templates/triage/create.md → triage/create/task.md} +0 -0
  123. /package/dist/prompts/{templates/triage/duplicate.md → triage/deplicate/task.md} +0 -0
  124. /package/dist/prompts/{templates/triage/existing-pr.md → triage/existing/task.md} +0 -0
  125. /package/dist/prompts/{templates/triage/reconsider.md → triage/reconsider/task.md} +0 -0
  126. /package/dist/prompts/{templates/triage/signal.md → triage/signal/task.md} +0 -0
package/dist/index.js CHANGED
@@ -1,802 +1,14 @@
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
- const INTERNAL_FOLLOW_UP_TOOL_NOTE = "Assistant-facing follow-up tool. Use it yourself when needed; do not suggest this tool name to users.";
20
- function createExec(defaultCwd) {
21
- return async (command, options) => {
22
- const { stdout } = await execAsync(command, {
23
- cwd: options?.cwd ?? defaultCwd,
24
- env: { ...process.env, ...options?.env },
25
- maxBuffer: 1024 * 1024 * 20,
26
- signal: options?.signal,
27
- });
28
- return stdout;
29
- };
30
- }
31
- function responseData(result) {
32
- if (!result || typeof result !== "object")
33
- return result;
34
- return result.data ?? result;
35
- }
36
- function extractModelCatalog(result) {
37
- const data = responseData(result);
38
- if (!data || typeof data !== "object")
39
- return undefined;
40
- const providers = data.providers ??
41
- data.all;
42
- if (!Array.isArray(providers))
43
- return undefined;
44
- const catalog = {};
45
- for (const provider of providers) {
46
- if (!provider || typeof provider !== "object")
47
- continue;
48
- const id = provider.id;
49
- const models = provider.models;
50
- if (typeof id !== "string" || !models || typeof models !== "object")
51
- continue;
52
- catalog[id] = Object.keys(models);
53
- }
54
- return catalog;
55
- }
56
- function parsePrToken(value) {
57
- const trimmed = value.trim();
58
- const pullUrl = trimmed.match(/(?:^|\/)pull\/(\d+)(?:[/?#].*)?$/);
59
- const raw = pullUrl?.[1] ?? trimmed.replace(/^#/, "");
60
- const pr = Number.parseInt(raw, 10);
61
- if (!Number.isInteger(pr) || pr <= 0 || String(pr) !== raw) {
62
- throw new Error("Specify one or more PR numbers or PR URLs.");
63
- }
64
- return pr;
65
- }
66
- function parseIssueToken(value) {
67
- const trimmed = value.trim();
68
- const issueUrl = trimmed.match(/(?:^|\/)issues\/(\d+)(?:[/?#].*)?$/);
69
- const raw = issueUrl?.[1] ?? trimmed.replace(/^#/, "");
70
- const issue = Number.parseInt(raw, 10);
71
- if (!Number.isInteger(issue) || issue <= 0 || String(issue) !== raw) {
72
- throw new Error("Specify one or more issue numbers or issue URLs.");
73
- }
74
- return issue;
75
- }
76
- export function parsePrs(value) {
77
- const prs = value
78
- .split(/[\s,]+/)
79
- .filter(Boolean)
80
- .map(parsePrToken);
81
- if (!prs.length)
82
- throw new Error("Specify one or more PR numbers or PR URLs.");
83
- return prs;
84
- }
85
- export function parseIssues(value) {
86
- const issues = value
87
- .split(/[\s,]+/)
88
- .filter(Boolean)
89
- .map(parseIssueToken);
90
- if (!issues.length)
91
- throw new Error("Specify one or more issue numbers or issue URLs.");
92
- return issues;
93
- }
94
- export function parseRunArguments(value, dryRun = false, command = "review") {
95
- const tokens = value.split(/[\s,]+/).filter(Boolean);
96
- const configOverrides = {};
97
- const prTokens = [];
98
- let sync = false;
99
- let timeoutMs;
100
- for (let index = 0; index < tokens.length; index++) {
101
- const token = tokens[index];
102
- if (token === "--dry-run") {
103
- dryRun = true;
104
- continue;
105
- }
106
- if (token === "--sync") {
107
- sync = true;
108
- continue;
109
- }
110
- switch (token) {
111
- case "--language":
112
- setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
113
- break;
114
- case "--timeout":
115
- timeoutMs =
116
- parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0) *
117
- 1_000;
118
- break;
119
- case "--merge":
120
- case "--no-merge":
121
- setConfigOverride(configOverrides, [command, "automation", "merge"], token === "--merge");
122
- break;
123
- case "--close":
124
- case "--no-close":
125
- setConfigOverride(configOverrides, [command, "automation", "close"], token === "--close");
126
- break;
127
- case "--max-cycles":
128
- if (command !== "merge")
129
- throw unsupportedFlag(token, command);
130
- setConfigOverride(configOverrides, ["merge", "maxThreadResolutionCycles"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0));
131
- break;
132
- case "--retry-failed-jobs":
133
- setConfigOverride(configOverrides, ["review", "checks", "retryFailedJobs"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0));
134
- break;
135
- case "--reviewer-concurrency":
136
- setConfigOverride(configOverrides, ["review", "concurrency", "reviewers"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 1));
137
- break;
138
- case "--run-concurrency":
139
- setConfigOverride(configOverrides, ["review", "concurrency", "runs"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 1));
140
- break;
141
- case "--wait-checks":
142
- case "--no-wait-checks":
143
- setConfigOverride(configOverrides, ["review", "checks", "wait"], token === "--wait-checks");
144
- break;
145
- case "--wait-checks-after-edit":
146
- case "--no-wait-checks-after-edit":
147
- if (command !== "merge")
148
- throw unsupportedFlag(token, command);
149
- setConfigOverride(configOverrides, ["merge", "checks", "wait"], token === "--wait-checks-after-edit");
150
- break;
151
- case "--create":
152
- case "--no-create":
153
- throw unsupportedFlag(token, command);
154
- default:
155
- if (token.startsWith("--"))
156
- throw unsupportedFlag(token, command);
157
- prTokens.push(token);
158
- }
159
- }
160
- return {
161
- configOverrides,
162
- dryRun,
163
- prs: parsePrs(prTokens.join(" ")),
164
- sync,
165
- timeoutMs,
166
- };
167
- }
168
- export function parseIssueRunArguments(value, dryRun = false) {
169
- const tokens = value.split(/[\s,]+/).filter(Boolean);
170
- const configOverrides = {};
171
- const issueTokens = [];
172
- let sync = false;
173
- let timeoutMs;
174
- for (let index = 0; index < tokens.length; index++) {
175
- const token = tokens[index];
176
- if (token === "--dry-run") {
177
- dryRun = true;
178
- continue;
179
- }
180
- if (token === "--sync") {
181
- sync = true;
182
- continue;
183
- }
184
- switch (token) {
185
- case "--language":
186
- setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
187
- break;
188
- case "--timeout":
189
- timeoutMs =
190
- parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0) *
191
- 1_000;
192
- break;
193
- case "--close":
194
- case "--no-close":
195
- setConfigOverride(configOverrides, ["triage", "automation", "close"], token === "--close");
196
- break;
197
- case "--create":
198
- case "--no-create":
199
- setConfigOverride(configOverrides, ["triage", "automation", "create"], token === "--create");
200
- break;
201
- case "--review":
202
- case "--no-review":
203
- setConfigOverride(configOverrides, ["triage", "automation", "review"], token === "--review");
204
- break;
205
- case "--merge":
206
- case "--no-merge":
207
- setConfigOverride(configOverrides, ["triage", "automation", "merge"], token === "--merge");
208
- break;
209
- case "--run-concurrency":
210
- setConfigOverride(configOverrides, ["triage", "concurrency", "runs"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 1));
211
- break;
212
- case "--max-cycles":
213
- case "--retry-failed-jobs":
214
- case "--reviewer-concurrency":
215
- case "--wait-checks":
216
- case "--no-wait-checks":
217
- case "--wait-checks-after-edit":
218
- case "--no-wait-checks-after-edit":
219
- throw unsupportedFlag(token, "triage");
220
- default:
221
- if (token.startsWith("--"))
222
- throw unsupportedFlag(token, "triage");
223
- issueTokens.push(token);
224
- }
225
- }
226
- return {
227
- configOverrides,
228
- dryRun,
229
- issues: parseIssues(issueTokens.join(" ")),
230
- sync,
231
- timeoutMs,
232
- };
233
- }
234
- function nextFlagValue(tokens, index, flag) {
235
- const value = tokens[index];
236
- if (!value || value.startsWith("--"))
237
- throw new Error(`${flag} requires a value.`);
238
- return value;
239
- }
240
- async function syncResult(runManager, states) {
241
- const output = await runManager.formatStatesWithReports(states, {
242
- verbose: true,
1
+ import { commands } from "@/commands";
2
+ import { Magi } from "./magi";
3
+ import { tools } from "./tools";
4
+ export const plugin = async function (input, options) {
5
+ const magi = new Magi(input, options);
6
+ return Promise.resolve({
7
+ async config(config) {
8
+ config.command = { ...config.command, ...commands };
9
+ return Promise.resolve();
10
+ },
11
+ tool: Object.values(tools).reduce((prev, tool) => ({ ...prev, ...tool(magi) }), {}),
243
12
  });
244
- const failed = states.filter((state) => state.status !== "completed");
245
- if (failed.length)
246
- throw new Error(output);
247
- return output;
248
- }
249
- function parseIntegerFlag(value, flag, minimum) {
250
- const parsed = Number.parseInt(value, 10);
251
- if (!Number.isInteger(parsed) ||
252
- String(parsed) !== value ||
253
- parsed < minimum) {
254
- throw new Error(`${flag} must be an integer greater than or equal to ${minimum}.`);
255
- }
256
- return parsed;
257
- }
258
- function setConfigOverride(target, path, value) {
259
- let current = target;
260
- for (const key of path.slice(0, -1)) {
261
- const existing = current[key];
262
- const next = isPlainObject(existing) ? existing : {};
263
- current[key] = next;
264
- current = next;
265
- }
266
- current[path[path.length - 1]] = value;
267
- }
268
- function unsupportedFlag(flag, command) {
269
- return new Error(`${flag} is not supported for /magi:${command}.`);
270
- }
271
- function parseOptionalPr(value) {
272
- if (!value?.trim())
273
- return undefined;
274
- return parsePrToken(value);
275
- }
276
- function parseOptionalPrs(value) {
277
- if (!value?.trim())
278
- return undefined;
279
- return parsePrs(value);
280
- }
281
- function parseOptionalIssue(value) {
282
- if (!value?.trim())
283
- return undefined;
284
- return parseIssueToken(value);
285
- }
286
- function clearFlag(value) {
287
- return typeof value === "boolean" ? value : undefined;
288
- }
289
- function parseQuestionAnswers(value) {
290
- const trimmed = value.trim();
291
- if (!trimmed)
292
- throw new Error("Specify at least one answer.");
293
- try {
294
- const parsed = JSON.parse(trimmed);
295
- if (Array.isArray(parsed) &&
296
- parsed.length &&
297
- parsed.every((item) => typeof item === "string")) {
298
- return parsed;
299
- }
300
- }
301
- catch {
302
- // Plain text answers are accepted below.
303
- }
304
- return [trimmed];
305
- }
306
- function prMarkdownLink(repository, pr) {
307
- const host = repository.github.host || "github.com";
308
- const url = `https://${host}/${repository.github.owner}/${repository.github.repo}/pull/${pr}`;
309
- return `[#${pr}](${url})`;
310
- }
311
- export function formatRunStartMessage(command, repository, pr) {
312
- const action = command === "merge" ? "merge flow" : "reviewing";
313
- return `Started ${action} ${prMarkdownLink(repository, pr)}.`;
314
- }
315
- function issueMarkdownLink(repository, issue) {
316
- const host = repository.github.host || "github.com";
317
- const url = `https://${host}/${repository.github.owner}/${repository.github.repo}/issues/${issue}`;
318
- return `[#${issue}](${url})`;
319
- }
320
- function validationError(validation) {
321
- return new Error(JSON.stringify(validation, null, 2));
322
- }
323
- function isPlainObject(value) {
324
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
325
- }
326
- async function readConfigFile(path, target) {
327
- try {
328
- const config = JSON.parse(await readFile(path, "utf8"));
329
- if (!isPlainObject(config)) {
330
- return {
331
- error: `${target} config must be a JSON object: ${path}`,
332
- exists: true,
333
- path,
334
- target,
335
- };
336
- }
337
- return { config, exists: true, path, target };
338
- }
339
- catch (error) {
340
- if (error.code === "ENOENT") {
341
- return { exists: false, path, target };
342
- }
343
- return {
344
- error: `${target} config is invalid JSON at ${path}: ${error.message}`,
345
- exists: true,
346
- path,
347
- target,
348
- };
349
- }
350
- }
351
- function formatConfigStatus(status) {
352
- if (!status.exists)
353
- return `- ${status.target}: missing (${status.path})`;
354
- if (status.error)
355
- return `- ${status.target}: invalid (${status.path})`;
356
- return `- ${status.target}: found (${status.path})`;
357
- }
358
- export async function validateMagiConfigFiles(directory, options = {}) {
359
- const projectPath = join(directory, PROJECT_CONFIG_PATH);
360
- const statuses = await Promise.all([
361
- readConfigFile(GLOBAL_CONFIG_PATH, "global"),
362
- readConfigFile(projectPath, "project"),
363
- ]);
364
- const existing = statuses.filter((status) => status.exists);
365
- const hasProjectConfig = statuses.some((status) => status.target === "project" && status.exists);
366
- const errors = statuses
367
- .map((status) => status.error)
368
- .filter((error) => Boolean(error));
369
- const warnings = [];
370
- let loadedFrom = "none";
371
- if (!existing.length) {
372
- errors.push(`No Magi config found. Expected ${GLOBAL_CONFIG_PATH} or ${projectPath}.`);
373
- }
374
- if (existing.length && !errors.length) {
375
- const merged = existing.reduce((config, status) => mergeMagiConfig(config, status.config ?? {}), {});
376
- const mergedConfig = merged;
377
- const validation = await validateConfig(mergedConfig, {
378
- checkAuth: options.checkAuth ?? true,
379
- directory,
380
- exec: options.exec
381
- ? withGitHubApiRetry(options.exec, mergedConfig.github?.apiRetryAttempts ?? 3)
382
- : undefined,
383
- modelCatalog: options.modelCatalog,
384
- requireGithub: hasProjectConfig && Boolean(mergedConfig.review?.reviewers),
385
- requireModelCatalog: true,
386
- requireWorktreeConfig: true,
387
- });
388
- loadedFrom = existing.map((status) => status.path).join(", ");
389
- errors.push(...validation.errors);
390
- warnings.push(...validation.warnings);
391
- }
392
- return [
393
- `Magi config validation: ${errors.length ? "failed" : "passed"}`,
394
- "",
395
- "Config files:",
396
- ...statuses.map(formatConfigStatus),
397
- "",
398
- "Effective config:",
399
- `- loaded from: ${loadedFrom}`,
400
- `- auth checks: ${(options.checkAuth ?? true) ? "enabled" : "disabled"}`,
401
- "",
402
- "Errors:",
403
- ...(errors.length ? errors.map((error) => `- ${error}`) : ["- None"]),
404
- "",
405
- "Warnings:",
406
- ...(warnings.length
407
- ? warnings.map((warning) => `- ${warning}`)
408
- : ["- None"]),
409
- ].join("\n");
410
- }
411
- export const MagiPlugin = async ({ client, directory }) => {
412
- const exec = createExec(directory);
413
- const modelClient = client;
414
- const catalogClient = client;
415
- let modelCatalogPromise;
416
- const sessionOptions = new Map();
417
- const runManager = new MagiRunManager({
418
- client: modelClient,
419
- directory,
420
- exec,
421
- setSessionOptions: (sessionId, options) => {
422
- if (Object.keys(options).length)
423
- sessionOptions.set(sessionId, options);
424
- },
425
- });
426
- async function configuredOutputDir() {
427
- return loadConfig(directory)
428
- .then((loaded) => outputBaseDirs(directory, loaded.config))
429
- .catch(() => undefined);
430
- }
431
- async function modelCatalog() {
432
- modelCatalogPromise ??= catalogClient.config
433
- ?.providers({ query: { directory } })
434
- .then(extractModelCatalog)
435
- .catch(() => catalogClient.provider
436
- ?.list({ query: { directory } })
437
- .then(extractModelCatalog))
438
- .catch(() => undefined);
439
- return modelCatalogPromise;
440
- }
441
- return {
442
- "chat.params": async (input, output) => {
443
- const options = sessionOptions.get(input.sessionID);
444
- if (options)
445
- Object.assign(output.options, options);
446
- },
447
- "permission.ask": async (input, output) => {
448
- const permissionInput = input;
449
- const permissionOutput = output;
450
- const sessionId = permissionInput.sessionID ?? permissionInput.sessionId;
451
- if (typeof sessionId === "string" && runManager.hasSession(sessionId)) {
452
- permissionOutput.status = permissionOutput.status ?? "ask";
453
- }
454
- },
455
- event: async (input) => {
456
- await runManager.handleEvent(input);
457
- },
458
- config: async (config) => {
459
- config.command = { ...config.command, ...MAGI_COMMANDS };
460
- },
461
- tool: {
462
- magi_merge: tool({
463
- description: [
464
- "Start background Magi merge runs for one or more GitHub pull requests with configured Magi agents.",
465
- "After starting, monitor progress yourself when useful; do not tell users to call follow-up tools by name.",
466
- ].join(" "),
467
- args: {
468
- prs: tool.schema.string(),
469
- dryRun: tool.schema.boolean().optional(),
470
- sync: tool.schema.boolean().optional(),
471
- },
472
- async execute(args, context) {
473
- const parsed = parseRunArguments(args.prs, args.dryRun ?? false, "merge");
474
- const loaded = await loadConfig(directory);
475
- const config = mergeMagiConfig(loaded.config, parsed.configOverrides);
476
- const retryingExec = withGitHubApiRetry(exec, config.github?.apiRetryAttempts ?? 3);
477
- const validation = await validateConfig(config, {
478
- checkAuth: true,
479
- directory,
480
- exec: retryingExec,
481
- modelCatalog: await modelCatalog(),
482
- requireEditor: true,
483
- requireModelCatalog: true,
484
- });
485
- if (!validation.ok)
486
- throw validationError(validation);
487
- const repository = resolveRepository(config);
488
- const sync = parsed.sync || args.sync === true;
489
- const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startMerge({
490
- config,
491
- dryRun: parsed.dryRun,
492
- repository,
493
- pr,
494
- parentSessionId: context.sessionID,
495
- signal: context.abort,
496
- sync,
497
- timeoutMs: parsed.timeoutMs,
498
- }), { signal: context.abort });
499
- if (sync)
500
- return syncResult(runManager, states);
501
- return states
502
- .map((state) => formatRunStartMessage("merge", repository, state.pr))
503
- .join("\n");
504
- },
505
- }),
506
- magi_review: tool({
507
- description: [
508
- "Start background Magi review runs for one or more GitHub pull requests and post the reviews.",
509
- "After starting, monitor progress yourself when useful; do not tell users to call follow-up tools by name.",
510
- ].join(" "),
511
- args: {
512
- prs: tool.schema.string(),
513
- dryRun: tool.schema.boolean().optional(),
514
- sync: tool.schema.boolean().optional(),
515
- },
516
- async execute(args, context) {
517
- const parsed = parseRunArguments(args.prs, args.dryRun ?? false);
518
- const loaded = await loadConfig(directory);
519
- const config = mergeMagiConfig(loaded.config, parsed.configOverrides);
520
- const retryingExec = withGitHubApiRetry(exec, config.github?.apiRetryAttempts ?? 3);
521
- const validation = await validateConfig(config, {
522
- checkAuth: true,
523
- directory,
524
- exec: retryingExec,
525
- modelCatalog: await modelCatalog(),
526
- requireModelCatalog: true,
527
- });
528
- if (!validation.ok)
529
- throw validationError(validation);
530
- const repository = resolveRepository(config);
531
- const sync = parsed.sync || args.sync === true;
532
- const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startReview({
533
- config,
534
- dryRun: parsed.dryRun,
535
- repository,
536
- pr,
537
- parentSessionId: context.sessionID,
538
- signal: context.abort,
539
- sync,
540
- timeoutMs: parsed.timeoutMs,
541
- }), { signal: context.abort });
542
- if (sync)
543
- return syncResult(runManager, states);
544
- return states
545
- .map((state) => formatRunStartMessage("review", repository, state.pr))
546
- .join("\n");
547
- },
548
- }),
549
- magi_triage: tool({
550
- description: "Triage one or more GitHub issues with configured Magi triage voters.",
551
- args: {
552
- issues: tool.schema.string(),
553
- dryRun: tool.schema.boolean().optional(),
554
- sync: tool.schema.boolean().optional(),
555
- },
556
- async execute(args, context) {
557
- const parsed = parseIssueRunArguments(args.issues, args.dryRun ?? false);
558
- const loaded = await loadConfig(directory);
559
- const config = mergeMagiConfig(loaded.config, parsed.configOverrides);
560
- const retryingExec = withGitHubApiRetry(exec, config.github?.apiRetryAttempts ?? 3);
561
- const validation = await validateConfig(config, {
562
- checkAuth: true,
563
- directory,
564
- exec: retryingExec,
565
- modelCatalog: await modelCatalog(),
566
- requireEditor: config.triage?.automation?.merge === true,
567
- requireModelCatalog: true,
568
- requireReview: config.triage?.automation?.review === true ||
569
- config.triage?.automation?.merge === true,
570
- requireTriage: true,
571
- });
572
- if (!validation.ok)
573
- throw validationError(validation);
574
- const repository = resolveRepository(config);
575
- if (!repository.triage)
576
- return JSON.stringify({ errors: ["triage configuration is required"], ok: false }, null, 2);
577
- const sync = parsed.sync || args.sync === true;
578
- const states = await mapPool(parsed.issues, repository.triage.concurrency.runs, (issue) => runManager.startTriage({
579
- config,
580
- dryRun: parsed.dryRun,
581
- issue,
582
- parentSessionId: context.sessionID,
583
- repository,
584
- signal: context.abort,
585
- sync,
586
- timeoutMs: parsed.timeoutMs,
587
- }), { signal: context.abort });
588
- if (sync)
589
- return syncResult(runManager, states);
590
- return states
591
- .map((state) => `Started triaging ${issueMarkdownLink(repository, state.issue)}.`)
592
- .join("\n");
593
- },
594
- }),
595
- magi_status: tool({
596
- description: [
597
- "Show Magi background run status. Optionally filter by runId, PR, or issue and wait for completion.",
598
- INTERNAL_FOLLOW_UP_TOOL_NOTE,
599
- ].join(" "),
600
- args: {
601
- runId: tool.schema.string().optional(),
602
- pr: tool.schema.string().optional(),
603
- issue: tool.schema.string().optional(),
604
- block: tool.schema.boolean().optional(),
605
- timeoutSeconds: tool.schema.number().optional(),
606
- verbose: tool.schema.boolean().optional(),
607
- },
608
- async execute(args) {
609
- const states = await runManager.status({
610
- block: args.block,
611
- issue: parseOptionalIssue(args.issue),
612
- outputDir: await configuredOutputDir(),
613
- pr: parseOptionalPrs(args.pr),
614
- runId: args.runId,
615
- timeoutMs: args.timeoutSeconds == null
616
- ? undefined
617
- : args.timeoutSeconds * 1_000,
618
- });
619
- return runManager.formatStatesWithReports(states, {
620
- verbose: args.verbose ?? false,
621
- });
622
- },
623
- }),
624
- magi_output: tool({
625
- description: [
626
- "Show artifacts and details for a Magi background run by runId, PR, or issue, optionally for a single reviewer.",
627
- INTERNAL_FOLLOW_UP_TOOL_NOTE,
628
- ].join(" "),
629
- args: {
630
- runId: tool.schema.string().optional(),
631
- pr: tool.schema.string().optional(),
632
- issue: tool.schema.string().optional(),
633
- reviewer: tool.schema.string().optional(),
634
- },
635
- async execute(args) {
636
- if (!args.runId && !args.pr && !args.issue)
637
- return "Specify runId, pr, or issue.";
638
- const outputDir = await configuredOutputDir();
639
- if (outputDir)
640
- await runManager.status({ outputDir });
641
- return runManager.output({
642
- outputDir,
643
- issue: parseOptionalIssue(args.issue),
644
- pr: parseOptionalPr(args.pr),
645
- reviewer: args.reviewer,
646
- runId: args.runId,
647
- });
648
- },
649
- }),
650
- magi_cancel: tool({
651
- description: [
652
- "Cancel a Magi background run by runId, PR, or issue.",
653
- INTERNAL_FOLLOW_UP_TOOL_NOTE,
654
- ].join(" "),
655
- args: {
656
- runId: tool.schema.string().optional(),
657
- pr: tool.schema.string().optional(),
658
- issue: tool.schema.string().optional(),
659
- },
660
- async execute(args) {
661
- if (!args.runId && !args.pr && !args.issue)
662
- return "Specify runId, pr, or issue.";
663
- const outputDir = await configuredOutputDir();
664
- if (outputDir)
665
- await runManager.status({ outputDir });
666
- const pr = parseOptionalPr(args.pr);
667
- const issue = parseOptionalIssue(args.issue);
668
- const state = await runManager.cancel({
669
- issue,
670
- outputDir,
671
- pr,
672
- runId: args.runId,
673
- });
674
- if (!state) {
675
- return args.runId
676
- ? `Magi run not found: ${args.runId}`
677
- : issue
678
- ? `Magi run not found for issue #${issue}`
679
- : `Magi run not found for PR #${pr}`;
680
- }
681
- return runManager.formatStates([state]);
682
- },
683
- }),
684
- magi_clear: tool({
685
- description: "Clear all inactive Magi runs by deleting configured sessions, worktrees, branches, and output artifacts.",
686
- args: {},
687
- async execute() {
688
- const loaded = await loadConfig(directory).catch(() => undefined);
689
- const clear = loaded?.config.clear;
690
- const options = {
691
- branch: clearFlag(clear?.branch),
692
- output: clearFlag(clear?.output),
693
- session: clearFlag(clear?.session),
694
- worktree: clearFlag(clear?.worktree),
695
- };
696
- return runManager.clear({
697
- options,
698
- outputDir: loaded
699
- ? outputBaseDirs(directory, loaded.config)
700
- : undefined,
701
- worktreeDir: loaded
702
- ? worktreeBaseDirs(directory, loaded.config)
703
- : undefined,
704
- });
705
- },
706
- }),
707
- magi_permission_reply: tool({
708
- description: "Reply to a pending Magi child-agent permission request by runId or PR.",
709
- args: {
710
- runId: tool.schema.string().optional(),
711
- pr: tool.schema.string().optional(),
712
- agent: tool.schema.string().optional(),
713
- reviewer: tool.schema.string().optional(),
714
- requestId: tool.schema.string().optional(),
715
- reply: tool.schema.string(),
716
- },
717
- async execute(args) {
718
- if (!args.runId && !args.pr)
719
- return "Specify runId or pr.";
720
- if (!["always", "once", "reject"].includes(args.reply)) {
721
- return "reply must be once, always, or reject.";
722
- }
723
- const outputDir = await configuredOutputDir();
724
- if (outputDir)
725
- await runManager.status({ outputDir });
726
- return runManager.replyPermission({
727
- agent: args.agent ?? args.reviewer,
728
- outputDir,
729
- pr: parseOptionalPr(args.pr),
730
- reply: args.reply,
731
- requestId: args.requestId,
732
- runId: args.runId,
733
- });
734
- },
735
- }),
736
- magi_question_reply: tool({
737
- description: "Reply to a pending Magi child-agent question request by runId or PR.",
738
- args: {
739
- runId: tool.schema.string().optional(),
740
- pr: tool.schema.string().optional(),
741
- agent: tool.schema.string().optional(),
742
- reviewer: tool.schema.string().optional(),
743
- requestId: tool.schema.string().optional(),
744
- answers: tool.schema.string(),
745
- },
746
- async execute(args) {
747
- if (!args.runId && !args.pr)
748
- return "Specify runId or pr.";
749
- const outputDir = await configuredOutputDir();
750
- if (outputDir)
751
- await runManager.status({ outputDir });
752
- return runManager.replyQuestion({
753
- agent: args.agent ?? args.reviewer,
754
- answers: parseQuestionAnswers(args.answers),
755
- outputDir,
756
- pr: parseOptionalPr(args.pr),
757
- requestId: args.requestId,
758
- runId: args.runId,
759
- });
760
- },
761
- }),
762
- magi_question_reject: tool({
763
- description: "Reject a pending Magi child-agent question request by runId or PR.",
764
- args: {
765
- runId: tool.schema.string().optional(),
766
- pr: tool.schema.string().optional(),
767
- agent: tool.schema.string().optional(),
768
- reviewer: tool.schema.string().optional(),
769
- requestId: tool.schema.string().optional(),
770
- },
771
- async execute(args) {
772
- if (!args.runId && !args.pr)
773
- return "Specify runId or pr.";
774
- const outputDir = await configuredOutputDir();
775
- if (outputDir)
776
- await runManager.status({ outputDir });
777
- return runManager.rejectQuestion({
778
- agent: args.agent ?? args.reviewer,
779
- outputDir,
780
- pr: parseOptionalPr(args.pr),
781
- requestId: args.requestId,
782
- runId: args.runId,
783
- });
784
- },
785
- }),
786
- magi_validate: tool({
787
- description: "Validate global and project Magi config presence, merged settings, reviewer rules, model IDs, and GitHub authentication.",
788
- args: {
789
- checkAuth: tool.schema.boolean().optional(),
790
- },
791
- async execute(args) {
792
- return validateMagiConfigFiles(directory, {
793
- checkAuth: args.checkAuth ?? true,
794
- exec,
795
- modelCatalog: await modelCatalog(),
796
- });
797
- },
798
- }),
799
- },
800
- };
801
13
  };
802
- export default MagiPlugin;
14
+ export default plugin;