mergestorm-mcp 0.2.25 → 0.2.27

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 (71) hide show
  1. package/README.md +17 -4
  2. package/dist/index.js +456 -101
  3. package/dist/lib/brand-icons.d.ts +16 -0
  4. package/dist/lib/brand-icons.js +23 -0
  5. package/dist/lib/hosted.d.ts +7 -0
  6. package/dist/lib/hosted.js +21 -0
  7. package/dist/lib/index.d.ts +1 -0
  8. package/dist/lib/index.js +5 -0
  9. package/dist/lib/pr-loop-instructions.d.ts +13 -0
  10. package/dist/lib/pr-loop-instructions.js +62 -0
  11. package/dist/lib/server.d.ts +22 -0
  12. package/dist/lib/server.js +558 -0
  13. package/dist/lib/tools/agent-request.d.ts +6 -0
  14. package/dist/lib/tools/agent-request.js +25 -0
  15. package/dist/lib/tools/chat-wake.d.ts +21 -0
  16. package/dist/lib/tools/chat-wake.js +26 -0
  17. package/dist/lib/tools/credits.d.ts +3 -0
  18. package/dist/lib/tools/credits.js +38 -0
  19. package/dist/lib/tools/cyclone-job.d.ts +29 -0
  20. package/dist/lib/tools/cyclone-job.js +70 -0
  21. package/dist/lib/tools/envelope.d.ts +5 -0
  22. package/dist/lib/tools/envelope.js +30 -0
  23. package/dist/lib/tools/issue-create.d.ts +47 -0
  24. package/dist/lib/tools/issue-create.js +53 -0
  25. package/dist/lib/tools/queue-status.d.ts +3 -0
  26. package/dist/lib/tools/queue-status.js +79 -0
  27. package/dist/lib/tools/repo-settings.d.ts +6 -0
  28. package/dist/lib/tools/repo-settings.js +54 -0
  29. package/dist/lib/tools/review-dismiss.d.ts +16 -0
  30. package/dist/lib/tools/review-dismiss.js +68 -0
  31. package/dist/lib/tools/review-get-pr.d.ts +22 -0
  32. package/dist/lib/tools/review-get-pr.js +52 -0
  33. package/dist/lib/tools/review-get.d.ts +3 -0
  34. package/dist/lib/tools/review-get.js +27 -0
  35. package/dist/lib/tools/review-list.d.ts +3 -0
  36. package/dist/lib/tools/review-list.js +10 -0
  37. package/dist/lib/tools/review-start.d.ts +11 -0
  38. package/dist/lib/tools/review-start.js +21 -0
  39. package/dist/lib/tools/review-submit.d.ts +36 -0
  40. package/dist/lib/tools/review-submit.js +172 -0
  41. package/dist/lib/tools/review-wait-pr.d.ts +11 -0
  42. package/dist/lib/tools/review-wait-pr.js +74 -0
  43. package/dist/lib/tools/review-wait.d.ts +6 -0
  44. package/dist/lib/tools/review-wait.js +33 -0
  45. package/dist/lib/tools/settings-get.d.ts +8 -0
  46. package/dist/lib/tools/settings-get.js +33 -0
  47. package/dist/lib/tools/settings-set.d.ts +11 -0
  48. package/dist/lib/tools/settings-set.js +62 -0
  49. package/dist/lib/tools/stack-actions.d.ts +4 -0
  50. package/dist/lib/tools/stack-actions.js +22 -0
  51. package/dist/lib/tools/stack-adopt.d.ts +44 -0
  52. package/dist/lib/tools/stack-adopt.js +55 -0
  53. package/dist/lib/tools/stack-list.d.ts +3 -0
  54. package/dist/lib/tools/stack-list.js +30 -0
  55. package/dist/lib/tools/stack-publish.d.ts +78 -0
  56. package/dist/lib/tools/stack-publish.js +89 -0
  57. package/dist/lib/tools/stack-set.d.ts +16 -0
  58. package/dist/lib/tools/stack-set.js +69 -0
  59. package/dist/lib/tools/stack-status.d.ts +3 -0
  60. package/dist/lib/tools/stack-status.js +57 -0
  61. package/dist/lib/tools/stack-summary.d.ts +2 -0
  62. package/dist/lib/tools/stack-summary.js +29 -0
  63. package/dist/lib/tools/stack-wait.d.ts +24 -0
  64. package/dist/lib/tools/stack-wait.js +84 -0
  65. package/dist/lib/tools/surge-setup.d.ts +6 -0
  66. package/dist/lib/tools/surge-setup.js +4 -0
  67. package/dist/lib/tools/types.d.ts +5 -0
  68. package/dist/lib/tools/types.js +1 -0
  69. package/dist/lib/tools/whoami.d.ts +3 -0
  70. package/dist/lib/tools/whoami.js +27 -0
  71. package/package.json +11 -3
@@ -0,0 +1,3 @@
1
+ import { type Config } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export declare function credits(cfg?: Config): Promise<ToolPayload>;
@@ -0,0 +1,38 @@
1
+ import { CommandError, getMe, loadConfig } from "mergestorm/client";
2
+ function formatMinutes(value) {
3
+ return value.toLocaleString("en-US", { maximumFractionDigits: 1 });
4
+ }
5
+ function surgeSummary(surge) {
6
+ if (!surge)
7
+ return "";
8
+ const wallet = `wallet ${formatMinutes(surge.walletMinutes)} min`;
9
+ if (surge.includedMinutes <= 0)
10
+ return `\nSurge: ${wallet}`;
11
+ return (`\nSurge: ${formatMinutes(surge.usedMinutes)} of ${formatMinutes(surge.includedMinutes)} included min this period` +
12
+ ` · ${wallet}`);
13
+ }
14
+ export async function credits(cfg) {
15
+ const resolved = cfg ?? (await loadConfig());
16
+ const me = await getMe(resolved);
17
+ if (!me) {
18
+ throw new CommandError("Credits are not available right now (Mergestorm API is offline, unreachable, or too old).");
19
+ }
20
+ if (!me.usage) {
21
+ throw new CommandError("Credits are not available on this server yet. Update the API or use the dashboard.");
22
+ }
23
+ const standard = me.usage.standard;
24
+ const remaining = standard.remaining ??
25
+ (standard.limit != null ? Math.max(0, standard.limit - standard.used) : null);
26
+ const bonusRemaining = me.usage.bonus?.remaining;
27
+ const surge = me.usage.surge;
28
+ const bonusSummary = bonusRemaining == null ? "" : ` · ${bonusRemaining} bonus remaining`;
29
+ return {
30
+ summary: remaining == null
31
+ ? `${standard.used} used · unlimited${bonusSummary}${surgeSummary(surge)}`
32
+ : `${standard.used} used · ${remaining} remaining${bonusSummary}${surgeSummary(surge)}`,
33
+ data: {
34
+ usage: me.usage,
35
+ resets_at: me.resets_at ?? null,
36
+ },
37
+ };
38
+ }
@@ -0,0 +1,29 @@
1
+ import type { Config } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export declare const CYCLONE_TASKS: readonly ["fix_findings", "fix_ci", "resolve_conflicts", "build"];
4
+ export type CycloneTask = (typeof CYCLONE_TASKS)[number];
5
+ export declare const CYCLONE_JOB_WAIT_DEFAULT_S = 45;
6
+ export declare const CYCLONE_JOB_WAIT_MAX_S = 100;
7
+ export declare const CYCLONE_JOB_POLL_MS = 5000;
8
+ export declare const CYCLONE_JOB_TERMINAL_STATES: readonly ["pushed", "completed", "failed"];
9
+ export declare const CYCLONE_START_WAIT_NOTE = "Wait for it now: call cyclone_job with this dispatch_id and timeout_s 45, again while it is queued or running, then tell the user what Cyclone pushed or why it failed.";
10
+ export declare const CHAT_WAKE_CYCLONE_NOTE = "If your reply ends before Cyclone finishes, this chat is woken with the job's result.";
11
+ export declare const CYCLONE_JOB_STILL_RUNNING_NOTE = "Cyclone is not done. Call cyclone_job again with this dispatch_id and timeout_s 45, and report the result once it is pushed, completed or failed.";
12
+ export type CycloneStartInput = {
13
+ task: CycloneTask;
14
+ owner: string;
15
+ repo: string;
16
+ pr_number?: number;
17
+ failing_check?: string;
18
+ brief?: string;
19
+ };
20
+ export declare function cycloneStart(input: CycloneStartInput, cfg?: Config, chatId?: string | null): Promise<ToolPayload>;
21
+ export declare function isCycloneJobTerminal(job: Record<string, unknown>): boolean;
22
+ export declare function cycloneJobSummary(job: Record<string, unknown>): string;
23
+ export type CycloneJobWaitDeps = {
24
+ pollMs?: number;
25
+ now?: () => number;
26
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
27
+ signal?: AbortSignal;
28
+ };
29
+ export declare function cycloneJob(dispatchId: string, cfg?: Config, timeoutS?: number, deps?: CycloneJobWaitDeps): Promise<ToolPayload>;
@@ -0,0 +1,70 @@
1
+ import { agentRequest } from "./agent-request.js";
2
+ export const CYCLONE_TASKS = ["fix_findings", "fix_ci", "resolve_conflicts", "build"];
3
+ export const CYCLONE_JOB_WAIT_DEFAULT_S = 45;
4
+ export const CYCLONE_JOB_WAIT_MAX_S = 100;
5
+ export const CYCLONE_JOB_POLL_MS = 5_000;
6
+ export const CYCLONE_JOB_TERMINAL_STATES = ["pushed", "completed", "failed"];
7
+ export const CYCLONE_START_WAIT_NOTE = `Wait for it now: call cyclone_job with this dispatch_id and timeout_s ${CYCLONE_JOB_WAIT_DEFAULT_S}, again while it is queued or running, then tell the user what Cyclone pushed or why it failed.`;
8
+ export const CHAT_WAKE_CYCLONE_NOTE = "If your reply ends before Cyclone finishes, this chat is woken with the job's result.";
9
+ export const CYCLONE_JOB_STILL_RUNNING_NOTE = `Cyclone is not done. Call cyclone_job again with this dispatch_id and timeout_s ${CYCLONE_JOB_WAIT_DEFAULT_S}, and report the result once it is pushed, completed or failed.`;
10
+ export async function cycloneStart(input, cfg, chatId) {
11
+ const payload = await agentRequest("/api/v1/agents/cyclone/jobs", {
12
+ method: "POST",
13
+ json: {
14
+ task: input.task,
15
+ owner: input.owner,
16
+ repo: input.repo,
17
+ ...(input.pr_number !== undefined ? { pr_number: input.pr_number } : {}),
18
+ ...(input.failing_check ? { failing_check: input.failing_check } : {}),
19
+ ...(input.brief ? { brief: input.brief } : {}),
20
+ ...(chatId ? { chat_id: chatId } : {}),
21
+ },
22
+ }, cfg);
23
+ if (payload.isError || typeof payload.data.dispatch_id !== "string" || isCycloneJobTerminal(payload.data))
24
+ return payload;
25
+ return { ...payload, summary: [payload.summary, CYCLONE_START_WAIT_NOTE, ...(payload.data.chat_wake ? [CHAT_WAKE_CYCLONE_NOTE] : [])].join(" ") };
26
+ }
27
+ export function isCycloneJobTerminal(job) {
28
+ return CYCLONE_JOB_TERMINAL_STATES.includes(job.state);
29
+ }
30
+ export function cycloneJobSummary(job) {
31
+ const target = job.pr_number ? `${job.owner}/${job.repo}#${job.pr_number}` : `${job.owner}/${job.repo}`;
32
+ const parts = [`Cyclone ${job.task ?? "job"} on ${target}: ${job.state}`];
33
+ if (typeof job.outcome === "string" && job.outcome)
34
+ parts[0] += ` (${job.outcome})`;
35
+ if (typeof job.result_sha === "string" && job.result_sha)
36
+ parts.push(`pushed ${job.result_sha.slice(0, 10)}`);
37
+ if (typeof job.detail === "string" && job.detail)
38
+ parts.push(job.detail);
39
+ return parts.join(". ");
40
+ }
41
+ function sleepFor(ms, signal) {
42
+ return new Promise((resolve) => {
43
+ if (signal?.aborted)
44
+ return resolve();
45
+ const timer = setTimeout(done, ms);
46
+ function done() {
47
+ clearTimeout(timer);
48
+ signal?.removeEventListener("abort", done);
49
+ resolve();
50
+ }
51
+ signal?.addEventListener("abort", done, { once: true });
52
+ });
53
+ }
54
+ export async function cycloneJob(dispatchId, cfg, timeoutS = 0, deps = {}) {
55
+ const now = deps.now ?? Date.now;
56
+ const sleep = deps.sleep ?? sleepFor;
57
+ const deadline = now() + Math.min(Math.max(timeoutS, 0), CYCLONE_JOB_WAIT_MAX_S) * 1000;
58
+ const route = `/api/v1/agents/cyclone/jobs/${encodeURIComponent(dispatchId)}`;
59
+ for (;;) {
60
+ const payload = await agentRequest(route, { method: "GET" }, cfg);
61
+ if (payload.isError)
62
+ return payload;
63
+ if (isCycloneJobTerminal(payload.data))
64
+ return { ...payload, summary: cycloneJobSummary(payload.data) };
65
+ const left = deadline - now();
66
+ if (left <= 0 || deps.signal?.aborted)
67
+ return { ...payload, summary: `${cycloneJobSummary(payload.data)}. ${CYCLONE_JOB_STILL_RUNNING_NOTE}` };
68
+ await sleep(Math.min(deps.pollMs ?? CYCLONE_JOB_POLL_MS, left), deps.signal);
69
+ }
70
+ }
@@ -0,0 +1,5 @@
1
+ import { type ReviewEnvelopeFallbacks, type ReviewJobRow } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export declare const MCP_REVIEW_STATUSES: readonly ["no_changes", "queued", "in_progress", "completed", "failed", "quota_exceeded", "rate_limited"];
4
+ export declare function payloadFromRow(row: ReviewJobRow | null, fallback?: ReviewEnvelopeFallbacks): ToolPayload;
5
+ export declare function payloadFromRateLimit(err: unknown, jobId?: string | null): ToolPayload | null;
@@ -0,0 +1,30 @@
1
+ import { isCommandErrorCode, toReviewJobEnvelope, } from "mergestorm/client";
2
+ export const MCP_REVIEW_STATUSES = [
3
+ "no_changes",
4
+ "queued",
5
+ "in_progress",
6
+ "completed",
7
+ "failed",
8
+ "quota_exceeded",
9
+ "rate_limited",
10
+ ];
11
+ export function payloadFromRow(row, fallback = {}) {
12
+ const envelope = toReviewJobEnvelope(row, fallback);
13
+ const bits = [envelope.status];
14
+ if (envelope.verdict)
15
+ bits.push(envelope.verdict);
16
+ return {
17
+ summary: `${envelope.job_id ?? fallback.jobId ?? "review"} · ${bits.join(" · ")}`,
18
+ data: envelope,
19
+ };
20
+ }
21
+ export function payloadFromRateLimit(err, jobId) {
22
+ if (!isCommandErrorCode(err, "rate_limited"))
23
+ return null;
24
+ return payloadFromRow(null, {
25
+ jobId,
26
+ status: "rate_limited",
27
+ error: err.message,
28
+ retryAfterSeconds: err.retryAfterSeconds,
29
+ });
30
+ }
@@ -0,0 +1,47 @@
1
+ import { openStackIssue, parseGithubOriginRepo, type Config } from "mergestorm/client";
2
+ import { z } from "zod";
3
+ import type { ToolPayload } from "./types.js";
4
+ export declare const issueCreateSchema: {
5
+ title: z.ZodString;
6
+ body: z.ZodOptional<z.ZodString>;
7
+ labels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
8
+ owner: z.ZodOptional<z.ZodString>;
9
+ repo: z.ZodOptional<z.ZodString>;
10
+ cwd: z.ZodOptional<z.ZodString>;
11
+ };
12
+ export declare const hostedIssueCreateSchema: {
13
+ title: z.ZodString;
14
+ body: z.ZodOptional<z.ZodString>;
15
+ labels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
16
+ owner: z.ZodString;
17
+ repo: z.ZodString;
18
+ };
19
+ declare const inputSchema: z.ZodObject<{
20
+ title: z.ZodString;
21
+ body: z.ZodOptional<z.ZodString>;
22
+ labels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
23
+ owner: z.ZodOptional<z.ZodString>;
24
+ repo: z.ZodOptional<z.ZodString>;
25
+ cwd: z.ZodOptional<z.ZodString>;
26
+ }, "strip", z.ZodTypeAny, {
27
+ title: string;
28
+ owner?: string | undefined;
29
+ body?: string | undefined;
30
+ labels?: string[] | undefined;
31
+ repo?: string | undefined;
32
+ cwd?: string | undefined;
33
+ }, {
34
+ title: string;
35
+ owner?: string | undefined;
36
+ body?: string | undefined;
37
+ labels?: string[] | undefined;
38
+ repo?: string | undefined;
39
+ cwd?: string | undefined;
40
+ }>;
41
+ export type IssueCreateInput = z.infer<typeof inputSchema>;
42
+ export type IssueCreateDeps = {
43
+ openStackIssue?: typeof openStackIssue;
44
+ parseGithubOriginRepo?: typeof parseGithubOriginRepo;
45
+ };
46
+ export declare function issueCreate(input: IssueCreateInput, cfg?: Config, deps?: IssueCreateDeps): Promise<ToolPayload>;
47
+ export {};
@@ -0,0 +1,53 @@
1
+ import { CommandError, openStackIssue, parseGithubOriginRepo } from "mergestorm/client";
2
+ import { z } from "zod";
3
+ export const issueCreateSchema = {
4
+ title: z.string().trim().min(1).max(256),
5
+ body: z.string().max(60_000).optional(),
6
+ labels: z.array(z.string().trim().min(1).max(50)).max(10).optional(),
7
+ owner: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/).optional(),
8
+ repo: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/).optional(),
9
+ cwd: z.string().trim().min(1).optional(),
10
+ };
11
+ export const hostedIssueCreateSchema = {
12
+ title: issueCreateSchema.title,
13
+ body: issueCreateSchema.body,
14
+ labels: issueCreateSchema.labels,
15
+ owner: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/),
16
+ repo: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/),
17
+ };
18
+ const inputSchema = z.object(issueCreateSchema);
19
+ export async function issueCreate(input, cfg, deps = {}) {
20
+ const parsed = inputSchema.safeParse(input);
21
+ if (!parsed.success || Boolean(parsed.data.owner) !== Boolean(parsed.data.repo)) {
22
+ const message = "issue_create needs a one-line title of at most 256 characters, and owner and repo together";
23
+ return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
24
+ }
25
+ const { title, body, labels, owner, repo, cwd } = parsed.data;
26
+ try {
27
+ const target = owner && repo ? { owner, repo } : (deps.parseGithubOriginRepo ?? parseGithubOriginRepo)(cwd);
28
+ if (!target) {
29
+ const message = "Could not read a GitHub owner/repo from origin. Pass owner and repo.";
30
+ return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
31
+ }
32
+ const issue = await (deps.openStackIssue ?? openStackIssue)({ owner: target.owner, repo: target.repo, title, body: body ?? "", labels: labels ?? [] }, cfg);
33
+ return {
34
+ summary: `${issue.created ? "Opened" : "Already open:"} ${target.owner}/${target.repo}#${issue.number} ${issue.url}. Put "Fixes #${issue.number}" in the commit or pull request body.`,
35
+ data: { owner: target.owner, repo: target.repo, ...issue },
36
+ };
37
+ }
38
+ catch (err) {
39
+ const message = err instanceof Error ? err.message : String(err);
40
+ return {
41
+ summary: message,
42
+ data: {
43
+ error: {
44
+ code: err instanceof CommandError ? err.code ?? "issue_create_failed" : "issue_create_failed",
45
+ message,
46
+ ...(err instanceof CommandError && err.retryAfterSeconds !== undefined
47
+ ? { retry_after_seconds: err.retryAfterSeconds } : {}),
48
+ },
49
+ },
50
+ isError: true,
51
+ };
52
+ }
53
+ }
@@ -0,0 +1,3 @@
1
+ import { type Config } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export declare function queueStatus(stackId?: string, cfg?: Config): Promise<ToolPayload>;
@@ -0,0 +1,79 @@
1
+ import { apiFetch, CommandError, isCommandErrorCode, loadConfig, rateLimitedMessage, REVIEW_EXIT, } from "mergestorm/client";
2
+ function entrySummary(entry) {
3
+ const prNumber = entry.bounceDetail?.prNumber;
4
+ const repo = `${entry.owner}/${entry.repo}${prNumber ? `#${prNumber}` : ""}`;
5
+ const reason = entry.bounceDetail?.kind ?? entry.bounceReason ?? entry.waitReason ?? undefined;
6
+ const sha = entry.bounceDetail?.headSha ?? entry.verifyHeadSha ?? undefined;
7
+ return [
8
+ entry.state,
9
+ repo,
10
+ `stack ${entry.stackId}`,
11
+ reason,
12
+ sha?.slice(0, 7),
13
+ ].filter(Boolean).join(" · ");
14
+ }
15
+ export async function queueStatus(stackId, cfg) {
16
+ try {
17
+ const resolved = cfg ?? (await loadConfig());
18
+ const id = stackId?.trim();
19
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, id
20
+ ? `/api/v1/stacks/queue?stackId=${encodeURIComponent(id)}`
21
+ : "/api/v1/stacks/queue");
22
+ if (status === 404) {
23
+ throw new CommandError("Merge queue route returned 404. Deploy the API.");
24
+ }
25
+ if (status === 429) {
26
+ throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds });
27
+ }
28
+ if (status !== 200) {
29
+ throw new CommandError(`Failed to get queue status (HTTP ${status}): ${JSON.stringify(body)}`);
30
+ }
31
+ const entries = body.entries;
32
+ if (!Array.isArray(entries)) {
33
+ throw new CommandError(`Failed to get queue status (HTTP 200): ${JSON.stringify(body)}`);
34
+ }
35
+ if (id) {
36
+ const matchingEntries = entries.filter((candidate) => candidate?.stackId?.toLowerCase() === id.toLowerCase());
37
+ if (matchingEntries.length === 0) {
38
+ const message = `Queue entry not found for stack: ${id}`;
39
+ return {
40
+ summary: message,
41
+ data: {
42
+ error: {
43
+ code: "queue_entry_not_found",
44
+ message,
45
+ stack_id: id,
46
+ },
47
+ },
48
+ isError: true,
49
+ };
50
+ }
51
+ return {
52
+ summary: matchingEntries.map(entrySummary).join("\n"),
53
+ data: { entries: matchingEntries },
54
+ };
55
+ }
56
+ return {
57
+ summary: entries.length === 0
58
+ ? "No merge queue entries (live or bounced)"
59
+ : entries.map(entrySummary).join("\n"),
60
+ data: { entries },
61
+ };
62
+ }
63
+ catch (err) {
64
+ if (isCommandErrorCode(err, "rate_limited")) {
65
+ return {
66
+ summary: err.message,
67
+ data: {
68
+ error: {
69
+ code: "rate_limited",
70
+ message: err.message,
71
+ retry_after_seconds: err.retryAfterSeconds,
72
+ },
73
+ },
74
+ isError: true,
75
+ };
76
+ }
77
+ throw err;
78
+ }
79
+ }
@@ -0,0 +1,6 @@
1
+ import { type Config, type RepoSettings, type RepoSettingsPatch } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export declare function repoSettingsSummary(entry: RepoSettings): string;
4
+ export declare function repoSettingsPatchFromArgs(args: Record<string, unknown>): RepoSettingsPatch;
5
+ export declare function repoSettingsGet(args: Record<string, unknown>, cfg?: Config): Promise<ToolPayload>;
6
+ export declare function repoSettingsSet(args: Record<string, unknown>, cfg?: Config): Promise<ToolPayload>;
@@ -0,0 +1,54 @@
1
+ import { CommandError, REPO_SETTINGS_KEYS, listRepoSettings, patchRepoSettings, } from "mergestorm/client";
2
+ const ARG_BY_KEY = {
3
+ auto_patch_enabled: "auto_patch",
4
+ auto_review_enabled: "auto_review",
5
+ auto_land_default: "auto_land",
6
+ };
7
+ function parseRepo(raw) {
8
+ const match = typeof raw === "string" ? /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(raw.trim()) : null;
9
+ if (!match)
10
+ throw new CommandError("repo takes <owner>/<repo>.", 2, "usage");
11
+ return { owner: match[1], repo: match[2] };
12
+ }
13
+ function words(value) {
14
+ return value === null ? "inherit" : value ? "on" : "off";
15
+ }
16
+ export function repoSettingsSummary(entry) {
17
+ return `${entry.owner}/${entry.repo}: ${REPO_SETTINGS_KEYS.map((key) => `${ARG_BY_KEY[key]} ${words(entry[key])}`).join(" · ")}`;
18
+ }
19
+ export function repoSettingsPatchFromArgs(args) {
20
+ const patch = {};
21
+ for (const key of REPO_SETTINGS_KEYS) {
22
+ const value = args[ARG_BY_KEY[key]];
23
+ if (value === undefined)
24
+ continue;
25
+ if (value !== null && typeof value !== "boolean") {
26
+ throw new CommandError(`${ARG_BY_KEY[key]} takes true, false, or null.`, 2, "usage");
27
+ }
28
+ patch[key] = value;
29
+ }
30
+ if (Object.keys(patch).length === 0) {
31
+ throw new CommandError(`At least one of ${Object.values(ARG_BY_KEY).join(", ")} is required.`, 2, "usage");
32
+ }
33
+ return patch;
34
+ }
35
+ export async function repoSettingsGet(args, cfg) {
36
+ const wanted = args.repo === undefined ? null : parseRepo(args.repo);
37
+ const all = await listRepoSettings(cfg);
38
+ if (!wanted) {
39
+ return {
40
+ summary: all.length === 0 ? "No repositories are connected to this account." : all.map(repoSettingsSummary).join("\n"),
41
+ data: { repos: all },
42
+ };
43
+ }
44
+ const slug = `${wanted.owner}/${wanted.repo}`.toLowerCase();
45
+ const entry = all.find((candidate) => `${candidate.owner}/${candidate.repo}`.toLowerCase() === slug);
46
+ if (!entry)
47
+ throw new CommandError(`${wanted.owner}/${wanted.repo} is not connected to this account.`);
48
+ return { summary: repoSettingsSummary(entry), data: { ...entry } };
49
+ }
50
+ export async function repoSettingsSet(args, cfg) {
51
+ const { owner, repo } = parseRepo(args.repo);
52
+ const entry = await patchRepoSettings(owner, repo, repoSettingsPatchFromArgs(args), cfg);
53
+ return { summary: repoSettingsSummary(entry), data: { ...entry } };
54
+ }
@@ -0,0 +1,16 @@
1
+ import { type Config, type PrFindingDismissResult } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export type ReviewDismissInput = {
4
+ owner: string;
5
+ repo: string;
6
+ pr_number: number;
7
+ head_sha: string;
8
+ review_id: number;
9
+ finding_ids?: string[];
10
+ scope?: "findings" | "review";
11
+ reason?: string;
12
+ evidence_url?: string;
13
+ preview?: boolean;
14
+ };
15
+ export declare function reviewDismissSummary(result: PrFindingDismissResult): string;
16
+ export declare function reviewDismiss(input: ReviewDismissInput, cfg?: Config): Promise<ToolPayload>;
@@ -0,0 +1,68 @@
1
+ import { dismissPrFindings, dismissThreadsLine, stackWatchObligation, } from "mergestorm/client";
2
+ import { withWatchText } from "./stack-wait.js";
3
+ function findingText(finding) {
4
+ return `${finding.finding_id} (${finding.path ?? "review body"}: ${finding.title})`;
5
+ }
6
+ export function reviewDismissSummary(result) {
7
+ const pr = `${result.owner}/${result.repo}#${result.pr_number}`;
8
+ const kind = result.review_kind === "seam" ? "seam review" : "Vortex review";
9
+ const head = `${kind} ${result.review_id} at ${result.head_sha.slice(0, 7)}`;
10
+ const lines = [];
11
+ if (result.status === "preview") {
12
+ lines.push(`${pr} ${head}: ${result.remaining.length} open finding(s). Nothing was written.`);
13
+ for (const finding of result.remaining)
14
+ lines.push(`- ${findingText(finding)}`);
15
+ }
16
+ else {
17
+ lines.push(`${pr} ${head}: dismissed ${result.dismissed.length}, already dismissed ${result.already_dismissed.length}, still open ${result.remaining.length}.`);
18
+ for (const finding of result.remaining)
19
+ lines.push(`- open: ${findingText(finding)}`);
20
+ const threads = dismissThreadsLine(result);
21
+ if (threads)
22
+ lines.push(threads);
23
+ }
24
+ const seam = result.gate.seam;
25
+ if (seam) {
26
+ lines.push(seam.cleared
27
+ ? "Seam gate cleared at this head. CI, other reviews and Auto land policy are unchanged."
28
+ : seam.blocking
29
+ ? `Seam gate still blocking (seam_state=${seam.state}).`
30
+ : `Seam gate not blocking (seam_state=${seam.state}).`);
31
+ }
32
+ const vortex = result.gate.vortex;
33
+ if (vortex?.cleared) {
34
+ lines.push("Vortex gate cleared at this head: every Vortex Blocker (error finding) at this head is dismissed. CI and Auto land policy are unchanged.");
35
+ }
36
+ else if (vortex?.message && vortex.reason !== "review_findings_open") {
37
+ lines.push(`Vortex gate not cleared: ${vortex.message}`);
38
+ }
39
+ return lines.join("\n");
40
+ }
41
+ export async function reviewDismiss(input, cfg) {
42
+ const request = {
43
+ owner: input.owner,
44
+ repo: input.repo,
45
+ prNumber: input.pr_number,
46
+ headSha: input.head_sha,
47
+ reviewId: input.review_id,
48
+ ...(input.scope ? { scope: input.scope } : {}),
49
+ ...(input.finding_ids ? { findingIds: input.finding_ids } : {}),
50
+ ...(input.reason !== undefined ? { reason: input.reason } : {}),
51
+ ...(input.evidence_url ? { evidenceUrl: input.evidence_url } : {}),
52
+ ...(input.preview ? { preview: true } : {}),
53
+ };
54
+ const outcome = await dismissPrFindings(request, cfg);
55
+ if (!outcome.ok) {
56
+ return {
57
+ summary: `review_dismiss refused (${outcome.error}): ${outcome.message}`,
58
+ isError: true,
59
+ data: { error: { code: outcome.error, message: outcome.message, status: outcome.status } },
60
+ };
61
+ }
62
+ const result = outcome.result;
63
+ const summary = reviewDismissSummary(result);
64
+ if (!result.stack_id)
65
+ return { summary, data: { ...result } };
66
+ const watch = stackWatchObligation({ stackId: result.stack_id, terminal: null, unread: true, freshCursor: true });
67
+ return { summary: withWatchText(watch, summary), data: { ...result, watch } };
68
+ }
@@ -0,0 +1,22 @@
1
+ import { type Config, type PrVortexReview } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export declare function prReviewTarget(owner: string, repo: string, prNumber: number): {
4
+ owner: string;
5
+ repo: string;
6
+ prNumber: number;
7
+ };
8
+ export declare function prReviewSummary(owner: string, repo: string, prNumber: number, envelope: {
9
+ status: string;
10
+ head_sha?: string | null;
11
+ finding_count?: number | null;
12
+ phase?: string | null;
13
+ }): string;
14
+ export declare function payloadFromPrReview(owner: string, repo: string, prNumber: number, envelope: PrVortexReview): ToolPayload;
15
+ export declare function payloadFromPrRateLimit(err: unknown, owner: string, repo: string, prNumber: number): ToolPayload | null;
16
+ /** SHA + pass selection (#2027): `pass` is one exact attempt, `afterPass` a later one on the head. */
17
+ export type ReviewGetPrOptions = {
18
+ afterSha?: string;
19
+ pass?: number;
20
+ afterPass?: number;
21
+ };
22
+ export declare function reviewGetPr(owner: string, repo: string, prNumber: number, cfg?: Config, opts?: ReviewGetPrOptions): Promise<ToolPayload>;
@@ -0,0 +1,52 @@
1
+ import { CommandError, getPrVortexReview, isCommandErrorCode, } from "mergestorm/client";
2
+ export function prReviewTarget(owner, repo, prNumber) {
3
+ const cleanOwner = owner.trim();
4
+ const cleanRepo = repo.trim();
5
+ if (!cleanOwner || !cleanRepo || !Number.isInteger(prNumber) || prNumber <= 0) {
6
+ throw new CommandError("owner, repo, and a positive pr_number are required", 2, "usage");
7
+ }
8
+ return { owner: cleanOwner, repo: cleanRepo, prNumber };
9
+ }
10
+ export function prReviewSummary(owner, repo, prNumber, envelope) {
11
+ const bits = [`${owner}/${repo}#${prNumber}`, envelope.status];
12
+ const sha = envelope.head_sha?.trim();
13
+ if (sha)
14
+ bits.push(`sha ${sha.slice(0, 7)}`);
15
+ if (typeof envelope.finding_count === "number") {
16
+ bits.push(`${envelope.finding_count} finding${envelope.finding_count === 1 ? "" : "s"}`);
17
+ }
18
+ if (envelope.phase)
19
+ bits.push(String(envelope.phase));
20
+ return bits.join(" · ");
21
+ }
22
+ export function payloadFromPrReview(owner, repo, prNumber, envelope) {
23
+ return {
24
+ summary: prReviewSummary(owner, repo, prNumber, envelope),
25
+ data: { ...envelope },
26
+ };
27
+ }
28
+ export function payloadFromPrRateLimit(err, owner, repo, prNumber) {
29
+ if (!isCommandErrorCode(err, "rate_limited"))
30
+ return null;
31
+ return {
32
+ summary: `${owner}/${repo}#${prNumber} · rate_limited`,
33
+ data: {
34
+ status: "rate_limited",
35
+ error: err.message,
36
+ retry_after_seconds: err.retryAfterSeconds,
37
+ },
38
+ };
39
+ }
40
+ export async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
41
+ const target = prReviewTarget(owner, repo, prNumber);
42
+ try {
43
+ const envelope = await getPrVortexReview(target.owner, target.repo, target.prNumber, cfg, { afterSha: opts.afterSha, pass: opts.pass, afterPass: opts.afterPass });
44
+ return payloadFromPrReview(target.owner, target.repo, target.prNumber, envelope);
45
+ }
46
+ catch (err) {
47
+ const rateLimited = payloadFromPrRateLimit(err, target.owner, target.repo, target.prNumber);
48
+ if (rateLimited)
49
+ return rateLimited;
50
+ throw err;
51
+ }
52
+ }
@@ -0,0 +1,3 @@
1
+ import { type Config } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export declare function reviewGet(jobId: string, cfg?: Config): Promise<ToolPayload>;
@@ -0,0 +1,27 @@
1
+ import { CommandError, getReview, toReviewJobEnvelope, } from "mergestorm/client";
2
+ import { payloadFromRateLimit } from "./envelope.js";
3
+ export async function reviewGet(jobId, cfg) {
4
+ let row;
5
+ try {
6
+ row = await getReview(jobId, cfg);
7
+ }
8
+ catch (err) {
9
+ const rateLimited = payloadFromRateLimit(err, jobId);
10
+ if (rateLimited)
11
+ return rateLimited;
12
+ throw err;
13
+ }
14
+ if (!row ||
15
+ typeof row !== "object" ||
16
+ typeof row.status !== "string") {
17
+ throw new CommandError(`Unexpected response for review ${jobId}.`);
18
+ }
19
+ const envelope = toReviewJobEnvelope(row, { jobId });
20
+ const bits = [envelope.status];
21
+ if (envelope.verdict)
22
+ bits.push(envelope.verdict);
23
+ return {
24
+ summary: `${envelope.job_id ?? jobId} · ${bits.join(" · ")}`,
25
+ data: envelope,
26
+ };
27
+ }
@@ -0,0 +1,3 @@
1
+ import { type Config } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export declare function reviewList(limit?: number, cfg?: Config): Promise<ToolPayload>;
@@ -0,0 +1,10 @@
1
+ import { listJobs, toReviewJobEnvelope } from "mergestorm/client";
2
+ export async function reviewList(limit = 10, cfg) {
3
+ const items = await listJobs(limit, cfg);
4
+ const envelopes = items.map((row) => toReviewJobEnvelope(row));
5
+ const n = envelopes.length;
6
+ return {
7
+ summary: n === 0 ? "No review jobs" : `${n} review job${n === 1 ? "" : "s"}`,
8
+ data: { items: envelopes },
9
+ };
10
+ }
@@ -0,0 +1,11 @@
1
+ import type { Config } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export type PullRequestInput = {
4
+ owner: string;
5
+ repo: string;
6
+ pr_number: number;
7
+ };
8
+ export declare function reviewStartPr(input: PullRequestInput & {
9
+ head_sha?: string;
10
+ }, cfg?: Config, chatId?: string | null): Promise<ToolPayload>;
11
+ export declare function reviewStopPr(input: PullRequestInput, cfg?: Config): Promise<ToolPayload>;