@paradigma-inc/flywheel 0.1.74 → 0.1.78

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 CHANGED
@@ -57,6 +57,35 @@ Configure your AI host (Claude Code, Codex, etc.) to call Flywheel over MCP:
57
57
  npx --yes @paradigma-inc/flywheel@latest setup --mode mcp --install-skill
58
58
  ```
59
59
 
60
+ ## Shell Completion
61
+
62
+ `flywheel completion <shell>` generates shell completion scripts from the public
63
+ Flywheel command spec. It requires the official `usage` executable on `PATH`;
64
+ install it with one of:
65
+
66
+ ```bash
67
+ mise use -g usage
68
+ brew install usage
69
+ cargo install usage-cli
70
+ cargo binstall usage-cli
71
+ pacman -S usage
72
+ ```
73
+
74
+ Generate the script for your shell and redirect it wherever your shell setup
75
+ loads completions:
76
+
77
+ ```bash
78
+ flywheel completion bash --include-bash-completion-lib > ~/.local/share/bash-completion/completions/flywheel
79
+ flywheel completion zsh > ~/.zsh/completions/_flywheel
80
+ flywheel completion fish > ~/.config/fish/completions/flywheel.fish
81
+ flywheel completion powershell > flywheel.ps1
82
+ flywheel completion nu > ~/.config/nushell/autoload/flywheel.nu
83
+ flywheel completion nushell > ~/.config/nushell/autoload/flywheel.nu
84
+ ```
85
+
86
+ This command generates scripts only. It does not install completions. It does
87
+ nothing to shell startup files and does not edit shell profiles.
88
+
60
89
  ## Uninstall
61
90
 
62
91
  One-shot uninstaller (macOS and Linux):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paradigma-inc/flywheel",
3
- "version": "0.1.74",
3
+ "version": "0.1.78",
4
4
  "description": "One-command setup for Flywheel MCP hosts",
5
5
  "type": "module",
6
6
  "files": [
@@ -93,7 +93,8 @@ Hook workflow-if contract (first version):
93
93
  - Workflow-if operators: `all`, `any`, `not`, `event`, `any_artifact`; predicate operators: `eq`, `in`, `exists`.
94
94
  - Canonical none-artifact expression: `not: { any_artifact: ... }`.
95
95
  - Run cardinality is fixed at one run per `(hook_id, event_id)` (no artifact fanout).
96
- - Submission-only matching is typically `any_artifact.field=metadata.role` with `eq: submission`.
96
+ - Submission-only matching is typically `any_artifact.field=metadata.campaign_role` with `eq: submission`.
97
+ - Campaign submission artifacts are valid only on public attempt nodes; private and unlisted submissions do not count in campaign snapshots or dispatch submission hook runs.
97
98
  - Workflow step types include `flywheel/http_request@v1`, `flywheel/http_poll@v1`, `flywheel/json_extract@v1`, `flywheel/load_artifact@v1`, `flywheel/upsert_artifact@v1`, and `flywheel/add_node_tags@v1`.
98
99
  - `flywheel/upsert_artifact@v1` selectors are authored under `with.match`; `match.metadata` is subset matching and ambiguous selectors fail terminally.
99
100
  - `flywheel/add_node_tags@v1` adds ordinary root graph tags to a target node and preserves existing tag assignments; one-only tags are rejected in v1.
package/src/cli.mjs CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  isSkillOnlyHost,
16
16
  normalizeHosts,
17
17
  } from "./agents.mjs";
18
+ import { generateCompletionScript } from "./completion.mjs";
18
19
  import {
19
20
  readEntryServerUrl,
20
21
  readNamedConfigEntry,
@@ -45,6 +46,7 @@ import {
45
46
  buildGuidedSkillInstallPrompt,
46
47
  runMcpModeCommand,
47
48
  } from "./setup/modes/mcp-command.mjs";
49
+ import { COMPLETION_SHELL_CHOICES } from "./public-command-metadata.mjs";
48
50
  import { findFlywheelMcpEntryNames } from "./setup/shared/prior-mode-detect.mjs";
49
51
  import { renderSetupSummary } from "./setup/shared/summary.mjs";
50
52
 
@@ -475,6 +477,20 @@ async function runSetupCommandViaRouter(options) {
475
477
  });
476
478
  }
477
479
 
480
+ async function runCompletionCommand(shell, options) {
481
+ try {
482
+ const script = await generateCompletionScript({
483
+ shell,
484
+ includeBashCompletionLib: options.includeBashCompletionLib === true,
485
+ });
486
+ process.stdout.write(script);
487
+ } catch (error) {
488
+ const message = error instanceof Error ? error.message : String(error);
489
+ process.stderr.write(`${message}\n`);
490
+ process.exitCode = 1;
491
+ }
492
+ }
493
+
478
494
  function parseUninstallScope(value) {
479
495
  const normalized = String(value || "")
480
496
  .trim()
@@ -1146,6 +1162,45 @@ Examples:
1146
1162
  `,
1147
1163
  );
1148
1164
 
1165
+ program
1166
+ .command("completion <shell>")
1167
+ .description("Generate a shell completion script")
1168
+ .addHelpText(
1169
+ "before",
1170
+ `${pc.bold(pc.cyan("✻ Flywheel Completion"))}
1171
+
1172
+ Generate shell completion scripts from the public Flywheel Usage spec.
1173
+ Supported shells: ${COMPLETION_SHELL_CHOICES.join(", ")}.
1174
+ Requires the official Usage CLI executable on PATH.
1175
+ `,
1176
+ )
1177
+ .option(
1178
+ "--include-bash-completion-lib",
1179
+ "Include Usage's bash completion library in bash output",
1180
+ )
1181
+ .addHelpText(
1182
+ "after",
1183
+ `
1184
+ Usage CLI prerequisite:
1185
+ ${pc.green("mise use -g usage")}
1186
+ ${pc.green("brew install usage")}
1187
+ ${pc.green("cargo install usage-cli")}
1188
+ ${pc.green("cargo binstall usage-cli")}
1189
+ ${pc.green("pacman -S usage")}
1190
+
1191
+ Examples:
1192
+ ${pc.green("flywheel completion bash --include-bash-completion-lib > ~/.local/share/bash-completion/completions/flywheel")}
1193
+ ${pc.green("flywheel completion zsh > ~/.zsh/completions/_flywheel")}
1194
+ ${pc.green("flywheel completion fish > ~/.config/fish/completions/flywheel.fish")}
1195
+ ${pc.green("flywheel completion powershell > flywheel.ps1")}
1196
+ ${pc.green("flywheel completion nu > ~/.config/nushell/autoload/flywheel.nu")}
1197
+ ${pc.green("flywheel completion nushell > ~/.config/nushell/autoload/flywheel.nu")}
1198
+ `,
1199
+ )
1200
+ .action(async (shell, options) => {
1201
+ await runCompletionCommand(shell, options);
1202
+ });
1203
+
1149
1204
  program
1150
1205
  .command("setup")
1151
1206
  .description("Set up Flywheel for your AI coding host")
@@ -0,0 +1,326 @@
1
+ import { spawn } from "node:child_process";
2
+ import { accessSync, constants } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ loadPackageVersion,
7
+ usageExtrasArg,
8
+ usageVersionArg,
9
+ } from "./public-command-metadata.mjs";
10
+ import { runRuntimeCli } from "./runtime/delegate.mjs";
11
+
12
+ export const SUPPORTED_COMPLETION_SHELLS = [
13
+ "bash",
14
+ "zsh",
15
+ "fish",
16
+ "powershell",
17
+ "nu",
18
+ "nushell",
19
+ ];
20
+
21
+ const DEFAULT_WINDOWS_PATHEXT = ".COM;.EXE;.BAT;.CMD";
22
+
23
+ const SUPPORTED_COMPLETION_SHELLS_FOR_ERRORS = [
24
+ "bash",
25
+ "fish",
26
+ "nu",
27
+ "nushell",
28
+ "powershell",
29
+ "zsh",
30
+ ];
31
+
32
+ const USAGE_INSTALL_HINTS = [
33
+ "mise use -g usage",
34
+ "brew install usage",
35
+ "cargo install usage-cli",
36
+ "cargo binstall usage-cli",
37
+ "pacman -S usage",
38
+ ];
39
+
40
+ const DEFAULT_USAGE_BIN = "usage";
41
+ const MAX_COMPLETION_BUFFER_BYTES = 50 * 1024 * 1024;
42
+
43
+ function supportedShellList() {
44
+ return SUPPORTED_COMPLETION_SHELLS_FOR_ERRORS.join(", ");
45
+ }
46
+
47
+ function missingUsageMessage(name) {
48
+ return [
49
+ `official Usage CLI not found or not executable: ${name}`,
50
+ "Install usage with one of:",
51
+ ...USAGE_INSTALL_HINTS.map((hint) => ` ${hint}`),
52
+ ].join("\n");
53
+ }
54
+
55
+ function isExecutable(filePath) {
56
+ try {
57
+ accessSync(filePath, constants.X_OK);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ function hasPathSeparator(value) {
65
+ return (
66
+ value.includes(path.sep) ||
67
+ value.includes(path.posix.sep) ||
68
+ value.includes(path.win32.sep)
69
+ );
70
+ }
71
+
72
+ function pathextCandidates(env, platform) {
73
+ const useWindowsDefaultPathext =
74
+ platform === "win32" && String(env.PATHEXT ?? "").trim() === "";
75
+ const rawPathext =
76
+ useWindowsDefaultPathext
77
+ ? DEFAULT_WINDOWS_PATHEXT
78
+ : String(env.PATHEXT ?? "");
79
+ const candidates = [];
80
+ const seen = new Set();
81
+ for (const rawExtension of rawPathext.split(";")) {
82
+ const trimmed = rawExtension.trim();
83
+ if (!trimmed) continue;
84
+ const extension = trimmed.startsWith(".") ? trimmed : `.${trimmed}`;
85
+ const variants = useWindowsDefaultPathext
86
+ ? [extension.toLowerCase(), extension, extension.toUpperCase()]
87
+ : [extension, extension.toLowerCase(), extension.toUpperCase()];
88
+ for (const candidate of variants) {
89
+ if (!seen.has(candidate)) {
90
+ candidates.push(candidate);
91
+ seen.add(candidate);
92
+ }
93
+ }
94
+ }
95
+ return candidates;
96
+ }
97
+
98
+ function executableNameCandidates(name, env, platform = process.platform) {
99
+ const candidates = [name];
100
+ const lowerName = name.toLowerCase();
101
+ const seen = new Set(candidates);
102
+ for (const extension of pathextCandidates(env, platform)) {
103
+ if (lowerName.endsWith(extension.toLowerCase())) continue;
104
+ const candidate = `${name}${extension}`;
105
+ if (!seen.has(candidate)) {
106
+ candidates.push(candidate);
107
+ seen.add(candidate);
108
+ }
109
+ }
110
+ return candidates;
111
+ }
112
+
113
+ export function sanitizeUsageEnv(env = process.env) {
114
+ const sanitized = {};
115
+ for (const [key, value] of Object.entries(env)) {
116
+ if (key.toUpperCase().startsWith("FLYWHEEL_")) continue;
117
+ if (value !== undefined) {
118
+ sanitized[key] = value;
119
+ }
120
+ }
121
+ return sanitized;
122
+ }
123
+
124
+ function isWindowsCommandShim(filePath, platform) {
125
+ return (
126
+ platform === "win32" &&
127
+ [".bat", ".cmd"].includes(path.extname(filePath).toLowerCase())
128
+ );
129
+ }
130
+
131
+ function quoteWindowsCommandPath(filePath) {
132
+ return `"${String(filePath).replaceAll('"', '""')}"`;
133
+ }
134
+
135
+ function resolveWindowsCommandShell(env) {
136
+ return env.ComSpec || env.COMSPEC || "cmd.exe";
137
+ }
138
+
139
+ function buildUsageInvocation({ usagePath, args, env, platform }) {
140
+ if (!isWindowsCommandShim(usagePath, platform)) {
141
+ return { command: usagePath, args };
142
+ }
143
+ const commandLine = `"${[quoteWindowsCommandPath(usagePath), ...args].join(" ")}"`;
144
+ return {
145
+ command: resolveWindowsCommandShell(env),
146
+ args: ["/d", "/s", "/c", commandLine],
147
+ };
148
+ }
149
+
150
+ export function normalizeCompletionShell(value) {
151
+ if (value === "nushell") {
152
+ return "nu";
153
+ }
154
+ if (SUPPORTED_COMPLETION_SHELLS.includes(value)) {
155
+ return value;
156
+ }
157
+ throw new Error(
158
+ `unsupported completion shell "${value}". Supported shells: ${supportedShellList()}`,
159
+ );
160
+ }
161
+
162
+ export function buildUsageCompletionArgs({
163
+ shell,
164
+ includeBashCompletionLib = false,
165
+ }) {
166
+ const normalizedShell = normalizeCompletionShell(shell);
167
+ if (includeBashCompletionLib && normalizedShell !== "bash") {
168
+ throw new Error("--include-bash-completion-lib is only supported for bash");
169
+ }
170
+ const args = ["generate", "completion", normalizedShell, "flywheel", "-f", "-"];
171
+ if (includeBashCompletionLib) {
172
+ args.push("--include-bash-completion-lib");
173
+ }
174
+ return args;
175
+ }
176
+
177
+ export function findExecutable(
178
+ name,
179
+ env = process.env,
180
+ platform = process.platform,
181
+ ) {
182
+ if (!name) {
183
+ return null;
184
+ }
185
+ if (path.isAbsolute(name) || hasPathSeparator(name)) {
186
+ return isExecutable(name) ? name : null;
187
+ }
188
+ const delimiter = platform === "win32" ? ";" : path.delimiter;
189
+ for (const entry of String(env.PATH ?? "").split(delimiter)) {
190
+ if (!entry) continue;
191
+ for (const candidateName of executableNameCandidates(name, env, platform)) {
192
+ const candidate = path.join(entry, candidateName);
193
+ if (isExecutable(candidate)) {
194
+ return candidate;
195
+ }
196
+ }
197
+ }
198
+ return null;
199
+ }
200
+
201
+ async function capturePublicUsageSpec() {
202
+ let stdout = "";
203
+ let stderr = "";
204
+ const version = await loadPackageVersion();
205
+ const exitCode = await runRuntimeCli([
206
+ "--usage",
207
+ usageExtrasArg(),
208
+ usageVersionArg(version),
209
+ ], {
210
+ stdout: (chunk) => {
211
+ stdout += chunk instanceof Uint8Array ? Buffer.from(chunk).toString("utf8") : chunk;
212
+ },
213
+ stderr: (chunk) => {
214
+ stderr += chunk instanceof Uint8Array ? Buffer.from(chunk).toString("utf8") : chunk;
215
+ },
216
+ });
217
+ if (exitCode !== 0) {
218
+ const detail = stderr.trim() ? `: ${stderr.trim()}` : "";
219
+ throw new Error(`failed to capture Flywheel Usage spec${detail}`);
220
+ }
221
+ return stdout;
222
+ }
223
+
224
+ function runUsageCompletion({ command, args, usageSpec, env }) {
225
+ return awaitChildProcess({
226
+ child: spawn(command, args, {
227
+ env: sanitizeUsageEnv(env),
228
+ stdio: ["pipe", "pipe", "pipe"],
229
+ }),
230
+ input: usageSpec,
231
+ });
232
+ }
233
+
234
+ function awaitChildProcess({ child, input }) {
235
+ return new Promise((resolve, reject) => {
236
+ let stdout = "";
237
+ let stderr = "";
238
+ let settled = false;
239
+ let stdinError = null;
240
+
241
+ function settle(callback, value) {
242
+ if (settled) return;
243
+ settled = true;
244
+ callback(value);
245
+ }
246
+
247
+ function append(streamName, chunk) {
248
+ if (streamName === "stdout") {
249
+ stdout += chunk.toString("utf8");
250
+ if (Buffer.byteLength(stdout, "utf8") > MAX_COMPLETION_BUFFER_BYTES) {
251
+ child.kill();
252
+ settle(reject, new Error("usage completion stdout exceeded 50 MiB"));
253
+ }
254
+ } else {
255
+ stderr += chunk.toString("utf8");
256
+ if (Buffer.byteLength(stderr, "utf8") > MAX_COMPLETION_BUFFER_BYTES) {
257
+ child.kill();
258
+ settle(reject, new Error("usage completion stderr exceeded 50 MiB"));
259
+ }
260
+ }
261
+ }
262
+
263
+ child.stdout.on("data", (chunk) => append("stdout", chunk));
264
+ child.stderr.on("data", (chunk) => append("stderr", chunk));
265
+ child.stdin.on("error", (error) => {
266
+ if (error.code === "EPIPE") {
267
+ stdinError = error;
268
+ return;
269
+ }
270
+ child.kill();
271
+ settle(reject, error);
272
+ });
273
+ child.on("error", (error) => settle(reject, error));
274
+ child.on("close", (code, signal) => {
275
+ if (settled) return;
276
+ if (code === 0) {
277
+ if (stdinError) {
278
+ settle(
279
+ reject,
280
+ new Error(
281
+ `usage stdin closed before Flywheel Usage spec was written: ${stdinError.message}`,
282
+ ),
283
+ );
284
+ return;
285
+ }
286
+ settle(resolve, stdout);
287
+ return;
288
+ }
289
+ const detail = stderr.trim() ? `\n${stderr.trim()}` : "";
290
+ const status = signal ? `signal ${signal}` : `exit code ${code}`;
291
+ settle(reject, new Error(`usage generate completion failed with ${status}.${detail}`));
292
+ });
293
+ child.stdin.end(input);
294
+ });
295
+ }
296
+
297
+ export async function generateCompletionScript({
298
+ shell,
299
+ includeBashCompletionLib = false,
300
+ usageBin,
301
+ env = process.env,
302
+ platform = process.platform,
303
+ usageSpec,
304
+ captureUsageSpec = capturePublicUsageSpec,
305
+ } = {}) {
306
+ const args = buildUsageCompletionArgs({ shell, includeBashCompletionLib });
307
+ const usageName = usageBin ?? env.USAGE_BIN ?? DEFAULT_USAGE_BIN;
308
+ const usagePath = findExecutable(usageName, env, platform);
309
+ if (!usagePath) {
310
+ throw new Error(missingUsageMessage(usageName));
311
+ }
312
+
313
+ const spec = usageSpec ?? (await captureUsageSpec());
314
+ const usageInvocation = buildUsageInvocation({
315
+ usagePath,
316
+ args,
317
+ env,
318
+ platform,
319
+ });
320
+ return await runUsageCompletion({
321
+ command: usageInvocation.command,
322
+ args: usageInvocation.args,
323
+ usageSpec: spec,
324
+ env,
325
+ });
326
+ }
@@ -0,0 +1,240 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const PACKAGE_JSON_PATH = path.resolve(__dirname, "..", "package.json");
7
+
8
+ export const COMPLETION_SHELL_CHOICES = [
9
+ "bash",
10
+ "zsh",
11
+ "fish",
12
+ "powershell",
13
+ "nu",
14
+ "nushell",
15
+ ];
16
+
17
+ export const HELP_EXTRAS = [
18
+ {
19
+ group: "setup-admin",
20
+ name: "completion",
21
+ summary: "Generate shell completion scripts.",
22
+ },
23
+ {
24
+ group: "setup-admin",
25
+ name: "setup",
26
+ summary: "Install Flywheel CLI + MCP + skills.",
27
+ },
28
+ {
29
+ group: "setup-admin",
30
+ name: "uninstall",
31
+ summary: "Uninstall Flywheel artifacts.",
32
+ },
33
+ ];
34
+
35
+ export const USAGE_EXTRAS = [
36
+ {
37
+ group: "setup-admin",
38
+ name: "completion",
39
+ summary: "Generate a shell completion script from the public Flywheel Usage spec",
40
+ endpoints: [],
41
+ args: [
42
+ {
43
+ name: "shell",
44
+ type: "string",
45
+ description: "Shell to generate completions for",
46
+ choices: COMPLETION_SHELL_CHOICES,
47
+ },
48
+ ],
49
+ required: [],
50
+ optional: [
51
+ {
52
+ name: "include-bash-completion-lib",
53
+ type: "boolean",
54
+ description: "Include Usage's bash completion library in bash output",
55
+ },
56
+ ],
57
+ examples: [
58
+ {
59
+ description: "Generate bash completions",
60
+ invocation: "flywheel completion bash",
61
+ },
62
+ {
63
+ description: "Generate zsh completions",
64
+ invocation: "flywheel completion zsh",
65
+ },
66
+ {
67
+ description: "Generate Nushell completions with the alias",
68
+ invocation: "flywheel completion nushell",
69
+ },
70
+ ],
71
+ related: ["setup", "uninstall"],
72
+ },
73
+ {
74
+ group: "setup-admin",
75
+ name: "setup",
76
+ summary: "Set up Flywheel for your AI coding host",
77
+ endpoints: [],
78
+ required: [],
79
+ optional: [
80
+ { name: "claude", type: "boolean", description: "Set up for Claude Code" },
81
+ { name: "cursor", type: "boolean", description: "Set up for Cursor" },
82
+ { name: "universal", type: "boolean", description: "Set up for Universal (.agents/skills)" },
83
+ { name: "antigravity", type: "boolean", description: "Set up for Antigravity (.agent/skills)" },
84
+ { name: "opencode", type: "boolean", description: "Set up for OpenCode" },
85
+ { name: "codex", type: "boolean", description: "Set up for Codex" },
86
+ { name: "gemini", type: "boolean", description: "Set up for Gemini CLI" },
87
+ { name: "pi-mono", type: "boolean", description: "Set up for Pi (pi-mono)" },
88
+ { name: "hermes-agent", type: "boolean", description: "Set up for Hermes Agent" },
89
+ { name: "openclaw", type: "boolean", description: "Set up for OpenClaw" },
90
+ {
91
+ name: "mode",
92
+ type: "string",
93
+ usage: "--mode <MODE>",
94
+ description: "Install mode: mcp | cli",
95
+ choices: ["mcp", "cli"],
96
+ },
97
+ { name: "mcp", type: "boolean", description: "Alias for --mode mcp" },
98
+ { name: "cli", type: "boolean", description: "Alias for --mode cli" },
99
+ {
100
+ name: "project",
101
+ type: "boolean",
102
+ usage: "-p --project",
103
+ description: "Configure for current project instead of globally",
104
+ },
105
+ {
106
+ name: "yes",
107
+ type: "boolean",
108
+ usage: "-y --yes",
109
+ description: "Skip confirmation prompts",
110
+ },
111
+ {
112
+ name: "api-key",
113
+ type: "string",
114
+ usage: "--api-key <KEY>",
115
+ description: "Use API key authentication",
116
+ },
117
+ { name: "oauth", type: "boolean", description: "Use OAuth endpoint (IDE handles auth flow)" },
118
+ { name: "install-skill", type: "boolean", description: "Install or refresh the bundled Flywheel skills" },
119
+ { name: "skip-skill", type: "boolean", description: "Skip bundled skill installation (MCP-only setup)" },
120
+ {
121
+ name: "auth-mode",
122
+ type: "string",
123
+ usage: "--auth-mode <MODE>",
124
+ description: "API-key mint flow: auto | loopback | device",
125
+ default: "auto",
126
+ choices: ["auto", "loopback", "device"],
127
+ },
128
+ {
129
+ name: "base-url",
130
+ type: "string",
131
+ usage: "--base-url <URL>",
132
+ description: "Public Flywheel origin used for setup and MCP config",
133
+ },
134
+ { name: "name", type: "string", usage: "--name <NAME>", description: "MCP server name" },
135
+ { name: "force", type: "boolean", description: "Reconcile prior install mode automatically via uninstall" },
136
+ ],
137
+ examples: [
138
+ {
139
+ description: "Install MCP wiring and bundled skills",
140
+ invocation: "npx --yes @paradigma-inc/flywheel setup --mode mcp --install-skill",
141
+ },
142
+ {
143
+ description: "Configure Codex MCP wiring without bundled skills",
144
+ invocation: "npx --yes @paradigma-inc/flywheel setup --mode mcp --skip-skill --codex --project",
145
+ },
146
+ {
147
+ description: "Install CLI-mode bundled skills for Codex",
148
+ invocation: "npx --yes @paradigma-inc/flywheel setup --mode cli --codex --project",
149
+ },
150
+ ],
151
+ related: ["uninstall"],
152
+ },
153
+ {
154
+ group: "setup-admin",
155
+ name: "uninstall",
156
+ summary: "Remove Flywheel MCP entries and bundled skills from selected hosts",
157
+ endpoints: [],
158
+ required: [],
159
+ optional: [
160
+ { name: "claude", type: "boolean", description: "Uninstall for Claude Code" },
161
+ { name: "cursor", type: "boolean", description: "Uninstall for Cursor" },
162
+ { name: "universal", type: "boolean", description: "Uninstall bundled skills from .agents/skills" },
163
+ { name: "antigravity", type: "boolean", description: "Uninstall bundled skills from .agent/skills" },
164
+ { name: "opencode", type: "boolean", description: "Uninstall for OpenCode" },
165
+ { name: "codex", type: "boolean", description: "Uninstall for Codex" },
166
+ { name: "gemini", type: "boolean", description: "Uninstall for Gemini CLI" },
167
+ { name: "pi-mono", type: "boolean", description: "Uninstall for Pi (pi-mono)" },
168
+ { name: "hermes-agent", type: "boolean", description: "Uninstall for Hermes Agent" },
169
+ { name: "openclaw", type: "boolean", description: "Uninstall for OpenClaw" },
170
+ {
171
+ name: "hosts",
172
+ type: "string",
173
+ usage: "--hosts <LIST>",
174
+ description: "Comma-separated host list",
175
+ },
176
+ {
177
+ name: "scope",
178
+ type: "string",
179
+ usage: "--scope <SCOPE>",
180
+ description: "all | global | project",
181
+ choices: ["all", "global", "project"],
182
+ },
183
+ {
184
+ name: "name",
185
+ type: "string",
186
+ usage: "--name <NAME>",
187
+ description: "MCP server name to remove",
188
+ },
189
+ {
190
+ name: "yes",
191
+ type: "boolean",
192
+ usage: "-y --yes",
193
+ description: "Skip uninstall selection prompts",
194
+ },
195
+ ],
196
+ examples: [
197
+ {
198
+ description: "Uninstall Codex project-scope artifacts",
199
+ invocation: "npx --yes @paradigma-inc/flywheel uninstall --codex --scope project",
200
+ },
201
+ {
202
+ description: "Uninstall selected hosts across all scopes",
203
+ invocation: "npx --yes @paradigma-inc/flywheel uninstall --hosts claude,codex --scope all --yes",
204
+ },
205
+ ],
206
+ related: ["setup"],
207
+ },
208
+ ];
209
+
210
+ export function helpExtrasArg() {
211
+ return `--_help_extras=${JSON.stringify(HELP_EXTRAS)}`;
212
+ }
213
+
214
+ export function usageExtrasArg() {
215
+ return `--_usage_extras=${JSON.stringify(USAGE_EXTRAS)}`;
216
+ }
217
+
218
+ export function usageVersionArg(version) {
219
+ return `--_usage_version=${version}`;
220
+ }
221
+
222
+ let cachedPackageVersion = null;
223
+
224
+ export async function loadPackageVersion() {
225
+ if (cachedPackageVersion !== null) {
226
+ return cachedPackageVersion;
227
+ }
228
+ try {
229
+ const raw = await readFile(PACKAGE_JSON_PATH, "utf8");
230
+ const parsed = JSON.parse(raw);
231
+ if (typeof parsed.version === "string" && parsed.version.trim()) {
232
+ cachedPackageVersion = parsed.version.trim();
233
+ return cachedPackageVersion;
234
+ }
235
+ } catch {
236
+ // Fall through to deterministic fallback.
237
+ }
238
+ cachedPackageVersion = "0.0.0";
239
+ return cachedPackageVersion;
240
+ }