@indigoai-us/hq-cli 5.109.15 → 5.110.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/assets/bot-workers/setup/context/USER-GUIDE.md +363 -0
  3. package/assets/bot-workers/setup/context/quick-reference.md +199 -0
  4. package/assets/bot-workers/setup/skills/first-company.md +71 -0
  5. package/assets/bot-workers/setup/skills/standing-help.md +74 -0
  6. package/assets/bot-workers/setup/worker.yaml +422 -0
  7. package/dist/commands/bot-continuity.d.ts +28 -0
  8. package/dist/commands/bot-continuity.js +68 -0
  9. package/dist/commands/bot.d.ts +73 -0
  10. package/dist/commands/bot.js +776 -0
  11. package/dist/commands/workers.d.ts +2 -14
  12. package/dist/commands/workers.js +2 -8
  13. package/dist/lib/bot/api.d.ts +202 -0
  14. package/dist/lib/bot/api.js +202 -0
  15. package/dist/lib/bot/company-bind.d.ts +27 -0
  16. package/dist/lib/bot/company-bind.js +62 -0
  17. package/dist/lib/bot/config.d.ts +106 -0
  18. package/dist/lib/bot/config.js +141 -0
  19. package/dist/lib/bot/continuity-download.d.ts +28 -0
  20. package/dist/lib/bot/continuity-download.js +75 -0
  21. package/dist/lib/bot/continuity-install.d.ts +14 -0
  22. package/dist/lib/bot/continuity-install.js +101 -0
  23. package/dist/lib/bot/continuity.d.ts +66 -0
  24. package/dist/lib/bot/continuity.js +301 -0
  25. package/dist/lib/bot/creds.d.ts +24 -0
  26. package/dist/lib/bot/creds.js +51 -0
  27. package/dist/lib/bot/daemon.d.ts +75 -0
  28. package/dist/lib/bot/daemon.js +316 -0
  29. package/dist/lib/bot/inbox-state.d.ts +18 -0
  30. package/dist/lib/bot/inbox-state.js +51 -0
  31. package/dist/lib/bot/index.d.ts +16 -0
  32. package/dist/lib/bot/index.js +16 -0
  33. package/dist/lib/bot/inflight.d.ts +40 -0
  34. package/dist/lib/bot/inflight.js +44 -0
  35. package/dist/lib/bot/log.d.ts +13 -0
  36. package/dist/lib/bot/log.js +59 -0
  37. package/dist/lib/bot/owner-context.d.ts +75 -0
  38. package/dist/lib/bot/owner-context.js +151 -0
  39. package/dist/lib/bot/paths.d.ts +61 -0
  40. package/dist/lib/bot/paths.js +103 -0
  41. package/dist/lib/bot/progress.d.ts +84 -0
  42. package/dist/lib/bot/progress.js +167 -0
  43. package/dist/lib/bot/promote.d.ts +16 -0
  44. package/dist/lib/bot/promote.js +106 -0
  45. package/dist/lib/bot/promotion-hold.d.ts +24 -0
  46. package/dist/lib/bot/promotion-hold.js +103 -0
  47. package/dist/lib/bot/promotion-receipt.d.ts +9 -0
  48. package/dist/lib/bot/promotion-receipt.js +56 -0
  49. package/dist/lib/bot/promotion-upload.d.ts +16 -0
  50. package/dist/lib/bot/promotion-upload.js +65 -0
  51. package/dist/lib/bot/prompt.d.ts +103 -0
  52. package/dist/lib/bot/prompt.js +329 -0
  53. package/dist/lib/bot/room-policy.d.ts +53 -0
  54. package/dist/lib/bot/room-policy.js +73 -0
  55. package/dist/lib/bot/run.d.ts +98 -0
  56. package/dist/lib/bot/run.js +787 -0
  57. package/dist/lib/bot/runtime/claude.d.ts +49 -0
  58. package/dist/lib/bot/runtime/claude.js +151 -0
  59. package/dist/lib/bot/runtime/codex.d.ts +28 -0
  60. package/dist/lib/bot/runtime/codex.js +147 -0
  61. package/dist/lib/bot/runtime/grok.d.ts +16 -0
  62. package/dist/lib/bot/runtime/grok.js +67 -0
  63. package/dist/lib/bot/runtime/index.d.ts +35 -0
  64. package/dist/lib/bot/runtime/index.js +279 -0
  65. package/dist/lib/bot/runtime/messages-stream.d.ts +27 -0
  66. package/dist/lib/bot/runtime/messages-stream.js +85 -0
  67. package/dist/lib/bot/runtime/types.d.ts +136 -0
  68. package/dist/lib/bot/runtime/types.js +51 -0
  69. package/dist/lib/bot/scaffold.d.ts +38 -0
  70. package/dist/lib/bot/scaffold.js +94 -0
  71. package/dist/lib/bot/session.d.ts +19 -0
  72. package/dist/lib/bot/session.js +39 -0
  73. package/dist/lib/bot/status.d.ts +40 -0
  74. package/dist/lib/bot/status.js +66 -0
  75. package/dist/lib/bot/worker-source.d.ts +66 -0
  76. package/dist/lib/bot/worker-source.js +283 -0
  77. package/dist/lib/workers-registry/read.d.ts +15 -0
  78. package/dist/lib/workers-registry/read.js +17 -0
  79. package/dist/register-all.js +2 -0
  80. package/package.json +3 -2
@@ -0,0 +1,776 @@
1
+ import { OwnerContextCache, lookupOwnerContext } from "../lib/bot/owner-context.js";
2
+ import { hasActivePromotion } from "../lib/bot/promotion-receipt.js";
3
+ import { hasPromotionHold, promotionHoldSummary } from "../lib/bot/promotion-hold.js";
4
+ /**
5
+ * `hq bot` — personal local bots (local-bots US-003/US-004/US-005).
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]
8
+ * hq bot workers [--json] (company + core workers a bot can be created from)
9
+ * hq bot list [--json]
10
+ * hq bot start|stop|restart <name>
11
+ * hq bot status <name> [--json]
12
+ * hq bot logs <name> [--follow] [-n N]
13
+ * hq bot run <name> (the resident process; launchd runs this)
14
+ * hq bot daemon install|uninstall|status <name>
15
+ * hq bot rm <name> [--yes]
16
+ * hq bot intro <name> (re-send the introduction DM)
17
+ *
18
+ * The bot's own identity (agt_) authenticates with 0600 machine creds under
19
+ * ~/.hq/bots/<name>/; the OWNER's cached session is used only for create,
20
+ * list (online flag), and rm. Secrets are never printed.
21
+ */
22
+ import { promoteLocalBot } from "../lib/bot/promote.js";
23
+ import { registerBotContinuityCommand } from "./bot-continuity.js";
24
+ import chalk from "chalk";
25
+ import { execFileSync, spawn } from "node:child_process";
26
+ import * as fs from "node:fs";
27
+ import * as path from "node:path";
28
+ import * as readline from "node:readline";
29
+ import { DEFAULT_COGNITO, ensureCognitoToken, resolveDefaultHqRoot, } from "../utils/cognito-session.js";
30
+ import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
31
+ import { resolveCallerPersonUid } from "../utils/vault-api.js";
32
+ 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";
34
+ function fail(message) {
35
+ console.error(chalk.red(message));
36
+ process.exit(1);
37
+ }
38
+ function wrap(action) {
39
+ return async () => {
40
+ try {
41
+ await action();
42
+ }
43
+ catch (err) {
44
+ fail(err instanceof Error ? err.message : String(err));
45
+ }
46
+ };
47
+ }
48
+ /** Real launchctl; every failure is a non-ok result, never a throw. */
49
+ export const defaultLaunchctl = (args) => {
50
+ try {
51
+ const stdout = execFileSync("launchctl", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
52
+ return { ok: true, stdout, stderr: "" };
53
+ }
54
+ catch (err) {
55
+ const e = err;
56
+ return { ok: false, stdout: String(e.stdout ?? ""), stderr: String(e.stderr ?? e.message ?? "") };
57
+ }
58
+ };
59
+ /** The desktop app creates HQ's setup bot under this name, always from the `setup` worker. */
60
+ export const RESERVED_SETUP_BOT_NAME = "setup";
61
+ function daemonDeps() {
62
+ return process.platform === "darwin" ? { launchctl: defaultLaunchctl } : {};
63
+ }
64
+ export function listBotNames(root) {
65
+ if (!fs.existsSync(root))
66
+ return [];
67
+ return fs
68
+ .readdirSync(root, { withFileTypes: true })
69
+ .filter((d) => d.isDirectory() && readBotConfig(path.join(root, d.name)) !== null)
70
+ .map((d) => d.name)
71
+ .sort();
72
+ }
73
+ function requireBot(name) {
74
+ const valid = validateBotName(name);
75
+ const dir = botDir(valid);
76
+ const config = readBotConfig(dir);
77
+ if (!config) {
78
+ throw Object.assign(new Error(`No bot named "${valid}". Create one with: hq bot create ${valid}`), {
79
+ expected: true,
80
+ });
81
+ }
82
+ return { name: valid, dir, config };
83
+ }
84
+ /** Bot-identity token supplier: per-bot creds + per-bot token cache dir. */
85
+ export function botTokenSupplier(dir) {
86
+ process.env.HQ_MACHINE_CREDS_FILE = botCredsPath(dir);
87
+ process.env.HQ_MACHINE_TOKEN_STATE_DIR = botTokenStateDir(dir);
88
+ return () => ensureCognitoToken({ tokenSource: "machine", interactive: false });
89
+ }
90
+ async function ownerToken(interactive = process.stdin.isTTY === true) {
91
+ // Under the desktop app (no TTY) never open a browser login; the app owns
92
+ // sign-in and shares the same cached session file.
93
+ const cred = await resolveVaultCredential({ interactive });
94
+ if (cred.kind !== "cognito") {
95
+ throw Object.assign(new Error("hq bot needs your HQ login session (HQ_API_KEY is not supported here). Run `hq login`."), {
96
+ expected: true,
97
+ });
98
+ }
99
+ return cred.token;
100
+ }
101
+ function rowFor(name, opts = {}) {
102
+ const dir = botDir(name);
103
+ const config = readBotConfig(dir);
104
+ const status = readBotStatus(dir);
105
+ const pid = status?.pid ?? null;
106
+ const processAlive = pid !== null && isPidAlive(pid) && status?.state === "running";
107
+ let daemonInstalled = false;
108
+ let daemonLoaded = false;
109
+ if (opts.withDaemon !== false) {
110
+ try {
111
+ const d = botDaemonStatus(buildBotDaemonPaths({ name, botDir: dir, hqRoot: config.hqRoot }), daemonDeps());
112
+ daemonInstalled = d.installed;
113
+ daemonLoaded = d.loaded;
114
+ }
115
+ catch {
116
+ /* hq binary unresolvable in odd environments — daemon status unknown */
117
+ }
118
+ }
119
+ return {
120
+ name,
121
+ agentUid: config.agentUid,
122
+ ownerUid: config.ownerUid,
123
+ runtime: config.runtime,
124
+ model: config.model,
125
+ effort: effectiveBotEffort(config),
126
+ effortIsDefault: !config.effort,
127
+ state: processAlive ? "running" : status?.state === "failed" ? "failed" : "stopped",
128
+ pid: processAlive ? pid : null,
129
+ processAlive,
130
+ hosting: hasActivePromotion(dir, config.agentUid, config.ownerUid) ? "cloud" : "local",
131
+ promotionHold: promotionHoldSummary(dir, config.agentUid, config.ownerUid),
132
+ online: null,
133
+ lastHeartbeatAt: status?.lastHeartbeatAt ?? null,
134
+ daemonInstalled,
135
+ daemonLoaded,
136
+ dir,
137
+ introSentAt: config.introSentAt,
138
+ workerSource: config.workerSource ?? "scaffold",
139
+ workerDir: config.workerDir,
140
+ ...(config.workerId ? { workerId: config.workerId } : {}),
141
+ ...(config.companySlug ? { companySlug: config.companySlug } : {}),
142
+ memory: botMemoryMode(config),
143
+ memoryDir: resolveBotMemoryDir(config),
144
+ ...(config.intro ? { intro: config.intro } : {}),
145
+ ...(config.kickoff ? { kickoff: config.kickoff } : {}),
146
+ };
147
+ }
148
+ async function decorateOnline(rows) {
149
+ if (rows.length === 0)
150
+ return;
151
+ let token;
152
+ try {
153
+ token = await ownerToken(false);
154
+ }
155
+ catch {
156
+ return;
157
+ }
158
+ const api = new BotApi({ token: async () => token });
159
+ await Promise.all(rows.map(async (row) => {
160
+ try {
161
+ const rec = await api.getAgent(row.agentUid);
162
+ row.online = rec.online === true;
163
+ row.lastHeartbeatAt = rec.runtime?.lastHeartbeat?.at ?? row.lastHeartbeatAt;
164
+ }
165
+ catch {
166
+ row.online = null;
167
+ }
168
+ }));
169
+ }
170
+ function humanOnline(row) {
171
+ if (row.online === true)
172
+ return chalk.green("online");
173
+ if (row.online === false)
174
+ return chalk.gray("offline");
175
+ return chalk.gray("unknown");
176
+ }
177
+ /** Spawn `hq bot run <name>` detached (no launchd). */
178
+ export function spawnDetachedRun(name, dir) {
179
+ const entry = process.argv[1] && fs.existsSync(process.argv[1]) ? process.argv[1] : resolveHqBinary();
180
+ fs.mkdirSync(path.dirname(botLogPath(dir)), { recursive: true, mode: 0o700 });
181
+ const fd = fs.openSync(botLogPath(dir), "a");
182
+ const child = spawn(process.execPath, [entry, "bot", "run", name], {
183
+ detached: true,
184
+ stdio: ["ignore", fd, fd],
185
+ env: { ...process.env, HQ_NO_UPDATE_CHECK: "1" },
186
+ });
187
+ fs.closeSync(fd);
188
+ child.unref();
189
+ if (!child.pid)
190
+ throw new Error("Unable to start the bot process");
191
+ return child.pid;
192
+ }
193
+ async function waitFor(pred, timeoutMs, stepMs = 250) {
194
+ const until = Date.now() + timeoutMs;
195
+ while (Date.now() < until) {
196
+ if (pred())
197
+ return true;
198
+ await new Promise((r) => setTimeout(r, stepMs));
199
+ }
200
+ return pred();
201
+ }
202
+ function stopProcess(dir) {
203
+ const status = readBotStatus(dir);
204
+ if (!status || !isPidAlive(status.pid))
205
+ return false;
206
+ try {
207
+ process.kill(status.pid, "SIGTERM");
208
+ return true;
209
+ }
210
+ catch {
211
+ return false;
212
+ }
213
+ }
214
+ async function startBot(name, dir, config, json, extra = {}) {
215
+ if (hasPromotionHold(dir))
216
+ throw new Error("This bot is held for cloud promotion. Continue its promotion before changing local settings or lifecycle.");
217
+ if (config.enabled === false)
218
+ patchBotConfig(dir, { enabled: true });
219
+ const paths = buildBotDaemonPaths({ name, botDir: dir, hqRoot: config.hqRoot });
220
+ const d = botDaemonStatus(paths, daemonDeps());
221
+ let how;
222
+ if (d.installed && process.platform === "darwin") {
223
+ const r = startBotDaemon(paths, daemonDeps());
224
+ how = r.message;
225
+ }
226
+ else {
227
+ const before = readBotStatus(dir);
228
+ if (before && isPidAlive(before.pid) && before.state === "running") {
229
+ how = `already running (pid ${before.pid})`;
230
+ }
231
+ else {
232
+ how = `started pid ${spawnDetachedRun(name, dir)}`;
233
+ }
234
+ }
235
+ const running = await waitFor(() => {
236
+ const s = readBotStatus(dir);
237
+ return !!s && s.state === "running" && isPidAlive(s.pid);
238
+ }, 15_000);
239
+ const status = readBotStatus(dir);
240
+ if (json) {
241
+ console.log(JSON.stringify({ ok: running, name, ...extra, how, state: status?.state ?? "unknown", pid: status?.pid ?? null, lastError: status?.lastError ?? null }, null, 2));
242
+ return;
243
+ }
244
+ if (running)
245
+ console.log(chalk.green(`${name} is running`) + chalk.dim(` (${how})`));
246
+ else {
247
+ console.log(chalk.yellow(`${name} did not report running within 15s`) + chalk.dim(` (${how})`));
248
+ if (status?.lastError)
249
+ console.log(chalk.dim(` last error: ${status.lastError}`));
250
+ console.log(chalk.dim(` logs: hq bot logs ${name}`));
251
+ }
252
+ }
253
+ /**
254
+ * `output`: "json" | "text" print a result; "silent" prints nothing — used
255
+ * when stop is a step inside rm/restart, whose own JSON must be the only
256
+ * document on stdout (the desktop app parses it; a second object made it
257
+ * "unreadable output: trailing characters").
258
+ */
259
+ async function stopBot(name, dir, config, output) {
260
+ if (hasPromotionHold(dir))
261
+ throw new Error("This bot is held for cloud promotion. Continue its promotion before changing local settings or lifecycle.");
262
+ const paths = buildBotDaemonPaths({ name, botDir: dir, hqRoot: config.hqRoot });
263
+ if (process.platform === "darwin")
264
+ stopBotDaemon(paths, daemonDeps());
265
+ const signalled = stopProcess(dir);
266
+ const gone = await waitFor(() => {
267
+ const s = readBotStatus(dir);
268
+ return !s || !isPidAlive(s.pid);
269
+ }, 10_000);
270
+ if (!gone) {
271
+ const s = readBotStatus(dir);
272
+ if (s) {
273
+ try {
274
+ process.kill(s.pid, "SIGKILL");
275
+ }
276
+ catch {
277
+ /* ignore */
278
+ }
279
+ }
280
+ }
281
+ if (output === "silent")
282
+ return;
283
+ if (output === "json") {
284
+ console.log(JSON.stringify({ ok: true, name, signalled, stopped: gone }, null, 2));
285
+ return;
286
+ }
287
+ console.log(signalled || gone ? chalk.green(`${name} stopped`) : chalk.dim(`${name} was not running`));
288
+ }
289
+ // ── create ───────────────────────────────────────────────────────────────────
290
+ export async function runBotCreate(nameArg, opts) {
291
+ const name = validateBotName(nameArg);
292
+ const runtime = opts.runtime ?? "claude";
293
+ if (!isBotRuntimeId(runtime)) {
294
+ throw Object.assign(new Error(`--runtime must be one of ${BOT_RUNTIMES.join(", ")}`), { expected: true });
295
+ }
296
+ const memoryMode = opts.memory ?? "synced";
297
+ if (!isBotMemoryMode(memoryMode)) {
298
+ throw Object.assign(new Error(`--memory must be one of ${BOT_MEMORY_MODES.join(", ")}`), { expected: true });
299
+ }
300
+ const intro = opts.intro !== undefined ? validateBotIntro(opts.intro) : undefined;
301
+ const kickoff = opts.kickoff !== undefined ? validateBotKickoff(opts.kickoff) : undefined;
302
+ const model = opts.model !== undefined ? validateBotModel(opts.model) : undefined;
303
+ const effort = opts.effort !== undefined ? validateBotEffort(runtime, opts.effort) : undefined;
304
+ if (name === RESERVED_SETUP_BOT_NAME && opts.worker?.trim() !== RESERVED_SETUP_BOT_NAME) {
305
+ throw Object.assign(new Error(`The name ${RESERVED_SETUP_BOT_NAME} is reserved for HQ's setup bot; create it with --worker ${RESERVED_SETUP_BOT_NAME}.`), { expected: true });
306
+ }
307
+ const dir = botDir(name);
308
+ if (readBotConfig(dir)) {
309
+ throw Object.assign(new Error(`Bot "${name}" already exists (${dir}). Use hq bot start ${name} or hq bot rm ${name}.`), {
310
+ expected: true,
311
+ });
312
+ }
313
+ const hqRoot = resolveDefaultHqRoot({ onMissing: "throw" });
314
+ // Resolve the company worker before provisioning anything (a typo must not
315
+ // cost an identity).
316
+ const worker = opts.worker?.trim() ? resolveBotWorker(hqRoot, opts.worker) : null;
317
+ const token = await ownerToken();
318
+ const owner = await resolveCallerPersonUid(token);
319
+ if (!owner)
320
+ throw Object.assign(new Error("Your HQ login has no person entity yet; open the HQ desktop app once, then retry."), { expected: true });
321
+ const api = new BotApi({ token: async () => token });
322
+ let created;
323
+ try {
324
+ created = await api.createLocalBot({ name });
325
+ }
326
+ catch (err) {
327
+ if (err instanceof BotApiError && err.code === "LOCAL_BOT_CAP_REACHED") {
328
+ // Only an older server still enforces a cap; current hq-pro-agents has none.
329
+ 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."), {
330
+ expected: true,
331
+ });
332
+ }
333
+ throw err;
334
+ }
335
+ const agentUid = created.agent.uid;
336
+ writeBotCreds(dir, {
337
+ username: created.identity.cognitoUsername,
338
+ secret: created.identity.secret,
339
+ entityType: "agent",
340
+ entityUid: agentUid,
341
+ clientId: DEFAULT_COGNITO.clientId,
342
+ region: DEFAULT_COGNITO.region,
343
+ });
344
+ const config = {
345
+ v: 1,
346
+ name,
347
+ agentUid,
348
+ ownerUid: created.agent.ownerUid ?? owner,
349
+ runtime,
350
+ ...(model ? { model } : {}),
351
+ ...(effort ? { effort } : {}),
352
+ ...(opts.autoApprove === false ? { autoApprove: false } : {}),
353
+ hqRoot,
354
+ ...(worker
355
+ ? {
356
+ workerDir: worker.path,
357
+ workerSource: "worker",
358
+ workerId: worker.id,
359
+ ...(worker.company ? { companySlug: worker.company } : {}),
360
+ }
361
+ : { workerDir: botWorkerRelDir(name) }),
362
+ // "local" memory is an absolute path outside the synced HQ tree; "synced"
363
+ // memory is hqRoot-relative (and, for a worker-sourced bot, the bot's own
364
+ // personal/workers/<name>/memory so the shared company tree is never
365
+ // written to).
366
+ ...(memoryMode === "local"
367
+ ? { memoryDir: botLocalMemoryDir(dir) }
368
+ : worker
369
+ ? { memoryDir: path.join(botWorkerRelDir(name), "memory") }
370
+ : {}),
371
+ ...(intro ? { intro } : {}),
372
+ ...(kickoff ? { kickoff } : {}),
373
+ createdAt: new Date().toISOString(),
374
+ enabled: true,
375
+ };
376
+ writeBotConfig(dir, config);
377
+ // A worker-sourced bot never writes into the company tree: only its own
378
+ // memory folder is scaffolded.
379
+ const scaffoldOpts = { memoryDir: config.memoryDir };
380
+ const scaffold = worker
381
+ ? scaffoldBotMemory(hqRoot, name, undefined, scaffoldOpts)
382
+ : scaffoldBotWorker(hqRoot, name, agentUid, runtime, undefined, scaffoldOpts);
383
+ createBotLogger(dir)("info", `created ${name} (${agentUid}) runtime=${runtime}${worker ? ` worker=${worker.id} company=${worker.company ?? "-"}` : ""} memory=${memoryMode}`);
384
+ const created_ = {
385
+ agentUid,
386
+ runtime,
387
+ dir,
388
+ worker: worker ? path.join(hqRoot, worker.path) : scaffold.dir,
389
+ workerId: worker?.id ?? null,
390
+ companySlug: worker?.company ?? null,
391
+ memory: memoryMode,
392
+ memoryDir: scaffold.memoryDir,
393
+ intro: intro ?? null,
394
+ kickoff: kickoff ?? null,
395
+ model: model ?? null,
396
+ effort: effort ?? DEFAULT_BOT_EFFORT,
397
+ };
398
+ let daemonMessage = null;
399
+ if (opts.daemon !== false && process.platform === "darwin") {
400
+ const r = installBotDaemon(buildBotDaemonPaths({ name, botDir: dir, hqRoot }), daemonDeps());
401
+ daemonMessage = r.message;
402
+ }
403
+ if (!opts.json) {
404
+ console.log(chalk.green(`Created bot ${chalk.bold(name)}`) + chalk.dim(` (${agentUid}, ${runtime})`));
405
+ if (worker)
406
+ console.log(chalk.dim(` worker: ${worker.id}${worker.company ? ` (${worker.company})` : ""} — ${path.join(hqRoot, worker.path)}`));
407
+ if (!worker)
408
+ console.log(chalk.dim(` worker: ${scaffold.dir}`));
409
+ console.log(chalk.dim(` memory: ${scaffold.memoryDir} (${memoryMode})`));
410
+ console.log(chalk.dim(` state: ${dir}`));
411
+ if (daemonMessage)
412
+ console.log(chalk.dim(` ${daemonMessage}`));
413
+ }
414
+ if (opts.start !== false) {
415
+ if (daemonMessage && /loaded\)/.test(daemonMessage)) {
416
+ // RunAtLoad already started it.
417
+ const running = await waitFor(() => readBotStatus(dir)?.state === "running", 20_000);
418
+ if (opts.json) {
419
+ console.log(JSON.stringify({ ok: true, name, ...created_, daemon: daemonMessage, running }, null, 2));
420
+ }
421
+ else if (running)
422
+ console.log(chalk.green(`${name} is online — it will DM you an introduction shortly.`));
423
+ else
424
+ console.log(chalk.yellow(`${name} has not reported running yet; check hq bot logs ${name}`));
425
+ return;
426
+ }
427
+ await startBot(name, dir, config, Boolean(opts.json), { ...created_, daemon: daemonMessage });
428
+ return;
429
+ }
430
+ if (opts.json) {
431
+ console.log(JSON.stringify({ ok: true, name, ...created_, daemon: daemonMessage, running: false }, null, 2));
432
+ }
433
+ }
434
+ // ── workers ──────────────────────────────────────────────────────────────────
435
+ export async function runBotWorkers(opts) {
436
+ const hqRoot = resolveDefaultHqRoot({ onMissing: "throw" });
437
+ const workers = listBotWorkerOptions(hqRoot);
438
+ if (opts.json) {
439
+ console.log(JSON.stringify({ workers }, null, 2));
440
+ return;
441
+ }
442
+ if (workers.length === 0) {
443
+ console.log(chalk.gray("No workers in this HQ (a worker needs a worker.yaml under companies/<co>/workers/<id>/ or core/workers/)."));
444
+ return;
445
+ }
446
+ for (const w of workers) {
447
+ console.log(`${chalk.bold(w.id.padEnd(28))} ${(w.company ?? (w.source === "core" ? "core" : "-")).padEnd(14)} ${w.type.padEnd(12)} ${chalk.dim(w.summary.slice(0, 80))}`);
448
+ }
449
+ console.log(chalk.dim(`\nCreate a bot from one: hq bot create <name> --worker <id>`));
450
+ }
451
+ // ── run (resident) ───────────────────────────────────────────────────────────
452
+ async function runBotResident(nameArg) {
453
+ const { name, dir, config } = requireBot(nameArg);
454
+ const api = new BotApi({ token: botTokenSupplier(dir) });
455
+ const owner = new OwnerContextCache(() => lookupOwnerContext(config.ownerUid), Date.now, undefined, config.ownerUid);
456
+ const handle = await runBot({ dir, config, api, runtime: runtimeFor(config.runtime), ownerContext: () => owner.get() });
457
+ await handle.done;
458
+ void name;
459
+ }
460
+ // ── list / status / logs ─────────────────────────────────────────────────────
461
+ async function runBotList(opts) {
462
+ const rows = listBotNames(botsRoot()).map((n) => rowFor(n));
463
+ await decorateOnline(rows);
464
+ if (opts.json) {
465
+ console.log(JSON.stringify({ bots: rows }, null, 2));
466
+ return;
467
+ }
468
+ if (rows.length === 0) {
469
+ console.log(chalk.gray("No local bots yet. Create one with: hq bot create assistant"));
470
+ return;
471
+ }
472
+ for (const r of rows) {
473
+ 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})` : ""}`) : ""}`);
474
+ }
475
+ }
476
+ async function runBotStatus(nameArg, opts) {
477
+ const { name, dir } = requireBot(nameArg);
478
+ const row = rowFor(name);
479
+ await decorateOnline([row]);
480
+ const status = readBotStatus(dir);
481
+ const creds = readBotCredsIdentity(dir);
482
+ if (opts.json) {
483
+ console.log(JSON.stringify({ ...row, credsPresent: !!creds, status }, null, 2));
484
+ return;
485
+ }
486
+ console.log(`${chalk.bold(name)} ${row.agentUid}`);
487
+ console.log(` runtime: ${row.runtime} model ${row.model ?? "default"} thinking ${row.effort}${row.effortIsDefault ? " (default)" : ""}`);
488
+ if (row.workerId)
489
+ console.log(` worker: ${row.workerId}${row.companySlug ? ` (company ${row.companySlug})` : ""} ${chalk.dim(row.workerDir)}`);
490
+ console.log(` memory: ${row.memory} ${chalk.dim(row.memoryDir)}`);
491
+ console.log(` process: ${row.processAlive ? chalk.green(`running (pid ${row.pid})`) : chalk.gray(row.state)}`);
492
+ console.log(` HQ online: ${humanOnline(row)}${row.lastHeartbeatAt ? chalk.dim(` last heartbeat ${row.lastHeartbeatAt}`) : ""}`);
493
+ console.log(` launchd: ${row.daemonInstalled ? (row.daemonLoaded ? "installed, loaded" : "installed, not loaded") : "not installed"}`);
494
+ console.log(` creds: ${creds ? `present for ${creds.entityUid}` : chalk.red("missing")}`);
495
+ if (status) {
496
+ console.log(` started: ${status.startedAt} restarts: ${status.restarts} replies: ${status.repliesSent}`);
497
+ if (status.lastError)
498
+ console.log(` last error: ${chalk.yellow(status.lastError)}`);
499
+ }
500
+ console.log(chalk.dim(` state dir: ${dir}`));
501
+ }
502
+ async function runBotLogs(nameArg, opts) {
503
+ const { dir } = requireBot(nameArg);
504
+ const file = botLogPath(dir);
505
+ const n = Math.max(1, Number.parseInt(opts.lines ?? "50", 10) || 50);
506
+ if (!fs.existsSync(file)) {
507
+ console.log(chalk.gray("No log yet."));
508
+ return;
509
+ }
510
+ const initial = fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
511
+ for (const line of initial.slice(-n))
512
+ console.log(line);
513
+ if (!opts.follow)
514
+ return;
515
+ let size = fs.statSync(file).size;
516
+ await new Promise(() => {
517
+ fs.watchFile(file, { interval: 500 }, (curr) => {
518
+ if (curr.size < size)
519
+ size = 0;
520
+ if (curr.size > size) {
521
+ const fd = fs.openSync(file, "r");
522
+ const buf = Buffer.alloc(curr.size - size);
523
+ fs.readSync(fd, buf, 0, buf.length, size);
524
+ fs.closeSync(fd);
525
+ process.stdout.write(buf.toString());
526
+ size = curr.size;
527
+ }
528
+ });
529
+ });
530
+ }
531
+ // ── rm ───────────────────────────────────────────────────────────────────────
532
+ async function confirm(question) {
533
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
534
+ return new Promise((resolve) => {
535
+ rl.question(question, (answer) => {
536
+ rl.close();
537
+ resolve(/^y(es)?$/i.test(answer.trim()));
538
+ });
539
+ });
540
+ }
541
+ async function runBotRm(nameArg, opts) {
542
+ const { name, dir, config } = requireBot(nameArg);
543
+ if (hasPromotionHold(dir))
544
+ throw new Error("This bot is held for cloud promotion. Continue its promotion before changing local settings or lifecycle.");
545
+ if (!opts.yes) {
546
+ const ok = await confirm(`Remove bot "${name}" (${config.agentUid}), its identity, and its local state? [y/N] `);
547
+ if (!ok) {
548
+ console.log(chalk.dim("Cancelled."));
549
+ return;
550
+ }
551
+ }
552
+ await stopBot(name, dir, config, "silent").catch(() => { });
553
+ const paths = buildBotDaemonPaths({ name, botDir: dir, hqRoot: config.hqRoot });
554
+ const daemon = uninstallBotDaemon(paths, daemonDeps());
555
+ const token = await ownerToken();
556
+ const api = new BotApi({ token: async () => token });
557
+ let identity;
558
+ try {
559
+ identity = await api.deleteLocalBot(config.agentUid);
560
+ }
561
+ catch (err) {
562
+ if (err instanceof BotApiError && err.status === 404)
563
+ identity = { alreadyGone: true };
564
+ else
565
+ throw err;
566
+ }
567
+ const terminal = "alreadyGone" in identity ? true : identity.terminal;
568
+ deleteBotCreds(dir);
569
+ if (terminal)
570
+ fs.rmSync(dir, { recursive: true, force: true });
571
+ if (opts.json) {
572
+ 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));
573
+ return;
574
+ }
575
+ if (terminal) {
576
+ console.log(chalk.green(`Removed bot ${name}`) + chalk.dim(` (${config.agentUid}); worker folder kept at ${path.join(config.hqRoot, config.workerDir)}`));
577
+ }
578
+ else {
579
+ console.log(chalk.yellow(`Bot ${name} is stopped locally but its cloud identity is not fully removed yet.`));
580
+ if ("failures" in identity && identity.failures) {
581
+ for (const [step, msg] of Object.entries(identity.failures))
582
+ console.log(chalk.dim(` ${step}: ${msg}`));
583
+ }
584
+ console.log(chalk.dim(` Re-run: hq bot rm ${name} --yes`));
585
+ }
586
+ }
587
+ /**
588
+ * `hq bot set <name> --model <id|default> --effort <level|default>`: change
589
+ * what the bot thinks with. bot.json is re-read on every turn, so the next
590
+ * message uses it; no restart.
591
+ */
592
+ export async function runBotSet(nameArg, opts) {
593
+ const { name, dir, config } = requireBot(nameArg);
594
+ if (hasPromotionHold(dir))
595
+ throw new Error("This bot is held for cloud promotion. Continue its promotion before changing local settings or lifecycle.");
596
+ if (opts.model === undefined && opts.effort === undefined) {
597
+ throw Object.assign(new Error("Nothing to change: pass --model <id|default> and/or --effort <level|default>."), { expected: true });
598
+ }
599
+ const next = { ...config };
600
+ if (opts.model !== undefined) {
601
+ const model = validateBotModel(opts.model);
602
+ if (model)
603
+ next.model = model;
604
+ else
605
+ delete next.model;
606
+ }
607
+ if (opts.effort !== undefined) {
608
+ const effort = validateBotEffort(config.runtime, opts.effort);
609
+ if (effort)
610
+ next.effort = effort;
611
+ else
612
+ delete next.effort;
613
+ }
614
+ writeBotConfig(dir, next);
615
+ createBotLogger(dir)("info", `settings: model=${next.model ?? "default"} effort=${effectiveBotEffort(next)}${next.effort ? "" : " (default)"}`);
616
+ const result = {
617
+ ok: true,
618
+ name,
619
+ runtime: config.runtime,
620
+ model: next.model ?? null,
621
+ effort: effectiveBotEffort(next),
622
+ effortIsDefault: !next.effort,
623
+ effortLevels: BOT_EFFORT_LEVELS[config.runtime],
624
+ };
625
+ if (opts.json) {
626
+ console.log(JSON.stringify(result, null, 2));
627
+ return;
628
+ }
629
+ console.log(chalk.green(`${name} now thinks with ${result.model ?? "the default model"} at ${result.effort} effort (from its next message).`));
630
+ }
631
+ async function runBotIntro(nameArg) {
632
+ const { name, dir, config } = requireBot(nameArg);
633
+ if (hasPromotionHold(dir))
634
+ throw new Error("This bot is held for cloud promotion. Continue its promotion before changing local settings or lifecycle.");
635
+ const api = new BotApi({ token: botTokenSupplier(dir) });
636
+ await api.sendDm({ toPersonUid: config.ownerUid, body: introDmText(name, config.runtime, config.intro) });
637
+ patchBotConfig(dir, { introSentAt: new Date().toISOString() });
638
+ console.log(chalk.green(`${name} introduced itself to you.`));
639
+ }
640
+ // ── registration ─────────────────────────────────────────────────────────────
641
+ export function registerBotCommand(program) {
642
+ const bot = program
643
+ .command("bot")
644
+ .description("Personal local bots that run on this computer with your own model login");
645
+ registerBotContinuityCommand(bot);
646
+ bot.command("promote <name>")
647
+ .description("Move this local bot to cloud compute, preserving its identity and private context")
648
+ .requiredOption("--company <uid>", "Destination company identity")
649
+ .option("--json", "Print promotion progress as JSON")
650
+ .action((name, opts) => wrap(async () => {
651
+ const { dir, config } = requireBot(name);
652
+ const token = await ownerToken(false);
653
+ const api = new BotApi({ token: async () => token });
654
+ const result = await promoteLocalBot({ dir, config, companyUid: opts.company, api });
655
+ const details = result.pending ? await api.promotionDetails(config.agentUid, opts.company) : null;
656
+ if (opts.json)
657
+ console.log(JSON.stringify({ ...result, pairing: details?.pairing ?? null, ...(details?.failure ? { failure: details.failure } : {}) }));
658
+ else if (details?.failure)
659
+ console.log(`${name}: ${details.failure}`);
660
+ else if (details?.pairing)
661
+ console.log(`${name}: connect the cloud bot to your ChatGPT subscription at ${details.pairing.url}, using code ${details.pairing.code}. Then run the same command to continue.`);
662
+ else
663
+ console.log(`${name}: ${result.promotion.phase}.${result.pending ? " Run the same command to continue promotion." : ""}`);
664
+ })());
665
+ bot
666
+ .command("create <name>")
667
+ .description("Provision a local bot identity, scaffold its worker folder, install its launchd agent, and start it")
668
+ .option("--runtime <runtime>", `Model CLI to run the bot with: ${BOT_RUNTIMES.join("|")}`, "claude")
669
+ .option("--model <model>", "Model override passed to the runtime CLI")
670
+ .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})`)
671
+ .option("--no-auto-approve", "Keep the runtime's permission gates (default: everything pre-approved; a headless bot cannot answer prompts)")
672
+ .option("--worker <workerId>", "Run an existing company or core worker (see hq bot workers): its instructions, skills, context, and company policies; a company worker binds the bot to that company on every turn")
673
+ .option("--intro <text>", `The hello the bot DMs you when it first comes online (verbatim, up to ${BOT_INTRO_MAX_CHARS} chars)`)
674
+ .option("--kickoff <prompt>", `A first task the bot starts on by itself right after its hello: one model turn run as if you had sent it, answered in your DM, once (up to ${BOT_KICKOFF_MAX_CHARS} chars)`)
675
+ .option("--memory <where>", "Where the bot keeps its memory: synced (personal/workers/<name>/memory in HQ) or local (~/.hq/bots/<name>/memory, this computer only)", "synced")
676
+ .option("--no-daemon", "Do not install the launchd agent (macOS)")
677
+ .option("--no-start", "Create without starting")
678
+ .option("--json", "Print machine-readable JSON")
679
+ .action((name, opts) => wrap(() => runBotCreate(name, opts))());
680
+ bot
681
+ .command("workers")
682
+ .description("List the workers (templates) a bot can be created from (hq bot create <name> --worker <id>)")
683
+ .option("--json", "Print machine-readable JSON")
684
+ .action((opts) => wrap(() => runBotWorkers(opts))());
685
+ bot
686
+ .command("list")
687
+ .description("List local bots with runtime, online state, and pid")
688
+ .option("--json", "Print machine-readable JSON")
689
+ .action((opts) => wrap(() => runBotList(opts))());
690
+ bot
691
+ .command("start <name>")
692
+ .description("Start the bot (via launchd when installed, else a detached process)")
693
+ .option("--json", "Print machine-readable JSON")
694
+ .action((name, opts) => wrap(async () => {
695
+ const b = requireBot(name);
696
+ await startBot(b.name, b.dir, b.config, Boolean(opts.json));
697
+ })());
698
+ bot
699
+ .command("stop <name>")
700
+ .description("Stop the bot process (it starts again at next login while its launchd agent is installed)")
701
+ .option("--json", "Print machine-readable JSON")
702
+ .action((name, opts) => wrap(async () => {
703
+ const b = requireBot(name);
704
+ await stopBot(b.name, b.dir, b.config, opts.json ? "json" : "text");
705
+ })());
706
+ bot
707
+ .command("restart <name>")
708
+ .description("Stop then start the bot")
709
+ .option("--json", "Print machine-readable JSON")
710
+ .action((name, opts) => wrap(async () => {
711
+ const b = requireBot(name);
712
+ await stopBot(b.name, b.dir, b.config, "silent");
713
+ await startBot(b.name, b.dir, b.config, Boolean(opts.json));
714
+ })());
715
+ bot
716
+ .command("status <name>")
717
+ .description("Show the bot's process, heartbeat, launchd, and credential state")
718
+ .option("--json", "Print machine-readable JSON")
719
+ .action((name, opts) => wrap(() => runBotStatus(name, opts))());
720
+ bot
721
+ .command("logs <name>")
722
+ .description("Show the bot's log")
723
+ .option("-f, --follow", "Keep printing new lines")
724
+ .option("-n, --lines <n>", "Number of trailing lines", "50")
725
+ .action((name, opts) => wrap(() => runBotLogs(name, opts))());
726
+ bot
727
+ .command("run <name>")
728
+ .description("Run the bot in the foreground (this is what launchd runs)")
729
+ .action((name) => wrap(() => runBotResident(name))());
730
+ bot
731
+ .command("rm <name>")
732
+ .description("Stop the bot, remove its launchd agent, delete its cloud identity and local state")
733
+ .option("-y, --yes", "Skip the confirmation prompt")
734
+ .option("--json", "Print machine-readable JSON")
735
+ .action((name, opts) => wrap(() => runBotRm(name, opts))());
736
+ bot
737
+ .command("set <name>")
738
+ .description("Change the model and thinking level a bot uses (applies from its next message)")
739
+ .option("--model <model>", "Model id for the runtime CLI, or default for the CLI's own default")
740
+ .option("--effort <level>", `Thinking level (claude ${BOT_EFFORT_LEVELS.claude.join("|")}; codex ${BOT_EFFORT_LEVELS.codex.join("|")}; grok ${BOT_EFFORT_LEVELS.grok.join("|")}), or default (${DEFAULT_BOT_EFFORT})`)
741
+ .option("--json", "Print machine-readable JSON")
742
+ .action((name, opts) => wrap(() => runBotSet(name, opts))());
743
+ bot
744
+ .command("intro <name>")
745
+ .description("Have the bot re-send its introduction DM to you")
746
+ .action((name) => wrap(() => runBotIntro(name))());
747
+ const daemon = bot.command("daemon").description("Manage the bot's launchd user agent (macOS)");
748
+ daemon
749
+ .command("install <name>")
750
+ .description("Install and load the LaunchAgent so the bot starts at login and restarts on crash")
751
+ .option("--json", "Print machine-readable JSON")
752
+ .action((name, opts) => wrap(async () => {
753
+ const b = requireBot(name);
754
+ const r = installBotDaemon(buildBotDaemonPaths({ name: b.name, botDir: b.dir, hqRoot: b.config.hqRoot }), daemonDeps());
755
+ console.log(opts.json ? JSON.stringify({ ok: r.installed, ...r }, null, 2) : r.message);
756
+ })());
757
+ daemon
758
+ .command("uninstall <name>")
759
+ .description("Unload and remove the LaunchAgent")
760
+ .option("--json", "Print machine-readable JSON")
761
+ .action((name, opts) => wrap(async () => {
762
+ const b = requireBot(name);
763
+ const r = uninstallBotDaemon(buildBotDaemonPaths({ name: b.name, botDir: b.dir, hqRoot: b.config.hqRoot }), daemonDeps());
764
+ console.log(opts.json ? JSON.stringify({ ok: true, ...r }, null, 2) : r.message);
765
+ })());
766
+ daemon
767
+ .command("status <name>")
768
+ .description("Show whether the LaunchAgent is installed and loaded")
769
+ .option("--json", "Print machine-readable JSON")
770
+ .action((name, opts) => wrap(async () => {
771
+ const b = requireBot(name);
772
+ const r = botDaemonStatus(buildBotDaemonPaths({ name: b.name, botDir: b.dir, hqRoot: b.config.hqRoot }), daemonDeps());
773
+ console.log(opts.json ? JSON.stringify({ ok: true, ...r }, null, 2) : r.message);
774
+ })());
775
+ }
776
+ //# sourceMappingURL=bot.js.map