@paradigma-inc/flywheel 0.1.53 → 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 (121) 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/runtime/vendor/flywheel-cli-dist/commands/compute-policy-get.d.ts +0 -1
  115. package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-get.js +0 -6
  116. package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-get.js.map +0 -1
  117. package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-set.d.ts +0 -2
  118. package/src/runtime/vendor/flywheel-cli-dist/commands/compute-policy-set.js.map +0 -1
  119. package/src/runtime/vendor/flywheel-cli-dist/commands/nodes-audit-list.d.ts +0 -2
  120. package/src/runtime/vendor/flywheel-cli-dist/commands/nodes-audit-list.js +0 -7
  121. package/src/runtime/vendor/flywheel-cli-dist/commands/nodes-audit-list.js.map +0 -1
package/src/cli.mjs CHANGED
@@ -2,18 +2,21 @@ import { Command } from "commander";
2
2
  import { checkbox, select } from "@inquirer/prompts";
3
3
  import ora from "ora";
4
4
  import pc from "picocolors";
5
- import { readFile } from "node:fs/promises";
5
+ import { access, readFile, rm } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
 
8
8
  import {
9
9
  ALL_HOST_NAMES,
10
10
  SERVER_NAME,
11
11
  SETUP_HOST_NAMES,
12
+ SKILL_ONLY_HOST_NAMES,
12
13
  detectHosts,
13
14
  getHost,
15
+ isSkillOnlyHost,
14
16
  normalizeHosts,
15
17
  } from "./agents.mjs";
16
18
  import {
19
+ readEntryServerUrl,
17
20
  readNamedConfigEntry,
18
21
  readJsonConfig,
19
22
  readTomlServerEntry,
@@ -28,6 +31,8 @@ import {
28
31
  listBundledSkills,
29
32
  partitionInstallSkillHosts,
30
33
  removeInstalledSkillsForHosts,
34
+ resolveInstalledSkillDir,
35
+ resolveFlywheelInstalledMarkerPath,
31
36
  SUPPORTED_INSTALL_SKILL_HOSTS,
32
37
  resolvePackageRoot,
33
38
  } from "./skill-installer.mjs";
@@ -42,6 +47,15 @@ import {
42
47
  import { findFlywheelMcpEntryNames } from "./setup/shared/prior-mode-detect.mjs";
43
48
  import { renderSetupSummary } from "./setup/shared/summary.mjs";
44
49
 
50
+ async function pathExists(targetPath) {
51
+ try {
52
+ await access(targetPath);
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
45
59
  export { buildGuidedSkillInstallPrompt };
46
60
 
47
61
  const INSTALL_SKILL_BASE_URL = "https://flywheel.paradigma.inc";
@@ -95,9 +109,12 @@ function normalizeAuthMode(value) {
95
109
  function selectedHostsFromOptions(options) {
96
110
  const hosts = [];
97
111
  if (options.claude) hosts.push("claude");
112
+ if (options.cursor) hosts.push("cursor");
113
+ if (options.universal) hosts.push("universal");
114
+ if (options.antigravity) hosts.push("antigravity");
98
115
  if (options.opencode) hosts.push("opencode");
99
116
  if (options.codex) hosts.push("codex");
100
- if (options.cursor) hosts.push("cursor");
117
+ if (options.gemini) hosts.push("gemini");
101
118
  if (options.piMono) hosts.push("pi-mono");
102
119
  if (options.hermesAgent) hosts.push("hermes-agent");
103
120
  if (options.openclaw) hosts.push("openclaw");
@@ -109,6 +126,9 @@ const UNINSTALL_OPTION_KEY_BY_HOST = Object.freeze({
109
126
  codex: "codex",
110
127
  opencode: "opencode",
111
128
  cursor: "cursor",
129
+ gemini: "gemini",
130
+ universal: "universal",
131
+ antigravity: "antigravity",
112
132
  "pi-mono": "piMono",
113
133
  "hermes-agent": "hermesAgent",
114
134
  openclaw: "openclaw",
@@ -167,6 +187,9 @@ async function isAlreadyConfigured(hostName, scope, serverName) {
167
187
 
168
188
  async function readExistingServerState(hostName, scope, serverName) {
169
189
  const host = getHost(hostName);
190
+ if (isSkillOnlyHost(host)) {
191
+ return { exists: false, url: null, mcpPath: null };
192
+ }
170
193
  const mcpPath = await resolveMcpPath(mcpCandidatesForScope(host, scope));
171
194
 
172
195
  if (host.mcp.configType === "toml") {
@@ -181,7 +204,7 @@ async function readExistingServerState(hostName, scope, serverName) {
181
204
  const entry = readNamedConfigEntry(existing, host.mcp.configKey, serverName);
182
205
  return {
183
206
  exists: entry !== null,
184
- url: typeof entry?.url === "string" ? entry.url : null,
207
+ url: readEntryServerUrl(entry),
185
208
  mcpPath,
186
209
  };
187
210
  }
@@ -289,7 +312,19 @@ async function promptSetupMode() {
289
312
  return await select(buildSetupModePromptConfig());
290
313
  }
291
314
 
292
- function validateCliModeSkillFlags(options) {
315
+ /**
316
+ * Validate the flag combination for --mode cli. Rejections raise
317
+ * `SetupModeUsageError` so the top-level handler reports them as usage
318
+ * errors (exit code 2) and not as operational failures.
319
+ *
320
+ * `--oauth` controls the MCP config written in --mode mcp (it omits the
321
+ * Authorization header so the IDE can discover MCP 2.0 OAuth instead).
322
+ * --mode cli writes no MCP config at all, so there is nothing for --oauth
323
+ * to modify. Silently accepting it would look successful but still fall
324
+ * back to API-key auth in the runtime config — a misconfiguration. Reject
325
+ * it up front with guidance toward the correct invocation.
326
+ */
327
+ function validateCliModeFlags(options) {
293
328
  if (options.installSkill && options.skipSkill) {
294
329
  throw new SetupModeUsageError(
295
330
  "--install-skill and --skip-skill cannot be used together.",
@@ -302,10 +337,15 @@ function validateCliModeSkillFlags(options) {
302
337
  "--skip-skill is not supported with --mode cli; use --mode mcp --skip-skill for MCP-only setup.",
303
338
  );
304
339
  }
340
+ if (options.oauth === true) {
341
+ throw new SetupModeUsageError(
342
+ "--oauth is not supported with --mode cli. --oauth only affects the MCP config written by --mode mcp; re-run with --mode mcp --oauth, or drop --oauth for a CLI-mode setup.",
343
+ );
344
+ }
305
345
  }
306
346
 
307
347
  async function runCliModeCommand(options) {
308
- validateCliModeSkillFlags(options);
348
+ validateCliModeFlags(options);
309
349
  const scope = options.project ? "project" : "global";
310
350
  const baseUrl = normalizeBaseUrl(options.baseUrl || DEFAULT_BASE_URL);
311
351
  const serverUrl = `${baseUrl}/mcp-server`;
@@ -484,6 +524,9 @@ function isLikelyFlywheelMcpUrl(url) {
484
524
 
485
525
  async function listHostMcpEntries(hostName, scope) {
486
526
  const host = getHost(hostName);
527
+ if (isSkillOnlyHost(host)) {
528
+ return [];
529
+ }
487
530
  const filePath = await resolveMcpPath(mcpCandidatesForScope(host, scope));
488
531
 
489
532
  if (host.mcp.configType === "toml") {
@@ -517,7 +560,7 @@ async function listHostMcpEntries(hostName, scope) {
517
560
  .filter(([, entry]) => entry && typeof entry === "object" && !Array.isArray(entry))
518
561
  .map(([serverName, entry]) => ({
519
562
  serverName,
520
- url: typeof entry.url === "string" ? entry.url : null,
563
+ url: readEntryServerUrl(entry),
521
564
  }));
522
565
  }
523
566
 
@@ -533,14 +576,68 @@ async function resolveHostsWithRemainingFlywheelMcpEntries({ scope }) {
533
576
  return remainingHosts;
534
577
  }
535
578
 
579
+ /**
580
+ * Determine which non-uninstalled hosts still claim bundled skill artifacts
581
+ * via their install marker, and therefore must keep their (possibly shared)
582
+ * skill directories intact.
583
+ *
584
+ * The install marker (`.flywheel-installed-by-<host>`) is written by
585
+ * `installBundledSkillForHosts` for any host whose install path produces no
586
+ * MCP entry:
587
+ *
588
+ * - skill-only hosts (universal, antigravity): marker always written.
589
+ * - MCP-capable hosts (codex, opencode, cursor, claude, pi-mono, openclaw,
590
+ * gemini): marker written ONLY when the host was installed via
591
+ * `--mode cli`, because CLI-mode installs write no MCP entry.
592
+ *
593
+ * A marker at a skill-root is therefore the unambiguous "this host still
594
+ * owns these skills" signal for both categories. Narrowing the retain pass
595
+ * to skill-only hosts would let `uninstall --universal` silently wipe the
596
+ * shared `.agents/skills/` dir out from under a still-installed CLI-mode
597
+ * codex. Using dir existence alone would over-retain for hosts that never
598
+ * installed anything. The per-host marker resolves both.
599
+ *
600
+ * `excludedHostNames` is the list of hosts currently being uninstalled;
601
+ * their markers will be removed and their artifacts are expected to go, so
602
+ * they are excluded from retention here.
603
+ */
604
+ async function resolveSkillOnlyHostsRetainingArtifacts({
605
+ scope,
606
+ excludedHostNames = [],
607
+ }) {
608
+ const excluded = new Set(excludedHostNames);
609
+ const retained = [];
610
+ for (const hostName of SUPPORTED_INSTALL_SKILL_HOSTS) {
611
+ if (excluded.has(hostName)) continue;
612
+ let markerPath;
613
+ try {
614
+ markerPath = resolveFlywheelInstalledMarkerPath(
615
+ process.cwd(),
616
+ hostName,
617
+ scope,
618
+ );
619
+ } catch {
620
+ // Host isn't registered with the flywheel skill installer; no marker
621
+ // to consult, nothing to retain.
622
+ continue;
623
+ }
624
+ // eslint-disable-next-line no-await-in-loop
625
+ const markerExists = await pathExists(markerPath);
626
+ if (markerExists) {
627
+ retained.push(hostName);
628
+ }
629
+ }
630
+ return retained;
631
+ }
632
+
536
633
  async function findFlywheelServerNamesOnHost(hostName, scope) {
537
634
  // Mirror setup's prior-mode detector: walk every MCP entry on the host and
538
635
  // keep those whose URL is Flywheel-identifying (hostname contains "flywheel"
539
636
  // or path === /mcp-server with a matching target origin). Uninstall must
540
- // catch renamed/drifted entries (e.g. `[mcp_servers.flywheel-staging]`) that
541
- // setup already detects; otherwise a user whose host has a non-default entry
542
- // name is stuck between setup ("already installed") and uninstall ("not
543
- // configured").
637
+ // catch renamed or drifted entries (e.g. a non-default TOML section name
638
+ // under `[mcp_servers.*]`) that setup already detects; otherwise a user
639
+ // whose host has a non-default entry name is stuck between setup ("already
640
+ // installed") and uninstall ("not configured").
544
641
  return await findFlywheelMcpEntryNames({ hostName, scope }).catch(() => []);
545
642
  }
546
643
 
@@ -574,10 +671,92 @@ async function isConfiguredForUninstallScope(
574
671
  { explicitServerName },
575
672
  );
576
673
  if (targets.length > 0) return true;
674
+ // Skill-only hosts and CLI-mode MCP-capable hosts never write an MCP
675
+ // entry, so the sweep above finds nothing for them. Fall back to the
676
+ // install marker: if this host wrote its own ownership marker in the
677
+ // selected scope, it IS configured for the purposes of the uninstall
678
+ // prompt.
679
+ if (SUPPORTED_INSTALL_SKILL_HOSTS.includes(hostName)) {
680
+ try {
681
+ const markerPath = resolveFlywheelInstalledMarkerPath(
682
+ process.cwd(),
683
+ hostName,
684
+ singleScope,
685
+ );
686
+ // eslint-disable-next-line no-await-in-loop
687
+ if (await pathExists(markerPath)) return true;
688
+ } catch {
689
+ // Host isn't registered with the skill installer; nothing to check.
690
+ }
691
+ }
577
692
  }
578
693
  return false;
579
694
  }
580
695
 
696
+ /**
697
+ * Public wrapper exposing the uninstall-eligibility check used by the
698
+ * interactive prompt. Returns true when the host has either a Flywheel MCP
699
+ * entry OR a Flywheel install marker in the requested scope. Exported so
700
+ * tests can verify that skill-only hosts surface as configurable in the
701
+ * prompt even though they write no MCP entry.
702
+ */
703
+ export async function isHostConfiguredForUninstall({
704
+ hostName,
705
+ scope = "project",
706
+ serverName = SERVER_NAME,
707
+ } = {}) {
708
+ // Mirror `runUninstallCommand`'s derivation: only treat the caller's
709
+ // `serverName` as "explicit" when it differs from the package default.
710
+ // An explicit name short-circuits the Flywheel-identifying sweep in
711
+ // `resolveUninstallServerNamesForHost`, which the default-named case
712
+ // needs to run so drifted entries (and the marker fallback for
713
+ // skill-only / CLI-mode hosts) are reachable.
714
+ const explicitServerName =
715
+ typeof serverName === "string" &&
716
+ serverName.length > 0 &&
717
+ serverName !== SERVER_NAME
718
+ ? serverName
719
+ : null;
720
+ return isConfiguredForUninstallScope(hostName, scope, { explicitServerName });
721
+ }
722
+
723
+ /**
724
+ * Remove every Flywheel install marker for the requested hosts across the
725
+ * requested scopes. Exported and implemented as a standalone helper so it
726
+ * can run independently of bundled-skill discovery: a package with a
727
+ * missing or trimmed `skills/` directory must still be able to uninstall
728
+ * cleanly, otherwise stale markers leak into later prompt-eligibility,
729
+ * retention, and prior-mode-detection logic. `rm ..., { force: true }`
730
+ * makes the operation idempotent — hosts that never wrote a marker (e.g.
731
+ * MCP-capable hosts installed only via `--mode mcp`) are silent no-ops.
732
+ *
733
+ * Hosts not registered with the Flywheel skill installer are skipped
734
+ * rather than throwing, so callers can pass the full uninstall host list
735
+ * without pre-filtering.
736
+ */
737
+ export async function cleanupHostInstallMarkers({
738
+ cwd = process.cwd(),
739
+ scopes = ["project"],
740
+ hostNames = [],
741
+ } = {}) {
742
+ for (const singleScope of scopes) {
743
+ for (const hostName of hostNames) {
744
+ let markerPath;
745
+ try {
746
+ markerPath = resolveFlywheelInstalledMarkerPath(
747
+ cwd,
748
+ hostName,
749
+ singleScope,
750
+ );
751
+ } catch {
752
+ continue;
753
+ }
754
+ // eslint-disable-next-line no-await-in-loop
755
+ await rm(markerPath, { force: true });
756
+ }
757
+ }
758
+ }
759
+
581
760
  async function promptUninstallScope(defaultScope) {
582
761
  try {
583
762
  return await select({
@@ -674,6 +853,14 @@ async function resolveUninstallTargets(options, { explicitServerName }) {
674
853
 
675
854
  async function removeHostConfig(hostName, scope, serverName) {
676
855
  const host = getHost(hostName);
856
+ if (isSkillOnlyHost(host)) {
857
+ return {
858
+ host: host.displayName,
859
+ scope,
860
+ filePath: null,
861
+ status: "not present",
862
+ };
863
+ }
677
864
  const filePath = await resolveMcpPath(mcpCandidatesForScope(host, scope));
678
865
 
679
866
  if (host.mcp.configType === "toml") {
@@ -733,7 +920,8 @@ async function runUninstallCommand(options) {
733
920
  // Resolve the concrete server name(s) to remove at the host/scope level.
734
921
  // When --name was not given, this iterates every Flywheel-identifying
735
922
  // entry (matching setup's prior-mode detector) so renamed or drifted
736
- // entries (e.g. `[mcp_servers.flywheel-staging]`) are actually cleared.
923
+ // entries (non-default TOML section names under `[mcp_servers.*]`) are
924
+ // actually cleared.
737
925
  // eslint-disable-next-line no-await-in-loop
738
926
  const targetNames = await resolveUninstallServerNamesForHost(
739
927
  host,
@@ -743,10 +931,16 @@ async function runUninstallCommand(options) {
743
931
  if (targetNames.length === 0) {
744
932
  // eslint-disable-next-line no-await-in-loop
745
933
  const hostForDisplay = getHost(host);
746
- // eslint-disable-next-line no-await-in-loop
747
- const filePath = await resolveMcpPath(
748
- mcpCandidatesForScope(hostForDisplay, singleScope),
749
- );
934
+ // Skill-only hosts (universal, antigravity) have no MCP surface
935
+ // at all `host.mcp` is null, so dereferencing `projectPaths` via
936
+ // `mcpCandidatesForScope` throws. Report the absence without
937
+ // consulting a path that doesn't exist for them.
938
+ const filePath = isSkillOnlyHost(hostForDisplay)
939
+ ? null
940
+ : // eslint-disable-next-line no-await-in-loop
941
+ await resolveMcpPath(
942
+ mcpCandidatesForScope(hostForDisplay, singleScope),
943
+ );
750
944
  mcpResults.push({
751
945
  host: hostForDisplay.displayName,
752
946
  scope: singleScope,
@@ -774,12 +968,26 @@ async function runUninstallCommand(options) {
774
968
  if (bundledSkillNames.length > 0) {
775
969
  const skillSpinner = ora("Removing Flywheel bundled skills...").start();
776
970
  for (const singleScope of scopes) {
777
- // Prevent deleting shared skill directories while non-selected hosts
778
- // mapped to the same location still have Flywheel MCP entries.
971
+ // Retain any skill directory still claimed by a non-uninstalled host.
972
+ // Two signals combine: (a) MCP-backed hosts whose Flywheel entries
973
+ // survived the MCP-removal pass above, and (b) any host with bundled
974
+ // skill artifacts still on disk (including skill-only hosts like
975
+ // universal/antigravity, which never advertise themselves via MCP
976
+ // entries). Without (b), uninstalling one MCP host would silently
977
+ // wipe a shared `.agents/skills/<skill>/` that another host still
978
+ // relies on.
779
979
  // eslint-disable-next-line no-await-in-loop
780
- const configuredRetainHostNames = await resolveHostsWithRemainingFlywheelMcpEntries({
980
+ const mcpRetained = await resolveHostsWithRemainingFlywheelMcpEntries({
781
981
  scope: singleScope,
782
982
  });
983
+ // eslint-disable-next-line no-await-in-loop
984
+ const skillOnlyRetained = await resolveSkillOnlyHostsRetainingArtifacts({
985
+ scope: singleScope,
986
+ excludedHostNames: supportedHosts,
987
+ });
988
+ const configuredRetainHostNames = [
989
+ ...new Set([...mcpRetained, ...skillOnlyRetained]),
990
+ ];
783
991
  skillSpinner.text = `Removing bundled skills from ${supportedHosts.length} host(s) (${singleScope})...`;
784
992
  // eslint-disable-next-line no-await-in-loop
785
993
  bundledSkillResults.push(
@@ -794,6 +1002,19 @@ async function runUninstallCommand(options) {
794
1002
  }
795
1003
  skillSpinner.succeed("Bundled skill uninstall complete");
796
1004
  }
1005
+
1006
+ // Install-marker cleanup runs independently of bundled-skill discovery.
1007
+ // A broken or trimmed package (missing `skills/`) can leave
1008
+ // `bundledSkillNames` empty — the skill-removal pass above legitimately
1009
+ // no-ops in that case, but the ownership markers must still be cleared,
1010
+ // otherwise a later `uninstall`/`setup` would treat the uninstalled
1011
+ // host as still owning shared skill dirs (wrong retention) or as a
1012
+ // prior CLI install (false cross-mode conflict).
1013
+ await cleanupHostInstallMarkers({
1014
+ cwd: process.cwd(),
1015
+ scopes,
1016
+ hostNames: supportedHosts,
1017
+ });
797
1018
  }
798
1019
 
799
1020
  log.blank();
@@ -882,15 +1103,25 @@ Examples:
882
1103
  ${pc.green("npx --yes @paradigma-inc/flywheel setup --mode mcp --auth-mode device --codex --project")}
883
1104
  ${pc.green("npx --yes @paradigma-inc/flywheel setup --mode cli --codex --project")}
884
1105
  ${pc.green("npx --yes @paradigma-inc/flywheel setup --mode mcp --yes --codex --claude --cursor")}
885
- ${pc.green(
886
- "npx --yes @paradigma-inc/flywheel uninstall --scope all --hosts codex,claude,opencode,cursor,pi-mono,hermes-agent,openclaw",
887
- )}
1106
+ ${pc.green("npx --yes @paradigma-inc/flywheel uninstall --yes --scope all")}
888
1107
  `,
889
1108
  );
890
1109
 
891
1110
  program
892
1111
  .command("setup")
893
1112
  .description("Set up Flywheel for your AI coding host")
1113
+ .optionsGroup("Hosts:")
1114
+ .option("--claude", "Set up for Claude Code")
1115
+ .option("--cursor", "Set up for Cursor")
1116
+ .option("--universal", "Set up for Universal (.agents/skills)")
1117
+ .option("--antigravity", "Set up for Antigravity (.agent/skills)")
1118
+ .option("--opencode", "Set up for OpenCode")
1119
+ .option("--codex", "Set up for Codex")
1120
+ .option("--gemini", "Set up for Gemini CLI")
1121
+ .option("--pi-mono", "Set up for Pi (pi-mono)")
1122
+ .option("--hermes-agent", "Set up for Hermes Agent")
1123
+ .option("--openclaw", "Set up for OpenClaw")
1124
+ .optionsGroup("Mode:")
894
1125
  .option(
895
1126
  "--mode <mode>",
896
1127
  "Install mode: mcp | cli (required in non-interactive; prompted in TTY, default: mcp)",
@@ -899,33 +1130,31 @@ Examples:
899
1130
  )
900
1131
  .option("--mcp", "Alias for --mode mcp")
901
1132
  .option("--cli", "Alias for --mode cli")
902
- .option("--claude", "Set up for Claude Code")
903
- .option("--opencode", "Set up for OpenCode")
904
- .option("--codex", "Set up for Codex")
905
- .option("--cursor", "Set up for Cursor")
906
- .option("--pi-mono", "Set up for Pi (pi-mono)")
907
- .option("--hermes-agent", "Set up for Hermes Agent")
908
- .option("--openclaw", "Set up for OpenClaw")
1133
+ .optionsGroup("Scope:")
909
1134
  .option(
910
1135
  "-p, --project",
911
1136
  "Configure for current project instead of globally",
912
1137
  )
913
- .option("-y, --yes", "Skip host selection prompts")
914
- .option("--force", "Reconcile prior install mode automatically via uninstall")
1138
+ .option("-y, --yes", "Skip confirmation prompts")
1139
+ .optionsGroup("Authentication:")
915
1140
  .option("--api-key <key>", "Use API key authentication")
1141
+ .option("--oauth", "Use OAuth endpoint (IDE handles auth flow)")
1142
+ .optionsGroup("Skills:")
1143
+ .option("--install-skill", "Install or refresh the bundled Flywheel skills")
1144
+ .option("--skip-skill", "Skip bundled skill installation (MCP-only setup)")
1145
+ .optionsGroup("Advanced:")
916
1146
  .option(
917
1147
  "--auth-mode <mode>",
918
- "Authentication mode: auto | loopback | device (default: auto)",
1148
+ "API-key mint flow: auto | loopback | device (default: auto)",
919
1149
  normalizeAuthMode,
920
1150
  "auto",
921
1151
  )
922
- .option("--install-skill", "Install or refresh the bundled Flywheel skills")
923
- .option("--skip-skill", "Skip bundled skill installation (MCP-only setup)")
924
1152
  .option(
925
1153
  "--base-url <url>",
926
1154
  `Public Flywheel origin used for setup and MCP config (default: ${DEFAULT_BASE_URL})`,
927
1155
  )
928
1156
  .option("--name <name>", `MCP server name (default: ${SERVER_NAME})`)
1157
+ .option("--force", "Reconcile prior install mode automatically via uninstall")
929
1158
  .action(async (options) => {
930
1159
  await runSetupCommandViaRouter(options);
931
1160
  });
@@ -934,15 +1163,18 @@ Examples:
934
1163
  .command("uninstall")
935
1164
  .description("Remove Flywheel MCP entries and bundled skills from selected hosts")
936
1165
  .option("--claude", "Uninstall for Claude Code")
1166
+ .option("--cursor", "Uninstall for Cursor")
1167
+ .option("--universal", "Uninstall bundled skills from .agents/skills")
1168
+ .option("--antigravity", "Uninstall bundled skills from .agent/skills")
937
1169
  .option("--opencode", "Uninstall for OpenCode")
938
1170
  .option("--codex", "Uninstall for Codex")
939
- .option("--cursor", "Uninstall for Cursor")
1171
+ .option("--gemini", "Uninstall for Gemini CLI")
940
1172
  .option("--pi-mono", "Uninstall for Pi (pi-mono)")
941
1173
  .option("--hermes-agent", "Uninstall for Hermes Agent")
942
1174
  .option("--openclaw", "Uninstall for OpenClaw")
943
1175
  .option(
944
1176
  "--hosts <list>",
945
- "Comma-separated: codex,claude,opencode,cursor,pi-mono,hermes-agent,openclaw",
1177
+ "Comma-separated: claude,cursor,universal,antigravity,opencode,codex,gemini,pi-mono,hermes-agent,openclaw",
946
1178
  )
947
1179
  .option("--scope <scope>", "all | global | project")
948
1180
  .option(