opencode-magi 0.0.0-dev-20260519083642 → 0.0.0-dev-20260519091322

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.
@@ -475,7 +475,7 @@ async function validateAuth(config, exec, errors) {
475
475
  }
476
476
  async function fetchPermissions(config, exec, account) {
477
477
  const token = (await exec(`gh auth token${ghHostOption(config)} --user ${JSON.stringify(account)}`)).trim();
478
- const raw = await exec(`GH_TOKEN=${JSON.stringify(token)} gh api${ghHostOption(config)} repos/${config.github?.owner}/${config.github?.repo} --jq .permissions`);
478
+ const raw = await exec(`gh api${ghHostOption(config)} repos/${config.github?.owner}/${config.github?.repo} --jq .permissions`, { env: { GH_TOKEN: token } });
479
479
  return JSON.parse(raw);
480
480
  }
481
481
  async function validateRepositoryPermissions(config, exec, errors, warnings) {
@@ -80,6 +80,19 @@ export function ghHostOption(repository) {
80
80
  export async function ghToken(exec, repository, account) {
81
81
  return (await exec(`gh auth token${ghHostOption(repository)} --user ${shellQuote(account)}`)).trim();
82
82
  }
83
+ function ghTokenEnv(token) {
84
+ return { env: { GH_TOKEN: token } };
85
+ }
86
+ async function fetchPullRequestQueueInput(exec, repository, pr, token) {
87
+ const query = `query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { id headRefOid } } }`;
88
+ const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F pr=${pr}`, ghTokenEnv(token));
89
+ const data = JSON.parse(raw);
90
+ const pullRequest = data.data?.repository?.pullRequest;
91
+ if (!pullRequest?.id || !pullRequest.headRefOid) {
92
+ throw new Error(`Could not fetch pull request queue metadata for #${pr}`);
93
+ }
94
+ return { headRefOid: pullRequest.headRefOid, id: pullRequest.id };
95
+ }
83
96
  export async function fetchPullRequest(exec, repository, pr) {
84
97
  const json = await exec(`gh pr view ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,url,isDraft,baseRefOid,headRefOid,baseRefName,headRefName,headRepository,headRepositoryOwner`);
85
98
  return JSON.parse(json);
@@ -247,14 +260,14 @@ export async function removeBranch(exec, branch) {
247
260
  }
248
261
  export async function postApproval(exec, repository, pr, account) {
249
262
  const token = await ghToken(exec, repository, account);
250
- return exec(`GH_TOKEN=${shellQuote(token)} gh pr review ${pr} --repo ${shellQuote(repoSpecifier(repository))} --approve`);
263
+ return exec(`gh pr review ${pr} --repo ${shellQuote(repoSpecifier(repository))} --approve`, ghTokenEnv(token));
251
264
  }
252
265
  export async function postCloseComment(exec, repository, pr, account, body) {
253
266
  const token = await ghToken(exec, repository, account);
254
267
  const payloadPath = join(tmpdir(), `magi-close-${process.pid}-${Date.now()}.json`);
255
268
  await writeFile(payloadPath, JSON.stringify({ body, event: "COMMENT" }));
256
269
  try {
257
- return await exec(`GH_TOKEN=${shellQuote(token)} gh api${ghHostOption(repository)} repos/${repository.github.owner}/${repository.github.repo}/pulls/${pr}/reviews --method POST --input ${shellQuote(payloadPath)} --jq .html_url`);
270
+ return await exec(`gh api${ghHostOption(repository)} repos/${repository.github.owner}/${repository.github.repo}/pulls/${pr}/reviews --method POST --input ${shellQuote(payloadPath)} --jq .html_url`, ghTokenEnv(token));
258
271
  }
259
272
  finally {
260
273
  await rm(payloadPath, { force: true });
@@ -285,7 +298,7 @@ export async function postChangesRequested(exec, repository, pr, account, findin
285
298
  event: "REQUEST_CHANGES",
286
299
  }));
287
300
  try {
288
- return await exec(`GH_TOKEN=${shellQuote(token)} gh api${ghHostOption(repository)} repos/${repository.github.owner}/${repository.github.repo}/pulls/${pr}/reviews --method POST --input ${shellQuote(payloadPath)} --jq .html_url`);
301
+ return await exec(`gh api${ghHostOption(repository)} repos/${repository.github.owner}/${repository.github.repo}/pulls/${pr}/reviews --method POST --input ${shellQuote(payloadPath)} --jq .html_url`, ghTokenEnv(token));
289
302
  }
290
303
  finally {
291
304
  await rm(payloadPath, { force: true });
@@ -293,6 +306,11 @@ export async function postChangesRequested(exec, repository, pr, account, findin
293
306
  }
294
307
  export async function mergePullRequest(exec, repository, pr, account) {
295
308
  const token = await ghToken(exec, repository, account);
309
+ if (repository.merge.mergeQueue) {
310
+ const queueInput = await fetchPullRequestQueueInput(exec, repository, pr, token);
311
+ const query = `mutation($pullRequestId: ID!, $expectedHeadOid: GitObjectID!) { enqueuePullRequest(input: { pullRequestId: $pullRequestId, expectedHeadOid: $expectedHeadOid }) { mergeQueueEntry { id } } }`;
312
+ return exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F pullRequestId=${shellQuote(queueInput.id)} -F expectedHeadOid=${shellQuote(queueInput.headRefOid)} --jq .data.enqueuePullRequest.mergeQueueEntry.id`, ghTokenEnv(token));
313
+ }
296
314
  const methodFlag = repository.merge.method === "merge"
297
315
  ? "--merge"
298
316
  : repository.merge.method === "rebase"
@@ -300,21 +318,29 @@ export async function mergePullRequest(exec, repository, pr, account) {
300
318
  : "--squash";
301
319
  const autoFlag = repository.merge.auto ? " --auto" : "";
302
320
  const deleteFlag = repository.merge.deleteBranch ? " --delete-branch" : "";
303
- const mergeFlags = repository.merge.mergeQueue
304
- ? ""
305
- : ` ${methodFlag}${autoFlag}${deleteFlag}`;
306
- return exec(`GH_TOKEN=${shellQuote(token)} gh pr merge ${pr} --repo ${shellQuote(repoSpecifier(repository))}${mergeFlags}`);
321
+ return exec(`gh pr merge ${pr} --repo ${shellQuote(repoSpecifier(repository))} ${methodFlag}${autoFlag}${deleteFlag}`, ghTokenEnv(token));
307
322
  }
308
323
  export async function fetchPullRequestMergeStatus(exec, repository, pr) {
309
324
  const json = await exec(`gh pr view ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json state,mergeStateStatus,autoMergeRequest`);
310
325
  return JSON.parse(json);
311
326
  }
327
+ export async function fetchPullRequestQueueStatus(exec, repository, pr) {
328
+ const query = `query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { state isInMergeQueue mergeQueueEntry { id } } } }`;
329
+ const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F pr=${pr}`);
330
+ const data = JSON.parse(raw);
331
+ const status = data.data?.repository?.pullRequest;
332
+ if (!status)
333
+ throw new Error(`Could not fetch merge queue status for #${pr}`);
334
+ return status;
335
+ }
312
336
  export async function waitForMergeQueue(exec, repository, pr, intervalMs = 30_000) {
313
337
  for (;;) {
314
- const status = await fetchPullRequestMergeStatus(exec, repository, pr);
338
+ const status = await fetchPullRequestQueueStatus(exec, repository, pr);
315
339
  if (status.state === "MERGED")
316
340
  return "merged";
317
- if (status.state === "OPEN" && status.autoMergeRequest == null) {
341
+ if (status.state === "OPEN" &&
342
+ !status.isInMergeQueue &&
343
+ status.mergeQueueEntry == null) {
318
344
  return "dequeued";
319
345
  }
320
346
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
@@ -322,12 +348,23 @@ export async function waitForMergeQueue(exec, repository, pr, intervalMs = 30_00
322
348
  }
323
349
  export async function closePullRequest(exec, repository, pr, account) {
324
350
  const token = await ghToken(exec, repository, account);
325
- return exec(`GH_TOKEN=${shellQuote(token)} gh pr close ${pr} --repo ${shellQuote(repoSpecifier(repository))}`);
351
+ return exec(`gh pr close ${pr} --repo ${shellQuote(repoSpecifier(repository))}`, ghTokenEnv(token));
326
352
  }
327
353
  export async function pushHead(exec, repository, worktreePath, account, head) {
328
354
  const token = await ghToken(exec, repository, account);
329
355
  const url = repositoryGitUrl(repository, head.owner, head.repo);
330
- await exec(`git -c credential.helper= -c credential.helper=${shellQuote(`!f() { echo username=x-access-token; echo password=${token}; }; f`)} push ${shellQuote(url)} ${shellQuote(`HEAD:refs/heads/${head.ref}`)}`, { cwd: worktreePath });
356
+ await exec(`git push ${shellQuote(url)} ${shellQuote(`HEAD:refs/heads/${head.ref}`)}`, {
357
+ cwd: worktreePath,
358
+ env: {
359
+ GIT_CONFIG_COUNT: "2",
360
+ GIT_CONFIG_KEY_0: "credential.helper",
361
+ GIT_CONFIG_KEY_1: "credential.helper",
362
+ GIT_CONFIG_VALUE_0: "",
363
+ GIT_CONFIG_VALUE_1: "!f() { echo username=x-access-token; echo password=$GIT_PASSWORD; }; f",
364
+ GIT_PASSWORD: token,
365
+ GIT_TERMINAL_PROMPT: "0",
366
+ },
367
+ });
331
368
  }
332
369
  export async function configureGitIdentity(exec, worktreePath, identity) {
333
370
  if (identity.name) {
@@ -392,7 +429,7 @@ export async function postReply(exec, repository, pr, account, commentId, body)
392
429
  const payloadPath = join(tmpdir(), `magi-reply-${process.pid}-${Date.now()}-${commentId}.json`);
393
430
  await writeFile(payloadPath, JSON.stringify({ body }));
394
431
  try {
395
- return await exec(`GH_TOKEN=${shellQuote(token)} gh api${ghHostOption(repository)} repos/${repository.github.owner}/${repository.github.repo}/pulls/${pr}/comments/${commentId}/replies --method POST --input ${shellQuote(payloadPath)} --jq .html_url`);
432
+ return await exec(`gh api${ghHostOption(repository)} repos/${repository.github.owner}/${repository.github.repo}/pulls/${pr}/comments/${commentId}/replies --method POST --input ${shellQuote(payloadPath)} --jq .html_url`, ghTokenEnv(token));
396
433
  }
397
434
  finally {
398
435
  await rm(payloadPath, { force: true });
@@ -401,5 +438,5 @@ export async function postReply(exec, repository, pr, account, commentId, body)
401
438
  export async function resolveThread(exec, repository, account, threadId) {
402
439
  const token = await ghToken(exec, repository, account);
403
440
  const query = `mutation($threadId: ID!) { resolveReviewThread(input: { threadId: $threadId }) { thread { id } } }`;
404
- await exec(`GH_TOKEN=${shellQuote(token)} gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F threadId=${shellQuote(threadId)}`);
441
+ await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F threadId=${shellQuote(threadId)}`, ghTokenEnv(token));
405
442
  }
@@ -20,6 +20,14 @@ function createRunId() {
20
20
  function now() {
21
21
  return new Date().toISOString();
22
22
  }
23
+ export function redactSecrets(value) {
24
+ return value
25
+ .replace(/\b(GH_TOKEN|GITHUB_TOKEN|GH_ENTERPRISE_TOKEN)=('[^']*'|"[^"]*"|\S+)/g, "$1=<redacted>")
26
+ .replace(/(password=)([^;'\s]+)/g, "$1<redacted>");
27
+ }
28
+ function errorMessage(error) {
29
+ return redactSecrets(error instanceof Error ? error.message : String(error));
30
+ }
23
31
  function isActiveStatus(status) {
24
32
  return (status === "blocked" ||
25
33
  status === "preparing" ||
@@ -927,7 +935,7 @@ export class MagiRunManager {
927
935
  }
928
936
  if (input.event.type === "session.error") {
929
937
  agent.status = "failed";
930
- agent.error = JSON.stringify(input.event.properties?.error ?? "session error");
938
+ agent.error = redactSecrets(JSON.stringify(input.event.properties?.error ?? "session error"));
931
939
  markUpdated(true);
932
940
  dirty = true;
933
941
  }
@@ -1226,7 +1234,7 @@ export class MagiRunManager {
1226
1234
  if (progress.type === "ci_classifier_failed") {
1227
1235
  const classifier = state.ciClassifiers?.[progress.reviewer];
1228
1236
  if (classifier) {
1229
- classifier.error = progress.error;
1237
+ classifier.error = redactSecrets(progress.error);
1230
1238
  classifier.status = "failed";
1231
1239
  classifier.lastUpdate = now();
1232
1240
  }
@@ -1282,7 +1290,7 @@ export class MagiRunManager {
1282
1290
  if (!reviewer)
1283
1291
  return;
1284
1292
  reviewer.status = "failed";
1285
- reviewer.error = progress.error;
1293
+ reviewer.error = redactSecrets(progress.error);
1286
1294
  reviewer.lastUpdate = now();
1287
1295
  }
1288
1296
  if (progress.type === "reviewer_completed") {
@@ -1328,7 +1336,7 @@ export class MagiRunManager {
1328
1336
  await this.notify(state, `**CI classifier ${progress.reviewer}** completed for ${prMarkdownLink(state)}: ${progress.classification} - ${progress.reason}`);
1329
1337
  }
1330
1338
  if (progress.type === "ci_classifier_failed") {
1331
- await this.notify(state, `**CI classifier ${progress.reviewer}** failed for ${prMarkdownLink(state)}: ${progress.error}`);
1339
+ await this.notify(state, `**CI classifier ${progress.reviewer}** failed for ${prMarkdownLink(state)}: ${redactSecrets(progress.error)}`);
1332
1340
  }
1333
1341
  if (progress.type === "worktree_created") {
1334
1342
  await this.notify(state, `Worktree is ready for ${prMarkdownLink(state)}.`);
@@ -1344,7 +1352,7 @@ export class MagiRunManager {
1344
1352
  }
1345
1353
  if (progress.type === "reviewer_failed") {
1346
1354
  await this.notify(state, reviewerFailureText({
1347
- error: progress.error,
1355
+ error: redactSecrets(progress.error),
1348
1356
  pr: prMarkdownLink(state),
1349
1357
  repairAttempts: state.reviewers[progress.reviewer]?.repairAttempts ?? 0,
1350
1358
  reviewer: progress.reviewer,
@@ -1451,7 +1459,7 @@ export class MagiRunManager {
1451
1459
  }
1452
1460
  if (progress.type === "editor_failed") {
1453
1461
  editor.status = "failed";
1454
- editor.error = progress.error;
1462
+ editor.error = redactSecrets(progress.error);
1455
1463
  editor.lastUpdate = now();
1456
1464
  }
1457
1465
  if (progress.type === "editor_completed") {
@@ -1475,7 +1483,7 @@ export class MagiRunManager {
1475
1483
  }
1476
1484
  if (progress.type === "editor_failed") {
1477
1485
  await this.notify(state, editorFailureText({
1478
- error: progress.error,
1486
+ error: redactSecrets(progress.error),
1479
1487
  pr: prMarkdownLink(state),
1480
1488
  repairAttempts: state.editor?.repairAttempts ?? 0,
1481
1489
  }));
@@ -1493,7 +1501,7 @@ export class MagiRunManager {
1493
1501
  state.status = "failed";
1494
1502
  state.phase = "failed";
1495
1503
  state.completedAt = now();
1496
- state.error = error instanceof Error ? error.message : String(error);
1504
+ state.error = errorMessage(error);
1497
1505
  if (state.editor?.status === "pending" ||
1498
1506
  state.editor?.status === "running" ||
1499
1507
  state.editor?.status === "repairing" ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.0.0-dev-20260519083642",
3
+ "version": "0.0.0-dev-20260519091322",
4
4
  "description": "Multi-agent PR review and merge orchestration plugin for OpenCode.",
5
5
  "license": "MIT",
6
6
  "author": "Hirotomo Yamada <hirotomo.yamada@avap.co.jp>",