@nonbot/cli 0.10.14 → 0.10.16
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/CHANGELOG.md +19 -0
- package/LICENSE +22 -11
- package/README.md +21 -6
- package/dist/commands/daemon.js +2 -0
- package/dist/commands/doctor.js +184 -127
- package/dist/commands/logout.js +78 -0
- package/dist/commands/mcp.js +409 -0
- package/dist/commands/update.js +157 -0
- package/dist/index.js +125 -43
- package/dist/lib/mcp-account.js +232 -0
- package/dist/lib/mcp-config.js +202 -0
- package/dist/lib/mcp-probe.js +226 -0
- package/dist/lib/mcp-status.js +127 -0
- package/dist/lib/mcp-trust.js +63 -0
- package/dist/lib/output.js +40 -6
- package/dist/lib/quickstart-menu.js +86 -0
- package/dist/lib/update-check.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6,98 +6,180 @@ import { runDaemonCommand } from './commands/daemon.js';
|
|
|
6
6
|
import { runLoginCommand } from './commands/login.js';
|
|
7
7
|
import { runRunCommand } from './commands/run.js';
|
|
8
8
|
import { runDoctorCommand } from './commands/doctor.js';
|
|
9
|
+
import { runLogoutCommand } from './commands/logout.js';
|
|
10
|
+
import { runUpdateCommand } from './commands/update.js';
|
|
11
|
+
import { runMcpCommand } from './commands/mcp.js';
|
|
12
|
+
import { menuIsInteractive, waitForMenuChoice, linesBelow } from './lib/quickstart-menu.js';
|
|
9
13
|
import { runLogsCommand } from './commands/logs.js';
|
|
10
14
|
import { runProfilesCommand } from './commands/profiles.js';
|
|
11
15
|
import { runChoirCommand } from './commands/choir.js';
|
|
12
16
|
import { runRunMcpCommand } from './commands/run-mcp.js';
|
|
13
17
|
import { runRunPromptHookCommand } from './commands/run-prompt-hook.js';
|
|
14
18
|
import { setActiveProfile } from './lib/auth.js';
|
|
15
|
-
import { header, kvRow, ANSI, isTTY } from './lib/output.js';
|
|
19
|
+
import { header, kvRow, ANSI, isTTY, wordmarkArt, WORDMARK_WIDTH } from './lib/output.js';
|
|
16
20
|
const COMMANDS = [
|
|
17
21
|
{
|
|
18
22
|
name: 'login',
|
|
19
|
-
description: '
|
|
23
|
+
description: 'Sign in with a token from non.bot Settings → Connections',
|
|
24
|
+
group: 'start',
|
|
20
25
|
run: (args) => runLoginCommand(args),
|
|
21
26
|
},
|
|
27
|
+
{
|
|
28
|
+
name: 'logout',
|
|
29
|
+
description: 'Sign out on this machine and stop the daemon',
|
|
30
|
+
group: 'start',
|
|
31
|
+
run: (args) => runLogoutCommand(args),
|
|
32
|
+
},
|
|
22
33
|
{
|
|
23
34
|
name: 'daemon',
|
|
24
|
-
description: '
|
|
35
|
+
description: 'Listen for runs from non.bot; each opens a terminal here',
|
|
36
|
+
group: 'start',
|
|
25
37
|
run: (args) => runDaemonCommand(args),
|
|
26
38
|
},
|
|
39
|
+
{
|
|
40
|
+
name: 'update',
|
|
41
|
+
description: 'Install the latest version · update mcp: the non.bot MCP',
|
|
42
|
+
usage: 'update [mcp]',
|
|
43
|
+
group: 'start',
|
|
44
|
+
run: (args) => runUpdateCommand(args),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'status',
|
|
48
|
+
description: 'Who is signed in and whether the daemon is connected',
|
|
49
|
+
group: 'check',
|
|
50
|
+
run: (args) => runStatusCommand(args),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'doctor',
|
|
54
|
+
description: 'Health check: account, agents, terminal, Claude Code',
|
|
55
|
+
group: 'check',
|
|
56
|
+
run: (args) => runDoctorCommand(args),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'mcp',
|
|
60
|
+
description: 'The non.bot MCP: health + version · also: test, add, clean',
|
|
61
|
+
group: 'check',
|
|
62
|
+
run: (args) => runMcpCommand(args),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'logs',
|
|
66
|
+
description: 'Runs launched on this machine (--limit <n>, --json, -f)',
|
|
67
|
+
group: 'check',
|
|
68
|
+
run: (args) => runLogsCommand(args),
|
|
69
|
+
},
|
|
27
70
|
{
|
|
28
71
|
name: 'run',
|
|
29
|
-
|
|
72
|
+
usage: 'run <id>',
|
|
73
|
+
description: 'Launch one run by id, then exit',
|
|
74
|
+
group: 'more',
|
|
30
75
|
run: (args) => runRunCommand(args),
|
|
31
76
|
},
|
|
32
77
|
{
|
|
33
78
|
name: 'choir',
|
|
34
|
-
description: 'Coordinated-run engine
|
|
79
|
+
description: 'Coordinated-run engine — one agent per git worktree',
|
|
80
|
+
usage: 'choir <session>',
|
|
81
|
+
group: 'more',
|
|
35
82
|
run: (args) => runChoirCommand(args),
|
|
36
83
|
},
|
|
84
|
+
{
|
|
85
|
+
name: 'profiles',
|
|
86
|
+
description: 'List signed-in accounts (pick one with --profile <name>)',
|
|
87
|
+
group: 'more',
|
|
88
|
+
run: (args) => runProfilesCommand(args),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'test',
|
|
92
|
+
description: 'Print a local diagnostic banner (no server needed)',
|
|
93
|
+
group: 'more',
|
|
94
|
+
run: () => runTestCommand(),
|
|
95
|
+
},
|
|
37
96
|
{
|
|
38
97
|
name: 'run-mcp',
|
|
39
98
|
description: 'Per-pane fleet MCP server over stdio. Launched by .mcp.json — not run by hand.',
|
|
99
|
+
group: 'hidden',
|
|
40
100
|
run: () => runRunMcpCommand(),
|
|
41
101
|
},
|
|
42
102
|
{
|
|
43
103
|
name: 'choir-mcp',
|
|
44
104
|
description: 'Deprecated alias for run-mcp.',
|
|
105
|
+
group: 'hidden',
|
|
45
106
|
run: () => runRunMcpCommand(),
|
|
46
107
|
},
|
|
47
108
|
{
|
|
48
109
|
name: 'run-prompt-hook',
|
|
49
110
|
description: "Claude Code Notification-hook handler — surfaces a Run's mid-run prompt. Not run by hand.",
|
|
111
|
+
group: 'hidden',
|
|
50
112
|
run: () => runRunPromptHookCommand(),
|
|
51
113
|
},
|
|
52
|
-
{
|
|
53
|
-
name: 'status',
|
|
54
|
-
description: 'Report login state + last daemon heartbeat.',
|
|
55
|
-
run: (args) => runStatusCommand(args),
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
name: 'doctor',
|
|
59
|
-
description: 'Health check — auth, server, daemon, provider CLIs, terminal.',
|
|
60
|
-
run: (args) => runDoctorCommand(args),
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: 'logs',
|
|
64
|
-
description: 'Local activation history. Flags: --limit <n>, --json, --follow/-f.',
|
|
65
|
-
run: (args) => runLogsCommand(args),
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: 'profiles',
|
|
69
|
-
description: 'List multi-account auth profiles; marks the active one.',
|
|
70
|
-
run: (args) => runProfilesCommand(args),
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
name: 'test',
|
|
74
|
-
description: 'Print the diagnostic banner locally — no server roundtrip.',
|
|
75
|
-
run: () => runTestCommand(),
|
|
76
|
-
},
|
|
77
114
|
];
|
|
78
115
|
function printVersion() {
|
|
79
116
|
process.stdout.write(VERSION + '\n');
|
|
80
117
|
}
|
|
81
118
|
function printHelp() {
|
|
82
119
|
const tty = isTTY(process.stdout);
|
|
120
|
+
const head = (t) => (tty ? ` ${ANSI.bold}${t}${ANSI.reset}` : ` ${t}`);
|
|
121
|
+
const KEY = 18;
|
|
83
122
|
const lines = [];
|
|
84
|
-
lines.push(header('nonbot', `non.bot CLI
|
|
123
|
+
lines.push(header('nonbot', `non.bot CLI · v${VERSION}`));
|
|
85
124
|
lines.push('');
|
|
86
|
-
lines.push(
|
|
125
|
+
lines.push(kvRow('Usage', 'nonbot <command> [args]', { keyWidth: KEY }));
|
|
126
|
+
const section = (title, group) => {
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push(head(title));
|
|
129
|
+
for (const cmd of COMMANDS.filter((c) => c.group === group)) {
|
|
130
|
+
lines.push(kvRow(cmd.usage ?? cmd.name, cmd.description, { keyWidth: KEY }));
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
section('Get started', 'start');
|
|
87
134
|
lines.push('');
|
|
88
|
-
lines.push(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
135
|
+
lines.push(head('Stop the daemon'));
|
|
136
|
+
lines.push(kvRow('Ctrl-C', 'in the daemon\'s window', { keyWidth: KEY }));
|
|
137
|
+
lines.push(kvRow('daemon stop', 'from another terminal (--machine <id> / --all)', { keyWidth: KEY }));
|
|
138
|
+
lines.push(kvRow('daemon attach', 'bring back a closed daemon window', { keyWidth: KEY }));
|
|
139
|
+
section('Check', 'check');
|
|
140
|
+
section('More', 'more');
|
|
93
141
|
lines.push('');
|
|
94
|
-
lines.push(
|
|
95
|
-
lines.push(kvRow('--profile <name>', '
|
|
96
|
-
lines.push(kvRow('-v, --version', '
|
|
97
|
-
lines.push(kvRow('-h, --help', '
|
|
142
|
+
lines.push(head('Flags'));
|
|
143
|
+
lines.push(kvRow('--profile <name>', 'use a named account (works with any command)', { keyWidth: KEY }));
|
|
144
|
+
lines.push(kvRow('-v, --version', 'print the version', { keyWidth: KEY }));
|
|
145
|
+
lines.push(kvRow('-h, --help', 'print this help', { keyWidth: KEY }));
|
|
98
146
|
lines.push('');
|
|
99
147
|
process.stdout.write(lines.join('\n') + '\n');
|
|
100
148
|
}
|
|
149
|
+
async function printQuickStart() {
|
|
150
|
+
const KEY = 8;
|
|
151
|
+
const io = { stdin: process.stdin, stdout: process.stdout };
|
|
152
|
+
const interactive = menuIsInteractive(io);
|
|
153
|
+
const lines = [];
|
|
154
|
+
const cols = process.stdout.columns ?? 0;
|
|
155
|
+
if (cols === 0 || cols >= WORDMARK_WIDTH + 4) {
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push(wordmarkArt({ indent: ' ' }));
|
|
158
|
+
lines.push('');
|
|
159
|
+
lines.push(kvRow('', `the non.bot CLI · v${VERSION}`, { keyWidth: KEY }));
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
lines.push(header('non.bot CLI', `v${VERSION}`));
|
|
163
|
+
}
|
|
164
|
+
lines.push('');
|
|
165
|
+
lines.push(kvRow('login', 'sign in', { keyWidth: KEY }));
|
|
166
|
+
lines.push(kvRow('daemon', 'listen for runs — Ctrl-C stops it', { keyWidth: KEY }));
|
|
167
|
+
lines.push(kvRow('doctor', 'check that everything is set up', { keyWidth: KEY }));
|
|
168
|
+
lines.push(kvRow('logout', 'sign out and stop the daemon', { keyWidth: KEY }));
|
|
169
|
+
lines.push(kvRow('update', 'install the latest version', { keyWidth: KEY }));
|
|
170
|
+
const HELP_MARK = 'all commands';
|
|
171
|
+
lines.push(kvRow('help', interactive ? `${HELP_MARK} ← click, or press h` : `${HELP_MARK} · nonbot help`, { keyWidth: KEY }));
|
|
172
|
+
lines.push('');
|
|
173
|
+
lines.push(kvRow('', interactive ? 'press h for help · any other key to exit' : 'type nonbot <command> · nonbot help lists everything', { keyWidth: KEY }));
|
|
174
|
+
lines.push('');
|
|
175
|
+
const printed = lines.join('\n') + '\n';
|
|
176
|
+
process.stdout.write(printed);
|
|
177
|
+
if (!interactive)
|
|
178
|
+
return;
|
|
179
|
+
const choice = await waitForMenuChoice(io, linesBelow(printed, HELP_MARK));
|
|
180
|
+
if (choice === 'help')
|
|
181
|
+
printHelp();
|
|
182
|
+
}
|
|
101
183
|
function extractProfileFlag(args) {
|
|
102
184
|
let profile;
|
|
103
185
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -131,7 +213,7 @@ async function main(argv) {
|
|
|
131
213
|
}
|
|
132
214
|
}
|
|
133
215
|
if (args.length === 0) {
|
|
134
|
-
|
|
216
|
+
await printQuickStart();
|
|
135
217
|
return 0;
|
|
136
218
|
}
|
|
137
219
|
const first = args[0];
|
|
@@ -139,7 +221,7 @@ async function main(argv) {
|
|
|
139
221
|
printVersion();
|
|
140
222
|
return 0;
|
|
141
223
|
}
|
|
142
|
-
if (first === '-h' || first === '--help') {
|
|
224
|
+
if (first === '-h' || first === '--help' || first === 'help') {
|
|
143
225
|
printHelp();
|
|
144
226
|
return 0;
|
|
145
227
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { compareVersions, parseSemver } from './update-check.js';
|
|
5
|
+
export function tokenFor(entry, spec, cliAuth) {
|
|
6
|
+
if (spec) {
|
|
7
|
+
const auth = Object.entries(spec.headers).find(([k]) => k.toLowerCase() === 'authorization')?.[1];
|
|
8
|
+
const bearer = auth && /^Bearer\s+(\S+)$/i.exec(auth)?.[1];
|
|
9
|
+
if (bearer)
|
|
10
|
+
return { source: 'config', token: bearer };
|
|
11
|
+
const env = spec.env.NON_BOT_API_TOKEN;
|
|
12
|
+
if (env)
|
|
13
|
+
return { source: 'config', token: env };
|
|
14
|
+
}
|
|
15
|
+
if (entry.kind === 'remote')
|
|
16
|
+
return { source: 'none' };
|
|
17
|
+
return cliAuth?.pat ? { source: 'login', token: cliAuth.pat } : { source: 'none' };
|
|
18
|
+
}
|
|
19
|
+
export function maskToken(token) {
|
|
20
|
+
const tail = token.slice(-4);
|
|
21
|
+
return token.startsWith('pat_') ? `pat_…${tail}` : `…${tail}`;
|
|
22
|
+
}
|
|
23
|
+
export function isReadOnly(spec) {
|
|
24
|
+
if (!spec)
|
|
25
|
+
return false;
|
|
26
|
+
if (spec.env.NON_BOT_MCP_READONLY === '1')
|
|
27
|
+
return true;
|
|
28
|
+
if (spec.url) {
|
|
29
|
+
try {
|
|
30
|
+
return new URL(spec.url).searchParams.get('readonly') === '1';
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
export async function checkToken(baseUrl, token, fetchImpl = fetch) {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetchImpl(`${baseUrl}/api/auth/me`, {
|
|
41
|
+
headers: { Authorization: `Bearer ${token}`, 'X-Requested-With': 'ConradPM-Native' },
|
|
42
|
+
signal: AbortSignal.timeout(8_000),
|
|
43
|
+
});
|
|
44
|
+
if (res.status === 401)
|
|
45
|
+
return { valid: false, scopes: null };
|
|
46
|
+
if (!res.ok)
|
|
47
|
+
return { valid: null, scopes: null };
|
|
48
|
+
const body = (await res.json());
|
|
49
|
+
if (!body.authenticated)
|
|
50
|
+
return { valid: false, scopes: null };
|
|
51
|
+
const t = body.token;
|
|
52
|
+
return {
|
|
53
|
+
valid: true,
|
|
54
|
+
name: typeof t?.name === 'string' ? t.name : null,
|
|
55
|
+
scopes: Array.isArray(t?.scopes) ? t.scopes.filter((s) => typeof s === 'string') : null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { valid: null, scopes: null };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function familyOf(tool) {
|
|
63
|
+
if (tool.startsWith('jobshop_'))
|
|
64
|
+
return { family: 'jobshop', resources: ['jobshop'] };
|
|
65
|
+
if (tool.startsWith('radbug_'))
|
|
66
|
+
return { family: 'radbugs', resources: ['radbugs', 'admin', 'team'] };
|
|
67
|
+
if (tool.startsWith('run_'))
|
|
68
|
+
return { family: 'run', resources: ['portfolio', 'runs'] };
|
|
69
|
+
return { family: 'portfolio', resources: ['portfolio'] };
|
|
70
|
+
}
|
|
71
|
+
export function scopeGaps(scopes, tools) {
|
|
72
|
+
if (scopes === null || scopes.includes('*'))
|
|
73
|
+
return { full: true, blocked: [], readOnly: [] };
|
|
74
|
+
const has = (res) => scopes.some((s) => s.startsWith(`${res}:`));
|
|
75
|
+
const canWrite = (res) => scopes.some((s) => s === `${res}:write` || (res === 'runs' && /^runs:(fire|control|respond)$/.test(s)));
|
|
76
|
+
const counts = new Map();
|
|
77
|
+
for (const t of tools) {
|
|
78
|
+
const f = familyOf(t.name);
|
|
79
|
+
const c = counts.get(f.family) ?? { resources: f.resources, n: 0 };
|
|
80
|
+
c.n++;
|
|
81
|
+
counts.set(f.family, c);
|
|
82
|
+
}
|
|
83
|
+
const blocked = [];
|
|
84
|
+
const readOnly = [];
|
|
85
|
+
for (const [family, { resources, n }] of counts) {
|
|
86
|
+
if (!resources.some(has))
|
|
87
|
+
blocked.push({ family, tools: n, needs: resources.map((r) => `${r}:*`).join(' or ') });
|
|
88
|
+
else if (!resources.some(canWrite))
|
|
89
|
+
readOnly.push({ family, tools: n });
|
|
90
|
+
}
|
|
91
|
+
return { full: false, blocked, readOnly };
|
|
92
|
+
}
|
|
93
|
+
export function preApprovals(serverName, dirs, home = homedir(), read = readIfExists) {
|
|
94
|
+
const files = [join(home, '.claude', 'settings.json'), join(home, '.claude', 'settings.local.json')];
|
|
95
|
+
for (const d of dirs)
|
|
96
|
+
files.push(join(d, '.claude', 'settings.json'), join(d, '.claude', 'settings.local.json'));
|
|
97
|
+
const whole = new Set([`mcp__${serverName}`, `mcp__${serverName}__*`]);
|
|
98
|
+
const prefix = `mcp__${serverName}__`;
|
|
99
|
+
const out = { all: false, some: 0, denied: 0, files: [] };
|
|
100
|
+
const allowed = new Set();
|
|
101
|
+
const denied = new Set();
|
|
102
|
+
for (const f of [...new Set(files)]) {
|
|
103
|
+
let json = null;
|
|
104
|
+
try {
|
|
105
|
+
const text = read(f);
|
|
106
|
+
json = text ? JSON.parse(text) : null;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
json = null;
|
|
110
|
+
}
|
|
111
|
+
const allow = Array.isArray(json?.permissions?.allow) ? json.permissions.allow : [];
|
|
112
|
+
const deny = Array.isArray(json?.permissions?.deny) ? json.permissions.deny : [];
|
|
113
|
+
let said = false;
|
|
114
|
+
for (const r of allow) {
|
|
115
|
+
if (typeof r !== 'string')
|
|
116
|
+
continue;
|
|
117
|
+
if (whole.has(r)) {
|
|
118
|
+
out.all = true;
|
|
119
|
+
said = true;
|
|
120
|
+
}
|
|
121
|
+
else if (r.startsWith(prefix)) {
|
|
122
|
+
allowed.add(r);
|
|
123
|
+
said = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const r of deny) {
|
|
127
|
+
if (typeof r !== 'string')
|
|
128
|
+
continue;
|
|
129
|
+
if (whole.has(r)) {
|
|
130
|
+
out.denied = 'all';
|
|
131
|
+
said = true;
|
|
132
|
+
}
|
|
133
|
+
else if (r.startsWith(prefix) && out.denied !== 'all') {
|
|
134
|
+
denied.add(r);
|
|
135
|
+
said = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (said)
|
|
139
|
+
out.files.push(f.startsWith(home) ? '~' + f.slice(home.length) : f);
|
|
140
|
+
}
|
|
141
|
+
out.some = allowed.size;
|
|
142
|
+
if (out.denied !== 'all')
|
|
143
|
+
out.denied = denied.size;
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
function readIfExists(p) {
|
|
147
|
+
try {
|
|
148
|
+
return existsSync(p) ? readFileSync(p, 'utf-8') : null;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const RUN_PORTFOLIO_TOOLS = new Set([
|
|
155
|
+
'portfolio_create_activation',
|
|
156
|
+
'portfolio_get_activation',
|
|
157
|
+
'portfolio_update_activation_status',
|
|
158
|
+
'portfolio_subscribe_to_activations',
|
|
159
|
+
'portfolio_attach_brief',
|
|
160
|
+
'portfolio_link_chat_session',
|
|
161
|
+
'portfolio_mark_run_ready',
|
|
162
|
+
'portfolio_repo_path',
|
|
163
|
+
]);
|
|
164
|
+
export function appOfTool(tool) {
|
|
165
|
+
if (tool.startsWith('run_') || RUN_PORTFOLIO_TOOLS.has(tool))
|
|
166
|
+
return 'run';
|
|
167
|
+
if (tool.startsWith('jobshop_'))
|
|
168
|
+
return 'jobshop';
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
export async function fetchAppStates(baseUrl, token, fetchImpl = fetch) {
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetchImpl(`${baseUrl}/api/feature-flags/me`, {
|
|
174
|
+
headers: { Authorization: `Bearer ${token}`, 'X-Requested-With': 'ConradPM-Native' },
|
|
175
|
+
signal: AbortSignal.timeout(8_000),
|
|
176
|
+
});
|
|
177
|
+
if (!res.ok)
|
|
178
|
+
return null;
|
|
179
|
+
const body = (await res.json());
|
|
180
|
+
if (!Array.isArray(body.apps))
|
|
181
|
+
return null;
|
|
182
|
+
return body.apps
|
|
183
|
+
.filter((a) => !!a && typeof a.id === 'string')
|
|
184
|
+
.map((a) => ({ id: a.id, installed: !!a.installed, enabled: !!a.enabled, available: a.available }));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export function appsOff(tools, apps) {
|
|
191
|
+
const out = [];
|
|
192
|
+
for (const app of ['run', 'jobshop']) {
|
|
193
|
+
const n = tools.filter((t) => appOfTool(t.name) === app).length;
|
|
194
|
+
if (n === 0)
|
|
195
|
+
continue;
|
|
196
|
+
const s = apps.find((a) => a.id === app);
|
|
197
|
+
if (!s)
|
|
198
|
+
continue;
|
|
199
|
+
if (s.available === false)
|
|
200
|
+
out.push({ app, tools: n, state: 'not offered' });
|
|
201
|
+
else if (!s.installed)
|
|
202
|
+
out.push({ app, tools: n, state: 'not added' });
|
|
203
|
+
else if (!s.enabled)
|
|
204
|
+
out.push({ app, tools: n, state: 'off' });
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
export async function newerReleases(installed, latest, fetchImpl = fetch) {
|
|
209
|
+
if (!parseSemver(installed) || !parseSemver(latest) || compareVersions(latest, installed) <= 0)
|
|
210
|
+
return [];
|
|
211
|
+
try {
|
|
212
|
+
const res = await fetchImpl('https://registry.npmjs.org/@nonbot/mcp', {
|
|
213
|
+
headers: { Accept: 'application/json' },
|
|
214
|
+
signal: AbortSignal.timeout(8_000),
|
|
215
|
+
});
|
|
216
|
+
if (!res.ok)
|
|
217
|
+
return null;
|
|
218
|
+
const doc = (await res.json());
|
|
219
|
+
const out = [];
|
|
220
|
+
for (const [v, t] of Object.entries(doc.time ?? {})) {
|
|
221
|
+
if (!parseSemver(v) || /-/.test(v))
|
|
222
|
+
continue;
|
|
223
|
+
if (compareVersions(v, installed) > 0 && compareVersions(v, latest) <= 0 && typeof t === 'string') {
|
|
224
|
+
out.push({ version: v, date: t.slice(0, 10) });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return out.sort((a, b) => compareVersions(a.version, b.version));
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, dirname, join, resolve, isAbsolute } from 'node:path';
|
|
4
|
+
export const LEGACY_MCP_NAMES = ['portfolio-mcp', 'context-graph', 'jobshop-mcp'];
|
|
5
|
+
const LAUNCH = new WeakMap();
|
|
6
|
+
export function launchSpecOf(entry) {
|
|
7
|
+
return LAUNCH.get(entry);
|
|
8
|
+
}
|
|
9
|
+
export function isNonbotRemoteUrl(value) {
|
|
10
|
+
if (!value)
|
|
11
|
+
return false;
|
|
12
|
+
try {
|
|
13
|
+
const u = new URL(value);
|
|
14
|
+
return u.protocol === 'https:' && u.hostname.toLowerCase().replace(/\.$/, '') === 'mcp.non.bot';
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function projectServerApproved(name, dir, claudeJson, home, read) {
|
|
21
|
+
const enabled = new Set();
|
|
22
|
+
const disabled = new Set();
|
|
23
|
+
let all = false;
|
|
24
|
+
const takeFrom = (o, mayApprove) => {
|
|
25
|
+
if (!o || typeof o !== 'object')
|
|
26
|
+
return;
|
|
27
|
+
const r = o;
|
|
28
|
+
if (Array.isArray(r.disabledMcpjsonServers))
|
|
29
|
+
for (const n of r.disabledMcpjsonServers)
|
|
30
|
+
if (typeof n === 'string')
|
|
31
|
+
disabled.add(n);
|
|
32
|
+
if (!mayApprove)
|
|
33
|
+
return;
|
|
34
|
+
if (Array.isArray(r.enabledMcpjsonServers))
|
|
35
|
+
for (const n of r.enabledMcpjsonServers)
|
|
36
|
+
if (typeof n === 'string')
|
|
37
|
+
enabled.add(n);
|
|
38
|
+
if (r.enableAllProjectMcpServers === true)
|
|
39
|
+
all = true;
|
|
40
|
+
};
|
|
41
|
+
let real = dir;
|
|
42
|
+
try {
|
|
43
|
+
real = realpathSync(dir);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
}
|
|
47
|
+
takeFrom(claudeJson?.projects?.[dir], true);
|
|
48
|
+
if (real !== dir)
|
|
49
|
+
takeFrom(claudeJson?.projects?.[real], true);
|
|
50
|
+
takeFrom(parse(read(join(home, '.claude', 'settings.json'))), true);
|
|
51
|
+
takeFrom(parse(read(join(dir, '.claude', 'settings.json'))), false);
|
|
52
|
+
takeFrom(parse(read(join(dir, '.claude', 'settings.local.json'))), false);
|
|
53
|
+
if (disabled.has(name))
|
|
54
|
+
return false;
|
|
55
|
+
return enabled.has(name) || all;
|
|
56
|
+
}
|
|
57
|
+
function stringMap(v) {
|
|
58
|
+
const out = {};
|
|
59
|
+
if (v && typeof v === 'object') {
|
|
60
|
+
for (const [k, val] of Object.entries(v))
|
|
61
|
+
if (typeof val === 'string')
|
|
62
|
+
out[k] = val;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function defaultReadFile(path) {
|
|
67
|
+
try {
|
|
68
|
+
return existsSync(path) ? readFileSync(path, 'utf-8') : null;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function tilde(path, home) {
|
|
75
|
+
return path.startsWith(home) ? '~' + path.slice(home.length) : path;
|
|
76
|
+
}
|
|
77
|
+
export function classifyServer(name, server, baseDir) {
|
|
78
|
+
const args = Array.isArray(server.args) ? server.args.filter((a) => typeof a === 'string') : [];
|
|
79
|
+
const command = typeof server.command === 'string' ? server.command : '';
|
|
80
|
+
const url = typeof server.url === 'string' ? server.url : '';
|
|
81
|
+
if (isNonbotRemoteUrl(url) || args.some(isNonbotRemoteUrl)) {
|
|
82
|
+
return { kind: 'remote' };
|
|
83
|
+
}
|
|
84
|
+
const spec = args.find((a) => /^@nonbot\/mcp(@[^\s]+)?$/.test(a));
|
|
85
|
+
if (spec) {
|
|
86
|
+
const at = spec.indexOf('@', 1);
|
|
87
|
+
const version = at > 0 ? spec.slice(at + 1) : undefined;
|
|
88
|
+
return { kind: 'npx', pinned: version && version !== 'latest' ? version : undefined };
|
|
89
|
+
}
|
|
90
|
+
if (basename(command) === 'nonbot-mcp' || basename(command) === 'nonbot-mcp.cmd') {
|
|
91
|
+
return { kind: 'global' };
|
|
92
|
+
}
|
|
93
|
+
const script = [command, ...args].find((a) => /nonbot-mcp[\\/]+dist[\\/]+stdio\.js$/.test(a));
|
|
94
|
+
if (script) {
|
|
95
|
+
const abs = isAbsolute(script) ? script : resolve(baseDir, script);
|
|
96
|
+
return { kind: 'checkout', checkoutDir: dirname(dirname(abs)) };
|
|
97
|
+
}
|
|
98
|
+
if (name === 'nonbot')
|
|
99
|
+
return { kind: 'unknown' };
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function serversIn(json) {
|
|
103
|
+
const servers = json?.mcpServers;
|
|
104
|
+
if (!servers || typeof servers !== 'object')
|
|
105
|
+
return [];
|
|
106
|
+
return Object.entries(servers).filter(([, v]) => v !== null && typeof v === 'object');
|
|
107
|
+
}
|
|
108
|
+
function parse(text) {
|
|
109
|
+
if (!text)
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(text);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function scanMcpConfigs(deps = {}) {
|
|
119
|
+
const home = deps.home ?? homedir();
|
|
120
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
121
|
+
const platform = deps.platform ?? process.platform;
|
|
122
|
+
const read = deps.readFile ?? defaultReadFile;
|
|
123
|
+
const names = new Set();
|
|
124
|
+
const where = new Map();
|
|
125
|
+
const nonbot = [];
|
|
126
|
+
const locations = [];
|
|
127
|
+
const seenFiles = new Set();
|
|
128
|
+
const take = (source, json, baseDir, scope, file, dir) => {
|
|
129
|
+
for (const [name, server] of serversIn(json)) {
|
|
130
|
+
names.add(name);
|
|
131
|
+
if (!where.has(name))
|
|
132
|
+
where.set(name, source);
|
|
133
|
+
locations.push({ name, source, scope, file, ...(dir ? { dir } : {}) });
|
|
134
|
+
const c = classifyServer(name, server, baseDir);
|
|
135
|
+
if (c) {
|
|
136
|
+
const approved = scope !== 'project' || (!!dir && projectServerApproved(name, dir, claudeJson, home, read));
|
|
137
|
+
const entry = { source, name, ...c, scope, ...(dir ? { dir } : {}), approved };
|
|
138
|
+
nonbot.push(entry);
|
|
139
|
+
LAUNCH.set(entry, {
|
|
140
|
+
command: typeof server.command === 'string' ? server.command : undefined,
|
|
141
|
+
args: Array.isArray(server.args) ? server.args.filter((a) => typeof a === 'string') : [],
|
|
142
|
+
env: stringMap(server.env),
|
|
143
|
+
url: typeof server.url === 'string' ? server.url : undefined,
|
|
144
|
+
headers: stringMap(server.headers),
|
|
145
|
+
cwd: baseDir,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const claudeJsonPath = join(home, '.claude.json');
|
|
151
|
+
const claudeJson = parse(read(claudeJsonPath));
|
|
152
|
+
const projectDirs = [];
|
|
153
|
+
if (claudeJson) {
|
|
154
|
+
take('~/.claude.json', claudeJson, home, 'user', claudeJsonPath);
|
|
155
|
+
if (claudeJson.projects && typeof claudeJson.projects === 'object') {
|
|
156
|
+
for (const [dir, proj] of Object.entries(claudeJson.projects)) {
|
|
157
|
+
take(`~/.claude.json (${tilde(dir, home)})`, proj, dir, 'local', claudeJsonPath, dir);
|
|
158
|
+
projectDirs.push(dir);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const mcpJsonDirs = [];
|
|
163
|
+
for (let d = resolve(cwd);; d = dirname(d)) {
|
|
164
|
+
mcpJsonDirs.push(d);
|
|
165
|
+
if (d === dirname(d) || d === home)
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
mcpJsonDirs.push(...projectDirs);
|
|
169
|
+
for (const dir of mcpJsonDirs) {
|
|
170
|
+
const file = join(dir, '.mcp.json');
|
|
171
|
+
if (seenFiles.has(file))
|
|
172
|
+
continue;
|
|
173
|
+
seenFiles.add(file);
|
|
174
|
+
const json = parse(read(file));
|
|
175
|
+
if (json)
|
|
176
|
+
take(tilde(file, home), json, dir, 'project', file, dir);
|
|
177
|
+
}
|
|
178
|
+
if (platform === 'darwin') {
|
|
179
|
+
const desktop = desktopConfigPath(home);
|
|
180
|
+
const json = parse(read(desktop));
|
|
181
|
+
if (json)
|
|
182
|
+
take('Claude Desktop', json, home, 'desktop', desktop);
|
|
183
|
+
}
|
|
184
|
+
return { names, where, nonbot, locations };
|
|
185
|
+
}
|
|
186
|
+
export function desktopConfigPath(home = homedir()) {
|
|
187
|
+
return join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
188
|
+
}
|
|
189
|
+
export function describeNonbotMcp(e, home = homedir()) {
|
|
190
|
+
switch (e.kind) {
|
|
191
|
+
case 'remote':
|
|
192
|
+
return 'remote (mcp.non.bot) · always current';
|
|
193
|
+
case 'npx':
|
|
194
|
+
return e.pinned ? `npx, pinned to ${e.pinned} · gets no updates` : 'npx @nonbot/mcp · updates itself';
|
|
195
|
+
case 'global':
|
|
196
|
+
return 'global install · update: npm i -g @nonbot/mcp';
|
|
197
|
+
case 'checkout':
|
|
198
|
+
return `source checkout ${e.checkoutDir ? tilde(e.checkoutDir, home) : ''}`.trim();
|
|
199
|
+
default:
|
|
200
|
+
return 'configured';
|
|
201
|
+
}
|
|
202
|
+
}
|