blackops-onboard 0.3.0 → 0.4.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 +112 -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.4.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,68 @@ 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
|
+
function getClientConfigs(homedir, platform) {
|
|
72
|
+
const APPDATA = process.env.APPDATA || ''
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
id: 'claude-code',
|
|
76
|
+
label: 'Claude Code',
|
|
77
|
+
path: path.join(homedir, '.claude', 'settings.json'),
|
|
78
|
+
alwaysInstall: true,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'claude-desktop',
|
|
82
|
+
label: 'Claude Desktop',
|
|
83
|
+
path:
|
|
84
|
+
platform === 'darwin'
|
|
85
|
+
? path.join(homedir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
|
|
86
|
+
: platform === 'win32'
|
|
87
|
+
? path.join(APPDATA, 'Claude', 'claude_desktop_config.json')
|
|
88
|
+
: path.join(homedir, '.config', 'Claude', 'claude_desktop_config.json'),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'cursor',
|
|
92
|
+
label: 'Cursor',
|
|
93
|
+
path: path.join(homedir, '.cursor', 'mcp.json'),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'cline',
|
|
97
|
+
label: 'Cline (VS Code)',
|
|
98
|
+
path: path.join(homedir, '.cline', 'mcp_settings.json'),
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function clientIsInstalled(cfg) {
|
|
104
|
+
if (cfg.alwaysInstall) return true
|
|
105
|
+
// File exists OR its parent dir exists (covers cases where the user
|
|
106
|
+
// has the app installed but hasn't created an MCP config file yet).
|
|
107
|
+
if (fs.existsSync(cfg.path)) return true
|
|
108
|
+
if (fs.existsSync(path.dirname(cfg.path))) return true
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function injectMcpServer(cfg, { mcpServerName, mcpUrl, token }) {
|
|
113
|
+
const existing = readJsonSafe(cfg.path) ?? {}
|
|
114
|
+
existing.mcpServers = existing.mcpServers ?? {}
|
|
115
|
+
existing.mcpServers[mcpServerName] = {
|
|
116
|
+
type: 'http',
|
|
117
|
+
url: mcpUrl,
|
|
118
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
119
|
+
}
|
|
120
|
+
writeJsonAtomic(cfg.path, existing)
|
|
121
|
+
}
|
|
122
|
+
|
|
72
123
|
async function callInstallEndpoint() {
|
|
73
124
|
const response = await fetch(INSTALL_URL, {
|
|
74
125
|
method: 'POST',
|
|
@@ -86,24 +137,6 @@ async function callInstallEndpoint() {
|
|
|
86
137
|
return data
|
|
87
138
|
}
|
|
88
139
|
|
|
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
140
|
async function main() {
|
|
108
141
|
log(`${BOLD}${CYAN}BlackOps Center — agent-first onboarding${RESET}`)
|
|
109
142
|
log(`${DIM}v${VERSION}${RESET}`)
|
|
@@ -146,26 +179,57 @@ async function main() {
|
|
|
146
179
|
writeJsonAtomic(CONFIG_PATH, config)
|
|
147
180
|
log(`${GREEN}✓${RESET} Saved config to ${CONFIG_PATH}`)
|
|
148
181
|
|
|
149
|
-
// 3.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
182
|
+
// 3. Detect installed MCP clients and write to each one we recognize.
|
|
183
|
+
const clientConfigs = getClientConfigs(os.homedir(), process.platform)
|
|
184
|
+
const installedTo = []
|
|
185
|
+
const failed = []
|
|
186
|
+
for (const c of clientConfigs) {
|
|
187
|
+
if (!clientIsInstalled(c)) continue
|
|
188
|
+
try {
|
|
189
|
+
injectMcpServer(c, {
|
|
190
|
+
mcpServerName: config.mcp_server_name,
|
|
191
|
+
mcpUrl: config.mcp_url,
|
|
192
|
+
token: config.token,
|
|
193
|
+
})
|
|
194
|
+
installedTo.push(c)
|
|
195
|
+
} catch (e) {
|
|
196
|
+
failed.push({ client: c, error: e.message })
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (installedTo.length === 0) {
|
|
201
|
+
log(`${YELLOW}No supported MCP client configs detected.${RESET}`)
|
|
202
|
+
} else {
|
|
203
|
+
for (const c of installedTo) {
|
|
204
|
+
log(`${GREEN}✓${RESET} Added MCP server "${config.mcp_server_name}" to ${c.label} (${c.path})`)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const f of failed) {
|
|
208
|
+
err(`Could not write ${f.client.label} config: ${f.error}`)
|
|
162
209
|
}
|
|
163
210
|
|
|
164
|
-
// 4.
|
|
211
|
+
// 4. Always print the universal snippet as fallback for any other MCP client.
|
|
212
|
+
log()
|
|
213
|
+
log(`${DIM}Using a different MCP client? Add this to its config under \`mcpServers\`:${RESET}`)
|
|
214
|
+
log()
|
|
215
|
+
const snippet = JSON.stringify(
|
|
216
|
+
{
|
|
217
|
+
[config.mcp_server_name]: {
|
|
218
|
+
type: 'http',
|
|
219
|
+
url: config.mcp_url,
|
|
220
|
+
headers: { Authorization: `Bearer ${config.token}` },
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
null,
|
|
224
|
+
2
|
|
225
|
+
)
|
|
226
|
+
log(`${DIM}${snippet}${RESET}`)
|
|
227
|
+
|
|
228
|
+
// 5. Tell the user the one thing to say next. Single line, copy-pasteable.
|
|
165
229
|
log()
|
|
166
230
|
log(`${GREEN}✓${RESET} Installed.`)
|
|
167
231
|
log()
|
|
168
|
-
log(`Open
|
|
232
|
+
log(`Open your MCP-capable agent (Claude Code, Cursor, etc.) and say:`)
|
|
169
233
|
log()
|
|
170
234
|
log(` ${CYAN}${config.prompt}${RESET}`)
|
|
171
235
|
log()
|
package/package.json
CHANGED