@indigoai-us/hq-cli 5.109.15 → 5.110.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/assets/bot-workers/setup/context/USER-GUIDE.md +363 -0
  3. package/assets/bot-workers/setup/context/quick-reference.md +199 -0
  4. package/assets/bot-workers/setup/skills/first-company.md +71 -0
  5. package/assets/bot-workers/setup/skills/standing-help.md +74 -0
  6. package/assets/bot-workers/setup/worker.yaml +422 -0
  7. package/dist/commands/bot-continuity.d.ts +28 -0
  8. package/dist/commands/bot-continuity.js +68 -0
  9. package/dist/commands/bot.d.ts +73 -0
  10. package/dist/commands/bot.js +776 -0
  11. package/dist/commands/workers.d.ts +2 -14
  12. package/dist/commands/workers.js +2 -8
  13. package/dist/lib/bot/api.d.ts +202 -0
  14. package/dist/lib/bot/api.js +202 -0
  15. package/dist/lib/bot/company-bind.d.ts +27 -0
  16. package/dist/lib/bot/company-bind.js +62 -0
  17. package/dist/lib/bot/config.d.ts +106 -0
  18. package/dist/lib/bot/config.js +141 -0
  19. package/dist/lib/bot/continuity-download.d.ts +28 -0
  20. package/dist/lib/bot/continuity-download.js +75 -0
  21. package/dist/lib/bot/continuity-install.d.ts +14 -0
  22. package/dist/lib/bot/continuity-install.js +101 -0
  23. package/dist/lib/bot/continuity.d.ts +66 -0
  24. package/dist/lib/bot/continuity.js +301 -0
  25. package/dist/lib/bot/creds.d.ts +24 -0
  26. package/dist/lib/bot/creds.js +51 -0
  27. package/dist/lib/bot/daemon.d.ts +75 -0
  28. package/dist/lib/bot/daemon.js +316 -0
  29. package/dist/lib/bot/inbox-state.d.ts +18 -0
  30. package/dist/lib/bot/inbox-state.js +51 -0
  31. package/dist/lib/bot/index.d.ts +16 -0
  32. package/dist/lib/bot/index.js +16 -0
  33. package/dist/lib/bot/inflight.d.ts +40 -0
  34. package/dist/lib/bot/inflight.js +44 -0
  35. package/dist/lib/bot/log.d.ts +13 -0
  36. package/dist/lib/bot/log.js +59 -0
  37. package/dist/lib/bot/owner-context.d.ts +75 -0
  38. package/dist/lib/bot/owner-context.js +151 -0
  39. package/dist/lib/bot/paths.d.ts +61 -0
  40. package/dist/lib/bot/paths.js +103 -0
  41. package/dist/lib/bot/progress.d.ts +84 -0
  42. package/dist/lib/bot/progress.js +167 -0
  43. package/dist/lib/bot/promote.d.ts +16 -0
  44. package/dist/lib/bot/promote.js +106 -0
  45. package/dist/lib/bot/promotion-hold.d.ts +24 -0
  46. package/dist/lib/bot/promotion-hold.js +103 -0
  47. package/dist/lib/bot/promotion-receipt.d.ts +9 -0
  48. package/dist/lib/bot/promotion-receipt.js +56 -0
  49. package/dist/lib/bot/promotion-upload.d.ts +16 -0
  50. package/dist/lib/bot/promotion-upload.js +65 -0
  51. package/dist/lib/bot/prompt.d.ts +103 -0
  52. package/dist/lib/bot/prompt.js +329 -0
  53. package/dist/lib/bot/room-policy.d.ts +53 -0
  54. package/dist/lib/bot/room-policy.js +73 -0
  55. package/dist/lib/bot/run.d.ts +98 -0
  56. package/dist/lib/bot/run.js +787 -0
  57. package/dist/lib/bot/runtime/claude.d.ts +49 -0
  58. package/dist/lib/bot/runtime/claude.js +151 -0
  59. package/dist/lib/bot/runtime/codex.d.ts +28 -0
  60. package/dist/lib/bot/runtime/codex.js +147 -0
  61. package/dist/lib/bot/runtime/grok.d.ts +16 -0
  62. package/dist/lib/bot/runtime/grok.js +67 -0
  63. package/dist/lib/bot/runtime/index.d.ts +35 -0
  64. package/dist/lib/bot/runtime/index.js +279 -0
  65. package/dist/lib/bot/runtime/messages-stream.d.ts +27 -0
  66. package/dist/lib/bot/runtime/messages-stream.js +85 -0
  67. package/dist/lib/bot/runtime/types.d.ts +136 -0
  68. package/dist/lib/bot/runtime/types.js +51 -0
  69. package/dist/lib/bot/scaffold.d.ts +38 -0
  70. package/dist/lib/bot/scaffold.js +94 -0
  71. package/dist/lib/bot/session.d.ts +19 -0
  72. package/dist/lib/bot/session.js +39 -0
  73. package/dist/lib/bot/status.d.ts +40 -0
  74. package/dist/lib/bot/status.js +66 -0
  75. package/dist/lib/bot/worker-source.d.ts +66 -0
  76. package/dist/lib/bot/worker-source.js +283 -0
  77. package/dist/lib/workers-registry/read.d.ts +15 -0
  78. package/dist/lib/workers-registry/read.js +17 -0
  79. package/dist/register-all.js +2 -0
  80. package/package.json +3 -2
@@ -0,0 +1,316 @@
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
+ import * as os from "node:os";
14
+ import * as path from "node:path";
15
+ import { resolveHqBinary, resolveNodeBinary } from "../mesh/live/daemon/install.js";
16
+ import { botLogPath, launchdLabel } from "./paths.js";
17
+ export function detectBotPlatform(platform = process.platform) {
18
+ if (platform === "darwin")
19
+ return "darwin";
20
+ if (platform === "linux")
21
+ return "linux";
22
+ return "other";
23
+ }
24
+ export function defaultPathEntries(home) {
25
+ const toolchain = path.join(home, "Library", "Application Support", "Indigo HQ", "toolchain");
26
+ return [
27
+ // launchd does not load shell profiles; npm CLIs also need env node.
28
+ path.join(toolchain, "node", "bin"),
29
+ path.join(toolchain, "npm-global", "bin"),
30
+ path.join(home, ".local", "bin"),
31
+ path.join(home, ".grok", "bin"),
32
+ path.join(home, "Library", "pnpm"),
33
+ path.join(home, ".npm-global", "bin"),
34
+ "/opt/homebrew/bin",
35
+ "/usr/local/bin",
36
+ "/usr/bin",
37
+ "/bin",
38
+ ];
39
+ }
40
+ export function buildBotDaemonPaths(opts) {
41
+ const home = opts.home ?? os.homedir();
42
+ const env = opts.env ?? process.env;
43
+ const label = launchdLabel(opts.name);
44
+ return {
45
+ home,
46
+ name: opts.name,
47
+ label,
48
+ hqBinary: opts.hqBinary ?? resolveHqBinary(opts.argv, env),
49
+ nodeBinary: opts.nodeBinary ?? resolveNodeBinary(opts.execPath, env),
50
+ botDir: opts.botDir,
51
+ logPath: botLogPath(opts.botDir),
52
+ plistPath: path.join(home, "Library", "LaunchAgents", `${label}.plist`),
53
+ systemdUnitPath: path.join(home, ".config", "systemd", "user", `hq-bot-${opts.name}.service`),
54
+ pathEntries: defaultPathEntries(home),
55
+ hqRoot: opts.hqRoot,
56
+ };
57
+ }
58
+ function escapeXml(value) {
59
+ return value
60
+ .replace(/&/g, "&amp;")
61
+ .replace(/</g, "&lt;")
62
+ .replace(/>/g, "&gt;")
63
+ .replace(/"/g, "&quot;");
64
+ }
65
+ export function renderBotLaunchdPlist(p) {
66
+ const envRows = [
67
+ ["HOME", p.home],
68
+ ["PATH", p.pathEntries.join(":")],
69
+ ["HQ_NO_UPDATE_CHECK", "1"],
70
+ ...(p.hqRoot ? [["HQ_ROOT", p.hqRoot]] : []),
71
+ ]
72
+ .map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
73
+ .join("\n");
74
+ return `<?xml version="1.0" encoding="UTF-8"?>
75
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
76
+ <plist version="1.0">
77
+ <dict>
78
+ <key>Label</key>
79
+ <string>${escapeXml(p.label)}</string>
80
+ <key>ProgramArguments</key>
81
+ <array>
82
+ <string>${escapeXml(p.nodeBinary)}</string>
83
+ <string>${escapeXml(p.hqBinary)}</string>
84
+ <string>bot</string>
85
+ <string>run</string>
86
+ <string>${escapeXml(p.name)}</string>
87
+ </array>
88
+ <key>WorkingDirectory</key>
89
+ <string>${escapeXml(p.hqRoot ?? p.botDir)}</string>
90
+ <key>RunAtLoad</key>
91
+ <true/>
92
+ <key>KeepAlive</key>
93
+ <dict>
94
+ <key>SuccessfulExit</key>
95
+ <false/>
96
+ </dict>
97
+ <key>ThrottleInterval</key>
98
+ <integer>5</integer>
99
+ <key>ProcessType</key>
100
+ <!-- Interactive, not Background: launchd throttles Background processes
101
+ (CPU/IO/network), which doubled every model turn (63 s vs 28 s for the
102
+ same Grok call, 2026-09-11). The bot answers a person in real time. -->
103
+ <string>Interactive</string>
104
+ <key>StandardOutPath</key>
105
+ <string>${escapeXml(p.logPath)}</string>
106
+ <key>StandardErrorPath</key>
107
+ <string>${escapeXml(p.logPath)}</string>
108
+ <key>EnvironmentVariables</key>
109
+ <dict>
110
+ ${envRows}
111
+ </dict>
112
+ </dict>
113
+ </plist>
114
+ `;
115
+ }
116
+ export function renderBotSystemdUnit(p) {
117
+ const q = (v) => (/^[A-Za-z0-9_./:-]+$/.test(v) ? v : `'${v.replace(/'/g, `'\\''`)}'`);
118
+ return `[Unit]
119
+ Description=HQ local bot ${p.name}
120
+ After=network-online.target
121
+ Wants=network-online.target
122
+
123
+ [Service]
124
+ Type=simple
125
+ ExecStart=${q(p.nodeBinary)} ${q(p.hqBinary)} bot run ${p.name}
126
+ WorkingDirectory=${p.hqRoot ?? p.botDir}
127
+ Restart=on-failure
128
+ RestartSec=5
129
+ Environment=HOME=${p.home}
130
+ Environment=PATH=${p.pathEntries.join(":")}
131
+ Environment=HQ_NO_UPDATE_CHECK=1
132
+ ${p.hqRoot ? `Environment=HQ_ROOT=${p.hqRoot}\n` : ""}StandardOutput=append:${p.logPath}
133
+ StandardError=append:${p.logPath}
134
+
135
+ [Install]
136
+ WantedBy=default.target
137
+ `;
138
+ }
139
+ export function launchdDomain(uid = os.userInfo().uid) {
140
+ return `gui/${uid}`;
141
+ }
142
+ function blockingSleep(ms) {
143
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
144
+ }
145
+ /**
146
+ * `launchctl bootout` returns before the job is actually gone; a `bootstrap`
147
+ * issued right away fails ("service already loaded" / EIO) and the old job
148
+ * then finishes unloading, leaving nothing running even though `print` said
149
+ * loaded a moment ago. Wait for the unload, then bootstrap with retries.
150
+ */
151
+ function rebootstrapLaunchd(p, deps) {
152
+ const launchctl = deps.launchctl;
153
+ const sleep = deps.sleepMs ?? blockingSleep;
154
+ launchctl(["bootout", `${launchdDomain(deps.uid)}/${p.label}`]);
155
+ for (let i = 0; i < 20 && launchdLoaded(p, deps); i++)
156
+ sleep(250);
157
+ let r = launchctl(["bootstrap", launchdDomain(deps.uid), p.plistPath]);
158
+ for (let i = 0; i < 3 && !r.ok; i++) {
159
+ sleep(500);
160
+ r = launchctl(["bootstrap", launchdDomain(deps.uid), p.plistPath]);
161
+ }
162
+ return r;
163
+ }
164
+ function launchdLoaded(p, deps) {
165
+ if (!deps.launchctl)
166
+ return false;
167
+ return deps.launchctl(["print", `${launchdDomain(deps.uid)}/${p.label}`]).ok;
168
+ }
169
+ /** Write the unit and load it (RunAtLoad starts the bot immediately). */
170
+ export function installBotDaemon(p, deps = {}) {
171
+ const platform = deps.platform ?? detectBotPlatform();
172
+ const io = deps.fsImpl ?? fs;
173
+ io.mkdirSync(p.botDir, { recursive: true, mode: 0o700 });
174
+ io.mkdirSync(path.dirname(p.logPath), { recursive: true, mode: 0o700 });
175
+ if (platform === "darwin") {
176
+ io.mkdirSync(path.dirname(p.plistPath), { recursive: true });
177
+ io.writeFileSync(p.plistPath, renderBotLaunchdPlist(p), { mode: 0o600 });
178
+ let loaded = false;
179
+ if (deps.launchctl) {
180
+ // bootout first so a re-install picks up a changed plist.
181
+ const r = rebootstrapLaunchd(p, deps);
182
+ loaded = r.ok || launchdLoaded(p, deps);
183
+ if (!loaded) {
184
+ return {
185
+ platform,
186
+ action: "install",
187
+ installed: true,
188
+ loaded: false,
189
+ dest: p.plistPath,
190
+ message: `Wrote ${p.plistPath} but launchctl bootstrap failed: ${(r.stderr || r.stdout).trim().slice(0, 200)}`,
191
+ };
192
+ }
193
+ }
194
+ return {
195
+ platform,
196
+ action: "install",
197
+ installed: true,
198
+ loaded,
199
+ dest: p.plistPath,
200
+ message: `Installed LaunchAgent ${p.label} (${loaded ? "loaded" : "not loaded"})`,
201
+ };
202
+ }
203
+ if (platform === "linux") {
204
+ io.mkdirSync(path.dirname(p.systemdUnitPath), { recursive: true });
205
+ io.writeFileSync(p.systemdUnitPath, renderBotSystemdUnit(p), { mode: 0o600 });
206
+ return {
207
+ platform,
208
+ action: "install",
209
+ installed: true,
210
+ loaded: false,
211
+ dest: p.systemdUnitPath,
212
+ message: `Wrote systemd user unit ${p.systemdUnitPath}. Enable with: systemctl --user daemon-reload && systemctl --user enable --now hq-bot-${p.name}.service`,
213
+ };
214
+ }
215
+ return {
216
+ platform,
217
+ action: "install",
218
+ installed: false,
219
+ loaded: false,
220
+ message: `No service manager integration on this platform; run: ${p.nodeBinary} ${p.hqBinary} bot run ${p.name}`,
221
+ };
222
+ }
223
+ export function uninstallBotDaemon(p, deps = {}) {
224
+ const platform = deps.platform ?? detectBotPlatform();
225
+ const io = deps.fsImpl ?? fs;
226
+ if (platform === "darwin") {
227
+ deps.launchctl?.(["bootout", `${launchdDomain(deps.uid)}/${p.label}`]);
228
+ const existed = io.existsSync(p.plistPath);
229
+ if (existed)
230
+ io.unlinkSync(p.plistPath);
231
+ return {
232
+ platform,
233
+ action: "uninstall",
234
+ installed: false,
235
+ loaded: false,
236
+ dest: p.plistPath,
237
+ message: existed ? `Removed LaunchAgent ${p.label}` : `LaunchAgent ${p.label} was not installed`,
238
+ };
239
+ }
240
+ if (platform === "linux") {
241
+ const existed = io.existsSync(p.systemdUnitPath);
242
+ if (existed)
243
+ io.unlinkSync(p.systemdUnitPath);
244
+ return {
245
+ platform,
246
+ action: "uninstall",
247
+ installed: false,
248
+ loaded: false,
249
+ dest: p.systemdUnitPath,
250
+ message: existed ? `Removed systemd unit for ${p.name}` : `No systemd unit for ${p.name}`,
251
+ };
252
+ }
253
+ return { platform, action: "uninstall", installed: false, loaded: false, message: "Nothing to uninstall on this platform" };
254
+ }
255
+ export function botDaemonStatus(p, deps = {}) {
256
+ const platform = deps.platform ?? detectBotPlatform();
257
+ const io = deps.fsImpl ?? fs;
258
+ if (platform === "darwin") {
259
+ const installed = io.existsSync(p.plistPath);
260
+ const loaded = installed && launchdLoaded(p, deps);
261
+ return {
262
+ platform,
263
+ action: "status",
264
+ installed,
265
+ loaded,
266
+ dest: p.plistPath,
267
+ message: installed
268
+ ? `LaunchAgent ${p.label}: ${loaded ? "loaded" : "installed (not loaded)"}`
269
+ : `LaunchAgent ${p.label}: not installed`,
270
+ };
271
+ }
272
+ if (platform === "linux") {
273
+ const installed = io.existsSync(p.systemdUnitPath);
274
+ return {
275
+ platform,
276
+ action: "status",
277
+ installed,
278
+ loaded: false,
279
+ dest: p.systemdUnitPath,
280
+ message: installed ? `systemd unit for ${p.name}: installed` : `systemd unit for ${p.name}: not installed`,
281
+ };
282
+ }
283
+ return { platform, action: "status", installed: false, loaded: false, message: "No service manager integration on this platform" };
284
+ }
285
+ /** Load an installed LaunchAgent (idempotent: already-loaded is fine). */
286
+ export function startBotDaemon(p, deps = {}) {
287
+ const platform = deps.platform ?? detectBotPlatform();
288
+ const io = deps.fsImpl ?? fs;
289
+ if (platform !== "darwin" || !deps.launchctl || !io.existsSync(p.plistPath)) {
290
+ return { ...botDaemonStatus(p, deps), action: "start" };
291
+ }
292
+ if (launchdLoaded(p, deps)) {
293
+ // Already loaded: kick the job so a stopped (exit-0) bot runs again.
294
+ deps.launchctl(["kickstart", `${launchdDomain(deps.uid)}/${p.label}`]);
295
+ return { platform, action: "start", installed: true, loaded: true, dest: p.plistPath, message: `LaunchAgent ${p.label} kicked` };
296
+ }
297
+ const r = deps.launchctl(["bootstrap", launchdDomain(deps.uid), p.plistPath]);
298
+ const loaded = r.ok || launchdLoaded(p, deps);
299
+ return {
300
+ platform,
301
+ action: "start",
302
+ installed: true,
303
+ loaded,
304
+ dest: p.plistPath,
305
+ message: loaded ? `LaunchAgent ${p.label} loaded` : `launchctl bootstrap failed: ${(r.stderr || r.stdout).trim().slice(0, 200)}`,
306
+ };
307
+ }
308
+ /** Unload the job (plist stays, so the bot returns at next login unless disabled). */
309
+ export function stopBotDaemon(p, deps = {}) {
310
+ const platform = deps.platform ?? detectBotPlatform();
311
+ if (platform === "darwin" && deps.launchctl) {
312
+ deps.launchctl(["bootout", `${launchdDomain(deps.uid)}/${p.label}`]);
313
+ }
314
+ return { ...botDaemonStatus(p, deps), action: "stop" };
315
+ }
316
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * inbox-state.json — processed messageIds so a DM pulled but not acked
3
+ * before a crash is answered exactly once after restart (local-bots US-004).
4
+ */
5
+ export declare const INBOX_STATE_MAX_ENTRIES = 500;
6
+ export interface InboxStateFile {
7
+ v: 1;
8
+ /** messageId → ISO time the reply was sent (or the refusal was sent). */
9
+ processed: Record<string, string>;
10
+ updatedAt: string;
11
+ }
12
+ export declare function readInboxState(dir: string): InboxStateFile;
13
+ export declare function writeInboxState(dir: string, state: InboxStateFile): void;
14
+ export declare function isProcessed(state: InboxStateFile, messageId: string): boolean;
15
+ /** Record a processed id, trimming the oldest entries past the cap. Pure. */
16
+ export declare function withProcessed(state: InboxStateFile, messageId: string, at: string, cap?: number): InboxStateFile;
17
+ export declare function markProcessed(dir: string, messageId: string, at?: string): InboxStateFile;
18
+ //# sourceMappingURL=inbox-state.d.ts.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * inbox-state.json — processed messageIds so a DM pulled but not acked
3
+ * before a crash is answered exactly once after restart (local-bots US-004).
4
+ */
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ import { botInboxStatePath } from "./paths.js";
8
+ export const INBOX_STATE_MAX_ENTRIES = 500;
9
+ export function readInboxState(dir) {
10
+ const p = botInboxStatePath(dir);
11
+ if (!fs.existsSync(p))
12
+ return { v: 1, processed: {}, updatedAt: new Date(0).toISOString() };
13
+ try {
14
+ const raw = JSON.parse(fs.readFileSync(p, "utf8"));
15
+ if (raw.v !== 1 || typeof raw.processed !== "object" || !raw.processed) {
16
+ return { v: 1, processed: {}, updatedAt: new Date(0).toISOString() };
17
+ }
18
+ return raw;
19
+ }
20
+ catch {
21
+ return { v: 1, processed: {}, updatedAt: new Date(0).toISOString() };
22
+ }
23
+ }
24
+ export function writeInboxState(dir, state) {
25
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
26
+ const dest = botInboxStatePath(dir);
27
+ const tmp = path.join(dir, `.inbox.tmp.${process.pid}.${Date.now()}`);
28
+ fs.writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
29
+ fs.renameSync(tmp, dest);
30
+ }
31
+ export function isProcessed(state, messageId) {
32
+ return Object.prototype.hasOwnProperty.call(state.processed, messageId);
33
+ }
34
+ /** Record a processed id, trimming the oldest entries past the cap. Pure. */
35
+ export function withProcessed(state, messageId, at, cap = INBOX_STATE_MAX_ENTRIES) {
36
+ const processed = { ...state.processed, [messageId]: at };
37
+ const keys = Object.keys(processed);
38
+ if (keys.length > cap) {
39
+ keys
40
+ .sort((a, b) => processed[a].localeCompare(processed[b]))
41
+ .slice(0, keys.length - cap)
42
+ .forEach((k) => delete processed[k]);
43
+ }
44
+ return { v: 1, processed, updatedAt: at };
45
+ }
46
+ export function markProcessed(dir, messageId, at = new Date().toISOString()) {
47
+ const next = withProcessed(readInboxState(dir), messageId, at);
48
+ writeInboxState(dir, next);
49
+ return next;
50
+ }
51
+ //# sourceMappingURL=inbox-state.js.map
@@ -0,0 +1,16 @@
1
+ export * from "./paths.js";
2
+ export * from "./config.js";
3
+ export * from "./creds.js";
4
+ export * from "./status.js";
5
+ export * from "./log.js";
6
+ export * from "./inbox-state.js";
7
+ export * from "./session.js";
8
+ export * from "./prompt.js";
9
+ export * from "./api.js";
10
+ export * from "./daemon.js";
11
+ export * from "./scaffold.js";
12
+ export * from "./worker-source.js";
13
+ export * from "./company-bind.js";
14
+ export * from "./run.js";
15
+ export * from "./runtime/index.js";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ export * from "./paths.js";
2
+ export * from "./config.js";
3
+ export * from "./creds.js";
4
+ export * from "./status.js";
5
+ export * from "./log.js";
6
+ export * from "./inbox-state.js";
7
+ export * from "./session.js";
8
+ export * from "./prompt.js";
9
+ export * from "./api.js";
10
+ export * from "./daemon.js";
11
+ export * from "./scaffold.js";
12
+ export * from "./worker-source.js";
13
+ export * from "./company-bind.js";
14
+ export * from "./run.js";
15
+ export * from "./runtime/index.js";
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * inflight.json — the message a model turn is working on right now.
3
+ *
4
+ * Written before the model process starts and removed once the message is
5
+ * answered (or answered with "did not finish"). If the bot restarts while the
6
+ * file exists, the turn was cut off: the bot tells the person instead of
7
+ * running the message again, because the first run may already have done
8
+ * things outside this Mac (sent something, published something).
9
+ */
10
+ export declare const BOT_INFLIGHT_NAME = "inflight.json";
11
+ export interface InflightTurn {
12
+ v: 1;
13
+ /** Inbox message id, or `kickoff` for the first-start kickoff turn. */
14
+ messageId: string;
15
+ /** Where replies for this message go. */
16
+ target: {
17
+ kind: "dm";
18
+ peerUid: string;
19
+ } | {
20
+ kind: "room";
21
+ channelId: string;
22
+ };
23
+ rootEventId?: string;
24
+ /** DM message carrying the thinking reaction, so a restart can clear it. */
25
+ reactionEventId?: string;
26
+ startedAt: string;
27
+ /** Progress messages already posted for this turn. */
28
+ progressPosted?: number;
29
+ /**
30
+ * The finished answer, when the turn completed but posting it failed. A
31
+ * restart posts this instead of "did not finish".
32
+ */
33
+ reply?: string;
34
+ }
35
+ export declare function botInflightPath(dir: string): string;
36
+ export declare function readInflight(dir: string): InflightTurn | null;
37
+ export declare function writeInflight(dir: string, turn: InflightTurn): void;
38
+ export declare function patchInflight(dir: string, messageId: string, patch: Partial<InflightTurn>): void;
39
+ export declare function clearInflight(dir: string, messageId?: string): void;
40
+ //# sourceMappingURL=inflight.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * inflight.json — the message a model turn is working on right now.
3
+ *
4
+ * Written before the model process starts and removed once the message is
5
+ * answered (or answered with "did not finish"). If the bot restarts while the
6
+ * file exists, the turn was cut off: the bot tells the person instead of
7
+ * running the message again, because the first run may already have done
8
+ * things outside this Mac (sent something, published something).
9
+ */
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ export const BOT_INFLIGHT_NAME = "inflight.json";
13
+ export function botInflightPath(dir) {
14
+ return path.join(dir, BOT_INFLIGHT_NAME);
15
+ }
16
+ export function readInflight(dir) {
17
+ try {
18
+ const raw = JSON.parse(fs.readFileSync(botInflightPath(dir), "utf8"));
19
+ if (raw?.v !== 1 || typeof raw.messageId !== "string" || !raw.target)
20
+ return null;
21
+ return raw;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ export function writeInflight(dir, turn) {
28
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
29
+ const tmp = path.join(dir, `.inflight.tmp.${process.pid}.${Date.now()}`);
30
+ fs.writeFileSync(tmp, `${JSON.stringify(turn, null, 2)}\n`, { mode: 0o600 });
31
+ fs.renameSync(tmp, botInflightPath(dir));
32
+ }
33
+ export function patchInflight(dir, messageId, patch) {
34
+ const current = readInflight(dir);
35
+ if (!current || current.messageId !== messageId)
36
+ return;
37
+ writeInflight(dir, { ...current, ...patch });
38
+ }
39
+ export function clearInflight(dir, messageId) {
40
+ if (messageId !== undefined && readInflight(dir)?.messageId !== messageId)
41
+ return;
42
+ fs.rmSync(botInflightPath(dir), { force: true });
43
+ }
44
+ //# sourceMappingURL=inflight.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * ~/.hq/bots/<name>/logs/bot.log with rotation at 5MB. Never writes prompts,
3
+ * tokens, credentials, or DM bodies — only metadata and bounded error text.
4
+ */
5
+ export declare function ensureBotLog(dir: string): string;
6
+ export declare function rotateBotLogIfNeeded(dir: string, maxBytes?: number): void;
7
+ /** Strip anything that looks like a bearer token, secret, or op:// ref. */
8
+ export declare function redactForLog(message: string): string;
9
+ export declare function botLogLine(level: "info" | "warn" | "error", message: string, now?: () => Date): string;
10
+ export declare function appendBotLog(dir: string, line: string): void;
11
+ export type BotLogger = (level: "info" | "warn" | "error", message: string) => void;
12
+ export declare function createBotLogger(dir: string, now?: () => Date): BotLogger;
13
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * ~/.hq/bots/<name>/logs/bot.log with rotation at 5MB. Never writes prompts,
3
+ * tokens, credentials, or DM bodies — only metadata and bounded error text.
4
+ */
5
+ import * as fs from "node:fs";
6
+ import { BOT_LOG_MAX_BYTES, botLogPath, botLogsDir } from "./paths.js";
7
+ export function ensureBotLog(dir) {
8
+ fs.mkdirSync(botLogsDir(dir), { recursive: true, mode: 0o700 });
9
+ const logPath = botLogPath(dir);
10
+ if (!fs.existsSync(logPath))
11
+ fs.writeFileSync(logPath, "", { mode: 0o600 });
12
+ return logPath;
13
+ }
14
+ export function rotateBotLogIfNeeded(dir, maxBytes = BOT_LOG_MAX_BYTES) {
15
+ const logPath = botLogPath(dir);
16
+ try {
17
+ if (!fs.existsSync(logPath))
18
+ return;
19
+ if (fs.statSync(logPath).size <= maxBytes)
20
+ return;
21
+ const rotated = `${logPath}.1`;
22
+ try {
23
+ fs.unlinkSync(rotated);
24
+ }
25
+ catch {
26
+ /* ignore */
27
+ }
28
+ fs.renameSync(logPath, rotated);
29
+ fs.writeFileSync(logPath, "", { mode: 0o600 });
30
+ }
31
+ catch {
32
+ /* best-effort */
33
+ }
34
+ }
35
+ /** Strip anything that looks like a bearer token, secret, or op:// ref. */
36
+ export function redactForLog(message) {
37
+ return message
38
+ .replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [REDACTED]")
39
+ .replace(/op:\/\/[^\s"]+/g, "op://[REDACTED]")
40
+ .replace(/"secret"\s*:\s*"[^"]*"/g, '"secret":"[REDACTED]"')
41
+ .replace(/eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}/g, "[REDACTED_JWT]");
42
+ }
43
+ export function botLogLine(level, message, now = () => new Date()) {
44
+ return `${now().toISOString()} ${level} ${redactForLog(message).slice(0, 600)}`;
45
+ }
46
+ export function appendBotLog(dir, line) {
47
+ rotateBotLogIfNeeded(dir);
48
+ const logPath = ensureBotLog(dir);
49
+ try {
50
+ fs.appendFileSync(logPath, line.endsWith("\n") ? line : `${line}\n`);
51
+ }
52
+ catch {
53
+ /* best-effort */
54
+ }
55
+ }
56
+ export function createBotLogger(dir, now = () => new Date()) {
57
+ return (level, message) => appendBotLog(dir, botLogLine(level, message, now));
58
+ }
59
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Owner context: who the bot's owner is and which companies they belong to.
3
+ *
4
+ * A local bot runs under its OWN HQ identity (per-bot machine credentials set
5
+ * for the whole resident process, and inherited by the model subprocess). So
6
+ * any membership lookup the model makes with `hq` describes the bot, and a bot
7
+ * usually belongs to no company. Bots were reading that empty answer as "your
8
+ * owner has no company".
9
+ *
10
+ * The resident process (trusted code, not the model) looks the owner up with
11
+ * the owner's own cached HQ session, strictly non-interactively, and verifies
12
+ * the session really is the owner before trusting it. Only the result (company
13
+ * slugs, uids, roles) reaches the model; the owner's token never does. A failed
14
+ * lookup is reported as "could not check", never as "no companies".
15
+ */
16
+ export interface OwnerCompany {
17
+ companyUid: string;
18
+ /** Present when the server enriched it. */
19
+ companySlug?: string;
20
+ role: string;
21
+ }
22
+ export type OwnerContext = {
23
+ status: "ok";
24
+ ownerUid: string;
25
+ companies: OwnerCompany[];
26
+ checkedAt: string;
27
+ } | {
28
+ status: "unavailable";
29
+ ownerUid: string;
30
+ reason: string;
31
+ checkedAt: string;
32
+ } | {
33
+ status: "mismatch";
34
+ ownerUid: string;
35
+ signedInAs: string;
36
+ checkedAt: string;
37
+ };
38
+ export interface OwnerLookupIo {
39
+ /** The owner's own session token; must never prompt or open a browser. */
40
+ personToken: () => Promise<string>;
41
+ /** `prs_*` uid the token belongs to. */
42
+ callerPersonUid: (token: string) => Promise<string>;
43
+ /** Raw memberships for the token's subject. */
44
+ memberships: (token: string) => Promise<Array<{
45
+ companyUid: string;
46
+ companySlug?: string;
47
+ role: string;
48
+ status: string;
49
+ }>>;
50
+ now?: () => Date;
51
+ }
52
+ export declare const defaultOwnerLookupIo: OwnerLookupIo;
53
+ export declare function lookupOwnerContext(ownerUid: string, io?: OwnerLookupIo): Promise<OwnerContext>;
54
+ /** Successful lookups are reused for 5 minutes, failures retried after 1 minute. */
55
+ export declare const OWNER_CONTEXT_TTL_MS: number;
56
+ export declare const OWNER_CONTEXT_RETRY_MS = 60000;
57
+ /** A turn never waits longer than this for a refresh; the lookup keeps going in the background. */
58
+ export declare const OWNER_CONTEXT_WAIT_MS = 8000;
59
+ export declare class OwnerContextCache {
60
+ private readonly lookup;
61
+ private readonly nowMs;
62
+ private readonly waitMs;
63
+ private readonly ownerUid;
64
+ private value;
65
+ private at;
66
+ private pending;
67
+ constructor(lookup: () => Promise<OwnerContext>, nowMs?: () => number, waitMs?: number, ownerUid?: string);
68
+ get(): Promise<OwnerContext>;
69
+ }
70
+ /**
71
+ * The block prepended to every owner DM turn. It names its subject explicitly so the
72
+ * model cannot confuse the owner's memberships with its own.
73
+ */
74
+ export declare function ownerContextBlock(ctx: OwnerContext, agentUid: string): string;
75
+ //# sourceMappingURL=owner-context.d.ts.map