@paradigma-inc/flywheel 0.1.12 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/flywheel-auto/evals/evals.json +3 -3
- package/skills/flywheel-auto/references/INTERFACES.md +9 -7
- package/skills/flywheel-lookahead/SKILL.md +27 -12
- package/skills/flywheel-lookahead/evals/evals.json +2 -2
- package/skills/flywheel-lookahead/references/INTERFACES.md +9 -7
- package/skills/flywheel-reproduce/references/INTERFACES.md +9 -7
- package/skills/flywheel-to-graph/references/INTERFACES.md +9 -7
- package/src/cli.mjs +203 -9
- package/src/skill-installer.mjs +84 -0
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"id": "continuation-from-existing-frontier",
|
|
4
|
-
"prompt": "Use $flywheel-auto
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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/
|
|
73
|
-
- `GET /mcp/nodes/{node_id}/
|
|
74
|
-
- `
|
|
75
|
-
- `
|
|
76
|
-
- `
|
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: flywheel-lookahead
|
|
3
|
-
description: Stage next-step frontier
|
|
3
|
+
description: Stage a single-path next-step frontier from existing Flywheel graph state with Flywheel MCP using lookahead depth `n` and per-hop option width `k`, without execution or compute acquisition. Use for planning what to do next, not claim validation, source-to-graph authoring, or autonomous execution.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# flywheel-lookahead
|
|
@@ -15,14 +15,14 @@ If exact tool or field semantics are unclear, load [references/flywheel-mcp-tool
|
|
|
15
15
|
|
|
16
16
|
Before planning, recover or establish these inputs:
|
|
17
17
|
|
|
18
|
-
- Required before planning: start nodes, objective, lookahead depth `n`,
|
|
18
|
+
- Required before planning: start nodes, objective, lookahead depth `n`, step option width `k`, and an explicit terminal condition.
|
|
19
19
|
- Optional: budget ceiling and budget unit, but only when the user already supplied them or wants them tracked for later execution.
|
|
20
|
-
- Defaults: `lookahead_depth` defaults to `n=1`; `
|
|
20
|
+
- Defaults: `lookahead_depth` defaults to `n=1`; `step_option_width` defaults to `k=1`.
|
|
21
21
|
- Recovery rules:
|
|
22
22
|
- Ask only when a required planning input cannot be recovered from explicit user instructions, current conversational context, or recent graph state.
|
|
23
23
|
- If start nodes cannot be recovered from explicit ids/slugs, named current context, or recent graph context, ask once.
|
|
24
24
|
- If the objective is missing, ask once; if the user refuses, infer it from graph context and persist it before planning.
|
|
25
|
-
- If the terminal condition is missing, ask once; if it remains unspecified, persist a planning-local terminal condition such as stopping after staging this lookahead pass through the requested `n` hops
|
|
25
|
+
- If the terminal condition is missing, ask once; if it remains unspecified, persist a planning-local terminal condition such as stopping after staging this lookahead pass through the requested `n` hops with at most `k` considered options per hop and one selected continuation per hop.
|
|
26
26
|
|
|
27
27
|
## Working Terms
|
|
28
28
|
|
|
@@ -31,6 +31,19 @@ Before planning, recover or establish these inputs:
|
|
|
31
31
|
- `control node`: the dedicated `insight` node that stores the canonical planning contract for the current frontier.
|
|
32
32
|
- `terminal condition`: the explicit condition that tells this planning pass when to stop staging additional frontier nodes.
|
|
33
33
|
- `expected_revision`: the node's current revision token used for optimistic-locking writes.
|
|
34
|
+
- `lookahead depth (n)`: the number of sequential hops in one planned path.
|
|
35
|
+
- `step option width (k)`: the number of candidate directions evaluated at each hop before choosing one continuation.
|
|
36
|
+
|
|
37
|
+
## Width Semantics (Unambiguous)
|
|
38
|
+
|
|
39
|
+
- Lookahead depth `n`: number of sequential hops in one plan path.
|
|
40
|
+
- Step option width `k`: number of candidate directions to evaluate at each hop.
|
|
41
|
+
- Expansion rule: at hop `i`, generate up to `k` non-redundant options, select exactly 1 winner, and only that winner continues to hop `i+1`.
|
|
42
|
+
- Graph shape for this mode: a single chain of `n` staged nodes (not `k` parallel branches).
|
|
43
|
+
- Each step node must record:
|
|
44
|
+
- the `k` considered options,
|
|
45
|
+
- the selection criterion,
|
|
46
|
+
- the chosen continuation.
|
|
34
47
|
|
|
35
48
|
## Core Rules
|
|
36
49
|
|
|
@@ -51,7 +64,7 @@ Before planning, recover or establish these inputs:
|
|
|
51
64
|
- The flywheel-lookahead skill requires a measurable objective. Ask once if it is missing; if the user refuses to specify one, infer it from the available graph context and state it explicitly before continuing.
|
|
52
65
|
- Default `n=1` and `k=1` unless the user specified otherwise.
|
|
53
66
|
- Persist the planning contract in a dedicated `insight` control node for this frontier. Put the canonical contract in node `content`, keep a one-line synopsis in `summary`, and keep brief planning rationale in `insights` so the commit contract stays valid.
|
|
54
|
-
- The control contract must name: objective, decision criterion, start nodes, lookahead depth `n`,
|
|
67
|
+
- The control contract must name: objective, decision criterion, start nodes, lookahead depth `n`, step option width `k`, and an explicit terminal condition. Only persist a budget ceiling and unit when the user already supplied one or wants them tracked for later execution.
|
|
55
68
|
- Later lookahead passes must read the control node `content` first and continue from that persisted contract rather than from chat memory.
|
|
56
69
|
3. Map each stable page, claim, or concept to a node.
|
|
57
70
|
- For exact node-mutation shapes, load [references/flywheel-mcp-tool-map.md](references/flywheel-mcp-tool-map.md).
|
|
@@ -69,12 +82,14 @@ Before planning, recover or establish these inputs:
|
|
|
69
82
|
6. Add graph structure sparingly.
|
|
70
83
|
- Promote only decision-relevant relationships to edges.
|
|
71
84
|
- Leave ordinary cross-links inside the markdown body.
|
|
72
|
-
7. Expand the frontier
|
|
85
|
+
7. Expand the frontier as a single selected path.
|
|
73
86
|
- Expand from the resolved frontier rather than mirroring the source corpus again.
|
|
74
|
-
- Depth `n` means plan `n` hops ahead from the currently resolved frontier. Default `n=1`.
|
|
75
|
-
- Width `k` means
|
|
76
|
-
-
|
|
77
|
-
-
|
|
87
|
+
- Depth `n` means plan `n` sequential hops ahead from the currently resolved frontier. Default `n=1`.
|
|
88
|
+
- Width `k` means evaluate up to `k` distinct non-redundant candidate directions at each hop. Default `k=1`.
|
|
89
|
+
- At each hop, stage the considered options in the step record, apply the selection criterion, and choose exactly one continuation.
|
|
90
|
+
- Only the chosen continuation advances to hop `i+1`; do not stage `k` parallel continuation branches for this mode.
|
|
91
|
+
- The staged graph shape for this mode is a single chain of up to `n` unresolved step nodes (shorter only when the terminal condition is met early).
|
|
92
|
+
- Each step node must record: the `k` considered options, the selection criterion used, and the chosen continuation.
|
|
78
93
|
8. Commit after the node snapshot is coherent with `flywheel_commit_node`.
|
|
79
94
|
- Commit resolved insight nodes only when they have non-empty `insights`.
|
|
80
95
|
- Commit resolved empirical nodes only when outcome and artifact rules are satisfied.
|
|
@@ -85,7 +100,7 @@ Before planning, recover or establish these inputs:
|
|
|
85
100
|
When the skill completes a pass, it should leave behind:
|
|
86
101
|
|
|
87
102
|
- One persisted `insight` control node containing the canonical planning contract.
|
|
88
|
-
-
|
|
103
|
+
- A single staged next-step chain up to the current `n` limit where each hop records up to `k` considered options and one selected continuation.
|
|
89
104
|
- Updated node summaries, rationale, and edges only where the current planning pass justifies them.
|
|
90
105
|
- No hidden execution, no compute approval, and no managed compute acquisition.
|
|
91
106
|
|
|
@@ -114,7 +129,7 @@ Canonical contract shape:
|
|
|
114
129
|
- Decision criterion:
|
|
115
130
|
- Start nodes:
|
|
116
131
|
- Lookahead depth:
|
|
117
|
-
-
|
|
132
|
+
- Step option width:
|
|
118
133
|
- Terminal condition:
|
|
119
134
|
- Budget ceiling: optional
|
|
120
135
|
- Budget unit: optional
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"id": "planning-only-with-explicit-depth-and-width",
|
|
16
|
-
"prompt": "Use $flywheel-lookahead to stage two hops of next-step work
|
|
17
|
-
"expected_output": "Persist `n=2` and `k=3` in the planning contract,
|
|
16
|
+
"prompt": "Use $flywheel-lookahead to stage two sequential hops of next-step work while evaluating up to three non-redundant options at each hop from the current Flywheel frontier. Plan only for later work and do not execute anything.",
|
|
17
|
+
"expected_output": "Persist `n=2` and `k=3` in the planning contract, evaluate up to three non-redundant options at each hop, record the considered options plus selection criterion plus chosen continuation for each step node, continue only along the single chosen path, and avoid compute approval, managed compute acquisition, or execution launch.",
|
|
18
18
|
"files": []
|
|
19
19
|
}
|
|
20
20
|
]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Canonical reference for Flywheel user-facing interfaces and public contracts.
|
|
4
4
|
|
|
5
|
-
Last updated: 2026-
|
|
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
|
|
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/
|
|
73
|
-
- `GET /mcp/nodes/{node_id}/
|
|
74
|
-
- `
|
|
75
|
-
- `
|
|
76
|
-
- `
|
|
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-
|
|
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
|
|
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/
|
|
73
|
-
- `GET /mcp/nodes/{node_id}/
|
|
74
|
-
- `
|
|
75
|
-
- `
|
|
76
|
-
- `
|
|
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-
|
|
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
|
|
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/
|
|
73
|
-
- `GET /mcp/nodes/{node_id}/
|
|
74
|
-
- `
|
|
75
|
-
- `
|
|
76
|
-
- `
|
|
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
|
|
911
|
-
const
|
|
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
|
-
|
|
1016
|
+
mcpSpinner.text = `Removing from ${getHost(host).displayName} (${singleScope})...`;
|
|
915
1017
|
// eslint-disable-next-line no-await-in-loop
|
|
916
|
-
|
|
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
|
|
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
|
|
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")
|
package/src/skill-installer.mjs
CHANGED
|
@@ -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,
|