@indigoai-us/hq-cli 5.345.183 → 5.345.188

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 (46) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/command-catalog.generated.d.ts +29 -0
  3. package/dist/command-catalog.generated.js +35 -0
  4. package/dist/commands/agent-inbox.js +43 -11
  5. package/dist/commands/agent-mcp.js +5 -2
  6. package/dist/commands/bot.d.ts +5 -5
  7. package/dist/commands/bot.js +30 -31
  8. package/dist/commands/sources.d.ts +37 -0
  9. package/dist/commands/sources.js +104 -1
  10. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +34414 -49801
  11. package/dist/hq-cloud-sync-runner/package.json +1 -1
  12. package/dist/lib/agent-kit/inbox-fetch.d.ts +92 -0
  13. package/dist/lib/agent-kit/inbox-fetch.js +135 -0
  14. package/dist/lib/agent-kit/mcp/tools.d.ts +11 -2
  15. package/dist/lib/agent-kit/mcp/tools.js +17 -3
  16. package/dist/lib/anywhere-person-setting.js +24 -10
  17. package/dist/lib/bot/fleet-local.d.ts +4 -2
  18. package/dist/lib/bot/fleet-local.js +23 -7
  19. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +57 -7
  20. package/dist/lib/bot/jobs/hot-signal-sync.js +106 -9
  21. package/dist/lib/bot/local-jobs.d.ts +4 -2
  22. package/dist/lib/bot/local-jobs.js +33 -4
  23. package/dist/lib/bot/run.js +2 -2
  24. package/dist/lib/hot-channels/adapters/attio.js +3 -0
  25. package/dist/lib/hot-channels/adapters/linear.js +4 -0
  26. package/dist/lib/hot-channels/adapters/meetings.js +4 -0
  27. package/dist/lib/hot-channels/adapters/slack.js +3 -0
  28. package/dist/lib/hot-channels/contract.d.ts +7 -0
  29. package/dist/lib/hot-channels/ledger.d.ts +8 -0
  30. package/dist/lib/hot-channels/ledger.js +6 -2
  31. package/dist/lib/hot-channels/loop.d.ts +6 -0
  32. package/dist/lib/hot-channels/loop.js +16 -2
  33. package/dist/lib/hot-channels/person-vault-reader.d.ts +41 -0
  34. package/dist/lib/hot-channels/person-vault-reader.js +59 -0
  35. package/dist/lib/hot-channels/run.d.ts +8 -0
  36. package/dist/lib/hot-channels/run.js +19 -0
  37. package/dist/lib/hot-channels/schedule.d.ts +64 -0
  38. package/dist/lib/hot-channels/schedule.js +162 -0
  39. package/dist/lib/hot-channels/selection.d.ts +87 -0
  40. package/dist/lib/hot-channels/selection.js +175 -0
  41. package/dist/utils/cognito-session.d.ts +1 -1
  42. package/dist/utils/cognito-session.js +16 -4
  43. package/dist/utils/shipped-codex-hooks.js +1 -1
  44. package/dist/utils/version-report.d.ts +1 -1
  45. package/dist/utils/version-report.js +20 -7
  46. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.188] — 2026-10-30
6
+ - `hq agent inbox` and the `hq_inbox_read` MCP tool now bound the server read (token mint plus GET) at 30 s
7
+ (`HQ_AGENT_INBOX_TIMEOUT_MS`, floor 1000) and retry it once after a short delay, honouring the server's
8
+ numeric `Retry-After` up to 5 s, when the token mint is held by another process, the request times out or
9
+ fails in transport, or the server answers 408, 425, 429, 500, 502 or 503. A 504 is left to the existing
10
+ single retry inside the API client, and a 401, 403, 404 or an invalid 200 body is not retried. The
11
+ deadline now reaches the hq-cloud mint, so a retry no longer queues behind a first mint that is still
12
+ waiting on another process's lock. The list command sets `HQ_MACHINE_CREDS_FILE`,
13
+ `HQ_REQUIRE_MACHINE_IDENTITY` and `HQ_VAULT_API_URL` before minting, as `hq agent inbox done` already did.
14
+ The JSON output gains `stale: true|false` next to `source`; whenever a retry happened, including one that
15
+ succeeded, it also carries `attempts: 2`, and a note that still reports a fallback ends with
16
+ "(after 2 attempts)", so automation can tell a hard failure from a single blip (DEV-4065).
17
+ - Keep downloaded revisions current and retry locally deleted files until the cloud confirms their removal.
18
+
19
+ ## [5.345.187] — 2026-10-30
20
+ - The HQ Anywhere person gate now follows the desktop Settings key `hq-anywhere-opt-out`. Turning HQ Anywhere on in the app enables `hq mcp serve` without a separate `hq-anywhere` write. Older servers that only have `hq-anywhere` still work.
21
+
22
+ - `hq version` and the desktop HQ Core row now send your GitHub token when they ask GitHub for the latest HQ Core release. Without it, GitHub's anonymous cap made the check fail and the app showed Not checked. A stale or revoked token falls back to the anonymous public lookup once.
23
+ - Repairing an existing local bot now installs the CLI's pinned HQ Fleet Runtime release before restarting it.
24
+ - Local bots now keep each HQ DM thread in its own conversation, so a reply cannot use context from another thread.
25
+ - Updating an existing local bot now imports its refreshed machine credential exactly once.
26
+ - Updating an existing local bot now records the HQ Fleet Runtime release it actually installed.
27
+ - The hot-channel sync now honours the channels, issues and records you picked for each connection. Slack lands only threads in picked channels, Linear only picked issues (by id or identifier such as ENG-123) and Attio only picked records. An empty pick on a Slack, Linear or Attio connection counts as no choice yet and syncs everything; saving an empty pick on every meeting recorder leaves meetings out. A connection with no saved pick syncs everything. When a saved pick may exist but cannot be read (an error, no answer within 5 seconds, or a malformed file), that connection is skipped for the run and its run-ledger line is marked failed with reason `selection-unreadable`; the other connections still sync. Gmail does not use picks yet.
28
+ - A bot with no company of its own, such as the personal bot first-run setup creates, now registers the daily hot-channel sync for your active company (`activeCompany` in the HQ config, the company `hq sources sync` uses). Only one local bot registers it per company, and a job you removed is not added back. When the job is first created the bot runs one sync right away in the background, marked `firstRun` in the run ledger, so a new install does not wait for the first scheduled time.
29
+ - New `hq sources schedule` shows when the bot's daily hot-channel sync runs, on which bot, and the latest run of each adapter over the last 7 days. `--at HH:MM` moves it to that time each day in the job's own time zone; `--json` prints `{ company, job, lastRuns }`.
30
+ - Adding a job to a local bot no longer replaces an unreadable `jobs.json` with a list holding only the new job; it stops with an error and leaves the file as it is.
31
+
5
32
  ## [5.345.183] — 2026-10-30
6
33
  - New local Grok bots now reuse an existing encrypted Grok login on the same computer instead of asking you to sign in again.
7
34
 
@@ -5239,6 +5239,35 @@ export declare const COMMAND_CATALOG: readonly [{
5239
5239
  readonly defaultValue: "<runtime-path>";
5240
5240
  }];
5241
5241
  readonly subcommands: readonly [];
5242
+ }, {
5243
+ readonly name: "schedule";
5244
+ readonly description: "Show when the bot's daily hot-channel sync runs, or set it with --at HH:MM";
5245
+ readonly aliases: readonly [];
5246
+ readonly hidden: false;
5247
+ readonly helpOption: true;
5248
+ readonly helpCommand: false;
5249
+ readonly usage: "[options]";
5250
+ readonly helpText: readonly [];
5251
+ readonly arguments: readonly [];
5252
+ readonly options: readonly [{
5253
+ readonly flags: "--company <slug>";
5254
+ readonly description: "Company slug (defaults to .hq/config.json activeCompany)";
5255
+ readonly hidden: false;
5256
+ }, {
5257
+ readonly flags: "--at <HH:MM>";
5258
+ readonly description: "Run the sync daily at this 24-hour time, in the job's time zone";
5259
+ readonly hidden: false;
5260
+ }, {
5261
+ readonly flags: "--json";
5262
+ readonly description: "Print the schedule and last runs as JSON";
5263
+ readonly hidden: false;
5264
+ }, {
5265
+ readonly flags: "--hq-root <path>";
5266
+ readonly description: "Local HQ tree root (default: <runtime-path>)";
5267
+ readonly hidden: false;
5268
+ readonly defaultValue: "<runtime-path>";
5269
+ }];
5270
+ readonly subcommands: readonly [];
5242
5271
  }, {
5243
5272
  readonly name: "sync";
5244
5273
  readonly description: "Run every hot channel now: each adapter once per connection you can use";
@@ -6353,6 +6353,41 @@ export const COMMAND_CATALOG = [
6353
6353
  ],
6354
6354
  "subcommands": []
6355
6355
  },
6356
+ {
6357
+ "name": "schedule",
6358
+ "description": "Show when the bot's daily hot-channel sync runs, or set it with --at HH:MM",
6359
+ "aliases": [],
6360
+ "hidden": false,
6361
+ "helpOption": true,
6362
+ "helpCommand": false,
6363
+ "usage": "[options]",
6364
+ "helpText": [],
6365
+ "arguments": [],
6366
+ "options": [
6367
+ {
6368
+ "flags": "--company <slug>",
6369
+ "description": "Company slug (defaults to .hq/config.json activeCompany)",
6370
+ "hidden": false
6371
+ },
6372
+ {
6373
+ "flags": "--at <HH:MM>",
6374
+ "description": "Run the sync daily at this 24-hour time, in the job's time zone",
6375
+ "hidden": false
6376
+ },
6377
+ {
6378
+ "flags": "--json",
6379
+ "description": "Print the schedule and last runs as JSON",
6380
+ "hidden": false
6381
+ },
6382
+ {
6383
+ "flags": "--hq-root <path>",
6384
+ "description": "Local HQ tree root (default: <runtime-path>)",
6385
+ "hidden": false,
6386
+ "defaultValue": "<runtime-path>"
6387
+ }
6388
+ ],
6389
+ "subcommands": []
6390
+ },
6356
6391
  {
6357
6392
  "name": "sync",
6358
6393
  "description": "Run every hot channel now: each adapter once per connection you can use",
@@ -8,6 +8,7 @@ import chalk from "chalk";
8
8
  import * as os from "node:os";
9
9
  import { ensureCognitoToken } from "../utils/cognito-session.js";
10
10
  import { vaultApiFetch } from "../utils/vault-api.js";
11
+ import { annotateInboxAttempts, fetchServerInboxWithRetry, parseRetryAfterSeconds, resolveAgentInboxTimeoutMs } from "../lib/agent-kit/inbox-fetch.js";
11
12
  import { redactAgentInboxItem } from "../lib/agent-kit/inbox-redact.js";
12
13
  import { markInboxDone, mergeAgentInbox, readDoneIds, summarizeInboxEntry } from "../lib/agent-kit/inbox-state.js";
13
14
  import { agentKitPaths } from "../lib/agent-kit/paths.js";
@@ -23,25 +24,56 @@ export function registerAgentInboxCommand(agent) {
23
24
  const paths = agentKitPaths(os.homedir(), process.env);
24
25
  const creds = requireExternalCreds(paths);
25
26
  const done = readDoneIds(paths);
27
+ // Pin the enrolled machine identity and API, as `done` does below, so the
28
+ // mint cannot fall through to a person session or another API base.
29
+ process.env.HQ_MACHINE_CREDS_FILE = paths.machineCredsPath;
30
+ process.env.HQ_REQUIRE_MACHINE_IDENTITY = "1";
31
+ process.env.HQ_VAULT_API_URL = creds.apiBaseUrl;
26
32
  // Merged with the server, never the local mirror alone: a dead kit
27
33
  // poller would otherwise make this command answer confidently with
28
- // yesterday's mail and no sign anything was wrong.
29
- const { entries, note, source } = await mergeAgentInbox(paths, async () => {
30
- const token = await ensureCognitoToken({ tokenSource: "machine", interactive: false });
31
- const res = await vaultApiFetch({
32
- token,
33
- baseUrl: creds.apiBaseUrl,
34
- path: `/v1/agents/${encodeURIComponent(creds.entityUid)}/inbox`,
34
+ // yesterday's mail and no sign anything was wrong. The server read is
35
+ // bounded and retried once (inbox-fetch.ts) so a shared-lock mint, a
36
+ // 503 with Retry-After or a hung socket does not become a fallback.
37
+ let attempts = 1;
38
+ const merged = await mergeAgentInbox(paths, async () => {
39
+ const outcome = await fetchServerInboxWithRetry({
40
+ timeoutMs: resolveAgentInboxTimeoutMs(process.env),
41
+ mint: (signal) => ensureCognitoToken({ tokenSource: "machine", interactive: false, signal }),
42
+ fetch: async (token, signal) => {
43
+ const res = await vaultApiFetch({
44
+ token,
45
+ baseUrl: creds.apiBaseUrl,
46
+ path: `/v1/agents/${encodeURIComponent(creds.entityUid)}/inbox`,
47
+ signal,
48
+ });
49
+ const retryAfterSeconds = parseRetryAfterSeconds(res.headers?.get?.("retry-after"));
50
+ // Preserve a decode failure as an invalid body for mergeAgentInbox to
51
+ // classify. Parser errors can contain response bytes: do not echo them.
52
+ return {
53
+ status: res.status,
54
+ body: await res.json().catch(() => undefined),
55
+ ...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}),
56
+ };
57
+ },
35
58
  });
36
- // Preserve a decode failure as an invalid body for mergeAgentInbox to
37
- // classify. Parser errors can contain response bytes: do not echo them.
38
- return { status: res.status, body: await res.json().catch(() => undefined) };
59
+ attempts = outcome.attempts;
60
+ if (!outcome.ok)
61
+ throw outcome.error;
62
+ return outcome.response;
39
63
  });
64
+ const { entries, note, source, stale, attempts: attemptsMade } = annotateInboxAttempts(merged, attempts);
40
65
  const rows = entries
41
66
  .filter((e) => opts.all || !done.has(e.id))
42
67
  .map((e) => summarizeInboxEntry(redactAgentInboxItem(e), done.has(e.id)));
43
68
  if (opts.json) {
44
- console.log(JSON.stringify({ count: rows.length, source, messages: rows, ...(note ? { note } : {}) }, null, 2));
69
+ console.log(JSON.stringify({
70
+ count: rows.length,
71
+ source,
72
+ stale,
73
+ ...(attemptsMade !== undefined ? { attempts: attemptsMade } : {}),
74
+ messages: rows,
75
+ ...(note ? { note } : {}),
76
+ }, null, 2));
45
77
  return;
46
78
  }
47
79
  if (note)
@@ -13,6 +13,7 @@ import * as os from "node:os";
13
13
  import { CLI_VERSION } from "../cli-version.js";
14
14
  import { ensureCognitoToken } from "../utils/cognito-session.js";
15
15
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
16
+ import { parseRetryAfterSeconds } from "../lib/agent-kit/inbox-fetch.js";
16
17
  import { McpServer } from "../lib/agent-kit/mcp/jsonrpc.js";
17
18
  import { createVaultObjectDownloader } from "../lib/agent-kit/attachment-download.js";
18
19
  import { buildAgentMcpTools } from "../lib/agent-kit/mcp/tools.js";
@@ -59,7 +60,7 @@ export function defaultMcpClients(creds, credsPath, paths = agentKitPaths(os.hom
59
60
  HQ_VAULT_API_URL: creds.apiBaseUrl,
60
61
  NO_COLOR: "1",
61
62
  };
62
- const getToken = () => ensureCognitoToken({ tokenSource: "machine", interactive: false });
63
+ const getToken = (signal) => ensureCognitoToken({ tokenSource: "machine", interactive: false, signal });
63
64
  let companyUid;
64
65
  return {
65
66
  creds,
@@ -89,9 +90,11 @@ export function defaultMcpClients(creds, credsPath, paths = agentKitPaths(os.hom
89
90
  method: init?.method,
90
91
  body: init?.body,
91
92
  query: init?.query,
93
+ signal: init?.signal,
92
94
  });
93
95
  const body = await res.json().catch(() => ({}));
94
- return { status: res.status, body };
96
+ const retryAfterSeconds = parseRetryAfterSeconds(res.headers.get("retry-after"));
97
+ return { status: res.status, body, ...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}) };
95
98
  },
96
99
  };
97
100
  }
@@ -409,10 +409,10 @@ export interface RemoteBotRow {
409
409
  export declare function registerBotCommand(program: Command): void;
410
410
  /**
411
411
  * The company a bot's hot-signal-sync job syncs: the bot's own company, else
412
- * the first company it serves. Null = none, and no job is registered. There is
413
- * no fallback to the HQ's active company: a personal bot syncs a company only
414
- * when it was given one explicitly (`--company`), so the job never writes into
415
- * a vault the bot does not belong to.
412
+ * the first company it serves, else (for a bot with no company) the HQ's
413
+ * active company. Null = none, and no job is registered.
416
414
  */
417
- export declare function hotSignalSyncCompany(config: Pick<BotConfig, "companySlug" | "kind" | "companies" | "workerId">): string | null;
415
+ export declare function hotSignalSyncCompany(config: Pick<BotConfig, "companySlug" | "kind" | "companies" | "workerId"> & {
416
+ hqRoot?: string;
417
+ }): string | null;
418
418
  //# sourceMappingURL=bot.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { withLocalJobs } from "../lib/bot/local-jobs.js";
2
- import { currentHotSignalSyncCompany, ensureHotSignalSyncJob, removeStaleHotSignalSyncJobs, runHotSignalSyncJob } from "../lib/bot/jobs/hot-signal-sync.js";
2
+ import { currentHotSignalSyncCompany, readActiveCompanySlug, registerHotSignalSyncOnStart, runHotSignalSyncJob } from "../lib/bot/jobs/hot-signal-sync.js";
3
3
  import { createClaudeSignalsPass } from "../lib/hot-channels/signals-pass.js";
4
4
  import { machineTimeZone } from "../lib/bot/job-run.js";
5
5
  import { runCompanyHotChannelSync } from "./sources.js";
@@ -478,6 +478,7 @@ async function adoptFleetRuntimeProfile(name, dir, config) {
478
478
  provider: fleetProviderForRuntime(config.runtime, config.model),
479
479
  ownerCompanies: companyScope,
480
480
  }, { python, start: false });
481
+ patchBotConfig(dir, { engine: installation.engine });
481
482
  const credentials = botCredsPath(dir);
482
483
  if (fs.existsSync(credentials))
483
484
  importFleetLocalMachineCredential(config.agentUid, credentials);
@@ -1310,24 +1311,31 @@ async function runBotResident(nameArg) {
1310
1311
  // The built-in hot-signal-sync job, registered once in the bot's own
1311
1312
  // jobs.json (06:30 daily in this computer's time zone until the owner
1312
1313
  // reschedules it with `hq bot jobs`). It runs in-process, with no model turn.
1313
- const syncCompany = hotSignalSyncCompany(config);
1314
+ // A bot with no company (the personal bot first-run setup creates) syncs the
1315
+ // HQ's active company. Only one local bot registers the job per company, and
1316
+ // the first sync starts in the background right after the job is created.
1317
+ const runHotSignalSync = (input) => runHotSignalSyncJob({
1318
+ ...input,
1319
+ sync: (syncInput) => runCompanyHotChannelSync({ hqRoot: config.hqRoot, ...syncInput }),
1320
+ // A new pass per run: the login preflight and the 20-document cap are per run.
1321
+ signalsPass: createClaudeSignalsPass({ hqRoot: config.hqRoot, company: input.company }),
1322
+ });
1323
+ let otherBotDirs = [];
1314
1324
  try {
1315
- // A job for a company the bot no longer serves is removed before anything runs.
1316
- for (const stale of removeStaleHotSignalSyncJobs(dir, syncCompany)) {
1317
- console.log(`Removed the hot-channel sync job ${stale.jobId}: this bot no longer syncs that company.`);
1318
- }
1319
- }
1320
- catch (err) {
1321
- console.error(`Could not check the hot-channel sync jobs: ${err instanceof Error ? err.message : String(err)}`);
1325
+ otherBotDirs = listBotNames(botsRoot()).map((n) => botDir(n));
1322
1326
  }
1323
- if (syncCompany) {
1324
- try {
1325
- ensureHotSignalSyncJob(dir, { company: syncCompany, timezone: machineTimeZone() });
1326
- }
1327
- catch (err) {
1328
- console.error(`Could not register the hot-channel sync job for ${syncCompany}: ${err instanceof Error ? err.message : String(err)}`);
1329
- }
1327
+ catch {
1328
+ // No other bots could be listed; this bot is the only one checked.
1330
1329
  }
1330
+ registerHotSignalSyncOnStart({
1331
+ dir,
1332
+ config,
1333
+ activeCompany: readActiveCompanySlug(config.hqRoot) ?? null,
1334
+ otherBotDirs,
1335
+ timezone: machineTimeZone(),
1336
+ runFirst: (company) => runHotSignalSync({ company, catchUp: false, firstRun: true, now: () => new Date() }),
1337
+ log: (level, message) => (level === "warn" ? console.error(message) : console.log(message)),
1338
+ });
1331
1339
  const owner = new OwnerContextCache(() => lookupOwnerContext(config.ownerUid), Date.now, undefined, config.ownerUid);
1332
1340
  const handle = await runBot({
1333
1341
  dir,
@@ -1335,15 +1343,7 @@ async function runBotResident(nameArg) {
1335
1343
  api,
1336
1344
  runtime: runtimeFor(config.runtime),
1337
1345
  ownerContext: () => owner.get(),
1338
- hotSignalSync: ({ company, catchUp, now, sendAlert }) => runHotSignalSyncJob({
1339
- company,
1340
- catchUp,
1341
- now,
1342
- sendAlert,
1343
- sync: (input) => runCompanyHotChannelSync({ hqRoot: config.hqRoot, ...input }),
1344
- // A new pass per run: the login preflight and the 20-document cap are per run.
1345
- signalsPass: createClaudeSignalsPass({ hqRoot: config.hqRoot, company }),
1346
- }),
1346
+ hotSignalSync: ({ company, catchUp, now, sendAlert }) => runHotSignalSync({ company, catchUp, now, sendAlert }),
1347
1347
  // US-005: decision picks in the owner DM, with the bot's own machine token
1348
1348
  // (never the owner's login).
1349
1349
  decisions: new DecisionsClient(token),
@@ -2556,7 +2556,8 @@ async function repairLocalBot(name, api, bot, daemon) {
2556
2556
  createBotLogger(dir)("info", `re-issued machine credentials for ${name} (${bot.agentUid}) on ${localMachineId()}`);
2557
2557
  if (isFleetRuntimeBot(config)) {
2558
2558
  const adopted = await adoptFleetRuntimeProfile(name, dir, config);
2559
- importFleetLocalMachineCredential(bot.agentUid, botCredsPath(dir));
2559
+ if (!adopted)
2560
+ importFleetLocalMachineCredential(bot.agentUid, botCredsPath(dir));
2560
2561
  const restarted = controlFleetLocalBot(bot.agentUid, adopted ? "install" : "restart").ok === true;
2561
2562
  return restarted ? "new credentials, HQ Fleet Runtime restarted" : "new credentials; the HQ Fleet Runtime did not restart (hq bot start " + name + ")";
2562
2563
  }
@@ -2937,12 +2938,10 @@ export function registerBotCommand(program) {
2937
2938
  }
2938
2939
  /**
2939
2940
  * The company a bot's hot-signal-sync job syncs: the bot's own company, else
2940
- * the first company it serves. Null = none, and no job is registered. There is
2941
- * no fallback to the HQ's active company: a personal bot syncs a company only
2942
- * when it was given one explicitly (`--company`), so the job never writes into
2943
- * a vault the bot does not belong to.
2941
+ * the first company it serves, else (for a bot with no company) the HQ's
2942
+ * active company. Null = none, and no job is registered.
2944
2943
  */
2945
2944
  export function hotSignalSyncCompany(config) {
2946
- return currentHotSignalSyncCompany(config);
2945
+ return currentHotSignalSyncCompany(config, config.hqRoot ? (readActiveCompanySlug(config.hqRoot) ?? null) : null);
2947
2946
  }
2948
2947
  //# sourceMappingURL=bot.js.map
@@ -13,6 +13,7 @@
13
13
  * hq sources entities List entities the caller has access to.
14
14
  * hq sources runs Show the local hot-channel run ledger for one day.
15
15
  * hq sources sync Run every hot channel now for the signed-in person.
16
+ * hq sources schedule Show or set when the bot's daily hot-channel sync runs.
16
17
  *
17
18
  * Defaults:
18
19
  * --entity falls back to .hq/config.json activeCompany (per sync.ts).
@@ -22,6 +23,8 @@
22
23
  import { Command } from "commander";
23
24
  import { type RunLedgerAdapterSummary, type RunLedgerEntry, type RunTrigger, type SignalsGeneratedFn } from "../lib/hot-channels/ledger.js";
24
25
  import type { HotChannelRegistry } from "../lib/hot-channels/registry.js";
26
+ import { type SelectionLoader } from "../lib/hot-channels/selection.js";
27
+ import { type ScheduleReport } from "../lib/hot-channels/schedule.js";
25
28
  import { type OnDemandSyncDeps, type OnDemandSyncReport } from "../lib/hot-channels/run.js";
26
29
  type ListFormat = "table" | "json";
27
30
  export interface RunsOptions {
@@ -58,6 +61,8 @@ export interface SyncOverrides {
58
61
  now?: () => Date;
59
62
  /** The signals pass; defaults to the local `claude -p` pass under the user's own login. */
60
63
  signalsGenerated?: SignalsGeneratedFn;
64
+ /** This run's saved-selection loader; defaults to the person's vault bucket. */
65
+ createSelections?: () => SelectionLoader | null;
61
66
  }
62
67
  /**
63
68
  * Network access for one sync: the catalog, the person's usable connections,
@@ -74,6 +79,8 @@ export interface CompanyHotChannelSyncInput {
74
79
  /** `scheduled` for the bot's hot-signal-sync job; `on-demand` by default. */
75
80
  trigger?: RunTrigger;
76
81
  catchUp?: boolean;
82
+ /** The first run after the bot registered its hot-signal-sync job; marked in the ledger. */
83
+ firstRun?: boolean;
77
84
  signalsGenerated?: SignalsGeneratedFn;
78
85
  /** The signed-in person's `prs_` person uid in this company; defaults to a roster lookup by email. */
79
86
  resolvePersonUid?: (token: string, companyUid: string, email: string) => Promise<string | null>;
@@ -83,6 +90,12 @@ export interface CompanyHotChannelSyncInput {
83
90
  * `settings/signals/preferences.yaml`, through the HQ integrations gateway.
84
91
  */
85
92
  signalsAlert?: SignalsAlertSenders;
93
+ /**
94
+ * Builds this run's saved-selection loader (one per run). Defaults to
95
+ * reading `integrations/hot-channels/<connectionId>.json` from the
96
+ * person's own vault bucket. Return null to sync without selections.
97
+ */
98
+ createSelections?: () => SelectionLoader | null;
86
99
  }
87
100
  export interface SignalsAlertSenders {
88
101
  /** The primary alert to the owner; throws when it was not delivered. */
@@ -106,6 +119,30 @@ export declare function runCompanyHotChannelSync(input: CompanyHotChannelSyncInp
106
119
  * line under `companies/{company}/sources/`, and nothing else.
107
120
  */
108
121
  export declare function runSync(options: SyncOptions, overrides?: SyncOverrides): Promise<OnDemandSyncReport>;
122
+ export interface ScheduleOptions {
123
+ company?: string;
124
+ at?: string;
125
+ json?: boolean;
126
+ hqRoot: string;
127
+ }
128
+ export interface ScheduleOverrides {
129
+ now?: Date;
130
+ /** Where local bots live; defaults to `~/.hq/bots` (or HQ_BOTS_ROOT). */
131
+ botsRoot?: string;
132
+ /** Whose ledger to read; defaults to the signed-in person. */
133
+ email?: string;
134
+ }
135
+ export declare class NoHotSignalSyncJobError extends Error {
136
+ readonly company: string;
137
+ constructor(company: string);
138
+ }
139
+ /**
140
+ * Show the company's hot-signal-sync job (bot, schedule, next and last run)
141
+ * and each adapter's most recent ledger entry over the last 7 UTC days. With
142
+ * `--at HH:MM`, reschedule the job to daily at that time in its own time zone
143
+ * first. Exits non-zero when `--at` is given and no job exists.
144
+ */
145
+ export declare function runSchedule(options: ScheduleOptions, overrides?: ScheduleOverrides): Promise<ScheduleReport>;
109
146
  export declare function registerSourcesCommand(program: Command): void;
110
147
  export {};
111
148
  //# sourceMappingURL=sources.d.ts.map
@@ -13,6 +13,7 @@
13
13
  * hq sources entities List entities the caller has access to.
14
14
  * hq sources runs Show the local hot-channel run ledger for one day.
15
15
  * hq sources sync Run every hot channel now for the signed-in person.
16
+ * hq sources schedule Show or set when the bot's daily hot-channel sync runs.
16
17
  *
17
18
  * Defaults:
18
19
  * --entity falls back to .hq/config.json activeCompany (per sync.ts).
@@ -24,7 +25,7 @@ import * as fs from "node:fs";
24
25
  import * as path from "node:path";
25
26
  import chalk from "chalk";
26
27
  import { SOURCE_CHANNELS, assertSourceChannel, resolveEntity, listAvailableEntities, listSources, getSource, VaultClient, } from "@indigoai-us/hq-cloud";
27
- import { ensureCognitoToken, ensureCognitoIdToken, buildVaultConfig, DEFAULT_HQ_ROOT, } from "../utils/cognito-session.js";
28
+ import { ensureCognitoToken, ensureCognitoIdToken, buildVaultConfig, DEFAULT_COGNITO, DEFAULT_HQ_ROOT, } from "../utils/cognito-session.js";
28
29
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
29
30
  import { isAuthError } from "../utils/auth-error.js";
30
31
  import { IntegrationsCliError, callGateway, fetchUsableApps, gatewayResultIsError, isBuiltInGatewayProvider, raiseForResponse, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
@@ -37,6 +38,12 @@ import { createClaudeSignalsPass } from "../lib/hot-channels/signals-pass.js";
37
38
  import { assertSafeSegment } from "../lib/hot-channels/landing.js";
38
39
  import { acquireSyncLock, SYNC_LOCK_BUSY_REASON } from "../lib/hot-channels/sync-lock.js";
39
40
  import { createBuiltinHotChannelRegistry } from "../lib/hot-channels/adapters/index.js";
41
+ import { createSelectionLoader } from "../lib/hot-channels/selection.js";
42
+ import { createPersonVaultReader } from "../lib/hot-channels/person-vault-reader.js";
43
+ import { S3Client } from "@aws-sdk/client-s3";
44
+ import { resolveCanonicalPersonUid } from "./cloud.js";
45
+ import { botsRoot as defaultBotsRoot } from "../lib/bot/paths.js";
46
+ import { findHotSignalSyncJobs, lastRunsPerAdapter, parseDailyAt, renderScheduleReport, rescheduleDaily, toJobView, } from "../lib/hot-channels/schedule.js";
40
47
  import { listActiveMembers } from "./members.js";
41
48
  import { runOnDemandSync, } from "../lib/hot-channels/run.js";
42
49
  // ---------------------------------------------------------------------------
@@ -376,6 +383,36 @@ async function signedInSession(company) {
376
383
  throw err;
377
384
  }
378
385
  }
386
+ /**
387
+ * The default selection loader: reads each connection's saved pick from the
388
+ * signed-in person's vault bucket through the same STS vend-self path as
389
+ * `hq files cat --personal`. Read failures log to stderr and fall back to
390
+ * landing everything; they never fail the run.
391
+ */
392
+ function defaultSelections() {
393
+ let client;
394
+ const vault = () => {
395
+ client ??= (async () => new VaultClient(buildVaultConfig(process.env.HQ_ACCESS_TOKEN || (await ensureCognitoToken({ interactive: false })))))();
396
+ return client;
397
+ };
398
+ const reader = createPersonVaultReader({
399
+ resolvePersonUid: async () => {
400
+ const c = await vault();
401
+ return resolveCanonicalPersonUid({
402
+ listMyMemberships: () => c.listMyMemberships(),
403
+ listPersonEntities: () => c.entity.listByType("person"),
404
+ getEntity: async () => null,
405
+ });
406
+ },
407
+ getEntity: async (uid) => (await vault()).entity.get(uid),
408
+ vendSelf: async (personUid) => (await vault()).sts.vendSelf({ personUid }),
409
+ s3Factory: ({ region, credentials }) => new S3Client({ region, credentials }),
410
+ region: DEFAULT_COGNITO.region,
411
+ });
412
+ return createSelectionLoader(reader, {
413
+ log: (message) => void process.stderr.write(chalk.yellow(`${message}\n`)),
414
+ });
415
+ }
379
416
  /** The signed-in member's `prs_` uid from the company roster, or null when the roster is unavailable. */
380
417
  async function rosterPersonUid(token, companyUid, email) {
381
418
  try {
@@ -440,6 +477,7 @@ async function skippedReport(input, vaultRoot, personKey, personUid, now) {
440
477
  reason: SYNC_LOCK_BUSY_REASON,
441
478
  personUid,
442
479
  catchUp: input.catchUp ?? false,
480
+ ...(input.firstRun ? { firstRun: true } : {}),
443
481
  };
444
482
  await appendRunLedgerEntry(vaultRoot, personKey, line);
445
483
  return {
@@ -454,7 +492,10 @@ async function skippedReport(input, vaultRoot, personKey, personUid, now) {
454
492
  }
455
493
  async function runLockedCompanySync(input, vaultRoot, personKey, author, session, lockHeld) {
456
494
  const deps = gatewaySyncDeps(session.token, session.companyUid);
495
+ // A fresh loader per run: selections are cached for this run only.
496
+ const selections = input.createSelections ? input.createSelections() : defaultSelections();
457
497
  const report = await runOnDemandSync({
498
+ ...(selections ? { selections } : {}),
458
499
  deps,
459
500
  registry: input.registry ??
460
501
  createBuiltinHotChannelRegistry({ vaultRoot, identities: await meetingIdentities(input, session, author.email) }),
@@ -465,6 +506,7 @@ async function runLockedCompanySync(input, vaultRoot, personKey, author, session
465
506
  ...(input.now ? { now: input.now } : {}),
466
507
  ...(input.trigger ? { trigger: input.trigger } : {}),
467
508
  ...(input.catchUp === undefined ? {} : { catchUp: input.catchUp }),
509
+ ...(input.firstRun ? { firstRun: true } : {}),
468
510
  ...(input.signalsGenerated ? { signalsGenerated: input.signalsGenerated } : {}),
469
511
  beforeWrite: lockHeld,
470
512
  });
@@ -500,6 +542,7 @@ export async function runSync(options, overrides = {}) {
500
542
  ...(options.adapter !== undefined ? { adapter: options.adapter } : {}),
501
543
  ...(overrides.registry ? { registry: overrides.registry } : {}),
502
544
  ...(overrides.now ? { now: overrides.now } : {}),
545
+ ...(overrides.createSelections ? { createSelections: overrides.createSelections } : {}),
503
546
  signalsGenerated: overrides.signalsGenerated ?? createClaudeSignalsPass({ hqRoot: options.hqRoot, company }),
504
547
  // The CLI has no owner-DM sender: the terminal is the primary alert here.
505
548
  signalsAlert: {
@@ -549,6 +592,56 @@ function renderSyncTable(company, report) {
549
592
  }
550
593
  return lines.join("\n");
551
594
  }
595
+ export class NoHotSignalSyncJobError extends Error {
596
+ company;
597
+ constructor(company) {
598
+ super(`No hot-channel sync job for ${company} on any local bot, so there is nothing to reschedule. ` +
599
+ "Start a bot bound to this company (it adds the job on first start), then run this again.");
600
+ this.company = company;
601
+ this.name = "NoHotSignalSyncJobError";
602
+ }
603
+ }
604
+ /**
605
+ * Show the company's hot-signal-sync job (bot, schedule, next and last run)
606
+ * and each adapter's most recent ledger entry over the last 7 UTC days. With
607
+ * `--at HH:MM`, reschedule the job to daily at that time in its own time zone
608
+ * first. Exits non-zero when `--at` is given and no job exists.
609
+ */
610
+ export async function runSchedule(options, overrides = {}) {
611
+ const now = overrides.now ?? new Date();
612
+ const company = options.company ?? readActiveCompanySlug(options.hqRoot);
613
+ if (!company) {
614
+ throw new Error("No company specified. Pass --company <slug> or set activeCompany in .hq/config.json.");
615
+ }
616
+ assertSafeSegment(company, "company");
617
+ // Validate before touching any job file.
618
+ if (options.at !== undefined)
619
+ parseDailyAt(options.at);
620
+ let found = findHotSignalSyncJobs(overrides.botsRoot ?? defaultBotsRoot(), company);
621
+ if (options.at !== undefined) {
622
+ if (found.length === 0)
623
+ throw new NoHotSignalSyncJobError(company);
624
+ found = rescheduleDaily(found, options.at, now);
625
+ }
626
+ const email = overrides.email ?? resolveUploadAuthor()?.email;
627
+ const lastRuns = email
628
+ ? await lastRunsPerAdapter(path.join(options.hqRoot, "companies", company), audienceKey([email]), now)
629
+ : [];
630
+ const report = {
631
+ company,
632
+ job: found[0] ? toJobView(found[0], now) : null,
633
+ lastRuns,
634
+ };
635
+ if (options.json) {
636
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
637
+ }
638
+ else {
639
+ process.stdout.write(renderScheduleReport(report, found.slice(1).map((f) => f.botName)) + "\n");
640
+ if (!email)
641
+ process.stdout.write("Sign in (hq login) to see recent runs.\n");
642
+ }
643
+ return report;
644
+ }
552
645
  // ---------------------------------------------------------------------------
553
646
  // Registration
554
647
  // ---------------------------------------------------------------------------
@@ -612,6 +705,16 @@ export function registerSourcesCommand(program) {
612
705
  .action(withErrorHandling(async (options) => {
613
706
  await runRuns(options);
614
707
  }));
708
+ sources
709
+ .command("schedule")
710
+ .description("Show when the bot's daily hot-channel sync runs, or set it with --at HH:MM")
711
+ .option("--company <slug>", "Company slug (defaults to .hq/config.json activeCompany)")
712
+ .option("--at <HH:MM>", "Run the sync daily at this 24-hour time, in the job's time zone")
713
+ .option("--json", "Print the schedule and last runs as JSON")
714
+ .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
715
+ .action(withErrorHandling(async (options) => {
716
+ await runSchedule(options);
717
+ }));
615
718
  sources
616
719
  .command("sync")
617
720
  .description("Run every hot channel now: each adapter once per connection you can use")