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.
- package/README.md +491 -15
- package/package.json +1 -1
- package/src/adapters/codex.ts +1 -0
- package/src/adapters/types.ts +1 -0
- package/src/agents.ts +205 -0
- package/src/ai-state.ts +80 -0
- package/src/ai.ts +1763 -0
- package/src/audit/update-index.ts +13 -10
- package/src/autosync.ts +1028 -0
- package/src/builtin.ts +61 -0
- package/src/cli-context.ts +198 -0
- package/src/doctor.ts +128 -0
- package/src/enable-disable.ts +13 -7
- package/src/global-docs.ts +505 -0
- package/src/graph-query.ts +175 -0
- package/src/graph.ts +119 -0
- package/src/index-builder.ts +1104 -44
- package/src/index.ts +458 -24
- package/src/manage.ts +2482 -215
- package/src/paths.ts +181 -17
- package/src/query.ts +147 -7
- package/src/remote.ts +145 -10
- package/src/snippets.ts +106 -0
- package/src/trust-list.ts +1 -0
- package/src/trust.ts +13 -11
package/src/index-builder.ts
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
1
|
import { mkdir, readdir } from "node:fs/promises";
|
|
2
|
-
import { basename, join, relative } from "node:path";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
|
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
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
549
|
-
|
|
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({
|
|
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
|
}
|