@indigoai-us/hq-cli 5.345.156 → 5.345.159

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 (144) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
  3. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
  4. package/assets/scaffold/core/scripts/worktree.sh +4 -0
  5. package/dist/command-catalog.generated.d.ts +531 -6
  6. package/dist/command-catalog.generated.js +674 -27
  7. package/dist/command-registration-plan.d.ts +18 -0
  8. package/dist/command-registration-plan.js +4 -0
  9. package/dist/commands/_patterns.d.ts +17 -0
  10. package/dist/commands/_patterns.js +22 -0
  11. package/dist/commands/agent-kit.js +8 -6
  12. package/dist/commands/bot.js +7 -4
  13. package/dist/commands/channel-suggest.d.ts +56 -0
  14. package/dist/commands/channel-suggest.js +170 -0
  15. package/dist/commands/channels.js +7 -1
  16. package/dist/commands/cloud.d.ts +45 -0
  17. package/dist/commands/cloud.js +150 -10
  18. package/dist/commands/company.js +4 -3
  19. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  20. package/dist/commands/core-checkpoint-native.js +67 -2
  21. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  22. package/dist/commands/core-pipeline-driver-native.js +131 -5
  23. package/dist/commands/core.js +12 -0
  24. package/dist/commands/crm.js +83 -6
  25. package/dist/commands/deploy-access.d.ts +33 -0
  26. package/dist/commands/deploy-access.js +322 -0
  27. package/dist/commands/dm-attach.d.ts +106 -0
  28. package/dist/commands/dm-attach.js +184 -0
  29. package/dist/commands/dm.d.ts +37 -0
  30. package/dist/commands/dm.js +172 -10
  31. package/dist/commands/feedback-show.d.ts +40 -0
  32. package/dist/commands/feedback-show.js +261 -0
  33. package/dist/commands/feedback.d.ts +21 -0
  34. package/dist/commands/feedback.js +152 -2
  35. package/dist/commands/files.d.ts +24 -0
  36. package/dist/commands/files.js +52 -1
  37. package/dist/commands/integrations-connect.js +7 -3
  38. package/dist/commands/lanes-accounts.js +16 -1
  39. package/dist/commands/lanes-loop.js +14 -4
  40. package/dist/commands/lanes-message.js +6 -4
  41. package/dist/commands/lanes-resume.js +36 -13
  42. package/dist/commands/lanes-shared.d.ts +5 -0
  43. package/dist/commands/lanes-shared.js +13 -3
  44. package/dist/commands/lanes-watch.js +1 -0
  45. package/dist/commands/lanes.d.ts +20 -1
  46. package/dist/commands/lanes.js +201 -51
  47. package/dist/commands/mesh.js +149 -1
  48. package/dist/commands/monitor.d.ts +1 -0
  49. package/dist/commands/monitor.js +4 -1
  50. package/dist/commands/repo.d.ts +41 -0
  51. package/dist/commands/repo.js +331 -0
  52. package/dist/commands/scan.d.ts +3 -1
  53. package/dist/commands/scan.js +7 -3
  54. package/dist/commands/secrets.d.ts +21 -0
  55. package/dist/commands/secrets.js +114 -14
  56. package/dist/commands/signals.js +17 -5
  57. package/dist/commands/staff/listings.d.ts +41 -0
  58. package/dist/commands/staff/listings.js +160 -0
  59. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  60. package/dist/commands/staff/reset-test-account.js +4 -3
  61. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
  62. package/dist/hq-cloud-sync-runner/package.json +1 -1
  63. package/dist/lib/agent-kit/run/inbox.js +2 -1
  64. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  65. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  66. package/dist/lib/agent-kit/wake.d.ts +4 -0
  67. package/dist/lib/agent-kit/wake.js +49 -3
  68. package/dist/lib/bot/inflight.d.ts +7 -1
  69. package/dist/lib/bot/inflight.js +10 -2
  70. package/dist/lib/bot/prompt.js +3 -0
  71. package/dist/lib/bot/run.js +72 -20
  72. package/dist/lib/bot/session.d.ts +14 -0
  73. package/dist/lib/bot/session.js +70 -0
  74. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  75. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  76. package/dist/lib/capacity/lane-login.d.ts +6 -0
  77. package/dist/lib/capacity/lane-login.js +5 -0
  78. package/dist/lib/core-utils/worktree.js +2 -0
  79. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  80. package/dist/lib/daemon/exit-cause.js +88 -0
  81. package/dist/lib/daemon/handoff.d.ts +2 -0
  82. package/dist/lib/daemon/handoff.js +6 -1
  83. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  84. package/dist/lib/daemon/paths.d.ts +2 -0
  85. package/dist/lib/daemon/paths.js +1 -0
  86. package/dist/lib/daemon/run.js +13 -0
  87. package/dist/lib/daemon/supervisor.d.ts +54 -1
  88. package/dist/lib/daemon/supervisor.js +297 -21
  89. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  90. package/dist/lib/daemon/unit-holds.js +86 -0
  91. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  92. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  93. package/dist/lib/index-render/company-knowledge.js +1 -3
  94. package/dist/lib/index-render/projects.js +1 -3
  95. package/dist/lib/lanes/account-company.d.ts +28 -0
  96. package/dist/lib/lanes/account-company.js +53 -0
  97. package/dist/lib/lanes/accounts.d.ts +22 -3
  98. package/dist/lib/lanes/accounts.js +38 -2
  99. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  100. package/dist/lib/lanes/claude-balance.js +6 -1
  101. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  102. package/dist/lib/lanes/claude-credential.js +163 -0
  103. package/dist/lib/lanes/compaction.d.ts +2 -0
  104. package/dist/lib/lanes/compaction.js +9 -0
  105. package/dist/lib/lanes/config.js +6 -0
  106. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  107. package/dist/lib/lanes/hooks-install.js +2 -1
  108. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  109. package/dist/lib/lanes/host-capacity.js +1 -0
  110. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  111. package/dist/lib/lanes/keep-alive.js +3 -1
  112. package/dist/lib/lanes/links.js +10 -4
  113. package/dist/lib/lanes/loop.js +16 -1
  114. package/dist/lib/lanes/node-binary.d.ts +17 -0
  115. package/dist/lib/lanes/node-binary.js +30 -0
  116. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  117. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  118. package/dist/lib/lanes/reconcile.js +4 -0
  119. package/dist/lib/lanes/remote-control-lane.js +2 -0
  120. package/dist/lib/lanes/spawn.d.ts +3 -0
  121. package/dist/lib/lanes/spawn.js +119 -9
  122. package/dist/lib/lanes/store.d.ts +6 -0
  123. package/dist/lib/lanes/store.js +107 -18
  124. package/dist/lib/lanes/types.d.ts +2 -0
  125. package/dist/lib/lanes/watch.js +28 -5
  126. package/dist/lib/lanes/watcher-main.js +4 -3
  127. package/dist/lib/lanes/watcher.d.ts +4 -0
  128. package/dist/lib/lanes/watcher.js +110 -21
  129. package/dist/lib/mesh/api.d.ts +32 -0
  130. package/dist/lib/mesh/api.js +75 -0
  131. package/dist/lib/secret-scan/detect.js +9 -0
  132. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  133. package/dist/lib/secret-scan/repo-config.js +237 -0
  134. package/dist/lib/secret-scan/rules.d.ts +7 -1
  135. package/dist/lib/secret-scan/rules.js +3 -1
  136. package/dist/lib/secret-scan/staged.d.ts +5 -0
  137. package/dist/lib/secret-scan/staged.js +19 -2
  138. package/dist/lib/secret-scan/types.d.ts +2 -0
  139. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  140. package/dist/lib/sync/server-owned-local-only.js +75 -0
  141. package/dist/utils/package-use-lease.js +27 -9
  142. package/dist/utils/version-gate.d.ts +7 -0
  143. package/dist/utils/version-gate.js +66 -0
  144. package/package.json +8 -8
@@ -400,6 +400,12 @@ export declare const REGISTRATION_PLAN: readonly [{
400
400
  readonly parent: "program";
401
401
  readonly module: "./commands/mcp-status.js";
402
402
  readonly exportName: "registerMcpCommand";
403
+ }, {
404
+ readonly type: "registrar";
405
+ readonly root: "repo";
406
+ readonly parent: "program";
407
+ readonly module: "./commands/repo.js";
408
+ readonly exportName: "registerRepoCommand";
403
409
  }, {
404
410
  readonly type: "registrar";
405
411
  readonly root: "crm";
@@ -533,6 +539,12 @@ export declare const REGISTRATION_PLAN: readonly [{
533
539
  readonly parent: "deploy";
534
540
  readonly module: "./commands/deploy-list.js";
535
541
  readonly exportName: "registerDeployListCommand";
542
+ }, {
543
+ readonly type: "registrar";
544
+ readonly root: "deploy";
545
+ readonly parent: "deploy";
546
+ readonly module: "./commands/deploy-access.js";
547
+ readonly exportName: "registerDeployAccessCommand";
536
548
  }, {
537
549
  readonly type: "group";
538
550
  readonly root: "staff";
@@ -551,6 +563,12 @@ export declare const REGISTRATION_PLAN: readonly [{
551
563
  readonly parent: "staff";
552
564
  readonly module: "./commands/staff/test-account.js";
553
565
  readonly exportName: "registerTestAccountCommand";
566
+ }, {
567
+ readonly type: "registrar";
568
+ readonly root: "staff";
569
+ readonly parent: "staff";
570
+ readonly module: "./commands/staff/listings.js";
571
+ readonly exportName: "registerListingsCommand";
554
572
  }];
555
573
  export declare function registerAllCommands(program: Command): Promise<void>;
556
574
  export declare function registerCommandRoot(program: Command, root: string): Promise<boolean>;
@@ -69,6 +69,7 @@ export const REGISTRATION_PLAN = [
69
69
  { type: "registrar", root: "reindex", parent: "program", module: "./commands/reindex.js", exportName: "registerReindexCommand" },
70
70
  { type: "registrar", root: "rescue", parent: "program", module: "./commands/rescue.js", exportName: "registerRescueCommand" },
71
71
  { type: "registrar", root: "mcp", parent: "program", module: "./commands/mcp-status.js", exportName: "registerMcpCommand" },
72
+ { type: "registrar", root: "repo", parent: "program", module: "./commands/repo.js", exportName: "registerRepoCommand" },
72
73
  { type: "registrar", root: "crm", parent: "program", module: "./commands/crm.js", exportName: "registerCrmCommand" },
73
74
  { type: "registrar", root: "company", parent: "program", module: "./commands/company.js", exportName: "registerCompanyCommand" },
74
75
  { type: "registrar", root: "agents", parent: "program", module: "./commands/agents.js", exportName: "registerAgentsCommand" },
@@ -91,12 +92,15 @@ export const REGISTRATION_PLAN = [
91
92
  { type: "group", root: "deploy", id: "deploy", name: "deploy", description: "Manage deployments in hq-deploy" },
92
93
  { type: "registrar", root: "deploy", parent: "deploy", module: "./commands/deploy-comments.js", exportName: "registerDeployCommentsCommand" },
93
94
  { type: "registrar", root: "deploy", parent: "deploy", module: "./commands/deploy-list.js", exportName: "registerDeployListCommand" },
95
+ { type: "registrar", root: "deploy", parent: "deploy", module: "./commands/deploy-access.js", exportName: "registerDeployAccessCommand" },
94
96
  // onboarding-test-accounts US-007 — super-admin staff CLI wrappers over the
95
97
  // hq-pro admin surface. New wrappers land under this group; the reset
96
98
  // command mirrors POST /v1/admin/test-account/reset.
97
99
  { type: "group", root: "staff", id: "staff", name: "staff", description: "Staff-only commands (super-admin required)" },
98
100
  { type: "registrar", root: "staff", parent: "staff", module: "./commands/staff/reset-test-account.js", exportName: "registerResetTestAccountCommand" },
99
101
  { type: "registrar", root: "staff", parent: "staff", module: "./commands/staff/test-account.js", exportName: "registerTestAccountCommand" },
102
+ // Marketplace moderation: queue / approve / reject over /v1/moderation.
103
+ { type: "registrar", root: "staff", parent: "staff", module: "./commands/staff/listings.js", exportName: "registerListingsCommand" },
100
104
  ];
101
105
  async function executePlan(program, steps) {
102
106
  const parents = new Map([["program", program]]);
@@ -7,6 +7,23 @@ export declare const EMAIL_PATTERN: RegExp;
7
7
  * grantee slot on the wire.
8
8
  */
9
9
  export declare const OUTPOST_UID_PATTERN: RegExp;
10
+ /**
11
+ * A fleet agent uid: `agt_` plus a 26-character Crockford ULID, upper-case,
12
+ * the same shape as OUTPOST_UID_PATTERN. A typo (wrong length, lower-case,
13
+ * or a letter outside the Crockford alphabet: I, L, O, U) fails locally.
14
+ */
15
+ export declare const AGENT_UID_PATTERN: RegExp;
16
+ /**
17
+ * The machine email a fleet agent signs in with:
18
+ * `agt-<ulid>@agents.getindigo.ai`, lower-cased. The CLI sends this email as
19
+ * the grantee; hq-pro canonicalizes it onto the agent's `agt_…` principal
20
+ * when the agent is an active member of the company.
21
+ */
22
+ export declare function agentMachineEmail(agentUid: string): string;
23
+ /** Matches a fleet agent machine email; group 1 is the lower-case ULID. */
24
+ export declare const AGENT_MACHINE_EMAIL_PATTERN: RegExp;
25
+ /** The `agt_…` uid behind a machine email, or null when it is not one. */
26
+ export declare function agentUidFromMachineEmail(email: string): string | null;
10
27
  /**
11
28
  * Keep an ACL pattern exactly as the caller supplied it.
12
29
  *
@@ -7,6 +7,28 @@ export const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
7
7
  * grantee slot on the wire.
8
8
  */
9
9
  export const OUTPOST_UID_PATTERN = /^out_[0-9A-HJKMNP-TV-Z]{26}$/;
10
+ /**
11
+ * A fleet agent uid: `agt_` plus a 26-character Crockford ULID, upper-case,
12
+ * the same shape as OUTPOST_UID_PATTERN. A typo (wrong length, lower-case,
13
+ * or a letter outside the Crockford alphabet: I, L, O, U) fails locally.
14
+ */
15
+ export const AGENT_UID_PATTERN = /^agt_[0-9A-HJKMNP-TV-Z]{26}$/;
16
+ /**
17
+ * The machine email a fleet agent signs in with:
18
+ * `agt-<ulid>@agents.getindigo.ai`, lower-cased. The CLI sends this email as
19
+ * the grantee; hq-pro canonicalizes it onto the agent's `agt_…` principal
20
+ * when the agent is an active member of the company.
21
+ */
22
+ export function agentMachineEmail(agentUid) {
23
+ return `agt-${agentUid.slice(4).toLowerCase()}@agents.getindigo.ai`;
24
+ }
25
+ /** Matches a fleet agent machine email; group 1 is the lower-case ULID. */
26
+ export const AGENT_MACHINE_EMAIL_PATTERN = /^agt-([0-9a-hjkmnp-tv-z]{26})@agents\.getindigo\.ai$/i;
27
+ /** The `agt_…` uid behind a machine email, or null when it is not one. */
28
+ export function agentUidFromMachineEmail(email) {
29
+ const m = AGENT_MACHINE_EMAIL_PATTERN.exec(email.trim());
30
+ return m ? `agt_${m[1].toUpperCase()}` : null;
31
+ }
10
32
  /**
11
33
  * Keep an ACL pattern exactly as the caller supplied it.
12
34
  *
@@ -17,6 +17,7 @@
17
17
  * output in ~/.hq-agent/logs/kit.log). Logs: ~/.hq-agent/logs/<service>.log. Skills:
18
18
  * ~/.hq-agent/skills/<name>/SKILL.md.
19
19
  */
20
+ import { createWakeSecretResolver } from "../lib/agent-kit/wake-secrets.js";
20
21
  import chalk from "chalk";
21
22
  import * as fs from "node:fs";
22
23
  import * as os from "node:os";
@@ -37,7 +38,7 @@ import { serviceLogPath } from "../lib/agent-kit/paths.js";
37
38
  import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
38
39
  import { spawn } from "node:child_process";
39
40
  import { pendingInbox } from "../lib/agent-kit/inbox-state.js";
40
- import { buildWakeNotice, clearWakeConfig, currentWakeState, DEFAULT_WAKE_RETRY_MS, describeWake, fireWake, readWakeConfig, validateWakeUrl, writeWakeConfig, } from "../lib/agent-kit/wake.js";
41
+ import { parseHeaderSecrets, buildWakeNotice, clearWakeConfig, currentWakeState, DEFAULT_WAKE_RETRY_MS, describeWake, fireWake, readWakeConfig, validateWakeUrl, writeWakeConfig, } from "../lib/agent-kit/wake.js";
41
42
  import { runSyncLoop } from "../lib/agent-kit/run/sync.js";
42
43
  import { resolveHqBinary, resolveNodeBinary, } from "../lib/mesh/live/daemon/install.js";
43
44
  import { installServices, servicesStatus, uninstallServices, } from "../lib/service-manager/index.js";
@@ -380,6 +381,7 @@ export function registerAgentKitCommand(agent) {
380
381
  .description("POST a notice to a webhook URL (e.g. a Grok Bot routine webhook) when mail is pending. Prefer --stdin so the URL stays out of shell history")
381
382
  .option("--stdin", "Read the URL from stdin")
382
383
  .option("--retry-minutes <n>", "Re-wake while items stay pending (default 10, minimum 1)")
384
+ .option("--header-secret <Header=SECRET>", "Send a header using a stored company secret (repeatable; value resolved at wake time)", (value, previous) => [...previous, value], [])
383
385
  .action(async (url, opts) => {
384
386
  try {
385
387
  const paths = agentKitPaths(os.homedir(), process.env);
@@ -393,7 +395,8 @@ export function registerAgentKitCommand(agent) {
393
395
  }
394
396
  if (!raw)
395
397
  throw new Error("Provide the URL as an argument or with --stdin");
396
- const config = { kind: "url", url: validateWakeUrl(raw), retryMs: retryMsFrom(opts.retryMinutes) };
398
+ const headerSecrets = parseHeaderSecrets(opts.headerSecret);
399
+ const config = { kind: "url", url: validateWakeUrl(raw), retryMs: retryMsFrom(opts.retryMinutes), ...(Object.keys(headerSecrets).length ? { headerSecrets } : {}) };
397
400
  writeWakeConfig(paths, config);
398
401
  console.log(chalk.green(`Wake set: ${describeWake(config)}`));
399
402
  console.log("Test it: hq agent kit wake test");
@@ -450,15 +453,14 @@ export function registerAgentKitCommand(agent) {
450
453
  .description("Fire the wake once now with the current pending count")
451
454
  .action(async () => {
452
455
  try {
453
- const paths = agentKitPaths(os.homedir(), process.env);
454
- const creds = requireExternalCreds(paths);
455
- ensureKitDirs(paths);
456
+ const runtime = buildKitRuntime("inbox");
457
+ const { paths, creds } = runtime;
456
458
  const config = readWakeConfig(paths);
457
459
  if (!config)
458
460
  throw new Error("No wake configured. Run hq agent kit wake set-url --stdin (or set-command).");
459
461
  const log = createKitLogger(paths, "wake", { echo: false });
460
462
  const ids = pendingInbox(paths).map((e) => e.id);
461
- const result = await fireWake(config, buildWakeNotice(creds.entityUid, "test", ids, new Date()), { paths, agentUid: creds.entityUid, log });
463
+ const result = await fireWake(config, buildWakeNotice(creds.entityUid, "test", ids, new Date()), { paths, agentUid: creds.entityUid, log, resolveHeaderSecrets: createWakeSecretResolver(creds.companySlug, runtime.getToken, creds.apiBaseUrl) });
462
464
  console.log(result.ok ? chalk.green(result.detail) : chalk.red(result.detail));
463
465
  if (!result.ok)
464
466
  process.exit(1);
@@ -44,7 +44,7 @@ import { DEFAULT_COGNITO, ensureCognitoToken, resolveDefaultHqRoot, } from "../u
44
44
  import { peekHqApiKey } from "../utils/resolve-vault-credential.js";
45
45
  import { getCompanyUid, resolveCallerPersonUid } from "../utils/vault-api.js";
46
46
  import { resolveHqBinary } from "../lib/mesh/live/daemon/install.js";
47
- import { askDaemonToStart, askDaemonToStop, daemonOwnsProcess, daemonSupervises, daemonWillSupervise, handOffInstallHere, } from "../lib/daemon/handoff.js";
47
+ import { askDaemonToStart, askDaemonToStop, daemonOwnsProcess, daemonHoldReason, daemonSupervises, daemonWillSupervise, handOffInstallHere, } from "../lib/daemon/handoff.js";
48
48
  import { daemonPaths } from "../lib/daemon/paths.js";
49
49
  import { launchdLabel } from "../lib/bot/paths.js";
50
50
  import { isAncestorProcess } from "../lib/bot/self-process.js";
@@ -286,9 +286,11 @@ async function startBot(name, dir, config, output, extra = {}) {
286
286
  const d = botDaemonStatus(paths, daemonDeps());
287
287
  const hqDaemon = daemonPaths();
288
288
  let how;
289
+ let supervised = false;
289
290
  // A bot created since the daemon last read its plan is the daemon's too,
290
291
  // unless its own LaunchAgent runs it.
291
292
  if (daemonSupervises(hqDaemon, `bots:${name}`) || (!d.installed && daemonWillSupervise(hqDaemon, `bots:${name}`))) {
293
+ supervised = true;
292
294
  // `hq daemon` runs this bot; starting a copy here would run it twice.
293
295
  const before = readBotStatus(dir);
294
296
  if (before && isPidAlive(before.pid) && before.state === "running") {
@@ -322,18 +324,19 @@ async function startBot(name, dir, config, output, extra = {}) {
322
324
  return !!s && s.state === "running" && isPidAlive(s.pid);
323
325
  }, 15_000);
324
326
  const status = readBotStatus(dir);
327
+ const lastError = (!running && supervised ? daemonHoldReason(hqDaemon, `bots:${name}`) : null) ?? status?.lastError ?? null;
325
328
  if (output === "silent")
326
329
  return { running, how };
327
330
  if (output === "json") {
328
- console.log(JSON.stringify({ ok: running, name, ...extra, running, how, state: status?.state ?? "unknown", pid: status?.pid ?? null, lastError: status?.lastError ?? null }, null, 2));
331
+ console.log(JSON.stringify({ ok: running, name, ...extra, running, how, state: status?.state ?? "unknown", pid: status?.pid ?? null, lastError }, null, 2));
329
332
  return { running, how };
330
333
  }
331
334
  if (running)
332
335
  console.log(chalk.green(`${name} is running`) + chalk.dim(` (${how})`));
333
336
  else {
334
337
  console.log(chalk.yellow(`${name} did not report running within 15s`) + chalk.dim(` (${how})`));
335
- if (status?.lastError)
336
- console.log(chalk.dim(` last error: ${status.lastError}`));
338
+ if (lastError)
339
+ console.log(chalk.dim(` last error: ${lastError}`));
337
340
  console.log(chalk.dim(` logs: hq bot logs ${name}`));
338
341
  }
339
342
  return { running, how };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Near-match suggestions for a channel name that resolved to nothing.
3
+ *
4
+ * `hq dm channel <name>`, `hq dm <channel> "msg"`, `hq channels members` and
5
+ * `hq files share --channel` all resolve a channel by exact slug or display
6
+ * name. When that misses, the user usually typed something close to a real
7
+ * channel (for example "desktop-agent-coordination" for the project channel
8
+ * "project-desktop-agent-creation-1331ed7f"). This module ranks the caller's
9
+ * channels against the typed name so the error can list the closest few.
10
+ *
11
+ * Pure: no I/O, so it is unit-testable.
12
+ */
13
+ /** The fields the ranker reads. Structurally compatible with ChannelSummary. */
14
+ export interface SuggestableChannel {
15
+ channelId: string;
16
+ name?: string;
17
+ slug?: string;
18
+ scope?: string;
19
+ }
20
+ export interface ChannelSuggestion {
21
+ id: string;
22
+ name: string;
23
+ }
24
+ /** Most suggestions an error ever lists. */
25
+ export declare const MAX_CHANNEL_SUGGESTIONS = 3;
26
+ /**
27
+ * Tokens used for comparison. Drops a leading "project" token and a trailing
28
+ * 8-hex id, which every project channel carries and which would otherwise
29
+ * dominate the comparison. Falls back to the raw tokens when stripping would
30
+ * leave nothing (a channel literally named "project").
31
+ */
32
+ export declare function comparableTokens(value: string): string[];
33
+ /** Levenshtein distance over code points (so astral characters count once). */
34
+ export declare function editDistance(a: string, b: string): number;
35
+ /**
36
+ * Rank the caller's channels against a name that matched nothing and return
37
+ * up to `max` close ones, best first. Group DMs are unnamed and cannot be
38
+ * addressed by name, so they are never suggested. Each channel is compared by
39
+ * both its slug and its display name; the better score wins.
40
+ */
41
+ export declare function suggestChannels(channels: readonly SuggestableChannel[], query: string, max?: number): ChannelSuggestion[];
42
+ /**
43
+ * Append a "Did you mean" block to a not-found message. Returns the message
44
+ * unchanged when there are no suggestions.
45
+ */
46
+ export declare function withChannelSuggestions(message: string, suggestions: readonly ChannelSuggestion[]): string;
47
+ /**
48
+ * Thrown by channel resolvers when a name or id matches nothing. Carries the
49
+ * near-match suggestions so a --json caller can emit them structurally.
50
+ */
51
+ export declare class ChannelNotFoundError extends Error {
52
+ readonly baseMessage: string;
53
+ readonly suggestions: ChannelSuggestion[];
54
+ constructor(baseMessage: string, suggestions: ChannelSuggestion[]);
55
+ }
56
+ //# sourceMappingURL=channel-suggest.d.ts.map
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Near-match suggestions for a channel name that resolved to nothing.
3
+ *
4
+ * `hq dm channel <name>`, `hq dm <channel> "msg"`, `hq channels members` and
5
+ * `hq files share --channel` all resolve a channel by exact slug or display
6
+ * name. When that misses, the user usually typed something close to a real
7
+ * channel (for example "desktop-agent-coordination" for the project channel
8
+ * "project-desktop-agent-creation-1331ed7f"). This module ranks the caller's
9
+ * channels against the typed name so the error can list the closest few.
10
+ *
11
+ * Pure: no I/O, so it is unit-testable.
12
+ */
13
+ /** Most suggestions an error ever lists. */
14
+ export const MAX_CHANNEL_SUGGESTIONS = 3;
15
+ /**
16
+ * Comparison strings are capped so a very long name cannot make the edit
17
+ * distance (quadratic) expensive across a large channel list.
18
+ */
19
+ const MAX_COMPARE_LENGTH = 64;
20
+ /** Lowercase, Unicode-normalised tokens split on '-', '_' and whitespace. */
21
+ function rawTokens(value) {
22
+ return value
23
+ .normalize("NFKC")
24
+ .toLowerCase()
25
+ .trim()
26
+ .replace(/^#/, "")
27
+ .split(/[\s_-]+/u)
28
+ .filter((token) => token.length > 0);
29
+ }
30
+ /**
31
+ * Tokens used for comparison. Drops a leading "project" token and a trailing
32
+ * 8-hex id, which every project channel carries and which would otherwise
33
+ * dominate the comparison. Falls back to the raw tokens when stripping would
34
+ * leave nothing (a channel literally named "project").
35
+ */
36
+ export function comparableTokens(value) {
37
+ const tokens = rawTokens(value);
38
+ let stripped = tokens;
39
+ if (stripped[0] === "project")
40
+ stripped = stripped.slice(1);
41
+ const last = stripped[stripped.length - 1];
42
+ if (last !== undefined && /^[0-9a-f]{8}$/.test(last)) {
43
+ stripped = stripped.slice(0, -1);
44
+ }
45
+ return stripped.length > 0 ? stripped : tokens;
46
+ }
47
+ function sharedPrefixLength(a, b) {
48
+ const max = Math.min(a.length, b.length);
49
+ let i = 0;
50
+ while (i < max && a[i] === b[i])
51
+ i += 1;
52
+ return i;
53
+ }
54
+ /** Levenshtein distance over code points (so astral characters count once). */
55
+ export function editDistance(a, b) {
56
+ const left = Array.from(a);
57
+ const right = Array.from(b);
58
+ if (left.length === 0)
59
+ return right.length;
60
+ if (right.length === 0)
61
+ return left.length;
62
+ let previous = Array.from({ length: right.length + 1 }, (_, j) => j);
63
+ for (let i = 1; i <= left.length; i += 1) {
64
+ const current = [i];
65
+ for (let j = 1; j <= right.length; j += 1) {
66
+ const cost = left[i - 1] === right[j - 1] ? 0 : 1;
67
+ current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost);
68
+ }
69
+ previous = current;
70
+ }
71
+ return previous[right.length];
72
+ }
73
+ function toComparable(value) {
74
+ const tokens = comparableTokens(value);
75
+ return {
76
+ tokens: new Set(tokens),
77
+ joined: Array.from(tokens.join("-")).slice(0, MAX_COMPARE_LENGTH).join(""),
78
+ };
79
+ }
80
+ /**
81
+ * Similarity in [0, 1], or 0 when the two are not close enough to suggest.
82
+ * A candidate qualifies when it shares a token, shares a prefix of at least
83
+ * three characters, or is within 40% edit distance of the query.
84
+ */
85
+ function similarity(query, candidate) {
86
+ if (!query.joined || !candidate.joined)
87
+ return 0;
88
+ let shared = 0;
89
+ for (const token of query.tokens)
90
+ if (candidate.tokens.has(token))
91
+ shared += 1;
92
+ const union = query.tokens.size + candidate.tokens.size - shared;
93
+ const tokenScore = union > 0 ? shared / union : 0;
94
+ const prefix = sharedPrefixLength(query.joined, candidate.joined);
95
+ const longest = Math.max(Array.from(query.joined).length, Array.from(candidate.joined).length);
96
+ const prefixScore = longest > 0 ? prefix / longest : 0;
97
+ const editScore = longest > 0
98
+ ? 1 - editDistance(query.joined, candidate.joined) / longest
99
+ : 0;
100
+ const close = shared > 0 || prefix >= 3 || editScore >= 0.6;
101
+ if (!close)
102
+ return 0;
103
+ return 0.5 * tokenScore + 0.2 * prefixScore + 0.3 * editScore;
104
+ }
105
+ /**
106
+ * Rank the caller's channels against a name that matched nothing and return
107
+ * up to `max` close ones, best first. Group DMs are unnamed and cannot be
108
+ * addressed by name, so they are never suggested. Each channel is compared by
109
+ * both its slug and its display name; the better score wins.
110
+ */
111
+ export function suggestChannels(channels, query, max = MAX_CHANNEL_SUGGESTIONS) {
112
+ if (max <= 0)
113
+ return [];
114
+ const target = toComparable(query);
115
+ if (!target.joined)
116
+ return [];
117
+ const scored = [];
118
+ for (const channel of channels) {
119
+ if (channel.scope === "group")
120
+ continue;
121
+ const slug = channel.slug?.trim() ?? "";
122
+ const name = channel.name?.trim() ?? "";
123
+ if (!slug && !name)
124
+ continue;
125
+ const score = Math.max(slug ? similarity(target, toComparable(slug)) : 0, name ? similarity(target, toComparable(name)) : 0);
126
+ if (score <= 0)
127
+ continue;
128
+ scored.push({
129
+ suggestion: { id: channel.channelId, name: slug || name },
130
+ score,
131
+ });
132
+ }
133
+ scored.sort((a, b) => b.score - a.score ||
134
+ a.suggestion.name.localeCompare(b.suggestion.name) ||
135
+ a.suggestion.id.localeCompare(b.suggestion.id));
136
+ return scored.slice(0, max).map((entry) => entry.suggestion);
137
+ }
138
+ /**
139
+ * Strip control and bidi-override characters so a server-supplied name cannot
140
+ * drive the terminal or visually reorder the line.
141
+ */
142
+ function printable(value) {
143
+ // eslint-disable-next-line no-control-regex
144
+ return value.replace(/[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
145
+ }
146
+ /**
147
+ * Append a "Did you mean" block to a not-found message. Returns the message
148
+ * unchanged when there are no suggestions.
149
+ */
150
+ export function withChannelSuggestions(message, suggestions) {
151
+ if (suggestions.length === 0)
152
+ return message;
153
+ const lines = suggestions.map((s) => ` ${printable(s.name)} (${printable(s.id)})`);
154
+ return `${message}\nDid you mean:\n${lines.join("\n")}`;
155
+ }
156
+ /**
157
+ * Thrown by channel resolvers when a name or id matches nothing. Carries the
158
+ * near-match suggestions so a --json caller can emit them structurally.
159
+ */
160
+ export class ChannelNotFoundError extends Error {
161
+ baseMessage;
162
+ suggestions;
163
+ constructor(baseMessage, suggestions) {
164
+ super(withChannelSuggestions(baseMessage, suggestions));
165
+ this.name = "ChannelNotFoundError";
166
+ this.baseMessage = baseMessage;
167
+ this.suggestions = suggestions;
168
+ }
169
+ }
170
+ //# sourceMappingURL=channel-suggest.js.map
@@ -1,6 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { ensureCognitoToken } from "../utils/cognito-session.js";
3
3
  import { vaultApiFetch } from "../utils/vault-api.js";
4
+ import { suggestChannels, withChannelSuggestions } from "./channel-suggest.js";
4
5
  import { callerFromToken, channelSlug, fetchChannelMembers, fetchChannels, matchChannelsByName, } from "./dm.js";
5
6
  /**
6
7
  * Human label for a channel row. Named channels (personal/company) show their
@@ -189,7 +190,12 @@ async function runChannelsMembers(channelName, opts) {
189
190
  const channels = await fetchChannels(token);
190
191
  const matches = matchChannelsByName(channels, channelName);
191
192
  if (matches.length === 0) {
192
- console.error(chalk.red(`No channel named '${channelName}' — run \`hq channels\` to see your channels.`));
193
+ const message = `No channel named '${channelName}' — run \`hq channels\` to see your channels.`;
194
+ const suggestions = suggestChannels(channels, channelName);
195
+ if (opts.json) {
196
+ console.log(JSON.stringify({ ok: false, error: message, suggestions }, null, 2));
197
+ }
198
+ console.error(chalk.red(withChannelSuggestions(message, suggestions)));
193
199
  process.exit(1);
194
200
  }
195
201
  if (matches.length > 1) {
@@ -69,6 +69,32 @@ export declare function scopeExcludedExitCode(count: number): 0 | 1;
69
69
  export declare function scopeExcludedWarning(count: number): string | null;
70
70
  /** Actionable recovery for files refused because the vault baseline is stale. */
71
71
  export declare function staleUploadRefusalLines(count: number, paths?: readonly string[]): string[];
72
+ /**
73
+ * Upload-failure fields newer hq-cloud ShareResult carries. Read through
74
+ * readUploadFailures so this CLI also builds against a release without them.
75
+ */
76
+ export interface UploadFailureCounts {
77
+ filesUploadFailed?: number;
78
+ uploadFailedPaths?: string[];
79
+ }
80
+ export declare function readUploadFailures(result: object): {
81
+ count: number;
82
+ paths: string[];
83
+ };
84
+ /**
85
+ * Files the vault accepted in the plan but refused or failed at upload
86
+ * (feedback_943456f4). Without these lines a push that uploaded nothing
87
+ * printed "✓ Pushed 0" and exited 0.
88
+ */
89
+ export declare function uploadFailedLines(count: number, paths?: readonly string[]): string[];
90
+ /**
91
+ * share() option that turns a user-named path an ignore rule drops into an
92
+ * "unreachable path" error instead of a silent 0-file success
93
+ * (feedback_63bfc302, feedback_8c294805). Spread as a plain record so this
94
+ * builds against an hq-cloud release whose ShareOptions predates the field;
95
+ * older releases ignore it.
96
+ */
97
+ export declare function namedPathShareOptions(userNamedPaths: boolean): Record<string, boolean>;
72
98
  /**
73
99
  * Optional detail line listing the effective write prefixes when a push
74
100
  * dropped files — helps debug ACL vs scope-excluded mismatches (feedback_7e9c535e).
@@ -241,6 +267,8 @@ export interface ShareCallResult {
241
267
  filesExcludedByScope: number;
242
268
  filesRefusedStale?: number;
243
269
  filesRefusedStalePaths?: string[];
270
+ filesUploadFailed?: number;
271
+ uploadFailedPaths?: string[];
244
272
  conflictPaths: string[];
245
273
  aborted: boolean;
246
274
  /** Files this leg's plan limit refused; absent or null when none were. */
@@ -286,6 +314,8 @@ export interface PushAllResult {
286
314
  filesExcludedByScope: number;
287
315
  filesRefusedStale?: number;
288
316
  filesRefusedStalePaths?: string[];
317
+ filesUploadFailed?: number;
318
+ uploadFailedPaths?: string[];
289
319
  errors: Array<{
290
320
  company: string;
291
321
  message: string;
@@ -418,4 +448,19 @@ export declare function printReadOnlyLocalOnly(hqRoot: string, journals: Readonl
418
448
  files?: Record<string, unknown>;
419
449
  };
420
450
  }>, log?: (line: string) => void): number;
451
+ /**
452
+ * `hq sync status`: list local files under server-owned folders (sources/,
453
+ * signals/, ontology/ outside entities/) that are not in the sync journal.
454
+ * The server pipeline writes these folders; a client push refuses them and
455
+ * the recurring runner skips them quietly, so a file written locally there
456
+ * never reaches the vault and `pendingFiles: 0` still reports a clean tree
457
+ * (feedback_0414d71c). Uses only the journals already loaded, with no
458
+ * network calls. Prints nothing when nothing applies.
459
+ */
460
+ export declare function printServerOwnedLocalOnly(hqRoot: string, journals: ReadonlyArray<{
461
+ slug: string;
462
+ journal: {
463
+ files?: Record<string, unknown>;
464
+ };
465
+ }>, log?: (line: string) => void): number;
421
466
  //# sourceMappingURL=cloud.d.ts.map