@paradigma-inc/flywheel 0.1.0 → 0.1.4

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
@@ -1,12 +1,6 @@
1
1
  # flywheel setup CLI
2
2
 
3
- This package provides a Context7-style setup workflow for Flywheel MCP.
4
-
5
- Design intentionally mirrors Context7's setup implementation patterns from:
6
-
7
- - `packages/cli/src/commands/setup.ts`
8
- - `packages/cli/src/setup/agents.ts`
9
- - `packages/cli/src/setup/mcp-writer.ts`
3
+ This package provides setup and uninstall workflows for Flywheel MCP.
10
4
 
11
5
  ## Usage
12
6
 
@@ -15,44 +9,31 @@ npx @paradigma-inc/flywheel setup
15
9
  npx @paradigma-inc/flywheel uninstall
16
10
  ```
17
11
 
18
- ## Production vs staging
19
-
20
- - `npx @paradigma-inc/flywheel setup` defaults to production
21
- (`https://flywheel.paradigma.inc`).
22
- - To target staging explicitly, pass a staging base URL:
12
+ `--base-url` must be the public Flywheel origin, not a backend URL or an `/api`
13
+ path. Example:
23
14
 
24
15
  ```bash
25
- npx @paradigma-inc/flywheel setup --base-url https://flywheel-staging.paradigma.inc
16
+ npx @paradigma-inc/flywheel@staging setup \
17
+ --name flywheel-staging \
18
+ --base-url https://flywheel-staging.paradigma.inc
26
19
  ```
27
20
 
28
- - Staging remains protected by the existing staging password gate in the WebUI.
29
- The browser auth step must pass that gate before key creation can complete.
30
- - Setup writes one Flywheel server entry per host+scope. Re-running setup for a
31
- different base URL in the same host+scope updates that same entry.
32
-
33
21
  ## Commands
34
22
 
35
23
  - `setup`: interview + browser auth bridge + idempotent host config writes.
36
24
  - `uninstall`: remove Flywheel MCP entries from selected host configs.
37
-
38
- ## Release channels
39
-
40
- - Public production installs use npm (`latest`):
41
-
42
- ```bash
43
- npx @paradigma-inc/flywheel setup
44
- ```
45
-
46
- - Internal staging builds are published to GitHub Packages (`staging` tag) and
47
- require npm auth for `npm.pkg.github.com`.
48
- - Example internal install:
49
-
50
- ```bash
51
- npx --registry=https://npm.pkg.github.com @paradigma-inc/flywheel@staging setup
52
- ```
25
+ - Add `--yes` to skip interactive prompts in the Flywheel wizard.
26
+ - `npx --yes` only auto-confirms `npx`; it does not imply `flywheel --yes`.
53
27
 
54
28
  ## Supported hosts
55
29
 
56
30
  - Codex (`~/.codex/config.toml` or `.codex/config.toml`)
57
31
  - Claude Code (`~/.claude.json` or `.mcp.json`)
58
32
  - OpenCode (`~/.config/opencode/opencode.json` or `opencode.json`)
33
+ - Cursor (`~/.cursor/mcp.json` or `.cursor/mcp.json`)
34
+ - Hermes Agent (`~/.hermes/config.yaml` or `.hermes/config.yaml`)
35
+ - OpenClaw (`~/.openclaw/openclaw.json` or `.openclaw/openclaw.json`)
36
+ - Pi (pi-mono) (`~/.pi/agent/settings.json` or `.pi/settings.json`)
37
+
38
+ Pi (pi-mono) support writes `mcp.servers` into Pi settings. It requires an
39
+ MCP-capable Pi extension/package to consume those entries.
package/package.json CHANGED
@@ -1,14 +1,31 @@
1
1
  {
2
2
  "name": "@paradigma-inc/flywheel",
3
- "version": "0.1.0",
4
- "description": "One-command setup for Flywheel MCP on Codex, Claude Code, and OpenCode",
3
+ "version": "0.1.4",
4
+ "description": "One-command setup for Flywheel MCP hosts",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/paradigma-inc/paradigma",
8
+ "directory": "project/flywheel-setup"
9
+ },
5
10
  "type": "module",
11
+ "files": [
12
+ "bin/",
13
+ "src/",
14
+ "README.md"
15
+ ],
6
16
  "bin": {
7
17
  "flywheel": "bin/flywheel.js"
8
18
  },
9
19
  "scripts": {
10
20
  "test": "node --test tests/*.test.mjs"
11
21
  },
22
+ "dependencies": {
23
+ "@inquirer/prompts": "^8.2.0",
24
+ "commander": "^14.0.1",
25
+ "js-yaml": "^4.1.0",
26
+ "ora": "^9.0.0",
27
+ "picocolors": "^1.1.1"
28
+ },
12
29
  "engines": {
13
30
  "node": ">=20"
14
31
  },
package/src/agents.mjs CHANGED
@@ -1,97 +1,256 @@
1
+ import { access } from "node:fs/promises";
1
2
  import os from "node:os";
2
3
  import path from "node:path";
3
4
 
4
5
  export const SERVER_NAME = "flywheel";
5
- export const HOSTS = ["codex", "claude", "opencode"];
6
-
7
- export const HOST_LABELS = {
8
- codex: "Codex",
6
+ export const ALL_HOST_NAMES = [
7
+ "claude",
8
+ "opencode",
9
+ "codex",
10
+ "cursor",
11
+ "pi-mono",
12
+ "hermes-agent",
13
+ "openclaw",
14
+ ];
15
+ export const SETUP_HOST_NAMES = {
9
16
  claude: "Claude Code",
10
17
  opencode: "OpenCode",
18
+ codex: "Codex",
19
+ cursor: "Cursor",
20
+ "pi-mono": "Pi (pi-mono)",
21
+ "hermes-agent": "Hermes Agent",
22
+ openclaw: "OpenClaw",
11
23
  };
12
24
 
13
25
  const HOME = os.homedir();
14
26
 
15
- /**
16
- * Patterns mirrored from Context7's `setup/agents.ts`.
17
- */
18
- export const AGENT_CONFIG = {
19
- codex: {
20
- configType: "toml",
21
- projectPath: path.join(".codex", "config.toml"),
22
- globalPath: path.join(HOME, ".codex", "config.toml"),
23
- buildEntry: ({ serverUrl, apiKey }) => ({
24
- type: "http",
25
- url: serverUrl,
26
- headers: {
27
- Authorization: `Bearer ${apiKey}`,
28
- },
29
- }),
30
- },
27
+ const hosts = {
31
28
  claude: {
32
- configType: "json",
33
- projectPath: ".mcp.json",
34
- globalPath: path.join(HOME, ".claude.json"),
35
- configKey: "mcpServers",
36
- buildEntry: ({ serverUrl, apiKey }) => ({
37
- type: "http",
38
- url: serverUrl,
39
- headers: {
40
- Authorization: `Bearer ${apiKey}`,
41
- },
42
- }),
29
+ name: "claude",
30
+ displayName: "Claude Code",
31
+ mcp: {
32
+ configType: "json",
33
+ projectPaths: [".mcp.json"],
34
+ globalPaths: [path.join(HOME, ".claude.json")],
35
+ configKey: "mcpServers",
36
+ buildEntry: ({ serverUrl, apiKey }) => ({
37
+ type: "http",
38
+ url: serverUrl,
39
+ headers: {
40
+ Authorization: `Bearer ${apiKey}`,
41
+ },
42
+ }),
43
+ },
44
+ detect: {
45
+ projectPaths: [".mcp.json", ".claude"],
46
+ globalPaths: [
47
+ path.join(HOME, ".claude.json"),
48
+ path.join(HOME, ".claude"),
49
+ ],
50
+ },
43
51
  },
44
52
  opencode: {
45
- configType: "json",
46
- projectPath: "opencode.json",
47
- globalPath: path.join(HOME, ".config", "opencode", "opencode.json"),
48
- configKey: "mcp",
49
- buildEntry: ({ serverUrl, apiKey }) => ({
50
- type: "remote",
51
- url: serverUrl,
52
- enabled: true,
53
- headers: {
54
- Authorization: `Bearer ${apiKey}`,
55
- },
56
- }),
53
+ name: "opencode",
54
+ displayName: "OpenCode",
55
+ mcp: {
56
+ configType: "json",
57
+ projectPaths: [
58
+ "opencode.json",
59
+ "opencode.jsonc",
60
+ ".opencode.json",
61
+ ".opencode.jsonc",
62
+ ],
63
+ globalPaths: [
64
+ path.join(HOME, ".config", "opencode", "opencode.json"),
65
+ path.join(HOME, ".config", "opencode", "opencode.jsonc"),
66
+ path.join(HOME, ".config", "opencode", ".opencode.json"),
67
+ path.join(HOME, ".config", "opencode", ".opencode.jsonc"),
68
+ ],
69
+ configKey: "mcp",
70
+ buildEntry: ({ serverUrl, apiKey }) => ({
71
+ type: "remote",
72
+ url: serverUrl,
73
+ enabled: true,
74
+ headers: {
75
+ Authorization: `Bearer ${apiKey}`,
76
+ },
77
+ }),
78
+ },
79
+ detect: {
80
+ projectPaths: [
81
+ "opencode.json",
82
+ "opencode.jsonc",
83
+ ".opencode.json",
84
+ ".opencode.jsonc",
85
+ ],
86
+ globalPaths: [path.join(HOME, ".config", "opencode")],
87
+ },
88
+ },
89
+ codex: {
90
+ name: "codex",
91
+ displayName: "Codex",
92
+ mcp: {
93
+ configType: "toml",
94
+ projectPaths: [path.join(".codex", "config.toml")],
95
+ globalPaths: [path.join(HOME, ".codex", "config.toml")],
96
+ configKey: "mcp_servers",
97
+ buildEntry: ({ serverUrl, apiKey }) => ({
98
+ type: "http",
99
+ url: serverUrl,
100
+ headers: {
101
+ Authorization: `Bearer ${apiKey}`,
102
+ },
103
+ }),
104
+ },
105
+ detect: {
106
+ projectPaths: [".codex", path.join(".codex", "config.toml")],
107
+ globalPaths: [path.join(HOME, ".codex")],
108
+ },
109
+ },
110
+ cursor: {
111
+ name: "cursor",
112
+ displayName: "Cursor",
113
+ mcp: {
114
+ configType: "json",
115
+ projectPaths: [path.join(".cursor", "mcp.json")],
116
+ globalPaths: [path.join(HOME, ".cursor", "mcp.json")],
117
+ configKey: "mcpServers",
118
+ buildEntry: ({ serverUrl, apiKey }) => ({
119
+ url: serverUrl,
120
+ headers: {
121
+ Authorization: `Bearer ${apiKey}`,
122
+ },
123
+ }),
124
+ },
125
+ detect: {
126
+ projectPaths: [".cursor", path.join(".cursor", "mcp.json")],
127
+ globalPaths: [path.join(HOME, ".cursor")],
128
+ },
129
+ },
130
+ "pi-mono": {
131
+ name: "pi-mono",
132
+ displayName: "Pi (pi-mono)",
133
+ mcp: {
134
+ configType: "json",
135
+ projectPaths: [path.join(".pi", "settings.json")],
136
+ globalPaths: [path.join(HOME, ".pi", "agent", "settings.json")],
137
+ configKey: ["mcp", "servers"],
138
+ buildEntry: ({ serverUrl, apiKey }) => ({
139
+ url: serverUrl,
140
+ headers: {
141
+ Authorization: `Bearer ${apiKey}`,
142
+ },
143
+ }),
144
+ },
145
+ detect: {
146
+ projectPaths: [".pi", path.join(".pi", "settings.json")],
147
+ globalPaths: [path.join(HOME, ".pi"), path.join(HOME, ".pi", "agent")],
148
+ },
149
+ },
150
+ "hermes-agent": {
151
+ name: "hermes-agent",
152
+ displayName: "Hermes Agent",
153
+ mcp: {
154
+ configType: "yaml",
155
+ projectPaths: [path.join(".hermes", "config.yaml")],
156
+ globalPaths: [path.join(HOME, ".hermes", "config.yaml")],
157
+ configKey: "mcp_servers",
158
+ buildEntry: ({ serverUrl, apiKey }) => ({
159
+ url: serverUrl,
160
+ headers: {
161
+ Authorization: `Bearer ${apiKey}`,
162
+ },
163
+ }),
164
+ },
165
+ detect: {
166
+ projectPaths: [".hermes", path.join(".hermes", "config.yaml")],
167
+ globalPaths: [path.join(HOME, ".hermes"), path.join(HOME, ".hermes", "config.yaml")],
168
+ },
169
+ },
170
+ openclaw: {
171
+ name: "openclaw",
172
+ displayName: "OpenClaw",
173
+ mcp: {
174
+ configType: "json",
175
+ projectPaths: [path.join(".openclaw", "openclaw.json")],
176
+ globalPaths: [path.join(HOME, ".openclaw", "openclaw.json")],
177
+ configKey: ["mcp", "servers"],
178
+ buildEntry: ({ serverUrl, apiKey }) => ({
179
+ url: serverUrl,
180
+ headers: {
181
+ Authorization: `Bearer ${apiKey}`,
182
+ },
183
+ }),
184
+ },
185
+ detect: {
186
+ projectPaths: [".openclaw", path.join(".openclaw", "openclaw.json")],
187
+ globalPaths: [
188
+ path.join(HOME, ".openclaw"),
189
+ path.join(HOME, ".openclaw", "openclaw.json"),
190
+ ],
191
+ },
57
192
  },
58
193
  };
59
194
 
60
- export function normalizeScope(scope) {
61
- if (scope === "global" || scope === "project") return scope;
62
- return "global";
195
+ export function getHost(name) {
196
+ const host = hosts[name];
197
+ if (!host) {
198
+ throw new Error(`Unsupported host: ${name}`);
199
+ }
200
+ return host;
63
201
  }
64
202
 
65
- export function resolveHostConfigPath(host, scope) {
66
- const hostConfig = AGENT_CONFIG[host];
67
- if (!hostConfig) {
68
- throw new Error(`Unsupported host: ${host}`);
203
+ async function pathExists(filePath) {
204
+ try {
205
+ await access(filePath);
206
+ return true;
207
+ } catch {
208
+ return false;
69
209
  }
70
- if (scope === "project") {
71
- return path.join(process.cwd(), hostConfig.projectPath);
210
+ }
211
+
212
+ export async function detectHosts(scope) {
213
+ const detected = [];
214
+ for (const hostName of ALL_HOST_NAMES) {
215
+ const host = getHost(hostName);
216
+ const candidates =
217
+ scope === "global" ? host.detect.globalPaths : host.detect.projectPaths;
218
+ for (const candidate of candidates) {
219
+ const fullPath =
220
+ scope === "global" ? candidate : path.join(process.cwd(), candidate);
221
+ // eslint-disable-next-line no-await-in-loop
222
+ if (await pathExists(fullPath)) {
223
+ detected.push(hostName);
224
+ break;
225
+ }
226
+ }
72
227
  }
73
- return hostConfig.globalPath;
228
+ return detected;
229
+ }
230
+
231
+ export function normalizeScope(scope) {
232
+ if (scope === "global" || scope === "project") return scope;
233
+ return "global";
74
234
  }
75
235
 
76
236
  export function normalizeHosts(rawHosts) {
77
237
  if (!Array.isArray(rawHosts) || rawHosts.length === 0) {
78
- return [...HOSTS];
238
+ return [...ALL_HOST_NAMES];
79
239
  }
80
240
  const normalized = [];
81
241
  for (const host of rawHosts) {
82
- const candidate = String(host || "").trim().toLowerCase();
242
+ const candidate = String(host || "")
243
+ .trim()
244
+ .toLowerCase();
83
245
  if (!candidate) continue;
84
- if (!HOSTS.includes(candidate)) {
246
+ if (!ALL_HOST_NAMES.includes(candidate)) {
85
247
  throw new Error(
86
- `Unsupported host '${candidate}'. Supported: ${HOSTS.join(", ")}`,
248
+ `Unsupported host '${candidate}'. Supported: ${ALL_HOST_NAMES.join(", ")}`,
87
249
  );
88
250
  }
89
251
  if (!normalized.includes(candidate)) {
90
252
  normalized.push(candidate);
91
253
  }
92
254
  }
93
- if (normalized.length === 0) {
94
- return [...HOSTS];
95
- }
96
- return normalized;
255
+ return normalized.length > 0 ? normalized : [...ALL_HOST_NAMES];
97
256
  }