@indigoai-us/hq-cli 5.174.0 → 5.176.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.176.0] — 2026-09-24
6
+
7
+ ### Fixed
8
+
9
+ - `hq bot create setup` restores the account's existing setup bot when the local copy is missing.
10
+
11
+ ## [5.175.0] — 2026-09-24
12
+
13
+ ### Fixed
14
+
15
+ - `hq agent kit` now shows missing external credentials at a home-relative path and reports the missing enrollment as an actionable precondition.
16
+ - Missing external agent credentials show an actionable error without creating a Sentry fault.
17
+
5
18
  ## [5.174.0] — 2026-09-24
6
19
 
7
20
  ### Fixed
@@ -37,6 +37,8 @@ import { buildWakeNotice, clearWakeConfig, currentWakeState, DEFAULT_WAKE_RETRY_
37
37
  import { runSyncLoop } from "../lib/agent-kit/run/sync.js";
38
38
  import { resolveHqBinary, resolveNodeBinary, } from "../lib/mesh/live/daemon/install.js";
39
39
  import { installServices, servicesStatus, uninstallServices, } from "../lib/service-manager/index.js";
40
+ import { markExpectedUserError } from "../utils/expected-cli-error.js";
41
+ import { scrubHomeDirectoryPrefix } from "../utils/install-tree-torn.js";
40
42
  export class KitError extends Error {
41
43
  constructor(message) {
42
44
  super(message);
@@ -47,8 +49,9 @@ export class KitError extends Error {
47
49
  export function requireExternalCreds(paths) {
48
50
  const creds = readExternalMachineCreds(paths);
49
51
  if (!creds) {
50
- throw new KitError(`No external agent credentials at ${paths.machineCredsPath}. Run ` +
51
- `\`hq agent enroll <code>\` first (an admin creates the code in the HQ console).`);
52
+ const credentialsPath = scrubHomeDirectoryPrefix(paths.machineCredsPath, paths.home);
53
+ throw markExpectedUserError(new KitError(`No external agent credentials at ${credentialsPath}. Run ` +
54
+ `\`hq agent enroll <code>\` first (an admin creates the code in the HQ console).`));
52
55
  }
53
56
  return creds;
54
57
  }
@@ -209,6 +209,7 @@ export declare function configFromLocalConfig(input: {
209
209
  runtime?: BotRuntimeId;
210
210
  model?: string;
211
211
  };
212
+ preserveFirstStart?: boolean;
212
213
  now?: () => Date;
213
214
  }): {
214
215
  config: BotConfig;
@@ -481,9 +481,6 @@ export async function runBotCreate(nameArg, opts) {
481
481
  const kickoff = opts.kickoff !== undefined ? validateBotKickoff(opts.kickoff) : undefined;
482
482
  const model = opts.model !== undefined ? validateBotModel(opts.model) : undefined;
483
483
  const effort = opts.effort !== undefined ? validateBotEffort(runtime, opts.effort) : undefined;
484
- if (name === RESERVED_SETUP_BOT_NAME && opts.worker?.trim() !== RESERVED_SETUP_BOT_NAME) {
485
- throw Object.assign(new Error(`The name ${RESERVED_SETUP_BOT_NAME} is reserved for HQ's setup bot; create it with --worker ${RESERVED_SETUP_BOT_NAME}.`), { expected: true });
486
- }
487
484
  const dir = botDir(name);
488
485
  if (readBotConfig(dir)) {
489
486
  throw Object.assign(new Error(`Bot "${name}" already exists (${dir}). Use hq bot start ${name} or hq bot rm ${name}.`), {
@@ -495,11 +492,84 @@ export async function runBotCreate(nameArg, opts) {
495
492
  // cost an identity).
496
493
  const worker = opts.worker?.trim() ? resolveBotWorker(hqRoot, opts.worker) : null;
497
494
  const { kind, companies } = resolveCreateKind(opts, worker);
495
+ const isSetupBot = name === RESERVED_SETUP_BOT_NAME && kind === "personal" && worker?.id === SETUP_BOT_WORKER_ID;
496
+ if (name === RESERVED_SETUP_BOT_NAME && kind === "personal" && !isSetupBot) {
497
+ throw Object.assign(new Error(`The name ${RESERVED_SETUP_BOT_NAME} is reserved for HQ's setup bot; create it with --worker ${RESERVED_SETUP_BOT_NAME}.`), { expected: true });
498
+ }
498
499
  const token = await ownerToken();
499
500
  const owner = await resolveCallerPersonUid(token);
500
501
  if (!owner)
501
502
  throw Object.assign(new Error("Your HQ login has no person entity yet; open the HQ desktop app once, then retry."), { expected: true });
502
503
  const api = new BotApi({ token: async () => token });
504
+ const adoptOwnedSetupBot = async (allowUnsupportedListing) => {
505
+ let mine;
506
+ try {
507
+ mine = await api.listMine({ local: true });
508
+ }
509
+ catch (err) {
510
+ const failure = remoteBotsUnavailable(err);
511
+ if (allowUnsupportedListing && failure.reason === "server-unsupported")
512
+ return false;
513
+ if (opts.json)
514
+ console.log(JSON.stringify({ ok: false, ...failure }, null, 2));
515
+ throw Object.assign(new Error(failure.message), { expected: true });
516
+ }
517
+ const matches = matchRemoteBots(mine, name);
518
+ if (matches.length > 1) {
519
+ throw Object.assign(new Error(`You own more than one bot called "${name}": ${matches.map((bot) => bot.agentUid).join(", ")}. Name the one you mean by its id.`), { expected: true });
520
+ }
521
+ if (matches.length === 0)
522
+ return false;
523
+ const remote = matches[0];
524
+ if (!isReservedSetupBotRecord(remote, owner))
525
+ return false;
526
+ const { result, config } = await adoptRemoteBot({
527
+ bot: remote,
528
+ name,
529
+ api,
530
+ hqRoot,
531
+ overrides: { ...(model ? { model } : {}) },
532
+ daemon: opts.daemon !== false,
533
+ preserveFirstStart: true,
534
+ });
535
+ const recovered = {
536
+ agentUid: result.agentUid,
537
+ kind: effectiveBotKind(config),
538
+ companies: effectiveBotCompanies(config),
539
+ runtime: config.runtime,
540
+ dir: result.dir,
541
+ worker: config.workerSource === "worker" ? path.join(hqRoot, config.workerDir) : result.worker,
542
+ workerId: config.workerId ?? null,
543
+ companySlug: config.companySlug ?? null,
544
+ memory: botMemoryMode(config),
545
+ memoryDir: result.memoryDir,
546
+ intro: config.intro ?? null,
547
+ kickoff: config.kickoff ?? null,
548
+ model: config.model ?? null,
549
+ effort: config.effort ?? DEFAULT_BOT_EFFORT,
550
+ };
551
+ if (!opts.json) {
552
+ console.log(chalk.green(`Created bot ${chalk.bold(name)}`) + chalk.dim(` (${result.agentUid}, ${result.runtime})`));
553
+ console.log(chalk.dim(` kind: ${describeKind(recovered.kind, recovered.companies)}`));
554
+ console.log(chalk.dim(` worker: ${result.worker}`));
555
+ console.log(chalk.dim(` memory: ${result.memoryDir} (${result.memory})`));
556
+ console.log(chalk.dim(` state: ${result.dir}`));
557
+ if (result.daemon)
558
+ console.log(chalk.dim(` ${result.daemon}`));
559
+ }
560
+ if (opts.start !== false) {
561
+ await startBot(name, result.dir, config, opts.json ? "json" : "text", { ...recovered, daemon: result.daemon });
562
+ return true;
563
+ }
564
+ if (opts.json)
565
+ console.log(JSON.stringify({ ok: true, name, ...recovered, daemon: result.daemon, running: false }, null, 2));
566
+ return true;
567
+ };
568
+ // HQ adds the owner suffix to the setup bot slug. If a create reached the
569
+ // server but its response was lost, recover the caller-owned bot instead
570
+ // of sending another create that the entity store will reject.
571
+ if (isSetupBot && (await adoptOwnedSetupBot(true)))
572
+ return;
503
573
  // A company bot's companies are resolved first (a typo must not cost an
504
574
  // identity) and sent with the create: the server makes the bot a member of
505
575
  // each and refuses the whole create if the owner cannot add agents there.
@@ -549,6 +619,12 @@ export async function runBotCreate(nameArg, opts) {
549
619
  });
550
620
  }
551
621
  if (err instanceof BotApiError && err.status === 409) {
622
+ if (isSetupBot) {
623
+ if (await adoptOwnedSetupBot(false))
624
+ return;
625
+ throw Object.assign(new Error(`HQ could not create the reserved ${name} bot. If you created it on another computer, bring it back with: ` +
626
+ `hq bot adopt ${name}. Otherwise, choose a different name or ask your HQ admin.`), { expected: true });
627
+ }
552
628
  // The cloud already has this bot; it is simply not set up on this
553
629
  // computer (a reinstall, or a second Mac). Re-creating it is the wrong
554
630
  // move — adopting brings the existing one back, memory and all.
@@ -1223,6 +1299,18 @@ export function matchRemoteBots(bots, wanted) {
1223
1299
  (b.name ?? "").trim().toLowerCase() === needle ||
1224
1300
  localNameForRemoteBot(b) === needle);
1225
1301
  }
1302
+ /** A name match alone does not prove that a remote record is HQ's setup bot. */
1303
+ function isReservedSetupBotRecord(bot, ownerUid) {
1304
+ const owner = ownerUid.trim();
1305
+ const local = parseBotLocalConfig(bot.localConfig);
1306
+ return (!!owner &&
1307
+ bot.ownerUid === owner &&
1308
+ bot.slug === `${RESERVED_SETUP_BOT_NAME}-${owner.slice(-8).toLowerCase()}` &&
1309
+ bot.botKind === "personal" &&
1310
+ bot.computeMode === "local" &&
1311
+ local?.workerSource === "worker" &&
1312
+ local.workerId === SETUP_BOT_WORKER_ID);
1313
+ }
1226
1314
  export function resolveRemoteBot(bots, wanted) {
1227
1315
  const hits = matchRemoteBots(bots, wanted);
1228
1316
  if (hits.length === 1)
@@ -1392,8 +1480,10 @@ export function configFromLocalConfig(input) {
1392
1480
  enabled: true,
1393
1481
  // A restore is not a first start: the bot's one-off kickoff task already
1394
1482
  // ran on the machine it was created on, and `runs once` has to keep
1395
- // meaning once. `hq bot intro <name>` has it say hello again.
1396
- introSentAt: (input.now ?? (() => new Date()))().toISOString(),
1483
+ // meaning once. Recovery after a create response was lost is different:
1484
+ // this machine has not started the bot yet, so its first intro/kickoff
1485
+ // must still run.
1486
+ ...(input.preserveFirstStart ? {} : { introSentAt: (input.now ?? (() => new Date()))().toISOString() }),
1397
1487
  };
1398
1488
  return { config, notes };
1399
1489
  }
@@ -1440,6 +1530,7 @@ async function adoptRemoteBot(input) {
1440
1530
  dir,
1441
1531
  local,
1442
1532
  ...(input.overrides ? { overrides: input.overrides } : {}),
1533
+ ...(input.preserveFirstStart ? { preserveFirstStart: true } : {}),
1443
1534
  });
1444
1535
  writeBotConfig(dir, config);
1445
1536
  const scaffoldOpts = { memoryDir: config.memoryDir };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.174.0",
3
+ "version": "5.176.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {