@nonbot/cli 0.10.15 → 0.10.17

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 CHANGED
@@ -3,6 +3,21 @@
3
3
  What changed for the person running the CLI, newest first. Update with
4
4
  `npm i -g @nonbot/cli@latest`.
5
5
 
6
+ ## 0.10.17 (2026-11-11)
7
+
8
+ - Typing `nonbot` alone now shows who you're signed in as and whether the daemon is running (or "not signed in — start with: nonbot login"). The login row says "switch account" when you're already signed in.
9
+ - `nonbot help` and `nonbot -h` open with the gradient wordmark.
10
+ - Under the wordmark: "non.bot CLI v… · non.bot MCP v…" — both versions side by side (the MCP's is read from this machine, so the screen stays instant).
11
+ - Help and error messages say "To use" instead of "Usage", which read like a page of your account usage.
12
+
13
+ ## 0.10.16 (2026-11-11)
14
+
15
+ - New: `nonbot mcp` shows the non.bot MCP in one place: its version and what's new since, every Claude app and config file it's set up in (and duplicates), a live start with its tool count by group, how much of each conversation its tool list uses, which tools your apps switch off, the token it uses (last four characters only), whether that token is valid and what it may do, and which tools Claude runs without asking. `--quick` skips the live start.
16
+ - Typing `nonbot` alone now has a **help** option under update: click it (or press h) and the full command list prints right below; any other key exits. Outside a real terminal it never waits. `nonbot help` also works.
17
+ - `nonbot doctor` opens with the gradient wordmark and reads as one tidy grid. It now starts the MCP too and shows the result ("170 tools in 281ms"), lists retired MCP servers on one row with `nonbot mcp clean`, and points at `nonbot mcp` for the full check.
18
+ - A project's own `.mcp.json` is never started until you approve it once (`nonbot mcp test --include-project` shows the exact command and asks). The approval is remembered for that exact command only, so a repo that later changes it has to be approved again.
19
+ - New: `nonbot mcp test` starts every configured copy and checks it answers; `nonbot mcp add` registers it in Claude Code; `nonbot mcp clean` removes retired MCP servers from the files they're in (it asks first, and keeps a backup of Claude Desktop's config).
20
+
6
21
  ## 0.10.15 (2026-11-11)
7
22
 
8
23
  - New: `nonbot logout` signs you out on this machine and stops the daemon.
package/LICENSE CHANGED
@@ -6,24 +6,35 @@ This software (the "@nonbot/cli" package and all of its contents, the
6
6
  "Software") is the proprietary and confidential property of non.bot. It is
7
7
  published to the npm registry for the sole purpose of allowing authorized
8
8
  non.bot account holders to install and run it as the local host for the
9
- non.bot "Run" feature.
9
+ non.bot "Run" feature. Publication on the npm registry, or on any mirror, cache
10
+ or CDN that copies it, does not make the Software open source, does not place
11
+ it in the public domain, and grants no right beyond the limited license below.
10
12
 
11
13
  PERMITTED USE
12
14
 
13
- Subject to your compliance with the non.bot Terms of Service, you are granted
14
- a limited, non-exclusive, non-transferable, revocable license to download,
15
- install, and execute unmodified copies of the Software on machines you control,
16
- solely to connect to and operate the non.bot service.
15
+ Subject to your compliance with the non.bot Terms of Service
16
+ (https://non.bot/terms-of-service), you are granted a limited, non-exclusive,
17
+ non-transferable, non-sublicensable, revocable license to download, install,
18
+ and execute unmodified copies of the Software on machines you control, solely
19
+ to connect to and operate the non.bot service. This license ends automatically
20
+ if you breach it or the Terms of Service, or when your non.bot account ends;
21
+ you must then stop using the Software and delete every copy of it.
17
22
 
18
23
  PROHIBITED WITHOUT PRIOR WRITTEN PERMISSION FROM non.bot
19
24
 
20
- - Copying, redistributing, sublicensing, selling, or publishing the Software
21
- or any portion of it;
25
+ - Copying, redistributing, sublicensing, selling, renting, or publishing the
26
+ Software or any portion of it;
22
27
  - Modifying, adapting, translating, or creating derivative works;
23
- - Reverse-engineering, decompiling, or disassembling the Software except to
24
- the limited extent such restriction is prohibited by applicable law;
25
- - Using the Software, or any technique, method, or design embodied in it, to
26
- build, train, or operate a competing product or service.
28
+ - Reverse-engineering, decompiling, or disassembling the Software, except to
29
+ the limited extent applicable law expressly permits despite this
30
+ restriction;
31
+ - Using the Software, or any technique, method, prompt, or design embodied in
32
+ it, to build, train, or operate a competing product or service;
33
+ - Using the Software, or any part or output of it, to train, fine-tune,
34
+ evaluate, or otherwise improve any machine-learning or artificial-
35
+ intelligence model, dataset, or system;
36
+ - Removing, obscuring, or altering any copyright, license, or proprietary
37
+ notice.
27
38
 
28
39
  NO PATENT OR OTHER IP LICENSE
29
40
 
package/README.md CHANGED
@@ -78,8 +78,14 @@ nonbot daemon Run the poll loop and launch terminals (the main command
78
78
  nonbot run <id> Launch a single Run by id, then exit
79
79
  nonbot status Show login + daemon status
80
80
  nonbot doctor Health check — platform, auth, server, providers, terminal, MCP
81
+ nonbot mcp The non.bot MCP: version, where it's set up, a live start, tools,
82
+ your token's permissions (add --quick to skip starting it)
83
+ nonbot mcp test Start every configured copy of the MCP and check it answers
84
+ nonbot mcp add Register the MCP in Claude Code (claude mcp add … @nonbot/mcp)
85
+ nonbot mcp clean Remove retired MCP servers (portfolio-mcp, context-graph, jobshop-mcp)
81
86
  nonbot logs Show recent local Run launches
82
87
  nonbot update Install the latest version (npm i -g @nonbot/cli@latest)
88
+ nonbot update mcp Show the MCP's version and update it
83
89
  nonbot profiles List configured accounts
84
90
  nonbot choir <session> Coordinated-run engine — one agent per git worktree, merged back by hand
85
91
  nonbot run-mcp Per-pane MCP server over stdio (launched by .mcp.json, not by hand)
@@ -135,7 +141,7 @@ daemon asks the npm registry for the latest `@nonbot/cli`. If it is newer than t
135
141
  running, it prints a single line:
136
142
 
137
143
  ```
138
- Update available: @nonbot/cli 0.9.13 → 0.9.14 (npm i -g @nonbot/cli)
144
+ Update available: @nonbot/cli 0.9.13 → 0.9.14 (run: nonbot update)
139
145
  ```
140
146
 
141
147
  The probe has a 3-second timeout and any failure is silent. Set
@@ -183,6 +189,13 @@ are redacted. Redaction is pattern-based; treat a snapshot as terminal output.
183
189
  saved end-of-run output in the local history (`nonbot logs`). A cloud runner
184
190
  (`--cloud`) additionally registers its machine id and its allowed repository origins.
185
191
 
192
+ **When you run `nonbot mcp` or `nonbot update mcp`:** your token goes to non.bot to
193
+ check it is valid and read its permissions and which apps are on; the CLI asks the npm
194
+ registry for `@nonbot/mcp`'s versions and fetches the mcp.non.bot home page to read
195
+ its version. `nonbot mcp` (not `--quick`) also starts the MCP the way Claude does,
196
+ which makes the MCP's own requests to non.bot. The token is shown only as its last
197
+ four characters.
198
+
186
199
  To show a neutral name instead of the host name, set `NONBOT_MACHINE_NAME` before
187
200
  starting the daemon (up to 64 characters):
188
201
 
@@ -3,15 +3,18 @@ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { loadAuth } from '../lib/auth.js';
6
- import { scanMcpConfigs, describeNonbotMcp } from '../lib/mcp-config.js';
6
+ import { scanMcpConfigs, describeNonbotMcp, LEGACY_MCP_NAMES } from '../lib/mcp-config.js';
7
7
  import { nonbotMcpStatuses, mcpRow } from '../lib/mcp-status.js';
8
+ import { launchSpecOf } from '../lib/mcp-config.js';
9
+ import { probeMcp } from '../lib/mcp-probe.js';
10
+ import { mayStartEntry } from '../lib/mcp-trust.js';
8
11
  import { resolveTerminal } from '../lib/terminal.js';
9
12
  import { VERSION } from '../version.js';
10
- import { header, statusRow, errorBlock, isTTY, ANSI } from '../lib/output.js';
13
+ import { header, statusRow, errorBlock, isTTY, ANSI, wordmarkArt, WORDMARK_WIDTH } from '../lib/output.js';
11
14
  const HEARTBEAT_FRESH_MS = 35_000;
12
15
  const PROVIDER_CLIS = ['claude', 'codex', 'gemini'];
13
16
  export const NONBOT_MCP_ADD_HINT = 'claude mcp add nonbot -- npx -y @nonbot/mcp';
14
- const LEGACY_MCPS = ['portfolio-mcp', 'context-graph', 'jobshop-mcp'];
17
+ const LEGACY_MCPS = LEGACY_MCP_NAMES;
15
18
  const EXPECTED_SKILLS = [];
16
19
  function isSkillInstalled(skillName) {
17
20
  const base = join(homedir(), '.claude');
@@ -137,7 +140,15 @@ export async function runDoctorCommand(args = [], deps = {}) {
137
140
  const errRow = (sig, label, detail = '') => errLog(statusRow(sig, label, detail, { labelWidth: LABEL_WIDTH, stream: process.stderr }) + '\n');
138
141
  const hint = (text) => log(' '.repeat(HINT_INDENT) + paintDim(text) + '\n');
139
142
  const section = (title) => log('\n' + paintSection(title) + '\n');
140
- log(header('non.bot doctor', `v${VERSION} · ${process.platform}/${process.arch}`) + '\n');
143
+ const cols = deps.columns ?? process.stdout.columns ?? 0;
144
+ const sub = `doctor · v${VERSION} · ${process.platform}/${process.arch}`;
145
+ if (cols === 0 || cols >= WORDMARK_WIDTH + 4) {
146
+ log('\n' + wordmarkArt({ indent: ' ' }) + '\n\n');
147
+ log(` ${paintDim(sub)}\n`);
148
+ }
149
+ else {
150
+ log(header('non.bot doctor', `v${VERSION} · ${process.platform}/${process.arch}`) + '\n');
151
+ }
141
152
  const auth = await loader();
142
153
  if (!auth) {
143
154
  log('\n');
@@ -154,7 +165,7 @@ export async function runDoctorCommand(args = [], deps = {}) {
154
165
  try {
155
166
  const res = await fetchImpl(`${auth.baseUrl}/api/auth/me`, { headers });
156
167
  if (res.ok) {
157
- row('✓', 'Server', `${auth.baseUrl} · token accepted`);
168
+ row('✓', 'Server', `${auth.baseUrl.replace(/^https?:\/\//, '')} · token accepted`);
158
169
  }
159
170
  else if (res.status === 401) {
160
171
  errRow('✗', 'Server', 'token rejected (HTTP 401) — sign in again: nonbot login');
@@ -196,7 +207,7 @@ export async function runDoctorCommand(args = [], deps = {}) {
196
207
  }
197
208
  const storeUrl = `${auth.baseUrl}/whygraph/store?tab=apps&app=${gate.app ?? 'run'}`;
198
209
  if (gate.error === 'app_disabled') {
199
- row('✗', 'Run app', 'switched off — the daemon gets no runs until it is on');
210
+ row('✗', 'Run app', 'switched off — the daemon gets no runs');
200
211
  hint(`turn it on: ${storeUrl}`);
201
212
  }
202
213
  else if (gate.error === 'app_not_installed') {
@@ -217,7 +228,8 @@ export async function runDoctorCommand(args = [], deps = {}) {
217
228
  section('AGENTS');
218
229
  const providers = PROVIDER_CLIS.map((bin) => ({ bin, r: checkBinary(bin) }));
219
230
  const anyProvider = providers.some((p) => p.r.installed);
220
- for (const { bin, r } of providers) {
231
+ const ordered = [...providers.filter((p) => p.r.installed), ...providers.filter((p) => !p.r.installed)];
232
+ for (const { bin, r } of ordered) {
221
233
  if (r.installed)
222
234
  row('✓', bin, bareVersion(r.version));
223
235
  else if (anyProvider)
@@ -232,7 +244,7 @@ export async function runDoctorCommand(args = [], deps = {}) {
232
244
  const { cmd } = profile.launch('/tmp/nonbot-doctor-probe.sh');
233
245
  const launcher = checkBinary(cmd);
234
246
  if (launcher.installed) {
235
- row('✓', 'Opens runs in', `${profile.displayName} · via ${cmd}`);
247
+ row('✓', 'Opens runs in', `${profile.displayName} (via ${cmd})`);
236
248
  }
237
249
  else {
238
250
  row('⚠', 'Terminal', `${profile.displayName} — launcher "${cmd}" not found on PATH`);
@@ -253,7 +265,7 @@ export async function runDoctorCommand(args = [], deps = {}) {
253
265
  const appl = checkBinary('iterm-applescript');
254
266
  if (appl.installed) {
255
267
  const v = bareVersion(appl.version);
256
- row('✓', 'iTerm', v ? `${v} · running · AppleScript reachable` : 'running · AppleScript reachable');
268
+ row('✓', 'iTerm', v ? `${v} · AppleScript OK` : 'AppleScript OK');
257
269
  }
258
270
  else {
259
271
  row('⚠', 'iTerm', appl.version ? `AppleScript blocked: ${appl.version}` : 'AppleScript blocked');
@@ -261,29 +273,60 @@ export async function runDoctorCommand(args = [], deps = {}) {
261
273
  }
262
274
  }
263
275
  }
264
- section('CLAUDE CODE');
276
+ section('MCP');
265
277
  const statusSource = deps.nonbotMcp ?? (deps.checkBinary ? null : () => nonbotMcpStatuses({ fetchImpl: deps.fetchImpl }));
266
278
  const statuses = statusSource ? await statusSource().catch(() => []) : null;
267
279
  const nonbotMcp = statuses ? { installed: statuses.length > 0 } : checkBinary('mcp:nonbot');
268
280
  if (statuses && statuses.length > 0) {
269
- const r = mcpRow(statuses[0]);
270
- row(r.sigil, 'nonbot MCP', r.detail);
281
+ const primary = statuses.find((s) => s.entry.scope && s.entry.scope !== 'desktop') ?? statuses[0];
282
+ const r = mcpRow(primary);
283
+ row(r.sigil, 'Installed', r.detail);
271
284
  if (r.hint)
272
285
  hint(r.hint);
286
+ const startSource = deps.mcpStart ??
287
+ (deps.checkBinary
288
+ ? null
289
+ : async (entry) => {
290
+ const spec = launchSpecOf(entry);
291
+ if (!spec)
292
+ return { state: 'unknown' };
293
+ if (!mayStartEntry(entry, spec))
294
+ return { state: 'unapproved' };
295
+ return { state: 'started', result: await probeMcp(spec, { fetchImpl: deps.fetchImpl, fallbackBearer: auth.pat, timeoutMs: 20_000 }) };
296
+ });
297
+ if (startSource) {
298
+ const s = await startSource(primary.entry).catch(() => ({ state: 'unknown' }));
299
+ if (s.state === 'started' && s.result.ok) {
300
+ const took = s.result.ms < 1000 ? `${s.result.ms}ms` : `${(s.result.ms / 1000).toFixed(1)}s`;
301
+ const v = s.result.serverVersion;
302
+ const differs = v && primary.installed && v !== primary.installed ? ` · but reports v${v}` : '';
303
+ row(differs ? '⚠' : '✓', 'Starts', `${s.result.tools.length} tools in ${took}${differs}`);
304
+ }
305
+ else if (s.state === 'started') {
306
+ row('✗', 'Starts', s.result.error ?? 'it did not answer');
307
+ hint('see it fail: nonbot mcp test');
308
+ }
309
+ else if (s.state === 'unapproved') {
310
+ row('ℹ', 'Starts', `not started — ${primary.entry.source} is a project file`);
311
+ hint('approve it once: nonbot mcp test --include-project');
312
+ }
313
+ else {
314
+ row('ℹ', 'Starts', 'not checked — could not read how it is launched');
315
+ }
316
+ }
273
317
  }
274
318
  else if (nonbotMcp.installed) {
275
319
  const how = nonbotMcp.version ?? 'configured (@nonbot/mcp)';
276
- row(/pinned/.test(how) ? '⚠' : '✓', 'nonbot MCP', how);
320
+ row(/pinned/.test(how) ? '⚠' : '✓', 'Installed', how);
277
321
  }
278
322
  else {
279
- row('⚠', 'nonbot MCP', 'not set up — runs cannot report back without it');
280
- hint(`add it: ${NONBOT_MCP_ADD_HINT}`);
323
+ row('⚠', 'Not set up', 'runs cannot report back without the non.bot MCP');
324
+ hint('add it: nonbot mcp add');
281
325
  }
282
- for (const name of LEGACY_MCPS) {
283
- const legacy = checkBinary(`mcp:${name}`);
284
- if (legacy.installed) {
285
- row('ℹ', name, legacy.version ? `retired — safe to delete from ${legacy.version}` : 'retired — safe to remove');
286
- }
326
+ const legacy = LEGACY_MCPS.map((name) => ({ name, r: checkBinary(`mcp:${name}`) })).filter((l) => l.r.installed);
327
+ if (legacy.length) {
328
+ const files = [...new Set(legacy.map((l) => l.r.version).filter((v) => !!v))];
329
+ row('ℹ', 'Retired', `${legacy.map((l) => l.name).join(', ')}${files.length === 1 ? ` in ${files[0]}` : ''} · remove: nonbot mcp clean`);
287
330
  }
288
331
  for (const name of EXPECTED_SKILLS) {
289
332
  const r = checkBinary(`skill:${name}`);
@@ -294,9 +337,9 @@ export async function runDoctorCommand(args = [], deps = {}) {
294
337
  }
295
338
  section('COMMANDS');
296
339
  log(commandRow('nonbot daemon', 'start listening for runs'));
297
- log(commandRow('Ctrl-C', 'stop the daemon (in its window)'));
298
- log(commandRow('nonbot daemon stop', 'stop it from any other terminal'));
299
- log(commandRow('nonbot logout', 'sign out on this machine and stop the daemon'));
340
+ log(commandRow('Ctrl-C', 'stop it · from another terminal: nonbot daemon stop'));
341
+ log(commandRow('nonbot mcp', 'the full MCP check'));
342
+ log(commandRow('nonbot logout', 'sign out and stop the daemon'));
300
343
  return exitCode;
301
344
  }
302
345
  const LABEL_WIDTH = 14;
@@ -308,7 +351,7 @@ function paintSection(title) {
308
351
  return isTTY(process.stdout) ? ` ${ANSI.bold}${title}${ANSI.reset}` : ` ${title}`;
309
352
  }
310
353
  function commandRow(cmd, what) {
311
- const padded = cmd.padEnd(LABEL_WIDTH + 6, ' ');
354
+ const padded = cmd.padEnd(LABEL_WIDTH + 1, ' ');
312
355
  if (!isTTY(process.stdout))
313
356
  return ` ${padded}${what}\n`;
314
357
  return ` ${ANSI.cyan}${padded}${ANSI.reset}${ANSI.dim}${what}${ANSI.reset}\n`;
@@ -73,7 +73,7 @@ export async function runLogsCommand(args = [], deps = {}) {
73
73
  const opts = parseArgs(args);
74
74
  if (opts.error) {
75
75
  errLog(statusRow('✗', 'nonbot logs', opts.error, { stream: process.stderr }) + '\n');
76
- errLog(' Usage: nonbot logs [--limit <n>] [--json] [--follow|-f]\n');
76
+ errLog(' To use: nonbot logs [--limit <n>] [--json] [--follow|-f]\n');
77
77
  return 1;
78
78
  }
79
79
  const entries = await readActivityLog(opts.limit);