@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
@@ -2,7 +2,7 @@ import chalk from "chalk";
2
2
  import { ensureCognitoToken } from "../utils/cognito-session.js";
3
3
  import { vaultApiFetch } from "../utils/vault-api.js";
4
4
  import { suggestChannels, withChannelSuggestions } from "./channel-suggest.js";
5
- import { callerFromToken, channelSlug, fetchChannelMembers, fetchChannels, matchChannelsByName, } from "./dm.js";
5
+ import { callerFromToken, channelSlug, fetchChannelMembers, fetchChannels, matchChannelsByName, matchContactByName, } from "./dm.js";
6
6
  /**
7
7
  * Human label for a channel row. Named channels (personal/company) show their
8
8
  * name + a `hq dm <slug>` hint; unnamed group DMs are identified by their
@@ -294,6 +294,176 @@ async function runChannelsDelete(channelName, opts) {
294
294
  process.exit(1);
295
295
  }
296
296
  }
297
+ // ─── Channel membership: add / remove ────────────────────────────────────────
298
+ /**
299
+ * Resolve the `<member>` argument of `hq channels add|remove` to a person or
300
+ * agent uid. Accepts a uid (`prs_…`, `agt_…`), an email, or a display name /
301
+ * @handle matched against the caller's DM contacts (which include the bots the
302
+ * caller owns). Pure → unit-testable. Returns an error sentence on no or
303
+ * ambiguous match so the caller never guesses. Feedback e165b286.
304
+ */
305
+ export function resolveMemberTarget(contacts, raw) {
306
+ const target = raw.trim().replace(/^@/, "");
307
+ if (!target)
308
+ return { ok: false, error: "Name a member: a @handle, an email, or a prs_/agt_ uid." };
309
+ if (/^(prs|agt)_[A-Za-z0-9_-]+$/.test(target)) {
310
+ const known = contacts.find((c) => c.personUid === target);
311
+ return { ok: true, personUid: target, displayName: known?.displayName?.trim() || target };
312
+ }
313
+ if (target.includes("@")) {
314
+ const email = target.toLowerCase();
315
+ const hits = contacts.filter((c) => c.personUid && c.email?.trim().toLowerCase() === email);
316
+ const unique = [...new Map(hits.map((c) => [c.personUid, c])).values()];
317
+ if (unique.length === 1) {
318
+ return { ok: true, personUid: unique[0].personUid, displayName: unique[0].displayName?.trim() || email };
319
+ }
320
+ return {
321
+ ok: false,
322
+ error: unique.length === 0
323
+ ? `No contact with the email '${target}'. They must share a company with you; pass their prs_/agt_ uid to add them anyway.`
324
+ : `'${target}' matches ${unique.length} contacts. Pass the prs_/agt_ uid instead.`,
325
+ };
326
+ }
327
+ const match = matchContactByName(contacts, target);
328
+ if (match?.personUid) {
329
+ return { ok: true, personUid: match.personUid, displayName: match.displayName?.trim() || target };
330
+ }
331
+ const names = contacts
332
+ .map((c) => c.displayName?.trim())
333
+ .filter((n) => Boolean(n))
334
+ .sort((a, b) => a.localeCompare(b));
335
+ return {
336
+ ok: false,
337
+ error: `No contact matches '${raw.trim()}'. Contacts: ${names.slice(0, 12).join(", ") || "(none)"}` +
338
+ `${names.length > 12 ? ", …" : ""}. Use the exact name, an email, or a prs_/agt_ uid.`,
339
+ };
340
+ }
341
+ async function fetchDmContacts(token) {
342
+ const res = await vaultApiFetch({ token, path: "/v1/notify/contacts" });
343
+ if (!res.ok)
344
+ return [];
345
+ const data = (await res.json().catch(() => ({})));
346
+ return Array.isArray(data.contacts) ? data.contacts : [];
347
+ }
348
+ async function resolveOneChannel(token, channelName) {
349
+ const channels = await fetchChannels(token);
350
+ const matches = matchChannelsByName(channels, channelName);
351
+ if (matches.length === 0) {
352
+ const message = `No channel named '${channelName}'. Run \`hq channels\` to see your channels.`;
353
+ throw new Error(withChannelSuggestions(message, suggestChannels(channels, channelName)));
354
+ }
355
+ if (matches.length > 1) {
356
+ const scopes = matches.map((match) => match.scope ?? "?").join(", ");
357
+ throw new Error(`'${channelName}' matches ${matches.length} channels (${scopes}). Name one of them.`);
358
+ }
359
+ return matches[0];
360
+ }
361
+ /** The server's refusal, as one plain sentence. */
362
+ export async function channelMembershipError(res, verb, fallback) {
363
+ const err = (await res.json().catch(() => ({})));
364
+ switch (err.code) {
365
+ case "CHANNEL_NOT_MEMBER":
366
+ return "Join the channel before adding members (so far you only browse it). Post once with `hq dm <channel> \"…\"`, or add yourself first with your own uid.";
367
+ case "INVITEE_NOT_FOUND":
368
+ return "That person or bot is not reachable from this company.";
369
+ case "CHANNEL_NOT_OWNER":
370
+ return verb === "add"
371
+ ? "Only the channel owner or a company admin can add members."
372
+ : "Only the channel owner or a company admin can remove other members, and the channel owner cannot be removed.";
373
+ default: {
374
+ const detail = err.error ?? err.message;
375
+ return detail ? `${fallback} (HTTP ${res.status}): ${detail}` : `${fallback} (HTTP ${res.status})`;
376
+ }
377
+ }
378
+ }
379
+ /**
380
+ * `hq channels add <channel> <member…>`: POST /v1/notify/channels/{id}/members
381
+ * once per member. Agents (`agt_…`) are allowed; this is how an owner adds a
382
+ * personal bot to a channel so everyone there can @mention it. The server
383
+ * reports `alreadyMember` on a repeat, which is printed, not treated as an error.
384
+ */
385
+ async function runChannelsAdd(channelName, members, opts) {
386
+ let failed;
387
+ try {
388
+ const token = await ensureCognitoToken();
389
+ const channel = await resolveOneChannel(token, channelName);
390
+ const contacts = await fetchDmContacts(token);
391
+ const results = [];
392
+ for (const raw of members) {
393
+ const target = resolveMemberTarget(contacts, raw);
394
+ if (!target.ok) {
395
+ results.push({ member: raw, ok: false, error: target.error });
396
+ continue;
397
+ }
398
+ const res = await vaultApiFetch({
399
+ token,
400
+ path: `/v1/notify/channels/${encodeURIComponent(channel.channelId)}/members`,
401
+ method: "POST",
402
+ body: { toPersonUid: target.personUid },
403
+ });
404
+ if (!res.ok) {
405
+ results.push({ member: raw, personUid: target.personUid, ok: false, error: await channelMembershipError(res, "add", "Could not add the member") });
406
+ continue;
407
+ }
408
+ const data = (await res.json().catch(() => ({})));
409
+ results.push({ member: target.displayName, personUid: target.personUid, ok: true, alreadyMember: data.alreadyMember === true });
410
+ }
411
+ const label = channel.name ?? channelName;
412
+ if (opts.json) {
413
+ console.log(JSON.stringify({ channelId: channel.channelId, name: label, results }, null, 2));
414
+ }
415
+ else {
416
+ for (const r of results) {
417
+ if (r.ok) {
418
+ console.log(r.alreadyMember ? chalk.dim(`${r.member} is already in #${label}.`) : chalk.green(`Added ${r.member} to #${label}.`));
419
+ }
420
+ else {
421
+ console.error(chalk.red(`${r.member}: ${r.error}`));
422
+ }
423
+ }
424
+ }
425
+ failed = results.some((r) => !r.ok);
426
+ }
427
+ catch (err) {
428
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
429
+ failed = true;
430
+ }
431
+ if (failed)
432
+ process.exit(1);
433
+ }
434
+ /** `hq channels remove <channel> <member>`: DELETE /v1/notify/channels/{id}/members/{uid}. */
435
+ async function runChannelsRemove(channelName, member, opts) {
436
+ try {
437
+ const token = await ensureCognitoToken();
438
+ const channel = await resolveOneChannel(token, channelName);
439
+ // The member is in the channel, so resolve against the roster first, then contacts.
440
+ const roster = await fetchChannelMembers(token, channel.channelId);
441
+ const rosterContacts = roster.map((m) => ({ personUid: m.personUid, email: m.email, displayName: m.displayName }));
442
+ let target = resolveMemberTarget(rosterContacts, member);
443
+ if (!target.ok)
444
+ target = resolveMemberTarget(await fetchDmContacts(token), member);
445
+ if (!target.ok)
446
+ throw new Error(target.error);
447
+ const res = await vaultApiFetch({
448
+ token,
449
+ path: `/v1/notify/channels/${encodeURIComponent(channel.channelId)}/members/${encodeURIComponent(target.personUid)}`,
450
+ method: "DELETE",
451
+ });
452
+ if (!res.ok)
453
+ throw new Error(await channelMembershipError(res, "remove", "Could not remove the member"));
454
+ const label = channel.name ?? channelName;
455
+ if (opts.json) {
456
+ console.log(JSON.stringify({ channelId: channel.channelId, personUid: target.personUid, removed: true }, null, 2));
457
+ }
458
+ else {
459
+ console.log(chalk.green(`Removed ${target.displayName} from #${label}.`));
460
+ }
461
+ }
462
+ catch (err) {
463
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
464
+ process.exit(1);
465
+ }
466
+ }
297
467
  export function registerChannelsCommand(program) {
298
468
  const channels = program
299
469
  .command("channels")
@@ -329,5 +499,19 @@ export function registerChannelsCommand(program) {
329
499
  .action(async (channelName, opts) => {
330
500
  await runChannelsDelete(channelName, opts);
331
501
  });
502
+ channels
503
+ .command("add <channel> <member...>")
504
+ .description("Add people or your own bots to a channel, by @handle, email, or prs_/agt_ uid.")
505
+ .option("--json", "Output the per-member results as JSON.")
506
+ .action(async (channelName, members, opts) => {
507
+ await runChannelsAdd(channelName, members, opts);
508
+ });
509
+ channels
510
+ .command("remove <channel> <member>")
511
+ .description("Remove a member or bot from a channel (owner or company admin), or leave it yourself.")
512
+ .option("--json", "Output the result as JSON.")
513
+ .action(async (channelName, member, opts) => {
514
+ await runChannelsRemove(channelName, member, opts);
515
+ });
332
516
  }
333
517
  //# sourceMappingURL=channels.js.map
@@ -59,6 +59,8 @@ export interface DaemonCommandDeps {
59
59
  /** Reads stdin for the one-shot daemon machine login; never reads env or argv credentials. */
60
60
  readStdin?: () => Promise<string>;
61
61
  windowsUser?: string;
62
+ /** The Windows user's SID; the daemon's Scheduled Task name carries it so each user on a PC has their own task. */
63
+ windowsSid?: string;
62
64
  windowsHeadlessLauncher?: string;
63
65
  systemUnitDir?: string;
64
66
  systemRecordPath?: string;
@@ -24,7 +24,7 @@ import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits }
24
24
  import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
25
25
  import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
26
26
  import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
27
- import { DAEMON_WINDOWS_TASK, ROOT_REFUSAL, SYSTEM_UNIT_DIR, WINDOWS_START_WAIT_MS, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, windowsInteractiveSession, } from "../lib/daemon/install.js";
27
+ import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, WINDOWS_START_WAIT_MS, daemonRootRefusal, installDaemonUnit, installScopeFor, classifyWindowsTask, legacyWindowsTask, readWindowsSid, uninstallDaemonUnit, windowsInteractiveSession, windowsTaskName, } from "../lib/daemon/install.js";
28
28
  import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
29
29
  import { isDaemonProcess, liveDaemonLock, runningDaemonPid } from "../lib/daemon/lock.js";
30
30
  import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy, readHqCloudUpdateState } from "../lib/daemon/hq-cloud.js";
@@ -132,7 +132,10 @@ async function loadDefaults(over) {
132
132
  existsSync: fs.existsSync,
133
133
  listDir: (dir) => fs.readdirSync(dir),
134
134
  launchdJobLoaded: (label) => run("launchctl", ["print", `gui/${process.getuid?.() ?? 0}/${label}`]).status === 0,
135
- windowsTaskExists: (task) => run("schtasks.exe", ["/Query", "/TN", task]).status === 0,
135
+ windowsTaskOwner: (task) => classifyWindowsTask(run, task, {
136
+ windowsSid: over.windowsSid ?? readWindowsSid(run),
137
+ windowsUser: over.windowsUser ?? mesh.resolveWindowsUser(env),
138
+ }).state,
136
139
  })),
137
140
  resolveHqRoot: over.resolveHqRoot ?? ((flag) => resolveDaemonHqRoot(flag, { env, cwd: process.cwd(), home })),
138
141
  userHome: over.userHome ?? ((user) => loginHome(user, run)),
@@ -140,6 +143,7 @@ async function loadDefaults(over) {
140
143
  sleep: over.sleep,
141
144
  interactiveSession: over.interactiveSession,
142
145
  windowsUser: over.windowsUser ?? (platform === "win32" ? mesh.resolveWindowsUser(env) : ""),
146
+ windowsSid: over.windowsSid ?? (platform === "win32" ? readWindowsSid(run) : undefined),
143
147
  windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
144
148
  systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
145
149
  syncModeClient: over.syncModeClient,
@@ -196,6 +200,7 @@ function hostFor(d, paths, pathEnv) {
196
200
  uid: d.uid,
197
201
  isRoot: d.isRoot,
198
202
  windowsUser: d.windowsUser ?? "",
203
+ windowsSid: d.windowsSid,
199
204
  windowsHeadlessLauncher: d.windowsHeadlessLauncher,
200
205
  systemUnitDir: d.systemUnitDir ?? SYSTEM_UNIT_DIR,
201
206
  env,
@@ -306,6 +311,8 @@ async function runInstall(d, opts) {
306
311
  runningDaemon: () => liveDaemonLock(paths.lockPath, d.daemonAlive),
307
312
  });
308
313
  handOver(d, { paths, detected, runAsUser });
314
+ if (result.notice)
315
+ d.out(result.notice);
309
316
  if (result.warning)
310
317
  d.err(result.warning);
311
318
  if (result.kind === "manual") {
@@ -333,7 +340,7 @@ async function runInstall(d, opts) {
333
340
  ? `Installed ${result.unitPath}, but the daemon did not start within ${seconds} seconds although you are signed in to Windows.`
334
341
  : `Installed ${result.unitPath}, but the daemon did not start within ${seconds} seconds, and HQ could not check whether you are signed in to Windows.`,
335
342
  "Check it with: hq daemon status",
336
- `The task's last result: powershell -NoProfile -Command "(Get-ScheduledTaskInfo -TaskName '${DAEMON_WINDOWS_TASK}').LastTaskResult"`,
343
+ `The task's last result: powershell -NoProfile -Command "(Get-ScheduledTaskInfo -TaskName '${windowsTaskName({ windowsSid: d.windowsSid, windowsUser: d.windowsUser ?? "" })}').LastTaskResult"`,
337
344
  ].join("\n"));
338
345
  }
339
346
  if (!result.running) {
@@ -386,6 +393,8 @@ async function runUninstall(d, opts) {
386
393
  }
387
394
  const result = uninstallDaemonUnit(hostFor(d, paths, false), { scope: installScopeFor(detected.type) }, { run: d.run });
388
395
  d.out(result.removed ? `Removed ${result.unitPath}` : "hq daemon was not installed.");
396
+ if (result.notice)
397
+ d.out(result.notice);
389
398
  if (result.warning)
390
399
  d.err(result.warning);
391
400
  if (opts.restoreLegacy) {
@@ -869,7 +878,9 @@ async function runMigrate(d, opts) {
869
878
  platform: d.platform,
870
879
  home: d.home,
871
880
  systemUnitDir: d.systemUnitDir,
881
+ windowsTaskName: windowsTaskName({ windowsSid: d.windowsSid, windowsUser: d.windowsUser ?? "" }),
872
882
  windowsTaskExists: (task) => d.run("schtasks.exe", ["/Query", "/TN", task]).status === 0,
883
+ windowsLegacyTaskIsMine: () => legacyWindowsTask(d.run, { windowsSid: d.windowsSid, windowsUser: d.windowsUser ?? "" }).state === "mine",
873
884
  });
874
885
  if (!installed) {
875
886
  throw new DaemonCommandError("hq daemon is not installed here yet. Run `hq daemon install` first, then `hq daemon migrate --apply`.");
@@ -893,7 +904,7 @@ async function runMigrate(d, opts) {
893
904
  if (u.migratable)
894
905
  d.out(`Would take over ${u.unitId} from ${describeLegacyUnit(u)}${u.path ? ` (${u.path})` : ""}`);
895
906
  else
896
- d.out(`Leaving ${u.unitId} to ${describeLegacyUnit(u)} (owned by the HQ desktop app)`);
907
+ d.out(`Leaving ${u.unitId} to ${describeLegacyUnit(u)} (${u.notMineReason ?? "owned by the HQ desktop app"})`);
897
908
  }
898
909
  d.out("Nothing was changed. `hq daemon migrate --apply` takes them over.");
899
910
  }
@@ -299,6 +299,8 @@ export interface UsableApp {
299
299
  * provider, else `--connection <id>`.
300
300
  */
301
301
  selector: string;
302
+ /** The caller created this connection (`viewerAccess.isCreator`). */
303
+ ownedByCaller: boolean;
302
304
  }
303
305
  export interface UsableAppsResult {
304
306
  companyUid: string;
@@ -529,6 +529,7 @@ export async function fetchUsableApps(token, companyUid) {
529
529
  status: row.status,
530
530
  providerArg,
531
531
  selector: shared ? `--connection ${row.id}` : `--provider ${providerArg}`,
532
+ ownedByCaller: row.viewerAccess?.isCreator === true,
532
533
  };
533
534
  })
534
535
  .sort((a, b) => compareNames(a.name, b.name))
@@ -147,7 +147,7 @@ export declare function checkpointInstruction(worker: Pick<ResolvedLaneWorker, "
147
147
  export declare function composeWorkerBrief(worker: ResolvedLaneWorker, taskBrief: string, hqRoot: string): string;
148
148
  /**
149
149
  * A lane with no worker still carries the standing authorization paragraph,
150
- * the foreground-only rule and the temp-home rule.
150
+ * the foreground-only rule, the temp-home rule and the self-review rule.
151
151
  */
152
152
  export declare function composeGeneralBrief(taskBrief: string, hqRoot: string): string;
153
153
  export declare const DISPATCH_TERMINAL_REASON: "outpost_dispatch_failed";
@@ -108,7 +108,7 @@ import { resolveKrFreshnessForShow } from "../lib/lanes/kr-freshness.js";
108
108
  import { ensureSessionHome } from "../lib/lanes/session-relink.js";
109
109
  import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
110
110
  import { readLaneClaudeCredential } from "../lib/lanes/claude-credential.js";
111
- import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, tempHomeRule, spawnLaneWorker, spawnEnvelopeRepairRequest, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
111
+ import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, selfReviewRule, tempHomeRule, spawnLaneWorker, spawnEnvelopeRepairRequest, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
112
112
  import { handOffInstallHere } from "../lib/daemon/handoff.js";
113
113
  export { spawnLaneWorker };
114
114
  export function openPullRequestsForProject(hqRoot, metadata) {
@@ -3041,11 +3041,13 @@ export function composeWorkerBrief(worker, taskBrief, hqRoot) {
3041
3041
  "",
3042
3042
  tempHomeRule(hqRoot),
3043
3043
  "",
3044
+ selfReviewRule(hqRoot),
3045
+ "",
3044
3046
  ].join("\n");
3045
3047
  }
3046
3048
  /**
3047
3049
  * A lane with no worker still carries the standing authorization paragraph,
3048
- * the foreground-only rule and the temp-home rule.
3050
+ * the foreground-only rule, the temp-home rule and the self-review rule.
3049
3051
  */
3050
3052
  export function composeGeneralBrief(taskBrief, hqRoot) {
3051
3053
  return [
@@ -3058,6 +3060,8 @@ export function composeGeneralBrief(taskBrief, hqRoot) {
3058
3060
  "",
3059
3061
  tempHomeRule(hqRoot),
3060
3062
  "",
3063
+ selfReviewRule(hqRoot),
3064
+ "",
3061
3065
  ].join("\n");
3062
3066
  }
3063
3067
  function parseMaxRuntimeSeconds(raw, workerId) {
@@ -3071,6 +3071,25 @@ export function registerMeshCommand(program, deps = {}) {
3071
3071
  .description("Report daemon running/MQTT/spool/held/dead-letter/outbox/flush health")
3072
3072
  .option("--json", "Print machine-readable JSON")
3073
3073
  .action((opts) => wrap(() => runDaemonDoctorCmd(opts))());
3074
+ mesh
3075
+ .command("push-status")
3076
+ .description("Report whether the owner DM push signal is live or stale, with the last event time")
3077
+ .option("--json", "Print machine-readable JSON")
3078
+ .action((opts) => wrap(() => runMeshPushStatus(opts))());
3079
+ }
3080
+ async function runMeshPushStatus(opts) {
3081
+ const { ownerPushStatusForAccount, readOwnerInboxPushState, formatOwnerPushStatus } = await import("../lib/mesh/live/daemon/owner-inbox-push.js");
3082
+ const { describeCognitoTokenSource } = await import("../utils/cognito-session.js");
3083
+ const { actorKind } = describeCognitoTokenSource({ home: os.homedir(), env: process.env });
3084
+ const status = ownerPushStatusForAccount(readOwnerInboxPushState(workMeshRoot(undefined, process.env)), Date.now(), actorKind);
3085
+ if (opts.json) {
3086
+ console.log(JSON.stringify(status, null, 2));
3087
+ }
3088
+ else {
3089
+ console.log(formatOwnerPushStatus(status));
3090
+ }
3091
+ if (status.status === "stale")
3092
+ process.exitCode = 1;
3074
3093
  }
3075
3094
  async function runDaemonRun(opts = {}) {
3076
3095
  const { runMeshDaemon } = await import("../lib/mesh/live/daemon/run.js");
@@ -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