@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.
- package/README.md +121 -10
- package/package.json +1 -1
- package/skills/flywheel/references/flywheel-cli-tool-map.md +8 -0
- package/skills/flywheel/references/flywheel-mcp-tool-map.md +8 -0
- package/src/agents.mjs +114 -48
- package/src/cli.mjs +267 -35
- package/src/mcp-writer.mjs +190 -17
- package/src/runtime/required-runtime-commands.json +17 -4
- package/src/runtime/vendor/flywheel-cli-dist/auth/login.d.ts +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/auth/login.js +3 -6
- package/src/runtime/vendor/flywheel-cli-dist/auth/login.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/auth/profiles.d.ts +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/auth/profiles.js +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/auth/profiles.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/_admin-auth.d.ts +25 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/_admin-auth.js +35 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/_admin-auth.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/_stream-file-out.d.ts +39 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/_stream-file-out.js +72 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/_stream-file-out.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-compute-policy-get.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-compute-policy-get.js +10 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-compute-policy-get.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-compute-policy-set.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/{compute-policy-set.js → admin-compute-policy-set.js} +8 -3
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-compute-policy-set.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-export-stream.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-export-stream.js +114 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-export-stream.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-export.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-export.js +11 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-export.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-list.d.ts +4 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-list.js +38 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-graphs-list.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-nodes-audit-list.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-nodes-audit-list.js +14 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/admin-nodes-audit-list.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/enums.d.ts +6 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/enums.js +3 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/enums.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/env.d.ts +0 -4
- package/src/runtime/vendor/flywheel-cli-dist/commands/env.js +7 -12
- package/src/runtime/vendor/flywheel-cli-dist/commands/env.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/export-history.js +40 -30
- package/src/runtime/vendor/flywheel-cli-dist/commands/export-history.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/help.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/help.js +60 -10
- package/src/runtime/vendor/flywheel-cli-dist/commands/help.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-create.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-create.js +25 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-create.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-delete.d.ts +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-delete.js +6 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-delete.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-list.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-list.js +10 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-list.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-runs-list.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-runs-list.js +15 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-runs-list.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-create.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-create.js +8 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-create.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-delete.d.ts +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-delete.js +6 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-delete.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-list.d.ts +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-list.js +6 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-list.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-update.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-update.js +8 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-secrets-update.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-set-enabled.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-set-enabled.js +8 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-set-enabled.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-update.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-update.js +29 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/hooks-update.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/index.js +32 -6
- package/src/runtime/vendor/flywheel-cli-dist/commands/index.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/profile-set.js +4 -2
- package/src/runtime/vendor/flywheel-cli-dist/commands/profile-set.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/admin.d.ts +8 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/admin.js +118 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/admin.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/auth.js +2 -2
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/auth.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/env.js +7 -5
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/env.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/hooks.d.ts +2 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/hooks.js +244 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/hooks.js.map +1 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/lifecycle.js +1 -16
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/lifecycle.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/profile.js +2 -2
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/profile.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/resources.js +1 -31
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/resources.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry.d.ts +2 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry.js +4 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/main.js +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/main.js.map +1 -1
- package/src/runtime/vendor/manifest.json +61 -10
- package/src/setup/io/patchers/json.mjs +2 -0
- package/src/setup/io/patchers/toml.mjs +7 -1
- package/src/setup/io/patchers/yaml.mjs +2 -0
- package/src/setup/modes/cli.mjs +41 -16
- package/src/setup/modes/mcp-command.mjs +98 -19
- package/src/setup/modes/mcp.mjs +26 -1
- package/src/setup/shared/prior-mode-detect.mjs +133 -14
- package/src/skill-installer.mjs +179 -1
- package/src/unified-cli.mjs +38 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-get.d.ts +0 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-get.js +0 -6
- package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-get.js.map +0 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-set.d.ts +0 -2
- package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-set.js.map +0 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/nodes-audit-list.d.ts +0 -2
- package/src/runtime/vendor/flywheel-cli-dist/commands/nodes-audit-list.js +0 -7
- 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.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
407
|
+
status,
|
|
349
408
|
filePath:
|
|
350
|
-
agentResult.touchedFiles[0] ||
|
|
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") {
|
package/src/setup/modes/mcp.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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`,
|
|
91
|
-
// `flywheel.<tenant
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
333
|
+
foundAny = true;
|
|
334
|
+
break;
|
|
247
335
|
}
|
|
248
336
|
}
|
|
249
|
-
|
|
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
|
-
|
|
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(),
|