facult 2.7.1 → 2.7.2
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/package.json +1 -1
- package/src/ai-state.ts +131 -1
- package/src/index-builder.ts +79 -8
package/package.json
CHANGED
package/src/ai-state.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import { copyFile, mkdir, readdir, stat } from "node:fs/promises";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { buildIndex } from "./index-builder";
|
|
4
5
|
import {
|
|
5
6
|
facultAiGraphPath,
|
|
6
7
|
facultAiIndexPath,
|
|
7
8
|
legacyFacultStateDirForRoot,
|
|
9
|
+
preferredGlobalAiRoot,
|
|
10
|
+
projectRootFromAiRoot,
|
|
8
11
|
} from "./paths";
|
|
9
12
|
|
|
10
13
|
async function fileExists(path: string): Promise<boolean> {
|
|
@@ -15,6 +18,99 @@ async function fileExists(path: string): Promise<boolean> {
|
|
|
15
18
|
}
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
async function newestPathMtime(path: string): Promise<number> {
|
|
22
|
+
try {
|
|
23
|
+
const st = await stat(path);
|
|
24
|
+
if (st.isFile()) {
|
|
25
|
+
return st.mtimeMs;
|
|
26
|
+
}
|
|
27
|
+
if (!st.isDirectory()) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
let newest = st.mtimeMs;
|
|
31
|
+
let entries: Dirent<string>[] = [];
|
|
32
|
+
try {
|
|
33
|
+
entries = await readdir(path, { withFileTypes: true, encoding: "utf8" });
|
|
34
|
+
} catch {
|
|
35
|
+
return newest;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const child = join(path, entry.name);
|
|
40
|
+
if (entry.isFile()) {
|
|
41
|
+
try {
|
|
42
|
+
const childStat = await stat(child);
|
|
43
|
+
newest = Math.max(newest, childStat.mtimeMs);
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore unreadable children
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
newest = Math.max(newest, await newestPathMtime(child));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return newest;
|
|
54
|
+
} catch {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function watchedPathMtime(path: string): Promise<number> {
|
|
60
|
+
const newest = await newestPathMtime(path);
|
|
61
|
+
if (newest > 0) {
|
|
62
|
+
return newest;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return (await stat(dirname(path))).mtimeMs;
|
|
66
|
+
} catch {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function canonicalAssetsNewerThanIndex(args: {
|
|
72
|
+
homeDir: string;
|
|
73
|
+
rootDir: string;
|
|
74
|
+
indexPath: string;
|
|
75
|
+
}): Promise<boolean> {
|
|
76
|
+
let indexMtimeMs = 0;
|
|
77
|
+
try {
|
|
78
|
+
indexMtimeMs = (await stat(args.indexPath)).mtimeMs;
|
|
79
|
+
} catch {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const watchedRelPaths = [
|
|
84
|
+
"AGENTS.global.md",
|
|
85
|
+
"AGENTS.override.global.md",
|
|
86
|
+
"agents",
|
|
87
|
+
"config.toml",
|
|
88
|
+
"instructions",
|
|
89
|
+
"mcp",
|
|
90
|
+
"skills",
|
|
91
|
+
"snippets",
|
|
92
|
+
"tools",
|
|
93
|
+
];
|
|
94
|
+
const watchedRoots = [args.rootDir];
|
|
95
|
+
|
|
96
|
+
if (projectRootFromAiRoot(args.rootDir, args.homeDir)) {
|
|
97
|
+
const globalRoot = preferredGlobalAiRoot(args.homeDir);
|
|
98
|
+
if (globalRoot !== args.rootDir) {
|
|
99
|
+
watchedRoots.push(globalRoot);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const root of watchedRoots) {
|
|
104
|
+
for (const rel of watchedRelPaths) {
|
|
105
|
+
if ((await watchedPathMtime(join(root, rel))) > indexMtimeMs) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
18
114
|
export function legacyAiIndexPath(rootDir: string): string {
|
|
19
115
|
return join(rootDir, "index.json");
|
|
20
116
|
}
|
|
@@ -46,6 +142,21 @@ export async function ensureAiIndexPath(args: {
|
|
|
46
142
|
}> {
|
|
47
143
|
const generatedPath = facultAiIndexPath(args.homeDir, args.rootDir);
|
|
48
144
|
if (await fileExists(generatedPath)) {
|
|
145
|
+
if (
|
|
146
|
+
args.repair !== false &&
|
|
147
|
+
(await canonicalAssetsNewerThanIndex({
|
|
148
|
+
homeDir: args.homeDir,
|
|
149
|
+
rootDir: args.rootDir,
|
|
150
|
+
indexPath: generatedPath,
|
|
151
|
+
}))
|
|
152
|
+
) {
|
|
153
|
+
const { outputPath } = await buildIndex({
|
|
154
|
+
rootDir: args.rootDir,
|
|
155
|
+
homeDir: args.homeDir,
|
|
156
|
+
force: false,
|
|
157
|
+
});
|
|
158
|
+
return { path: outputPath, repaired: true, source: "rebuilt" };
|
|
159
|
+
}
|
|
49
160
|
return { path: generatedPath, repaired: false, source: "generated" };
|
|
50
161
|
}
|
|
51
162
|
|
|
@@ -100,6 +211,25 @@ export async function ensureAiGraphPath(args: {
|
|
|
100
211
|
}> {
|
|
101
212
|
const generatedPath = facultAiGraphPath(args.homeDir, args.rootDir);
|
|
102
213
|
if (await fileExists(generatedPath)) {
|
|
214
|
+
const generatedIndexPath = facultAiIndexPath(args.homeDir, args.rootDir);
|
|
215
|
+
const freshnessAnchor = (await fileExists(generatedIndexPath))
|
|
216
|
+
? generatedIndexPath
|
|
217
|
+
: generatedPath;
|
|
218
|
+
if (
|
|
219
|
+
args.repair !== false &&
|
|
220
|
+
(await canonicalAssetsNewerThanIndex({
|
|
221
|
+
homeDir: args.homeDir,
|
|
222
|
+
rootDir: args.rootDir,
|
|
223
|
+
indexPath: freshnessAnchor,
|
|
224
|
+
}))
|
|
225
|
+
) {
|
|
226
|
+
const { graphPath } = await buildIndex({
|
|
227
|
+
rootDir: args.rootDir,
|
|
228
|
+
homeDir: args.homeDir,
|
|
229
|
+
force: false,
|
|
230
|
+
});
|
|
231
|
+
return { path: graphPath, rebuilt: true };
|
|
232
|
+
}
|
|
103
233
|
return { path: generatedPath, rebuilt: false };
|
|
104
234
|
}
|
|
105
235
|
|
package/src/index-builder.ts
CHANGED
|
@@ -69,6 +69,12 @@ export interface AgentEntry {
|
|
|
69
69
|
path: string;
|
|
70
70
|
description?: string;
|
|
71
71
|
lastModifiedAt?: string;
|
|
72
|
+
enabledFor?: string[];
|
|
73
|
+
trusted?: boolean;
|
|
74
|
+
trustedAt?: string;
|
|
75
|
+
trustedBy?: string;
|
|
76
|
+
auditStatus?: "pending" | "passed" | "flagged";
|
|
77
|
+
lastAuditAt?: string;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
export interface SnippetEntry {
|
|
@@ -197,6 +203,53 @@ function extractIndexMeta(entry: unknown): {
|
|
|
197
203
|
};
|
|
198
204
|
}
|
|
199
205
|
|
|
206
|
+
function findPreviousEntryByCanonicalRef(
|
|
207
|
+
previous: Record<string, unknown> | undefined,
|
|
208
|
+
canonicalRef: string | undefined,
|
|
209
|
+
fallbackName: string
|
|
210
|
+
): unknown {
|
|
211
|
+
if (!previous) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
if (typeof canonicalRef === "string") {
|
|
215
|
+
for (const value of Object.values(previous)) {
|
|
216
|
+
if (!isPlainObject(value)) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (value.canonicalRef === canonicalRef) {
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const legacyFallback = previous[fallbackName];
|
|
225
|
+
if (
|
|
226
|
+
isPlainObject(legacyFallback) &&
|
|
227
|
+
typeof legacyFallback.canonicalRef !== "string"
|
|
228
|
+
) {
|
|
229
|
+
return legacyFallback;
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function findPreviousMcpEntry(
|
|
235
|
+
previous: Record<string, unknown> | undefined,
|
|
236
|
+
canonicalRef: string | undefined,
|
|
237
|
+
name: string
|
|
238
|
+
): unknown {
|
|
239
|
+
if (!previous) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
const candidate = previous[name];
|
|
243
|
+
if (!isPlainObject(candidate)) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
return typeof candidate.canonicalRef !== "string" ||
|
|
247
|
+
(typeof canonicalRef === "string" &&
|
|
248
|
+
candidate.canonicalRef === canonicalRef)
|
|
249
|
+
? candidate
|
|
250
|
+
: undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
200
253
|
function stripQuotes(s: string): string {
|
|
201
254
|
const t = s.trim();
|
|
202
255
|
if (
|
|
@@ -489,8 +542,12 @@ async function indexSkills(
|
|
|
489
542
|
const md = await Bun.file(skillMd).text();
|
|
490
543
|
const { description, tags } = parseSkillMarkdown(md);
|
|
491
544
|
const name = basename(d);
|
|
492
|
-
|
|
493
|
-
const prev =
|
|
545
|
+
const canonicalRef = canonicalRefForPath(source, "skills", d);
|
|
546
|
+
const prev = findPreviousEntryByCanonicalRef(
|
|
547
|
+
previous,
|
|
548
|
+
canonicalRef,
|
|
549
|
+
name
|
|
550
|
+
);
|
|
494
551
|
const meta = extractIndexMeta(prev);
|
|
495
552
|
|
|
496
553
|
out[name] = {
|
|
@@ -498,7 +555,7 @@ async function indexSkills(
|
|
|
498
555
|
path: d,
|
|
499
556
|
description,
|
|
500
557
|
tags,
|
|
501
|
-
canonicalRef
|
|
558
|
+
canonicalRef,
|
|
502
559
|
lastModifiedAt: await statIsoTime(skillMd),
|
|
503
560
|
enabledFor: meta.enabledFor,
|
|
504
561
|
trusted: meta.trusted ?? false,
|
|
@@ -550,12 +607,13 @@ async function indexMcpServers(
|
|
|
550
607
|
|
|
551
608
|
const lm = await statIsoTime(mcpConfigPath);
|
|
552
609
|
for (const name of Object.keys(serversObj).sort()) {
|
|
553
|
-
const
|
|
610
|
+
const canonicalRef = canonicalRefForPath(source, "mcp", mcpConfigPath);
|
|
611
|
+
const prev = findPreviousMcpEntry(previous, canonicalRef, name);
|
|
554
612
|
const meta = extractIndexMeta(prev);
|
|
555
613
|
out[name] = {
|
|
556
614
|
name,
|
|
557
615
|
path: mcpConfigPath,
|
|
558
|
-
canonicalRef
|
|
616
|
+
canonicalRef,
|
|
559
617
|
lastModifiedAt: lm,
|
|
560
618
|
definition: serversObj[name],
|
|
561
619
|
enabledFor: meta.enabledFor,
|
|
@@ -576,7 +634,8 @@ async function indexMcpServers(
|
|
|
576
634
|
|
|
577
635
|
async function indexAgents(
|
|
578
636
|
agentsDir: string,
|
|
579
|
-
source: IndexedSource
|
|
637
|
+
source: IndexedSource,
|
|
638
|
+
previous?: Record<string, unknown>
|
|
580
639
|
): Promise<Record<string, AgentEntry>> {
|
|
581
640
|
const out: Record<string, AgentEntry> = {};
|
|
582
641
|
const files: string[] = [];
|
|
@@ -596,6 +655,9 @@ async function indexAgents(
|
|
|
596
655
|
for (const p of files) {
|
|
597
656
|
const name =
|
|
598
657
|
basename(p) === "agent.toml" ? basename(dirname(p)) : basename(p);
|
|
658
|
+
const canonicalRef = canonicalRefForPath(source, "agents", p);
|
|
659
|
+
const prev = findPreviousEntryByCanonicalRef(previous, canonicalRef, name);
|
|
660
|
+
const meta = extractIndexMeta(prev);
|
|
599
661
|
let description: string | undefined;
|
|
600
662
|
try {
|
|
601
663
|
const raw = await Bun.file(p).text();
|
|
@@ -611,8 +673,14 @@ async function indexAgents(
|
|
|
611
673
|
name,
|
|
612
674
|
path: p,
|
|
613
675
|
description,
|
|
614
|
-
canonicalRef
|
|
676
|
+
canonicalRef,
|
|
615
677
|
lastModifiedAt: await statIsoTime(p),
|
|
678
|
+
enabledFor: meta.enabledFor,
|
|
679
|
+
trusted: meta.trusted ?? false,
|
|
680
|
+
trustedAt: meta.trustedAt,
|
|
681
|
+
trustedBy: meta.trustedBy,
|
|
682
|
+
auditStatus: meta.auditStatus ?? "pending",
|
|
683
|
+
lastAuditAt: meta.lastAuditAt,
|
|
616
684
|
...entryScopeMeta(source),
|
|
617
685
|
};
|
|
618
686
|
}
|
|
@@ -782,6 +850,9 @@ async function indexSourceAssets(
|
|
|
782
850
|
const prevSkills = isPlainObject(previousIndex?.skills)
|
|
783
851
|
? (previousIndex?.skills as Record<string, unknown>)
|
|
784
852
|
: undefined;
|
|
853
|
+
const prevAgents = isPlainObject(previousIndex?.agents)
|
|
854
|
+
? (previousIndex?.agents as Record<string, unknown>)
|
|
855
|
+
: undefined;
|
|
785
856
|
const prevMcpMap =
|
|
786
857
|
isPlainObject(previousIndex?.mcp) &&
|
|
787
858
|
isPlainObject((previousIndex.mcp as Record<string, unknown>).servers)
|
|
@@ -795,7 +866,7 @@ async function indexSourceAssets(
|
|
|
795
866
|
await Promise.all([
|
|
796
867
|
indexSkills(skillsDir, source, prevSkills),
|
|
797
868
|
indexMcpServers(canonicalMcpPath, source, prevMcpMap),
|
|
798
|
-
indexAgents(agentsDir, source),
|
|
869
|
+
indexAgents(agentsDir, source, prevAgents),
|
|
799
870
|
indexSnippets(snippetsDir, source),
|
|
800
871
|
indexInstructions(instructionsDir, source),
|
|
801
872
|
indexToolAssets(toolsDir, source),
|