blackops-onboard 0.3.0 → 0.6.0

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.
Files changed (3) hide show
  1. package/README.md +14 -1
  2. package/bin/onboard.mjs +155 -48
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,12 +8,25 @@ Agent-first onboarding for [BlackOps Center](https://blackopscenter.com).
8
8
  npx blackops-onboard
9
9
  ```
10
10
 
11
- Then open Claude Code and say:
11
+ Then open your MCP-capable agent (Claude Code, Claude Desktop, Cursor, Cline, …) and say:
12
12
 
13
13
  > Help me onboard to BlackOps Center and publish my first piece of content.
14
14
 
15
15
  That's it. The agent walks you through account creation, brand voice, social OAuth (X / Threads / LinkedIn), publishing your first four pieces of content, and getting a magic link to your dashboard.
16
16
 
17
+ ## Supported MCP clients
18
+
19
+ The CLI auto-detects and writes the BlackOps MCP server entry to any of these configs it finds:
20
+
21
+ | Client | Config path |
22
+ |---|---|
23
+ | Claude Code | `~/.claude/settings.json` |
24
+ | Claude Desktop | macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` · Windows: `%APPDATA%\Claude\claude_desktop_config.json` · Linux: `~/.config/Claude/claude_desktop_config.json` |
25
+ | Cursor | `~/.cursor/mcp.json` |
26
+ | Cline (VS Code) | `~/.cline/mcp_settings.json` |
27
+
28
+ For any other MCP client, the CLI also prints a universal JSON snippet you can paste into that client's `mcpServers` config.
29
+
17
30
  ## Re-running
18
31
 
19
32
  Re-running detects an existing config at `~/.blackops/config.json`. You can keep the existing token (and re-paste the saved prompt) or overwrite to start fresh.
package/bin/onboard.mjs CHANGED
@@ -4,8 +4,9 @@
4
4
  // What this does:
5
5
  // 1. POSTs to https://blackopscenter.com/api/install to mint an onboarding token.
6
6
  // 2. Writes ~/.blackops/config.json with token + MCP URL.
7
- // 3. Adds a `blackops` MCP server entry to Claude Code's settings.json.
8
- // 4. Prints the onboarding prompt for the user to paste into Claude Code.
7
+ // 3. Detects installed MCP clients (Claude Code, Claude Desktop, Cursor, Cline)
8
+ // and adds a `blackops` MCP server entry to each one's config.
9
+ // 4. Always prints a universal copy-paste snippet as fallback for unknown clients.
9
10
  //
10
11
  // Re-runs are idempotent: an existing ~/.blackops/config.json will prompt the
11
12
  // user before overwriting.
@@ -15,7 +16,7 @@ import path from 'node:path'
15
16
  import os from 'node:os'
16
17
  import readline from 'node:readline/promises'
17
18
 
18
- const VERSION = '0.3.0'
19
+ const VERSION = '0.6.0'
19
20
  const INSTALL_URL = process.env.BLACKOPS_INSTALL_URL || 'https://blackopscenter.com/api/install'
20
21
  const CONFIG_DIR = path.join(os.homedir(), '.blackops')
21
22
  const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json')
@@ -41,18 +42,6 @@ async function confirm(question) {
41
42
  }
42
43
  }
43
44
 
44
- function findClaudeCodeSettingsPath() {
45
- // Claude Code stores user-level settings in ~/.claude/settings.json on macOS/Linux.
46
- // (The same file holds permissions, hooks, and MCP servers.)
47
- const candidates = [
48
- path.join(os.homedir(), '.claude', 'settings.json'),
49
- ]
50
- for (const p of candidates) {
51
- if (fs.existsSync(p)) return p
52
- }
53
- return candidates[0] // first candidate; we'll create it
54
- }
55
-
56
45
  function readJsonSafe(filePath) {
57
46
  try {
58
47
  const raw = fs.readFileSync(filePath, 'utf8')
@@ -69,6 +58,105 @@ function writeJsonAtomic(filePath, value) {
69
58
  fs.renameSync(tmp, filePath)
70
59
  }
71
60
 
61
+ // ── MCP client detection ────────────────────────────────────────────────
62
+ //
63
+ // Each entry is one MCP client we know how to install into. `path` is where
64
+ // the client stores its MCP config; `installed` is a heuristic that checks
65
+ // whether the client appears to be set up. `alwaysInstall` is true for
66
+ // Claude Code (the canonical target — we create the file if missing).
67
+ //
68
+ // All known clients use the same `mcpServers.<name>.{type,url,headers}`
69
+ // JSON shape, so a single writer covers them.
70
+
71
+ // `transport: 'http'` writes the native streamable HTTP entry that
72
+ // Claude Code and Cursor both understand:
73
+ // { type: 'http', url, headers: { Authorization: 'Bearer ...' } }
74
+ //
75
+ // `transport: 'stdio-bridge'` writes an `mcp-remote` stdio shim, used for
76
+ // Claude Desktop and Cline which only speak stdio MCP today:
77
+ // { command: 'npx', args: ['-y', 'mcp-remote', url, '--header', 'Authorization: Bearer ...'] }
78
+
79
+ function getClientConfigs(homedir, platform) {
80
+ const APPDATA = process.env.APPDATA || ''
81
+ return [
82
+ {
83
+ id: 'claude-code',
84
+ label: 'Claude Code',
85
+ path: path.join(homedir, '.claude', 'settings.json'),
86
+ transport: 'http',
87
+ alwaysInstall: true,
88
+ },
89
+ {
90
+ id: 'claude-desktop',
91
+ label: 'Claude Desktop',
92
+ path:
93
+ platform === 'darwin'
94
+ ? path.join(homedir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
95
+ : platform === 'win32'
96
+ ? path.join(APPDATA, 'Claude', 'claude_desktop_config.json')
97
+ : path.join(homedir, '.config', 'Claude', 'claude_desktop_config.json'),
98
+ transport: 'stdio-bridge',
99
+ },
100
+ {
101
+ id: 'cursor',
102
+ label: 'Cursor',
103
+ path: path.join(homedir, '.cursor', 'mcp.json'),
104
+ transport: 'http',
105
+ },
106
+ {
107
+ id: 'cline',
108
+ label: 'Cline (VS Code)',
109
+ path: path.join(homedir, '.cline', 'mcp_settings.json'),
110
+ transport: 'stdio-bridge',
111
+ },
112
+ ]
113
+ }
114
+
115
+ function buildServerEntry(transport, { mcpUrl, token }) {
116
+ if (transport === 'stdio-bridge') {
117
+ // --transport http-only avoids mcp-remote's SSE-fallback path (which has
118
+ // historically hung for several minutes on slow tool calls).
119
+ // --timeout 30000 caps any single request at 30s instead of the 4-minute
120
+ // default, so a stuck call surfaces as an error the agent can recover
121
+ // from instead of a silent hang.
122
+ return {
123
+ command: 'npx',
124
+ args: [
125
+ '-y',
126
+ 'mcp-remote',
127
+ mcpUrl,
128
+ '--header',
129
+ `Authorization: Bearer ${token}`,
130
+ '--transport',
131
+ 'http-only',
132
+ '--timeout',
133
+ '30000',
134
+ ],
135
+ }
136
+ }
137
+ return {
138
+ type: 'http',
139
+ url: mcpUrl,
140
+ headers: { Authorization: `Bearer ${token}` },
141
+ }
142
+ }
143
+
144
+ function clientIsInstalled(cfg) {
145
+ if (cfg.alwaysInstall) return true
146
+ // File exists OR its parent dir exists (covers cases where the user
147
+ // has the app installed but hasn't created an MCP config file yet).
148
+ if (fs.existsSync(cfg.path)) return true
149
+ if (fs.existsSync(path.dirname(cfg.path))) return true
150
+ return false
151
+ }
152
+
153
+ function injectMcpServer(cfg, { mcpServerName, mcpUrl, token }) {
154
+ const existing = readJsonSafe(cfg.path) ?? {}
155
+ existing.mcpServers = existing.mcpServers ?? {}
156
+ existing.mcpServers[mcpServerName] = buildServerEntry(cfg.transport, { mcpUrl, token })
157
+ writeJsonAtomic(cfg.path, existing)
158
+ }
159
+
72
160
  async function callInstallEndpoint() {
73
161
  const response = await fetch(INSTALL_URL, {
74
162
  method: 'POST',
@@ -86,24 +174,6 @@ async function callInstallEndpoint() {
86
174
  return data
87
175
  }
88
176
 
89
- function injectClaudeCodeMcp({ mcpUrl, mcpServerName, token }) {
90
- const settingsPath = findClaudeCodeSettingsPath()
91
- const settings = readJsonSafe(settingsPath) ?? {}
92
- settings.mcpServers = settings.mcpServers ?? {}
93
-
94
- // Use the SSE/HTTP transport — Claude Code supports `type: "http"` MCP servers.
95
- settings.mcpServers[mcpServerName] = {
96
- type: 'http',
97
- url: mcpUrl,
98
- headers: {
99
- Authorization: `Bearer ${token}`,
100
- },
101
- }
102
-
103
- writeJsonAtomic(settingsPath, settings)
104
- return settingsPath
105
- }
106
-
107
177
  async function main() {
108
178
  log(`${BOLD}${CYAN}BlackOps Center — agent-first onboarding${RESET}`)
109
179
  log(`${DIM}v${VERSION}${RESET}`)
@@ -146,26 +216,63 @@ async function main() {
146
216
  writeJsonAtomic(CONFIG_PATH, config)
147
217
  log(`${GREEN}✓${RESET} Saved config to ${CONFIG_PATH}`)
148
218
 
149
- // 3. Wire up Claude Code MCP.
150
- let settingsPath
151
- try {
152
- settingsPath = injectClaudeCodeMcp({
153
- mcpUrl: config.mcp_url,
154
- mcpServerName: config.mcp_server_name,
155
- token: config.token,
156
- })
157
- log(`${GREEN}✓${RESET} Added MCP server "${config.mcp_server_name}" to ${settingsPath}`)
158
- } catch (e) {
159
- err(`Could not write Claude Code settings: ${e.message}`)
160
- log('You can configure manually by adding this MCP server to your Claude Code settings:')
161
- log(JSON.stringify({ [config.mcp_server_name]: { type: 'http', url: config.mcp_url, headers: { Authorization: `Bearer ${config.token}` } } }, null, 2))
219
+ // 3. Detect installed MCP clients and write to each one we recognize.
220
+ const clientConfigs = getClientConfigs(os.homedir(), process.platform)
221
+ const installedTo = []
222
+ const failed = []
223
+ for (const c of clientConfigs) {
224
+ if (!clientIsInstalled(c)) continue
225
+ try {
226
+ injectMcpServer(c, {
227
+ mcpServerName: config.mcp_server_name,
228
+ mcpUrl: config.mcp_url,
229
+ token: config.token,
230
+ })
231
+ installedTo.push(c)
232
+ } catch (e) {
233
+ failed.push({ client: c, error: e.message })
234
+ }
235
+ }
236
+
237
+ if (installedTo.length === 0) {
238
+ log(`${YELLOW}No supported MCP client configs detected.${RESET}`)
239
+ } else {
240
+ for (const c of installedTo) {
241
+ log(`${GREEN}✓${RESET} Added MCP server "${config.mcp_server_name}" to ${c.label} (${c.path})`)
242
+ }
162
243
  }
244
+ for (const f of failed) {
245
+ err(`Could not write ${f.client.label} config: ${f.error}`)
246
+ }
247
+
248
+ // 4. Always print universal snippets as fallback for any other MCP client.
249
+ // Two formats — pick whichever your client supports.
250
+ log()
251
+ log(`${DIM}Using a different MCP client? Add ONE of these to its config under \`mcpServers\`:${RESET}`)
252
+ log()
253
+ log(`${DIM}// HTTP transport (Claude Code, Cursor, etc.):${RESET}`)
254
+ log(
255
+ `${DIM}${JSON.stringify(
256
+ { [config.mcp_server_name]: buildServerEntry('http', { mcpUrl: config.mcp_url, token: config.token }) },
257
+ null,
258
+ 2
259
+ )}${RESET}`
260
+ )
261
+ log()
262
+ log(`${DIM}// stdio bridge (Claude Desktop, Cline, anything stdio-only):${RESET}`)
263
+ log(
264
+ `${DIM}${JSON.stringify(
265
+ { [config.mcp_server_name]: buildServerEntry('stdio-bridge', { mcpUrl: config.mcp_url, token: config.token }) },
266
+ null,
267
+ 2
268
+ )}${RESET}`
269
+ )
163
270
 
164
- // 4. Tell the user the one thing to say next. Single line, copy-pasteable.
271
+ // 5. Tell the user the one thing to say next. Single line, copy-pasteable.
165
272
  log()
166
273
  log(`${GREEN}✓${RESET} Installed.`)
167
274
  log()
168
- log(`Open ${BOLD}Claude Code${RESET} and say:`)
275
+ log(`Open your MCP-capable agent (Claude Code, Cursor, etc.) and say:`)
169
276
  log()
170
277
  log(` ${CYAN}${config.prompt}${RESET}`)
171
278
  log()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blackops-onboard",
3
- "version": "0.3.0",
3
+ "version": "0.6.0",
4
4
  "description": "Agent-first onboarding for BlackOps Center. Run via `npx blackops-onboard` to provision your account through Claude Code.",
5
5
  "type": "module",
6
6
  "publishConfig": {