@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +38 -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/skill.d.ts +12 -3
  12. package/dist/commands/skill.js +79 -3
  13. package/dist/commands/workers.d.ts +2 -14
  14. package/dist/commands/workers.js +2 -8
  15. package/dist/lib/bot/api.d.ts +202 -0
  16. package/dist/lib/bot/api.js +202 -0
  17. package/dist/lib/bot/company-bind.d.ts +27 -0
  18. package/dist/lib/bot/company-bind.js +62 -0
  19. package/dist/lib/bot/config.d.ts +106 -0
  20. package/dist/lib/bot/config.js +141 -0
  21. package/dist/lib/bot/continuity-download.d.ts +28 -0
  22. package/dist/lib/bot/continuity-download.js +75 -0
  23. package/dist/lib/bot/continuity-install.d.ts +14 -0
  24. package/dist/lib/bot/continuity-install.js +101 -0
  25. package/dist/lib/bot/continuity.d.ts +66 -0
  26. package/dist/lib/bot/continuity.js +301 -0
  27. package/dist/lib/bot/creds.d.ts +24 -0
  28. package/dist/lib/bot/creds.js +51 -0
  29. package/dist/lib/bot/daemon.d.ts +75 -0
  30. package/dist/lib/bot/daemon.js +316 -0
  31. package/dist/lib/bot/inbox-state.d.ts +18 -0
  32. package/dist/lib/bot/inbox-state.js +51 -0
  33. package/dist/lib/bot/index.d.ts +16 -0
  34. package/dist/lib/bot/index.js +16 -0
  35. package/dist/lib/bot/inflight.d.ts +40 -0
  36. package/dist/lib/bot/inflight.js +44 -0
  37. package/dist/lib/bot/log.d.ts +13 -0
  38. package/dist/lib/bot/log.js +59 -0
  39. package/dist/lib/bot/owner-context.d.ts +75 -0
  40. package/dist/lib/bot/owner-context.js +151 -0
  41. package/dist/lib/bot/paths.d.ts +61 -0
  42. package/dist/lib/bot/paths.js +103 -0
  43. package/dist/lib/bot/progress.d.ts +84 -0
  44. package/dist/lib/bot/progress.js +167 -0
  45. package/dist/lib/bot/promote.d.ts +16 -0
  46. package/dist/lib/bot/promote.js +106 -0
  47. package/dist/lib/bot/promotion-hold.d.ts +24 -0
  48. package/dist/lib/bot/promotion-hold.js +103 -0
  49. package/dist/lib/bot/promotion-receipt.d.ts +9 -0
  50. package/dist/lib/bot/promotion-receipt.js +56 -0
  51. package/dist/lib/bot/promotion-upload.d.ts +16 -0
  52. package/dist/lib/bot/promotion-upload.js +65 -0
  53. package/dist/lib/bot/prompt.d.ts +103 -0
  54. package/dist/lib/bot/prompt.js +329 -0
  55. package/dist/lib/bot/room-policy.d.ts +53 -0
  56. package/dist/lib/bot/room-policy.js +73 -0
  57. package/dist/lib/bot/run.d.ts +98 -0
  58. package/dist/lib/bot/run.js +787 -0
  59. package/dist/lib/bot/runtime/claude.d.ts +49 -0
  60. package/dist/lib/bot/runtime/claude.js +151 -0
  61. package/dist/lib/bot/runtime/codex.d.ts +28 -0
  62. package/dist/lib/bot/runtime/codex.js +147 -0
  63. package/dist/lib/bot/runtime/grok.d.ts +16 -0
  64. package/dist/lib/bot/runtime/grok.js +67 -0
  65. package/dist/lib/bot/runtime/index.d.ts +35 -0
  66. package/dist/lib/bot/runtime/index.js +279 -0
  67. package/dist/lib/bot/runtime/messages-stream.d.ts +27 -0
  68. package/dist/lib/bot/runtime/messages-stream.js +85 -0
  69. package/dist/lib/bot/runtime/types.d.ts +136 -0
  70. package/dist/lib/bot/runtime/types.js +51 -0
  71. package/dist/lib/bot/scaffold.d.ts +38 -0
  72. package/dist/lib/bot/scaffold.js +94 -0
  73. package/dist/lib/bot/session.d.ts +19 -0
  74. package/dist/lib/bot/session.js +39 -0
  75. package/dist/lib/bot/status.d.ts +40 -0
  76. package/dist/lib/bot/status.js +66 -0
  77. package/dist/lib/bot/worker-source.d.ts +66 -0
  78. package/dist/lib/bot/worker-source.js +283 -0
  79. package/dist/lib/workers-registry/read.d.ts +15 -0
  80. package/dist/lib/workers-registry/read.js +17 -0
  81. package/dist/register-all.js +2 -0
  82. package/package.json +2 -1
@@ -0,0 +1,301 @@
1
+ /** Portable, identity-bound context for the local -> Fleet provisioning bridge.
2
+ * This module does not provision compute, copy credentials, or enable a consumer.
3
+ */
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { createHash } from "node:crypto";
7
+ import { isUtf8 } from "node:buffer";
8
+ import * as yaml from "js-yaml";
9
+ import { resolveBotMemoryDir } from "./config.js";
10
+ import { packageRoot } from "../../utils/hq-roots.js";
11
+ const MAX_FILES = 2000;
12
+ const MAX_BYTES = 32 * 1024 * 1024;
13
+ const PRIVATE_NAME = /^(?:\.env(?:\..*)?|\.ssh|\.aws|\.gnupg|\.netrc|\.git|node_modules|machine-creds\.json|bot\.json|auth\.json|credentials(?:\..*)?|id_rsa|id_ed25519)$/i;
14
+ const BUNDLED_SETUP_WORKER_DIR = "assets/bot-workers/setup";
15
+ const BUNDLED_SETUP_CONTEXT = new Set([
16
+ "skills/",
17
+ "context/quick-reference.md",
18
+ "context/USER-GUIDE.md",
19
+ ]);
20
+ function sha(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
21
+ function reject(message) { throw new Error(`Bot continuity: ${message}`); }
22
+ function containsControl(value) { return [...value].some((character) => character.charCodeAt(0) < 32); }
23
+ function relativeFile(value) {
24
+ if (!value || value.includes("\\") || value.split("/").some((part) => !part || part.startsWith(".") || PRIVATE_NAME.test(part)) || containsControl(value)) {
25
+ reject(`unsafe or private path: ${value}`);
26
+ }
27
+ }
28
+ function assertIdentity(agentUid, ownerUid) {
29
+ if (!/^agt_[A-Z0-9]+$/.test(agentUid) || !/^prs_[A-Z0-9]+$/.test(ownerUid))
30
+ reject("invalid identity");
31
+ }
32
+ function digestOf(bundle) {
33
+ return sha(JSON.stringify(bundle));
34
+ }
35
+ /** Reject links in every component before reading anything from the subtree. */
36
+ function plainDirectory(absolute, allowPrivateComponents = false) {
37
+ const resolved = path.resolve(absolute);
38
+ let cursor = path.parse(resolved).root;
39
+ for (const part of resolved.slice(cursor.length).split(path.sep).filter(Boolean)) {
40
+ if (!allowPrivateComponents && PRIVATE_NAME.test(part))
41
+ reject(`private directory: ${part}`);
42
+ cursor = path.join(cursor, part);
43
+ const st = fs.lstatSync(cursor);
44
+ if (st.isSymbolicLink() || !st.isDirectory())
45
+ reject(`directory must not be a symlink: ${cursor}`);
46
+ }
47
+ return resolved;
48
+ }
49
+ /** Capture original bytes, including supporting scripts and Mac-only memory. */
50
+ export function snapshotBotContinuity(config, stateDir) {
51
+ assertIdentity(config.agentUid, config.ownerUid);
52
+ const configuredWorkerRoot = path.resolve(config.hqRoot, config.workerDir);
53
+ const bundledSetupRoot = path.resolve(packageRoot(), BUNDLED_SETUP_WORKER_DIR);
54
+ const isBundledSetup = configuredWorkerRoot === bundledSetupRoot;
55
+ if (!isBundledSetup)
56
+ relativeFile(config.workerDir);
57
+ const expectedPersonal = `personal/workers/${config.name}`;
58
+ const companyPrefix = config.companySlug ? `companies/${config.companySlug}/workers/` : null;
59
+ if (!isBundledSetup && config.workerDir !== expectedPersonal && !config.workerDir.startsWith("core/workers/") && !(companyPrefix && config.workerDir.startsWith(companyPrefix)))
60
+ reject("worker is outside this bot's scope");
61
+ const hqRoot = plainDirectory(config.hqRoot);
62
+ // The generic validator still rejects every node_modules path. This one
63
+ // trusted path is compared first, then every component is checked for links.
64
+ const workerRoot = plainDirectory(isBundledSetup ? bundledSetupRoot : path.join(hqRoot, config.workerDir), isBundledSetup);
65
+ const sourceWorkerDir = isBundledSetup ? BUNDLED_SETUP_WORKER_DIR : config.workerDir;
66
+ const memoryRoot = path.resolve(resolveBotMemoryDir(config));
67
+ const ownSyncedMemory = path.join(hqRoot, expectedPersonal, "memory");
68
+ const ownLocalMemory = path.resolve(stateDir, "memory");
69
+ if (memoryRoot !== ownSyncedMemory && memoryRoot !== ownLocalMemory)
70
+ reject("memory is outside this bot's own memory folders");
71
+ plainDirectory(memoryRoot);
72
+ const files = [];
73
+ let total = 0;
74
+ const walk = (root, area, rel = "") => {
75
+ for (const entry of fs.readdirSync(path.join(root, rel)).sort()) {
76
+ const child = rel ? `${rel}/${entry}` : entry;
77
+ relativeFile(child);
78
+ const absolute = path.join(root, child);
79
+ // Memory is captured once from its configured location, never a stale worker copy.
80
+ if (area === "worker" && child === "memory")
81
+ continue;
82
+ const st = fs.lstatSync(absolute);
83
+ if (st.isSymbolicLink())
84
+ reject(`symlink in context: ${child}`);
85
+ if (st.isDirectory()) {
86
+ walk(root, area, child);
87
+ continue;
88
+ }
89
+ if (!st.isFile())
90
+ reject(`not a regular file: ${child}`);
91
+ if (files.length >= MAX_FILES || total + st.size > MAX_BYTES)
92
+ reject("context exceeds transfer limit; nothing was truncated");
93
+ const fd = fs.openSync(absolute, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
94
+ try {
95
+ const current = fs.fstatSync(fd);
96
+ if (!current.isFile() || current.ino !== st.ino || current.dev !== st.dev)
97
+ reject(`file changed while reading: ${child}`);
98
+ const bytes = fs.readFileSync(fd);
99
+ const after = fs.fstatSync(fd);
100
+ if (after.size !== current.size || after.mtimeMs !== current.mtimeMs || bytes.length !== current.size)
101
+ reject(`file changed while reading: ${child}`);
102
+ total += bytes.length;
103
+ if (total > MAX_BYTES)
104
+ reject("context exceeds transfer limit");
105
+ files.push({ area, path: child, bytes: bytes.length, sha256: sha(bytes), executable: Boolean(st.mode & 0o111), base64: bytes.toString("base64") });
106
+ }
107
+ finally {
108
+ fs.closeSync(fd);
109
+ }
110
+ }
111
+ };
112
+ walk(workerRoot, "worker");
113
+ walk(memoryRoot, "memory");
114
+ if (!files.some((f) => f.area === "worker" && f.path === "worker.yaml"))
115
+ reject("worker.yaml is missing");
116
+ const payload = {
117
+ version: 1, agentUid: config.agentUid, ownerUid: config.ownerUid, name: config.name,
118
+ ...(config.companySlug ? { companySlug: config.companySlug } : {}),
119
+ source: { hqRoot, workerDir: sourceWorkerDir, memoryDir: memoryRoot }, files,
120
+ };
121
+ return { ...payload, digest: digestOf(payload) };
122
+ }
123
+ /** Validate all bytes and ownership before planning any destination write. */
124
+ export function validateBotContinuity(bundle, identity) {
125
+ assertIdentity(bundle.agentUid, bundle.ownerUid);
126
+ if (bundle.version !== 1 || bundle.agentUid !== identity.agentUid || bundle.ownerUid !== identity.ownerUid)
127
+ reject("identity mismatch");
128
+ if (!bundle.source || !path.isAbsolute(bundle.source.hqRoot) || !path.isAbsolute(bundle.source.memoryDir))
129
+ reject("invalid source roots");
130
+ relativeFile(bundle.source.workerDir);
131
+ if (typeof bundle.name !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(bundle.name))
132
+ reject("invalid bot name");
133
+ const { digest, ...payload } = bundle;
134
+ if (digestOf(payload) !== digest)
135
+ reject("manifest digest mismatch");
136
+ if (!Array.isArray(bundle.files) || bundle.files.length > MAX_FILES)
137
+ reject("invalid file count");
138
+ let total = 0;
139
+ const seen = new Set();
140
+ for (const file of bundle.files) {
141
+ relativeFile(file.path);
142
+ if (file.area !== "worker" && file.area !== "memory")
143
+ reject("invalid context area");
144
+ const key = `${file.area}/${file.path}`;
145
+ if (seen.has(key))
146
+ reject(`duplicate file: ${key}`);
147
+ seen.add(key);
148
+ if (typeof file.base64 !== "string" || file.base64.length > Math.ceil(MAX_BYTES / 3) * 4 || !Number.isSafeInteger(file.bytes) || file.bytes < 0 || typeof file.executable !== "boolean")
149
+ reject(`invalid file metadata: ${key}`);
150
+ const bytes = Buffer.from(file.base64, "base64");
151
+ total += bytes.length;
152
+ if (bytes.toString("base64") !== file.base64 || bytes.length !== file.bytes || sha(bytes) !== file.sha256 || total > MAX_BYTES)
153
+ reject(`invalid file bytes: ${key}`);
154
+ }
155
+ validateDeclaredDependencies(bundle);
156
+ }
157
+ /** A relocated reference must resolve to bytes actually present in the bundle. */
158
+ function validateDeclaredDependencies(bundle) {
159
+ const worker = bundle.files.find((file) => file.area === "worker" && file.path === "worker.yaml");
160
+ if (!worker)
161
+ reject("worker.yaml is missing");
162
+ const parsed = yaml.load(Buffer.from(worker.base64, "base64").toString("utf8"));
163
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
164
+ reject("invalid worker.yaml");
165
+ const spec = parsed;
166
+ const bundledSetup = bundle.source.workerDir === BUNDLED_SETUP_WORKER_DIR;
167
+ const requireReference = (reference, directory) => {
168
+ if (typeof reference !== "string" || !reference || containsControl(reference))
169
+ reject("invalid context or skill reference");
170
+ const absolute = path.resolve(bundle.source.hqRoot, reference);
171
+ const roots = [
172
+ ["memory", bundle.source.memoryDir],
173
+ ["worker", path.resolve(bundle.source.hqRoot, bundle.source.workerDir)],
174
+ ];
175
+ for (const [area, root] of roots) {
176
+ const relative = path.relative(root, absolute).split(path.sep).join("/");
177
+ if (relative === ".." || relative.startsWith("../") || path.isAbsolute(relative))
178
+ continue;
179
+ if (bundle.files.some((file) => file.area === area && (file.path === relative || (directory && (!relative || file.path.startsWith(`${relative}/`))))))
180
+ return;
181
+ }
182
+ reject(`dependency is not included in the transfer: ${reference}`);
183
+ };
184
+ if (spec.context?.base !== undefined) {
185
+ if (!Array.isArray(spec.context.base))
186
+ reject("context.base must be an array");
187
+ for (const reference of spec.context.base) {
188
+ const declared = typeof reference === "object" && reference !== null ? reference.path : reference;
189
+ if (bundledSetup) {
190
+ if (typeof declared !== "string" || !BUNDLED_SETUP_CONTEXT.has(declared))
191
+ reject(`unsupported bundled setup dependency: ${String(declared)}`);
192
+ requireReference(path.join(bundle.source.workerDir, declared), true);
193
+ }
194
+ else {
195
+ requireReference(declared, true);
196
+ }
197
+ }
198
+ }
199
+ if (spec.skills !== undefined) {
200
+ if (!Array.isArray(spec.skills))
201
+ reject("skills must be an array");
202
+ for (const skill of spec.skills) {
203
+ if (!skill || typeof skill !== "object" || Array.isArray(skill))
204
+ reject("invalid skill declaration");
205
+ for (const field of ["path", "run"]) {
206
+ if (skill[field] !== undefined)
207
+ requireReference(skill[field], false);
208
+ }
209
+ // Worker templates resolve `doc` relative to the worker, unlike the
210
+ // scaffold's HQ-root-relative `path` and `run` entries.
211
+ if (skill.doc !== undefined) {
212
+ if (typeof skill.doc !== "string")
213
+ reject("invalid skill doc reference");
214
+ requireReference(path.isAbsolute(skill.doc) ? skill.doc : path.join(bundle.source.workerDir, skill.doc), false);
215
+ }
216
+ }
217
+ }
218
+ }
219
+ /** Produce a reviewable write plan, never mutate a live Fleet installation.
220
+ * Source files remain in the bundle. Only known source roots are relocated;
221
+ * unaccounted Mac paths fail rather than silently producing broken skills.
222
+ */
223
+ export function planBotContinuityImport(bundle, target) {
224
+ validateBotContinuity(bundle, target);
225
+ if (bundle.companySlug && bundle.companySlug !== target.companySlug)
226
+ reject("company context cannot cross tenants");
227
+ if (!path.isAbsolute(target.hqRoot) || containsControl(target.hqRoot))
228
+ reject("invalid destination root");
229
+ const ownRoot = `personal/bots/${bundle.agentUid}/continuity`;
230
+ const worker = `${ownRoot}/worker`;
231
+ const memory = `${ownRoot}/memory`;
232
+ const replacements = [
233
+ [bundle.source.memoryDir, path.join(target.hqRoot, memory)],
234
+ [path.join(bundle.source.hqRoot, bundle.source.workerDir), path.join(target.hqRoot, worker)],
235
+ [bundle.source.workerDir, worker],
236
+ [bundle.source.hqRoot, target.hqRoot],
237
+ ].sort((a, b) => b[0].length - a[0].length);
238
+ const mapping = new Map(replacements.map(([from, to]) => [from, to]));
239
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
240
+ const roots = new RegExp(`(?:${[...mapping.keys()].map(escapeRegex).join("|")})(?![\\w.-])`, "g");
241
+ return bundle.files.map((file) => {
242
+ let bytes = Buffer.from(file.base64, "base64");
243
+ if (isUtf8(bytes)) {
244
+ let text = bytes.toString("utf8");
245
+ text = text.replace(roots, (from) => mapping.get(from));
246
+ if (file.area === "worker" && file.path === "worker.yaml" && bundle.source.workerDir === BUNDLED_SETUP_WORKER_DIR) {
247
+ const spec = yaml.load(text);
248
+ if (spec.context?.base) {
249
+ spec.context.base = spec.context.base.map((entry) => typeof entry === "string"
250
+ ? `${worker}/${entry}`
251
+ : { ...entry, path: `${worker}/${entry.path}` });
252
+ }
253
+ text = yaml.dump(spec, { lineWidth: -1 });
254
+ }
255
+ if (file.area === "worker" && /\/(?:Users|Volumes)\//.test(text))
256
+ reject(`unresolved Mac path in ${file.area}/${file.path}`);
257
+ bytes = Buffer.from(text);
258
+ }
259
+ return { path: `${ownRoot}/${file.area}/${file.path}`, bytes, executable: file.executable, sourceSha256: file.sha256, sha256: sha(bytes) };
260
+ });
261
+ }
262
+ /** Prepare the existing Fleet SOUL seam before the first gateway session.
263
+ * This is generated from the private continuity copy, not the owner's overlay.
264
+ * Installation, prompt-cache invalidation and gateway activation remain the
265
+ * provisioning coordinator's responsibility; this function performs no writes.
266
+ */
267
+ export function planFleetContinuity(bundle, target) {
268
+ const files = planBotContinuityImport(bundle, target);
269
+ const workerRoot = `personal/bots/${bundle.agentUid}/continuity/worker`;
270
+ const workerFile = files.find((file) => file.path === `${workerRoot}/worker.yaml`);
271
+ const spec = yaml.load(workerFile.bytes.toString("utf8"));
272
+ const persona = files.find((file) => file.path === `${workerRoot}/persona.md`);
273
+ const identity = { worker: spec.worker, identity: spec.identity, instructions: spec.instructions };
274
+ const sections = [
275
+ "# Continuing your HQ identity",
276
+ "You are the same teammate described below. Moving from this person's computer to cloud compute does not change your identity, role, voice, learned context, or relationship with them. Use your established name from the persona and identity below. Do not introduce yourself as a new bot or as the underlying runtime.",
277
+ `## Identity and role\n\n${JSON.stringify(identity, null, 2)}`,
278
+ ];
279
+ if (persona) {
280
+ if (!isUtf8(persona.bytes))
281
+ reject("persona must be UTF-8 text");
282
+ sections.push(`## Established persona\n\n${persona.bytes.toString("utf8")}`);
283
+ }
284
+ sections.push(`## Existing skills\n\n${JSON.stringify(spec.skills ?? [], null, 2)}\n\nRead the matching skill document before using a skill. Supporting scripts are preserved with the same relative layout. Worker-relative doc references resolve under ${path.join(target.hqRoot, workerRoot)}.`, "## Saved private context\n\nThe following files are your existing context, including memories learned before promotion. Consult the relevant saved files before answering questions about prior work or preferences. These private files are not company-shared knowledge; do not publish or move their contents into the company index. Memory may mention paths on the original Mac. Treat those as historical references, not files available on this cloud computer; do not claim to have accessed them. Runtime credentials and the owner's broader permissions are not part of this transfer. Follow the current HQ authorization and company policies for actions.", ...files.map((file) => `- ${JSON.stringify(path.join(target.hqRoot, file.path))}`));
285
+ const soul = `${sections.join("\n\n")}\n`;
286
+ // Current Fleet render-soul.sh replaces any SOUL containing this marker.
287
+ // Reject rather than ship a persona that the installer would erase.
288
+ if (soul.includes("Nous Research"))
289
+ reject("persona conflicts with Fleet's default-identity replacement marker");
290
+ if (Buffer.byteLength(soul, "utf8") > 32 * 1024)
291
+ reject("identity context exceeds the SOUL transfer budget; nothing was truncated");
292
+ const policyContent = `${JSON.stringify({ schemaVersion: 1, agentUid: bundle.agentUid, scope: "private" })}\n`;
293
+ return { files, soul, soulSha256: sha(soul), sourceDigest: bundle.digest,
294
+ memoryPolicy: {
295
+ path: `personal/bots/${bundle.agentUid}/continuity/memory-policy.json`,
296
+ content: policyContent,
297
+ sha256: sha(policyContent),
298
+ },
299
+ };
300
+ }
301
+ //# sourceMappingURL=continuity.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Per-bot Cognito machine credentials (local-bots US-005).
3
+ *
4
+ * Written 0600 in the exact shape `ensureCognitoToken({ tokenSource:
5
+ * "machine" })` / hq-cloud `loadMachineCreds` read from HQ_MACHINE_CREDS_FILE.
6
+ * The secret is never logged or printed; `hq bot status` only reports whether
7
+ * the file exists and which agt_ it is for.
8
+ */
9
+ export interface BotMachineCreds {
10
+ username: string;
11
+ secret: string;
12
+ entityType: "agent";
13
+ entityUid: string;
14
+ clientId: string;
15
+ region: string;
16
+ }
17
+ export declare function writeBotCreds(dir: string, creds: BotMachineCreds): string;
18
+ /** Non-secret view of the creds file: which identity it is for. */
19
+ export declare function readBotCredsIdentity(dir: string): {
20
+ entityUid: string;
21
+ username: string;
22
+ } | null;
23
+ export declare function deleteBotCreds(dir: string): void;
24
+ //# sourceMappingURL=creds.d.ts.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Per-bot Cognito machine credentials (local-bots US-005).
3
+ *
4
+ * Written 0600 in the exact shape `ensureCognitoToken({ tokenSource:
5
+ * "machine" })` / hq-cloud `loadMachineCreds` read from HQ_MACHINE_CREDS_FILE.
6
+ * The secret is never logged or printed; `hq bot status` only reports whether
7
+ * the file exists and which agt_ it is for.
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { botCredsPath } from "./paths.js";
12
+ export function writeBotCreds(dir, creds) {
13
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
14
+ const dest = botCredsPath(dir);
15
+ const tmp = path.join(dir, `.creds.tmp.${process.pid}.${Date.now()}`);
16
+ fs.writeFileSync(tmp, `${JSON.stringify(creds, null, 2)}\n`, { mode: 0o600 });
17
+ fs.renameSync(tmp, dest);
18
+ try {
19
+ fs.chmodSync(dest, 0o600);
20
+ }
21
+ catch {
22
+ /* best-effort */
23
+ }
24
+ return dest;
25
+ }
26
+ /** Non-secret view of the creds file: which identity it is for. */
27
+ export function readBotCredsIdentity(dir) {
28
+ const p = botCredsPath(dir);
29
+ if (!fs.existsSync(p))
30
+ return null;
31
+ try {
32
+ const raw = JSON.parse(fs.readFileSync(p, "utf8"));
33
+ if (typeof raw.entityUid !== "string" || typeof raw.username !== "string")
34
+ return null;
35
+ if (typeof raw.secret !== "string" || !raw.secret)
36
+ return null;
37
+ return { entityUid: raw.entityUid, username: raw.username };
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ export function deleteBotCreds(dir) {
44
+ try {
45
+ fs.unlinkSync(botCredsPath(dir));
46
+ }
47
+ catch {
48
+ /* already gone */
49
+ }
50
+ }
51
+ //# sourceMappingURL=creds.js.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Per-bot launchd user agent (local-bots US-005). macOS only for this PRD;
3
+ * Linux writes a systemd user unit as a best-effort extra.
4
+ *
5
+ * ~/Library/LaunchAgents/ai.getindigo.hq-bot.<name>.plist
6
+ * ProgramArguments: <node> <hq> bot run <name>
7
+ * RunAtLoad + KeepAlive{SuccessfulExit:false} — starts at login, restarts
8
+ * on crash, but a clean exit 0 (bot disabled via `hq bot stop`) stays down.
9
+ *
10
+ * `launchctl` is injected so tests never touch the real service manager.
11
+ */
12
+ import * as fs from "node:fs";
13
+ export type BotDaemonPlatform = "darwin" | "linux" | "other";
14
+ export interface BotDaemonPaths {
15
+ home: string;
16
+ name: string;
17
+ label: string;
18
+ hqBinary: string;
19
+ nodeBinary: string;
20
+ botDir: string;
21
+ logPath: string;
22
+ plistPath: string;
23
+ systemdUnitPath: string;
24
+ /** Extra PATH entries so the model CLI resolves under launchd. */
25
+ pathEntries: string[];
26
+ hqRoot?: string;
27
+ }
28
+ export interface LaunchctlExec {
29
+ (args: string[]): {
30
+ ok: boolean;
31
+ stdout: string;
32
+ stderr: string;
33
+ };
34
+ }
35
+ export declare function detectBotPlatform(platform?: NodeJS.Platform): BotDaemonPlatform;
36
+ export declare function defaultPathEntries(home: string): string[];
37
+ export declare function buildBotDaemonPaths(opts: {
38
+ name: string;
39
+ botDir: string;
40
+ home?: string;
41
+ hqBinary?: string;
42
+ nodeBinary?: string;
43
+ hqRoot?: string;
44
+ env?: NodeJS.ProcessEnv;
45
+ argv?: string[];
46
+ execPath?: string;
47
+ }): BotDaemonPaths;
48
+ export declare function renderBotLaunchdPlist(p: BotDaemonPaths): string;
49
+ export declare function renderBotSystemdUnit(p: BotDaemonPaths): string;
50
+ export declare function launchdDomain(uid?: number): string;
51
+ export interface BotDaemonResult {
52
+ platform: BotDaemonPlatform;
53
+ action: "install" | "uninstall" | "status" | "start" | "stop";
54
+ installed: boolean;
55
+ loaded: boolean;
56
+ dest?: string;
57
+ message: string;
58
+ }
59
+ export interface BotDaemonDeps {
60
+ platform?: BotDaemonPlatform;
61
+ launchctl?: LaunchctlExec;
62
+ uid?: number;
63
+ fsImpl?: Pick<typeof fs, "existsSync" | "writeFileSync" | "mkdirSync" | "unlinkSync">;
64
+ /** Blocking sleep, injectable for tests. */
65
+ sleepMs?: (ms: number) => void;
66
+ }
67
+ /** Write the unit and load it (RunAtLoad starts the bot immediately). */
68
+ export declare function installBotDaemon(p: BotDaemonPaths, deps?: BotDaemonDeps): BotDaemonResult;
69
+ export declare function uninstallBotDaemon(p: BotDaemonPaths, deps?: BotDaemonDeps): BotDaemonResult;
70
+ export declare function botDaemonStatus(p: BotDaemonPaths, deps?: BotDaemonDeps): BotDaemonResult;
71
+ /** Load an installed LaunchAgent (idempotent: already-loaded is fine). */
72
+ export declare function startBotDaemon(p: BotDaemonPaths, deps?: BotDaemonDeps): BotDaemonResult;
73
+ /** Unload the job (plist stays, so the bot returns at next login unless disabled). */
74
+ export declare function stopBotDaemon(p: BotDaemonPaths, deps?: BotDaemonDeps): BotDaemonResult;
75
+ //# sourceMappingURL=daemon.d.ts.map