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.
- package/README.md +14 -1
- package/bin/onboard.mjs +155 -48
- 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.
|
|
8
|
-
//
|
|
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.
|
|
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.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
//
|
|
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
|
|
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