mergestorm-mcp 0.2.26 → 0.2.28

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 (73) hide show
  1. package/README.md +16 -2
  2. package/dist/index.js +828 -223
  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 +14 -0
  10. package/dist/lib/pr-loop-instructions.js +73 -0
  11. package/dist/lib/server.d.ts +24 -0
  12. package/dist/lib/server.js +644 -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-task.d.ts +18 -0
  16. package/dist/lib/tools/chat-task.js +28 -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 +28 -0
  20. package/dist/lib/tools/cyclone-job.js +71 -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/memory.d.ts +68 -0
  26. package/dist/lib/tools/memory.js +74 -0
  27. package/dist/lib/tools/queue-status.d.ts +3 -0
  28. package/dist/lib/tools/queue-status.js +79 -0
  29. package/dist/lib/tools/repo-settings.d.ts +6 -0
  30. package/dist/lib/tools/repo-settings.js +78 -0
  31. package/dist/lib/tools/review-dismiss.d.ts +16 -0
  32. package/dist/lib/tools/review-dismiss.js +68 -0
  33. package/dist/lib/tools/review-get-pr.d.ts +22 -0
  34. package/dist/lib/tools/review-get-pr.js +52 -0
  35. package/dist/lib/tools/review-get.d.ts +3 -0
  36. package/dist/lib/tools/review-get.js +27 -0
  37. package/dist/lib/tools/review-list.d.ts +3 -0
  38. package/dist/lib/tools/review-list.js +10 -0
  39. package/dist/lib/tools/review-start.d.ts +11 -0
  40. package/dist/lib/tools/review-start.js +22 -0
  41. package/dist/lib/tools/review-submit.d.ts +36 -0
  42. package/dist/lib/tools/review-submit.js +172 -0
  43. package/dist/lib/tools/review-wait-pr.d.ts +11 -0
  44. package/dist/lib/tools/review-wait-pr.js +66 -0
  45. package/dist/lib/tools/review-wait.d.ts +6 -0
  46. package/dist/lib/tools/review-wait.js +33 -0
  47. package/dist/lib/tools/settings-get.d.ts +8 -0
  48. package/dist/lib/tools/settings-get.js +33 -0
  49. package/dist/lib/tools/settings-set.d.ts +11 -0
  50. package/dist/lib/tools/settings-set.js +62 -0
  51. package/dist/lib/tools/stack-actions.d.ts +4 -0
  52. package/dist/lib/tools/stack-actions.js +22 -0
  53. package/dist/lib/tools/stack-adopt.d.ts +44 -0
  54. package/dist/lib/tools/stack-adopt.js +55 -0
  55. package/dist/lib/tools/stack-list.d.ts +3 -0
  56. package/dist/lib/tools/stack-list.js +30 -0
  57. package/dist/lib/tools/stack-publish.d.ts +78 -0
  58. package/dist/lib/tools/stack-publish.js +89 -0
  59. package/dist/lib/tools/stack-set.d.ts +16 -0
  60. package/dist/lib/tools/stack-set.js +69 -0
  61. package/dist/lib/tools/stack-status.d.ts +3 -0
  62. package/dist/lib/tools/stack-status.js +57 -0
  63. package/dist/lib/tools/stack-summary.d.ts +2 -0
  64. package/dist/lib/tools/stack-summary.js +29 -0
  65. package/dist/lib/tools/stack-wait.d.ts +44 -0
  66. package/dist/lib/tools/stack-wait.js +146 -0
  67. package/dist/lib/tools/surge-setup.d.ts +6 -0
  68. package/dist/lib/tools/surge-setup.js +4 -0
  69. package/dist/lib/tools/types.d.ts +5 -0
  70. package/dist/lib/tools/types.js +1 -0
  71. package/dist/lib/tools/whoami.d.ts +3 -0
  72. package/dist/lib/tools/whoami.js +27 -0
  73. package/package.json +13 -3
@@ -0,0 +1,6 @@
1
+ import { type Config } from "mergestorm/client";
2
+ import type { ToolPayload } from "./types.js";
3
+ export declare function agentRequest(route: string, init: {
4
+ method: "GET" | "POST";
5
+ json?: Record<string, unknown>;
6
+ }, cfg?: Config): Promise<ToolPayload>;
@@ -0,0 +1,25 @@
1
+ import { apiFetch, CommandError, loadConfig } from "mergestorm/client";
2
+ export async function agentRequest(route, init, cfg) {
3
+ const resolved = cfg ?? (await loadConfig());
4
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, route, { method: init.method, ...(init.json ? { json: init.json } : {}) });
5
+ const data = body && typeof body === "object" && !Array.isArray(body) ? body : { body };
6
+ if (status === 404 && data.error === undefined) {
7
+ throw new CommandError("This Mergestorm server does not offer that action yet.", 1, "not_found");
8
+ }
9
+ const message = typeof data.message === "string" ? data.message : `Mergestorm answered ${status}.`;
10
+ if (status >= 400) {
11
+ return {
12
+ summary: message,
13
+ isError: true,
14
+ data: {
15
+ ...data,
16
+ error: {
17
+ code: typeof data.error === "string" ? data.error : `http_${status}`,
18
+ message,
19
+ ...(retryAfterSeconds !== undefined ? { retry_after_seconds: retryAfterSeconds } : {}),
20
+ },
21
+ },
22
+ };
23
+ }
24
+ return { summary: message, data };
25
+ }
@@ -0,0 +1,18 @@
1
+ import { type Config } from "mergestorm/client";
2
+ export type ChatTaskTarget = {
3
+ kind: "review";
4
+ owner: string;
5
+ repo: string;
6
+ prNumber: number;
7
+ } | {
8
+ kind: "stack";
9
+ stackId: string;
10
+ };
11
+ export type ChatTask = {
12
+ id: string;
13
+ title: string;
14
+ };
15
+ export declare function breezeChatId(value: string | null | undefined): string | null;
16
+ export declare function chatTaskOf(value: unknown): ChatTask | null;
17
+ export declare function chatTaskNote(task: ChatTask): string;
18
+ export declare function trackInChat(cfg: Config, chatId: string, target: ChatTaskTarget, signal?: AbortSignal): Promise<ChatTask | null>;
@@ -0,0 +1,28 @@
1
+ import { apiFetch } from "mergestorm/client";
2
+ const CHAT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3
+ export function breezeChatId(value) {
4
+ const trimmed = value?.trim() ?? "";
5
+ return CHAT_ID.test(trimmed) ? trimmed.toLowerCase() : null;
6
+ }
7
+ export function chatTaskOf(value) {
8
+ if (!value || typeof value !== "object")
9
+ return null;
10
+ const { id, title } = value;
11
+ return typeof id === "string" && typeof title === "string" ? { id, title } : null;
12
+ }
13
+ export function chatTaskNote(task) {
14
+ return `This chat tracks it as background task ${task.id} "${task.title}" and is told the result when it ends. If the user is waiting for the outcome, wait for it before you end the reply, then tell them what actually happened.`;
15
+ }
16
+ export async function trackInChat(cfg, chatId, target, signal) {
17
+ const json = target.kind === "review"
18
+ ? { chat_id: chatId, kind: "review", owner: target.owner, repo: target.repo, pr_number: target.prNumber }
19
+ : { chat_id: chatId, kind: "stack", stack_id: target.stackId };
20
+ try {
21
+ const response = await apiFetch(cfg, "/api/v1/stacks/chat-waits", { method: "POST", json, signal, timeoutMs: 10_000 });
22
+ const body = response.body;
23
+ return response.status === 200 ? chatTaskOf(body?.task) : null;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
@@ -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,28 @@
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 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.";
11
+ export type CycloneStartInput = {
12
+ task: CycloneTask;
13
+ owner: string;
14
+ repo: string;
15
+ pr_number?: number;
16
+ failing_check?: string;
17
+ brief?: string;
18
+ };
19
+ export declare function cycloneStart(input: CycloneStartInput, cfg?: Config, chatId?: string | null): Promise<ToolPayload>;
20
+ export declare function isCycloneJobTerminal(job: Record<string, unknown>): boolean;
21
+ export declare function cycloneJobSummary(job: Record<string, unknown>): string;
22
+ export type CycloneJobWaitDeps = {
23
+ pollMs?: number;
24
+ now?: () => number;
25
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
26
+ signal?: AbortSignal;
27
+ };
28
+ export declare function cycloneJob(dispatchId: string, cfg?: Config, timeoutS?: number, deps?: CycloneJobWaitDeps): Promise<ToolPayload>;
@@ -0,0 +1,71 @@
1
+ import { agentRequest } from "./agent-request.js";
2
+ import { chatTaskNote, chatTaskOf } from "./chat-task.js";
3
+ export const CYCLONE_TASKS = ["fix_findings", "fix_ci", "resolve_conflicts", "build"];
4
+ export const CYCLONE_JOB_WAIT_DEFAULT_S = 45;
5
+ export const CYCLONE_JOB_WAIT_MAX_S = 100;
6
+ export const CYCLONE_JOB_POLL_MS = 5_000;
7
+ export const CYCLONE_JOB_TERMINAL_STATES = ["pushed", "completed", "failed"];
8
+ 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.`;
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
+ const task = chatTaskOf(payload.data.background_task);
26
+ return { ...payload, summary: `${payload.summary} ${task ? chatTaskNote(task) : CYCLONE_START_WAIT_NOTE}` };
27
+ }
28
+ export function isCycloneJobTerminal(job) {
29
+ return CYCLONE_JOB_TERMINAL_STATES.includes(job.state);
30
+ }
31
+ export function cycloneJobSummary(job) {
32
+ const target = job.pr_number ? `${job.owner}/${job.repo}#${job.pr_number}` : `${job.owner}/${job.repo}`;
33
+ const parts = [`Cyclone ${job.task ?? "job"} on ${target}: ${job.state}`];
34
+ if (typeof job.outcome === "string" && job.outcome)
35
+ parts[0] += ` (${job.outcome})`;
36
+ if (typeof job.result_sha === "string" && job.result_sha)
37
+ parts.push(`pushed ${job.result_sha.slice(0, 10)}`);
38
+ if (typeof job.detail === "string" && job.detail)
39
+ parts.push(job.detail);
40
+ return parts.join(". ");
41
+ }
42
+ function sleepFor(ms, signal) {
43
+ return new Promise((resolve) => {
44
+ if (signal?.aborted)
45
+ return resolve();
46
+ const timer = setTimeout(done, ms);
47
+ function done() {
48
+ clearTimeout(timer);
49
+ signal?.removeEventListener("abort", done);
50
+ resolve();
51
+ }
52
+ signal?.addEventListener("abort", done, { once: true });
53
+ });
54
+ }
55
+ export async function cycloneJob(dispatchId, cfg, timeoutS = 0, deps = {}) {
56
+ const now = deps.now ?? Date.now;
57
+ const sleep = deps.sleep ?? sleepFor;
58
+ const deadline = now() + Math.min(Math.max(timeoutS, 0), CYCLONE_JOB_WAIT_MAX_S) * 1000;
59
+ const route = `/api/v1/agents/cyclone/jobs/${encodeURIComponent(dispatchId)}`;
60
+ for (;;) {
61
+ const payload = await agentRequest(route, { method: "GET" }, cfg);
62
+ if (payload.isError)
63
+ return payload;
64
+ if (isCycloneJobTerminal(payload.data))
65
+ return { ...payload, summary: cycloneJobSummary(payload.data) };
66
+ const left = deadline - now();
67
+ if (left <= 0 || deps.signal?.aborted)
68
+ return { ...payload, summary: `${cycloneJobSummary(payload.data)}. ${CYCLONE_JOB_STILL_RUNNING_NOTE}` };
69
+ await sleep(Math.min(deps.pollMs ?? CYCLONE_JOB_POLL_MS, left), deps.signal);
70
+ }
71
+ }
@@ -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,68 @@
1
+ import { MEMORY_KINDS, MEMORY_MODES, MEMORY_SCOPES, deleteMemory, getMemoryMode, listMemories, saveMemory, searchMemories, setMemoryMode, updateMemory, type Config, type MemoryItem } from "mergestorm/client";
2
+ import { z } from "zod";
3
+ import type { ToolPayload } from "./types.js";
4
+ export declare const listMemoriesSchema: {
5
+ scope: z.ZodOptional<z.ZodEnum<["user", "repo"]>>;
6
+ repo: z.ZodOptional<z.ZodString>;
7
+ kind: z.ZodOptional<z.ZodEnum<["rule", "style", "preference", "fact", "project"]>>;
8
+ };
9
+ export declare const searchMemorySchema: {
10
+ query: z.ZodString;
11
+ repo: z.ZodOptional<z.ZodString>;
12
+ limit: z.ZodOptional<z.ZodNumber>;
13
+ };
14
+ export declare const rememberSchema: {
15
+ text: z.ZodString;
16
+ kind: z.ZodEnum<["rule", "style", "preference", "fact", "project"]>;
17
+ repo: z.ZodOptional<z.ZodString>;
18
+ };
19
+ export declare const updateMemorySchema: {
20
+ id: z.ZodString;
21
+ text: z.ZodString;
22
+ kind: z.ZodOptional<z.ZodEnum<["rule", "style", "preference", "fact", "project"]>>;
23
+ };
24
+ export declare const forgetMemorySchema: {
25
+ id: z.ZodString;
26
+ };
27
+ export declare const setMemoryModeSchema: {
28
+ memory_mode: z.ZodEnum<["off", "on_request", "all"]>;
29
+ };
30
+ export type MemoryApi = {
31
+ listMemories: typeof listMemories;
32
+ searchMemories: typeof searchMemories;
33
+ saveMemory: typeof saveMemory;
34
+ updateMemory: typeof updateMemory;
35
+ deleteMemory: typeof deleteMemory;
36
+ getMemoryMode: typeof getMemoryMode;
37
+ setMemoryMode: typeof setMemoryMode;
38
+ };
39
+ export declare function memoryLine(memory: MemoryItem): string;
40
+ export declare function createMemoryToolHandlers(cfg: Config | undefined, chatId: string | null, api?: MemoryApi): {
41
+ list: (input: {
42
+ scope?: (typeof MEMORY_SCOPES)[number];
43
+ repo?: string;
44
+ kind?: (typeof MEMORY_KINDS)[number];
45
+ }) => Promise<ToolPayload>;
46
+ search: (input: {
47
+ query: string;
48
+ repo?: string;
49
+ limit?: number;
50
+ }) => Promise<ToolPayload>;
51
+ remember: (input: {
52
+ text: string;
53
+ kind: (typeof MEMORY_KINDS)[number];
54
+ repo?: string;
55
+ }) => Promise<ToolPayload>;
56
+ update: (input: {
57
+ id: string;
58
+ text: string;
59
+ kind?: (typeof MEMORY_KINDS)[number];
60
+ }) => Promise<ToolPayload>;
61
+ forget: (input: {
62
+ id: string;
63
+ }) => Promise<ToolPayload>;
64
+ getMode: () => Promise<ToolPayload>;
65
+ setMode: (input: {
66
+ memory_mode: (typeof MEMORY_MODES)[number];
67
+ }) => Promise<ToolPayload>;
68
+ };
@@ -0,0 +1,74 @@
1
+ import { CommandError, MEMORY_KINDS, MEMORY_MODES, MEMORY_SCOPES, MEMORY_SEARCH_LIMIT_MAX, deleteMemory, getMemoryMode, listMemories, saveMemory, searchMemories, setMemoryMode, updateMemory, } from "mergestorm/client";
2
+ import { z } from "zod";
3
+ const memoryId = z.string().uuid();
4
+ const memoryText = z.string().trim().min(1).max(500);
5
+ const repo = z.string().trim().min(3).max(200);
6
+ export const listMemoriesSchema = {
7
+ scope: z.enum(MEMORY_SCOPES).optional(),
8
+ repo: repo.optional(),
9
+ kind: z.enum(MEMORY_KINDS).optional(),
10
+ };
11
+ export const searchMemorySchema = {
12
+ query: z.string().trim().min(1).max(2000),
13
+ repo: repo.optional(),
14
+ limit: z.number().int().min(1).max(MEMORY_SEARCH_LIMIT_MAX).optional(),
15
+ };
16
+ export const rememberSchema = { text: memoryText, kind: z.enum(MEMORY_KINDS), repo: repo.optional() };
17
+ export const updateMemorySchema = { id: memoryId, text: memoryText, kind: z.enum(MEMORY_KINDS).optional() };
18
+ export const forgetMemorySchema = { id: memoryId };
19
+ export const setMemoryModeSchema = { memory_mode: z.enum(MEMORY_MODES) };
20
+ const liveApi = { listMemories, searchMemories, saveMemory, updateMemory, deleteMemory, getMemoryMode, setMemoryMode };
21
+ export function memoryLine(memory) {
22
+ return `- [${memory.id}] (${memory.repo ? `${memory.kind}, ${memory.repo}` : memory.kind}) ${memory.text}`;
23
+ }
24
+ async function guarded(run) {
25
+ try {
26
+ return await run();
27
+ }
28
+ catch (err) {
29
+ const message = err instanceof Error ? err.message : String(err);
30
+ const code = err instanceof CommandError ? err.code ?? "memory_failed" : "memory_failed";
31
+ return {
32
+ summary: message,
33
+ data: { error: { code, message, ...(err instanceof CommandError && err.retryAfterSeconds !== undefined ? { retry_after_seconds: err.retryAfterSeconds } : {}) } },
34
+ isError: true,
35
+ };
36
+ }
37
+ }
38
+ export function createMemoryToolHandlers(cfg, chatId, api = liveApi) {
39
+ return {
40
+ list: (input) => guarded(async () => {
41
+ const listed = await api.listMemories(input, cfg);
42
+ const count = listed.items.length === 1 ? "1 memory" : `${listed.items.length} memories`;
43
+ return { summary: [`Memory is ${listed.memory_mode}. ${count}.`, ...listed.items.map(memoryLine)].join("\n"), data: listed };
44
+ }),
45
+ search: (input) => guarded(async () => {
46
+ const found = await api.searchMemories({ ...input, chatId }, cfg);
47
+ const all = [...found.rules, ...found.items];
48
+ const summary = found.memory_mode === "off"
49
+ ? "Memory is off, so nothing was searched."
50
+ : all.length === 0 ? "Nothing in memory matches." : ["Memories that match, best first:", ...all.map(memoryLine)].join("\n");
51
+ return { summary, data: found };
52
+ }),
53
+ remember: (input) => guarded(async () => {
54
+ const saved = await api.saveMemory({ ...input, chatId }, cfg);
55
+ return { summary: saved.merged ? `That matched memory ${saved.id}, which now says this.` : `Saved to memory as ${saved.id}.`, data: saved };
56
+ }),
57
+ update: (input) => guarded(async () => {
58
+ const updated = await api.updateMemory(input.id, { text: input.text, ...(input.kind ? { kind: input.kind } : {}) }, cfg);
59
+ return { summary: `Memory ${updated.id} updated.`, data: updated };
60
+ }),
61
+ forget: (input) => guarded(async () => {
62
+ const deleted = await api.deleteMemory(input.id, cfg);
63
+ return { summary: `Memory ${deleted.id} forgotten.`, data: deleted };
64
+ }),
65
+ getMode: () => guarded(async () => {
66
+ const mode = await api.getMemoryMode(cfg);
67
+ return { summary: `Memory is ${mode.memory_mode}.`, data: mode };
68
+ }),
69
+ setMode: (input) => guarded(async () => {
70
+ const mode = await api.setMemoryMode(input.memory_mode, cfg);
71
+ return { summary: `Memory is now ${mode.memory_mode}.`, data: mode };
72
+ }),
73
+ };
74
+ }
@@ -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>;