openhome-cli 0.1.0

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.
Files changed (45) hide show
  1. package/README.md +470 -0
  2. package/bin/openhome.js +2 -0
  3. package/dist/chunk-Q4UKUXDB.js +164 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +3184 -0
  6. package/dist/store-DR7EKQ5T.js +16 -0
  7. package/package.json +44 -0
  8. package/src/api/client.ts +231 -0
  9. package/src/api/contracts.ts +103 -0
  10. package/src/api/endpoints.ts +19 -0
  11. package/src/api/mock-client.ts +145 -0
  12. package/src/cli.ts +339 -0
  13. package/src/commands/agents.ts +88 -0
  14. package/src/commands/assign.ts +123 -0
  15. package/src/commands/chat.ts +265 -0
  16. package/src/commands/config-edit.ts +163 -0
  17. package/src/commands/delete.ts +107 -0
  18. package/src/commands/deploy.ts +430 -0
  19. package/src/commands/init.ts +895 -0
  20. package/src/commands/list.ts +78 -0
  21. package/src/commands/login.ts +54 -0
  22. package/src/commands/logout.ts +14 -0
  23. package/src/commands/logs.ts +174 -0
  24. package/src/commands/status.ts +174 -0
  25. package/src/commands/toggle.ts +118 -0
  26. package/src/commands/trigger.ts +193 -0
  27. package/src/commands/validate.ts +53 -0
  28. package/src/commands/whoami.ts +54 -0
  29. package/src/config/keychain.ts +62 -0
  30. package/src/config/store.ts +137 -0
  31. package/src/ui/format.ts +95 -0
  32. package/src/util/zip.ts +74 -0
  33. package/src/validation/rules.ts +71 -0
  34. package/src/validation/validator.ts +204 -0
  35. package/tasks/feature-request-sdk-api.md +246 -0
  36. package/tasks/prd-openhome-cli.md +605 -0
  37. package/templates/api/README.md.tmpl +11 -0
  38. package/templates/api/__init__.py.tmpl +0 -0
  39. package/templates/api/config.json.tmpl +4 -0
  40. package/templates/api/main.py.tmpl +30 -0
  41. package/templates/basic/README.md.tmpl +7 -0
  42. package/templates/basic/__init__.py.tmpl +0 -0
  43. package/templates/basic/config.json.tmpl +4 -0
  44. package/templates/basic/main.py.tmpl +22 -0
  45. package/tsconfig.json +19 -0
@@ -0,0 +1,605 @@
1
+ # PRD: OpenHome CLI Tool
2
+
3
+ ## Introduction
4
+
5
+ The OpenHome CLI (`openhome`) lets developers manage voice AI abilities entirely from the terminal. No dashboard, no manual zipping, no browser tab switching. Create, validate, deploy, and monitor abilities with one tool.
6
+
7
+ This PRD covers what exists today (v0.1.0 MVP), what needs server-side support, and a proposed browser-based authentication system to replace manual API key entry.
8
+
9
+ **Target users:** OpenHome ability developers (internal team + community)
10
+ **Platform:** Node.js 18+, macOS primary, Linux/Windows supported
11
+
12
+ ---
13
+
14
+ ## Goals
15
+
16
+ - Replace the dashboard-driven dev loop (zip, delete, upload, toggle, wait) with a single CLI command
17
+ - Match the UX quality of GitHub CLI, Vercel CLI, and Claude Code
18
+ - Define API contracts the backend team can implement
19
+ - Ship an auth flow that works without copy-pasting API keys
20
+ - Validate abilities locally before upload to catch errors early
21
+
22
+ ---
23
+
24
+ ## Current State (v0.1.0)
25
+
26
+ ### What Works Today
27
+
28
+ | Command | Status | Notes |
29
+ |---------|--------|-------|
30
+ | `openhome` (interactive menu) | Working | Arrow-key navigable, loops after each command |
31
+ | `openhome init [name]` | Working | Scaffolds ability with templates, validates on create |
32
+ | `openhome validate [path]` | Working | 12 blocked patterns, 5 required patterns, config checks |
33
+ | `openhome login` | Working | Manual API key paste, verifies via `get_personalities` |
34
+ | `openhome deploy [path]` | Partial | Validates + zips. Upload endpoint not yet live on server. `--dry-run` and `--mock` work. |
35
+ | `openhome list` | Stubbed | Returns mock data. Server endpoint not yet live. |
36
+ | `openhome status [name]` | Stubbed | Returns mock data. Server endpoint not yet live. |
37
+
38
+ ### What Does Not Exist Yet
39
+
40
+ | Feature | Blocked By |
41
+ |---------|-----------|
42
+ | Browser-based auth | Server OAuth support needed |
43
+ | Ability upload | `POST /api/sdk/abilities` not implemented |
44
+ | Ability listing | `GET /api/sdk/abilities` not implemented |
45
+ | Ability detail/history | `GET /api/sdk/abilities/:id` not implemented |
46
+ | Log streaming | WebSocket endpoint not implemented |
47
+ | Ability deletion | `DELETE /api/sdk/abilities/:id` not implemented |
48
+ | Local testing | No mock CapabilityWorker runtime exists |
49
+
50
+ ---
51
+
52
+ ## User Stories
53
+
54
+ ### US-001: Interactive Menu Navigation
55
+ **Description:** As a developer, I want to run `openhome` and navigate commands with arrow keys so I do not need to memorize subcommands.
56
+
57
+ **Acceptance Criteria:**
58
+ - [x] Running `openhome` with no args shows scrollable menu
59
+ - [x] Arrow keys navigate, Enter selects
60
+ - [x] Menu loops after each command
61
+ - [x] Ctrl+C exits cleanly
62
+ - [x] Direct subcommands (`openhome deploy ./path`) still work
63
+
64
+ ---
65
+
66
+ ### US-002: Scaffold New Ability
67
+ **Description:** As a developer, I want to scaffold a new ability with one command so I start with valid boilerplate.
68
+
69
+ **Acceptance Criteria:**
70
+ - [x] `openhome init my-ability` creates directory with `main.py`, `config.json`, `__init__.py`, `README.md`
71
+ - [x] Prompts for ability type (Skill, Brain Skill, Background Daemon)
72
+ - [x] Prompts for template (Basic, API)
73
+ - [x] Prompts for trigger words
74
+ - [x] Auto-validates after generation
75
+ - [x] Generated code passes all validation rules
76
+
77
+ ---
78
+
79
+ ### US-003: Validate Ability Locally
80
+ **Description:** As a developer, I want to check my ability for errors before deploying so I do not waste time uploading broken code.
81
+
82
+ **Acceptance Criteria:**
83
+ - [x] `openhome validate` checks current directory
84
+ - [x] `openhome validate ./path` checks specific directory
85
+ - [x] Reports all errors (red) and warnings (yellow)
86
+ - [x] Blocks on errors, passes with warnings
87
+ - [x] Checks: required files, config schema, Python patterns, blocked imports
88
+
89
+ ---
90
+
91
+ ### US-004: Deploy Ability
92
+ **Description:** As a developer, I want to deploy an ability with one command so I do not need to manually zip and upload.
93
+
94
+ **Acceptance Criteria:**
95
+ - [x] `openhome deploy` validates, zips, and uploads
96
+ - [x] `--dry-run` shows what would deploy without uploading
97
+ - [x] `--mock` uses fake API for testing
98
+ - [x] `--personality <id>` overrides default agent
99
+ - [x] Confirmation prompt before real deploy
100
+ - [ ] Real upload works (blocked: server endpoint)
101
+ - [x] Graceful fallback when endpoint returns NOT_IMPLEMENTED (saves zip, shows manual instructions)
102
+
103
+ ---
104
+
105
+ ### US-005: Manual API Key Login
106
+ **Description:** As a developer, I want to authenticate with my API key so the CLI can make authenticated requests.
107
+
108
+ **Acceptance Criteria:**
109
+ - [x] `openhome login` prompts for API key (masked input)
110
+ - [x] Verifies key against `get_personalities` endpoint
111
+ - [x] Stores key in macOS Keychain (config file fallback)
112
+ - [x] Lists agents and lets user set a default
113
+ - [x] Clear error on invalid key
114
+
115
+ ---
116
+
117
+ ### US-006: Browser-Based Login (Proposed)
118
+ **Description:** As a developer, I want to log in by clicking a link and confirming in my browser so I do not need to find and paste API keys.
119
+
120
+ **Acceptance Criteria:**
121
+ - [ ] `openhome login` opens browser to OpenHome auth page
122
+ - [ ] User clicks "Authorize" in browser
123
+ - [ ] CLI receives token automatically (no paste)
124
+ - [ ] Fallback: display URL + code if browser cannot open
125
+ - [ ] Manual key paste available via `openhome login --token`
126
+ - [ ] Token stored securely (Keychain primary, file fallback with 0600 permissions)
127
+ - [ ] Auth session expires and can be refreshed
128
+ - [ ] Works in SSH/headless environments (device flow)
129
+
130
+ ---
131
+
132
+ ### US-007: List Abilities
133
+ **Description:** As a developer, I want to see all my deployed abilities so I know what is live.
134
+
135
+ **Acceptance Criteria:**
136
+ - [x] `openhome list` shows table with name, version, status, last update
137
+ - [x] Status is color-coded (green=active, yellow=processing, red=failed)
138
+ - [x] `--mock` works for testing
139
+ - [ ] Real data from API (blocked: server endpoint)
140
+
141
+ ---
142
+
143
+ ### US-008: Ability Status and History
144
+ **Description:** As a developer, I want to check one ability's status and deploy history so I can debug failed deployments.
145
+
146
+ **Acceptance Criteria:**
147
+ - [x] `openhome status my-ability` shows detail panel
148
+ - [x] Shows: name, status badge, version, timestamps, linked agents
149
+ - [x] Shows validation errors if any
150
+ - [x] Shows deploy history with version, status, message, timestamp
151
+ - [x] Reads from local `config.json` if no name given
152
+ - [x] `--mock` works for testing
153
+ - [ ] Real data from API (blocked: server endpoint)
154
+
155
+ ---
156
+
157
+ ### US-009: Log Streaming (Planned)
158
+ **Description:** As a developer, I want to stream my ability's logs in real-time so I can debug issues without using the dashboard.
159
+
160
+ **Acceptance Criteria:**
161
+ - [ ] `openhome logs my-ability` streams logs to terminal
162
+ - [ ] `--follow` keeps connection open for new logs
163
+ - [ ] `--tail 100` shows last N lines
164
+ - [ ] Color-coded log levels (error=red, warn=yellow, info=white)
165
+ - [ ] Ctrl+C cleanly disconnects
166
+
167
+ ---
168
+
169
+ ### US-010: Ability Deletion (Planned)
170
+ **Description:** As a developer, I want to delete a deployed ability from the CLI so I do not need to use the dashboard.
171
+
172
+ **Acceptance Criteria:**
173
+ - [ ] `openhome delete my-ability` removes the ability
174
+ - [ ] Confirmation prompt before deletion
175
+ - [ ] Clear success/error message
176
+
177
+ ---
178
+
179
+ ## Functional Requirements
180
+
181
+ ### FR-1: Authentication (Current)
182
+ The CLI stores an API key via `openhome login`. Key is verified against `POST /api/sdk/get_personalities`. Stored in macOS Keychain (service: `openhome-cli`, account: `api-key`) with plaintext config file fallback.
183
+
184
+ ### FR-2: Authentication (Proposed — Browser-Based)
185
+ See [Authentication Architecture](#authentication-architecture) section below.
186
+
187
+ ### FR-3: Ability Scaffolding
188
+ `openhome init [name]` creates a directory with four files from templates. Supports two templates (Basic, API) and three ability categories (Skill, Brain Skill, Background Daemon). Auto-validates after creation.
189
+
190
+ ### FR-4: Local Validation
191
+ `openhome validate [path]` runs all rules from `validation/rules.ts`. 12 blocked Python patterns, 4 blocked imports, 5 required patterns, config schema check, hardcoded key warning, multiple class warning. Returns structured `{passed, errors, warnings}`.
192
+
193
+ ### FR-5: Deploy Pipeline
194
+ `openhome deploy [path]` runs: validate, read config, create ZIP (excludes `__pycache__`, `.pyc`, `.git`), confirm, upload. Supports `--dry-run` (no zip, no upload), `--mock` (fake API), `--personality <id>` (override agent).
195
+
196
+ ### FR-6: Ability Listing
197
+ `openhome list` calls `GET /api/sdk/abilities` and renders a table. Supports `--mock`.
198
+
199
+ ### FR-7: Ability Status
200
+ `openhome status [name]` calls `GET /api/sdk/abilities/:id` and renders detail panels. Falls back to local `config.json` for ability name. Supports `--mock`.
201
+
202
+ ### FR-8: Interactive Menu
203
+ Bare `openhome` shows a `@clack/prompts` select menu with all commands. Loops after each command. Prompts for required arguments inline.
204
+
205
+ ---
206
+
207
+ ## Authentication Architecture
208
+
209
+ ### Recommended: OAuth Device Flow (RFC 8628)
210
+
211
+ This is the same pattern GitHub CLI uses. It works everywhere: local machines, SSH sessions, containers, CI.
212
+
213
+ ### How It Works
214
+
215
+ ```
216
+ Developer CLI OpenHome Server
217
+ | | |
218
+ | openhome login | |
219
+ |------------------------>| |
220
+ | | POST /oauth/device/code |
221
+ | | {client_id} |
222
+ | |------------------------------>|
223
+ | | |
224
+ | | {device_code, user_code, |
225
+ | | verification_uri, |
226
+ | | interval, expires_in} |
227
+ | |<------------------------------|
228
+ | | |
229
+ | "Open this URL and | |
230
+ | enter code: ABCD-1234"| |
231
+ |<------------------------| |
232
+ | (browser auto-opens) | |
233
+ | | |
234
+ | User visits URL -------|----> Enters code, clicks OK |
235
+ | | |
236
+ | | POST /oauth/token |
237
+ | | {device_code, grant_type= |
238
+ | | device_authorization} |
239
+ | |------------------------------>|
240
+ | | (polls every 5s) |
241
+ | | |
242
+ | | {access_token, |
243
+ | | refresh_token, expires_in} |
244
+ | |<------------------------------|
245
+ | | |
246
+ | "Logged in as Brady!" | Store in Keychain |
247
+ |<------------------------| |
248
+ ```
249
+
250
+ ### Server-Side Requirements
251
+
252
+ The OpenHome backend needs to implement these endpoints:
253
+
254
+ #### `POST /oauth/device/code`
255
+
256
+ Request:
257
+ ```json
258
+ {
259
+ "client_id": "openhome-cli"
260
+ }
261
+ ```
262
+
263
+ Response (200):
264
+ ```json
265
+ {
266
+ "device_code": "random-uuid-device-code",
267
+ "user_code": "ABCD-1234",
268
+ "verification_uri": "https://app.openhome.com/device",
269
+ "interval": 5,
270
+ "expires_in": 900
271
+ }
272
+ ```
273
+
274
+ - `device_code` — unique identifier for this auth session (server keeps it, CLI polls with it)
275
+ - `user_code` — short human-readable code the user types into the browser (8 chars, uppercase + digits)
276
+ - `verification_uri` — URL where user enters the code
277
+ - `interval` — seconds between poll attempts
278
+ - `expires_in` — seconds until the device code expires (15 minutes recommended)
279
+
280
+ #### `POST /oauth/token`
281
+
282
+ Request (polling):
283
+ ```json
284
+ {
285
+ "client_id": "openhome-cli",
286
+ "device_code": "random-uuid-device-code",
287
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code"
288
+ }
289
+ ```
290
+
291
+ Response (pending — user has not authorized yet):
292
+ ```json
293
+ {
294
+ "error": "authorization_pending"
295
+ }
296
+ ```
297
+
298
+ Response (success — user authorized):
299
+ ```json
300
+ {
301
+ "access_token": "oh_abc123...",
302
+ "refresh_token": "oh_ref456...",
303
+ "token_type": "Bearer",
304
+ "expires_in": 86400
305
+ }
306
+ ```
307
+
308
+ Response (expired — user took too long):
309
+ ```json
310
+ {
311
+ "error": "expired_token"
312
+ }
313
+ ```
314
+
315
+ Response (denied — user clicked Deny):
316
+ ```json
317
+ {
318
+ "error": "access_denied"
319
+ }
320
+ ```
321
+
322
+ #### `POST /oauth/token` (refresh)
323
+
324
+ Request:
325
+ ```json
326
+ {
327
+ "client_id": "openhome-cli",
328
+ "grant_type": "refresh_token",
329
+ "refresh_token": "oh_ref456..."
330
+ }
331
+ ```
332
+
333
+ Response:
334
+ ```json
335
+ {
336
+ "access_token": "oh_new789...",
337
+ "refresh_token": "oh_newref...",
338
+ "token_type": "Bearer",
339
+ "expires_in": 86400
340
+ }
341
+ ```
342
+
343
+ #### Web Page: `https://app.openhome.com/device`
344
+
345
+ A simple page where the user:
346
+ 1. Sees "Enter the code shown in your terminal"
347
+ 2. Types the 8-character code
348
+ 3. Sees their account info and what the CLI is requesting
349
+ 4. Clicks "Authorize" or "Deny"
350
+
351
+ This page should work on mobile too (user might be SSHed from a phone).
352
+
353
+ ### CLI-Side Implementation
354
+
355
+ ```
356
+ openhome login
357
+ |
358
+ ├── Request device code from server
359
+ ├── Display: "Open https://app.openhome.com/device"
360
+ ├── Display: "Enter code: ABCD-1234"
361
+ ├── Try to open browser (macOS: open, Linux: xdg-open)
362
+ ├── Poll /oauth/token every 5s
363
+ │ ├── "authorization_pending" → keep polling
364
+ │ ├── "slow_down" → increase interval
365
+ │ ├── "expired_token" → error, ask to retry
366
+ │ ├── "access_denied" → error, exit
367
+ │ └── success → store tokens
368
+ ├── Store access_token in Keychain
369
+ ├── Store refresh_token in Keychain
370
+ ├── Fetch user info / personalities
371
+ └── Display: "Logged in as [name]!"
372
+
373
+ openhome login --token
374
+ |
375
+ └── (fallback) Prompt for manual API key paste (current flow)
376
+ ```
377
+
378
+ ### Token Lifecycle
379
+
380
+ - Access token expires after 24 hours (configurable by server)
381
+ - Before each API call, check if token is expired
382
+ - If expired, use refresh token to get new access token
383
+ - If refresh fails, prompt user to `openhome login` again
384
+ - `openhome logout` clears all tokens from Keychain and config
385
+
386
+ ### Why Device Flow Over PKCE
387
+
388
+ | Factor | Device Flow | PKCE + Localhost |
389
+ |--------|------------|-----------------|
390
+ | Works in SSH/containers | Yes | No |
391
+ | Needs local HTTP server | No | Yes |
392
+ | Port conflicts | None | Possible |
393
+ | Corporate firewalls | Works | May block localhost |
394
+ | Implementation complexity | Simpler | More complex |
395
+ | UX | Type short code | Click authorize (slightly easier) |
396
+
397
+ GitHub CLI chose device flow. It is battle-tested at scale. OpenHome developers often work via SSH or in constrained environments. Device flow works everywhere.
398
+
399
+ ---
400
+
401
+ ## API Contracts (Server-Side)
402
+
403
+ These endpoints need to be implemented by the OpenHome backend team.
404
+
405
+ ### Existing (Working)
406
+
407
+ | Endpoint | Method | Auth | Purpose |
408
+ |----------|--------|------|---------|
409
+ | `/api/sdk/get_personalities` | POST | API key in body | List user's agents |
410
+
411
+ ### Needed for CLI
412
+
413
+ | Endpoint | Method | Auth | Purpose | Priority |
414
+ |----------|--------|------|---------|----------|
415
+ | `POST /api/sdk/abilities` | POST | Bearer token | Upload ability ZIP | High |
416
+ | `GET /api/sdk/abilities` | GET | Bearer token | List user's abilities | High |
417
+ | `GET /api/sdk/abilities/:id` | GET | Bearer token | Ability detail + deploy history | Medium |
418
+ | `DELETE /api/sdk/abilities/:id` | DELETE | Bearer token | Remove ability | Medium |
419
+ | `POST /oauth/device/code` | POST | None (public) | Start device auth flow | High |
420
+ | `POST /oauth/token` | POST | None (public) | Exchange/refresh tokens | High |
421
+ | `GET /api/sdk/abilities/:id/logs` | WebSocket | Bearer token | Stream ability logs | Low |
422
+
423
+ ### Upload Endpoint Detail
424
+
425
+ ```
426
+ POST /api/sdk/abilities
427
+ Content-Type: multipart/form-data
428
+
429
+ Fields:
430
+ ability: (binary ZIP file)
431
+ personality_id: (optional string — agent to attach to)
432
+
433
+ Response 200:
434
+ {
435
+ "ability_id": "abl_abc123",
436
+ "unique_name": "my-weather-bot",
437
+ "version": 3,
438
+ "status": "processing",
439
+ "validation_errors": [],
440
+ "created_at": "2026-03-18T12:00:00Z",
441
+ "message": "Upload received, processing..."
442
+ }
443
+
444
+ Response 400:
445
+ {
446
+ "error": {
447
+ "code": "VALIDATION_FAILED",
448
+ "message": "Missing required file: main.py",
449
+ "details": { "missing_files": ["main.py"] }
450
+ }
451
+ }
452
+ ```
453
+
454
+ ### Error Format (All Endpoints)
455
+
456
+ ```json
457
+ {
458
+ "error": {
459
+ "code": "UNAUTHORIZED | VALIDATION_FAILED | NOT_FOUND | NOT_IMPLEMENTED",
460
+ "message": "Human-readable error message",
461
+ "details": {}
462
+ }
463
+ }
464
+ ```
465
+
466
+ The CLI handles `NOT_IMPLEMENTED` gracefully — shows a helpful message and saves the request locally instead of crashing.
467
+
468
+ ---
469
+
470
+ ## Security Issues to Fix (Before Public Release)
471
+
472
+ ### HIGH: Shell Injection in Keychain Storage
473
+
474
+ **File:** `src/config/keychain.ts`
475
+ **Problem:** API key is interpolated directly into a shell command string via `execSync`. A malicious key value could execute arbitrary commands.
476
+ **Fix:** Replace `execSync` with `execFileSync` using an argument array (bypasses shell entirely).
477
+
478
+ ### HIGH: API Key Sent in Request Body
479
+
480
+ **File:** `src/api/client.ts` (`getPersonalities` method)
481
+ **Problem:** API key is sent both in the `Authorization` header AND in the POST body. Body values appear in server logs, proxy logs, and request inspection tools.
482
+ **Fix:** Remove `api_key` from request body. The header is sufficient.
483
+ **Server action needed:** Deprecate `api_key` body parameter in `get_personalities`, accept header-only auth.
484
+
485
+ ### MEDIUM: Config File Permissions
486
+
487
+ **File:** `src/config/store.ts`
488
+ **Problem:** `~/.openhome/config.json` is created with default permissions (world-readable). If API key falls back to config file, any local user can read it.
489
+ **Fix:** Create directory with `mode: 0o700`, write file with `mode: 0o600`.
490
+
491
+ ### MEDIUM: No HTTPS Enforcement
492
+
493
+ **File:** `src/api/client.ts`
494
+ **Problem:** Custom `api_base_url` accepts `http://`. All requests including auth headers would be sent unencrypted.
495
+ **Fix:** Validate that base URL starts with `https://` in the ApiClient constructor.
496
+
497
+ ### MEDIUM: ZIP Includes Sensitive Files
498
+
499
+ **File:** `src/util/zip.ts`
500
+ **Problem:** Only excludes `__pycache__`, `.pyc`, `.git`. Does not exclude `.env`, `secrets.json`, `*.key`, `*.pem`.
501
+ **Fix:** Add `.env`, `.env.*`, `secrets.*`, `*.key`, `*.pem` to exclusion list. Add a validator warning when these files exist.
502
+
503
+ ### MEDIUM: Narrow Hardcoded Key Detection
504
+
505
+ **File:** `src/validation/rules.ts`
506
+ **Problem:** Only catches keys starting with `sk_`, `sk-`, `key_`. Misses AWS keys (`AKIA...`), GitHub PATs (`ghp_...`), and generic `API_KEY = "..."` patterns.
507
+ **Fix:** Expand pattern list to cover common credential formats.
508
+
509
+ ---
510
+
511
+ ## Non-Goals (Out of Scope)
512
+
513
+ - **Local ability testing runtime** — No mock CapabilityWorker. Too complex for MVP. Deploy to test.
514
+ - **GUI/TUI dashboard** — CLI only. No Ink/Blessed terminal UI.
515
+ - **Multi-language abilities** — Python only. OpenHome does not support other languages yet.
516
+ - **Marketplace publishing** — Not in CLI scope. Use dashboard.
517
+ - **Team/org management** — Not in CLI scope.
518
+ - **Ability versioning/rollback** — Server would need to support this first.
519
+
520
+ ---
521
+
522
+ ## Technical Considerations
523
+
524
+ ### Dependencies (Current)
525
+
526
+ | Package | Version | Purpose |
527
+ |---------|---------|---------|
528
+ | commander | 12.x | CLI argument parsing |
529
+ | @clack/prompts | 1.x | Interactive menus, spinners, prompts |
530
+ | chalk | 5.x | Terminal colors |
531
+ | archiver | 7.x | ZIP creation |
532
+
533
+ ### Dependencies (Needed for Auth)
534
+
535
+ | Package | Purpose |
536
+ |---------|---------|
537
+ | `open` | Cross-platform browser opening |
538
+ | None for device flow | Just fetch + setInterval polling |
539
+
540
+ ### Build
541
+
542
+ - TypeScript compiled via tsup (ESM output)
543
+ - Node.js 18+ required
544
+ - `npm link` for global install during development
545
+ - Eventually publish to npm as `openhome-cli`
546
+
547
+ ### Compatibility
548
+
549
+ | Platform | Keychain | Browser Open | Status |
550
+ |----------|----------|-------------|--------|
551
+ | macOS | Yes (security CLI) | Yes (`open`) | Full support |
552
+ | Linux | No (config fallback) | Yes (`xdg-open`) | Works, less secure storage |
553
+ | Windows | No (config fallback) | Yes (`start`) | Works, less secure storage |
554
+ | SSH/headless | N/A | No | Device flow works, manual token fallback |
555
+
556
+ ---
557
+
558
+ ## Success Metrics
559
+
560
+ - Developer can go from `openhome init` to deployed ability in under 5 minutes
561
+ - Zero dashboard visits required for standard ability dev loop
562
+ - Auth flow completes in under 30 seconds (browser-based)
563
+ - All abilities pass validation before upload (no server-side validation failures for structure issues)
564
+ - CLI handles all API errors gracefully (no crashes, clear messages)
565
+
566
+ ---
567
+
568
+ ## Implementation Priority
569
+
570
+ ### Phase 1: Security Fixes (Do First)
571
+ 1. Fix shell injection in `keychain.ts`
572
+ 2. Harden config file permissions
573
+ 3. Add HTTPS enforcement
574
+ 4. Expand ZIP exclusion list
575
+
576
+ ### Phase 2: Server Endpoints (Backend Team)
577
+ 5. `POST /api/sdk/abilities` (upload)
578
+ 6. `GET /api/sdk/abilities` (list)
579
+ 7. `GET /api/sdk/abilities/:id` (detail)
580
+ 8. Wire CLI to real endpoints (remove mock fallback as default)
581
+
582
+ ### Phase 3: Browser Auth (Requires Server + CLI)
583
+ 9. Server: Implement `/oauth/device/code` and `/oauth/token`
584
+ 10. Server: Build device authorization web page
585
+ 11. CLI: Implement device flow in `openhome login`
586
+ 12. CLI: Token refresh before API calls
587
+ 13. CLI: `openhome logout` command
588
+
589
+ ### Phase 4: Extended Features
590
+ 14. `openhome logs` (WebSocket streaming)
591
+ 15. `openhome delete` (ability removal)
592
+ 16. `openhome watch` (auto-deploy on file changes)
593
+ 17. Publish to npm
594
+
595
+ ---
596
+
597
+ ## Open Questions
598
+
599
+ 1. **Token format:** What format will OpenHome access tokens use? JWT? Opaque? This affects whether the CLI can check expiry locally.
600
+ 2. **Rate limits:** What are the API rate limits? The CLI should show clear messages when rate-limited.
601
+ 3. **Ability size limit:** Is there a max ZIP size the server accepts? The CLI should enforce this client-side.
602
+ 4. **Multi-agent deploy:** Can one ability be attached to multiple agents in a single deploy? Or does the user need to deploy once per agent?
603
+ 5. **Versioning:** Does re-deploying the same `unique_name` auto-increment the version? Can the user roll back?
604
+ 6. **WebSocket auth:** How will the log streaming endpoint authenticate? Bearer token in query param? Upgrade header?
605
+ 7. **OAuth client registration:** Does the device flow need a registered OAuth client ID, or can it use a hardcoded public client ID (`openhome-cli`)?
@@ -0,0 +1,11 @@
1
+ # {{DISPLAY_NAME}}
2
+
3
+ A custom OpenHome ability that calls an external API.
4
+
5
+ ## Trigger Words
6
+
7
+ {{HOTWORD_LIST}}
8
+
9
+ ## Configuration
10
+
11
+ This ability requires an `api_key` secret configured in your OpenHome personality settings.
File without changes
@@ -0,0 +1,4 @@
1
+ {
2
+ "unique_name": "{{UNIQUE_NAME}}",
3
+ "matching_hotwords": {{HOTWORDS}}
4
+ }
@@ -0,0 +1,30 @@
1
+ import requests
2
+ from src.agent.capability import MatchingCapability
3
+ from src.main import AgentWorker
4
+ from src.agent.capability_worker import CapabilityWorker
5
+
6
+
7
+ class {{CLASS_NAME}}(MatchingCapability):
8
+ worker: AgentWorker = None
9
+ capability_worker: CapabilityWorker = None
10
+
11
+ @classmethod
12
+ def register_capability(cls) -> "MatchingCapability":
13
+ # {{register_capability}}
14
+ pass
15
+
16
+ def call(self, worker: AgentWorker):
17
+ self.worker = worker
18
+ self.capability_worker = CapabilityWorker(self.worker)
19
+ self.worker.session_tasks.create(self.run())
20
+
21
+ async def run(self):
22
+ api_key = self.capability_worker.get_single_key("api_key")
23
+ response = requests.get(
24
+ "https://api.example.com/data",
25
+ headers={"Authorization": f"Bearer {api_key}"},
26
+ timeout=10,
27
+ )
28
+ data = response.json()
29
+ await self.capability_worker.speak(f"Here's what I found: {data.get('result', 'nothing')}")
30
+ self.capability_worker.resume_normal_flow()
@@ -0,0 +1,7 @@
1
+ # {{DISPLAY_NAME}}
2
+
3
+ A custom OpenHome ability.
4
+
5
+ ## Trigger Words
6
+
7
+ {{HOTWORD_LIST}}
File without changes
@@ -0,0 +1,4 @@
1
+ {
2
+ "unique_name": "{{UNIQUE_NAME}}",
3
+ "matching_hotwords": {{HOTWORDS}}
4
+ }
@@ -0,0 +1,22 @@
1
+ from src.agent.capability import MatchingCapability
2
+ from src.main import AgentWorker
3
+ from src.agent.capability_worker import CapabilityWorker
4
+
5
+
6
+ class {{CLASS_NAME}}(MatchingCapability):
7
+ worker: AgentWorker = None
8
+ capability_worker: CapabilityWorker = None
9
+
10
+ @classmethod
11
+ def register_capability(cls) -> "MatchingCapability":
12
+ # {{register_capability}}
13
+ pass
14
+
15
+ def call(self, worker: AgentWorker):
16
+ self.worker = worker
17
+ self.capability_worker = CapabilityWorker(self.worker)
18
+ self.worker.session_tasks.create(self.run())
19
+
20
+ async def run(self):
21
+ await self.capability_worker.speak("Hello! This ability is working.")
22
+ self.capability_worker.resume_normal_flow()