@indigoai-us/hq-cli 5.345.178 → 5.345.180

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 (117) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/command-catalog.generated.d.ts +197 -1
  3. package/dist/command-catalog.generated.js +241 -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 +37 -1
  7. package/dist/commands/bot.js +289 -13
  8. package/dist/commands/channels.d.ts +18 -1
  9. package/dist/commands/channels.js +185 -1
  10. package/dist/commands/daemon.d.ts +2 -0
  11. package/dist/commands/daemon.js +15 -4
  12. package/dist/commands/integrations-core.d.ts +2 -0
  13. package/dist/commands/integrations-core.js +1 -0
  14. package/dist/commands/lanes.d.ts +1 -1
  15. package/dist/commands/lanes.js +6 -2
  16. package/dist/commands/mesh.js +19 -0
  17. package/dist/commands/sources.d.ts +89 -0
  18. package/dist/commands/sources.js +363 -1
  19. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2285 -1743
  20. package/dist/hq-cloud-sync-runner/package.json +1 -1
  21. package/dist/lib/bot/config.d.ts +8 -0
  22. package/dist/lib/bot/fleet-local.d.ts +1 -1
  23. package/dist/lib/bot/fleet-local.js +82 -30
  24. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
  25. package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
  26. package/dist/lib/bot/local-jobs.d.ts +40 -4
  27. package/dist/lib/bot/local-jobs.js +72 -5
  28. package/dist/lib/bot/run.d.ts +15 -0
  29. package/dist/lib/bot/run.js +99 -1
  30. package/dist/lib/bot/runtime-auth.d.ts +119 -0
  31. package/dist/lib/bot/runtime-auth.js +251 -0
  32. package/dist/lib/daemon/catalog.d.ts +1 -0
  33. package/dist/lib/daemon/catalog.js +1 -1
  34. package/dist/lib/daemon/handoff.d.ts +4 -0
  35. package/dist/lib/daemon/handoff.js +6 -2
  36. package/dist/lib/daemon/install.d.ts +70 -2
  37. package/dist/lib/daemon/install.js +185 -27
  38. package/dist/lib/daemon/legacy.d.ts +9 -3
  39. package/dist/lib/daemon/legacy.js +11 -4
  40. package/dist/lib/daemon/migrate.js +1 -1
  41. package/dist/lib/daemon/run.d.ts +5 -2
  42. package/dist/lib/daemon/run.js +74 -22
  43. package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
  44. package/dist/lib/doctor/checks/anywhere.js +35 -6
  45. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  46. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  47. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.d.ts +33 -0
  48. package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
  49. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
  50. package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
  51. package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
  52. package/dist/lib/hot-channels/adapters/attio.js +374 -0
  53. package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
  54. package/dist/lib/hot-channels/adapters/gmail.js +413 -0
  55. package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
  56. package/dist/lib/hot-channels/adapters/index.js +39 -0
  57. package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
  58. package/dist/lib/hot-channels/adapters/linear.js +321 -0
  59. package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
  60. package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
  61. package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
  62. package/dist/lib/hot-channels/adapters/meetings.js +525 -0
  63. package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
  64. package/dist/lib/hot-channels/adapters/slack.js +277 -0
  65. package/dist/lib/hot-channels/alert.d.ts +78 -0
  66. package/dist/lib/hot-channels/alert.js +179 -0
  67. package/dist/lib/hot-channels/audience.d.ts +15 -0
  68. package/dist/lib/hot-channels/audience.js +25 -0
  69. package/dist/lib/hot-channels/contract.d.ts +146 -0
  70. package/dist/lib/hot-channels/contract.js +20 -0
  71. package/dist/lib/hot-channels/cursors.d.ts +41 -0
  72. package/dist/lib/hot-channels/cursors.js +84 -0
  73. package/dist/lib/hot-channels/index.d.ts +10 -0
  74. package/dist/lib/hot-channels/index.js +10 -0
  75. package/dist/lib/hot-channels/landing.d.ts +83 -0
  76. package/dist/lib/hot-channels/landing.js +175 -0
  77. package/dist/lib/hot-channels/ledger.d.ts +101 -0
  78. package/dist/lib/hot-channels/ledger.js +203 -0
  79. package/dist/lib/hot-channels/loop.d.ts +74 -0
  80. package/dist/lib/hot-channels/loop.js +221 -0
  81. package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
  82. package/dist/lib/hot-channels/registry.d.ts +28 -0
  83. package/dist/lib/hot-channels/registry.js +121 -0
  84. package/dist/lib/hot-channels/run.d.ts +137 -0
  85. package/dist/lib/hot-channels/run.js +324 -0
  86. package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
  87. package/dist/lib/hot-channels/signals-pass.js +581 -0
  88. package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
  89. package/dist/lib/hot-channels/sync-lock.js +224 -0
  90. package/dist/lib/install/hook-target.d.ts +6 -2
  91. package/dist/lib/install/hook-target.js +9 -4
  92. package/dist/lib/lanes/accounts.d.ts +1 -17
  93. package/dist/lib/lanes/accounts.js +5 -45
  94. package/dist/lib/lanes/loop.d.ts +10 -0
  95. package/dist/lib/lanes/loop.js +18 -1
  96. package/dist/lib/lanes/provider-home.d.ts +24 -0
  97. package/dist/lib/lanes/provider-home.js +52 -0
  98. package/dist/lib/lanes/spawn.d.ts +12 -0
  99. package/dist/lib/lanes/spawn.js +27 -0
  100. package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
  101. package/dist/lib/mesh/live/daemon/doctor.js +13 -0
  102. package/dist/lib/mesh/live/daemon/owner-inbox-push.d.ts +94 -0
  103. package/dist/lib/mesh/live/daemon/owner-inbox-push.js +273 -0
  104. package/dist/lib/mesh/live/daemon/run.js +31 -0
  105. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +14 -0
  106. package/dist/lib/mesh/live/daemon/work-subscribe.js +210 -3
  107. package/dist/lib/mesh/live/paths.d.ts +2 -0
  108. package/dist/lib/mesh/live/paths.js +2 -0
  109. package/dist/lib/sync-core/flag.d.ts +1 -1
  110. package/dist/lib/sync-core/flag.js +1 -1
  111. package/dist/lib/sync-core/reconcile.d.ts +1 -1
  112. package/dist/lib/sync-core/reconcile.js +9 -7
  113. package/dist/lib/sync-core/release.d.ts +3 -3
  114. package/dist/lib/sync-core/release.js +4 -4
  115. package/dist/lib/sync-core/rollout-state.d.ts +1 -0
  116. package/dist/lib/sync-core/rollout-state.js +1 -0
  117. package/package.json +8 -8
@@ -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