@indigoai-us/hq-cli 5.58.0 → 5.59.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.
@@ -6,25 +6,21 @@
6
6
  * hq people search <keyword> [--company <slug>] [--json]
7
7
  * hq people resolve <name> [--company <slug>] [--json]
8
8
  *
9
- * Source of truth is `companies/<company>/people/<person>/meta.yaml`. Every
10
- * subcommand operates on exactly ONE company (the active company, or the one
11
- * named by `--company`); nothing reads across company boundaries.
9
+ * Local records from `companies/<company>/people/<person>/meta.yaml` are the
10
+ * curated primary source. On misses, commands may fall back to the membership
11
+ * roster for exactly ONE company (the active company, or the one named by
12
+ * `--company`); nothing reads across company boundaries.
12
13
  */
13
14
 
14
15
  import * as fs from "fs";
15
16
  import { Command, Option } from "commander";
16
17
  import chalk from "chalk";
17
- import { VaultClient } from "@indigoai-us/hq-cloud";
18
18
  import * as yaml from "js-yaml";
19
19
  import { findHqRoot } from "../utils/manifest.js";
20
20
  import { manifestPath, type ManifestDoc } from "./cloud-provision.js";
21
- import {
22
- DEFAULT_COGNITO,
23
- buildVaultConfig,
24
- ensureCognitoToken,
25
- } from "../utils/cognito-session.js";
21
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
26
22
  import { getCompanyUid } from "../utils/vault-api.js";
27
- import { createCompanyPresignClient, runGet } from "./files-browse.js";
23
+ import { listActiveMembers, type ActiveMember } from "./members.js";
28
24
  import {
29
25
  assertSafeCompanySlug,
30
26
  listCompanyPeople,
@@ -39,13 +35,13 @@ interface PeopleScopeOpts {
39
35
  hqRoot?: string;
40
36
  }
41
37
 
42
- export type RefreshPeopleRoster = (
38
+ export type FetchPeopleRoster = (
43
39
  hqRoot: string,
44
40
  companySlug: string,
45
- ) => Promise<void>;
41
+ ) => Promise<PersonRecord[]>;
46
42
 
47
43
  interface PeopleCommandDeps {
48
- refreshRoster?: RefreshPeopleRoster;
44
+ fetchRoster?: FetchPeopleRoster;
49
45
  }
50
46
 
51
47
  interface PeopleLookupOpts {
@@ -53,23 +49,71 @@ interface PeopleLookupOpts {
53
49
  json?: boolean;
54
50
  }
55
51
 
56
- export async function refreshPeopleRosterFromCloud(
52
+ function slugify(value: string): string {
53
+ return value
54
+ .toLowerCase()
55
+ .replace(/[^a-z0-9]+/g, "-")
56
+ .replace(/-+/g, "-")
57
+ .replace(/^-|-$/g, "");
58
+ }
59
+
60
+ export function activeMemberToPersonRecord(
61
+ m: ActiveMember,
62
+ ): PersonRecord | null {
63
+ const name =
64
+ m.personName?.trim() ||
65
+ m.personEmail?.trim() ||
66
+ m.personSlug?.trim() ||
67
+ "";
68
+ if (!name) return null;
69
+
70
+ const email = m.personEmail?.trim() || undefined;
71
+ const slug = m.personSlug?.trim() || slugify(name) || m.personUid;
72
+
73
+ return {
74
+ slug,
75
+ name,
76
+ email,
77
+ role: m.role || undefined,
78
+ type: "internal",
79
+ source: `hq-pro membership: /membership/company/${m.companyUid}`,
80
+ };
81
+ }
82
+
83
+ export async function fetchMembershipRoster(
57
84
  hqRoot: string,
58
85
  companySlug: string,
59
- ): Promise<void> {
60
- const accessToken = await ensureCognitoToken();
61
- const client = new VaultClient(buildVaultConfig(accessToken));
62
- await getCompanyUid(accessToken, companySlug);
63
-
64
- await runGet({
65
- path: `companies/${companySlug}/people/`,
66
- hqRoot,
67
- companySlug,
68
- vaultClient: client,
69
- companyClient: ({ companyUid }) =>
70
- createCompanyPresignClient({ token: accessToken, companyUid }),
71
- region: DEFAULT_COGNITO.region,
72
- });
86
+ ): Promise<PersonRecord[]> {
87
+ void hqRoot;
88
+ const token = await ensureCognitoToken();
89
+ const companyUid = await getCompanyUid(token, companySlug);
90
+ const members = await listActiveMembers(token, companyUid);
91
+ return members
92
+ .map(activeMemberToPersonRecord)
93
+ .filter((r): r is PersonRecord => r !== null);
94
+ }
95
+
96
+ function personIdentityKey(person: PersonRecord): string {
97
+ return (
98
+ person.email?.toLowerCase() ||
99
+ person.slug?.toLowerCase() ||
100
+ person.name.toLowerCase()
101
+ );
102
+ }
103
+
104
+ export function mergePeople(
105
+ primary: PersonRecord[],
106
+ extra: PersonRecord[],
107
+ ): PersonRecord[] {
108
+ const seen = new Set(primary.map(personIdentityKey));
109
+ const merged = [...primary];
110
+ for (const person of extra) {
111
+ const key = personIdentityKey(person);
112
+ if (seen.has(key)) continue;
113
+ seen.add(key);
114
+ merged.push(person);
115
+ }
116
+ return merged;
73
117
  }
74
118
 
75
119
  /** Companies that still exist (anything not explicitly `status: archived`). */
@@ -159,26 +203,25 @@ function fail(message: string): never {
159
203
  process.exit(1);
160
204
  }
161
205
 
162
- function logRefreshFailure(companySlug: string, err: unknown): void {
206
+ function logRosterFetchFailure(companySlug: string, err: unknown): void {
163
207
  const message = err instanceof Error ? err.message : String(err);
164
208
  console.error(
165
209
  chalk.dim(
166
- ` Could not refresh people roster for '${companySlug}': ${message}`,
210
+ ` Could not fetch people roster for '${companySlug}': ${message}`,
167
211
  ),
168
212
  );
169
213
  }
170
214
 
171
- async function tryRefreshRoster(
172
- refreshRoster: RefreshPeopleRoster,
215
+ async function tryFetchRoster(
216
+ fetchRoster: FetchPeopleRoster,
173
217
  hqRoot: string,
174
218
  slug: string,
175
- ): Promise<boolean> {
219
+ ): Promise<PersonRecord[] | null> {
176
220
  try {
177
- await refreshRoster(hqRoot, slug);
178
- return true;
221
+ return await fetchRoster(hqRoot, slug);
179
222
  } catch (err) {
180
- logRefreshFailure(slug, err);
181
- return false;
223
+ logRosterFetchFailure(slug, err);
224
+ return null;
182
225
  }
183
226
  }
184
227
 
@@ -188,25 +231,20 @@ export async function resolvePersonWithRosterFallback(
188
231
  slug: string;
189
232
  name: string;
190
233
  opts?: PeopleLookupOpts;
191
- refreshRoster?: RefreshPeopleRoster;
234
+ fetchRoster?: FetchPeopleRoster;
192
235
  },
193
236
  ): Promise<ReturnType<typeof resolveNameToEmail>> {
194
- const local = resolveNameToEmail(
195
- listCompanyPeople(input.hqRoot, input.slug),
196
- input.name,
197
- );
237
+ const localPeople = listCompanyPeople(input.hqRoot, input.slug);
238
+ const local = resolveNameToEmail(localPeople, input.name);
198
239
  if (local.status !== "not_found" || input.opts?.localOnly) return local;
199
240
 
200
- const refreshed = await tryRefreshRoster(
201
- input.refreshRoster ?? refreshPeopleRosterFromCloud,
241
+ const roster = await tryFetchRoster(
242
+ input.fetchRoster ?? fetchMembershipRoster,
202
243
  input.hqRoot,
203
244
  input.slug,
204
245
  );
205
- if (!refreshed) return local;
206
- return resolveNameToEmail(
207
- listCompanyPeople(input.hqRoot, input.slug),
208
- input.name,
209
- );
246
+ if (!roster) return local;
247
+ return resolveNameToEmail(mergePeople(localPeople, roster), input.name);
210
248
  }
211
249
 
212
250
  export async function searchPeopleWithRosterFallback(input: {
@@ -214,21 +252,19 @@ export async function searchPeopleWithRosterFallback(input: {
214
252
  slug: string;
215
253
  keyword: string;
216
254
  opts?: PeopleLookupOpts;
217
- refreshRoster?: RefreshPeopleRoster;
255
+ fetchRoster?: FetchPeopleRoster;
218
256
  }): Promise<PersonRecord[]> {
219
- const local = searchPeople(
220
- listCompanyPeople(input.hqRoot, input.slug),
221
- input.keyword,
222
- );
257
+ const localPeople = listCompanyPeople(input.hqRoot, input.slug);
258
+ const local = searchPeople(localPeople, input.keyword);
223
259
  if (local.length > 0 || input.opts?.localOnly) return local;
224
260
 
225
- const refreshed = await tryRefreshRoster(
226
- input.refreshRoster ?? refreshPeopleRosterFromCloud,
261
+ const roster = await tryFetchRoster(
262
+ input.fetchRoster ?? fetchMembershipRoster,
227
263
  input.hqRoot,
228
264
  input.slug,
229
265
  );
230
- if (!refreshed) return local;
231
- return searchPeople(listCompanyPeople(input.hqRoot, input.slug), input.keyword);
266
+ if (!roster) return local;
267
+ return searchPeople(mergePeople(localPeople, roster), input.keyword);
232
268
  }
233
269
 
234
270
  export function registerPeopleCommand(
@@ -257,12 +293,26 @@ export function registerPeopleCommand(
257
293
  .command("list")
258
294
  .description("List all people recorded for the company")
259
295
  .option("--json", "Output JSON instead of a table")
260
- .action((opts: { json?: boolean }) => {
296
+ .option(
297
+ "--local-only",
298
+ "Skip network fallback; list only the local people roster",
299
+ )
300
+ .action(async (opts: PeopleLookupOpts) => {
261
301
  try {
262
302
  const scope = people.opts() as PeopleScopeOpts;
263
303
  const hqRoot = resolveHqRoot(scope);
264
304
  const slug = resolveCompanySlug(hqRoot, scope.company);
265
- const records = listCompanyPeople(hqRoot, slug);
305
+ let records = listCompanyPeople(hqRoot, slug);
306
+
307
+ if (!opts.localOnly) {
308
+ const roster = await tryFetchRoster(
309
+ deps.fetchRoster ?? fetchMembershipRoster,
310
+ hqRoot,
311
+ slug,
312
+ );
313
+ if (roster) records = mergePeople(records, roster);
314
+ }
315
+ records = records.sort((a, b) => a.name.localeCompare(b.name));
266
316
 
267
317
  if (opts.json) {
268
318
  console.log(JSON.stringify(records, null, 2));
@@ -288,7 +338,7 @@ export function registerPeopleCommand(
288
338
  .option("--json", "Output JSON instead of a table")
289
339
  .option(
290
340
  "--local-only",
291
- "Skip cloud fallback; search only the local people roster",
341
+ "Skip network fallback; search only the local people roster",
292
342
  )
293
343
  .action(async (keyword: string, opts: PeopleLookupOpts) => {
294
344
  try {
@@ -300,7 +350,7 @@ export function registerPeopleCommand(
300
350
  slug,
301
351
  keyword,
302
352
  opts,
303
- refreshRoster: deps.refreshRoster,
353
+ fetchRoster: deps.fetchRoster,
304
354
  });
305
355
 
306
356
  if (opts.json) {
@@ -323,7 +373,7 @@ export function registerPeopleCommand(
323
373
  .option("--json", "Output JSON instead of plain text")
324
374
  .option(
325
375
  "--local-only",
326
- "Skip cloud fallback; resolve only from the local people roster",
376
+ "Skip network fallback; resolve only from the local people roster",
327
377
  )
328
378
  .action(async (name: string, opts: PeopleLookupOpts) => {
329
379
  try {
@@ -335,7 +385,7 @@ export function registerPeopleCommand(
335
385
  slug,
336
386
  name,
337
387
  opts,
338
- refreshRoster: deps.refreshRoster,
388
+ fetchRoster: deps.fetchRoster,
339
389
  });
340
390
 
341
391
  if (opts.json) {
package/src/index.ts CHANGED
@@ -41,6 +41,7 @@ import { registerFilesBrowseCommands } from "./commands/files-browse.js";
41
41
  import { registerMembersCommand } from "./commands/members.js";
42
42
  import { registerPeopleCommand } from "./commands/people.js";
43
43
  import { registerDmCommand } from "./commands/dm.js";
44
+ import { registerChannelsCommand } from "./commands/channels.js";
44
45
  import { registerFeedbackCommand } from "./commands/feedback.js";
45
46
  import { registerMeetingsCommand } from "./commands/meetings.js";
46
47
  import { registerSourcesCommand } from "./commands/sources.js";
@@ -184,6 +185,7 @@ registerMembersCommand(program);
184
185
  // the local companies/<co>/people store scoped to one company.
185
186
  registerPeopleCommand(program);
186
187
  registerDmCommand(program);
188
+ registerChannelsCommand(program);
187
189
 
188
190
  // Onboarding (top-level — Cognito + vault-service provisioning)
189
191
  registerOnboardCommand(program);