openhome-cli 0.1.2 → 0.1.4

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Command-line tool for managing OpenHome voice AI abilities. Create and deploy abilities without leaving your terminal.
4
4
 
5
- **Status:** v0.1.0 (MVP)
5
+ **Version:** v0.1.2
6
6
  **Node:** 18+
7
7
  **Platform:** macOS (primary), Linux/Windows (config-file fallback for keychain)
8
8
 
@@ -11,14 +11,11 @@ Command-line tool for managing OpenHome voice AI abilities. Create and deploy ab
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- # Clone and link globally
15
- git clone https://github.com/Bradymck/openhome-cli.git
16
- cd openhome-cli
17
- npm install
18
- npm run build
19
- npm link
14
+ # Use directly without installing
15
+ npx openhome-cli
20
16
 
21
- # Now available everywhere
17
+ # Or install globally
18
+ npm install -g openhome-cli
22
19
  openhome
23
20
  ```
24
21
 
@@ -49,21 +46,6 @@ Or just run `openhome` with no arguments for the interactive menu.
49
46
 
50
47
  Opens an interactive menu. Use arrow keys to navigate, Enter to select. The menu loops after each command — pick another or choose Exit.
51
48
 
52
- ```
53
- ┌ OpenHome CLI v0.1.0
54
-
55
- ◆ What would you like to do?
56
- │ ● Log Out
57
- │ ○ Create Ability
58
- │ ○ Deploy
59
- │ ○ Chat
60
- │ ○ My Abilities
61
- │ ○ My Agents
62
- │ ○ Status
63
- │ ○ Exit
64
-
65
- ```
66
-
67
49
  If you are not logged in, the CLI prompts for login before showing the menu.
68
50
 
69
51
  All commands below also work directly from the terminal.
@@ -84,6 +66,20 @@ openhome login
84
66
 
85
67
  ---
86
68
 
69
+ ### `openhome set-jwt [token]`
70
+
71
+ Save a session token to unlock management commands (`list`, `delete`, `toggle`, `assign`).
72
+
73
+ ```bash
74
+ openhome set-jwt eyJ...
75
+ ```
76
+
77
+ These management commands use OpenHome's web session API, which requires a JWT rather than the SDK API key. To get your token: open [app.openhome.com](https://app.openhome.com), open DevTools then Application then Local Storage then `token`, copy the value, and run `openhome set-jwt <token>`.
78
+
79
+ The token is saved to `~/.openhome/config.json`. You only need to do this once (until your session expires).
80
+
81
+ ---
82
+
87
83
  ### `openhome init [name]`
88
84
 
89
85
  Scaffold a new ability with all required files.
@@ -111,39 +107,7 @@ openhome init my-weather-bot
111
107
  | `__init__.py` | Required by OpenHome (empty) |
112
108
  | `README.md` | Description of your ability |
113
109
 
114
- The generated code auto-validates after creation.
115
-
116
- ---
117
-
118
- ### `openhome logout`
119
-
120
- Clear stored credentials and log out.
121
-
122
- ```bash
123
- openhome logout
124
- ```
125
-
126
- Removes the API key from macOS Keychain and clears the default agent from config. In the interactive menu, selecting Log Out immediately prompts you to log in again.
127
-
128
- ---
129
-
130
- ### `openhome chat [agent]`
131
-
132
- Chat with an agent via WebSocket. Send text messages and trigger abilities with keywords.
133
-
134
- ```bash
135
- # Pick an agent interactively
136
- openhome chat
137
-
138
- # Chat with a specific agent
139
- openhome chat pers_abc123
140
- ```
141
-
142
- Once connected, type messages and press Enter. The agent responds in real-time. Send trigger words (e.g. "play aquaprime") to activate abilities remotely.
143
-
144
- Commands inside chat: `/quit`, `/exit`, or `/q` to disconnect. Ctrl+C also works.
145
-
146
- > **Note:** Audio responses from the agent are not playable in the terminal. Text responses display normally.
110
+ The generated code auto-validates after creation. You're prompted to deploy immediately.
147
111
 
148
112
  ---
149
113
 
@@ -180,7 +144,7 @@ openhome deploy ./my-ability --personality pers_alice
180
144
  3. Asks for confirmation
181
145
  4. Uploads to OpenHome
182
146
 
183
- > **Note:** The upload endpoint is not yet live on the server. When it returns "Not Implemented", the CLI saves your zip to `~/.openhome/last-deploy.zip` for manual upload at [app.openhome.com](https://app.openhome.com).
147
+ > **Note:** There is no update/overwrite endpoint yet. Re-deploying with the same name will fail with a naming conflict. Delete the old version first with `openhome delete`.
184
148
 
185
149
  ---
186
150
 
@@ -199,7 +163,64 @@ Shows a table with name, version, status, and last update date.
199
163
 
200
164
  Status colors: green = active, yellow = processing, red = failed, gray = disabled.
201
165
 
202
- > **Note:** This endpoint is not yet live. Use `--mock` to preview the output format.
166
+ > **Requires session token.** Run `openhome set-jwt <token>` first. See [set-jwt](#openhome-set-jwt-token) above.
167
+
168
+ ---
169
+
170
+ ### `openhome delete [ability]`
171
+
172
+ Delete a deployed ability.
173
+
174
+ ```bash
175
+ # Pick from a list interactively
176
+ openhome delete
177
+
178
+ # Delete by name directly
179
+ openhome delete my-weather-bot
180
+
181
+ # Test with fake data
182
+ openhome delete --mock
183
+ ```
184
+
185
+ Prompts for confirmation before deleting.
186
+
187
+ > **Requires session token.** Run `openhome set-jwt <token>` first.
188
+
189
+ ---
190
+
191
+ ### `openhome toggle [ability]`
192
+
193
+ Enable or disable a deployed ability.
194
+
195
+ ```bash
196
+ # Interactive
197
+ openhome toggle
198
+
199
+ # By name with flag
200
+ openhome toggle my-weather-bot --enable
201
+ openhome toggle my-weather-bot --disable
202
+ ```
203
+
204
+ | Flag | What it does |
205
+ |------|-------------|
206
+ | `--enable` | Enable the ability |
207
+ | `--disable` | Disable the ability |
208
+
209
+ > **Requires session token.** Run `openhome set-jwt <token>` first.
210
+
211
+ ---
212
+
213
+ ### `openhome assign`
214
+
215
+ Assign abilities to an agent (multiselect).
216
+
217
+ ```bash
218
+ openhome assign
219
+ ```
220
+
221
+ Fetches your agents and abilities, lets you pick an agent, then multiselect which abilities to assign to it.
222
+
223
+ > **Requires session token.** Run `openhome set-jwt <token>` first.
203
224
 
204
225
  ---
205
226
 
@@ -209,12 +230,40 @@ View your agents and set a default for deploys.
209
230
 
210
231
  ```bash
211
232
  openhome agents
233
+ ```
212
234
 
213
- # Test with fake data
214
- openhome agents --mock
235
+ Shows all agents on your account with names and IDs. Optionally set or change your default agent (used by `deploy` when `--personality` is not specified).
236
+
237
+ ---
238
+
239
+ ### `openhome chat [agent]`
240
+
241
+ Chat with an agent via WebSocket. Send text messages and trigger abilities with keywords.
242
+
243
+ ```bash
244
+ # Pick an agent interactively
245
+ openhome chat
246
+
247
+ # Chat with a specific agent
248
+ openhome chat pers_abc123
215
249
  ```
216
250
 
217
- Shows all agents on your account with names and descriptions. Optionally set or change your default agent (used by `deploy` when `--personality` is not specified).
251
+ Once connected, type messages and press Enter. The agent responds in real-time.
252
+
253
+ Commands inside chat: `/quit`, `/exit`, or `/q` to disconnect. Ctrl+C also works.
254
+
255
+ > **Note:** Audio responses from the agent are not playable in the terminal. Text responses display normally.
256
+
257
+ ---
258
+
259
+ ### `openhome trigger [phrase]`
260
+
261
+ Send a trigger phrase to fire an ability remotely.
262
+
263
+ ```bash
264
+ openhome trigger "play aquaprime"
265
+ openhome trigger --agent pers_abc123 "check weather"
266
+ ```
218
267
 
219
268
  ---
220
269
 
@@ -233,9 +282,51 @@ openhome status
233
282
  openhome status my-weather-bot --mock
234
283
  ```
235
284
 
236
- Shows: name, display name, status, version, timestamps, linked agents, validation errors, and deploy history.
285
+ > **Requires session token** (uses the same list endpoint internally). Run `openhome set-jwt <token>` first.
237
286
 
238
- > **Note:** This endpoint is not yet live. Use `--mock` to preview the output format.
287
+ ---
288
+
289
+ ### `openhome logs`
290
+
291
+ Stream live agent messages and logs.
292
+
293
+ ```bash
294
+ openhome logs
295
+ openhome logs --agent pers_abc123
296
+ ```
297
+
298
+ ---
299
+
300
+ ### `openhome whoami`
301
+
302
+ Show auth status, default agent, and tracked abilities.
303
+
304
+ ```bash
305
+ openhome whoami
306
+ ```
307
+
308
+ ---
309
+
310
+ ### `openhome config [path]`
311
+
312
+ Edit trigger words, description, or category in a local `config.json`.
313
+
314
+ ```bash
315
+ openhome config
316
+ openhome config ./my-ability
317
+ ```
318
+
319
+ ---
320
+
321
+ ### `openhome logout`
322
+
323
+ Clear stored credentials and log out.
324
+
325
+ ```bash
326
+ openhome logout
327
+ ```
328
+
329
+ Removes the API key from macOS Keychain and clears the default agent from config.
239
330
 
240
331
  ---
241
332
 
@@ -259,8 +350,6 @@ Must contain:
259
350
 
260
351
  ### main.py Required Patterns
261
352
 
262
- Your main Python file must include:
263
-
264
353
  | What | Why |
265
354
  |------|-----|
266
355
  | Class extending `MatchingCapability` | OpenHome ability base class |
@@ -272,8 +361,6 @@ Your main Python file must include:
272
361
 
273
362
  ### Blocked Patterns (Errors)
274
363
 
275
- These are not allowed in any `.py` file:
276
-
277
364
  | Pattern | Use Instead |
278
365
  |---------|-------------|
279
366
  | `print()` | `self.worker.editor_logging_handler` |
@@ -310,8 +397,7 @@ These are not allowed in any `.py` file:
310
397
 
311
398
  ```
312
399
  ~/.openhome/
313
- config.json # Settings + fallback API key
314
- last-deploy.zip # Saved when upload endpoint unavailable
400
+ config.json # Settings, fallback API key, session token
315
401
  ```
316
402
 
317
403
  ### Config Fields
@@ -321,6 +407,7 @@ These are not allowed in any `.py` file:
321
407
  | `api_base_url` | Override API endpoint | `https://app.openhome.com` |
322
408
  | `default_personality_id` | Default agent for deploys | (none) |
323
409
  | `api_key` | Fallback key storage | (none — prefers Keychain) |
410
+ | `jwt` | Session token for management commands | (none — set via `set-jwt`) |
324
411
 
325
412
  ### API Key Storage
326
413
 
@@ -328,125 +415,51 @@ On macOS, your API key is stored in the system Keychain (service: `openhome-cli`
328
415
 
329
416
  ---
330
417
 
331
- ## Project Structure
332
-
333
- ```
334
- openhome-cli/
335
- ├── bin/openhome.js # Entry point shim
336
- ├── src/
337
- │ ├── cli.ts # Menu + Commander setup
338
- │ ├── commands/
339
- │ │ ├── login.ts # API key auth
340
- │ │ ├── logout.ts # Clear credentials
341
- │ │ ├── init.ts # Scaffold new ability
342
- │ │ ├── deploy.ts # Validate + zip + upload
343
- │ │ ├── chat.ts # WebSocket chat with agent
344
- │ │ ├── list.ts # List abilities table
345
- │ │ ├── agents.ts # View agents + set default
346
- │ │ └── status.ts # Ability detail view
347
- │ ├── api/
348
- │ │ ├── client.ts # HTTP client + error handling
349
- │ │ ├── mock-client.ts # Fake responses for testing
350
- │ │ ├── contracts.ts # TypeScript interfaces
351
- │ │ └── endpoints.ts # URL constants
352
- │ ├── validation/
353
- │ │ ├── rules.ts # All validation rules
354
- │ │ └── validator.ts # Rule runner
355
- │ ├── config/
356
- │ │ ├── store.ts # Config file + Keychain
357
- │ │ └── keychain.ts # macOS Keychain helpers
358
- │ ├── ui/
359
- │ │ └── format.ts # Colors, tables, prompts
360
- │ └── util/
361
- │ └── zip.ts # ZIP creation (archiver)
362
- └── templates/
363
- ├── basic/ # Simple ability template
364
- └── api/ # API-calling ability template
365
- ```
366
-
367
- ---
368
-
369
- ## Development
370
-
371
- ```bash
372
- # Install dependencies
373
- npm install
374
-
375
- # Build
376
- npm run build
377
-
378
- # Run without building (dev mode)
379
- npm run dev
380
-
381
- # Type check
382
- npm run lint
383
-
384
- # Run tests
385
- npm run test
386
- ```
387
-
388
- ### Tech Stack
418
+ ## What This Tool Does NOT Do
389
419
 
390
- | Package | Version | Purpose |
391
- |---------|---------|---------|
392
- | commander | 12.x | CLI argument parsing |
393
- | @clack/prompts | 1.x | Interactive menus, spinners, prompts |
394
- | chalk | 5.x | Terminal colors |
395
- | ws | 8.x | WebSocket client for agent chat |
396
- | archiver | 7.x | ZIP file creation |
397
- | typescript | 5.x | Type safety |
398
- | tsup | 8.x | Build tool |
399
- | vitest | 2.x | Testing |
420
+ - **No local ability testing** Abilities run on the OpenHome platform. Deploy and use "Start Live Test" in the web editor to test.
421
+ - **No ability editing** — The CLI does not modify deployed abilities. Edit locally, then re-deploy.
422
+ - **No update/redeploy** — There is no endpoint to overwrite an existing ability version. Deploy creates a new entry; delete the old one via `openhome delete`.
423
+ - **No Windows Keychain** API key stored in plaintext config on non-macOS platforms.
400
424
 
401
425
  ---
402
426
 
403
427
  ## API Status
404
428
 
405
- | Endpoint | Method | Status |
406
- |----------|--------|--------|
407
- | `/api/sdk/get_personalities` | POST | Live |
408
- | `/websocket/voice-stream/{key}/{agent}` | WebSocket | Live |
409
- | `/api/sdk/abilities` | POST (upload) | Not yet implemented |
410
- | `/api/sdk/abilities` | GET (list) | Not yet implemented |
411
- | `/api/sdk/abilities/:id` | GET (detail) | Not yet implemented |
412
-
413
- The CLI handles "Not Implemented" responses gracefully. When an endpoint is unavailable:
414
- - **Deploy**: Saves zip to `~/.openhome/last-deploy.zip` and shows manual upload instructions
415
- - **List/Status**: Suggests using `--mock` flag to preview the output format
429
+ | Command | Endpoint | Auth | Status |
430
+ |---------|----------|------|--------|
431
+ | `login` | `POST /api/sdk/verify_apikey/` | API key | Live |
432
+ | `agents` | `POST /api/sdk/get_personalities/` | API key | Live |
433
+ | `chat` | WebSocket `/websocket/voice-stream/` | API key | Live |
434
+ | `deploy` | `POST /api/capabilities/add-capability/` | API key | Live |
435
+ | `list` | `GET /api/capabilities/get-installed-capabilities/` | JWT | Live |
436
+ | `delete` | `POST /api/capabilities/delete-capability/` | JWT | Live |
437
+ | `toggle` | `PUT /api/capabilities/edit-installed-capability/:id/` | JWT | Live |
438
+ | `assign` | `PUT /api/personalities/edit-personality/` | JWT | Live |
416
439
 
417
- Use `--mock` on any command to test with fake data while endpoints are being built.
418
-
419
- ---
420
-
421
- ## What This Tool Does NOT Do
422
-
423
- - **No local ability testing** — Abilities run on the OpenHome platform. Deploy and use "Start Live Test" in the web editor to test.
424
- - **No log streaming** — `openhome logs` is not yet implemented.
425
- - **No ability deletion** — Must be done through the web dashboard.
426
- - **No ability editing** — The CLI does not modify deployed abilities. Edit locally, then re-deploy.
427
- - **No multi-agent deploy** — One ability deploys to one agent at a time.
428
- - **No Windows Keychain** — API key stored in plaintext config on non-macOS platforms.
440
+ Commands marked **JWT** require `openhome set-jwt <token>` first. OpenHome currently uses separate auth for SDK operations (API key) vs. account management (web session JWT). Once OpenHome adds API key support to management endpoints, the `set-jwt` step will no longer be needed.
429
441
 
430
442
  ---
431
443
 
432
444
  ## Roadmap
433
445
 
434
- ### Planned
435
-
436
- - [ ] `openhome logs [ability]` — Stream ability logs in real-time
437
- - [ ] `openhome delete [ability]` — Remove a deployed ability
438
- - [ ] `openhome update` — Re-deploy an existing ability (shortcut for deploy)
439
446
  - [ ] `openhome watch` — Auto-deploy on file changes
447
+ - [ ] `openhome update` — Re-deploy/overwrite an existing ability (pending server-side update endpoint)
440
448
  - [ ] Background Daemon and Brain Skill templates
441
449
  - [ ] Cross-platform secure key storage (Windows Credential Manager, Linux Secret Service)
450
+ - [ ] Management commands without JWT (pending OpenHome API update)
451
+
452
+ ---
442
453
 
443
- ### Needs Server-Side Work
454
+ ## Development
444
455
 
445
- - [ ] Upload endpoint (`POST /api/sdk/abilities`)
446
- - [ ] List endpoint (`GET /api/sdk/abilities`)
447
- - [ ] Detail endpoint (`GET /api/sdk/abilities/:id`)
448
- - [ ] Log streaming endpoint (WebSocket)
449
- - [ ] Delete endpoint (`DELETE /api/sdk/abilities/:id`)
456
+ ```bash
457
+ npm install
458
+ npm run build # Build
459
+ npm run dev # Run without building
460
+ npm run lint # Type check
461
+ npm run test # Run tests
462
+ ```
450
463
 
451
464
  ---
452
465
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getApiKey,
3
3
  getConfig,
4
- getJwt as getJwt2,
4
+ getJwt,
5
5
  getTrackedAbilities,
6
6
  keychainDelete,
7
7
  registerAbility,
@@ -16,6 +16,9 @@ import { fileURLToPath } from "url";
16
16
  import { dirname, join as join6 } from "path";
17
17
  import { readFileSync as readFileSync5 } from "fs";
18
18
 
19
+ // src/commands/login.ts
20
+ import { execFile } from "child_process";
21
+
19
22
  // src/api/endpoints.ts
20
23
  var API_BASE = "https://app.openhome.com";
21
24
  var WS_BASE = "wss://app.openhome.com";
@@ -272,10 +275,38 @@ function handleCancel(value) {
272
275
 
273
276
  // src/commands/login.ts
274
277
  import chalk2 from "chalk";
278
+ var SETTINGS_URL = "https://app.openhome.com/dashboard/settings";
279
+ function openBrowser(url) {
280
+ try {
281
+ if (process.platform === "darwin") {
282
+ execFile("open", [url]);
283
+ } else if (process.platform === "win32") {
284
+ execFile("cmd", ["/c", "start", url]);
285
+ } else {
286
+ execFile("xdg-open", [url]);
287
+ }
288
+ } catch {
289
+ }
290
+ }
275
291
  async function loginCommand() {
276
292
  p.intro("\u{1F511} OpenHome Login");
293
+ const open = await p.confirm({
294
+ message: `Press Enter to open your browser and navigate to the ${chalk2.bold("API Keys")} tab`,
295
+ initialValue: true,
296
+ active: "Open browser",
297
+ inactive: "Skip"
298
+ });
299
+ handleCancel(open);
300
+ if (open) {
301
+ openBrowser(SETTINGS_URL);
302
+ console.log(
303
+ `
304
+ ${chalk2.dim(`Opened ${chalk2.bold("app.openhome.com/dashboard/settings")} \u2014 click the ${chalk2.bold("API Keys")} tab`)}
305
+ `
306
+ );
307
+ }
277
308
  const apiKey = await p.password({
278
- message: "Enter your OpenHome API key",
309
+ message: "Paste your API key here",
279
310
  validate: (val) => {
280
311
  if (!val || !val.trim()) return "API key is required";
281
312
  }
@@ -548,7 +579,7 @@ import archiver from "archiver";
548
579
  import { createWriteStream } from "fs";
549
580
  import { Writable } from "stream";
550
581
  async function createAbilityZip(dirPath) {
551
- return new Promise((resolve5, reject) => {
582
+ return new Promise((resolve6, reject) => {
552
583
  const chunks = [];
553
584
  const writable = new Writable({
554
585
  write(chunk, _encoding, callback) {
@@ -557,7 +588,7 @@ async function createAbilityZip(dirPath) {
557
588
  }
558
589
  });
559
590
  writable.on("finish", () => {
560
- resolve5(Buffer.concat(chunks));
591
+ resolve6(Buffer.concat(chunks));
561
592
  });
562
593
  writable.on("error", reject);
563
594
  const archive = archiver("zip", { zlib: { level: 9 } });
@@ -1825,7 +1856,7 @@ async function deleteCommand(abilityArg, opts = {}) {
1825
1856
  client = new MockApiClient();
1826
1857
  } else {
1827
1858
  const apiKey = getApiKey() ?? "";
1828
- const jwt = getJwt2() ?? void 0;
1859
+ const jwt = getJwt() ?? void 0;
1829
1860
  if (!apiKey && !jwt) {
1830
1861
  error("Not authenticated. Run: openhome login");
1831
1862
  process.exit(1);
@@ -1914,7 +1945,7 @@ async function toggleCommand(abilityArg, opts = {}) {
1914
1945
  client = new MockApiClient();
1915
1946
  } else {
1916
1947
  const apiKey = getApiKey() ?? "";
1917
- const jwt = getJwt2() ?? void 0;
1948
+ const jwt = getJwt() ?? void 0;
1918
1949
  if (!apiKey && !jwt) {
1919
1950
  error("Not authenticated. Run: openhome login");
1920
1951
  process.exit(1);
@@ -2013,7 +2044,7 @@ async function assignCommand(opts = {}) {
2013
2044
  client = new MockApiClient();
2014
2045
  } else {
2015
2046
  const apiKey = getApiKey() ?? "";
2016
- const jwt = getJwt2() ?? void 0;
2047
+ const jwt = getJwt() ?? void 0;
2017
2048
  if (!apiKey && !jwt) {
2018
2049
  error("Not authenticated. Run: openhome login");
2019
2050
  process.exit(1);
@@ -2122,7 +2153,7 @@ async function listCommand(opts = {}) {
2122
2153
  client = new MockApiClient();
2123
2154
  } else {
2124
2155
  const apiKey = getApiKey() ?? "";
2125
- const jwt = getJwt2() ?? void 0;
2156
+ const jwt = getJwt() ?? void 0;
2126
2157
  if (!apiKey && !jwt) {
2127
2158
  error("Not authenticated. Run: openhome login");
2128
2159
  process.exit(1);
@@ -2422,7 +2453,7 @@ async function chatCommand(agentArg, opts = {}) {
2422
2453
  }
2423
2454
  const wsUrl = `${WS_BASE}${ENDPOINTS.voiceStream(apiKey, agentId)}`;
2424
2455
  info(`Connecting to agent ${chalk9.bold(agentId)}...`);
2425
- await new Promise((resolve5) => {
2456
+ await new Promise((resolve6) => {
2426
2457
  const ws = new WebSocket(wsUrl, {
2427
2458
  perMessageDeflate: false,
2428
2459
  headers: {
@@ -2550,7 +2581,7 @@ async function chatCommand(agentArg, opts = {}) {
2550
2581
  console.error("");
2551
2582
  error(`WebSocket error: ${err.message}`);
2552
2583
  rl.close();
2553
- resolve5();
2584
+ resolve6();
2554
2585
  });
2555
2586
  ws.on("close", (code) => {
2556
2587
  if (pingInterval) clearInterval(pingInterval);
@@ -2561,7 +2592,7 @@ async function chatCommand(agentArg, opts = {}) {
2561
2592
  info(`Connection closed (code: ${code})`);
2562
2593
  }
2563
2594
  rl.close();
2564
- resolve5();
2595
+ resolve6();
2565
2596
  });
2566
2597
  rl.on("close", () => {
2567
2598
  if (connected) {
@@ -2628,7 +2659,7 @@ async function triggerCommand(phraseArg, opts = {}) {
2628
2659
  info(`Sending "${chalk10.bold(phrase)}" to agent ${chalk10.bold(agentId)}...`);
2629
2660
  const s = p.spinner();
2630
2661
  s.start("Waiting for response...");
2631
- await new Promise((resolve5) => {
2662
+ await new Promise((resolve6) => {
2632
2663
  const ws = new WebSocket2(wsUrl, {
2633
2664
  perMessageDeflate: false,
2634
2665
  headers: {
@@ -2658,7 +2689,7 @@ async function triggerCommand(phraseArg, opts = {}) {
2658
2689
  ${chalk10.cyan("Agent:")} ${fullResponse}`);
2659
2690
  }
2660
2691
  cleanup();
2661
- resolve5();
2692
+ resolve6();
2662
2693
  }, RESPONSE_TIMEOUT);
2663
2694
  });
2664
2695
  ws.on("message", (raw) => {
@@ -2675,7 +2706,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}`);
2675
2706
  ${chalk10.cyan("Agent:")} ${fullResponse}
2676
2707
  `);
2677
2708
  cleanup();
2678
- resolve5();
2709
+ resolve6();
2679
2710
  }
2680
2711
  }
2681
2712
  break;
@@ -2692,7 +2723,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}
2692
2723
  ${chalk10.cyan("Agent:")} ${fullResponse}
2693
2724
  `);
2694
2725
  cleanup();
2695
- resolve5();
2726
+ resolve6();
2696
2727
  }
2697
2728
  }
2698
2729
  break;
@@ -2707,7 +2738,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}
2707
2738
  `Server error: ${errData?.message || errData?.title || "Unknown"}`
2708
2739
  );
2709
2740
  cleanup();
2710
- resolve5();
2741
+ resolve6();
2711
2742
  break;
2712
2743
  }
2713
2744
  }
@@ -2717,12 +2748,12 @@ ${chalk10.cyan("Agent:")} ${fullResponse}
2717
2748
  ws.on("error", (err) => {
2718
2749
  s.stop("Connection error.");
2719
2750
  error(err.message);
2720
- resolve5();
2751
+ resolve6();
2721
2752
  });
2722
2753
  ws.on("close", () => {
2723
2754
  if (pingInterval) clearInterval(pingInterval);
2724
2755
  if (responseTimer) clearTimeout(responseTimer);
2725
- resolve5();
2756
+ resolve6();
2726
2757
  });
2727
2758
  });
2728
2759
  }
@@ -2947,7 +2978,7 @@ async function logsCommand(opts = {}) {
2947
2978
  info(`Streaming logs from agent ${chalk12.bold(agentId)}...`);
2948
2979
  info(`Press ${chalk12.bold("Ctrl+C")} to stop.
2949
2980
  `);
2950
- await new Promise((resolve5) => {
2981
+ await new Promise((resolve6) => {
2951
2982
  const ws = new WebSocket3(wsUrl, {
2952
2983
  perMessageDeflate: false,
2953
2984
  headers: {
@@ -3027,13 +3058,13 @@ async function logsCommand(opts = {}) {
3027
3058
  });
3028
3059
  ws.on("error", (err) => {
3029
3060
  error(`WebSocket error: ${err.message}`);
3030
- resolve5();
3061
+ resolve6();
3031
3062
  });
3032
3063
  ws.on("close", (code) => {
3033
3064
  if (pingInterval) clearInterval(pingInterval);
3034
3065
  console.log("");
3035
3066
  info(`Connection closed (code: ${code})`);
3036
- resolve5();
3067
+ resolve6();
3037
3068
  });
3038
3069
  process.on("SIGINT", () => {
3039
3070
  console.log("");
@@ -3044,33 +3075,98 @@ async function logsCommand(opts = {}) {
3044
3075
  }
3045
3076
 
3046
3077
  // src/commands/set-jwt.ts
3078
+ import { execFile as execFile2 } from "child_process";
3079
+ import chalk13 from "chalk";
3080
+ var OPENHOME_URL = "https://app.openhome.com";
3081
+ function openBrowser2(url) {
3082
+ try {
3083
+ if (process.platform === "darwin") {
3084
+ execFile2("open", [url]);
3085
+ } else if (process.platform === "win32") {
3086
+ execFile2("cmd", ["/c", "start", url]);
3087
+ } else {
3088
+ execFile2("xdg-open", [url]);
3089
+ }
3090
+ } catch {
3091
+ }
3092
+ }
3047
3093
  async function setJwtCommand(token) {
3048
- p.intro("\u{1F511} Set Session Token");
3049
- let jwt = token;
3050
- if (!jwt) {
3051
- const input = await p.text({
3052
- message: "Paste your OpenHome session token",
3053
- placeholder: "eyJ...",
3054
- validate: (val) => {
3055
- if (!val || !val.trim()) return "Token is required";
3056
- if (!val.trim().startsWith("eyJ"))
3057
- return "Doesn't look like a JWT \u2014 should start with eyJ";
3058
- }
3059
- });
3060
- if (typeof input === "symbol") {
3061
- p.cancel("Cancelled.");
3062
- return;
3094
+ if (token) {
3095
+ p.intro("\u{1F511} Enable Management Features");
3096
+ try {
3097
+ saveJwt(token.trim());
3098
+ success("Session token saved.");
3099
+ p.outro(
3100
+ "Management commands (list, delete, toggle, assign) are now unlocked."
3101
+ );
3102
+ } catch (err) {
3103
+ error(
3104
+ `Failed to save token: ${err instanceof Error ? err.message : String(err)}`
3105
+ );
3106
+ process.exit(1);
3107
+ }
3108
+ return;
3109
+ }
3110
+ p.intro("\u{1F511} Enable Management Features");
3111
+ p.note(
3112
+ [
3113
+ "Some commands (list, delete, enable/disable abilities) need your",
3114
+ "OpenHome session token to work.",
3115
+ "",
3116
+ "Think of it as a temporary key that proves you're logged in to",
3117
+ "your account. You only need to do this once."
3118
+ ].join("\n"),
3119
+ "What's this?"
3120
+ );
3121
+ console.log("");
3122
+ console.log(
3123
+ chalk13.dim(` Opening ${chalk13.bold("app.openhome.com")} in your browser...`)
3124
+ );
3125
+ openBrowser2(OPENHOME_URL);
3126
+ console.log("");
3127
+ p.note(
3128
+ [
3129
+ `${chalk13.bold("Step 1")} Make sure you're logged in to app.openhome.com`,
3130
+ "",
3131
+ `${chalk13.bold("Step 2")} Open the browser console:`,
3132
+ ` Mac \u2192 ${chalk13.cyan("Cmd + Option + J")}`,
3133
+ ` Windows / Linux \u2192 ${chalk13.cyan("F12")}, then click the Console tab`,
3134
+ "",
3135
+ `${chalk13.bold("Step 3")} Paste this command into the console and press Enter:`,
3136
+ "",
3137
+ ` ${chalk13.green("copy(localStorage.getItem('token'))")}`,
3138
+ "",
3139
+ `${chalk13.bold("Step 4")} Your token is now in your clipboard.`,
3140
+ ` Come back here and paste it below.`
3141
+ ].join("\n"),
3142
+ "How to get your token"
3143
+ );
3144
+ const input = await p.password({
3145
+ message: "Paste your token here",
3146
+ validate: (val) => {
3147
+ if (!val || !val.trim()) return "Token is required";
3148
+ if (val.trim().length < 20)
3149
+ return "That doesn't look right \u2014 the token should be much longer";
3063
3150
  }
3064
- jwt = input;
3151
+ });
3152
+ if (typeof input === "symbol") {
3153
+ p.cancel("Cancelled.");
3154
+ return;
3065
3155
  }
3066
3156
  try {
3067
- saveJwt(jwt.trim());
3157
+ saveJwt(input.trim());
3068
3158
  success("Session token saved.");
3069
3159
  p.note(
3070
- "Management commands (list, delete, toggle, assign) are now unlocked.",
3071
- "Token saved"
3160
+ [
3161
+ "These commands are now unlocked:",
3162
+ ` ${chalk13.bold("openhome list")} \u2014 see all your deployed abilities`,
3163
+ ` ${chalk13.bold("openhome delete")} \u2014 remove an ability`,
3164
+ ` ${chalk13.bold("openhome toggle")} \u2014 enable or disable an ability`,
3165
+ ` ${chalk13.bold("openhome assign")} \u2014 link abilities to an agent`
3166
+ ].join("\n"),
3167
+ "You're all set"
3072
3168
  );
3073
- p.outro("Done.");
3169
+ p.outro("Done!");
3074
3170
  } catch (err) {
3075
3171
  error(
3076
3172
  `Failed to save token: ${err instanceof Error ? err.message : String(err)}`
@@ -3079,6 +3175,45 @@ async function setJwtCommand(token) {
3079
3175
  }
3080
3176
  }
3081
3177
 
3178
+ // src/commands/validate.ts
3179
+ import { resolve as resolve5 } from "path";
3180
+ import chalk14 from "chalk";
3181
+ async function validateCommand(pathArg = ".") {
3182
+ const targetDir = resolve5(pathArg);
3183
+ p.intro(`\u{1F50E} Validate ability`);
3184
+ const s = p.spinner();
3185
+ s.start("Running checks...");
3186
+ const result = validateAbility(targetDir);
3187
+ if (result.errors.length === 0 && result.warnings.length === 0) {
3188
+ s.stop("All checks passed.");
3189
+ p.outro("Ability is ready to deploy! \u{1F389}");
3190
+ return;
3191
+ }
3192
+ s.stop("Checks complete.");
3193
+ if (result.errors.length > 0) {
3194
+ p.note(
3195
+ result.errors.map(
3196
+ (issue) => `${chalk14.red("\u2717")} ${issue.file ? chalk14.bold(`[${issue.file}]`) + " " : ""}${issue.message}`
3197
+ ).join("\n"),
3198
+ `${result.errors.length} Error(s)`
3199
+ );
3200
+ }
3201
+ if (result.warnings.length > 0) {
3202
+ p.note(
3203
+ result.warnings.map(
3204
+ (w) => `${chalk14.yellow("\u26A0")} ${w.file ? chalk14.bold(`[${w.file}]`) + " " : ""}${w.message}`
3205
+ ).join("\n"),
3206
+ `${result.warnings.length} Warning(s)`
3207
+ );
3208
+ }
3209
+ if (result.passed) {
3210
+ p.outro("Validation passed (with warnings).");
3211
+ } else {
3212
+ error("Fix errors before deploying.");
3213
+ process.exit(1);
3214
+ }
3215
+ }
3216
+
3082
3217
  // src/cli.ts
3083
3218
  var __filename = fileURLToPath(import.meta.url);
3084
3219
  var __dirname = dirname(__filename);
@@ -3111,16 +3246,6 @@ async function interactiveMenu() {
3111
3246
  label: "\u2728 Create Ability",
3112
3247
  hint: "Scaffold and deploy a new ability"
3113
3248
  },
3114
- {
3115
- value: "chat",
3116
- label: "\u{1F4AC} Chat",
3117
- hint: "Talk to your agent"
3118
- },
3119
- {
3120
- value: "trigger",
3121
- label: "\u26A1 Trigger",
3122
- hint: "Fire an ability remotely with a phrase"
3123
- },
3124
3249
  {
3125
3250
  value: "list",
3126
3251
  label: "\u{1F4CB} My Abilities",
@@ -3147,25 +3272,15 @@ async function interactiveMenu() {
3147
3272
  hint: "View agents and set default"
3148
3273
  },
3149
3274
  {
3150
- value: "status",
3151
- label: "\u{1F50D} Status",
3152
- hint: "Check ability status"
3153
- },
3154
- {
3155
- value: "config",
3156
- label: "\u2699\uFE0F Edit Config",
3157
- hint: "Update trigger words, description, category"
3275
+ value: "chat",
3276
+ label: "\u{1F4AC} Chat",
3277
+ hint: "Talk to your agent"
3158
3278
  },
3159
3279
  {
3160
3280
  value: "logs",
3161
3281
  label: "\u{1F4E1} Logs",
3162
3282
  hint: "Stream live agent messages"
3163
3283
  },
3164
- {
3165
- value: "whoami",
3166
- label: "\u{1F464} Who Am I",
3167
- hint: "Show auth, default agent, tracked abilities"
3168
- },
3169
3284
  {
3170
3285
  value: "logout",
3171
3286
  label: "\u{1F513} Log Out",
@@ -3179,12 +3294,6 @@ async function interactiveMenu() {
3179
3294
  case "init":
3180
3295
  await initCommand();
3181
3296
  break;
3182
- case "chat":
3183
- await chatCommand();
3184
- break;
3185
- case "trigger":
3186
- await triggerCommand();
3187
- break;
3188
3297
  case "list":
3189
3298
  await listCommand();
3190
3299
  break;
@@ -3200,18 +3309,12 @@ async function interactiveMenu() {
3200
3309
  case "agents":
3201
3310
  await agentsCommand();
3202
3311
  break;
3203
- case "status":
3204
- await statusCommand();
3205
- break;
3206
- case "config":
3207
- await configEditCommand();
3312
+ case "chat":
3313
+ await chatCommand();
3208
3314
  break;
3209
3315
  case "logs":
3210
3316
  await logsCommand();
3211
3317
  break;
3212
- case "whoami":
3213
- await whoamiCommand();
3214
- break;
3215
3318
  case "logout":
3216
3319
  await logoutCommand();
3217
3320
  await ensureLoggedIn();
@@ -3277,6 +3380,9 @@ program.command("logs").description("Stream live agent messages and logs").optio
3277
3380
  program.command("whoami").description("Show auth status, default agent, and tracked abilities").action(async () => {
3278
3381
  await whoamiCommand();
3279
3382
  });
3383
+ program.command("validate [path]").description("Check an ability for errors before deploying").action(async (path) => {
3384
+ await validateCommand(path);
3385
+ });
3280
3386
  program.command("set-jwt [token]").description(
3281
3387
  "Save a session token to enable management commands (list, delete, toggle, assign)"
3282
3388
  ).action(async (token) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openhome-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "CLI for managing OpenHome voice AI abilities",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -19,6 +19,7 @@ import { whoamiCommand } from "./commands/whoami.js";
19
19
  import { configEditCommand } from "./commands/config-edit.js";
20
20
  import { logsCommand } from "./commands/logs.js";
21
21
  import { setJwtCommand } from "./commands/set-jwt.js";
22
+ import { validateCommand } from "./commands/validate.js";
22
23
  import { p, handleCancel } from "./ui/format.js";
23
24
 
24
25
  // Read version from package.json
@@ -61,16 +62,6 @@ async function interactiveMenu(): Promise<void> {
61
62
  label: "✨ Create Ability",
62
63
  hint: "Scaffold and deploy a new ability",
63
64
  },
64
- {
65
- value: "chat",
66
- label: "💬 Chat",
67
- hint: "Talk to your agent",
68
- },
69
- {
70
- value: "trigger",
71
- label: "⚡ Trigger",
72
- hint: "Fire an ability remotely with a phrase",
73
- },
74
65
  {
75
66
  value: "list",
76
67
  label: "📋 My Abilities",
@@ -97,25 +88,15 @@ async function interactiveMenu(): Promise<void> {
97
88
  hint: "View agents and set default",
98
89
  },
99
90
  {
100
- value: "status",
101
- label: "🔍 Status",
102
- hint: "Check ability status",
103
- },
104
- {
105
- value: "config",
106
- label: "⚙️ Edit Config",
107
- hint: "Update trigger words, description, category",
91
+ value: "chat",
92
+ label: "💬 Chat",
93
+ hint: "Talk to your agent",
108
94
  },
109
95
  {
110
96
  value: "logs",
111
97
  label: "📡 Logs",
112
98
  hint: "Stream live agent messages",
113
99
  },
114
- {
115
- value: "whoami",
116
- label: "👤 Who Am I",
117
- hint: "Show auth, default agent, tracked abilities",
118
- },
119
100
  {
120
101
  value: "logout",
121
102
  label: "🔓 Log Out",
@@ -130,12 +111,6 @@ async function interactiveMenu(): Promise<void> {
130
111
  case "init":
131
112
  await initCommand();
132
113
  break;
133
- case "chat":
134
- await chatCommand();
135
- break;
136
- case "trigger":
137
- await triggerCommand();
138
- break;
139
114
  case "list":
140
115
  await listCommand();
141
116
  break;
@@ -151,18 +126,12 @@ async function interactiveMenu(): Promise<void> {
151
126
  case "agents":
152
127
  await agentsCommand();
153
128
  break;
154
- case "status":
155
- await statusCommand();
156
- break;
157
- case "config":
158
- await configEditCommand();
129
+ case "chat":
130
+ await chatCommand();
159
131
  break;
160
132
  case "logs":
161
133
  await logsCommand();
162
134
  break;
163
- case "whoami":
164
- await whoamiCommand();
165
- break;
166
135
  case "logout":
167
136
  await logoutCommand();
168
137
  await ensureLoggedIn();
@@ -317,6 +286,13 @@ program
317
286
  await whoamiCommand();
318
287
  });
319
288
 
289
+ program
290
+ .command("validate [path]")
291
+ .description("Check an ability for errors before deploying")
292
+ .action(async (path?: string) => {
293
+ await validateCommand(path);
294
+ });
295
+
320
296
  program
321
297
  .command("set-jwt [token]")
322
298
  .description(
@@ -1,14 +1,46 @@
1
+ import { execFile } from "node:child_process";
1
2
  import { ApiClient } from "../api/client.js";
2
3
  import type { Personality } from "../api/contracts.js";
3
- import { saveApiKey, saveJwt } from "../config/store.js";
4
+ import { saveApiKey } from "../config/store.js";
4
5
  import { success, error, info, p, handleCancel } from "../ui/format.js";
5
6
  import chalk from "chalk";
6
7
 
8
+ const SETTINGS_URL = "https://app.openhome.com/dashboard/settings";
9
+
10
+ function openBrowser(url: string): void {
11
+ try {
12
+ if (process.platform === "darwin") {
13
+ execFile("open", [url]);
14
+ } else if (process.platform === "win32") {
15
+ execFile("cmd", ["/c", "start", url]);
16
+ } else {
17
+ execFile("xdg-open", [url]);
18
+ }
19
+ } catch {
20
+ // best-effort
21
+ }
22
+ }
23
+
7
24
  export async function loginCommand(): Promise<void> {
8
25
  p.intro("🔑 OpenHome Login");
9
26
 
27
+ const open = await p.confirm({
28
+ message: `Press Enter to open your browser and navigate to the ${chalk.bold("API Keys")} tab`,
29
+ initialValue: true,
30
+ active: "Open browser",
31
+ inactive: "Skip",
32
+ });
33
+ handleCancel(open);
34
+
35
+ if (open) {
36
+ openBrowser(SETTINGS_URL);
37
+ console.log(
38
+ `\n ${chalk.dim(`Opened ${chalk.bold("app.openhome.com/dashboard/settings")} — click the ${chalk.bold("API Keys")} tab`)}\n`,
39
+ );
40
+ }
41
+
10
42
  const apiKey = await p.password({
11
- message: "Enter your OpenHome API key",
43
+ message: "Paste your API key here",
12
44
  validate: (val) => {
13
45
  if (!val || !val.trim()) return "API key is required";
14
46
  },
@@ -38,7 +70,6 @@ export async function loginCommand(): Promise<void> {
38
70
  saveApiKey(apiKey as string);
39
71
  success("API key saved.");
40
72
 
41
- // Show agents on this account
42
73
  if (agents.length > 0) {
43
74
  p.note(
44
75
  agents
@@ -1,36 +1,110 @@
1
+ import { execFile } from "node:child_process";
1
2
  import { saveJwt } from "../config/store.js";
2
3
  import { success, error, p } from "../ui/format.js";
4
+ import chalk from "chalk";
5
+
6
+ const OPENHOME_URL = "https://app.openhome.com";
7
+
8
+ function openBrowser(url: string): void {
9
+ try {
10
+ if (process.platform === "darwin") {
11
+ execFile("open", [url]);
12
+ } else if (process.platform === "win32") {
13
+ execFile("cmd", ["/c", "start", url]);
14
+ } else {
15
+ execFile("xdg-open", [url]);
16
+ }
17
+ } catch {
18
+ // best-effort — user can open manually
19
+ }
20
+ }
3
21
 
4
22
  export async function setJwtCommand(token?: string): Promise<void> {
5
- p.intro("🔑 Set Session Token");
6
-
7
- let jwt = token;
8
-
9
- if (!jwt) {
10
- const input = await p.text({
11
- message: "Paste your OpenHome session token",
12
- placeholder: "eyJ...",
13
- validate: (val) => {
14
- if (!val || !val.trim()) return "Token is required";
15
- if (!val.trim().startsWith("eyJ"))
16
- return "Doesn't look like a JWT should start with eyJ";
17
- },
18
- });
19
- if (typeof input === "symbol") {
20
- p.cancel("Cancelled.");
21
- return;
23
+ // Direct usage: openhome set-jwt eyJ...
24
+ if (token) {
25
+ p.intro("🔑 Enable Management Features");
26
+ try {
27
+ saveJwt(token.trim());
28
+ success("Session token saved.");
29
+ p.outro(
30
+ "Management commands (list, delete, toggle, assign) are now unlocked.",
31
+ );
32
+ } catch (err) {
33
+ error(
34
+ `Failed to save token: ${err instanceof Error ? err.message : String(err)}`,
35
+ );
36
+ process.exit(1);
22
37
  }
23
- jwt = input;
38
+ return;
39
+ }
40
+
41
+ // Guided interactive flow
42
+ p.intro("🔑 Enable Management Features");
43
+
44
+ p.note(
45
+ [
46
+ "Some commands (list, delete, enable/disable abilities) need your",
47
+ "OpenHome session token to work.",
48
+ "",
49
+ "Think of it as a temporary key that proves you're logged in to",
50
+ "your account. You only need to do this once.",
51
+ ].join("\n"),
52
+ "What's this?",
53
+ );
54
+
55
+ console.log("");
56
+ console.log(
57
+ chalk.dim(` Opening ${chalk.bold("app.openhome.com")} in your browser...`),
58
+ );
59
+ openBrowser(OPENHOME_URL);
60
+ console.log("");
61
+
62
+ p.note(
63
+ [
64
+ `${chalk.bold("Step 1")} Make sure you're logged in to app.openhome.com`,
65
+ "",
66
+ `${chalk.bold("Step 2")} Open the browser console:`,
67
+ ` Mac → ${chalk.cyan("Cmd + Option + J")}`,
68
+ ` Windows / Linux → ${chalk.cyan("F12")}, then click the Console tab`,
69
+ "",
70
+ `${chalk.bold("Step 3")} Paste this command into the console and press Enter:`,
71
+ "",
72
+ ` ${chalk.green("copy(localStorage.getItem('token'))")}`,
73
+ "",
74
+ `${chalk.bold("Step 4")} Your token is now in your clipboard.`,
75
+ ` Come back here and paste it below.`,
76
+ ].join("\n"),
77
+ "How to get your token",
78
+ );
79
+
80
+ const input = await p.password({
81
+ message: "Paste your token here",
82
+ validate: (val) => {
83
+ if (!val || !val.trim()) return "Token is required";
84
+ if (val.trim().length < 20)
85
+ return "That doesn't look right — the token should be much longer";
86
+ },
87
+ });
88
+
89
+ if (typeof input === "symbol") {
90
+ p.cancel("Cancelled.");
91
+ return;
24
92
  }
25
93
 
26
94
  try {
27
- saveJwt(jwt.trim());
95
+ saveJwt((input as string).trim());
28
96
  success("Session token saved.");
29
97
  p.note(
30
- "Management commands (list, delete, toggle, assign) are now unlocked.",
31
- "Token saved",
98
+ [
99
+ "These commands are now unlocked:",
100
+ ` ${chalk.bold("openhome list")} — see all your deployed abilities`,
101
+ ` ${chalk.bold("openhome delete")} — remove an ability`,
102
+ ` ${chalk.bold("openhome toggle")} — enable or disable an ability`,
103
+ ` ${chalk.bold("openhome assign")} — link abilities to an agent`,
104
+ ].join("\n"),
105
+ "You're all set",
32
106
  );
33
- p.outro("Done.");
107
+ p.outro("Done!");
34
108
  } catch (err) {
35
109
  error(
36
110
  `Failed to save token: ${err instanceof Error ? err.message : String(err)}`,
@@ -3,7 +3,12 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { ApiClient, NotImplementedError } from "../api/client.js";
5
5
  import { MockApiClient } from "../api/mock-client.js";
6
- import { getApiKey, getConfig, getTrackedAbilities } from "../config/store.js";
6
+ import {
7
+ getApiKey,
8
+ getConfig,
9
+ getJwt,
10
+ getTrackedAbilities,
11
+ } from "../config/store.js";
7
12
  import { error, warn, info, p, handleCancel } from "../ui/format.js";
8
13
  import chalk from "chalk";
9
14