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
package/src/cli.ts ADDED
@@ -0,0 +1,339 @@
1
+ import { Command } from "commander";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
4
+ import { readFileSync } from "node:fs";
5
+
6
+ import { loginCommand } from "./commands/login.js";
7
+ import { initCommand } from "./commands/init.js";
8
+ import { deployCommand } from "./commands/deploy.js";
9
+ import { deleteCommand } from "./commands/delete.js";
10
+ import { toggleCommand } from "./commands/toggle.js";
11
+ import { assignCommand } from "./commands/assign.js";
12
+ import { listCommand } from "./commands/list.js";
13
+ import { statusCommand } from "./commands/status.js";
14
+ import { agentsCommand } from "./commands/agents.js";
15
+ import { logoutCommand } from "./commands/logout.js";
16
+ import { chatCommand } from "./commands/chat.js";
17
+ import { triggerCommand } from "./commands/trigger.js";
18
+ import { whoamiCommand } from "./commands/whoami.js";
19
+ import { configEditCommand } from "./commands/config-edit.js";
20
+ import { logsCommand } from "./commands/logs.js";
21
+ import { p, handleCancel } from "./ui/format.js";
22
+
23
+ // Read version from package.json
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = dirname(__filename);
26
+ let version = "0.1.0";
27
+ try {
28
+ const pkg = JSON.parse(
29
+ readFileSync(join(__dirname, "..", "package.json"), "utf8"),
30
+ ) as { version?: string };
31
+ version = pkg.version ?? version;
32
+ } catch {
33
+ // fallback to default
34
+ }
35
+
36
+ // ── Interactive menu (bare `openhome` with no args) ──────────────
37
+
38
+ async function ensureLoggedIn(): Promise<void> {
39
+ const { getApiKey } = await import("./config/store.js");
40
+ const key = getApiKey();
41
+ if (!key) {
42
+ await loginCommand();
43
+ console.log("");
44
+ }
45
+ }
46
+
47
+ async function interactiveMenu(): Promise<void> {
48
+ p.intro(`🏠 OpenHome CLI v${version}`);
49
+
50
+ // Login first if not authenticated
51
+ await ensureLoggedIn();
52
+
53
+ let running = true;
54
+ while (running) {
55
+ const choice = await p.select({
56
+ message: "What would you like to do?",
57
+ options: [
58
+ {
59
+ value: "init",
60
+ label: "✨ Create Ability",
61
+ hint: "Scaffold a new ability from templates",
62
+ },
63
+ {
64
+ value: "deploy",
65
+ label: "🚀 Deploy",
66
+ hint: "Upload ability to OpenHome",
67
+ },
68
+ {
69
+ value: "chat",
70
+ label: "💬 Chat",
71
+ hint: "Talk to your agent",
72
+ },
73
+ {
74
+ value: "trigger",
75
+ label: "⚡ Trigger",
76
+ hint: "Fire an ability remotely with a phrase",
77
+ },
78
+ {
79
+ value: "list",
80
+ label: "📋 My Abilities",
81
+ hint: "List deployed abilities",
82
+ },
83
+ {
84
+ value: "delete",
85
+ label: "🗑️ Delete Ability",
86
+ hint: "Remove a deployed ability",
87
+ },
88
+ {
89
+ value: "toggle",
90
+ label: "⚡ Enable / Disable",
91
+ hint: "Toggle an ability on or off",
92
+ },
93
+ {
94
+ value: "assign",
95
+ label: "🔗 Assign to Agent",
96
+ hint: "Link abilities to an agent",
97
+ },
98
+ {
99
+ value: "agents",
100
+ label: "🤖 My Agents",
101
+ hint: "View agents and set default",
102
+ },
103
+ {
104
+ value: "status",
105
+ label: "🔍 Status",
106
+ hint: "Check ability status",
107
+ },
108
+ {
109
+ value: "config",
110
+ label: "⚙️ Edit Config",
111
+ hint: "Update trigger words, description, category",
112
+ },
113
+ {
114
+ value: "logs",
115
+ label: "📡 Logs",
116
+ hint: "Stream live agent messages",
117
+ },
118
+ {
119
+ value: "whoami",
120
+ label: "👤 Who Am I",
121
+ hint: "Show auth, default agent, tracked abilities",
122
+ },
123
+ {
124
+ value: "logout",
125
+ label: "🔓 Log Out",
126
+ hint: "Clear credentials and re-authenticate",
127
+ },
128
+ { value: "exit", label: "👋 Exit", hint: "Quit" },
129
+ ],
130
+ });
131
+ handleCancel(choice);
132
+
133
+ switch (choice) {
134
+ case "init":
135
+ await initCommand();
136
+ break;
137
+ case "deploy":
138
+ await deployCommand();
139
+ break;
140
+ case "chat":
141
+ await chatCommand();
142
+ break;
143
+ case "trigger":
144
+ await triggerCommand();
145
+ break;
146
+ case "list":
147
+ await listCommand();
148
+ break;
149
+ case "delete":
150
+ await deleteCommand();
151
+ break;
152
+ case "toggle":
153
+ await toggleCommand();
154
+ break;
155
+ case "assign":
156
+ await assignCommand();
157
+ break;
158
+ case "agents":
159
+ await agentsCommand();
160
+ break;
161
+ case "status":
162
+ await statusCommand();
163
+ break;
164
+ case "config":
165
+ await configEditCommand();
166
+ break;
167
+ case "logs":
168
+ await logsCommand();
169
+ break;
170
+ case "whoami":
171
+ await whoamiCommand();
172
+ break;
173
+ case "logout":
174
+ await logoutCommand();
175
+ await ensureLoggedIn();
176
+ break;
177
+ case "exit":
178
+ running = false;
179
+ break;
180
+ }
181
+
182
+ if (running) {
183
+ console.log(""); // spacing between commands
184
+ }
185
+ }
186
+
187
+ p.outro("See you next time!");
188
+ }
189
+
190
+ // ── Commander subcommands (direct usage) ─────────────────────────
191
+
192
+ const program = new Command();
193
+
194
+ program
195
+ .name("openhome")
196
+ .description("OpenHome CLI — manage abilities from your terminal")
197
+ .version(version, "-v, --version", "Output the current version");
198
+
199
+ program
200
+ .command("login")
201
+ .description("Authenticate with your OpenHome API key")
202
+ .action(async () => {
203
+ await loginCommand();
204
+ });
205
+
206
+ program
207
+ .command("logout")
208
+ .description("Log out and clear stored credentials")
209
+ .action(async () => {
210
+ await logoutCommand();
211
+ });
212
+
213
+ program
214
+ .command("init [name]")
215
+ .description("Scaffold a new ability from templates")
216
+ .action(async (name?: string) => {
217
+ await initCommand(name);
218
+ });
219
+
220
+ program
221
+ .command("deploy [path]")
222
+ .description("Validate and deploy an ability to OpenHome")
223
+ .option("--dry-run", "Show what would be deployed without sending")
224
+ .option("--mock", "Use mock API client (no real network calls)")
225
+ .option("--personality <id>", "Agent ID to attach the ability to")
226
+ .action(
227
+ async (
228
+ path: string | undefined,
229
+ opts: { dryRun?: boolean; mock?: boolean; personality?: string },
230
+ ) => {
231
+ await deployCommand(path, opts);
232
+ },
233
+ );
234
+
235
+ program
236
+ .command("chat [agent]")
237
+ .description("Chat with an agent via WebSocket")
238
+ .action(async (agent?: string) => {
239
+ await chatCommand(agent);
240
+ });
241
+
242
+ program
243
+ .command("trigger [phrase]")
244
+ .description("Send a trigger phrase to fire an ability remotely")
245
+ .option("--agent <id>", "Agent ID (uses default if not set)")
246
+ .action(async (phrase?: string, opts?: { agent?: string }) => {
247
+ await triggerCommand(phrase, opts);
248
+ });
249
+
250
+ program
251
+ .command("list")
252
+ .description("List all deployed abilities")
253
+ .option("--mock", "Use mock API client")
254
+ .action(async (opts: { mock?: boolean }) => {
255
+ await listCommand(opts);
256
+ });
257
+
258
+ program
259
+ .command("delete [ability]")
260
+ .description("Delete a deployed ability")
261
+ .option("--mock", "Use mock API client")
262
+ .action(async (ability: string | undefined, opts: { mock?: boolean }) => {
263
+ await deleteCommand(ability, opts);
264
+ });
265
+
266
+ program
267
+ .command("toggle [ability]")
268
+ .description("Enable or disable a deployed ability")
269
+ .option("--enable", "Enable the ability")
270
+ .option("--disable", "Disable the ability")
271
+ .option("--mock", "Use mock API client")
272
+ .action(
273
+ async (
274
+ ability: string | undefined,
275
+ opts: { mock?: boolean; enable?: boolean; disable?: boolean },
276
+ ) => {
277
+ await toggleCommand(ability, opts);
278
+ },
279
+ );
280
+
281
+ program
282
+ .command("assign")
283
+ .description("Assign abilities to an agent")
284
+ .option("--mock", "Use mock API client")
285
+ .action(async (opts: { mock?: boolean }) => {
286
+ await assignCommand(opts);
287
+ });
288
+
289
+ program
290
+ .command("agents")
291
+ .description("View your agents and set a default")
292
+ .option("--mock", "Use mock API client")
293
+ .action(async (opts: { mock?: boolean }) => {
294
+ await agentsCommand(opts);
295
+ });
296
+
297
+ program
298
+ .command("status [ability]")
299
+ .description("Show detailed status of an ability")
300
+ .option("--mock", "Use mock API client")
301
+ .action(async (ability: string | undefined, opts: { mock?: boolean }) => {
302
+ await statusCommand(ability, opts);
303
+ });
304
+
305
+ program
306
+ .command("config [path]")
307
+ .description("Edit trigger words, description, or category in config.json")
308
+ .action(async (path?: string) => {
309
+ await configEditCommand(path);
310
+ });
311
+
312
+ program
313
+ .command("logs")
314
+ .description("Stream live agent messages and logs")
315
+ .option("--agent <id>", "Agent ID (uses default if not set)")
316
+ .action(async (opts: { agent?: string }) => {
317
+ await logsCommand(opts);
318
+ });
319
+
320
+ program
321
+ .command("whoami")
322
+ .description("Show auth status, default agent, and tracked abilities")
323
+ .action(async () => {
324
+ await whoamiCommand();
325
+ });
326
+
327
+ // ── Entry point: menu if no args, subcommand otherwise ───────────
328
+
329
+ if (process.argv.length <= 2) {
330
+ interactiveMenu().catch((err: unknown) => {
331
+ console.error(err instanceof Error ? err.message : String(err));
332
+ process.exit(1);
333
+ });
334
+ } else {
335
+ program.parseAsync(process.argv).catch((err: unknown) => {
336
+ console.error(err instanceof Error ? err.message : String(err));
337
+ process.exit(1);
338
+ });
339
+ }
@@ -0,0 +1,88 @@
1
+ import { ApiClient, NotImplementedError } from "../api/client.js";
2
+ import { MockApiClient } from "../api/mock-client.js";
3
+ import { getApiKey, getConfig, saveConfig } from "../config/store.js";
4
+ import { error, success, info, p, handleCancel } from "../ui/format.js";
5
+ import chalk from "chalk";
6
+
7
+ export async function agentsCommand(
8
+ opts: { mock?: boolean } = {},
9
+ ): Promise<void> {
10
+ p.intro("🤖 Your Agents");
11
+
12
+ let client: ApiClient | MockApiClient;
13
+
14
+ if (opts.mock) {
15
+ client = new MockApiClient();
16
+ } else {
17
+ const apiKey = getApiKey();
18
+ if (!apiKey) {
19
+ error("Not authenticated. Run: openhome login");
20
+ process.exit(1);
21
+ }
22
+ client = new ApiClient(apiKey, getConfig().api_base_url);
23
+ }
24
+
25
+ const s = p.spinner();
26
+ s.start("Fetching agents...");
27
+
28
+ try {
29
+ const personalities = await client.getPersonalities();
30
+ s.stop(`Found ${personalities.length} agent(s).`);
31
+
32
+ if (personalities.length === 0) {
33
+ info("No agents found. Create one at https://app.openhome.com");
34
+ p.outro("Done.");
35
+ return;
36
+ }
37
+
38
+ p.note(
39
+ personalities
40
+ .map((pers) => `${chalk.bold(pers.name)} ${chalk.gray(pers.id)}`)
41
+ .join("\n"),
42
+ "Agents",
43
+ );
44
+
45
+ const config = getConfig();
46
+ const currentDefault = config.default_personality_id;
47
+
48
+ if (currentDefault) {
49
+ const match = personalities.find((p) => p.id === currentDefault);
50
+ info(`Default agent: ${match ? match.name : currentDefault}`);
51
+ }
52
+
53
+ const setDefault = await p.confirm({
54
+ message: "Set or change your default agent?",
55
+ });
56
+ handleCancel(setDefault);
57
+
58
+ if (setDefault) {
59
+ const selected = await p.select({
60
+ message: "Choose default agent",
61
+ options: personalities.map((pers) => ({
62
+ value: pers.id,
63
+ label: pers.name,
64
+ hint: pers.id,
65
+ })),
66
+ });
67
+ handleCancel(selected);
68
+
69
+ config.default_personality_id = selected as string;
70
+ saveConfig(config);
71
+ success(`Default agent set: ${String(selected)}`);
72
+ }
73
+
74
+ p.outro("Done.");
75
+ } catch (err) {
76
+ s.stop("Failed.");
77
+
78
+ if (err instanceof NotImplementedError) {
79
+ p.note("Use --mock to see example output.", "API Not Available Yet");
80
+ p.outro("Agents endpoint not yet implemented.");
81
+ return;
82
+ }
83
+ error(
84
+ `Failed to fetch agents: ${err instanceof Error ? err.message : String(err)}`,
85
+ );
86
+ process.exit(1);
87
+ }
88
+ }
@@ -0,0 +1,123 @@
1
+ import { ApiClient, NotImplementedError } from "../api/client.js";
2
+ import { MockApiClient } from "../api/mock-client.js";
3
+ import { getApiKey, getConfig } from "../config/store.js";
4
+ import { error, success, info, p, handleCancel } from "../ui/format.js";
5
+ import chalk from "chalk";
6
+
7
+ export async function assignCommand(
8
+ opts: { mock?: boolean } = {},
9
+ ): Promise<void> {
10
+ p.intro("🔗 Assign abilities to agent");
11
+
12
+ let client: ApiClient | MockApiClient;
13
+
14
+ if (opts.mock) {
15
+ client = new MockApiClient();
16
+ } else {
17
+ const apiKey = getApiKey();
18
+ if (!apiKey) {
19
+ error("Not authenticated. Run: openhome login");
20
+ process.exit(1);
21
+ }
22
+ client = new ApiClient(apiKey, getConfig().api_base_url);
23
+ }
24
+
25
+ const s = p.spinner();
26
+ s.start("Fetching agents and abilities...");
27
+
28
+ let personalities: Awaited<ReturnType<typeof client.getPersonalities>>;
29
+ let abilities: Awaited<ReturnType<typeof client.listAbilities>>["abilities"];
30
+
31
+ try {
32
+ [personalities, { abilities }] = await Promise.all([
33
+ client.getPersonalities(),
34
+ client.listAbilities(),
35
+ ]);
36
+ s.stop(
37
+ `Found ${personalities.length} agent(s), ${abilities.length} ability(s).`,
38
+ );
39
+ } catch (err) {
40
+ s.stop("Failed to fetch data.");
41
+ error(err instanceof Error ? err.message : String(err));
42
+ process.exit(1);
43
+ }
44
+
45
+ if (personalities.length === 0) {
46
+ p.outro("No agents found. Create one at https://app.openhome.com");
47
+ return;
48
+ }
49
+
50
+ if (abilities.length === 0) {
51
+ p.outro("No abilities found. Run: openhome deploy");
52
+ return;
53
+ }
54
+
55
+ // Pick agent
56
+ const agentId = await p.select({
57
+ message: "Which agent do you want to update?",
58
+ options: personalities.map((pers) => ({
59
+ value: pers.id,
60
+ label: pers.name,
61
+ hint: chalk.gray(pers.id),
62
+ })),
63
+ });
64
+ handleCancel(agentId);
65
+
66
+ const agentName =
67
+ personalities.find((p) => p.id === agentId)?.name ?? String(agentId);
68
+
69
+ // Show current assignments and let user pick which abilities to assign
70
+ info(
71
+ `Select abilities to assign to "${agentName}". Deselecting all unassigns everything.`,
72
+ );
73
+
74
+ const selectedIds = await p.multiselect({
75
+ message: `Abilities for "${agentName}"`,
76
+ options: abilities.map((a) => ({
77
+ value: a.ability_id,
78
+ label: a.unique_name,
79
+ hint: `${a.status} v${a.version}`,
80
+ })),
81
+ required: false,
82
+ });
83
+ handleCancel(selectedIds);
84
+
85
+ const chosenIds = selectedIds as string[];
86
+
87
+ // Convert ability_ids to numbers for the API payload
88
+ // The API expects numeric IDs; if the real API returns string IDs we send them as-is
89
+ const numericIds = chosenIds
90
+ .map((id) => Number(id))
91
+ .filter((id) => !Number.isNaN(id));
92
+
93
+ // If any ID couldn't be parsed as a number, fall back to the raw list
94
+ // (lets the server validate — better than silently dropping)
95
+ const capabilityIds =
96
+ numericIds.length === chosenIds.length
97
+ ? numericIds
98
+ : (chosenIds as unknown as number[]);
99
+
100
+ s.start(`Assigning ${chosenIds.length} ability(s) to "${agentName}"...`);
101
+ try {
102
+ const result = await client.assignCapabilities(
103
+ agentId as string,
104
+ capabilityIds,
105
+ );
106
+ s.stop("Done.");
107
+ success(
108
+ result.message ??
109
+ `"${agentName}" updated with ${chosenIds.length} ability(s).`,
110
+ );
111
+ p.outro("Done.");
112
+ } catch (err) {
113
+ s.stop("Failed.");
114
+
115
+ if (err instanceof NotImplementedError) {
116
+ p.note("Assign endpoint not yet implemented.", "API Not Available Yet");
117
+ return;
118
+ }
119
+
120
+ error(`Assign failed: ${err instanceof Error ? err.message : String(err)}`);
121
+ process.exit(1);
122
+ }
123
+ }