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 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(ENDPOINTS.uploadCapability, {
126
- method: "POST",
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 chalk3 from "chalk";
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: `${chalk3.gray(a.status)} v${a.version}`
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 chalk4 from "chalk";
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" ? chalk4.gray("disabled") : chalk4.green("enabled")} v${a.version}`
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 chalk5 from "chalk";
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: chalk5.gray(pers.id)
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 chalk6 from "chalk";
2456
+ import chalk7 from "chalk";
2424
2457
  function statusColor(status) {
2425
2458
  switch (status) {
2426
2459
  case "active":
2427
- return chalk6.green(status);
2460
+ return chalk7.green(status);
2428
2461
  case "processing":
2429
- return chalk6.yellow(status);
2462
+ return chalk7.yellow(status);
2430
2463
  case "failed":
2431
- return chalk6.red(status);
2464
+ return chalk7.red(status);
2432
2465
  case "disabled":
2433
- return chalk6.gray(status);
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 chalk7 from "chalk";
2530
+ import chalk8 from "chalk";
2497
2531
  function statusBadge(status) {
2498
2532
  switch (status) {
2499
2533
  case "active":
2500
- return chalk7.bgGreen.black(` ${status.toUpperCase()} `);
2534
+ return chalk8.bgGreen.black(` ${status.toUpperCase()} `);
2501
2535
  case "processing":
2502
- return chalk7.bgYellow.black(` ${status.toUpperCase()} `);
2536
+ return chalk8.bgYellow.black(` ${status.toUpperCase()} `);
2503
2537
  case "failed":
2504
- return chalk7.bgRed.white(` ${status.toUpperCase()} `);
2538
+ return chalk8.bgRed.white(` ${status.toUpperCase()} `);
2505
2539
  case "disabled":
2506
- return chalk7.bgGray.white(` ${status.toUpperCase()} `);
2540
+ return chalk8.bgGray.white(` ${status.toUpperCase()} `);
2507
2541
  default:
2508
- return chalk7.bgWhite.black(` ${status.toUpperCase()} `);
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) => chalk7.red(`\u2717 ${e}`)).join("\n"),
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" ? chalk7.green("\u2713") : chalk7.red("\u2717");
2604
- return `${icon} v${event.version} ${event.message} ${chalk7.gray(new Date(event.timestamp).toLocaleString())}`;
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 chalk8 from "chalk";
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) => `${chalk8.bold(pers.name)} ${chalk8.gray(pers.id)}`).join("\n"),
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 chalk9 from "chalk";
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 ${chalk9.bold(agentId)}...`);
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(chalk9.green("You: "), (input) => {
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
- chalk9.gray(
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 = `${chalk9.cyan("Agent:")} `;
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(`${chalk9.cyan("Agent:")} ${data.content}`);
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
- chalk9.gray(" (Agent sent audio \u2014 text-only mode)")
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 chalk10 from "chalk";
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 "${chalk10.bold(phrase)}" to agent ${chalk10.bold(agentId)}...`);
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
- ${chalk10.cyan("Agent:")} ${fullResponse}`);
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
- ${chalk10.cyan("Agent:")} ${fullResponse}
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
- ${chalk10.cyan("Agent:")} ${fullResponse}
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 chalk11 from "chalk";
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: ${chalk11.green("yes")} (key: ${chalk11.gray(masked)})`);
3095
+ info(`Authenticated: ${chalk12.green("yes")} (key: ${chalk12.gray(masked)})`);
3062
3096
  } else {
3063
3097
  info(
3064
- `Authenticated: ${chalk11.red("no")} \u2014 run ${chalk11.bold("openhome login")}`
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: ${chalk11.bold(config.default_personality_id)}`);
3102
+ info(`Default agent: ${chalk12.bold(config.default_personality_id)}`);
3069
3103
  } else {
3070
3104
  info(
3071
- `Default agent: ${chalk11.gray("not set")} \u2014 run ${chalk11.bold("openhome agents")}`
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 ` ${chalk11.bold(a.name)} ${chalk11.gray(shortPath)}`;
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: ${chalk11.gray("none")} \u2014 run ${chalk11.bold("openhome init")}`
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 chalk12 from "chalk";
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 ${chalk12.bold(agentId)}...`);
3268
- info(`Press ${chalk12.bold("Ctrl+C")} to stop.
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 = chalk12.gray((/* @__PURE__ */ new Date()).toLocaleTimeString());
3324
+ const ts = chalk13.gray((/* @__PURE__ */ new Date()).toLocaleTimeString());
3291
3325
  switch (msg.type) {
3292
3326
  case "log":
3293
3327
  console.log(
3294
- `${ts} ${chalk12.blue("[LOG]")} ${JSON.stringify(msg.data)}`
3328
+ `${ts} ${chalk13.blue("[LOG]")} ${JSON.stringify(msg.data)}`
3295
3329
  );
3296
3330
  break;
3297
3331
  case "action":
3298
3332
  console.log(
3299
- `${ts} ${chalk12.magenta("[ACTION]")} ${JSON.stringify(msg.data)}`
3333
+ `${ts} ${chalk13.magenta("[ACTION]")} ${JSON.stringify(msg.data)}`
3300
3334
  );
3301
3335
  break;
3302
3336
  case "progress":
3303
3337
  console.log(
3304
- `${ts} ${chalk12.yellow("[PROGRESS]")} ${JSON.stringify(msg.data)}`
3338
+ `${ts} ${chalk13.yellow("[PROGRESS]")} ${JSON.stringify(msg.data)}`
3305
3339
  );
3306
3340
  break;
3307
3341
  case "question":
3308
3342
  console.log(
3309
- `${ts} ${chalk12.cyan("[QUESTION]")} ${JSON.stringify(msg.data)}`
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" ? chalk12.cyan("AGENT") : chalk12.green("USER");
3316
- console.log(`${ts} ${chalk12.white(`[${role}]`)} ${data.content}`);
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} ${chalk12.red("[ERROR]")} ${errData?.message || errData?.title || JSON.stringify(msg.data)}`
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} ${chalk12.gray(`[${msg.type}]`)} ${JSON.stringify(msg.data)}`
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 chalk13 from "chalk";
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
- `${chalk13.bold("1.")} We'll open ${chalk13.bold("app.openhome.com")} \u2014 make sure you're logged in`,
3423
+ `${chalk14.bold("1.")} We'll open ${chalk14.bold("app.openhome.com")} \u2014 make sure you're logged in`,
3390
3424
  "",
3391
- `${chalk13.bold("2.")} Open the browser console:`,
3392
- ` Mac \u2192 ${chalk13.cyan("Cmd + Option + J")}`,
3393
- ` Windows / Linux \u2192 ${chalk13.cyan("F12")} then click ${chalk13.cyan("Console")}`,
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
- `${chalk13.bold("3.")} Chrome may show this warning \u2014 it's expected:`,
3396
- ` ${chalk13.yellow(`"Don't paste code you don't understand..."`)}`,
3397
- ` Type ${chalk13.cyan("allow pasting")} and press Enter to dismiss it.`,
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
- `${chalk13.bold("4.")} Paste this command and press Enter:`,
3433
+ `${chalk14.bold("4.")} Paste this command and press Enter:`,
3400
3434
  "",
3401
- ` ${chalk13.green("copy(localStorage.getItem('access_token')), '\u2713 Token copied to clipboard!'")}`,
3435
+ ` ${chalk14.green("copy(localStorage.getItem('access_token')), '\u2713 Token copied to clipboard!'")}`,
3402
3436
  "",
3403
- `${chalk13.bold("5.")} Your token is copied to clipboard \u2014 paste it back here.`
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 chalk14 from "chalk";
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) => `${chalk14.red("\u2717")} ${issue.file ? chalk14.bold(`[${issue.file}]`) + " " : ""}${issue.message}`
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) => `${chalk14.yellow("\u26A0")} ${w.file ? chalk14.bold(`[${w.file}]`) + " " : ""}${w.message}`
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: chalk15 } = await import("chalk");
3528
+ const { default: chalk16 } = await import("chalk");
3495
3529
  console.log(
3496
- chalk15.yellow(
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openhome-cli",
3
- "version": "0.1.16",
3
+ "version": "0.1.19",
4
4
  "description": "CLI for managing OpenHome voice AI abilities",
5
5
  "type": "module",
6
6
  "bin": {
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 as unknown as ArrayBuffer], {
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 as unknown as ArrayBuffer], { type: imageMime }),
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>(ENDPOINTS.uploadCapability, {
163
- method: "POST",
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> {
@@ -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
  }
@@ -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
  }
@@ -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
+ }
@@ -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
  );
@@ -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
  }