@remixhq/mcp 0.1.8 → 0.1.10

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 CHANGED
@@ -83,6 +83,8 @@ var ERROR_CODES = {
83
83
  METADATA_CONFLICT: "METADATA_CONFLICT",
84
84
  DESTRUCTIVE_OPERATION_BLOCKED: "DESTRUCTIVE_OPERATION_BLOCKED",
85
85
  CONFIG_INVALID: "CONFIG_INVALID",
86
+ ACCESS_DENIED: "ACCESS_DENIED",
87
+ RESOURCE_NOT_FOUND: "RESOURCE_NOT_FOUND",
86
88
  INTERNAL_ERROR: "INTERNAL_ERROR"
87
89
  };
88
90
  var RemixMcpError = class extends Error {
@@ -100,6 +102,15 @@ function toStringOrNull(value) {
100
102
  const trimmed = value.trim();
101
103
  return trimmed.length > 0 ? trimmed : null;
102
104
  }
105
+ function parseJsonObject(value) {
106
+ if (!value) return null;
107
+ try {
108
+ const parsed = JSON.parse(value);
109
+ return parsed && typeof parsed === "object" ? parsed : null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
103
114
  function makeNormalized(params) {
104
115
  return {
105
116
  code: params.code,
@@ -113,6 +124,8 @@ function normalizeByMessage(err) {
113
124
  const code = toStringOrNull(err.code);
114
125
  const message = toStringOrNull(err.message) ?? "Unexpected error.";
115
126
  const hint = toStringOrNull(err.hint);
127
+ const hintBody = parseJsonObject(hint);
128
+ const statusCode = typeof hintBody?.statusCode === "number" ? hintBody.statusCode : typeof hintBody?.statusCode === "string" ? Number(hintBody.statusCode) : null;
116
129
  if (code === ERROR_CODES.REPO_LOCK_HELD) {
117
130
  return makeNormalized({
118
131
  code: ERROR_CODES.REPO_LOCK_HELD,
@@ -213,6 +226,22 @@ function normalizeByMessage(err) {
213
226
  category: "remote_state"
214
227
  });
215
228
  }
229
+ if (statusCode === 403) {
230
+ return makeNormalized({
231
+ code: ERROR_CODES.ACCESS_DENIED,
232
+ message,
233
+ hint,
234
+ category: "remote_state"
235
+ });
236
+ }
237
+ if (statusCode === 404) {
238
+ return makeNormalized({
239
+ code: ERROR_CODES.RESOURCE_NOT_FOUND,
240
+ message,
241
+ hint,
242
+ category: "remote_state"
243
+ });
244
+ }
216
245
  if (message.includes("Timed out") || message.includes("failed") || message.includes("error state")) {
217
246
  return makeNormalized({
218
247
  code: ERROR_CODES.REMOTE_ERROR,
@@ -405,6 +434,11 @@ function makeErrorResult(envelope) {
405
434
  // src/contracts/collab.ts
406
435
  var genericRecordSchema = z2.record(z2.string(), z2.unknown());
407
436
  var genericArraySchema = z2.array(genericRecordSchema);
437
+ var paginationSchema = z2.object({
438
+ limit: z2.number().int().positive(),
439
+ offset: z2.number().int().nonnegative(),
440
+ hasMore: z2.boolean()
441
+ });
408
442
  var mergeRequestQueueSchema = z2.enum([
409
443
  "reviewable",
410
444
  "created_by_me",
@@ -426,7 +460,12 @@ var initInputSchema = {
426
460
  var listInputSchema = {
427
461
  requestId: z2.string().trim().min(1).optional(),
428
462
  outputMode: z2.enum(["summary", "full"]).optional(),
429
- forked: z2.enum(["only", "exclude", "all"]).optional()
463
+ ownership: z2.enum(["mine", "shared", "all"]).optional(),
464
+ accessScope: z2.enum(["all_readable", "explicit_member"]).optional(),
465
+ createdBy: z2.string().trim().min(1).optional(),
466
+ forked: z2.enum(["only", "exclude", "all"]).optional(),
467
+ limit: z2.number().int().positive().max(50).optional(),
468
+ offset: z2.number().int().nonnegative().optional()
430
469
  };
431
470
  var remixInputSchema = {
432
471
  ...commonRequestFieldsSchema,
@@ -455,6 +494,16 @@ var recordTurnInputSchema = {
455
494
  allowBranchMismatch: z2.boolean().optional(),
456
495
  idempotencyKey: z2.string().trim().min(1).optional()
457
496
  };
497
+ var finalizeTurnInputSchema = {
498
+ ...commonRequestFieldsSchema,
499
+ prompt: z2.string().trim().min(1),
500
+ assistantResponse: z2.string().trim().min(1),
501
+ diffSource: z2.enum(["worktree", "external"]).optional(),
502
+ externalDiff: z2.string().optional(),
503
+ sync: z2.boolean().optional(),
504
+ allowBranchMismatch: z2.boolean().optional(),
505
+ idempotencyKey: z2.string().trim().min(1).optional()
506
+ };
458
507
  var previewInputSchema = {
459
508
  ...commonRequestFieldsSchema
460
509
  };
@@ -470,20 +519,26 @@ var reviewQueueInputSchema = {
470
519
  requestId: z2.string().trim().min(1).optional(),
471
520
  outputMode: z2.enum(["summary", "full"]).optional(),
472
521
  status: z2.string().trim().min(1).optional(),
473
- kind: z2.enum(["merge", "sync", "all"]).optional()
522
+ kind: z2.enum(["merge", "sync", "all"]).optional(),
523
+ limit: z2.number().int().positive().max(50).optional(),
524
+ offset: z2.number().int().nonnegative().optional()
474
525
  };
475
526
  var myMergeRequestsInputSchema = {
476
527
  requestId: z2.string().trim().min(1).optional(),
477
528
  outputMode: z2.enum(["summary", "full"]).optional(),
478
529
  status: z2.string().trim().min(1).optional(),
479
- kind: z2.enum(["merge", "sync", "all"]).optional()
530
+ kind: z2.enum(["merge", "sync", "all"]).optional(),
531
+ limit: z2.number().int().positive().max(50).optional(),
532
+ offset: z2.number().int().nonnegative().optional()
480
533
  };
481
534
  var appMergeRequestsInputSchema = {
482
535
  ...commonRequestFieldsSchema,
483
536
  queue: appScopedMergeRequestQueueSchema,
484
537
  appId: z2.string().trim().min(1).optional(),
485
538
  status: z2.string().trim().min(1).optional(),
486
- kind: z2.enum(["merge", "sync", "all"]).optional()
539
+ kind: z2.enum(["merge", "sync", "all"]).optional(),
540
+ limit: z2.number().int().positive().max(50).optional(),
541
+ offset: z2.number().int().nonnegative().optional()
487
542
  };
488
543
  var viewMergeRequestInputSchema = {
489
544
  requestId: z2.string().trim().min(1).optional(),
@@ -514,6 +569,38 @@ var inviteInputSchema = {
514
569
  var listMembersInputSchema = {
515
570
  ...commonRequestFieldsSchema,
516
571
  scope: memberScopeSchema,
572
+ targetId: z2.string().trim().min(1).optional(),
573
+ limit: z2.number().int().positive().max(50).optional(),
574
+ offset: z2.number().int().nonnegative().optional()
575
+ };
576
+ var listInvitesInputSchema = {
577
+ ...commonRequestFieldsSchema,
578
+ scope: memberScopeSchema.optional(),
579
+ targetId: z2.string().trim().min(1).optional(),
580
+ limit: z2.number().int().positive().max(50).optional(),
581
+ offset: z2.number().int().nonnegative().optional()
582
+ };
583
+ var resendInviteInputSchema = {
584
+ ...commonRequestFieldsSchema,
585
+ scope: memberScopeSchema.optional(),
586
+ targetId: z2.string().trim().min(1).optional(),
587
+ inviteId: z2.string().trim().min(1),
588
+ ttlDays: z2.number().int().positive().max(30).optional()
589
+ };
590
+ var revokeInviteInputSchema = {
591
+ ...commonRequestFieldsSchema,
592
+ scope: memberScopeSchema.optional(),
593
+ targetId: z2.string().trim().min(1).optional(),
594
+ inviteId: z2.string().trim().min(1),
595
+ confirm: z2.boolean()
596
+ };
597
+ var acceptInvitationInputSchema = {
598
+ requestId: z2.string().trim().min(1).optional(),
599
+ token: z2.string().trim().min(20)
600
+ };
601
+ var accessDebugInputSchema = {
602
+ ...commonRequestFieldsSchema,
603
+ scope: memberScopeSchema.optional(),
517
604
  targetId: z2.string().trim().min(1).optional()
518
605
  };
519
606
  var updateMemberRoleInputSchema = {
@@ -537,7 +624,8 @@ var initDataSchema = z2.object({
537
624
  repoRoot: z2.string()
538
625
  });
539
626
  var listDataSchema = z2.object({
540
- apps: genericArraySchema
627
+ apps: genericArraySchema,
628
+ pagination: paginationSchema
541
629
  });
542
630
  var remixDataSchema = z2.object({
543
631
  appId: z2.string(),
@@ -560,12 +648,21 @@ var addDataSchema = z2.object({
560
648
  autoSync: genericRecordSchema
561
649
  });
562
650
  var recordTurnDataSchema = genericRecordSchema;
651
+ var finalizeTurnDataSchema = z2.object({
652
+ mode: z2.enum(["changed_turn", "no_diff_turn"]),
653
+ idempotencyKey: z2.string().min(1),
654
+ changeStep: genericRecordSchema.nullable(),
655
+ collabTurn: genericRecordSchema.nullable(),
656
+ autoSync: genericRecordSchema.nullable(),
657
+ warnings: z2.array(z2.string())
658
+ });
563
659
  var syncDataSchema = genericRecordSchema;
564
660
  var requestMergeDataSchema = genericRecordSchema;
565
661
  var mergeRequestQueueDataSchema = z2.object({
566
662
  queue: mergeRequestQueueSchema,
567
663
  appId: z2.string().nullable(),
568
- mergeRequests: z2.array(genericRecordSchema)
664
+ mergeRequests: z2.array(genericRecordSchema),
665
+ pagination: paginationSchema
569
666
  });
570
667
  var viewMergeRequestDataSchema = genericRecordSchema;
571
668
  var approveDataSchema = genericRecordSchema;
@@ -577,7 +674,49 @@ var memberRecordSchema = genericRecordSchema;
577
674
  var listMembersDataSchema = z2.object({
578
675
  scopeType: memberScopeSchema,
579
676
  targetId: z2.string(),
580
- members: z2.array(memberRecordSchema)
677
+ members: z2.array(memberRecordSchema),
678
+ pagination: paginationSchema
679
+ });
680
+ var listInvitesDataSchema = z2.object({
681
+ scopeType: memberScopeSchema,
682
+ targetId: z2.string(),
683
+ invites: z2.array(genericRecordSchema),
684
+ pagination: paginationSchema
685
+ });
686
+ var resendInviteDataSchema = genericRecordSchema;
687
+ var revokeInviteDataSchema = z2.object({
688
+ scopeType: memberScopeSchema,
689
+ targetId: z2.string(),
690
+ inviteId: z2.string(),
691
+ revoked: z2.boolean()
692
+ });
693
+ var acceptInvitationDataSchema = genericRecordSchema;
694
+ var accessDebugDataSchema = z2.object({
695
+ viewer: genericRecordSchema,
696
+ scope: z2.object({
697
+ scopeType: memberScopeSchema,
698
+ targetId: z2.string()
699
+ }),
700
+ entities: z2.object({
701
+ organization: genericRecordSchema.nullable(),
702
+ project: genericRecordSchema.nullable(),
703
+ app: genericRecordSchema.nullable()
704
+ }),
705
+ binding: z2.object({
706
+ repoRoot: z2.string().nullable(),
707
+ binding: genericRecordSchema.nullable()
708
+ }),
709
+ access: z2.object({
710
+ appContext: genericRecordSchema.nullable(),
711
+ viewerMember: genericRecordSchema.nullable(),
712
+ viewerProjectMember: genericRecordSchema.nullable(),
713
+ inviteForViewerEmail: genericRecordSchema.nullable(),
714
+ effectiveAppAccess: genericRecordSchema.nullable()
715
+ }),
716
+ members: z2.array(genericRecordSchema),
717
+ membersPageInfo: paginationSchema,
718
+ invites: z2.array(genericRecordSchema),
719
+ invitesPageInfo: paginationSchema
581
720
  });
582
721
  var updateMemberRoleDataSchema = z2.object({
583
722
  scopeType: memberScopeSchema,
@@ -591,6 +730,7 @@ var remixSuccessSchema = makeSuccessSchema(remixDataSchema);
591
730
  var checkoutSuccessSchema = makeSuccessSchema(checkoutDataSchema);
592
731
  var addSuccessSchema = makeSuccessSchema(addDataSchema);
593
732
  var recordTurnSuccessSchema = makeSuccessSchema(recordTurnDataSchema);
733
+ var finalizeTurnSuccessSchema = makeSuccessSchema(finalizeTurnDataSchema);
594
734
  var syncSuccessSchema = makeSuccessSchema(syncDataSchema);
595
735
  var requestMergeSuccessSchema = makeSuccessSchema(requestMergeDataSchema);
596
736
  var mergeRequestQueueSuccessSchema = makeSuccessSchema(mergeRequestQueueDataSchema);
@@ -601,13 +741,20 @@ var syncUpstreamSuccessSchema = makeSuccessSchema(syncUpstreamDataSchema);
601
741
  var reconcileSuccessSchema = makeSuccessSchema(reconcileDataSchema);
602
742
  var inviteSuccessSchema = makeSuccessSchema(inviteDataSchema);
603
743
  var listMembersSuccessSchema = makeSuccessSchema(listMembersDataSchema);
744
+ var listInvitesSuccessSchema = makeSuccessSchema(listInvitesDataSchema);
745
+ var resendInviteSuccessSchema = makeSuccessSchema(resendInviteDataSchema);
746
+ var revokeInviteSuccessSchema = makeSuccessSchema(revokeInviteDataSchema);
747
+ var acceptInvitationSuccessSchema = makeSuccessSchema(acceptInvitationDataSchema);
748
+ var accessDebugSuccessSchema = makeSuccessSchema(accessDebugDataSchema);
604
749
  var updateMemberRoleSuccessSchema = makeSuccessSchema(updateMemberRoleDataSchema);
605
750
 
606
751
  // src/domain/coreAdapter.ts
607
752
  import {
753
+ collabList as coreCollabList,
608
754
  collabListMembers as coreCollabListMembers,
609
755
  collabUpdateMemberRole as coreCollabUpdateMemberRole,
610
756
  collabAdd as coreCollabAdd,
757
+ collabFinalizeTurn as coreCollabFinalizeTurn,
611
758
  collabRecordTurn as coreCollabRecordTurn,
612
759
  collabApprove as coreCollabApprove,
613
760
  collabCheckout as coreCollabCheckout,
@@ -624,13 +771,6 @@ import {
624
771
  collabView as coreCollabView
625
772
  } from "@remixhq/core/collab";
626
773
  import { findGitRoot, getHeadCommitHash, listUntrackedFiles } from "@remixhq/core/repo";
627
- function unwrapResponseObject(resp, label) {
628
- const obj = resp?.responseObject;
629
- if (obj === void 0 || obj === null) {
630
- throw new Error(typeof resp?.message === "string" && resp.message.trim() ? resp.message : `Missing ${label} response`);
631
- }
632
- return obj;
633
- }
634
774
  function getRiskLevel(status) {
635
775
  if (status.recommendedAction === "reconcile") return "high";
636
776
  if (status.recommendedAction === "sync" || status.remote.incomingOpenMergeRequestCount) return "medium";
@@ -717,13 +857,20 @@ async function initCollab(params) {
717
857
  };
718
858
  }
719
859
  async function listApps(params) {
720
- const api = await createApiClient();
721
- const resp = await api.listApps({ forked: params.forked ?? "all" });
722
- const apps = unwrapResponseObject(resp, "apps");
860
+ const api = await createCollabApiClient();
861
+ const result = await coreCollabList({
862
+ api,
863
+ ownership: params.ownership,
864
+ accessScope: params.accessScope,
865
+ createdBy: params.createdBy,
866
+ forked: params.forked,
867
+ limit: params.limit,
868
+ offset: params.offset
869
+ });
723
870
  return {
724
- data: { apps },
871
+ data: result,
725
872
  warnings: [],
726
- recommendedNextActions: [],
873
+ recommendedNextActions: result.pagination.hasMore ? [`Pass offset=${result.pagination.offset + result.pagination.limit} to load the next page.`] : [],
727
874
  logContext: {}
728
875
  };
729
876
  }
@@ -764,70 +911,28 @@ async function checkoutCollab(params) {
764
911
  }
765
912
  };
766
913
  }
767
- async function addCollabStep(params) {
914
+ async function finalizeCollabTurn(params) {
768
915
  const api = await createCollabApiClient();
769
916
  const repoRoot = await findGitRoot(params.cwd);
770
- const preHead = await getHeadCommitHash(repoRoot);
771
- const untrackedBefore = params.diffSource === "worktree" ? await listUntrackedFiles(repoRoot) : [];
772
- const changeStep = await coreCollabAdd({
917
+ const result = await coreCollabFinalizeTurn({
773
918
  api,
774
919
  cwd: params.cwd,
775
920
  prompt: params.prompt,
776
- assistantResponse: params.assistantResponse ?? null,
921
+ assistantResponse: params.assistantResponse,
777
922
  diff: params.externalDiff ?? null,
778
923
  diffSource: params.diffSource,
779
- allowBranchMismatch: params.allowBranchMismatch ?? false,
780
- idempotencyKey: params.idempotencyKey ?? null,
781
- actor: params.agent
782
- });
783
- const postHead = await getHeadCommitHash(repoRoot);
784
- const autoSyncEligible = params.diffSource === "worktree";
785
- const localRepoMutated = autoSyncEligible && preHead !== postHead;
786
- return {
787
- data: {
788
- changeStep,
789
- autoSync: {
790
- requested: true,
791
- eligible: autoSyncEligible,
792
- attempted: autoSyncEligible,
793
- applied: autoSyncEligible,
794
- trackedChangesDiscarded: autoSyncEligible,
795
- capturedUntrackedPathsCandidate: untrackedBefore,
796
- localHeadBefore: preHead,
797
- localHeadAfter: postHead,
798
- localRepoMutated
799
- }
800
- },
801
- warnings: [
802
- ...collectResultWarnings(changeStep),
803
- ...params.diffSource === "external" ? [
804
- "Automatic local discard+sync was skipped because the diff came from an external source and may not match the current worktree."
805
- ] : []
806
- ],
807
- recommendedNextActions: [],
808
- logContext: {
809
- repoRoot
810
- }
811
- };
812
- }
813
- async function recordCollabTurn(params) {
814
- const api = await createCollabApiClient();
815
- const result = await coreCollabRecordTurn({
816
- api,
817
- cwd: params.cwd,
818
- prompt: params.prompt,
819
- assistantResponse: params.assistantResponse,
924
+ sync: params.sync,
820
925
  allowBranchMismatch: params.allowBranchMismatch ?? false,
821
926
  idempotencyKey: params.idempotencyKey ?? null,
822
927
  actor: params.agent
823
928
  });
824
929
  return {
825
930
  data: result,
826
- warnings: [],
931
+ warnings: result.warnings,
827
932
  recommendedNextActions: [],
828
933
  logContext: {
829
- repoRoot: params.cwd,
830
- appId: result.appId
934
+ repoRoot,
935
+ appId: result.changeStep?.appId ?? result.collabTurn?.appId ?? null
831
936
  }
832
937
  };
833
938
  }
@@ -869,16 +974,19 @@ async function reviewQueue(params) {
869
974
  api,
870
975
  queue: "reviewable",
871
976
  status: params.status ?? "open",
872
- kind: params.kind ?? "merge"
977
+ kind: params.kind ?? "merge",
978
+ limit: params.limit,
979
+ offset: params.offset
873
980
  });
874
981
  return {
875
982
  data: {
876
983
  queue: result.queue,
877
984
  appId: result.appId,
878
- mergeRequests: result.mergeRequests
985
+ mergeRequests: result.mergeRequests,
986
+ pagination: result.pagination
879
987
  },
880
988
  warnings: [],
881
- recommendedNextActions: [],
989
+ recommendedNextActions: result.pagination.hasMore ? [`Pass offset=${result.pagination.offset + result.pagination.limit} to load the next page.`] : [],
882
990
  logContext: {}
883
991
  };
884
992
  }
@@ -888,16 +996,19 @@ async function myMergeRequests(params) {
888
996
  api,
889
997
  queue: "created_by_me",
890
998
  status: params.status ?? "open",
891
- kind: params.kind ?? "merge"
999
+ kind: params.kind ?? "merge",
1000
+ limit: params.limit,
1001
+ offset: params.offset
892
1002
  });
893
1003
  return {
894
1004
  data: {
895
1005
  queue: result.queue,
896
1006
  appId: result.appId,
897
- mergeRequests: result.mergeRequests
1007
+ mergeRequests: result.mergeRequests,
1008
+ pagination: result.pagination
898
1009
  },
899
1010
  warnings: [],
900
- recommendedNextActions: [],
1011
+ recommendedNextActions: result.pagination.hasMore ? [`Pass offset=${result.pagination.offset + result.pagination.limit} to load the next page.`] : [],
901
1012
  logContext: {}
902
1013
  };
903
1014
  }
@@ -909,16 +1020,19 @@ async function listAppMergeRequests(params) {
909
1020
  appId: params.appId,
910
1021
  queue: params.queue,
911
1022
  status: params.status ?? "open",
912
- kind: params.kind ?? "merge"
1023
+ kind: params.kind ?? "merge",
1024
+ limit: params.limit,
1025
+ offset: params.offset
913
1026
  });
914
1027
  return {
915
1028
  data: {
916
1029
  queue: result.queue,
917
1030
  appId: result.appId,
918
- mergeRequests: result.mergeRequests
1031
+ mergeRequests: result.mergeRequests,
1032
+ pagination: result.pagination
919
1033
  },
920
1034
  warnings: [],
921
- recommendedNextActions: [],
1035
+ recommendedNextActions: result.pagination.hasMore ? [`Pass offset=${result.pagination.offset + result.pagination.limit} to load the next page.`] : [],
922
1036
  logContext: {
923
1037
  appId: result.appId
924
1038
  }
@@ -1043,12 +1157,14 @@ async function listMembers(params) {
1043
1157
  api,
1044
1158
  cwd: params.cwd,
1045
1159
  scope: params.scope,
1046
- targetId: params.targetId ?? null
1160
+ targetId: params.targetId ?? null,
1161
+ limit: params.limit,
1162
+ offset: params.offset
1047
1163
  });
1048
1164
  return {
1049
1165
  data: result,
1050
1166
  warnings: [],
1051
- recommendedNextActions: [],
1167
+ recommendedNextActions: result.pagination.hasMore ? [`Pass offset=${result.pagination.offset + result.pagination.limit} to load the next page.`] : [],
1052
1168
  logContext: {}
1053
1169
  };
1054
1170
  }
@@ -1070,83 +1186,578 @@ async function updateMemberRole(params) {
1070
1186
  };
1071
1187
  }
1072
1188
 
1073
- // src/tools/collab/register.ts
1074
- function getAnnotations(access) {
1075
- if (access === "read") {
1189
+ // src/domain/shared.ts
1190
+ import { readCollabBinding } from "@remixhq/core/binding";
1191
+ import { findGitRoot as findGitRoot2 } from "@remixhq/core/repo";
1192
+ function unwrapResponseObject(resp, label) {
1193
+ const obj = resp?.responseObject;
1194
+ if (obj === void 0 || obj === null) {
1195
+ throw new Error(typeof resp?.message === "string" && resp.message.trim() ? resp.message : `Missing ${label} response`);
1196
+ }
1197
+ return obj;
1198
+ }
1199
+ async function maybeFindGitRoot(cwd) {
1200
+ try {
1201
+ return await findGitRoot2(cwd);
1202
+ } catch {
1203
+ return null;
1204
+ }
1205
+ }
1206
+ async function loadBindingContext(cwd) {
1207
+ if (!cwd) return { repoRoot: null, binding: null };
1208
+ const repoRoot = await maybeFindGitRoot(cwd);
1209
+ if (!repoRoot) return { repoRoot: null, binding: null };
1210
+ const binding = await readCollabBinding(repoRoot);
1211
+ return {
1212
+ repoRoot,
1213
+ binding
1214
+ };
1215
+ }
1216
+ function makeNotBoundError(message = "Repository is not bound to Remix.", hint) {
1217
+ const error = new Error(message);
1218
+ error.hint = hint ?? "Run `remix_collab_init` in this repository, or pass the explicit id for a direct read.";
1219
+ return error;
1220
+ }
1221
+ function parseJsonObject2(value) {
1222
+ if (!value) return null;
1223
+ try {
1224
+ const parsed = JSON.parse(value);
1225
+ return parsed && typeof parsed === "object" ? parsed : null;
1226
+ } catch {
1227
+ return null;
1228
+ }
1229
+ }
1230
+ function getBackendStatusCode(error) {
1231
+ if (!error || typeof error !== "object") return null;
1232
+ const hint = "hint" in error && typeof error.hint === "string" ? error.hint : null;
1233
+ const body = parseJsonObject2(hint);
1234
+ if (typeof body?.statusCode === "number") return body.statusCode;
1235
+ if (typeof body?.statusCode === "string") {
1236
+ const parsed = Number(body.statusCode);
1237
+ return Number.isFinite(parsed) ? parsed : null;
1238
+ }
1239
+ return null;
1240
+ }
1241
+ function isBackendForbidden(error) {
1242
+ return getBackendStatusCode(error) === 403;
1243
+ }
1244
+ function isBackendNotFound(error) {
1245
+ return getBackendStatusCode(error) === 404;
1246
+ }
1247
+ function createAccessDeniedError(message, hint) {
1248
+ return new RemixMcpError({
1249
+ code: ERROR_CODES.ACCESS_DENIED,
1250
+ message,
1251
+ hint: hint ?? null,
1252
+ retryable: false,
1253
+ category: "remote_state"
1254
+ });
1255
+ }
1256
+
1257
+ // src/domain/collabAdminAdapter.ts
1258
+ var MEMBERSHIP_PAGE_SIZE = 100;
1259
+ function normalizePagination(params) {
1260
+ const rawLimit = typeof params?.limit === "number" ? Math.trunc(params.limit) : 25;
1261
+ const rawOffset = typeof params?.offset === "number" ? Math.trunc(params.offset) : 0;
1262
+ return {
1263
+ limit: Math.max(1, Math.min(50, rawLimit)),
1264
+ offset: Math.max(0, rawOffset)
1265
+ };
1266
+ }
1267
+ async function resolveScopeTarget(api, params) {
1268
+ const explicitTargetId = params.targetId?.trim();
1269
+ const bindingContext = await loadBindingContext(params.cwd);
1270
+ if (explicitTargetId) {
1076
1271
  return {
1077
- readOnlyHint: true,
1078
- idempotentHint: true,
1079
- openWorldHint: false
1272
+ scopeType: params.scope,
1273
+ targetId: explicitTargetId,
1274
+ repoRoot: bindingContext.repoRoot
1275
+ };
1276
+ }
1277
+ if (!bindingContext.binding) {
1278
+ throw makeNotBoundError("Scope target was not provided and the current repository is not bound to Remix.");
1279
+ }
1280
+ if (params.scope === "app") {
1281
+ return {
1282
+ scopeType: "app",
1283
+ targetId: bindingContext.binding.currentAppId,
1284
+ repoRoot: bindingContext.repoRoot
1285
+ };
1286
+ }
1287
+ const appContext = unwrapResponseObject(
1288
+ await api.getAppContext(bindingContext.binding.currentAppId),
1289
+ "bound app context"
1290
+ );
1291
+ if (params.scope === "project") {
1292
+ if (!appContext.readableScopes.project) {
1293
+ throw createAccessDeniedError(
1294
+ "The bound app's project is not readable to the current user.",
1295
+ "Use `scope=app` for app-level diagnostics, or pass an explicit readable project id."
1296
+ );
1297
+ }
1298
+ return {
1299
+ scopeType: "project",
1300
+ targetId: appContext.projectId,
1301
+ repoRoot: bindingContext.repoRoot
1080
1302
  };
1081
1303
  }
1304
+ if (!appContext.readableScopes.organization) {
1305
+ throw createAccessDeniedError(
1306
+ "The bound app's organization is not readable to the current user.",
1307
+ "Use `scope=app` or `scope=project` for narrower diagnostics, or pass an explicit readable organization id."
1308
+ );
1309
+ }
1082
1310
  return {
1083
- readOnlyHint: false,
1084
- destructiveHint: access === "local_write",
1085
- idempotentHint: false,
1086
- openWorldHint: false
1311
+ scopeType: "organization",
1312
+ targetId: appContext.organizationId,
1313
+ repoRoot: bindingContext.repoRoot
1087
1314
  };
1088
1315
  }
1089
- function buildSuccessEnvelope(tool, requestId, result) {
1316
+ async function getScopeEntity(api, scopeType, targetId) {
1317
+ if (scopeType === "organization") {
1318
+ return {
1319
+ organization: unwrapResponseObject(await api.getOrganization(targetId), "organization"),
1320
+ project: null,
1321
+ app: null
1322
+ };
1323
+ }
1324
+ if (scopeType === "project") {
1325
+ const project2 = unwrapResponseObject(await api.getProject(targetId), "project");
1326
+ const organizationId2 = typeof project2.organizationId === "string" ? project2.organizationId : null;
1327
+ return {
1328
+ organization: organizationId2 == null ? null : unwrapResponseObject(await api.getOrganization(organizationId2), "organization"),
1329
+ project: project2,
1330
+ app: null
1331
+ };
1332
+ }
1333
+ const app = unwrapResponseObject(await api.getApp(targetId), "app");
1334
+ const projectId = typeof app.projectId === "string" ? app.projectId : null;
1335
+ const project = projectId == null ? null : unwrapResponseObject(await api.getProject(projectId), "project");
1336
+ const organizationId = typeof project?.organizationId === "string" ? project.organizationId : null;
1090
1337
  return {
1091
- schemaVersion: SCHEMA_VERSION,
1092
- ok: true,
1093
- tool,
1094
- requestId: requestId ?? null,
1095
- data: result.data,
1096
- warnings: result.warnings ?? [],
1097
- risks: result.risks ?? [],
1098
- recommendedNextActions: result.recommendedNextActions ?? []
1338
+ organization: organizationId == null ? null : unwrapResponseObject(await api.getOrganization(organizationId), "organization"),
1339
+ project,
1340
+ app
1099
1341
  };
1100
1342
  }
1101
- function deriveErrorRisks(tool, normalized) {
1102
- if ((tool === "remix_collab_add" || tool === "remix_collab_add_change_step") && normalized.message === "Change step succeeded remotely, but automatic local sync failed.") {
1103
- return ["The change step succeeded remotely, but the local repository may need manual recovery or a follow-up sync."];
1343
+ function mapProjectRoleToAppRole(role) {
1344
+ switch (role) {
1345
+ case "owner":
1346
+ case "maintainer":
1347
+ return "maintainer";
1348
+ case "editor":
1349
+ return "editor";
1350
+ case "viewer":
1351
+ return "viewer";
1352
+ default:
1353
+ return null;
1104
1354
  }
1105
- if (normalized.code === "DESTRUCTIVE_OPERATION_BLOCKED") {
1106
- return ["A policy guard blocked a potentially destructive or state-mutating operation."];
1355
+ }
1356
+ function strongestRole(...roles) {
1357
+ if (roles.includes("owner")) return "owner";
1358
+ if (roles.includes("maintainer")) return "maintainer";
1359
+ if (roles.includes("editor")) return "editor";
1360
+ if (roles.includes("viewer")) return "viewer";
1361
+ return null;
1362
+ }
1363
+ function resolveAppAccessSource(params) {
1364
+ if (params.directAppRole && params.inheritedProjectRole) return "both";
1365
+ if (params.directAppRole) return "direct_app_membership";
1366
+ if (params.inheritedProjectRole) return "project_membership";
1367
+ return "none";
1368
+ }
1369
+ function buildEffectiveAppAccess(params) {
1370
+ const directAppRole = params.directAppRole ?? null;
1371
+ const inheritedProjectRole = mapProjectRoleToAppRole(params.projectRole);
1372
+ const effectiveRole = strongestRole(directAppRole, inheritedProjectRole);
1373
+ return {
1374
+ effectiveRole,
1375
+ directAppRole,
1376
+ projectRole: params.projectRole,
1377
+ inheritedProjectRole,
1378
+ accessSource: resolveAppAccessSource({ directAppRole, inheritedProjectRole }),
1379
+ canReadWorkflow: effectiveRole !== null,
1380
+ canEdit: effectiveRole === "owner" || effectiveRole === "maintainer" || effectiveRole === "editor",
1381
+ canManage: effectiveRole === "owner" || effectiveRole === "maintainer",
1382
+ isOwner: directAppRole === "owner"
1383
+ };
1384
+ }
1385
+ async function listMembersForScope(api, scopeType, targetId, params) {
1386
+ if (scopeType === "organization") {
1387
+ return unwrapResponseObject(await api.listOrganizationMembers(targetId, params), "organization members");
1107
1388
  }
1108
- if (normalized.code === "REPO_LOCK_TIMEOUT") {
1109
- return ["Another Remix mutation was already in progress for this repository, so the local mutation did not run."];
1389
+ if (scopeType === "project") {
1390
+ return unwrapResponseObject(await api.listProjectMembers(targetId, params), "project members");
1110
1391
  }
1111
- if (normalized.code === "REPO_STATE_CHANGED_DURING_OPERATION") {
1112
- return ["The repository changed during the operation, so Remix aborted before applying a destructive local mutation."];
1392
+ return unwrapResponseObject(await api.listAppMembers(targetId, params), "app members");
1393
+ }
1394
+ async function loadFirstPageSample(fetchPage) {
1395
+ const items = await fetchPage(MEMBERSHIP_PAGE_SIZE, 0);
1396
+ const hasMore = items.length < MEMBERSHIP_PAGE_SIZE ? false : (await fetchPage(1, MEMBERSHIP_PAGE_SIZE)).length > 0;
1397
+ return {
1398
+ items,
1399
+ pageInfo: {
1400
+ limit: MEMBERSHIP_PAGE_SIZE,
1401
+ offset: 0,
1402
+ hasMore
1403
+ }
1404
+ };
1405
+ }
1406
+ function emptyFirstPageSample() {
1407
+ return {
1408
+ items: [],
1409
+ pageInfo: {
1410
+ limit: MEMBERSHIP_PAGE_SIZE,
1411
+ offset: 0,
1412
+ hasMore: false
1413
+ }
1414
+ };
1415
+ }
1416
+ async function listInvitesForScope(api, scopeType, targetId, params) {
1417
+ if (scopeType === "organization") {
1418
+ return unwrapResponseObject(
1419
+ await api.listOrganizationInvites(targetId, params),
1420
+ "organization invites"
1421
+ );
1422
+ }
1423
+ if (scopeType === "project") {
1424
+ return unwrapResponseObject(
1425
+ await api.listProjectInvites(targetId, params),
1426
+ "project invites"
1427
+ );
1428
+ }
1429
+ return unwrapResponseObject(await api.listAppInvites(targetId, params), "app invites");
1430
+ }
1431
+ function getSelfMember(meId, members) {
1432
+ return meId == null ? null : members.find((member) => member.userId === meId || member.user_id === meId) ?? null;
1433
+ }
1434
+ async function findPendingInviteForEmailForScope(api, scopeType, targetId, email) {
1435
+ if (email == null) return null;
1436
+ let offset = 0;
1437
+ for (; ; ) {
1438
+ const invites = await listInvitesForScope(api, scopeType, targetId, {
1439
+ limit: MEMBERSHIP_PAGE_SIZE,
1440
+ offset
1441
+ });
1442
+ const match = invites.find((invite) => invite.email.toLowerCase() === email && invite.state === "pending") ?? null;
1443
+ if (match) return match;
1444
+ if (invites.length < MEMBERSHIP_PAGE_SIZE) return null;
1445
+ offset += MEMBERSHIP_PAGE_SIZE;
1113
1446
  }
1114
- return [];
1115
1447
  }
1116
- function buildErrorEnvelope(tool, requestId, error) {
1117
- const normalized = normalizeToolError(error);
1118
- 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."] : [];
1448
+ async function findSelfMemberForScope(api, scopeType, targetId, meId) {
1449
+ if (meId == null) return null;
1450
+ let offset = 0;
1451
+ for (; ; ) {
1452
+ const members = scopeType === "organization" ? unwrapResponseObject(
1453
+ await api.listOrganizationMembers(targetId, { limit: MEMBERSHIP_PAGE_SIZE, offset }),
1454
+ "organization members"
1455
+ ) : scopeType === "project" ? unwrapResponseObject(
1456
+ await api.listProjectMembers(targetId, { limit: MEMBERSHIP_PAGE_SIZE, offset }),
1457
+ "project members"
1458
+ ) : unwrapResponseObject(
1459
+ await api.listAppMembers(targetId, { limit: MEMBERSHIP_PAGE_SIZE, offset }),
1460
+ "app members"
1461
+ );
1462
+ const match = getSelfMember(meId, members);
1463
+ if (match) return match;
1464
+ if (members.length < MEMBERSHIP_PAGE_SIZE) return null;
1465
+ offset += MEMBERSHIP_PAGE_SIZE;
1466
+ }
1467
+ }
1468
+ async function listInvites(params) {
1469
+ const api = await createApiClient();
1470
+ const target = await resolveScopeTarget(api, {
1471
+ scope: params.scope ?? "project",
1472
+ targetId: params.targetId,
1473
+ cwd: params.cwd
1474
+ });
1475
+ const pagination = normalizePagination(params);
1476
+ const invites = await listInvitesForScope(api, target.scopeType, target.targetId, {
1477
+ limit: pagination.limit + 1,
1478
+ offset: pagination.offset
1479
+ });
1119
1480
  return {
1120
- schemaVersion: SCHEMA_VERSION,
1121
- ok: false,
1122
- tool,
1123
- requestId: requestId ?? null,
1124
- error: normalized,
1481
+ data: {
1482
+ scopeType: target.scopeType,
1483
+ targetId: target.targetId,
1484
+ invites: invites.slice(0, pagination.limit),
1485
+ pagination: {
1486
+ ...pagination,
1487
+ hasMore: invites.length > pagination.limit
1488
+ }
1489
+ },
1125
1490
  warnings: [],
1126
- risks: deriveErrorRisks(tool, normalized),
1127
- recommendedNextActions
1491
+ recommendedNextActions: invites.length > pagination.limit ? [`Pass offset=${pagination.offset + pagination.limit} to load the next page.`] : invites.length > 0 ? ["Use `remix_collab_resend_invite` or `remix_collab_revoke_invite` for lifecycle actions on a selected invite id."] : ["Use `remix_collab_invite` if you need to create a new invitation for this scope."],
1492
+ logContext: {
1493
+ repoRoot: target.repoRoot
1494
+ }
1128
1495
  };
1129
1496
  }
1130
- function registerTool(server, context, params) {
1131
- const errorSchema = makeErrorSchema();
1132
- server.registerTool(
1133
- params.name,
1134
- {
1135
- title: params.name,
1136
- description: params.description,
1137
- inputSchema: params.inputSchema,
1138
- outputSchema: params.outputSchema,
1139
- annotations: getAnnotations(params.access)
1497
+ async function resendInvite(params) {
1498
+ const api = await createApiClient();
1499
+ const target = await resolveScopeTarget(api, {
1500
+ scope: params.scope ?? "project",
1501
+ targetId: params.targetId,
1502
+ cwd: params.cwd
1503
+ });
1504
+ const response = target.scopeType === "organization" ? await api.resendOrganizationInvite(target.targetId, params.inviteId, { ttlDays: params.ttlDays }) : target.scopeType === "project" ? await api.resendProjectInvite(target.targetId, params.inviteId, { ttlDays: params.ttlDays }) : await api.resendAppInvite(target.targetId, params.inviteId, { ttlDays: params.ttlDays });
1505
+ return {
1506
+ data: unwrapResponseObject(response, "resent invite"),
1507
+ warnings: [],
1508
+ recommendedNextActions: ["Use `remix_collab_list_invites` to confirm the refreshed expiry and delivery state."],
1509
+ logContext: {
1510
+ repoRoot: target.repoRoot
1511
+ }
1512
+ };
1513
+ }
1514
+ async function revokeInvite(params) {
1515
+ const api = await createApiClient();
1516
+ const target = await resolveScopeTarget(api, {
1517
+ scope: params.scope ?? "project",
1518
+ targetId: params.targetId,
1519
+ cwd: params.cwd
1520
+ });
1521
+ if (target.scopeType === "organization") {
1522
+ await api.revokeOrganizationInvite(target.targetId, params.inviteId);
1523
+ } else if (target.scopeType === "project") {
1524
+ await api.revokeProjectInvite(target.targetId, params.inviteId);
1525
+ } else {
1526
+ await api.revokeAppInvite(target.targetId, params.inviteId);
1527
+ }
1528
+ return {
1529
+ data: {
1530
+ scopeType: target.scopeType,
1531
+ targetId: target.targetId,
1532
+ inviteId: params.inviteId,
1533
+ revoked: true
1140
1534
  },
1141
- async (rawArgs) => {
1142
- const requestId = typeof rawArgs.requestId === "string" ? rawArgs.requestId : void 0;
1143
- const startedAt = Date.now();
1144
- try {
1145
- assertToolAccess(context.policy, params.access);
1146
- const result = await params.run(rawArgs);
1147
- const envelope = buildSuccessEnvelope(params.name, requestId, result);
1148
- params.outputSchema.parse(envelope);
1149
- context.logger.log({
1535
+ warnings: [],
1536
+ recommendedNextActions: ["Use `remix_collab_list_invites` to confirm the invite state is now `revoked`."],
1537
+ logContext: {
1538
+ repoRoot: target.repoRoot
1539
+ }
1540
+ };
1541
+ }
1542
+ async function acceptInvitation(params) {
1543
+ const api = await createApiClient();
1544
+ const result = unwrapResponseObject(await api.acceptInvitation({ token: params.token }), "accepted invitation");
1545
+ return {
1546
+ data: result,
1547
+ warnings: [],
1548
+ recommendedNextActions: result.redirectPath ? [`The invitation is accepted. Use the returned redirect metadata to continue with the newly granted Remix scope.`] : [],
1549
+ logContext: {}
1550
+ };
1551
+ }
1552
+ async function accessDebug(params) {
1553
+ const api = await createApiClient();
1554
+ const target = await resolveScopeTarget(api, {
1555
+ scope: params.scope ?? "project",
1556
+ targetId: params.targetId,
1557
+ cwd: params.cwd
1558
+ });
1559
+ const bindingContext = await loadBindingContext(params.cwd);
1560
+ const viewer = unwrapResponseObject(await api.getMe(), "current user");
1561
+ const viewerId = typeof viewer.id === "string" ? viewer.id : null;
1562
+ const viewerEmail = typeof viewer.email === "string" ? viewer.email.toLowerCase() : null;
1563
+ const warnings = [...bindingContext.binding ? [] : ["No local Remix binding was detected for the provided cwd."]];
1564
+ let entities;
1565
+ let appContext = null;
1566
+ let membersPage = emptyFirstPageSample();
1567
+ let invitesPage = emptyFirstPageSample();
1568
+ let viewerMember = null;
1569
+ let viewerProjectMember = null;
1570
+ let inviteForViewerEmail = null;
1571
+ let effectiveAppAccess = null;
1572
+ if (target.scopeType !== "app") {
1573
+ [entities, membersPage, invitesPage] = await Promise.all([
1574
+ getScopeEntity(api, target.scopeType, target.targetId),
1575
+ loadFirstPageSample((limit, offset) => listMembersForScope(api, target.scopeType, target.targetId, { limit, offset })),
1576
+ loadFirstPageSample((limit, offset) => listInvitesForScope(api, target.scopeType, target.targetId, { limit, offset }))
1577
+ ]);
1578
+ viewerMember = await findSelfMemberForScope(api, target.scopeType, target.targetId, viewerId);
1579
+ inviteForViewerEmail = await findPendingInviteForEmailForScope(api, target.scopeType, target.targetId, viewerEmail);
1580
+ } else {
1581
+ const [app, loadedAppContext] = await Promise.all([
1582
+ unwrapResponseObject(await api.getApp(target.targetId), "app"),
1583
+ unwrapResponseObject(await api.getAppContext(target.targetId), "app context")
1584
+ ]);
1585
+ appContext = loadedAppContext;
1586
+ entities = {
1587
+ organization: null,
1588
+ project: null,
1589
+ app
1590
+ };
1591
+ if (appContext.readableScopes.project) {
1592
+ try {
1593
+ entities.project = unwrapResponseObject(await api.getProject(appContext.projectId), "project");
1594
+ } catch (error) {
1595
+ if (!isBackendForbidden(error) && !isBackendNotFound(error)) throw error;
1596
+ warnings.push("The app is readable, but its project/workspace metadata is not currently readable to the current user.");
1597
+ }
1598
+ } else {
1599
+ warnings.push("The app is readable, but its project/workspace is not readable to the current user.");
1600
+ }
1601
+ if (appContext.readableScopes.organization) {
1602
+ try {
1603
+ entities.organization = unwrapResponseObject(await api.getOrganization(appContext.organizationId), "organization");
1604
+ } catch (error) {
1605
+ if (!isBackendForbidden(error) && !isBackendNotFound(error)) throw error;
1606
+ warnings.push("The app is readable, but its organization metadata is not currently readable to the current user.");
1607
+ }
1608
+ } else {
1609
+ warnings.push("The app's organization is not readable to the current user.");
1610
+ }
1611
+ effectiveAppAccess = {
1612
+ ...buildEffectiveAppAccess({
1613
+ directAppRole: appContext.roles.appRole,
1614
+ projectRole: appContext.roles.projectRole
1615
+ }),
1616
+ accessPath: appContext.accessPath,
1617
+ readableScopes: appContext.readableScopes,
1618
+ visibility: appContext.visibility,
1619
+ organizationRole: appContext.roles.organizationRole ?? null
1620
+ };
1621
+ if (appContext.capabilities.canReadWorkflow) {
1622
+ membersPage = await loadFirstPageSample((limit, offset) => listMembersForScope(api, "app", target.targetId, { limit, offset }));
1623
+ viewerMember = await findSelfMemberForScope(api, "app", target.targetId, viewerId);
1624
+ if (appContext.readableScopes.project) {
1625
+ viewerProjectMember = await findSelfMemberForScope(api, "project", appContext.projectId, viewerId);
1626
+ }
1627
+ try {
1628
+ invitesPage = await loadFirstPageSample((limit, offset) => listInvitesForScope(api, "app", target.targetId, { limit, offset }));
1629
+ inviteForViewerEmail = await findPendingInviteForEmailForScope(api, "app", target.targetId, viewerEmail);
1630
+ } catch (error) {
1631
+ if (!isBackendForbidden(error) && !isBackendNotFound(error)) throw error;
1632
+ warnings.push("App invite data is not readable to the current user, so invite diagnostics are partial.");
1633
+ }
1634
+ } else {
1635
+ warnings.push("The current viewer can read the app, but cannot read workflow-scoped app membership data.");
1636
+ }
1637
+ }
1638
+ if (membersPage.pageInfo.hasMore) {
1639
+ warnings.push("The `members` array is a first-page sample only; use the scope member list tool to inspect the remaining entries.");
1640
+ }
1641
+ if (invitesPage.pageInfo.hasMore) {
1642
+ warnings.push("The `invites` array is a first-page sample only; use the invite list tool with pagination to inspect the remaining entries.");
1643
+ }
1644
+ return {
1645
+ data: {
1646
+ viewer,
1647
+ scope: {
1648
+ scopeType: target.scopeType,
1649
+ targetId: target.targetId
1650
+ },
1651
+ entities,
1652
+ binding: {
1653
+ repoRoot: bindingContext.repoRoot,
1654
+ binding: bindingContext.binding
1655
+ },
1656
+ access: {
1657
+ appContext,
1658
+ viewerMember,
1659
+ viewerProjectMember,
1660
+ inviteForViewerEmail,
1661
+ effectiveAppAccess: effectiveAppAccess ?? null
1662
+ },
1663
+ members: membersPage.items,
1664
+ membersPageInfo: membersPage.pageInfo,
1665
+ invites: invitesPage.items,
1666
+ invitesPageInfo: invitesPage.pageInfo
1667
+ },
1668
+ warnings,
1669
+ recommendedNextActions: inviteForViewerEmail ? ["A pending invite exists for the current viewer email. Use `remix_collab_accept_invitation` if the viewer should accept it."] : target.scopeType === "app" ? ["Use `access.appContext` and `access.effectiveAppAccess` together to explain app-level visibility versus workspace-level visibility."] : ["Use the member and invite lists above to explain missing access, role mismatches, or stale invite state."],
1670
+ logContext: {
1671
+ repoRoot: target.repoRoot,
1672
+ appId: entities.app?.id ?? bindingContext.binding?.currentAppId ?? null
1673
+ }
1674
+ };
1675
+ }
1676
+
1677
+ // src/tools/collab/register.ts
1678
+ function getAnnotations(access, options) {
1679
+ if (access === "read") {
1680
+ return {
1681
+ readOnlyHint: true,
1682
+ idempotentHint: true,
1683
+ openWorldHint: false
1684
+ };
1685
+ }
1686
+ return {
1687
+ readOnlyHint: false,
1688
+ destructiveHint: access === "local_write",
1689
+ idempotentHint: options?.idempotent ?? false,
1690
+ openWorldHint: false
1691
+ };
1692
+ }
1693
+ function buildSuccessEnvelope(tool, requestId, result) {
1694
+ return {
1695
+ schemaVersion: SCHEMA_VERSION,
1696
+ ok: true,
1697
+ tool,
1698
+ requestId: requestId ?? null,
1699
+ data: result.data,
1700
+ warnings: result.warnings ?? [],
1701
+ risks: result.risks ?? [],
1702
+ recommendedNextActions: result.recommendedNextActions ?? []
1703
+ };
1704
+ }
1705
+ function deriveErrorRisks(tool, normalized) {
1706
+ if (isFinalizeTurnLocalSyncFailure(tool, normalized)) {
1707
+ return ["The change step succeeded remotely, but the local repository may need manual recovery or a follow-up sync."];
1708
+ }
1709
+ if (normalized.code === "DESTRUCTIVE_OPERATION_BLOCKED") {
1710
+ return ["A policy guard blocked a potentially destructive or state-mutating operation."];
1711
+ }
1712
+ if (normalized.code === "REPO_LOCK_TIMEOUT") {
1713
+ return ["Another Remix mutation was already in progress for this repository, so the local mutation did not run."];
1714
+ }
1715
+ if (normalized.code === "REPO_STATE_CHANGED_DURING_OPERATION") {
1716
+ return ["The repository changed during the operation, so Remix aborted before applying a destructive local mutation."];
1717
+ }
1718
+ return [];
1719
+ }
1720
+ function isFinalizeTurnLocalSyncFailure(tool, normalized) {
1721
+ return tool === "remix_collab_finalize_turn" && normalized.message === "Change step succeeded remotely, but automatic local sync failed.";
1722
+ }
1723
+ function buildErrorEnvelope(tool, requestId, error) {
1724
+ const normalized = normalizeToolError(error);
1725
+ const recommendedNextActions = isFinalizeTurnLocalSyncFailure(tool, normalized) ? [
1726
+ "Run `remix_collab_status` to confirm the bound repo state before attempting recovery.",
1727
+ "Run `remix_collab_sync_preview` next, then `remix_collab_sync_apply` with `confirm=true` if the preview looks correct.",
1728
+ "Inspect `error.hint` for any preserved diff backup path before retrying local recovery, and do not rerun `remix_collab_finalize_turn` immediately."
1729
+ ] : 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."] : [];
1730
+ return {
1731
+ schemaVersion: SCHEMA_VERSION,
1732
+ ok: false,
1733
+ tool,
1734
+ requestId: requestId ?? null,
1735
+ error: normalized,
1736
+ warnings: [],
1737
+ risks: deriveErrorRisks(tool, normalized),
1738
+ recommendedNextActions
1739
+ };
1740
+ }
1741
+ function registerTool(server, context, params) {
1742
+ const errorSchema = makeErrorSchema();
1743
+ server.registerTool(
1744
+ params.name,
1745
+ {
1746
+ title: params.name,
1747
+ description: params.description,
1748
+ inputSchema: params.inputSchema,
1749
+ outputSchema: params.outputSchema,
1750
+ annotations: params.annotations ?? getAnnotations(params.access)
1751
+ },
1752
+ async (rawArgs) => {
1753
+ const requestId = typeof rawArgs.requestId === "string" ? rawArgs.requestId : void 0;
1754
+ const startedAt = Date.now();
1755
+ try {
1756
+ assertToolAccess(context.policy, params.access);
1757
+ const result = await params.run(rawArgs);
1758
+ const envelope = buildSuccessEnvelope(params.name, requestId, result);
1759
+ params.outputSchema.parse(envelope);
1760
+ context.logger.log({
1150
1761
  level: "info",
1151
1762
  message: "tool_completed",
1152
1763
  tool: params.name,
@@ -1210,13 +1821,20 @@ function registerCollabTools(server, context) {
1210
1821
  });
1211
1822
  registerTool(server, context, {
1212
1823
  name: "remix_collab_list",
1213
- description: "List Remix apps visible to the current authenticated user.",
1824
+ description: "List Remix apps visible to the current authenticated user. Defaults to membership-oriented discovery; pass accessScope=all_readable explicitly for broader readable discovery.",
1214
1825
  access: "read",
1215
1826
  inputSchema: listInputSchema,
1216
1827
  outputSchema: listSuccessSchema,
1217
1828
  run: async (args) => {
1218
1829
  const input = z3.object(listInputSchema).parse(args);
1219
- return listApps({ forked: input.forked });
1830
+ return listApps({
1831
+ ownership: input.ownership,
1832
+ accessScope: input.accessScope,
1833
+ createdBy: input.createdBy,
1834
+ forked: input.forked,
1835
+ limit: input.limit,
1836
+ offset: input.offset
1837
+ });
1220
1838
  }
1221
1839
  });
1222
1840
  registerTool(server, context, {
@@ -1253,89 +1871,25 @@ function registerCollabTools(server, context) {
1253
1871
  }
1254
1872
  });
1255
1873
  registerTool(server, context, {
1256
- name: "remix_collab_add",
1257
- description: "Authoritative way to record completed code changes for the current bound repository, using the live worktree diff by default instead of raw git commit or push.",
1258
- access: "local_write",
1259
- inputSchema: addInputSchema,
1260
- outputSchema: addSuccessSchema,
1261
- run: async (args) => {
1262
- const input = z3.object(addInputSchema).parse(args);
1263
- const cwd = resolvePolicyCwd(context.policy, input.cwd);
1264
- const diffSource = input.diffSource ?? "worktree";
1265
- if (diffSource === "external") {
1266
- const externalDiff = input.externalDiff ?? "";
1267
- assertDiffWithinLimit(context.policy, externalDiff);
1268
- }
1269
- return addCollabStep({
1270
- cwd,
1271
- prompt: input.prompt,
1272
- assistantResponse: input.assistantResponse,
1273
- diffSource,
1274
- externalDiff: input.externalDiff,
1275
- allowBranchMismatch: input.allowBranchMismatch ?? false,
1276
- idempotencyKey: input.idempotencyKey,
1277
- agent: context.agentMetadata
1278
- });
1279
- }
1280
- });
1281
- registerTool(server, context, {
1282
- name: "remix_collab_add_change_step",
1283
- description: "Alias of remix_collab_add with a more explicit name: record a code-diff change step for the current bound repository.",
1874
+ name: "remix_collab_finalize_turn",
1875
+ description: "Primary turn recorder for the current bound repository. Call this exactly once before the final response; it records a changed turn when the worktree has a diff, records a no-diff turn when it does not, and can accept an explicit external diff when needed.",
1284
1876
  access: "local_write",
1285
- inputSchema: addInputSchema,
1286
- outputSchema: addSuccessSchema,
1877
+ inputSchema: finalizeTurnInputSchema,
1878
+ outputSchema: finalizeTurnSuccessSchema,
1879
+ annotations: getAnnotations("local_write", { idempotent: true }),
1287
1880
  run: async (args) => {
1288
- const input = z3.object(addInputSchema).parse(args);
1881
+ const input = z3.object(finalizeTurnInputSchema).parse(args);
1289
1882
  const cwd = resolvePolicyCwd(context.policy, input.cwd);
1290
- const diffSource = input.diffSource ?? "worktree";
1291
- if (diffSource === "external") {
1292
- const externalDiff = input.externalDiff ?? "";
1293
- assertDiffWithinLimit(context.policy, externalDiff);
1883
+ if ((input.diffSource ?? "worktree") === "external" || typeof input.externalDiff === "string") {
1884
+ assertDiffWithinLimit(context.policy, input.externalDiff ?? "");
1294
1885
  }
1295
- return addCollabStep({
1886
+ return finalizeCollabTurn({
1296
1887
  cwd,
1297
1888
  prompt: input.prompt,
1298
1889
  assistantResponse: input.assistantResponse,
1299
- diffSource,
1890
+ diffSource: input.diffSource,
1300
1891
  externalDiff: input.externalDiff,
1301
- allowBranchMismatch: input.allowBranchMismatch ?? false,
1302
- idempotencyKey: input.idempotencyKey,
1303
- agent: context.agentMetadata
1304
- });
1305
- }
1306
- });
1307
- registerTool(server, context, {
1308
- name: "remix_collab_record_turn",
1309
- description: "Record one no-diff collaboration turn for the current bound repository after a completed assistant response. This is for prompt/response history only and will fail if the worktree has code changes.",
1310
- access: "remote_write",
1311
- inputSchema: recordTurnInputSchema,
1312
- outputSchema: recordTurnSuccessSchema,
1313
- run: async (args) => {
1314
- const input = z3.object(recordTurnInputSchema).parse(args);
1315
- const cwd = resolvePolicyCwd(context.policy, input.cwd);
1316
- return recordCollabTurn({
1317
- cwd,
1318
- prompt: input.prompt,
1319
- assistantResponse: input.assistantResponse,
1320
- allowBranchMismatch: input.allowBranchMismatch ?? false,
1321
- idempotencyKey: input.idempotencyKey,
1322
- agent: context.agentMetadata
1323
- });
1324
- }
1325
- });
1326
- registerTool(server, context, {
1327
- name: "remix_collab_record_no_diff_turn",
1328
- description: "Alias of remix_collab_record_turn with a more explicit name: record a prompt/response turn only when the worktree has no code diff.",
1329
- access: "remote_write",
1330
- inputSchema: recordTurnInputSchema,
1331
- outputSchema: recordTurnSuccessSchema,
1332
- run: async (args) => {
1333
- const input = z3.object(recordTurnInputSchema).parse(args);
1334
- const cwd = resolvePolicyCwd(context.policy, input.cwd);
1335
- return recordCollabTurn({
1336
- cwd,
1337
- prompt: input.prompt,
1338
- assistantResponse: input.assistantResponse,
1892
+ sync: input.sync,
1339
1893
  allowBranchMismatch: input.allowBranchMismatch ?? false,
1340
1894
  idempotencyKey: input.idempotencyKey,
1341
1895
  agent: context.agentMetadata
@@ -1387,7 +1941,12 @@ function registerCollabTools(server, context) {
1387
1941
  outputSchema: mergeRequestQueueSuccessSchema,
1388
1942
  run: async (args) => {
1389
1943
  const input = z3.object(reviewQueueInputSchema).parse(args);
1390
- return reviewQueue({ status: input.status, kind: input.kind });
1944
+ return reviewQueue({
1945
+ status: input.status,
1946
+ kind: input.kind,
1947
+ limit: input.limit,
1948
+ offset: input.offset
1949
+ });
1391
1950
  }
1392
1951
  });
1393
1952
  registerTool(server, context, {
@@ -1398,7 +1957,12 @@ function registerCollabTools(server, context) {
1398
1957
  outputSchema: mergeRequestQueueSuccessSchema,
1399
1958
  run: async (args) => {
1400
1959
  const input = z3.object(myMergeRequestsInputSchema).parse(args);
1401
- return myMergeRequests({ status: input.status, kind: input.kind });
1960
+ return myMergeRequests({
1961
+ status: input.status,
1962
+ kind: input.kind,
1963
+ limit: input.limit,
1964
+ offset: input.offset
1965
+ });
1402
1966
  }
1403
1967
  });
1404
1968
  registerTool(server, context, {
@@ -1415,7 +1979,9 @@ function registerCollabTools(server, context) {
1415
1979
  appId: input.appId,
1416
1980
  queue: input.queue,
1417
1981
  status: input.status,
1418
- kind: input.kind
1982
+ kind: input.kind,
1983
+ limit: input.limit,
1984
+ offset: input.offset
1419
1985
  });
1420
1986
  }
1421
1987
  });
@@ -1549,6 +2115,92 @@ function registerCollabTools(server, context) {
1549
2115
  return listMembers({
1550
2116
  cwd,
1551
2117
  scope: input.scope,
2118
+ targetId: input.targetId,
2119
+ limit: input.limit,
2120
+ offset: input.offset
2121
+ });
2122
+ }
2123
+ });
2124
+ registerTool(server, context, {
2125
+ name: "remix_collab_list_invites",
2126
+ description: "List invitations for an organization, project, or app, using the current repository binding unless targetId is provided.",
2127
+ access: "read",
2128
+ inputSchema: listInvitesInputSchema,
2129
+ outputSchema: listInvitesSuccessSchema,
2130
+ run: async (args) => {
2131
+ const input = z3.object(listInvitesInputSchema).parse(args);
2132
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
2133
+ return listInvites({
2134
+ cwd,
2135
+ scope: input.scope ?? "project",
2136
+ targetId: input.targetId,
2137
+ limit: input.limit,
2138
+ offset: input.offset
2139
+ });
2140
+ }
2141
+ });
2142
+ registerTool(server, context, {
2143
+ name: "remix_collab_resend_invite",
2144
+ description: "Resend an existing invitation for an organization, project, or app, using the current repository binding unless targetId is provided.",
2145
+ access: "remote_write",
2146
+ inputSchema: resendInviteInputSchema,
2147
+ outputSchema: resendInviteSuccessSchema,
2148
+ run: async (args) => {
2149
+ const input = z3.object(resendInviteInputSchema).parse(args);
2150
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
2151
+ return resendInvite({
2152
+ cwd,
2153
+ scope: input.scope ?? "project",
2154
+ targetId: input.targetId,
2155
+ inviteId: input.inviteId,
2156
+ ttlDays: input.ttlDays
2157
+ });
2158
+ }
2159
+ });
2160
+ registerTool(server, context, {
2161
+ name: "remix_collab_revoke_invite",
2162
+ description: "Revoke an existing invitation for an organization, project, or app, using the current repository binding unless targetId is provided.",
2163
+ access: "remote_write",
2164
+ inputSchema: revokeInviteInputSchema,
2165
+ outputSchema: revokeInviteSuccessSchema,
2166
+ run: async (args) => {
2167
+ const input = z3.object(revokeInviteInputSchema).parse(args);
2168
+ assertConfirm(input.confirm, "remix_collab_revoke_invite");
2169
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
2170
+ return revokeInvite({
2171
+ cwd,
2172
+ scope: input.scope ?? "project",
2173
+ targetId: input.targetId,
2174
+ inviteId: input.inviteId
2175
+ });
2176
+ }
2177
+ });
2178
+ registerTool(server, context, {
2179
+ name: "remix_collab_accept_invitation",
2180
+ description: "Accept an invitation token for the currently authenticated Remix user.",
2181
+ access: "remote_write",
2182
+ inputSchema: acceptInvitationInputSchema,
2183
+ outputSchema: acceptInvitationSuccessSchema,
2184
+ annotations: getAnnotations("remote_write", { idempotent: true }),
2185
+ run: async (args) => {
2186
+ const input = z3.object(acceptInvitationInputSchema).parse(args);
2187
+ return acceptInvitation({
2188
+ token: input.token
2189
+ });
2190
+ }
2191
+ });
2192
+ registerTool(server, context, {
2193
+ name: "remix_access_debug",
2194
+ description: "Explain why the current user does or does not have access to an organization, project, or app by composing binding, membership, invite, and scope context.",
2195
+ access: "read",
2196
+ inputSchema: accessDebugInputSchema,
2197
+ outputSchema: accessDebugSuccessSchema,
2198
+ run: async (args) => {
2199
+ const input = z3.object(accessDebugInputSchema).parse(args);
2200
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
2201
+ return accessDebug({
2202
+ cwd,
2203
+ scope: input.scope ?? "project",
1552
2204
  targetId: input.targetId
1553
2205
  });
1554
2206
  }
@@ -1573,92 +2225,663 @@ function registerCollabTools(server, context) {
1573
2225
  });
1574
2226
  }
1575
2227
 
1576
- // src/tools/memory/register.ts
2228
+ // src/tools/identity/register.ts
1577
2229
  import { z as z5 } from "zod";
1578
2230
 
1579
- // src/contracts/memory.ts
2231
+ // src/contracts/identity.ts
1580
2232
  import { z as z4 } from "zod";
1581
2233
  var genericRecordSchema2 = z4.record(z4.string(), z4.unknown());
1582
2234
  var genericArraySchema2 = z4.array(genericRecordSchema2);
1583
- var memoryKindSchema = z4.enum(["collab_turn", "change_step", "merge_request", "reconcile"]);
1584
- var paginationSchema = z4.object({
1585
- limit: z4.number().int().nonnegative(),
2235
+ var paginationSchema2 = z4.object({
2236
+ limit: z4.number().int().positive(),
1586
2237
  offset: z4.number().int().nonnegative(),
1587
2238
  hasMore: z4.boolean()
1588
2239
  });
1589
- var memorySummaryInputSchema = {
2240
+ var whoamiInputSchema = {
2241
+ ...commonRequestFieldsSchema
2242
+ };
2243
+ var directoryOrganizationInputSchema = {
1590
2244
  ...commonRequestFieldsSchema,
1591
- appId: z4.string().trim().min(1).optional()
2245
+ organizationId: z4.string().trim().min(1).optional()
1592
2246
  };
1593
- var memorySearchInputSchema = {
2247
+ var directoryListProjectsInputSchema = {
1594
2248
  ...commonRequestFieldsSchema,
1595
- appId: z4.string().trim().min(1).optional(),
1596
- query: z4.string().trim().min(1),
1597
- kinds: z4.array(memoryKindSchema).max(4).optional(),
1598
- limit: z4.number().int().positive().max(50).optional(),
1599
- offset: z4.number().int().nonnegative().optional(),
1600
- createdAfter: z4.string().trim().min(1).optional(),
1601
- createdBefore: z4.string().trim().min(1).optional()
2249
+ organizationId: z4.string().trim().min(1).optional(),
2250
+ clientAppId: z4.string().trim().min(1).optional()
1602
2251
  };
1603
- var memoryTimelineInputSchema = {
2252
+ var directoryProjectInputSchema = {
2253
+ ...commonRequestFieldsSchema,
2254
+ projectId: z4.string().trim().min(1).optional()
2255
+ };
2256
+ var directoryListAppsInputSchema = {
1604
2257
  ...commonRequestFieldsSchema,
1605
- appId: z4.string().trim().min(1).optional(),
1606
- kinds: z4.array(memoryKindSchema).max(4).optional(),
2258
+ projectId: z4.string().trim().min(1).optional(),
2259
+ organizationId: z4.string().trim().min(1).optional(),
2260
+ ownership: z4.enum(["mine", "shared", "all"]).optional(),
2261
+ accessScope: z4.enum(["all_readable", "explicit_member"]).optional(),
2262
+ createdBy: z4.string().trim().min(1).optional(),
2263
+ forked: z4.enum(["only", "exclude", "all"]).optional(),
1607
2264
  limit: z4.number().int().positive().max(50).optional(),
1608
- offset: z4.number().int().nonnegative().optional(),
1609
- createdAfter: z4.string().trim().min(1).optional(),
1610
- createdBefore: z4.string().trim().min(1).optional()
2265
+ offset: z4.number().int().nonnegative().optional()
1611
2266
  };
1612
- var changeStepDiffInputSchema = {
2267
+ var directoryAppInputSchema = {
1613
2268
  ...commonRequestFieldsSchema,
1614
- appId: z4.string().trim().min(1).optional(),
1615
- changeStepId: z4.string().trim().min(1)
2269
+ appId: z4.string().trim().min(1).optional()
1616
2270
  };
1617
- var memorySummaryDataSchema = genericRecordSchema2;
1618
- var memorySearchDataSchema = z4.object({
1619
- items: genericArraySchema2,
1620
- pagination: paginationSchema
2271
+ var whoamiDataSchema = z4.object({
2272
+ id: z4.string().nullable(),
2273
+ name: z4.string().nullable(),
2274
+ email: z4.string().nullable(),
2275
+ organizationId: z4.string().nullable(),
2276
+ roles: genericRecordSchema2,
2277
+ binding: genericRecordSchema2.nullable()
1621
2278
  });
1622
- var memoryTimelineDataSchema = z4.object({
1623
- items: genericArraySchema2,
1624
- pagination: paginationSchema
2279
+ var listOrganizationsDataSchema = z4.object({
2280
+ organizations: genericArraySchema2
1625
2281
  });
1626
- var changeStepDiffDataSchema = z4.object({
1627
- changeStepId: z4.string(),
1628
- appId: z4.string(),
1629
- diff: z4.string(),
1630
- diffSha256: z4.string(),
1631
- contentType: z4.string(),
1632
- encoding: z4.string(),
1633
- expiresIn: z4.number().int().positive()
2282
+ var getOrganizationDataSchema = z4.object({
2283
+ organization: genericRecordSchema2
1634
2284
  });
1635
- var memorySummarySuccessSchema = makeSuccessSchema(memorySummaryDataSchema);
1636
- var memorySearchSuccessSchema = makeSuccessSchema(memorySearchDataSchema);
1637
- var memoryTimelineSuccessSchema = makeSuccessSchema(memoryTimelineDataSchema);
1638
- var changeStepDiffSuccessSchema = makeSuccessSchema(changeStepDiffDataSchema);
2285
+ var listProjectsDataSchema = z4.object({
2286
+ organizationId: z4.string().nullable(),
2287
+ projects: genericArraySchema2
2288
+ });
2289
+ var getProjectDataSchema = z4.object({
2290
+ project: genericRecordSchema2
2291
+ });
2292
+ var listAppsDataSchema = z4.object({
2293
+ apps: genericArraySchema2,
2294
+ pagination: paginationSchema2,
2295
+ filters: z4.object({
2296
+ projectId: z4.string().nullable(),
2297
+ organizationId: z4.string().nullable(),
2298
+ ownership: z4.enum(["mine", "shared", "all"]),
2299
+ accessScope: z4.enum(["all_readable", "explicit_member"]),
2300
+ createdBy: z4.string().nullable(),
2301
+ forked: z4.enum(["only", "exclude", "all"])
2302
+ })
2303
+ });
2304
+ var getAppDataSchema = z4.object({
2305
+ app: genericRecordSchema2
2306
+ });
2307
+ var whoamiSuccessSchema = makeSuccessSchema(whoamiDataSchema);
2308
+ var listOrganizationsSuccessSchema = makeSuccessSchema(listOrganizationsDataSchema);
2309
+ var getOrganizationSuccessSchema = makeSuccessSchema(getOrganizationDataSchema);
2310
+ var listProjectsSuccessSchema = makeSuccessSchema(listProjectsDataSchema);
2311
+ var getProjectSuccessSchema = makeSuccessSchema(getProjectDataSchema);
2312
+ var listAppsSuccessSchema = makeSuccessSchema(listAppsDataSchema);
2313
+ var getAppSuccessSchema = makeSuccessSchema(getAppDataSchema);
1639
2314
 
1640
- // src/domain/memoryAdapter.ts
1641
- import { readCollabBinding } from "@remixhq/core/binding";
1642
- import { findGitRoot as findGitRoot2 } from "@remixhq/core/repo";
1643
- function buildSummaryNextActions(summary) {
1644
- const actions = [];
1645
- actions.push(
1646
- "Use `remix_collab_memory_search` next for focused why/history/failed-attempt questions about this app."
1647
- );
1648
- const recentItemCount = summary.recent.collabTurns.length + summary.recent.changeSteps.length + summary.recent.mergeRequests.length + summary.recent.reconciles.length;
1649
- if (recentItemCount > 0) {
1650
- actions.push("Use `remix_collab_memory_timeline` next if you need the chronological sequence of recent activity.");
2315
+ // src/domain/directoryAdapter.ts
2316
+ function normalizePagination2(params) {
2317
+ const rawLimit = typeof params?.limit === "number" ? Math.trunc(params.limit) : 25;
2318
+ const rawOffset = typeof params?.offset === "number" ? Math.trunc(params.offset) : 0;
2319
+ return {
2320
+ limit: Math.max(1, Math.min(50, rawLimit)),
2321
+ offset: Math.max(0, rawOffset)
2322
+ };
2323
+ }
2324
+ async function resolveOrganizationId(api, params) {
2325
+ const explicitId = params.organizationId?.trim();
2326
+ if (explicitId) return explicitId;
2327
+ const bindingContext = await loadBindingContext(params.cwd);
2328
+ if (!bindingContext.binding) {
2329
+ throw makeNotBoundError("Organization id was not provided and the current repository is not bound to Remix.");
1651
2330
  }
1652
- if (summary.counts.failedChangeStepCount > 0 || summary.counts.reconcileCount > 0) {
1653
- actions.push(
1654
- "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."
2331
+ const appContext = unwrapResponseObject(
2332
+ await api.getAppContext(bindingContext.binding.currentAppId),
2333
+ "bound app context"
2334
+ );
2335
+ if (!appContext.readableScopes.organization) {
2336
+ throw createAccessDeniedError(
2337
+ "The bound app's organization is not readable to the current user.",
2338
+ "Use an explicit organization id you can access, or inspect the bound app with `remix_directory_get_app` / `remix_access_debug` for app-level context."
1655
2339
  );
1656
2340
  }
1657
- return actions;
1658
- }
1659
- function getFirstChangeStepId(items) {
1660
- const changeStep = items.find((item) => item.kind === "change_step");
1661
- return changeStep?.id ?? null;
2341
+ return appContext.organizationId;
2342
+ }
2343
+ async function resolveProjectId(api, params) {
2344
+ const explicitId = params.projectId?.trim();
2345
+ if (explicitId) {
2346
+ const bindingContext2 = await loadBindingContext(params.cwd);
2347
+ return { projectId: explicitId, repoRoot: bindingContext2.repoRoot };
2348
+ }
2349
+ const bindingContext = await loadBindingContext(params.cwd);
2350
+ if (!bindingContext.binding) {
2351
+ throw makeNotBoundError("Project id was not provided and the current repository is not bound to Remix.");
2352
+ }
2353
+ const appContext = unwrapResponseObject(
2354
+ await api.getAppContext(bindingContext.binding.currentAppId),
2355
+ "bound app context"
2356
+ );
2357
+ if (!appContext.readableScopes.project) {
2358
+ throw createAccessDeniedError(
2359
+ "The bound app's project is not readable to the current user.",
2360
+ "Use `remix_directory_get_app` or `remix_access_debug` for app-level diagnostics, or pass an explicit project id you can access."
2361
+ );
2362
+ }
2363
+ return {
2364
+ projectId: appContext.projectId,
2365
+ repoRoot: bindingContext.repoRoot
2366
+ };
2367
+ }
2368
+ async function resolveAppId(params) {
2369
+ const explicitId = params.appId?.trim();
2370
+ if (explicitId) {
2371
+ const bindingContext2 = await loadBindingContext(params.cwd);
2372
+ return { appId: explicitId, repoRoot: bindingContext2.repoRoot };
2373
+ }
2374
+ const bindingContext = await loadBindingContext(params.cwd);
2375
+ if (!bindingContext.binding) {
2376
+ throw makeNotBoundError("App id was not provided and the current repository is not bound to Remix.");
2377
+ }
2378
+ return {
2379
+ appId: bindingContext.binding.currentAppId,
2380
+ repoRoot: bindingContext.repoRoot
2381
+ };
2382
+ }
2383
+ function toEffectiveAppRole(role) {
2384
+ switch (role) {
2385
+ case "owner":
2386
+ case "maintainer":
2387
+ case "editor":
2388
+ case "viewer":
2389
+ return role;
2390
+ default:
2391
+ return null;
2392
+ }
2393
+ }
2394
+ function resolveAppAccessSource2(params) {
2395
+ if (params.directAppRole && params.inheritedProjectRole) return "both";
2396
+ if (params.directAppRole) return "direct_app_membership";
2397
+ if (params.inheritedProjectRole) return "project_membership";
2398
+ return "none";
2399
+ }
2400
+ function emptySelfRoles() {
2401
+ return {
2402
+ organizationRole: null,
2403
+ projectRole: null,
2404
+ appRole: null,
2405
+ directAppRole: null,
2406
+ inheritedProjectRole: null,
2407
+ appAccessSource: "none"
2408
+ };
2409
+ }
2410
+ function resolveSelfRolesFromAppContext(appContext) {
2411
+ if (!appContext) return emptySelfRoles();
2412
+ const directAppRole = toEffectiveAppRole(appContext.roles.appRole);
2413
+ const inheritedProjectRole = toEffectiveAppRole(appContext.roles.inheritedProjectRole);
2414
+ return {
2415
+ organizationRole: appContext.roles.organizationRole ?? null,
2416
+ projectRole: appContext.roles.projectRole ?? null,
2417
+ appRole: toEffectiveAppRole(appContext.roles.effectiveAppRole),
2418
+ directAppRole,
2419
+ inheritedProjectRole,
2420
+ appAccessSource: resolveAppAccessSource2({ directAppRole, inheritedProjectRole })
2421
+ };
2422
+ }
2423
+ async function whoAmI(params) {
2424
+ const api = await createApiClient();
2425
+ const bindingContext = await loadBindingContext(params.cwd);
2426
+ const me = unwrapResponseObject(await api.getMe(), "current user");
2427
+ const warnings = bindingContext.binding ? [] : ["No local Remix binding was detected for the provided cwd."];
2428
+ let boundAppContext = null;
2429
+ let boundProject = null;
2430
+ let boundApp = null;
2431
+ if (bindingContext.binding) {
2432
+ try {
2433
+ boundAppContext = unwrapResponseObject(
2434
+ await api.getAppContext(bindingContext.binding.currentAppId),
2435
+ "bound app context"
2436
+ );
2437
+ } catch (error) {
2438
+ if (!isBackendForbidden(error) && !isBackendNotFound(error)) throw error;
2439
+ warnings.push(
2440
+ "The bound app context could not be loaded. The local binding may be stale, or the current user may no longer be able to read that app."
2441
+ );
2442
+ }
2443
+ try {
2444
+ boundApp = unwrapResponseObject(await api.getApp(bindingContext.binding.currentAppId), "app");
2445
+ } catch (error) {
2446
+ if (!isBackendForbidden(error) && !isBackendNotFound(error)) throw error;
2447
+ warnings.push("The bound app metadata could not be loaded for the current user.");
2448
+ }
2449
+ if (boundAppContext?.readableScopes.project) {
2450
+ try {
2451
+ boundProject = unwrapResponseObject(await api.getProject(boundAppContext.projectId), "project");
2452
+ } catch (error) {
2453
+ if (!isBackendForbidden(error) && !isBackendNotFound(error)) throw error;
2454
+ warnings.push("The bound app is readable, but its project metadata is not currently readable to the current user.");
2455
+ }
2456
+ } else if (boundAppContext) {
2457
+ warnings.push("The bound app is readable, but its project/workspace is not readable to the current user.");
2458
+ }
2459
+ }
2460
+ const roles = resolveSelfRolesFromAppContext(boundAppContext);
2461
+ return {
2462
+ data: {
2463
+ id: me.id ?? null,
2464
+ name: me.name ?? null,
2465
+ email: me.email ?? null,
2466
+ organizationId: me.organizationId ?? null,
2467
+ roles,
2468
+ binding: bindingContext.binding == null ? null : {
2469
+ repoRoot: bindingContext.repoRoot,
2470
+ ...bindingContext.binding,
2471
+ projectId: boundAppContext?.projectId ?? bindingContext.binding.projectId,
2472
+ organizationId: boundAppContext?.organizationId ?? null,
2473
+ visibility: boundAppContext?.visibility ?? null,
2474
+ accessPath: boundAppContext?.accessPath ?? null,
2475
+ readableScopes: boundAppContext?.readableScopes ?? null,
2476
+ projectName: boundProject?.name ?? null,
2477
+ appName: boundApp?.name ?? null
2478
+ }
2479
+ },
2480
+ warnings,
2481
+ recommendedNextActions: bindingContext.binding ? ["Use `remix_access_debug` next when you need to explain repository binding or membership issues in this workspace."] : ["Use `remix_directory_list_organizations` to inspect visible tenancy context, or run `remix_collab_init` in a repository to create a local binding."],
2482
+ logContext: {
2483
+ repoRoot: bindingContext.repoRoot,
2484
+ appId: bindingContext.binding?.currentAppId ?? null
2485
+ }
2486
+ };
2487
+ }
2488
+ async function listOrganizations() {
2489
+ const api = await createApiClient();
2490
+ const organizations = unwrapResponseObject(await api.listOrganizations(), "organizations");
2491
+ return {
2492
+ data: { organizations },
2493
+ warnings: [],
2494
+ recommendedNextActions: organizations.length ? ["Use `remix_directory_get_organization` for one organization, or `remix_directory_list_projects` to inspect projects under a chosen organization."] : [],
2495
+ logContext: {}
2496
+ };
2497
+ }
2498
+ async function getOrganization(params) {
2499
+ const api = await createApiClient();
2500
+ const organizationId = await resolveOrganizationId(api, params);
2501
+ const bindingContext = await loadBindingContext(params.cwd);
2502
+ const organization = unwrapResponseObject(await api.getOrganization(organizationId), "organization");
2503
+ return {
2504
+ data: { organization },
2505
+ warnings: [],
2506
+ recommendedNextActions: ["Use `remix_directory_list_projects` with this organization id to inspect its project directory."],
2507
+ logContext: {
2508
+ repoRoot: bindingContext.repoRoot
2509
+ }
2510
+ };
2511
+ }
2512
+ async function listProjects(params) {
2513
+ const api = await createApiClient();
2514
+ const bindingContext = await loadBindingContext(params.cwd);
2515
+ const organizationId = params.organizationId !== void 0 ? await resolveOrganizationId(api, { organizationId: params.organizationId, cwd: params.cwd }) : null;
2516
+ const projects = unwrapResponseObject(
2517
+ await api.listProjects({
2518
+ organizationId: organizationId ?? void 0,
2519
+ clientAppId: params.clientAppId
2520
+ }),
2521
+ "projects"
2522
+ );
2523
+ return {
2524
+ data: {
2525
+ projects,
2526
+ organizationId
2527
+ },
2528
+ warnings: [],
2529
+ recommendedNextActions: projects.length ? ["Use `remix_directory_get_project` for one project, or `remix_directory_list_apps` to inspect apps under a chosen project or organization."] : [],
2530
+ logContext: {
2531
+ repoRoot: bindingContext.repoRoot,
2532
+ appId: bindingContext.binding?.currentAppId ?? null
2533
+ }
2534
+ };
2535
+ }
2536
+ async function getProject(params) {
2537
+ const api = await createApiClient();
2538
+ const target = await resolveProjectId(api, params);
2539
+ const project = unwrapResponseObject(await api.getProject(target.projectId), "project");
2540
+ return {
2541
+ data: { project },
2542
+ warnings: [],
2543
+ recommendedNextActions: ["Use `remix_directory_list_apps` with this project id to inspect apps under the project."],
2544
+ logContext: {
2545
+ repoRoot: target.repoRoot
2546
+ }
2547
+ };
2548
+ }
2549
+ async function listApps2(params) {
2550
+ const api = await createApiClient();
2551
+ const bindingContext = await loadBindingContext(params.cwd);
2552
+ const pagination = normalizePagination2(params);
2553
+ const apps = unwrapResponseObject(
2554
+ await api.listApps({
2555
+ projectId: params.projectId,
2556
+ organizationId: params.organizationId,
2557
+ ownership: params.ownership ?? "all",
2558
+ accessScope: params.accessScope ?? "explicit_member",
2559
+ createdBy: params.createdBy,
2560
+ forked: params.forked,
2561
+ limit: pagination.limit + 1,
2562
+ offset: pagination.offset
2563
+ }),
2564
+ "apps"
2565
+ );
2566
+ return {
2567
+ data: {
2568
+ apps: apps.slice(0, pagination.limit),
2569
+ pagination: {
2570
+ ...pagination,
2571
+ hasMore: apps.length > pagination.limit
2572
+ },
2573
+ filters: {
2574
+ projectId: params.projectId ?? null,
2575
+ organizationId: params.organizationId ?? null,
2576
+ ownership: params.ownership ?? "all",
2577
+ accessScope: params.accessScope ?? "explicit_member",
2578
+ createdBy: params.createdBy ?? null,
2579
+ forked: params.forked ?? "all"
2580
+ }
2581
+ },
2582
+ warnings: [],
2583
+ recommendedNextActions: apps.length > pagination.limit ? [`Pass offset=${pagination.offset + pagination.limit} to load the next page.`] : ["Use `remix_directory_get_app` for one app, or `remix_context_get_app_overview` for operational context on a chosen app."],
2584
+ logContext: {
2585
+ repoRoot: bindingContext.repoRoot,
2586
+ appId: bindingContext.binding?.currentAppId ?? null
2587
+ }
2588
+ };
2589
+ }
2590
+ async function getApp(params) {
2591
+ const api = await createApiClient();
2592
+ const target = await resolveAppId(params);
2593
+ const app = unwrapResponseObject(await api.getApp(target.appId), "app");
2594
+ return {
2595
+ data: { app },
2596
+ warnings: [],
2597
+ recommendedNextActions: ["Use `remix_context_get_app_overview` for status and capability context, or `remix_ops_list_timeline` for bounded historical activity."],
2598
+ logContext: {
2599
+ repoRoot: target.repoRoot,
2600
+ appId: target.appId
2601
+ }
2602
+ };
2603
+ }
2604
+
2605
+ // src/tools/identity/register.ts
2606
+ function getAnnotations2(access) {
2607
+ return {
2608
+ readOnlyHint: access === "read",
2609
+ destructiveHint: false,
2610
+ idempotentHint: true,
2611
+ openWorldHint: false
2612
+ };
2613
+ }
2614
+ function buildSuccessEnvelope2(tool, requestId, result) {
2615
+ return {
2616
+ schemaVersion: SCHEMA_VERSION,
2617
+ ok: true,
2618
+ tool,
2619
+ requestId: requestId ?? null,
2620
+ data: result.data,
2621
+ warnings: result.warnings ?? [],
2622
+ risks: result.risks ?? [],
2623
+ recommendedNextActions: result.recommendedNextActions ?? []
2624
+ };
2625
+ }
2626
+ function deriveErrorRisks2(normalized) {
2627
+ if (normalized.code === "DESTRUCTIVE_OPERATION_BLOCKED") {
2628
+ return ["A policy guard blocked a disallowed operation."];
2629
+ }
2630
+ return [];
2631
+ }
2632
+ function buildErrorEnvelope2(tool, requestId, error) {
2633
+ const normalized = normalizeToolError(error);
2634
+ return {
2635
+ schemaVersion: SCHEMA_VERSION,
2636
+ ok: false,
2637
+ tool,
2638
+ requestId: requestId ?? null,
2639
+ error: normalized,
2640
+ warnings: [],
2641
+ risks: deriveErrorRisks2(normalized),
2642
+ recommendedNextActions: normalized.code === "AUTH_REQUIRED" ? ["Run `remix login` or set COMERGE_ACCESS_TOKEN, then retry."] : []
2643
+ };
2644
+ }
2645
+ function registerTool2(server, context, params) {
2646
+ const errorSchema = makeErrorSchema();
2647
+ server.registerTool(
2648
+ params.name,
2649
+ {
2650
+ title: params.name,
2651
+ description: params.description,
2652
+ inputSchema: params.inputSchema,
2653
+ outputSchema: params.outputSchema,
2654
+ annotations: getAnnotations2(params.access)
2655
+ },
2656
+ async (rawArgs) => {
2657
+ const requestId = typeof rawArgs.requestId === "string" ? rawArgs.requestId : void 0;
2658
+ const startedAt = Date.now();
2659
+ try {
2660
+ assertToolAccess(context.policy, params.access);
2661
+ const result = await params.run(rawArgs);
2662
+ const envelope = buildSuccessEnvelope2(params.name, requestId, result);
2663
+ params.outputSchema.parse(envelope);
2664
+ context.logger.log({
2665
+ level: "info",
2666
+ message: "tool_completed",
2667
+ tool: params.name,
2668
+ requestId: envelope.requestId,
2669
+ durationMs: Date.now() - startedAt,
2670
+ result: "success",
2671
+ repoRoot: result.logContext?.repoRoot ?? null,
2672
+ appId: result.logContext?.appId ?? null,
2673
+ mrId: result.logContext?.mrId ?? null
2674
+ });
2675
+ return makeSuccessResult(envelope);
2676
+ } catch (error) {
2677
+ const envelope = buildErrorEnvelope2(params.name, requestId, error);
2678
+ errorSchema.parse(envelope);
2679
+ context.logger.log({
2680
+ level: "error",
2681
+ message: "tool_failed",
2682
+ tool: params.name,
2683
+ requestId: envelope.requestId,
2684
+ durationMs: Date.now() - startedAt,
2685
+ result: "error",
2686
+ errorCode: envelope.error.code
2687
+ });
2688
+ return makeErrorResult(envelope);
2689
+ }
2690
+ }
2691
+ );
2692
+ }
2693
+ function registerIdentityTools(server, context) {
2694
+ registerTool2(server, context, {
2695
+ name: "remix_identity_whoami",
2696
+ description: "Show the authenticated Remix user and, when cwd is provided, the current local binding and any immediately derivable roles for that bound workspace.",
2697
+ access: "read",
2698
+ inputSchema: whoamiInputSchema,
2699
+ outputSchema: whoamiSuccessSchema,
2700
+ run: async (args) => {
2701
+ const input = z5.object(whoamiInputSchema).parse(args);
2702
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
2703
+ return whoAmI({ cwd });
2704
+ }
2705
+ });
2706
+ registerTool2(server, context, {
2707
+ name: "remix_directory_list_organizations",
2708
+ description: "List organizations visible to the authenticated Remix user.",
2709
+ access: "read",
2710
+ inputSchema: whoamiInputSchema,
2711
+ outputSchema: listOrganizationsSuccessSchema,
2712
+ run: async () => listOrganizations()
2713
+ });
2714
+ registerTool2(server, context, {
2715
+ name: "remix_directory_get_organization",
2716
+ description: "Fetch one organization by id, or infer the bound repository's organization from cwd when possible.",
2717
+ access: "read",
2718
+ inputSchema: directoryOrganizationInputSchema,
2719
+ outputSchema: getOrganizationSuccessSchema,
2720
+ run: async (args) => {
2721
+ const input = z5.object(directoryOrganizationInputSchema).parse(args);
2722
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
2723
+ return getOrganization({
2724
+ organizationId: input.organizationId,
2725
+ cwd
2726
+ });
2727
+ }
2728
+ });
2729
+ registerTool2(server, context, {
2730
+ name: "remix_directory_list_projects",
2731
+ description: "List projects visible to the authenticated user, optionally narrowed to one organization or client app.",
2732
+ access: "read",
2733
+ inputSchema: directoryListProjectsInputSchema,
2734
+ outputSchema: listProjectsSuccessSchema,
2735
+ run: async (args) => {
2736
+ const input = z5.object(directoryListProjectsInputSchema).parse(args);
2737
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
2738
+ return listProjects({
2739
+ organizationId: input.organizationId,
2740
+ clientAppId: input.clientAppId,
2741
+ cwd
2742
+ });
2743
+ }
2744
+ });
2745
+ registerTool2(server, context, {
2746
+ name: "remix_directory_get_project",
2747
+ description: "Fetch one project by id, or infer the bound repository's project from cwd when possible.",
2748
+ access: "read",
2749
+ inputSchema: directoryProjectInputSchema,
2750
+ outputSchema: getProjectSuccessSchema,
2751
+ run: async (args) => {
2752
+ const input = z5.object(directoryProjectInputSchema).parse(args);
2753
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
2754
+ return getProject({
2755
+ projectId: input.projectId,
2756
+ cwd
2757
+ });
2758
+ }
2759
+ });
2760
+ registerTool2(server, context, {
2761
+ name: "remix_directory_list_apps",
2762
+ description: "List apps visible to the authenticated user, with optional organization, project, ownership, and access-scope filters. Defaults to membership-oriented discovery unless accessScope=all_readable is passed explicitly.",
2763
+ access: "read",
2764
+ inputSchema: directoryListAppsInputSchema,
2765
+ outputSchema: listAppsSuccessSchema,
2766
+ run: async (args) => {
2767
+ const input = z5.object(directoryListAppsInputSchema).parse(args);
2768
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
2769
+ return listApps2({
2770
+ projectId: input.projectId,
2771
+ organizationId: input.organizationId,
2772
+ ownership: input.ownership,
2773
+ accessScope: input.accessScope,
2774
+ createdBy: input.createdBy,
2775
+ forked: input.forked,
2776
+ limit: input.limit,
2777
+ offset: input.offset,
2778
+ cwd
2779
+ });
2780
+ }
2781
+ });
2782
+ registerTool2(server, context, {
2783
+ name: "remix_directory_get_app",
2784
+ description: "Fetch one app by id, or infer the bound repository's current app from cwd when possible.",
2785
+ access: "read",
2786
+ inputSchema: directoryAppInputSchema,
2787
+ outputSchema: getAppSuccessSchema,
2788
+ run: async (args) => {
2789
+ const input = z5.object(directoryAppInputSchema).parse(args);
2790
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
2791
+ return getApp({
2792
+ appId: input.appId,
2793
+ cwd
2794
+ });
2795
+ }
2796
+ });
2797
+ }
2798
+
2799
+ // src/tools/memory/register.ts
2800
+ import { z as z7 } from "zod";
2801
+
2802
+ // src/contracts/memory.ts
2803
+ import { z as z6 } from "zod";
2804
+ var genericRecordSchema3 = z6.record(z6.string(), z6.unknown());
2805
+ var genericArraySchema3 = z6.array(genericRecordSchema3);
2806
+ var memoryKindSchema = z6.enum(["collab_turn", "change_step", "merge_request", "reconcile"]);
2807
+ var paginationSchema3 = z6.object({
2808
+ limit: z6.number().int().nonnegative(),
2809
+ offset: z6.number().int().nonnegative(),
2810
+ hasMore: z6.boolean()
2811
+ });
2812
+ var memorySummaryInputSchema = {
2813
+ ...commonRequestFieldsSchema,
2814
+ appId: z6.string().trim().min(1).optional()
2815
+ };
2816
+ var memorySearchInputSchema = {
2817
+ ...commonRequestFieldsSchema,
2818
+ appId: z6.string().trim().min(1).optional(),
2819
+ query: z6.string().trim().min(1),
2820
+ kinds: z6.array(memoryKindSchema).max(4).optional(),
2821
+ limit: z6.number().int().positive().max(50).optional(),
2822
+ offset: z6.number().int().nonnegative().optional(),
2823
+ createdAfter: z6.string().trim().min(1).optional(),
2824
+ createdBefore: z6.string().trim().min(1).optional()
2825
+ };
2826
+ var memoryTimelineInputSchema = {
2827
+ ...commonRequestFieldsSchema,
2828
+ appId: z6.string().trim().min(1).optional(),
2829
+ kinds: z6.array(memoryKindSchema).max(4).optional(),
2830
+ limit: z6.number().int().positive().max(50).optional(),
2831
+ offset: z6.number().int().nonnegative().optional(),
2832
+ createdAfter: z6.string().trim().min(1).optional(),
2833
+ createdBefore: z6.string().trim().min(1).optional()
2834
+ };
2835
+ var changeStepDiffInputSchema = {
2836
+ ...commonRequestFieldsSchema,
2837
+ appId: z6.string().trim().min(1).optional(),
2838
+ changeStepId: z6.string().trim().min(1)
2839
+ };
2840
+ var memorySummaryDataSchema = genericRecordSchema3;
2841
+ var memorySearchDataSchema = z6.object({
2842
+ items: genericArraySchema3,
2843
+ pagination: paginationSchema3
2844
+ });
2845
+ var memoryTimelineDataSchema = z6.object({
2846
+ items: genericArraySchema3,
2847
+ pagination: paginationSchema3
2848
+ });
2849
+ var changeStepDiffDataSchema = z6.object({
2850
+ changeStepId: z6.string(),
2851
+ appId: z6.string(),
2852
+ diff: z6.string(),
2853
+ diffSha256: z6.string(),
2854
+ contentType: z6.string(),
2855
+ encoding: z6.string(),
2856
+ expiresIn: z6.number().int().positive()
2857
+ });
2858
+ var memorySummarySuccessSchema = makeSuccessSchema(memorySummaryDataSchema);
2859
+ var memorySearchSuccessSchema = makeSuccessSchema(memorySearchDataSchema);
2860
+ var memoryTimelineSuccessSchema = makeSuccessSchema(memoryTimelineDataSchema);
2861
+ var changeStepDiffSuccessSchema = makeSuccessSchema(changeStepDiffDataSchema);
2862
+
2863
+ // src/domain/memoryAdapter.ts
2864
+ import { readCollabBinding as readCollabBinding2 } from "@remixhq/core/binding";
2865
+ import { findGitRoot as findGitRoot3 } from "@remixhq/core/repo";
2866
+ function buildSummaryNextActions(summary) {
2867
+ const actions = [];
2868
+ actions.push(
2869
+ "Use `remix_collab_memory_search` next for focused why/history/failed-attempt questions about this app."
2870
+ );
2871
+ const recentItemCount = summary.recent.collabTurns.length + summary.recent.changeSteps.length + summary.recent.mergeRequests.length + summary.recent.reconciles.length;
2872
+ if (recentItemCount > 0) {
2873
+ actions.push("Use `remix_collab_memory_timeline` next if you need the chronological sequence of recent activity.");
2874
+ }
2875
+ if (summary.counts.failedChangeStepCount > 0 || summary.counts.reconcileCount > 0) {
2876
+ actions.push(
2877
+ "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."
2878
+ );
2879
+ }
2880
+ return actions;
2881
+ }
2882
+ function getFirstChangeStepId(items) {
2883
+ const changeStep = items.find((item) => item.kind === "change_step");
2884
+ return changeStep?.id ?? null;
1662
2885
  }
1663
2886
  function buildSearchNextActions(result) {
1664
2887
  if (result.items.length === 0) {
@@ -1713,14 +2936,14 @@ function unwrapResponseObject2(resp, label) {
1713
2936
  }
1714
2937
  return obj;
1715
2938
  }
1716
- function makeNotBoundError() {
2939
+ function makeNotBoundError2() {
1717
2940
  const error = new Error("Repository is not bound to Remix.");
1718
2941
  error.hint = "Run `remix_collab_init` in this repository, or pass `appId` explicitly for a direct memory read.";
1719
2942
  return error;
1720
2943
  }
1721
- async function maybeFindGitRoot(cwd) {
2944
+ async function maybeFindGitRoot2(cwd) {
1722
2945
  try {
1723
- return await findGitRoot2(cwd);
2946
+ return await findGitRoot3(cwd);
1724
2947
  } catch {
1725
2948
  return null;
1726
2949
  }
@@ -1730,13 +2953,13 @@ async function resolveMemoryTarget(params) {
1730
2953
  if (explicitAppId) {
1731
2954
  return {
1732
2955
  appId: explicitAppId,
1733
- repoRoot: await maybeFindGitRoot(params.cwd)
2956
+ repoRoot: await maybeFindGitRoot2(params.cwd)
1734
2957
  };
1735
2958
  }
1736
- const repoRoot = await findGitRoot2(params.cwd);
1737
- const binding = await readCollabBinding(repoRoot);
2959
+ const repoRoot = await findGitRoot3(params.cwd);
2960
+ const binding = await readCollabBinding2(repoRoot);
1738
2961
  if (!binding) {
1739
- throw makeNotBoundError();
2962
+ throw makeNotBoundError2();
1740
2963
  }
1741
2964
  return {
1742
2965
  appId: binding.currentAppId,
@@ -1806,7 +3029,7 @@ async function getChangeStepDiff(params) {
1806
3029
  }
1807
3030
 
1808
3031
  // src/tools/memory/register.ts
1809
- function getAnnotations2(access) {
3032
+ function getAnnotations3(access) {
1810
3033
  return {
1811
3034
  readOnlyHint: access === "read",
1812
3035
  destructiveHint: false,
@@ -1814,7 +3037,7 @@ function getAnnotations2(access) {
1814
3037
  openWorldHint: false
1815
3038
  };
1816
3039
  }
1817
- function buildSuccessEnvelope2(tool, requestId, result) {
3040
+ function buildSuccessEnvelope3(tool, requestId, result) {
1818
3041
  return {
1819
3042
  schemaVersion: SCHEMA_VERSION,
1820
3043
  ok: true,
@@ -1826,7 +3049,7 @@ function buildSuccessEnvelope2(tool, requestId, result) {
1826
3049
  recommendedNextActions: result.recommendedNextActions ?? []
1827
3050
  };
1828
3051
  }
1829
- function buildErrorEnvelope2(tool, requestId, error) {
3052
+ function buildErrorEnvelope3(tool, requestId, error) {
1830
3053
  const normalized = normalizeToolError(error);
1831
3054
  return {
1832
3055
  schemaVersion: SCHEMA_VERSION,
@@ -1835,17 +3058,17 @@ function buildErrorEnvelope2(tool, requestId, error) {
1835
3058
  requestId: requestId ?? null,
1836
3059
  error: normalized,
1837
3060
  warnings: [],
1838
- risks: deriveErrorRisks2(normalized),
3061
+ risks: deriveErrorRisks3(normalized),
1839
3062
  recommendedNextActions: normalized.code === "AUTH_REQUIRED" ? ["Run `remix login` or set COMERGE_ACCESS_TOKEN, then retry."] : []
1840
3063
  };
1841
3064
  }
1842
- function deriveErrorRisks2(normalized) {
3065
+ function deriveErrorRisks3(normalized) {
1843
3066
  if (normalized.code === "DESTRUCTIVE_OPERATION_BLOCKED") {
1844
3067
  return ["A policy guard blocked a disallowed operation."];
1845
3068
  }
1846
3069
  return [];
1847
3070
  }
1848
- function registerTool2(server, context, params) {
3071
+ function registerTool3(server, context, params) {
1849
3072
  const errorSchema = makeErrorSchema();
1850
3073
  server.registerTool(
1851
3074
  params.name,
@@ -1854,7 +3077,7 @@ function registerTool2(server, context, params) {
1854
3077
  description: params.description,
1855
3078
  inputSchema: params.inputSchema,
1856
3079
  outputSchema: params.outputSchema,
1857
- annotations: getAnnotations2(params.access)
3080
+ annotations: getAnnotations3(params.access)
1858
3081
  },
1859
3082
  async (rawArgs) => {
1860
3083
  const requestId = typeof rawArgs.requestId === "string" ? rawArgs.requestId : void 0;
@@ -1862,7 +3085,7 @@ function registerTool2(server, context, params) {
1862
3085
  try {
1863
3086
  assertToolAccess(context.policy, params.access);
1864
3087
  const result = await params.run(rawArgs);
1865
- const envelope = buildSuccessEnvelope2(params.name, requestId, result);
3088
+ const envelope = buildSuccessEnvelope3(params.name, requestId, result);
1866
3089
  params.outputSchema.parse(envelope);
1867
3090
  context.logger.log({
1868
3091
  level: "info",
@@ -1878,7 +3101,7 @@ function registerTool2(server, context, params) {
1878
3101
  });
1879
3102
  return makeSuccessResult(envelope);
1880
3103
  } catch (error) {
1881
- const envelope = buildErrorEnvelope2(params.name, requestId, error);
3104
+ const envelope = buildErrorEnvelope3(params.name, requestId, error);
1882
3105
  errorSchema.parse(envelope);
1883
3106
  context.logger.log({
1884
3107
  level: "error",
@@ -1895,14 +3118,14 @@ function registerTool2(server, context, params) {
1895
3118
  );
1896
3119
  }
1897
3120
  function registerMemoryTools(server, context) {
1898
- registerTool2(server, context, {
3121
+ registerTool3(server, context, {
1899
3122
  name: "remix_collab_memory_summary",
1900
3123
  description: "First read for a bound app's current collaboration state, recent reasoning context, and merge or reconcile history before deeper inspection or any raw git history lookup.",
1901
3124
  access: "read",
1902
3125
  inputSchema: memorySummaryInputSchema,
1903
3126
  outputSchema: memorySummarySuccessSchema,
1904
3127
  run: async (args) => {
1905
- const input = z5.object(memorySummaryInputSchema).parse(args);
3128
+ const input = z7.object(memorySummaryInputSchema).parse(args);
1906
3129
  const cwd = resolvePolicyCwd(context.policy, input.cwd);
1907
3130
  return getMemorySummary({
1908
3131
  cwd,
@@ -1910,14 +3133,14 @@ function registerMemoryTools(server, context) {
1910
3133
  });
1911
3134
  }
1912
3135
  });
1913
- registerTool2(server, context, {
3136
+ registerTool3(server, context, {
1914
3137
  name: "remix_collab_memory_search",
1915
3138
  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 for exact repository facts.",
1916
3139
  access: "read",
1917
3140
  inputSchema: memorySearchInputSchema,
1918
3141
  outputSchema: memorySearchSuccessSchema,
1919
3142
  run: async (args) => {
1920
- const input = z5.object(memorySearchInputSchema).parse(args);
3143
+ const input = z7.object(memorySearchInputSchema).parse(args);
1921
3144
  const cwd = resolvePolicyCwd(context.policy, input.cwd);
1922
3145
  return searchMemory({
1923
3146
  cwd,
@@ -1931,14 +3154,14 @@ function registerMemoryTools(server, context) {
1931
3154
  });
1932
3155
  }
1933
3156
  });
1934
- registerTool2(server, context, {
3157
+ registerTool3(server, context, {
1935
3158
  name: "remix_collab_memory_timeline",
1936
3159
  description: "Chronological view of collaboration memory for understanding what happened and in what order, with optional filters for bounded historical inspection before any exact-facts raw git follow-up.",
1937
3160
  access: "read",
1938
3161
  inputSchema: memoryTimelineInputSchema,
1939
3162
  outputSchema: memoryTimelineSuccessSchema,
1940
3163
  run: async (args) => {
1941
- const input = z5.object(memoryTimelineInputSchema).parse(args);
3164
+ const input = z7.object(memoryTimelineInputSchema).parse(args);
1942
3165
  const cwd = resolvePolicyCwd(context.policy, input.cwd);
1943
3166
  return getMemoryTimeline({
1944
3167
  cwd,
@@ -1951,14 +3174,14 @@ function registerMemoryTools(server, context) {
1951
3174
  });
1952
3175
  }
1953
3176
  });
1954
- registerTool2(server, context, {
3177
+ registerTool3(server, context, {
1955
3178
  name: "remix_collab_memory_change_step_diff",
1956
3179
  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`, keeping historical inspection inside Remix before raw git fallback.",
1957
3180
  access: "read",
1958
3181
  inputSchema: changeStepDiffInputSchema,
1959
3182
  outputSchema: changeStepDiffSuccessSchema,
1960
3183
  run: async (args) => {
1961
- const input = z5.object(changeStepDiffInputSchema).parse(args);
3184
+ const input = z7.object(changeStepDiffInputSchema).parse(args);
1962
3185
  const cwd = resolvePolicyCwd(context.policy, input.cwd);
1963
3186
  return getChangeStepDiff({
1964
3187
  cwd,
@@ -1969,6 +3192,442 @@ function registerMemoryTools(server, context) {
1969
3192
  });
1970
3193
  }
1971
3194
 
3195
+ // src/tools/ops/register.ts
3196
+ import { z as z9 } from "zod";
3197
+
3198
+ // src/contracts/ops.ts
3199
+ import { z as z8 } from "zod";
3200
+ var genericRecordSchema4 = z8.record(z8.string(), z8.unknown());
3201
+ var appScopedInputSchema = {
3202
+ ...commonRequestFieldsSchema,
3203
+ appId: z8.string().trim().min(1).optional()
3204
+ };
3205
+ var editQueueInputSchema = {
3206
+ ...appScopedInputSchema,
3207
+ limit: z8.number().int().positive().max(100).optional(),
3208
+ offset: z8.number().int().nonnegative().optional()
3209
+ };
3210
+ var bundleInputSchema = {
3211
+ ...appScopedInputSchema,
3212
+ bundleId: z8.string().trim().min(1)
3213
+ };
3214
+ var timelineInputSchema = {
3215
+ ...appScopedInputSchema,
3216
+ limit: z8.number().int().positive().max(100).optional(),
3217
+ cursor: z8.string().trim().min(1).optional()
3218
+ };
3219
+ var agentRunsInputSchema = {
3220
+ ...appScopedInputSchema,
3221
+ limit: z8.number().int().positive().max(100).optional(),
3222
+ offset: z8.number().int().nonnegative().optional(),
3223
+ status: z8.string().trim().min(1).optional(),
3224
+ currentPhase: z8.string().trim().min(1).optional(),
3225
+ createdAfter: z8.string().datetime().optional(),
3226
+ createdBefore: z8.string().datetime().optional()
3227
+ };
3228
+ var agentRunInputSchema = {
3229
+ ...appScopedInputSchema,
3230
+ runId: z8.string().trim().min(1)
3231
+ };
3232
+ var agentRunEventsInputSchema = {
3233
+ ...appScopedInputSchema,
3234
+ runId: z8.string().trim().min(1),
3235
+ limit: z8.number().int().positive().max(100).optional(),
3236
+ offset: z8.number().int().nonnegative().optional(),
3237
+ createdAfter: z8.string().datetime().optional(),
3238
+ createdBefore: z8.string().datetime().optional()
3239
+ };
3240
+ var appOverviewSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3241
+ var editQueueSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3242
+ var bundleSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3243
+ var timelineSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3244
+ var agentRunsSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3245
+ var agentRunSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3246
+ var agentRunEventsSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3247
+ var sandboxStatusSuccessSchema = makeSuccessSchema(genericRecordSchema4);
3248
+
3249
+ // src/domain/opsAdapter.ts
3250
+ async function resolveAppTarget(_api, params) {
3251
+ const explicitAppId = params.appId?.trim();
3252
+ const bindingContext = await loadBindingContext(params.cwd);
3253
+ if (explicitAppId) {
3254
+ return {
3255
+ appId: explicitAppId,
3256
+ repoRoot: bindingContext.repoRoot
3257
+ };
3258
+ }
3259
+ if (!bindingContext.binding) {
3260
+ throw makeNotBoundError("App id was not provided and the current repository is not bound to Remix.");
3261
+ }
3262
+ return {
3263
+ appId: bindingContext.binding.currentAppId,
3264
+ repoRoot: bindingContext.repoRoot
3265
+ };
3266
+ }
3267
+ async function getAppOverview(params) {
3268
+ const api = await createApiClient();
3269
+ const target = await resolveAppTarget(api, params);
3270
+ const data = unwrapResponseObject(await api.getAppOverview(target.appId), "app overview");
3271
+ return {
3272
+ data,
3273
+ warnings: [],
3274
+ recommendedNextActions: [
3275
+ "Use `remix_ops_list_timeline`, `remix_ops_list_agent_runs`, `remix_ops_get_edit_queue`, or `remix_ops_get_sandbox_status` for the next operational drill-down on this app."
3276
+ ],
3277
+ logContext: target
3278
+ };
3279
+ }
3280
+ async function getEditQueue(params) {
3281
+ const api = await createApiClient();
3282
+ const target = await resolveAppTarget(api, params);
3283
+ const data = unwrapResponseObject(
3284
+ await api.listAppEditQueue(target.appId, {
3285
+ limit: params.limit,
3286
+ offset: params.offset
3287
+ }),
3288
+ "edit queue"
3289
+ );
3290
+ const pageInfo = typeof data.pageInfo === "object" && data.pageInfo ? data.pageInfo : null;
3291
+ return {
3292
+ data,
3293
+ warnings: [],
3294
+ recommendedNextActions: pageInfo?.hasMore === true && typeof pageInfo.limit === "number" && typeof pageInfo.offset === "number" ? [`Pass offset=${pageInfo.offset + pageInfo.limit} to load the next edit queue page.`] : [],
3295
+ logContext: target
3296
+ };
3297
+ }
3298
+ async function getBundle(params) {
3299
+ const api = await createApiClient();
3300
+ const target = await resolveAppTarget(api, params);
3301
+ const data = unwrapResponseObject(await api.getBundle(target.appId, params.bundleId), "bundle");
3302
+ return {
3303
+ data,
3304
+ warnings: [],
3305
+ recommendedNextActions: ["Use the bundle download-url client methods only when you need the actual artifact, not just metadata inspection."],
3306
+ logContext: target
3307
+ };
3308
+ }
3309
+ async function listTimeline(params) {
3310
+ const api = await createApiClient();
3311
+ const target = await resolveAppTarget(api, params);
3312
+ const data = unwrapResponseObject(
3313
+ await api.listAppTimeline(target.appId, {
3314
+ limit: params.limit,
3315
+ cursor: params.cursor
3316
+ }),
3317
+ "app timeline"
3318
+ );
3319
+ const pageInfo = typeof data.pageInfo === "object" && data.pageInfo ? data.pageInfo : null;
3320
+ return {
3321
+ data,
3322
+ warnings: [],
3323
+ recommendedNextActions: pageInfo?.hasMore === true && typeof pageInfo.nextCursor === "string" ? [`Pass cursor=${pageInfo.nextCursor} to load the next timeline page.`] : [],
3324
+ logContext: target
3325
+ };
3326
+ }
3327
+ async function listAgentRuns(params) {
3328
+ const api = await createApiClient();
3329
+ const target = await resolveAppTarget(api, params);
3330
+ const data = unwrapResponseObject(
3331
+ await api.listAgentRuns(target.appId, {
3332
+ limit: params.limit,
3333
+ offset: params.offset,
3334
+ status: params.status,
3335
+ currentPhase: params.currentPhase,
3336
+ createdAfter: params.createdAfter,
3337
+ createdBefore: params.createdBefore
3338
+ }),
3339
+ "agent runs"
3340
+ );
3341
+ const pageInfo = typeof data.pageInfo === "object" && data.pageInfo ? data.pageInfo : null;
3342
+ const items = Array.isArray(data.items) ? data.items : [];
3343
+ const firstRunId = items.length > 0 && items[0] && typeof items[0] === "object" && typeof items[0].id === "string" ? items[0].id : null;
3344
+ const recommendedNextActions = [];
3345
+ if (firstRunId) {
3346
+ recommendedNextActions.push(
3347
+ `Use \`remix_ops_get_agent_run\` with \`runId=${firstRunId}\` for the top listed run, then \`remix_ops_list_agent_run_events\` if you need its event stream.`
3348
+ );
3349
+ }
3350
+ if (pageInfo?.hasMore === true && typeof pageInfo.limit === "number" && typeof pageInfo.offset === "number") {
3351
+ recommendedNextActions.push(`Pass offset=${pageInfo.offset + pageInfo.limit} to load the next agent-runs page.`);
3352
+ }
3353
+ return {
3354
+ data,
3355
+ warnings: [],
3356
+ recommendedNextActions,
3357
+ logContext: target
3358
+ };
3359
+ }
3360
+ async function getAgentRun(params) {
3361
+ const api = await createApiClient();
3362
+ const target = await resolveAppTarget(api, params);
3363
+ const data = unwrapResponseObject(await api.getAgentRun(target.appId, params.runId), "agent run");
3364
+ return {
3365
+ data,
3366
+ warnings: [],
3367
+ recommendedNextActions: [`Use \`remix_ops_list_agent_run_events\` with \`runId=${params.runId}\` for the event stream behind this run.`],
3368
+ logContext: target
3369
+ };
3370
+ }
3371
+ async function listAgentRunEvents(params) {
3372
+ const api = await createApiClient();
3373
+ const target = await resolveAppTarget(api, params);
3374
+ const data = unwrapResponseObject(
3375
+ await api.listAgentRunEvents(target.appId, params.runId, {
3376
+ limit: params.limit,
3377
+ offset: params.offset,
3378
+ createdAfter: params.createdAfter,
3379
+ createdBefore: params.createdBefore
3380
+ }),
3381
+ "agent run events"
3382
+ );
3383
+ const pageInfo = typeof data.pageInfo === "object" && data.pageInfo ? data.pageInfo : null;
3384
+ return {
3385
+ data,
3386
+ warnings: [],
3387
+ recommendedNextActions: pageInfo?.hasMore === true && typeof pageInfo.limit === "number" && typeof pageInfo.offset === "number" ? [`Pass offset=${pageInfo.offset + pageInfo.limit} to load the next event page.`] : [],
3388
+ logContext: target
3389
+ };
3390
+ }
3391
+ async function getSandboxStatus(params) {
3392
+ const api = await createApiClient();
3393
+ const target = await resolveAppTarget(api, params);
3394
+ const data = unwrapResponseObject(await api.getSandboxStatus(target.appId), "sandbox status");
3395
+ return {
3396
+ data,
3397
+ warnings: [],
3398
+ recommendedNextActions: ["Use the sandbox metadata here to decide whether a resume is plausible before attempting any write-side sandbox action."],
3399
+ logContext: target
3400
+ };
3401
+ }
3402
+
3403
+ // src/tools/ops/register.ts
3404
+ function getAnnotations4(access) {
3405
+ return {
3406
+ readOnlyHint: access === "read",
3407
+ destructiveHint: false,
3408
+ idempotentHint: true,
3409
+ openWorldHint: false
3410
+ };
3411
+ }
3412
+ function buildSuccessEnvelope4(tool, requestId, result) {
3413
+ return {
3414
+ schemaVersion: SCHEMA_VERSION,
3415
+ ok: true,
3416
+ tool,
3417
+ requestId: requestId ?? null,
3418
+ data: result.data,
3419
+ warnings: result.warnings ?? [],
3420
+ risks: result.risks ?? [],
3421
+ recommendedNextActions: result.recommendedNextActions ?? []
3422
+ };
3423
+ }
3424
+ function deriveErrorRisks4(normalized) {
3425
+ if (normalized.code === "DESTRUCTIVE_OPERATION_BLOCKED") {
3426
+ return ["A policy guard blocked a disallowed operation."];
3427
+ }
3428
+ return [];
3429
+ }
3430
+ function buildErrorEnvelope4(tool, requestId, error) {
3431
+ const normalized = normalizeToolError(error);
3432
+ return {
3433
+ schemaVersion: SCHEMA_VERSION,
3434
+ ok: false,
3435
+ tool,
3436
+ requestId: requestId ?? null,
3437
+ error: normalized,
3438
+ warnings: [],
3439
+ risks: deriveErrorRisks4(normalized),
3440
+ recommendedNextActions: normalized.code === "AUTH_REQUIRED" ? ["Run `remix login` or set COMERGE_ACCESS_TOKEN, then retry."] : []
3441
+ };
3442
+ }
3443
+ function registerTool4(server, context, params) {
3444
+ const errorSchema = makeErrorSchema();
3445
+ server.registerTool(
3446
+ params.name,
3447
+ {
3448
+ title: params.name,
3449
+ description: params.description,
3450
+ inputSchema: params.inputSchema,
3451
+ outputSchema: params.outputSchema,
3452
+ annotations: getAnnotations4(params.access)
3453
+ },
3454
+ async (rawArgs) => {
3455
+ const requestId = typeof rawArgs.requestId === "string" ? rawArgs.requestId : void 0;
3456
+ const startedAt = Date.now();
3457
+ try {
3458
+ assertToolAccess(context.policy, params.access);
3459
+ const result = await params.run(rawArgs);
3460
+ const envelope = buildSuccessEnvelope4(params.name, requestId, result);
3461
+ params.outputSchema.parse(envelope);
3462
+ context.logger.log({
3463
+ level: "info",
3464
+ message: "tool_completed",
3465
+ tool: params.name,
3466
+ requestId: envelope.requestId,
3467
+ durationMs: Date.now() - startedAt,
3468
+ result: "success",
3469
+ repoRoot: result.logContext?.repoRoot ?? null,
3470
+ appId: result.logContext?.appId ?? null,
3471
+ mrId: result.logContext?.mrId ?? null
3472
+ });
3473
+ return makeSuccessResult(envelope);
3474
+ } catch (error) {
3475
+ const envelope = buildErrorEnvelope4(params.name, requestId, error);
3476
+ errorSchema.parse(envelope);
3477
+ context.logger.log({
3478
+ level: "error",
3479
+ message: "tool_failed",
3480
+ tool: params.name,
3481
+ requestId: envelope.requestId,
3482
+ durationMs: Date.now() - startedAt,
3483
+ result: "error",
3484
+ errorCode: envelope.error.code
3485
+ });
3486
+ return makeErrorResult(envelope);
3487
+ }
3488
+ }
3489
+ );
3490
+ }
3491
+ function registerOpsTools(server, context) {
3492
+ registerTool4(server, context, {
3493
+ name: "remix_context_get_app_overview",
3494
+ description: "Read the current app's overview, capabilities, and workflow readiness without mutating state.",
3495
+ access: "read",
3496
+ inputSchema: appScopedInputSchema,
3497
+ outputSchema: appOverviewSuccessSchema,
3498
+ run: async (args) => {
3499
+ const input = z9.object(appScopedInputSchema).parse(args);
3500
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
3501
+ return getAppOverview({
3502
+ appId: input.appId,
3503
+ cwd
3504
+ });
3505
+ }
3506
+ });
3507
+ registerTool4(server, context, {
3508
+ name: "remix_ops_get_edit_queue",
3509
+ description: "Inspect the pending edit queue for one app with bounded pagination.",
3510
+ access: "read",
3511
+ inputSchema: editQueueInputSchema,
3512
+ outputSchema: editQueueSuccessSchema,
3513
+ run: async (args) => {
3514
+ const input = z9.object(editQueueInputSchema).parse(args);
3515
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
3516
+ return getEditQueue({
3517
+ appId: input.appId,
3518
+ cwd,
3519
+ limit: input.limit,
3520
+ offset: input.offset
3521
+ });
3522
+ }
3523
+ });
3524
+ registerTool4(server, context, {
3525
+ name: "remix_ops_get_bundle",
3526
+ description: "Inspect one app bundle by id without downloading the artifact payload.",
3527
+ access: "read",
3528
+ inputSchema: bundleInputSchema,
3529
+ outputSchema: bundleSuccessSchema,
3530
+ run: async (args) => {
3531
+ const input = z9.object(bundleInputSchema).parse(args);
3532
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
3533
+ return getBundle({
3534
+ appId: input.appId,
3535
+ cwd,
3536
+ bundleId: input.bundleId
3537
+ });
3538
+ }
3539
+ });
3540
+ registerTool4(server, context, {
3541
+ name: "remix_ops_list_timeline",
3542
+ description: "List bounded timeline events for one app using the backend cursor pagination model.",
3543
+ access: "read",
3544
+ inputSchema: timelineInputSchema,
3545
+ outputSchema: timelineSuccessSchema,
3546
+ run: async (args) => {
3547
+ const input = z9.object(timelineInputSchema).parse(args);
3548
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
3549
+ return listTimeline({
3550
+ appId: input.appId,
3551
+ cwd,
3552
+ limit: input.limit,
3553
+ cursor: input.cursor
3554
+ });
3555
+ }
3556
+ });
3557
+ registerTool4(server, context, {
3558
+ name: "remix_ops_get_agent_run",
3559
+ description: "Fetch one stored agent run for an app, including status, phase, and summary metadata.",
3560
+ access: "read",
3561
+ inputSchema: agentRunInputSchema,
3562
+ outputSchema: agentRunSuccessSchema,
3563
+ run: async (args) => {
3564
+ const input = z9.object(agentRunInputSchema).parse(args);
3565
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
3566
+ return getAgentRun({
3567
+ appId: input.appId,
3568
+ cwd,
3569
+ runId: input.runId
3570
+ });
3571
+ }
3572
+ });
3573
+ registerTool4(server, context, {
3574
+ name: "remix_ops_list_agent_runs",
3575
+ description: "List paginated agent runs for one app so run ids can be discovered before deeper inspection.",
3576
+ access: "read",
3577
+ inputSchema: agentRunsInputSchema,
3578
+ outputSchema: agentRunsSuccessSchema,
3579
+ run: async (args) => {
3580
+ const input = z9.object(agentRunsInputSchema).parse(args);
3581
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
3582
+ return listAgentRuns({
3583
+ appId: input.appId,
3584
+ cwd,
3585
+ limit: input.limit,
3586
+ offset: input.offset,
3587
+ status: input.status,
3588
+ currentPhase: input.currentPhase,
3589
+ createdAfter: input.createdAfter,
3590
+ createdBefore: input.createdBefore
3591
+ });
3592
+ }
3593
+ });
3594
+ registerTool4(server, context, {
3595
+ name: "remix_ops_list_agent_run_events",
3596
+ description: "List paginated agent-run events for one stored run, with optional time bounds.",
3597
+ access: "read",
3598
+ inputSchema: agentRunEventsInputSchema,
3599
+ outputSchema: agentRunEventsSuccessSchema,
3600
+ run: async (args) => {
3601
+ const input = z9.object(agentRunEventsInputSchema).parse(args);
3602
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
3603
+ return listAgentRunEvents({
3604
+ appId: input.appId,
3605
+ cwd,
3606
+ runId: input.runId,
3607
+ limit: input.limit,
3608
+ offset: input.offset,
3609
+ createdAfter: input.createdAfter,
3610
+ createdBefore: input.createdBefore
3611
+ });
3612
+ }
3613
+ });
3614
+ registerTool4(server, context, {
3615
+ name: "remix_ops_get_sandbox_status",
3616
+ description: "Read safe sandbox metadata and the latest migration status for one app without exposing execution handles or secrets.",
3617
+ access: "read",
3618
+ inputSchema: appScopedInputSchema,
3619
+ outputSchema: sandboxStatusSuccessSchema,
3620
+ run: async (args) => {
3621
+ const input = z9.object(appScopedInputSchema).parse(args);
3622
+ const cwd = input.cwd ? resolvePolicyCwd(context.policy, input.cwd) : void 0;
3623
+ return getSandboxStatus({
3624
+ appId: input.appId,
3625
+ cwd
3626
+ });
3627
+ }
3628
+ });
3629
+ }
3630
+
1972
3631
  // src/server.ts
1973
3632
  function createRemixMcpServer(params) {
1974
3633
  const context = createServerContext({ version: params.version });
@@ -1976,7 +3635,9 @@ function createRemixMcpServer(params) {
1976
3635
  name: context.serverName,
1977
3636
  version: context.version
1978
3637
  });
3638
+ registerIdentityTools(server, context);
1979
3639
  registerCollabTools(server, context);
3640
+ registerOpsTools(server, context);
1980
3641
  registerMemoryTools(server, context);
1981
3642
  return { server, context };
1982
3643
  }