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