@indigoai-us/hq-cli 5.55.0 → 5.56.0

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.
@@ -15,6 +15,13 @@
15
15
  import { Command } from "commander";
16
16
  import { type ConflictStrategy, type MembershipSyncConfig, type SyncMode, type PullScope, type ExplicitGrant } from "@indigoai-us/hq-cloud";
17
17
  import { type BannerLevel } from "../lib/narrow-hint-banner.js";
18
+ /**
19
+ * Build a loud, human-readable warning when a push dropped files because
20
+ * they fell outside the caller's granted write scope. Returns null when
21
+ * nothing was scope-excluded. Keeping this pure makes the "never silently
22
+ * succeed when files were dropped" guarantee unit-testable.
23
+ */
24
+ export declare function scopeExcludedWarning(count: number): string | null;
18
25
  export interface PullAllVaultClient {
19
26
  listMyMemberships(): Promise<Array<{
20
27
  companyUid: string;
@@ -140,6 +147,7 @@ export interface ShareCallResult {
140
147
  bytesUploaded: number;
141
148
  filesSkipped: number;
142
149
  filesDeleted: number;
150
+ filesExcludedByScope: number;
143
151
  conflictPaths: string[];
144
152
  aborted: boolean;
145
153
  }
@@ -169,6 +177,7 @@ export interface PushAllResult {
169
177
  filesUploaded: number;
170
178
  bytesUploaded: number;
171
179
  filesDeleted: number;
180
+ filesExcludedByScope: number;
172
181
  errors: Array<{
173
182
  company: string;
174
183
  message: string;
@@ -13,13 +13,27 @@
13
13
  * hq sync status — show local journal summary
14
14
  */
15
15
 
16
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="59cfec0e-76a2-5beb-8eb6-80a129dd62c6")}catch(e){}}();
16
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="fa72780b-ae22-5125-98b6-4e7b866b7d17")}catch(e){}}();
17
17
  import chalk from "chalk";
18
18
  import * as fs from "fs";
19
19
  import * as path from "path";
20
20
  import { share, sync, getStateDir, listJournals, loadCachedTokens, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, } from "@indigoai-us/hq-cloud";
21
21
  import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
22
22
  import { emitNarrowHint, isStrictRefusal, resolveBannerLevel, } from "../lib/narrow-hint-banner.js";
23
+ /**
24
+ * Build a loud, human-readable warning when a push dropped files because
25
+ * they fell outside the caller's granted write scope. Returns null when
26
+ * nothing was scope-excluded. Keeping this pure makes the "never silently
27
+ * succeed when files were dropped" guarantee unit-testable.
28
+ */
29
+ export function scopeExcludedWarning(count) {
30
+ if (count <= 0)
31
+ return null;
32
+ return (`⚠ ${count} file(s) were NOT uploaded — they fall outside the ` +
33
+ `prefixes you have write access to (company-wide or direct grants). ` +
34
+ `They were skipped, not synced. Re-run with --json to list them, or ` +
35
+ `ask an admin to grant you write on those paths.`);
36
+ }
23
37
  /**
24
38
  * Resolve the `propagateDeletePolicy` for share() calls.
25
39
  *
@@ -251,6 +265,7 @@ export async function pushAll(options, deps) {
251
265
  filesUploaded: 0,
252
266
  bytesUploaded: 0,
253
267
  filesDeleted: 0,
268
+ filesExcludedByScope: 0,
254
269
  errors: [],
255
270
  perCompany: [],
256
271
  };
@@ -261,6 +276,7 @@ export async function pushAll(options, deps) {
261
276
  result.filesUploaded += r.filesUploaded;
262
277
  result.bytesUploaded += r.bytesUploaded;
263
278
  result.filesDeleted += r.filesDeleted;
279
+ result.filesExcludedByScope += r.filesExcludedByScope;
264
280
  result.perCompany.push({ slug: entry.slug, result: r });
265
281
  }
266
282
  catch (err) {
@@ -621,7 +637,14 @@ export function registerCloudCommands(program) {
621
637
  log(chalk.yellow(`\n⚠ Push aborted (${result.filesUploaded} uploaded, ${result.filesSkipped} skipped)`));
622
638
  process.exit(1);
623
639
  }
624
- log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
640
+ if (result.filesExcludedByScope > 0) {
641
+ log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
642
+ `${result.filesSkipped} skipped, ${result.filesExcludedByScope} scope-excluded)`));
643
+ log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)));
644
+ }
645
+ else {
646
+ log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
647
+ }
625
648
  }
626
649
  catch (err) {
627
650
  const message = err instanceof Error ? err.message : String(err);
@@ -1051,18 +1074,27 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
1051
1074
  }
1052
1075
  else if (row.result) {
1053
1076
  const r = row.result;
1054
- const status = r.aborted ? chalk.yellow("⚠") : chalk.green("✓");
1077
+ const status = r.aborted || r.filesExcludedByScope > 0
1078
+ ? chalk.yellow("⚠")
1079
+ : chalk.green("✓");
1055
1080
  console.log(` ${status} ${row.slug}: ${r.filesUploaded} file(s), ` +
1056
1081
  `${formatBytes(r.bytesUploaded)}, ${r.filesSkipped} skipped, ` +
1057
- `${r.filesDeleted} deleted, ${r.conflictPaths.length} conflict(s)` +
1082
+ `${r.filesDeleted} deleted, ${r.filesExcludedByScope} scope-excluded, ` +
1083
+ `${r.conflictPaths.length} conflict(s)` +
1058
1084
  (r.aborted ? " — aborted" : ""));
1059
1085
  }
1060
1086
  }
1061
1087
  const errored = result.errors.length;
1062
1088
  const summary = `\nPushed ${result.filesUploaded} file(s) ` +
1063
1089
  `(${formatBytes(result.bytesUploaded)}) across ${result.attempted} ` +
1064
- `target(s); ${result.filesDeleted} deleted; ${errored} error(s)`;
1065
- console.log(errored > 0 ? chalk.yellow(summary) : chalk.green(summary));
1090
+ `target(s); ${result.filesDeleted} deleted; ${errored} error(s); ` +
1091
+ `${result.filesExcludedByScope} scope-excluded`;
1092
+ console.log(errored > 0 || result.filesExcludedByScope > 0
1093
+ ? chalk.yellow(summary)
1094
+ : chalk.green(summary));
1095
+ if (result.filesExcludedByScope > 0) {
1096
+ console.log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)));
1097
+ }
1066
1098
  if (errored > 0)
1067
1099
  process.exit(1);
1068
1100
  }
@@ -1124,10 +1156,16 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1124
1156
  ...(journalSlug !== undefined ? { journalSlug } : {}),
1125
1157
  ...(author ? { author } : {}),
1126
1158
  });
1127
- console.log(` ${pushResult.aborted ? chalk.yellow("⚠") : chalk.green("✓")} ` +
1159
+ const pushStatus = pushResult.aborted || pushResult.filesExcludedByScope > 0
1160
+ ? chalk.yellow("⚠")
1161
+ : chalk.green("✓");
1162
+ console.log(` ${pushStatus} ` +
1128
1163
  `${pushResult.filesUploaded} uploaded, ${pushResult.filesSkipped} skipped, ` +
1129
- `${pushResult.filesDeleted} deleted` +
1164
+ `${pushResult.filesDeleted} deleted, ${pushResult.filesExcludedByScope} scope-excluded` +
1130
1165
  (pushResult.aborted ? " — aborted" : ""));
1166
+ if (pushResult.filesExcludedByScope > 0) {
1167
+ console.log(chalk.yellow(scopeExcludedWarning(pushResult.filesExcludedByScope)));
1168
+ }
1131
1169
  if (pushResult.aborted) {
1132
1170
  console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
1133
1171
  process.exit(1);
@@ -1330,4 +1368,4 @@ function resolveUploadAuthorFromCache() {
1330
1368
  }
1331
1369
  }
1332
1370
  //# sourceMappingURL=cloud.js.map
1333
- //# debugId=59cfec0e-76a2-5beb-8eb6-80a129dd62c6
1371
+ //# debugId=fa72780b-ae22-5125-98b6-4e7b866b7d17
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="ae9ed929-9821-5f8b-bbbe-8f7b6105f5ad")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2640a391-a3e6-5830-b56f-03b5fab3a4bd")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import { ensureCognitoToken } from "../utils/cognito-session.js";
5
5
  import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
@@ -35,9 +35,14 @@ function statusBadge(status) {
35
35
  async function resolveShortId(token, prefix, query) {
36
36
  if (prefix.includes("-") && prefix.length > 8)
37
37
  return prefix;
38
- const res = await vaultApiFetch({ token, path: "/v1/meetings", query });
39
- if (!res.ok)
40
- return prefix;
38
+ const res = await vaultApiFetch({
39
+ token,
40
+ path: "/v1/meetings",
41
+ query: { ...query, limit: "500" },
42
+ });
43
+ if (!res.ok) {
44
+ throw new Error(`Could not resolve meeting ID "${prefix}" — failed to list meetings (${res.status}). Pass the full meeting id.`);
45
+ }
41
46
  const data = (await res.json());
42
47
  const matches = data.meetings.filter((m) => m.meetingId.startsWith(prefix));
43
48
  if (matches.length === 1)
@@ -46,7 +51,8 @@ async function resolveShortId(token, prefix, query) {
46
51
  console.error(chalk.red(`Ambiguous ID prefix "${prefix}" — matches ${matches.length} meetings. Use a longer prefix.`));
47
52
  process.exit(1);
48
53
  }
49
- return prefix;
54
+ console.error(chalk.red(`No meeting matches ID "${prefix}". It may be older than the meetings shown by \`hq meetings list\`, still processing, or attributed to a different company. Pass the full meeting id, add --company <slug>, or widen the list with \`hq meetings list --limit <n>\`.`));
55
+ process.exit(1);
50
56
  }
51
57
  async function handleApiError(res) {
52
58
  const body = (await res.json().catch(() => ({})));
@@ -426,4 +432,4 @@ export function registerMeetingsCommand(program) {
426
432
  });
427
433
  }
428
434
  //# sourceMappingURL=meetings.js.map
429
- //# debugId=ae9ed929-9821-5f8b-bbbe-8f7b6105f5ad
435
+ //# debugId=2640a391-a3e6-5830-b56f-03b5fab3a4bd
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.55.0",
3
+ "version": "5.56.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -82,6 +82,7 @@ function makeShareSpy(
82
82
  bytesUploaded: 0,
83
83
  filesSkipped: 0,
84
84
  filesDeleted: 0,
85
+ filesExcludedByScope: 0,
85
86
  conflictPaths: [],
86
87
  aborted: false,
87
88
  ...override,
@@ -307,6 +308,7 @@ describe("pushAll", () => {
307
308
  filesUploaded: 0,
308
309
  bytesUploaded: 0,
309
310
  filesDeleted: 0,
311
+ filesExcludedByScope: 0,
310
312
  errors: [],
311
313
  perCompany: [],
312
314
  });
@@ -379,6 +381,33 @@ describe("pushAll", () => {
379
381
  expect(result.filesDeleted).toBe(4);
380
382
  });
381
383
 
384
+ it("aggregates filesExcludedByScope across every successful target", async () => {
385
+ const hqRoot = makeHqRoot(["companies", "docs"]);
386
+ const vaultClient = makeVaultClient({
387
+ memberships: [{ companyUid: "cmp_acme" }],
388
+ persons: [
389
+ {
390
+ uid: "psn_alice",
391
+ type: "person",
392
+ slug: "alice",
393
+ createdAt: "2026-01-01T00:00:00Z",
394
+ },
395
+ ],
396
+ entitiesBySlug: { cmp_acme: { slug: "acme" } },
397
+ });
398
+ const share = makeShareSpy({
399
+ cmp_acme: { filesExcludedByScope: 2 },
400
+ psn_alice: { filesExcludedByScope: 3 },
401
+ });
402
+
403
+ const result = await pushAll(
404
+ { hqRoot },
405
+ { vaultClient, share: share.fn },
406
+ );
407
+
408
+ expect(result.filesExcludedByScope).toBe(5);
409
+ });
410
+
382
411
  // ── skipPersonal — fanout omits the canonical-person leg ──────────────────
383
412
  //
384
413
  // Symmetric with the `pullAll` skipPersonal coverage in
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { scopeExcludedWarning } from "./cloud.js";
4
+
5
+ describe("scopeExcludedWarning", () => {
6
+ it("returns null for zero and negative counts", () => {
7
+ expect(scopeExcludedWarning(0)).toBeNull();
8
+ expect(scopeExcludedWarning(-1)).toBeNull();
9
+ });
10
+
11
+ it("returns a loud warning for positive counts", () => {
12
+ const warning = scopeExcludedWarning(3);
13
+
14
+ expect(warning).toBeTruthy();
15
+ expect(warning).toContain("3");
16
+ // The warning must loudly state the files did NOT reach the vault and
17
+ // explain why (outside the caller's write access) — without leaning on
18
+ // the jargon word "scope" in user-facing copy.
19
+ expect(warning).toContain("NOT uploaded");
20
+ expect(warning?.toLowerCase()).toContain("write access");
21
+ });
22
+ });
@@ -50,6 +50,22 @@ import {
50
50
  type BannerLevel,
51
51
  } from "../lib/narrow-hint-banner.js";
52
52
 
53
+ /**
54
+ * Build a loud, human-readable warning when a push dropped files because
55
+ * they fell outside the caller's granted write scope. Returns null when
56
+ * nothing was scope-excluded. Keeping this pure makes the "never silently
57
+ * succeed when files were dropped" guarantee unit-testable.
58
+ */
59
+ export function scopeExcludedWarning(count: number): string | null {
60
+ if (count <= 0) return null;
61
+ return (
62
+ `⚠ ${count} file(s) were NOT uploaded — they fall outside the ` +
63
+ `prefixes you have write access to (company-wide or direct grants). ` +
64
+ `They were skipped, not synced. Re-run with --json to list them, or ` +
65
+ `ask an admin to grant you write on those paths.`
66
+ );
67
+ }
68
+
53
69
  /**
54
70
  * Resolve the `propagateDeletePolicy` for share() calls.
55
71
  *
@@ -234,6 +250,7 @@ export interface ShareCallResult {
234
250
  bytesUploaded: number;
235
251
  filesSkipped: number;
236
252
  filesDeleted: number;
253
+ filesExcludedByScope: number;
237
254
  conflictPaths: string[];
238
255
  aborted: boolean;
239
256
  }
@@ -267,6 +284,7 @@ export interface PushAllResult {
267
284
  filesUploaded: number;
268
285
  bytesUploaded: number;
269
286
  filesDeleted: number;
287
+ filesExcludedByScope: number;
270
288
  errors: Array<{ company: string; message: string }>;
271
289
  perCompany: PushAllRow[];
272
290
  }
@@ -513,6 +531,7 @@ export async function pushAll(
513
531
  filesUploaded: 0,
514
532
  bytesUploaded: 0,
515
533
  filesDeleted: 0,
534
+ filesExcludedByScope: 0,
516
535
  errors: [],
517
536
  perCompany: [],
518
537
  };
@@ -524,6 +543,7 @@ export async function pushAll(
524
543
  result.filesUploaded += r.filesUploaded;
525
544
  result.bytesUploaded += r.bytesUploaded;
526
545
  result.filesDeleted += r.filesDeleted;
546
+ result.filesExcludedByScope += r.filesExcludedByScope;
527
547
  result.perCompany.push({ slug: entry.slug, result: r });
528
548
  } catch (err) {
529
549
  const message = err instanceof Error ? err.message : String(err);
@@ -1016,11 +1036,21 @@ export function registerCloudCommands(program: Command): void {
1016
1036
  process.exit(1);
1017
1037
  }
1018
1038
 
1019
- log(
1020
- chalk.green(
1021
- `\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`,
1022
- ),
1023
- );
1039
+ if (result.filesExcludedByScope > 0) {
1040
+ log(
1041
+ chalk.yellow(
1042
+ `\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
1043
+ `${result.filesSkipped} skipped, ${result.filesExcludedByScope} scope-excluded)`,
1044
+ ),
1045
+ );
1046
+ log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)!));
1047
+ } else {
1048
+ log(
1049
+ chalk.green(
1050
+ `\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`,
1051
+ ),
1052
+ );
1053
+ }
1024
1054
  } catch (err) {
1025
1055
  const message = err instanceof Error ? err.message : String(err);
1026
1056
  if (jsonMode) {
@@ -1657,11 +1687,15 @@ async function runPushAll(
1657
1687
  console.log(chalk.red(` ✗ ${row.slug}: ${row.error}`));
1658
1688
  } else if (row.result) {
1659
1689
  const r = row.result;
1660
- const status = r.aborted ? chalk.yellow("⚠") : chalk.green("✓");
1690
+ const status =
1691
+ r.aborted || r.filesExcludedByScope > 0
1692
+ ? chalk.yellow("⚠")
1693
+ : chalk.green("✓");
1661
1694
  console.log(
1662
1695
  ` ${status} ${row.slug}: ${r.filesUploaded} file(s), ` +
1663
1696
  `${formatBytes(r.bytesUploaded)}, ${r.filesSkipped} skipped, ` +
1664
- `${r.filesDeleted} deleted, ${r.conflictPaths.length} conflict(s)` +
1697
+ `${r.filesDeleted} deleted, ${r.filesExcludedByScope} scope-excluded, ` +
1698
+ `${r.conflictPaths.length} conflict(s)` +
1665
1699
  (r.aborted ? " — aborted" : ""),
1666
1700
  );
1667
1701
  }
@@ -1671,8 +1705,16 @@ async function runPushAll(
1671
1705
  const summary =
1672
1706
  `\nPushed ${result.filesUploaded} file(s) ` +
1673
1707
  `(${formatBytes(result.bytesUploaded)}) across ${result.attempted} ` +
1674
- `target(s); ${result.filesDeleted} deleted; ${errored} error(s)`;
1675
- console.log(errored > 0 ? chalk.yellow(summary) : chalk.green(summary));
1708
+ `target(s); ${result.filesDeleted} deleted; ${errored} error(s); ` +
1709
+ `${result.filesExcludedByScope} scope-excluded`;
1710
+ console.log(
1711
+ errored > 0 || result.filesExcludedByScope > 0
1712
+ ? chalk.yellow(summary)
1713
+ : chalk.green(summary),
1714
+ );
1715
+ if (result.filesExcludedByScope > 0) {
1716
+ console.log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)!));
1717
+ }
1676
1718
  if (errored > 0) process.exit(1);
1677
1719
  }
1678
1720
 
@@ -1747,12 +1789,19 @@ async function runNowSingle(
1747
1789
  ...(journalSlug !== undefined ? { journalSlug } : {}),
1748
1790
  ...(author ? { author } : {}),
1749
1791
  });
1792
+ const pushStatus =
1793
+ pushResult.aborted || pushResult.filesExcludedByScope > 0
1794
+ ? chalk.yellow("⚠")
1795
+ : chalk.green("✓");
1750
1796
  console.log(
1751
- ` ${pushResult.aborted ? chalk.yellow("⚠") : chalk.green("✓")} ` +
1797
+ ` ${pushStatus} ` +
1752
1798
  `${pushResult.filesUploaded} uploaded, ${pushResult.filesSkipped} skipped, ` +
1753
- `${pushResult.filesDeleted} deleted` +
1799
+ `${pushResult.filesDeleted} deleted, ${pushResult.filesExcludedByScope} scope-excluded` +
1754
1800
  (pushResult.aborted ? " — aborted" : ""),
1755
1801
  );
1802
+ if (pushResult.filesExcludedByScope > 0) {
1803
+ console.log(chalk.yellow(scopeExcludedWarning(pushResult.filesExcludedByScope)!));
1804
+ }
1756
1805
  if (pushResult.aborted) {
1757
1806
  console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
1758
1807
  process.exit(1);
@@ -65,6 +65,85 @@ function jsonRes(body: unknown, status = 200): Response {
65
65
  });
66
66
  }
67
67
 
68
+ describe("meetings get — short id resolution", () => {
69
+ it("resolves an 8-char short id to the full id before calling the by-id endpoint", async () => {
70
+ const fullId = "589bfd78-aaaa-bbbb-cccc-1234567890ab";
71
+ vi.mocked(vaultApiFetch)
72
+ .mockResolvedValueOnce(
73
+ jsonRes({
74
+ meetings: [
75
+ {
76
+ meetingId: fullId,
77
+ title: "T",
78
+ status: "ready",
79
+ startTime: "2026-01-01T00:00:00Z",
80
+ endTime: "2026-01-01T00:00:00Z",
81
+ duration: 0,
82
+ participantCount: 0,
83
+ hasTranscript: false,
84
+ hasNotes: false,
85
+ },
86
+ ],
87
+ }),
88
+ )
89
+ .mockResolvedValueOnce(
90
+ jsonRes({
91
+ meetingId: fullId,
92
+ title: "T",
93
+ startTime: "2026-01-01T00:00:00Z",
94
+ endTime: "2026-01-01T00:00:00Z",
95
+ duration: 0,
96
+ participants: [],
97
+ calendarEventId: null,
98
+ botProvider: "recall",
99
+ sourceApp: "calendar",
100
+ companyId: "cmp_acme",
101
+ status: "ready",
102
+ recallBotId: null,
103
+ isShared: false,
104
+ createdAt: "2026-01-01T00:00:00Z",
105
+ updatedAt: "2026-01-01T00:00:00Z",
106
+ documentUrl: "",
107
+ hasTranscript: false,
108
+ hasNotes: false,
109
+ }),
110
+ );
111
+
112
+ const program = buildProgram();
113
+ await program.parseAsync(["node", "hq", "meetings", "get", "589bfd78"]);
114
+
115
+ expect(vaultApiFetch).toHaveBeenNthCalledWith(1, {
116
+ token: "test-token",
117
+ path: "/v1/meetings",
118
+ query: { limit: "500" },
119
+ });
120
+ expect(vaultApiFetch).toHaveBeenNthCalledWith(2, {
121
+ token: "test-token",
122
+ path: `/v1/meetings/${fullId}`,
123
+ query: {},
124
+ });
125
+ });
126
+
127
+ it("errors and never sends the truncated id when the short id matches no meeting", async () => {
128
+ vi.spyOn(process, "exit").mockImplementation((() => {
129
+ throw new Error("process.exit");
130
+ }) as never);
131
+ vi.mocked(vaultApiFetch).mockResolvedValueOnce(jsonRes({ meetings: [] }));
132
+
133
+ const program = buildProgram();
134
+ await expect(
135
+ program.parseAsync(["node", "hq", "meetings", "get", "589bfd78"]),
136
+ ).rejects.toThrow("process.exit");
137
+
138
+ expect(errSpy).toHaveBeenCalledWith(
139
+ expect.stringContaining("No meeting matches ID \"589bfd78\""),
140
+ );
141
+ expect(vaultApiFetch).not.toHaveBeenCalledWith(
142
+ expect.objectContaining({ path: "/v1/meetings/589bfd78" }),
143
+ );
144
+ });
145
+ });
146
+
68
147
  describe("meetings set-company", () => {
69
148
  it("POSTs the resolved company id and applies to the recurring series by default", async () => {
70
149
  vi.mocked(vaultApiFetch).mockResolvedValueOnce(
@@ -105,8 +105,16 @@ async function resolveShortId(
105
105
  query: Record<string, string>,
106
106
  ): Promise<string> {
107
107
  if (prefix.includes("-") && prefix.length > 8) return prefix;
108
- const res = await vaultApiFetch({ token, path: "/v1/meetings", query });
109
- if (!res.ok) return prefix;
108
+ const res = await vaultApiFetch({
109
+ token,
110
+ path: "/v1/meetings",
111
+ query: { ...query, limit: "500" },
112
+ });
113
+ if (!res.ok) {
114
+ throw new Error(
115
+ `Could not resolve meeting ID "${prefix}" — failed to list meetings (${res.status}). Pass the full meeting id.`,
116
+ );
117
+ }
110
118
  const data = (await res.json()) as { meetings: MeetingListItem[] };
111
119
  const matches = data.meetings.filter((m) => m.meetingId.startsWith(prefix));
112
120
  if (matches.length === 1) return matches[0].meetingId;
@@ -114,7 +122,12 @@ async function resolveShortId(
114
122
  console.error(chalk.red(`Ambiguous ID prefix "${prefix}" — matches ${matches.length} meetings. Use a longer prefix.`));
115
123
  process.exit(1);
116
124
  }
117
- return prefix;
125
+ console.error(
126
+ chalk.red(
127
+ `No meeting matches ID "${prefix}". It may be older than the meetings shown by \`hq meetings list\`, still processing, or attributed to a different company. Pass the full meeting id, add --company <slug>, or widen the list with \`hq meetings list --limit <n>\`.`,
128
+ ),
129
+ );
130
+ process.exit(1);
118
131
  }
119
132
 
120
133
  async function handleApiError(res: Response): Promise<never> {