@paradigma-inc/flywheel 0.1.52 → 0.1.60

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.
Files changed (122) hide show
  1. package/README.md +121 -10
  2. package/package.json +1 -1
  3. package/skills/flywheel/references/flywheel-cli-tool-map.md +8 -0
  4. package/skills/flywheel/references/flywheel-mcp-tool-map.md +8 -0
  5. package/src/agents.mjs +114 -48
  6. package/src/cli.mjs +267 -35
  7. package/src/mcp-writer.mjs +190 -17
  8. package/src/runtime/required-runtime-commands.json +17 -4
  9. package/src/runtime/vendor/flywheel-cli-dist/auth/login.d.ts +1 -0
  10. package/src/runtime/vendor/flywheel-cli-dist/auth/login.js +3 -6
  11. package/src/runtime/vendor/flywheel-cli-dist/auth/login.js.map +1 -1
  12. package/src/runtime/vendor/flywheel-cli-dist/auth/profiles.d.ts +1 -0
  13. package/src/runtime/vendor/flywheel-cli-dist/auth/profiles.js +1 -1
  14. package/src/runtime/vendor/flywheel-cli-dist/auth/profiles.js.map +1 -1
  15. package/src/runtime/vendor/flywheel-cli-dist/commands/_admin-auth.d.ts +25 -0
  16. package/src/runtime/vendor/flywheel-cli-dist/commands/_admin-auth.js +35 -0
  17. package/src/runtime/vendor/flywheel-cli-dist/commands/_admin-auth.js.map +1 -0
  18. package/src/runtime/vendor/flywheel-cli-dist/commands/_stream-file-out.d.ts +39 -0
  19. package/src/runtime/vendor/flywheel-cli-dist/commands/_stream-file-out.js +72 -0
  20. package/src/runtime/vendor/flywheel-cli-dist/commands/_stream-file-out.js.map +1 -0
  21. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-compute-policy-get.d.ts +2 -0
  22. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-compute-policy-get.js +10 -0
  23. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-compute-policy-get.js.map +1 -0
  24. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-compute-policy-set.d.ts +2 -0
  25. package/src/runtime/vendor/flywheel-cli-dist/commands/{compute-policy-set.js → admin-compute-policy-set.js} +8 -3
  26. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-compute-policy-set.js.map +1 -0
  27. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-export-stream.d.ts +2 -0
  28. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-export-stream.js +114 -0
  29. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-export-stream.js.map +1 -0
  30. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-export.d.ts +2 -0
  31. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-export.js +11 -0
  32. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-export.js.map +1 -0
  33. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-list.d.ts +4 -0
  34. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-list.js +38 -0
  35. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-list.js.map +1 -0
  36. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-nodes-audit-list.d.ts +2 -0
  37. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-nodes-audit-list.js +14 -0
  38. package/src/runtime/vendor/flywheel-cli-dist/commands/admin-nodes-audit-list.js.map +1 -0
  39. package/src/runtime/vendor/flywheel-cli-dist/commands/enums.d.ts +6 -0
  40. package/src/runtime/vendor/flywheel-cli-dist/commands/enums.js +3 -0
  41. package/src/runtime/vendor/flywheel-cli-dist/commands/enums.js.map +1 -1
  42. package/src/runtime/vendor/flywheel-cli-dist/commands/env.d.ts +0 -4
  43. package/src/runtime/vendor/flywheel-cli-dist/commands/env.js +7 -12
  44. package/src/runtime/vendor/flywheel-cli-dist/commands/env.js.map +1 -1
  45. package/src/runtime/vendor/flywheel-cli-dist/commands/export-history.js +40 -30
  46. package/src/runtime/vendor/flywheel-cli-dist/commands/export-history.js.map +1 -1
  47. package/src/runtime/vendor/flywheel-cli-dist/commands/help.d.ts +2 -0
  48. package/src/runtime/vendor/flywheel-cli-dist/commands/help.js +60 -10
  49. package/src/runtime/vendor/flywheel-cli-dist/commands/help.js.map +1 -1
  50. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-create.d.ts +2 -0
  51. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-create.js +25 -0
  52. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-create.js.map +1 -0
  53. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-delete.d.ts +1 -0
  54. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-delete.js +6 -0
  55. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-delete.js.map +1 -0
  56. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-list.d.ts +2 -0
  57. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-list.js +10 -0
  58. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-list.js.map +1 -0
  59. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-runs-list.d.ts +2 -0
  60. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-runs-list.js +15 -0
  61. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-runs-list.js.map +1 -0
  62. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-create.d.ts +2 -0
  63. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-create.js +8 -0
  64. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-create.js.map +1 -0
  65. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-delete.d.ts +1 -0
  66. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-delete.js +6 -0
  67. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-delete.js.map +1 -0
  68. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-list.d.ts +1 -0
  69. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-list.js +6 -0
  70. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-list.js.map +1 -0
  71. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-update.d.ts +2 -0
  72. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-update.js +8 -0
  73. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-update.js.map +1 -0
  74. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-set-enabled.d.ts +2 -0
  75. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-set-enabled.js +8 -0
  76. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-set-enabled.js.map +1 -0
  77. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-update.d.ts +2 -0
  78. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-update.js +29 -0
  79. package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-update.js.map +1 -0
  80. package/src/runtime/vendor/flywheel-cli-dist/commands/index.js +32 -6
  81. package/src/runtime/vendor/flywheel-cli-dist/commands/index.js.map +1 -1
  82. package/src/runtime/vendor/flywheel-cli-dist/commands/profile-set.js +4 -2
  83. package/src/runtime/vendor/flywheel-cli-dist/commands/profile-set.js.map +1 -1
  84. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/admin.d.ts +8 -0
  85. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/admin.js +118 -0
  86. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/admin.js.map +1 -0
  87. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/auth.js +2 -2
  88. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/auth.js.map +1 -1
  89. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/env.js +7 -5
  90. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/env.js.map +1 -1
  91. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/hooks.d.ts +2 -0
  92. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/hooks.js +244 -0
  93. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/hooks.js.map +1 -0
  94. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/lifecycle.js +1 -16
  95. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/lifecycle.js.map +1 -1
  96. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/profile.js +2 -2
  97. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/profile.js.map +1 -1
  98. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/resources.js +1 -31
  99. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/resources.js.map +1 -1
  100. package/src/runtime/vendor/flywheel-cli-dist/commands/registry.d.ts +2 -1
  101. package/src/runtime/vendor/flywheel-cli-dist/commands/registry.js +4 -0
  102. package/src/runtime/vendor/flywheel-cli-dist/commands/registry.js.map +1 -1
  103. package/src/runtime/vendor/flywheel-cli-dist/main.js +1 -1
  104. package/src/runtime/vendor/flywheel-cli-dist/main.js.map +1 -1
  105. package/src/runtime/vendor/manifest.json +61 -10
  106. package/src/setup/io/patchers/json.mjs +2 -0
  107. package/src/setup/io/patchers/toml.mjs +7 -1
  108. package/src/setup/io/patchers/yaml.mjs +2 -0
  109. package/src/setup/modes/cli.mjs +41 -16
  110. package/src/setup/modes/mcp-command.mjs +98 -19
  111. package/src/setup/modes/mcp.mjs +26 -1
  112. package/src/setup/shared/prior-mode-detect.mjs +133 -14
  113. package/src/skill-installer.mjs +179 -1
  114. package/src/unified-cli.mjs +38 -0
  115. package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-get.d.ts +0 -1
  116. package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-get.js +0 -6
  117. package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-get.js.map +0 -1
  118. package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-set.d.ts +0 -2
  119. package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-set.js.map +0 -1
  120. package/src/runtime/vendor/flywheel-cli-dist/commands/nodes-audit-list.d.ts +0 -2
  121. package/src/runtime/vendor/flywheel-cli-dist/commands/nodes-audit-list.js +0 -7
  122. package/src/runtime/vendor/flywheel-cli-dist/commands/nodes-audit-list.js.map +0 -1
@@ -9,9 +9,11 @@ import {
9
9
  SERVER_NAME,
10
10
  detectHosts,
11
11
  getHost,
12
+ isSkillOnlyHost,
12
13
  normalizeHosts,
13
14
  } from "../../agents.mjs";
14
15
  import {
16
+ readEntryServerUrl,
15
17
  readNamedConfigEntry,
16
18
  readJsonConfig,
17
19
  readTomlServerEntry,
@@ -26,6 +28,7 @@ import {
26
28
  formatSupportedInstallSkillHosts,
27
29
  inspectInstalledSkillArtifactsForHosts,
28
30
  installBundledSkillForHosts,
31
+ isDirectCopySkillHost,
29
32
  listBundledSkills,
30
33
  partitionInstallSkillHosts,
31
34
  resolveInstalledSkillDir,
@@ -94,9 +97,12 @@ function normalizeServerName(value) {
94
97
  function selectedHostsFromOptions(options) {
95
98
  const hosts = [];
96
99
  if (options.claude) hosts.push("claude");
100
+ if (options.cursor) hosts.push("cursor");
101
+ if (options.universal) hosts.push("universal");
102
+ if (options.antigravity) hosts.push("antigravity");
97
103
  if (options.opencode) hosts.push("opencode");
98
104
  if (options.codex) hosts.push("codex");
99
- if (options.cursor) hosts.push("cursor");
105
+ if (options.gemini) hosts.push("gemini");
100
106
  if (options.piMono) hosts.push("pi-mono");
101
107
  if (options.hermesAgent) hosts.push("hermes-agent");
102
108
  if (options.openclaw) hosts.push("openclaw");
@@ -112,6 +118,9 @@ function mcpCandidatesForScope(host, scope) {
112
118
 
113
119
  async function readExistingServerState(hostName, scope, serverName) {
114
120
  const host = getHost(hostName);
121
+ if (isSkillOnlyHost(host)) {
122
+ return { exists: false, url: null, mcpPath: null };
123
+ }
115
124
  const mcpPath = await resolveMcpPath(mcpCandidatesForScope(host, scope));
116
125
 
117
126
  if (host.mcp.configType === "toml") {
@@ -126,7 +135,7 @@ async function readExistingServerState(hostName, scope, serverName) {
126
135
  const entry = readNamedConfigEntry(existing, host.mcp.configKey, serverName);
127
136
  return {
128
137
  exists: entry !== null,
129
- url: typeof entry?.url === "string" ? entry.url : null,
138
+ url: readEntryServerUrl(entry),
130
139
  mcpPath,
131
140
  };
132
141
  }
@@ -151,6 +160,21 @@ async function inspectInstallSkillHosts({
151
160
  const driftHosts = [];
152
161
 
153
162
  for (const hostName of hostNames) {
163
+ const host = getHost(hostName);
164
+ // Skill-only hosts (universal, antigravity) never write an MCP config, so
165
+ // they are not candidates for "needs setup" / "drift" / "matching"
166
+ // classification. Skipping them here is what keeps the auth-resolution
167
+ // gate downstream accurate: without this branch they would land in
168
+ // `needsSetupHosts` and force an API-key mint on `setup --mode mcp
169
+ // --universal --skip-skill`, which is a pure no-op path that must stay
170
+ // hermetic (no browser, no blocking prompt).
171
+ if (isSkillOnlyHost(host)) {
172
+ hostStates.set(hostName, {
173
+ classification: "skill-only",
174
+ mcpPath: null,
175
+ });
176
+ continue;
177
+ }
154
178
  // eslint-disable-next-line no-await-in-loop
155
179
  const existingState = await readExistingServerState(
156
180
  hostName,
@@ -315,7 +339,53 @@ async function resolveApiKey(options, baseUrl) {
315
339
  }
316
340
  }
317
341
 
318
- function buildSetupResultRows({ selectedHosts, hostStates, setupResult }) {
342
+ /**
343
+ * Classify an agent result for the human-readable MCP setup summary.
344
+ *
345
+ * The terminal outcomes are:
346
+ * - "failed" → explicit host-level failure (success === false).
347
+ * - "skipped" → the host was intentionally bypassed (skill-only host in
348
+ * MCP mode, or any agent result that carries success === null
349
+ * or status === "skipped"). Agents that fall into this
350
+ * bucket must NOT be rendered as failed, because the setup
351
+ * itself exited 0 and the user's expectation is that a
352
+ * green exit implies a green summary row.
353
+ * - "already configured" → the desired MCP entry was already on disk.
354
+ * - "configured (OAuth)" → oauth mode wrote a config without any
355
+ * Authorization header; labelling this as
356
+ * "API Key" would be inaccurate and would
357
+ * confuse users auditing the auth model.
358
+ * - "configured with API Key" → default MCP-mode success path, which writes
359
+ * a Bearer token into the host config.
360
+ */
361
+ function classifyAgentOutcome(agentResult, { oauthMode = false } = {}) {
362
+ if (agentResult.success === false) {
363
+ return "failed";
364
+ }
365
+ if (
366
+ agentResult.success === null ||
367
+ agentResult.status === "skipped" ||
368
+ (Array.isArray(agentResult.skippedSteps) &&
369
+ (agentResult.skippedSteps.includes("skill-only-host") ||
370
+ agentResult.skippedSteps.includes("unsupported-host")))
371
+ ) {
372
+ return "skipped";
373
+ }
374
+ const alreadyConfigured =
375
+ Array.isArray(agentResult.skippedSteps) &&
376
+ agentResult.skippedSteps.includes("already-configured");
377
+ if (alreadyConfigured) {
378
+ return "already configured";
379
+ }
380
+ return oauthMode ? "configured (OAuth)" : "configured with API Key";
381
+ }
382
+
383
+ function buildSetupResultRows({
384
+ selectedHosts,
385
+ hostStates,
386
+ setupResult,
387
+ oauthMode = false,
388
+ }) {
319
389
  const byAgent = new Map(
320
390
  setupResult.agentResults.map((result) => [result.agent, result]),
321
391
  );
@@ -331,23 +401,14 @@ function buildSetupResultRows({ selectedHosts, hostStates, setupResult }) {
331
401
  };
332
402
  }
333
403
 
334
- if (agentResult.success !== true) {
335
- return {
336
- host: host.displayName,
337
- status: "failed",
338
- filePath: hostStates.get(hostName)?.mcpPath || "(unknown)",
339
- };
340
- }
341
-
342
- const alreadyConfigured =
343
- Array.isArray(agentResult.skippedSteps) &&
344
- agentResult.skippedSteps.includes("already-configured");
345
-
404
+ const status = classifyAgentOutcome(agentResult, { oauthMode });
346
405
  return {
347
406
  host: host.displayName,
348
- status: alreadyConfigured ? "already configured" : "configured with API Key",
407
+ status,
349
408
  filePath:
350
- agentResult.touchedFiles[0] || hostStates.get(hostName)?.mcpPath || "(unknown)",
409
+ agentResult.touchedFiles?.[0] ||
410
+ hostStates.get(hostName)?.mcpPath ||
411
+ "(unknown)",
351
412
  };
352
413
  });
353
414
  }
@@ -402,6 +463,12 @@ function resolveExperimentalSyncAgentsForHosts(supportedHosts) {
402
463
  const seenAgentNames = new Set();
403
464
 
404
465
  for (const hostName of supportedHosts) {
466
+ // Direct-copy hosts (gemini, universal, antigravity) bypass the `skills`
467
+ // CLI entirely, so they have no agent name to sync with. Skip them from
468
+ // the experimental_sync guidance block.
469
+ if (isDirectCopySkillHost(hostName)) {
470
+ continue;
471
+ }
405
472
  const agentName = resolveSkillsAgentName(hostName);
406
473
  if (seenAgentNames.has(agentName)) {
407
474
  continue;
@@ -522,6 +589,13 @@ export async function runMcpModeCommand(options, deps = {}) {
522
589
  throw new Error(NON_INTERACTIVE_SKILL_DECISION_ERROR);
523
590
  }
524
591
 
592
+ const oauthMode = options.oauth === true;
593
+ if (oauthMode && Object.hasOwn(options, "apiKey")) {
594
+ throw new SetupModeUsageError(
595
+ "--api-key and --oauth are mutually exclusive: choose one.",
596
+ );
597
+ }
598
+
525
599
  const scope = options.project ? "project" : "global";
526
600
  const baseUrl = normalizeBaseUrl(options.baseUrl || DEFAULT_BASE_URL);
527
601
  const serverUrl = `${baseUrl}/mcp-server`;
@@ -682,8 +756,7 @@ export async function runMcpModeCommand(options, deps = {}) {
682
756
  let apiKey = null;
683
757
  const explicitApiKeyProvided = Object.hasOwn(options, "apiKey");
684
758
  const shouldResolveApiKey =
685
- needsSetupHosts.length > 0 ||
686
- explicitApiKeyProvided;
759
+ !oauthMode && (needsSetupHosts.length > 0 || explicitApiKeyProvided);
687
760
  if (shouldResolveApiKey) {
688
761
  apiKey = await resolveApiKey(options, baseUrl);
689
762
  if (!apiKey) {
@@ -714,6 +787,11 @@ export async function runMcpModeCommand(options, deps = {}) {
714
787
  serverName,
715
788
  serverUrl,
716
789
  apiKey,
790
+ // Tell the writers to reconcile auth-mode mismatches (Authorization
791
+ // header added/removed) only when the user explicitly set `--oauth` or
792
+ // `--api-key`. Without that, a passive rerun must stay idempotent and
793
+ // preserve the existing bearer header on disk rather than stripping it.
794
+ enforceAuthMode: oauthMode || explicitApiKeyProvided,
717
795
  detectPriorMode: async () => ({
718
796
  priorMode: null,
719
797
  affectedHosts: [],
@@ -749,6 +827,7 @@ export async function runMcpModeCommand(options, deps = {}) {
749
827
  selectedHosts: hosts,
750
828
  hostStates,
751
829
  setupResult,
830
+ oauthMode,
752
831
  });
753
832
 
754
833
  if (skillDecisionMode !== "guided") {
@@ -1,6 +1,9 @@
1
1
  import path from "node:path";
2
2
 
3
- import { getHost as defaultGetHost } from "../../agents.mjs";
3
+ import {
4
+ getHost as defaultGetHost,
5
+ isSkillOnlyHost,
6
+ } from "../../agents.mjs";
4
7
  import { SUPPORTED_INSTALL_SKILL_HOSTS } from "../../skill-installer.mjs";
5
8
  import { resolveMcpPath as defaultResolveMcpPath } from "../../mcp-writer.mjs";
6
9
  import { SetupModeUsageError } from "../mode.mjs";
@@ -38,6 +41,12 @@ export async function runMcpSetupMode({
38
41
  serverName,
39
42
  serverUrl,
40
43
  apiKey,
44
+ // `enforceAuthMode` is the "explicit auth intent" signal forwarded by the
45
+ // CLI layer (true when `--oauth` is set or `--api-key` was passed). When
46
+ // true, writers reconcile a mismatched Authorization header on an existing
47
+ // entry; when false, they stay idempotent by name so a benign rerun does
48
+ // not wipe the previously-minted bearer key.
49
+ enforceAuthMode = false,
41
50
  supportedCliHosts = SUPPORTED_INSTALL_SKILL_HOSTS,
42
51
  detectPriorMode = detectPriorModeForSetup,
43
52
  reconcilePriorMode = null,
@@ -116,6 +125,19 @@ export async function runMcpSetupMode({
116
125
  let host = null;
117
126
  try {
118
127
  host = getHost(hostName);
128
+ if (isSkillOnlyHost(host)) {
129
+ agentResults.push({
130
+ agent: hostName,
131
+ success: null,
132
+ status: "skipped",
133
+ touchedFiles: [],
134
+ skippedSteps: ["skill-only-host"],
135
+ warnings: [
136
+ `${host.displayName} is a skill-only host; MCP config write skipped.`,
137
+ ],
138
+ });
139
+ continue;
140
+ }
119
141
  const candidates = resolveCandidatesForScope(host, scope, cwd);
120
142
  const filePath = await resolveMcpPath(candidates);
121
143
  const entry = host.mcp.buildEntry({ serverUrl, apiKey });
@@ -127,6 +149,7 @@ export async function runMcpSetupMode({
127
149
  configKey: host.mcp.configKey,
128
150
  serverName,
129
151
  entry,
152
+ enforceAuthMode,
130
153
  });
131
154
  } else if (host.mcp.configType === "yaml") {
132
155
  patchResult = await patchYamlServer({
@@ -134,6 +157,7 @@ export async function runMcpSetupMode({
134
157
  configKey: host.mcp.configKey,
135
158
  serverName,
136
159
  entry,
160
+ enforceAuthMode,
137
161
  });
138
162
  } else {
139
163
  patchResult = await patchJsonServer({
@@ -141,6 +165,7 @@ export async function runMcpSetupMode({
141
165
  configKey: host.mcp.configKey,
142
166
  serverName,
143
167
  entry,
168
+ enforceAuthMode,
144
169
  });
145
170
  }
146
171
 
@@ -1,10 +1,15 @@
1
1
  import path from "node:path";
2
2
  import { access, readFile } from "node:fs/promises";
3
3
 
4
- import { getHost as defaultGetHost } from "../../agents.mjs";
4
+ import {
5
+ getHost as defaultGetHost,
6
+ isSkillOnlyHost,
7
+ SKILL_ONLY_HOST_NAMES,
8
+ } from "../../agents.mjs";
5
9
  import {
6
10
  parseTomlSectionName,
7
11
  parseTomlStringValue,
12
+ readEntryServerUrl,
8
13
  readJsonConfig as defaultReadJsonConfig,
9
14
  readNamedConfigEntry as defaultReadNamedConfigEntry,
10
15
  readTomlServerEntry as defaultReadTomlServerEntry,
@@ -15,6 +20,7 @@ import {
15
20
  listBundledSkills as defaultListBundledSkills,
16
21
  resolveInstalledSkillDir as defaultResolveInstalledSkillDir,
17
22
  resolvePackageRoot as defaultResolvePackageRoot,
23
+ resolveFlywheelInstalledMarkerPath,
18
24
  SUPPORTED_INSTALL_SKILL_HOSTS,
19
25
  } from "../../skill-installer.mjs";
20
26
 
@@ -70,7 +76,8 @@ async function hasMatchingMcpEntry({
70
76
  ? await readYamlConfig(filePath)
71
77
  : await readJsonConfig(filePath);
72
78
  const entry = readNamedConfigEntry(config, host.mcp.configKey, serverName);
73
- return typeof entry?.url === "string" && entry.url === serverUrl;
79
+ const entryUrl = readEntryServerUrl(entry);
80
+ return entryUrl !== null && entryUrl === serverUrl;
74
81
  } catch {
75
82
  return false;
76
83
  }
@@ -87,8 +94,8 @@ function originOf(url) {
87
94
  // A URL is treated as a Flywheel MCP endpoint only when both its pathname is
88
95
  // `/mcp-server` AND its origin is Flywheel-identifying. "Flywheel-identifying"
89
96
  // means either (a) the hostname contains the substring "flywheel" (covers
90
- // `flywheel.paradigma.inc`, `flywheel-staging.*`, self-hosted
91
- // `flywheel.<tenant>.*`, etc.) or (b) the URL's full origin (scheme + host +
97
+ // the canonical `flywheel.paradigma.inc`, subdomain variants, and self-hosted
98
+ // `flywheel.<tenant>.*`) or (b) the URL's full origin (scheme + host +
92
99
  // port) matches the target base URL's origin, so dev installs against
93
100
  // `http://localhost:8000` and self-hosted tenants still round-trip. Comparing
94
101
  // full origin rather than hostname alone is load-bearing on multi-tenant
@@ -215,13 +222,8 @@ export async function findFlywheelMcpEntryNames({
215
222
  }
216
223
  const matches = [];
217
224
  for (const [entryName, entry] of Object.entries(section)) {
218
- if (
219
- entry &&
220
- typeof entry === "object" &&
221
- !Array.isArray(entry) &&
222
- typeof entry.url === "string" &&
223
- isFlywheelMcpUrl(entry.url, targetOrigin)
224
- ) {
225
+ const entryUrl = readEntryServerUrl(entry);
226
+ if (entryUrl !== null && isFlywheelMcpUrl(entryUrl, targetOrigin)) {
225
227
  matches.push(entryName);
226
228
  }
227
229
  }
@@ -231,6 +233,90 @@ export async function findFlywheelMcpEntryNames({
231
233
  }
232
234
  }
233
235
 
236
+ /**
237
+ * Determine whether a truly skill-only host's ownership marker is present at
238
+ * the skills-root that `hostName` would write into. The two destination
239
+ * roots can coincide on shared paths — e.g. `--universal` writes into
240
+ * `.agents/skills/` which is also where `--codex` / `--opencode` / `--cursor`
241
+ * land. When a skill-only host marker is present there, any skill artifacts
242
+ * at that root are attributable to the skill-only host, not to `hostName`.
243
+ *
244
+ * This MUST iterate `SKILL_ONLY_HOST_NAMES`, not the wider direct-copy set:
245
+ * Gemini CLI also uses the direct-copy skill path but is MCP-capable, so
246
+ * attributing its own `.gemini/skills/` artifacts to itself via this helper
247
+ * would silently suppress the legitimate cross-mode conflict when the user
248
+ * transitions `--mode cli --gemini` → `--mode mcp --gemini`.
249
+ */
250
+ async function skillsRootClaimedBySkillOnlyHost({
251
+ hostName,
252
+ scope,
253
+ cwd,
254
+ skillNames,
255
+ resolveInstalledSkillDir,
256
+ }) {
257
+ if (!Array.isArray(skillNames) || skillNames.length === 0) {
258
+ return false;
259
+ }
260
+ const candidateSkillPath = resolveInstalledSkillDir(
261
+ cwd,
262
+ hostName,
263
+ scope,
264
+ skillNames[0],
265
+ );
266
+ const candidateSkillsRoot = path.dirname(candidateSkillPath);
267
+
268
+ // Case where the target host has its own install marker: the artifacts
269
+ // are co-owned — the skill-only marker alone no longer fully explains the
270
+ // skill dir contents, so the suppression must NOT fire. This is the
271
+ // difference between:
272
+ // `setup --mode cli --universal` → only universal marker
273
+ // `setup --mode cli --universal --codex` → universal + codex markers
274
+ // In the first case, codex's MCP setup is a fresh install and no conflict
275
+ // should be raised. In the second case, codex had a real CLI install of
276
+ // its own and the cross-mode conflict must fire.
277
+ try {
278
+ const selfMarkerPath = resolveFlywheelInstalledMarkerPath(cwd, hostName, scope);
279
+ if (await pathExists(selfMarkerPath)) {
280
+ return false;
281
+ }
282
+ } catch {
283
+ // hostName isn't registered with the flywheel skill installer; nothing
284
+ // to subtract — fall through to the skill-only scan below.
285
+ }
286
+
287
+ // Note: there is no reliable post-hoc signal for a CLI install of an
288
+ // MCP-capable host that pre-dates the per-host ownership marker scheme.
289
+ // `skills-lock.json` would seem like one, but its entries are keyed by
290
+ // skill name (not by agent) and are left behind by BOTH CLI-mode installs
291
+ // and MCP-mode installs with `--install-skill`, including stale entries
292
+ // that survive a host uninstall. Using the lock as a legacy fallback
293
+ // therefore false-positives on the common flow "MCP install → uninstall →
294
+ // universal install → MCP re-install", so we do not treat it as a cross-
295
+ // mode conflict signal. The legacy marker-less upgrade cohort (users on a
296
+ // release before ownership markers existed) can pass `--force` or re-run
297
+ // their CLI setup to acquire the marker; from there everything converges
298
+ // on the accurate per-host signal.
299
+ for (const skillOnlyHostName of SKILL_ONLY_HOST_NAMES) {
300
+ // Skill-only hosts whose destination path cannot overlap this host's
301
+ // path cannot explain any artifact found here. Only markers at the same
302
+ // skills-root are load-bearing.
303
+ let markerPath;
304
+ try {
305
+ markerPath = resolveFlywheelInstalledMarkerPath(cwd, skillOnlyHostName, scope);
306
+ } catch {
307
+ continue;
308
+ }
309
+ if (path.dirname(markerPath) !== candidateSkillsRoot) {
310
+ continue;
311
+ }
312
+ // eslint-disable-next-line no-await-in-loop
313
+ if (await pathExists(markerPath)) {
314
+ return true;
315
+ }
316
+ }
317
+ return false;
318
+ }
319
+
234
320
  async function hostHasBundledSkillArtifacts({
235
321
  hostName,
236
322
  scope,
@@ -238,15 +324,32 @@ async function hostHasBundledSkillArtifacts({
238
324
  skillNames,
239
325
  resolveInstalledSkillDir,
240
326
  }) {
327
+ let foundAny = false;
241
328
  for (const skillName of skillNames) {
242
329
  // eslint-disable-next-line no-await-in-loop
243
330
  const skillPath = resolveInstalledSkillDir(cwd, hostName, scope, skillName);
244
331
  // eslint-disable-next-line no-await-in-loop
245
332
  if (await pathExists(skillPath)) {
246
- return true;
333
+ foundAny = true;
334
+ break;
247
335
  }
248
336
  }
249
- return false;
337
+ if (!foundAny) {
338
+ return false;
339
+ }
340
+ // Found skill artifacts at this host's path. Attribute them to a
341
+ // skill-only host that explicitly claims the same skills-root (via its
342
+ // installation marker) rather than to this host — otherwise an MCP host
343
+ // sharing `.agents/skills/` with `--universal` would be misreported as
344
+ // having a prior CLI-mode install of its own.
345
+ const claimed = await skillsRootClaimedBySkillOnlyHost({
346
+ hostName,
347
+ scope,
348
+ cwd,
349
+ skillNames,
350
+ resolveInstalledSkillDir,
351
+ });
352
+ return !claimed;
250
353
  }
251
354
 
252
355
  export function formatPriorModeConflictError({
@@ -363,7 +466,23 @@ export async function detectPriorModeForSetup({
363
466
  };
364
467
  }
365
468
 
366
- const supportedHostSet = new Set(unique(supportedCliHosts));
469
+ // Skill-only hosts (universal, antigravity) never write an MCP config, so
470
+ // every one of their installs — regardless of whether it was invoked with
471
+ // `--mode cli` or `--mode mcp --install-skill` — leaves the same on-disk
472
+ // shape: bundled skill dirs and nothing else. Treating a lingering skill
473
+ // dir under such a host as "a prior CLI install" would raise a bogus
474
+ // cross-mode conflict on every rerun of the same command. Filter them
475
+ // out of the CLI candidate universe up front so a subsequent `--mode mcp`
476
+ // pass stays idempotent.
477
+ const supportedHostSet = new Set(
478
+ unique(supportedCliHosts).filter((hostName) => {
479
+ try {
480
+ return !isSkillOnlyHost(getHost(hostName));
481
+ } catch {
482
+ return true;
483
+ }
484
+ }),
485
+ );
367
486
  const mcpTouchedHosts = new Set([
368
487
  ...exactMatchHosts,
369
488
  ...flywheelEntryNamesByHost.keys(),