@indigoai-us/hq-cli 5.113.1 → 5.115.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,63 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.115.0] — 2026-09-15
6
+
7
+ ### Added
8
+
9
+ - Local bots now come in two kinds. A **personal bot** acts as you: everything
10
+ it does — in your direct messages, in rooms, and on its first task — runs
11
+ under your own account, so it reaches every company and file you can and
12
+ can create companies, bots, invites and shares for you. A **company bot**
13
+ acts as itself, like a cloud agent: it is a member of one or more companies
14
+ and reaches only those companies' files, memory and secrets; it never acts
15
+ as you and says so if asked to create something for you, suggesting you ask
16
+ a personal bot instead. `hq bot create <name> --kind personal|company
17
+ --company <slug>` (repeat `--company` for several) picks the kind; a
18
+ company bot is invited into each company as you create it, and the command
19
+ tells you exactly which invite did not go through. `hq bot companies
20
+ <name> --add/--remove <slug>` changes a company bot's memberships later.
21
+ `hq bot list --json` and `hq bot status` show each bot's kind and
22
+ companies, and every bot's instructions say which kind it is. Bots created
23
+ before this release keep working, as personal bots (including one created
24
+ from a company worker, and the setup bot). Removing a company bot with
25
+ `hq bot rm` also takes it out of its companies.
26
+ - A personal bot now runs `hq` commands as you in rooms too, not only in your
27
+ direct messages (before, its room turns used the bot's own identity).
28
+
29
+ ### Fixed
30
+
31
+ - When a local bot's coding tool (Claude Code, Codex or Grok) has lost its
32
+ sign-in, the bot no longer answers every message with "Failed to
33
+ authenticate". It says once that the tool needs you to sign in again, keeps
34
+ your message, and answers it on its own as soon as the sign-in works (right
35
+ away after `hq bot restart`). A setup bot's first step waits the same way.
36
+ `hq bot list --json` reports `runtimeSignIn` while this is the case.
37
+ - In your direct messages, a local bot now does what you ask under your own
38
+ account: creating a company, a bot, an invite or a share works directly,
39
+ instead of the bot explaining that the cloud refuses to let it act in your
40
+ name and asking you to run a command in a terminal. In rooms, bots still act
41
+ as themselves. A company you create this way now also adds that bot as a
42
+ member, so it can keep helping inside the new company.
43
+
44
+ ## [5.114.0] — 2026-09-15
45
+
46
+ ### Changed
47
+
48
+ - Local bots now work on two conversations at once. A question in one
49
+ channel no longer waits for the bot to finish answering in another; messages
50
+ in the same conversation (your DMs with the bot, or one channel) are still
51
+ answered in order. A restart or stop during two turns reports each one that
52
+ did not finish.
53
+
54
+ ### Fixed
55
+
56
+ - The log bundle attached to `hq feedback` reports is now a zip instead of a
57
+ gzip. Slack refuses a gzip file from the HQ app at any size, so for several
58
+ weeks every report's logs were uploaded and then silently dropped before
59
+ anyone could read them. Nothing about what the bundle contains or how it is
60
+ redacted has changed.
61
+
5
62
  ## [5.113.1] — 2026-09-15
6
63
 
7
64
  ### Fixed
@@ -6129,6 +6129,12 @@ export declare const COMMAND_CATALOG: readonly [{
6129
6129
  readonly variadic: false;
6130
6130
  }];
6131
6131
  readonly options: readonly [{
6132
+ readonly flags: "--kind <kind>";
6133
+ readonly description: "personal (acts as you, everywhere you are) or company (acts as itself, as a member of its companies). Default: personal, or company when created from a company worker";
6134
+ }, {
6135
+ readonly flags: "--company <slug>";
6136
+ readonly description: "Company bots: a company the bot is a member of (repeat for several); the owner invites it on creation";
6137
+ }, {
6132
6138
  readonly flags: "--runtime <runtime>";
6133
6139
  readonly description: "Model CLI to run the bot with: claude|codex|grok";
6134
6140
  }, {
@@ -6163,6 +6169,28 @@ export declare const COMMAND_CATALOG: readonly [{
6163
6169
  readonly description: "Print machine-readable JSON";
6164
6170
  }];
6165
6171
  readonly subcommands: readonly [];
6172
+ }, {
6173
+ readonly name: "companies";
6174
+ readonly description: "Show or change which companies a company bot is a member of (you invite or remove it; a personal bot has no list of its own)";
6175
+ readonly aliases: readonly [];
6176
+ readonly hidden: false;
6177
+ readonly usage: "[options] <name>";
6178
+ readonly arguments: readonly [{
6179
+ readonly name: "name";
6180
+ readonly required: true;
6181
+ readonly variadic: false;
6182
+ }];
6183
+ readonly options: readonly [{
6184
+ readonly flags: "--add <slug>";
6185
+ readonly description: "Add the bot to this company (repeatable)";
6186
+ }, {
6187
+ readonly flags: "--remove <slug>";
6188
+ readonly description: "Remove the bot from this company (repeatable)";
6189
+ }, {
6190
+ readonly flags: "--json";
6191
+ readonly description: "Print machine-readable JSON";
6192
+ }];
6193
+ readonly subcommands: readonly [];
6166
6194
  }, {
6167
6195
  readonly name: "workers";
6168
6196
  readonly description: "List the workers (templates) a bot can be created from (hq bot create <name> --worker <id>)";
@@ -7923,6 +7923,14 @@ export const COMMAND_CATALOG = [
7923
7923
  }
7924
7924
  ],
7925
7925
  "options": [
7926
+ {
7927
+ "flags": "--kind <kind>",
7928
+ "description": "personal (acts as you, everywhere you are) or company (acts as itself, as a member of its companies). Default: personal, or company when created from a company worker"
7929
+ },
7930
+ {
7931
+ "flags": "--company <slug>",
7932
+ "description": "Company bots: a company the bot is a member of (repeat for several); the owner invites it on creation"
7933
+ },
7926
7934
  {
7927
7935
  "flags": "--runtime <runtime>",
7928
7936
  "description": "Model CLI to run the bot with: claude|codex|grok"
@@ -7970,6 +7978,35 @@ export const COMMAND_CATALOG = [
7970
7978
  ],
7971
7979
  "subcommands": []
7972
7980
  },
7981
+ {
7982
+ "name": "companies",
7983
+ "description": "Show or change which companies a company bot is a member of (you invite or remove it; a personal bot has no list of its own)",
7984
+ "aliases": [],
7985
+ "hidden": false,
7986
+ "usage": "[options] <name>",
7987
+ "arguments": [
7988
+ {
7989
+ "name": "name",
7990
+ "required": true,
7991
+ "variadic": false
7992
+ }
7993
+ ],
7994
+ "options": [
7995
+ {
7996
+ "flags": "--add <slug>",
7997
+ "description": "Add the bot to this company (repeatable)"
7998
+ },
7999
+ {
8000
+ "flags": "--remove <slug>",
8001
+ "description": "Remove the bot from this company (repeatable)"
8002
+ },
8003
+ {
8004
+ "flags": "--json",
8005
+ "description": "Print machine-readable JSON"
8006
+ }
8007
+ ],
8008
+ "subcommands": []
8009
+ },
7973
8010
  {
7974
8011
  "name": "workers",
7975
8012
  "description": "List the workers (templates) a bot can be created from (hq bot create <name> --worker <id>)",
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A company bot's memberships (BOT-KINDS, 2026-09-15). A company bot acts as
3
+ * itself, so it must be a member of every company it works for; the OWNER
4
+ * (person token) invites it, the same `agt_` member/admin route fleet agents
5
+ * use. Slugs resolve to company uids with the owner's sign-in.
6
+ *
7
+ * Both helpers keep going after a failure and report every slug's outcome,
8
+ * so `hq bot create` can say exactly which invite did not go through.
9
+ */
10
+ import { inviteMember, revokeInvite } from "./members.js";
11
+ export interface BotCompanyDeps {
12
+ companyUid: (token: string, slug: string) => Promise<string>;
13
+ callerUid: (token: string) => Promise<string>;
14
+ invite: typeof inviteMember;
15
+ revoke: typeof revokeInvite;
16
+ }
17
+ export interface BotCompanyOutcome {
18
+ /** Slugs the change went through for. */
19
+ done: string[];
20
+ /** Slugs it did not, with the reason. */
21
+ failed: Array<{
22
+ slug: string;
23
+ reason: string;
24
+ }>;
25
+ }
26
+ /** Invite the bot as a member of each company, with the person's token. */
27
+ export declare function addBotToCompanies(input: {
28
+ agentUid: string;
29
+ companies: readonly string[];
30
+ token: string;
31
+ }, deps?: BotCompanyDeps): Promise<BotCompanyOutcome>;
32
+ /** Remove the bot's membership in each company (the `hq members revoke` route). */
33
+ export declare function removeBotFromCompanies(input: {
34
+ agentUid: string;
35
+ companies: readonly string[];
36
+ token: string;
37
+ }, deps?: BotCompanyDeps): Promise<BotCompanyOutcome>;
38
+ /** One line per failure, for an error message or a log. */
39
+ export declare function describeCompanyFailures(failed: BotCompanyOutcome["failed"]): string;
40
+ //# sourceMappingURL=bot-companies.d.ts.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A company bot's memberships (BOT-KINDS, 2026-09-15). A company bot acts as
3
+ * itself, so it must be a member of every company it works for; the OWNER
4
+ * (person token) invites it, the same `agt_` member/admin route fleet agents
5
+ * use. Slugs resolve to company uids with the owner's sign-in.
6
+ *
7
+ * Both helpers keep going after a failure and report every slug's outcome,
8
+ * so `hq bot create` can say exactly which invite did not go through.
9
+ */
10
+ import { getCallerPersonUid, inviteMember, resolveRevokeTargetToMembershipKey, revokeInvite } from "./members.js";
11
+ import { getCompanyUid } from "../utils/vault-api.js";
12
+ const defaultDeps = {
13
+ companyUid: getCompanyUid,
14
+ callerUid: getCallerPersonUid,
15
+ invite: inviteMember,
16
+ revoke: revokeInvite,
17
+ };
18
+ function reasonOf(err) {
19
+ return err instanceof Error ? err.message : String(err);
20
+ }
21
+ /** Invite the bot as a member of each company, with the person's token. */
22
+ export async function addBotToCompanies(input, deps = defaultDeps) {
23
+ const out = { done: [], failed: [] };
24
+ if (input.companies.length === 0)
25
+ return out;
26
+ let callerUid;
27
+ try {
28
+ callerUid = await deps.callerUid(input.token);
29
+ }
30
+ catch (err) {
31
+ return { done: [], failed: input.companies.map((slug) => ({ slug, reason: reasonOf(err) })) };
32
+ }
33
+ for (const slug of input.companies) {
34
+ try {
35
+ const companyUid = await deps.companyUid(input.token, slug);
36
+ await deps.invite({ target: input.agentUid, role: "member", sendEmail: false, companyUid, callerUid, token: input.token });
37
+ out.done.push(slug);
38
+ }
39
+ catch (err) {
40
+ out.failed.push({ slug, reason: reasonOf(err) });
41
+ }
42
+ }
43
+ return out;
44
+ }
45
+ /** Remove the bot's membership in each company (the `hq members revoke` route). */
46
+ export async function removeBotFromCompanies(input, deps = defaultDeps) {
47
+ const out = { done: [], failed: [] };
48
+ for (const slug of input.companies) {
49
+ try {
50
+ const companyUid = await deps.companyUid(input.token, slug);
51
+ await deps.revoke(input.token, resolveRevokeTargetToMembershipKey(input.agentUid, companyUid), companyUid);
52
+ out.done.push(slug);
53
+ }
54
+ catch (err) {
55
+ out.failed.push({ slug, reason: reasonOf(err) });
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+ /** One line per failure, for an error message or a log. */
61
+ export function describeCompanyFailures(failed) {
62
+ return failed.map((f) => `${f.slug} (${f.reason})`).join("; ");
63
+ }
64
+ //# sourceMappingURL=bot-companies.js.map
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
- import { type BotMemoryMode, type BotRuntimeId, type LaunchctlExec } from "../lib/bot/index.js";
2
+ import { type RuntimeSignInIssue } from "../lib/bot/runtime-sign-in.js";
3
+ import { type BotKind, type BotMemoryMode, type BotRuntimeId, type LaunchctlExec } from "../lib/bot/index.js";
3
4
  /** Real launchctl; every failure is a non-ok result, never a throw. */
4
5
  export declare const defaultLaunchctl: LaunchctlExec;
5
6
  /** The desktop app creates HQ's setup bot under this name, always from the `setup` worker. */
@@ -23,10 +24,16 @@ export interface BotRow {
23
24
  } | null;
24
25
  online: boolean | null;
25
26
  lastHeartbeatAt: string | null;
27
+ /** Present while the coding tool needs a new sign-in before the bot can work. */
28
+ runtimeSignIn: RuntimeSignInIssue | null;
26
29
  daemonInstalled: boolean;
27
30
  daemonLoaded: boolean;
28
31
  dir: string;
29
32
  introSentAt?: string;
33
+ /** "personal" acts as its owner everywhere; "company" acts as itself. */
34
+ kind: BotKind;
35
+ /** Company bots: the companies the bot is a member of (empty for a personal bot). */
36
+ companies: string[];
30
37
  workerSource: "scaffold" | "worker";
31
38
  workerDir: string;
32
39
  workerId?: string;
@@ -43,6 +50,24 @@ export declare function listBotNames(root: string): string[];
43
50
  export declare function botTokenSupplier(dir: string): () => Promise<string>;
44
51
  /** Spawn `hq bot run <name>` detached (no launchd). */
45
52
  export declare function spawnDetachedRun(name: string, dir: string): number;
53
+ /**
54
+ * The kind a new bot gets, before anything is provisioned. Personal unless
55
+ * `--kind company` is given (the desktop app always passes --kind). A company
56
+ * bot needs at least one company: `--company`, or the company worker's own
57
+ * company when created from one. A personal bot takes no `--company`, and
58
+ * HQ's setup bot is always personal.
59
+ */
60
+ export declare function resolveCreateKind(opts: {
61
+ kind?: string;
62
+ company?: string[];
63
+ worker?: string;
64
+ }, worker: {
65
+ id: string;
66
+ company?: string | null;
67
+ } | null): {
68
+ kind: BotKind;
69
+ companies: string[];
70
+ };
46
71
  export declare function runBotCreate(nameArg: string, opts: {
47
72
  runtime?: string;
48
73
  model?: string;
@@ -55,6 +80,13 @@ export declare function runBotCreate(nameArg: string, opts: {
55
80
  intro?: string;
56
81
  kickoff?: string;
57
82
  memory?: string;
83
+ kind?: string;
84
+ company?: string[];
85
+ }): Promise<void>;
86
+ export declare function runBotCompanies(nameArg: string, opts: {
87
+ add?: string[];
88
+ remove?: string[];
89
+ json?: boolean;
58
90
  }): Promise<void>;
59
91
  export declare function runBotWorkers(opts: {
60
92
  json?: boolean;
@@ -4,7 +4,8 @@ import { hasPromotionHold, promotionHoldSummary } from "../lib/bot/promotion-hol
4
4
  /**
5
5
  * `hq bot` — personal local bots (local-bots US-003/US-004/US-005).
6
6
  *
7
- * hq bot create <name> [--runtime claude|codex|grok] [--model m] [--effort level] [--worker id] [--intro text] [--kickoff prompt] [--memory synced|local] [--no-daemon]
7
+ * hq bot create <name> [--kind personal|company] [--company slug]... [--runtime claude|codex|grok] [--model m] [--effort level] [--worker id] [--intro text] [--kickoff prompt] [--memory synced|local] [--no-daemon]
8
+ * hq bot companies <name> [--add slug]... [--remove slug]... (a company bot's memberships)
8
9
  * hq bot workers [--json] (company + core workers a bot can be created from)
9
10
  * hq bot list [--json]
10
11
  * hq bot start|stop|restart <name>
@@ -20,6 +21,7 @@ import { hasPromotionHold, promotionHoldSummary } from "../lib/bot/promotion-hol
20
21
  * list (online flag), and rm. Secrets are never printed.
21
22
  */
22
23
  import { promoteLocalBot } from "../lib/bot/promote.js";
24
+ import { addBotToCompanies, describeCompanyFailures, removeBotFromCompanies } from "./bot-companies.js";
23
25
  import { registerBotContinuityCommand } from "./bot-continuity.js";
24
26
  import chalk from "chalk";
25
27
  import { execFileSync, spawn } from "node:child_process";
@@ -30,7 +32,11 @@ import { DEFAULT_COGNITO, ensureCognitoToken, resolveDefaultHqRoot, } from "../u
30
32
  import { peekHqApiKey } from "../utils/resolve-vault-credential.js";
31
33
  import { resolveCallerPersonUid } from "../utils/vault-api.js";
32
34
  import { resolveHqBinary } from "../lib/mesh/live/daemon/install.js";
33
- import { BotApi, BotApiError, botDaemonStatus, botDir, botCredsPath, botLogPath, botsRoot, botTokenStateDir, botWorkerRelDir, buildBotDaemonPaths, createBotLogger, deleteBotCreds, installBotDaemon, introDmText, isBotMemoryMode, botLocalMemoryDir, botMemoryMode, resolveBotMemoryDir, validateBotIntro, validateBotKickoff, validateBotEffort, validateBotModel, effectiveBotEffort, BOT_EFFORT_LEVELS, DEFAULT_BOT_EFFORT, BOT_MEMORY_MODES, isBotRuntimeId, isPidAlive, listBotWorkerOptions, patchBotConfig, readBotConfig, readBotCredsIdentity, readBotStatus, resolveBotWorker, runBot, runtimeFor, scaffoldBotMemory, scaffoldBotWorker, startBotDaemon, stopBotDaemon, uninstallBotDaemon, validateBotName, writeBotConfig, writeBotCreds, BOT_RUNTIMES, BOT_INTRO_MAX_CHARS, BOT_KICKOFF_MAX_CHARS, } from "../lib/bot/index.js";
35
+ import { runtimeDisplayName } from "../lib/bot/runtime-sign-in.js";
36
+ import { BotApi, BotApiError, botDaemonStatus, botDir, botCredsPath, botLogPath, botsRoot, botTokenStateDir, botWorkerRelDir, buildBotDaemonPaths, createBotLogger, deleteBotCreds, installBotDaemon, introDmText, isBotMemoryMode, botLocalMemoryDir, botMemoryMode, resolveBotMemoryDir, validateBotIntro, validateBotKickoff, validateBotEffort, validateBotModel, effectiveBotEffort, BOT_EFFORT_LEVELS, DEFAULT_BOT_EFFORT, BOT_MEMORY_MODES, BOT_KINDS, isBotKind, effectiveBotKind, effectiveBotCompanies, normalizeBotCompanies, SETUP_BOT_WORKER_ID, isBotRuntimeId, isPidAlive, listBotWorkerOptions, patchBotConfig, readBotConfig, readBotCredsIdentity, readBotStatus, resolveBotWorker, runBot, runtimeFor, scaffoldBotMemory, scaffoldBotWorker, startBotDaemon, stopBotDaemon, uninstallBotDaemon, validateBotName, writeBotConfig, writeBotCreds, BOT_RUNTIMES, BOT_INTRO_MAX_CHARS, BOT_KICKOFF_MAX_CHARS, } from "../lib/bot/index.js";
37
+ function collectRepeated(value, previous) {
38
+ return [...previous, value];
39
+ }
34
40
  function fail(message) {
35
41
  console.error(chalk.red(message));
36
42
  process.exit(1);
@@ -134,10 +140,13 @@ function rowFor(name, opts = {}) {
134
140
  promotionHold: promotionHoldSummary(dir, config.agentUid, config.ownerUid),
135
141
  online: null,
136
142
  lastHeartbeatAt: status?.lastHeartbeatAt ?? null,
143
+ runtimeSignIn: status?.runtimeSignIn ?? null,
137
144
  daemonInstalled,
138
145
  daemonLoaded,
139
146
  dir,
140
147
  introSentAt: config.introSentAt,
148
+ kind: effectiveBotKind(config),
149
+ companies: effectiveBotCompanies(config),
141
150
  workerSource: config.workerSource ?? "scaffold",
142
151
  workerDir: config.workerDir,
143
152
  ...(config.workerId ? { workerId: config.workerId } : {}),
@@ -290,6 +299,42 @@ async function stopBot(name, dir, config, output) {
290
299
  console.log(signalled || gone ? chalk.green(`${name} stopped`) : chalk.dim(`${name} was not running`));
291
300
  }
292
301
  // ── create ───────────────────────────────────────────────────────────────────
302
+ /** "personal (acts as you)" / "company (acts as itself; member of a, b)". */
303
+ function describeKind(kind, companies) {
304
+ return kind === "company"
305
+ ? `company (acts as itself; member of ${companies.length ? companies.join(", ") : "no company"})`
306
+ : "personal (acts as you)";
307
+ }
308
+ /**
309
+ * The kind a new bot gets, before anything is provisioned. Personal unless
310
+ * `--kind company` is given (the desktop app always passes --kind). A company
311
+ * bot needs at least one company: `--company`, or the company worker's own
312
+ * company when created from one. A personal bot takes no `--company`, and
313
+ * HQ's setup bot is always personal.
314
+ */
315
+ export function resolveCreateKind(opts, worker) {
316
+ const given = opts.kind?.trim().toLowerCase();
317
+ if (given !== undefined && given !== "" && !isBotKind(given)) {
318
+ throw Object.assign(new Error(`--kind must be one of ${BOT_KINDS.join(", ")}`), { expected: true });
319
+ }
320
+ const requested = normalizeBotCompanies(opts.company);
321
+ const isSetup = worker?.id === SETUP_BOT_WORKER_ID;
322
+ const kind = isBotKind(given) ? given : "personal";
323
+ if (kind === "personal") {
324
+ if (requested.length > 0) {
325
+ throw Object.assign(new Error("--company only applies to a company bot (a personal bot acts as you, in every company you belong to). Add --kind company, or drop --company."), { expected: true });
326
+ }
327
+ return { kind, companies: [] };
328
+ }
329
+ if (isSetup) {
330
+ throw Object.assign(new Error("HQ's setup bot is always a personal bot; drop --kind company."), { expected: true });
331
+ }
332
+ const companies = requested.length > 0 ? requested : normalizeBotCompanies(worker?.company ? [worker.company] : []);
333
+ if (companies.length === 0) {
334
+ throw Object.assign(new Error("A company bot needs at least one company: add --company <slug> (repeat it for several), or create the bot from a company worker."), { expected: true });
335
+ }
336
+ return { kind, companies };
337
+ }
293
338
  export async function runBotCreate(nameArg, opts) {
294
339
  const name = validateBotName(nameArg);
295
340
  const runtime = opts.runtime ?? "claude";
@@ -317,6 +362,7 @@ export async function runBotCreate(nameArg, opts) {
317
362
  // Resolve the company worker before provisioning anything (a typo must not
318
363
  // cost an identity).
319
364
  const worker = opts.worker?.trim() ? resolveBotWorker(hqRoot, opts.worker) : null;
365
+ const { kind, companies } = resolveCreateKind(opts, worker);
320
366
  const token = await ownerToken();
321
367
  const owner = await resolveCallerPersonUid(token);
322
368
  if (!owner)
@@ -324,7 +370,7 @@ export async function runBotCreate(nameArg, opts) {
324
370
  const api = new BotApi({ token: async () => token });
325
371
  let created;
326
372
  try {
327
- created = await api.createLocalBot({ name });
373
+ created = await api.createLocalBot({ name, kind });
328
374
  }
329
375
  catch (err) {
330
376
  if (err instanceof BotApiError && err.code === "LOCAL_BOT_CAP_REACHED") {
@@ -350,6 +396,9 @@ export async function runBotCreate(nameArg, opts) {
350
396
  agentUid,
351
397
  ownerUid: created.agent.ownerUid ?? owner,
352
398
  runtime,
399
+ kind,
400
+ // A company bot's list is filled in below, with the invites that went through.
401
+ ...(kind === "company" ? { companies: [] } : {}),
353
402
  ...(model ? { model } : {}),
354
403
  ...(effort ? { effort } : {}),
355
404
  ...(opts.autoApprove === false ? { autoApprove: false } : {}),
@@ -377,15 +426,32 @@ export async function runBotCreate(nameArg, opts) {
377
426
  enabled: true,
378
427
  };
379
428
  writeBotConfig(dir, config);
429
+ // A company bot acts as itself, so it must be a member of each of its
430
+ // companies: the owner invites it now (person token). Only the companies
431
+ // it actually joined are recorded, so after a failed invite the identity
432
+ // and bot.json stay on disk and `hq bot companies <name> --add <slug>`
433
+ // retries exactly the missing ones (or `hq bot rm <name>` undoes it all).
434
+ if (kind === "company") {
435
+ const outcome = await addBotToCompanies({ agentUid, companies, token });
436
+ config.companies = outcome.done;
437
+ writeBotConfig(dir, config);
438
+ if (outcome.failed.length > 0) {
439
+ createBotLogger(dir)("error", `created ${name} (${agentUid}) but could not add it to: ${describeCompanyFailures(outcome.failed)}`);
440
+ throw Object.assign(new Error(`Created the bot identity ${agentUid} but could not add it to ${outcome.failed.length === 1 ? "a company" : `${outcome.failed.length} companies`}: ${describeCompanyFailures(outcome.failed)}. ` +
441
+ `Fix the cause and run \`hq bot companies ${name} --add ${outcome.failed.map((f) => f.slug).join(" --add ")}\`, or remove the bot with \`hq bot rm ${name}\`.`), { expected: true });
442
+ }
443
+ }
380
444
  // A worker-sourced bot never writes into the company tree: only its own
381
445
  // memory folder is scaffolded.
382
446
  const scaffoldOpts = { memoryDir: config.memoryDir };
383
447
  const scaffold = worker
384
448
  ? scaffoldBotMemory(hqRoot, name, undefined, scaffoldOpts)
385
449
  : scaffoldBotWorker(hqRoot, name, agentUid, runtime, undefined, scaffoldOpts);
386
- createBotLogger(dir)("info", `created ${name} (${agentUid}) runtime=${runtime}${worker ? ` worker=${worker.id} company=${worker.company ?? "-"}` : ""} memory=${memoryMode}`);
450
+ createBotLogger(dir)("info", `created ${name} (${agentUid}) kind=${kind}${companies.length ? ` companies=${companies.join(",")}` : ""} runtime=${runtime}${worker ? ` worker=${worker.id} company=${worker.company ?? "-"}` : ""} memory=${memoryMode}`);
387
451
  const created_ = {
388
452
  agentUid,
453
+ kind,
454
+ companies,
389
455
  runtime,
390
456
  dir,
391
457
  worker: worker ? path.join(hqRoot, worker.path) : scaffold.dir,
@@ -405,6 +471,7 @@ export async function runBotCreate(nameArg, opts) {
405
471
  }
406
472
  if (!opts.json) {
407
473
  console.log(chalk.green(`Created bot ${chalk.bold(name)}`) + chalk.dim(` (${agentUid}, ${runtime})`));
474
+ console.log(chalk.dim(` kind: ${describeKind(kind, companies)}`));
408
475
  if (worker)
409
476
  console.log(chalk.dim(` worker: ${worker.id}${worker.company ? ` (${worker.company})` : ""} — ${path.join(hqRoot, worker.path)}`));
410
477
  if (!worker)
@@ -434,6 +501,66 @@ export async function runBotCreate(nameArg, opts) {
434
501
  console.log(JSON.stringify({ ok: true, name, ...created_, daemon: daemonMessage, running: false }, null, 2));
435
502
  }
436
503
  }
504
+ // ── companies ────────────────────────────────────────────────────────────────
505
+ export async function runBotCompanies(nameArg, opts) {
506
+ const { name, dir, config } = requireBot(nameArg);
507
+ const kind = effectiveBotKind(config);
508
+ const add = normalizeBotCompanies(opts.add);
509
+ const remove = normalizeBotCompanies(opts.remove);
510
+ if (kind !== "company") {
511
+ throw Object.assign(new Error(`${name} is a personal bot: it acts as you, in every company you belong to, so it has no company list of its own. Create a company bot with: hq bot create <name> --kind company --company <slug>`), { expected: true });
512
+ }
513
+ const current = effectiveBotCompanies(config);
514
+ const both = add.filter((c) => remove.includes(c));
515
+ if (both.length > 0) {
516
+ throw Object.assign(new Error(`--add and --remove name the same company: ${both.join(", ")}`), { expected: true });
517
+ }
518
+ const toAdd = add.filter((c) => !current.includes(c));
519
+ const toRemove = remove.filter((c) => current.includes(c));
520
+ const unknown = remove.filter((c) => !current.includes(c));
521
+ if (unknown.length > 0) {
522
+ throw Object.assign(new Error(`${name} is not in ${unknown.join(", ")} (its companies: ${current.join(", ") || "none"}).`), { expected: true });
523
+ }
524
+ const next = [...current.filter((c) => !toRemove.includes(c)), ...toAdd];
525
+ if (next.length === 0) {
526
+ throw Object.assign(new Error(`A company bot needs at least one company; ${name} would be left with none. Remove the bot instead with: hq bot rm ${name}`), { expected: true });
527
+ }
528
+ const failed = [];
529
+ let added = [];
530
+ let removed = [];
531
+ if (toAdd.length > 0 || toRemove.length > 0) {
532
+ const token = await ownerToken();
533
+ if (toAdd.length > 0) {
534
+ const r = await addBotToCompanies({ agentUid: config.agentUid, companies: toAdd, token });
535
+ added = r.done;
536
+ failed.push(...r.failed);
537
+ }
538
+ if (toRemove.length > 0) {
539
+ const r = await removeBotFromCompanies({ agentUid: config.agentUid, companies: toRemove, token });
540
+ removed = r.done;
541
+ failed.push(...r.failed);
542
+ }
543
+ }
544
+ const companies = [...current.filter((c) => !removed.includes(c)), ...added];
545
+ if (added.length > 0 || removed.length > 0)
546
+ patchBotConfig(dir, { kind: "company", companies });
547
+ if (opts.json) {
548
+ console.log(JSON.stringify({ ok: failed.length === 0, name, kind, companies, added, removed, failed }, null, 2));
549
+ }
550
+ else {
551
+ console.log(`${chalk.bold(name)} company bot, member of: ${companies.join(", ")}`);
552
+ if (added.length > 0)
553
+ console.log(chalk.green(` added: ${added.join(", ")}`));
554
+ if (removed.length > 0)
555
+ console.log(chalk.green(` removed: ${removed.join(", ")}`));
556
+ if (companies.length > 0 && (added.length > 0 || removed.length > 0)) {
557
+ console.log(chalk.dim(` restart the bot so its instructions pick this up: hq bot restart ${name}`));
558
+ }
559
+ }
560
+ if (failed.length > 0) {
561
+ throw Object.assign(new Error(`Could not change ${name}'s membership in: ${describeCompanyFailures(failed)}`), { expected: true });
562
+ }
563
+ }
437
564
  // ── workers ──────────────────────────────────────────────────────────────────
438
565
  export async function runBotWorkers(opts) {
439
566
  const hqRoot = resolveDefaultHqRoot({ onMissing: "throw" });
@@ -473,7 +600,7 @@ async function runBotList(opts) {
473
600
  return;
474
601
  }
475
602
  for (const r of rows) {
476
- console.log(`${chalk.bold(r.name.padEnd(16))} ${r.agentUid} ${r.runtime.padEnd(6)} ${humanOnline(r).padEnd(8)} ${r.processAlive ? chalk.green(`pid ${r.pid}`) : chalk.gray(r.state)}${r.daemonInstalled ? chalk.dim(" launchd") : ""}${r.workerId ? chalk.dim(` worker ${r.workerId}${r.companySlug ? ` (${r.companySlug})` : ""}`) : ""}`);
603
+ console.log(`${chalk.bold(r.name.padEnd(16))} ${r.agentUid} ${r.runtime.padEnd(6)} ${humanOnline(r).padEnd(8)} ${r.processAlive ? chalk.green(`pid ${r.pid}`) : chalk.gray(r.state)}${chalk.dim(` ${r.kind === "company" ? `company bot: ${r.companies.join(", ") || "no company"}` : "personal"}`)}${r.daemonInstalled ? chalk.dim(" launchd") : ""}${r.workerId ? chalk.dim(` worker ${r.workerId}${r.companySlug ? ` (${r.companySlug})` : ""}`) : ""}`);
477
604
  }
478
605
  }
479
606
  async function runBotStatus(nameArg, opts) {
@@ -487,6 +614,7 @@ async function runBotStatus(nameArg, opts) {
487
614
  return;
488
615
  }
489
616
  console.log(`${chalk.bold(name)} ${row.agentUid}`);
617
+ console.log(` kind: ${describeKind(row.kind, row.companies)}`);
490
618
  console.log(` runtime: ${row.runtime} model ${row.model ?? "default"} thinking ${row.effort}${row.effortIsDefault ? " (default)" : ""}`);
491
619
  if (row.workerId)
492
620
  console.log(` worker: ${row.workerId}${row.companySlug ? ` (company ${row.companySlug})` : ""} ${chalk.dim(row.workerDir)}`);
@@ -497,6 +625,10 @@ async function runBotStatus(nameArg, opts) {
497
625
  console.log(` creds: ${creds ? `present for ${creds.entityUid}` : chalk.red("missing")}`);
498
626
  if (status) {
499
627
  console.log(` started: ${status.startedAt} restarts: ${status.restarts} replies: ${status.repliesSent}`);
628
+ if (status.runtimeSignIn?.state === "expired") {
629
+ const tool = runtimeDisplayName(status.runtimeSignIn.runtime);
630
+ console.log(` ${chalk.yellow(`${tool} needs to sign in again`)} — sign in from HQ, or run \`${status.runtimeSignIn.runtime} ${status.runtimeSignIn.runtime === "claude" ? "auth login" : "login"}\`, then \`hq bot restart ${name}\``);
631
+ }
500
632
  if (status.lastError)
501
633
  console.log(` last error: ${chalk.yellow(status.lastError)}`);
502
634
  }
@@ -568,15 +700,22 @@ async function runBotRm(nameArg, opts) {
568
700
  throw err;
569
701
  }
570
702
  const terminal = "alreadyGone" in identity ? true : identity.terminal;
703
+ // A company bot is a member of its companies: leave them too. Best effort —
704
+ // a stale membership row must not keep the bot's local state around.
705
+ const memberships = effectiveBotCompanies(config);
706
+ const left = memberships.length > 0 ? await removeBotFromCompanies({ agentUid: config.agentUid, companies: memberships, token }) : { done: [], failed: [] };
571
707
  deleteBotCreds(dir);
572
708
  if (terminal)
573
709
  fs.rmSync(dir, { recursive: true, force: true });
574
710
  if (opts.json) {
575
- console.log(JSON.stringify({ ok: terminal, name, agentUid: config.agentUid, daemon: daemon.message, identity, localStateRemoved: terminal, workerKept: path.join(config.hqRoot, config.workerDir) }, null, 2));
711
+ console.log(JSON.stringify({ ok: terminal, name, agentUid: config.agentUid, daemon: daemon.message, identity, companiesLeft: left.done, companiesNotLeft: left.failed, localStateRemoved: terminal, workerKept: path.join(config.hqRoot, config.workerDir) }, null, 2));
576
712
  return;
577
713
  }
714
+ for (const f of left.failed) {
715
+ console.log(chalk.yellow(` could not remove ${name} from ${f.slug}: ${f.reason}`) + chalk.dim(` — remove it by hand: hq members --company ${f.slug} revoke ${config.agentUid}`));
716
+ }
578
717
  if (terminal) {
579
- console.log(chalk.green(`Removed bot ${name}`) + chalk.dim(` (${config.agentUid}); worker folder kept at ${path.join(config.hqRoot, config.workerDir)}`));
718
+ console.log(chalk.green(`Removed bot ${name}`) + chalk.dim(` (${config.agentUid}${left.done.length ? `; left ${left.done.join(", ")}` : ""}); worker folder kept at ${path.join(config.hqRoot, config.workerDir)}`));
580
719
  }
581
720
  else {
582
721
  console.log(chalk.yellow(`Bot ${name} is stopped locally but its cloud identity is not fully removed yet.`));
@@ -668,6 +807,8 @@ export function registerBotCommand(program) {
668
807
  bot
669
808
  .command("create <name>")
670
809
  .description("Provision a local bot identity, scaffold its worker folder, install its launchd agent, and start it")
810
+ .option("--kind <kind>", `personal (acts as you, everywhere you are) or company (acts as itself, as a member of its companies). Default: personal, or company when created from a company worker`)
811
+ .option("--company <slug>", "Company bots: a company the bot is a member of (repeat for several); the owner invites it on creation", collectRepeated, [])
671
812
  .option("--runtime <runtime>", `Model CLI to run the bot with: ${BOT_RUNTIMES.join("|")}`, "claude")
672
813
  .option("--model <model>", "Model override passed to the runtime CLI")
673
814
  .option("--effort <level>", `Thinking level: claude ${BOT_EFFORT_LEVELS.claude.join("|")}; codex ${BOT_EFFORT_LEVELS.codex.join("|")}; grok ${BOT_EFFORT_LEVELS.grok.join("|")} (default ${DEFAULT_BOT_EFFORT})`)
@@ -680,6 +821,13 @@ export function registerBotCommand(program) {
680
821
  .option("--no-start", "Create without starting")
681
822
  .option("--json", "Print machine-readable JSON")
682
823
  .action((name, opts) => wrap(() => runBotCreate(name, opts))());
824
+ bot
825
+ .command("companies <name>")
826
+ .description("Show or change which companies a company bot is a member of (you invite or remove it; a personal bot has no list of its own)")
827
+ .option("--add <slug>", "Add the bot to this company (repeatable)", collectRepeated, [])
828
+ .option("--remove <slug>", "Remove the bot from this company (repeatable)", collectRepeated, [])
829
+ .option("--json", "Print machine-readable JSON")
830
+ .action((name, opts) => wrap(() => runBotCompanies(name, opts))());
683
831
  bot
684
832
  .command("workers")
685
833
  .description("List the workers (templates) a bot can be created from (hq bot create <name> --worker <id>)")
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A company created from a local bot's conversation is owned by the person
3
+ * (the turn runs with the owner's sign-in), so the bot that helped create it
4
+ * is not in it. The bot passes its own id along (HQ_BOT_AGENT_UID — an id,
5
+ * never a credential), and `hq onboard create-company` adds that bot as a
6
+ * member right after, so it can keep helping inside the new company.
7
+ *
8
+ * Best effort: a failure here never fails the company that was just created.
9
+ */
10
+ import { inviteMember } from "./members.js";
11
+ export declare const HQ_BOT_AGENT_UID_ENV = "HQ_BOT_AGENT_UID";
12
+ /** The bot to add to `companyUid`, or null when this is not a bot's turn. */
13
+ export declare function botToAddAfterCreateCompany(env: NodeJS.ProcessEnv, companyUid: string | undefined): {
14
+ agentUid: string;
15
+ companyUid: string;
16
+ } | null;
17
+ export interface AddBotDeps {
18
+ invite: typeof inviteMember;
19
+ callerUid: (token: string) => Promise<string>;
20
+ }
21
+ /** Add the bot as a member. Returns a one-line outcome for the log; never throws. */
22
+ export declare function addBotToNewCompany(input: {
23
+ agentUid: string;
24
+ companyUid: string;
25
+ token: string;
26
+ }, deps?: AddBotDeps): Promise<{
27
+ ok: true;
28
+ } | {
29
+ ok: false;
30
+ reason: string;
31
+ }>;
32
+ //# sourceMappingURL=onboard-bot-membership.d.ts.map