@indigoai-us/hq-cli 5.114.0 → 5.115.1
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 +45 -0
- package/dist/command-catalog.generated.d.ts +28 -0
- package/dist/command-catalog.generated.js +37 -0
- package/dist/commands/bot-companies.d.ts +40 -0
- package/dist/commands/bot-companies.js +64 -0
- package/dist/commands/bot.d.ts +33 -1
- package/dist/commands/bot.js +181 -8
- package/dist/commands/onboard-bot-membership.d.ts +32 -0
- package/dist/commands/onboard-bot-membership.js +40 -0
- package/dist/commands/onboard.js +10 -0
- package/dist/lib/bot/api.d.ts +11 -0
- package/dist/lib/bot/api.js +8 -1
- package/dist/lib/bot/config.d.ts +35 -0
- package/dist/lib/bot/config.js +69 -2
- package/dist/lib/bot/prompt.d.ts +13 -1
- package/dist/lib/bot/prompt.js +37 -12
- package/dist/lib/bot/run.d.ts +30 -3
- package/dist/lib/bot/run.js +137 -14
- package/dist/lib/bot/runtime-sign-in.d.ts +24 -0
- package/dist/lib/bot/runtime-sign-in.js +31 -0
- package/dist/lib/bot/status.d.ts +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.115.1] — 2026-09-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq bot create --kind company` now sends the bot's companies with the create, as the cloud requires, so a company bot can actually be created; the cloud makes it a member of each company, and a company you are not an owner or admin of is refused before anything is created.
|
|
10
|
+
|
|
11
|
+
## [5.115.0] — 2026-09-15
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Local bots now come in two kinds. A **personal bot** acts as you: everything
|
|
16
|
+
it does — in your direct messages, in rooms, and on its first task — runs
|
|
17
|
+
under your own account, so it reaches every company and file you can and
|
|
18
|
+
can create companies, bots, invites and shares for you. A **company bot**
|
|
19
|
+
acts as itself, like a cloud agent: it is a member of one or more companies
|
|
20
|
+
and reaches only those companies' files, memory and secrets; it never acts
|
|
21
|
+
as you and says so if asked to create something for you, suggesting you ask
|
|
22
|
+
a personal bot instead. `hq bot create <name> --kind personal|company
|
|
23
|
+
--company <slug>` (repeat `--company` for several) picks the kind; a
|
|
24
|
+
company bot is invited into each company as you create it, and the command
|
|
25
|
+
tells you exactly which invite did not go through. `hq bot companies
|
|
26
|
+
<name> --add/--remove <slug>` changes a company bot's memberships later.
|
|
27
|
+
`hq bot list --json` and `hq bot status` show each bot's kind and
|
|
28
|
+
companies, and every bot's instructions say which kind it is. Bots created
|
|
29
|
+
before this release keep working, as personal bots (including one created
|
|
30
|
+
from a company worker, and the setup bot). Removing a company bot with
|
|
31
|
+
`hq bot rm` also takes it out of its companies.
|
|
32
|
+
- A personal bot now runs `hq` commands as you in rooms too, not only in your
|
|
33
|
+
direct messages (before, its room turns used the bot's own identity).
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- When a local bot's coding tool (Claude Code, Codex or Grok) has lost its
|
|
38
|
+
sign-in, the bot no longer answers every message with "Failed to
|
|
39
|
+
authenticate". It says once that the tool needs you to sign in again, keeps
|
|
40
|
+
your message, and answers it on its own as soon as the sign-in works (right
|
|
41
|
+
away after `hq bot restart`). A setup bot's first step waits the same way.
|
|
42
|
+
`hq bot list --json` reports `runtimeSignIn` while this is the case.
|
|
43
|
+
- In your direct messages, a local bot now does what you ask under your own
|
|
44
|
+
account: creating a company, a bot, an invite or a share works directly,
|
|
45
|
+
instead of the bot explaining that the cloud refuses to let it act in your
|
|
46
|
+
name and asking you to run a command in a terminal. In rooms, bots still act
|
|
47
|
+
as themselves. A company you create this way now also adds that bot as a
|
|
48
|
+
member, so it can keep helping inside the new company.
|
|
49
|
+
|
|
5
50
|
## [5.114.0] — 2026-09-15
|
|
6
51
|
|
|
7
52
|
### Changed
|
|
@@ -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
|
package/dist/commands/bot.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { type
|
|
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;
|
package/dist/commands/bot.js
CHANGED
|
@@ -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";
|
|
@@ -28,9 +30,13 @@ import * as path from "node:path";
|
|
|
28
30
|
import * as readline from "node:readline";
|
|
29
31
|
import { DEFAULT_COGNITO, ensureCognitoToken, resolveDefaultHqRoot, } from "../utils/cognito-session.js";
|
|
30
32
|
import { peekHqApiKey } from "../utils/resolve-vault-credential.js";
|
|
31
|
-
import { resolveCallerPersonUid } from "../utils/vault-api.js";
|
|
33
|
+
import { getCompanyUid, resolveCallerPersonUid } from "../utils/vault-api.js";
|
|
32
34
|
import { resolveHqBinary } from "../lib/mesh/live/daemon/install.js";
|
|
33
|
-
import {
|
|
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,16 +362,38 @@ 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)
|
|
323
369
|
throw Object.assign(new Error("Your HQ login has no person entity yet; open the HQ desktop app once, then retry."), { expected: true });
|
|
324
370
|
const api = new BotApi({ token: async () => token });
|
|
371
|
+
// A company bot's companies are resolved first (a typo must not cost an
|
|
372
|
+
// identity) and sent with the create: the server makes the bot a member of
|
|
373
|
+
// each and refuses the whole create if the owner cannot add agents there.
|
|
374
|
+
const companyUids = [];
|
|
375
|
+
for (const slug of companies) {
|
|
376
|
+
try {
|
|
377
|
+
companyUids.push(await getCompanyUid(token, slug));
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
throw Object.assign(new Error(`Cannot create ${name}: ${err instanceof Error ? err.message : String(err)} (company "${slug}").`), { expected: true });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
325
383
|
let created;
|
|
326
384
|
try {
|
|
327
|
-
created = await api.createLocalBot({
|
|
385
|
+
created = await api.createLocalBot({
|
|
386
|
+
name,
|
|
387
|
+
kind,
|
|
388
|
+
...(kind === "company" ? { companyUid: companyUids[0], companyMemberships: companyUids.slice(1) } : {}),
|
|
389
|
+
});
|
|
328
390
|
}
|
|
329
391
|
catch (err) {
|
|
392
|
+
if (err instanceof BotApiError && err.code === "COMPANY_BOT_CREATE_NOT_ALLOWED") {
|
|
393
|
+
throw Object.assign(new Error(`The server refused to create ${name}: ${err.message} Only an owner or admin of each company can create a company bot there.`), {
|
|
394
|
+
expected: true,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
330
397
|
if (err instanceof BotApiError && err.code === "LOCAL_BOT_CAP_REACHED") {
|
|
331
398
|
// Only an older server still enforces a cap; current hq-pro-agents has none.
|
|
332
399
|
throw Object.assign(new Error("The server refused another bot (its bot limit is reached). Remove one with hq bot rm <name>, or update the server."), {
|
|
@@ -350,6 +417,9 @@ export async function runBotCreate(nameArg, opts) {
|
|
|
350
417
|
agentUid,
|
|
351
418
|
ownerUid: created.agent.ownerUid ?? owner,
|
|
352
419
|
runtime,
|
|
420
|
+
kind,
|
|
421
|
+
// A company bot's list is filled in below, with the invites that went through.
|
|
422
|
+
...(kind === "company" ? { companies: [] } : {}),
|
|
353
423
|
...(model ? { model } : {}),
|
|
354
424
|
...(effort ? { effort } : {}),
|
|
355
425
|
...(opts.autoApprove === false ? { autoApprove: false } : {}),
|
|
@@ -377,15 +447,36 @@ export async function runBotCreate(nameArg, opts) {
|
|
|
377
447
|
enabled: true,
|
|
378
448
|
};
|
|
379
449
|
writeBotConfig(dir, config);
|
|
450
|
+
// A company bot acts as itself, so it must be a member of each of its
|
|
451
|
+
// companies. Only the companies it actually joined are recorded, so after a failed invite the identity
|
|
452
|
+
// and bot.json stay on disk and `hq bot companies <name> --add <slug>`
|
|
453
|
+
// retries exactly the missing ones (or `hq bot rm <name>` undoes it all).
|
|
454
|
+
if (kind === "company") {
|
|
455
|
+
// The server already made it a member of the companies it reports back;
|
|
456
|
+
// only the rest (an older server reports none) are invited here.
|
|
457
|
+
const joined = new Set(created.agent.companyMemberships ?? []);
|
|
458
|
+
const already = companies.filter((_, i) => joined.has(companyUids[i]));
|
|
459
|
+
const missing = companies.filter((_, i) => !joined.has(companyUids[i]));
|
|
460
|
+
const outcome = await addBotToCompanies({ agentUid, companies: missing, token });
|
|
461
|
+
config.companies = [...already, ...outcome.done];
|
|
462
|
+
writeBotConfig(dir, config);
|
|
463
|
+
if (outcome.failed.length > 0) {
|
|
464
|
+
createBotLogger(dir)("error", `created ${name} (${agentUid}) but could not add it to: ${describeCompanyFailures(outcome.failed)}`);
|
|
465
|
+
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)}. ` +
|
|
466
|
+
`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 });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
380
469
|
// A worker-sourced bot never writes into the company tree: only its own
|
|
381
470
|
// memory folder is scaffolded.
|
|
382
471
|
const scaffoldOpts = { memoryDir: config.memoryDir };
|
|
383
472
|
const scaffold = worker
|
|
384
473
|
? scaffoldBotMemory(hqRoot, name, undefined, scaffoldOpts)
|
|
385
474
|
: 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}`);
|
|
475
|
+
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
476
|
const created_ = {
|
|
388
477
|
agentUid,
|
|
478
|
+
kind,
|
|
479
|
+
companies,
|
|
389
480
|
runtime,
|
|
390
481
|
dir,
|
|
391
482
|
worker: worker ? path.join(hqRoot, worker.path) : scaffold.dir,
|
|
@@ -405,6 +496,7 @@ export async function runBotCreate(nameArg, opts) {
|
|
|
405
496
|
}
|
|
406
497
|
if (!opts.json) {
|
|
407
498
|
console.log(chalk.green(`Created bot ${chalk.bold(name)}`) + chalk.dim(` (${agentUid}, ${runtime})`));
|
|
499
|
+
console.log(chalk.dim(` kind: ${describeKind(kind, companies)}`));
|
|
408
500
|
if (worker)
|
|
409
501
|
console.log(chalk.dim(` worker: ${worker.id}${worker.company ? ` (${worker.company})` : ""} — ${path.join(hqRoot, worker.path)}`));
|
|
410
502
|
if (!worker)
|
|
@@ -434,6 +526,66 @@ export async function runBotCreate(nameArg, opts) {
|
|
|
434
526
|
console.log(JSON.stringify({ ok: true, name, ...created_, daemon: daemonMessage, running: false }, null, 2));
|
|
435
527
|
}
|
|
436
528
|
}
|
|
529
|
+
// ── companies ────────────────────────────────────────────────────────────────
|
|
530
|
+
export async function runBotCompanies(nameArg, opts) {
|
|
531
|
+
const { name, dir, config } = requireBot(nameArg);
|
|
532
|
+
const kind = effectiveBotKind(config);
|
|
533
|
+
const add = normalizeBotCompanies(opts.add);
|
|
534
|
+
const remove = normalizeBotCompanies(opts.remove);
|
|
535
|
+
if (kind !== "company") {
|
|
536
|
+
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 });
|
|
537
|
+
}
|
|
538
|
+
const current = effectiveBotCompanies(config);
|
|
539
|
+
const both = add.filter((c) => remove.includes(c));
|
|
540
|
+
if (both.length > 0) {
|
|
541
|
+
throw Object.assign(new Error(`--add and --remove name the same company: ${both.join(", ")}`), { expected: true });
|
|
542
|
+
}
|
|
543
|
+
const toAdd = add.filter((c) => !current.includes(c));
|
|
544
|
+
const toRemove = remove.filter((c) => current.includes(c));
|
|
545
|
+
const unknown = remove.filter((c) => !current.includes(c));
|
|
546
|
+
if (unknown.length > 0) {
|
|
547
|
+
throw Object.assign(new Error(`${name} is not in ${unknown.join(", ")} (its companies: ${current.join(", ") || "none"}).`), { expected: true });
|
|
548
|
+
}
|
|
549
|
+
const next = [...current.filter((c) => !toRemove.includes(c)), ...toAdd];
|
|
550
|
+
if (next.length === 0) {
|
|
551
|
+
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 });
|
|
552
|
+
}
|
|
553
|
+
const failed = [];
|
|
554
|
+
let added = [];
|
|
555
|
+
let removed = [];
|
|
556
|
+
if (toAdd.length > 0 || toRemove.length > 0) {
|
|
557
|
+
const token = await ownerToken();
|
|
558
|
+
if (toAdd.length > 0) {
|
|
559
|
+
const r = await addBotToCompanies({ agentUid: config.agentUid, companies: toAdd, token });
|
|
560
|
+
added = r.done;
|
|
561
|
+
failed.push(...r.failed);
|
|
562
|
+
}
|
|
563
|
+
if (toRemove.length > 0) {
|
|
564
|
+
const r = await removeBotFromCompanies({ agentUid: config.agentUid, companies: toRemove, token });
|
|
565
|
+
removed = r.done;
|
|
566
|
+
failed.push(...r.failed);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const companies = [...current.filter((c) => !removed.includes(c)), ...added];
|
|
570
|
+
if (added.length > 0 || removed.length > 0)
|
|
571
|
+
patchBotConfig(dir, { kind: "company", companies });
|
|
572
|
+
if (opts.json) {
|
|
573
|
+
console.log(JSON.stringify({ ok: failed.length === 0, name, kind, companies, added, removed, failed }, null, 2));
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
console.log(`${chalk.bold(name)} company bot, member of: ${companies.join(", ")}`);
|
|
577
|
+
if (added.length > 0)
|
|
578
|
+
console.log(chalk.green(` added: ${added.join(", ")}`));
|
|
579
|
+
if (removed.length > 0)
|
|
580
|
+
console.log(chalk.green(` removed: ${removed.join(", ")}`));
|
|
581
|
+
if (companies.length > 0 && (added.length > 0 || removed.length > 0)) {
|
|
582
|
+
console.log(chalk.dim(` restart the bot so its instructions pick this up: hq bot restart ${name}`));
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (failed.length > 0) {
|
|
586
|
+
throw Object.assign(new Error(`Could not change ${name}'s membership in: ${describeCompanyFailures(failed)}`), { expected: true });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
437
589
|
// ── workers ──────────────────────────────────────────────────────────────────
|
|
438
590
|
export async function runBotWorkers(opts) {
|
|
439
591
|
const hqRoot = resolveDefaultHqRoot({ onMissing: "throw" });
|
|
@@ -473,7 +625,7 @@ async function runBotList(opts) {
|
|
|
473
625
|
return;
|
|
474
626
|
}
|
|
475
627
|
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})` : ""}`) : ""}`);
|
|
628
|
+
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
629
|
}
|
|
478
630
|
}
|
|
479
631
|
async function runBotStatus(nameArg, opts) {
|
|
@@ -487,6 +639,7 @@ async function runBotStatus(nameArg, opts) {
|
|
|
487
639
|
return;
|
|
488
640
|
}
|
|
489
641
|
console.log(`${chalk.bold(name)} ${row.agentUid}`);
|
|
642
|
+
console.log(` kind: ${describeKind(row.kind, row.companies)}`);
|
|
490
643
|
console.log(` runtime: ${row.runtime} model ${row.model ?? "default"} thinking ${row.effort}${row.effortIsDefault ? " (default)" : ""}`);
|
|
491
644
|
if (row.workerId)
|
|
492
645
|
console.log(` worker: ${row.workerId}${row.companySlug ? ` (company ${row.companySlug})` : ""} ${chalk.dim(row.workerDir)}`);
|
|
@@ -497,6 +650,10 @@ async function runBotStatus(nameArg, opts) {
|
|
|
497
650
|
console.log(` creds: ${creds ? `present for ${creds.entityUid}` : chalk.red("missing")}`);
|
|
498
651
|
if (status) {
|
|
499
652
|
console.log(` started: ${status.startedAt} restarts: ${status.restarts} replies: ${status.repliesSent}`);
|
|
653
|
+
if (status.runtimeSignIn?.state === "expired") {
|
|
654
|
+
const tool = runtimeDisplayName(status.runtimeSignIn.runtime);
|
|
655
|
+
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}\``);
|
|
656
|
+
}
|
|
500
657
|
if (status.lastError)
|
|
501
658
|
console.log(` last error: ${chalk.yellow(status.lastError)}`);
|
|
502
659
|
}
|
|
@@ -568,15 +725,22 @@ async function runBotRm(nameArg, opts) {
|
|
|
568
725
|
throw err;
|
|
569
726
|
}
|
|
570
727
|
const terminal = "alreadyGone" in identity ? true : identity.terminal;
|
|
728
|
+
// A company bot is a member of its companies: leave them too. Best effort —
|
|
729
|
+
// a stale membership row must not keep the bot's local state around.
|
|
730
|
+
const memberships = effectiveBotCompanies(config);
|
|
731
|
+
const left = memberships.length > 0 ? await removeBotFromCompanies({ agentUid: config.agentUid, companies: memberships, token }) : { done: [], failed: [] };
|
|
571
732
|
deleteBotCreds(dir);
|
|
572
733
|
if (terminal)
|
|
573
734
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
574
735
|
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));
|
|
736
|
+
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
737
|
return;
|
|
577
738
|
}
|
|
739
|
+
for (const f of left.failed) {
|
|
740
|
+
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}`));
|
|
741
|
+
}
|
|
578
742
|
if (terminal) {
|
|
579
|
-
console.log(chalk.green(`Removed bot ${name}`) + chalk.dim(` (${config.agentUid}); worker folder kept at ${path.join(config.hqRoot, config.workerDir)}`));
|
|
743
|
+
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
744
|
}
|
|
581
745
|
else {
|
|
582
746
|
console.log(chalk.yellow(`Bot ${name} is stopped locally but its cloud identity is not fully removed yet.`));
|
|
@@ -668,6 +832,8 @@ export function registerBotCommand(program) {
|
|
|
668
832
|
bot
|
|
669
833
|
.command("create <name>")
|
|
670
834
|
.description("Provision a local bot identity, scaffold its worker folder, install its launchd agent, and start it")
|
|
835
|
+
.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`)
|
|
836
|
+
.option("--company <slug>", "Company bots: a company the bot is a member of (repeat for several); the owner invites it on creation", collectRepeated, [])
|
|
671
837
|
.option("--runtime <runtime>", `Model CLI to run the bot with: ${BOT_RUNTIMES.join("|")}`, "claude")
|
|
672
838
|
.option("--model <model>", "Model override passed to the runtime CLI")
|
|
673
839
|
.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 +846,13 @@ export function registerBotCommand(program) {
|
|
|
680
846
|
.option("--no-start", "Create without starting")
|
|
681
847
|
.option("--json", "Print machine-readable JSON")
|
|
682
848
|
.action((name, opts) => wrap(() => runBotCreate(name, opts))());
|
|
849
|
+
bot
|
|
850
|
+
.command("companies <name>")
|
|
851
|
+
.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)")
|
|
852
|
+
.option("--add <slug>", "Add the bot to this company (repeatable)", collectRepeated, [])
|
|
853
|
+
.option("--remove <slug>", "Remove the bot from this company (repeatable)", collectRepeated, [])
|
|
854
|
+
.option("--json", "Print machine-readable JSON")
|
|
855
|
+
.action((name, opts) => wrap(() => runBotCompanies(name, opts))());
|
|
683
856
|
bot
|
|
684
857
|
.command("workers")
|
|
685
858
|
.description("List the workers (templates) a bot can be created from (hq bot create <name> --worker <id>)")
|