@indigoai-us/hq-cli 5.109.16 → 5.111.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 +38 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +363 -0
- package/assets/bot-workers/setup/context/quick-reference.md +199 -0
- package/assets/bot-workers/setup/skills/first-company.md +71 -0
- package/assets/bot-workers/setup/skills/standing-help.md +74 -0
- package/assets/bot-workers/setup/worker.yaml +422 -0
- package/dist/commands/bot-continuity.d.ts +28 -0
- package/dist/commands/bot-continuity.js +68 -0
- package/dist/commands/bot.d.ts +73 -0
- package/dist/commands/bot.js +776 -0
- package/dist/commands/skill.d.ts +12 -3
- package/dist/commands/skill.js +79 -3
- package/dist/commands/workers.d.ts +2 -14
- package/dist/commands/workers.js +2 -8
- package/dist/lib/bot/api.d.ts +202 -0
- package/dist/lib/bot/api.js +202 -0
- package/dist/lib/bot/company-bind.d.ts +27 -0
- package/dist/lib/bot/company-bind.js +62 -0
- package/dist/lib/bot/config.d.ts +106 -0
- package/dist/lib/bot/config.js +141 -0
- package/dist/lib/bot/continuity-download.d.ts +28 -0
- package/dist/lib/bot/continuity-download.js +75 -0
- package/dist/lib/bot/continuity-install.d.ts +14 -0
- package/dist/lib/bot/continuity-install.js +101 -0
- package/dist/lib/bot/continuity.d.ts +66 -0
- package/dist/lib/bot/continuity.js +301 -0
- package/dist/lib/bot/creds.d.ts +24 -0
- package/dist/lib/bot/creds.js +51 -0
- package/dist/lib/bot/daemon.d.ts +75 -0
- package/dist/lib/bot/daemon.js +316 -0
- package/dist/lib/bot/inbox-state.d.ts +18 -0
- package/dist/lib/bot/inbox-state.js +51 -0
- package/dist/lib/bot/index.d.ts +16 -0
- package/dist/lib/bot/index.js +16 -0
- package/dist/lib/bot/inflight.d.ts +40 -0
- package/dist/lib/bot/inflight.js +44 -0
- package/dist/lib/bot/log.d.ts +13 -0
- package/dist/lib/bot/log.js +59 -0
- package/dist/lib/bot/owner-context.d.ts +75 -0
- package/dist/lib/bot/owner-context.js +151 -0
- package/dist/lib/bot/paths.d.ts +61 -0
- package/dist/lib/bot/paths.js +103 -0
- package/dist/lib/bot/progress.d.ts +84 -0
- package/dist/lib/bot/progress.js +167 -0
- package/dist/lib/bot/promote.d.ts +16 -0
- package/dist/lib/bot/promote.js +106 -0
- package/dist/lib/bot/promotion-hold.d.ts +24 -0
- package/dist/lib/bot/promotion-hold.js +103 -0
- package/dist/lib/bot/promotion-receipt.d.ts +9 -0
- package/dist/lib/bot/promotion-receipt.js +56 -0
- package/dist/lib/bot/promotion-upload.d.ts +16 -0
- package/dist/lib/bot/promotion-upload.js +65 -0
- package/dist/lib/bot/prompt.d.ts +103 -0
- package/dist/lib/bot/prompt.js +329 -0
- package/dist/lib/bot/room-policy.d.ts +53 -0
- package/dist/lib/bot/room-policy.js +73 -0
- package/dist/lib/bot/run.d.ts +98 -0
- package/dist/lib/bot/run.js +787 -0
- package/dist/lib/bot/runtime/claude.d.ts +49 -0
- package/dist/lib/bot/runtime/claude.js +151 -0
- package/dist/lib/bot/runtime/codex.d.ts +28 -0
- package/dist/lib/bot/runtime/codex.js +147 -0
- package/dist/lib/bot/runtime/grok.d.ts +16 -0
- package/dist/lib/bot/runtime/grok.js +67 -0
- package/dist/lib/bot/runtime/index.d.ts +35 -0
- package/dist/lib/bot/runtime/index.js +279 -0
- package/dist/lib/bot/runtime/messages-stream.d.ts +27 -0
- package/dist/lib/bot/runtime/messages-stream.js +85 -0
- package/dist/lib/bot/runtime/types.d.ts +136 -0
- package/dist/lib/bot/runtime/types.js +51 -0
- package/dist/lib/bot/scaffold.d.ts +38 -0
- package/dist/lib/bot/scaffold.js +94 -0
- package/dist/lib/bot/session.d.ts +19 -0
- package/dist/lib/bot/session.js +39 -0
- package/dist/lib/bot/status.d.ts +40 -0
- package/dist/lib/bot/status.js +66 -0
- package/dist/lib/bot/worker-source.d.ts +66 -0
- package/dist/lib/bot/worker-source.js +283 -0
- package/dist/lib/workers-registry/read.d.ts +15 -0
- package/dist/lib/workers-registry/read.js +17 -0
- package/dist/register-all.js +2 -0
- package/package.json +2 -1
|
@@ -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
|