@indigoai-us/hq-cli 5.345.180 → 5.345.181

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,9 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.181] — 2026-10-30
6
+ - Local personal and company bots now share one HQ Fleet runtime installation while keeping isolated native profiles and gateway services. Existing local Fleet bots migrate in place without losing profile state.
7
+
5
8
  ## [5.345.180] — 2026-10-30
6
9
  - New `hq channels add <channel> <member...>` and `hq channels remove <channel> <member>`. An owner can now add a personal bot (or a teammate) to a channel from the CLI by @handle, email, or prs_/agt_ uid, so everyone in the channel can @mention it. Before this there was no verb for it, and posting "@bot" into the channel was refused with "No channel member matches". The server's join-first refusal and the owner-or-company-admin rule for adding and removing members are explained in plain words. Feedback e165b286-b059-4d5c-bd44-ac344f00c821.
7
10
  - A local bot can now have a model login of its own, so it no longer has to use yours. `hq bot login <name> [--runtime claude|codex]` runs the runtime's own sign-in into `~/.hq/bots/<name>/auth/<runtime>/` (`CLAUDE_CONFIG_DIR` or `CODEX_HOME`). `hq bot logout <name>` signs that login out and deletes the folder. `hq bot set <name> --account <id|default>` points the bot at an existing `hq capacity` account instead. Every model turn uses the chosen login. When that login is missing or its account is gone, the turn does not fall back to your login: the bot replies in the DM saying so, keeps the message pending, and `hq bot status` shows the problem. `hq bot status` now has a `login:` line (default, own, or the account name) and never shows a token.
@@ -57,7 +57,7 @@ import { runtimeDisplayName, signInCommand } from "../lib/bot/runtime-sign-in.js
57
57
  import { classifyRuntimeFailure, runtimeFailureReason } from "../lib/bot/runtime-failure.js";
58
58
  import { PROBE_DEFAULT_TIMEOUT_MS, probeBotRuntime } from "../lib/bot/probe.js";
59
59
  import { BOT_LOGIN_RUNTIMES, botAuthEnv, botRuntimeAuth, capacityAccountHome, describeBotAuth, formatBotAuth, isBotLoginRuntime, restoreBotSessions, setAsideBotSessions, signInBotRuntime, signOutBotRuntime, withRuntimeAuth, } from "../lib/bot/runtime-auth.js";
60
- import { BotApi, BotApiError, BOT_RESTORE_DEFAULTS, buildBotLocalConfig, describeBotLocalConfig, orphanBotRunMessage, parseBotLocalConfig, removeOrphanBotDaemon, botDaemonStatus, botDir, botCredsPath, botLogPath, botsRoot, botTokenStateDir, botWorkerRelDir, buildBotDaemonPaths, createBotLogger, deleteBotCreds, installBotDaemon, repairBotDaemonIfStale, healBotDaemonNodeFromJob, introDmText, isBotMemoryMode, botLocalMemoryDir, botMemoryMode, resolveBotMemoryDir, validateBotIntro, validateBotDisplayName, validateBotKickoff, validateBotEffort, validateBotModel, effectiveBotEffort, BOT_EFFORT_LEVELS, DEFAULT_BOT_EFFORT, BOT_MEMORY_MODES, BOT_KINDS, isBotKind, effectiveBotKind, effectiveBotCompanies, normalizeBotCompanies, SETUP_BOT_WORKER_ID, isBotRuntimeId, localNameForRemoteBot, remoteBotRunnability, isPidAlive, listBotWorkerOptions, patchBotConfig, readBotConfig, readBotCredsIdentity, readBotStatus, runtimeCrashSinceStart, resolveBotWorker, runBot, runtimeFor, scaffoldBotMemory, scaffoldBotWorker, startBotDaemon, stopBotDaemon, uninstallBotDaemon, validateBotName, writeBotConfig, writeBotCreds, BOT_RUNTIMES, BOT_INTRO_MAX_CHARS, BOT_DISPLAY_NAME_MAX_CHARS, BOT_KICKOFF_MAX_CHARS, detectBotPlatform, withBotLock, localBotRole, remoteBotRole, remoteBotDisplayName, personFacingName, writeCloudDisplayName, setupSlugFor, ASSISTANT_ROLE, HQ_FLEET_RUNTIME_ENGINE_ID, authenticateFleetLocalBot, bootstrapFleetLocalBot, buildSystemPrompt, controlFleetLocalBot, ensureFleetPython, fleetLocalGatewayPid, fleetProviderForRuntime, importFleetLocalMachineCredential, fleetProviderNeedsSignIn, fleetDefaultCompanies, fleetOwnerCompanies, fleetRuntimeIsDefault, refreshFleetLocalInstructions, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, readPromptSources, removeFleetLocalBot, } from "../lib/bot/index.js";
60
+ import { BotApi, BotApiError, BOT_RESTORE_DEFAULTS, buildBotLocalConfig, describeBotLocalConfig, orphanBotRunMessage, parseBotLocalConfig, removeOrphanBotDaemon, botDaemonStatus, botDir, botCredsPath, botLogPath, botsRoot, botTokenStateDir, botWorkerRelDir, buildBotDaemonPaths, createBotLogger, deleteBotCreds, installBotDaemon, repairBotDaemonIfStale, healBotDaemonNodeFromJob, introDmText, isBotMemoryMode, botLocalMemoryDir, botMemoryMode, resolveBotMemoryDir, validateBotIntro, validateBotDisplayName, validateBotKickoff, validateBotEffort, validateBotModel, effectiveBotEffort, BOT_EFFORT_LEVELS, DEFAULT_BOT_EFFORT, BOT_MEMORY_MODES, BOT_KINDS, isBotKind, effectiveBotKind, effectiveBotCompanies, normalizeBotCompanies, SETUP_BOT_WORKER_ID, isBotRuntimeId, localNameForRemoteBot, remoteBotRunnability, isPidAlive, listBotWorkerOptions, patchBotConfig, readBotConfig, readBotCredsIdentity, readBotStatus, runtimeCrashSinceStart, resolveBotWorker, runBot, runtimeFor, scaffoldBotMemory, scaffoldBotWorker, startBotDaemon, stopBotDaemon, uninstallBotDaemon, validateBotName, writeBotConfig, writeBotCreds, BOT_RUNTIMES, BOT_INTRO_MAX_CHARS, BOT_DISPLAY_NAME_MAX_CHARS, BOT_KICKOFF_MAX_CHARS, detectBotPlatform, withBotLock, localBotRole, remoteBotRole, remoteBotDisplayName, personFacingName, writeCloudDisplayName, setupSlugFor, ASSISTANT_ROLE, HQ_FLEET_RUNTIME_ENGINE_ID, authenticateFleetLocalBot, bootstrapFleetLocalBot, buildSystemPrompt, controlFleetLocalBot, ensureFleetPython, fleetLocalGatewayPid, fleetLocalProfileNeedsAdoption, fleetProviderForRuntime, importFleetLocalMachineCredential, fleetProviderNeedsSignIn, fleetDefaultCompanies, fleetOwnerCompanies, fleetRuntimeIsDefault, refreshFleetLocalInstructions, fleetLocalPromotionRuntime, installPinnedFleetLocalDistribution, readPromptSources, removeFleetLocalBot, } from "../lib/bot/index.js";
61
61
  function collectRepeated(value, previous) {
62
62
  return [...previous, value];
63
63
  }
@@ -153,7 +153,7 @@ function fleetAuthCommand(name, config) {
153
153
  return `hq bot auth ${name} --provider ${config.runtime === "grok" ? "grok" : config.runtime === "codex" ? "chatgpt" : "claude"}`;
154
154
  }
155
155
  function runFleetRuntimeControl(name, config, action, json) {
156
- if ((action === "start" || action === "restart") && config.fleetAuthPending === true) {
156
+ if ((action === "install" || action === "start" || action === "restart") && config.fleetAuthPending === true) {
157
157
  const authCommand = fleetAuthCommand(name, config);
158
158
  if (json)
159
159
  console.log(JSON.stringify({ ok: false, name, running: false, state: "stopped", needsSignIn: true, authCommand }, null, 2));
@@ -161,10 +161,10 @@ function runFleetRuntimeControl(name, config, action, json) {
161
161
  throw Object.assign(new Error(`Sign this bot in first: ${authCommand}`), { expected: true });
162
162
  return;
163
163
  }
164
- if (action === "start" || action === "restart") {
164
+ if (action === "install" || action === "start" || action === "restart") {
165
165
  refreshFleetLocalInstructions(config.agentUid, fleetBotSystemPrompt(name, config));
166
166
  }
167
- const installService = config.fleetServiceInstallPending === true && (action === "start" || action === "restart");
167
+ const installService = action === "install" || (config.fleetServiceInstallPending === true && (action === "start" || action === "restart"));
168
168
  const result = controlFleetLocalBot(config.agentUid, installService ? "install" : action);
169
169
  if (installService && result.ok)
170
170
  patchBotConfig(botDir(name), { fleetServiceInstallPending: false, enabled: true });
@@ -434,10 +434,11 @@ async function startFleetRuntimeBot(name, dir, config, output, extra) {
434
434
  throw Object.assign(new Error(`Sign this bot in first: ${authCommand}`), { expected: true });
435
435
  return { running: false, how: "waiting for sign-in" };
436
436
  }
437
+ const adopted = await adoptFleetRuntimeProfile(name, dir, config);
437
438
  refreshFleetLocalInstructions(config.agentUid, fleetBotSystemPrompt(name, config));
438
439
  const before = readBotStatus(dir);
439
440
  const alreadyRunning = !!before && before.state === "running" && isPidAlive(before.pid);
440
- const installService = config.fleetServiceInstallPending === true;
441
+ const installService = adopted || config.fleetServiceInstallPending === true;
441
442
  const result = controlFleetLocalBot(config.agentUid, installService ? "install" : alreadyRunning ? "restart" : "start");
442
443
  if (installService && result.ok)
443
444
  patchBotConfig(dir, { fleetServiceInstallPending: false, enabled: true });
@@ -459,6 +460,29 @@ async function startFleetRuntimeBot(name, dir, config, output, extra) {
459
460
  console.log(chalk.yellow(`${name} did not report running within 45s`) + chalk.dim(` logs: ~/.hq-fleet/profiles/${config.agentUid}/logs`));
460
461
  return { running, how };
461
462
  }
463
+ async function adoptFleetRuntimeProfile(name, dir, config) {
464
+ if (!fleetLocalProfileNeedsAdoption(config.agentUid))
465
+ return false;
466
+ const installation = await installPinnedFleetLocalDistribution();
467
+ const python = await ensureFleetPython();
468
+ const companyScope = effectiveBotKind(config) === "company"
469
+ ? effectiveBotCompanies(config)
470
+ : await fleetOwnerCompanies();
471
+ await bootstrapFleetLocalBot({
472
+ agentUid: config.agentUid,
473
+ ownerUid: config.ownerUid,
474
+ hqRoot: config.hqRoot,
475
+ botDir: dir,
476
+ installation,
477
+ systemPrompt: fleetBotSystemPrompt(name, config),
478
+ provider: fleetProviderForRuntime(config.runtime, config.model),
479
+ ownerCompanies: companyScope,
480
+ }, { python, start: false });
481
+ const credentials = botCredsPath(dir);
482
+ if (fs.existsSync(credentials))
483
+ importFleetLocalMachineCredential(config.agentUid, credentials);
484
+ return true;
485
+ }
462
486
  /**
463
487
  * `output`: "json" | "text" print a result; "silent" prints nothing — used
464
488
  * when stop is a step inside rm/restart, whose own JSON must be the only
@@ -2073,7 +2097,8 @@ export function botExists(name) {
2073
2097
  export async function runBotRestart(nameArg, opts = {}) {
2074
2098
  const b = requireBot(nameArg);
2075
2099
  if (isFleetRuntimeBot(b.config)) {
2076
- runFleetRuntimeControl(b.name, b.config, "restart", opts.json === true || opts.output === "json");
2100
+ const adopted = await adoptFleetRuntimeProfile(b.name, b.dir, b.config);
2101
+ runFleetRuntimeControl(b.name, b.config, adopted ? "install" : "restart", opts.json === true || opts.output === "json");
2077
2102
  return;
2078
2103
  }
2079
2104
  const running = readBotStatus(b.dir);
@@ -2094,10 +2119,11 @@ async function runBotAuth(nameArg, opts) {
2094
2119
  if (opts.apiKey && !apiKey) {
2095
2120
  throw Object.assign(new Error("No API key was received on stdin."), { expected: true });
2096
2121
  }
2122
+ const adopted = await adoptFleetRuntimeProfile(b.name, b.dir, b.config);
2097
2123
  const result = authenticateFleetLocalBot({
2098
2124
  agentUid: b.config.agentUid,
2099
2125
  provider: opts.provider,
2100
- ...(b.config.fleetAuthPending === true ? { installService: true } : {}),
2126
+ ...(adopted || b.config.fleetAuthPending === true ? { installService: true } : {}),
2101
2127
  ...(opts.model ? { model: opts.model } : {}),
2102
2128
  ...(apiKey ? { apiKey } : {}),
2103
2129
  ...(opts.baseUrl ? { baseUrl: opts.baseUrl } : {}),
@@ -2529,8 +2555,9 @@ async function repairLocalBot(name, api, bot, daemon) {
2529
2555
  });
2530
2556
  createBotLogger(dir)("info", `re-issued machine credentials for ${name} (${bot.agentUid}) on ${localMachineId()}`);
2531
2557
  if (isFleetRuntimeBot(config)) {
2558
+ const adopted = await adoptFleetRuntimeProfile(name, dir, config);
2532
2559
  importFleetLocalMachineCredential(bot.agentUid, botCredsPath(dir));
2533
- const restarted = controlFleetLocalBot(bot.agentUid, "restart").ok === true;
2560
+ const restarted = controlFleetLocalBot(bot.agentUid, adopted ? "install" : "restart").ok === true;
2534
2561
  return restarted ? "new credentials, HQ Fleet Runtime restarted" : "new credentials; the HQ Fleet Runtime did not restart (hq bot start " + name + ")";
2535
2562
  }
2536
2563
  if (daemon && process.platform === "darwin") {
@@ -2782,7 +2809,8 @@ export function registerBotCommand(program) {
2782
2809
  .action((name, opts) => wrap(async () => {
2783
2810
  const b = requireBot(name);
2784
2811
  if (isFleetRuntimeBot(b.config)) {
2785
- runFleetRuntimeControl(b.name, b.config, "start", opts.json === true);
2812
+ const adopted = await adoptFleetRuntimeProfile(b.name, b.dir, b.config);
2813
+ runFleetRuntimeControl(b.name, b.config, adopted ? "install" : "start", opts.json === true);
2786
2814
  return;
2787
2815
  }
2788
2816
  await startBot(b.name, b.dir, b.config, opts.json ? "json" : "text");
@@ -99,6 +99,9 @@ export interface FleetLocalBootstrapDeps {
99
99
  start?: boolean;
100
100
  python?: FleetPython;
101
101
  }
102
+ type FleetLocalProfileDeps = Pick<FleetLocalBootstrapDeps, "fleetHome" | "platform"> & {
103
+ runtimeRoot?: string;
104
+ };
102
105
  export interface FleetLocalBootstrapResult {
103
106
  profile: string;
104
107
  runtime: string;
@@ -144,12 +147,13 @@ export declare const FLEET_LOCAL_RUNTIME_DOWNLOAD_TIMEOUT_MS: number;
144
147
  export declare function installPinnedFleetLocalDistribution(deps?: FleetLocalInstallDeps): Promise<FleetLocalInstallation>;
145
148
  /** The process id the bot's gateway recorded, when its file reads; the caller checks whether it is alive. */
146
149
  export declare function fleetLocalGatewayPid(agentUid: string, deps?: Pick<FleetLocalControlDeps, "fleetHome">): number | null;
150
+ export declare function fleetLocalProfileNeedsAdoption(agentUid: string, deps?: Pick<FleetLocalControlDeps, "fleetHome" | "platform">): boolean;
147
151
  export declare function controlFleetLocalBot(agentUid: string, action: FleetLocalServiceAction, deps?: FleetLocalControlDeps): Record<string, unknown>;
148
152
  export declare function exportFleetLocalMachineCredential(agentUid: string, deps?: FleetLocalControlDeps): BotMachineCreds;
149
153
  export declare function fleetLocalPromotionRuntime(agentUid: string, deps?: FleetLocalControlDeps): LocalBotPromotionRuntime;
150
154
  export declare function removeFleetLocalBot(agentUid: string, deps?: FleetLocalControlDeps): FleetLocalRemovalResult;
151
155
  export declare function authenticateFleetLocalBot(input: FleetLocalAuthInput, deps?: FleetLocalControlDeps): FleetLocalAuthResult;
152
- export declare function writeFleetLocalProfile(input: FleetLocalProfileInput, deps?: Pick<FleetLocalBootstrapDeps, "fleetHome" | "platform">): string;
156
+ export declare function writeFleetLocalProfile(input: FleetLocalProfileInput, deps?: FleetLocalProfileDeps): string;
153
157
  /** The runtime's inbox plugin sends the intro and runs the kickoff once, then stamps this file. */
154
158
  export declare function writeFleetLocalFirstStart(profileDir: string, firstStart: {
155
159
  intro?: string;
@@ -167,7 +171,7 @@ export declare function refreshFleetLocalInstructions(agentUid: string, systemPr
167
171
  /** Moves a fresh machine credential into the bot's secure store and deletes the file. */
168
172
  export declare function importFleetLocalMachineCredential(agentUid: string, machineCredentials: string, deps?: FleetLocalControlDeps): void;
169
173
  export declare function bootstrapFleetLocalBot(input: FleetLocalProfileInput & {
170
- machineCredentials: string;
174
+ machineCredentials?: string;
171
175
  }, deps?: FleetLocalBootstrapDeps): Promise<FleetLocalBootstrapResult>;
172
176
  export {};
173
177
  //# sourceMappingURL=fleet-local.d.ts.map
@@ -322,14 +322,83 @@ export async function installPinnedFleetLocalDistribution(deps = {}) {
322
322
  engine: engineMetadata(release),
323
323
  };
324
324
  }
325
+ function profileName(agentUid) {
326
+ return agentUid.toLowerCase();
327
+ }
325
328
  function profileRoot(agentUid, root) {
326
- return path.join(root, "profiles", agentUid);
329
+ const profiles = path.join(root, "profiles");
330
+ const canonicalName = profileName(agentUid);
331
+ const canonical = path.join(profiles, canonicalName);
332
+ if (fs.existsSync(canonical))
333
+ return canonical;
334
+ try {
335
+ const legacy = fs.readdirSync(profiles, { withFileTypes: true })
336
+ .find((entry) => entry.isDirectory() && entry.name.toLowerCase() === canonicalName);
337
+ return legacy ? path.join(profiles, legacy.name) : canonical;
338
+ }
339
+ catch {
340
+ return canonical;
341
+ }
342
+ }
343
+ function adoptLegacyProfileDirectory(agentUid, root) {
344
+ const profiles = path.join(root, "profiles");
345
+ const canonicalName = profileName(agentUid);
346
+ fs.mkdirSync(profiles, { recursive: true, mode: 0o700 });
347
+ const matches = fs.readdirSync(profiles, { withFileTypes: true })
348
+ .filter((entry) => entry.isDirectory() && entry.name.toLowerCase() === canonicalName)
349
+ .map((entry) => entry.name);
350
+ if (matches.length > 1) {
351
+ throw expectedError(`HQ Fleet has conflicting profile directories for ${agentUid}.`);
352
+ }
353
+ const existing = matches[0];
354
+ const canonical = path.join(profiles, canonicalName);
355
+ if (!existing || existing === canonicalName)
356
+ return canonical;
357
+ const source = path.join(profiles, existing);
358
+ const temporary = path.join(profiles, `.adopt-${canonicalName}-${process.pid}-${crypto.randomUUID()}`);
359
+ fs.renameSync(source, temporary);
360
+ try {
361
+ fs.renameSync(temporary, canonical);
362
+ }
363
+ catch (error) {
364
+ fs.renameSync(temporary, source);
365
+ throw error;
366
+ }
367
+ return canonical;
327
368
  }
328
369
  function venvExecutable(runtimeDir, name, platform) {
329
370
  return platform === "win32"
330
371
  ? path.join(runtimeDir, "Scripts", `${name}.exe`)
331
372
  : path.join(runtimeDir, "bin", name);
332
373
  }
374
+ function sharedRuntimeFromMarker(profileDir, root) {
375
+ try {
376
+ const marker = JSON.parse(fs.readFileSync(path.join(profileDir, "bootstrap.json"), "utf8"));
377
+ if (marker.schemaVersion !== 2 || typeof marker.runtimeId !== "string" || !/^[a-f0-9]{64}$/.test(marker.runtimeId))
378
+ return null;
379
+ const sharedRoot = path.join(root, "runtimes", marker.runtimeId);
380
+ return {
381
+ id: marker.runtimeId,
382
+ root: sharedRoot,
383
+ runtimeDir: path.join(sharedRoot, "runtime"),
384
+ sourcesDir: path.join(sharedRoot, "sources"),
385
+ };
386
+ }
387
+ catch {
388
+ return null;
389
+ }
390
+ }
391
+ function hasEmbeddedRuntime(profileDir, platform) {
392
+ const runtimeDir = path.join(profileDir, "runtime");
393
+ try {
394
+ if (fs.lstatSync(runtimeDir).isSymbolicLink())
395
+ return true;
396
+ }
397
+ catch {
398
+ return false;
399
+ }
400
+ return ["hq-fleet", "hq-fleet-local"].some((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)));
401
+ }
333
402
  function fleetLocalExecutables(agentUid, deps) {
334
403
  if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid)) {
335
404
  throw expectedError(`Cannot use an HQ Fleet profile for invalid bot id ${agentUid}.`);
@@ -337,14 +406,16 @@ function fleetLocalExecutables(agentUid, deps) {
337
406
  const root = deps.fleetHome ?? fleetHome();
338
407
  const platform = deps.platform ?? process.platform;
339
408
  const profileDir = profileRoot(agentUid, root);
340
- const runtimeDir = path.join(profileDir, "runtime");
409
+ const shared = sharedRuntimeFromMarker(profileDir, root);
410
+ const runtimeDir = shared?.runtimeDir ?? path.join(profileDir, "runtime");
411
+ const sourcesDir = shared?.sourcesDir ?? path.join(profileDir, "sources");
341
412
  const profile = path.join(profileDir, "profile.json");
342
413
  const runtime = venvExecutable(runtimeDir, "hq-fleet", platform);
343
414
  const helper = venvExecutable(runtimeDir, "hq-fleet-local", platform);
344
415
  if (![profile, runtime, helper].every((item) => fs.existsSync(item))) {
345
416
  throw expectedError(`HQ Fleet Local is not fully installed for ${agentUid}.`);
346
417
  }
347
- return { profileDir, profile, runtime, helper };
418
+ return { profileDir, profile, runtime, helper, runtimeDir, sourcesDir };
348
419
  }
349
420
  function runJson(run, executable, args, options, label) {
350
421
  let output;
@@ -377,10 +448,39 @@ export function fleetLocalGatewayPid(agentUid, deps = {}) {
377
448
  return null;
378
449
  }
379
450
  }
451
+ export function fleetLocalProfileNeedsAdoption(agentUid, deps = {}) {
452
+ if (!/^agt_[A-Za-z0-9_-]+$/.test(agentUid))
453
+ return true;
454
+ const root = deps.fleetHome ?? fleetHome();
455
+ const profiles = path.join(root, "profiles");
456
+ const canonicalName = profileName(agentUid);
457
+ let entries;
458
+ try {
459
+ entries = fs.readdirSync(profiles);
460
+ }
461
+ catch {
462
+ return true;
463
+ }
464
+ if (!entries.includes(canonicalName))
465
+ return true;
466
+ const profileDir = path.join(profiles, canonicalName);
467
+ const shared = sharedRuntimeFromMarker(profileDir, root);
468
+ if (!shared)
469
+ return true;
470
+ const platform = deps.platform ?? process.platform;
471
+ if (!["hq-fleet", "hq-fleet-local", "python"].every((name) => fs.existsSync(venvExecutable(shared.runtimeDir, name, platform))))
472
+ return true;
473
+ if (hasEmbeddedRuntime(profileDir, platform) || fs.existsSync(path.join(profileDir, "sources")))
474
+ return true;
475
+ return false;
476
+ }
380
477
  export function controlFleetLocalBot(agentUid, action, deps = {}) {
381
478
  const run = deps.run ?? execFileSync;
382
479
  const files = fleetLocalExecutables(agentUid, deps);
383
- return runJson(run, files.helper, ["service", action, "--profile", files.profileDir, "--runtime", files.runtime], { stdio: ["ignore", "pipe", "pipe"] }, `service ${action}`);
480
+ const result = runJson(run, files.helper, ["service", action, "--profile", files.profileDir, "--runtime", files.runtime], { stdio: ["ignore", "pipe", "pipe"] }, `service ${action}`);
481
+ if (action === "install" && result.ok === true)
482
+ removeEmbeddedRuntime(files.profileDir, deps.platform ?? process.platform);
483
+ return result;
384
484
  }
385
485
  const providerProfiles = {
386
486
  chatgpt: {
@@ -511,14 +611,15 @@ export function removeFleetLocalBot(agentUid, deps = {}) {
511
611
  const profileDir = profileRoot(agentUid, deps.fleetHome ?? fleetHome());
512
612
  if (!fs.existsSync(profileDir))
513
613
  return { removed: false, credentialsRemoved: 0 };
614
+ if (!fs.existsSync(path.join(profileDir, "profile.json"))) {
615
+ fs.rmSync(profileDir, { recursive: true, force: true });
616
+ return { removed: true, credentialsRemoved: 0 };
617
+ }
514
618
  const platform = deps.platform ?? process.platform;
515
- const runtimeDir = path.join(profileDir, "runtime");
516
- const partial = ![
517
- path.join(profileDir, "profile.json"),
518
- venvExecutable(runtimeDir, "hq-fleet", platform),
519
- venvExecutable(runtimeDir, "hq-fleet-local", platform),
520
- ].every((item) => fs.existsSync(item));
521
- if (partial) {
619
+ const legacyRuntime = path.join(profileDir, "runtime");
620
+ const shared = sharedRuntimeFromMarker(profileDir, deps.fleetHome ?? fleetHome());
621
+ const installedRuntime = shared?.runtimeDir ?? legacyRuntime;
622
+ if (!["hq-fleet", "hq-fleet-local"].every((name) => fs.existsSync(venvExecutable(installedRuntime, name, platform)))) {
522
623
  fs.rmSync(profileDir, { recursive: true, force: true });
523
624
  return { removed: true, credentialsRemoved: 0 };
524
625
  }
@@ -634,7 +735,9 @@ export function writeFleetLocalProfile(input, deps = {}) {
634
735
  }
635
736
  const root = deps.fleetHome ?? fleetHome();
636
737
  const dir = profileRoot(input.agentUid, root);
637
- const runtimeDir = path.join(dir, "runtime");
738
+ const markerRuntime = sharedRuntimeFromMarker(dir, root);
739
+ const runtimeRoot = deps.runtimeRoot ?? markerRuntime?.root ?? dir;
740
+ const runtimeDir = path.join(runtimeRoot, "runtime");
638
741
  const destination = path.join(dir, "profile.json");
639
742
  const temporary = path.join(dir, `.profile-${process.pid}-${crypto.randomUUID()}`);
640
743
  const profile = {
@@ -647,7 +750,7 @@ export function writeFleetLocalProfile(input, deps = {}) {
647
750
  profileDir: path.resolve(dir),
648
751
  runtime: { commit: input.installation.release.runtimeCommit, path: path.resolve(runtimeDir) },
649
752
  paths: {
650
- pluginSource: path.resolve(path.join(dir, "sources", "distribution", "src", "hq_fleet_local", "plugins", "hqdm")),
753
+ pluginSource: path.resolve(path.join(runtimeRoot, "sources", "distribution", "src", "hq_fleet_local", "plugins", "hqdm")),
651
754
  statusFile: path.resolve(path.join(input.botDir, "status.json")),
652
755
  logsDir: path.resolve(path.join(input.botDir, "logs")),
653
756
  },
@@ -706,9 +809,13 @@ export function scanFleetLocalInstructions(agentUid, text, deps = {}) {
706
809
  throw expectedError(`Cannot use an HQ Fleet profile for invalid bot id ${agentUid}.`);
707
810
  }
708
811
  const run = deps.run ?? execFileSync;
709
- const profileDir = profileRoot(agentUid, deps.fleetHome ?? fleetHome());
710
- const python = venvExecutable(path.join(profileDir, "runtime"), "python", deps.platform ?? process.platform);
711
- const source = path.join(profileDir, "sources", "runtime");
812
+ const root = deps.fleetHome ?? fleetHome();
813
+ const profileDir = profileRoot(agentUid, root);
814
+ const shared = sharedRuntimeFromMarker(profileDir, root);
815
+ const runtimeDir = shared?.runtimeDir ?? path.join(profileDir, "runtime");
816
+ const sourcesDir = shared?.sourcesDir ?? path.join(profileDir, "sources");
817
+ const python = venvExecutable(runtimeDir, "python", deps.platform ?? process.platform);
818
+ const source = path.join(sourcesDir, "runtime");
712
819
  if (!fs.existsSync(python) || !fs.existsSync(source)) {
713
820
  throw expectedError("HQ Fleet Local is not installed for this bot, so its instructions could not be checked.");
714
821
  }
@@ -774,6 +881,12 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
774
881
  extras: ["mcp", "anthropic"],
775
882
  })));
776
883
  const shared = path.join(root, "runtimes", identity);
884
+ const descriptor = () => ({
885
+ id: identity,
886
+ root: shared,
887
+ runtimeDir: path.join(shared, "runtime"),
888
+ sourcesDir: path.join(shared, "sources"),
889
+ });
777
890
  return withBotLock(identity, async () => {
778
891
  const marker = path.join(shared, "ready.json");
779
892
  const sources = path.join(shared, "sources");
@@ -785,7 +898,7 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
785
898
  if (!executables.every((name) => fs.existsSync(venvExecutable(runtimeDir, name, platform)))) {
786
899
  throw expectedError("The shared HQ Fleet Runtime is incomplete. Reinstall it before creating another bot.");
787
900
  }
788
- return shared;
901
+ return descriptor();
789
902
  }
790
903
  fs.rmSync(shared, { recursive: true, force: true });
791
904
  fs.mkdirSync(distributionSource, { recursive: true, mode: 0o700 });
@@ -827,54 +940,59 @@ async function installSharedFleetRuntime(installation, root, python, platform, r
827
940
  throw expectedError("HQ Fleet Local installed without its runtime executables.");
828
941
  }
829
942
  fs.writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, engine: installation.engine })}\n`, { mode: 0o600 });
830
- return shared;
943
+ return descriptor();
831
944
  }, { root: path.join(root, "runtimes"), command: "HQ Fleet Runtime installation", waitMs: 20 * 60_000, staleMs: 2 * 60 * 60_000 });
832
945
  }
946
+ function writeNativeProfileMarker(profileDir, shared, engine) {
947
+ const marker = path.join(profileDir, "bootstrap.json");
948
+ const temporary = `${marker}.${process.pid}.${crypto.randomUUID()}`;
949
+ fs.writeFileSync(temporary, `${JSON.stringify({ schemaVersion: 2, runtimeId: shared.id, engine }, null, 2)}\n`, { mode: 0o600 });
950
+ fs.renameSync(temporary, marker);
951
+ }
952
+ function removeEmbeddedRuntime(profileDir, platform = process.platform) {
953
+ fs.rmSync(path.join(profileDir, "sources"), { recursive: true, force: true });
954
+ if (hasEmbeddedRuntime(profileDir, platform))
955
+ fs.rmSync(path.join(profileDir, "runtime"), { recursive: true, force: true });
956
+ }
833
957
  export async function bootstrapFleetLocalBot(input, deps = {}) {
834
958
  const run = deps.run ?? execFileSync;
835
959
  const platform = deps.platform ?? process.platform;
836
960
  const root = deps.fleetHome ?? fleetHome();
837
961
  const python = deps.python ?? await ensureFleetPython({ fleetHome: root, platform, run });
838
- const profileDir = profileRoot(input.agentUid, root);
839
- const runtimeDir = path.join(profileDir, "runtime");
840
- const marker = path.join(profileDir, "bootstrap.json");
962
+ const profileDir = adoptLegacyProfileDirectory(input.agentUid, root);
841
963
  fs.mkdirSync(profileDir, { recursive: true, mode: 0o700 });
842
- if (!fs.existsSync(marker)) {
843
- const shared = await installSharedFleetRuntime(input.installation, root, python, platform, run);
844
- for (const name of ["sources", "runtime"]) {
845
- const target = path.join(profileDir, name);
846
- fs.rmSync(target, { recursive: true, force: true });
847
- fs.symlinkSync(path.join(shared, name), target, platform === "win32" ? "junction" : "dir");
848
- }
849
- fs.writeFileSync(marker, `${JSON.stringify({ schemaVersion: 1, engine: input.installation.engine }, null, 2)}\n`, { mode: 0o600 });
850
- }
851
- const { systemPrompt, ...profileInput } = input;
964
+ const shared = await installSharedFleetRuntime(input.installation, root, python, platform, run);
965
+ writeNativeProfileMarker(profileDir, shared, input.installation.engine);
966
+ const { systemPrompt, machineCredentials, ...profileInput } = input;
852
967
  if (systemPrompt !== undefined)
853
968
  refreshFleetLocalInstructions(input.agentUid, systemPrompt, { fleetHome: root, platform, run });
854
- const profile = writeFleetLocalProfile(profileInput, { fleetHome: root, platform });
855
- const helper = venvExecutable(runtimeDir, "hq-fleet-local", platform);
856
- const runtime = venvExecutable(runtimeDir, "hq-fleet", platform);
969
+ const profile = writeFleetLocalProfile(profileInput, { fleetHome: root, platform, runtimeRoot: shared.root });
970
+ const helper = venvExecutable(shared.runtimeDir, "hq-fleet-local", platform);
971
+ const runtime = venvExecutable(shared.runtimeDir, "hq-fleet", platform);
857
972
  if (!fs.existsSync(helper) || !fs.existsSync(runtime)) {
858
973
  throw expectedError("HQ Fleet Local installed without its runtime executables.");
859
974
  }
860
975
  checkedRun(run, helper, ["render-profile", "--spec", profile, "--mode", "create"], { stdio: "ignore" }, "profile rendering");
861
- const machineValue = fs.readFileSync(input.machineCredentials, "utf8").trim();
862
- if (!machineValue) {
863
- throw expectedError(`HQ Fleet Local machine credential file is empty: ${input.machineCredentials}`);
976
+ if (machineCredentials) {
977
+ const machineValue = fs.readFileSync(machineCredentials, "utf8").trim();
978
+ if (!machineValue) {
979
+ throw expectedError(`HQ Fleet Local machine credential file is empty: ${machineCredentials}`);
980
+ }
981
+ checkedRun(run, helper, [
982
+ "machine-credential",
983
+ "import",
984
+ "--profile",
985
+ profileDir,
986
+ "--bot-id",
987
+ input.agentUid,
988
+ ], { input: machineValue, stdio: ["pipe", "ignore", "pipe"] }, "machine credential import");
989
+ fs.rmSync(machineCredentials, { force: true });
864
990
  }
865
- checkedRun(run, helper, [
866
- "machine-credential",
867
- "import",
868
- "--profile",
869
- profileDir,
870
- "--bot-id",
871
- input.agentUid,
872
- ], { input: machineValue, stdio: ["pipe", "ignore", "pipe"] }, "machine credential import");
873
- fs.rmSync(input.machineCredentials, { force: true });
874
991
  checkedRun(run, helper, ["doctor-profile", "--profile", profileDir, "--runtime", runtime], { stdio: "ignore" }, "profile validation");
875
992
  const start = deps.start !== false;
876
993
  if (start) {
877
994
  checkedRun(run, helper, ["service", "install", "--profile", profileDir, "--runtime", runtime], { stdio: ["ignore", 2, 2] }, "service installation");
995
+ removeEmbeddedRuntime(profileDir, platform);
878
996
  }
879
997
  return { profile, runtime, helper, running: start };
880
998
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.180",
3
+ "version": "5.345.181",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -114,11 +114,11 @@
114
114
  "*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
115
115
  },
116
116
  "optionalDependencies": {
117
- "@indigoai-us/hq-hook-win32-x64": "5.345.180",
118
- "@indigoai-us/hq-hook-win32-arm64": "5.345.180",
119
- "@indigoai-us/hq-hook-darwin-x64": "5.345.180",
120
- "@indigoai-us/hq-hook-darwin-arm64": "5.345.180",
121
- "@indigoai-us/hq-hook-linux-x64": "5.345.180",
122
- "@indigoai-us/hq-hook-linux-arm64": "5.345.180"
117
+ "@indigoai-us/hq-hook-win32-x64": "5.345.181",
118
+ "@indigoai-us/hq-hook-win32-arm64": "5.345.181",
119
+ "@indigoai-us/hq-hook-darwin-x64": "5.345.181",
120
+ "@indigoai-us/hq-hook-darwin-arm64": "5.345.181",
121
+ "@indigoai-us/hq-hook-linux-x64": "5.345.181",
122
+ "@indigoai-us/hq-hook-linux-arm64": "5.345.181"
123
123
  }
124
124
  }