@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.
@@ -0,0 +1,16 @@
1
+ import { Command } from "commander";
2
+ import { type ChannelSummary } from "./dm.js";
3
+ /**
4
+ * Human label for a channel row. Named channels (personal/company) show their
5
+ * name + a `hq dm <slug>` hint; unnamed group DMs are identified by their
6
+ * members (caller excluded), matching how HQ Sync renders them. Pure →
7
+ * unit-testable.
8
+ */
9
+ export declare function describeChannel(c: ChannelSummary): string;
10
+ /**
11
+ * Render the caller's channels for `hq channels`. Pure (takes the already
12
+ * fetched list) so the formatting is unit-testable without network/auth.
13
+ */
14
+ export declare function formatChannelsList(channels: ChannelSummary[]): string;
15
+ export declare function registerChannelsCommand(program: Command): void;
16
+ //# sourceMappingURL=channels.d.ts.map
@@ -0,0 +1,78 @@
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]="891a3265-c9ff-53d8-8a3c-2a964f5a74b6")}catch(e){}}();
3
+ import chalk from "chalk";
4
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
5
+ import { vaultApiFetch } from "../utils/vault-api.js";
6
+ import { channelSlug } from "./dm.js";
7
+ /**
8
+ * Human label for a channel row. Named channels (personal/company) show their
9
+ * name + a `hq dm <slug>` hint; unnamed group DMs are identified by their
10
+ * members (caller excluded), matching how HQ Sync renders them. Pure →
11
+ * unit-testable.
12
+ */
13
+ export function describeChannel(c) {
14
+ if (c.scope === "group") {
15
+ const names = (c.members ?? [])
16
+ .map((m) => m.displayName)
17
+ .filter((n) => !!n);
18
+ const who = names.length > 0
19
+ ? names.join(", ")
20
+ : `${c.memberCount ?? "?"}-person group`;
21
+ return `${who} ${chalk.dim("(group DM)")}`;
22
+ }
23
+ const slug = c.slug ?? (c.name ? channelSlug(c.name) : "");
24
+ const scopeTag = c.scope ? chalk.dim(`(${c.scope})`) : "";
25
+ const hint = slug ? chalk.dim(`— hq dm ${slug} "…"`) : "";
26
+ return `${chalk.bold(c.name ?? slug ?? c.channelId)} ${scopeTag} ${hint}`.trim();
27
+ }
28
+ /**
29
+ * Render the caller's channels for `hq channels`. Pure (takes the already
30
+ * fetched list) so the formatting is unit-testable without network/auth.
31
+ */
32
+ export function formatChannelsList(channels) {
33
+ if (channels.length === 0) {
34
+ return chalk.dim("No channels yet. Group DMs and named channels you're in will appear here.");
35
+ }
36
+ const lines = [
37
+ chalk.green(`${channels.length} channel${channels.length === 1 ? "" : "s"}:`),
38
+ ];
39
+ for (const c of channels) {
40
+ lines.push(` ${describeChannel(c)}`);
41
+ }
42
+ return lines.join("\n");
43
+ }
44
+ async function runChannelsList() {
45
+ try {
46
+ const token = await ensureCognitoToken();
47
+ const res = await vaultApiFetch({ token, path: "/v1/notify/channels" });
48
+ if (!res.ok) {
49
+ if (res.status === 401) {
50
+ console.error(chalk.red("Not authenticated — run `hq login` and try again."));
51
+ }
52
+ else {
53
+ const err = (await res.json().catch(() => ({})));
54
+ console.error(chalk.red(`Could not list channels: ${err.error ?? err.message ?? res.statusText}`));
55
+ }
56
+ process.exit(1);
57
+ }
58
+ const data = (await res.json());
59
+ console.log(formatChannelsList(data.channels ?? []));
60
+ }
61
+ catch (err) {
62
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
63
+ process.exit(1);
64
+ }
65
+ }
66
+ export function registerChannelsCommand(program) {
67
+ const channels = program
68
+ .command("channels")
69
+ .description("List the DM channels you're in (name them with `hq dm <name> \"…\"`).");
70
+ channels
71
+ .command("list", { isDefault: true })
72
+ .description("List your DM channels — named channels and group DMs.")
73
+ .action(async () => {
74
+ await runChannelsList();
75
+ });
76
+ }
77
+ //# sourceMappingURL=channels.js.map
78
+ //# debugId=891a3265-c9ff-53d8-8a3c-2a964f5a74b6
@@ -16,6 +16,47 @@ export declare function detectRecipient(recipient: string): DmRecipient | null;
16
16
  * (the normal 1:1 path). Pure → unit-testable.
17
17
  */
18
18
  export declare function parseGroupRecipients(recipient: string): string[] | null;
19
+ /**
20
+ * Normalize a channel name into a stable slug for name matching. MIRRORS the
21
+ * server's `channelSlug` (hq-pro src/vault-service/lib/channels.ts): lowercase,
22
+ * collapse runs of non-alphanumerics to single hyphens, trim leading/trailing
23
+ * hyphens. Keep in lockstep with the server so `hq dm vyg-dev` matches the
24
+ * channel the server stored as slug `vyg-dev`. Pure → unit-testable.
25
+ */
26
+ export declare function channelSlug(name: string): string;
27
+ /**
28
+ * Decide whether a positional recipient token addresses a DM CHANNEL by name
29
+ * (rather than a person/agent or a group). Returns the bare channel name to
30
+ * resolve, or null when the token is a person path (email / prs_ / agt_ uid) or
31
+ * a comma group. Pure → unit-testable.
32
+ *
33
+ * "#vyg-dev" → "vyg-dev" (explicit hash form)
34
+ * "vyg-dev" → "vyg-dev" (bare name — not an email/uid/group)
35
+ * "a@b.com" → null (person)
36
+ * "prs_…" → null (person) "agt_…" → null (agent)
37
+ * "a@x,b@y" → null (group DM)
38
+ */
39
+ export declare function parseChannelName(recipient: string): string | null;
40
+ /** Minimal channel shape the CLI reads back from GET /v1/notify/channels. */
41
+ export interface ChannelSummary {
42
+ channelId: string;
43
+ name?: string;
44
+ slug?: string;
45
+ scope?: string;
46
+ memberCount?: number;
47
+ members?: {
48
+ personUid: string;
49
+ displayName?: string;
50
+ }[];
51
+ }
52
+ /**
53
+ * Find the caller's channel(s) whose name matches `name`, by slug or
54
+ * case-insensitive display name. Group DMs are unnamed (participant-keyed), so
55
+ * they never match a name. Returns ALL matches so the caller can detect an
56
+ * ambiguous name (same slug across personal + company scope). Pure →
57
+ * unit-testable.
58
+ */
59
+ export declare function matchChannelsByName(channels: ChannelSummary[], name: string): ChannelSummary[];
19
60
  /**
20
61
  * Parse a relative duration like `30s`, `10m`, `2h`, `1d` into milliseconds.
21
62
  * Returns null on anything that doesn't match. Pure → unit-testable.
@@ -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]="f4bc9146-4033-5a73-a246-6f80a5e0f7ef")}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]="e95f345b-0e15-52b3-a851-fc97da935a99")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { ensureCognitoToken } from "../utils/cognito-session.js";
@@ -37,6 +37,72 @@ export function parseGroupRecipients(recipient) {
37
37
  .filter(Boolean);
38
38
  return [...new Set(parts)];
39
39
  }
40
+ /**
41
+ * Normalize a channel name into a stable slug for name matching. MIRRORS the
42
+ * server's `channelSlug` (hq-pro src/vault-service/lib/channels.ts): lowercase,
43
+ * collapse runs of non-alphanumerics to single hyphens, trim leading/trailing
44
+ * hyphens. Keep in lockstep with the server so `hq dm vyg-dev` matches the
45
+ * channel the server stored as slug `vyg-dev`. Pure → unit-testable.
46
+ */
47
+ export function channelSlug(name) {
48
+ return name
49
+ .trim()
50
+ .toLowerCase()
51
+ .replace(/[^a-z0-9]+/g, "-")
52
+ .replace(/^-+|-+$/g, "");
53
+ }
54
+ /**
55
+ * Decide whether a positional recipient token addresses a DM CHANNEL by name
56
+ * (rather than a person/agent or a group). Returns the bare channel name to
57
+ * resolve, or null when the token is a person path (email / prs_ / agt_ uid) or
58
+ * a comma group. Pure → unit-testable.
59
+ *
60
+ * "#vyg-dev" → "vyg-dev" (explicit hash form)
61
+ * "vyg-dev" → "vyg-dev" (bare name — not an email/uid/group)
62
+ * "a@b.com" → null (person)
63
+ * "prs_…" → null (person) "agt_…" → null (agent)
64
+ * "a@x,b@y" → null (group DM)
65
+ */
66
+ export function parseChannelName(recipient) {
67
+ const r = recipient.trim();
68
+ if (!r)
69
+ return null;
70
+ if (r.startsWith("#")) {
71
+ const name = r.slice(1).trim();
72
+ return name || null;
73
+ }
74
+ // A comma is the group-DM signal; emails and prs_/agt_ uids are person paths.
75
+ if (r.includes(","))
76
+ return null;
77
+ if (EMAIL_PATTERN.test(r))
78
+ return null;
79
+ if (RECIPIENT_UID_PATTERN.test(r))
80
+ return null;
81
+ return r;
82
+ }
83
+ /**
84
+ * Find the caller's channel(s) whose name matches `name`, by slug or
85
+ * case-insensitive display name. Group DMs are unnamed (participant-keyed), so
86
+ * they never match a name. Returns ALL matches so the caller can detect an
87
+ * ambiguous name (same slug across personal + company scope). Pure →
88
+ * unit-testable.
89
+ */
90
+ export function matchChannelsByName(channels, name) {
91
+ const targetSlug = channelSlug(name);
92
+ const targetName = name.trim().toLowerCase();
93
+ if (!targetSlug && !targetName)
94
+ return [];
95
+ return channels.filter((c) => {
96
+ if (c.scope === "group")
97
+ return false;
98
+ const slug = c.slug ?? (c.name ? channelSlug(c.name) : "");
99
+ if (slug && slug === targetSlug)
100
+ return true;
101
+ if (c.name && c.name.trim().toLowerCase() === targetName)
102
+ return true;
103
+ return false;
104
+ });
105
+ }
40
106
  /**
41
107
  * Parse a relative duration like `30s`, `10m`, `2h`, `1d` into milliseconds.
42
108
  * Returns null on anything that doesn't match. Pure → unit-testable.
@@ -266,13 +332,95 @@ async function runGroupSend(recipients, message) {
266
332
  process.exit(1);
267
333
  }
268
334
  }
335
+ /** Fetch the caller's channels (GET /v1/notify/channels). */
336
+ async function fetchChannels(token) {
337
+ const res = await vaultApiFetch({ token, path: "/v1/notify/channels" });
338
+ if (!res.ok) {
339
+ const err = (await res.json().catch(() => ({})));
340
+ throw new Error(friendlyDmError(res.status, err.code, err.error ?? err.message ?? res.statusText));
341
+ }
342
+ const data = (await res.json());
343
+ return data.channels ?? [];
344
+ }
345
+ /**
346
+ * Channel DM path: `hq dm vyg-dev "msg"`, `hq dm '#vyg-dev' "msg"`, or
347
+ * `hq dm --channel vyg-dev "msg"`. Resolves the caller's channel by name via
348
+ * GET /v1/notify/channels, then posts the message into it. Scheduling /
349
+ * prompt / details are 1:1-DM features and are rejected here rather than
350
+ * silently dropped.
351
+ */
352
+ async function runChannelSend(channelName, message, opts) {
353
+ try {
354
+ const body = (message ?? "").trim();
355
+ if (!body) {
356
+ console.error(chalk.red(`A message body is required: hq dm ${channelName} "<message>" (or hq dm --channel ${channelName} "<message>").`));
357
+ process.exit(1);
358
+ }
359
+ const unsupported = [
360
+ opts.prompt || opts.promptFile ? "--prompt/--prompt-file" : null,
361
+ opts.details || opts.detailsFile ? "--details/--details-file" : null,
362
+ opts.at ? "--at" : null,
363
+ opts.in ? "--in" : null,
364
+ ].filter(Boolean);
365
+ if (unsupported.length > 0) {
366
+ console.error(chalk.red(`${unsupported.join(", ")} ${unsupported.length === 1 ? "is" : "are"} only supported for 1:1 DMs, not channel messages.`));
367
+ process.exit(1);
368
+ }
369
+ const token = await ensureCognitoToken();
370
+ const channels = await fetchChannels(token);
371
+ const matches = matchChannelsByName(channels, channelName);
372
+ if (matches.length === 0) {
373
+ console.error(chalk.red(`No channel named '${channelName}' — run \`hq channels\` to see your channels.`));
374
+ process.exit(1);
375
+ }
376
+ if (matches.length > 1) {
377
+ const scopes = matches.map((m) => m.scope ?? "?").join(", ");
378
+ console.error(chalk.red(`'${channelName}' matches ${matches.length} channels (${scopes}) — this is ambiguous. Open the channel in HQ Sync to post, or rename one.`));
379
+ process.exit(1);
380
+ }
381
+ const channel = matches[0];
382
+ const sendRes = await vaultApiFetch({
383
+ token,
384
+ path: `/v1/notify/channels/${encodeURIComponent(channel.channelId)}/messages`,
385
+ method: "POST",
386
+ body: { body },
387
+ });
388
+ if (!sendRes.ok) {
389
+ const err = (await sendRes.json().catch(() => ({})));
390
+ console.error(chalk.red(friendlyDmError(sendRes.status, err.code, err.error ?? err.message ?? sendRes.statusText)));
391
+ process.exit(1);
392
+ }
393
+ console.log(chalk.green(`Message posted to #${channel.name ?? channelName}.`));
394
+ }
395
+ catch (err) {
396
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
397
+ process.exit(1);
398
+ }
399
+ }
269
400
  async function runDmSend(recipient, message, opts) {
401
+ // --channel <name> is an explicit channel target; the positional carries the
402
+ // message (recipient slot), e.g. `hq dm --channel vyg-dev "hello"`.
403
+ if (opts.channel !== undefined) {
404
+ await runChannelSend(opts.channel, message ?? recipient, opts);
405
+ return;
406
+ }
407
+ if (recipient === undefined) {
408
+ console.error(chalk.red('A recipient is required: hq dm <email|personUid|#channel> "<message>" (or --channel <name>).'));
409
+ process.exit(1);
410
+ return;
411
+ }
270
412
  // A comma in the recipient means a group DM — fan into the channel path.
271
413
  const group = parseGroupRecipients(recipient);
272
414
  if (group) {
273
415
  await runGroupSend(group, message);
274
416
  return;
275
417
  }
418
+ // A bare name or #hash addresses a named DM channel.
419
+ const channelName = parseChannelName(recipient);
420
+ if (channelName !== null) {
421
+ await runChannelSend(channelName, message, opts);
422
+ return;
423
+ }
276
424
  try {
277
425
  // Resolve prompt/details from inline text or a file.
278
426
  let prompt = opts.prompt;
@@ -354,14 +502,15 @@ export function registerDmCommand(program) {
354
502
  .command("dm")
355
503
  .description("Send a direct message and manage connection requests.");
356
504
  dm
357
- .command("send <recipient> [message]", { isDefault: true, hidden: true })
358
- .description('Send a direct message to someone (email, personUid, or agentUid). A person receives it as an HQ Sync notification; an agent receives it in its durable box inbox and replies by DM. If you aren\'t connected yet, it sends a connection request that holds your message. For a GROUP DM, pass a comma-separated recipient: hq dm send "a@x.com,b@y.com" "hi".')
359
- .option("--prompt <text>", "Agent-context prompt the recipient can one-click copy into their agent")
360
- .option("--prompt-file <path>", "Read the agent prompt from a file")
361
- .option("--details <text>", "Longer detail shown in the recipient's DM detail window")
362
- .option("--details-file <path>", "Read the details from a file")
363
- .option("--at <iso>", "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time)")
364
- .option("--in <duration>", "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d")
505
+ .command("send [recipient] [message]", { isDefault: true, hidden: true })
506
+ .description('Send a direct message. RECIPIENT can be a person (email, personUid, or agentUid), a GROUP DM (comma-separated: "a@x.com,b@y.com"), or one of your DM CHANNELS by name — bare (hq dm vyg-dev "hi"), hash form (hq dm "#vyg-dev" "hi"), or via --channel (hq dm --channel vyg-dev "hi"). A person receives a DM as an HQ Sync notification; an agent receives it in its durable box inbox. If you aren\'t connected yet, it sends a connection request that holds your message. See your channels with `hq channels`.')
507
+ .option("--channel <name>", "Post the message to one of your DM channels by name (e.g. --channel vyg-dev)")
508
+ .option("--prompt <text>", "Agent-context prompt the recipient can one-click copy into their agent (1:1 DMs only)")
509
+ .option("--prompt-file <path>", "Read the agent prompt from a file (1:1 DMs only)")
510
+ .option("--details <text>", "Longer detail shown in the recipient's DM detail window (1:1 DMs only)")
511
+ .option("--details-file <path>", "Read the details from a file (1:1 DMs only)")
512
+ .option("--at <iso>", "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time; 1:1 DMs only)")
513
+ .option("--in <duration>", "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d (1:1 DMs only)")
365
514
  .action(async (recipient, message, opts) => {
366
515
  await runDmSend(recipient, message, opts);
367
516
  });
@@ -391,4 +540,4 @@ export function registerDmCommand(program) {
391
540
  });
392
541
  }
393
542
  //# sourceMappingURL=dm.js.map
394
- //# debugId=f4bc9146-4033-5a73-a246-6f80a5e0f7ef
543
+ //# debugId=e95f345b-0e15-52b3-a851-fc97da935a99
@@ -6,21 +6,25 @@
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
  import { Command } from "commander";
15
+ import { type ActiveMember } from "./members.js";
14
16
  import { resolveNameToEmail, type PersonRecord } from "../utils/people.js";
15
- export type RefreshPeopleRoster = (hqRoot: string, companySlug: string) => Promise<void>;
17
+ export type FetchPeopleRoster = (hqRoot: string, companySlug: string) => Promise<PersonRecord[]>;
16
18
  interface PeopleCommandDeps {
17
- refreshRoster?: RefreshPeopleRoster;
19
+ fetchRoster?: FetchPeopleRoster;
18
20
  }
19
21
  interface PeopleLookupOpts {
20
22
  localOnly?: boolean;
21
23
  json?: boolean;
22
24
  }
23
- export declare function refreshPeopleRosterFromCloud(hqRoot: string, companySlug: string): Promise<void>;
25
+ export declare function activeMemberToPersonRecord(m: ActiveMember): PersonRecord | null;
26
+ export declare function fetchMembershipRoster(hqRoot: string, companySlug: string): Promise<PersonRecord[]>;
27
+ export declare function mergePeople(primary: PersonRecord[], extra: PersonRecord[]): PersonRecord[];
24
28
  /**
25
29
  * Resolve the single company to operate on. Explicit `--company` always wins
26
30
  * (after a path-safety check). Otherwise the active company is inferred from
@@ -33,14 +37,14 @@ export declare function resolvePersonWithRosterFallback(input: {
33
37
  slug: string;
34
38
  name: string;
35
39
  opts?: PeopleLookupOpts;
36
- refreshRoster?: RefreshPeopleRoster;
40
+ fetchRoster?: FetchPeopleRoster;
37
41
  }): Promise<ReturnType<typeof resolveNameToEmail>>;
38
42
  export declare function searchPeopleWithRosterFallback(input: {
39
43
  hqRoot: string;
40
44
  slug: string;
41
45
  keyword: string;
42
46
  opts?: PeopleLookupOpts;
43
- refreshRoster?: RefreshPeopleRoster;
47
+ fetchRoster?: FetchPeopleRoster;
44
48
  }): Promise<PersonRecord[]>;
45
49
  export declare function registerPeopleCommand(program: Command, deps?: PeopleCommandDeps): void;
46
50
  export {};
@@ -6,35 +6,73 @@
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
- !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]="79abc1f5-4f5a-5c95-b29b-10f04417355f")}catch(e){}}();
15
+ !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]="555055d9-98e2-5fcd-b736-4b1df12daf4d")}catch(e){}}();
15
16
  import * as fs from "fs";
16
17
  import { Option } from "commander";
17
18
  import chalk from "chalk";
18
- import { VaultClient } from "@indigoai-us/hq-cloud";
19
19
  import * as yaml from "js-yaml";
20
20
  import { findHqRoot } from "../utils/manifest.js";
21
21
  import { manifestPath } from "./cloud-provision.js";
22
- import { DEFAULT_COGNITO, buildVaultConfig, ensureCognitoToken, } from "../utils/cognito-session.js";
22
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
23
23
  import { getCompanyUid } from "../utils/vault-api.js";
24
- import { createCompanyPresignClient, runGet } from "./files-browse.js";
24
+ import { listActiveMembers } from "./members.js";
25
25
  import { assertSafeCompanySlug, listCompanyPeople, searchPeople, resolveNameToEmail, companyPeopleDir, } from "../utils/people.js";
26
- export async function refreshPeopleRosterFromCloud(hqRoot, companySlug) {
27
- const accessToken = await ensureCognitoToken();
28
- const client = new VaultClient(buildVaultConfig(accessToken));
29
- await getCompanyUid(accessToken, companySlug);
30
- await runGet({
31
- path: `companies/${companySlug}/people/`,
32
- hqRoot,
33
- companySlug,
34
- vaultClient: client,
35
- companyClient: ({ companyUid }) => createCompanyPresignClient({ token: accessToken, companyUid }),
36
- region: DEFAULT_COGNITO.region,
37
- });
26
+ function slugify(value) {
27
+ return value
28
+ .toLowerCase()
29
+ .replace(/[^a-z0-9]+/g, "-")
30
+ .replace(/-+/g, "-")
31
+ .replace(/^-|-$/g, "");
32
+ }
33
+ export function activeMemberToPersonRecord(m) {
34
+ const name = m.personName?.trim() ||
35
+ m.personEmail?.trim() ||
36
+ m.personSlug?.trim() ||
37
+ "";
38
+ if (!name)
39
+ return null;
40
+ const email = m.personEmail?.trim() || undefined;
41
+ const slug = m.personSlug?.trim() || slugify(name) || m.personUid;
42
+ return {
43
+ slug,
44
+ name,
45
+ email,
46
+ role: m.role || undefined,
47
+ type: "internal",
48
+ source: `hq-pro membership: /membership/company/${m.companyUid}`,
49
+ };
50
+ }
51
+ export async function fetchMembershipRoster(hqRoot, companySlug) {
52
+ void hqRoot;
53
+ const token = await ensureCognitoToken();
54
+ const companyUid = await getCompanyUid(token, companySlug);
55
+ const members = await listActiveMembers(token, companyUid);
56
+ return members
57
+ .map(activeMemberToPersonRecord)
58
+ .filter((r) => r !== null);
59
+ }
60
+ function personIdentityKey(person) {
61
+ return (person.email?.toLowerCase() ||
62
+ person.slug?.toLowerCase() ||
63
+ person.name.toLowerCase());
64
+ }
65
+ export function mergePeople(primary, extra) {
66
+ const seen = new Set(primary.map(personIdentityKey));
67
+ const merged = [...primary];
68
+ for (const person of extra) {
69
+ const key = personIdentityKey(person);
70
+ if (seen.has(key))
71
+ continue;
72
+ seen.add(key);
73
+ merged.push(person);
74
+ }
75
+ return merged;
38
76
  }
39
77
  /** Companies that still exist (anything not explicitly `status: archived`). */
40
78
  function activeCompanySlugs(manifest) {
@@ -103,37 +141,38 @@ function fail(message) {
103
141
  console.error(chalk.red(message));
104
142
  process.exit(1);
105
143
  }
106
- function logRefreshFailure(companySlug, err) {
144
+ function logRosterFetchFailure(companySlug, err) {
107
145
  const message = err instanceof Error ? err.message : String(err);
108
- console.error(chalk.dim(` Could not refresh people roster for '${companySlug}': ${message}`));
146
+ console.error(chalk.dim(` Could not fetch people roster for '${companySlug}': ${message}`));
109
147
  }
110
- async function tryRefreshRoster(refreshRoster, hqRoot, slug) {
148
+ async function tryFetchRoster(fetchRoster, hqRoot, slug) {
111
149
  try {
112
- await refreshRoster(hqRoot, slug);
113
- return true;
150
+ return await fetchRoster(hqRoot, slug);
114
151
  }
115
152
  catch (err) {
116
- logRefreshFailure(slug, err);
117
- return false;
153
+ logRosterFetchFailure(slug, err);
154
+ return null;
118
155
  }
119
156
  }
120
157
  export async function resolvePersonWithRosterFallback(input) {
121
- const local = resolveNameToEmail(listCompanyPeople(input.hqRoot, input.slug), input.name);
158
+ const localPeople = listCompanyPeople(input.hqRoot, input.slug);
159
+ const local = resolveNameToEmail(localPeople, input.name);
122
160
  if (local.status !== "not_found" || input.opts?.localOnly)
123
161
  return local;
124
- const refreshed = await tryRefreshRoster(input.refreshRoster ?? refreshPeopleRosterFromCloud, input.hqRoot, input.slug);
125
- if (!refreshed)
162
+ const roster = await tryFetchRoster(input.fetchRoster ?? fetchMembershipRoster, input.hqRoot, input.slug);
163
+ if (!roster)
126
164
  return local;
127
- return resolveNameToEmail(listCompanyPeople(input.hqRoot, input.slug), input.name);
165
+ return resolveNameToEmail(mergePeople(localPeople, roster), input.name);
128
166
  }
129
167
  export async function searchPeopleWithRosterFallback(input) {
130
- const local = searchPeople(listCompanyPeople(input.hqRoot, input.slug), input.keyword);
168
+ const localPeople = listCompanyPeople(input.hqRoot, input.slug);
169
+ const local = searchPeople(localPeople, input.keyword);
131
170
  if (local.length > 0 || input.opts?.localOnly)
132
171
  return local;
133
- const refreshed = await tryRefreshRoster(input.refreshRoster ?? refreshPeopleRosterFromCloud, input.hqRoot, input.slug);
134
- if (!refreshed)
172
+ const roster = await tryFetchRoster(input.fetchRoster ?? fetchMembershipRoster, input.hqRoot, input.slug);
173
+ if (!roster)
135
174
  return local;
136
- return searchPeople(listCompanyPeople(input.hqRoot, input.slug), input.keyword);
175
+ return searchPeople(mergePeople(localPeople, roster), input.keyword);
137
176
  }
138
177
  export function registerPeopleCommand(program, deps = {}) {
139
178
  const people = program
@@ -147,12 +186,19 @@ export function registerPeopleCommand(program, deps = {}) {
147
186
  .command("list")
148
187
  .description("List all people recorded for the company")
149
188
  .option("--json", "Output JSON instead of a table")
150
- .action((opts) => {
189
+ .option("--local-only", "Skip network fallback; list only the local people roster")
190
+ .action(async (opts) => {
151
191
  try {
152
192
  const scope = people.opts();
153
193
  const hqRoot = resolveHqRoot(scope);
154
194
  const slug = resolveCompanySlug(hqRoot, scope.company);
155
- const records = listCompanyPeople(hqRoot, slug);
195
+ let records = listCompanyPeople(hqRoot, slug);
196
+ if (!opts.localOnly) {
197
+ const roster = await tryFetchRoster(deps.fetchRoster ?? fetchMembershipRoster, hqRoot, slug);
198
+ if (roster)
199
+ records = mergePeople(records, roster);
200
+ }
201
+ records = records.sort((a, b) => a.name.localeCompare(b.name));
156
202
  if (opts.json) {
157
203
  console.log(JSON.stringify(records, null, 2));
158
204
  return;
@@ -171,7 +217,7 @@ export function registerPeopleCommand(program, deps = {}) {
171
217
  .command("search <keyword>")
172
218
  .description("Keyword search over people names and emails")
173
219
  .option("--json", "Output JSON instead of a table")
174
- .option("--local-only", "Skip cloud fallback; search only the local people roster")
220
+ .option("--local-only", "Skip network fallback; search only the local people roster")
175
221
  .action(async (keyword, opts) => {
176
222
  try {
177
223
  const scope = people.opts();
@@ -182,7 +228,7 @@ export function registerPeopleCommand(program, deps = {}) {
182
228
  slug,
183
229
  keyword,
184
230
  opts,
185
- refreshRoster: deps.refreshRoster,
231
+ fetchRoster: deps.fetchRoster,
186
232
  });
187
233
  if (opts.json) {
188
234
  console.log(JSON.stringify(matches, null, 2));
@@ -202,7 +248,7 @@ export function registerPeopleCommand(program, deps = {}) {
202
248
  .command("resolve <name>")
203
249
  .description("Resolve a person name to their email address")
204
250
  .option("--json", "Output JSON instead of plain text")
205
- .option("--local-only", "Skip cloud fallback; resolve only from the local people roster")
251
+ .option("--local-only", "Skip network fallback; resolve only from the local people roster")
206
252
  .action(async (name, opts) => {
207
253
  try {
208
254
  const scope = people.opts();
@@ -213,7 +259,7 @@ export function registerPeopleCommand(program, deps = {}) {
213
259
  slug,
214
260
  name,
215
261
  opts,
216
- refreshRoster: deps.refreshRoster,
262
+ fetchRoster: deps.fetchRoster,
217
263
  });
218
264
  if (opts.json) {
219
265
  console.log(JSON.stringify(result, null, 2));
@@ -247,4 +293,4 @@ export function registerPeopleCommand(program, deps = {}) {
247
293
  });
248
294
  }
249
295
  //# sourceMappingURL=people.js.map
250
- //# debugId=79abc1f5-4f5a-5c95-b29b-10f04417355f
296
+ //# debugId=555055d9-98e2-5fcd-b736-4b1df12daf4d
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // MUST be first: guard the Node version before any dependency that needs a
6
6
  // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
7
7
 
8
- !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]="6ad01242-c165-5be2-bb31-e929c0b38f5e")}catch(e){}}();
8
+ !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]="8839ee7d-9f6d-54fe-8f90-99ad858e349e")}catch(e){}}();
9
9
  import "./node-preflight.js";
10
10
  import { Command } from "commander";
11
11
  import { initSentry, Sentry } from "./sentry.js";
@@ -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";
@@ -152,6 +153,7 @@ registerMembersCommand(program);
152
153
  // the local companies/<co>/people store scoped to one company.
153
154
  registerPeopleCommand(program);
154
155
  registerDmCommand(program);
156
+ registerChannelsCommand(program);
155
157
  // Onboarding (top-level — Cognito + vault-service provisioning)
156
158
  registerOnboardCommand(program);
157
159
  // Feedback (subcommand group — hq feedback bug|feature)
@@ -242,4 +244,4 @@ registerCompanyCommand(program);
242
244
  }
243
245
  })();
244
246
  //# sourceMappingURL=index.js.map
245
- //# debugId=6ad01242-c165-5be2-bb31-e929c0b38f5e
247
+ //# debugId=8839ee7d-9f6d-54fe-8f90-99ad858e349e
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.58.0",
3
+ "version": "5.59.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {