@remixhq/mcp 0.1.13 → 0.1.15

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { createRequire } from "module";
5
- import { drainPendingFinalizeQueue } from "@remixhq/core/collab";
5
+ import { drainAsyncJobs, drainPendingFinalizeQueue } from "@remixhq/core/collab";
6
6
 
7
7
  // src/domain/apiClient.ts
8
8
  import { createApiClient as createCoreApiClient, resolveConfig as resolveConfig2 } from "@remixhq/core";
@@ -623,7 +623,7 @@ var statusDataSchema = z2.object({
623
623
  status: genericRecordSchema,
624
624
  riskLevel: z2.enum(["low", "medium", "high"])
625
625
  });
626
- var initDataSchema = z2.object({
626
+ var initSyncDataSchema = z2.object({
627
627
  reused: z2.boolean(),
628
628
  projectId: z2.string(),
629
629
  appId: z2.string(),
@@ -635,6 +635,23 @@ var initDataSchema = z2.object({
635
635
  createdCanonicalFamily: z2.boolean().optional(),
636
636
  baselineStatus: z2.enum(["seeded", "existing", "requires_re_anchor", "requires_sync"]).optional()
637
637
  });
638
+ var initQueuedDataSchema = z2.object({
639
+ queued: z2.literal(true),
640
+ status: z2.literal("ready_to_record"),
641
+ doNotRetry: z2.literal(true),
642
+ jobId: z2.string(),
643
+ projectId: z2.string(),
644
+ appId: z2.string(),
645
+ dashboardUrl: z2.string(),
646
+ upstreamAppId: z2.string(),
647
+ bindingPath: z2.string(),
648
+ repoRoot: z2.string(),
649
+ bindingMode: z2.enum(["legacy", "lane", "explicit_root"]),
650
+ createdCanonicalFamily: z2.boolean(),
651
+ remoteUrl: z2.string().nullable(),
652
+ defaultBranch: z2.string().nullable()
653
+ });
654
+ var initDataSchema = initSyncDataSchema;
638
655
  var listDataSchema = z2.object({
639
656
  apps: genericArraySchema,
640
657
  pagination: paginationSchema
@@ -814,9 +831,15 @@ function getRecommendedNextActions(status) {
814
831
  case "pull":
815
832
  return ["Run remix_collab_sync_preview, then remix_collab_sync_apply if the preview is acceptable. This pulls the server delta into the local working tree without rewriting local git history."];
816
833
  case "re_anchor":
817
- return ["Run remix_collab_re_anchor_preview, then remix_collab_re_anchor_apply to re-anchor the lane after manual Git/GitHub history movement."];
834
+ return [
835
+ "Run remix_collab_re_anchor_preview, then remix_collab_re_anchor_apply. This seeds a local Remix baseline. It is required because no local baseline exists for this lane yet (fresh clone, deleted .remix/ state, or first init didn't seed) \u2014 not because of any specific git operation. After it succeeds, normal recording (remix_collab_finalize_turn) becomes available."
836
+ ];
837
+ case "record":
838
+ return [
839
+ "Run remix_collab_finalize_turn to capture the local boundary delta. This is the catch-all for any local content change since the last recorded turn, regardless of whether the change came from agent edits, manual user edits, git commit, git pull, git merge, git rebase, or git reset."
840
+ ];
818
841
  case "reconcile":
819
- return ["Run remix_collab_reconcile_preview before attempting remix_collab_reconcile_apply. Reconcile now routes through the explicit Remix recovery/re-anchor flow for diverged state."];
842
+ return ["Run remix_collab_reconcile_preview before attempting remix_collab_reconcile_apply. Reconcile applies only when both the local workspace and the server lane changed since the last agreed baseline."];
820
843
  case "await_finalize":
821
844
  return [
822
845
  "Run remix_collab_drain_finalize_queue before merge-related or recovery flows. finalize_turn is queued only until the local finalize queue is drained."
@@ -865,6 +888,10 @@ function spawnFinalizeQueueDrainer() {
865
888
  child.unref();
866
889
  return true;
867
890
  }
891
+ async function drainBeforeMutation(api) {
892
+ const results = await coreDrainPendingFinalizeQueue({ api });
893
+ return results.flatMap((result) => collectResultWarnings(result));
894
+ }
868
895
  async function getStatus(params) {
869
896
  const api = params.includeRemote ? await createCollabApiClient() : null;
870
897
  const status = await coreCollabStatus({
@@ -890,19 +917,26 @@ async function initCollab(params) {
890
917
  api,
891
918
  cwd: params.cwd,
892
919
  appName: params.appName ?? null,
893
- forceNew: params.forceNew ?? false
920
+ forceNew: params.forceNew ?? false,
921
+ asyncSubmit: false
894
922
  });
923
+ if ("queued" in result && result.queued) {
924
+ throw new Error(
925
+ "Unexpected queued init result on the MCP path. Async init is currently disabled for agent callers; if you intended to re-enable it, also restore the queued shape in initDataSchema (see remix-mcp/src/contracts/collab.ts)."
926
+ );
927
+ }
928
+ const syncResult = result;
895
929
  return {
896
- data: result,
930
+ data: syncResult,
897
931
  warnings: collectResultWarnings(result),
898
- recommendedNextActions: result.baselineStatus === "requires_re_anchor" ? [
899
- "Run remix_collab_re_anchor_preview, then remix_collab_re_anchor_apply to anchor this checkout before recording turns."
900
- ] : result.baselineStatus === "requires_sync" ? [
932
+ recommendedNextActions: syncResult.baselineStatus === "requires_re_anchor" ? [
933
+ "This checkout has no local Remix baseline yet. Run remix_collab_re_anchor_preview, then remix_collab_re_anchor_apply to seed one. After it succeeds, normal recording (remix_collab_finalize_turn) becomes available."
934
+ ] : syncResult.baselineStatus === "requires_sync" ? [
901
935
  "Run remix_collab_sync_preview, then remix_collab_sync_apply to pull the server delta and create the first local baseline for this checkout."
902
936
  ] : ["Run remix_collab_status to inspect sync, reconcile, and merge-request readiness before mutating bound-repo state."],
903
937
  logContext: {
904
- repoRoot: result.repoRoot,
905
- appId: result.appId
938
+ repoRoot: syncResult.repoRoot,
939
+ appId: syncResult.appId
906
940
  }
907
941
  };
908
942
  }
@@ -972,9 +1006,11 @@ async function finalizeCollabTurn(params) {
972
1006
  sync: params.sync,
973
1007
  allowBranchMismatch: params.allowBranchMismatch ?? false,
974
1008
  idempotencyKey: params.idempotencyKey ?? null,
975
- actor: params.agent
1009
+ actor: params.agent,
1010
+ awaitingUsageDeadlineMs: params.awaitingUsageDeadlineMs ?? null
976
1011
  });
977
- if (result.queued) {
1012
+ const hasAwaitingDeadline = typeof params.awaitingUsageDeadlineMs === "number" && params.awaitingUsageDeadlineMs > 0;
1013
+ if (result.queued && !hasAwaitingDeadline) {
978
1014
  if (!spawnFinalizeQueueDrainer()) {
979
1015
  await coreDrainPendingFinalizeQueue({ api });
980
1016
  }
@@ -1038,7 +1074,9 @@ async function reAnchor(params) {
1038
1074
  return {
1039
1075
  data: result,
1040
1076
  warnings: collectWarnings(result.warnings),
1041
- recommendedNextActions: params.dryRun ? ["Run remix_collab_re_anchor_apply with confirm=true to re-anchor this lane after manual Git/GitHub history movement."] : [],
1077
+ recommendedNextActions: params.dryRun ? [
1078
+ "Run remix_collab_re_anchor_apply with confirm=true to seed a local Remix baseline for this checkout. Re-anchor is for missing-baseline cases only and does not replace remix_collab_finalize_turn for ordinary local content changes."
1079
+ ] : [],
1042
1080
  logContext: {
1043
1081
  repoRoot: result.repoRoot,
1044
1082
  appId: result.currentAppId
@@ -1047,13 +1085,14 @@ async function reAnchor(params) {
1047
1085
  }
1048
1086
  async function requestMerge(params) {
1049
1087
  const api = await createCollabApiClient();
1088
+ const drainWarnings = await drainBeforeMutation(api);
1050
1089
  const result = await coreCollabRequestMerge({
1051
1090
  api,
1052
1091
  cwd: params.cwd
1053
1092
  });
1054
1093
  return {
1055
1094
  data: result,
1056
- warnings: [],
1095
+ warnings: drainWarnings,
1057
1096
  recommendedNextActions: result.id ? [`Run remix_collab_view_merge_request with mrId=${String(result.id)} to inspect the request before deciding whether to approve or reject it.`] : [],
1058
1097
  logContext: {
1059
1098
  mrId: typeof result.id === "string" ? result.id : null
@@ -1209,6 +1248,7 @@ async function syncUpstream(params) {
1209
1248
  }
1210
1249
  async function reconcile(params) {
1211
1250
  const api = await createCollabApiClient();
1251
+ const drainWarnings = params.dryRun ? [] : await drainBeforeMutation(api);
1212
1252
  const result = await coreCollabReconcile({
1213
1253
  api,
1214
1254
  cwd: params.cwd,
@@ -1217,9 +1257,9 @@ async function reconcile(params) {
1217
1257
  });
1218
1258
  return {
1219
1259
  data: result,
1220
- warnings: collectWarnings(result.warnings),
1260
+ warnings: [...drainWarnings, ...collectWarnings(result.warnings)],
1221
1261
  recommendedNextActions: params.dryRun ? ["Run remix_collab_reconcile_apply with confirm=true only if the preview is acceptable. This is the explicit Remix recovery flow for diverged state."] : [],
1222
- risks: params.dryRun ? ["Reconcile may upload local history to re-anchor the server lane before future recording continues."] : [],
1262
+ risks: params.dryRun ? ["Reconcile may upload local history to recover the server lane onto the latest agreed state before future recording continues."] : [],
1223
1263
  logContext: {
1224
1264
  repoRoot: result.repoRoot ?? null
1225
1265
  }
@@ -1766,6 +1806,50 @@ async function accessDebug(params) {
1766
1806
  };
1767
1807
  }
1768
1808
 
1809
+ // src/tools/collab/autoSpawnHistoryImport.ts
1810
+ import { spawn as spawn2 } from "child_process";
1811
+ import { existsSync, mkdirSync, openSync } from "fs";
1812
+ import path2 from "path";
1813
+ var MARKER_REL_PATH = path2.join(".remix", ".history-imported");
1814
+ var LOG_REL_PATH = path2.join(".remix", "history-import.log");
1815
+ function shouldAutoSpawnHistoryImport(repoRoot) {
1816
+ try {
1817
+ return !existsSync(path2.join(repoRoot, MARKER_REL_PATH));
1818
+ } catch {
1819
+ return false;
1820
+ }
1821
+ }
1822
+ function spawnHistoryImportDetached(repoRoot) {
1823
+ const remixDir = path2.join(repoRoot, ".remix");
1824
+ try {
1825
+ mkdirSync(remixDir, { recursive: true });
1826
+ } catch {
1827
+ }
1828
+ const logPath = path2.join(repoRoot, LOG_REL_PATH);
1829
+ const out = openSync(logPath, "a");
1830
+ const err = openSync(logPath, "a");
1831
+ const child = spawn2(
1832
+ "remix",
1833
+ [
1834
+ "history",
1835
+ "import",
1836
+ "--repo",
1837
+ repoRoot,
1838
+ // Include prompt text for parity with the CLI auto-spawn path:
1839
+ // first-time UX is a lot worse if the dashboard renders every
1840
+ // historical row as "(prompt not uploaded)".
1841
+ "--include-prompt-text"
1842
+ ],
1843
+ {
1844
+ detached: true,
1845
+ stdio: ["ignore", out, err],
1846
+ env: { ...process.env, REMIX_HISTORY_AUTO_SPAWN: "1" }
1847
+ }
1848
+ );
1849
+ child.unref();
1850
+ return { pid: child.pid, logPath };
1851
+ }
1852
+
1769
1853
  // src/tools/collab/register.ts
1770
1854
  function getAnnotations(access, options) {
1771
1855
  if (access === "read") {
@@ -1887,18 +1971,37 @@ function registerCollabTools(server, context) {
1887
1971
  });
1888
1972
  registerTool(server, context, {
1889
1973
  name: "remix_collab_init",
1890
- description: "Import the current repository into Remix and write the local binding file.",
1974
+ description: "Import the current repository into Remix and write the local binding file. Synchronous: by the time this tool resolves, the local binding file AND the local Remix baseline are both on disk, so the very next call to remix_collab_finalize_turn will succeed. Brand-new init on the default branch typically takes ~10s; non-default-branch init can take 30-90s while the server provisions a feature lane. The result includes `reused: boolean` (false for a brand-new app, true if a binding already existed) plus the canonical app/project identifiers and the dashboard URL. Use forceNew=true only when intentionally creating a new canonical family from scratch in a previously-bound repo; do NOT use forceNew as a retry mechanism for a failed init \u2014 it creates orphan backend apps and triggers canonical-family ambiguity errors on subsequent inits in this directory.",
1891
1975
  access: "remote_write",
1892
1976
  inputSchema: initInputSchema,
1893
1977
  outputSchema: initSuccessSchema,
1894
1978
  run: async (args) => {
1895
1979
  const input = z3.object(initInputSchema).parse(args);
1896
1980
  const cwd = resolvePolicyCwd(context.policy, input.cwd);
1897
- return initCollab({
1981
+ const result = await initCollab({
1898
1982
  cwd,
1899
1983
  appName: input.appName,
1900
1984
  forceNew: input.forceNew
1901
1985
  });
1986
+ try {
1987
+ const repoRoot = result && typeof result === "object" && "data" in result && result.data && typeof result.data.repoRoot === "string" ? result.data.repoRoot : null;
1988
+ if (repoRoot && shouldAutoSpawnHistoryImport(repoRoot)) {
1989
+ const spawned = spawnHistoryImportDetached(repoRoot);
1990
+ context.logger.log({
1991
+ level: "info",
1992
+ message: `history_import_auto_spawned pid=${spawned.pid ?? "?"} log=${spawned.logPath}`,
1993
+ tool: "remix_collab_init",
1994
+ repoRoot
1995
+ });
1996
+ }
1997
+ } catch (spawnError) {
1998
+ context.logger.log({
1999
+ level: "error",
2000
+ message: `history_import_auto_spawn_failed: ${spawnError instanceof Error ? spawnError.message : String(spawnError)}`,
2001
+ tool: "remix_collab_init"
2002
+ });
2003
+ }
2004
+ return result;
1902
2005
  }
1903
2006
  });
1904
2007
  registerTool(server, context, {
@@ -1954,7 +2057,7 @@ function registerCollabTools(server, context) {
1954
2057
  });
1955
2058
  registerTool(server, context, {
1956
2059
  name: "remix_collab_finalize_turn",
1957
- description: "Primary turn recorder for the current bound repository. Call this exactly once before the final response; it captures the current boundary locally and queues remote processing. Queued only: no remote change step exists yet until the finalize queue is drained.",
2060
+ description: "Primary turn recorder for the current bound repository. Required: call this exactly once before the final response on every turn that touched a Remix-bound repo. Captures the current boundary locally and queues remote processing. Queued only: no remote change step exists yet until the finalize queue drains. Runtime exception: if this turn's context contains a [Remix runtime status] system note from the Remix UserPromptSubmit hook stating that the Stop hook will record this turn automatically, do NOT call this tool \u2014 the runtime is recording it on your behalf and a manual call would create a duplicate record. The runtime note is the only sanctioned override; trivial prompts, error states, and ambiguity all still require this call.",
1958
2061
  access: "local_write",
1959
2062
  inputSchema: finalizeTurnInputSchema,
1960
2063
  outputSchema: finalizeTurnSuccessSchema,
@@ -1969,13 +2072,14 @@ function registerCollabTools(server, context) {
1969
2072
  sync: input.sync,
1970
2073
  allowBranchMismatch: input.allowBranchMismatch ?? false,
1971
2074
  idempotencyKey: input.idempotencyKey,
1972
- agent: context.agentMetadata
2075
+ agent: context.agentMetadata,
2076
+ awaitingUsageDeadlineMs: 3e4
1973
2077
  });
1974
2078
  }
1975
2079
  });
1976
2080
  registerTool(server, context, {
1977
2081
  name: "remix_collab_drain_finalize_queue",
1978
- description: "Drain the local finalize queue and record queued finalize_turn jobs immediately. Use this before request-merge, reconcile, or other flows that require the remote change step to exist already.",
2082
+ description: "Drain the local finalize queue and record queued finalize_turn jobs immediately. NOT required as a precondition for `remix_collab_request_merge` or `remix_collab_reconcile_apply` \u2014 those tools drain the queue internally before they run. Useful only for explicit recovery flows (e.g. status reports `await_finalize` and you want to flush before re-checking). Runtime exception: if this turn's context contains a [Remix runtime status] system note from the Remix UserPromptSubmit hook, the runtime drains the queue automatically in the background; do NOT call this tool unless an explicit recovery flow requires it.",
1979
2083
  access: "local_write",
1980
2084
  inputSchema: drainFinalizeQueueInputSchema,
1981
2085
  outputSchema: drainFinalizeQueueSuccessSchema,
@@ -2013,7 +2117,7 @@ function registerCollabTools(server, context) {
2013
2117
  });
2014
2118
  registerTool(server, context, {
2015
2119
  name: "remix_collab_re_anchor_preview",
2016
- description: "Preview whether the current checkout needs an explicit re-anchor to adopt or recover external Git/GitHub history.",
2120
+ description: "Preview whether this checkout needs a fresh local Remix baseline. Use only when status reports `re_anchor` (no local baseline exists for this lane yet \u2014 fresh clone, deleted `.remix/` state, or first init didn't seed). Re-anchor does not replace `remix_collab_finalize_turn`; ordinary local content changes (including merges, pulls, and rebases) are recorded by `finalize-turn`, not by re-anchor.",
2017
2121
  access: "read",
2018
2122
  inputSchema: previewInputSchema,
2019
2123
  outputSchema: reAnchorSuccessSchema,
@@ -2025,7 +2129,7 @@ function registerCollabTools(server, context) {
2025
2129
  });
2026
2130
  registerTool(server, context, {
2027
2131
  name: "remix_collab_re_anchor_apply",
2028
- description: "Explicitly re-anchor the current lane to adopted or recovered external Git/GitHub history, without rewriting the local checkout afterward.",
2132
+ description: "Establish a local Remix baseline for the current checkout against the existing app head, without rewriting the local checkout afterward. Required only when status reports `re_anchor` (missing local baseline). It does not replace `remix_collab_finalize_turn` \u2014 local commits, pulls, merges, and rebases must still be recorded with `finalize-turn`.",
2029
2133
  access: "local_write",
2030
2134
  inputSchema: reAnchorInputSchema,
2031
2135
  outputSchema: reAnchorSuccessSchema,
@@ -3847,6 +3951,11 @@ async function main() {
3847
3951
  await drainPendingFinalizeQueue({ api });
3848
3952
  return;
3849
3953
  }
3954
+ if (process.argv.includes("--drain-async-jobs")) {
3955
+ const api = await createCollabApiClient();
3956
+ await drainAsyncJobs({ api });
3957
+ return;
3958
+ }
3850
3959
  await startStdioServer({ version });
3851
3960
  }
3852
3961
  main().catch((error) => {