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.
- package/README.md +470 -0
- package/bin/openhome.js +2 -0
- package/dist/chunk-Q4UKUXDB.js +164 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3184 -0
- package/dist/store-DR7EKQ5T.js +16 -0
- package/package.json +44 -0
- package/src/api/client.ts +231 -0
- package/src/api/contracts.ts +103 -0
- package/src/api/endpoints.ts +19 -0
- package/src/api/mock-client.ts +145 -0
- package/src/cli.ts +339 -0
- package/src/commands/agents.ts +88 -0
- package/src/commands/assign.ts +123 -0
- package/src/commands/chat.ts +265 -0
- package/src/commands/config-edit.ts +163 -0
- package/src/commands/delete.ts +107 -0
- package/src/commands/deploy.ts +430 -0
- package/src/commands/init.ts +895 -0
- package/src/commands/list.ts +78 -0
- package/src/commands/login.ts +54 -0
- package/src/commands/logout.ts +14 -0
- package/src/commands/logs.ts +174 -0
- package/src/commands/status.ts +174 -0
- package/src/commands/toggle.ts +118 -0
- package/src/commands/trigger.ts +193 -0
- package/src/commands/validate.ts +53 -0
- package/src/commands/whoami.ts +54 -0
- package/src/config/keychain.ts +62 -0
- package/src/config/store.ts +137 -0
- package/src/ui/format.ts +95 -0
- package/src/util/zip.ts +74 -0
- package/src/validation/rules.ts +71 -0
- package/src/validation/validator.ts +204 -0
- package/tasks/feature-request-sdk-api.md +246 -0
- package/tasks/prd-openhome-cli.md +605 -0
- package/templates/api/README.md.tmpl +11 -0
- package/templates/api/__init__.py.tmpl +0 -0
- package/templates/api/config.json.tmpl +4 -0
- package/templates/api/main.py.tmpl +30 -0
- package/templates/basic/README.md.tmpl +7 -0
- package/templates/basic/__init__.py.tmpl +0 -0
- package/templates/basic/config.json.tmpl +4 -0
- package/templates/basic/main.py.tmpl +22 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const REQUIRED_FILES = ["main.py", "README.md", "__init__.py"];
|
|
2
|
+
|
|
3
|
+
export const BLOCKED_IMPORTS = [
|
|
4
|
+
"redis",
|
|
5
|
+
"from src.utils.db_handler",
|
|
6
|
+
"connection_manager",
|
|
7
|
+
"user_config",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export interface BlockedPattern {
|
|
11
|
+
regex: RegExp;
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const BLOCKED_PATTERNS: BlockedPattern[] = [
|
|
16
|
+
{
|
|
17
|
+
regex: /\bprint\s*\(/,
|
|
18
|
+
message: "Use self.worker.editor_logging_handler instead of print()",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
regex: /\basyncio\.sleep\s*\(/,
|
|
22
|
+
message: "Use self.worker.session_tasks.sleep() instead",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
regex: /\basyncio\.create_task\s*\(/,
|
|
26
|
+
message: "Use self.worker.session_tasks.create() instead",
|
|
27
|
+
},
|
|
28
|
+
{ regex: /\bexec\s*\(/, message: "exec() not allowed" },
|
|
29
|
+
{ regex: /\beval\s*\(/, message: "eval() not allowed" },
|
|
30
|
+
{ regex: /\bpickle\./, message: "pickle not allowed" },
|
|
31
|
+
{ regex: /\bdill\./, message: "dill not allowed" },
|
|
32
|
+
{ regex: /\bshelve\./, message: "shelve not allowed" },
|
|
33
|
+
{ regex: /\bmarshal\./, message: "marshal not allowed" },
|
|
34
|
+
{
|
|
35
|
+
regex: /\bopen\s*\(/,
|
|
36
|
+
message: "raw open() not allowed — use capability_worker file helpers",
|
|
37
|
+
},
|
|
38
|
+
{ regex: /\bassert\s+/, message: "assert not allowed" },
|
|
39
|
+
{ regex: /\bhashlib\.md5\s*\(/, message: "MD5 not allowed" },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export interface RequiredPattern {
|
|
43
|
+
regex: RegExp;
|
|
44
|
+
message: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const REQUIRED_PATTERNS: RequiredPattern[] = [
|
|
48
|
+
{
|
|
49
|
+
regex: /resume_normal_flow\s*\(/,
|
|
50
|
+
message: "resume_normal_flow() must be called",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
regex: /class\s+\w+.*MatchingCapability/,
|
|
54
|
+
message: "Class must extend MatchingCapability",
|
|
55
|
+
},
|
|
56
|
+
{ regex: /def\s+call\s*\(/, message: "Must have a call() method" },
|
|
57
|
+
{
|
|
58
|
+
regex: /worker\s*:\s*AgentWorker\s*=\s*None/,
|
|
59
|
+
message: "Must declare worker: AgentWorker = None",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
regex: /capability_worker\s*:\s*CapabilityWorker\s*=\s*None/,
|
|
63
|
+
message: "Must declare capability_worker: CapabilityWorker = None",
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export const REGISTER_CAPABILITY_PATTERN = /#\s?\{\{register[_ ]capability\}\}/;
|
|
68
|
+
|
|
69
|
+
export const HARDCODED_KEY_PATTERN = /(sk_|sk-|key_)[a-zA-Z0-9]{20,}/;
|
|
70
|
+
|
|
71
|
+
export const MULTIPLE_CLASSES_PATTERN = /^class\s+/gm;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
REQUIRED_FILES,
|
|
5
|
+
BLOCKED_IMPORTS,
|
|
6
|
+
BLOCKED_PATTERNS,
|
|
7
|
+
REQUIRED_PATTERNS,
|
|
8
|
+
REGISTER_CAPABILITY_PATTERN,
|
|
9
|
+
HARDCODED_KEY_PATTERN,
|
|
10
|
+
MULTIPLE_CLASSES_PATTERN,
|
|
11
|
+
} from "./rules.js";
|
|
12
|
+
|
|
13
|
+
export interface ValidationIssue {
|
|
14
|
+
severity: "error" | "warning";
|
|
15
|
+
message: string;
|
|
16
|
+
file?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ValidationResult {
|
|
20
|
+
passed: boolean;
|
|
21
|
+
errors: ValidationIssue[];
|
|
22
|
+
warnings: ValidationIssue[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readFile(filePath: string): string | null {
|
|
26
|
+
try {
|
|
27
|
+
return readFileSync(filePath, "utf8");
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validateAbility(dirPath: string): ValidationResult {
|
|
34
|
+
const errors: ValidationIssue[] = [];
|
|
35
|
+
const warnings: ValidationIssue[] = [];
|
|
36
|
+
|
|
37
|
+
// 1. Check required files exist
|
|
38
|
+
for (const required of REQUIRED_FILES) {
|
|
39
|
+
const fullPath = join(dirPath, required);
|
|
40
|
+
if (!existsSync(fullPath)) {
|
|
41
|
+
errors.push({
|
|
42
|
+
severity: "error",
|
|
43
|
+
message: `Missing required file: ${required}`,
|
|
44
|
+
file: required,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. Validate config.json
|
|
50
|
+
const configPath = join(dirPath, "config.json");
|
|
51
|
+
if (existsSync(configPath)) {
|
|
52
|
+
const configContent = readFile(configPath);
|
|
53
|
+
if (configContent) {
|
|
54
|
+
try {
|
|
55
|
+
const config = JSON.parse(configContent) as Record<string, unknown>;
|
|
56
|
+
if (typeof config.unique_name !== "string" || !config.unique_name) {
|
|
57
|
+
errors.push({
|
|
58
|
+
severity: "error",
|
|
59
|
+
message: "config.json: unique_name must be a non-empty string",
|
|
60
|
+
file: "config.json",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
!Array.isArray(config.matching_hotwords) ||
|
|
65
|
+
!(config.matching_hotwords as unknown[]).every(
|
|
66
|
+
(h) => typeof h === "string",
|
|
67
|
+
)
|
|
68
|
+
) {
|
|
69
|
+
errors.push({
|
|
70
|
+
severity: "error",
|
|
71
|
+
message:
|
|
72
|
+
"config.json: matching_hotwords must be an array of strings",
|
|
73
|
+
file: "config.json",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
errors.push({
|
|
78
|
+
severity: "error",
|
|
79
|
+
message: "config.json: invalid JSON",
|
|
80
|
+
file: "config.json",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
errors.push({
|
|
86
|
+
severity: "error",
|
|
87
|
+
message: "Missing required file: config.json",
|
|
88
|
+
file: "config.json",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 3. Validate main.py in detail
|
|
93
|
+
const mainPath = join(dirPath, "main.py");
|
|
94
|
+
const mainContent = readFile(mainPath);
|
|
95
|
+
|
|
96
|
+
if (mainContent) {
|
|
97
|
+
const lines = mainContent.split("\n");
|
|
98
|
+
|
|
99
|
+
// Check blocked imports — only match actual import/from statements, not substrings in strings
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
const line = lines[i].trim();
|
|
102
|
+
// Skip lines that are clearly string content (inside quotes)
|
|
103
|
+
if (
|
|
104
|
+
!line.startsWith("import ") &&
|
|
105
|
+
!line.startsWith("from ") &&
|
|
106
|
+
!line.includes("import ")
|
|
107
|
+
)
|
|
108
|
+
continue;
|
|
109
|
+
for (const blocked of BLOCKED_IMPORTS) {
|
|
110
|
+
if (line.includes(blocked)) {
|
|
111
|
+
errors.push({
|
|
112
|
+
severity: "error",
|
|
113
|
+
message: `Blocked import "${blocked}" on line ${i + 1}`,
|
|
114
|
+
file: "main.py",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check blocked patterns (line by line)
|
|
121
|
+
for (let i = 0; i < lines.length; i++) {
|
|
122
|
+
const line = lines[i];
|
|
123
|
+
for (const { regex, message } of BLOCKED_PATTERNS) {
|
|
124
|
+
if (regex.test(line)) {
|
|
125
|
+
errors.push({
|
|
126
|
+
severity: "error",
|
|
127
|
+
message: `${message} (line ${i + 1})`,
|
|
128
|
+
file: "main.py",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check required patterns (whole file)
|
|
135
|
+
for (const { regex, message } of REQUIRED_PATTERNS) {
|
|
136
|
+
if (!regex.test(mainContent)) {
|
|
137
|
+
errors.push({ severity: "error", message, file: "main.py" });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check register_capability tag
|
|
142
|
+
if (!REGISTER_CAPABILITY_PATTERN.test(mainContent)) {
|
|
143
|
+
errors.push({
|
|
144
|
+
severity: "error",
|
|
145
|
+
message: "Missing #{{register_capability}} tag in main.py",
|
|
146
|
+
file: "main.py",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check for hardcoded keys (warning)
|
|
151
|
+
const keyMatches = mainContent.match(HARDCODED_KEY_PATTERN);
|
|
152
|
+
if (keyMatches) {
|
|
153
|
+
warnings.push({
|
|
154
|
+
severity: "warning",
|
|
155
|
+
message: `Possible hardcoded API key detected in main.py — use capability_worker.get_single_key() instead`,
|
|
156
|
+
file: "main.py",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check for multiple classes (warning)
|
|
161
|
+
const classMatches = mainContent.match(MULTIPLE_CLASSES_PATTERN);
|
|
162
|
+
if (classMatches && classMatches.length > 1) {
|
|
163
|
+
warnings.push({
|
|
164
|
+
severity: "warning",
|
|
165
|
+
message: `Multiple class definitions found (${classMatches.length}). Only one MatchingCapability class is expected.`,
|
|
166
|
+
file: "main.py",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 4. Scan all .py files for blocked patterns
|
|
172
|
+
let pyFiles: string[] = [];
|
|
173
|
+
try {
|
|
174
|
+
pyFiles = readdirSync(dirPath).filter(
|
|
175
|
+
(f) => f.endsWith(".py") && f !== "main.py",
|
|
176
|
+
);
|
|
177
|
+
} catch {
|
|
178
|
+
// ignore
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const pyFile of pyFiles) {
|
|
182
|
+
const content = readFile(join(dirPath, pyFile));
|
|
183
|
+
if (!content) continue;
|
|
184
|
+
const lines = content.split("\n");
|
|
185
|
+
for (let i = 0; i < lines.length; i++) {
|
|
186
|
+
const line = lines[i];
|
|
187
|
+
for (const { regex, message } of BLOCKED_PATTERNS) {
|
|
188
|
+
if (regex.test(line)) {
|
|
189
|
+
errors.push({
|
|
190
|
+
severity: "error",
|
|
191
|
+
message: `${message} (line ${i + 1})`,
|
|
192
|
+
file: pyFile,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
passed: errors.length === 0,
|
|
201
|
+
errors,
|
|
202
|
+
warnings,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Feature Request: SDK API Endpoints for Ability Management
|
|
2
|
+
|
|
3
|
+
**From:** OpenHome CLI Team
|
|
4
|
+
**Date:** 2026-03-18
|
|
5
|
+
**Priority:** High
|
|
6
|
+
**Context:** Building an open-source CLI tool (`openhome-cli`) for developers to manage abilities from the terminal
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
The OpenHome SDK currently exposes one endpoint (`POST /api/sdk/get_personalities`). To enable CLI-based ability deployment, we need CRUD endpoints for abilities.
|
|
13
|
+
|
|
14
|
+
The CLI is fully built and ready to call these endpoints — it currently falls back to saving a zip locally with instructions to upload via the web dashboard.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Current State
|
|
19
|
+
|
|
20
|
+
| Capability | Web Dashboard | SDK/API | CLI Ready? |
|
|
21
|
+
|-----------|--------------|---------|------------|
|
|
22
|
+
| List agents | Yes | **Yes** (`get_personalities`) | Yes |
|
|
23
|
+
| Create ability | Yes | No | Yes (blocked) |
|
|
24
|
+
| List abilities | Yes | No | Yes (blocked) |
|
|
25
|
+
| Get ability detail | Yes | No | Yes (blocked) |
|
|
26
|
+
| Delete ability | Yes | No | Not yet |
|
|
27
|
+
| Update ability code | Yes (Live Editor) | No | Not yet |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Requested Endpoints
|
|
32
|
+
|
|
33
|
+
### 1. `POST /api/sdk/abilities` — Create/Upload Ability
|
|
34
|
+
|
|
35
|
+
Upload a new ability with all metadata matching the web form.
|
|
36
|
+
|
|
37
|
+
**Request:** `multipart/form-data`
|
|
38
|
+
|
|
39
|
+
| Field | Type | Required | Description |
|
|
40
|
+
|-------|------|----------|-------------|
|
|
41
|
+
| `api_key` | string | Yes | Authentication |
|
|
42
|
+
| `ability` | file (zip) | Yes | Ability code archive |
|
|
43
|
+
| `image` | file (png/jpg) | Yes | Marketplace icon |
|
|
44
|
+
| `name` | string | Yes | Unique ability name |
|
|
45
|
+
| `description` | string | Yes | Marketplace description |
|
|
46
|
+
| `category` | enum | Yes | `skill`, `brain_skill`, `background_daemon` |
|
|
47
|
+
| `matching_hotwords` | JSON string | Yes | Array of trigger words |
|
|
48
|
+
| `personality_id` | string | No | Agent to attach ability to |
|
|
49
|
+
|
|
50
|
+
**Response (200):**
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"ability_id": "abl_abc123",
|
|
54
|
+
"unique_name": "my-weather-bot",
|
|
55
|
+
"version": 1,
|
|
56
|
+
"status": "processing",
|
|
57
|
+
"message": "Ability uploaded successfully"
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Errors:**
|
|
62
|
+
- `401` — Invalid API key
|
|
63
|
+
- `400 VALIDATION_FAILED` — Missing files, bad config, blocked imports
|
|
64
|
+
- `409` — Ability with this name already exists (use update endpoint instead)
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### 2. `GET /api/sdk/abilities` — List User's Abilities
|
|
69
|
+
|
|
70
|
+
**Request:**
|
|
71
|
+
```
|
|
72
|
+
POST /api/sdk/abilities/list
|
|
73
|
+
Content-Type: application/json
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
"api_key": "..."
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
(Using POST with `api_key` in body to match the existing `get_personalities` pattern.)
|
|
81
|
+
|
|
82
|
+
**Response (200):**
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"abilities": [
|
|
86
|
+
{
|
|
87
|
+
"ability_id": "abl_abc123",
|
|
88
|
+
"unique_name": "weather-check",
|
|
89
|
+
"name": "Weather Check",
|
|
90
|
+
"description": "Check the weather by city",
|
|
91
|
+
"category": "skill",
|
|
92
|
+
"version": 3,
|
|
93
|
+
"status": "active",
|
|
94
|
+
"personality_ids": ["pers_alice"],
|
|
95
|
+
"created_at": "2026-01-10T12:00:00Z",
|
|
96
|
+
"updated_at": "2026-03-01T09:30:00Z"
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### 3. `POST /api/sdk/abilities/get` — Get Ability Detail
|
|
105
|
+
|
|
106
|
+
**Request:**
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"api_key": "...",
|
|
110
|
+
"ability_id": "abl_abc123"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Response:** Same as list item plus:
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"matching_hotwords": ["check weather", "weather please"],
|
|
118
|
+
"validation_errors": [],
|
|
119
|
+
"deploy_history": [
|
|
120
|
+
{
|
|
121
|
+
"version": 3,
|
|
122
|
+
"status": "success",
|
|
123
|
+
"timestamp": "2026-03-01T09:30:00Z"
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### 4. `POST /api/sdk/abilities/delete` — Delete Ability
|
|
132
|
+
|
|
133
|
+
**Request:**
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"api_key": "...",
|
|
137
|
+
"ability_id": "abl_abc123"
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Response (200):**
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"message": "Ability deleted",
|
|
145
|
+
"ability_id": "abl_abc123"
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Authentication Pattern
|
|
152
|
+
|
|
153
|
+
Following the existing `get_personalities` pattern: `api_key` in the JSON body rather than Bearer header. This keeps the SDK consistent. (The CLI also sends a Bearer header for forward compatibility.)
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Why This Matters
|
|
158
|
+
|
|
159
|
+
1. **Developer experience** — Developers want to stay in their terminal during ability development: edit code → deploy → test → iterate
|
|
160
|
+
2. **CI/CD** — Teams can automate ability deployment in their pipelines
|
|
161
|
+
3. **Open source adoption** — A CLI lowers the barrier for the developer community
|
|
162
|
+
4. **Parity with modern platforms** — Vercel, Netlify, Cloudflare Workers, etc. all have CLI deploy tools
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## What's Already Built
|
|
167
|
+
|
|
168
|
+
The CLI (`openhome-cli`) handles:
|
|
169
|
+
- Login/logout with API key (macOS Keychain + config fallback)
|
|
170
|
+
- Agent listing (using existing `get_personalities`)
|
|
171
|
+
- Ability scaffolding with templates (Skill, Brain Skill, Background Daemon)
|
|
172
|
+
- Validation (required files, Python patterns, blocked imports)
|
|
173
|
+
- ZIP creation with security exclusions
|
|
174
|
+
- Multipart form upload with name, description, image, category, trigger words
|
|
175
|
+
- Graceful fallback when endpoints return NOT_IMPLEMENTED
|
|
176
|
+
- Interactive arrow-key menu (bare `openhome` command)
|
|
177
|
+
- Direct subcommands (`openhome deploy ./my-ability --dry-run`)
|
|
178
|
+
- Mock mode for testing without network
|
|
179
|
+
|
|
180
|
+
**The CLI is ready to ship the moment these endpoints are live.**
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Already Working: WebSocket Chat + Ability Triggering
|
|
185
|
+
|
|
186
|
+
We discovered the WebSocket endpoint is live:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
wss://app.openhome.com/websocket/voice-stream/{API_KEY}/{AGENT_ID}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The CLI now has an `openhome chat` command that connects to this WebSocket and lets developers:
|
|
193
|
+
- Send text messages to their agent from the terminal
|
|
194
|
+
- **Trigger abilities by sending trigger words** (e.g., typing "play aquaprime" activates the ability)
|
|
195
|
+
- Receive text responses back in real-time
|
|
196
|
+
- Test ability trigger words without needing voice/browser
|
|
197
|
+
|
|
198
|
+
This is a powerful development tool — developers can test whether their trigger words activate abilities correctly without leaving the terminal.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Additional Feature Requests: Device/Agent Control
|
|
203
|
+
|
|
204
|
+
The dashboard exposes these controls that would be valuable as SDK endpoints:
|
|
205
|
+
|
|
206
|
+
### 5. Agent Control Endpoints (Proposed)
|
|
207
|
+
|
|
208
|
+
| Endpoint | Purpose |
|
|
209
|
+
|----------|---------|
|
|
210
|
+
| `POST /api/sdk/agents/restart` | Restart an agent (equivalent to "Restart Agent" button) |
|
|
211
|
+
| `POST /api/sdk/agents/settings` | Update agent settings (voice, LLM, STT provider, temperature, etc.) |
|
|
212
|
+
| `GET /api/sdk/agents/settings` | Read current agent settings |
|
|
213
|
+
|
|
214
|
+
These would enable CLI commands like:
|
|
215
|
+
- `openhome restart [agent]` — restart an agent remotely
|
|
216
|
+
- `openhome config [agent] --voice <id>` — change voice settings
|
|
217
|
+
- `openhome config [agent] --temperature 0.7` — adjust LLM temperature
|
|
218
|
+
|
|
219
|
+
### 6. DevKit Hardware Control (Future)
|
|
220
|
+
|
|
221
|
+
For OpenHome DevKit (Raspberry Pi) users, these would enable remote management:
|
|
222
|
+
- Volume control
|
|
223
|
+
- Start/stop agent session
|
|
224
|
+
- Reboot device
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Category Enum Clarification
|
|
229
|
+
|
|
230
|
+
The web form uses three buttons: **Skill**, **Brain Skill**, **Background Daemon**. What string values should we send in the API?
|
|
231
|
+
|
|
232
|
+
Suggested: `"skill"`, `"brain_skill"`, `"background_daemon"`
|
|
233
|
+
|
|
234
|
+
Please confirm the exact enum values the backend expects.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Open Questions
|
|
239
|
+
|
|
240
|
+
1. What are the exact category enum values? (`skill` / `brain_skill` / `background_daemon`?)
|
|
241
|
+
2. Is there a max file size for the zip upload?
|
|
242
|
+
3. Is there a max image size or required dimensions for the icon?
|
|
243
|
+
4. Should `name` be globally unique or scoped to the user's account?
|
|
244
|
+
5. Can an ability be updated (new version) via the same create endpoint, or is a separate update endpoint needed?
|
|
245
|
+
6. Are there rate limits on the upload endpoint?
|
|
246
|
+
7. Should the response include a URL to the ability in the web editor?
|