@paradigma-inc/flywheel 0.1.13 → 0.1.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paradigma-inc/flywheel",
3
- "version": "0.1.13",
3
+ "version": "0.1.16",
4
4
  "description": "One-command setup for Flywheel MCP hosts",
5
5
  "type": "module",
6
6
  "files": [
@@ -50,3 +50,9 @@ Choose one primary file first, then add at most one supporting file only if need
50
50
  - Prefer concrete steps and decision points over long narrative explanations.
51
51
  - Do not run broad document reads; load only what is required by the current request.
52
52
  - Do not start compute-heavy execution before design intent is clear.
53
+ - For managed compute cleanup in autonomous runs, default to releasing only
54
+ known leases (leases acquired in the current run or explicitly selected by
55
+ the user).
56
+ - When unknown active leases are present, report and skip by default.
57
+ - Use account-wide release (`flywheel_compute_release_all`) only when the user
58
+ explicitly asks for account-wide cleanup.
@@ -17,6 +17,11 @@ Flywheel can provision managed GPU instances for you from providers like Lambda
17
17
  3. **The agent handles the rest.** It acquires a machine, waits for it to become ready, and gets SSH access automatically. Provisioning typically takes a few minutes — the agent polls for readiness in the background.
18
18
  4. **Release when done.** When you're finished, the agent releases the lease. You can also release leases from the WebUI at any time.
19
19
 
20
+ For autonomous runs, agents should default to releasing only known leases
21
+ (leases acquired in the current run or explicitly selected by the user). If
22
+ unknown active leases are detected, agents should report and skip those leases
23
+ by default. Use account-wide release only when the user explicitly requests it.
24
+
20
25
  ## [](https://flywheel.paradigma.inc//#managing-machines-in-the-webui)Managing machines in the WebUI
21
26
 
22
27
  In **Settings > Machines**, you can see all GPU leases across your account — including the provider, state, hourly rate, total spend, and the acquire context that originally launched each lease. You can release individual leases or all at once.
@@ -84,7 +84,9 @@ Before execution, recover or establish these inputs:
84
84
  - Spawn up to `k` workers for distinct executable frontier nodes.
85
85
  - Route additional viable branches sequentially when there are more than `k` worthwhile directions.
86
86
  - After each resolved node, refresh the lookahead so the graph remains `n` hops ahead.
87
- - Release managed compute with `flywheel_compute_release` or `flywheel_compute_release_all` when a branch is done.
87
+ - Release managed compute when a branch is done. Default to releasing only
88
+ known leases. Use `flywheel_compute_release_all` only when the user
89
+ explicitly requests account-wide cleanup.
88
90
  - Stop only when the graph-local terminal condition says to stop, such as: objective met, budget exhausted, or no non-redundant frontier branch is likely to produce enough decision-relevant information to justify the remaining budget and duplication risk.
89
91
  9. Commit after the node snapshot is coherent with `flywheel_commit_node`.
90
92
  - Commit resolved insight nodes only when they have non-empty `insights`.
@@ -1,19 +1,19 @@
1
1
  [
2
2
  {
3
3
  "id": "continuation-from-existing-frontier",
4
- "prompt": "Use $flywheel-auto at /Users/g.f.g.f./Documents/GitHub/paradigma/project/flywheel-setup/skills/flywheel-auto to continue a Flywheel frontier that already has a persisted control node. Keep going under the existing credit cap and do not ask again for the objective or budget unless the control node is missing or contradictory.",
4
+ "prompt": "Use $flywheel-auto to continue a Flywheel frontier that already has a persisted control node. Keep going under the existing credit cap and do not ask again for the objective or budget unless the control node is missing or contradictory.",
5
5
  "expected_output": "Read the persisted contract from the control node content before replanning, continue from that stored objective and budget, avoid re-asking for the objective or budget when the control node is coherent, and leave behind an updated control node plus staged or committed frontier work.",
6
6
  "files": []
7
7
  },
8
8
  {
9
9
  "id": "cold-start-with-non-credit-budget",
10
- "prompt": "Use $flywheel-auto at /Users/g.f.g.f./Documents/GitHub/paradigma/project/flywheel-setup/skills/flywheel-auto to explore this frontier autonomously with at most $25 of managed-compute spend. Persist the dollar-denominated cap verbatim and derive whatever operational compute approval cap is needed before acquisition.",
10
+ "prompt": "Use $flywheel-auto to explore this frontier autonomously with at most $25 of managed-compute spend. Persist the dollar-denominated cap verbatim and derive whatever operational compute approval cap is needed before acquisition.",
11
11
  "expected_output": "Persist the dollar-denominated budget ceiling and unit verbatim in the control node, derive an operational compute approval cap before acquisition, record both values before any compute request is made, and leave a coherent run contract behind for later continuation.",
12
12
  "files": []
13
13
  },
14
14
  {
15
15
  "id": "cold-start-with-no-objective",
16
- "prompt": "Use $flywheel-auto at /Users/g.f.g.f./Documents/GitHub/paradigma/project/flywheel-setup/skills/flywheel-auto to go autonomous on this Flywheel graph until you find something interesting. If I refuse to specify an objective, infer one from the graph context and make it explicit before you execute.",
16
+ "prompt": "Use $flywheel-auto to go autonomous on this Flywheel graph until you find something interesting. If I refuse to specify an objective, infer one from the graph context and make it explicit before you execute.",
17
17
  "expected_output": "Ask for an objective first, infer one from graph context if the user refuses, write that objective explicitly into the control node, and only then proceed toward execution with a coherent control node and frontier plan.",
18
18
  "files": []
19
19
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  Canonical reference for Flywheel user-facing interfaces and public contracts.
4
4
 
5
- Last updated: 2026-03-21
5
+ Last updated: 2026-04-08
6
6
 
7
7
  ## Primary Clients
8
8
 
@@ -19,7 +19,8 @@ All interfaces operate on the same core entities:
19
19
  - artifact
20
20
  - execution
21
21
  - compute lease
22
- - budget grant
22
+ - compute budget
23
+ - compute grant (session-scoped)
23
24
 
24
25
  Node snapshots are the canonical durable contract for resume, review, and
25
26
  cross-client interoperability.
@@ -69,11 +70,12 @@ cross-client interoperability.
69
70
  - `GET /mcp/compute/connection`
70
71
  - `POST /mcp/compute/release`
71
72
  - `POST /mcp/compute/release-all`
72
- - `GET /mcp/nodes/{node_id}/budget/grants`
73
- - `GET /mcp/nodes/{node_id}/budget/campaign-grants`
74
- - `POST /mcp/nodes/{node_id}/budget/campaign-grants`
75
- - `PATCH /mcp/nodes/{node_id}/budget/campaign-grants/{budget_grant_id}`
76
- - `DELETE /mcp/nodes/{node_id}/budget/campaign-grants/{budget_grant_id}`
73
+ - `GET /mcp/compute/grants`
74
+ - `GET /mcp/nodes/{node_id}/budgets`
75
+ - `GET /mcp/nodes/{root_node_id}/campaign-budgets`
76
+ - `POST /mcp/nodes/{root_node_id}/campaign-budgets`
77
+ - `PATCH /mcp/nodes/{root_node_id}/campaign-budgets/{compute_budget_id}`
78
+ - `DELETE /mcp/nodes/{root_node_id}/campaign-budgets/{compute_budget_id}`
77
79
 
78
80
  ### Sharing, tags, and audit
79
81
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Canonical reference for Flywheel user-facing interfaces and public contracts.
4
4
 
5
- Last updated: 2026-03-21
5
+ Last updated: 2026-04-08
6
6
 
7
7
  ## Primary Clients
8
8
 
@@ -19,7 +19,8 @@ All interfaces operate on the same core entities:
19
19
  - artifact
20
20
  - execution
21
21
  - compute lease
22
- - budget grant
22
+ - compute budget
23
+ - compute grant (session-scoped)
23
24
 
24
25
  Node snapshots are the canonical durable contract for resume, review, and
25
26
  cross-client interoperability.
@@ -69,11 +70,12 @@ cross-client interoperability.
69
70
  - `GET /mcp/compute/connection`
70
71
  - `POST /mcp/compute/release`
71
72
  - `POST /mcp/compute/release-all`
72
- - `GET /mcp/nodes/{node_id}/budget/grants`
73
- - `GET /mcp/nodes/{node_id}/budget/campaign-grants`
74
- - `POST /mcp/nodes/{node_id}/budget/campaign-grants`
75
- - `PATCH /mcp/nodes/{node_id}/budget/campaign-grants/{budget_grant_id}`
76
- - `DELETE /mcp/nodes/{node_id}/budget/campaign-grants/{budget_grant_id}`
73
+ - `GET /mcp/compute/grants`
74
+ - `GET /mcp/nodes/{node_id}/budgets`
75
+ - `GET /mcp/nodes/{root_node_id}/campaign-budgets`
76
+ - `POST /mcp/nodes/{root_node_id}/campaign-budgets`
77
+ - `PATCH /mcp/nodes/{root_node_id}/campaign-budgets/{compute_budget_id}`
78
+ - `DELETE /mcp/nodes/{root_node_id}/campaign-budgets/{compute_budget_id}`
77
79
 
78
80
  ### Sharing, tags, and audit
79
81
 
@@ -80,7 +80,9 @@ Before execution, recover or establish these inputs:
80
80
  - Prefer explicit validation branches such as baseline checks, mechanism or intermediate-signal checks, ablations, efficiency checks, robustness checks, failure analysis, and follow-up analysis branches after results land.
81
81
  - If the branch can run through Flywheel execution directly, use `flywheel_launch_execution` and inspect terminal status before commit.
82
82
  - If the branch needs managed compute, use `flywheel_request_compute_grant_approval`, resolve the approved `compute_grant_id` with `flywheel_list_compute_grants` or `flywheel_list_approval_sessions`, then `flywheel_compute_acquire`, poll `flywheel_compute_status`, and use `flywheel_compute_connection` when the lease is ready.
83
- - Release managed compute with `flywheel_compute_release` or `flywheel_compute_release_all` when a branch is done.
83
+ - Release managed compute when a branch is done. Default to releasing only
84
+ known leases. Use `flywheel_compute_release_all` only when the user
85
+ explicitly requests account-wide cleanup.
84
86
  - Stop when the core claim is resolved or the budget ceiling is reached.
85
87
  9. Commit after the node snapshot is coherent with `flywheel_commit_node`.
86
88
  - Commit resolved insight nodes only when they have non-empty `insights`.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Canonical reference for Flywheel user-facing interfaces and public contracts.
4
4
 
5
- Last updated: 2026-03-21
5
+ Last updated: 2026-04-08
6
6
 
7
7
  ## Primary Clients
8
8
 
@@ -19,7 +19,8 @@ All interfaces operate on the same core entities:
19
19
  - artifact
20
20
  - execution
21
21
  - compute lease
22
- - budget grant
22
+ - compute budget
23
+ - compute grant (session-scoped)
23
24
 
24
25
  Node snapshots are the canonical durable contract for resume, review, and
25
26
  cross-client interoperability.
@@ -69,11 +70,12 @@ cross-client interoperability.
69
70
  - `GET /mcp/compute/connection`
70
71
  - `POST /mcp/compute/release`
71
72
  - `POST /mcp/compute/release-all`
72
- - `GET /mcp/nodes/{node_id}/budget/grants`
73
- - `GET /mcp/nodes/{node_id}/budget/campaign-grants`
74
- - `POST /mcp/nodes/{node_id}/budget/campaign-grants`
75
- - `PATCH /mcp/nodes/{node_id}/budget/campaign-grants/{budget_grant_id}`
76
- - `DELETE /mcp/nodes/{node_id}/budget/campaign-grants/{budget_grant_id}`
73
+ - `GET /mcp/compute/grants`
74
+ - `GET /mcp/nodes/{node_id}/budgets`
75
+ - `GET /mcp/nodes/{root_node_id}/campaign-budgets`
76
+ - `POST /mcp/nodes/{root_node_id}/campaign-budgets`
77
+ - `PATCH /mcp/nodes/{root_node_id}/campaign-budgets/{compute_budget_id}`
78
+ - `DELETE /mcp/nodes/{root_node_id}/campaign-budgets/{compute_budget_id}`
77
79
 
78
80
  ### Sharing, tags, and audit
79
81
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Canonical reference for Flywheel user-facing interfaces and public contracts.
4
4
 
5
- Last updated: 2026-03-21
5
+ Last updated: 2026-04-08
6
6
 
7
7
  ## Primary Clients
8
8
 
@@ -19,7 +19,8 @@ All interfaces operate on the same core entities:
19
19
  - artifact
20
20
  - execution
21
21
  - compute lease
22
- - budget grant
22
+ - compute budget
23
+ - compute grant (session-scoped)
23
24
 
24
25
  Node snapshots are the canonical durable contract for resume, review, and
25
26
  cross-client interoperability.
@@ -69,11 +70,12 @@ cross-client interoperability.
69
70
  - `GET /mcp/compute/connection`
70
71
  - `POST /mcp/compute/release`
71
72
  - `POST /mcp/compute/release-all`
72
- - `GET /mcp/nodes/{node_id}/budget/grants`
73
- - `GET /mcp/nodes/{node_id}/budget/campaign-grants`
74
- - `POST /mcp/nodes/{node_id}/budget/campaign-grants`
75
- - `PATCH /mcp/nodes/{node_id}/budget/campaign-grants/{budget_grant_id}`
76
- - `DELETE /mcp/nodes/{node_id}/budget/campaign-grants/{budget_grant_id}`
73
+ - `GET /mcp/compute/grants`
74
+ - `GET /mcp/nodes/{node_id}/budgets`
75
+ - `GET /mcp/nodes/{root_node_id}/campaign-budgets`
76
+ - `POST /mcp/nodes/{root_node_id}/campaign-budgets`
77
+ - `PATCH /mcp/nodes/{root_node_id}/campaign-budgets/{compute_budget_id}`
78
+ - `DELETE /mcp/nodes/{root_node_id}/campaign-budgets/{compute_budget_id}`
77
79
 
78
80
  ### Sharing, tags, and audit
79
81
 
package/src/cli.mjs CHANGED
@@ -2,7 +2,7 @@ import { Command } from "commander";
2
2
  import { checkbox, confirm, select } from "@inquirer/prompts";
3
3
  import ora from "ora";
4
4
  import pc from "picocolors";
5
- import { rm } from "node:fs/promises";
5
+ import { readFile, rm } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { randomBytes } from "node:crypto";
8
8
 
@@ -39,6 +39,8 @@ import {
39
39
  installBundledSkillForHosts,
40
40
  listBundledSkills,
41
41
  partitionInstallSkillHosts,
42
+ removeInstalledSkillsForHosts,
43
+ SUPPORTED_INSTALL_SKILL_HOSTS,
42
44
  resolveInstalledSkillDir,
43
45
  resolvePackageRoot,
44
46
  } from "./skill-installer.mjs";
@@ -751,6 +753,106 @@ function scopesFromUninstallScope(scope) {
751
753
  return scope === "all" ? ["global", "project"] : [scope];
752
754
  }
753
755
 
756
+ function keyPath(configKey) {
757
+ return Array.isArray(configKey) ? configKey : [configKey];
758
+ }
759
+
760
+ function readConfigSection(config, configKey) {
761
+ const pathParts = keyPath(configKey);
762
+ let current = config && typeof config === "object" ? config : {};
763
+ for (const part of pathParts) {
764
+ if (
765
+ !current ||
766
+ typeof current !== "object" ||
767
+ Array.isArray(current) ||
768
+ !(part in current)
769
+ ) {
770
+ return {};
771
+ }
772
+ const next = current[part];
773
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
774
+ return {};
775
+ }
776
+ current = next;
777
+ }
778
+ return current;
779
+ }
780
+
781
+ function parseTomlServerNames(tomlText) {
782
+ const serverNames = new Set();
783
+ for (const line of String(tomlText || "").replace(/\r\n/g, "\n").split("\n")) {
784
+ const match = line.trim().match(/^\[mcp_servers\.([A-Za-z0-9_-]+)\](?:\s*#.*)?$/);
785
+ if (!match) {
786
+ continue;
787
+ }
788
+ serverNames.add(match[1]);
789
+ }
790
+ return [...serverNames].sort();
791
+ }
792
+
793
+ function isLikelyFlywheelMcpUrl(url) {
794
+ if (typeof url !== "string" || url.trim().length === 0) {
795
+ return false;
796
+ }
797
+ try {
798
+ const parsed = new URL(url);
799
+ return parsed.pathname === "/mcp-server";
800
+ } catch {
801
+ return false;
802
+ }
803
+ }
804
+
805
+ async function listHostMcpEntries(hostName, scope) {
806
+ const host = getHost(hostName);
807
+ const filePath = await resolveMcpPath(mcpCandidatesForScope(host, scope));
808
+
809
+ if (host.mcp.configType === "toml") {
810
+ let raw = "";
811
+ try {
812
+ raw = await readFile(filePath, "utf8");
813
+ } catch {
814
+ return [];
815
+ }
816
+ const entries = [];
817
+ for (const serverName of parseTomlServerNames(raw)) {
818
+ // eslint-disable-next-line no-await-in-loop
819
+ const entry = await readTomlServerEntry(filePath, serverName);
820
+ if (!entry.exists) {
821
+ continue;
822
+ }
823
+ entries.push({
824
+ serverName,
825
+ url: typeof entry.url === "string" ? entry.url : null,
826
+ });
827
+ }
828
+ return entries;
829
+ }
830
+
831
+ const config =
832
+ host.mcp.configType === "yaml"
833
+ ? await readYamlConfig(filePath)
834
+ : await readJsonConfig(filePath);
835
+ const section = readConfigSection(config, host.mcp.configKey);
836
+ return Object.entries(section)
837
+ .filter(([, entry]) => entry && typeof entry === "object" && !Array.isArray(entry))
838
+ .map(([serverName, entry]) => ({
839
+ serverName,
840
+ url: typeof entry.url === "string" ? entry.url : null,
841
+ }));
842
+ }
843
+
844
+ async function resolveHostsWithRemainingFlywheelMcpEntries({ scope }) {
845
+ const remainingHosts = [];
846
+ for (const hostName of SUPPORTED_INSTALL_SKILL_HOSTS) {
847
+ // eslint-disable-next-line no-await-in-loop
848
+ const hostEntries = await listHostMcpEntries(hostName, scope).catch(() => []);
849
+ if (hostEntries.some((entry) => isLikelyFlywheelMcpUrl(entry.url))) {
850
+ remainingHosts.push(hostName);
851
+ }
852
+ }
853
+ return remainingHosts;
854
+ }
855
+
754
856
  async function isConfiguredForUninstallScope(hostName, scope, serverName) {
755
857
  const scopes = scopesFromUninstallScope(scope);
756
858
  for (const singleScope of scopes) {
@@ -907,26 +1009,118 @@ async function runUninstallCommand(options) {
907
1009
  const { scope, hosts } = resolved;
908
1010
  const scopes = scopesFromUninstallScope(scope);
909
1011
 
910
- const spinner = ora("Removing Flywheel MCP entries...").start();
911
- const results = [];
1012
+ const mcpSpinner = ora("Removing Flywheel MCP entries...").start();
1013
+ const mcpResults = [];
912
1014
  for (const singleScope of scopes) {
913
1015
  for (const host of hosts) {
914
- spinner.text = `Removing from ${getHost(host).displayName} (${singleScope})...`;
1016
+ mcpSpinner.text = `Removing from ${getHost(host).displayName} (${singleScope})...`;
915
1017
  // eslint-disable-next-line no-await-in-loop
916
- results.push(await removeHostConfig(host, singleScope, serverName));
1018
+ mcpResults.push(await removeHostConfig(host, singleScope, serverName));
1019
+ }
1020
+ }
1021
+ mcpSpinner.succeed("MCP uninstall complete");
1022
+
1023
+ const { supportedHosts, unsupportedHosts } =
1024
+ partitionInstallSkillHosts(hosts);
1025
+ const bundledSkillResults = [];
1026
+ let bundledSkillNames = [];
1027
+
1028
+ if (supportedHosts.length > 0) {
1029
+ const packageRoot = resolvePackageRoot();
1030
+ bundledSkillNames = await listBundledSkills(packageRoot);
1031
+ if (bundledSkillNames.length > 0) {
1032
+ const skillSpinner = ora("Removing Flywheel bundled skills...").start();
1033
+ for (const singleScope of scopes) {
1034
+ // Prevent deleting shared skill directories while non-selected hosts
1035
+ // mapped to the same location still have Flywheel MCP entries.
1036
+ // eslint-disable-next-line no-await-in-loop
1037
+ const configuredRetainHostNames = await resolveHostsWithRemainingFlywheelMcpEntries({
1038
+ scope: singleScope,
1039
+ });
1040
+ skillSpinner.text = `Removing bundled skills from ${supportedHosts.length} host(s) (${singleScope})...`;
1041
+ // eslint-disable-next-line no-await-in-loop
1042
+ bundledSkillResults.push(
1043
+ ...(await removeInstalledSkillsForHosts({
1044
+ projectRoot: process.cwd(),
1045
+ hostNames: supportedHosts,
1046
+ retainHostNames: configuredRetainHostNames,
1047
+ scope: singleScope,
1048
+ skillNames: bundledSkillNames,
1049
+ })),
1050
+ );
1051
+ }
1052
+ skillSpinner.succeed("Bundled skill uninstall complete");
917
1053
  }
918
1054
  }
919
- spinner.succeed("Uninstall complete");
920
1055
 
921
1056
  log.blank();
922
- log.plain(pc.green("✔ Flywheel uninstall complete"));
1057
+ log.plain(pc.green("✔ Flywheel uninstall complete (MCP entries and bundled skills)"));
923
1058
  log.blank();
924
- for (const result of results) {
1059
+ for (const result of mcpResults) {
925
1060
  const icon = result.status === "removed" ? pc.green("-") : pc.dim("~");
926
1061
  log.plain(` ${pc.bold(result.host)} ${pc.dim(`(${result.scope})`)}`);
927
1062
  log.plain(` ${icon} ${result.status}`);
928
1063
  log.plain(` ${pc.dim(result.filePath)}`);
929
1064
  }
1065
+
1066
+ log.blank();
1067
+ log.plain(` ${pc.bold("Bundled skills")}`);
1068
+ if (supportedHosts.length === 0) {
1069
+ log.plain(` ${pc.dim("~")} no skill-supporting hosts selected`);
1070
+ } else if (bundledSkillNames.length === 0) {
1071
+ log.plain(` ${pc.dim("~")} no bundled skills found in this package`);
1072
+ } else {
1073
+ const groupedSkillResults = new Map();
1074
+ for (const skillResult of bundledSkillResults) {
1075
+ for (const hostName of skillResult.hostNames) {
1076
+ const groupingKey = `${hostName}:${skillResult.scope}`;
1077
+ const existingGroup = groupedSkillResults.get(groupingKey);
1078
+ if (existingGroup) {
1079
+ existingGroup.entries.push(skillResult);
1080
+ continue;
1081
+ }
1082
+ groupedSkillResults.set(groupingKey, {
1083
+ hostName,
1084
+ scope: skillResult.scope,
1085
+ entries: [skillResult],
1086
+ });
1087
+ }
1088
+ }
1089
+
1090
+ const sortedGroups = [...groupedSkillResults.values()].sort((left, right) => {
1091
+ const leftKey = `${left.hostName}:${left.scope}`;
1092
+ const rightKey = `${right.hostName}:${right.scope}`;
1093
+ return leftKey.localeCompare(rightKey);
1094
+ });
1095
+ for (const groupedResult of sortedGroups) {
1096
+ log.plain(
1097
+ ` ${pc.bold(getHost(groupedResult.hostName).displayName)} ${pc.dim(`(${groupedResult.scope})`)}`,
1098
+ );
1099
+ for (const entry of groupedResult.entries) {
1100
+ const icon =
1101
+ entry.status === "removed"
1102
+ ? pc.green("-")
1103
+ : entry.status === "retained"
1104
+ ? pc.yellow("~")
1105
+ : pc.dim("~");
1106
+ const retainedSuffix =
1107
+ entry.status === "retained" && Array.isArray(entry.retainedHostNames)
1108
+ ? ` (shared with configured host(s): ${entry.retainedHostNames.join(", ")})`
1109
+ : "";
1110
+ log.plain(` ${icon} ${entry.status}${retainedSuffix}: ${entry.skillNames.join(", ")}`);
1111
+ log.plain(` ${pc.dim(entry.installedSkillDir)}`);
1112
+ }
1113
+ }
1114
+ }
1115
+
1116
+ for (const unsupportedHostName of unsupportedHosts) {
1117
+ for (const singleScope of scopes) {
1118
+ log.plain(
1119
+ ` ${pc.bold(getHost(unsupportedHostName).displayName)} ${pc.dim(`(${singleScope})`)}`,
1120
+ );
1121
+ log.plain(` ${pc.dim("~")} skill uninstall not supported (MCP-only host)`);
1122
+ }
1123
+ }
930
1124
  log.blank();
931
1125
  }
932
1126
 
@@ -986,7 +1180,7 @@ Examples:
986
1180
 
987
1181
  program
988
1182
  .command("uninstall")
989
- .description("Remove Flywheel MCP entries from host configs")
1183
+ .description("Remove Flywheel MCP entries and bundled skills from selected hosts")
990
1184
  .option("--claude", "Uninstall for Claude Code")
991
1185
  .option("--opencode", "Uninstall for OpenCode")
992
1186
  .option("--codex", "Uninstall for Codex")
@@ -397,6 +397,90 @@ export async function installBundledSkillForHosts({
397
397
  }
398
398
  }
399
399
 
400
+ export async function removeInstalledSkillsForHosts({
401
+ projectRoot = process.cwd(),
402
+ hostNames = [],
403
+ retainHostNames = [],
404
+ scope = "project",
405
+ skillNames,
406
+ } = {}) {
407
+ const normalizedScope = normalizeInstallScope(scope);
408
+ const normalizedSkillNames = normalizeBundledSkillNames(skillNames);
409
+ const normalizedRetainHostNames = uniqueValues(retainHostNames);
410
+ const removalTargetsByPath = new Map();
411
+ const retainedTargetsByPath = new Map();
412
+
413
+ for (const hostName of uniqueValues(hostNames)) {
414
+ for (const skillName of normalizedSkillNames) {
415
+ const installedSkillDir = resolveInstalledSkillDir(
416
+ projectRoot,
417
+ hostName,
418
+ normalizedScope,
419
+ skillName,
420
+ );
421
+ const existingTarget = removalTargetsByPath.get(installedSkillDir);
422
+ if (existingTarget) {
423
+ existingTarget.hostNames.add(hostName);
424
+ existingTarget.skillNames.add(skillName);
425
+ continue;
426
+ }
427
+ removalTargetsByPath.set(installedSkillDir, {
428
+ installedSkillDir,
429
+ hostNames: new Set([hostName]),
430
+ skillNames: new Set([skillName]),
431
+ });
432
+ }
433
+ }
434
+
435
+ for (const hostName of normalizedRetainHostNames) {
436
+ for (const skillName of normalizedSkillNames) {
437
+ const installedSkillDir = resolveInstalledSkillDir(
438
+ projectRoot,
439
+ hostName,
440
+ normalizedScope,
441
+ skillName,
442
+ );
443
+ const existingRetainedTarget = retainedTargetsByPath.get(installedSkillDir);
444
+ if (existingRetainedTarget) {
445
+ existingRetainedTarget.hostNames.add(hostName);
446
+ continue;
447
+ }
448
+ retainedTargetsByPath.set(installedSkillDir, {
449
+ hostNames: new Set([hostName]),
450
+ });
451
+ }
452
+ }
453
+
454
+ const removalResults = [];
455
+ for (const removalTarget of [...removalTargetsByPath.values()].sort((left, right) =>
456
+ left.installedSkillDir.localeCompare(right.installedSkillDir),
457
+ )) {
458
+ const hadInstalledSkillDir = await pathExists(removalTarget.installedSkillDir);
459
+ const retainedTarget = retainedTargetsByPath.get(removalTarget.installedSkillDir);
460
+ if (retainedTarget) {
461
+ removalResults.push({
462
+ scope: normalizedScope,
463
+ installedSkillDir: removalTarget.installedSkillDir,
464
+ hostNames: [...removalTarget.hostNames].sort(),
465
+ retainedHostNames: [...retainedTarget.hostNames].sort(),
466
+ skillNames: [...removalTarget.skillNames].sort(),
467
+ status: hadInstalledSkillDir ? "retained" : "not present",
468
+ });
469
+ continue;
470
+ }
471
+ await rm(removalTarget.installedSkillDir, { recursive: true, force: true });
472
+ removalResults.push({
473
+ scope: normalizedScope,
474
+ installedSkillDir: removalTarget.installedSkillDir,
475
+ hostNames: [...removalTarget.hostNames].sort(),
476
+ skillNames: [...removalTarget.skillNames].sort(),
477
+ status: hadInstalledSkillDir ? "removed" : "not present",
478
+ });
479
+ }
480
+
481
+ return removalResults;
482
+ }
483
+
400
484
  async function rewriteOpenAiAgentMetadataMcpTarget({
401
485
  skillDir,
402
486
  serverName,