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/src/snippets.ts CHANGED
@@ -230,6 +230,12 @@ export interface SyncResult {
230
230
  errors: string[];
231
231
  }
232
232
 
233
+ export interface RenderSnippetTextResult {
234
+ text: string;
235
+ changes: SyncChange[];
236
+ errors: string[];
237
+ }
238
+
233
239
  function isSafePathString(p: string): boolean {
234
240
  // Protect filesystem APIs from null-byte paths.
235
241
  return !p.includes("\0");
@@ -564,6 +570,106 @@ export async function syncFile(args: {
564
570
  return { filePath, dryRun, changed: true, changes, errors: [] };
565
571
  }
566
572
 
573
+ export async function renderSnippetText(args: {
574
+ text: string;
575
+ project?: string | null;
576
+ filePath?: string;
577
+ rootDir?: string;
578
+ }): Promise<RenderSnippetTextResult> {
579
+ const rootDir = args.rootDir ?? facultRootDir();
580
+ const text = args.text;
581
+ const filePath = args.filePath;
582
+ const errors: string[] = [];
583
+ const changes: SyncChange[] = [];
584
+
585
+ if (!hasMarkers(text)) {
586
+ return { text, changes, errors };
587
+ }
588
+
589
+ const found = findMarkersInText(text, filePath);
590
+ if (found.errors.length) {
591
+ return {
592
+ text,
593
+ changes,
594
+ errors: found.errors,
595
+ };
596
+ }
597
+
598
+ let lastEnd = -1;
599
+ for (const p of found.pairs) {
600
+ if (p.open.start < lastEnd) {
601
+ const location = filePath
602
+ ? `${filePath}:${p.open.line}`
603
+ : `line ${p.open.line}`;
604
+ errors.push(
605
+ `${location}: snippet markers may not be nested or overlapping (found ${p.name})`
606
+ );
607
+ }
608
+ lastEnd = p.close.end;
609
+ }
610
+ if (errors.length) {
611
+ return { text, changes, errors };
612
+ }
613
+
614
+ type Replacement = { start: number; end: number; next: string };
615
+ const replacements: Replacement[] = [];
616
+
617
+ for (const pair of found.pairs) {
618
+ const marker = pair.name;
619
+ const existing = text.slice(pair.contentStart, pair.contentEnd);
620
+ const existingNorm = normalizeSnippetBody(existing);
621
+
622
+ const snippet = await findSnippet({
623
+ marker,
624
+ project: args.project ?? null,
625
+ rootDir,
626
+ });
627
+
628
+ if (!snippet) {
629
+ changes.push({ marker, status: "not-found" });
630
+ continue;
631
+ }
632
+
633
+ const snippetNorm = normalizeSnippetBody(snippet.content);
634
+ if (existingNorm === snippetNorm) {
635
+ changes.push({
636
+ marker,
637
+ status: "unchanged",
638
+ snippetPath: snippet.path,
639
+ lines: countLines(snippetNorm),
640
+ });
641
+ continue;
642
+ }
643
+
644
+ replacements.push({
645
+ start: pair.contentStart,
646
+ end: pair.contentEnd,
647
+ next: formatSnippetInjection(snippet.content),
648
+ });
649
+ changes.push({
650
+ marker,
651
+ status: "updated",
652
+ snippetPath: snippet.path,
653
+ lines: countLines(snippetNorm),
654
+ });
655
+ }
656
+
657
+ if (replacements.length === 0) {
658
+ return { text, changes, errors };
659
+ }
660
+
661
+ let updated = text;
662
+ for (const r of replacements.sort((a, b) => b.start - a.start)) {
663
+ updated = updated.slice(0, r.start) + r.next + updated.slice(r.end);
664
+ }
665
+
666
+ return {
667
+ text: updated,
668
+ changes,
669
+ errors,
670
+ };
671
+ }
672
+
567
673
  export async function syncAll(args?: {
568
674
  dryRun?: boolean;
569
675
  /** Override canonical root (useful for tests). */
package/src/trust-list.ts CHANGED
@@ -226,6 +226,7 @@ export async function applyOrgTrustList(
226
226
  },
227
227
  agents: index.agents ?? {},
228
228
  snippets: index.snippets ?? {},
229
+ instructions: index.instructions ?? {},
229
230
  };
230
231
 
231
232
  return next;
package/src/trust.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { homedir } from "node:os";
2
- import { join } from "node:path";
2
+ import { ensureAiIndexPath } from "./ai-state";
3
3
  import type { FacultIndex } from "./index-builder";
4
- import { facultRootDir } from "./paths";
4
+ import { facultAiIndexPath, facultRootDir } from "./paths";
5
5
 
6
6
  type TrustMode = "trust" | "untrust";
7
7
 
@@ -17,6 +17,7 @@ function ensureIndexStructure(index: FacultIndex): FacultIndex {
17
17
  mcp: index.mcp ?? { servers: {} },
18
18
  agents: index.agents ?? {},
19
19
  snippets: index.snippets ?? {},
20
+ instructions: index.instructions ?? {},
20
21
  };
21
22
  }
22
23
 
@@ -27,8 +28,12 @@ function parseEntryName(raw: string): { kind: "skill" | "mcp"; name: string } {
27
28
  return { kind: "skill", name: raw };
28
29
  }
29
30
 
30
- async function loadIndex(rootDir: string): Promise<FacultIndex> {
31
- const indexPath = join(rootDir, "index.json");
31
+ async function loadIndex(homeDir: string): Promise<FacultIndex> {
32
+ const { path: indexPath } = await ensureAiIndexPath({
33
+ homeDir,
34
+ rootDir: facultRootDir(homeDir),
35
+ repair: true,
36
+ });
32
37
  const file = Bun.file(indexPath);
33
38
  if (!(await file.exists())) {
34
39
  throw new Error(`Index not found at ${indexPath}. Run "facult index".`);
@@ -37,8 +42,8 @@ async function loadIndex(rootDir: string): Promise<FacultIndex> {
37
42
  return JSON.parse(raw) as FacultIndex;
38
43
  }
39
44
 
40
- async function writeIndex(rootDir: string, index: FacultIndex) {
41
- const indexPath = join(rootDir, "index.json");
45
+ async function writeIndex(homeDir: string, index: FacultIndex) {
46
+ const indexPath = facultAiIndexPath(homeDir);
42
47
  await Bun.write(indexPath, `${JSON.stringify(index, null, 2)}\n`);
43
48
  }
44
49
 
@@ -68,20 +73,17 @@ export async function applyTrust({
68
73
  names,
69
74
  mode,
70
75
  homeDir,
71
- rootDir,
72
76
  }: {
73
77
  names: string[];
74
78
  mode: TrustMode;
75
79
  homeDir?: string;
76
- rootDir?: string;
77
80
  }) {
78
81
  if (!names.length) {
79
82
  throw new Error("At least one name is required.");
80
83
  }
81
84
  const home = homeDir ?? homedir();
82
- const root = rootDir ?? facultRootDir(home);
83
85
 
84
- const index = ensureIndexStructure(await loadIndex(root));
86
+ const index = ensureIndexStructure(await loadIndex(home));
85
87
  const now = new Date().toISOString();
86
88
 
87
89
  const missing: string[] = [];
@@ -109,7 +111,7 @@ export async function applyTrust({
109
111
  }
110
112
 
111
113
  index.updatedAt = new Date().toISOString();
112
- await writeIndex(root, index);
114
+ await writeIndex(home, index);
113
115
  }
114
116
 
115
117
  function parseNamesFromArgv(argv: string[]): string[] {