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/agents.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { facultBuiltinPackRoot } from "./builtin";
|
|
3
|
+
|
|
4
|
+
const AI_REF_RE = /(?<![\w@])@ai\/([^\s"'`<>]+)/g;
|
|
5
|
+
const BUILTIN_REF_RE = /(?<![\w@])@builtin\/([^\s"'`<>]+)/g;
|
|
6
|
+
const PROJECT_REF_RE = /(?<![\w@])@project\/([^\s"'`<>]+)/g;
|
|
7
|
+
const INTERPOLATION_RE = /\$\{([^}]+)\}/g;
|
|
8
|
+
const TRAILING_PUNCTUATION_RE = /[.,;:!?)}\]]+$/;
|
|
9
|
+
const MAX_RENDER_PASSES = 10;
|
|
10
|
+
|
|
11
|
+
export interface RenderCanonicalTextOptions {
|
|
12
|
+
homeDir?: string;
|
|
13
|
+
rootDir: string;
|
|
14
|
+
projectSlug?: string;
|
|
15
|
+
projectRoot?: string;
|
|
16
|
+
targetTool?: string;
|
|
17
|
+
targetPath?: string;
|
|
18
|
+
overrides?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type RenderContext = Record<string, unknown>;
|
|
22
|
+
|
|
23
|
+
function trimTrailingPunctuation(refPath: string): {
|
|
24
|
+
path: string;
|
|
25
|
+
suffix: string;
|
|
26
|
+
} {
|
|
27
|
+
const match = TRAILING_PUNCTUATION_RE.exec(refPath);
|
|
28
|
+
if (!match) {
|
|
29
|
+
return { path: refPath, suffix: "" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const suffix = match[0];
|
|
33
|
+
return {
|
|
34
|
+
path: refPath.slice(0, -suffix.length),
|
|
35
|
+
suffix,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function renderAiRefs(input: string, canonicalRoot: string): string {
|
|
40
|
+
return input.replace(AI_REF_RE, (_match, refPath: string) => {
|
|
41
|
+
const { path, suffix } = trimTrailingPunctuation(refPath);
|
|
42
|
+
return `${join(canonicalRoot, path)}${suffix}`;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function renderBuiltinRefs(input: string): string {
|
|
47
|
+
const builtinRoot = facultBuiltinPackRoot();
|
|
48
|
+
return input.replace(BUILTIN_REF_RE, (_match, refPath: string) => {
|
|
49
|
+
const { path, suffix } = trimTrailingPunctuation(refPath);
|
|
50
|
+
const relative = path.startsWith("facult-operating-model/")
|
|
51
|
+
? path.slice("facult-operating-model/".length)
|
|
52
|
+
: path;
|
|
53
|
+
return `${join(builtinRoot, relative)}${suffix}`;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function renderProjectRefs(input: string, projectRoot: string): string {
|
|
58
|
+
return input.replace(PROJECT_REF_RE, (_match, refPath: string) => {
|
|
59
|
+
const { path, suffix } = trimTrailingPunctuation(refPath);
|
|
60
|
+
return `${join(projectRoot, ".ai", path)}${suffix}`;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
65
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function mergeContexts(
|
|
69
|
+
base: Record<string, unknown>,
|
|
70
|
+
override: Record<string, unknown>
|
|
71
|
+
): Record<string, unknown> {
|
|
72
|
+
const merged: Record<string, unknown> = { ...base };
|
|
73
|
+
|
|
74
|
+
for (const [key, value] of Object.entries(override)) {
|
|
75
|
+
const current = merged[key];
|
|
76
|
+
if (isPlainObject(current) && isPlainObject(value)) {
|
|
77
|
+
merged[key] = mergeContexts(current, value);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
merged[key] = value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return merged;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function readTomlFile(
|
|
87
|
+
pathValue: string
|
|
88
|
+
): Promise<Record<string, unknown> | null> {
|
|
89
|
+
const file = Bun.file(pathValue);
|
|
90
|
+
if (!(await file.exists())) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const text = await file.text();
|
|
95
|
+
const parsed = Bun.TOML.parse(text);
|
|
96
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getContextValue(
|
|
100
|
+
context: Record<string, unknown>,
|
|
101
|
+
dottedPath: string
|
|
102
|
+
): unknown {
|
|
103
|
+
const segments = dottedPath
|
|
104
|
+
.split(".")
|
|
105
|
+
.map((segment) => segment.trim())
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
if (segments.length === 0) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let current: unknown = context;
|
|
112
|
+
for (const segment of segments) {
|
|
113
|
+
if (!(isPlainObject(current) && segment in current)) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
current = current[segment];
|
|
117
|
+
}
|
|
118
|
+
return current;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function interpolateString(
|
|
122
|
+
input: string,
|
|
123
|
+
context: Record<string, unknown>
|
|
124
|
+
): string {
|
|
125
|
+
return input.replace(INTERPOLATION_RE, (match, keyPath: string) => {
|
|
126
|
+
const value = getContextValue(context, keyPath.trim());
|
|
127
|
+
return typeof value === "string" ? value : match;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function loadRenderContext(
|
|
132
|
+
options: RenderCanonicalTextOptions
|
|
133
|
+
): Promise<RenderContext> {
|
|
134
|
+
const {
|
|
135
|
+
homeDir,
|
|
136
|
+
overrides,
|
|
137
|
+
projectRoot,
|
|
138
|
+
projectSlug,
|
|
139
|
+
rootDir,
|
|
140
|
+
targetPath,
|
|
141
|
+
targetTool,
|
|
142
|
+
} = options;
|
|
143
|
+
const contextBase: RenderContext = {
|
|
144
|
+
AI_ROOT: rootDir,
|
|
145
|
+
HOME: homeDir ?? "",
|
|
146
|
+
PROJECT_ROOT: projectRoot ?? "",
|
|
147
|
+
PROJECT_SLUG: projectSlug ?? "",
|
|
148
|
+
TARGET_PATH: targetPath ?? "",
|
|
149
|
+
TARGET_TOOL: targetTool ?? "",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
let context = contextBase;
|
|
153
|
+
const layers = [
|
|
154
|
+
await readTomlFile(join(rootDir, "config.toml")),
|
|
155
|
+
await readTomlFile(join(rootDir, "config.local.toml")),
|
|
156
|
+
projectSlug
|
|
157
|
+
? await readTomlFile(
|
|
158
|
+
join(rootDir, "projects", projectSlug, "config.toml")
|
|
159
|
+
)
|
|
160
|
+
: null,
|
|
161
|
+
projectSlug
|
|
162
|
+
? await readTomlFile(
|
|
163
|
+
join(rootDir, "projects", projectSlug, "config.local.toml")
|
|
164
|
+
)
|
|
165
|
+
: null,
|
|
166
|
+
overrides && isPlainObject(overrides) ? overrides : null,
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
for (const layer of layers) {
|
|
170
|
+
if (layer) {
|
|
171
|
+
context = mergeContexts(context, layer);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return context;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function renderCanonicalText(
|
|
179
|
+
input: string,
|
|
180
|
+
options: RenderCanonicalTextOptions
|
|
181
|
+
): Promise<string> {
|
|
182
|
+
const context = await loadRenderContext(options);
|
|
183
|
+
let rendered = input;
|
|
184
|
+
const seen = new Set<string>();
|
|
185
|
+
|
|
186
|
+
for (let pass = 0; pass < MAX_RENDER_PASSES; pass += 1) {
|
|
187
|
+
if (seen.has(rendered)) {
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
seen.add(rendered);
|
|
191
|
+
|
|
192
|
+
const interpolated = interpolateString(rendered, context);
|
|
193
|
+
const withAiRefs = renderAiRefs(interpolated, options.rootDir);
|
|
194
|
+
const withBuiltinRefs = renderBuiltinRefs(withAiRefs);
|
|
195
|
+
const withRefs = options.projectRoot
|
|
196
|
+
? renderProjectRefs(withBuiltinRefs, options.projectRoot)
|
|
197
|
+
: withBuiltinRefs;
|
|
198
|
+
if (withRefs === rendered) {
|
|
199
|
+
return withRefs;
|
|
200
|
+
}
|
|
201
|
+
rendered = withRefs;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return rendered;
|
|
205
|
+
}
|
package/src/ai-state.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { copyFile, mkdir, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { buildIndex } from "./index-builder";
|
|
4
|
+
import { facultAiGraphPath, facultAiIndexPath } from "./paths";
|
|
5
|
+
|
|
6
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
7
|
+
try {
|
|
8
|
+
return (await stat(path)).isFile();
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function legacyAiIndexPath(rootDir: string): string {
|
|
15
|
+
return join(rootDir, "index.json");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function ensureAiIndexPath(args: {
|
|
19
|
+
homeDir: string;
|
|
20
|
+
rootDir: string;
|
|
21
|
+
repair?: boolean;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
path: string;
|
|
24
|
+
repaired: boolean;
|
|
25
|
+
source: "generated" | "legacy" | "rebuilt" | "missing";
|
|
26
|
+
}> {
|
|
27
|
+
const generatedPath = facultAiIndexPath(args.homeDir, args.rootDir);
|
|
28
|
+
if (await fileExists(generatedPath)) {
|
|
29
|
+
return { path: generatedPath, repaired: false, source: "generated" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const legacyPath = legacyAiIndexPath(args.rootDir);
|
|
33
|
+
if (await fileExists(legacyPath)) {
|
|
34
|
+
if (args.repair !== false) {
|
|
35
|
+
await mkdir(dirname(generatedPath), { recursive: true });
|
|
36
|
+
await copyFile(legacyPath, generatedPath);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
path: generatedPath,
|
|
40
|
+
repaired: args.repair !== false,
|
|
41
|
+
source: "legacy",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (args.repair !== false) {
|
|
46
|
+
const { outputPath } = await buildIndex({
|
|
47
|
+
rootDir: args.rootDir,
|
|
48
|
+
homeDir: args.homeDir,
|
|
49
|
+
force: false,
|
|
50
|
+
});
|
|
51
|
+
return { path: outputPath, repaired: true, source: "rebuilt" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { path: generatedPath, repaired: false, source: "missing" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function ensureAiGraphPath(args: {
|
|
58
|
+
homeDir: string;
|
|
59
|
+
rootDir: string;
|
|
60
|
+
repair?: boolean;
|
|
61
|
+
}): Promise<{
|
|
62
|
+
path: string;
|
|
63
|
+
rebuilt: boolean;
|
|
64
|
+
}> {
|
|
65
|
+
const generatedPath = facultAiGraphPath(args.homeDir, args.rootDir);
|
|
66
|
+
if (await fileExists(generatedPath)) {
|
|
67
|
+
return { path: generatedPath, rebuilt: false };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (args.repair !== false) {
|
|
71
|
+
const { graphPath } = await buildIndex({
|
|
72
|
+
rootDir: args.rootDir,
|
|
73
|
+
homeDir: args.homeDir,
|
|
74
|
+
force: false,
|
|
75
|
+
});
|
|
76
|
+
return { path: graphPath, rebuilt: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { path: generatedPath, rebuilt: false };
|
|
80
|
+
}
|