@indigoai-us/hq-cli 5.345.178 → 5.345.179

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 +19 -0
  2. package/dist/command-catalog.generated.d.ts +71 -1
  3. package/dist/command-catalog.generated.js +86 -1
  4. package/dist/commands/bot-jobs.d.ts +11 -9
  5. package/dist/commands/bot-jobs.js +56 -21
  6. package/dist/commands/bot.d.ts +8 -0
  7. package/dist/commands/bot.js +44 -0
  8. package/dist/commands/daemon.d.ts +2 -0
  9. package/dist/commands/daemon.js +15 -4
  10. package/dist/commands/integrations-core.d.ts +2 -0
  11. package/dist/commands/integrations-core.js +1 -0
  12. package/dist/commands/sources.d.ts +89 -0
  13. package/dist/commands/sources.js +363 -1
  14. package/dist/lib/bot/fleet-local.d.ts +1 -1
  15. package/dist/lib/bot/fleet-local.js +7 -7
  16. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
  17. package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
  18. package/dist/lib/bot/local-jobs.d.ts +40 -4
  19. package/dist/lib/bot/local-jobs.js +72 -5
  20. package/dist/lib/bot/run.d.ts +15 -0
  21. package/dist/lib/bot/run.js +61 -0
  22. package/dist/lib/daemon/handoff.d.ts +4 -0
  23. package/dist/lib/daemon/handoff.js +6 -2
  24. package/dist/lib/daemon/install.d.ts +70 -2
  25. package/dist/lib/daemon/install.js +185 -27
  26. package/dist/lib/daemon/legacy.d.ts +9 -3
  27. package/dist/lib/daemon/legacy.js +11 -4
  28. package/dist/lib/daemon/migrate.js +1 -1
  29. package/dist/lib/daemon/run.d.ts +3 -2
  30. package/dist/lib/daemon/run.js +10 -6
  31. package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
  32. package/dist/lib/doctor/checks/anywhere.js +35 -6
  33. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.d.ts +33 -0
  34. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
  35. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
  36. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
  37. package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
  38. package/dist/lib/hot-channels/adapters/attio.js +374 -0
  39. package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
  40. package/dist/lib/hot-channels/adapters/gmail.js +413 -0
  41. package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
  42. package/dist/lib/hot-channels/adapters/index.js +39 -0
  43. package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
  44. package/dist/lib/hot-channels/adapters/linear.js +321 -0
  45. package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
  46. package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
  47. package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
  48. package/dist/lib/hot-channels/adapters/meetings.js +523 -0
  49. package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
  50. package/dist/lib/hot-channels/adapters/slack.js +277 -0
  51. package/dist/lib/hot-channels/alert.d.ts +78 -0
  52. package/dist/lib/hot-channels/alert.js +179 -0
  53. package/dist/lib/hot-channels/audience.d.ts +15 -0
  54. package/dist/lib/hot-channels/audience.js +25 -0
  55. package/dist/lib/hot-channels/contract.d.ts +140 -0
  56. package/dist/lib/hot-channels/contract.js +20 -0
  57. package/dist/lib/hot-channels/cursors.d.ts +41 -0
  58. package/dist/lib/hot-channels/cursors.js +84 -0
  59. package/dist/lib/hot-channels/index.d.ts +10 -0
  60. package/dist/lib/hot-channels/index.js +10 -0
  61. package/dist/lib/hot-channels/landing.d.ts +83 -0
  62. package/dist/lib/hot-channels/landing.js +175 -0
  63. package/dist/lib/hot-channels/ledger.d.ts +101 -0
  64. package/dist/lib/hot-channels/ledger.js +186 -0
  65. package/dist/lib/hot-channels/loop.d.ts +74 -0
  66. package/dist/lib/hot-channels/loop.js +219 -0
  67. package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
  68. package/dist/lib/hot-channels/registry.d.ts +28 -0
  69. package/dist/lib/hot-channels/registry.js +121 -0
  70. package/dist/lib/hot-channels/run.d.ts +137 -0
  71. package/dist/lib/hot-channels/run.js +324 -0
  72. package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
  73. package/dist/lib/hot-channels/signals-pass.js +581 -0
  74. package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
  75. package/dist/lib/hot-channels/sync-lock.js +224 -0
  76. package/dist/lib/install/hook-target.d.ts +6 -2
  77. package/dist/lib/install/hook-target.js +9 -4
  78. package/dist/lib/lanes/loop.d.ts +10 -0
  79. package/dist/lib/lanes/loop.js +18 -1
  80. package/package.json +7 -7
@@ -11,6 +11,8 @@
11
11
  * hq sources get Fetch one source by id.
12
12
  * hq sources channels Print the canonical SOURCE_CHANNELS enum.
13
13
  * hq sources entities List entities the caller has access to.
14
+ * hq sources runs Show the local hot-channel run ledger for one day.
15
+ * hq sources sync Run every hot channel now for the signed-in person.
14
16
  *
15
17
  * Defaults:
16
18
  * --entity falls back to .hq/config.json activeCompany (per sync.ts).
@@ -18,5 +20,92 @@
18
20
  * --limit 50.
19
21
  */
20
22
  import { Command } from "commander";
23
+ import { type RunLedgerAdapterSummary, type RunLedgerEntry, type RunTrigger, type SignalsGeneratedFn } from "../lib/hot-channels/ledger.js";
24
+ import type { HotChannelRegistry } from "../lib/hot-channels/registry.js";
25
+ import { type OnDemandSyncDeps, type OnDemandSyncReport } from "../lib/hot-channels/run.js";
26
+ type ListFormat = "table" | "json";
27
+ export interface RunsOptions {
28
+ today?: boolean;
29
+ date?: string;
30
+ company?: string;
31
+ email?: string;
32
+ format?: ListFormat;
33
+ hqRoot: string;
34
+ }
35
+ export interface RunsReport {
36
+ company: string;
37
+ personKey: string;
38
+ day: string;
39
+ adapters: RunLedgerAdapterSummary[];
40
+ entries: RunLedgerEntry[];
41
+ malformed: number;
42
+ }
43
+ /**
44
+ * Reads one day's run ledger (`sources/_runs/@{personKey}/{day}.jsonl` in the
45
+ * company folder of the local HQ tree). The person key is
46
+ * `audienceKey([email])` of the signed-in person. No network calls.
47
+ */
48
+ export declare function runRuns(options: RunsOptions, now?: Date): Promise<RunsReport>;
49
+ export interface SyncOptions {
50
+ adapter?: string;
51
+ company?: string;
52
+ hqRoot: string;
53
+ json?: boolean;
54
+ }
55
+ export interface SyncOverrides {
56
+ /** Adapters to run. This build registers none, so the default is empty. */
57
+ registry?: HotChannelRegistry;
58
+ now?: () => Date;
59
+ /** The signals pass; defaults to the local `claude -p` pass under the user's own login. */
60
+ signalsGenerated?: SignalsGeneratedFn;
61
+ }
62
+ /**
63
+ * Network access for one sync: the catalog, the person's usable connections,
64
+ * and tool calls through the HQ gateway. Integration tokens stay server-side;
65
+ * the gateway routes each call by connection id.
66
+ */
67
+ export declare function gatewaySyncDeps(token: string, companyUid: string): OnDemandSyncDeps;
68
+ export interface CompanyHotChannelSyncInput {
69
+ hqRoot: string;
70
+ company: string;
71
+ adapter?: string;
72
+ registry?: HotChannelRegistry;
73
+ now?: () => Date;
74
+ /** `scheduled` for the bot's hot-signal-sync job; `on-demand` by default. */
75
+ trigger?: RunTrigger;
76
+ catchUp?: boolean;
77
+ signalsGenerated?: SignalsGeneratedFn;
78
+ /** The signed-in person's `prs_` person uid in this company; defaults to a roster lookup by email. */
79
+ resolvePersonUid?: (token: string, companyUid: string, email: string) => Promise<string | null>;
80
+ /**
81
+ * Where the signals-failure alert goes (US-012). Unset = no alert check.
82
+ * The company-channel post defaults to the Slack channel in the company's
83
+ * `settings/signals/preferences.yaml`, through the HQ integrations gateway.
84
+ */
85
+ signalsAlert?: SignalsAlertSenders;
86
+ }
87
+ export interface SignalsAlertSenders {
88
+ /** The primary alert to the owner; throws when it was not delivered. */
89
+ sendOwnerDm: (text: string) => Promise<void>;
90
+ postCompanyChannel?: (text: string) => Promise<void>;
91
+ log?: (message: string) => void;
92
+ }
93
+ /**
94
+ * One hot-channel sync for the signed-in person in one company: each adapter
95
+ * once per usable connection. Writes under `companies/{company}/`: landed
96
+ * sources in `sources/`, index rows in `sources/_index/`, cursors in
97
+ * `sources/_cursors/`, one ledger line per adapter in `sources/_runs/`, and
98
+ * signals in `signals/@{key}/`. `hq sources sync` and the bot's scheduled
99
+ * hot-signal-sync job both call it. Only one run per person per company goes
100
+ * at a time: a second one writes a `skipped` ledger line and lands nothing.
101
+ */
102
+ export declare function runCompanyHotChannelSync(input: CompanyHotChannelSyncInput): Promise<OnDemandSyncReport>;
103
+ /**
104
+ * Run every hot channel for the signed-in person now: each adapter once per
105
+ * usable connection. Writes cursors, landed sources and an `on-demand` ledger
106
+ * line under `companies/{company}/sources/`, and nothing else.
107
+ */
108
+ export declare function runSync(options: SyncOptions, overrides?: SyncOverrides): Promise<OnDemandSyncReport>;
21
109
  export declare function registerSourcesCommand(program: Command): void;
110
+ export {};
22
111
  //# sourceMappingURL=sources.d.ts.map
@@ -11,18 +11,34 @@
11
11
  * hq sources get Fetch one source by id.
12
12
  * hq sources channels Print the canonical SOURCE_CHANNELS enum.
13
13
  * hq sources entities List entities the caller has access to.
14
+ * hq sources runs Show the local hot-channel run ledger for one day.
15
+ * hq sources sync Run every hot channel now for the signed-in person.
14
16
  *
15
17
  * Defaults:
16
18
  * --entity falls back to .hq/config.json activeCompany (per sync.ts).
17
19
  * --format 'table' when stdout is a TTY, 'json' when piped.
18
20
  * --limit 50.
19
21
  */
22
+ import { randomUUID } from "node:crypto";
20
23
  import * as fs from "node:fs";
21
24
  import * as path from "node:path";
22
25
  import chalk from "chalk";
23
26
  import { SOURCE_CHANNELS, assertSourceChannel, resolveEntity, listAvailableEntities, listSources, getSource, VaultClient, } from "@indigoai-us/hq-cloud";
24
- import { ensureCognitoToken, buildVaultConfig, DEFAULT_HQ_ROOT, } from "../utils/cognito-session.js";
27
+ import { ensureCognitoToken, ensureCognitoIdToken, buildVaultConfig, DEFAULT_HQ_ROOT, } from "../utils/cognito-session.js";
28
+ import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
29
+ import { isAuthError } from "../utils/auth-error.js";
30
+ import { IntegrationsCliError, callGateway, fetchUsableApps, gatewayResultIsError, isBuiltInGatewayProvider, raiseForResponse, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
25
31
  import { Sentry } from "../sentry.js";
32
+ import { resolveUploadAuthor } from "../utils/upload-author.js";
33
+ import { createSignalsAlertChannelPoster, raiseSignalsFailureAlerts } from "../lib/hot-channels/alert.js";
34
+ import { audienceKey } from "../lib/hot-channels/audience.js";
35
+ import { appendRunLedgerEntry, ledgerDay, readRunLedger, summarizeRunLedger, } from "../lib/hot-channels/ledger.js";
36
+ import { createClaudeSignalsPass } from "../lib/hot-channels/signals-pass.js";
37
+ import { assertSafeSegment } from "../lib/hot-channels/landing.js";
38
+ import { acquireSyncLock, SYNC_LOCK_BUSY_REASON } from "../lib/hot-channels/sync-lock.js";
39
+ import { createBuiltinHotChannelRegistry } from "../lib/hot-channels/adapters/index.js";
40
+ import { listActiveMembers } from "./members.js";
41
+ import { runOnDemandSync, } from "../lib/hot-channels/run.js";
26
42
  // ---------------------------------------------------------------------------
27
43
  // Access token resolution — honors HQ_ACCESS_TOKEN for tests + CI smoke
28
44
  // ---------------------------------------------------------------------------
@@ -209,6 +225,330 @@ async function runEntities(options) {
209
225
  .join("\n");
210
226
  process.stdout.write([headerLine, sep, body].join("\n") + "\n");
211
227
  }
228
+ /**
229
+ * Reads one day's run ledger (`sources/_runs/@{personKey}/{day}.jsonl` in the
230
+ * company folder of the local HQ tree). The person key is
231
+ * `audienceKey([email])` of the signed-in person. No network calls.
232
+ */
233
+ export async function runRuns(options, now = new Date()) {
234
+ if (options.today && options.date) {
235
+ throw new Error("Pass either --today or --date, not both.");
236
+ }
237
+ if (options.date !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(options.date)) {
238
+ throw new Error(`--date must be YYYY-MM-DD (got '${options.date}').`);
239
+ }
240
+ const day = options.date ?? ledgerDay(now);
241
+ const company = options.company ?? readActiveCompanySlug(options.hqRoot);
242
+ if (!company) {
243
+ throw new Error("No company specified. Pass --company <slug> or set activeCompany in .hq/config.json.");
244
+ }
245
+ const email = options.email ?? resolveUploadAuthor()?.email;
246
+ if (!email) {
247
+ throw new Error("Could not tell who is signed in. Run `hq login` or pass --email <address>.");
248
+ }
249
+ const personKey = audienceKey([email]);
250
+ const vaultRoot = path.join(options.hqRoot, "companies", company);
251
+ const { entries, malformed } = await readRunLedger(vaultRoot, personKey, day);
252
+ const report = {
253
+ company,
254
+ personKey,
255
+ day,
256
+ adapters: summarizeRunLedger(entries),
257
+ entries,
258
+ malformed,
259
+ };
260
+ const format = options.format ?? defaultListFormat();
261
+ if (format === "json") {
262
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
263
+ return report;
264
+ }
265
+ process.stdout.write(renderRunsTable(report) + "\n");
266
+ return report;
267
+ }
268
+ function renderRunsTable(report) {
269
+ const lines = [`Runs for ${report.company} on ${report.day} (UTC), person key @${report.personKey}`];
270
+ if (report.entries.length === 0) {
271
+ lines.push("(no runs logged)");
272
+ }
273
+ else {
274
+ const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
275
+ const header = ["ADAPTER", "RUNS", "SOURCES", "SIGNALS", "FAILED", "LAST FINISHED"];
276
+ const rows = report.adapters.map((a) => [
277
+ a.adapter,
278
+ String(a.runs),
279
+ String(a.sourcesLanded),
280
+ String(a.signalsGenerated),
281
+ String(a.failed),
282
+ a.lastFinishedAt,
283
+ ]);
284
+ const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
285
+ lines.push("", header.map((h, i) => pad(h, widths[i])).join(" "));
286
+ for (const row of rows)
287
+ lines.push(row.map((cell, i) => pad(cell, widths[i])).join(" "));
288
+ lines.push("", "Runs:");
289
+ for (const e of [...report.entries].sort((a, b) => a.startedAt.localeCompare(b.startedAt))) {
290
+ const outcome = e.status === "ok"
291
+ ? "ok"
292
+ : e.status === "skipped"
293
+ ? `skipped: ${e.reason ?? "no reason given"}`
294
+ : `failed: ${e.error ?? "unknown error"}`;
295
+ lines.push(` ${e.startedAt} ${e.adapter} ${e.trigger} sources=${e.sourcesLanded} signals=${e.signalsGenerated} ${outcome}`);
296
+ }
297
+ }
298
+ if (report.malformed > 0)
299
+ lines.push(`(${report.malformed} unreadable line(s) skipped)`);
300
+ return lines.join("\n");
301
+ }
302
+ const SIGN_IN_PROMPT = "Your HQ session has expired or you're not signed in. Run `hq login`, then `hq sources sync` again.";
303
+ const MAX_TOOL_ERROR_TEXT = 500;
304
+ /** The catalog API's `hotChannels` array, or null when the server has none. */
305
+ async function fetchHotChannels(token, companyUid) {
306
+ const res = await vaultApiFetch({
307
+ token,
308
+ path: "/v1/integrations/factory/catalog",
309
+ query: { companyUid },
310
+ });
311
+ if (!res.ok)
312
+ await raiseForResponse(res, "Failed to read the integrations catalog");
313
+ const data = (await res.json());
314
+ return Array.isArray(data.hotChannels) ? data.hotChannels : null;
315
+ }
316
+ /**
317
+ * Network access for one sync: the catalog, the person's usable connections,
318
+ * and tool calls through the HQ gateway. Integration tokens stay server-side;
319
+ * the gateway routes each call by connection id.
320
+ */
321
+ export function gatewaySyncDeps(token, companyUid) {
322
+ return {
323
+ fetchHotChannels: () => fetchHotChannels(token, companyUid),
324
+ fetchUsableApps: async () => (await fetchUsableApps(token, companyUid)).apps,
325
+ async callTool(app, tool, args) {
326
+ const idempotencyKey = `hq-sources-sync-${randomUUID()}`;
327
+ const params = isBuiltInGatewayProvider(app.provider)
328
+ ? {
329
+ companyUid,
330
+ acceptAsync: true,
331
+ name: tool,
332
+ arguments: { ...args, companyUid, connectionId: app.connectionId, idempotencyKey },
333
+ }
334
+ : {
335
+ companyUid,
336
+ acceptAsync: true,
337
+ name: `${toolPrefixForProvider(app.provider)}.mcp.tools.call`,
338
+ arguments: { companyUid, connectionId: app.connectionId, toolName: tool, arguments: args, idempotencyKey },
339
+ };
340
+ const message = await callGateway(token, params, idempotencyKey, "tools/call");
341
+ const payload = unwrapGatewayResult(message.result);
342
+ if (gatewayResultIsError(message.result) || gatewayResultIsError(payload)) {
343
+ const text = typeof payload === "string" ? payload : JSON.stringify(payload);
344
+ throw new IntegrationsCliError((text ?? "tool call failed").slice(0, MAX_TOOL_ERROR_TEXT), {
345
+ expected: true,
346
+ });
347
+ }
348
+ return payload;
349
+ },
350
+ };
351
+ }
352
+ function signInReport() {
353
+ return {
354
+ runId: null,
355
+ signInRequired: true,
356
+ outcomes: [],
357
+ entries: [],
358
+ notices: [SIGN_IN_PROMPT],
359
+ exitCode: 1,
360
+ };
361
+ }
362
+ async function signedInSession(company) {
363
+ let token;
364
+ try {
365
+ token = process.env.HQ_ACCESS_TOKEN || (await ensureCognitoIdToken({ interactive: false }));
366
+ }
367
+ catch {
368
+ return null;
369
+ }
370
+ try {
371
+ return { token, companyUid: await getCompanyUid(token, company) };
372
+ }
373
+ catch (err) {
374
+ if (isAuthError(err) || err.status === 401)
375
+ return null;
376
+ throw err;
377
+ }
378
+ }
379
+ /** The signed-in member's `prs_` uid from the company roster, or null when the roster is unavailable. */
380
+ async function rosterPersonUid(token, companyUid, email) {
381
+ try {
382
+ const wanted = email.toLowerCase();
383
+ const member = (await listActiveMembers(token, companyUid)).find((m) => m.personEmail?.toLowerCase() === wanted);
384
+ return member?.personUid ?? null;
385
+ }
386
+ catch {
387
+ return null;
388
+ }
389
+ }
390
+ /**
391
+ * The signed-in person's identities a meeting audience can name: their email
392
+ * and their `prs_` person uid. The run's `personUid` is the Cognito sub, which
393
+ * meeting transcripts never carry.
394
+ */
395
+ async function meetingIdentities(input, session, email) {
396
+ const personUid = await (input.resolvePersonUid ?? rosterPersonUid)(session.token, session.companyUid, email);
397
+ return personUid ? [email, personUid] : [email];
398
+ }
399
+ /**
400
+ * One hot-channel sync for the signed-in person in one company: each adapter
401
+ * once per usable connection. Writes under `companies/{company}/`: landed
402
+ * sources in `sources/`, index rows in `sources/_index/`, cursors in
403
+ * `sources/_cursors/`, one ledger line per adapter in `sources/_runs/`, and
404
+ * signals in `signals/@{key}/`. `hq sources sync` and the bot's scheduled
405
+ * hot-signal-sync job both call it. Only one run per person per company goes
406
+ * at a time: a second one writes a `skipped` ledger line and lands nothing.
407
+ */
408
+ export async function runCompanyHotChannelSync(input) {
409
+ assertSafeSegment(input.company, "company");
410
+ const author = resolveUploadAuthor();
411
+ const session = author ? await signedInSession(input.company) : null;
412
+ if (!author || !session)
413
+ return signInReport();
414
+ const vaultRoot = path.join(input.hqRoot, "companies", input.company);
415
+ const personKey = audienceKey([author.email]);
416
+ const now = input.now ?? (() => new Date());
417
+ const lock = await acquireSyncLock(vaultRoot, personKey, { now });
418
+ if (!lock)
419
+ return skippedReport(input, vaultRoot, personKey, author.userSub, now);
420
+ try {
421
+ return await runLockedCompanySync(input, vaultRoot, personKey, author, session, lock.assertHeld);
422
+ }
423
+ finally {
424
+ await lock.release();
425
+ }
426
+ }
427
+ async function skippedReport(input, vaultRoot, personKey, personUid, now) {
428
+ const at = now().toISOString();
429
+ const line = {
430
+ runId: `run_${randomUUID()}`,
431
+ trigger: input.trigger ?? "on-demand",
432
+ adapter: "all",
433
+ connectionId: null,
434
+ sourcesLanded: 0,
435
+ signalsGenerated: 0,
436
+ startedAt: at,
437
+ finishedAt: at,
438
+ status: "skipped",
439
+ error: null,
440
+ reason: SYNC_LOCK_BUSY_REASON,
441
+ personUid,
442
+ catchUp: input.catchUp ?? false,
443
+ };
444
+ await appendRunLedgerEntry(vaultRoot, personKey, line);
445
+ return {
446
+ runId: line.runId,
447
+ signInRequired: false,
448
+ skipped: true,
449
+ outcomes: [],
450
+ entries: [line],
451
+ notices: [`Another hot-channel sync for ${input.company} is already running, so this one was skipped.`],
452
+ exitCode: 0,
453
+ };
454
+ }
455
+ async function runLockedCompanySync(input, vaultRoot, personKey, author, session, lockHeld) {
456
+ const deps = gatewaySyncDeps(session.token, session.companyUid);
457
+ const report = await runOnDemandSync({
458
+ deps,
459
+ registry: input.registry ??
460
+ createBuiltinHotChannelRegistry({ vaultRoot, identities: await meetingIdentities(input, session, author.email) }),
461
+ vaultRoot,
462
+ personUid: author.userSub,
463
+ personKey,
464
+ ...(input.adapter !== undefined ? { adapter: input.adapter } : {}),
465
+ ...(input.now ? { now: input.now } : {}),
466
+ ...(input.trigger ? { trigger: input.trigger } : {}),
467
+ ...(input.catchUp === undefined ? {} : { catchUp: input.catchUp }),
468
+ ...(input.signalsGenerated ? { signalsGenerated: input.signalsGenerated } : {}),
469
+ beforeWrite: lockHeld,
470
+ });
471
+ if (input.signalsAlert) {
472
+ const postCompanyChannel = input.signalsAlert.postCompanyChannel ??
473
+ createSignalsAlertChannelPoster({ hqRoot: input.hqRoot, company: input.company, deps });
474
+ await raiseSignalsFailureAlerts({
475
+ vaultRoot,
476
+ personKey,
477
+ entries: report.entries,
478
+ now: input.now ?? (() => new Date()),
479
+ personName: author.email,
480
+ sendOwnerDm: input.signalsAlert.sendOwnerDm,
481
+ ...(postCompanyChannel ? { postCompanyChannel } : {}),
482
+ ...(input.signalsAlert.log ? { log: input.signalsAlert.log } : {}),
483
+ });
484
+ }
485
+ return report;
486
+ }
487
+ /**
488
+ * Run every hot channel for the signed-in person now: each adapter once per
489
+ * usable connection. Writes cursors, landed sources and an `on-demand` ledger
490
+ * line under `companies/{company}/sources/`, and nothing else.
491
+ */
492
+ export async function runSync(options, overrides = {}) {
493
+ const company = options.company ?? readActiveCompanySlug(options.hqRoot);
494
+ if (!company) {
495
+ throw new Error("No company specified. Pass --company <slug> or set activeCompany in .hq/config.json.");
496
+ }
497
+ const report = await runCompanyHotChannelSync({
498
+ hqRoot: options.hqRoot,
499
+ company,
500
+ ...(options.adapter !== undefined ? { adapter: options.adapter } : {}),
501
+ ...(overrides.registry ? { registry: overrides.registry } : {}),
502
+ ...(overrides.now ? { now: overrides.now } : {}),
503
+ signalsGenerated: overrides.signalsGenerated ?? createClaudeSignalsPass({ hqRoot: options.hqRoot, company }),
504
+ // The CLI has no owner-DM sender: the terminal is the primary alert here.
505
+ signalsAlert: {
506
+ sendOwnerDm: async (text) => {
507
+ process.stderr.write(chalk.red(`${text}\n`));
508
+ },
509
+ log: (message) => process.stderr.write(chalk.yellow(`${message}\n`)),
510
+ },
511
+ });
512
+ if (options.json) {
513
+ process.stdout.write(JSON.stringify({ company, ...report }, null, 2) + "\n");
514
+ }
515
+ else {
516
+ process.stdout.write(renderSyncTable(company, report) + "\n");
517
+ }
518
+ for (const outcome of report.outcomes) {
519
+ if (outcome.reconnect)
520
+ process.stderr.write(chalk.yellow(`${outcome.reconnect.prompt}\n`));
521
+ }
522
+ if (report.signInRequired)
523
+ process.stderr.write(chalk.yellow(`${SIGN_IN_PROMPT}\n`));
524
+ if (report.exitCode !== 0)
525
+ process.exitCode = report.exitCode;
526
+ return report;
527
+ }
528
+ function renderSyncTable(company, report) {
529
+ const lines = [`Hot-channel sync for ${company}`];
530
+ for (const notice of report.notices) {
531
+ if (!report.signInRequired)
532
+ lines.push(notice);
533
+ }
534
+ if (report.outcomes.length > 0) {
535
+ const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
536
+ const header = ["ADAPTER", "CONNECTION", "LANDED", "STATUS"];
537
+ const rows = report.outcomes.map((o) => [
538
+ o.adapter,
539
+ o.connectionId ?? "-",
540
+ String(o.landed),
541
+ o.status === "ok" ? "ok" : o.message ? `${o.status}: ${o.message}` : o.status,
542
+ ]);
543
+ const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
544
+ lines.push("", header.map((h, i) => pad(h, widths[i])).join(" "));
545
+ for (const row of rows)
546
+ lines.push(row.map((cell, i) => pad(cell, widths[i])).join(" "));
547
+ const landed = report.outcomes.reduce((sum, o) => sum + o.landed, 0);
548
+ lines.push("", `${landed} source(s) landed.`);
549
+ }
550
+ return lines.join("\n");
551
+ }
212
552
  // ---------------------------------------------------------------------------
213
553
  // Registration
214
554
  // ---------------------------------------------------------------------------
@@ -260,5 +600,27 @@ export function registerSourcesCommand(program) {
260
600
  .description("List entities (companies/personal) your account has access to")
261
601
  .option("--format <fmt>", "Output format: table | json (default: table for TTY, json piped)")
262
602
  .action(withErrorHandling(async (options) => runEntities(options)));
603
+ sources
604
+ .command("runs")
605
+ .description("Show the hot-channel run log: sources landed and signals generated per adapter")
606
+ .option("--today", "Show today's runs (UTC); the default")
607
+ .option("--date <YYYY-MM-DD>", "Show runs for this UTC day")
608
+ .option("--company <slug>", "Company slug (defaults to .hq/config.json activeCompany)")
609
+ .option("--email <address>", "Whose runs to show (defaults to the signed-in person)")
610
+ .option("--format <fmt>", "Output format: table | json (default: table for TTY, json piped)")
611
+ .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
612
+ .action(withErrorHandling(async (options) => {
613
+ await runRuns(options);
614
+ }));
615
+ sources
616
+ .command("sync")
617
+ .description("Run every hot channel now: each adapter once per connection you can use")
618
+ .option("--adapter <id>", "Run one adapter only (a catalog provider or adapter id)")
619
+ .option("--company <slug>", "Company slug (defaults to .hq/config.json activeCompany)")
620
+ .option("--json", "Print the run report as JSON")
621
+ .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
622
+ .action(withErrorHandling(async (options) => {
623
+ await runSync(options);
624
+ }));
263
625
  }
264
626
  //# sourceMappingURL=sources.js.map
@@ -133,7 +133,7 @@ export interface FleetLocalRemovalResult {
133
133
  credentialsRemoved: number;
134
134
  }
135
135
  export declare const HQ_FLEET_LOCAL_RELEASE: FleetLocalReleasePin;
136
- export declare const HQ_FLEET_LOCAL_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAOOL7NRKQgjMYI5cWxLY+HGppXf2AtwpQbCg5v+nBksM=\n-----END PUBLIC KEY-----\n";
136
+ export declare const HQ_FLEET_LOCAL_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAbYvc1pwOmiCUPJv8yIa3E9RXfniFA84nH2+ROmLMGZU=\n-----END PUBLIC KEY-----\n";
137
137
  export declare function fleetHome(home?: string, env?: NodeJS.ProcessEnv): string;
138
138
  export declare function verifyFleetLocalRelease(files: FleetLocalReleaseFiles, release?: FleetLocalReleasePin, publicKey?: string): void;
139
139
  export declare function resolveFleetReleaseToken(env?: NodeJS.ProcessEnv): string;
@@ -35,18 +35,18 @@ export function fleetProviderNeedsSignIn(provider) {
35
35
  }
36
36
  export const HQ_FLEET_LOCAL_RELEASE = {
37
37
  repository: "indigoai-us/hq-fleet-local",
38
- tag: "v0.4.12",
39
- version: "0.4.12",
40
- artifact: "hq-fleet-local-0.4.12.zip",
41
- artifactSha256: "2a07e79b1740e8ef68b19078b73b10cdf9377065a2fd1f53287b7e353099c577",
38
+ tag: "v0.4.13",
39
+ version: "0.4.13",
40
+ artifact: "hq-fleet-local-0.4.13.zip",
41
+ artifactSha256: "81b129ac129578240af9091b06ae190f47f6be38ff933b35ae5459d5b78c72df",
42
42
  runtimeArtifact: "hq-fleet-runtime-abccac13bd9e.tar",
43
43
  runtimeArtifactSha256: "29bcfe718ad7fbd3636afd00623772f2a25bc84c4f6f10d8d57f38e549d528d5",
44
- manifestSha256: "cdac590bdcebea6629de0c164c42c7baf30c025a299ff5829380989dd22a9ff1",
45
- signatureSha256: "f07911dd3187393cd325fc8f88ddaa6a7568825aee6d8748a992cea822d5d9b1",
44
+ manifestSha256: "42654f91264099ce173ce3a9f42e207f566e1412ea4b550dc21775d764327bbb",
45
+ signatureSha256: "93cf9b0a94d75c532454243e3391e421e2743f59f8e65989a5a56ec9152e2fa4",
46
46
  runtimeCommit: "abccac13bd9ea593b24c8cd9c0ba28d5c6050cc9",
47
47
  };
48
48
  export const HQ_FLEET_LOCAL_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
49
- MCowBQYDK2VwAyEAOOL7NRKQgjMYI5cWxLY+HGppXf2AtwpQbCg5v+nBksM=
49
+ MCowBQYDK2VwAyEAbYvc1pwOmiCUPJv8yIa3E9RXfniFA84nH2+ROmLMGZU=
50
50
  -----END PUBLIC KEY-----
51
51
  `;
52
52
  function digest(value) {
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The bot's built-in hot-signal-sync job.
3
+ *
4
+ * A local bot that is bound to a company registers one recurring job in its
5
+ * own `jobs.json` the first time it starts (`ensureHotSignalSyncJob`). The job
6
+ * fires from the bot's inbox loop like every other local job
7
+ * (lib/bot/local-jobs.ts): no crontab, launchd or systemd entry is written.
8
+ * The default schedule is 06:30 daily in the machine's time zone, ahead of a
9
+ * morning brief; the owner changes it with
10
+ * `hq bot jobs <bot> --reschedule <jobId> --every "<when>"`, or removes it with
11
+ * `--rm`, and a removed job is not added back on the next start.
12
+ *
13
+ * When the job fires the bot runs the sync in-process, with no model turn
14
+ * (`runHotSignalSyncJob`): the same hot-channel sync `hq sources sync` runs,
15
+ * with `trigger: "scheduled"` and `catchUp` set when the run fired late after
16
+ * a missed window. Catch-up is bounded by each adapter's `catchUpWindow`, and
17
+ * per-connection cursors keep a redelivered run from landing anything twice.
18
+ * The claude signals pass (US-011) runs after each adapter when the bot wires
19
+ * one in; with none the signals count is 0.
20
+ */
21
+ import type { HotChannelRunResult, WriteGuard } from "../../hot-channels/loop.js";
22
+ import type { RunTrigger } from "../../hot-channels/ledger.js";
23
+ import type { OnDemandSyncReport } from "../../hot-channels/run.js";
24
+ import { type BotConfig } from "../config.js";
25
+ import { type LocalJob } from "../local-jobs.js";
26
+ export declare const HOT_SIGNAL_SYNC_JOB_KIND = "hot-signal-sync";
27
+ /**
28
+ * 06:30 every day, in the job's time zone (the stored EventBridge form of
29
+ * "30 6 * * *"). Only the first registration uses it; after that the job's
30
+ * own schedule in `jobs.json` is the one that runs.
31
+ */
32
+ export declare const HOT_SIGNAL_SYNC_DEFAULT_CRON = "cron(30 6 * * ? *)";
33
+ /** The private context that marks a job as this built-in, for one company. */
34
+ export declare function hotSignalSyncJobContext(company: string): Record<string, unknown>;
35
+ /** The company a job item syncs, when its context marks it as this built-in; else null. */
36
+ export declare function hotSignalSyncJobCompany(context: unknown): string | null;
37
+ /**
38
+ * The company a bot's hot-signal-sync job syncs: the bot's own company, else
39
+ * the first company it serves. Null = none, and no job is registered. There is
40
+ * no fallback to the HQ's active company: a personal bot syncs a company only
41
+ * when it was given one explicitly (`--company`), so the job never writes into
42
+ * a vault the bot does not belong to. Checked again each time the job fires.
43
+ */
44
+ export declare function currentHotSignalSyncCompany(config: Pick<BotConfig, "companySlug" | "kind" | "companies" | "workerId">): string | null;
45
+ /**
46
+ * Remove every hot-signal-sync job whose company is not `current` (all of them
47
+ * when `current` is null), and forget their registration so the company can be
48
+ * registered again if the bot is given it back. Returns the removed jobs.
49
+ */
50
+ export declare function removeStaleHotSignalSyncJobs(dir: string, current: string | null): LocalJob[];
51
+ export interface EnsureHotSignalSyncJobResult {
52
+ /** The job, or null when the owner removed it after it was first registered. */
53
+ job: LocalJob | null;
54
+ created: boolean;
55
+ }
56
+ /**
57
+ * Register the hot-signal-sync job for `company` once. Safe to call on every
58
+ * bot start: an existing job is left as it is (including a changed schedule),
59
+ * and a job the owner removed is not added again.
60
+ */
61
+ export declare function ensureHotSignalSyncJob(dir: string, input: {
62
+ company: string;
63
+ timezone: string;
64
+ }, now?: Date): EnsureHotSignalSyncJobResult;
65
+ /**
66
+ * The signals pass over one adapter's landed sources: how many signals it
67
+ * generated. The bot wires the local `claude -p` pass
68
+ * (`createClaudeSignalsPass`); `noLocalSignalsPass` is the default when no
69
+ * pass is given.
70
+ */
71
+ export type LocalSignalsPass = (result: HotChannelRunResult, guard?: WriteGuard) => Promise<number>;
72
+ export declare const noLocalSignalsPass: LocalSignalsPass;
73
+ /** One company's hot-channel sync, as `runCompanyHotChannelSync` runs it (minus the HQ root). */
74
+ export type HotSignalSyncFn = (input: {
75
+ company: string;
76
+ trigger: RunTrigger;
77
+ catchUp: boolean;
78
+ signalsGenerated: LocalSignalsPass;
79
+ now: () => Date;
80
+ /** The owner DM for the signals-failure alert (US-012); unset = no alert check. */
81
+ signalsAlert?: {
82
+ sendOwnerDm: (text: string) => Promise<void>;
83
+ log?: (message: string) => void;
84
+ };
85
+ }) => Promise<OnDemandSyncReport>;
86
+ export interface HotSignalSyncRunInput {
87
+ company: string;
88
+ /** The run fired late, after a missed window (job-run.ts `isCatchUpRun`). */
89
+ catchUp: boolean;
90
+ now: () => Date;
91
+ sync: HotSignalSyncFn;
92
+ signalsPass?: LocalSignalsPass;
93
+ /**
94
+ * Sends the signals-failure alert to the owner as its own message, even when
95
+ * the job is quiet. Throws when the message was not delivered.
96
+ */
97
+ sendAlert?: (text: string) => Promise<void>;
98
+ log?: (message: string) => void;
99
+ }
100
+ export interface HotSignalSyncRunOutcome {
101
+ ok: boolean;
102
+ /** Plain report for the owner's DM and the job's run output. */
103
+ text: string;
104
+ report: OnDemandSyncReport | null;
105
+ }
106
+ /** Run the scheduled sync once and describe it in plain words. Never throws. */
107
+ export declare function runHotSignalSyncJob(input: HotSignalSyncRunInput): Promise<HotSignalSyncRunOutcome>;
108
+ export declare function describeSyncReport(company: string, report: OnDemandSyncReport): string;
109
+ //# sourceMappingURL=hot-signal-sync.d.ts.map