openhome-cli 0.1.16 → 0.1.19
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/dist/cli.js +111 -77
- package/package.json +1 -1
- package/src/api/client.ts +25 -8
- package/src/commands/assign.ts +2 -0
- package/src/commands/delete.ts +2 -0
- package/src/commands/deploy.ts +3 -0
- package/src/commands/handle-session-expired.ts +23 -0
- package/src/commands/list.ts +2 -0
- package/src/commands/toggle.ts +2 -0
package/dist/cli.js
CHANGED
|
@@ -49,6 +49,12 @@ var ApiError = class extends Error {
|
|
|
49
49
|
this.name = "ApiError";
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
|
+
var SessionExpiredError = class extends Error {
|
|
53
|
+
constructor() {
|
|
54
|
+
super("Session token expired or invalid");
|
|
55
|
+
this.name = "SessionExpiredError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
52
58
|
var ApiClient = class {
|
|
53
59
|
constructor(apiKey, baseUrl, jwt) {
|
|
54
60
|
this.apiKey = apiKey;
|
|
@@ -82,6 +88,9 @@ var ApiClient = class {
|
|
|
82
88
|
throw new NotImplementedError(path);
|
|
83
89
|
}
|
|
84
90
|
const message = body?.detail ?? body?.error?.message ?? response.statusText;
|
|
91
|
+
if (useJwt && (response.status === 401 || message.toLowerCase().includes("token not valid") || message.toLowerCase().includes("token is invalid") || message.toLowerCase().includes("not valid for any token"))) {
|
|
92
|
+
throw new SessionExpiredError();
|
|
93
|
+
}
|
|
85
94
|
throw new ApiError(String(response.status), message);
|
|
86
95
|
}
|
|
87
96
|
return response.json();
|
|
@@ -101,9 +110,7 @@ var ApiClient = class {
|
|
|
101
110
|
const form = new FormData();
|
|
102
111
|
form.append(
|
|
103
112
|
"zip_file",
|
|
104
|
-
new Blob([zipBuffer], {
|
|
105
|
-
type: "application/zip"
|
|
106
|
-
}),
|
|
113
|
+
new Blob([new Uint8Array(zipBuffer)], { type: "application/zip" }),
|
|
107
114
|
"ability.zip"
|
|
108
115
|
);
|
|
109
116
|
if (imageBuffer && imageName) {
|
|
@@ -111,7 +118,7 @@ var ApiClient = class {
|
|
|
111
118
|
const imageMime = imageExt === "jpg" || imageExt === "jpeg" ? "image/jpeg" : "image/png";
|
|
112
119
|
form.append(
|
|
113
120
|
"image_file",
|
|
114
|
-
new Blob([imageBuffer], { type: imageMime }),
|
|
121
|
+
new Blob([new Uint8Array(imageBuffer)], { type: imageMime }),
|
|
115
122
|
imageName
|
|
116
123
|
);
|
|
117
124
|
}
|
|
@@ -122,10 +129,12 @@ var ApiClient = class {
|
|
|
122
129
|
if (metadata.personality_id) {
|
|
123
130
|
form.append("personality_id", metadata.personality_id);
|
|
124
131
|
}
|
|
125
|
-
return this.request(
|
|
126
|
-
|
|
127
|
-
body: form
|
|
128
|
-
|
|
132
|
+
return this.request(
|
|
133
|
+
ENDPOINTS.uploadCapability,
|
|
134
|
+
{ method: "POST", body: form },
|
|
135
|
+
true
|
|
136
|
+
// uses JWT
|
|
137
|
+
);
|
|
129
138
|
}
|
|
130
139
|
async listAbilities() {
|
|
131
140
|
const data = await this.request(
|
|
@@ -682,6 +691,25 @@ async function createAbilityZip(dirPath) {
|
|
|
682
691
|
});
|
|
683
692
|
}
|
|
684
693
|
|
|
694
|
+
// src/commands/handle-session-expired.ts
|
|
695
|
+
import chalk3 from "chalk";
|
|
696
|
+
async function handleIfSessionExpired(err) {
|
|
697
|
+
if (!(err instanceof SessionExpiredError)) return false;
|
|
698
|
+
console.log("");
|
|
699
|
+
p.note(
|
|
700
|
+
[
|
|
701
|
+
"Your session token has expired or been invalidated.",
|
|
702
|
+
"This happens when you log into the OpenHome website again.",
|
|
703
|
+
"",
|
|
704
|
+
`You need to grab a fresh token \u2014 it only takes 30 seconds.`
|
|
705
|
+
].join("\n"),
|
|
706
|
+
chalk3.yellow("Session expired")
|
|
707
|
+
);
|
|
708
|
+
await setupJwt();
|
|
709
|
+
p.note("Token updated. Run the command again to continue.", "Ready");
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
|
|
685
713
|
// src/api/mock-client.ts
|
|
686
714
|
var MOCK_PERSONALITIES = [
|
|
687
715
|
{ id: "pers_alice", name: "Alice", description: "Friendly assistant" },
|
|
@@ -1217,6 +1245,7 @@ async function deployCommand(pathArg, opts = {}) {
|
|
|
1217
1245
|
p.outro("Zip ready for manual upload.");
|
|
1218
1246
|
return;
|
|
1219
1247
|
}
|
|
1248
|
+
if (await handleIfSessionExpired(err)) return;
|
|
1220
1249
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1221
1250
|
if (msg.toLowerCase().includes("same name")) {
|
|
1222
1251
|
error(`An ability named "${uniqueName}" already exists.`);
|
|
@@ -1339,6 +1368,7 @@ async function deployZip(zipPath, opts = {}) {
|
|
|
1339
1368
|
p.outro("Deployed successfully! \u{1F389}");
|
|
1340
1369
|
} catch (err) {
|
|
1341
1370
|
s.stop("Upload failed.");
|
|
1371
|
+
if (await handleIfSessionExpired(err)) return;
|
|
1342
1372
|
error(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1343
1373
|
process.exit(1);
|
|
1344
1374
|
}
|
|
@@ -2137,7 +2167,7 @@ async function initCommand(nameArg) {
|
|
|
2137
2167
|
}
|
|
2138
2168
|
|
|
2139
2169
|
// src/commands/delete.ts
|
|
2140
|
-
import
|
|
2170
|
+
import chalk4 from "chalk";
|
|
2141
2171
|
async function deleteCommand(abilityArg, opts = {}) {
|
|
2142
2172
|
p.intro("\u{1F5D1}\uFE0F Delete ability");
|
|
2143
2173
|
let client;
|
|
@@ -2192,7 +2222,7 @@ async function deleteCommand(abilityArg, opts = {}) {
|
|
|
2192
2222
|
options: abilities.map((a) => ({
|
|
2193
2223
|
value: a.ability_id,
|
|
2194
2224
|
label: a.unique_name,
|
|
2195
|
-
hint: `${
|
|
2225
|
+
hint: `${chalk4.gray(a.status)} v${a.version}`
|
|
2196
2226
|
}))
|
|
2197
2227
|
});
|
|
2198
2228
|
handleCancel(selected);
|
|
@@ -2220,13 +2250,14 @@ async function deleteCommand(abilityArg, opts = {}) {
|
|
|
2220
2250
|
p.note("API Not Available Yet", "Delete endpoint not yet implemented.");
|
|
2221
2251
|
return;
|
|
2222
2252
|
}
|
|
2253
|
+
if (await handleIfSessionExpired(err)) return;
|
|
2223
2254
|
error(`Delete failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2224
2255
|
process.exit(1);
|
|
2225
2256
|
}
|
|
2226
2257
|
}
|
|
2227
2258
|
|
|
2228
2259
|
// src/commands/toggle.ts
|
|
2229
|
-
import
|
|
2260
|
+
import chalk5 from "chalk";
|
|
2230
2261
|
async function toggleCommand(abilityArg, opts = {}) {
|
|
2231
2262
|
p.intro("\u26A1 Enable / Disable ability");
|
|
2232
2263
|
let client;
|
|
@@ -2281,7 +2312,7 @@ async function toggleCommand(abilityArg, opts = {}) {
|
|
|
2281
2312
|
options: abilities.map((a) => ({
|
|
2282
2313
|
value: a.ability_id,
|
|
2283
2314
|
label: a.unique_name,
|
|
2284
|
-
hint: `${a.status === "disabled" ?
|
|
2315
|
+
hint: `${a.status === "disabled" ? chalk5.gray("disabled") : chalk5.green("enabled")} v${a.version}`
|
|
2285
2316
|
}))
|
|
2286
2317
|
});
|
|
2287
2318
|
handleCancel(selected);
|
|
@@ -2319,13 +2350,14 @@ async function toggleCommand(abilityArg, opts = {}) {
|
|
|
2319
2350
|
p.note("Toggle endpoint not yet implemented.", "API Not Available Yet");
|
|
2320
2351
|
return;
|
|
2321
2352
|
}
|
|
2353
|
+
if (await handleIfSessionExpired(err)) return;
|
|
2322
2354
|
error(`Toggle failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2323
2355
|
process.exit(1);
|
|
2324
2356
|
}
|
|
2325
2357
|
}
|
|
2326
2358
|
|
|
2327
2359
|
// src/commands/assign.ts
|
|
2328
|
-
import
|
|
2360
|
+
import chalk6 from "chalk";
|
|
2329
2361
|
async function assignCommand(opts = {}) {
|
|
2330
2362
|
p.intro("\u{1F517} Assign abilities to agent");
|
|
2331
2363
|
let client;
|
|
@@ -2376,7 +2408,7 @@ async function assignCommand(opts = {}) {
|
|
|
2376
2408
|
options: personalities.map((pers) => ({
|
|
2377
2409
|
value: pers.id,
|
|
2378
2410
|
label: pers.name,
|
|
2379
|
-
hint:
|
|
2411
|
+
hint: chalk6.gray(pers.id)
|
|
2380
2412
|
}))
|
|
2381
2413
|
});
|
|
2382
2414
|
handleCancel(agentId);
|
|
@@ -2414,23 +2446,24 @@ async function assignCommand(opts = {}) {
|
|
|
2414
2446
|
p.note("Assign endpoint not yet implemented.", "API Not Available Yet");
|
|
2415
2447
|
return;
|
|
2416
2448
|
}
|
|
2449
|
+
if (await handleIfSessionExpired(err)) return;
|
|
2417
2450
|
error(`Assign failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2418
2451
|
process.exit(1);
|
|
2419
2452
|
}
|
|
2420
2453
|
}
|
|
2421
2454
|
|
|
2422
2455
|
// src/commands/list.ts
|
|
2423
|
-
import
|
|
2456
|
+
import chalk7 from "chalk";
|
|
2424
2457
|
function statusColor(status) {
|
|
2425
2458
|
switch (status) {
|
|
2426
2459
|
case "active":
|
|
2427
|
-
return
|
|
2460
|
+
return chalk7.green(status);
|
|
2428
2461
|
case "processing":
|
|
2429
|
-
return
|
|
2462
|
+
return chalk7.yellow(status);
|
|
2430
2463
|
case "failed":
|
|
2431
|
-
return
|
|
2464
|
+
return chalk7.red(status);
|
|
2432
2465
|
case "disabled":
|
|
2433
|
-
return
|
|
2466
|
+
return chalk7.gray(status);
|
|
2434
2467
|
default:
|
|
2435
2468
|
return status;
|
|
2436
2469
|
}
|
|
@@ -2482,6 +2515,7 @@ async function listCommand(opts = {}) {
|
|
|
2482
2515
|
p.outro("List endpoint not yet implemented.");
|
|
2483
2516
|
return;
|
|
2484
2517
|
}
|
|
2518
|
+
if (await handleIfSessionExpired(err)) return;
|
|
2485
2519
|
error(
|
|
2486
2520
|
`Failed to list abilities: ${err instanceof Error ? err.message : String(err)}`
|
|
2487
2521
|
);
|
|
@@ -2493,19 +2527,19 @@ async function listCommand(opts = {}) {
|
|
|
2493
2527
|
import { join as join4, resolve as resolve3 } from "path";
|
|
2494
2528
|
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
2495
2529
|
import { homedir as homedir3 } from "os";
|
|
2496
|
-
import
|
|
2530
|
+
import chalk8 from "chalk";
|
|
2497
2531
|
function statusBadge(status) {
|
|
2498
2532
|
switch (status) {
|
|
2499
2533
|
case "active":
|
|
2500
|
-
return
|
|
2534
|
+
return chalk8.bgGreen.black(` ${status.toUpperCase()} `);
|
|
2501
2535
|
case "processing":
|
|
2502
|
-
return
|
|
2536
|
+
return chalk8.bgYellow.black(` ${status.toUpperCase()} `);
|
|
2503
2537
|
case "failed":
|
|
2504
|
-
return
|
|
2538
|
+
return chalk8.bgRed.white(` ${status.toUpperCase()} `);
|
|
2505
2539
|
case "disabled":
|
|
2506
|
-
return
|
|
2540
|
+
return chalk8.bgGray.white(` ${status.toUpperCase()} `);
|
|
2507
2541
|
default:
|
|
2508
|
-
return
|
|
2542
|
+
return chalk8.bgWhite.black(` ${status.toUpperCase()} `);
|
|
2509
2543
|
}
|
|
2510
2544
|
}
|
|
2511
2545
|
function readAbilityName(dir) {
|
|
@@ -2594,14 +2628,14 @@ async function statusCommand(abilityArg, opts = {}) {
|
|
|
2594
2628
|
);
|
|
2595
2629
|
if (ability.validation_errors.length > 0) {
|
|
2596
2630
|
p.note(
|
|
2597
|
-
ability.validation_errors.map((e) =>
|
|
2631
|
+
ability.validation_errors.map((e) => chalk8.red(`\u2717 ${e}`)).join("\n"),
|
|
2598
2632
|
"Validation Errors"
|
|
2599
2633
|
);
|
|
2600
2634
|
}
|
|
2601
2635
|
if (ability.deploy_history.length > 0) {
|
|
2602
2636
|
const historyLines = ability.deploy_history.map((event) => {
|
|
2603
|
-
const icon = event.status === "success" ?
|
|
2604
|
-
return `${icon} v${event.version} ${event.message} ${
|
|
2637
|
+
const icon = event.status === "success" ? chalk8.green("\u2713") : chalk8.red("\u2717");
|
|
2638
|
+
return `${icon} v${event.version} ${event.message} ${chalk8.gray(new Date(event.timestamp).toLocaleString())}`;
|
|
2605
2639
|
});
|
|
2606
2640
|
p.note(historyLines.join("\n"), "Deploy History");
|
|
2607
2641
|
}
|
|
@@ -2621,7 +2655,7 @@ async function statusCommand(abilityArg, opts = {}) {
|
|
|
2621
2655
|
}
|
|
2622
2656
|
|
|
2623
2657
|
// src/commands/agents.ts
|
|
2624
|
-
import
|
|
2658
|
+
import chalk9 from "chalk";
|
|
2625
2659
|
async function agentsCommand(opts = {}) {
|
|
2626
2660
|
p.intro("\u{1F916} Your Agents");
|
|
2627
2661
|
let client;
|
|
@@ -2646,7 +2680,7 @@ async function agentsCommand(opts = {}) {
|
|
|
2646
2680
|
return;
|
|
2647
2681
|
}
|
|
2648
2682
|
p.note(
|
|
2649
|
-
personalities.map((pers) => `${
|
|
2683
|
+
personalities.map((pers) => `${chalk9.bold(pers.name)} ${chalk9.gray(pers.id)}`).join("\n"),
|
|
2650
2684
|
"Agents"
|
|
2651
2685
|
);
|
|
2652
2686
|
const config = getConfig();
|
|
@@ -2700,7 +2734,7 @@ async function logoutCommand() {
|
|
|
2700
2734
|
|
|
2701
2735
|
// src/commands/chat.ts
|
|
2702
2736
|
import WebSocket from "ws";
|
|
2703
|
-
import
|
|
2737
|
+
import chalk10 from "chalk";
|
|
2704
2738
|
import * as readline from "readline";
|
|
2705
2739
|
var PING_INTERVAL = 3e4;
|
|
2706
2740
|
async function chatCommand(agentArg, opts = {}) {
|
|
@@ -2741,7 +2775,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2741
2775
|
}
|
|
2742
2776
|
}
|
|
2743
2777
|
const wsUrl = `${WS_BASE}${ENDPOINTS.voiceStream(apiKey, agentId)}`;
|
|
2744
|
-
info(`Connecting to agent ${
|
|
2778
|
+
info(`Connecting to agent ${chalk10.bold(agentId)}...`);
|
|
2745
2779
|
await new Promise((resolve6) => {
|
|
2746
2780
|
const ws = new WebSocket(wsUrl, {
|
|
2747
2781
|
perMessageDeflate: false,
|
|
@@ -2757,7 +2791,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2757
2791
|
output: process.stdout
|
|
2758
2792
|
});
|
|
2759
2793
|
function promptUser() {
|
|
2760
|
-
rl.question(
|
|
2794
|
+
rl.question(chalk10.green("You: "), (input) => {
|
|
2761
2795
|
const trimmed = input.trim();
|
|
2762
2796
|
if (!trimmed) {
|
|
2763
2797
|
promptUser();
|
|
@@ -2793,7 +2827,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2793
2827
|
}, PING_INTERVAL);
|
|
2794
2828
|
success("Connected! Type a message and press Enter. Type /quit to exit.");
|
|
2795
2829
|
console.log(
|
|
2796
|
-
|
|
2830
|
+
chalk10.gray(
|
|
2797
2831
|
" Tip: Send trigger words to activate abilities (e.g. 'play aquaprime')"
|
|
2798
2832
|
)
|
|
2799
2833
|
);
|
|
@@ -2808,7 +2842,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2808
2842
|
const data = msg.data;
|
|
2809
2843
|
if (data.content && data.role === "assistant") {
|
|
2810
2844
|
if (data.live && !data.final) {
|
|
2811
|
-
const prefix = `${
|
|
2845
|
+
const prefix = `${chalk10.cyan("Agent:")} `;
|
|
2812
2846
|
readline.clearLine(process.stdout, 0);
|
|
2813
2847
|
readline.cursorTo(process.stdout, 0);
|
|
2814
2848
|
process.stdout.write(`${prefix}${data.content}`);
|
|
@@ -2817,7 +2851,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2817
2851
|
if (currentResponse !== "") {
|
|
2818
2852
|
console.log("");
|
|
2819
2853
|
} else {
|
|
2820
|
-
console.log(`${
|
|
2854
|
+
console.log(`${chalk10.cyan("Agent:")} ${data.content}`);
|
|
2821
2855
|
}
|
|
2822
2856
|
currentResponse = "";
|
|
2823
2857
|
console.log("");
|
|
@@ -2833,7 +2867,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2833
2867
|
ws.send(JSON.stringify({ type: "text", data: "bot-speak-end" }));
|
|
2834
2868
|
if (currentResponse === "") {
|
|
2835
2869
|
console.log(
|
|
2836
|
-
|
|
2870
|
+
chalk10.gray(" (Agent sent audio \u2014 text-only mode)")
|
|
2837
2871
|
);
|
|
2838
2872
|
console.log("");
|
|
2839
2873
|
}
|
|
@@ -2893,7 +2927,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2893
2927
|
|
|
2894
2928
|
// src/commands/trigger.ts
|
|
2895
2929
|
import WebSocket2 from "ws";
|
|
2896
|
-
import
|
|
2930
|
+
import chalk11 from "chalk";
|
|
2897
2931
|
var PING_INTERVAL2 = 3e4;
|
|
2898
2932
|
var RESPONSE_TIMEOUT = 3e4;
|
|
2899
2933
|
async function triggerCommand(phraseArg, opts = {}) {
|
|
@@ -2945,7 +2979,7 @@ async function triggerCommand(phraseArg, opts = {}) {
|
|
|
2945
2979
|
}
|
|
2946
2980
|
}
|
|
2947
2981
|
const wsUrl = `${WS_BASE}${ENDPOINTS.voiceStream(apiKey, agentId)}`;
|
|
2948
|
-
info(`Sending "${
|
|
2982
|
+
info(`Sending "${chalk11.bold(phrase)}" to agent ${chalk11.bold(agentId)}...`);
|
|
2949
2983
|
const s = p.spinner();
|
|
2950
2984
|
s.start("Waiting for response...");
|
|
2951
2985
|
await new Promise((resolve6) => {
|
|
@@ -2975,7 +3009,7 @@ async function triggerCommand(phraseArg, opts = {}) {
|
|
|
2975
3009
|
s.stop("Timed out waiting for response.");
|
|
2976
3010
|
if (fullResponse) {
|
|
2977
3011
|
console.log(`
|
|
2978
|
-
${
|
|
3012
|
+
${chalk11.cyan("Agent:")} ${fullResponse}`);
|
|
2979
3013
|
}
|
|
2980
3014
|
cleanup();
|
|
2981
3015
|
resolve6();
|
|
@@ -2992,7 +3026,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}`);
|
|
|
2992
3026
|
if (!data.live || data.final) {
|
|
2993
3027
|
s.stop("Response received.");
|
|
2994
3028
|
console.log(`
|
|
2995
|
-
${
|
|
3029
|
+
${chalk11.cyan("Agent:")} ${fullResponse}
|
|
2996
3030
|
`);
|
|
2997
3031
|
cleanup();
|
|
2998
3032
|
resolve6();
|
|
@@ -3009,7 +3043,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}
|
|
|
3009
3043
|
if (fullResponse) {
|
|
3010
3044
|
s.stop("Response received.");
|
|
3011
3045
|
console.log(`
|
|
3012
|
-
${
|
|
3046
|
+
${chalk11.cyan("Agent:")} ${fullResponse}
|
|
3013
3047
|
`);
|
|
3014
3048
|
cleanup();
|
|
3015
3049
|
resolve6();
|
|
@@ -3048,7 +3082,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}
|
|
|
3048
3082
|
}
|
|
3049
3083
|
|
|
3050
3084
|
// src/commands/whoami.ts
|
|
3051
|
-
import
|
|
3085
|
+
import chalk12 from "chalk";
|
|
3052
3086
|
import { homedir as homedir4 } from "os";
|
|
3053
3087
|
async function whoamiCommand() {
|
|
3054
3088
|
p.intro("\u{1F464} OpenHome CLI Status");
|
|
@@ -3058,17 +3092,17 @@ async function whoamiCommand() {
|
|
|
3058
3092
|
const home = homedir4();
|
|
3059
3093
|
if (apiKey) {
|
|
3060
3094
|
const masked = apiKey.slice(0, 6) + "..." + apiKey.slice(-4);
|
|
3061
|
-
info(`Authenticated: ${
|
|
3095
|
+
info(`Authenticated: ${chalk12.green("yes")} (key: ${chalk12.gray(masked)})`);
|
|
3062
3096
|
} else {
|
|
3063
3097
|
info(
|
|
3064
|
-
`Authenticated: ${
|
|
3098
|
+
`Authenticated: ${chalk12.red("no")} \u2014 run ${chalk12.bold("openhome login")}`
|
|
3065
3099
|
);
|
|
3066
3100
|
}
|
|
3067
3101
|
if (config.default_personality_id) {
|
|
3068
|
-
info(`Default agent: ${
|
|
3102
|
+
info(`Default agent: ${chalk12.bold(config.default_personality_id)}`);
|
|
3069
3103
|
} else {
|
|
3070
3104
|
info(
|
|
3071
|
-
`Default agent: ${
|
|
3105
|
+
`Default agent: ${chalk12.gray("not set")} \u2014 run ${chalk12.bold("openhome agents")}`
|
|
3072
3106
|
);
|
|
3073
3107
|
}
|
|
3074
3108
|
if (config.api_base_url) {
|
|
@@ -3077,12 +3111,12 @@ async function whoamiCommand() {
|
|
|
3077
3111
|
if (tracked.length > 0) {
|
|
3078
3112
|
const lines = tracked.map((a) => {
|
|
3079
3113
|
const shortPath = a.path.startsWith(home) ? `~${a.path.slice(home.length)}` : a.path;
|
|
3080
|
-
return ` ${
|
|
3114
|
+
return ` ${chalk12.bold(a.name)} ${chalk12.gray(shortPath)}`;
|
|
3081
3115
|
});
|
|
3082
3116
|
p.note(lines.join("\n"), `${tracked.length} tracked ability(s)`);
|
|
3083
3117
|
} else {
|
|
3084
3118
|
info(
|
|
3085
|
-
`Tracked abilities: ${
|
|
3119
|
+
`Tracked abilities: ${chalk12.gray("none")} \u2014 run ${chalk12.bold("openhome init")}`
|
|
3086
3120
|
);
|
|
3087
3121
|
}
|
|
3088
3122
|
p.outro("Done.");
|
|
@@ -3224,7 +3258,7 @@ async function configEditCommand(pathArg) {
|
|
|
3224
3258
|
|
|
3225
3259
|
// src/commands/logs.ts
|
|
3226
3260
|
import WebSocket3 from "ws";
|
|
3227
|
-
import
|
|
3261
|
+
import chalk13 from "chalk";
|
|
3228
3262
|
var PING_INTERVAL3 = 3e4;
|
|
3229
3263
|
async function logsCommand(opts = {}) {
|
|
3230
3264
|
p.intro("\u{1F4E1} Stream agent logs");
|
|
@@ -3264,8 +3298,8 @@ async function logsCommand(opts = {}) {
|
|
|
3264
3298
|
}
|
|
3265
3299
|
}
|
|
3266
3300
|
const wsUrl = `${WS_BASE}${ENDPOINTS.voiceStream(apiKey, agentId)}`;
|
|
3267
|
-
info(`Streaming logs from agent ${
|
|
3268
|
-
info(`Press ${
|
|
3301
|
+
info(`Streaming logs from agent ${chalk13.bold(agentId)}...`);
|
|
3302
|
+
info(`Press ${chalk13.bold("Ctrl+C")} to stop.
|
|
3269
3303
|
`);
|
|
3270
3304
|
await new Promise((resolve6) => {
|
|
3271
3305
|
const ws = new WebSocket3(wsUrl, {
|
|
@@ -3287,33 +3321,33 @@ async function logsCommand(opts = {}) {
|
|
|
3287
3321
|
ws.on("message", (raw) => {
|
|
3288
3322
|
try {
|
|
3289
3323
|
const msg = JSON.parse(raw.toString());
|
|
3290
|
-
const ts =
|
|
3324
|
+
const ts = chalk13.gray((/* @__PURE__ */ new Date()).toLocaleTimeString());
|
|
3291
3325
|
switch (msg.type) {
|
|
3292
3326
|
case "log":
|
|
3293
3327
|
console.log(
|
|
3294
|
-
`${ts} ${
|
|
3328
|
+
`${ts} ${chalk13.blue("[LOG]")} ${JSON.stringify(msg.data)}`
|
|
3295
3329
|
);
|
|
3296
3330
|
break;
|
|
3297
3331
|
case "action":
|
|
3298
3332
|
console.log(
|
|
3299
|
-
`${ts} ${
|
|
3333
|
+
`${ts} ${chalk13.magenta("[ACTION]")} ${JSON.stringify(msg.data)}`
|
|
3300
3334
|
);
|
|
3301
3335
|
break;
|
|
3302
3336
|
case "progress":
|
|
3303
3337
|
console.log(
|
|
3304
|
-
`${ts} ${
|
|
3338
|
+
`${ts} ${chalk13.yellow("[PROGRESS]")} ${JSON.stringify(msg.data)}`
|
|
3305
3339
|
);
|
|
3306
3340
|
break;
|
|
3307
3341
|
case "question":
|
|
3308
3342
|
console.log(
|
|
3309
|
-
`${ts} ${
|
|
3343
|
+
`${ts} ${chalk13.cyan("[QUESTION]")} ${JSON.stringify(msg.data)}`
|
|
3310
3344
|
);
|
|
3311
3345
|
break;
|
|
3312
3346
|
case "message": {
|
|
3313
3347
|
const data = msg.data;
|
|
3314
3348
|
if (data.content && !data.live) {
|
|
3315
|
-
const role = data.role === "assistant" ?
|
|
3316
|
-
console.log(`${ts} ${
|
|
3349
|
+
const role = data.role === "assistant" ? chalk13.cyan("AGENT") : chalk13.green("USER");
|
|
3350
|
+
console.log(`${ts} ${chalk13.white(`[${role}]`)} ${data.content}`);
|
|
3317
3351
|
}
|
|
3318
3352
|
break;
|
|
3319
3353
|
}
|
|
@@ -3332,13 +3366,13 @@ async function logsCommand(opts = {}) {
|
|
|
3332
3366
|
case "error-event": {
|
|
3333
3367
|
const errData = msg.data;
|
|
3334
3368
|
console.log(
|
|
3335
|
-
`${ts} ${
|
|
3369
|
+
`${ts} ${chalk13.red("[ERROR]")} ${errData?.message || errData?.title || JSON.stringify(msg.data)}`
|
|
3336
3370
|
);
|
|
3337
3371
|
break;
|
|
3338
3372
|
}
|
|
3339
3373
|
default:
|
|
3340
3374
|
console.log(
|
|
3341
|
-
`${ts} ${
|
|
3375
|
+
`${ts} ${chalk13.gray(`[${msg.type}]`)} ${JSON.stringify(msg.data)}`
|
|
3342
3376
|
);
|
|
3343
3377
|
break;
|
|
3344
3378
|
}
|
|
@@ -3364,7 +3398,7 @@ async function logsCommand(opts = {}) {
|
|
|
3364
3398
|
}
|
|
3365
3399
|
|
|
3366
3400
|
// src/commands/set-jwt.ts
|
|
3367
|
-
import
|
|
3401
|
+
import chalk14 from "chalk";
|
|
3368
3402
|
async function setJwtCommand(token) {
|
|
3369
3403
|
p.intro("\u{1F511} Enable Management Features");
|
|
3370
3404
|
if (token) {
|
|
@@ -3386,21 +3420,21 @@ async function setJwtCommand(token) {
|
|
|
3386
3420
|
[
|
|
3387
3421
|
"Here's what you'll do:",
|
|
3388
3422
|
"",
|
|
3389
|
-
`${
|
|
3423
|
+
`${chalk14.bold("1.")} We'll open ${chalk14.bold("app.openhome.com")} \u2014 make sure you're logged in`,
|
|
3390
3424
|
"",
|
|
3391
|
-
`${
|
|
3392
|
-
` Mac \u2192 ${
|
|
3393
|
-
` Windows / Linux \u2192 ${
|
|
3425
|
+
`${chalk14.bold("2.")} Open the browser console:`,
|
|
3426
|
+
` Mac \u2192 ${chalk14.cyan("Cmd + Option + J")}`,
|
|
3427
|
+
` Windows / Linux \u2192 ${chalk14.cyan("F12")} then click ${chalk14.cyan("Console")}`,
|
|
3394
3428
|
"",
|
|
3395
|
-
`${
|
|
3396
|
-
` ${
|
|
3397
|
-
` Type ${
|
|
3429
|
+
`${chalk14.bold("3.")} Chrome may show this warning \u2014 it's expected:`,
|
|
3430
|
+
` ${chalk14.yellow(`"Don't paste code you don't understand..."`)}`,
|
|
3431
|
+
` Type ${chalk14.cyan("allow pasting")} and press Enter to dismiss it.`,
|
|
3398
3432
|
"",
|
|
3399
|
-
`${
|
|
3433
|
+
`${chalk14.bold("4.")} Paste this command and press Enter:`,
|
|
3400
3434
|
"",
|
|
3401
|
-
` ${
|
|
3435
|
+
` ${chalk14.green("copy(localStorage.getItem('access_token')), '\u2713 Token copied to clipboard!'")}`,
|
|
3402
3436
|
"",
|
|
3403
|
-
`${
|
|
3437
|
+
`${chalk14.bold("5.")} Your token is copied to clipboard \u2014 paste it back here.`
|
|
3404
3438
|
].join("\n"),
|
|
3405
3439
|
"Enable management features (one-time setup)"
|
|
3406
3440
|
);
|
|
@@ -3421,7 +3455,7 @@ async function setJwtCommand(token) {
|
|
|
3421
3455
|
|
|
3422
3456
|
// src/commands/validate.ts
|
|
3423
3457
|
import { resolve as resolve5 } from "path";
|
|
3424
|
-
import
|
|
3458
|
+
import chalk15 from "chalk";
|
|
3425
3459
|
async function validateCommand(pathArg = ".") {
|
|
3426
3460
|
const targetDir = resolve5(pathArg);
|
|
3427
3461
|
p.intro(`\u{1F50E} Validate ability`);
|
|
@@ -3437,7 +3471,7 @@ async function validateCommand(pathArg = ".") {
|
|
|
3437
3471
|
if (result.errors.length > 0) {
|
|
3438
3472
|
p.note(
|
|
3439
3473
|
result.errors.map(
|
|
3440
|
-
(issue) => `${
|
|
3474
|
+
(issue) => `${chalk15.red("\u2717")} ${issue.file ? chalk15.bold(`[${issue.file}]`) + " " : ""}${issue.message}`
|
|
3441
3475
|
).join("\n"),
|
|
3442
3476
|
`${result.errors.length} Error(s)`
|
|
3443
3477
|
);
|
|
@@ -3445,7 +3479,7 @@ async function validateCommand(pathArg = ".") {
|
|
|
3445
3479
|
if (result.warnings.length > 0) {
|
|
3446
3480
|
p.note(
|
|
3447
3481
|
result.warnings.map(
|
|
3448
|
-
(w) => `${
|
|
3482
|
+
(w) => `${chalk15.yellow("\u26A0")} ${w.file ? chalk15.bold(`[${w.file}]`) + " " : ""}${w.message}`
|
|
3449
3483
|
).join("\n"),
|
|
3450
3484
|
`${result.warnings.length} Warning(s)`
|
|
3451
3485
|
);
|
|
@@ -3491,9 +3525,9 @@ async function checkForUpdates() {
|
|
|
3491
3525
|
);
|
|
3492
3526
|
process.exit(0);
|
|
3493
3527
|
} else {
|
|
3494
|
-
const { default:
|
|
3528
|
+
const { default: chalk16 } = await import("chalk");
|
|
3495
3529
|
console.log(
|
|
3496
|
-
|
|
3530
|
+
chalk16.yellow(
|
|
3497
3531
|
` Update available: v${version} \u2192 v${latest} Run: npm install -g openhome-cli@latest
|
|
3498
3532
|
`
|
|
3499
3533
|
)
|
package/package.json
CHANGED
package/src/api/client.ts
CHANGED
|
@@ -33,6 +33,13 @@ export class ApiError extends Error {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export class SessionExpiredError extends Error {
|
|
37
|
+
constructor() {
|
|
38
|
+
super("Session token expired or invalid");
|
|
39
|
+
this.name = "SessionExpiredError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
export interface IApiClient {
|
|
37
44
|
getPersonalities(): Promise<Personality[]>;
|
|
38
45
|
verifyApiKey(apiKey: string): Promise<VerifyApiKeyResponse>;
|
|
@@ -107,6 +114,17 @@ export class ApiClient implements IApiClient {
|
|
|
107
114
|
(body as ApiErrorResponse | null)?.error?.message ??
|
|
108
115
|
response.statusText;
|
|
109
116
|
|
|
117
|
+
// Detect expired/invalid JWT
|
|
118
|
+
if (
|
|
119
|
+
useJwt &&
|
|
120
|
+
(response.status === 401 ||
|
|
121
|
+
message.toLowerCase().includes("token not valid") ||
|
|
122
|
+
message.toLowerCase().includes("token is invalid") ||
|
|
123
|
+
message.toLowerCase().includes("not valid for any token"))
|
|
124
|
+
) {
|
|
125
|
+
throw new SessionExpiredError();
|
|
126
|
+
}
|
|
127
|
+
|
|
110
128
|
throw new ApiError(String(response.status), message);
|
|
111
129
|
}
|
|
112
130
|
|
|
@@ -134,9 +152,7 @@ export class ApiClient implements IApiClient {
|
|
|
134
152
|
const form = new FormData();
|
|
135
153
|
form.append(
|
|
136
154
|
"zip_file",
|
|
137
|
-
new Blob([zipBuffer
|
|
138
|
-
type: "application/zip",
|
|
139
|
-
}),
|
|
155
|
+
new Blob([new Uint8Array(zipBuffer)], { type: "application/zip" }),
|
|
140
156
|
"ability.zip",
|
|
141
157
|
);
|
|
142
158
|
|
|
@@ -146,7 +162,7 @@ export class ApiClient implements IApiClient {
|
|
|
146
162
|
imageExt === "jpg" || imageExt === "jpeg" ? "image/jpeg" : "image/png";
|
|
147
163
|
form.append(
|
|
148
164
|
"image_file",
|
|
149
|
-
new Blob([imageBuffer
|
|
165
|
+
new Blob([new Uint8Array(imageBuffer)], { type: imageMime }),
|
|
150
166
|
imageName,
|
|
151
167
|
);
|
|
152
168
|
}
|
|
@@ -159,10 +175,11 @@ export class ApiClient implements IApiClient {
|
|
|
159
175
|
form.append("personality_id", metadata.personality_id);
|
|
160
176
|
}
|
|
161
177
|
|
|
162
|
-
return this.request<UploadAbilityResponse>(
|
|
163
|
-
|
|
164
|
-
body: form,
|
|
165
|
-
|
|
178
|
+
return this.request<UploadAbilityResponse>(
|
|
179
|
+
ENDPOINTS.uploadCapability,
|
|
180
|
+
{ method: "POST", body: form },
|
|
181
|
+
true, // uses JWT
|
|
182
|
+
);
|
|
166
183
|
}
|
|
167
184
|
|
|
168
185
|
async listAbilities(): Promise<ListAbilitiesResponse> {
|
package/src/commands/assign.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
|
+
import { handleIfSessionExpired } from "./handle-session-expired.js";
|
|
2
3
|
import { MockApiClient } from "../api/mock-client.js";
|
|
3
4
|
import { getApiKey, getConfig, getJwt } from "../config/store.js";
|
|
4
5
|
import { error, success, info, p, handleCancel } from "../ui/format.js";
|
|
@@ -124,6 +125,7 @@ export async function assignCommand(
|
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
if (await handleIfSessionExpired(err)) return;
|
|
127
129
|
error(`Assign failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
128
130
|
process.exit(1);
|
|
129
131
|
}
|
package/src/commands/delete.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
|
+
import { handleIfSessionExpired } from "./handle-session-expired.js";
|
|
2
3
|
import { MockApiClient } from "../api/mock-client.js";
|
|
3
4
|
import { getApiKey, getConfig, getJwt } from "../config/store.js";
|
|
4
5
|
import { error, success, p, handleCancel } from "../ui/format.js";
|
|
@@ -108,6 +109,7 @@ export async function deleteCommand(
|
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
if (await handleIfSessionExpired(err)) return;
|
|
111
113
|
error(`Delete failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
112
114
|
process.exit(1);
|
|
113
115
|
}
|
package/src/commands/deploy.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { homedir } from "node:os";
|
|
|
10
10
|
import { validateAbility } from "../validation/validator.js";
|
|
11
11
|
import { createAbilityZip } from "../util/zip.js";
|
|
12
12
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
13
|
+
import { handleIfSessionExpired } from "./handle-session-expired.js";
|
|
13
14
|
import { MockApiClient } from "../api/mock-client.js";
|
|
14
15
|
import {
|
|
15
16
|
getApiKey,
|
|
@@ -549,6 +550,7 @@ export async function deployCommand(
|
|
|
549
550
|
return;
|
|
550
551
|
}
|
|
551
552
|
|
|
553
|
+
if (await handleIfSessionExpired(err)) return;
|
|
552
554
|
const msg = err instanceof Error ? err.message : String(err);
|
|
553
555
|
if (msg.toLowerCase().includes("same name")) {
|
|
554
556
|
error(`An ability named "${uniqueName}" already exists.`);
|
|
@@ -693,6 +695,7 @@ async function deployZip(
|
|
|
693
695
|
p.outro("Deployed successfully! 🎉");
|
|
694
696
|
} catch (err) {
|
|
695
697
|
s.stop("Upload failed.");
|
|
698
|
+
if (await handleIfSessionExpired(err)) return;
|
|
696
699
|
error(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
697
700
|
process.exit(1);
|
|
698
701
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SessionExpiredError } from "../api/client.js";
|
|
2
|
+
import { setupJwt } from "./login.js";
|
|
3
|
+
import { error, p } from "../ui/format.js";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
|
|
6
|
+
export async function handleIfSessionExpired(err: unknown): Promise<boolean> {
|
|
7
|
+
if (!(err instanceof SessionExpiredError)) return false;
|
|
8
|
+
|
|
9
|
+
console.log("");
|
|
10
|
+
p.note(
|
|
11
|
+
[
|
|
12
|
+
"Your session token has expired or been invalidated.",
|
|
13
|
+
"This happens when you log into the OpenHome website again.",
|
|
14
|
+
"",
|
|
15
|
+
`You need to grab a fresh token — it only takes 30 seconds.`,
|
|
16
|
+
].join("\n"),
|
|
17
|
+
chalk.yellow("Session expired"),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
await setupJwt();
|
|
21
|
+
p.note("Token updated. Run the command again to continue.", "Ready");
|
|
22
|
+
return true;
|
|
23
|
+
}
|
package/src/commands/list.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
|
+
import { handleIfSessionExpired } from "./handle-session-expired.js";
|
|
2
3
|
import { MockApiClient } from "../api/mock-client.js";
|
|
3
4
|
import { getApiKey, getConfig, getJwt } from "../config/store.js";
|
|
4
5
|
import { error, warn, info, table, p } from "../ui/format.js";
|
|
@@ -77,6 +78,7 @@ export async function listCommand(
|
|
|
77
78
|
p.outro("List endpoint not yet implemented.");
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
81
|
+
if (await handleIfSessionExpired(err)) return;
|
|
80
82
|
error(
|
|
81
83
|
`Failed to list abilities: ${err instanceof Error ? err.message : String(err)}`,
|
|
82
84
|
);
|
package/src/commands/toggle.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
|
+
import { handleIfSessionExpired } from "./handle-session-expired.js";
|
|
2
3
|
import { MockApiClient } from "../api/mock-client.js";
|
|
3
4
|
import { getApiKey, getConfig, getJwt } from "../config/store.js";
|
|
4
5
|
import { error, success, p, handleCancel } from "../ui/format.js";
|
|
@@ -119,6 +120,7 @@ export async function toggleCommand(
|
|
|
119
120
|
return;
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
if (await handleIfSessionExpired(err)) return;
|
|
122
124
|
error(`Toggle failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
123
125
|
process.exit(1);
|
|
124
126
|
}
|