@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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
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.16 (2026-11-11)
|
|
7
|
+
|
|
8
|
+
- 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.
|
|
9
|
+
- 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.
|
|
10
|
+
- `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.
|
|
11
|
+
- 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.
|
|
12
|
+
- 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).
|
|
13
|
+
|
|
14
|
+
## 0.10.15 (2026-11-11)
|
|
15
|
+
|
|
16
|
+
- New: `nonbot logout` signs you out on this machine and stops the daemon.
|
|
17
|
+
- New: `nonbot update` installs the latest version. The daemon's update notice now points at it.
|
|
18
|
+
- The daemon's startup list now says how to stop it (Ctrl-C in its window, or `nonbot daemon stop`) and how to sign out.
|
|
19
|
+
- `nonbot doctor` is easier to read: grouped sections, bare version numbers, no wrapped lines, no rows for retired MCP servers, and a short list of commands at the end. If Run is switched off for your account, it now says so and links to where you switch it on, instead of showing "HTTP 403".
|
|
20
|
+
- `nonbot doctor` now finds the non.bot MCP in a project's `.mcp.json` and in Claude Desktop, not only in `~/.claude.json`, and shows its version and how it is installed (npx, remote, global or a source checkout).
|
|
21
|
+
- New: `nonbot update mcp` shows the non.bot MCP's version and updates it. A global install is updated with npm. The npx setup updates itself each time Claude Code starts it and the remote server is always current, so for those it tells you what to do instead of running anything.
|
|
22
|
+
- The non.bot block wordmark is back with a shaded aqua → violet → orchid gradient, on the daemon and on the bare `nonbot` screen. The dot takes the same gradient as the letters beside it.
|
|
23
|
+
- Typing `nonbot` alone shows the wordmark and a short start-here list (login · daemon · doctor · logout · update). `nonbot --help` is grouped, fits 80 columns, and no longer lists the commands only Claude Code runs.
|
|
24
|
+
|
|
6
25
|
## 0.10.14 (2026-11-11)
|
|
7
26
|
|
|
8
27
|
- The README now lists what the daemon sends from your machine and who can read it.
|
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
|
|
14
|
-
a limited, non-exclusive,
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
@@ -58,10 +58,10 @@ nonbot daemon
|
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
`nonbot doctor` is the one-stop health check and the first thing to run when something
|
|
61
|
-
looks off. It reports your
|
|
62
|
-
heartbeat, which
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
looks off. It reports your login, server reachability, whether the Run app is on for
|
|
62
|
+
your account, the last daemon heartbeat, which agent CLIs are installed, which terminal
|
|
63
|
+
Runs open in, the macOS Automation state for iTerm, and whether the `nonbot` MCP server
|
|
64
|
+
is set up in Claude Code — then lists how to start, stop and sign out.
|
|
65
65
|
|
|
66
66
|
Non-interactive login (for CI or scripting):
|
|
67
67
|
|
|
@@ -73,11 +73,19 @@ nonbot login --token "$NONBOT_PAT"
|
|
|
73
73
|
|
|
74
74
|
```
|
|
75
75
|
nonbot login Log in with a Personal Access Token
|
|
76
|
+
nonbot logout Sign out on this machine and stop the daemon
|
|
76
77
|
nonbot daemon Run the poll loop and launch terminals (the main command)
|
|
77
78
|
nonbot run <id> Launch a single Run by id, then exit
|
|
78
79
|
nonbot status Show login + daemon status
|
|
79
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)
|
|
80
86
|
nonbot logs Show recent local Run launches
|
|
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
|
|
81
89
|
nonbot profiles List configured accounts
|
|
82
90
|
nonbot choir <session> Coordinated-run engine — one agent per git worktree, merged back by hand
|
|
83
91
|
nonbot run-mcp Per-pane MCP server over stdio (launched by .mcp.json, not by hand)
|
|
@@ -95,7 +103,7 @@ nonbot --version Print the version
|
|
|
95
103
|
| `--profile <name>` | Use a named account instead of the default (works anywhere in the command) |
|
|
96
104
|
| `daemon --install` / `--uninstall` | Run the daemon automatically at login (macOS launchd / Linux systemd user unit; not available on Windows) |
|
|
97
105
|
| `daemon --cloud` | Containerized runner mode — see **Cloud runners** below |
|
|
98
|
-
| `daemon stop` | Stop the local daemon; `--machine <id>` / `--all` stop daemons on other machines |
|
|
106
|
+
| `daemon stop` | Stop the local daemon from another terminal (or press Ctrl-C in the daemon's own window); `--machine <id>` / `--all` stop daemons on other machines |
|
|
99
107
|
|
|
100
108
|
## Cost ceilings
|
|
101
109
|
|
|
@@ -133,7 +141,7 @@ daemon asks the npm registry for the latest `@nonbot/cli`. If it is newer than t
|
|
|
133
141
|
running, it prints a single line:
|
|
134
142
|
|
|
135
143
|
```
|
|
136
|
-
Update available: @nonbot/cli 0.9.13 → 0.9.14 (
|
|
144
|
+
Update available: @nonbot/cli 0.9.13 → 0.9.14 (run: nonbot update)
|
|
137
145
|
```
|
|
138
146
|
|
|
139
147
|
The probe has a 3-second timeout and any failure is silent. Set
|
|
@@ -181,6 +189,13 @@ are redacted. Redaction is pattern-based; treat a snapshot as terminal output.
|
|
|
181
189
|
saved end-of-run output in the local history (`nonbot logs`). A cloud runner
|
|
182
190
|
(`--cloud`) additionally registers its machine id and its allowed repository origins.
|
|
183
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
|
+
|
|
184
199
|
To show a neutral name instead of the host name, set `NONBOT_MACHINE_NAME` before
|
|
185
200
|
starting the daemon (up to 64 characters):
|
|
186
201
|
|
package/dist/commands/daemon.js
CHANGED
|
@@ -572,6 +572,8 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
572
572
|
['LISTENING', `${hostUrl}/api/cli/activations/pending`],
|
|
573
573
|
['PROFILE', profileName],
|
|
574
574
|
['LANDING', terminalLabel],
|
|
575
|
+
['STOP', 'Ctrl-C in this window · or nonbot daemon stop from another terminal'],
|
|
576
|
+
['SIGN OUT', 'nonbot logout · signs out on this machine and stops the daemon'],
|
|
575
577
|
];
|
|
576
578
|
const LABEL_WIDTH = 9;
|
|
577
579
|
for (const [k, v] of ctxRows) {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -3,66 +3,19 @@ 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, LEGACY_MCP_NAMES } from '../lib/mcp-config.js';
|
|
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';
|
|
6
11
|
import { resolveTerminal } from '../lib/terminal.js';
|
|
7
12
|
import { VERSION } from '../version.js';
|
|
8
|
-
import { header, statusRow, errorBlock } from '../lib/output.js';
|
|
13
|
+
import { header, statusRow, errorBlock, isTTY, ANSI, wordmarkArt, WORDMARK_WIDTH } from '../lib/output.js';
|
|
9
14
|
const HEARTBEAT_FRESH_MS = 35_000;
|
|
10
15
|
const PROVIDER_CLIS = ['claude', 'codex', 'gemini'];
|
|
11
|
-
const NONBOT_MCP_ADD_HINT = 'claude mcp add nonbot
|
|
12
|
-
const
|
|
13
|
-
{
|
|
14
|
-
name: 'nonbot',
|
|
15
|
-
required: true,
|
|
16
|
-
present: '@nonbot/mcp',
|
|
17
|
-
absent: `not configured in ~/.claude.json — ${NONBOT_MCP_ADD_HINT}`,
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
name: 'portfolio-mcp',
|
|
21
|
-
required: false,
|
|
22
|
-
present: 'legacy — superseded by @nonbot/mcp',
|
|
23
|
-
absent: 'optional — not configured (legacy, superseded by @nonbot/mcp)',
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
name: 'context-graph',
|
|
27
|
-
required: false,
|
|
28
|
-
present: 'legacy — superseded by @nonbot/mcp',
|
|
29
|
-
absent: 'optional — not configured (legacy, superseded by @nonbot/mcp)',
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: 'jobshop-mcp',
|
|
33
|
-
required: false,
|
|
34
|
-
present: 'optional — JobShop is hidden for launch',
|
|
35
|
-
absent: 'optional — not configured (JobShop is hidden for launch)',
|
|
36
|
-
},
|
|
37
|
-
];
|
|
16
|
+
export const NONBOT_MCP_ADD_HINT = 'claude mcp add nonbot -- npx -y @nonbot/mcp';
|
|
17
|
+
const LEGACY_MCPS = LEGACY_MCP_NAMES;
|
|
38
18
|
const EXPECTED_SKILLS = [];
|
|
39
|
-
function readConfiguredMcpNames() {
|
|
40
|
-
const out = new Set();
|
|
41
|
-
try {
|
|
42
|
-
const path = join(homedir(), '.claude.json');
|
|
43
|
-
if (!existsSync(path))
|
|
44
|
-
return out;
|
|
45
|
-
const json = JSON.parse(readFileSync(path, 'utf-8'));
|
|
46
|
-
const projects = json?.projects && typeof json.projects === 'object' ? json.projects : null;
|
|
47
|
-
if (projects) {
|
|
48
|
-
for (const proj of Object.values(projects)) {
|
|
49
|
-
const servers = proj?.mcpServers;
|
|
50
|
-
if (servers && typeof servers === 'object') {
|
|
51
|
-
for (const name of Object.keys(servers))
|
|
52
|
-
out.add(name);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
const top = json?.mcpServers;
|
|
57
|
-
if (top && typeof top === 'object') {
|
|
58
|
-
for (const name of Object.keys(top))
|
|
59
|
-
out.add(name);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
}
|
|
64
|
-
return out;
|
|
65
|
-
}
|
|
66
19
|
function isSkillInstalled(skillName) {
|
|
67
20
|
const base = join(homedir(), '.claude');
|
|
68
21
|
const candidates = [];
|
|
@@ -136,7 +89,12 @@ function probe(cmd, args, interpret = (out) => ({
|
|
|
136
89
|
function defaultCheckBinary(bin) {
|
|
137
90
|
if (bin.startsWith('mcp:')) {
|
|
138
91
|
const name = bin.slice('mcp:'.length);
|
|
139
|
-
|
|
92
|
+
const scan = scanMcpConfigs();
|
|
93
|
+
if (name === 'nonbot') {
|
|
94
|
+
const first = scan.nonbot[0];
|
|
95
|
+
return first ? { installed: true, version: describeNonbotMcp(first) } : { installed: false };
|
|
96
|
+
}
|
|
97
|
+
return scan.names.has(name) ? { installed: true, version: scan.where.get(name) } : { installed: false };
|
|
140
98
|
}
|
|
141
99
|
if (bin.startsWith('skill:')) {
|
|
142
100
|
const name = bin.slice('skill:'.length);
|
|
@@ -160,6 +118,15 @@ function defaultCheckBinary(bin) {
|
|
|
160
118
|
return { installed: false };
|
|
161
119
|
}
|
|
162
120
|
}
|
|
121
|
+
export function bareVersion(raw) {
|
|
122
|
+
if (!raw)
|
|
123
|
+
return '';
|
|
124
|
+
const first = raw.split('\n')[0].trim();
|
|
125
|
+
const m = first.match(/\d+(?:\.\d+)+[0-9A-Za-z.+-]*/);
|
|
126
|
+
if (m)
|
|
127
|
+
return m[0];
|
|
128
|
+
return first.length > 40 ? first.slice(0, 39) + '…' : first;
|
|
129
|
+
}
|
|
163
130
|
export async function runDoctorCommand(args = [], deps = {}) {
|
|
164
131
|
void args;
|
|
165
132
|
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
@@ -169,46 +136,50 @@ export async function runDoctorCommand(args = [], deps = {}) {
|
|
|
169
136
|
const now = deps.now ?? (() => Date.now());
|
|
170
137
|
const checkBinary = deps.checkBinary ?? defaultCheckBinary;
|
|
171
138
|
let exitCode = 0;
|
|
172
|
-
log(
|
|
173
|
-
|
|
139
|
+
const row = (sig, label, detail = '') => log(statusRow(sig, label, detail, { labelWidth: LABEL_WIDTH }) + '\n');
|
|
140
|
+
const errRow = (sig, label, detail = '') => errLog(statusRow(sig, label, detail, { labelWidth: LABEL_WIDTH, stream: process.stderr }) + '\n');
|
|
141
|
+
const hint = (text) => log(' '.repeat(HINT_INDENT) + paintDim(text) + '\n');
|
|
142
|
+
const section = (title) => log('\n' + paintSection(title) + '\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
|
+
}
|
|
174
152
|
const auth = await loader();
|
|
175
153
|
if (!auth) {
|
|
154
|
+
log('\n');
|
|
176
155
|
log(errorBlock('Not logged in', 'Run `nonbot login` to authenticate.'));
|
|
177
156
|
return 1;
|
|
178
157
|
}
|
|
179
|
-
|
|
180
|
-
|
|
158
|
+
section('ACCOUNT');
|
|
159
|
+
row('✓', 'Signed in', auth.email);
|
|
160
|
+
const headers = {
|
|
161
|
+
Authorization: `Bearer ${auth.pat}`,
|
|
162
|
+
'X-Requested-With': 'ConradPM-Native',
|
|
163
|
+
'X-CLI-Version': VERSION,
|
|
164
|
+
};
|
|
181
165
|
try {
|
|
182
|
-
const res = await fetchImpl(`${auth.baseUrl}/api/auth/me`, {
|
|
183
|
-
headers: {
|
|
184
|
-
Authorization: `Bearer ${auth.pat}`,
|
|
185
|
-
'X-Requested-With': 'ConradPM-Native',
|
|
186
|
-
'X-CLI-Version': VERSION,
|
|
187
|
-
},
|
|
188
|
-
});
|
|
166
|
+
const res = await fetchImpl(`${auth.baseUrl}/api/auth/me`, { headers });
|
|
189
167
|
if (res.ok) {
|
|
190
|
-
|
|
191
|
-
log(statusRow('✓', 'Server reachable', `${auth.baseUrl} · PAT valid`) + '\n');
|
|
168
|
+
row('✓', 'Server', `${auth.baseUrl.replace(/^https?:\/\//, '')} · token accepted`);
|
|
192
169
|
}
|
|
193
170
|
else if (res.status === 401) {
|
|
194
|
-
|
|
171
|
+
errRow('✗', 'Server', 'token rejected (HTTP 401) — sign in again: nonbot login');
|
|
195
172
|
exitCode = 1;
|
|
196
173
|
}
|
|
197
174
|
else {
|
|
198
|
-
|
|
175
|
+
row('⚠', 'Server', `${auth.baseUrl} answered HTTP ${res.status}`);
|
|
199
176
|
}
|
|
200
177
|
}
|
|
201
178
|
catch (e) {
|
|
202
|
-
|
|
179
|
+
row('⚠', 'Server', `Could not reach ${auth.baseUrl} (${e.message})`);
|
|
203
180
|
}
|
|
204
181
|
try {
|
|
205
|
-
const res = await fetchImpl(`${auth.baseUrl}/api/cli/daemon-status`, {
|
|
206
|
-
headers: {
|
|
207
|
-
Authorization: `Bearer ${auth.pat}`,
|
|
208
|
-
'X-Requested-With': 'ConradPM-Native',
|
|
209
|
-
'X-CLI-Version': VERSION,
|
|
210
|
-
},
|
|
211
|
-
});
|
|
182
|
+
const res = await fetchImpl(`${auth.baseUrl}/api/cli/daemon-status`, { headers });
|
|
212
183
|
if (res.ok) {
|
|
213
184
|
const body = (await res.json());
|
|
214
185
|
const last = body.lastSeenAt ?? body.lastHeartbeatAt;
|
|
@@ -216,86 +187,172 @@ export async function runDoctorCommand(args = [], deps = {}) {
|
|
|
216
187
|
(typeof last === 'number' && now() - last <= HEARTBEAT_FRESH_MS);
|
|
217
188
|
if (fresh) {
|
|
218
189
|
const detail = typeof last === 'number'
|
|
219
|
-
? `last heartbeat ${Math.round((now() - last) / 1000)}s ago`
|
|
220
|
-
: '
|
|
221
|
-
|
|
190
|
+
? `running · last heartbeat ${Math.round((now() - last) / 1000)}s ago`
|
|
191
|
+
: 'running';
|
|
192
|
+
row('✓', 'Daemon', detail);
|
|
222
193
|
}
|
|
223
194
|
else {
|
|
224
|
-
|
|
195
|
+
row('✗', 'Daemon', 'not running — start it: nonbot daemon');
|
|
225
196
|
}
|
|
226
197
|
}
|
|
227
198
|
else if (res.status === 401) {
|
|
228
|
-
|
|
199
|
+
row('⚠', 'Daemon', 'status unknown (auth rejected)');
|
|
200
|
+
}
|
|
201
|
+
else if (res.status === 403) {
|
|
202
|
+
let gate = {};
|
|
203
|
+
try {
|
|
204
|
+
gate = (await res.json());
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
}
|
|
208
|
+
const storeUrl = `${auth.baseUrl}/whygraph/store?tab=apps&app=${gate.app ?? 'run'}`;
|
|
209
|
+
if (gate.error === 'app_disabled') {
|
|
210
|
+
row('✗', 'Run app', 'switched off — the daemon gets no runs');
|
|
211
|
+
hint(`turn it on: ${storeUrl}`);
|
|
212
|
+
}
|
|
213
|
+
else if (gate.error === 'app_not_installed') {
|
|
214
|
+
row('✗', 'Run app', 'not added — the daemon gets no runs until it is');
|
|
215
|
+
hint(`add it: ${storeUrl}`);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
row('⚠', 'Daemon', 'status unknown (HTTP 403)');
|
|
219
|
+
}
|
|
229
220
|
}
|
|
230
221
|
else {
|
|
231
|
-
|
|
222
|
+
row('⚠', 'Daemon', `status unknown (HTTP ${res.status})`);
|
|
232
223
|
}
|
|
233
224
|
}
|
|
234
225
|
catch (e) {
|
|
235
|
-
|
|
226
|
+
row('⚠', 'Daemon', `status unknown (${e.message})`);
|
|
236
227
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
228
|
+
section('AGENTS');
|
|
229
|
+
const providers = PROVIDER_CLIS.map((bin) => ({ bin, r: checkBinary(bin) }));
|
|
230
|
+
const anyProvider = providers.some((p) => p.r.installed);
|
|
231
|
+
const ordered = [...providers.filter((p) => p.r.installed), ...providers.filter((p) => !p.r.installed)];
|
|
232
|
+
for (const { bin, r } of ordered) {
|
|
233
|
+
if (r.installed)
|
|
234
|
+
row('✓', bin, bareVersion(r.version));
|
|
235
|
+
else if (anyProvider)
|
|
236
|
+
row('ℹ', bin, 'not installed (optional)');
|
|
237
|
+
else
|
|
238
|
+
row('⚠', bin, 'not on PATH');
|
|
245
239
|
}
|
|
240
|
+
if (!anyProvider)
|
|
241
|
+
hint('Runs need one of these — install Claude Code: npm i -g @anthropic-ai/claude-code');
|
|
242
|
+
section('TERMINAL');
|
|
246
243
|
const profile = resolveTerminal(undefined);
|
|
247
244
|
const { cmd } = profile.launch('/tmp/nonbot-doctor-probe.sh');
|
|
248
245
|
const launcher = checkBinary(cmd);
|
|
249
246
|
if (launcher.installed) {
|
|
250
|
-
|
|
247
|
+
row('✓', 'Opens runs in', `${profile.displayName} (via ${cmd})`);
|
|
251
248
|
}
|
|
252
249
|
else {
|
|
253
|
-
|
|
250
|
+
row('⚠', 'Terminal', `${profile.displayName} — launcher "${cmd}" not found on PATH`);
|
|
254
251
|
}
|
|
255
252
|
if (process.platform === 'win32') {
|
|
256
253
|
const bash = checkBinary('bash');
|
|
257
|
-
if (bash.installed)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
log(statusRow('⚠', 'Windows via WSL or Git Bash', 'bash not found on PATH — install WSL or Git Bash; Run scripts are #!/bin/bash') + '\n');
|
|
262
|
-
}
|
|
254
|
+
if (bash.installed)
|
|
255
|
+
row('✓', 'bash', 'found · Windows via WSL or Git Bash — runs are #!/bin/bash');
|
|
256
|
+
else
|
|
257
|
+
row('⚠', 'bash', 'not found · Windows via WSL or Git Bash — runs are #!/bin/bash');
|
|
263
258
|
}
|
|
264
259
|
if (process.platform === 'darwin') {
|
|
265
260
|
const running = checkBinary('iterm-running');
|
|
266
|
-
if (running.installed) {
|
|
267
|
-
|
|
261
|
+
if (!running.installed) {
|
|
262
|
+
row('ℹ', 'iTerm', 'not running — only needed if runs open in iTerm');
|
|
268
263
|
}
|
|
269
264
|
else {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
265
|
+
const appl = checkBinary('iterm-applescript');
|
|
266
|
+
if (appl.installed) {
|
|
267
|
+
const v = bareVersion(appl.version);
|
|
268
|
+
row('✓', 'iTerm', v ? `${v} · AppleScript OK` : 'AppleScript OK');
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
row('⚠', 'iTerm', appl.version ? `AppleScript blocked: ${appl.version}` : 'AppleScript blocked');
|
|
272
|
+
hint('System Settings → Privacy & Security → Automation → allow your terminal to control iTerm');
|
|
273
|
+
}
|
|
279
274
|
}
|
|
280
275
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
276
|
+
section('MCP');
|
|
277
|
+
const statusSource = deps.nonbotMcp ?? (deps.checkBinary ? null : () => nonbotMcpStatuses({ fetchImpl: deps.fetchImpl }));
|
|
278
|
+
const statuses = statusSource ? await statusSource().catch(() => []) : null;
|
|
279
|
+
const nonbotMcp = statuses ? { installed: statuses.length > 0 } : checkBinary('mcp:nonbot');
|
|
280
|
+
if (statuses && statuses.length > 0) {
|
|
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);
|
|
284
|
+
if (r.hint)
|
|
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
|
+
}
|
|
288
316
|
}
|
|
289
317
|
}
|
|
318
|
+
else if (nonbotMcp.installed) {
|
|
319
|
+
const how = nonbotMcp.version ?? 'configured (@nonbot/mcp)';
|
|
320
|
+
row(/pinned/.test(how) ? '⚠' : '✓', 'Installed', how);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
row('⚠', 'Not set up', 'runs cannot report back without the non.bot MCP');
|
|
324
|
+
hint('add it: nonbot mcp add');
|
|
325
|
+
}
|
|
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`);
|
|
330
|
+
}
|
|
290
331
|
for (const name of EXPECTED_SKILLS) {
|
|
291
332
|
const r = checkBinary(`skill:${name}`);
|
|
292
|
-
if (r.installed)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
log(statusRow('⚠', `Skill ${name}`, 'not installed under ~/.claude/skills/') + '\n');
|
|
297
|
-
}
|
|
333
|
+
if (r.installed)
|
|
334
|
+
row('✓', `Skill ${name}`);
|
|
335
|
+
else
|
|
336
|
+
row('⚠', `Skill ${name}`, 'not installed under ~/.claude/skills/');
|
|
298
337
|
}
|
|
299
|
-
|
|
338
|
+
section('COMMANDS');
|
|
339
|
+
log(commandRow('nonbot daemon', 'start listening for runs'));
|
|
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
|
}
|
|
345
|
+
const LABEL_WIDTH = 14;
|
|
346
|
+
const HINT_INDENT = 3 + 1 + 2 + LABEL_WIDTH + 1;
|
|
347
|
+
function paintDim(s) {
|
|
348
|
+
return isTTY(process.stdout) ? `${ANSI.dim}${s}${ANSI.reset}` : s;
|
|
349
|
+
}
|
|
350
|
+
function paintSection(title) {
|
|
351
|
+
return isTTY(process.stdout) ? ` ${ANSI.bold}${title}${ANSI.reset}` : ` ${title}`;
|
|
352
|
+
}
|
|
353
|
+
function commandRow(cmd, what) {
|
|
354
|
+
const padded = cmd.padEnd(LABEL_WIDTH + 1, ' ');
|
|
355
|
+
if (!isTTY(process.stdout))
|
|
356
|
+
return ` ${padded}${what}\n`;
|
|
357
|
+
return ` ${ANSI.cyan}${padded}${ANSI.reset}${ANSI.dim}${what}${ANSI.reset}\n`;
|
|
358
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { spawnSync as nodeSpawnSync } from 'node:child_process';
|
|
2
|
+
import { loadAuth, clearAuth, getActiveProfile } from '../lib/auth.js';
|
|
3
|
+
import { readPidFile, stopDaemonLocally } from '../lib/daemon-lifecycle.js';
|
|
4
|
+
import { header, statusRow } from '../lib/output.js';
|
|
5
|
+
function defaultDaemonRunning() {
|
|
6
|
+
const pid = readPidFile();
|
|
7
|
+
if (pid !== null) {
|
|
8
|
+
try {
|
|
9
|
+
process.kill(pid, 0);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const r = nodeSpawnSync('tmux', ['has-session', '-t', 'nonbot'], {
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
timeout: 2000,
|
|
19
|
+
windowsHide: true,
|
|
20
|
+
});
|
|
21
|
+
return r.status === 0;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function runLogoutCommand(args = [], deps = {}) {
|
|
28
|
+
void args;
|
|
29
|
+
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
30
|
+
const errLog = deps.errLog ?? ((s) => process.stderr.write(s));
|
|
31
|
+
const loader = deps.loadAuth ?? loadAuth;
|
|
32
|
+
const clear = deps.clearAuth ?? clearAuth;
|
|
33
|
+
const daemonRunning = deps.daemonRunning ?? defaultDaemonRunning;
|
|
34
|
+
const stopDaemon = deps.stopDaemon ?? ((l, e) => stopDaemonLocally({ log: l, errLog: e }));
|
|
35
|
+
const profile = getActiveProfile();
|
|
36
|
+
log(header('non.bot logout', profile === 'default' ? '' : `profile ${profile}`) + '\n\n');
|
|
37
|
+
const auth = await loader();
|
|
38
|
+
const running = daemonRunning();
|
|
39
|
+
if (!auth && !running) {
|
|
40
|
+
log(statusRow('✓', 'Already signed out', 'nothing saved on this machine, no daemon running') + '\n');
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
let exitCode = 0;
|
|
44
|
+
if (auth) {
|
|
45
|
+
try {
|
|
46
|
+
await clear();
|
|
47
|
+
log(statusRow('✓', 'Signed out', `${auth.email} · token removed from this machine`) + '\n');
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
errLog(statusRow('✗', 'Sign-out failed', `could not remove the saved token: ${e.message}`, { stream: process.stderr }) + '\n');
|
|
51
|
+
exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
log(statusRow('✓', 'Signed out', 'no token was saved on this machine') + '\n');
|
|
56
|
+
}
|
|
57
|
+
if (running) {
|
|
58
|
+
const said = [];
|
|
59
|
+
const code = await stopDaemon((s) => said.push(s), (s) => said.push(s));
|
|
60
|
+
if (code === 0) {
|
|
61
|
+
log(statusRow('✓', 'Daemon stopped', '') + '\n');
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const why = said.join(' ').replace(/\s+/g, ' ').replace(/^[✓✗⚠]\s*/, '').trim();
|
|
65
|
+
errLog(statusRow('⚠', 'Daemon still running', why || 'stop it with Ctrl-C in its window', { stream: process.stderr }) + '\n');
|
|
66
|
+
exitCode = 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
log(statusRow('✓', 'Daemon', 'was not running') + '\n');
|
|
71
|
+
}
|
|
72
|
+
if (auth) {
|
|
73
|
+
log('\n');
|
|
74
|
+
log(statusRow('ℹ', 'Token', `still exists on non.bot — delete it for good at ${auth.baseUrl}/settings/connections`) + '\n');
|
|
75
|
+
log(statusRow('ℹ', 'Sign back in', 'nonbot login') + '\n');
|
|
76
|
+
}
|
|
77
|
+
return exitCode;
|
|
78
|
+
}
|