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,21 @@
1
+ import { agentRequest } from "./agent-request.js";
2
+ import { REVIEW_START_WAIT_NOTE } from "./chat-wake.js";
3
+ export async function reviewStartPr(input, cfg, chatId) {
4
+ const payload = await agentRequest("/api/v1/agents/vortex/review", {
5
+ method: "POST",
6
+ json: {
7
+ owner: input.owner,
8
+ repo: input.repo,
9
+ pr_number: input.pr_number,
10
+ ...(input.head_sha ? { head_sha: input.head_sha } : {}),
11
+ ...(chatId ? { chat_id: chatId } : {}),
12
+ },
13
+ }, cfg);
14
+ return payload.data.chat_wake ? { ...payload, summary: `${payload.summary} ${REVIEW_START_WAIT_NOTE}` } : payload;
15
+ }
16
+ export async function reviewStopPr(input, cfg) {
17
+ return agentRequest("/api/v1/agents/vortex/stop", {
18
+ method: "POST",
19
+ json: { owner: input.owner, repo: input.repo, pr_number: input.pr_number },
20
+ }, cfg);
21
+ }
@@ -0,0 +1,36 @@
1
+ import { type Config } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ /** Hard maximum for an explicitly requested MCP poll slice. */
4
+ export declare const MCP_WAIT_CEILING_S = 300;
5
+ /**
6
+ * Default bound for local and PR review waits. Reviews can take minutes or hours;
7
+ * Cursor kills a single MCP call around a minute, so this is one poll
8
+ * slice. The skill loops until the review finishes.
9
+ */
10
+ export declare const MCP_PR_WAIT_DEFAULT_S = 45;
11
+ export declare function assertPinnableSpecialists(specialists: string[] | undefined): void;
12
+ /**
13
+ * Host-configured root that agent-chosen `context_files` may not escape.
14
+ * The `cwd` tool argument is agent-controlled, so it cannot serve as the
15
+ * sandbox root; default to the MCP server's own working directory.
16
+ */
17
+ export declare function mcpSandboxRoot(): string;
18
+ export type ReviewSubmitInput = {
19
+ cwd?: string;
20
+ base?: string;
21
+ head?: string;
22
+ diff?: string;
23
+ router?: string;
24
+ specialists?: string[];
25
+ context?: string;
26
+ context_files?: string[];
27
+ thread?: string;
28
+ idempotency_key?: string;
29
+ wait?: boolean;
30
+ timeout_s?: number;
31
+ };
32
+ export type ReviewSubmitOptions = {
33
+ pollIntervalMs?: number;
34
+ };
35
+ export declare function waitTimeoutMs(timeout_s?: number, defaultSeconds?: number): number;
36
+ export declare function reviewSubmit(input: ReviewSubmitInput, cfg?: Config, opts?: ReviewSubmitOptions): Promise<ToolPayload>;
@@ -0,0 +1,172 @@
1
+ import { createHash } from "node:crypto";
2
+ import { realpath } from "node:fs/promises";
3
+ import { relative, resolve, sep } from "node:path";
4
+ import { CommandError, REVIEW_THREAD_SLUG_RE, ReviewPollTimeoutError, collectReviewInput, discoverTrunk, formatReviewSubmitError, loadConfig, loadReviewContext, pollReview, rateLimitedMessage, submitReview, } from "mergestorm/client";
5
+ import { payloadFromRateLimit, payloadFromRow } from "./envelope.js";
6
+ /** Hard maximum for an explicitly requested MCP poll slice. */
7
+ export const MCP_WAIT_CEILING_S = 300;
8
+ /**
9
+ * Default bound for local and PR review waits. Reviews can take minutes or hours;
10
+ * Cursor kills a single MCP call around a minute, so this is one poll
11
+ * slice. The skill loops until the review finishes.
12
+ */
13
+ export const MCP_PR_WAIT_DEFAULT_S = 45;
14
+ /** Pinnable L0 lanes. `governance` and `seam` are L1 and cannot be requested. */
15
+ const PINNABLE_SPECIALISTS = [
16
+ "security",
17
+ "performance",
18
+ "architecture",
19
+ "tests",
20
+ "data",
21
+ "api",
22
+ "frontend",
23
+ ];
24
+ export function assertPinnableSpecialists(specialists) {
25
+ if (!specialists?.length)
26
+ return;
27
+ const unknown = specialists.filter((id) => !PINNABLE_SPECIALISTS.includes(id));
28
+ if (unknown.length === 0)
29
+ return;
30
+ throw new CommandError(`Unknown specialist: ${unknown.join(", ")}. Valid lanes: ${PINNABLE_SPECIALISTS.join(", ")}. governance and seam cannot be requested.`, 2, "usage");
31
+ }
32
+ /**
33
+ * Host-configured root that agent-chosen `context_files` may not escape.
34
+ * The `cwd` tool argument is agent-controlled, so it cannot serve as the
35
+ * sandbox root; default to the MCP server's own working directory.
36
+ */
37
+ export function mcpSandboxRoot() {
38
+ return process.env.MERGESTORM_SANDBOX_ROOT?.trim() || process.cwd();
39
+ }
40
+ export function waitTimeoutMs(timeout_s, defaultSeconds = MCP_PR_WAIT_DEFAULT_S) {
41
+ const seconds = timeout_s ?? defaultSeconds;
42
+ if (!Number.isFinite(seconds) || seconds <= 0) {
43
+ throw new CommandError("timeout_s must be a positive number of seconds", 2, "usage");
44
+ }
45
+ return Math.max(1, Math.round(Math.min(seconds, MCP_WAIT_CEILING_S) * 1_000));
46
+ }
47
+ async function resolveInput(input) {
48
+ const cwd = input.cwd ?? process.cwd();
49
+ const head = input.head?.trim() || "HEAD";
50
+ if (input.diff?.trim()) {
51
+ const branch = "local";
52
+ const thread = input.thread?.trim() ||
53
+ `local/diff-${createHash("sha256").update(input.diff).digest("hex").slice(0, 12)}`;
54
+ return {
55
+ thread,
56
+ branch,
57
+ baseLabel: input.base?.trim() ||
58
+ (() => {
59
+ try {
60
+ return discoverTrunk(cwd);
61
+ }
62
+ catch {
63
+ return "main";
64
+ }
65
+ })(),
66
+ headLabel: head,
67
+ diff: input.diff,
68
+ files: [],
69
+ };
70
+ }
71
+ const sandboxRoot = mcpSandboxRoot();
72
+ try {
73
+ const [sandboxReal, cwdReal] = await Promise.all([
74
+ realpath(resolve(sandboxRoot)),
75
+ realpath(resolve(cwd)),
76
+ ]);
77
+ const relToSandbox = relative(sandboxReal, cwdReal);
78
+ if (relToSandbox === ".." || relToSandbox.startsWith(`..${sep}`)) {
79
+ throw new CommandError("cwd must be inside the configured sandbox root", 2, "usage");
80
+ }
81
+ }
82
+ catch (err) {
83
+ if (err instanceof CommandError)
84
+ throw err;
85
+ throw new CommandError("cwd must be inside the configured sandbox root", 2, "usage");
86
+ }
87
+ const base = input.base?.trim() || discoverTrunk(cwd);
88
+ return collectReviewInput(base, head, cwd, sandboxRoot);
89
+ }
90
+ export async function reviewSubmit(input, cfg, opts = {}) {
91
+ const resolved = cfg ?? (await loadConfig());
92
+ const wait = input.wait === true;
93
+ const timeoutMs = waitTimeoutMs(input.timeout_s);
94
+ assertPinnableSpecialists(input.specialists);
95
+ if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
96
+ throw new CommandError("thread must be 1–120 chars [A-Za-z0-9._/-]", 2, "usage");
97
+ }
98
+ if (input.context === "-") {
99
+ throw new CommandError('context "-" (stdin) is not available over MCP; pass the text directly', 2, "usage");
100
+ }
101
+ const collected = await resolveInput(input);
102
+ if (!collected) {
103
+ return payloadFromRow(null, { status: "no_changes" });
104
+ }
105
+ if (input.thread)
106
+ collected.thread = input.thread;
107
+ const extras = await loadReviewContext({
108
+ context: input.context,
109
+ contextFiles: input.context_files ?? [],
110
+ cwd: input.cwd ?? process.cwd(),
111
+ sandboxCwd: mcpSandboxRoot(),
112
+ });
113
+ const submitted = await submitReview(resolved, collected, {
114
+ routerMode: input.router,
115
+ specialists: input.specialists,
116
+ context: extras.context,
117
+ contextFiles: extras.contextFiles,
118
+ idempotencyKey: input.idempotency_key,
119
+ });
120
+ const jobId = submitted.row.job_id ?? submitted.row.id ?? null;
121
+ if (submitted.status === 402) {
122
+ return payloadFromRow(submitted.row, {
123
+ jobId,
124
+ status: "quota_exceeded",
125
+ error: "Review limit reached for this period.",
126
+ });
127
+ }
128
+ if (submitted.status === 429) {
129
+ return payloadFromRow(submitted.row, {
130
+ jobId,
131
+ status: "rate_limited",
132
+ error: rateLimitedMessage(submitted.retryAfterSeconds),
133
+ retryAfterSeconds: submitted.retryAfterSeconds,
134
+ });
135
+ }
136
+ if (submitted.status !== 202 && submitted.status !== 200) {
137
+ return {
138
+ ...payloadFromRow({ ...submitted.row, status: "failed" }, {
139
+ jobId,
140
+ error: formatReviewSubmitError(submitted.status, submitted.row),
141
+ }),
142
+ isError: true,
143
+ };
144
+ }
145
+ if (!jobId) {
146
+ throw new CommandError("Review API returned no job_id");
147
+ }
148
+ if (!wait) {
149
+ return payloadFromRow(submitted.row, { jobId, status: submitted.row.status ?? "queued" });
150
+ }
151
+ let row;
152
+ try {
153
+ row = await pollReview(resolved, jobId, {
154
+ timeoutMs,
155
+ intervalMs: opts.pollIntervalMs,
156
+ });
157
+ }
158
+ catch (err) {
159
+ if (err instanceof ReviewPollTimeoutError) {
160
+ return payloadFromRow(err.lastRow, {
161
+ jobId,
162
+ status: err.lastRow.status ?? "in_progress",
163
+ error: "Timed out waiting for review.",
164
+ });
165
+ }
166
+ const rateLimited = payloadFromRateLimit(err, jobId);
167
+ if (rateLimited)
168
+ return rateLimited;
169
+ throw err;
170
+ }
171
+ return payloadFromRow(row, { jobId });
172
+ }
@@ -0,0 +1,11 @@
1
+ import { type Config } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export type ReviewWaitPrOptions = {
4
+ pollIntervalMs?: number;
5
+ /** Exact pass on the head (#2027). */
6
+ pass?: number;
7
+ /** Only a pass later than this one on the head; fixed for the whole wait (#2027). */
8
+ afterPass?: number;
9
+ chatId?: string;
10
+ };
11
+ export declare function reviewWaitPr(owner: string, repo: string, prNumber: number, afterSha?: string, timeout_s?: number, cfg?: Config, opts?: ReviewWaitPrOptions): Promise<ToolPayload>;
@@ -0,0 +1,74 @@
1
+ import { PrReviewPollTimeoutError, loadConfig, noPrReviewYetMessage, pollPrVortexReview, } from "mergestorm/client";
2
+ import { payloadFromPrRateLimit, payloadFromPrReview, prReviewSummary, prReviewTarget, reviewGetPr, } from "./review-get-pr.js";
3
+ import { MCP_PR_WAIT_DEFAULT_S, waitTimeoutMs } from "./review-submit.js";
4
+ import { CHAT_WAKE_REVIEW_NOTE, registerChatWake } from "./chat-wake.js";
5
+ export async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opts = {}) {
6
+ const target = prReviewTarget(owner, repo, prNumber);
7
+ const resolved = cfg ?? (await loadConfig());
8
+ if (timeout_s === 0) {
9
+ return reviewGetPr(target.owner, target.repo, target.prNumber, resolved, {
10
+ afterSha,
11
+ pass: opts.pass,
12
+ afterPass: opts.afterPass,
13
+ });
14
+ }
15
+ try {
16
+ const envelope = await pollPrVortexReview(resolved, target.owner, target.repo, target.prNumber, {
17
+ timeoutMs: waitTimeoutMs(timeout_s, MCP_PR_WAIT_DEFAULT_S),
18
+ afterSha,
19
+ pass: opts.pass,
20
+ afterPass: opts.afterPass,
21
+ intervalMs: opts.pollIntervalMs,
22
+ });
23
+ return payloadFromPrReview(target.owner, target.repo, target.prNumber, envelope);
24
+ }
25
+ catch (err) {
26
+ if (err instanceof PrReviewPollTimeoutError) {
27
+ const status = err.reviewSeen ? "in_progress" : "none";
28
+ const expectedSha = afterSha?.trim().toLowerCase() ?? "";
29
+ const observedSha = err.lastEnvelope?.head_sha?.trim().toLowerCase() ?? "";
30
+ const headShaMatches = Boolean(observedSha &&
31
+ expectedSha.length >= 7 &&
32
+ (observedSha.startsWith(expectedSha) || expectedSha.startsWith(observedSha)));
33
+ const findingCount = headShaMatches
34
+ ? err.lastEnvelope?.finding_count ?? null
35
+ : null;
36
+ const summary = prReviewSummary(target.owner, target.repo, target.prNumber, {
37
+ status,
38
+ head_sha: err.lastEnvelope?.head_sha ?? null,
39
+ finding_count: findingCount,
40
+ phase: err.lastEnvelope?.phase ?? null,
41
+ });
42
+ const wakeHead = expectedSha || observedSha || null;
43
+ const wake = opts.chatId
44
+ ? await registerChatWake(resolved, opts.chatId, {
45
+ kind: "vortex",
46
+ owner: target.owner,
47
+ repo: target.repo,
48
+ prNumber: target.prNumber,
49
+ headSha: wakeHead,
50
+ minPass: opts.pass ?? (opts.afterPass !== undefined ? opts.afterPass + 1 : (headShaMatches || !expectedSha) && observedSha ? err.lastEnvelope?.pass ?? null : null),
51
+ })
52
+ : null;
53
+ const shown = err.reviewSeen
54
+ ? summary
55
+ : `${summary} · ${noPrReviewYetMessage(target.owner, target.repo, target.prNumber, { afterSha, pass: opts.pass, afterPass: opts.afterPass })}`;
56
+ return {
57
+ summary: wake ? `${shown} · ${CHAT_WAKE_REVIEW_NOTE}` : shown,
58
+ data: {
59
+ status,
60
+ head_sha: err.lastEnvelope?.head_sha ?? null,
61
+ pass: headShaMatches ? err.lastEnvelope?.pass ?? null : null,
62
+ finding_count: findingCount,
63
+ phase: err.lastEnvelope?.phase ?? null,
64
+ error: err.message,
65
+ ...(wake ? { chat_wake: wake } : {}),
66
+ },
67
+ };
68
+ }
69
+ const rateLimited = payloadFromPrRateLimit(err, target.owner, target.repo, target.prNumber);
70
+ if (rateLimited)
71
+ return rateLimited;
72
+ throw err;
73
+ }
74
+ }
@@ -0,0 +1,6 @@
1
+ import { type Config } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export type ReviewWaitOptions = {
4
+ pollIntervalMs?: number;
5
+ };
6
+ export declare function reviewWait(jobId: string, timeout_s?: number, cfg?: Config, opts?: ReviewWaitOptions): Promise<ToolPayload>;
@@ -0,0 +1,33 @@
1
+ import { CommandError, ReviewPollTimeoutError, loadConfig, pollReview, } from "mergestorm/client";
2
+ import { payloadFromRateLimit, payloadFromRow } from "./envelope.js";
3
+ import { reviewGet } from "./review-get.js";
4
+ import { waitTimeoutMs } from "./review-submit.js";
5
+ export async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
6
+ const resolved = cfg ?? (await loadConfig());
7
+ const id = jobId.trim();
8
+ if (!id) {
9
+ throw new CommandError("job_id is required", 2, "usage");
10
+ }
11
+ if (timeout_s === 0)
12
+ return reviewGet(id, resolved);
13
+ try {
14
+ const row = await pollReview(resolved, id, {
15
+ timeoutMs: waitTimeoutMs(timeout_s),
16
+ intervalMs: opts.pollIntervalMs,
17
+ });
18
+ return payloadFromRow(row, { jobId: id });
19
+ }
20
+ catch (err) {
21
+ if (err instanceof ReviewPollTimeoutError) {
22
+ return payloadFromRow(err.lastRow, {
23
+ jobId: id,
24
+ status: err.lastRow.status ?? "in_progress",
25
+ error: "Timed out waiting for review.",
26
+ });
27
+ }
28
+ const rateLimited = payloadFromRateLimit(err, id);
29
+ if (rateLimited)
30
+ return rateLimited;
31
+ throw err;
32
+ }
33
+ }
@@ -0,0 +1,8 @@
1
+ import { type Config, type SettingsResponse } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ /**
4
+ * One line an agent can act on without opening the data: is Cyclone allowed
5
+ * to patch, and is it even connected.
6
+ */
7
+ export declare function settingsSummary(settings: SettingsResponse): string;
8
+ export declare function settingsGet(cfg?: Config): Promise<ToolPayload>;
@@ -0,0 +1,33 @@
1
+ import { CommandError, getSettings, } from "mergestorm/client";
2
+ /**
3
+ * One line an agent can act on without opening the data: is Cyclone allowed
4
+ * to patch, and is it even connected.
5
+ */
6
+ export function settingsSummary(settings) {
7
+ const autoPatch = settings.auto_patch_enabled ? "on" : "off";
8
+ const automations = [
9
+ settings.auto_resolve_conflicts_enabled ? "auto_resolve_conflicts on" : null,
10
+ settings.auto_fix_ci_enabled ? "auto_fix_ci on" : null,
11
+ ].filter((label) => label !== null);
12
+ const autoPatchLabel = [`auto_patch ${autoPatch}`, ...automations].join(" · ");
13
+ const cyclone = settings.cyclone_connected
14
+ ? "Cyclone connected"
15
+ : "Cyclone not connected";
16
+ if (typeof settings.stacks_ready !== "boolean")
17
+ return `${autoPatchLabel} · ${cyclone}`;
18
+ const stacks = settings.stacks_ready
19
+ ? "stacks, merge queue and Auto land ready"
20
+ : "stacks, merge queue and Auto land not ready (install Mergestorm Surge)";
21
+ return `${autoPatchLabel} · ${cyclone} (auto patch) · ${stacks}`;
22
+ }
23
+ export async function settingsGet(cfg) {
24
+ const settings = await getSettings(cfg);
25
+ if (!settings) {
26
+ throw new CommandError("Settings are not available (offline or API too old). " +
27
+ "Try again or open https://mergestorm.ai/settings");
28
+ }
29
+ return {
30
+ summary: settingsSummary(settings),
31
+ data: { ...settings },
32
+ };
33
+ }
@@ -0,0 +1,11 @@
1
+ import { type Config, type SettingsPatch } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ /** Read-only on `/api/v1/settings`; the API owns them, agents never write them. */
4
+ export declare const SETTINGS_READ_ONLY_KEYS: readonly ["cyclone_connected", "github_connected"];
5
+ /**
6
+ * Build a PATCH body from raw tool args. Only the Bearer-writable allowlist
7
+ * passes through; read-only keys and empty patches are usage errors so the
8
+ * failure is the caller's wording, never a silent no-op.
9
+ */
10
+ export declare function settingsPatchFromArgs(args: Record<string, unknown>): SettingsPatch;
11
+ export declare function settingsSet(args: Record<string, unknown>, cfg?: Config): Promise<ToolPayload>;
@@ -0,0 +1,62 @@
1
+ import { CommandError, BEARER_SETTINGS, SETTINGS_WRITABLE_KEYS, patchSettings, } from "mergestorm/client";
2
+ import { settingsSummary } from "./settings-get.js";
3
+ /** Read-only on `/api/v1/settings`; the API owns them, agents never write them. */
4
+ export const SETTINGS_READ_ONLY_KEYS = [
5
+ "cyclone_connected",
6
+ "github_connected",
7
+ ];
8
+ /**
9
+ * Build a PATCH body from raw tool args. Only the Bearer-writable allowlist
10
+ * passes through; read-only keys and empty patches are usage errors so the
11
+ * failure is the caller's wording, never a silent no-op.
12
+ */
13
+ export function settingsPatchFromArgs(args) {
14
+ for (const key of SETTINGS_READ_ONLY_KEYS) {
15
+ if (args[key] !== undefined) {
16
+ throw new CommandError(`${key} is read-only and cannot be set.`, 2, "usage");
17
+ }
18
+ }
19
+ const patch = {};
20
+ for (const key of SETTINGS_WRITABLE_KEYS) {
21
+ const value = args[key];
22
+ if (value === undefined)
23
+ continue;
24
+ const row = BEARER_SETTINGS.find((row) => row.key === key);
25
+ if ("kind" in row && (row.kind === "seconds" || row.kind === "count")) {
26
+ if (typeof value !== "number" ||
27
+ !Number.isInteger(value) ||
28
+ value < row.min ||
29
+ value > row.max) {
30
+ const unit = row.kind === "seconds" ? " of seconds" : "";
31
+ throw new CommandError(`${key} takes a whole number${unit} from ${row.min} through ${row.max}.`, 2, "usage");
32
+ }
33
+ Object.assign(patch, { [key]: value });
34
+ continue;
35
+ }
36
+ if ("kind" in row) {
37
+ const valid = row.kind === "logins"
38
+ ? Array.isArray(value) && value.every((login) => typeof login === "string")
39
+ : typeof value === "string" && row.values.includes(value);
40
+ if (!valid)
41
+ throw new CommandError(`Invalid value for ${key}.`, 2, "usage");
42
+ Object.assign(patch, { [key]: value });
43
+ continue;
44
+ }
45
+ if (typeof value !== "boolean") {
46
+ throw new CommandError(`${key} takes a boolean.`, 2, "usage");
47
+ }
48
+ Object.assign(patch, { [key]: value });
49
+ }
50
+ if (Object.keys(patch).length === 0) {
51
+ throw new CommandError(`At least one settings key is required: ${SETTINGS_WRITABLE_KEYS.join(", ")}.`, 2, "usage");
52
+ }
53
+ return patch;
54
+ }
55
+ export async function settingsSet(args, cfg) {
56
+ const patch = settingsPatchFromArgs(args);
57
+ const settings = await patchSettings(patch, cfg);
58
+ return {
59
+ summary: settingsSummary(settings),
60
+ data: { ...settings },
61
+ };
62
+ }
@@ -0,0 +1,4 @@
1
+ import { type Config } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export declare function stackRestack(stackId: string, cfg?: Config): Promise<ToolPayload>;
4
+ export declare function stackEnqueue(stackId: string, cfg?: Config, chatId?: string | null): Promise<ToolPayload>;
@@ -0,0 +1,22 @@
1
+ import { enqueueStack, loadConfig, restackStack, stackWatchObligation } from "mergestorm/client";
2
+ import { CHAT_WAKE_STACK_NOTE, registerChatWake } from "./chat-wake.js";
3
+ import { withWatchText } from "./stack-wait.js";
4
+ export async function stackRestack(stackId, cfg) {
5
+ const result = await restackStack(stackId, cfg);
6
+ const watch = stackWatchObligation({ stackId, terminal: null, unread: true, freshCursor: true });
7
+ return {
8
+ summary: withWatchText(watch, `Restacked stack ${stackId}: its layer branches were rebased onto their parents and pushed`),
9
+ data: { stack_id: stackId, result, watch },
10
+ };
11
+ }
12
+ export async function stackEnqueue(stackId, cfg, chatId) {
13
+ const resolved = cfg ?? (await loadConfig());
14
+ const result = await enqueueStack(stackId, resolved);
15
+ const watch = stackWatchObligation({ stackId, terminal: null, unread: true, freshCursor: true });
16
+ const wake = chatId ? await registerChatWake(resolved, chatId, { kind: "stack", stackId }) : null;
17
+ const summary = withWatchText(watch, `Sent stack ${stackId} to the Mergestorm merge queue`);
18
+ return {
19
+ summary: wake ? `${summary} ${CHAT_WAKE_STACK_NOTE}` : summary,
20
+ data: { stack_id: stackId, result, watch, ...(wake ? { chat_wake: wake } : {}) },
21
+ };
22
+ }
@@ -0,0 +1,44 @@
1
+ import { type Config } from "mergestorm/client";
2
+ import { z } from "zod";
3
+ import type { ToolPayload } from "./types.js";
4
+ export declare const stackAdoptSchema: {
5
+ owner: z.ZodString;
6
+ repo: z.ZodString;
7
+ pr_number: z.ZodNumber;
8
+ auto_land: z.ZodOptional<z.ZodBoolean>;
9
+ auto_review: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
10
+ auto_patch: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
11
+ auto_resolve_conflicts: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
12
+ auto_fix_ci: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
13
+ };
14
+ declare const inputSchema: z.ZodObject<{
15
+ owner: z.ZodString;
16
+ repo: z.ZodString;
17
+ pr_number: z.ZodNumber;
18
+ auto_land: z.ZodOptional<z.ZodBoolean>;
19
+ auto_review: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
20
+ auto_patch: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
21
+ auto_resolve_conflicts: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
22
+ auto_fix_ci: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
23
+ }, "strip", z.ZodTypeAny, {
24
+ owner: string;
25
+ repo: string;
26
+ pr_number: number;
27
+ auto_patch?: boolean | null | undefined;
28
+ auto_review?: boolean | null | undefined;
29
+ auto_land?: boolean | undefined;
30
+ auto_resolve_conflicts?: boolean | null | undefined;
31
+ auto_fix_ci?: boolean | null | undefined;
32
+ }, {
33
+ owner: string;
34
+ repo: string;
35
+ pr_number: number;
36
+ auto_patch?: boolean | null | undefined;
37
+ auto_review?: boolean | null | undefined;
38
+ auto_land?: boolean | undefined;
39
+ auto_resolve_conflicts?: boolean | null | undefined;
40
+ auto_fix_ci?: boolean | null | undefined;
41
+ }>;
42
+ export type StackAdoptInput = z.infer<typeof inputSchema>;
43
+ export declare function stackAdopt(input: StackAdoptInput, cfg?: Config): Promise<ToolPayload>;
44
+ export {};
@@ -0,0 +1,55 @@
1
+ import { adoptStack, CommandError, stackWatchObligation } from "mergestorm/client";
2
+ import { z } from "zod";
3
+ import { stackSetPatch } from "./stack-set.js";
4
+ import { withWatchText } from "./stack-wait.js";
5
+ export const stackAdoptSchema = {
6
+ owner: z.string().trim().min(1),
7
+ repo: z.string().trim().min(1),
8
+ pr_number: z.number().int().min(1),
9
+ auto_land: z.boolean().optional(),
10
+ auto_review: z.boolean().nullable().optional(),
11
+ auto_patch: z.boolean().nullable().optional(),
12
+ auto_resolve_conflicts: z.boolean().nullable().optional(),
13
+ auto_fix_ci: z.boolean().nullable().optional(),
14
+ };
15
+ const inputSchema = z.object(stackAdoptSchema);
16
+ export async function stackAdopt(input, cfg) {
17
+ const parsed = inputSchema.safeParse(input);
18
+ if (!parsed.success) {
19
+ const message = "stack_adopt requires owner, repo, a positive pr_number, and valid policy values";
20
+ return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
21
+ }
22
+ const { owner, repo, pr_number } = parsed.data;
23
+ try {
24
+ const result = await adoptStack(owner, repo, pr_number, cfg, stackSetPatch(parsed.data));
25
+ const summary = `Adopted stack for ${owner}/${repo}#${pr_number}`;
26
+ const stackId = typeof result === "object" && result !== null
27
+ ? result.stack?.id
28
+ : undefined;
29
+ if (typeof stackId !== "string" || !stackId) {
30
+ return { summary, data: { owner, repo, pr_number, result } };
31
+ }
32
+ const watch = stackWatchObligation({ stackId, terminal: null, unread: true, freshCursor: true });
33
+ return {
34
+ summary: withWatchText(watch, summary),
35
+ data: { owner, repo, pr_number, result, watch },
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 ?? "stack_adopt_failed" : "stack_adopt_failed",
45
+ message,
46
+ ...(err instanceof CommandError ? { exit_code: err.exitCode } : {}),
47
+ ...(err instanceof CommandError && err.reason !== undefined ? { reason: err.reason } : {}),
48
+ ...(err instanceof CommandError && err.retryAfterSeconds !== undefined
49
+ ? { retry_after_seconds: err.retryAfterSeconds } : {}),
50
+ },
51
+ },
52
+ isError: true,
53
+ };
54
+ }
55
+ }
@@ -0,0 +1,3 @@
1
+ import { type Config } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export declare function stackList(cfg?: Config): Promise<ToolPayload>;
@@ -0,0 +1,30 @@
1
+ import { isCommandErrorCode, listMergeQueueEntries, listStacks, } from "mergestorm/client";
2
+ import { stackSummary } from "./stack-summary.js";
3
+ export async function stackList(cfg) {
4
+ try {
5
+ const stacks = await listStacks(cfg);
6
+ const entries = await listMergeQueueEntries(cfg);
7
+ return {
8
+ summary: stacks.length === 0
9
+ ? "No stacks"
10
+ : stacks.map((stack) => stackSummary(stack, entries)).join("\n"),
11
+ data: { stacks },
12
+ };
13
+ }
14
+ catch (err) {
15
+ if (isCommandErrorCode(err, "rate_limited")) {
16
+ return {
17
+ summary: err.message,
18
+ data: {
19
+ error: {
20
+ code: "rate_limited",
21
+ message: err.message,
22
+ retry_after_seconds: err.retryAfterSeconds,
23
+ },
24
+ },
25
+ isError: true,
26
+ };
27
+ }
28
+ throw err;
29
+ }
30
+ }