openhome-cli 0.1.17 → 0.1.20

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" },
@@ -791,6 +817,12 @@ var MockApiClient = class {
791
817
  };
792
818
 
793
819
  // src/commands/deploy.ts
820
+ function expandPath(p2) {
821
+ if (p2.startsWith("~/") || p2 === "~") {
822
+ return join2(homedir(), p2.slice(2));
823
+ }
824
+ return resolve(p2);
825
+ }
794
826
  var IMAGE_EXTENSIONS = ["png", "jpg", "jpeg"];
795
827
  var ICON_NAMES = IMAGE_EXTENSIONS.flatMap((ext) => [
796
828
  `icon.${ext}`,
@@ -854,7 +886,7 @@ async function resolveAbilityDir(pathArg) {
854
886
  }
855
887
  });
856
888
  handleCancel(pathInput);
857
- return resolve(pathInput.trim());
889
+ return expandPath(pathInput.trim());
858
890
  }
859
891
  async function deployCommand(pathArg, opts = {}) {
860
892
  p.intro("\u{1F680} Upload Ability");
@@ -934,13 +966,13 @@ async function deployCommand(pathArg, opts = {}) {
934
966
  placeholder: "~/path/to/ability.zip",
935
967
  validate: (val) => {
936
968
  if (!val || !val.trim()) return "Path is required";
937
- if (!existsSync2(resolve(val.trim())))
969
+ if (!existsSync2(expandPath(val.trim())))
938
970
  return `File not found: ${val.trim()}`;
939
971
  if (!val.trim().endsWith(".zip")) return "Must be a .zip file";
940
972
  }
941
973
  });
942
974
  handleCancel(zipInput);
943
- zipPath = resolve(zipInput.trim());
975
+ zipPath = expandPath(zipInput.trim());
944
976
  } else {
945
977
  zipPath = selected;
946
978
  }
@@ -950,13 +982,13 @@ async function deployCommand(pathArg, opts = {}) {
950
982
  placeholder: "~/Downloads/my-ability.zip",
951
983
  validate: (val) => {
952
984
  if (!val || !val.trim()) return "Path is required";
953
- if (!existsSync2(resolve(val.trim())))
985
+ if (!existsSync2(expandPath(val.trim())))
954
986
  return `File not found: ${val.trim()}`;
955
987
  if (!val.trim().endsWith(".zip")) return "Must be a .zip file";
956
988
  }
957
989
  });
958
990
  handleCancel(zipInput);
959
- zipPath = resolve(zipInput.trim());
991
+ zipPath = expandPath(zipInput.trim());
960
992
  }
961
993
  await deployZip(zipPath, opts);
962
994
  return;
@@ -1079,7 +1111,7 @@ async function deployCommand(pathArg, opts = {}) {
1079
1111
  placeholder: "./icon.png",
1080
1112
  validate: (val) => {
1081
1113
  if (!val || !val.trim()) return void 0;
1082
- const resolved = resolve(val.trim());
1114
+ const resolved = expandPath(val.trim());
1083
1115
  if (!existsSync2(resolved)) return `File not found: ${val.trim()}`;
1084
1116
  if (!IMAGE_EXTS.has(extname(resolved).toLowerCase()))
1085
1117
  return "Image must be PNG or JPG";
@@ -1087,7 +1119,7 @@ async function deployCommand(pathArg, opts = {}) {
1087
1119
  });
1088
1120
  handleCancel(imgInput);
1089
1121
  const trimmed = imgInput.trim();
1090
- if (trimmed) imagePath = resolve(trimmed);
1122
+ if (trimmed) imagePath = expandPath(trimmed);
1091
1123
  } else if (selected !== "__skip__") {
1092
1124
  imagePath = selected;
1093
1125
  }
@@ -1097,7 +1129,7 @@ async function deployCommand(pathArg, opts = {}) {
1097
1129
  placeholder: "./icon.png",
1098
1130
  validate: (val) => {
1099
1131
  if (!val || !val.trim()) return void 0;
1100
- const resolved = resolve(val.trim());
1132
+ const resolved = expandPath(val.trim());
1101
1133
  if (!existsSync2(resolved)) return `File not found: ${val.trim()}`;
1102
1134
  if (!IMAGE_EXTS.has(extname(resolved).toLowerCase()))
1103
1135
  return "Image must be PNG or JPG";
@@ -1105,7 +1137,7 @@ async function deployCommand(pathArg, opts = {}) {
1105
1137
  });
1106
1138
  handleCancel(imgInput);
1107
1139
  const trimmed = imgInput.trim();
1108
- if (trimmed) imagePath = resolve(trimmed);
1140
+ if (trimmed) imagePath = expandPath(trimmed);
1109
1141
  }
1110
1142
  }
1111
1143
  const imageBuffer = imagePath ? readFileSync2(imagePath) : null;
@@ -1219,6 +1251,7 @@ async function deployCommand(pathArg, opts = {}) {
1219
1251
  p.outro("Zip ready for manual upload.");
1220
1252
  return;
1221
1253
  }
1254
+ if (await handleIfSessionExpired(err)) return;
1222
1255
  const msg = err instanceof Error ? err.message : String(err);
1223
1256
  if (msg.toLowerCase().includes("same name")) {
1224
1257
  error(`An ability named "${uniqueName}" already exists.`);
@@ -1341,6 +1374,7 @@ async function deployZip(zipPath, opts = {}) {
1341
1374
  p.outro("Deployed successfully! \u{1F389}");
1342
1375
  } catch (err) {
1343
1376
  s.stop("Upload failed.");
1377
+ if (await handleIfSessionExpired(err)) return;
1344
1378
  error(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
1345
1379
  process.exit(1);
1346
1380
  }
@@ -2139,7 +2173,7 @@ async function initCommand(nameArg) {
2139
2173
  }
2140
2174
 
2141
2175
  // src/commands/delete.ts
2142
- import chalk3 from "chalk";
2176
+ import chalk4 from "chalk";
2143
2177
  async function deleteCommand(abilityArg, opts = {}) {
2144
2178
  p.intro("\u{1F5D1}\uFE0F Delete ability");
2145
2179
  let client;
@@ -2194,7 +2228,7 @@ async function deleteCommand(abilityArg, opts = {}) {
2194
2228
  options: abilities.map((a) => ({
2195
2229
  value: a.ability_id,
2196
2230
  label: a.unique_name,
2197
- hint: `${chalk3.gray(a.status)} v${a.version}`
2231
+ hint: `${chalk4.gray(a.status)} v${a.version}`
2198
2232
  }))
2199
2233
  });
2200
2234
  handleCancel(selected);
@@ -2222,13 +2256,14 @@ async function deleteCommand(abilityArg, opts = {}) {
2222
2256
  p.note("API Not Available Yet", "Delete endpoint not yet implemented.");
2223
2257
  return;
2224
2258
  }
2259
+ if (await handleIfSessionExpired(err)) return;
2225
2260
  error(`Delete failed: ${err instanceof Error ? err.message : String(err)}`);
2226
2261
  process.exit(1);
2227
2262
  }
2228
2263
  }
2229
2264
 
2230
2265
  // src/commands/toggle.ts
2231
- import chalk4 from "chalk";
2266
+ import chalk5 from "chalk";
2232
2267
  async function toggleCommand(abilityArg, opts = {}) {
2233
2268
  p.intro("\u26A1 Enable / Disable ability");
2234
2269
  let client;
@@ -2283,7 +2318,7 @@ async function toggleCommand(abilityArg, opts = {}) {
2283
2318
  options: abilities.map((a) => ({
2284
2319
  value: a.ability_id,
2285
2320
  label: a.unique_name,
2286
- hint: `${a.status === "disabled" ? chalk4.gray("disabled") : chalk4.green("enabled")} v${a.version}`
2321
+ hint: `${a.status === "disabled" ? chalk5.gray("disabled") : chalk5.green("enabled")} v${a.version}`
2287
2322
  }))
2288
2323
  });
2289
2324
  handleCancel(selected);
@@ -2321,13 +2356,14 @@ async function toggleCommand(abilityArg, opts = {}) {
2321
2356
  p.note("Toggle endpoint not yet implemented.", "API Not Available Yet");
2322
2357
  return;
2323
2358
  }
2359
+ if (await handleIfSessionExpired(err)) return;
2324
2360
  error(`Toggle failed: ${err instanceof Error ? err.message : String(err)}`);
2325
2361
  process.exit(1);
2326
2362
  }
2327
2363
  }
2328
2364
 
2329
2365
  // src/commands/assign.ts
2330
- import chalk5 from "chalk";
2366
+ import chalk6 from "chalk";
2331
2367
  async function assignCommand(opts = {}) {
2332
2368
  p.intro("\u{1F517} Assign abilities to agent");
2333
2369
  let client;
@@ -2378,7 +2414,7 @@ async function assignCommand(opts = {}) {
2378
2414
  options: personalities.map((pers) => ({
2379
2415
  value: pers.id,
2380
2416
  label: pers.name,
2381
- hint: chalk5.gray(pers.id)
2417
+ hint: chalk6.gray(pers.id)
2382
2418
  }))
2383
2419
  });
2384
2420
  handleCancel(agentId);
@@ -2416,23 +2452,24 @@ async function assignCommand(opts = {}) {
2416
2452
  p.note("Assign endpoint not yet implemented.", "API Not Available Yet");
2417
2453
  return;
2418
2454
  }
2455
+ if (await handleIfSessionExpired(err)) return;
2419
2456
  error(`Assign failed: ${err instanceof Error ? err.message : String(err)}`);
2420
2457
  process.exit(1);
2421
2458
  }
2422
2459
  }
2423
2460
 
2424
2461
  // src/commands/list.ts
2425
- import chalk6 from "chalk";
2462
+ import chalk7 from "chalk";
2426
2463
  function statusColor(status) {
2427
2464
  switch (status) {
2428
2465
  case "active":
2429
- return chalk6.green(status);
2466
+ return chalk7.green(status);
2430
2467
  case "processing":
2431
- return chalk6.yellow(status);
2468
+ return chalk7.yellow(status);
2432
2469
  case "failed":
2433
- return chalk6.red(status);
2470
+ return chalk7.red(status);
2434
2471
  case "disabled":
2435
- return chalk6.gray(status);
2472
+ return chalk7.gray(status);
2436
2473
  default:
2437
2474
  return status;
2438
2475
  }
@@ -2484,6 +2521,7 @@ async function listCommand(opts = {}) {
2484
2521
  p.outro("List endpoint not yet implemented.");
2485
2522
  return;
2486
2523
  }
2524
+ if (await handleIfSessionExpired(err)) return;
2487
2525
  error(
2488
2526
  `Failed to list abilities: ${err instanceof Error ? err.message : String(err)}`
2489
2527
  );
@@ -2495,19 +2533,19 @@ async function listCommand(opts = {}) {
2495
2533
  import { join as join4, resolve as resolve3 } from "path";
2496
2534
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
2497
2535
  import { homedir as homedir3 } from "os";
2498
- import chalk7 from "chalk";
2536
+ import chalk8 from "chalk";
2499
2537
  function statusBadge(status) {
2500
2538
  switch (status) {
2501
2539
  case "active":
2502
- return chalk7.bgGreen.black(` ${status.toUpperCase()} `);
2540
+ return chalk8.bgGreen.black(` ${status.toUpperCase()} `);
2503
2541
  case "processing":
2504
- return chalk7.bgYellow.black(` ${status.toUpperCase()} `);
2542
+ return chalk8.bgYellow.black(` ${status.toUpperCase()} `);
2505
2543
  case "failed":
2506
- return chalk7.bgRed.white(` ${status.toUpperCase()} `);
2544
+ return chalk8.bgRed.white(` ${status.toUpperCase()} `);
2507
2545
  case "disabled":
2508
- return chalk7.bgGray.white(` ${status.toUpperCase()} `);
2546
+ return chalk8.bgGray.white(` ${status.toUpperCase()} `);
2509
2547
  default:
2510
- return chalk7.bgWhite.black(` ${status.toUpperCase()} `);
2548
+ return chalk8.bgWhite.black(` ${status.toUpperCase()} `);
2511
2549
  }
2512
2550
  }
2513
2551
  function readAbilityName(dir) {
@@ -2596,14 +2634,14 @@ async function statusCommand(abilityArg, opts = {}) {
2596
2634
  );
2597
2635
  if (ability.validation_errors.length > 0) {
2598
2636
  p.note(
2599
- ability.validation_errors.map((e) => chalk7.red(`\u2717 ${e}`)).join("\n"),
2637
+ ability.validation_errors.map((e) => chalk8.red(`\u2717 ${e}`)).join("\n"),
2600
2638
  "Validation Errors"
2601
2639
  );
2602
2640
  }
2603
2641
  if (ability.deploy_history.length > 0) {
2604
2642
  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())}`;
2643
+ const icon = event.status === "success" ? chalk8.green("\u2713") : chalk8.red("\u2717");
2644
+ return `${icon} v${event.version} ${event.message} ${chalk8.gray(new Date(event.timestamp).toLocaleString())}`;
2607
2645
  });
2608
2646
  p.note(historyLines.join("\n"), "Deploy History");
2609
2647
  }
@@ -2623,7 +2661,7 @@ async function statusCommand(abilityArg, opts = {}) {
2623
2661
  }
2624
2662
 
2625
2663
  // src/commands/agents.ts
2626
- import chalk8 from "chalk";
2664
+ import chalk9 from "chalk";
2627
2665
  async function agentsCommand(opts = {}) {
2628
2666
  p.intro("\u{1F916} Your Agents");
2629
2667
  let client;
@@ -2648,7 +2686,7 @@ async function agentsCommand(opts = {}) {
2648
2686
  return;
2649
2687
  }
2650
2688
  p.note(
2651
- personalities.map((pers) => `${chalk8.bold(pers.name)} ${chalk8.gray(pers.id)}`).join("\n"),
2689
+ personalities.map((pers) => `${chalk9.bold(pers.name)} ${chalk9.gray(pers.id)}`).join("\n"),
2652
2690
  "Agents"
2653
2691
  );
2654
2692
  const config = getConfig();
@@ -2702,7 +2740,7 @@ async function logoutCommand() {
2702
2740
 
2703
2741
  // src/commands/chat.ts
2704
2742
  import WebSocket from "ws";
2705
- import chalk9 from "chalk";
2743
+ import chalk10 from "chalk";
2706
2744
  import * as readline from "readline";
2707
2745
  var PING_INTERVAL = 3e4;
2708
2746
  async function chatCommand(agentArg, opts = {}) {
@@ -2743,7 +2781,7 @@ async function chatCommand(agentArg, opts = {}) {
2743
2781
  }
2744
2782
  }
2745
2783
  const wsUrl = `${WS_BASE}${ENDPOINTS.voiceStream(apiKey, agentId)}`;
2746
- info(`Connecting to agent ${chalk9.bold(agentId)}...`);
2784
+ info(`Connecting to agent ${chalk10.bold(agentId)}...`);
2747
2785
  await new Promise((resolve6) => {
2748
2786
  const ws = new WebSocket(wsUrl, {
2749
2787
  perMessageDeflate: false,
@@ -2759,7 +2797,7 @@ async function chatCommand(agentArg, opts = {}) {
2759
2797
  output: process.stdout
2760
2798
  });
2761
2799
  function promptUser() {
2762
- rl.question(chalk9.green("You: "), (input) => {
2800
+ rl.question(chalk10.green("You: "), (input) => {
2763
2801
  const trimmed = input.trim();
2764
2802
  if (!trimmed) {
2765
2803
  promptUser();
@@ -2795,7 +2833,7 @@ async function chatCommand(agentArg, opts = {}) {
2795
2833
  }, PING_INTERVAL);
2796
2834
  success("Connected! Type a message and press Enter. Type /quit to exit.");
2797
2835
  console.log(
2798
- chalk9.gray(
2836
+ chalk10.gray(
2799
2837
  " Tip: Send trigger words to activate abilities (e.g. 'play aquaprime')"
2800
2838
  )
2801
2839
  );
@@ -2810,7 +2848,7 @@ async function chatCommand(agentArg, opts = {}) {
2810
2848
  const data = msg.data;
2811
2849
  if (data.content && data.role === "assistant") {
2812
2850
  if (data.live && !data.final) {
2813
- const prefix = `${chalk9.cyan("Agent:")} `;
2851
+ const prefix = `${chalk10.cyan("Agent:")} `;
2814
2852
  readline.clearLine(process.stdout, 0);
2815
2853
  readline.cursorTo(process.stdout, 0);
2816
2854
  process.stdout.write(`${prefix}${data.content}`);
@@ -2819,7 +2857,7 @@ async function chatCommand(agentArg, opts = {}) {
2819
2857
  if (currentResponse !== "") {
2820
2858
  console.log("");
2821
2859
  } else {
2822
- console.log(`${chalk9.cyan("Agent:")} ${data.content}`);
2860
+ console.log(`${chalk10.cyan("Agent:")} ${data.content}`);
2823
2861
  }
2824
2862
  currentResponse = "";
2825
2863
  console.log("");
@@ -2835,7 +2873,7 @@ async function chatCommand(agentArg, opts = {}) {
2835
2873
  ws.send(JSON.stringify({ type: "text", data: "bot-speak-end" }));
2836
2874
  if (currentResponse === "") {
2837
2875
  console.log(
2838
- chalk9.gray(" (Agent sent audio \u2014 text-only mode)")
2876
+ chalk10.gray(" (Agent sent audio \u2014 text-only mode)")
2839
2877
  );
2840
2878
  console.log("");
2841
2879
  }
@@ -2895,7 +2933,7 @@ async function chatCommand(agentArg, opts = {}) {
2895
2933
 
2896
2934
  // src/commands/trigger.ts
2897
2935
  import WebSocket2 from "ws";
2898
- import chalk10 from "chalk";
2936
+ import chalk11 from "chalk";
2899
2937
  var PING_INTERVAL2 = 3e4;
2900
2938
  var RESPONSE_TIMEOUT = 3e4;
2901
2939
  async function triggerCommand(phraseArg, opts = {}) {
@@ -2947,7 +2985,7 @@ async function triggerCommand(phraseArg, opts = {}) {
2947
2985
  }
2948
2986
  }
2949
2987
  const wsUrl = `${WS_BASE}${ENDPOINTS.voiceStream(apiKey, agentId)}`;
2950
- info(`Sending "${chalk10.bold(phrase)}" to agent ${chalk10.bold(agentId)}...`);
2988
+ info(`Sending "${chalk11.bold(phrase)}" to agent ${chalk11.bold(agentId)}...`);
2951
2989
  const s = p.spinner();
2952
2990
  s.start("Waiting for response...");
2953
2991
  await new Promise((resolve6) => {
@@ -2977,7 +3015,7 @@ async function triggerCommand(phraseArg, opts = {}) {
2977
3015
  s.stop("Timed out waiting for response.");
2978
3016
  if (fullResponse) {
2979
3017
  console.log(`
2980
- ${chalk10.cyan("Agent:")} ${fullResponse}`);
3018
+ ${chalk11.cyan("Agent:")} ${fullResponse}`);
2981
3019
  }
2982
3020
  cleanup();
2983
3021
  resolve6();
@@ -2994,7 +3032,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}`);
2994
3032
  if (!data.live || data.final) {
2995
3033
  s.stop("Response received.");
2996
3034
  console.log(`
2997
- ${chalk10.cyan("Agent:")} ${fullResponse}
3035
+ ${chalk11.cyan("Agent:")} ${fullResponse}
2998
3036
  `);
2999
3037
  cleanup();
3000
3038
  resolve6();
@@ -3011,7 +3049,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}
3011
3049
  if (fullResponse) {
3012
3050
  s.stop("Response received.");
3013
3051
  console.log(`
3014
- ${chalk10.cyan("Agent:")} ${fullResponse}
3052
+ ${chalk11.cyan("Agent:")} ${fullResponse}
3015
3053
  `);
3016
3054
  cleanup();
3017
3055
  resolve6();
@@ -3050,7 +3088,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}
3050
3088
  }
3051
3089
 
3052
3090
  // src/commands/whoami.ts
3053
- import chalk11 from "chalk";
3091
+ import chalk12 from "chalk";
3054
3092
  import { homedir as homedir4 } from "os";
3055
3093
  async function whoamiCommand() {
3056
3094
  p.intro("\u{1F464} OpenHome CLI Status");
@@ -3060,17 +3098,17 @@ async function whoamiCommand() {
3060
3098
  const home = homedir4();
3061
3099
  if (apiKey) {
3062
3100
  const masked = apiKey.slice(0, 6) + "..." + apiKey.slice(-4);
3063
- info(`Authenticated: ${chalk11.green("yes")} (key: ${chalk11.gray(masked)})`);
3101
+ info(`Authenticated: ${chalk12.green("yes")} (key: ${chalk12.gray(masked)})`);
3064
3102
  } else {
3065
3103
  info(
3066
- `Authenticated: ${chalk11.red("no")} \u2014 run ${chalk11.bold("openhome login")}`
3104
+ `Authenticated: ${chalk12.red("no")} \u2014 run ${chalk12.bold("openhome login")}`
3067
3105
  );
3068
3106
  }
3069
3107
  if (config.default_personality_id) {
3070
- info(`Default agent: ${chalk11.bold(config.default_personality_id)}`);
3108
+ info(`Default agent: ${chalk12.bold(config.default_personality_id)}`);
3071
3109
  } else {
3072
3110
  info(
3073
- `Default agent: ${chalk11.gray("not set")} \u2014 run ${chalk11.bold("openhome agents")}`
3111
+ `Default agent: ${chalk12.gray("not set")} \u2014 run ${chalk12.bold("openhome agents")}`
3074
3112
  );
3075
3113
  }
3076
3114
  if (config.api_base_url) {
@@ -3079,12 +3117,12 @@ async function whoamiCommand() {
3079
3117
  if (tracked.length > 0) {
3080
3118
  const lines = tracked.map((a) => {
3081
3119
  const shortPath = a.path.startsWith(home) ? `~${a.path.slice(home.length)}` : a.path;
3082
- return ` ${chalk11.bold(a.name)} ${chalk11.gray(shortPath)}`;
3120
+ return ` ${chalk12.bold(a.name)} ${chalk12.gray(shortPath)}`;
3083
3121
  });
3084
3122
  p.note(lines.join("\n"), `${tracked.length} tracked ability(s)`);
3085
3123
  } else {
3086
3124
  info(
3087
- `Tracked abilities: ${chalk11.gray("none")} \u2014 run ${chalk11.bold("openhome init")}`
3125
+ `Tracked abilities: ${chalk12.gray("none")} \u2014 run ${chalk12.bold("openhome init")}`
3088
3126
  );
3089
3127
  }
3090
3128
  p.outro("Done.");
@@ -3226,7 +3264,7 @@ async function configEditCommand(pathArg) {
3226
3264
 
3227
3265
  // src/commands/logs.ts
3228
3266
  import WebSocket3 from "ws";
3229
- import chalk12 from "chalk";
3267
+ import chalk13 from "chalk";
3230
3268
  var PING_INTERVAL3 = 3e4;
3231
3269
  async function logsCommand(opts = {}) {
3232
3270
  p.intro("\u{1F4E1} Stream agent logs");
@@ -3266,8 +3304,8 @@ async function logsCommand(opts = {}) {
3266
3304
  }
3267
3305
  }
3268
3306
  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.
3307
+ info(`Streaming logs from agent ${chalk13.bold(agentId)}...`);
3308
+ info(`Press ${chalk13.bold("Ctrl+C")} to stop.
3271
3309
  `);
3272
3310
  await new Promise((resolve6) => {
3273
3311
  const ws = new WebSocket3(wsUrl, {
@@ -3289,33 +3327,33 @@ async function logsCommand(opts = {}) {
3289
3327
  ws.on("message", (raw) => {
3290
3328
  try {
3291
3329
  const msg = JSON.parse(raw.toString());
3292
- const ts = chalk12.gray((/* @__PURE__ */ new Date()).toLocaleTimeString());
3330
+ const ts = chalk13.gray((/* @__PURE__ */ new Date()).toLocaleTimeString());
3293
3331
  switch (msg.type) {
3294
3332
  case "log":
3295
3333
  console.log(
3296
- `${ts} ${chalk12.blue("[LOG]")} ${JSON.stringify(msg.data)}`
3334
+ `${ts} ${chalk13.blue("[LOG]")} ${JSON.stringify(msg.data)}`
3297
3335
  );
3298
3336
  break;
3299
3337
  case "action":
3300
3338
  console.log(
3301
- `${ts} ${chalk12.magenta("[ACTION]")} ${JSON.stringify(msg.data)}`
3339
+ `${ts} ${chalk13.magenta("[ACTION]")} ${JSON.stringify(msg.data)}`
3302
3340
  );
3303
3341
  break;
3304
3342
  case "progress":
3305
3343
  console.log(
3306
- `${ts} ${chalk12.yellow("[PROGRESS]")} ${JSON.stringify(msg.data)}`
3344
+ `${ts} ${chalk13.yellow("[PROGRESS]")} ${JSON.stringify(msg.data)}`
3307
3345
  );
3308
3346
  break;
3309
3347
  case "question":
3310
3348
  console.log(
3311
- `${ts} ${chalk12.cyan("[QUESTION]")} ${JSON.stringify(msg.data)}`
3349
+ `${ts} ${chalk13.cyan("[QUESTION]")} ${JSON.stringify(msg.data)}`
3312
3350
  );
3313
3351
  break;
3314
3352
  case "message": {
3315
3353
  const data = msg.data;
3316
3354
  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}`);
3355
+ const role = data.role === "assistant" ? chalk13.cyan("AGENT") : chalk13.green("USER");
3356
+ console.log(`${ts} ${chalk13.white(`[${role}]`)} ${data.content}`);
3319
3357
  }
3320
3358
  break;
3321
3359
  }
@@ -3334,13 +3372,13 @@ async function logsCommand(opts = {}) {
3334
3372
  case "error-event": {
3335
3373
  const errData = msg.data;
3336
3374
  console.log(
3337
- `${ts} ${chalk12.red("[ERROR]")} ${errData?.message || errData?.title || JSON.stringify(msg.data)}`
3375
+ `${ts} ${chalk13.red("[ERROR]")} ${errData?.message || errData?.title || JSON.stringify(msg.data)}`
3338
3376
  );
3339
3377
  break;
3340
3378
  }
3341
3379
  default:
3342
3380
  console.log(
3343
- `${ts} ${chalk12.gray(`[${msg.type}]`)} ${JSON.stringify(msg.data)}`
3381
+ `${ts} ${chalk13.gray(`[${msg.type}]`)} ${JSON.stringify(msg.data)}`
3344
3382
  );
3345
3383
  break;
3346
3384
  }
@@ -3366,7 +3404,7 @@ async function logsCommand(opts = {}) {
3366
3404
  }
3367
3405
 
3368
3406
  // src/commands/set-jwt.ts
3369
- import chalk13 from "chalk";
3407
+ import chalk14 from "chalk";
3370
3408
  async function setJwtCommand(token) {
3371
3409
  p.intro("\u{1F511} Enable Management Features");
3372
3410
  if (token) {
@@ -3388,21 +3426,21 @@ async function setJwtCommand(token) {
3388
3426
  [
3389
3427
  "Here's what you'll do:",
3390
3428
  "",
3391
- `${chalk13.bold("1.")} We'll open ${chalk13.bold("app.openhome.com")} \u2014 make sure you're logged in`,
3429
+ `${chalk14.bold("1.")} We'll open ${chalk14.bold("app.openhome.com")} \u2014 make sure you're logged in`,
3392
3430
  "",
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")}`,
3431
+ `${chalk14.bold("2.")} Open the browser console:`,
3432
+ ` Mac \u2192 ${chalk14.cyan("Cmd + Option + J")}`,
3433
+ ` Windows / Linux \u2192 ${chalk14.cyan("F12")} then click ${chalk14.cyan("Console")}`,
3396
3434
  "",
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.`,
3435
+ `${chalk14.bold("3.")} Chrome may show this warning \u2014 it's expected:`,
3436
+ ` ${chalk14.yellow(`"Don't paste code you don't understand..."`)}`,
3437
+ ` Type ${chalk14.cyan("allow pasting")} and press Enter to dismiss it.`,
3400
3438
  "",
3401
- `${chalk13.bold("4.")} Paste this command and press Enter:`,
3439
+ `${chalk14.bold("4.")} Paste this command and press Enter:`,
3402
3440
  "",
3403
- ` ${chalk13.green("copy(localStorage.getItem('access_token')), '\u2713 Token copied to clipboard!'")}`,
3441
+ ` ${chalk14.green("copy(localStorage.getItem('access_token')), '\u2713 Token copied to clipboard!'")}`,
3404
3442
  "",
3405
- `${chalk13.bold("5.")} Your token is copied to clipboard \u2014 paste it back here.`
3443
+ `${chalk14.bold("5.")} Your token is copied to clipboard \u2014 paste it back here.`
3406
3444
  ].join("\n"),
3407
3445
  "Enable management features (one-time setup)"
3408
3446
  );
@@ -3423,7 +3461,7 @@ async function setJwtCommand(token) {
3423
3461
 
3424
3462
  // src/commands/validate.ts
3425
3463
  import { resolve as resolve5 } from "path";
3426
- import chalk14 from "chalk";
3464
+ import chalk15 from "chalk";
3427
3465
  async function validateCommand(pathArg = ".") {
3428
3466
  const targetDir = resolve5(pathArg);
3429
3467
  p.intro(`\u{1F50E} Validate ability`);
@@ -3439,7 +3477,7 @@ async function validateCommand(pathArg = ".") {
3439
3477
  if (result.errors.length > 0) {
3440
3478
  p.note(
3441
3479
  result.errors.map(
3442
- (issue) => `${chalk14.red("\u2717")} ${issue.file ? chalk14.bold(`[${issue.file}]`) + " " : ""}${issue.message}`
3480
+ (issue) => `${chalk15.red("\u2717")} ${issue.file ? chalk15.bold(`[${issue.file}]`) + " " : ""}${issue.message}`
3443
3481
  ).join("\n"),
3444
3482
  `${result.errors.length} Error(s)`
3445
3483
  );
@@ -3447,7 +3485,7 @@ async function validateCommand(pathArg = ".") {
3447
3485
  if (result.warnings.length > 0) {
3448
3486
  p.note(
3449
3487
  result.warnings.map(
3450
- (w) => `${chalk14.yellow("\u26A0")} ${w.file ? chalk14.bold(`[${w.file}]`) + " " : ""}${w.message}`
3488
+ (w) => `${chalk15.yellow("\u26A0")} ${w.file ? chalk15.bold(`[${w.file}]`) + " " : ""}${w.message}`
3451
3489
  ).join("\n"),
3452
3490
  `${result.warnings.length} Warning(s)`
3453
3491
  );
@@ -3493,9 +3531,9 @@ async function checkForUpdates() {
3493
3531
  );
3494
3532
  process.exit(0);
3495
3533
  } else {
3496
- const { default: chalk15 } = await import("chalk");
3534
+ const { default: chalk16 } = await import("chalk");
3497
3535
  console.log(
3498
- chalk15.yellow(
3536
+ chalk16.yellow(
3499
3537
  ` Update available: v${version} \u2192 v${latest} Run: npm install -g openhome-cli@latest
3500
3538
  `
3501
3539
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openhome-cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.20",
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,
@@ -31,6 +32,13 @@ interface AbilityConfig {
31
32
  [key: string]: unknown;
32
33
  }
33
34
 
35
+ function expandPath(p: string): string {
36
+ if (p.startsWith("~/") || p === "~") {
37
+ return join(homedir(), p.slice(2));
38
+ }
39
+ return resolve(p);
40
+ }
41
+
34
42
  const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg"];
35
43
  const ICON_NAMES = IMAGE_EXTENSIONS.flatMap((ext) => [
36
44
  `icon.${ext}`,
@@ -113,7 +121,7 @@ async function resolveAbilityDir(pathArg?: string): Promise<string> {
113
121
  },
114
122
  });
115
123
  handleCancel(pathInput);
116
- return resolve((pathInput as string).trim());
124
+ return expandPath((pathInput as string).trim());
117
125
  }
118
126
 
119
127
  export async function deployCommand(
@@ -218,13 +226,13 @@ export async function deployCommand(
218
226
  placeholder: "~/path/to/ability.zip",
219
227
  validate: (val) => {
220
228
  if (!val || !val.trim()) return "Path is required";
221
- if (!existsSync(resolve(val.trim())))
229
+ if (!existsSync(expandPath(val.trim())))
222
230
  return `File not found: ${val.trim()}`;
223
231
  if (!val.trim().endsWith(".zip")) return "Must be a .zip file";
224
232
  },
225
233
  });
226
234
  handleCancel(zipInput);
227
- zipPath = resolve((zipInput as string).trim());
235
+ zipPath = expandPath((zipInput as string).trim());
228
236
  } else {
229
237
  zipPath = selected as string;
230
238
  }
@@ -234,13 +242,13 @@ export async function deployCommand(
234
242
  placeholder: "~/Downloads/my-ability.zip",
235
243
  validate: (val) => {
236
244
  if (!val || !val.trim()) return "Path is required";
237
- if (!existsSync(resolve(val.trim())))
245
+ if (!existsSync(expandPath(val.trim())))
238
246
  return `File not found: ${val.trim()}`;
239
247
  if (!val.trim().endsWith(".zip")) return "Must be a .zip file";
240
248
  },
241
249
  });
242
250
  handleCancel(zipInput);
243
- zipPath = resolve((zipInput as string).trim());
251
+ zipPath = expandPath((zipInput as string).trim());
244
252
  }
245
253
 
246
254
  await deployZip(zipPath, opts);
@@ -386,7 +394,7 @@ export async function deployCommand(
386
394
  placeholder: "./icon.png",
387
395
  validate: (val) => {
388
396
  if (!val || !val.trim()) return undefined;
389
- const resolved = resolve(val.trim());
397
+ const resolved = expandPath(val.trim());
390
398
  if (!existsSync(resolved)) return `File not found: ${val.trim()}`;
391
399
  if (!IMAGE_EXTS.has(extname(resolved).toLowerCase()))
392
400
  return "Image must be PNG or JPG";
@@ -394,7 +402,7 @@ export async function deployCommand(
394
402
  });
395
403
  handleCancel(imgInput);
396
404
  const trimmed = (imgInput as string).trim();
397
- if (trimmed) imagePath = resolve(trimmed);
405
+ if (trimmed) imagePath = expandPath(trimmed);
398
406
  } else if (selected !== "__skip__") {
399
407
  imagePath = selected as string;
400
408
  }
@@ -405,7 +413,7 @@ export async function deployCommand(
405
413
  placeholder: "./icon.png",
406
414
  validate: (val) => {
407
415
  if (!val || !val.trim()) return undefined;
408
- const resolved = resolve(val.trim());
416
+ const resolved = expandPath(val.trim());
409
417
  if (!existsSync(resolved)) return `File not found: ${val.trim()}`;
410
418
  if (!IMAGE_EXTS.has(extname(resolved).toLowerCase()))
411
419
  return "Image must be PNG or JPG";
@@ -413,7 +421,7 @@ export async function deployCommand(
413
421
  });
414
422
  handleCancel(imgInput);
415
423
  const trimmed = (imgInput as string).trim();
416
- if (trimmed) imagePath = resolve(trimmed);
424
+ if (trimmed) imagePath = expandPath(trimmed);
417
425
  }
418
426
  }
419
427
 
@@ -549,6 +557,7 @@ export async function deployCommand(
549
557
  return;
550
558
  }
551
559
 
560
+ if (await handleIfSessionExpired(err)) return;
552
561
  const msg = err instanceof Error ? err.message : String(err);
553
562
  if (msg.toLowerCase().includes("same name")) {
554
563
  error(`An ability named "${uniqueName}" already exists.`);
@@ -693,6 +702,7 @@ async function deployZip(
693
702
  p.outro("Deployed successfully! 🎉");
694
703
  } catch (err) {
695
704
  s.stop("Upload failed.");
705
+ if (await handleIfSessionExpired(err)) return;
696
706
  error(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
697
707
  process.exit(1);
698
708
  }
@@ -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
  }