openhome-cli 0.1.17 → 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
  }
@@ -684,6 +691,25 @@ async function createAbilityZip(dirPath) {
684
691
  });
685
692
  }
686
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
+
687
713
  // src/api/mock-client.ts
688
714
  var MOCK_PERSONALITIES = [
689
715
  { id: "pers_alice", name: "Alice", description: "Friendly assistant" },
@@ -1219,6 +1245,7 @@ async function deployCommand(pathArg, opts = {}) {
1219
1245
  p.outro("Zip ready for manual upload.");
1220
1246
  return;
1221
1247
  }
1248
+ if (await handleIfSessionExpired(err)) return;
1222
1249
  const msg = err instanceof Error ? err.message : String(err);
1223
1250
  if (msg.toLowerCase().includes("same name")) {
1224
1251
  error(`An ability named "${uniqueName}" already exists.`);
@@ -1341,6 +1368,7 @@ async function deployZip(zipPath, opts = {}) {
1341
1368
  p.outro("Deployed successfully! \u{1F389}");
1342
1369
  } catch (err) {
1343
1370
  s.stop("Upload failed.");
1371
+ if (await handleIfSessionExpired(err)) return;
1344
1372
  error(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
1345
1373
  process.exit(1);
1346
1374
  }
@@ -2139,7 +2167,7 @@ async function initCommand(nameArg) {
2139
2167
  }
2140
2168
 
2141
2169
  // src/commands/delete.ts
2142
- import chalk3 from "chalk";
2170
+ import chalk4 from "chalk";
2143
2171
  async function deleteCommand(abilityArg, opts = {}) {
2144
2172
  p.intro("\u{1F5D1}\uFE0F Delete ability");
2145
2173
  let client;
@@ -2194,7 +2222,7 @@ async function deleteCommand(abilityArg, opts = {}) {
2194
2222
  options: abilities.map((a) => ({
2195
2223
  value: a.ability_id,
2196
2224
  label: a.unique_name,
2197
- hint: `${chalk3.gray(a.status)} v${a.version}`
2225
+ hint: `${chalk4.gray(a.status)} v${a.version}`
2198
2226
  }))
2199
2227
  });
2200
2228
  handleCancel(selected);
@@ -2222,13 +2250,14 @@ async function deleteCommand(abilityArg, opts = {}) {
2222
2250
  p.note("API Not Available Yet", "Delete endpoint not yet implemented.");
2223
2251
  return;
2224
2252
  }
2253
+ if (await handleIfSessionExpired(err)) return;
2225
2254
  error(`Delete failed: ${err instanceof Error ? err.message : String(err)}`);
2226
2255
  process.exit(1);
2227
2256
  }
2228
2257
  }
2229
2258
 
2230
2259
  // src/commands/toggle.ts
2231
- import chalk4 from "chalk";
2260
+ import chalk5 from "chalk";
2232
2261
  async function toggleCommand(abilityArg, opts = {}) {
2233
2262
  p.intro("\u26A1 Enable / Disable ability");
2234
2263
  let client;
@@ -2283,7 +2312,7 @@ async function toggleCommand(abilityArg, opts = {}) {
2283
2312
  options: abilities.map((a) => ({
2284
2313
  value: a.ability_id,
2285
2314
  label: a.unique_name,
2286
- 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}`
2287
2316
  }))
2288
2317
  });
2289
2318
  handleCancel(selected);
@@ -2321,13 +2350,14 @@ async function toggleCommand(abilityArg, opts = {}) {
2321
2350
  p.note("Toggle endpoint not yet implemented.", "API Not Available Yet");
2322
2351
  return;
2323
2352
  }
2353
+ if (await handleIfSessionExpired(err)) return;
2324
2354
  error(`Toggle failed: ${err instanceof Error ? err.message : String(err)}`);
2325
2355
  process.exit(1);
2326
2356
  }
2327
2357
  }
2328
2358
 
2329
2359
  // src/commands/assign.ts
2330
- import chalk5 from "chalk";
2360
+ import chalk6 from "chalk";
2331
2361
  async function assignCommand(opts = {}) {
2332
2362
  p.intro("\u{1F517} Assign abilities to agent");
2333
2363
  let client;
@@ -2378,7 +2408,7 @@ async function assignCommand(opts = {}) {
2378
2408
  options: personalities.map((pers) => ({
2379
2409
  value: pers.id,
2380
2410
  label: pers.name,
2381
- hint: chalk5.gray(pers.id)
2411
+ hint: chalk6.gray(pers.id)
2382
2412
  }))
2383
2413
  });
2384
2414
  handleCancel(agentId);
@@ -2416,23 +2446,24 @@ async function assignCommand(opts = {}) {
2416
2446
  p.note("Assign endpoint not yet implemented.", "API Not Available Yet");
2417
2447
  return;
2418
2448
  }
2449
+ if (await handleIfSessionExpired(err)) return;
2419
2450
  error(`Assign failed: ${err instanceof Error ? err.message : String(err)}`);
2420
2451
  process.exit(1);
2421
2452
  }
2422
2453
  }
2423
2454
 
2424
2455
  // src/commands/list.ts
2425
- import chalk6 from "chalk";
2456
+ import chalk7 from "chalk";
2426
2457
  function statusColor(status) {
2427
2458
  switch (status) {
2428
2459
  case "active":
2429
- return chalk6.green(status);
2460
+ return chalk7.green(status);
2430
2461
  case "processing":
2431
- return chalk6.yellow(status);
2462
+ return chalk7.yellow(status);
2432
2463
  case "failed":
2433
- return chalk6.red(status);
2464
+ return chalk7.red(status);
2434
2465
  case "disabled":
2435
- return chalk6.gray(status);
2466
+ return chalk7.gray(status);
2436
2467
  default:
2437
2468
  return status;
2438
2469
  }
@@ -2484,6 +2515,7 @@ async function listCommand(opts = {}) {
2484
2515
  p.outro("List endpoint not yet implemented.");
2485
2516
  return;
2486
2517
  }
2518
+ if (await handleIfSessionExpired(err)) return;
2487
2519
  error(
2488
2520
  `Failed to list abilities: ${err instanceof Error ? err.message : String(err)}`
2489
2521
  );
@@ -2495,19 +2527,19 @@ async function listCommand(opts = {}) {
2495
2527
  import { join as join4, resolve as resolve3 } from "path";
2496
2528
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
2497
2529
  import { homedir as homedir3 } from "os";
2498
- import chalk7 from "chalk";
2530
+ import chalk8 from "chalk";
2499
2531
  function statusBadge(status) {
2500
2532
  switch (status) {
2501
2533
  case "active":
2502
- return chalk7.bgGreen.black(` ${status.toUpperCase()} `);
2534
+ return chalk8.bgGreen.black(` ${status.toUpperCase()} `);
2503
2535
  case "processing":
2504
- return chalk7.bgYellow.black(` ${status.toUpperCase()} `);
2536
+ return chalk8.bgYellow.black(` ${status.toUpperCase()} `);
2505
2537
  case "failed":
2506
- return chalk7.bgRed.white(` ${status.toUpperCase()} `);
2538
+ return chalk8.bgRed.white(` ${status.toUpperCase()} `);
2507
2539
  case "disabled":
2508
- return chalk7.bgGray.white(` ${status.toUpperCase()} `);
2540
+ return chalk8.bgGray.white(` ${status.toUpperCase()} `);
2509
2541
  default:
2510
- return chalk7.bgWhite.black(` ${status.toUpperCase()} `);
2542
+ return chalk8.bgWhite.black(` ${status.toUpperCase()} `);
2511
2543
  }
2512
2544
  }
2513
2545
  function readAbilityName(dir) {
@@ -2596,14 +2628,14 @@ async function statusCommand(abilityArg, opts = {}) {
2596
2628
  );
2597
2629
  if (ability.validation_errors.length > 0) {
2598
2630
  p.note(
2599
- ability.validation_errors.map((e) => chalk7.red(`\u2717 ${e}`)).join("\n"),
2631
+ ability.validation_errors.map((e) => chalk8.red(`\u2717 ${e}`)).join("\n"),
2600
2632
  "Validation Errors"
2601
2633
  );
2602
2634
  }
2603
2635
  if (ability.deploy_history.length > 0) {
2604
2636
  const historyLines = ability.deploy_history.map((event) => {
2605
- const icon = event.status === "success" ? chalk7.green("\u2713") : chalk7.red("\u2717");
2606
- 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())}`;
2607
2639
  });
2608
2640
  p.note(historyLines.join("\n"), "Deploy History");
2609
2641
  }
@@ -2623,7 +2655,7 @@ async function statusCommand(abilityArg, opts = {}) {
2623
2655
  }
2624
2656
 
2625
2657
  // src/commands/agents.ts
2626
- import chalk8 from "chalk";
2658
+ import chalk9 from "chalk";
2627
2659
  async function agentsCommand(opts = {}) {
2628
2660
  p.intro("\u{1F916} Your Agents");
2629
2661
  let client;
@@ -2648,7 +2680,7 @@ async function agentsCommand(opts = {}) {
2648
2680
  return;
2649
2681
  }
2650
2682
  p.note(
2651
- 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"),
2652
2684
  "Agents"
2653
2685
  );
2654
2686
  const config = getConfig();
@@ -2702,7 +2734,7 @@ async function logoutCommand() {
2702
2734
 
2703
2735
  // src/commands/chat.ts
2704
2736
  import WebSocket from "ws";
2705
- import chalk9 from "chalk";
2737
+ import chalk10 from "chalk";
2706
2738
  import * as readline from "readline";
2707
2739
  var PING_INTERVAL = 3e4;
2708
2740
  async function chatCommand(agentArg, opts = {}) {
@@ -2743,7 +2775,7 @@ async function chatCommand(agentArg, opts = {}) {
2743
2775
  }
2744
2776
  }
2745
2777
  const wsUrl = `${WS_BASE}${ENDPOINTS.voiceStream(apiKey, agentId)}`;
2746
- info(`Connecting to agent ${chalk9.bold(agentId)}...`);
2778
+ info(`Connecting to agent ${chalk10.bold(agentId)}...`);
2747
2779
  await new Promise((resolve6) => {
2748
2780
  const ws = new WebSocket(wsUrl, {
2749
2781
  perMessageDeflate: false,
@@ -2759,7 +2791,7 @@ async function chatCommand(agentArg, opts = {}) {
2759
2791
  output: process.stdout
2760
2792
  });
2761
2793
  function promptUser() {
2762
- rl.question(chalk9.green("You: "), (input) => {
2794
+ rl.question(chalk10.green("You: "), (input) => {
2763
2795
  const trimmed = input.trim();
2764
2796
  if (!trimmed) {
2765
2797
  promptUser();
@@ -2795,7 +2827,7 @@ async function chatCommand(agentArg, opts = {}) {
2795
2827
  }, PING_INTERVAL);
2796
2828
  success("Connected! Type a message and press Enter. Type /quit to exit.");
2797
2829
  console.log(
2798
- chalk9.gray(
2830
+ chalk10.gray(
2799
2831
  " Tip: Send trigger words to activate abilities (e.g. 'play aquaprime')"
2800
2832
  )
2801
2833
  );
@@ -2810,7 +2842,7 @@ async function chatCommand(agentArg, opts = {}) {
2810
2842
  const data = msg.data;
2811
2843
  if (data.content && data.role === "assistant") {
2812
2844
  if (data.live && !data.final) {
2813
- const prefix = `${chalk9.cyan("Agent:")} `;
2845
+ const prefix = `${chalk10.cyan("Agent:")} `;
2814
2846
  readline.clearLine(process.stdout, 0);
2815
2847
  readline.cursorTo(process.stdout, 0);
2816
2848
  process.stdout.write(`${prefix}${data.content}`);
@@ -2819,7 +2851,7 @@ async function chatCommand(agentArg, opts = {}) {
2819
2851
  if (currentResponse !== "") {
2820
2852
  console.log("");
2821
2853
  } else {
2822
- console.log(`${chalk9.cyan("Agent:")} ${data.content}`);
2854
+ console.log(`${chalk10.cyan("Agent:")} ${data.content}`);
2823
2855
  }
2824
2856
  currentResponse = "";
2825
2857
  console.log("");
@@ -2835,7 +2867,7 @@ async function chatCommand(agentArg, opts = {}) {
2835
2867
  ws.send(JSON.stringify({ type: "text", data: "bot-speak-end" }));
2836
2868
  if (currentResponse === "") {
2837
2869
  console.log(
2838
- chalk9.gray(" (Agent sent audio \u2014 text-only mode)")
2870
+ chalk10.gray(" (Agent sent audio \u2014 text-only mode)")
2839
2871
  );
2840
2872
  console.log("");
2841
2873
  }
@@ -2895,7 +2927,7 @@ async function chatCommand(agentArg, opts = {}) {
2895
2927
 
2896
2928
  // src/commands/trigger.ts
2897
2929
  import WebSocket2 from "ws";
2898
- import chalk10 from "chalk";
2930
+ import chalk11 from "chalk";
2899
2931
  var PING_INTERVAL2 = 3e4;
2900
2932
  var RESPONSE_TIMEOUT = 3e4;
2901
2933
  async function triggerCommand(phraseArg, opts = {}) {
@@ -2947,7 +2979,7 @@ async function triggerCommand(phraseArg, opts = {}) {
2947
2979
  }
2948
2980
  }
2949
2981
  const wsUrl = `${WS_BASE}${ENDPOINTS.voiceStream(apiKey, agentId)}`;
2950
- info(`Sending "${chalk10.bold(phrase)}" to agent ${chalk10.bold(agentId)}...`);
2982
+ info(`Sending "${chalk11.bold(phrase)}" to agent ${chalk11.bold(agentId)}...`);
2951
2983
  const s = p.spinner();
2952
2984
  s.start("Waiting for response...");
2953
2985
  await new Promise((resolve6) => {
@@ -2977,7 +3009,7 @@ async function triggerCommand(phraseArg, opts = {}) {
2977
3009
  s.stop("Timed out waiting for response.");
2978
3010
  if (fullResponse) {
2979
3011
  console.log(`
2980
- ${chalk10.cyan("Agent:")} ${fullResponse}`);
3012
+ ${chalk11.cyan("Agent:")} ${fullResponse}`);
2981
3013
  }
2982
3014
  cleanup();
2983
3015
  resolve6();
@@ -2994,7 +3026,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}`);
2994
3026
  if (!data.live || data.final) {
2995
3027
  s.stop("Response received.");
2996
3028
  console.log(`
2997
- ${chalk10.cyan("Agent:")} ${fullResponse}
3029
+ ${chalk11.cyan("Agent:")} ${fullResponse}
2998
3030
  `);
2999
3031
  cleanup();
3000
3032
  resolve6();
@@ -3011,7 +3043,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}
3011
3043
  if (fullResponse) {
3012
3044
  s.stop("Response received.");
3013
3045
  console.log(`
3014
- ${chalk10.cyan("Agent:")} ${fullResponse}
3046
+ ${chalk11.cyan("Agent:")} ${fullResponse}
3015
3047
  `);
3016
3048
  cleanup();
3017
3049
  resolve6();
@@ -3050,7 +3082,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}
3050
3082
  }
3051
3083
 
3052
3084
  // src/commands/whoami.ts
3053
- import chalk11 from "chalk";
3085
+ import chalk12 from "chalk";
3054
3086
  import { homedir as homedir4 } from "os";
3055
3087
  async function whoamiCommand() {
3056
3088
  p.intro("\u{1F464} OpenHome CLI Status");
@@ -3060,17 +3092,17 @@ async function whoamiCommand() {
3060
3092
  const home = homedir4();
3061
3093
  if (apiKey) {
3062
3094
  const masked = apiKey.slice(0, 6) + "..." + apiKey.slice(-4);
3063
- info(`Authenticated: ${chalk11.green("yes")} (key: ${chalk11.gray(masked)})`);
3095
+ info(`Authenticated: ${chalk12.green("yes")} (key: ${chalk12.gray(masked)})`);
3064
3096
  } else {
3065
3097
  info(
3066
- `Authenticated: ${chalk11.red("no")} \u2014 run ${chalk11.bold("openhome login")}`
3098
+ `Authenticated: ${chalk12.red("no")} \u2014 run ${chalk12.bold("openhome login")}`
3067
3099
  );
3068
3100
  }
3069
3101
  if (config.default_personality_id) {
3070
- info(`Default agent: ${chalk11.bold(config.default_personality_id)}`);
3102
+ info(`Default agent: ${chalk12.bold(config.default_personality_id)}`);
3071
3103
  } else {
3072
3104
  info(
3073
- `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")}`
3074
3106
  );
3075
3107
  }
3076
3108
  if (config.api_base_url) {
@@ -3079,12 +3111,12 @@ async function whoamiCommand() {
3079
3111
  if (tracked.length > 0) {
3080
3112
  const lines = tracked.map((a) => {
3081
3113
  const shortPath = a.path.startsWith(home) ? `~${a.path.slice(home.length)}` : a.path;
3082
- return ` ${chalk11.bold(a.name)} ${chalk11.gray(shortPath)}`;
3114
+ return ` ${chalk12.bold(a.name)} ${chalk12.gray(shortPath)}`;
3083
3115
  });
3084
3116
  p.note(lines.join("\n"), `${tracked.length} tracked ability(s)`);
3085
3117
  } else {
3086
3118
  info(
3087
- `Tracked abilities: ${chalk11.gray("none")} \u2014 run ${chalk11.bold("openhome init")}`
3119
+ `Tracked abilities: ${chalk12.gray("none")} \u2014 run ${chalk12.bold("openhome init")}`
3088
3120
  );
3089
3121
  }
3090
3122
  p.outro("Done.");
@@ -3226,7 +3258,7 @@ async function configEditCommand(pathArg) {
3226
3258
 
3227
3259
  // src/commands/logs.ts
3228
3260
  import WebSocket3 from "ws";
3229
- import chalk12 from "chalk";
3261
+ import chalk13 from "chalk";
3230
3262
  var PING_INTERVAL3 = 3e4;
3231
3263
  async function logsCommand(opts = {}) {
3232
3264
  p.intro("\u{1F4E1} Stream agent logs");
@@ -3266,8 +3298,8 @@ async function logsCommand(opts = {}) {
3266
3298
  }
3267
3299
  }
3268
3300
  const wsUrl = `${WS_BASE}${ENDPOINTS.voiceStream(apiKey, agentId)}`;
3269
- info(`Streaming logs from agent ${chalk12.bold(agentId)}...`);
3270
- 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.
3271
3303
  `);
3272
3304
  await new Promise((resolve6) => {
3273
3305
  const ws = new WebSocket3(wsUrl, {
@@ -3289,33 +3321,33 @@ async function logsCommand(opts = {}) {
3289
3321
  ws.on("message", (raw) => {
3290
3322
  try {
3291
3323
  const msg = JSON.parse(raw.toString());
3292
- const ts = chalk12.gray((/* @__PURE__ */ new Date()).toLocaleTimeString());
3324
+ const ts = chalk13.gray((/* @__PURE__ */ new Date()).toLocaleTimeString());
3293
3325
  switch (msg.type) {
3294
3326
  case "log":
3295
3327
  console.log(
3296
- `${ts} ${chalk12.blue("[LOG]")} ${JSON.stringify(msg.data)}`
3328
+ `${ts} ${chalk13.blue("[LOG]")} ${JSON.stringify(msg.data)}`
3297
3329
  );
3298
3330
  break;
3299
3331
  case "action":
3300
3332
  console.log(
3301
- `${ts} ${chalk12.magenta("[ACTION]")} ${JSON.stringify(msg.data)}`
3333
+ `${ts} ${chalk13.magenta("[ACTION]")} ${JSON.stringify(msg.data)}`
3302
3334
  );
3303
3335
  break;
3304
3336
  case "progress":
3305
3337
  console.log(
3306
- `${ts} ${chalk12.yellow("[PROGRESS]")} ${JSON.stringify(msg.data)}`
3338
+ `${ts} ${chalk13.yellow("[PROGRESS]")} ${JSON.stringify(msg.data)}`
3307
3339
  );
3308
3340
  break;
3309
3341
  case "question":
3310
3342
  console.log(
3311
- `${ts} ${chalk12.cyan("[QUESTION]")} ${JSON.stringify(msg.data)}`
3343
+ `${ts} ${chalk13.cyan("[QUESTION]")} ${JSON.stringify(msg.data)}`
3312
3344
  );
3313
3345
  break;
3314
3346
  case "message": {
3315
3347
  const data = msg.data;
3316
3348
  if (data.content && !data.live) {
3317
- const role = data.role === "assistant" ? chalk12.cyan("AGENT") : chalk12.green("USER");
3318
- 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}`);
3319
3351
  }
3320
3352
  break;
3321
3353
  }
@@ -3334,13 +3366,13 @@ async function logsCommand(opts = {}) {
3334
3366
  case "error-event": {
3335
3367
  const errData = msg.data;
3336
3368
  console.log(
3337
- `${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)}`
3338
3370
  );
3339
3371
  break;
3340
3372
  }
3341
3373
  default:
3342
3374
  console.log(
3343
- `${ts} ${chalk12.gray(`[${msg.type}]`)} ${JSON.stringify(msg.data)}`
3375
+ `${ts} ${chalk13.gray(`[${msg.type}]`)} ${JSON.stringify(msg.data)}`
3344
3376
  );
3345
3377
  break;
3346
3378
  }
@@ -3366,7 +3398,7 @@ async function logsCommand(opts = {}) {
3366
3398
  }
3367
3399
 
3368
3400
  // src/commands/set-jwt.ts
3369
- import chalk13 from "chalk";
3401
+ import chalk14 from "chalk";
3370
3402
  async function setJwtCommand(token) {
3371
3403
  p.intro("\u{1F511} Enable Management Features");
3372
3404
  if (token) {
@@ -3388,21 +3420,21 @@ async function setJwtCommand(token) {
3388
3420
  [
3389
3421
  "Here's what you'll do:",
3390
3422
  "",
3391
- `${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`,
3392
3424
  "",
3393
- `${chalk13.bold("2.")} Open the browser console:`,
3394
- ` Mac \u2192 ${chalk13.cyan("Cmd + Option + J")}`,
3395
- ` 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")}`,
3396
3428
  "",
3397
- `${chalk13.bold("3.")} Chrome may show this warning \u2014 it's expected:`,
3398
- ` ${chalk13.yellow(`"Don't paste code you don't understand..."`)}`,
3399
- ` 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.`,
3400
3432
  "",
3401
- `${chalk13.bold("4.")} Paste this command and press Enter:`,
3433
+ `${chalk14.bold("4.")} Paste this command and press Enter:`,
3402
3434
  "",
3403
- ` ${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!'")}`,
3404
3436
  "",
3405
- `${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.`
3406
3438
  ].join("\n"),
3407
3439
  "Enable management features (one-time setup)"
3408
3440
  );
@@ -3423,7 +3455,7 @@ async function setJwtCommand(token) {
3423
3455
 
3424
3456
  // src/commands/validate.ts
3425
3457
  import { resolve as resolve5 } from "path";
3426
- import chalk14 from "chalk";
3458
+ import chalk15 from "chalk";
3427
3459
  async function validateCommand(pathArg = ".") {
3428
3460
  const targetDir = resolve5(pathArg);
3429
3461
  p.intro(`\u{1F50E} Validate ability`);
@@ -3439,7 +3471,7 @@ async function validateCommand(pathArg = ".") {
3439
3471
  if (result.errors.length > 0) {
3440
3472
  p.note(
3441
3473
  result.errors.map(
3442
- (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}`
3443
3475
  ).join("\n"),
3444
3476
  `${result.errors.length} Error(s)`
3445
3477
  );
@@ -3447,7 +3479,7 @@ async function validateCommand(pathArg = ".") {
3447
3479
  if (result.warnings.length > 0) {
3448
3480
  p.note(
3449
3481
  result.warnings.map(
3450
- (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}`
3451
3483
  ).join("\n"),
3452
3484
  `${result.warnings.length} Warning(s)`
3453
3485
  );
@@ -3493,9 +3525,9 @@ async function checkForUpdates() {
3493
3525
  );
3494
3526
  process.exit(0);
3495
3527
  } else {
3496
- const { default: chalk15 } = await import("chalk");
3528
+ const { default: chalk16 } = await import("chalk");
3497
3529
  console.log(
3498
- chalk15.yellow(
3530
+ chalk16.yellow(
3499
3531
  ` Update available: v${version} \u2192 v${latest} Run: npm install -g openhome-cli@latest
3500
3532
  `
3501
3533
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openhome-cli",
3
- "version": "0.1.17",
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
  }
@@ -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
  }