facult 1.0.3 → 1.2.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.
@@ -1,8 +1,36 @@
1
1
  import { mkdir, readdir } from "node:fs/promises";
2
- import { basename, join, relative } from "node:path";
3
- import { facultRootDir } from "./paths";
2
+ import { basename, dirname, join, relative } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
5
+ import {
6
+ type AssetScope,
7
+ type AssetSourceKind,
8
+ extractExplicitReferences,
9
+ type FacultGraph,
10
+ type GraphEdge,
11
+ makeGraphNodeId,
12
+ snippetMarkerToSnippetRef,
13
+ } from "./graph";
14
+ import {
15
+ facultAiGraphPath,
16
+ facultAiIndexPath,
17
+ facultGeneratedStateDir,
18
+ facultRootDir,
19
+ projectRootFromAiRoot,
20
+ projectSlugFromAiRoot,
21
+ } from "./paths";
4
22
  import { lastModified } from "./util/skills";
5
23
 
24
+ interface AssetEntryBase {
25
+ sourceKind?: AssetSourceKind;
26
+ scope?: AssetScope;
27
+ canonicalRef?: string;
28
+ projectRoot?: string;
29
+ projectSlug?: string;
30
+ sourceRoot?: string;
31
+ shadow?: boolean;
32
+ }
33
+
6
34
  export interface SkillEntry {
7
35
  name: string;
8
36
  path: string;
@@ -34,15 +62,38 @@ export interface McpEntry {
34
62
  export interface AgentEntry {
35
63
  name: string;
36
64
  path: string;
65
+ description?: string;
37
66
  lastModifiedAt?: string;
38
67
  }
39
68
 
40
69
  export interface SnippetEntry {
41
70
  name: string;
42
71
  path: string;
72
+ description?: string;
73
+ tags?: string[];
74
+ lastModifiedAt?: string;
75
+ }
76
+
77
+ export interface InstructionEntry {
78
+ name: string;
79
+ path: string;
80
+ description: string;
81
+ tags: string[];
43
82
  lastModifiedAt?: string;
44
83
  }
45
84
 
85
+ interface ToolAssetEntry extends AssetEntryBase {
86
+ name: string;
87
+ path: string;
88
+ lastModifiedAt?: string;
89
+ }
90
+
91
+ export interface SkillEntry extends AssetEntryBase {}
92
+ export interface McpEntry extends AssetEntryBase {}
93
+ export interface AgentEntry extends AssetEntryBase {}
94
+ export interface SnippetEntry extends AssetEntryBase {}
95
+ export interface InstructionEntry extends AssetEntryBase {}
96
+
46
97
  export interface FacultIndex {
47
98
  version: number;
48
99
  updatedAt: string;
@@ -50,6 +101,41 @@ export interface FacultIndex {
50
101
  mcp: { servers: Record<string, McpEntry> };
51
102
  agents: Record<string, AgentEntry>;
52
103
  snippets: Record<string, SnippetEntry>;
104
+ instructions: Record<string, InstructionEntry>;
105
+ }
106
+
107
+ interface IndexedSource {
108
+ sourceKind: AssetSourceKind;
109
+ scope: AssetScope;
110
+ rootDir: string;
111
+ projectRoot?: string;
112
+ projectSlug?: string;
113
+ }
114
+
115
+ interface SourceAssets {
116
+ skills: Record<string, SkillEntry>;
117
+ mcpServers: Record<string, McpEntry>;
118
+ agents: Record<string, AgentEntry>;
119
+ snippets: Record<string, SnippetEntry>;
120
+ instructions: Record<string, InstructionEntry>;
121
+ toolConfigs: Record<string, ToolAssetEntry>;
122
+ toolRules: Record<string, ToolAssetEntry>;
123
+ }
124
+
125
+ interface ManagedToolStateLite {
126
+ tool: string;
127
+ agentsDir?: string;
128
+ toolHome?: string;
129
+ globalAgentsPath?: string;
130
+ globalAgentsOverridePath?: string;
131
+ mcpConfig?: string;
132
+ rulesDir?: string;
133
+ toolConfig?: string;
134
+ }
135
+
136
+ interface ManagedStateLite {
137
+ version?: number;
138
+ tools?: Record<string, ManagedToolStateLite>;
53
139
  }
54
140
 
55
141
  function isSafePathString(p: string): boolean {
@@ -120,6 +206,8 @@ function stripQuotes(s: string): string {
120
206
  const NEWLINE_RE = /\r?\n/;
121
207
  const FRONTMATTER_KEY_RE = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/;
122
208
  const FRONTMATTER_LIST_ITEM_RE = /^\s*-\s*(.+)$/;
209
+ const MARKDOWN_FILE_SUFFIX_RE = /\.md$/i;
210
+ const REFS_PREFIX_RE = /^refs\./;
123
211
 
124
212
  function normalizeTags(tags: string[]): string[] {
125
213
  return [...new Set(tags.map((t) => t.trim()).filter(Boolean))].sort();
@@ -275,7 +363,7 @@ function parseFrontmatter(md: string): {
275
363
  return { description, tags, body: block.body };
276
364
  }
277
365
 
278
- export function parseSkillMarkdown(md: string): {
366
+ function parseMarkdownAsset(md: string): {
279
367
  description: string;
280
368
  tags: string[];
281
369
  } {
@@ -287,6 +375,13 @@ export function parseSkillMarkdown(md: string): {
287
375
  return { description, tags: normalizeTags(fm.tags) };
288
376
  }
289
377
 
378
+ export function parseSkillMarkdown(md: string): {
379
+ description: string;
380
+ tags: string[];
381
+ } {
382
+ return parseMarkdownAsset(md);
383
+ }
384
+
290
385
  async function statIsoTime(p: string): Promise<string | undefined> {
291
386
  const lm = await lastModified(p);
292
387
  return lm ? lm.toISOString() : undefined;
@@ -297,6 +392,54 @@ async function readJsonSafe(p: string): Promise<unknown> {
297
392
  return JSON.parse(txt);
298
393
  }
299
394
 
395
+ function builtinAssetsRoot(): string {
396
+ const here = dirname(fileURLToPath(import.meta.url));
397
+ return join(here, "..", "assets", "packs", "facult-operating-model");
398
+ }
399
+
400
+ function canonicalRefForPath(
401
+ source: IndexedSource,
402
+ category:
403
+ | "skills"
404
+ | "agents"
405
+ | "snippets"
406
+ | "instructions"
407
+ | "mcp"
408
+ | "doc"
409
+ | "rendered",
410
+ filePath: string
411
+ ): string | undefined {
412
+ const rel = relative(source.rootDir, filePath).replace(/\\/g, "/");
413
+ if (!rel || rel.startsWith("..")) {
414
+ return undefined;
415
+ }
416
+ if (source.sourceKind === "global") {
417
+ return `@ai/${rel}`;
418
+ }
419
+ if (source.sourceKind === "project") {
420
+ return `@project/${rel}`;
421
+ }
422
+ if (source.sourceKind === "builtin") {
423
+ return `@builtin/facult-operating-model/${category}/${rel}`;
424
+ }
425
+ return undefined;
426
+ }
427
+
428
+ function entryScopeMeta(
429
+ source: IndexedSource
430
+ ): Pick<
431
+ AssetEntryBase,
432
+ "sourceKind" | "scope" | "projectRoot" | "projectSlug" | "sourceRoot"
433
+ > {
434
+ return {
435
+ sourceKind: source.sourceKind,
436
+ scope: source.scope,
437
+ projectRoot: source.projectRoot,
438
+ projectSlug: source.projectSlug,
439
+ sourceRoot: source.rootDir,
440
+ };
441
+ }
442
+
300
443
  async function listDirFiles(dir: string): Promise<string[]> {
301
444
  try {
302
445
  const ents = await readdir(dir, { withFileTypes: true });
@@ -325,6 +468,7 @@ async function listSubdirs(dir: string): Promise<string[]> {
325
468
 
326
469
  async function indexSkills(
327
470
  skillsDir: string,
471
+ source: IndexedSource,
328
472
  previous?: Record<string, unknown>
329
473
  ): Promise<Record<string, SkillEntry>> {
330
474
  const out: Record<string, SkillEntry> = {};
@@ -349,6 +493,7 @@ async function indexSkills(
349
493
  path: d,
350
494
  description,
351
495
  tags,
496
+ canonicalRef: canonicalRefForPath(source, "skills", d),
352
497
  lastModifiedAt: await statIsoTime(skillMd),
353
498
  enabledFor: meta.enabledFor,
354
499
  trusted: meta.trusted ?? false,
@@ -356,6 +501,7 @@ async function indexSkills(
356
501
  trustedBy: meta.trustedBy,
357
502
  auditStatus: meta.auditStatus ?? "pending",
358
503
  lastAuditAt: meta.lastAuditAt,
504
+ ...entryScopeMeta(source),
359
505
  };
360
506
  } catch {
361
507
  // Ignore missing/invalid skill entries.
@@ -366,6 +512,7 @@ async function indexSkills(
366
512
 
367
513
  async function indexMcpServers(
368
514
  mcpConfigPath: string,
515
+ source: IndexedSource,
369
516
  previous?: Record<string, unknown>
370
517
  ): Promise<Record<string, McpEntry>> {
371
518
  const out: Record<string, McpEntry> = {};
@@ -403,6 +550,7 @@ async function indexMcpServers(
403
550
  out[name] = {
404
551
  name,
405
552
  path: mcpConfigPath,
553
+ canonicalRef: canonicalRefForPath(source, "mcp", mcpConfigPath),
406
554
  lastModifiedAt: lm,
407
555
  definition: serversObj[name],
408
556
  enabledFor: meta.enabledFor,
@@ -411,6 +559,7 @@ async function indexMcpServers(
411
559
  trustedBy: meta.trustedBy,
412
560
  auditStatus: meta.auditStatus ?? "pending",
413
561
  lastAuditAt: meta.lastAuditAt,
562
+ ...entryScopeMeta(source),
414
563
  };
415
564
  }
416
565
  } catch {
@@ -421,23 +570,53 @@ async function indexMcpServers(
421
570
  }
422
571
 
423
572
  async function indexAgents(
424
- agentsDir: string
573
+ agentsDir: string,
574
+ source: IndexedSource
425
575
  ): Promise<Record<string, AgentEntry>> {
426
576
  const out: Record<string, AgentEntry> = {};
427
- const files = await listDirFiles(agentsDir);
577
+ const files: string[] = [];
578
+ const directFiles = await listDirFiles(agentsDir);
579
+ files.push(...directFiles);
580
+ for (const dir of await listSubdirs(agentsDir)) {
581
+ const candidate = join(dir, "agent.toml");
582
+ try {
583
+ const st = await Bun.file(candidate).stat();
584
+ if (st.isFile()) {
585
+ files.push(candidate);
586
+ }
587
+ } catch {
588
+ // Ignore missing nested manifests.
589
+ }
590
+ }
428
591
  for (const p of files) {
429
- const name = basename(p);
592
+ const name =
593
+ basename(p) === "agent.toml" ? basename(dirname(p)) : basename(p);
594
+ let description: string | undefined;
595
+ try {
596
+ const raw = await Bun.file(p).text();
597
+ const parsed = Bun.TOML.parse(raw) as Record<string, unknown>;
598
+ const parsedDescription = parsed.description;
599
+ if (typeof parsedDescription === "string" && parsedDescription.trim()) {
600
+ description = parsedDescription.trim();
601
+ }
602
+ } catch {
603
+ description = undefined;
604
+ }
430
605
  out[name] = {
431
606
  name,
432
607
  path: p,
608
+ description,
609
+ canonicalRef: canonicalRefForPath(source, "agents", p),
433
610
  lastModifiedAt: await statIsoTime(p),
611
+ ...entryScopeMeta(source),
434
612
  };
435
613
  }
436
614
  return out;
437
615
  }
438
616
 
439
617
  async function indexSnippets(
440
- snippetsDir: string
618
+ snippetsDir: string,
619
+ source: IndexedSource
441
620
  ): Promise<Record<string, SnippetEntry>> {
442
621
  const out: Record<string, SnippetEntry> = {};
443
622
  try {
@@ -459,33 +638,758 @@ async function indexSnippets(
459
638
  for (const p of files.sort()) {
460
639
  const rel = relative(snippetsDir, p);
461
640
  const name = rel || basename(p);
641
+ let description: string | undefined;
642
+ let tags: string[] | undefined;
643
+ try {
644
+ const raw = await Bun.file(p).text();
645
+ const parsed = parseMarkdownAsset(raw);
646
+ description = parsed.description || undefined;
647
+ tags = parsed.tags.length ? parsed.tags : undefined;
648
+ } catch {
649
+ description = undefined;
650
+ tags = undefined;
651
+ }
462
652
  out[name] = {
463
653
  name,
464
654
  path: p,
655
+ description,
656
+ tags,
657
+ canonicalRef: canonicalRefForPath(source, "snippets", p),
465
658
  lastModifiedAt: await statIsoTime(p),
659
+ ...entryScopeMeta(source),
466
660
  };
467
661
  }
468
662
  return out;
469
663
  }
470
664
 
471
- export async function buildIndex(opts?: {
472
- force?: boolean;
473
- /** Override the default canonical root dir (useful for tests). */
474
- rootDir?: string;
475
- }): Promise<{ index: FacultIndex; outputPath: string }> {
476
- const force = Boolean(opts?.force);
665
+ function instructionNameFromRelativePath(relPath: string): string {
666
+ return relPath.replace(MARKDOWN_FILE_SUFFIX_RE, "");
667
+ }
668
+
669
+ async function indexInstructions(
670
+ instructionsDir: string,
671
+ source: IndexedSource
672
+ ): Promise<Record<string, InstructionEntry>> {
673
+ const out: Record<string, InstructionEntry> = {};
674
+ try {
675
+ const st = await Bun.file(instructionsDir).stat();
676
+ if (!st.isDirectory()) {
677
+ return out;
678
+ }
679
+ } catch {
680
+ return out;
681
+ }
682
+
683
+ const glob = new Bun.Glob("**/*.md");
684
+ const files: string[] = [];
685
+ for await (const rel of glob.scan({
686
+ cwd: instructionsDir,
687
+ onlyFiles: true,
688
+ })) {
689
+ files.push(join(instructionsDir, rel));
690
+ }
691
+
692
+ for (const p of files.sort()) {
693
+ try {
694
+ const rel = relative(instructionsDir, p);
695
+ const raw = await Bun.file(p).text();
696
+ const parsed = parseMarkdownAsset(raw);
697
+ const name = instructionNameFromRelativePath(rel || basename(p));
698
+ out[name] = {
699
+ name,
700
+ path: p,
701
+ description: parsed.description,
702
+ tags: parsed.tags,
703
+ canonicalRef: canonicalRefForPath(source, "instructions", p),
704
+ lastModifiedAt: await statIsoTime(p),
705
+ ...entryScopeMeta(source),
706
+ };
707
+ } catch {
708
+ // Ignore unreadable instruction files.
709
+ }
710
+ }
477
711
 
478
- const rootDir = opts?.rootDir ?? facultRootDir();
479
- const skillsDir = join(rootDir, "skills");
480
- const agentsDir = join(rootDir, "agents");
481
- const snippetsDir = join(rootDir, "snippets");
482
- const serversJsonPath = join(rootDir, "mcp", "servers.json");
483
- const mcpJsonPath = join(rootDir, "mcp", "mcp.json");
712
+ return out;
713
+ }
714
+
715
+ async function indexToolAssets(
716
+ toolsDir: string,
717
+ source: IndexedSource
718
+ ): Promise<{
719
+ toolConfigs: Record<string, ToolAssetEntry>;
720
+ toolRules: Record<string, ToolAssetEntry>;
721
+ }> {
722
+ const toolConfigs: Record<string, ToolAssetEntry> = {};
723
+ const toolRules: Record<string, ToolAssetEntry> = {};
724
+ try {
725
+ const st = await Bun.file(toolsDir).stat();
726
+ if (!st.isDirectory()) {
727
+ return { toolConfigs, toolRules };
728
+ }
729
+ } catch {
730
+ return { toolConfigs, toolRules };
731
+ }
732
+
733
+ const configGlob = new Bun.Glob("*/config.toml");
734
+ for await (const rel of configGlob.scan({ cwd: toolsDir, onlyFiles: true })) {
735
+ const pathValue = join(toolsDir, rel);
736
+ const name = rel.replace(/\\/g, "/");
737
+ toolConfigs[name] = {
738
+ name,
739
+ path: pathValue,
740
+ canonicalRef: canonicalRefForPath(source, "rendered", pathValue),
741
+ lastModifiedAt: await statIsoTime(pathValue),
742
+ ...entryScopeMeta(source),
743
+ };
744
+ }
745
+
746
+ const ruleGlob = new Bun.Glob("*/rules/**/*.rules");
747
+ for await (const rel of ruleGlob.scan({ cwd: toolsDir, onlyFiles: true })) {
748
+ const pathValue = join(toolsDir, rel);
749
+ const name = rel.replace(/\\/g, "/");
750
+ toolRules[name] = {
751
+ name,
752
+ path: pathValue,
753
+ canonicalRef: canonicalRefForPath(source, "rendered", pathValue),
754
+ lastModifiedAt: await statIsoTime(pathValue),
755
+ ...entryScopeMeta(source),
756
+ };
757
+ }
758
+
759
+ return { toolConfigs, toolRules };
760
+ }
761
+
762
+ async function indexSourceAssets(
763
+ source: IndexedSource,
764
+ previousIndex?: Record<string, unknown> | null
765
+ ): Promise<SourceAssets> {
766
+ const skillsDir = join(source.rootDir, "skills");
767
+ const agentsDir = join(source.rootDir, "agents");
768
+ const snippetsDir = join(source.rootDir, "snippets");
769
+ const instructionsDir = join(source.rootDir, "instructions");
770
+ const toolsDir = join(source.rootDir, "tools");
771
+ const serversJsonPath = join(source.rootDir, "mcp", "servers.json");
772
+ const mcpJsonPath = join(source.rootDir, "mcp", "mcp.json");
484
773
  const canonicalMcpPath = (await Bun.file(serversJsonPath).exists())
485
774
  ? serversJsonPath
486
775
  : mcpJsonPath;
487
776
 
488
- const outputPath = join(rootDir, "index.json");
777
+ const prevSkills = isPlainObject(previousIndex?.skills)
778
+ ? (previousIndex?.skills as Record<string, unknown>)
779
+ : undefined;
780
+ const prevMcpMap =
781
+ isPlainObject(previousIndex?.mcp) &&
782
+ isPlainObject((previousIndex.mcp as Record<string, unknown>).servers)
783
+ ? ((previousIndex.mcp as Record<string, unknown>).servers as Record<
784
+ string,
785
+ unknown
786
+ >)
787
+ : undefined;
788
+
789
+ const [skills, mcpServers, agents, snippets, instructions, toolAssets] =
790
+ await Promise.all([
791
+ indexSkills(skillsDir, source, prevSkills),
792
+ indexMcpServers(canonicalMcpPath, source, prevMcpMap),
793
+ indexAgents(agentsDir, source),
794
+ indexSnippets(snippetsDir, source),
795
+ indexInstructions(instructionsDir, source),
796
+ indexToolAssets(toolsDir, source),
797
+ ]);
798
+
799
+ return {
800
+ skills,
801
+ mcpServers,
802
+ agents,
803
+ snippets,
804
+ instructions,
805
+ toolConfigs: toolAssets.toolConfigs,
806
+ toolRules: toolAssets.toolRules,
807
+ };
808
+ }
809
+
810
+ function mergeByName<T extends { name: string }>(
811
+ sources: Record<string, T>[]
812
+ ): Record<string, T> {
813
+ const merged: Record<string, T> = {};
814
+ for (const source of sources) {
815
+ for (const [name, entry] of Object.entries(source)) {
816
+ merged[name] = entry;
817
+ }
818
+ }
819
+ return merged;
820
+ }
821
+
822
+ function registerGraphEntries<
823
+ T extends AssetEntryBase & {
824
+ name: string;
825
+ path: string;
826
+ },
827
+ >(
828
+ graph: FacultGraph,
829
+ entries: Record<string, T>,
830
+ kind:
831
+ | "skill"
832
+ | "mcp"
833
+ | "agent"
834
+ | "snippet"
835
+ | "instruction"
836
+ | "doc"
837
+ | "tool-config"
838
+ | "tool-rule",
839
+ activeSelections?: Map<string, string>
840
+ ) {
841
+ for (const entry of Object.values(entries)) {
842
+ const sourceKind = entry.sourceKind ?? "global";
843
+ const scope = entry.scope ?? "global";
844
+ const activeIdentity = activeSelections?.get(
845
+ activeEntryKey(kind, entry.name)
846
+ );
847
+ const shadow =
848
+ entry.shadow ??
849
+ (activeIdentity
850
+ ? sourceIdentity({ sourceKind, scope }) !== activeIdentity
851
+ : false);
852
+ const id = makeGraphNodeId({
853
+ kind,
854
+ sourceKind,
855
+ scope,
856
+ name: entry.name,
857
+ });
858
+ graph.nodes[id] = {
859
+ id,
860
+ kind,
861
+ name: entry.name,
862
+ sourceKind,
863
+ scope,
864
+ path: entry.path,
865
+ canonicalRef: entry.canonicalRef,
866
+ projectRoot: entry.projectRoot,
867
+ projectSlug: entry.projectSlug,
868
+ shadow,
869
+ };
870
+ }
871
+ }
872
+
873
+ async function readTomlRefs(rootDir: string): Promise<Record<string, string>> {
874
+ const file = Bun.file(join(rootDir, "config.toml"));
875
+ if (!(await file.exists())) {
876
+ return {};
877
+ }
878
+ try {
879
+ const parsed = Bun.TOML.parse(await file.text()) as Record<string, unknown>;
880
+ const refs = parsed.refs;
881
+ if (!isPlainObject(refs)) {
882
+ return {};
883
+ }
884
+ return Object.fromEntries(
885
+ Object.entries(refs)
886
+ .filter(([, value]) => typeof value === "string")
887
+ .map(([key, value]) => [key, String(value)])
888
+ );
889
+ } catch {
890
+ return {};
891
+ }
892
+ }
893
+
894
+ function graphNodeIdByCanonicalRef(graph: FacultGraph): Record<string, string> {
895
+ const out: Record<string, string> = {};
896
+ for (const node of Object.values(graph.nodes)) {
897
+ if (node.canonicalRef) {
898
+ out[node.canonicalRef] = node.id;
899
+ }
900
+ }
901
+ return out;
902
+ }
903
+
904
+ function activeEntryKey(kind: string, name: string): string {
905
+ return `${kind}:${name}`;
906
+ }
907
+
908
+ function sourceIdentity(entry: {
909
+ sourceKind?: AssetSourceKind;
910
+ scope?: AssetScope;
911
+ }): string {
912
+ return `${entry.sourceKind ?? "global"}:${entry.scope ?? "global"}`;
913
+ }
914
+
915
+ function buildActiveEntryMap(
916
+ sourceIndexes: {
917
+ source: IndexedSource;
918
+ assets: SourceAssets;
919
+ docs: Record<string, AgentEntry>;
920
+ }[]
921
+ ): Map<string, string> {
922
+ const active = new Map<string, string>();
923
+ for (const sourceEntry of sourceIndexes) {
924
+ for (const [name, entry] of Object.entries(sourceEntry.assets.skills)) {
925
+ active.set(activeEntryKey("skill", name), sourceIdentity(entry));
926
+ }
927
+ for (const [name, entry] of Object.entries(sourceEntry.assets.mcpServers)) {
928
+ active.set(activeEntryKey("mcp", name), sourceIdentity(entry));
929
+ }
930
+ for (const [name, entry] of Object.entries(sourceEntry.assets.agents)) {
931
+ active.set(activeEntryKey("agent", name), sourceIdentity(entry));
932
+ }
933
+ for (const [name, entry] of Object.entries(sourceEntry.assets.snippets)) {
934
+ active.set(activeEntryKey("snippet", name), sourceIdentity(entry));
935
+ }
936
+ for (const [name, entry] of Object.entries(
937
+ sourceEntry.assets.instructions
938
+ )) {
939
+ active.set(activeEntryKey("instruction", name), sourceIdentity(entry));
940
+ }
941
+ for (const [name, entry] of Object.entries(
942
+ sourceEntry.assets.toolConfigs
943
+ )) {
944
+ active.set(activeEntryKey("tool-config", name), sourceIdentity(entry));
945
+ }
946
+ for (const [name, entry] of Object.entries(sourceEntry.assets.toolRules)) {
947
+ active.set(activeEntryKey("tool-rule", name), sourceIdentity(entry));
948
+ }
949
+ for (const [name, entry] of Object.entries(sourceEntry.docs)) {
950
+ active.set(activeEntryKey("doc", name), sourceIdentity(entry));
951
+ }
952
+ }
953
+ return active;
954
+ }
955
+
956
+ function addGraphEdge(
957
+ graph: FacultGraph,
958
+ edge: { from: string; to: string; kind: GraphEdge["kind"]; locator: string }
959
+ ) {
960
+ if (!(graph.nodes[edge.from] && graph.nodes[edge.to])) {
961
+ return;
962
+ }
963
+ if (
964
+ graph.edges.some(
965
+ (existing) =>
966
+ existing.from === edge.from &&
967
+ existing.to === edge.to &&
968
+ existing.kind === edge.kind &&
969
+ existing.locator === edge.locator
970
+ )
971
+ ) {
972
+ return;
973
+ }
974
+ graph.edges.push(edge);
975
+ }
976
+
977
+ function renderedTargetNodeName(
978
+ targetPath: string,
979
+ renderRoot: string
980
+ ): string {
981
+ const rel = relative(renderRoot, targetPath).replace(/\\/g, "/");
982
+ return rel || basename(targetPath);
983
+ }
984
+
985
+ async function readManagedState(
986
+ homeDir: string,
987
+ rootDir: string
988
+ ): Promise<ManagedStateLite | null> {
989
+ const statePath = join(
990
+ facultGeneratedStateDir({ home: homeDir, rootDir }),
991
+ "managed.json"
992
+ );
993
+ try {
994
+ const file = Bun.file(statePath);
995
+ if (!(await file.exists())) {
996
+ return null;
997
+ }
998
+ const parsed = JSON.parse(await file.text()) as ManagedStateLite;
999
+ return parsed && typeof parsed === "object" ? parsed : null;
1000
+ } catch {
1001
+ return null;
1002
+ }
1003
+ }
1004
+
1005
+ async function addReferenceEdgesForEntries<
1006
+ T extends AssetEntryBase & {
1007
+ name: string;
1008
+ path: string;
1009
+ },
1010
+ >(
1011
+ graph: FacultGraph,
1012
+ entries: Record<string, T>,
1013
+ kind:
1014
+ | "skill"
1015
+ | "agent"
1016
+ | "snippet"
1017
+ | "instruction"
1018
+ | "doc"
1019
+ | "tool-config"
1020
+ | "tool-rule",
1021
+ refsByRoot: Map<string, Record<string, string>>
1022
+ ) {
1023
+ const refsByCanonical = graphNodeIdByCanonicalRef(graph);
1024
+ for (const entry of Object.values(entries)) {
1025
+ const sourceKind = entry.sourceKind ?? "global";
1026
+ const scope = entry.scope ?? "global";
1027
+ const from = makeGraphNodeId({
1028
+ kind,
1029
+ sourceKind,
1030
+ scope,
1031
+ name: entry.name,
1032
+ });
1033
+ let raw = "";
1034
+ try {
1035
+ raw = await Bun.file(entry.path).text();
1036
+ } catch {
1037
+ continue;
1038
+ }
1039
+ const refs = extractExplicitReferences(raw);
1040
+ const refsConfig = refsByRoot.get(entry.sourceRoot ?? "") ?? {};
1041
+ for (const ref of refs) {
1042
+ if (ref.kind === "snippet_marker") {
1043
+ const targetName = snippetMarkerToSnippetRef(ref.value);
1044
+ const target = Object.values(graph.nodes).find(
1045
+ (node) =>
1046
+ node.kind === "snippet" &&
1047
+ (node.name === targetName ||
1048
+ node.name === `global/${targetName}` ||
1049
+ node.name.endsWith(`/${targetName}`))
1050
+ );
1051
+ if (target) {
1052
+ addGraphEdge(graph, {
1053
+ from,
1054
+ to: target.id,
1055
+ kind: "snippet_marker",
1056
+ locator: ref.value,
1057
+ });
1058
+ }
1059
+ continue;
1060
+ }
1061
+
1062
+ if (ref.kind === "ref_symbol") {
1063
+ const key = ref.value.replace(REFS_PREFIX_RE, "");
1064
+ const targetRef = refsConfig[key];
1065
+ const target = targetRef ? refsByCanonical[targetRef] : undefined;
1066
+ if (target) {
1067
+ addGraphEdge(graph, {
1068
+ from,
1069
+ to: target,
1070
+ kind: "ref_symbol",
1071
+ locator: ref.value,
1072
+ });
1073
+ }
1074
+ continue;
1075
+ }
1076
+
1077
+ const target = refsByCanonical[ref.value];
1078
+ if (target) {
1079
+ addGraphEdge(graph, {
1080
+ from,
1081
+ to: target,
1082
+ kind: ref.kind === "project_ref" ? "project_ref" : "canonical_ref",
1083
+ locator: ref.value,
1084
+ });
1085
+ }
1086
+ }
1087
+ }
1088
+ }
1089
+
1090
+ async function discoverDocs(
1091
+ source: IndexedSource
1092
+ ): Promise<Record<string, AgentEntry>> {
1093
+ const out: Record<string, AgentEntry> = {};
1094
+ const candidates = [
1095
+ join(source.rootDir, "AGENTS.global.md"),
1096
+ join(source.rootDir, "AGENTS.override.global.md"),
1097
+ ];
1098
+ for (const filePath of candidates) {
1099
+ try {
1100
+ const st = await Bun.file(filePath).stat();
1101
+ if (!st.isFile()) {
1102
+ continue;
1103
+ }
1104
+ const name = basename(filePath);
1105
+ out[name] = {
1106
+ name,
1107
+ path: filePath,
1108
+ description: undefined,
1109
+ canonicalRef: canonicalRefForPath(source, "doc", filePath),
1110
+ lastModifiedAt: await statIsoTime(filePath),
1111
+ ...entryScopeMeta(source),
1112
+ };
1113
+ } catch {
1114
+ // Ignore missing docs.
1115
+ }
1116
+ }
1117
+ return out;
1118
+ }
1119
+
1120
+ function registerRenderedTargetNode(args: {
1121
+ graph: FacultGraph;
1122
+ currentScope: IndexedSource;
1123
+ targetPath: string;
1124
+ targetType: string;
1125
+ sourceNodeId: string;
1126
+ sourceName: string;
1127
+ sourceKind: AssetSourceKind;
1128
+ sourceScope: AssetScope;
1129
+ renderRoot: string;
1130
+ targetTool: string;
1131
+ }) {
1132
+ const id = makeGraphNodeId({
1133
+ kind: "rendered-target",
1134
+ sourceKind: args.currentScope.sourceKind,
1135
+ scope: args.currentScope.scope,
1136
+ name: renderedTargetNodeName(args.targetPath, args.renderRoot),
1137
+ });
1138
+ args.graph.nodes[id] = {
1139
+ id,
1140
+ kind: "rendered-target",
1141
+ name: renderedTargetNodeName(args.targetPath, args.renderRoot),
1142
+ sourceKind: args.currentScope.sourceKind,
1143
+ scope: args.currentScope.scope,
1144
+ path: args.targetPath,
1145
+ projectRoot: args.currentScope.projectRoot,
1146
+ projectSlug: args.currentScope.projectSlug,
1147
+ shadow: true,
1148
+ meta: {
1149
+ targetTool: args.targetTool,
1150
+ targetType: args.targetType,
1151
+ sourceKind: args.sourceKind,
1152
+ sourceScope: args.sourceScope,
1153
+ sourceName: args.sourceName,
1154
+ },
1155
+ };
1156
+
1157
+ addGraphEdge(args.graph, {
1158
+ from: args.sourceNodeId,
1159
+ to: id,
1160
+ kind: "render_source",
1161
+ locator: args.targetPath,
1162
+ });
1163
+ }
1164
+
1165
+ function sourceNodeIdForEntry(args: {
1166
+ kind:
1167
+ | "skill"
1168
+ | "mcp"
1169
+ | "agent"
1170
+ | "snippet"
1171
+ | "instruction"
1172
+ | "doc"
1173
+ | "tool-config"
1174
+ | "tool-rule";
1175
+ entry: {
1176
+ name: string;
1177
+ sourceKind?: AssetSourceKind;
1178
+ scope?: AssetScope;
1179
+ };
1180
+ }): string {
1181
+ return makeGraphNodeId({
1182
+ kind: args.kind,
1183
+ sourceKind: args.entry.sourceKind ?? "global",
1184
+ scope: args.entry.scope ?? "global",
1185
+ name: args.entry.name,
1186
+ });
1187
+ }
1188
+
1189
+ function registerManagedRenderedTargets(args: {
1190
+ graph: FacultGraph;
1191
+ index: FacultIndex;
1192
+ sourceIndexes: {
1193
+ source: IndexedSource;
1194
+ assets: SourceAssets;
1195
+ docs: Record<string, AgentEntry>;
1196
+ }[];
1197
+ currentScope: IndexedSource;
1198
+ renderRoot: string;
1199
+ managedState: ManagedStateLite | null;
1200
+ }) {
1201
+ const mergedDocs = mergeByName(args.sourceIndexes.map((entry) => entry.docs));
1202
+ const mergedToolConfigs = mergeByName(
1203
+ args.sourceIndexes.map((entry) => entry.assets.toolConfigs)
1204
+ );
1205
+ const mergedToolRules = mergeByName(
1206
+ args.sourceIndexes.map((entry) => entry.assets.toolRules)
1207
+ );
1208
+ const toolStates = Object.values(args.managedState?.tools ?? {});
1209
+ if (!toolStates.length) {
1210
+ return;
1211
+ }
1212
+
1213
+ const nodes = args.graph.nodes;
1214
+ for (const toolState of toolStates) {
1215
+ if (toolState.agentsDir) {
1216
+ for (const entry of Object.values(args.index.agents)) {
1217
+ const sourceNodeId = sourceNodeIdForEntry({
1218
+ kind: "agent",
1219
+ entry,
1220
+ });
1221
+ if (!nodes[sourceNodeId]) {
1222
+ continue;
1223
+ }
1224
+ const targetPath = join(toolState.agentsDir, `${entry.name}.toml`);
1225
+ registerRenderedTargetNode({
1226
+ graph: args.graph,
1227
+ currentScope: args.currentScope,
1228
+ targetPath,
1229
+ targetType: "agent",
1230
+ sourceNodeId,
1231
+ sourceName: entry.name,
1232
+ sourceKind: entry.sourceKind ?? "global",
1233
+ sourceScope: entry.scope ?? "global",
1234
+ renderRoot: args.renderRoot,
1235
+ targetTool: toolState.tool,
1236
+ });
1237
+ }
1238
+ }
1239
+
1240
+ const globalDocTargets = [
1241
+ {
1242
+ name: "AGENTS.global.md",
1243
+ path: toolState.globalAgentsPath,
1244
+ },
1245
+ {
1246
+ name: "AGENTS.override.global.md",
1247
+ path: toolState.globalAgentsOverridePath,
1248
+ },
1249
+ ];
1250
+ for (const target of globalDocTargets) {
1251
+ if (!target.path) {
1252
+ continue;
1253
+ }
1254
+ const entry = mergedDocs[target.name];
1255
+ if (!entry) {
1256
+ continue;
1257
+ }
1258
+ const sourceNodeId = sourceNodeIdForEntry({
1259
+ kind: "doc",
1260
+ entry,
1261
+ });
1262
+ if (!nodes[sourceNodeId]) {
1263
+ continue;
1264
+ }
1265
+ registerRenderedTargetNode({
1266
+ graph: args.graph,
1267
+ currentScope: args.currentScope,
1268
+ targetPath: target.path,
1269
+ targetType: "doc",
1270
+ sourceNodeId,
1271
+ sourceName: entry.name,
1272
+ sourceKind: entry.sourceKind ?? "global",
1273
+ sourceScope: entry.scope ?? "global",
1274
+ renderRoot: args.renderRoot,
1275
+ targetTool: toolState.tool,
1276
+ });
1277
+ }
1278
+
1279
+ if (toolState.mcpConfig) {
1280
+ for (const entry of Object.values(args.index.mcp.servers)) {
1281
+ const sourceNodeId = sourceNodeIdForEntry({
1282
+ kind: "mcp",
1283
+ entry,
1284
+ });
1285
+ if (!nodes[sourceNodeId]) {
1286
+ continue;
1287
+ }
1288
+ registerRenderedTargetNode({
1289
+ graph: args.graph,
1290
+ currentScope: args.currentScope,
1291
+ targetPath: toolState.mcpConfig,
1292
+ targetType: "mcp",
1293
+ sourceNodeId,
1294
+ sourceName: entry.name,
1295
+ sourceKind: entry.sourceKind ?? "global",
1296
+ sourceScope: entry.scope ?? "global",
1297
+ renderRoot: args.renderRoot,
1298
+ targetTool: toolState.tool,
1299
+ });
1300
+ }
1301
+ }
1302
+
1303
+ if (toolState.toolConfig) {
1304
+ const entry = mergedToolConfigs[`${toolState.tool}/config.toml`];
1305
+ if (entry) {
1306
+ const sourceNodeId = sourceNodeIdForEntry({
1307
+ kind: "tool-config",
1308
+ entry,
1309
+ });
1310
+ if (nodes[sourceNodeId]) {
1311
+ registerRenderedTargetNode({
1312
+ graph: args.graph,
1313
+ currentScope: args.currentScope,
1314
+ targetPath: toolState.toolConfig,
1315
+ targetType: "tool-config",
1316
+ sourceNodeId,
1317
+ sourceName: entry.name,
1318
+ sourceKind: entry.sourceKind ?? "global",
1319
+ sourceScope: entry.scope ?? "global",
1320
+ renderRoot: args.renderRoot,
1321
+ targetTool: toolState.tool,
1322
+ });
1323
+ }
1324
+ }
1325
+ }
1326
+
1327
+ if (toolState.rulesDir) {
1328
+ for (const entry of Object.values(mergedToolRules)) {
1329
+ if (!entry.name.startsWith(`${toolState.tool}/rules/`)) {
1330
+ continue;
1331
+ }
1332
+ const relativeRulePath = entry.name.slice(
1333
+ `${toolState.tool}/rules/`.length
1334
+ );
1335
+ const sourceNodeId = sourceNodeIdForEntry({
1336
+ kind: "tool-rule",
1337
+ entry,
1338
+ });
1339
+ if (!nodes[sourceNodeId]) {
1340
+ continue;
1341
+ }
1342
+ registerRenderedTargetNode({
1343
+ graph: args.graph,
1344
+ currentScope: args.currentScope,
1345
+ targetPath: join(toolState.rulesDir, relativeRulePath),
1346
+ targetType: "tool-rule",
1347
+ sourceNodeId,
1348
+ sourceName: entry.name,
1349
+ sourceKind: entry.sourceKind ?? "global",
1350
+ sourceScope: entry.scope ?? "global",
1351
+ renderRoot: args.renderRoot,
1352
+ targetTool: toolState.tool,
1353
+ });
1354
+ }
1355
+ }
1356
+ }
1357
+ }
1358
+
1359
+ export async function buildIndex(opts?: {
1360
+ force?: boolean;
1361
+ /** Override the default canonical root dir (useful for tests). */
1362
+ rootDir?: string;
1363
+ /** Override home directory for generated state placement (useful for tests). */
1364
+ homeDir?: string;
1365
+ }): Promise<{
1366
+ index: FacultIndex;
1367
+ outputPath: string;
1368
+ graph: FacultGraph;
1369
+ graphPath: string;
1370
+ }> {
1371
+ const force = Boolean(opts?.force);
1372
+ const homeDir = opts?.homeDir ?? process.env.HOME ?? "";
1373
+ const rootDir =
1374
+ opts?.rootDir ?? (homeDir ? facultRootDir(homeDir) : facultRootDir());
1375
+ const outputPath = facultAiIndexPath(homeDir, rootDir);
1376
+ const graphPath = facultAiGraphPath(homeDir, rootDir);
1377
+ const projectRoot = projectRootFromAiRoot(rootDir, homeDir);
1378
+ const projectSlug = projectSlugFromAiRoot(rootDir, homeDir);
1379
+ const currentScope: IndexedSource = projectRoot
1380
+ ? {
1381
+ sourceKind: "project",
1382
+ scope: "project",
1383
+ rootDir,
1384
+ projectRoot,
1385
+ projectSlug: projectSlug ?? undefined,
1386
+ }
1387
+ : {
1388
+ sourceKind: "global",
1389
+ scope: "global",
1390
+ rootDir,
1391
+ };
1392
+ const managedState = await readManagedState(homeDir, rootDir);
489
1393
 
490
1394
  let previousIndex: Record<string, unknown> | null = null;
491
1395
  if (!force) {
@@ -510,24 +1414,52 @@ export async function buildIndex(opts?: {
510
1414
  }
511
1415
  }
512
1416
 
513
- const prevSkills = isPlainObject(previousIndex?.skills)
514
- ? (previousIndex?.skills as Record<string, unknown>)
515
- : undefined;
516
- const prevMcpMap =
517
- isPlainObject(previousIndex?.mcp) &&
518
- isPlainObject((previousIndex.mcp as Record<string, unknown>).servers)
519
- ? ((previousIndex.mcp as Record<string, unknown>).servers as Record<
520
- string,
521
- unknown
522
- >)
523
- : undefined;
1417
+ const globalRoot = facultRootDir(homeDir);
1418
+ const sources: IndexedSource[] = [];
1419
+ const builtinRoot = builtinAssetsRoot();
1420
+ try {
1421
+ const st = await Bun.file(builtinRoot).stat();
1422
+ if (st.isDirectory()) {
1423
+ sources.push({
1424
+ sourceKind: "builtin",
1425
+ scope: "global",
1426
+ rootDir: builtinRoot,
1427
+ });
1428
+ }
1429
+ } catch {
1430
+ // Ignore missing builtin asset packs in development.
1431
+ }
1432
+ sources.push({
1433
+ sourceKind: "global",
1434
+ scope: "global",
1435
+ rootDir: globalRoot,
1436
+ });
1437
+ if (projectRoot) {
1438
+ sources.push({
1439
+ ...currentScope,
1440
+ });
1441
+ }
524
1442
 
525
- const [skills, servers, agents, snippets] = await Promise.all([
526
- indexSkills(skillsDir, prevSkills),
527
- indexMcpServers(canonicalMcpPath, prevMcpMap),
528
- indexAgents(agentsDir),
529
- indexSnippets(snippetsDir),
530
- ]);
1443
+ const sourceIndexes = await Promise.all(
1444
+ sources.map(async (source) => ({
1445
+ source,
1446
+ assets: await indexSourceAssets(source, previousIndex),
1447
+ docs: await discoverDocs(source),
1448
+ refs: await readTomlRefs(source.rootDir),
1449
+ }))
1450
+ );
1451
+
1452
+ const skills = mergeByName(sourceIndexes.map((entry) => entry.assets.skills));
1453
+ const servers = mergeByName(
1454
+ sourceIndexes.map((entry) => entry.assets.mcpServers)
1455
+ );
1456
+ const agents = mergeByName(sourceIndexes.map((entry) => entry.assets.agents));
1457
+ const snippets = mergeByName(
1458
+ sourceIndexes.map((entry) => entry.assets.snippets)
1459
+ );
1460
+ const instructions = mergeByName(
1461
+ sourceIndexes.map((entry) => entry.assets.instructions)
1462
+ );
531
1463
 
532
1464
  const index: FacultIndex = {
533
1465
  version: 1,
@@ -536,27 +1468,155 @@ export async function buildIndex(opts?: {
536
1468
  mcp: { servers },
537
1469
  agents,
538
1470
  snippets,
1471
+ instructions,
1472
+ };
1473
+
1474
+ const graph: FacultGraph = {
1475
+ version: 1,
1476
+ generatedAt: new Date().toISOString(),
1477
+ nodes: {},
1478
+ edges: [],
539
1479
  };
540
1480
 
541
- await mkdir(rootDir, { recursive: true });
1481
+ const activeSelections = buildActiveEntryMap(sourceIndexes);
1482
+
1483
+ for (const sourceEntry of sourceIndexes) {
1484
+ registerGraphEntries(
1485
+ graph,
1486
+ sourceEntry.assets.skills,
1487
+ "skill",
1488
+ activeSelections
1489
+ );
1490
+ registerGraphEntries(
1491
+ graph,
1492
+ sourceEntry.assets.mcpServers,
1493
+ "mcp",
1494
+ activeSelections
1495
+ );
1496
+ registerGraphEntries(
1497
+ graph,
1498
+ sourceEntry.assets.agents,
1499
+ "agent",
1500
+ activeSelections
1501
+ );
1502
+ registerGraphEntries(
1503
+ graph,
1504
+ sourceEntry.assets.snippets,
1505
+ "snippet",
1506
+ activeSelections
1507
+ );
1508
+ registerGraphEntries(
1509
+ graph,
1510
+ sourceEntry.assets.instructions,
1511
+ "instruction",
1512
+ activeSelections
1513
+ );
1514
+ registerGraphEntries(
1515
+ graph,
1516
+ sourceEntry.assets.toolConfigs,
1517
+ "tool-config",
1518
+ activeSelections
1519
+ );
1520
+ registerGraphEntries(
1521
+ graph,
1522
+ sourceEntry.assets.toolRules,
1523
+ "tool-rule",
1524
+ activeSelections
1525
+ );
1526
+ registerGraphEntries(graph, sourceEntry.docs, "doc", activeSelections);
1527
+ }
1528
+
1529
+ registerManagedRenderedTargets({
1530
+ graph,
1531
+ index,
1532
+ sourceIndexes,
1533
+ currentScope,
1534
+ renderRoot: projectRoot ?? homeDir,
1535
+ managedState,
1536
+ });
1537
+
1538
+ const refsByRoot = new Map<string, Record<string, string>>();
1539
+ for (const sourceEntry of sourceIndexes) {
1540
+ refsByRoot.set(sourceEntry.source.rootDir, sourceEntry.refs);
1541
+ }
1542
+
1543
+ for (const sourceEntry of sourceIndexes) {
1544
+ await addReferenceEdgesForEntries(
1545
+ graph,
1546
+ sourceEntry.assets.skills,
1547
+ "skill",
1548
+ refsByRoot
1549
+ );
1550
+ await addReferenceEdgesForEntries(
1551
+ graph,
1552
+ sourceEntry.assets.agents,
1553
+ "agent",
1554
+ refsByRoot
1555
+ );
1556
+ await addReferenceEdgesForEntries(
1557
+ graph,
1558
+ sourceEntry.assets.snippets,
1559
+ "snippet",
1560
+ refsByRoot
1561
+ );
1562
+ await addReferenceEdgesForEntries(
1563
+ graph,
1564
+ sourceEntry.assets.instructions,
1565
+ "instruction",
1566
+ refsByRoot
1567
+ );
1568
+ await addReferenceEdgesForEntries(
1569
+ graph,
1570
+ sourceEntry.assets.toolConfigs,
1571
+ "tool-config",
1572
+ refsByRoot
1573
+ );
1574
+ await addReferenceEdgesForEntries(
1575
+ graph,
1576
+ sourceEntry.assets.toolRules,
1577
+ "tool-rule",
1578
+ refsByRoot
1579
+ );
1580
+ await addReferenceEdgesForEntries(
1581
+ graph,
1582
+ sourceEntry.docs,
1583
+ "doc",
1584
+ refsByRoot
1585
+ );
1586
+ }
1587
+
1588
+ await mkdir(dirname(outputPath), { recursive: true });
542
1589
  await Bun.write(outputPath, `${JSON.stringify(index, null, 2)}\n`);
1590
+ await Bun.write(graphPath, `${JSON.stringify(graph, null, 2)}\n`);
543
1591
 
544
- return { index, outputPath };
1592
+ return { index, outputPath, graph, graphPath };
545
1593
  }
546
1594
 
547
1595
  export async function indexCommand(argv: string[]) {
548
- if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
549
- console.log(`facult index — rebuild index.json under the canonical store
1596
+ const parsed = parseCliContextArgs(argv);
1597
+ if (
1598
+ parsed.argv.includes("--help") ||
1599
+ parsed.argv.includes("-h") ||
1600
+ parsed.argv[0] === "help"
1601
+ ) {
1602
+ console.log(`facult index — rebuild the generated index for the canonical store
550
1603
 
551
1604
  Usage:
552
- facult index [--force]
1605
+ facult index [--force] [--root PATH|--global|--project]
553
1606
 
554
1607
  Options:
555
1608
  --force Rebuild index from scratch (ignore existing metadata)
556
1609
  `);
557
1610
  return;
558
1611
  }
559
- const force = argv.includes("--force");
560
- const { outputPath } = await buildIndex({ force });
1612
+ const force = parsed.argv.includes("--force");
1613
+ const { outputPath } = await buildIndex({
1614
+ force,
1615
+ rootDir: resolveCliContextRoot({
1616
+ rootArg: parsed.rootArg,
1617
+ scope: parsed.scope,
1618
+ cwd: process.cwd(),
1619
+ }),
1620
+ });
561
1621
  console.log(`Index written to ${outputPath}`);
562
1622
  }