@remixhq/mcp 0.1.2

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/dist/index.js ADDED
@@ -0,0 +1,1708 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/domain/apiClient.ts
8
+ import { createApiClient as createCoreApiClient, resolveConfig as resolveConfig2 } from "@remixhq/core";
9
+
10
+ // src/bootstrap/auth.ts
11
+ import { createLocalSessionStore, createStoredSessionTokenProvider, createSupabaseAuthHelpers, resolveConfig } from "@remixhq/core";
12
+ async function createRemixTokenProvider(config) {
13
+ const resolvedConfig = config ?? await resolveConfig();
14
+ const sessionStore = createLocalSessionStore();
15
+ return createStoredSessionTokenProvider({
16
+ config: resolvedConfig,
17
+ sessionStore,
18
+ refreshStoredSession: async ({ config: refreshConfig, session }) => {
19
+ const supabase = createSupabaseAuthHelpers(refreshConfig);
20
+ return supabase.refreshWithStoredSession({ session });
21
+ }
22
+ });
23
+ }
24
+
25
+ // src/domain/apiClient.ts
26
+ async function createCollabApiClient() {
27
+ const config = await resolveConfig2();
28
+ const tokenProvider = await createRemixTokenProvider(config);
29
+ const api = createCoreApiClient(config, {
30
+ tokenProvider
31
+ });
32
+ return api;
33
+ }
34
+ async function createApiClient() {
35
+ const config = await resolveConfig2();
36
+ const tokenProvider = await createRemixTokenProvider(config);
37
+ return createCoreApiClient(config, {
38
+ tokenProvider
39
+ });
40
+ }
41
+
42
+ // src/observability/logger.ts
43
+ function createLogger() {
44
+ return {
45
+ log(event) {
46
+ const payload = {
47
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
48
+ ...event
49
+ };
50
+ const line = JSON.stringify(payload);
51
+ if (event.level === "error") {
52
+ process.stderr.write(`${line}
53
+ `);
54
+ return;
55
+ }
56
+ process.stderr.write(`${line}
57
+ `);
58
+ }
59
+ };
60
+ }
61
+
62
+ // src/policy/policy.ts
63
+ import path from "path";
64
+
65
+ // src/errors/normalizeError.ts
66
+ import { ZodError } from "zod";
67
+
68
+ // src/errors/errorCodes.ts
69
+ var ERROR_CODES = {
70
+ AUTH_REQUIRED: "AUTH_REQUIRED",
71
+ INVALID_INPUT: "INVALID_INPUT",
72
+ NOT_GIT_REPO: "NOT_GIT_REPO",
73
+ NOT_BOUND: "NOT_BOUND",
74
+ DIRTY_WORKTREE: "DIRTY_WORKTREE",
75
+ DETACHED_HEAD: "DETACHED_HEAD",
76
+ PREFERRED_BRANCH_MISMATCH: "PREFERRED_BRANCH_MISMATCH",
77
+ MISSING_HEAD: "MISSING_HEAD",
78
+ REPO_LOCK_HELD: "REPO_LOCK_HELD",
79
+ REPO_LOCK_TIMEOUT: "REPO_LOCK_TIMEOUT",
80
+ REPO_LOCK_STALE_RECOVERED: "REPO_LOCK_STALE_RECOVERED",
81
+ REPO_STATE_CHANGED_DURING_OPERATION: "REPO_STATE_CHANGED_DURING_OPERATION",
82
+ REMOTE_ERROR: "REMOTE_ERROR",
83
+ METADATA_CONFLICT: "METADATA_CONFLICT",
84
+ DESTRUCTIVE_OPERATION_BLOCKED: "DESTRUCTIVE_OPERATION_BLOCKED",
85
+ CONFIG_INVALID: "CONFIG_INVALID",
86
+ INTERNAL_ERROR: "INTERNAL_ERROR"
87
+ };
88
+ var RemixMcpError = class extends Error {
89
+ normalized;
90
+ constructor(normalized) {
91
+ super(normalized.message);
92
+ this.name = "RemixMcpError";
93
+ this.normalized = normalized;
94
+ }
95
+ };
96
+
97
+ // src/errors/normalizeError.ts
98
+ function toStringOrNull(value) {
99
+ if (typeof value !== "string") return null;
100
+ const trimmed = value.trim();
101
+ return trimmed.length > 0 ? trimmed : null;
102
+ }
103
+ function makeNormalized(params) {
104
+ return {
105
+ code: params.code,
106
+ message: params.message,
107
+ hint: params.hint ?? null,
108
+ retryable: params.retryable ?? false,
109
+ category: params.category
110
+ };
111
+ }
112
+ function normalizeByMessage(err) {
113
+ const code = toStringOrNull(err.code);
114
+ const message = toStringOrNull(err.message) ?? "Unexpected error.";
115
+ const hint = toStringOrNull(err.hint);
116
+ if (code === ERROR_CODES.REPO_LOCK_HELD) {
117
+ return makeNormalized({
118
+ code: ERROR_CODES.REPO_LOCK_HELD,
119
+ message,
120
+ hint,
121
+ retryable: true,
122
+ category: "local_state"
123
+ });
124
+ }
125
+ if (code === ERROR_CODES.REPO_LOCK_TIMEOUT) {
126
+ return makeNormalized({
127
+ code: ERROR_CODES.REPO_LOCK_TIMEOUT,
128
+ message,
129
+ hint,
130
+ retryable: true,
131
+ category: "local_state"
132
+ });
133
+ }
134
+ if (code === ERROR_CODES.REPO_STATE_CHANGED_DURING_OPERATION) {
135
+ return makeNormalized({
136
+ code: ERROR_CODES.REPO_STATE_CHANGED_DURING_OPERATION,
137
+ message,
138
+ hint,
139
+ retryable: true,
140
+ category: "local_state"
141
+ });
142
+ }
143
+ if (code === ERROR_CODES.REPO_LOCK_STALE_RECOVERED) {
144
+ return makeNormalized({
145
+ code: ERROR_CODES.REPO_LOCK_STALE_RECOVERED,
146
+ message,
147
+ hint,
148
+ retryable: true,
149
+ category: "local_state"
150
+ });
151
+ }
152
+ if (message === "Not signed in.") {
153
+ return makeNormalized({
154
+ code: ERROR_CODES.AUTH_REQUIRED,
155
+ message,
156
+ hint,
157
+ category: "auth"
158
+ });
159
+ }
160
+ if (message === "Not inside a git repository.") {
161
+ return makeNormalized({
162
+ code: ERROR_CODES.NOT_GIT_REPO,
163
+ message,
164
+ hint,
165
+ category: "local_state"
166
+ });
167
+ }
168
+ if (message === "Repository is not bound to Remix.") {
169
+ return makeNormalized({
170
+ code: ERROR_CODES.NOT_BOUND,
171
+ message,
172
+ hint,
173
+ category: "local_state"
174
+ });
175
+ }
176
+ if (message.includes("Working tree must be clean")) {
177
+ return makeNormalized({
178
+ code: ERROR_CODES.DIRTY_WORKTREE,
179
+ message,
180
+ hint,
181
+ category: "local_state"
182
+ });
183
+ }
184
+ if (message.includes("requires a checked out local branch") || message.includes("detached HEAD")) {
185
+ return makeNormalized({
186
+ code: ERROR_CODES.DETACHED_HEAD,
187
+ message,
188
+ hint,
189
+ category: "local_state"
190
+ });
191
+ }
192
+ if (code === ERROR_CODES.PREFERRED_BRANCH_MISMATCH || message.includes("preferred branch")) {
193
+ return makeNormalized({
194
+ code: ERROR_CODES.PREFERRED_BRANCH_MISMATCH,
195
+ message,
196
+ hint,
197
+ category: "local_state"
198
+ });
199
+ }
200
+ if (message.includes("Failed to resolve local HEAD")) {
201
+ return makeNormalized({
202
+ code: ERROR_CODES.MISSING_HEAD,
203
+ message,
204
+ hint,
205
+ category: "local_state"
206
+ });
207
+ }
208
+ if (message.includes("metadata conflicts with the bound Remix app") || message.includes("manual intervention")) {
209
+ return makeNormalized({
210
+ code: ERROR_CODES.METADATA_CONFLICT,
211
+ message,
212
+ hint,
213
+ category: "remote_state"
214
+ });
215
+ }
216
+ if (message.includes("Timed out") || message.includes("failed") || message.includes("error state")) {
217
+ return makeNormalized({
218
+ code: ERROR_CODES.REMOTE_ERROR,
219
+ message,
220
+ hint,
221
+ retryable: true,
222
+ category: "remote_state"
223
+ });
224
+ }
225
+ return makeNormalized({
226
+ code: ERROR_CODES.INTERNAL_ERROR,
227
+ message,
228
+ hint,
229
+ retryable: true,
230
+ category: "internal"
231
+ });
232
+ }
233
+ function createPolicyError(message, hint) {
234
+ return new RemixMcpError(
235
+ makeNormalized({
236
+ code: ERROR_CODES.DESTRUCTIVE_OPERATION_BLOCKED,
237
+ message,
238
+ hint,
239
+ category: "policy"
240
+ })
241
+ );
242
+ }
243
+ function normalizeToolError(error) {
244
+ if (error instanceof RemixMcpError) {
245
+ return error.normalized;
246
+ }
247
+ if (error instanceof ZodError) {
248
+ return makeNormalized({
249
+ code: ERROR_CODES.INVALID_INPUT,
250
+ message: "Tool arguments failed validation.",
251
+ hint: error.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; "),
252
+ category: "validation"
253
+ });
254
+ }
255
+ if (error && typeof error === "object") {
256
+ return normalizeByMessage(error);
257
+ }
258
+ return makeNormalized({
259
+ code: ERROR_CODES.INTERNAL_ERROR,
260
+ message: typeof error === "string" && error.trim() ? error.trim() : "Unexpected error.",
261
+ hint: null,
262
+ retryable: true,
263
+ category: "internal"
264
+ });
265
+ }
266
+
267
+ // src/policy/policy.ts
268
+ function parseBooleanEnv(name, fallback) {
269
+ const raw = process.env[name];
270
+ if (!raw) return fallback;
271
+ const normalized = raw.trim().toLowerCase();
272
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
273
+ }
274
+ function parsePositiveIntEnv(name, fallback) {
275
+ const raw = process.env[name];
276
+ if (!raw) return fallback;
277
+ const parsed = Number.parseInt(raw, 10);
278
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
279
+ }
280
+ function parseAllowedRoots(raw) {
281
+ if (!raw) return null;
282
+ const roots = raw.split(path.delimiter).map((entry) => entry.trim()).filter(Boolean).map((entry) => path.resolve(entry));
283
+ return roots.length > 0 ? roots : null;
284
+ }
285
+ function isWithinRoot(root, candidate) {
286
+ const relative = path.relative(root, candidate);
287
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
288
+ }
289
+ function loadPolicy() {
290
+ return {
291
+ allowLocalWrite: parseBooleanEnv("COMERGE_MCP_ALLOW_LOCAL_WRITE", true),
292
+ allowRemoteWrite: parseBooleanEnv("COMERGE_MCP_ALLOW_REMOTE_WRITE", true),
293
+ allowedRepoRoots: parseAllowedRoots(process.env.COMERGE_MCP_ALLOWED_REPO_ROOTS),
294
+ maxDiffBytes: parsePositiveIntEnv("COMERGE_MCP_MAX_DIFF_BYTES", 1024 * 1024),
295
+ maxDiffOutputChars: parsePositiveIntEnv("COMERGE_MCP_MAX_DIFF_OUTPUT_CHARS", 2e4)
296
+ };
297
+ }
298
+ function resolvePolicyCwd(policy, cwd) {
299
+ const resolved = path.resolve(cwd?.trim() || process.cwd());
300
+ if (!policy.allowedRepoRoots) return resolved;
301
+ if (policy.allowedRepoRoots.some((root) => isWithinRoot(root, resolved))) return resolved;
302
+ throw createPolicyError("Requested working directory is outside the allowed repository roots.", resolved);
303
+ }
304
+ function assertToolAccess(policy, access) {
305
+ if (access === "read") return;
306
+ if (access === "remote_write" && !policy.allowRemoteWrite) {
307
+ throw createPolicyError("Remote-mutating Remix tools are disabled by policy.");
308
+ }
309
+ if (access === "local_write" && !policy.allowLocalWrite) {
310
+ throw createPolicyError("Local-mutating Remix tools are disabled by policy.");
311
+ }
312
+ }
313
+ function assertConfirm(confirm, operation) {
314
+ if (confirm) return;
315
+ throw createPolicyError(`${operation} requires explicit confirmation.`, "Pass confirm=true to run this tool.");
316
+ }
317
+ function assertDiffWithinLimit(policy, diff) {
318
+ const sizeBytes = Buffer.byteLength(diff, "utf8");
319
+ if (sizeBytes <= policy.maxDiffBytes) return;
320
+ throw createPolicyError(
321
+ "Diff exceeds the configured maximum size for Remix MCP.",
322
+ `Configured limit=${policy.maxDiffBytes} bytes actual=${sizeBytes} bytes.`
323
+ );
324
+ }
325
+
326
+ // src/bootstrap/context.ts
327
+ function createServerContext(params) {
328
+ return {
329
+ serverName: "remix-mcp",
330
+ version: params.version,
331
+ policy: loadPolicy(),
332
+ logger: createLogger(),
333
+ getCollabApiClient: () => createCollabApiClient(),
334
+ agentMetadata: {
335
+ type: process.env.COMERGE_AGENT_TYPE?.trim() || "agent",
336
+ name: process.env.COMERGE_AGENT_NAME?.trim() || "remix-mcp",
337
+ version: process.env.COMERGE_AGENT_VERSION?.trim() || params.version,
338
+ provider: process.env.COMERGE_AGENT_PROVIDER?.trim() || "remix"
339
+ }
340
+ };
341
+ }
342
+
343
+ // src/tools/collab/register.ts
344
+ import { z as z3 } from "zod";
345
+
346
+ // src/contracts/collab.ts
347
+ import { z as z2 } from "zod";
348
+
349
+ // src/contracts/common.ts
350
+ import { z } from "zod";
351
+ var SCHEMA_VERSION = 1;
352
+ var commonRequestFieldsSchema = {
353
+ cwd: z.string().trim().min(1).optional(),
354
+ requestId: z.string().trim().min(1).optional(),
355
+ outputMode: z.enum(["summary", "full"]).optional()
356
+ };
357
+ var errorEnvelopeSchema = z.object({
358
+ code: z.custom(),
359
+ message: z.string().min(1),
360
+ hint: z.string().nullable(),
361
+ retryable: z.boolean(),
362
+ category: z.custom()
363
+ });
364
+ function makeSuccessSchema(dataSchema) {
365
+ return z.object({
366
+ schemaVersion: z.literal(SCHEMA_VERSION),
367
+ ok: z.literal(true),
368
+ tool: z.string().min(1),
369
+ requestId: z.string().nullable(),
370
+ data: dataSchema,
371
+ warnings: z.array(z.string()),
372
+ risks: z.array(z.string()),
373
+ recommendedNextActions: z.array(z.string())
374
+ });
375
+ }
376
+ function makeErrorSchema() {
377
+ return z.object({
378
+ schemaVersion: z.literal(SCHEMA_VERSION),
379
+ ok: z.literal(false),
380
+ tool: z.string().min(1),
381
+ requestId: z.string().nullable(),
382
+ error: errorEnvelopeSchema,
383
+ warnings: z.array(z.string()),
384
+ risks: z.array(z.string()),
385
+ recommendedNextActions: z.array(z.string())
386
+ });
387
+ }
388
+ function toJsonText(value) {
389
+ return JSON.stringify(value, null, 2);
390
+ }
391
+ function makeSuccessResult(envelope) {
392
+ return {
393
+ content: [{ type: "text", text: toJsonText(envelope) }],
394
+ structuredContent: envelope
395
+ };
396
+ }
397
+ function makeErrorResult(envelope) {
398
+ return {
399
+ content: [{ type: "text", text: toJsonText(envelope) }],
400
+ structuredContent: envelope,
401
+ isError: true
402
+ };
403
+ }
404
+
405
+ // src/contracts/collab.ts
406
+ var genericRecordSchema = z2.record(z2.string(), z2.unknown());
407
+ var genericArraySchema = z2.array(genericRecordSchema);
408
+ var statusInputSchema = {
409
+ ...commonRequestFieldsSchema,
410
+ includeRemote: z2.boolean().optional()
411
+ };
412
+ var initInputSchema = {
413
+ ...commonRequestFieldsSchema,
414
+ appName: z2.string().trim().min(1).optional(),
415
+ forceNew: z2.boolean().optional()
416
+ };
417
+ var listInputSchema = {
418
+ requestId: z2.string().trim().min(1).optional(),
419
+ outputMode: z2.enum(["summary", "full"]).optional(),
420
+ forked: z2.enum(["only", "exclude", "all"]).optional()
421
+ };
422
+ var remixInputSchema = {
423
+ ...commonRequestFieldsSchema,
424
+ appId: z2.string().trim().min(1),
425
+ name: z2.string().trim().min(1).optional(),
426
+ outputDir: z2.string().trim().min(1).optional()
427
+ };
428
+ var addInputSchema = {
429
+ ...commonRequestFieldsSchema,
430
+ prompt: z2.string().trim().min(1),
431
+ assistantResponse: z2.string().trim().min(1).optional(),
432
+ diffSource: z2.enum(["worktree", "external"]).optional(),
433
+ externalDiff: z2.string().optional(),
434
+ allowBranchMismatch: z2.boolean().optional(),
435
+ idempotencyKey: z2.string().trim().min(1).optional()
436
+ };
437
+ var recordTurnInputSchema = {
438
+ ...commonRequestFieldsSchema,
439
+ prompt: z2.string().trim().min(1),
440
+ assistantResponse: z2.string().trim().min(1),
441
+ allowBranchMismatch: z2.boolean().optional(),
442
+ idempotencyKey: z2.string().trim().min(1).optional()
443
+ };
444
+ var previewInputSchema = {
445
+ ...commonRequestFieldsSchema
446
+ };
447
+ var applyInputSchema = {
448
+ ...commonRequestFieldsSchema,
449
+ confirm: z2.boolean(),
450
+ allowBranchMismatch: z2.boolean().optional()
451
+ };
452
+ var requestMergeInputSchema = {
453
+ ...commonRequestFieldsSchema
454
+ };
455
+ var inboxInputSchema = {
456
+ requestId: z2.string().trim().min(1).optional(),
457
+ outputMode: z2.enum(["summary", "full"]).optional(),
458
+ status: z2.string().trim().min(1).optional()
459
+ };
460
+ var viewMergeRequestInputSchema = {
461
+ requestId: z2.string().trim().min(1).optional(),
462
+ outputMode: z2.enum(["summary", "full"]).optional(),
463
+ mrId: z2.string().trim().min(1),
464
+ includeUnifiedDiff: z2.boolean().optional(),
465
+ diffMaxChars: z2.number().int().positive().max(2e5).optional()
466
+ };
467
+ var approveInputSchema = {
468
+ ...commonRequestFieldsSchema,
469
+ mrId: z2.string().trim().min(1),
470
+ confirm: z2.boolean(),
471
+ allowBranchMismatch: z2.boolean().optional()
472
+ };
473
+ var rejectInputSchema = {
474
+ requestId: z2.string().trim().min(1).optional(),
475
+ mrId: z2.string().trim().min(1),
476
+ confirm: z2.boolean()
477
+ };
478
+ var inviteInputSchema = {
479
+ ...commonRequestFieldsSchema,
480
+ email: z2.string().email(),
481
+ scope: z2.enum(["organization", "project", "app"]).optional(),
482
+ targetId: z2.string().trim().min(1).optional(),
483
+ role: z2.string().trim().min(1).optional(),
484
+ ttlDays: z2.number().int().positive().max(30).optional()
485
+ };
486
+ var statusDataSchema = z2.object({
487
+ status: genericRecordSchema,
488
+ riskLevel: z2.enum(["low", "medium", "high"])
489
+ });
490
+ var initDataSchema = z2.object({
491
+ reused: z2.boolean(),
492
+ projectId: z2.string(),
493
+ appId: z2.string(),
494
+ upstreamAppId: z2.string(),
495
+ bindingPath: z2.string(),
496
+ repoRoot: z2.string()
497
+ });
498
+ var listDataSchema = z2.object({
499
+ apps: genericArraySchema
500
+ });
501
+ var remixDataSchema = z2.object({
502
+ appId: z2.string(),
503
+ projectId: z2.string(),
504
+ upstreamAppId: z2.string(),
505
+ bindingPath: z2.string(),
506
+ repoRoot: z2.string()
507
+ });
508
+ var addDataSchema = z2.object({
509
+ changeStep: genericRecordSchema,
510
+ autoSync: genericRecordSchema
511
+ });
512
+ var recordTurnDataSchema = genericRecordSchema;
513
+ var syncDataSchema = genericRecordSchema;
514
+ var requestMergeDataSchema = genericRecordSchema;
515
+ var inboxDataSchema = z2.object({
516
+ mergeRequests: z2.array(genericRecordSchema)
517
+ });
518
+ var viewMergeRequestDataSchema = genericRecordSchema;
519
+ var approveDataSchema = genericRecordSchema;
520
+ var rejectDataSchema = genericRecordSchema;
521
+ var syncUpstreamDataSchema = genericRecordSchema;
522
+ var reconcileDataSchema = genericRecordSchema;
523
+ var inviteDataSchema = genericRecordSchema;
524
+ var statusSuccessSchema = makeSuccessSchema(statusDataSchema);
525
+ var initSuccessSchema = makeSuccessSchema(initDataSchema);
526
+ var listSuccessSchema = makeSuccessSchema(listDataSchema);
527
+ var remixSuccessSchema = makeSuccessSchema(remixDataSchema);
528
+ var addSuccessSchema = makeSuccessSchema(addDataSchema);
529
+ var recordTurnSuccessSchema = makeSuccessSchema(recordTurnDataSchema);
530
+ var syncSuccessSchema = makeSuccessSchema(syncDataSchema);
531
+ var requestMergeSuccessSchema = makeSuccessSchema(requestMergeDataSchema);
532
+ var inboxSuccessSchema = makeSuccessSchema(inboxDataSchema);
533
+ var viewMergeRequestSuccessSchema = makeSuccessSchema(viewMergeRequestDataSchema);
534
+ var approveSuccessSchema = makeSuccessSchema(approveDataSchema);
535
+ var rejectSuccessSchema = makeSuccessSchema(rejectDataSchema);
536
+ var syncUpstreamSuccessSchema = makeSuccessSchema(syncUpstreamDataSchema);
537
+ var reconcileSuccessSchema = makeSuccessSchema(reconcileDataSchema);
538
+ var inviteSuccessSchema = makeSuccessSchema(inviteDataSchema);
539
+
540
+ // src/domain/coreAdapter.ts
541
+ import {
542
+ collabAdd as coreCollabAdd,
543
+ collabRecordTurn as coreCollabRecordTurn,
544
+ collabApprove as coreCollabApprove,
545
+ collabInbox as coreCollabInbox,
546
+ collabInit as coreCollabInit,
547
+ collabInvite as coreCollabInvite,
548
+ collabReconcile as coreCollabReconcile,
549
+ collabReject as coreCollabReject,
550
+ collabRemix as coreCollabRemix,
551
+ collabRequestMerge as coreCollabRequestMerge,
552
+ collabStatus as coreCollabStatus,
553
+ collabSync as coreCollabSync,
554
+ collabSyncUpstream as coreCollabSyncUpstream,
555
+ collabView as coreCollabView
556
+ } from "@remixhq/core/collab";
557
+ import { findGitRoot, getHeadCommitHash, listUntrackedFiles } from "@remixhq/core/repo";
558
+ function unwrapResponseObject(resp, label) {
559
+ const obj = resp?.responseObject;
560
+ if (obj === void 0 || obj === null) {
561
+ throw new Error(typeof resp?.message === "string" && resp.message.trim() ? resp.message : `Missing ${label} response`);
562
+ }
563
+ return obj;
564
+ }
565
+ function normalizeMergeRequestsPayload(payload) {
566
+ if (Array.isArray(payload)) return payload;
567
+ if (!payload || typeof payload !== "object") return [];
568
+ return Object.values(payload).flatMap(
569
+ (value) => Array.isArray(value) ? value : []
570
+ );
571
+ }
572
+ function getRiskLevel(status) {
573
+ if (status.recommendedAction === "reconcile") return "high";
574
+ if (status.recommendedAction === "sync" || status.remote.incomingOpenMergeRequestCount) return "medium";
575
+ if (status.repo.branchMismatch || !status.repo.isGitRepo || !status.binding.isBound || !status.repo.worktree.isClean) return "medium";
576
+ return "low";
577
+ }
578
+ function getRecommendedNextActions(status) {
579
+ if (status.repo.branchMismatch) {
580
+ return [
581
+ `Switch to the preferred branch (${status.binding.preferredBranch ?? "configured in the binding"}) before mutating Remix state, or rerun with allowBranchMismatch=true if intentional.`
582
+ ];
583
+ }
584
+ switch (status.recommendedAction) {
585
+ case "init":
586
+ return ["Run remix_collab_init to bind the repository to Remix."];
587
+ case "sync":
588
+ return ["Run remix_collab_sync_preview, then remix_collab_sync_apply if the preview is acceptable."];
589
+ case "reconcile":
590
+ return ["Run remix_collab_reconcile_preview before attempting remix_collab_reconcile_apply."];
591
+ case "review_inbox":
592
+ return ["Run remix_collab_inbox to inspect open merge requests."];
593
+ default:
594
+ return [];
595
+ }
596
+ }
597
+ function collectWarnings(value) {
598
+ if (!value || !Array.isArray(value)) return [];
599
+ return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
600
+ }
601
+ function collectResultWarnings(value) {
602
+ return collectWarnings(value.warnings);
603
+ }
604
+ function truncateText(value, maxChars) {
605
+ if (value.length <= maxChars) {
606
+ return {
607
+ text: value,
608
+ truncated: false,
609
+ originalChars: value.length
610
+ };
611
+ }
612
+ return {
613
+ text: `${value.slice(0, maxChars)}
614
+
615
+ [truncated ${value.length - maxChars} chars]`,
616
+ truncated: true,
617
+ originalChars: value.length
618
+ };
619
+ }
620
+ async function getStatus(params) {
621
+ const api = params.includeRemote ? await createCollabApiClient() : null;
622
+ const status = await coreCollabStatus({
623
+ api,
624
+ cwd: params.cwd
625
+ });
626
+ return {
627
+ data: {
628
+ status,
629
+ riskLevel: getRiskLevel(status)
630
+ },
631
+ warnings: status.warnings,
632
+ recommendedNextActions: getRecommendedNextActions(status),
633
+ logContext: {
634
+ repoRoot: status.repo.repoRoot,
635
+ appId: status.binding.currentAppId
636
+ }
637
+ };
638
+ }
639
+ async function initCollab(params) {
640
+ const api = await createCollabApiClient();
641
+ const result = await coreCollabInit({
642
+ api,
643
+ cwd: params.cwd,
644
+ appName: params.appName ?? null,
645
+ forceNew: params.forceNew ?? false
646
+ });
647
+ return {
648
+ data: result,
649
+ warnings: collectResultWarnings(result),
650
+ recommendedNextActions: ["Run remix_collab_status to inspect sync and merge readiness."],
651
+ logContext: {
652
+ repoRoot: result.repoRoot,
653
+ appId: result.appId
654
+ }
655
+ };
656
+ }
657
+ async function listApps(params) {
658
+ const api = await createApiClient();
659
+ const resp = await api.listApps({ forked: params.forked ?? "all" });
660
+ const apps = unwrapResponseObject(resp, "apps");
661
+ return {
662
+ data: { apps },
663
+ warnings: [],
664
+ recommendedNextActions: [],
665
+ logContext: {}
666
+ };
667
+ }
668
+ async function remixCollab(params) {
669
+ const api = await createCollabApiClient();
670
+ const result = await coreCollabRemix({
671
+ api,
672
+ cwd: params.cwd,
673
+ appId: params.appId,
674
+ name: params.name ?? null,
675
+ outputDir: params.outputDir ?? null
676
+ });
677
+ return {
678
+ data: result,
679
+ warnings: collectResultWarnings(result),
680
+ recommendedNextActions: ["Run remix_collab_status inside the remix checkout to inspect its state."],
681
+ logContext: {
682
+ repoRoot: result.repoRoot,
683
+ appId: result.appId
684
+ }
685
+ };
686
+ }
687
+ async function addCollabStep(params) {
688
+ const api = await createCollabApiClient();
689
+ const repoRoot = await findGitRoot(params.cwd);
690
+ const preHead = await getHeadCommitHash(repoRoot);
691
+ const untrackedBefore = params.diffSource === "worktree" ? await listUntrackedFiles(repoRoot) : [];
692
+ const changeStep = await coreCollabAdd({
693
+ api,
694
+ cwd: params.cwd,
695
+ prompt: params.prompt,
696
+ assistantResponse: params.assistantResponse ?? null,
697
+ diff: params.externalDiff ?? null,
698
+ diffSource: params.diffSource,
699
+ allowBranchMismatch: params.allowBranchMismatch ?? false,
700
+ idempotencyKey: params.idempotencyKey ?? null,
701
+ actor: params.agent
702
+ });
703
+ const postHead = await getHeadCommitHash(repoRoot);
704
+ const autoSyncEligible = params.diffSource === "worktree";
705
+ const localRepoMutated = autoSyncEligible && preHead !== postHead;
706
+ return {
707
+ data: {
708
+ changeStep,
709
+ autoSync: {
710
+ requested: true,
711
+ eligible: autoSyncEligible,
712
+ attempted: autoSyncEligible,
713
+ applied: autoSyncEligible,
714
+ trackedChangesDiscarded: autoSyncEligible,
715
+ capturedUntrackedPathsCandidate: untrackedBefore,
716
+ localHeadBefore: preHead,
717
+ localHeadAfter: postHead,
718
+ localRepoMutated
719
+ }
720
+ },
721
+ warnings: [
722
+ ...collectResultWarnings(changeStep),
723
+ ...params.diffSource === "external" ? [
724
+ "Automatic local discard+sync was skipped because the diff came from an external source and may not match the current worktree."
725
+ ] : []
726
+ ],
727
+ recommendedNextActions: [],
728
+ logContext: {
729
+ repoRoot
730
+ }
731
+ };
732
+ }
733
+ async function recordCollabTurn(params) {
734
+ const api = await createCollabApiClient();
735
+ const result = await coreCollabRecordTurn({
736
+ api,
737
+ cwd: params.cwd,
738
+ prompt: params.prompt,
739
+ assistantResponse: params.assistantResponse,
740
+ allowBranchMismatch: params.allowBranchMismatch ?? false,
741
+ idempotencyKey: params.idempotencyKey ?? null,
742
+ actor: params.agent
743
+ });
744
+ return {
745
+ data: result,
746
+ warnings: [],
747
+ recommendedNextActions: [],
748
+ logContext: {
749
+ repoRoot: params.cwd,
750
+ appId: result.appId
751
+ }
752
+ };
753
+ }
754
+ async function syncCollab(params) {
755
+ const api = await createCollabApiClient();
756
+ const result = await coreCollabSync({
757
+ api,
758
+ cwd: params.cwd,
759
+ dryRun: params.dryRun,
760
+ allowBranchMismatch: params.allowBranchMismatch ?? false
761
+ });
762
+ return {
763
+ data: result,
764
+ warnings: collectResultWarnings(result),
765
+ recommendedNextActions: params.dryRun ? ["Run remix_collab_sync_apply with confirm=true to apply this fast-forward update."] : [],
766
+ logContext: {
767
+ repoRoot: result.repoRoot
768
+ }
769
+ };
770
+ }
771
+ async function requestMerge(params) {
772
+ const api = await createCollabApiClient();
773
+ const result = await coreCollabRequestMerge({
774
+ api,
775
+ cwd: params.cwd
776
+ });
777
+ return {
778
+ data: result,
779
+ warnings: [],
780
+ recommendedNextActions: result.id ? [`Run remix_collab_view_merge_request with mrId=${String(result.id)} to inspect the request.`] : [],
781
+ logContext: {
782
+ mrId: typeof result.id === "string" ? result.id : null
783
+ }
784
+ };
785
+ }
786
+ async function inbox(params) {
787
+ const api = await createCollabApiClient();
788
+ const result = await coreCollabInbox({ api });
789
+ const mergeRequests = normalizeMergeRequestsPayload(result.mergeRequests).filter(
790
+ (mr) => params.status ? String(mr.status ?? "") === params.status : true
791
+ );
792
+ return {
793
+ data: { mergeRequests },
794
+ warnings: [],
795
+ recommendedNextActions: [],
796
+ logContext: {}
797
+ };
798
+ }
799
+ async function viewMergeRequest(params) {
800
+ const api = await createCollabApiClient();
801
+ const review = await coreCollabView({
802
+ api,
803
+ mrId: params.mrId
804
+ });
805
+ const truncatedDiff = params.includeUnifiedDiff ? truncateText(review.unifiedDiff, params.diffMaxChars) : null;
806
+ return {
807
+ data: {
808
+ mergeRequest: review.mergeRequest,
809
+ prompts: review.prompts,
810
+ changeSteps: review.changeSteps,
811
+ stats: review.stats,
812
+ unifiedDiff: truncatedDiff?.text ?? null,
813
+ diffTruncated: truncatedDiff?.truncated ?? false,
814
+ originalUnifiedDiffChars: truncatedDiff?.originalChars ?? review.unifiedDiff.length
815
+ },
816
+ warnings: truncatedDiff?.truncated ? ["Unified diff output was truncated to respect the configured output limit."] : [],
817
+ recommendedNextActions: [],
818
+ logContext: {
819
+ mrId: params.mrId,
820
+ appId: review.mergeRequest.targetAppId
821
+ }
822
+ };
823
+ }
824
+ async function approveMergeRequest(params) {
825
+ const api = await createCollabApiClient();
826
+ const result = await coreCollabApprove({
827
+ api,
828
+ mrId: params.mrId,
829
+ cwd: params.cwd,
830
+ mode: params.mode,
831
+ allowBranchMismatch: params.allowBranchMismatch ?? false
832
+ });
833
+ return {
834
+ data: result,
835
+ warnings: collectResultWarnings(result),
836
+ recommendedNextActions: [],
837
+ logContext: {
838
+ repoRoot: result.repoRoot ?? null,
839
+ appId: result.targetAppId,
840
+ mrId: result.mergeRequestId
841
+ }
842
+ };
843
+ }
844
+ async function rejectMergeRequest(params) {
845
+ const api = await createCollabApiClient();
846
+ const result = await coreCollabReject({
847
+ api,
848
+ mrId: params.mrId
849
+ });
850
+ return {
851
+ data: result,
852
+ warnings: [],
853
+ recommendedNextActions: [],
854
+ logContext: {
855
+ mrId: params.mrId
856
+ }
857
+ };
858
+ }
859
+ async function syncUpstream(params) {
860
+ const api = await createCollabApiClient();
861
+ const result = await coreCollabSyncUpstream({
862
+ api,
863
+ cwd: params.cwd
864
+ });
865
+ return {
866
+ data: result,
867
+ warnings: collectResultWarnings(result),
868
+ recommendedNextActions: [],
869
+ logContext: {
870
+ repoRoot: result.repoRoot,
871
+ appId: result.appId,
872
+ mrId: "mergeRequestId" in result ? result.mergeRequestId ?? null : null
873
+ }
874
+ };
875
+ }
876
+ async function reconcile(params) {
877
+ const api = await createCollabApiClient();
878
+ const result = await coreCollabReconcile({
879
+ api,
880
+ cwd: params.cwd,
881
+ dryRun: params.dryRun,
882
+ allowBranchMismatch: params.allowBranchMismatch ?? false
883
+ });
884
+ return {
885
+ data: result,
886
+ warnings: collectWarnings(result.warnings),
887
+ recommendedNextActions: params.dryRun ? ["Run remix_collab_reconcile_apply with confirm=true only if the preview is acceptable."] : [],
888
+ risks: params.dryRun ? ["Reconcile apply rewrites local history and creates a backup branch."] : [],
889
+ logContext: {
890
+ repoRoot: result.repoRoot ?? null
891
+ }
892
+ };
893
+ }
894
+ async function inviteCollaborator(params) {
895
+ const api = await createCollabApiClient();
896
+ const result = await coreCollabInvite({
897
+ api,
898
+ cwd: params.cwd,
899
+ email: params.email,
900
+ role: params.role ?? null,
901
+ scope: params.scope ?? "project",
902
+ targetId: params.targetId ?? null,
903
+ ttlDays: params.ttlDays
904
+ });
905
+ return {
906
+ data: result,
907
+ warnings: [],
908
+ recommendedNextActions: [],
909
+ logContext: {}
910
+ };
911
+ }
912
+
913
+ // src/tools/collab/register.ts
914
+ function getAnnotations(access) {
915
+ if (access === "read") {
916
+ return {
917
+ readOnlyHint: true,
918
+ idempotentHint: true,
919
+ openWorldHint: false
920
+ };
921
+ }
922
+ return {
923
+ readOnlyHint: false,
924
+ destructiveHint: access === "local_write",
925
+ idempotentHint: false,
926
+ openWorldHint: false
927
+ };
928
+ }
929
+ function buildSuccessEnvelope(tool, requestId, result) {
930
+ return {
931
+ schemaVersion: SCHEMA_VERSION,
932
+ ok: true,
933
+ tool,
934
+ requestId: requestId ?? null,
935
+ data: result.data,
936
+ warnings: result.warnings ?? [],
937
+ risks: result.risks ?? [],
938
+ recommendedNextActions: result.recommendedNextActions ?? []
939
+ };
940
+ }
941
+ function deriveErrorRisks(tool, normalized) {
942
+ if (tool === "remix_collab_add" && normalized.message === "Change step succeeded remotely, but automatic local sync failed.") {
943
+ return ["The change step succeeded remotely, but the local repository may need manual recovery or a follow-up sync."];
944
+ }
945
+ if (normalized.code === "DESTRUCTIVE_OPERATION_BLOCKED") {
946
+ return ["A policy guard blocked a potentially destructive or state-mutating operation."];
947
+ }
948
+ if (normalized.code === "REPO_LOCK_TIMEOUT") {
949
+ return ["Another Remix mutation was already in progress for this repository, so the local mutation did not run."];
950
+ }
951
+ if (normalized.code === "REPO_STATE_CHANGED_DURING_OPERATION") {
952
+ return ["The repository changed during the operation, so Remix aborted before applying a destructive local mutation."];
953
+ }
954
+ return [];
955
+ }
956
+ function buildErrorEnvelope(tool, requestId, error) {
957
+ const normalized = normalizeToolError(error);
958
+ const recommendedNextActions = normalized.code === "AUTH_REQUIRED" ? ["Set COMERGE_ACCESS_TOKEN, then retry the tool call."] : normalized.code === "REPO_LOCK_TIMEOUT" ? ["Wait for the active Remix mutation to finish, then retry the tool call."] : normalized.code === "REPO_STATE_CHANGED_DURING_OPERATION" ? ["Review local repository changes, then rerun the tool once the worktree is stable."] : normalized.code === "PREFERRED_BRANCH_MISMATCH" ? ["Switch to the repository's preferred Remix branch, or rerun with allowBranchMismatch=true if intentional."] : [];
959
+ return {
960
+ schemaVersion: SCHEMA_VERSION,
961
+ ok: false,
962
+ tool,
963
+ requestId: requestId ?? null,
964
+ error: normalized,
965
+ warnings: [],
966
+ risks: deriveErrorRisks(tool, normalized),
967
+ recommendedNextActions
968
+ };
969
+ }
970
+ function registerTool(server, context, params) {
971
+ const errorSchema = makeErrorSchema();
972
+ server.registerTool(
973
+ params.name,
974
+ {
975
+ title: params.name,
976
+ description: params.description,
977
+ inputSchema: params.inputSchema,
978
+ outputSchema: params.outputSchema,
979
+ annotations: getAnnotations(params.access)
980
+ },
981
+ async (rawArgs) => {
982
+ const requestId = typeof rawArgs.requestId === "string" ? rawArgs.requestId : void 0;
983
+ const startedAt = Date.now();
984
+ try {
985
+ assertToolAccess(context.policy, params.access);
986
+ const result = await params.run(rawArgs);
987
+ const envelope = buildSuccessEnvelope(params.name, requestId, result);
988
+ params.outputSchema.parse(envelope);
989
+ context.logger.log({
990
+ level: "info",
991
+ message: "tool_completed",
992
+ tool: params.name,
993
+ requestId: envelope.requestId,
994
+ durationMs: Date.now() - startedAt,
995
+ result: "success",
996
+ repoRoot: result.logContext?.repoRoot ?? null,
997
+ appId: result.logContext?.appId ?? null,
998
+ mrId: result.logContext?.mrId ?? null,
999
+ truncated: result.warnings?.some((warning) => warning.toLowerCase().includes("truncated")) ?? false
1000
+ });
1001
+ return makeSuccessResult(envelope);
1002
+ } catch (error) {
1003
+ const envelope = buildErrorEnvelope(params.name, requestId, error);
1004
+ errorSchema.parse(envelope);
1005
+ context.logger.log({
1006
+ level: "error",
1007
+ message: "tool_failed",
1008
+ tool: params.name,
1009
+ requestId: envelope.requestId,
1010
+ durationMs: Date.now() - startedAt,
1011
+ result: "error",
1012
+ errorCode: envelope.error.code
1013
+ });
1014
+ return makeErrorResult(envelope);
1015
+ }
1016
+ }
1017
+ );
1018
+ }
1019
+ function registerCollabTools(server, context) {
1020
+ registerTool(server, context, {
1021
+ name: "remix_collab_status",
1022
+ description: "Summarize repository binding, worktree state, merge request counts, and sync or reconcile readiness.",
1023
+ access: "read",
1024
+ inputSchema: statusInputSchema,
1025
+ outputSchema: statusSuccessSchema,
1026
+ run: async (args) => {
1027
+ const input = z3.object(statusInputSchema).parse(args);
1028
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1029
+ return getStatus({
1030
+ cwd,
1031
+ includeRemote: input.includeRemote ?? true
1032
+ });
1033
+ }
1034
+ });
1035
+ registerTool(server, context, {
1036
+ name: "remix_collab_init",
1037
+ description: "Import the current repository into Remix and write the local binding file.",
1038
+ access: "remote_write",
1039
+ inputSchema: initInputSchema,
1040
+ outputSchema: initSuccessSchema,
1041
+ run: async (args) => {
1042
+ const input = z3.object(initInputSchema).parse(args);
1043
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1044
+ return initCollab({
1045
+ cwd,
1046
+ appName: input.appName,
1047
+ forceNew: input.forceNew
1048
+ });
1049
+ }
1050
+ });
1051
+ registerTool(server, context, {
1052
+ name: "remix_collab_list",
1053
+ description: "List Remix apps visible to the current authenticated user.",
1054
+ access: "read",
1055
+ inputSchema: listInputSchema,
1056
+ outputSchema: listSuccessSchema,
1057
+ run: async (args) => {
1058
+ const input = z3.object(listInputSchema).parse(args);
1059
+ return listApps({ forked: input.forked });
1060
+ }
1061
+ });
1062
+ registerTool(server, context, {
1063
+ name: "remix_collab_remix",
1064
+ description: "Fork a Remix app and materialize a new local checkout bound to the remix. Prefer `outputDir` for an exact destination outside any existing git repo; `cwd` is only the parent-directory fallback.",
1065
+ access: "remote_write",
1066
+ inputSchema: remixInputSchema,
1067
+ outputSchema: remixSuccessSchema,
1068
+ run: async (args) => {
1069
+ const input = z3.object(remixInputSchema).parse(args);
1070
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1071
+ return remixCollab({
1072
+ cwd,
1073
+ appId: input.appId,
1074
+ name: input.name,
1075
+ outputDir: input.outputDir
1076
+ });
1077
+ }
1078
+ });
1079
+ registerTool(server, context, {
1080
+ name: "remix_collab_add",
1081
+ description: "Record one collaboration change step for the current bound repository, using the live worktree by default.",
1082
+ access: "local_write",
1083
+ inputSchema: addInputSchema,
1084
+ outputSchema: addSuccessSchema,
1085
+ run: async (args) => {
1086
+ const input = z3.object(addInputSchema).parse(args);
1087
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1088
+ const diffSource = input.diffSource ?? "worktree";
1089
+ if (diffSource === "external") {
1090
+ const externalDiff = input.externalDiff ?? "";
1091
+ assertDiffWithinLimit(context.policy, externalDiff);
1092
+ }
1093
+ return addCollabStep({
1094
+ cwd,
1095
+ prompt: input.prompt,
1096
+ assistantResponse: input.assistantResponse,
1097
+ diffSource,
1098
+ externalDiff: input.externalDiff,
1099
+ allowBranchMismatch: input.allowBranchMismatch ?? false,
1100
+ idempotencyKey: input.idempotencyKey,
1101
+ agent: context.agentMetadata
1102
+ });
1103
+ }
1104
+ });
1105
+ registerTool(server, context, {
1106
+ name: "remix_collab_record_turn",
1107
+ description: "Record one no-diff collaboration turn for the current bound repository after a completed assistant response.",
1108
+ access: "remote_write",
1109
+ inputSchema: recordTurnInputSchema,
1110
+ outputSchema: recordTurnSuccessSchema,
1111
+ run: async (args) => {
1112
+ const input = z3.object(recordTurnInputSchema).parse(args);
1113
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1114
+ return recordCollabTurn({
1115
+ cwd,
1116
+ prompt: input.prompt,
1117
+ assistantResponse: input.assistantResponse,
1118
+ allowBranchMismatch: input.allowBranchMismatch ?? false,
1119
+ idempotencyKey: input.idempotencyKey,
1120
+ agent: context.agentMetadata
1121
+ });
1122
+ }
1123
+ });
1124
+ registerTool(server, context, {
1125
+ name: "remix_collab_sync_preview",
1126
+ description: "Preview whether the current bound repository can be fast-forward synced to the Remix app state.",
1127
+ access: "read",
1128
+ inputSchema: previewInputSchema,
1129
+ outputSchema: syncSuccessSchema,
1130
+ run: async (args) => {
1131
+ const input = z3.object(previewInputSchema).parse(args);
1132
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1133
+ return syncCollab({ cwd, dryRun: true });
1134
+ }
1135
+ });
1136
+ registerTool(server, context, {
1137
+ name: "remix_collab_sync_apply",
1138
+ description: "Fast-forward sync the current bound repository to the Remix app state.",
1139
+ access: "local_write",
1140
+ inputSchema: applyInputSchema,
1141
+ outputSchema: syncSuccessSchema,
1142
+ run: async (args) => {
1143
+ const input = z3.object(applyInputSchema).parse(args);
1144
+ assertConfirm(input.confirm, "remix_collab_sync_apply");
1145
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1146
+ return syncCollab({ cwd, dryRun: false, allowBranchMismatch: input.allowBranchMismatch ?? false });
1147
+ }
1148
+ });
1149
+ registerTool(server, context, {
1150
+ name: "remix_collab_request_merge",
1151
+ description: "Open a merge request from the current bound repository to its upstream app.",
1152
+ access: "remote_write",
1153
+ inputSchema: requestMergeInputSchema,
1154
+ outputSchema: requestMergeSuccessSchema,
1155
+ run: async (args) => {
1156
+ const input = z3.object(requestMergeInputSchema).parse(args);
1157
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1158
+ return requestMerge({ cwd });
1159
+ }
1160
+ });
1161
+ registerTool(server, context, {
1162
+ name: "remix_collab_inbox",
1163
+ description: "List merge requests available for review.",
1164
+ access: "read",
1165
+ inputSchema: inboxInputSchema,
1166
+ outputSchema: inboxSuccessSchema,
1167
+ run: async (args) => {
1168
+ const input = z3.object(inboxInputSchema).parse(args);
1169
+ return inbox({ status: input.status });
1170
+ }
1171
+ });
1172
+ registerTool(server, context, {
1173
+ name: "remix_collab_view_merge_request",
1174
+ description: "View merge request metadata, prompts, change steps, and optionally a bounded unified diff.",
1175
+ access: "read",
1176
+ inputSchema: viewMergeRequestInputSchema,
1177
+ outputSchema: viewMergeRequestSuccessSchema,
1178
+ run: async (args) => {
1179
+ const input = z3.object(viewMergeRequestInputSchema).parse(args);
1180
+ return viewMergeRequest({
1181
+ mrId: input.mrId,
1182
+ includeUnifiedDiff: input.includeUnifiedDiff ?? false,
1183
+ diffMaxChars: input.diffMaxChars ?? context.policy.maxDiffOutputChars
1184
+ });
1185
+ }
1186
+ });
1187
+ registerTool(server, context, {
1188
+ name: "remix_collab_approve_remote",
1189
+ description: "Approve a merge request remotely and wait for terminal completion without mutating the local repository.",
1190
+ access: "remote_write",
1191
+ inputSchema: approveInputSchema,
1192
+ outputSchema: approveSuccessSchema,
1193
+ run: async (args) => {
1194
+ const input = z3.object(approveInputSchema).parse(args);
1195
+ assertConfirm(input.confirm, "remix_collab_approve_remote");
1196
+ return approveMergeRequest({
1197
+ mrId: input.mrId,
1198
+ mode: "remote-only",
1199
+ allowBranchMismatch: input.allowBranchMismatch ?? false
1200
+ });
1201
+ }
1202
+ });
1203
+ registerTool(server, context, {
1204
+ name: "remix_collab_approve_and_sync_target",
1205
+ description: "Approve a merge request, wait for completion, and sync the target repository locally.",
1206
+ access: "local_write",
1207
+ inputSchema: approveInputSchema,
1208
+ outputSchema: approveSuccessSchema,
1209
+ run: async (args) => {
1210
+ const input = z3.object(approveInputSchema).parse(args);
1211
+ assertConfirm(input.confirm, "remix_collab_approve_and_sync_target");
1212
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1213
+ return approveMergeRequest({
1214
+ mrId: input.mrId,
1215
+ cwd,
1216
+ mode: "sync-target-repo",
1217
+ allowBranchMismatch: input.allowBranchMismatch ?? false
1218
+ });
1219
+ }
1220
+ });
1221
+ registerTool(server, context, {
1222
+ name: "remix_collab_reject",
1223
+ description: "Reject a merge request.",
1224
+ access: "remote_write",
1225
+ inputSchema: rejectInputSchema,
1226
+ outputSchema: rejectSuccessSchema,
1227
+ run: async (args) => {
1228
+ const input = z3.object(rejectInputSchema).parse(args);
1229
+ assertConfirm(input.confirm, "remix_collab_reject");
1230
+ return rejectMergeRequest({ mrId: input.mrId });
1231
+ }
1232
+ });
1233
+ registerTool(server, context, {
1234
+ name: "remix_collab_sync_upstream",
1235
+ description: "Sync upstream changes into the current remix and update the local checkout.",
1236
+ access: "local_write",
1237
+ inputSchema: applyInputSchema,
1238
+ outputSchema: syncUpstreamSuccessSchema,
1239
+ run: async (args) => {
1240
+ const input = z3.object(applyInputSchema).parse(args);
1241
+ assertConfirm(input.confirm, "remix_collab_sync_upstream");
1242
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1243
+ return syncUpstream({ cwd });
1244
+ }
1245
+ });
1246
+ registerTool(server, context, {
1247
+ name: "remix_collab_reconcile_preview",
1248
+ description: "Preview reconcile readiness when the local repository cannot be fast-forward synced.",
1249
+ access: "read",
1250
+ inputSchema: previewInputSchema,
1251
+ outputSchema: reconcileSuccessSchema,
1252
+ run: async (args) => {
1253
+ const input = z3.object(previewInputSchema).parse(args);
1254
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1255
+ return reconcile({ cwd, dryRun: true });
1256
+ }
1257
+ });
1258
+ registerTool(server, context, {
1259
+ name: "remix_collab_reconcile_apply",
1260
+ description: "Reconcile divergent local history against the bound Remix app and update the local checkout.",
1261
+ access: "local_write",
1262
+ inputSchema: applyInputSchema,
1263
+ outputSchema: reconcileSuccessSchema,
1264
+ run: async (args) => {
1265
+ const input = z3.object(applyInputSchema).parse(args);
1266
+ assertConfirm(input.confirm, "remix_collab_reconcile_apply");
1267
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1268
+ return reconcile({ cwd, dryRun: false, allowBranchMismatch: input.allowBranchMismatch ?? false });
1269
+ }
1270
+ });
1271
+ registerTool(server, context, {
1272
+ name: "remix_collab_invite",
1273
+ description: "Invite a collaborator to an organization, project, or app, using the current repository binding unless targetId is provided.",
1274
+ access: "remote_write",
1275
+ inputSchema: inviteInputSchema,
1276
+ outputSchema: inviteSuccessSchema,
1277
+ run: async (args) => {
1278
+ const input = z3.object(inviteInputSchema).parse(args);
1279
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1280
+ return inviteCollaborator({
1281
+ cwd,
1282
+ email: input.email,
1283
+ role: input.role,
1284
+ scope: input.scope,
1285
+ targetId: input.targetId,
1286
+ ttlDays: input.ttlDays
1287
+ });
1288
+ }
1289
+ });
1290
+ }
1291
+
1292
+ // src/tools/memory/register.ts
1293
+ import { z as z5 } from "zod";
1294
+
1295
+ // src/contracts/memory.ts
1296
+ import { z as z4 } from "zod";
1297
+ var genericRecordSchema2 = z4.record(z4.string(), z4.unknown());
1298
+ var genericArraySchema2 = z4.array(genericRecordSchema2);
1299
+ var memoryKindSchema = z4.enum(["collab_turn", "change_step", "merge_request", "reconcile"]);
1300
+ var paginationSchema = z4.object({
1301
+ limit: z4.number().int().nonnegative(),
1302
+ offset: z4.number().int().nonnegative(),
1303
+ hasMore: z4.boolean()
1304
+ });
1305
+ var memorySummaryInputSchema = {
1306
+ ...commonRequestFieldsSchema,
1307
+ appId: z4.string().trim().min(1).optional()
1308
+ };
1309
+ var memorySearchInputSchema = {
1310
+ ...commonRequestFieldsSchema,
1311
+ appId: z4.string().trim().min(1).optional(),
1312
+ query: z4.string().trim().min(1),
1313
+ kinds: z4.array(memoryKindSchema).max(4).optional(),
1314
+ limit: z4.number().int().positive().max(50).optional(),
1315
+ offset: z4.number().int().nonnegative().optional(),
1316
+ createdAfter: z4.string().trim().min(1).optional(),
1317
+ createdBefore: z4.string().trim().min(1).optional()
1318
+ };
1319
+ var memoryTimelineInputSchema = {
1320
+ ...commonRequestFieldsSchema,
1321
+ appId: z4.string().trim().min(1).optional(),
1322
+ kinds: z4.array(memoryKindSchema).max(4).optional(),
1323
+ limit: z4.number().int().positive().max(50).optional(),
1324
+ offset: z4.number().int().nonnegative().optional(),
1325
+ createdAfter: z4.string().trim().min(1).optional(),
1326
+ createdBefore: z4.string().trim().min(1).optional()
1327
+ };
1328
+ var changeStepDiffInputSchema = {
1329
+ ...commonRequestFieldsSchema,
1330
+ appId: z4.string().trim().min(1).optional(),
1331
+ changeStepId: z4.string().trim().min(1)
1332
+ };
1333
+ var memorySummaryDataSchema = genericRecordSchema2;
1334
+ var memorySearchDataSchema = z4.object({
1335
+ items: genericArraySchema2,
1336
+ pagination: paginationSchema
1337
+ });
1338
+ var memoryTimelineDataSchema = z4.object({
1339
+ items: genericArraySchema2,
1340
+ pagination: paginationSchema
1341
+ });
1342
+ var changeStepDiffDataSchema = z4.object({
1343
+ changeStepId: z4.string(),
1344
+ appId: z4.string(),
1345
+ diff: z4.string(),
1346
+ diffSha256: z4.string(),
1347
+ contentType: z4.string(),
1348
+ encoding: z4.string(),
1349
+ expiresIn: z4.number().int().positive()
1350
+ });
1351
+ var memorySummarySuccessSchema = makeSuccessSchema(memorySummaryDataSchema);
1352
+ var memorySearchSuccessSchema = makeSuccessSchema(memorySearchDataSchema);
1353
+ var memoryTimelineSuccessSchema = makeSuccessSchema(memoryTimelineDataSchema);
1354
+ var changeStepDiffSuccessSchema = makeSuccessSchema(changeStepDiffDataSchema);
1355
+
1356
+ // src/domain/memoryAdapter.ts
1357
+ import { readCollabBinding } from "@remixhq/core/binding";
1358
+ import { findGitRoot as findGitRoot2 } from "@remixhq/core/repo";
1359
+ function buildSummaryNextActions(summary) {
1360
+ const actions = [];
1361
+ actions.push(
1362
+ "Use `remix_collab_memory_search` next for focused why/history/failed-attempt questions about this app."
1363
+ );
1364
+ const recentItemCount = summary.recent.collabTurns.length + summary.recent.changeSteps.length + summary.recent.mergeRequests.length + summary.recent.reconciles.length;
1365
+ if (recentItemCount > 0) {
1366
+ actions.push("Use `remix_collab_memory_timeline` next if you need the chronological sequence of recent activity.");
1367
+ }
1368
+ if (summary.counts.failedChangeStepCount > 0 || summary.counts.reconcileCount > 0) {
1369
+ actions.push(
1370
+ "For prior failures or recovery history, run `remix_collab_memory_search` with a focused query and `kinds` narrowed to `change_step` and `reconcile` when appropriate."
1371
+ );
1372
+ }
1373
+ return actions;
1374
+ }
1375
+ function getFirstChangeStepId(items) {
1376
+ const changeStep = items.find((item) => item.kind === "change_step");
1377
+ return changeStep?.id ?? null;
1378
+ }
1379
+ function buildSearchNextActions(result) {
1380
+ if (result.items.length === 0) {
1381
+ return [
1382
+ "Try a broader `remix_collab_memory_search` query or fewer `kinds` filters if this topic likely uses different wording.",
1383
+ "Use `remix_collab_memory_timeline` if you need a bounded chronological scan instead of relevance-ranked search."
1384
+ ];
1385
+ }
1386
+ const actions = [
1387
+ "Review the top matched memory items before falling back to raw git history so you keep the reasoning context in view."
1388
+ ];
1389
+ const changeStepId = getFirstChangeStepId(result.items);
1390
+ if (changeStepId) {
1391
+ actions.push(
1392
+ `Use \`remix_collab_memory_change_step_diff\` with \`changeStepId=${changeStepId}\` if you need the full stored diff for the most relevant change step.`
1393
+ );
1394
+ }
1395
+ if (result.pagination.hasMore) {
1396
+ actions.push("Narrow the search with `kinds`, `createdAfter`, or `createdBefore` if you need a tighter historical slice.");
1397
+ }
1398
+ return actions;
1399
+ }
1400
+ function buildTimelineNextActions(result) {
1401
+ if (result.items.length === 0) {
1402
+ return [
1403
+ "Use `remix_collab_memory_summary` for current state, then retry `remix_collab_memory_timeline` with broader filters if needed."
1404
+ ];
1405
+ }
1406
+ const actions = [
1407
+ "Use `remix_collab_memory_search` if you need relevance-ranked results for a specific feature, bug, or design decision."
1408
+ ];
1409
+ const changeStepId = getFirstChangeStepId(result.items);
1410
+ if (changeStepId) {
1411
+ actions.push(
1412
+ `Use \`remix_collab_memory_change_step_diff\` with \`changeStepId=${changeStepId}\` after the timeline identifies the change step you need to inspect.`
1413
+ );
1414
+ }
1415
+ if (result.pagination.hasMore) {
1416
+ actions.push("Apply `kinds`, `createdAfter`, or `createdBefore` filters to focus the timeline on a smaller historical window.");
1417
+ }
1418
+ return actions;
1419
+ }
1420
+ function buildChangeStepDiffNextActions(changeStepId) {
1421
+ return [
1422
+ `Inspect the stored diff for \`changeStepId=${changeStepId}\`, then use raw git only if you still need exact repository-level commit or ancestry details.`
1423
+ ];
1424
+ }
1425
+ function unwrapResponseObject2(resp, label) {
1426
+ const obj = resp?.responseObject;
1427
+ if (obj === void 0 || obj === null) {
1428
+ throw new Error(typeof resp?.message === "string" && resp.message.trim() ? resp.message : `Missing ${label} response`);
1429
+ }
1430
+ return obj;
1431
+ }
1432
+ function makeNotBoundError() {
1433
+ const error = new Error("Repository is not bound to Remix.");
1434
+ error.hint = "Run `remix_collab_init` in this repository, or pass `appId` explicitly for a direct memory read.";
1435
+ return error;
1436
+ }
1437
+ async function maybeFindGitRoot(cwd) {
1438
+ try {
1439
+ return await findGitRoot2(cwd);
1440
+ } catch {
1441
+ return null;
1442
+ }
1443
+ }
1444
+ async function resolveMemoryTarget(params) {
1445
+ const explicitAppId = params.appId?.trim();
1446
+ if (explicitAppId) {
1447
+ return {
1448
+ appId: explicitAppId,
1449
+ repoRoot: await maybeFindGitRoot(params.cwd)
1450
+ };
1451
+ }
1452
+ const repoRoot = await findGitRoot2(params.cwd);
1453
+ const binding = await readCollabBinding(repoRoot);
1454
+ if (!binding) {
1455
+ throw makeNotBoundError();
1456
+ }
1457
+ return {
1458
+ appId: binding.currentAppId,
1459
+ repoRoot
1460
+ };
1461
+ }
1462
+ async function getMemorySummary(params) {
1463
+ const target = await resolveMemoryTarget(params);
1464
+ const api = await createApiClient();
1465
+ const resp = await api.getAgentMemorySummary(target.appId);
1466
+ const data = unwrapResponseObject2(resp, "agent memory summary");
1467
+ return {
1468
+ data,
1469
+ warnings: [],
1470
+ recommendedNextActions: buildSummaryNextActions(data),
1471
+ logContext: target
1472
+ };
1473
+ }
1474
+ async function searchMemory(params) {
1475
+ const target = await resolveMemoryTarget(params);
1476
+ const api = await createApiClient();
1477
+ const resp = await api.searchAgentMemory(target.appId, {
1478
+ q: params.query,
1479
+ kinds: params.kinds,
1480
+ limit: params.limit,
1481
+ offset: params.offset,
1482
+ createdAfter: params.createdAfter,
1483
+ createdBefore: params.createdBefore
1484
+ });
1485
+ const data = unwrapResponseObject2(resp, "agent memory search");
1486
+ return {
1487
+ data,
1488
+ warnings: [],
1489
+ recommendedNextActions: buildSearchNextActions(data),
1490
+ logContext: target
1491
+ };
1492
+ }
1493
+ async function getMemoryTimeline(params) {
1494
+ const target = await resolveMemoryTarget(params);
1495
+ const api = await createApiClient();
1496
+ const resp = await api.listAgentMemoryTimeline(target.appId, {
1497
+ kinds: params.kinds,
1498
+ limit: params.limit,
1499
+ offset: params.offset,
1500
+ createdAfter: params.createdAfter,
1501
+ createdBefore: params.createdBefore
1502
+ });
1503
+ const data = unwrapResponseObject2(resp, "agent memory timeline");
1504
+ return {
1505
+ data,
1506
+ warnings: [],
1507
+ recommendedNextActions: buildTimelineNextActions(data),
1508
+ logContext: target
1509
+ };
1510
+ }
1511
+ async function getChangeStepDiff(params) {
1512
+ const target = await resolveMemoryTarget(params);
1513
+ const api = await createApiClient();
1514
+ const resp = await api.getChangeStepDiff(target.appId, params.changeStepId);
1515
+ const data = unwrapResponseObject2(resp, "change step diff");
1516
+ return {
1517
+ data,
1518
+ warnings: [],
1519
+ recommendedNextActions: buildChangeStepDiffNextActions(data.changeStepId),
1520
+ logContext: target
1521
+ };
1522
+ }
1523
+
1524
+ // src/tools/memory/register.ts
1525
+ function getAnnotations2(access) {
1526
+ return {
1527
+ readOnlyHint: access === "read",
1528
+ destructiveHint: false,
1529
+ idempotentHint: true,
1530
+ openWorldHint: false
1531
+ };
1532
+ }
1533
+ function buildSuccessEnvelope2(tool, requestId, result) {
1534
+ return {
1535
+ schemaVersion: SCHEMA_VERSION,
1536
+ ok: true,
1537
+ tool,
1538
+ requestId: requestId ?? null,
1539
+ data: result.data,
1540
+ warnings: result.warnings ?? [],
1541
+ risks: result.risks ?? [],
1542
+ recommendedNextActions: result.recommendedNextActions ?? []
1543
+ };
1544
+ }
1545
+ function buildErrorEnvelope2(tool, requestId, error) {
1546
+ const normalized = normalizeToolError(error);
1547
+ return {
1548
+ schemaVersion: SCHEMA_VERSION,
1549
+ ok: false,
1550
+ tool,
1551
+ requestId: requestId ?? null,
1552
+ error: normalized,
1553
+ warnings: [],
1554
+ risks: deriveErrorRisks2(normalized),
1555
+ recommendedNextActions: normalized.code === "AUTH_REQUIRED" ? ["Run `remix login` or set COMERGE_ACCESS_TOKEN, then retry."] : []
1556
+ };
1557
+ }
1558
+ function deriveErrorRisks2(normalized) {
1559
+ if (normalized.code === "DESTRUCTIVE_OPERATION_BLOCKED") {
1560
+ return ["A policy guard blocked a disallowed operation."];
1561
+ }
1562
+ return [];
1563
+ }
1564
+ function registerTool2(server, context, params) {
1565
+ const errorSchema = makeErrorSchema();
1566
+ server.registerTool(
1567
+ params.name,
1568
+ {
1569
+ title: params.name,
1570
+ description: params.description,
1571
+ inputSchema: params.inputSchema,
1572
+ outputSchema: params.outputSchema,
1573
+ annotations: getAnnotations2(params.access)
1574
+ },
1575
+ async (rawArgs) => {
1576
+ const requestId = typeof rawArgs.requestId === "string" ? rawArgs.requestId : void 0;
1577
+ const startedAt = Date.now();
1578
+ try {
1579
+ assertToolAccess(context.policy, params.access);
1580
+ const result = await params.run(rawArgs);
1581
+ const envelope = buildSuccessEnvelope2(params.name, requestId, result);
1582
+ params.outputSchema.parse(envelope);
1583
+ context.logger.log({
1584
+ level: "info",
1585
+ message: "tool_completed",
1586
+ tool: params.name,
1587
+ requestId: envelope.requestId,
1588
+ durationMs: Date.now() - startedAt,
1589
+ result: "success",
1590
+ repoRoot: result.logContext?.repoRoot ?? null,
1591
+ appId: result.logContext?.appId ?? null,
1592
+ mrId: result.logContext?.mrId ?? null,
1593
+ truncated: result.warnings?.some((warning) => warning.toLowerCase().includes("truncated")) ?? false
1594
+ });
1595
+ return makeSuccessResult(envelope);
1596
+ } catch (error) {
1597
+ const envelope = buildErrorEnvelope2(params.name, requestId, error);
1598
+ errorSchema.parse(envelope);
1599
+ context.logger.log({
1600
+ level: "error",
1601
+ message: "tool_failed",
1602
+ tool: params.name,
1603
+ requestId: envelope.requestId,
1604
+ durationMs: Date.now() - startedAt,
1605
+ result: "error",
1606
+ errorCode: envelope.error.code
1607
+ });
1608
+ return makeErrorResult(envelope);
1609
+ }
1610
+ }
1611
+ );
1612
+ }
1613
+ function registerMemoryTools(server, context) {
1614
+ registerTool2(server, context, {
1615
+ name: "remix_collab_memory_summary",
1616
+ description: "First read for a bound app's current collaboration state, recent reasoning context, and merge or reconcile history before deeper inspection.",
1617
+ access: "read",
1618
+ inputSchema: memorySummaryInputSchema,
1619
+ outputSchema: memorySummarySuccessSchema,
1620
+ run: async (args) => {
1621
+ const input = z5.object(memorySummaryInputSchema).parse(args);
1622
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1623
+ return getMemorySummary({
1624
+ cwd,
1625
+ appId: input.appId
1626
+ });
1627
+ }
1628
+ });
1629
+ registerTool2(server, context, {
1630
+ name: "remix_collab_memory_search",
1631
+ description: "Default tool for why/history/failed-attempt/user-intent questions. Search prompts, diffs, merge activity, reconciles, and other historical context before using raw git history.",
1632
+ access: "read",
1633
+ inputSchema: memorySearchInputSchema,
1634
+ outputSchema: memorySearchSuccessSchema,
1635
+ run: async (args) => {
1636
+ const input = z5.object(memorySearchInputSchema).parse(args);
1637
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1638
+ return searchMemory({
1639
+ cwd,
1640
+ appId: input.appId,
1641
+ query: input.query,
1642
+ kinds: input.kinds,
1643
+ limit: input.limit,
1644
+ offset: input.offset,
1645
+ createdAfter: input.createdAfter,
1646
+ createdBefore: input.createdBefore
1647
+ });
1648
+ }
1649
+ });
1650
+ registerTool2(server, context, {
1651
+ name: "remix_collab_memory_timeline",
1652
+ description: "Chronological view of collaboration memory for understanding what happened and in what order, with optional filters for bounded historical inspection.",
1653
+ access: "read",
1654
+ inputSchema: memoryTimelineInputSchema,
1655
+ outputSchema: memoryTimelineSuccessSchema,
1656
+ run: async (args) => {
1657
+ const input = z5.object(memoryTimelineInputSchema).parse(args);
1658
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1659
+ return getMemoryTimeline({
1660
+ cwd,
1661
+ appId: input.appId,
1662
+ kinds: input.kinds,
1663
+ limit: input.limit,
1664
+ offset: input.offset,
1665
+ createdAfter: input.createdAfter,
1666
+ createdBefore: input.createdBefore
1667
+ });
1668
+ }
1669
+ });
1670
+ registerTool2(server, context, {
1671
+ name: "remix_collab_memory_change_step_diff",
1672
+ description: "Second-hop expansion tool that fetches the full stored diff for a specific change step after memory search, timeline, or review work has identified the relevant `changeStepId`.",
1673
+ access: "read",
1674
+ inputSchema: changeStepDiffInputSchema,
1675
+ outputSchema: changeStepDiffSuccessSchema,
1676
+ run: async (args) => {
1677
+ const input = z5.object(changeStepDiffInputSchema).parse(args);
1678
+ const cwd = resolvePolicyCwd(context.policy, input.cwd);
1679
+ return getChangeStepDiff({
1680
+ cwd,
1681
+ appId: input.appId,
1682
+ changeStepId: input.changeStepId
1683
+ });
1684
+ }
1685
+ });
1686
+ }
1687
+
1688
+ // src/server.ts
1689
+ function createRemixMcpServer(params) {
1690
+ const context = createServerContext({ version: params.version });
1691
+ const server = new McpServer({
1692
+ name: context.serverName,
1693
+ version: context.version
1694
+ });
1695
+ registerCollabTools(server, context);
1696
+ registerMemoryTools(server, context);
1697
+ return { server, context };
1698
+ }
1699
+ async function startStdioServer(params) {
1700
+ const { server } = createRemixMcpServer(params);
1701
+ const transport = new StdioServerTransport();
1702
+ await server.connect(transport);
1703
+ }
1704
+ export {
1705
+ createRemixMcpServer,
1706
+ startStdioServer
1707
+ };
1708
+ //# sourceMappingURL=index.js.map