mergestorm-mcp 0.2.27 → 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.
- package/README.md +3 -2
- package/dist/index.js +478 -227
- package/dist/lib/hosted.d.ts +1 -1
- package/dist/lib/hosted.js +1 -1
- package/dist/lib/pr-loop-instructions.d.ts +1 -0
- package/dist/lib/pr-loop-instructions.js +12 -1
- package/dist/lib/server.d.ts +6 -4
- package/dist/lib/server.js +111 -25
- package/dist/lib/tools/chat-task.d.ts +18 -0
- package/dist/lib/tools/chat-task.js +28 -0
- package/dist/lib/tools/cyclone-job.d.ts +0 -1
- package/dist/lib/tools/cyclone-job.js +3 -2
- package/dist/lib/tools/memory.d.ts +68 -0
- package/dist/lib/tools/memory.js +74 -0
- package/dist/lib/tools/repo-settings.js +25 -1
- package/dist/lib/tools/review-start.js +3 -2
- package/dist/lib/tools/review-wait-pr.js +5 -13
- package/dist/lib/tools/settings-set.js +2 -2
- package/dist/lib/tools/stack-actions.js +4 -4
- package/dist/lib/tools/stack-publish.d.ts +2 -2
- package/dist/lib/tools/stack-wait.d.ts +21 -1
- package/dist/lib/tools/stack-wait.js +72 -10
- package/package.json +4 -2
- package/dist/lib/tools/chat-wake.d.ts +0 -21
- package/dist/lib/tools/chat-wake.js +0 -26
|
@@ -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
|
+
}
|
|
@@ -3,6 +3,10 @@ const ARG_BY_KEY = {
|
|
|
3
3
|
auto_patch_enabled: "auto_patch",
|
|
4
4
|
auto_review_enabled: "auto_review",
|
|
5
5
|
auto_land_default: "auto_land",
|
|
6
|
+
merge_queue_batch_enabled: "merge_queue_batch",
|
|
7
|
+
merge_queue_batch_size: "merge_queue_batch_size",
|
|
8
|
+
merge_queue_lands_alone_paths: "merge_queue_lands_alone_paths",
|
|
9
|
+
merge_queue_overdrive: "merge_queue_overdrive",
|
|
6
10
|
};
|
|
7
11
|
function parseRepo(raw) {
|
|
8
12
|
const match = typeof raw === "string" ? /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(raw.trim()) : null;
|
|
@@ -11,7 +15,13 @@ function parseRepo(raw) {
|
|
|
11
15
|
return { owner: match[1], repo: match[2] };
|
|
12
16
|
}
|
|
13
17
|
function words(value) {
|
|
14
|
-
|
|
18
|
+
if (value === null)
|
|
19
|
+
return "inherit";
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return value.length === 0 ? "none" : value.join(",");
|
|
22
|
+
if (typeof value === "number")
|
|
23
|
+
return String(value);
|
|
24
|
+
return value ? "on" : "off";
|
|
15
25
|
}
|
|
16
26
|
export function repoSettingsSummary(entry) {
|
|
17
27
|
return `${entry.owner}/${entry.repo}: ${REPO_SETTINGS_KEYS.map((key) => `${ARG_BY_KEY[key]} ${words(entry[key])}`).join(" · ")}`;
|
|
@@ -22,6 +32,20 @@ export function repoSettingsPatchFromArgs(args) {
|
|
|
22
32
|
const value = args[ARG_BY_KEY[key]];
|
|
23
33
|
if (value === undefined)
|
|
24
34
|
continue;
|
|
35
|
+
if (key === "merge_queue_batch_size") {
|
|
36
|
+
if (value !== null && (typeof value !== "number" || !Number.isInteger(value))) {
|
|
37
|
+
throw new CommandError(`${ARG_BY_KEY[key]} takes a whole number or null.`, 2, "usage");
|
|
38
|
+
}
|
|
39
|
+
patch[key] = value;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (key === "merge_queue_lands_alone_paths") {
|
|
43
|
+
if (value !== null && !(Array.isArray(value) && value.every((entry) => typeof entry === "string"))) {
|
|
44
|
+
throw new CommandError(`${ARG_BY_KEY[key]} takes a list of path patterns or null.`, 2, "usage");
|
|
45
|
+
}
|
|
46
|
+
patch[key] = value;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
25
49
|
if (value !== null && typeof value !== "boolean") {
|
|
26
50
|
throw new CommandError(`${ARG_BY_KEY[key]} takes true, false, or null.`, 2, "usage");
|
|
27
51
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agentRequest } from "./agent-request.js";
|
|
2
|
-
import {
|
|
2
|
+
import { chatTaskNote, chatTaskOf } from "./chat-task.js";
|
|
3
3
|
export async function reviewStartPr(input, cfg, chatId) {
|
|
4
4
|
const payload = await agentRequest("/api/v1/agents/vortex/review", {
|
|
5
5
|
method: "POST",
|
|
@@ -11,7 +11,8 @@ export async function reviewStartPr(input, cfg, chatId) {
|
|
|
11
11
|
...(chatId ? { chat_id: chatId } : {}),
|
|
12
12
|
},
|
|
13
13
|
}, cfg);
|
|
14
|
-
|
|
14
|
+
const task = chatTaskOf(payload.data.background_task);
|
|
15
|
+
return task ? { ...payload, summary: `${payload.summary} ${chatTaskNote(task)}` } : payload;
|
|
15
16
|
}
|
|
16
17
|
export async function reviewStopPr(input, cfg) {
|
|
17
18
|
return agentRequest("/api/v1/agents/vortex/stop", {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PrReviewPollTimeoutError, loadConfig, noPrReviewYetMessage, pollPrVortexReview, } from "mergestorm/client";
|
|
2
2
|
import { payloadFromPrRateLimit, payloadFromPrReview, prReviewSummary, prReviewTarget, reviewGetPr, } from "./review-get-pr.js";
|
|
3
3
|
import { MCP_PR_WAIT_DEFAULT_S, waitTimeoutMs } from "./review-submit.js";
|
|
4
|
-
import {
|
|
4
|
+
import { chatTaskNote, trackInChat } from "./chat-task.js";
|
|
5
5
|
export async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opts = {}) {
|
|
6
6
|
const target = prReviewTarget(owner, repo, prNumber);
|
|
7
7
|
const resolved = cfg ?? (await loadConfig());
|
|
@@ -39,22 +39,14 @@ export async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, c
|
|
|
39
39
|
finding_count: findingCount,
|
|
40
40
|
phase: err.lastEnvelope?.phase ?? null,
|
|
41
41
|
});
|
|
42
|
-
const
|
|
43
|
-
|
|
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
|
-
})
|
|
42
|
+
const task = opts.chatId
|
|
43
|
+
? await trackInChat(resolved, opts.chatId, { kind: "review", owner: target.owner, repo: target.repo, prNumber: target.prNumber })
|
|
52
44
|
: null;
|
|
53
45
|
const shown = err.reviewSeen
|
|
54
46
|
? summary
|
|
55
47
|
: `${summary} · ${noPrReviewYetMessage(target.owner, target.repo, target.prNumber, { afterSha, pass: opts.pass, afterPass: opts.afterPass })}`;
|
|
56
48
|
return {
|
|
57
|
-
summary:
|
|
49
|
+
summary: task ? `${shown} · ${chatTaskNote(task)}` : shown,
|
|
58
50
|
data: {
|
|
59
51
|
status,
|
|
60
52
|
head_sha: err.lastEnvelope?.head_sha ?? null,
|
|
@@ -62,7 +54,7 @@ export async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, c
|
|
|
62
54
|
finding_count: findingCount,
|
|
63
55
|
phase: err.lastEnvelope?.phase ?? null,
|
|
64
56
|
error: err.message,
|
|
65
|
-
...(
|
|
57
|
+
...(task ? { background_task: task } : {}),
|
|
66
58
|
},
|
|
67
59
|
};
|
|
68
60
|
}
|
|
@@ -34,8 +34,8 @@ export function settingsPatchFromArgs(args) {
|
|
|
34
34
|
continue;
|
|
35
35
|
}
|
|
36
36
|
if ("kind" in row) {
|
|
37
|
-
const valid = row.kind === "logins"
|
|
38
|
-
? Array.isArray(value) && value.every((
|
|
37
|
+
const valid = row.kind === "logins" || row.kind === "paths"
|
|
38
|
+
? Array.isArray(value) && value.every((entry) => typeof entry === "string")
|
|
39
39
|
: typeof value === "string" && row.values.includes(value);
|
|
40
40
|
if (!valid)
|
|
41
41
|
throw new CommandError(`Invalid value for ${key}.`, 2, "usage");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { enqueueStack, loadConfig, restackStack, stackWatchObligation } from "mergestorm/client";
|
|
2
|
-
import {
|
|
2
|
+
import { chatTaskNote, trackInChat } from "./chat-task.js";
|
|
3
3
|
import { withWatchText } from "./stack-wait.js";
|
|
4
4
|
export async function stackRestack(stackId, cfg) {
|
|
5
5
|
const result = await restackStack(stackId, cfg);
|
|
@@ -13,10 +13,10 @@ export async function stackEnqueue(stackId, cfg, chatId) {
|
|
|
13
13
|
const resolved = cfg ?? (await loadConfig());
|
|
14
14
|
const result = await enqueueStack(stackId, resolved);
|
|
15
15
|
const watch = stackWatchObligation({ stackId, terminal: null, unread: true, freshCursor: true });
|
|
16
|
-
const
|
|
16
|
+
const task = chatId ? await trackInChat(resolved, chatId, { kind: "stack", stackId }) : null;
|
|
17
17
|
const summary = withWatchText(watch, `Sent stack ${stackId} to the Mergestorm merge queue`);
|
|
18
18
|
return {
|
|
19
|
-
summary:
|
|
20
|
-
data: { stack_id: stackId, result, watch, ...(
|
|
19
|
+
summary: task ? `${summary} ${chatTaskNote(task)}` : summary,
|
|
20
|
+
data: { stack_id: stackId, result, watch, ...(task ? { background_task: task } : {}) },
|
|
21
21
|
};
|
|
22
22
|
}
|
|
@@ -31,8 +31,8 @@ declare const publishInput: z.ZodObject<{
|
|
|
31
31
|
repo: z.ZodOptional<z.ZodString>;
|
|
32
32
|
branch: z.ZodOptional<z.ZodString>;
|
|
33
33
|
}, "strip", z.ZodTypeAny, {
|
|
34
|
-
owner?: string | undefined;
|
|
35
34
|
title?: string | undefined;
|
|
35
|
+
owner?: string | undefined;
|
|
36
36
|
body?: string | undefined;
|
|
37
37
|
repo?: string | undefined;
|
|
38
38
|
cwd?: string | undefined;
|
|
@@ -42,8 +42,8 @@ declare const publishInput: z.ZodObject<{
|
|
|
42
42
|
extend?: boolean | undefined;
|
|
43
43
|
replace_head?: string | undefined;
|
|
44
44
|
}, {
|
|
45
|
-
owner?: string | undefined;
|
|
46
45
|
title?: string | undefined;
|
|
46
|
+
owner?: string | undefined;
|
|
47
47
|
body?: string | undefined;
|
|
48
48
|
repo?: string | undefined;
|
|
49
49
|
cwd?: string | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pollStackWatch, type Config, type StackRepairHint, type StackWatchObligation } from "mergestorm/client";
|
|
1
|
+
import { pollStackWatch, type Config, type StackRepairHint, type StackWatchObligation, type StackWatchNextArgs } from "mergestorm/client";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { ToolPayload } from "./types.js";
|
|
4
4
|
export declare const stackWaitSchema: {
|
|
@@ -17,8 +17,28 @@ export type StackWaitInput = {
|
|
|
17
17
|
};
|
|
18
18
|
export declare function repairSummary(repair: StackRepairHint | null | undefined): string;
|
|
19
19
|
export declare function withWatchText(watch: StackWatchObligation, body: string): string;
|
|
20
|
+
export type HostedStackWatch = Omit<StackWatchObligation, "next"> & {
|
|
21
|
+
next: {
|
|
22
|
+
tool: "stack_wait";
|
|
23
|
+
args: StackWatchNextArgs;
|
|
24
|
+
} | null;
|
|
25
|
+
};
|
|
26
|
+
export declare function hostedStackWatch(watch: StackWatchObligation): HostedStackWatch;
|
|
27
|
+
type McpTextResult = {
|
|
28
|
+
isError?: boolean;
|
|
29
|
+
content: {
|
|
30
|
+
type: "text";
|
|
31
|
+
text: string;
|
|
32
|
+
}[];
|
|
33
|
+
structuredContent?: Record<string, unknown>;
|
|
34
|
+
};
|
|
35
|
+
export type HostedStackRepair = Omit<StackRepairHint, "steps" | "note">;
|
|
36
|
+
export declare function hostedStackRepair(repair: StackRepairHint): HostedStackRepair;
|
|
37
|
+
export declare function hostedToolResult<T extends McpTextResult>(result: T): T;
|
|
20
38
|
export declare function stackWait(input: StackWaitInput, cfg?: Config, deps?: {
|
|
21
39
|
pollStackWatch?: typeof pollStackWatch;
|
|
22
40
|
signal?: AbortSignal;
|
|
23
41
|
chatId?: string;
|
|
42
|
+
hosted?: boolean;
|
|
24
43
|
}): Promise<ToolPayload>;
|
|
44
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadConfig, stackBlockersSummary, pollStackWatch, StackWatchError, StackWatchTimeoutError, stackWatchBackgroundCommand, stackWatchObligation, } from "mergestorm/client";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { chatTaskNote, trackInChat } from "./chat-task.js";
|
|
4
4
|
export const stackWaitSchema = {
|
|
5
5
|
stack_id: z.string().trim().min(1),
|
|
6
6
|
timeout_s: z.number().finite().min(0).max(45).default(45),
|
|
@@ -18,15 +18,76 @@ export function repairSummary(repair) {
|
|
|
18
18
|
export function withWatchText(watch, body) {
|
|
19
19
|
return `${watch.message}\n\n${body}`;
|
|
20
20
|
}
|
|
21
|
+
const KEEP_EARLIER_CURSOR = "If you already hold a cursor from an earlier stack_wait on this stack, keep passing that cursor instead.";
|
|
22
|
+
export function hostedStackWatch(watch) {
|
|
23
|
+
if (watch.done) {
|
|
24
|
+
return {
|
|
25
|
+
...watch,
|
|
26
|
+
next: null,
|
|
27
|
+
message: watch.reason === "not_found"
|
|
28
|
+
? "No stack with this id exists for this account. Mergestorm deletes a stack once none of its layers is open, which can mean merged or closed, so landing is unconfirmed: check on GitHub that each PR is merged before reporting it landed. The watch is done."
|
|
29
|
+
: watch.message,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const args = watch.next?.args;
|
|
33
|
+
const attention = watch.reason === "attention";
|
|
34
|
+
const retry = watch.reason === "rate_limited" ? " Wait retry_after_seconds first." : "";
|
|
35
|
+
const failed = watch.reason === "failed" ? " This read failed; if the next one fails too, tell the human the stack is no longer watched." : "";
|
|
36
|
+
const keep = watch.message.includes(KEEP_EARLIER_CURSOR) ? ` ${KEEP_EARLIER_CURSOR}` : "";
|
|
37
|
+
const resume = args
|
|
38
|
+
? attention
|
|
39
|
+
? ` After the blocker is handled, call stack_wait ${JSON.stringify(args)}.`
|
|
40
|
+
: ` Next call: stack_wait ${JSON.stringify(args)}.`
|
|
41
|
+
: "";
|
|
42
|
+
return {
|
|
43
|
+
...watch,
|
|
44
|
+
next: args && !attention ? { tool: "stack_wait", args } : null,
|
|
45
|
+
message: `This stack is not landed and the watch is not done.${retry}${failed}${resume}${keep}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function hostedStackRepair(repair) {
|
|
49
|
+
const { steps: _steps, note: _note, findings: _findings, ...facts } = repair;
|
|
50
|
+
return facts;
|
|
51
|
+
}
|
|
52
|
+
function hostedRepairSummary(repair) {
|
|
53
|
+
const files = "files" in repair && repair.files.length ? ` Files: ${repair.files.join(", ")}.` : "";
|
|
54
|
+
const breeze = "publishedBy" in repair && repair.publishedBy === "breeze"
|
|
55
|
+
? ` ${repair.branch} is a Breeze branch${repair.liveParent ? ` on ${repair.liveParent}` : ""}: open_pull_request publishes it with single-parent commits only, so a merge commit cannot be published.`
|
|
56
|
+
: "";
|
|
57
|
+
return `\nRepair #${repair.prNumber} (${repair.kind}).${files}${breeze}`;
|
|
58
|
+
}
|
|
59
|
+
export function hostedToolResult(result) {
|
|
60
|
+
const data = result.structuredContent;
|
|
61
|
+
const watch = data?.watch;
|
|
62
|
+
const repair = data?.repair;
|
|
63
|
+
const hasWatch = !!watch && typeof watch.message === "string";
|
|
64
|
+
const hasRepair = !!repair && typeof repair.steps === "string";
|
|
65
|
+
if (!data || (!hasWatch && !hasRepair))
|
|
66
|
+
return result;
|
|
67
|
+
let text = result.content[0]?.text ?? "";
|
|
68
|
+
const structured = { ...data };
|
|
69
|
+
if (hasWatch) {
|
|
70
|
+
const hosted = hostedStackWatch(watch);
|
|
71
|
+
structured.watch = hosted;
|
|
72
|
+
if (text.startsWith(watch.message))
|
|
73
|
+
text = `${hosted.message}${text.slice(watch.message.length)}`;
|
|
74
|
+
}
|
|
75
|
+
if (hasRepair) {
|
|
76
|
+
const hosted = hostedStackRepair(repair);
|
|
77
|
+
structured.repair = hosted;
|
|
78
|
+
text = text.replace(repairSummary(repair), hostedRepairSummary(repair));
|
|
79
|
+
}
|
|
80
|
+
return { ...result, content: [{ type: "text", text }, { type: "text", text: JSON.stringify(structured) }], structuredContent: structured };
|
|
81
|
+
}
|
|
21
82
|
function envelopeWatch(envelope) {
|
|
22
83
|
return envelope.watch ?? stackWatchObligation({
|
|
23
84
|
stackId: envelope.stackId, terminal: null, cursor: envelope.cursor, status: envelope.status,
|
|
24
85
|
});
|
|
25
86
|
}
|
|
26
|
-
function stackWaitSummary(envelope) {
|
|
27
|
-
return withWatchText(envelopeWatch(envelope), `${stackWaitStatusLine(envelope)}${repairSummary(envelope.repair)}`);
|
|
87
|
+
function stackWaitSummary(envelope, hosted = false) {
|
|
88
|
+
return withWatchText(envelopeWatch(envelope), `${stackWaitStatusLine(envelope, hosted)}${repairSummary(envelope.repair)}`);
|
|
28
89
|
}
|
|
29
|
-
function stackWaitStatusLine(envelope) {
|
|
90
|
+
function stackWaitStatusLine(envelope, hosted) {
|
|
30
91
|
const base = `Stack ${envelope.stackId} · ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy, envelope.landGatePending, envelope.actAfter ?? null, envelope.queueWait ?? null)}${envelope.assessment === "unavailable" ? " · assessment unavailable" : ""}`;
|
|
31
92
|
if (envelopeWatch(envelope).done || (envelope.status !== "waiting" && envelope.status !== "in_progress"))
|
|
32
93
|
return base;
|
|
@@ -36,7 +97,8 @@ function stackWaitStatusLine(envelope) {
|
|
|
36
97
|
...(envelope.cursor.afterFinishedAt !== undefined ? [`after_finished_at ${JSON.stringify(envelope.cursor.afterFinishedAt)}`] : []),
|
|
37
98
|
...(envelope.cursor.bounceId !== undefined ? [`bounce_id ${JSON.stringify(envelope.cursor.bounceId)}`] : []),
|
|
38
99
|
];
|
|
39
|
-
|
|
100
|
+
const background = hosted ? "" : `only with a confirmed host notification that resumes this task, run ${stackWatchBackgroundCommand(envelope.stackId, envelope.cursor)} as a background command and end your turn; otherwise `;
|
|
101
|
+
return `${base} · watch not finished: ${background}call stack_wait again with timeout_s 45 and the same cursor: ${selectors.join(", ")}`;
|
|
40
102
|
}
|
|
41
103
|
export async function stackWait(input, cfg, deps = {}) {
|
|
42
104
|
const args = z.object(stackWaitSchema).parse(input);
|
|
@@ -61,7 +123,7 @@ export async function stackWait(input, cfg, deps = {}) {
|
|
|
61
123
|
if (err instanceof StackWatchError) {
|
|
62
124
|
const envelope = { ...err.lastEnvelope, watch: envelopeWatch(err.lastEnvelope) };
|
|
63
125
|
return {
|
|
64
|
-
summary: stackWaitSummary(envelope),
|
|
126
|
+
summary: stackWaitSummary(envelope, deps.hosted),
|
|
65
127
|
data: {
|
|
66
128
|
...envelope,
|
|
67
129
|
...(err.retryAfterSeconds !== undefined ? { retry_after_seconds: err.retryAfterSeconds } : {}),
|
|
@@ -74,11 +136,11 @@ export async function stackWait(input, cfg, deps = {}) {
|
|
|
74
136
|
envelope = err.lastEnvelope;
|
|
75
137
|
}
|
|
76
138
|
const sealed = { ...envelope, watch: envelopeWatch(envelope) };
|
|
77
|
-
const
|
|
78
|
-
? await
|
|
139
|
+
const task = deps.chatId && !sealed.watch.done
|
|
140
|
+
? await trackInChat(config, deps.chatId, { kind: "stack", stackId: sealed.stackId }, deps.signal)
|
|
79
141
|
: null;
|
|
80
142
|
return {
|
|
81
|
-
summary:
|
|
82
|
-
data:
|
|
143
|
+
summary: task ? `${stackWaitSummary(sealed, deps.hosted)}\n\n${chatTaskNote(task)}` : stackWaitSummary(sealed, deps.hosted),
|
|
144
|
+
data: task ? { ...sealed, background_task: task } : sealed,
|
|
83
145
|
};
|
|
84
146
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mergestorm-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.28",
|
|
4
4
|
"description": "Mergestorm MCP server — identity, review, and read-only stack tools over stdio",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"dist",
|
|
26
|
+
"!dist/.tsbuildinfo",
|
|
27
|
+
"!dist/.build-inputs",
|
|
26
28
|
"README.md",
|
|
27
29
|
"LICENSE"
|
|
28
30
|
],
|
|
@@ -37,7 +39,7 @@
|
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
41
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
40
|
-
"mergestorm": "0.3.
|
|
42
|
+
"mergestorm": "0.3.64",
|
|
41
43
|
"zod": "^3.24.2"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { type Config } from "mergestorm/client";
|
|
2
|
-
export type ChatWakeTarget = {
|
|
3
|
-
kind: "vortex";
|
|
4
|
-
owner: string;
|
|
5
|
-
repo: string;
|
|
6
|
-
prNumber: number;
|
|
7
|
-
headSha: string | null;
|
|
8
|
-
minPass: number | null;
|
|
9
|
-
} | {
|
|
10
|
-
kind: "stack";
|
|
11
|
-
stackId: string;
|
|
12
|
-
};
|
|
13
|
-
export declare function breezeChatId(value: string | null | undefined): string | null;
|
|
14
|
-
export type ChatWake = {
|
|
15
|
-
id: string;
|
|
16
|
-
result: "waiting" | "existing" | "finished";
|
|
17
|
-
};
|
|
18
|
-
export declare const CHAT_WAKE_REVIEW_NOTE = "Keep waiting: call review_wait_pr again until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
|
|
19
|
-
export declare const REVIEW_START_WAIT_NOTE = "Wait for it now: call review_wait_pr for this pull request until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
|
|
20
|
-
export declare const CHAT_WAKE_STACK_NOTE = "If your reply ends before the stack is done, this chat is woken when the stack lands, is bounced out of the merge queue, or is archived.";
|
|
21
|
-
export declare function registerChatWake(cfg: Config, chatId: string, target: ChatWakeTarget, signal?: AbortSignal): Promise<ChatWake | null>;
|
|
@@ -1,26 +0,0 @@
|
|
|
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 const CHAT_WAKE_REVIEW_NOTE = "Keep waiting: call review_wait_pr again until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
|
|
8
|
-
export const REVIEW_START_WAIT_NOTE = "Wait for it now: call review_wait_pr for this pull request until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
|
|
9
|
-
export const CHAT_WAKE_STACK_NOTE = "If your reply ends before the stack is done, this chat is woken when the stack lands, is bounced out of the merge queue, or is archived.";
|
|
10
|
-
export async function registerChatWake(cfg, chatId, target, signal) {
|
|
11
|
-
const json = target.kind === "vortex"
|
|
12
|
-
? { chat_id: chatId, kind: "vortex", owner: target.owner, repo: target.repo, pr_number: target.prNumber, head_sha: target.headSha, min_pass: target.headSha ? target.minPass : null }
|
|
13
|
-
: { chat_id: chatId, kind: "stack", stack_id: target.stackId };
|
|
14
|
-
try {
|
|
15
|
-
const response = await apiFetch(cfg, "/api/v1/stacks/chat-waits", { method: "POST", json, signal, timeoutMs: 10_000 });
|
|
16
|
-
const body = response.body;
|
|
17
|
-
if (response.status !== 200 || !body || typeof body.id !== "string")
|
|
18
|
-
return null;
|
|
19
|
-
if (body.result !== "waiting" && body.result !== "existing" && body.result !== "finished")
|
|
20
|
-
return null;
|
|
21
|
-
return { id: body.id, result: body.result };
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|