@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 +15 -34
- package/package.json +19 -2
- package/src/agents.mjs +220 -61
- package/src/cli.mjs +482 -330
- package/src/mcp-writer.mjs +171 -80
- package/src/setup-auth.mjs +90 -9
- package/tests/mcp-writer.test.mjs +0 -117
package/README.md
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
# flywheel setup CLI
|
|
2
2
|
|
|
3
|
-
This package provides
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "One-command setup for Flywheel MCP
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
203
|
+
async function pathExists(filePath) {
|
|
204
|
+
try {
|
|
205
|
+
await access(filePath);
|
|
206
|
+
return true;
|
|
207
|
+
} catch {
|
|
208
|
+
return false;
|
|
69
209
|
}
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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 [...
|
|
238
|
+
return [...ALL_HOST_NAMES];
|
|
79
239
|
}
|
|
80
240
|
const normalized = [];
|
|
81
241
|
for (const host of rawHosts) {
|
|
82
|
-
const candidate = String(host || "")
|
|
242
|
+
const candidate = String(host || "")
|
|
243
|
+
.trim()
|
|
244
|
+
.toLowerCase();
|
|
83
245
|
if (!candidate) continue;
|
|
84
|
-
if (!
|
|
246
|
+
if (!ALL_HOST_NAMES.includes(candidate)) {
|
|
85
247
|
throw new Error(
|
|
86
|
-
`Unsupported host '${candidate}'. Supported: ${
|
|
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
|
-
|
|
94
|
-
return [...HOSTS];
|
|
95
|
-
}
|
|
96
|
-
return normalized;
|
|
255
|
+
return normalized.length > 0 ? normalized : [...ALL_HOST_NAMES];
|
|
97
256
|
}
|