@indigoai-us/hq-cli 5.47.14 → 5.47.16

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.
@@ -238,6 +238,20 @@ export declare function computeArtifactHash(tarballBytes: Uint8Array): string;
238
238
  */
239
239
  export declare function verifyArtifact(input: VerifyArtifactInput): void;
240
240
  export declare function validateManifest(payloadDir: string, hqVersion: string | null): PackManifest;
241
+ /**
242
+ * Derive the safe, auto-generated "get started" line for a freshly installed
243
+ * pack from its `initialization.entrypoint` ONLY. PHASE 1 deliberately ignores
244
+ * the free-text `initialization.prompt` prose (rendering/moderation is a later
245
+ * story) so untrusted prose can't reach the operator's terminal.
246
+ *
247
+ * The command is slash-normalized to exactly one leading slash regardless of
248
+ * whether `entrypoint` was stored with or without one, matching the HQ Sync
249
+ * desktop render: ``Run `/email-assistant` to get started``.
250
+ *
251
+ * Returns `null` when there is no initialization block (backwards-compatible —
252
+ * the caller prints nothing extra).
253
+ */
254
+ export declare function getStartedLine(initialization?: PackManifest['initialization']): string | null;
241
255
  /**
242
256
  * Install the fetched payload to `<hqRoot>/core/packages/<pkg.name>/` (HQ
243
257
  * v12+ layout). The HQ template (`hq-core` / `hq-core-staging`) ships
@@ -34,7 +34,7 @@
34
34
  * from each pack's package.yaml; rationale lives in the layout-fix PR.)
35
35
  */
36
36
 
37
- !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]="cbb11285-034d-50c4-917d-797ab226db66")}catch(e){}}();
37
+ !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]="cb8b5583-b60f-52a5-802d-befc33f2a07b")}catch(e){}}();
38
38
  import * as fs from 'fs';
39
39
  import * as os from 'os';
40
40
  import * as path from 'path';
@@ -807,9 +807,75 @@ export function validateManifest(payloadDir, hqVersion) {
807
807
  throw new Error('capabilities must be a list of strings');
808
808
  }
809
809
  }
810
+ // initialization (US-004) — OPTIONAL and backwards-compatible. Absent → fine
811
+ // (legacy packs). Present → the `entrypoint` is REQUIRED and MUST resolve to a
812
+ // declared `contributes.skills` or `contributes.commands` entry (this is the
813
+ // content-pack system, so entries are named under `contributes.*` — NOT the
814
+ // registry `exposes.*` system). The post-install initialization prompt is
815
+ // rendered/moderated in a later story; here we only validate shape so a
816
+ // malformed block can't slip through to install.
817
+ if (m.initialization !== undefined) {
818
+ const init = m.initialization;
819
+ if (!init || typeof init !== 'object' || Array.isArray(init)) {
820
+ throw new Error('initialization must be a mapping with an entrypoint');
821
+ }
822
+ const initObj = init;
823
+ const entrypoint = initObj.entrypoint;
824
+ if (typeof entrypoint !== 'string' || entrypoint.trim() === '') {
825
+ throw new Error('initialization.entrypoint is required and must be a non-empty string');
826
+ }
827
+ // Resolve the entrypoint against declared skills/commands. Normalize a
828
+ // leading slash on BOTH sides so `/email-assistant` matches a contributes
829
+ // entry named `email-assistant` and vice-versa.
830
+ const stripSlash = (s) => (s.startsWith('/') ? s.slice(1) : s);
831
+ const target = stripSlash(entrypoint.trim());
832
+ const declared = [
833
+ ...(contributes.skills ?? []),
834
+ ...(contributes.commands ?? []),
835
+ ];
836
+ const resolves = declared.some((d) => stripSlash(d) === target);
837
+ if (!resolves) {
838
+ throw new Error(`initialization.entrypoint "${entrypoint}" does not resolve to a declared ` +
839
+ `contributes.skills or contributes.commands entry. ` +
840
+ `Valid entries: ${declared.length ? declared.join(', ') : '(none declared)'}`);
841
+ }
842
+ // initialization.prompt — OPTIONAL. When present it must be a string ≤ 2000
843
+ // chars. (Rendering/moderation is a later story; we only validate type/length.)
844
+ if (initObj.prompt !== undefined) {
845
+ if (typeof initObj.prompt !== 'string') {
846
+ throw new Error('initialization.prompt must be a string');
847
+ }
848
+ if (initObj.prompt.length > 2000) {
849
+ throw new Error(`initialization.prompt must be ≤ 2000 characters (got ${initObj.prompt.length})`);
850
+ }
851
+ }
852
+ }
810
853
  return m;
811
854
  }
812
855
  // ---------------------------------------------------------------------------
856
+ // Post-install get-started line (US-005)
857
+ // ---------------------------------------------------------------------------
858
+ /**
859
+ * Derive the safe, auto-generated "get started" line for a freshly installed
860
+ * pack from its `initialization.entrypoint` ONLY. PHASE 1 deliberately ignores
861
+ * the free-text `initialization.prompt` prose (rendering/moderation is a later
862
+ * story) so untrusted prose can't reach the operator's terminal.
863
+ *
864
+ * The command is slash-normalized to exactly one leading slash regardless of
865
+ * whether `entrypoint` was stored with or without one, matching the HQ Sync
866
+ * desktop render: ``Run `/email-assistant` to get started``.
867
+ *
868
+ * Returns `null` when there is no initialization block (backwards-compatible —
869
+ * the caller prints nothing extra).
870
+ */
871
+ export function getStartedLine(initialization) {
872
+ const entrypoint = initialization?.entrypoint;
873
+ if (typeof entrypoint !== 'string' || entrypoint.trim() === '')
874
+ return null;
875
+ const command = '/' + entrypoint.trim().replace(/^\/+/, '');
876
+ return `Run \`${command}\` to get started`;
877
+ }
878
+ // ---------------------------------------------------------------------------
813
879
  // Hooks confirmation
814
880
  // ---------------------------------------------------------------------------
815
881
  async function confirmHooks(pkg, allowHooks) {
@@ -1070,10 +1136,18 @@ export async function installPack(source, opts = {}) {
1070
1136
  say(chalk.green(`\nOK Installed ${pkg.name}@${pkg.version} -> ${path.relative(hqRoot, destDir)}/`));
1071
1137
  say(chalk.dim(` Wired ${Object.values(pkg.contributes).flat().filter(Boolean).length} ` +
1072
1138
  `contribution(s) into host-side paths.`));
1139
+ // US-005 — when the pack declares an `initialization` block, print a safe,
1140
+ // auto-generated "get started" line right after the success output. PHASE 1
1141
+ // derives the line from `initialization.entrypoint` ONLY (never the
1142
+ // free-text `initialization.prompt` prose), and matches the HQ Sync desktop
1143
+ // wording for consistency. Absent block → nothing extra (backwards-compat).
1144
+ const getStarted = getStartedLine(pkg.initialization);
1145
+ if (getStarted)
1146
+ say(chalk.cyan(getStarted));
1073
1147
  }
1074
1148
  finally {
1075
1149
  fs.rmSync(tmpDir, { recursive: true, force: true });
1076
1150
  }
1077
1151
  }
1078
1152
  //# sourceMappingURL=pack-install.js.map
1079
- //# debugId=cbb11285-034d-50c4-917d-797ab226db66
1153
+ //# debugId=cb8b5583-b60f-52a5-802d-befc33f2a07b
@@ -18,5 +18,37 @@
18
18
  * Spec: knowledge/public/hq-core/package-yaml-spec.md.
19
19
  */
20
20
  import { Command } from 'commander';
21
+ import { type InstalledPack, type LinkStatus } from '../utils/pack-contributions.js';
22
+ import type { PackContributeKey } from '../types.js';
23
+ interface InstalledPackView {
24
+ name: string;
25
+ version?: string;
26
+ publisher?: string;
27
+ source?: string;
28
+ transport: string | null;
29
+ requiresHqCore?: string;
30
+ hqCoreSatisfied: boolean | null;
31
+ contributes: Partial<Record<PackContributeKey, number>>;
32
+ links: Record<LinkStatus, number>;
33
+ brokenLinks: Array<{
34
+ key: PackContributeKey;
35
+ item: string;
36
+ dst: string;
37
+ }>;
38
+ inCatalog: boolean;
39
+ updateAvailable: boolean | null;
40
+ /**
41
+ * Post-install initialization (US-005). Present only when the pack's
42
+ * package.yaml declares an `initialization` block — drives the HQ Sync
43
+ * "Installed" panel get-started affordance. Absent → omitted (no null noise).
44
+ */
45
+ initialization?: {
46
+ entrypoint: string;
47
+ prompt?: string;
48
+ };
49
+ error?: string;
50
+ }
51
+ export declare function buildInstalledView(hqRoot: string, hqVersion: string | null, pack: InstalledPack, installedSources: Set<string>, checkUpdates: boolean): InstalledPackView;
21
52
  export declare function registerPacksCommand(parent: Command): void;
53
+ export {};
22
54
  //# sourceMappingURL=packs.d.ts.map
@@ -18,7 +18,7 @@
18
18
  * Spec: knowledge/public/hq-core/package-yaml-spec.md.
19
19
  */
20
20
 
21
- !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]="fa6cf3bf-90fa-5261-a15f-c8810e4b5615")}catch(e){}}();
21
+ !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]="527d0e7d-2101-5438-8251-2579a40453e6")}catch(e){}}();
22
22
  import * as fs from 'fs';
23
23
  import * as path from 'path';
24
24
  import * as readline from 'readline';
@@ -61,7 +61,7 @@ async function confirm(question) {
61
61
  });
62
62
  return /^(y|yes)$/i.test(answer.trim());
63
63
  }
64
- function buildInstalledView(hqRoot, hqVersion, pack, installedSources, checkUpdates) {
64
+ export function buildInstalledView(hqRoot, hqVersion, pack, installedSources, checkUpdates) {
65
65
  if (!pack.manifest) {
66
66
  return {
67
67
  name: pack.name,
@@ -97,6 +97,22 @@ function buildInstalledView(hqRoot, hqVersion, pack, installedSources, checkUpda
97
97
  if (checkUpdates && m.source) {
98
98
  updateAvailable = resolveLatest(m.source, m.version).updateAvailable;
99
99
  }
100
+ // US-005 — surface the pack's `initialization` block so the HQ Sync
101
+ // "Installed" panel can render its get-started affordance. `readPackManifest`
102
+ // already parses the full package.yaml, so `m.initialization` is available;
103
+ // we still shape it defensively (tolerate a malformed/absent block) and omit
104
+ // the field entirely when absent so the JSON carries no null noise.
105
+ let initialization;
106
+ const rawInit = m.initialization;
107
+ if (rawInit && typeof rawInit === 'object' && !Array.isArray(rawInit)) {
108
+ const initObj = rawInit;
109
+ const entrypoint = initObj.entrypoint;
110
+ if (typeof entrypoint === 'string' && entrypoint.trim() !== '') {
111
+ initialization = { entrypoint };
112
+ if (typeof initObj.prompt === 'string')
113
+ initialization.prompt = initObj.prompt;
114
+ }
115
+ }
100
116
  return {
101
117
  name: m.name ?? pack.name,
102
118
  version: m.version,
@@ -110,6 +126,7 @@ function buildInstalledView(hqRoot, hqVersion, pack, installedSources, checkUpda
110
126
  brokenLinks,
111
127
  inCatalog: m.source ? installedSources.has(m.source) : false,
112
128
  updateAvailable,
129
+ ...(initialization ? { initialization } : {}),
113
130
  };
114
131
  }
115
132
  function buildListView(hqRoot, checkUpdates, evalConditionals) {
@@ -408,4 +425,4 @@ export function registerPacksCommand(parent) {
408
425
  });
409
426
  }
410
427
  //# sourceMappingURL=packs.js.map
411
- //# debugId=fa6cf3bf-90fa-5261-a15f-c8810e4b5615
428
+ //# debugId=527d0e7d-2101-5438-8251-2579a40453e6
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `hq people` — read a company's people (membership) records from the local HQ
3
+ * tree and search/resolve over them.
4
+ *
5
+ * hq people list [--company <slug>] [--json]
6
+ * hq people search <keyword> [--company <slug>] [--json]
7
+ * hq people resolve <name> [--company <slug>] [--json]
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.
12
+ */
13
+ import { Command } from "commander";
14
+ /**
15
+ * Resolve the single company to operate on. Explicit `--company` always wins
16
+ * (after a path-safety check). Otherwise the active company is inferred from
17
+ * `companies/manifest.yaml`: if exactly one company exists it's used; if several
18
+ * do, the caller must disambiguate with `--company`.
19
+ */
20
+ export declare function resolveCompanySlug(hqRoot: string, explicit: string | undefined): string;
21
+ export declare function registerPeopleCommand(program: Command): void;
22
+ //# sourceMappingURL=people.d.ts.map
@@ -0,0 +1,187 @@
1
+ /**
2
+ * `hq people` — read a company's people (membership) records from the local HQ
3
+ * tree and search/resolve over them.
4
+ *
5
+ * hq people list [--company <slug>] [--json]
6
+ * hq people search <keyword> [--company <slug>] [--json]
7
+ * hq people resolve <name> [--company <slug>] [--json]
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.
12
+ */
13
+
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]="076b7f64-6626-5733-8992-ab9361a3e31d")}catch(e){}}();
15
+ import * as fs from "fs";
16
+ import { Option } from "commander";
17
+ import chalk from "chalk";
18
+ import * as yaml from "js-yaml";
19
+ import { findHqRoot } from "../utils/manifest.js";
20
+ import { manifestPath } from "./cloud-provision.js";
21
+ import { assertSafeCompanySlug, listCompanyPeople, searchPeople, resolveNameToEmail, companyPeopleDir, } from "../utils/people.js";
22
+ /** Companies that still exist (anything not explicitly `status: archived`). */
23
+ function activeCompanySlugs(manifest) {
24
+ const companies = manifest.companies ?? {};
25
+ return Object.entries(companies)
26
+ .filter(([, entry]) => (entry?.status ?? "active") !== "archived")
27
+ .map(([slug]) => slug)
28
+ .sort();
29
+ }
30
+ /**
31
+ * Resolve the single company to operate on. Explicit `--company` always wins
32
+ * (after a path-safety check). Otherwise the active company is inferred from
33
+ * `companies/manifest.yaml`: if exactly one company exists it's used; if several
34
+ * do, the caller must disambiguate with `--company`.
35
+ */
36
+ export function resolveCompanySlug(hqRoot, explicit) {
37
+ if (explicit) {
38
+ assertSafeCompanySlug(explicit);
39
+ return explicit;
40
+ }
41
+ const mPath = manifestPath(hqRoot);
42
+ if (!fs.existsSync(mPath)) {
43
+ throw new Error("Could not determine the active company — companies/manifest.yaml not found. " +
44
+ "Re-run with --company <slug>.");
45
+ }
46
+ let manifest;
47
+ try {
48
+ manifest = yaml.load(fs.readFileSync(mPath, "utf-8"));
49
+ }
50
+ catch (err) {
51
+ throw new Error(`companies/manifest.yaml is malformed: ${err instanceof Error ? err.message : String(err)}`);
52
+ }
53
+ const slugs = activeCompanySlugs(manifest ?? {});
54
+ if (slugs.length === 1)
55
+ return slugs[0];
56
+ if (slugs.length === 0) {
57
+ throw new Error("No companies found in companies/manifest.yaml — re-run with --company <slug>.");
58
+ }
59
+ throw new Error("Multiple companies found — re-run with --company <slug> to pick one:\n" +
60
+ slugs.map((s) => ` --company ${s}`).join("\n"));
61
+ }
62
+ /** Resolve the HQ root: explicit override (tests) → cwd walk-up. */
63
+ function resolveHqRoot(opts) {
64
+ return opts.hqRoot ?? findHqRoot();
65
+ }
66
+ function printPeopleTable(people) {
67
+ const nameW = Math.max(4, ...people.map((p) => p.name.length));
68
+ const emailW = Math.max(5, ...people.map((p) => (p.email ?? "—").length));
69
+ const roleW = Math.max(4, ...people.map((p) => (p.role ?? "—").length));
70
+ console.log(chalk.bold([
71
+ "NAME".padEnd(nameW),
72
+ "EMAIL".padEnd(emailW),
73
+ "ROLE".padEnd(roleW),
74
+ "TYPE",
75
+ ].join(" ")));
76
+ for (const p of people) {
77
+ console.log([
78
+ p.name.padEnd(nameW),
79
+ (p.email ?? "—").padEnd(emailW),
80
+ (p.role ?? "—").padEnd(roleW),
81
+ p.type ?? "—",
82
+ ].join(" "));
83
+ }
84
+ }
85
+ function fail(message) {
86
+ console.error(chalk.red(message));
87
+ process.exit(1);
88
+ }
89
+ export function registerPeopleCommand(program) {
90
+ const people = program
91
+ .command("people")
92
+ .description("List, search, and resolve a company's people (from companies/<co>/people)")
93
+ .option("--company <slug>", "Company slug to scope to (defaults to the active company)")
94
+ // Hidden escape hatch for tests / non-standard layouts — point the reader at
95
+ // an explicit HQ tree root instead of walking up from cwd.
96
+ .addOption(new Option("--hq-root <path>", "Override the HQ tree root (advanced)").hideHelp());
97
+ people
98
+ .command("list")
99
+ .description("List all people recorded for the company")
100
+ .option("--json", "Output JSON instead of a table")
101
+ .action((opts) => {
102
+ try {
103
+ const scope = people.opts();
104
+ const hqRoot = resolveHqRoot(scope);
105
+ const slug = resolveCompanySlug(hqRoot, scope.company);
106
+ const records = listCompanyPeople(hqRoot, slug);
107
+ if (opts.json) {
108
+ console.log(JSON.stringify(records, null, 2));
109
+ return;
110
+ }
111
+ if (records.length === 0) {
112
+ console.log(chalk.gray(`No people recorded for '${slug}' (looked in ${companyPeopleDir(hqRoot, slug)}).`));
113
+ return;
114
+ }
115
+ printPeopleTable(records);
116
+ }
117
+ catch (err) {
118
+ fail(err instanceof Error ? err.message : String(err));
119
+ }
120
+ });
121
+ people
122
+ .command("search <keyword>")
123
+ .description("Keyword search over people names and emails")
124
+ .option("--json", "Output JSON instead of a table")
125
+ .action((keyword, opts) => {
126
+ try {
127
+ const scope = people.opts();
128
+ const hqRoot = resolveHqRoot(scope);
129
+ const slug = resolveCompanySlug(hqRoot, scope.company);
130
+ const matches = searchPeople(listCompanyPeople(hqRoot, slug), keyword);
131
+ if (opts.json) {
132
+ console.log(JSON.stringify(matches, null, 2));
133
+ return;
134
+ }
135
+ if (matches.length === 0) {
136
+ console.log(chalk.gray(`No people in '${slug}' match "${keyword}".`));
137
+ return;
138
+ }
139
+ printPeopleTable(matches);
140
+ }
141
+ catch (err) {
142
+ fail(err instanceof Error ? err.message : String(err));
143
+ }
144
+ });
145
+ people
146
+ .command("resolve <name>")
147
+ .description("Resolve a person name to their email address")
148
+ .option("--json", "Output JSON instead of plain text")
149
+ .action((name, opts) => {
150
+ try {
151
+ const scope = people.opts();
152
+ const hqRoot = resolveHqRoot(scope);
153
+ const slug = resolveCompanySlug(hqRoot, scope.company);
154
+ const result = resolveNameToEmail(listCompanyPeople(hqRoot, slug), name);
155
+ if (opts.json) {
156
+ console.log(JSON.stringify(result, null, 2));
157
+ if (result.status === "found")
158
+ return;
159
+ process.exit(1);
160
+ }
161
+ switch (result.status) {
162
+ case "found":
163
+ // Bare email on stdout so callers can capture it directly.
164
+ console.log(result.email);
165
+ return;
166
+ case "no_email":
167
+ fail(`Found '${result.person.name}' in '${slug}' but no email is recorded for them.`);
168
+ break;
169
+ case "ambiguous":
170
+ console.error(chalk.yellow(`"${name}" matches ${result.matches.length} people in '${slug}' — be more specific:`));
171
+ for (const m of result.matches) {
172
+ console.error(` ${m.name}${m.email ? ` <${m.email}>` : ""}`);
173
+ }
174
+ process.exit(1);
175
+ break;
176
+ case "not_found":
177
+ fail(`No person matching "${name}" found in '${slug}'.`);
178
+ break;
179
+ }
180
+ }
181
+ catch (err) {
182
+ fail(err instanceof Error ? err.message : String(err));
183
+ }
184
+ });
185
+ }
186
+ //# sourceMappingURL=people.js.map
187
+ //# debugId=076b7f64-6626-5733-8992-ab9361a3e31d
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
5
 
6
- !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]="964b8c71-056c-51fb-893a-d51f8f6ad5d9")}catch(e){}}();
6
+ !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]="aeb143c7-7e19-5999-b6cb-790fc79034cc")}catch(e){}}();
7
7
  import { Command } from "commander";
8
8
  import { initSentry, Sentry } from "./sentry.js";
9
9
  import { registerAddCommand } from "./commands/add.js";
@@ -36,6 +36,7 @@ import { registerGroupGrantsCommand } from "./commands/group-grants.js";
36
36
  import { registerFilesCommand } from "./commands/files.js";
37
37
  import { registerFilesBrowseCommands } from "./commands/files-browse.js";
38
38
  import { registerMembersCommand } from "./commands/members.js";
39
+ import { registerPeopleCommand } from "./commands/people.js";
39
40
  import { registerDmCommand } from "./commands/dm.js";
40
41
  import { registerFeedbackCommand } from "./commands/feedback.js";
41
42
  import { registerMeetingsCommand } from "./commands/meetings.js";
@@ -134,6 +135,9 @@ const filesCmd = registerFilesCommand(program);
134
135
  registerFilesBrowseCommands(filesCmd);
135
136
  // Membership management (subcommand group — hq members invite|list|revoke)
136
137
  registerMembersCommand(program);
138
+ // People directory (subcommand group — hq people list|search|resolve), reading
139
+ // the local companies/<co>/people store scoped to one company.
140
+ registerPeopleCommand(program);
137
141
  registerDmCommand(program);
138
142
  // Onboarding (top-level — Cognito + vault-service provisioning)
139
143
  registerOnboardCommand(program);
@@ -180,4 +184,4 @@ registerRescueCommand(program);
180
184
  }
181
185
  })();
182
186
  //# sourceMappingURL=index.js.map
183
- //# debugId=964b8c71-056c-51fb-893a-d51f8f6ad5d9
187
+ //# debugId=aeb143c7-7e19-5999-b6cb-790fc79034cc
package/dist/types.d.ts CHANGED
@@ -93,5 +93,15 @@ export interface PackManifest {
93
93
  * yet enforced.
94
94
  */
95
95
  capabilities?: string[];
96
+ /**
97
+ * Post-install initialization (US-004/US-005). Optional — absent on legacy
98
+ * packs. `entrypoint` names a declared `contributes.skills`/`commands` entry
99
+ * (slash-normalized); `prompt` is optional free-text prose (PHASE 1 does NOT
100
+ * render the prose — only an auto-generated get-started line from entrypoint).
101
+ */
102
+ initialization?: {
103
+ entrypoint: string;
104
+ prompt?: string;
105
+ };
96
106
  }
97
107
  //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Company people (membership) directory reader + search.
3
+ *
4
+ * The local source of truth for "who belongs to a company" is the per-company
5
+ * people store on disk:
6
+ *
7
+ * companies/<company-slug>/people/<person-slug>/meta.yaml
8
+ *
9
+ * Each `meta.yaml` carries at least `name` and `type` ("internal" | "external")
10
+ * plus optional `email`, `role`, `organization`, `tags`, etc. (see
11
+ * `companies/_template/people/_example/meta.yaml` for the canonical schema).
12
+ *
13
+ * Everything here is scoped to ONE company directory. Callers resolve a single
14
+ * company slug up front; nothing in this module ever enumerates or reads across
15
+ * company boundaries — HQ tenancy rules forbid cross-company member lookups.
16
+ */
17
+ /** A single person/member record parsed from a `people/<slug>/meta.yaml`. */
18
+ export interface PersonRecord {
19
+ /** Folder slug under `companies/<company>/people/<slug>/`. */
20
+ slug: string;
21
+ /** Display name (required field in `meta.yaml`). */
22
+ name: string;
23
+ /** Contact email, when recorded. */
24
+ email?: string;
25
+ /** "internal" (team member) | "external" (client, vendor, …). */
26
+ type?: string;
27
+ /** Role/title inside (or relative to) the company. */
28
+ role?: string;
29
+ /** External-only: the person's own organization. */
30
+ organization?: string;
31
+ /** Freeform tags for filtering. */
32
+ tags?: string[];
33
+ /** Absolute path to the `meta.yaml` this record was parsed from. */
34
+ source: string;
35
+ }
36
+ export declare function assertSafeCompanySlug(slug: string): void;
37
+ /** Absolute path to a company's `people/` directory. */
38
+ export declare function companyPeopleDir(hqRoot: string, companySlug: string): string;
39
+ /**
40
+ * Parse a single `meta.yaml` body into a `PersonRecord`. Returns `null` when the
41
+ * file is empty, unparseable, or has no usable `name` — a person record without
42
+ * a name can't be listed, searched, or resolved, so it's skipped rather than
43
+ * surfaced as a half-row. Exported for unit testing.
44
+ */
45
+ export declare function parsePersonMeta(raw: string, slug: string, source: string): PersonRecord | null;
46
+ /**
47
+ * List every person recorded for ONE company. Reads
48
+ * `companies/<companySlug>/people/<personSlug>/meta.yaml` for each person
49
+ * folder.
50
+ *
51
+ * - Folders whose name starts with `_` are skipped (e.g. the `_example`
52
+ * template that ships in `companies/_template/people/`).
53
+ * - Folders without a `meta.yaml`, or whose `meta.yaml` has no `name`, are
54
+ * skipped silently — they aren't members yet.
55
+ * - Returns `[]` when the company has no `people/` directory at all.
56
+ *
57
+ * Results are sorted by name (case-insensitive) for stable output.
58
+ */
59
+ export declare function listCompanyPeople(hqRoot: string, companySlug: string): PersonRecord[];
60
+ /**
61
+ * Case-insensitive keyword search over a person's NAME and EMAIL (plus the
62
+ * folder slug, which is a normalized alias of the name). Pure — operates on an
63
+ * already-loaded list so it's trivially testable and reusable.
64
+ *
65
+ * An empty/whitespace keyword matches nothing (callers should treat that as a
66
+ * usage error rather than "return everyone").
67
+ */
68
+ export declare function searchPeople(people: PersonRecord[], keyword: string): PersonRecord[];
69
+ /** Outcome of resolving a name to an email address. */
70
+ export type ResolveResult = {
71
+ status: "found";
72
+ email: string;
73
+ person: PersonRecord;
74
+ } | {
75
+ status: "no_email";
76
+ person: PersonRecord;
77
+ } | {
78
+ status: "ambiguous";
79
+ matches: PersonRecord[];
80
+ } | {
81
+ status: "not_found";
82
+ };
83
+ /**
84
+ * Resolve a person NAME to their email, built on top of {@link searchPeople}.
85
+ *
86
+ * Match precedence (narrowest first) so a precise query isn't drowned out by
87
+ * looser substring hits:
88
+ * 1. exact name match (case-insensitive, trimmed)
89
+ * 2. exact folder-slug match
90
+ * 3. substring search over name/email/slug
91
+ *
92
+ * The first tier that yields any match decides the result:
93
+ * - exactly one match with an email → `found`
94
+ * - exactly one match, no email → `no_email`
95
+ * - more than one match → `ambiguous` (caller disambiguates)
96
+ * - no match in any tier → `not_found`
97
+ */
98
+ export declare function resolveNameToEmail(people: PersonRecord[], name: string): ResolveResult;
99
+ //# sourceMappingURL=people.d.ts.map