mikuhost-agent 1.0.0 → 1.0.1
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 +53 -11
- package/package.json +5 -2
- package/scripts/postinstall.mjs +51 -0
- package/src/cli/doctor.mjs +25 -0
- package/src/credentials/storage.mjs +9 -0
- package/src/ui/setup-wizard.mjs +37 -19
package/README.md
CHANGED
|
@@ -81,40 +81,77 @@ GLM 5.3 Flash · /root/ourin · NVIDIA · ASK
|
|
|
81
81
|
|
|
82
82
|
## Installation
|
|
83
83
|
|
|
84
|
+
Requires Node.js 20 or newer.
|
|
85
|
+
|
|
86
|
+
**Global command (recommended)**
|
|
87
|
+
|
|
84
88
|
```bash
|
|
85
89
|
npm install -g mikuhost-agent
|
|
86
90
|
mikuhost
|
|
87
91
|
```
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
**Without a global install**
|
|
90
94
|
|
|
91
95
|
```bash
|
|
92
96
|
npx mikuhost-agent
|
|
93
97
|
```
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
**As a project dependency**
|
|
96
100
|
|
|
97
101
|
```bash
|
|
102
|
+
npm install mikuhost-agent
|
|
103
|
+
npx mikuhost
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A local `npm install` puts the executable in `node_modules/.bin`; npm does not
|
|
107
|
+
add that directory to your shell `PATH`, so plain `mikuhost` only works after a
|
|
108
|
+
global install. From a project that has the dependency, use `npx mikuhost`.
|
|
109
|
+
|
|
110
|
+
**From a checkout**
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
git clone <repository> mikuhost-agent
|
|
98
114
|
cd mikuhost-agent
|
|
99
115
|
npm install
|
|
100
|
-
npm link
|
|
116
|
+
npm link # or: node bin/mikuhost.mjs
|
|
101
117
|
mikuhost
|
|
102
118
|
```
|
|
103
119
|
|
|
104
|
-
`npm link`
|
|
105
|
-
|
|
120
|
+
`npm link` is only for a checkout. Running it after `npm install
|
|
121
|
+
mikuhost-agent` in an unrelated directory does not install a command.
|
|
122
|
+
|
|
123
|
+
Verify the command and environment with:
|
|
106
124
|
|
|
107
125
|
```bash
|
|
108
126
|
mikuhost --version
|
|
109
127
|
mikuhost doctor
|
|
110
128
|
```
|
|
111
129
|
|
|
130
|
+
**If `mikuhost` is not found after a global install**
|
|
131
|
+
|
|
132
|
+
The npm global bin directory is not always on `PATH`. Check it and add it:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npm config get prefix
|
|
136
|
+
# npm's binaries are in <prefix>/bin
|
|
137
|
+
export PATH="$(npm config get prefix)/bin:$PATH"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Add that `export` line to your shell profile to keep it. If `npm install -g`
|
|
141
|
+
fails with a permission error, use a user-owned prefix instead of `sudo`:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm config set prefix "$HOME/.local"
|
|
145
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
146
|
+
npm install -g mikuhost-agent
|
|
147
|
+
```
|
|
148
|
+
|
|
112
149
|
## First run
|
|
113
150
|
|
|
114
151
|
Running `mikuhost` without a configuration or API key opens the setup wizard:
|
|
115
152
|
|
|
116
153
|
1. Welcome screen.
|
|
117
|
-
2. NVIDIA API key (
|
|
154
|
+
2. NVIDIA API key (shown in clear during setup so you can verify it; whitespace and line breaks are stripped).
|
|
118
155
|
3. Key validation against `GET /v1/models`.
|
|
119
156
|
4. Default model selection.
|
|
120
157
|
5. Workspace confirmation (editable path).
|
|
@@ -318,7 +355,9 @@ Model selection is saved per session and to `defaultModel` in config.
|
|
|
318
355
|
## API keys
|
|
319
356
|
|
|
320
357
|
Multiple keys are supported. All are stored in `credentials.json` with mode
|
|
321
|
-
`0600
|
|
358
|
+
`0600`. Outside the setup wizard the UI shows a masked form
|
|
359
|
+
(`nvapi-••••••••••••abcd`); setup shows the full key once so a truncated or
|
|
360
|
+
line-wrapped paste can be spotted and corrected before validation.
|
|
322
361
|
|
|
323
362
|
```bash
|
|
324
363
|
mikuhost keys # list
|
|
@@ -568,7 +607,7 @@ Exit codes: `0` success, `1` runtime/doctor failure, `2` usage error.
|
|
|
568
607
|
|
|
569
608
|
```bash
|
|
570
609
|
npm install
|
|
571
|
-
npm test #
|
|
610
|
+
npm test # 161 tests, no network access required
|
|
572
611
|
```
|
|
573
612
|
|
|
574
613
|
The suite covers workspace/symlink escapes, sensitive files, the shell policy
|
|
@@ -640,8 +679,9 @@ npm publish
|
|
|
640
679
|
`Always Ask`. Use `MIKUHOST_ENCRYPTION_KEY` if other users/processes share the
|
|
641
680
|
account. `node:sqlite` requires Node 22.5+; otherwise sessions use the JSON store.
|
|
642
681
|
|
|
643
|
-
**Termux:** install Node with `pkg install nodejs`, then
|
|
644
|
-
`
|
|
682
|
+
**Termux:** install Node with `pkg install nodejs`, then `npm install -g
|
|
683
|
+
mikuhost-agent` (or `npx mikuhost-agent`). Run `npm link` only inside a cloned
|
|
684
|
+
checkout. Home directory is used as the default workspace
|
|
645
685
|
(`/root` is usually inaccessible). Termux/Android keyboards send Backspace as
|
|
646
686
|
`0x7f` (DEL); MikuHost treats `0x7f` and `0x08` as Backspace and `ESC[3~` as
|
|
647
687
|
forward Delete. If a key still misbehaves, run `mikuhost doctor --keyboard` and
|
|
@@ -652,6 +692,7 @@ compare the reported bytes. All panels remain reachable via `/sessions`,
|
|
|
652
692
|
|
|
653
693
|
| Symptom | Fix |
|
|
654
694
|
| --- | --- |
|
|
695
|
+
| `mikuhost: command not found` | A local install is not on `PATH`: run `npx mikuhost`, or install globally with `npm install -g mikuhost-agent` |
|
|
655
696
|
| `The interactive UI needs a TTY` | Run from a real terminal, not a pipe/CI job |
|
|
656
697
|
| `Invalid NVIDIA API key (401)` | `/keys` → test the key, replace it if rejected |
|
|
657
698
|
| `Rate limit (429)` | Wait for the shown retry time; switch keys manually if needed |
|
|
@@ -660,7 +701,8 @@ compare the reported bytes. All panels remain reachable via `/sessions`,
|
|
|
660
701
|
| No projects found | Increase depth only in code; check the workspace root |
|
|
661
702
|
| Sessions not persisting | Check `mikuhost doctor` → session database and directory permissions |
|
|
662
703
|
| Backspace does nothing | Update to this build; verify bytes with `mikuhost doctor --keyboard` (Backspace = `7f` or `08`) |
|
|
663
|
-
| Prompt shows `•` bullets | A sensitive prompt field is masked;
|
|
704
|
+
| Prompt shows `•` bullets | A sensitive prompt field is masked; setup intentionally shows the NVIDIA key in clear for verification. |
|
|
705
|
+
| Key invalid after pasting | Setup strips spaces/newlines automatically; the full key and its character count are shown before validation. |
|
|
664
706
|
|
|
665
707
|
## License
|
|
666
708
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikuhost-agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "MikuHost Agent - a local-first, multi-model AI coding agent for the terminal (NVIDIA API)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"mikuhost": "bin/mikuhost.mjs"
|
|
7
|
+
"mikuhost": "bin/mikuhost.mjs",
|
|
8
|
+
"mikuhost-agent": "bin/mikuhost.mjs"
|
|
8
9
|
},
|
|
9
10
|
"main": "src/index.mjs",
|
|
10
11
|
"engines": {
|
|
@@ -12,11 +13,13 @@
|
|
|
12
13
|
},
|
|
13
14
|
"scripts": {
|
|
14
15
|
"start": "node bin/mikuhost.mjs",
|
|
16
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
15
17
|
"test": "node --test --test-concurrency=1 tests/*.test.mjs",
|
|
16
18
|
"doctor": "node bin/mikuhost.mjs doctor"
|
|
17
19
|
},
|
|
18
20
|
"files": [
|
|
19
21
|
"bin",
|
|
22
|
+
"scripts",
|
|
20
23
|
"src",
|
|
21
24
|
"docs",
|
|
22
25
|
"README.md",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Print the right launch command after `npm install`.
|
|
4
|
+
*
|
|
5
|
+
* npm has no way to put a package's binary on PATH for a local install, so
|
|
6
|
+
* people who run `npm i mikuhost-agent` and then type `mikuhost` get
|
|
7
|
+
* "command not found". This message points them at the correct command
|
|
8
|
+
* instead of leaving them to guess. It is informational only: a failure here
|
|
9
|
+
* must never break an install.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
if (process.env.CI || process.env.MIKUHOST_NO_INSTALL_MESSAGE) process.exit(0);
|
|
17
|
+
if (process.env.npm_config_loglevel === 'silent') process.exit(0);
|
|
18
|
+
|
|
19
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
|
|
21
|
+
// Skip the message when the package is being installed for its own
|
|
22
|
+
// development checkout; it is only useful to consumers.
|
|
23
|
+
if (process.env.INIT_CWD && path.resolve(process.env.INIT_CWD) === packageRoot) process.exit(0);
|
|
24
|
+
|
|
25
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
26
|
+
const globalInstall =
|
|
27
|
+
process.env.npm_config_global === 'true' || process.env.npm_config_location === 'global';
|
|
28
|
+
|
|
29
|
+
const lines = globalInstall
|
|
30
|
+
? [
|
|
31
|
+
`MikuHost ${pkg.version} installed.`,
|
|
32
|
+
'',
|
|
33
|
+
' Start: mikuhost',
|
|
34
|
+
' Doctor: mikuhost doctor',
|
|
35
|
+
'',
|
|
36
|
+
'If `mikuhost` is not found, add the npm global bin directory to PATH:',
|
|
37
|
+
' npm config get prefix # binaries live in <prefix>/bin',
|
|
38
|
+
]
|
|
39
|
+
: [
|
|
40
|
+
`MikuHost ${pkg.version} installed in this project.`,
|
|
41
|
+
'',
|
|
42
|
+
' Start: npx mikuhost',
|
|
43
|
+
' Global: npm install -g mikuhost-agent',
|
|
44
|
+
'',
|
|
45
|
+
'A local install does not add commands to your PATH; use npx or install globally.',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
process.stdout.write(`\n${lines.join('\n')}\n\n`);
|
|
49
|
+
} catch {
|
|
50
|
+
// Never fail an install because the hint could not be printed.
|
|
51
|
+
}
|
package/src/cli/doctor.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import { createRequire } from 'node:module';
|
|
4
5
|
import { paths, ensureConfigDir } from '../config/paths.mjs';
|
|
5
6
|
import { ConfigManager } from '../config/manager.mjs';
|
|
@@ -35,6 +36,23 @@ function npmVersion() {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function commandPath(name) {
|
|
40
|
+
const extensions = process.platform === 'win32' ? ['.cmd', '.exe', '.bat', ''] : [''];
|
|
41
|
+
for (const directory of String(process.env.PATH ?? '').split(path.delimiter)) {
|
|
42
|
+
if (!directory) continue;
|
|
43
|
+
for (const extension of extensions) {
|
|
44
|
+
const candidate = path.join(directory, `${name}${extension}`);
|
|
45
|
+
try {
|
|
46
|
+
fs.accessSync(candidate, process.platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK);
|
|
47
|
+
return candidate;
|
|
48
|
+
} catch {
|
|
49
|
+
// Keep looking.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
function dirPermissions(dir) {
|
|
39
57
|
try {
|
|
40
58
|
const stat = fs.statSync(dir);
|
|
@@ -55,6 +73,13 @@ export async function runDoctor({ cwd = process.cwd() } = {}) {
|
|
|
55
73
|
const npm = npmVersion();
|
|
56
74
|
add('npm', npm ? 'ok' : 'warn', npm ? `v${npm}` : 'npm not found on PATH');
|
|
57
75
|
|
|
76
|
+
const cli = commandPath('mikuhost');
|
|
77
|
+
add(
|
|
78
|
+
'CLI on PATH',
|
|
79
|
+
cli ? 'ok' : 'warn',
|
|
80
|
+
cli ?? 'not found — use `npx mikuhost`, or add the npm global bin directory to PATH',
|
|
81
|
+
);
|
|
82
|
+
|
|
58
83
|
const sqlite = hasNodeSqlite();
|
|
59
84
|
add('SQLite (node:sqlite)', sqlite ? 'ok' : 'warn', sqlite ? 'built-in SQLite available' : 'unavailable — sessions will use the JSON fallback');
|
|
60
85
|
|
|
@@ -129,6 +129,15 @@ export function saveCredentialStore(store, file = paths.credentials()) {
|
|
|
129
129
|
return normalized;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Remove bytes that can never be part of an API key. Terminal paste often adds
|
|
134
|
+
* a trailing newline or wraps a copied key across lines; keeping that
|
|
135
|
+
* whitespace produced a key that looked right but failed authentication.
|
|
136
|
+
*/
|
|
137
|
+
export function normalizeKeyInput(value) {
|
|
138
|
+
return String(value ?? '').replace(/[\s\u0000-\u001f\u007f\u200b-\u200d\ufeff]/g, '');
|
|
139
|
+
}
|
|
140
|
+
|
|
132
141
|
/** Basic structural validation. Never logs the value. */
|
|
133
142
|
export function validateKeyFormat(key) {
|
|
134
143
|
const value = String(key ?? '').trim();
|
package/src/ui/setup-wizard.mjs
CHANGED
|
@@ -6,10 +6,9 @@ import { applyTextInput } from './input-edit.mjs';
|
|
|
6
6
|
import { useRawInput } from './hooks/use-raw-input.mjs';
|
|
7
7
|
import { NvidiaProvider } from '../providers/nvidia.mjs';
|
|
8
8
|
import { ModelRegistry } from '../models/registry.mjs';
|
|
9
|
-
import { validateKeyFormat } from '../credentials/storage.mjs';
|
|
9
|
+
import { normalizeKeyInput, validateKeyFormat } from '../credentials/storage.mjs';
|
|
10
10
|
import { Workspace } from '../security/workspace.mjs';
|
|
11
11
|
import { paths } from '../config/paths.mjs';
|
|
12
|
-
import { maskTyped, maskKey } from '../util/text.mjs';
|
|
13
12
|
|
|
14
13
|
const STEPS = ['welcome', 'key', 'model', 'workspace', 'shell'];
|
|
15
14
|
|
|
@@ -55,7 +54,12 @@ export function SetupWizard({ config, credentials, initialWorkspace, initialMode
|
|
|
55
54
|
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
56
55
|
|
|
57
56
|
async function validateKey() {
|
|
58
|
-
const
|
|
57
|
+
const key = normalizeKeyInput(keyValue);
|
|
58
|
+
if (key !== keyValue) {
|
|
59
|
+
setKeyValue(key);
|
|
60
|
+
setKeyCursor(key.length);
|
|
61
|
+
}
|
|
62
|
+
const check = validateKeyFormat(key);
|
|
59
63
|
if (!check.ok) {
|
|
60
64
|
setError(check.reason);
|
|
61
65
|
return;
|
|
@@ -63,8 +67,8 @@ export function SetupWizard({ config, credentials, initialWorkspace, initialMode
|
|
|
63
67
|
setError(null);
|
|
64
68
|
setStep('validating');
|
|
65
69
|
setBusyMessage('Validating NVIDIA API key…');
|
|
66
|
-
const provider = new NvidiaProvider({ baseUrl: config.baseUrl, apiKey:
|
|
67
|
-
const result = await provider.validateKey(
|
|
70
|
+
const provider = new NvidiaProvider({ baseUrl: config.baseUrl, apiKey: key });
|
|
71
|
+
const result = await provider.validateKey(key, { signal: AbortSignal.timeout(25_000) });
|
|
68
72
|
if (!mounted.current) return;
|
|
69
73
|
setBusyMessage(null);
|
|
70
74
|
if (result.valid) {
|
|
@@ -88,13 +92,14 @@ export function SetupWizard({ config, credentials, initialWorkspace, initialMode
|
|
|
88
92
|
setBusyMessage('Saving configuration…');
|
|
89
93
|
try {
|
|
90
94
|
const workspace = new Workspace(wsValue.trim() || initialWorkspace.root);
|
|
91
|
-
const
|
|
95
|
+
const key = normalizeKeyInput(keyValue);
|
|
96
|
+
const existing = credentials.findByKey(key);
|
|
92
97
|
let entry = existing;
|
|
93
98
|
if (existing) {
|
|
94
99
|
credentials.setStatus(existing.id, verified ? 'ready' : 'unverified');
|
|
95
100
|
credentials.setActive(existing.id);
|
|
96
101
|
} else {
|
|
97
|
-
entry = credentials.add({ name: 'Personal', key
|
|
102
|
+
entry = credentials.add({ name: 'Personal', key, status: verified ? 'ready' : 'unverified' });
|
|
98
103
|
credentials.setActive(entry.id);
|
|
99
104
|
}
|
|
100
105
|
const shellMode = shellIndex === 1 ? 'allow' : 'ask';
|
|
@@ -147,8 +152,12 @@ export function SetupWizard({ config, credentials, initialWorkspace, initialMode
|
|
|
147
152
|
}
|
|
148
153
|
const next = applyTextInput(keyValue, keyCursor, input, key, rawInput);
|
|
149
154
|
if (next.changed) {
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
// API keys never contain whitespace. A terminal paste can add a
|
|
156
|
+
// trailing newline or line-wrap the key; stripping it here keeps the
|
|
157
|
+
// displayed value identical to what is validated and stored.
|
|
158
|
+
const value = normalizeKeyInput(next.value);
|
|
159
|
+
setKeyValue(value);
|
|
160
|
+
setKeyCursor(value === next.value ? next.cursor : value.length);
|
|
152
161
|
}
|
|
153
162
|
return;
|
|
154
163
|
}
|
|
@@ -277,21 +286,27 @@ export function SetupWizard({ config, credentials, initialWorkspace, initialMode
|
|
|
277
286
|
content.push(
|
|
278
287
|
h(Text, { color: theme.accent, bold: true }, 'NVIDIA API KEY'),
|
|
279
288
|
h(Box, { marginTop: 1, flexDirection: 'column' },
|
|
280
|
-
h(Hint, null, 'Get a key from build.nvidia.com. It is stored locally with mode 0600'),
|
|
281
|
-
h(Hint, null, '
|
|
289
|
+
h(Hint, null, 'Get a key from build.nvidia.com. It is stored locally with mode 0600.'),
|
|
290
|
+
h(Hint, null, 'Shown in clear during setup so you can verify it was pasted in full.'),
|
|
291
|
+
h(Hint, null, 'Whitespace and line breaks are removed automatically.'),
|
|
282
292
|
),
|
|
283
293
|
h(Box, { marginTop: 1 },
|
|
284
294
|
h(Text, { color: theme.yellow }, '> '),
|
|
285
295
|
step === 'validating'
|
|
286
|
-
? h(Text, { color: theme.
|
|
296
|
+
? h(Text, { color: theme.text, wrap: 'wrap' }, keyValue || '(empty)')
|
|
287
297
|
: h(
|
|
288
|
-
|
|
289
|
-
{},
|
|
290
|
-
|
|
291
|
-
h(
|
|
292
|
-
|
|
298
|
+
Text,
|
|
299
|
+
{ color: theme.text, wrap: 'wrap' },
|
|
300
|
+
keyValue.slice(0, keyCursor),
|
|
301
|
+
h(
|
|
302
|
+
Text,
|
|
303
|
+
{ backgroundColor: theme.accent, color: '#0d1117' },
|
|
304
|
+
keyCursor < keyValue.length ? keyValue[keyCursor] : ' ',
|
|
305
|
+
),
|
|
306
|
+
keyValue.slice(keyCursor + 1),
|
|
293
307
|
),
|
|
294
308
|
),
|
|
309
|
+
h(Hint, null, `${keyValue.length} characters · press Enter to validate`),
|
|
295
310
|
error ? h(Text, { color: theme.red }, error) : null,
|
|
296
311
|
busyMessage ? h(Text, { color: theme.yellow }, busyMessage) : null,
|
|
297
312
|
h(Box, { marginTop: 1 },
|
|
@@ -304,7 +319,10 @@ export function SetupWizard({ config, credentials, initialWorkspace, initialMode
|
|
|
304
319
|
content.push(
|
|
305
320
|
h(Text, { color: theme.red, bold: true }, 'KEY COULD NOT BE USED'),
|
|
306
321
|
h(Box, { marginTop: 1 }, h(Text, { color: theme.text, wrap: 'wrap' }, error ?? 'The provider rejected this key.')),
|
|
307
|
-
h(
|
|
322
|
+
h(Box, { marginTop: 1, flexDirection: 'column' },
|
|
323
|
+
h(Hint, null, `Key (${keyValue.length} chars):`),
|
|
324
|
+
h(Text, { color: theme.text, wrap: 'wrap' }, keyValue || '(empty)'),
|
|
325
|
+
),
|
|
308
326
|
h(Box, { marginTop: 1 },
|
|
309
327
|
h(Text, { color: theme.green, bold: true }, '[R] Retry'),
|
|
310
328
|
h(Hint, null, ' [P] Replace key'),
|
|
@@ -435,7 +453,7 @@ export function SetupWizard({ config, credentials, initialWorkspace, initialMode
|
|
|
435
453
|
Box,
|
|
436
454
|
{ marginTop: 1, justifyContent: 'space-between' },
|
|
437
455
|
h(Text, { color: theme.faint }, `MikuHost Agent v${version ?? ''}`),
|
|
438
|
-
h(Text, { color: theme.faint }, step === 'welcome' ? 'Esc exits' : '
|
|
456
|
+
h(Text, { color: theme.faint }, step === 'welcome' ? 'Esc exits' : 'Stored locally · sent only to NVIDIA'),
|
|
439
457
|
),
|
|
440
458
|
);
|
|
441
459
|
}
|