@indigoai-us/hq-cli 5.115.6 → 5.117.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 (76) hide show
  1. package/CHANGELOG.md +169 -11
  2. package/dist/command-catalog.generated.d.ts +220 -2
  3. package/dist/command-catalog.generated.js +281 -2
  4. package/dist/command-registration-plan.d.ts +6 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/agent-enroll.d.ts +105 -0
  7. package/dist/commands/agent-enroll.js +273 -0
  8. package/dist/commands/agent-kit.d.ts +53 -0
  9. package/dist/commands/agent-kit.js +260 -0
  10. package/dist/commands/agent-mcp.d.ts +22 -0
  11. package/dist/commands/agent-mcp.js +104 -0
  12. package/dist/commands/agent-probe.d.ts +71 -0
  13. package/dist/commands/agent-probe.js +294 -0
  14. package/dist/commands/agent.d.ts +12 -0
  15. package/dist/commands/agent.js +23 -0
  16. package/dist/commands/agents.d.ts +27 -0
  17. package/dist/commands/agents.js +280 -6
  18. package/dist/commands/bot.d.ts +140 -1
  19. package/dist/commands/bot.js +757 -22
  20. package/dist/commands/secrets.js +17 -5
  21. package/dist/lib/agent-kit/creds.d.ts +60 -0
  22. package/dist/lib/agent-kit/creds.js +123 -0
  23. package/dist/lib/agent-kit/kit-config.d.ts +29 -0
  24. package/dist/lib/agent-kit/kit-config.js +54 -0
  25. package/dist/lib/agent-kit/log.d.ts +17 -0
  26. package/dist/lib/agent-kit/log.js +46 -0
  27. package/dist/lib/agent-kit/mcp/jsonrpc.d.ts +84 -0
  28. package/dist/lib/agent-kit/mcp/jsonrpc.js +164 -0
  29. package/dist/lib/agent-kit/mcp/tools.d.ts +45 -0
  30. package/dist/lib/agent-kit/mcp/tools.js +280 -0
  31. package/dist/lib/agent-kit/paths.d.ts +42 -0
  32. package/dist/lib/agent-kit/paths.js +56 -0
  33. package/dist/lib/agent-kit/run/heartbeat.d.ts +52 -0
  34. package/dist/lib/agent-kit/run/heartbeat.js +97 -0
  35. package/dist/lib/agent-kit/run/inbox.d.ts +59 -0
  36. package/dist/lib/agent-kit/run/inbox.js +152 -0
  37. package/dist/lib/agent-kit/run/mesh-listener.d.ts +58 -0
  38. package/dist/lib/agent-kit/run/mesh-listener.js +193 -0
  39. package/dist/lib/agent-kit/run/sync.d.ts +33 -0
  40. package/dist/lib/agent-kit/run/sync.js +58 -0
  41. package/dist/lib/agent-kit/services.d.ts +21 -0
  42. package/dist/lib/agent-kit/services.js +46 -0
  43. package/dist/lib/agent-kit/skills.d.ts +18 -0
  44. package/dist/lib/agent-kit/skills.js +149 -0
  45. package/dist/lib/bot/api.d.ts +51 -0
  46. package/dist/lib/bot/api.js +32 -0
  47. package/dist/lib/bot/daemon.d.ts +17 -0
  48. package/dist/lib/bot/daemon.js +44 -3
  49. package/dist/lib/bot/index.d.ts +4 -0
  50. package/dist/lib/bot/index.js +4 -0
  51. package/dist/lib/bot/inflight.d.ts +14 -0
  52. package/dist/lib/bot/local-config.d.ts +70 -0
  53. package/dist/lib/bot/local-config.js +147 -0
  54. package/dist/lib/bot/local-name.d.ts +54 -0
  55. package/dist/lib/bot/local-name.js +114 -0
  56. package/dist/lib/bot/run.d.ts +9 -0
  57. package/dist/lib/bot/run.js +117 -24
  58. package/dist/lib/bot/runnable.d.ts +51 -0
  59. package/dist/lib/bot/runnable.js +65 -0
  60. package/dist/lib/bot/self-heal.d.ts +52 -0
  61. package/dist/lib/bot/self-heal.js +79 -0
  62. package/dist/lib/bot/split.d.ts +32 -0
  63. package/dist/lib/bot/split.js +241 -0
  64. package/dist/lib/service-manager/index.d.ts +43 -0
  65. package/dist/lib/service-manager/index.js +114 -0
  66. package/dist/lib/service-manager/launchd.d.ts +23 -0
  67. package/dist/lib/service-manager/launchd.js +81 -0
  68. package/dist/lib/service-manager/systemd.d.ts +19 -0
  69. package/dist/lib/service-manager/systemd.js +72 -0
  70. package/dist/lib/service-manager/types.d.ts +32 -0
  71. package/dist/lib/service-manager/types.js +26 -0
  72. package/dist/utils/self-update.js +2 -30
  73. package/dist/utils/update-command-supervisor.cjs +194 -0
  74. package/dist/utils/version-gate.d.ts +18 -0
  75. package/dist/utils/version-gate.js +126 -7
  76. package/package.json +2 -2
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The skills directory the kit ships to ~/.hq-agent/skills/<name>/SKILL.md.
3
+ *
4
+ * agentskills.io format: YAML frontmatter with `name` and `description`, then
5
+ * markdown the bot's framework loads as an instruction. Each skill drives
6
+ * the `hq` CLI, which authenticates as the machine identity on its own, so
7
+ * no skill ever embeds or asks for a token or secret value.
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ export const KIT_SKILLS = [
12
+ {
13
+ name: "dm",
14
+ description: "Send and read HQ direct messages and channel messages as this agent using the hq CLI.",
15
+ body: `# HQ direct messages
16
+
17
+ Use the \`hq dm\` command to talk to teammates (people and other agents) in HQ.
18
+ The CLI authenticates as this agent automatically — never ask for a token.
19
+
20
+ ## Send
21
+
22
+ \`\`\`bash
23
+ hq dm <email-or-uid> "message text" # 1:1 to a person (prs_…) or agent (agt_…)
24
+ hq dm <uid1>,<uid2> "message text" # group DM
25
+ hq dm '#channel-name' "message text" # channel
26
+ \`\`\`
27
+
28
+ ## Read
29
+
30
+ \`\`\`bash
31
+ hq dm inbox --unread # unread DMs addressed to you
32
+ hq dm thread <email-or-uid> # the 1:1 conversation
33
+ hq dm inbox --mark-read # mark what you have read
34
+ hq dm channel <name> # read a channel
35
+ \`\`\`
36
+
37
+ New inbound messages are also mirrored by the kit's inbox poller to
38
+ \`~/.hq-agent/inbox/<id>.json\` (and \`inbox.jsonl\`) — check there when
39
+ you are asked whether anyone has messaged you.
40
+
41
+ ## Rules
42
+
43
+ - Reply in the same thread you were messaged in.
44
+ - Keep messages short; link to files in the vault instead of pasting them.
45
+ - Never include secrets or credentials in a message.
46
+ `,
47
+ },
48
+ {
49
+ name: "search",
50
+ description: "Search the company vault (documents, knowledge, meeting notes) with the hq CLI's local index.",
51
+ body: `# HQ search
52
+
53
+ The company vault is synced to the kit's HQ root (see \`~/.hq-agent/kit.json\`,
54
+ \`hqRoot\`). Search it with:
55
+
56
+ \`\`\`bash
57
+ hq search "<query>" # ranked results across the synced vault
58
+ hq search "<query>" --mode hybrid # keyword + semantic
59
+ hq search "<query>" --json # machine-readable
60
+ hq files search "<query>" --company <slug> # server-side vault search (no local index needed)
61
+ \`\`\`
62
+
63
+ If results look stale, the sync loop may not have run yet:
64
+ \`hq sync pull --all --on-conflict keep --hq-root <hqRoot>\` pulls now.
65
+
66
+ Cite the file path of anything you quote so a teammate can open it.
67
+ `,
68
+ },
69
+ {
70
+ name: "files",
71
+ description: "Read, list and write files in the company vault through the hq CLI (hq files).",
72
+ body: `# HQ vault files
73
+
74
+ \`\`\`bash
75
+ hq files browse companies/<slug>/<folder> # list a vault folder without syncing it
76
+ hq files cat companies/<slug>/<file> # print one file
77
+ hq files get companies/<slug>/<file> # fetch one file into the local HQ tree
78
+ hq files share companies/<slug>/<path> # share a path with a teammate
79
+ hq files versions companies/<slug>/<file> # version history
80
+ \`\`\`
81
+
82
+ Paths are vault-relative and start with \`companies/<slug>/\`. The synced copy
83
+ lives under the kit's HQ root (\`~/.hq-agent/kit.json\` → \`hqRoot\`); edit files
84
+ there and run \`hq sync push --hq-root <hqRoot>\` to publish. Writes are
85
+ audited under this agent's identity — only write where you were asked to,
86
+ and prefer creating a new file over overwriting one you did not author.
87
+ `,
88
+ },
89
+ {
90
+ name: "secrets-exec",
91
+ description: "Run a command with company secrets injected as environment variables, without ever printing them.",
92
+ body: `# Run with HQ secrets
93
+
94
+ Secrets never appear in chat, logs or files. Inject them into a child
95
+ process instead:
96
+
97
+ \`\`\`bash
98
+ hq secrets list # names only, never values
99
+ hq secrets exec --only <NAME>[,<NAME>] -- <cmd> # run <cmd> with those vars set
100
+ hq run <script> # run a vault script with its declared secrets
101
+ \`\`\`
102
+
103
+ Rules:
104
+
105
+ - Never \`echo\`, \`printenv\` or otherwise print a secret, even to "check" it.
106
+ - Request the narrowest \`--only\` set the command needs.
107
+ - If a secret is missing, say which NAME is missing and ask a human admin to
108
+ add it with \`hq secrets set\` — do not ask for the value in chat.
109
+ `,
110
+ },
111
+ {
112
+ name: "work-mesh-status",
113
+ description: "Report and update this agent's live work status and read the team's work mesh via the hq CLI.",
114
+ body: `# Work mesh status
115
+
116
+ The work mesh is HQ's live board of who is working on what. It is the source
117
+ of truth for project stories and active sessions; the kit's listener keeps a
118
+ local cache under \`~/.hq/work-mesh/cache/\`.
119
+
120
+ \`\`\`bash
121
+ hq mesh session status --company <slug> # who is working on what right now
122
+ hq mesh start --company <slug> --project <slug> --summary "<what you are starting>"
123
+ hq mesh progress --company <slug> --project <slug> --summary "<what changed>"
124
+ hq mesh blocked --company <slug> --project <slug> --summary "<what blocks you>"
125
+ hq mesh done --company <slug> --project <slug> --summary "<what you finished>"
126
+ hq mesh note --company <slug> --project <slug> --summary "<short note>"
127
+ \`\`\`
128
+
129
+ Post \`start\` when you pick up a piece of work and \`done\` when you finish it.
130
+ Keep entries to one line. Presence (online / stale / offline) is derived
131
+ from the kit heartbeat automatically — you do not need to report it.
132
+ `,
133
+ },
134
+ ];
135
+ export function renderSkillMarkdown(skill) {
136
+ return `---\nname: ${skill.name}\ndescription: ${JSON.stringify(skill.description)}\n---\n\n${skill.body}`;
137
+ }
138
+ export function writeKitSkills(paths) {
139
+ const written = [];
140
+ for (const skill of KIT_SKILLS) {
141
+ const dir = path.join(paths.skillsDir, skill.name);
142
+ fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
143
+ const dest = path.join(dir, "SKILL.md");
144
+ fs.writeFileSync(dest, renderSkillMarkdown(skill), { mode: 0o644 });
145
+ written.push(dest);
146
+ }
147
+ return written;
148
+ }
149
+ //# sourceMappingURL=skills.js.map
@@ -6,6 +6,7 @@
6
6
  * existing Cognito machinery. `fetch` is injectable for tests.
7
7
  */
8
8
  import { vaultApiFetch } from "../../utils/vault-api.js";
9
+ import { type BotLocalConfig } from "./local-config.js";
9
10
  export type TokenSupplier = () => Promise<string>;
10
11
  /**
11
12
  * One agent-inbox item. DMs carry the base fields; room (channel / group chat)
@@ -55,6 +56,31 @@ export interface AgentRecord {
55
56
  } | null;
56
57
  };
57
58
  }
59
+ /**
60
+ * One of the caller's own local bots, as `GET /v1/agents/mine?local=1` lists
61
+ * them — enough to tell a bot that is missing from this machine from one that
62
+ * is already here, and to rebuild it.
63
+ */
64
+ export interface MyLocalBot {
65
+ agentUid: string;
66
+ slug?: string;
67
+ name?: string;
68
+ botKind?: "personal" | "company";
69
+ computeMode?: string;
70
+ ownerUid?: string;
71
+ online?: boolean;
72
+ lastHeartbeatAt?: string | null;
73
+ localConfig?: unknown;
74
+ }
75
+ /** What the credential re-issue route hands back (a rotated machine secret). */
76
+ export interface ReissuedBotCredentials {
77
+ agent: AgentRecord;
78
+ identity: {
79
+ cognitoUsername: string;
80
+ secret: string;
81
+ };
82
+ localConfig: unknown;
83
+ }
58
84
  export declare class BotApiError extends Error {
59
85
  readonly status: number;
60
86
  readonly path: string;
@@ -194,6 +220,12 @@ export declare class BotApi {
194
220
  /** Company bots: the company it acts for, plus any further companies. The server makes it a member of each. */
195
221
  companyUid?: string;
196
222
  companyMemberships?: string[];
223
+ /**
224
+ * The bot's non-secret settings, kept by the cloud so a reinstall can
225
+ * rebuild it. Checked here too: the server 400s on any key that looks
226
+ * like a credential.
227
+ */
228
+ localConfig?: BotLocalConfig;
197
229
  }): Promise<{
198
230
  agent: AgentRecord;
199
231
  identity: {
@@ -201,6 +233,25 @@ export declare class BotApi {
201
233
  secret: string;
202
234
  };
203
235
  }>;
236
+ /**
237
+ * POST /v1/agents/{uid}/credentials — owner-only, local bots only: mint a
238
+ * fresh machine secret for a bot this account already owns, so it can come
239
+ * back after a reinstall or move to another Mac.
240
+ *
241
+ * This ROTATES the secret: whatever machine held the bot before stops being
242
+ * able to authenticate as it (one machine runs a local bot at a time).
243
+ */
244
+ reissueCredentials(agentUid: string, input?: {
245
+ machineId?: string;
246
+ reason?: "reinstall" | "move";
247
+ }): Promise<ReissuedBotCredentials>;
248
+ /**
249
+ * GET /v1/agents/mine — the caller's own agents; `local: true` narrows it to
250
+ * the local bots they own in any company, which is what a restore works from.
251
+ */
252
+ listMine(opts?: {
253
+ local?: boolean;
254
+ }): Promise<MyLocalBot[]>;
204
255
  /** DELETE /v1/agents/{uid} — owner tears the identity down. */
205
256
  deleteLocalBot(agentUid: string): Promise<{
206
257
  uid: string;
@@ -6,6 +6,7 @@
6
6
  * existing Cognito machinery. `fetch` is injectable for tests.
7
7
  */
8
8
  import { vaultApiFetch } from "../../utils/vault-api.js";
9
+ import { assertNoSecretKeys } from "./local-config.js";
9
10
  export class BotApiError extends Error {
10
11
  status;
11
12
  path;
@@ -186,6 +187,8 @@ export class BotApi {
186
187
  * "company" is evaluated as itself through its memberships). Default personal.
187
188
  */
188
189
  async createLocalBot(input) {
190
+ if (input.localConfig)
191
+ assertNoSecretKeys(input.localConfig);
189
192
  const { body } = await this.call("/v1/agents", {
190
193
  method: "POST",
191
194
  body: {
@@ -196,10 +199,39 @@ export class BotApi {
196
199
  ...(input.companyUid ? { companyUid: input.companyUid } : {}),
197
200
  ...(input.companyMemberships?.length ? { companyMemberships: input.companyMemberships } : {}),
198
201
  role: "local-bot",
202
+ ...(input.localConfig ? { localConfig: input.localConfig } : {}),
199
203
  },
200
204
  });
201
205
  return body;
202
206
  }
207
+ /**
208
+ * POST /v1/agents/{uid}/credentials — owner-only, local bots only: mint a
209
+ * fresh machine secret for a bot this account already owns, so it can come
210
+ * back after a reinstall or move to another Mac.
211
+ *
212
+ * This ROTATES the secret: whatever machine held the bot before stops being
213
+ * able to authenticate as it (one machine runs a local bot at a time).
214
+ */
215
+ async reissueCredentials(agentUid, input = {}) {
216
+ const { body } = await this.call(`/v1/agents/${encodeURIComponent(agentUid)}/credentials`, {
217
+ method: "POST",
218
+ body: {
219
+ ...(input.machineId ? { machineId: input.machineId } : {}),
220
+ ...(input.reason ? { reason: input.reason } : {}),
221
+ },
222
+ }, [200]);
223
+ return { agent: body.agent, identity: body.identity, localConfig: body.localConfig ?? null };
224
+ }
225
+ /**
226
+ * GET /v1/agents/mine — the caller's own agents; `local: true` narrows it to
227
+ * the local bots they own in any company, which is what a restore works from.
228
+ */
229
+ async listMine(opts = {}) {
230
+ const { body } = await this.call("/v1/agents/mine", {
231
+ ...(opts.local ? { query: { local: "1" } } : {}),
232
+ });
233
+ return Array.isArray(body.agents) ? body.agents : [];
234
+ }
203
235
  /** DELETE /v1/agents/{uid} — owner tears the identity down. */
204
236
  async deleteLocalBot(agentUid) {
205
237
  const { body } = await this.call(`/v1/agents/${encodeURIComponent(agentUid)}`, { method: "DELETE" });
@@ -63,6 +63,23 @@ export interface BotDaemonDeps {
63
63
  fsImpl?: Pick<typeof fs, "existsSync" | "writeFileSync" | "mkdirSync" | "unlinkSync">;
64
64
  /** Blocking sleep, injectable for tests. */
65
65
  sleepMs?: (ms: number) => void;
66
+ /**
67
+ * How `uninstallBotDaemon` issues its final `launchctl bootout`.
68
+ *
69
+ * "inline" (default) runs it in this process, which is right when something
70
+ * other than the job itself is doing the removing. "detached" hands it to a
71
+ * short-lived grandchild in its own session instead, which is the only way
72
+ * that works when the caller IS the launchd job being booted out: `bootout`
73
+ * kills the job, so an inline call never returns.
74
+ */
75
+ bootout?: "inline" | "detached";
76
+ /** Injectable detached spawn, so tests never reach the real service manager. */
77
+ spawnDetached?: (command: string, args: string[]) => void;
78
+ /**
79
+ * Called once the removal is durable (plist gone) but before the bootout —
80
+ * the last moment a self-booting-out caller is still alive to say anything.
81
+ */
82
+ announce?: (result: BotDaemonResult) => void;
66
83
  }
67
84
  /** Write the unit and load it (RunAtLoad starts the bot immediately). */
68
85
  export declare function installBotDaemon(p: BotDaemonPaths, deps?: BotDaemonDeps): BotDaemonResult;
@@ -12,6 +12,7 @@
12
12
  import * as fs from "node:fs";
13
13
  import * as os from "node:os";
14
14
  import * as path from "node:path";
15
+ import { spawn } from "node:child_process";
15
16
  import { resolveHqBinary, resolveNodeBinary } from "../mesh/live/daemon/install.js";
16
17
  import { botLogPath, launchdLabel } from "./paths.js";
17
18
  export function detectBotPlatform(platform = process.platform) {
@@ -142,6 +143,31 @@ export function launchdDomain(uid = os.userInfo().uid) {
142
143
  function blockingSleep(ms) {
143
144
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
144
145
  }
146
+ function shQuote(value) {
147
+ return `'${value.replace(/'/g, `'\\''`)}'`;
148
+ }
149
+ /** Detached `launchctl bootout` that outlives the process asking for it. */
150
+ function defaultSpawnDetached(command, args) {
151
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
152
+ child.unref();
153
+ }
154
+ /**
155
+ * Take the job out of the launchd session.
156
+ *
157
+ * When `deps.bootout` is "detached" the call is handed to a new session
158
+ * (`sh -c 'sleep 1; exec launchctl bootout …'`) so the caller can be the job
159
+ * being removed: the caller exits 0 first — which KeepAlive{SuccessfulExit:
160
+ * false} leaves alone — and the grandchild, in a session launchd is not
161
+ * tearing down, finishes the job a second later.
162
+ */
163
+ function bootoutLaunchd(p, deps) {
164
+ const target = `${launchdDomain(deps.uid)}/${p.label}`;
165
+ if (deps.bootout === "detached") {
166
+ (deps.spawnDetached ?? defaultSpawnDetached)("/bin/sh", ["-c", `sleep 1; exec launchctl bootout ${shQuote(target)}`]);
167
+ return;
168
+ }
169
+ deps.launchctl?.(["bootout", target]);
170
+ }
145
171
  /**
146
172
  * `launchctl bootout` returns before the job is actually gone; a `bootstrap`
147
173
  * issued right away fails ("service already loaded" / EIO) and the old job
@@ -174,6 +200,11 @@ export function installBotDaemon(p, deps = {}) {
174
200
  io.mkdirSync(path.dirname(p.logPath), { recursive: true, mode: 0o700 });
175
201
  if (platform === "darwin") {
176
202
  io.mkdirSync(path.dirname(p.plistPath), { recursive: true });
203
+ // Replace rather than overwrite, so re-adopting a bot over the stale plist
204
+ // a wiped ~/.hq left behind lands a fresh file at 0600 (writeFileSync
205
+ // keeps the mode an existing file already has).
206
+ if (io.existsSync(p.plistPath))
207
+ io.unlinkSync(p.plistPath);
177
208
  io.writeFileSync(p.plistPath, renderBotLaunchdPlist(p), { mode: 0o600 });
178
209
  let loaded = false;
179
210
  if (deps.launchctl) {
@@ -224,11 +255,16 @@ export function uninstallBotDaemon(p, deps = {}) {
224
255
  const platform = deps.platform ?? detectBotPlatform();
225
256
  const io = deps.fsImpl ?? fs;
226
257
  if (platform === "darwin") {
227
- deps.launchctl?.(["bootout", `${launchdDomain(deps.uid)}/${p.label}`]);
258
+ // Order is load-bearing. `bootout` used to run first, which is fine from a
259
+ // shell and fatal from inside the job: when the orphan self-heal is itself
260
+ // the launchd job, bootout kills it, so the unlink below and the log line
261
+ // the person reads never happened — on a real Mac the stale plist survived
262
+ // and RunAtLoad resurrected the orphan at every login. Remove the file
263
+ // first, say so, and boot out last (detached when the caller is the job).
228
264
  const existed = io.existsSync(p.plistPath);
229
265
  if (existed)
230
266
  io.unlinkSync(p.plistPath);
231
- return {
267
+ const result = {
232
268
  platform,
233
269
  action: "uninstall",
234
270
  installed: false,
@@ -236,12 +272,15 @@ export function uninstallBotDaemon(p, deps = {}) {
236
272
  dest: p.plistPath,
237
273
  message: existed ? `Removed LaunchAgent ${p.label}` : `LaunchAgent ${p.label} was not installed`,
238
274
  };
275
+ deps.announce?.(result);
276
+ bootoutLaunchd(p, deps);
277
+ return result;
239
278
  }
240
279
  if (platform === "linux") {
241
280
  const existed = io.existsSync(p.systemdUnitPath);
242
281
  if (existed)
243
282
  io.unlinkSync(p.systemdUnitPath);
244
- return {
283
+ const result = {
245
284
  platform,
246
285
  action: "uninstall",
247
286
  installed: false,
@@ -249,6 +288,8 @@ export function uninstallBotDaemon(p, deps = {}) {
249
288
  dest: p.systemdUnitPath,
250
289
  message: existed ? `Removed systemd unit for ${p.name}` : `No systemd unit for ${p.name}`,
251
290
  };
291
+ deps.announce?.(result);
292
+ return result;
252
293
  }
253
294
  return { platform, action: "uninstall", installed: false, loaded: false, message: "Nothing to uninstall on this platform" };
254
295
  }
@@ -13,4 +13,8 @@ export * from "./worker-source.js";
13
13
  export * from "./company-bind.js";
14
14
  export * from "./run.js";
15
15
  export * from "./runtime/index.js";
16
+ export * from "./local-config.js";
17
+ export * from "./self-heal.js";
18
+ export * from "./runnable.js";
19
+ export * from "./local-name.js";
16
20
  //# sourceMappingURL=index.d.ts.map
@@ -13,4 +13,8 @@ export * from "./worker-source.js";
13
13
  export * from "./company-bind.js";
14
14
  export * from "./run.js";
15
15
  export * from "./runtime/index.js";
16
+ export * from "./local-config.js";
17
+ export * from "./self-heal.js";
18
+ export * from "./runnable.js";
19
+ export * from "./local-name.js";
16
20
  //# sourceMappingURL=index.js.map
@@ -37,6 +37,20 @@ export interface InflightTurn {
37
37
  * restart posts this instead of "did not finish".
38
38
  */
39
39
  reply?: string;
40
+ /**
41
+ * The message to deliver, already cut to the body limit (split.ts). Recovery
42
+ * resends from `replyPartsDelivered` on, so a part the person has already
43
+ * read is never posted twice.
44
+ */
45
+ replyParts?: string[];
46
+ /** How many of `replyParts` actually reached the chat. */
47
+ replyPartsDelivered?: number;
48
+ /**
49
+ * Consecutive failed attempts to deliver this marker. A body the server will
50
+ * never accept must not be retried for ever, so the bot gives up after
51
+ * RECOVERY_ATTEMPT_LIMIT and says so once (run.ts).
52
+ */
53
+ recoveryFailures?: number;
40
54
  }
41
55
  export declare function botInflightPath(dir: string): string;
42
56
  /** Every turn that was in progress, oldest first. */
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `localConfig` — the non-secret shape of a bot's bot.json that the cloud
3
+ * stores alongside the agent record, so a reinstall (or a second Mac) can
4
+ * rebuild the same bot instead of re-creating it.
5
+ *
6
+ * The server treats it as opaque, caps it at 8 KB, and refuses any value that
7
+ * carries a key named secret/token/password/creds (case-insensitive). The CLI
8
+ * therefore builds it from an explicit allow-list of bot.json fields and
9
+ * checks the result before it leaves this machine: a bot's machine credentials
10
+ * live only in ~/.hq/bots/<name>/machine-creds.json and never travel here.
11
+ */
12
+ import { type BotConfig, type BotKind, type BotMemoryMode, type BotRuntimeId } from "./config.js";
13
+ /** Same limit the server enforces on the stored value. */
14
+ export declare const BOT_LOCAL_CONFIG_MAX_BYTES: number;
15
+ /** Key names the server rejects outright (case-insensitive, substring). */
16
+ export declare const BOT_LOCAL_CONFIG_FORBIDDEN_KEY_RE: RegExp;
17
+ /**
18
+ * Everything needed to rebuild a bot here, minus anything secret. Written on
19
+ * create, returned by `GET /v1/agents/mine?local=1` and by the credential
20
+ * re-issue route.
21
+ */
22
+ export interface BotLocalConfig {
23
+ v: 1;
24
+ runtime?: BotRuntimeId;
25
+ /** "scaffold" (its own personal/workers/<name>) or "worker" (an HQ worker). */
26
+ workerSource?: "scaffold" | "worker";
27
+ workerId?: string;
28
+ companySlug?: string;
29
+ model?: string;
30
+ effort?: string;
31
+ kind?: BotKind;
32
+ /** Company bots: the company slugs it is a member of. */
33
+ companies?: string[];
34
+ autoApprove?: boolean;
35
+ /** Only the memory *kind* travels — never an absolute path from another Mac. */
36
+ memory?: BotMemoryMode;
37
+ intro?: string;
38
+ kickoff?: string;
39
+ }
40
+ /** Walks a value and throws when any key looks like it could carry a secret. */
41
+ export declare function assertNoSecretKeys(value: unknown, path?: string): void;
42
+ /** The bot.json fields a localConfig is built from — nothing else is read. */
43
+ export type BotLocalConfigSource = Pick<BotConfig, "runtime" | "workerSource" | "workerId" | "companySlug" | "model" | "effort" | "kind" | "companies" | "autoApprove" | "memoryDir" | "intro" | "kickoff">;
44
+ /**
45
+ * The non-secret settings of a bot, for the cloud to keep. Absolute paths
46
+ * (hqRoot, memoryDir, workerDir) are deliberately left out: they belong to the
47
+ * machine the bot was created on, and a restore rebuilds them locally.
48
+ */
49
+ export declare function buildBotLocalConfig(config: BotLocalConfigSource): BotLocalConfig;
50
+ /**
51
+ * Tolerant read of whatever the server handed back: an older bot has no
52
+ * localConfig at all, and a field HQ no longer understands is dropped rather
53
+ * than losing the bot. Returns null when nothing usable was stored.
54
+ */
55
+ export declare function parseBotLocalConfig(value: unknown): BotLocalConfig | null;
56
+ /** What a restore falls back to for a bot created before localConfig existed. */
57
+ export declare const BOT_RESTORE_DEFAULTS: {
58
+ runtime: BotRuntimeId;
59
+ memory: BotMemoryMode;
60
+ kind: BotKind;
61
+ };
62
+ /** Human summary of the settings a restore is about to apply. */
63
+ export declare function describeBotLocalConfig(local: BotLocalConfig | null): string;
64
+ /** Exported so callers can present the accepted values without importing config.ts. */
65
+ export declare const BOT_LOCAL_CONFIG_VALUES: {
66
+ readonly runtimes: readonly ["claude", "codex", "grok"];
67
+ readonly kinds: readonly ["personal", "company"];
68
+ readonly memoryModes: readonly ["synced", "local"];
69
+ };
70
+ //# sourceMappingURL=local-config.d.ts.map
@@ -0,0 +1,147 @@
1
+ /**
2
+ * `localConfig` — the non-secret shape of a bot's bot.json that the cloud
3
+ * stores alongside the agent record, so a reinstall (or a second Mac) can
4
+ * rebuild the same bot instead of re-creating it.
5
+ *
6
+ * The server treats it as opaque, caps it at 8 KB, and refuses any value that
7
+ * carries a key named secret/token/password/creds (case-insensitive). The CLI
8
+ * therefore builds it from an explicit allow-list of bot.json fields and
9
+ * checks the result before it leaves this machine: a bot's machine credentials
10
+ * live only in ~/.hq/bots/<name>/machine-creds.json and never travel here.
11
+ */
12
+ import { BOT_KINDS, BOT_MEMORY_MODES, BOT_RUNTIMES, botMemoryMode, effectiveBotCompanies, effectiveBotKind, isBotKind, isBotMemoryMode, isBotRuntimeId, } from "./config.js";
13
+ /** Same limit the server enforces on the stored value. */
14
+ export const BOT_LOCAL_CONFIG_MAX_BYTES = 8 * 1024;
15
+ /** Key names the server rejects outright (case-insensitive, substring). */
16
+ export const BOT_LOCAL_CONFIG_FORBIDDEN_KEY_RE = /secret|token|password|creds/i;
17
+ /** Walks a value and throws when any key looks like it could carry a secret. */
18
+ export function assertNoSecretKeys(value, path = "localConfig") {
19
+ if (Array.isArray(value)) {
20
+ value.forEach((item, i) => assertNoSecretKeys(item, `${path}[${i}]`));
21
+ return;
22
+ }
23
+ if (!value || typeof value !== "object")
24
+ return;
25
+ for (const [key, child] of Object.entries(value)) {
26
+ if (BOT_LOCAL_CONFIG_FORBIDDEN_KEY_RE.test(key)) {
27
+ throw Object.assign(new Error(`${path}.${key} looks like a credential; a bot's saved settings never carry one.`), {
28
+ expected: true,
29
+ });
30
+ }
31
+ assertNoSecretKeys(child, `${path}.${key}`);
32
+ }
33
+ }
34
+ /**
35
+ * The non-secret settings of a bot, for the cloud to keep. Absolute paths
36
+ * (hqRoot, memoryDir, workerDir) are deliberately left out: they belong to the
37
+ * machine the bot was created on, and a restore rebuilds them locally.
38
+ */
39
+ export function buildBotLocalConfig(config) {
40
+ const kind = effectiveBotKind(config);
41
+ const companies = effectiveBotCompanies(config);
42
+ const local = {
43
+ v: 1,
44
+ runtime: config.runtime,
45
+ workerSource: config.workerSource ?? "scaffold",
46
+ ...(config.workerId ? { workerId: config.workerId } : {}),
47
+ ...(config.companySlug ? { companySlug: config.companySlug } : {}),
48
+ ...(config.model ? { model: config.model } : {}),
49
+ ...(config.effort ? { effort: config.effort } : {}),
50
+ kind,
51
+ ...(kind === "company" ? { companies } : {}),
52
+ ...(config.autoApprove === false ? { autoApprove: false } : {}),
53
+ memory: botMemoryMode(config),
54
+ ...(config.intro ? { intro: config.intro } : {}),
55
+ ...(config.kickoff ? { kickoff: config.kickoff } : {}),
56
+ };
57
+ assertNoSecretKeys(local);
58
+ if (Buffer.byteLength(JSON.stringify(local), "utf8") > BOT_LOCAL_CONFIG_MAX_BYTES) {
59
+ throw Object.assign(new Error("This bot's settings are too large to save in the cloud (over 8 KB)."), {
60
+ expected: true,
61
+ });
62
+ }
63
+ return local;
64
+ }
65
+ /**
66
+ * Tolerant read of whatever the server handed back: an older bot has no
67
+ * localConfig at all, and a field HQ no longer understands is dropped rather
68
+ * than losing the bot. Returns null when nothing usable was stored.
69
+ */
70
+ export function parseBotLocalConfig(value) {
71
+ if (!value || typeof value !== "object" || Array.isArray(value))
72
+ return null;
73
+ const raw = value;
74
+ const str = (key) => {
75
+ const v = raw[key];
76
+ return typeof v === "string" && v.trim() ? v.trim() : undefined;
77
+ };
78
+ const out = { v: 1 };
79
+ const runtime = str("runtime");
80
+ if (isBotRuntimeId(runtime))
81
+ out.runtime = runtime;
82
+ const workerSource = str("workerSource");
83
+ if (workerSource === "worker" || workerSource === "scaffold")
84
+ out.workerSource = workerSource;
85
+ const workerId = str("workerId");
86
+ if (workerId)
87
+ out.workerId = workerId;
88
+ const companySlug = str("companySlug");
89
+ if (companySlug)
90
+ out.companySlug = companySlug;
91
+ const model = str("model");
92
+ if (model)
93
+ out.model = model;
94
+ const effort = str("effort");
95
+ if (effort)
96
+ out.effort = effort;
97
+ const kind = str("kind");
98
+ if (isBotKind(kind))
99
+ out.kind = kind;
100
+ if (Array.isArray(raw.companies)) {
101
+ const companies = raw.companies.filter((c) => typeof c === "string" && c.trim().length > 0).map((c) => c.trim());
102
+ if (companies.length > 0)
103
+ out.companies = [...new Set(companies)];
104
+ }
105
+ if (raw.autoApprove === false)
106
+ out.autoApprove = false;
107
+ const memory = str("memory");
108
+ if (isBotMemoryMode(memory))
109
+ out.memory = memory;
110
+ const intro = str("intro");
111
+ if (intro)
112
+ out.intro = intro;
113
+ const kickoff = str("kickoff");
114
+ if (kickoff)
115
+ out.kickoff = kickoff;
116
+ // A record with nothing recognisable in it is the same as none at all.
117
+ return Object.keys(out).length > 1 ? out : null;
118
+ }
119
+ /** What a restore falls back to for a bot created before localConfig existed. */
120
+ export const BOT_RESTORE_DEFAULTS = {
121
+ runtime: "claude",
122
+ memory: "synced",
123
+ kind: "personal",
124
+ };
125
+ /** Human summary of the settings a restore is about to apply. */
126
+ export function describeBotLocalConfig(local) {
127
+ if (!local) {
128
+ return `defaults (${BOT_RESTORE_DEFAULTS.runtime}, ${BOT_RESTORE_DEFAULTS.memory} memory, ${BOT_RESTORE_DEFAULTS.kind})`;
129
+ }
130
+ const bits = [
131
+ local.runtime ?? BOT_RESTORE_DEFAULTS.runtime,
132
+ `${local.memory ?? BOT_RESTORE_DEFAULTS.memory} memory`,
133
+ local.kind ?? BOT_RESTORE_DEFAULTS.kind,
134
+ ];
135
+ if (local.workerId)
136
+ bits.push(`worker ${local.workerId}`);
137
+ if (local.model)
138
+ bits.push(local.model);
139
+ return bits.join(", ");
140
+ }
141
+ /** Exported so callers can present the accepted values without importing config.ts. */
142
+ export const BOT_LOCAL_CONFIG_VALUES = {
143
+ runtimes: BOT_RUNTIMES,
144
+ kinds: BOT_KINDS,
145
+ memoryModes: BOT_MEMORY_MODES,
146
+ };
147
+ //# sourceMappingURL=local-config.js.map