facult 2.7.4 → 2.8.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 +23 -2
- package/package.json +1 -1
- package/src/adapters/codex.ts +1 -1
- package/src/ai.ts +44 -3
- package/src/doctor.ts +81 -6
- package/src/index.ts +138 -21
- package/src/inventory.ts +886 -0
- package/src/manage.ts +724 -71
- package/src/mcp-config.ts +5 -0
- package/src/project-sync.ts +15 -2
- package/src/scan.ts +47 -1
- package/src/status.ts +268 -0
package/src/mcp-config.ts
CHANGED
|
@@ -32,6 +32,11 @@ export function extractServersObject(
|
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
34
|
const raw = parsed as Record<string, unknown>;
|
|
35
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
36
|
+
if (key.endsWith(".mcpServers") && isPlainObject(value)) {
|
|
37
|
+
return value as Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
35
40
|
const servers =
|
|
36
41
|
(raw.servers as Record<string, unknown> | undefined) ??
|
|
37
42
|
(raw.mcpServers as Record<string, unknown> | undefined) ??
|
package/src/project-sync.ts
CHANGED
|
@@ -2,12 +2,17 @@ import { mkdir } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { projectRootFromAiRoot } from "./paths";
|
|
4
4
|
|
|
5
|
-
type ProjectSyncNamedSurface =
|
|
5
|
+
type ProjectSyncNamedSurface =
|
|
6
|
+
| "skills"
|
|
7
|
+
| "agents"
|
|
8
|
+
| "automations"
|
|
9
|
+
| "mcpServers";
|
|
6
10
|
type ProjectSyncToolSurface = "globalDocs" | "toolRules" | "toolConfig";
|
|
7
11
|
|
|
8
12
|
interface ProjectSyncToolPolicy {
|
|
9
13
|
skills: string[];
|
|
10
14
|
agents: string[];
|
|
15
|
+
automations: string[];
|
|
11
16
|
mcpServers: string[];
|
|
12
17
|
globalDocs: boolean;
|
|
13
18
|
toolRules: boolean;
|
|
@@ -69,6 +74,7 @@ function projectSyncToolPolicyFromObject(
|
|
|
69
74
|
return {
|
|
70
75
|
skills: parseStringList(table.skills),
|
|
71
76
|
agents: parseStringList(table.agents),
|
|
77
|
+
automations: parseStringList(table.automations ?? table.automation),
|
|
72
78
|
mcpServers: parseStringList(table.mcp_servers ?? table.mcp),
|
|
73
79
|
globalDocs: parseBoolean(table.global_docs ?? table.docs),
|
|
74
80
|
toolRules: parseBoolean(table.tool_rules ?? table.rules),
|
|
@@ -104,6 +110,7 @@ function emptyPolicy(): ProjectSyncToolPolicy {
|
|
|
104
110
|
return {
|
|
105
111
|
skills: [],
|
|
106
112
|
agents: [],
|
|
113
|
+
automations: [],
|
|
107
114
|
mcpServers: [],
|
|
108
115
|
globalDocs: false,
|
|
109
116
|
toolRules: false,
|
|
@@ -150,7 +157,9 @@ export async function projectSyncAllowsNamedAsset(args: {
|
|
|
150
157
|
? policy.skills
|
|
151
158
|
: args.surface === "agents"
|
|
152
159
|
? policy.agents
|
|
153
|
-
:
|
|
160
|
+
: args.surface === "automations"
|
|
161
|
+
? policy.automations
|
|
162
|
+
: policy.mcpServers;
|
|
154
163
|
return includesExplicitName(allowed, args.name);
|
|
155
164
|
}
|
|
156
165
|
|
|
@@ -256,6 +265,9 @@ export async function writeProjectSyncPolicy(args: {
|
|
|
256
265
|
const mergedPolicy: ProjectSyncToolPolicy = {
|
|
257
266
|
skills: parseStringList(partialPolicy.skills ?? previousPolicy.skills),
|
|
258
267
|
agents: parseStringList(partialPolicy.agents ?? previousPolicy.agents),
|
|
268
|
+
automations: parseStringList(
|
|
269
|
+
partialPolicy.automations ?? previousPolicy.automations
|
|
270
|
+
),
|
|
259
271
|
mcpServers: parseStringList(
|
|
260
272
|
partialPolicy.mcpServers ?? previousPolicy.mcpServers
|
|
261
273
|
),
|
|
@@ -267,6 +279,7 @@ export async function writeProjectSyncPolicy(args: {
|
|
|
267
279
|
projectSync[tool] = {
|
|
268
280
|
skills: mergedPolicy.skills,
|
|
269
281
|
agents: mergedPolicy.agents,
|
|
282
|
+
automations: mergedPolicy.automations,
|
|
270
283
|
mcp_servers: mergedPolicy.mcpServers,
|
|
271
284
|
global_docs: mergedPolicy.globalDocs,
|
|
272
285
|
tool_rules: mergedPolicy.toolRules,
|
package/src/scan.ts
CHANGED
|
@@ -1189,6 +1189,44 @@ async function buildFromRootResult(args: {
|
|
|
1189
1189
|
}
|
|
1190
1190
|
};
|
|
1191
1191
|
|
|
1192
|
+
const scanAiDir = async (aiDir: string) => {
|
|
1193
|
+
await scanToolDotDir(aiDir);
|
|
1194
|
+
|
|
1195
|
+
for (const name of ["servers.json", "mcp.json"]) {
|
|
1196
|
+
const p = join(aiDir, "mcp", name);
|
|
1197
|
+
if ((await statSafe(p))?.isFile) {
|
|
1198
|
+
if (addResult(1)) {
|
|
1199
|
+
mcpConfigPaths.add(p);
|
|
1200
|
+
} else {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
for (const name of ["AGENTS.global.md", "AGENTS.override.global.md"]) {
|
|
1207
|
+
const p = join(aiDir, name);
|
|
1208
|
+
if ((await statSafe(p))?.isFile) {
|
|
1209
|
+
addAsset("agents-instructions", p);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const instructionsDir = join(aiDir, "instructions");
|
|
1214
|
+
if ((await statSafe(instructionsDir))?.isDir) {
|
|
1215
|
+
const files = await listFilesRecursive(instructionsDir, {
|
|
1216
|
+
ignore: args.opts.ignoreDirNames,
|
|
1217
|
+
maxFiles: 2000,
|
|
1218
|
+
});
|
|
1219
|
+
for (const f of files) {
|
|
1220
|
+
if (f.endsWith(".md")) {
|
|
1221
|
+
addAsset("canonical-instruction", f);
|
|
1222
|
+
}
|
|
1223
|
+
if (truncated) {
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1192
1230
|
const MCP_NAMES = new Set([
|
|
1193
1231
|
"mcp.json",
|
|
1194
1232
|
"mcp.config.json",
|
|
@@ -1317,6 +1355,10 @@ async function buildFromRootResult(args: {
|
|
|
1317
1355
|
await scanToolDotDir(child);
|
|
1318
1356
|
continue;
|
|
1319
1357
|
}
|
|
1358
|
+
if (name === ".ai") {
|
|
1359
|
+
await scanAiDir(child);
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1320
1362
|
|
|
1321
1363
|
// Skills directories are typically called "skills"; scan them and don't descend further.
|
|
1322
1364
|
if (name === "skills") {
|
|
@@ -2268,6 +2310,7 @@ Notes:
|
|
|
2268
2310
|
|
|
2269
2311
|
Options:
|
|
2270
2312
|
--json Print full JSON (ScanResult)
|
|
2313
|
+
--persist Persist scan state when using --json
|
|
2271
2314
|
--show-duplicates Print duplicates for skills, MCP servers, and hook assets
|
|
2272
2315
|
--tui Render scan output in an interactive TUI (skills list)
|
|
2273
2316
|
--no-config-from Disable default scan roots from ~/.ai/.facult/config.json (scanFrom)
|
|
@@ -2287,6 +2330,7 @@ export async function scanCommand(argv: string[]) {
|
|
|
2287
2330
|
}
|
|
2288
2331
|
|
|
2289
2332
|
const json = argv.includes("--json");
|
|
2333
|
+
const persist = argv.includes("--persist") || !json;
|
|
2290
2334
|
const showDuplicates = argv.includes("--show-duplicates");
|
|
2291
2335
|
const tui = argv.includes("--tui");
|
|
2292
2336
|
const noConfigFrom = argv.includes("--no-config-from");
|
|
@@ -2427,7 +2471,9 @@ export async function scanCommand(argv: string[]) {
|
|
|
2427
2471
|
maxResults: fromMaxResults,
|
|
2428
2472
|
},
|
|
2429
2473
|
});
|
|
2430
|
-
|
|
2474
|
+
if (persist) {
|
|
2475
|
+
await writeState(res);
|
|
2476
|
+
}
|
|
2431
2477
|
|
|
2432
2478
|
if (json) {
|
|
2433
2479
|
if (tui) {
|
package/src/status.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
4
|
+
import { loadManagedState } from "./manage";
|
|
5
|
+
import {
|
|
6
|
+
facultAiGraphPath,
|
|
7
|
+
facultAiIndexPath,
|
|
8
|
+
facultAiProposalDir,
|
|
9
|
+
facultAiWritebackQueuePath,
|
|
10
|
+
facultMachineStateDir,
|
|
11
|
+
facultRootDir,
|
|
12
|
+
projectRootFromAiRoot,
|
|
13
|
+
} from "./paths";
|
|
14
|
+
import { parseJsonLenient } from "./util/json";
|
|
15
|
+
|
|
16
|
+
export interface StatusIssue {
|
|
17
|
+
severity: "info" | "warning" | "error";
|
|
18
|
+
code: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FacultStatus {
|
|
23
|
+
version: 1;
|
|
24
|
+
packageVersion: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
globalRoot: string;
|
|
27
|
+
contextRoot: string;
|
|
28
|
+
projectRoot: string | null;
|
|
29
|
+
machineStateDir: string;
|
|
30
|
+
managedTools: string[];
|
|
31
|
+
generatedOnlyProjectRoot: boolean;
|
|
32
|
+
index: {
|
|
33
|
+
path: string;
|
|
34
|
+
exists: boolean;
|
|
35
|
+
};
|
|
36
|
+
graph: {
|
|
37
|
+
path: string;
|
|
38
|
+
exists: boolean;
|
|
39
|
+
};
|
|
40
|
+
writeback: {
|
|
41
|
+
queuePath: string;
|
|
42
|
+
pendingCount: number;
|
|
43
|
+
proposalDir: string;
|
|
44
|
+
proposalCount: number;
|
|
45
|
+
};
|
|
46
|
+
issues: StatusIssue[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function fileExists(pathValue: string): Promise<boolean> {
|
|
50
|
+
try {
|
|
51
|
+
return (await Bun.file(pathValue).stat()).isFile();
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function dirHasVisibleEntries(pathValue: string): Promise<boolean> {
|
|
58
|
+
const entries = await readdir(pathValue).catch(() => [] as string[]);
|
|
59
|
+
return entries.some((entry) => !entry.startsWith("."));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function hasCanonicalSource(rootDir: string): Promise<boolean> {
|
|
63
|
+
for (const relPath of [
|
|
64
|
+
"config.toml",
|
|
65
|
+
"config.local.toml",
|
|
66
|
+
"AGENTS.global.md",
|
|
67
|
+
"AGENTS.override.global.md",
|
|
68
|
+
]) {
|
|
69
|
+
if (await fileExists(join(rootDir, relPath))) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const relPath of [
|
|
75
|
+
"agents",
|
|
76
|
+
"automations",
|
|
77
|
+
"instructions",
|
|
78
|
+
"mcp",
|
|
79
|
+
"rules",
|
|
80
|
+
"skills",
|
|
81
|
+
"snippets",
|
|
82
|
+
"tools",
|
|
83
|
+
]) {
|
|
84
|
+
if (await dirHasVisibleEntries(join(rootDir, relPath))) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function countPendingWritebacks(
|
|
93
|
+
homeDir: string,
|
|
94
|
+
rootDir: string
|
|
95
|
+
): Promise<number> {
|
|
96
|
+
const { listWritebacks } = await import("./ai");
|
|
97
|
+
const rows = await listWritebacks({ homeDir, rootDir }).catch(() => []);
|
|
98
|
+
return rows.filter(
|
|
99
|
+
(row) =>
|
|
100
|
+
row.status !== "dismissed" &&
|
|
101
|
+
row.status !== "promoted" &&
|
|
102
|
+
row.status !== "resolved" &&
|
|
103
|
+
row.status !== "superseded"
|
|
104
|
+
).length;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function countActiveProposals(
|
|
108
|
+
homeDir: string,
|
|
109
|
+
rootDir: string
|
|
110
|
+
): Promise<number> {
|
|
111
|
+
const { listProposals } = await import("./ai");
|
|
112
|
+
const rows = await listProposals({ homeDir, rootDir }).catch(() => []);
|
|
113
|
+
return rows.filter(
|
|
114
|
+
(row) =>
|
|
115
|
+
row.status !== "applied" &&
|
|
116
|
+
row.status !== "failed" &&
|
|
117
|
+
row.status !== "rejected" &&
|
|
118
|
+
row.status !== "superseded"
|
|
119
|
+
).length;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function packageVersion(): Promise<string> {
|
|
123
|
+
const packagePath = join(dirname(import.meta.dir), "package.json");
|
|
124
|
+
const parsed = parseJsonLenient(await Bun.file(packagePath).text());
|
|
125
|
+
if (
|
|
126
|
+
parsed &&
|
|
127
|
+
typeof parsed === "object" &&
|
|
128
|
+
!Array.isArray(parsed) &&
|
|
129
|
+
typeof (parsed as Record<string, unknown>).version === "string"
|
|
130
|
+
) {
|
|
131
|
+
const version = (parsed as Record<string, unknown>).version;
|
|
132
|
+
return typeof version === "string" ? version : "unknown";
|
|
133
|
+
}
|
|
134
|
+
return "unknown";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function buildStatus(opts?: {
|
|
138
|
+
cwd?: string;
|
|
139
|
+
homeDir?: string;
|
|
140
|
+
rootArg?: string;
|
|
141
|
+
scope?: "merged" | "global" | "project";
|
|
142
|
+
}): Promise<FacultStatus> {
|
|
143
|
+
const homeDir = opts?.homeDir ?? process.env.HOME ?? "";
|
|
144
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
145
|
+
const globalRoot = facultRootDir(homeDir);
|
|
146
|
+
const contextRoot = resolveCliContextRoot({
|
|
147
|
+
homeDir,
|
|
148
|
+
cwd,
|
|
149
|
+
rootArg: opts?.rootArg,
|
|
150
|
+
scope: opts?.scope,
|
|
151
|
+
});
|
|
152
|
+
const projectRoot = projectRootFromAiRoot(contextRoot, homeDir);
|
|
153
|
+
const generatedOnlyProjectRoot =
|
|
154
|
+
projectRoot !== null && !(await hasCanonicalSource(contextRoot));
|
|
155
|
+
const indexPath = facultAiIndexPath(homeDir, contextRoot);
|
|
156
|
+
const graphPath = facultAiGraphPath(homeDir, contextRoot);
|
|
157
|
+
const queuePath = facultAiWritebackQueuePath(homeDir, contextRoot);
|
|
158
|
+
const proposalDir = facultAiProposalDir(homeDir, contextRoot);
|
|
159
|
+
const managed = await loadManagedState(homeDir, contextRoot);
|
|
160
|
+
|
|
161
|
+
const issues: StatusIssue[] = [];
|
|
162
|
+
if (generatedOnlyProjectRoot) {
|
|
163
|
+
issues.push({
|
|
164
|
+
severity: "warning",
|
|
165
|
+
code: "project-generated-only",
|
|
166
|
+
message:
|
|
167
|
+
"Project .ai contains generated state only; managed project sync should stay paused until canonical source is restored or initialized.",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!(await fileExists(indexPath))) {
|
|
171
|
+
issues.push({
|
|
172
|
+
severity: "info",
|
|
173
|
+
code: "missing-index",
|
|
174
|
+
message:
|
|
175
|
+
'Generated AI index is missing. Run "fclt index" after canonical source changes.',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (!(await fileExists(graphPath))) {
|
|
179
|
+
issues.push({
|
|
180
|
+
severity: "info",
|
|
181
|
+
code: "missing-graph",
|
|
182
|
+
message: 'Generated AI graph is missing. Run "fclt index" to rebuild it.',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
version: 1,
|
|
188
|
+
packageVersion: await packageVersion(),
|
|
189
|
+
cwd,
|
|
190
|
+
globalRoot,
|
|
191
|
+
contextRoot,
|
|
192
|
+
projectRoot,
|
|
193
|
+
machineStateDir: facultMachineStateDir(homeDir, contextRoot),
|
|
194
|
+
managedTools: Object.keys(managed.tools).sort(),
|
|
195
|
+
generatedOnlyProjectRoot,
|
|
196
|
+
index: {
|
|
197
|
+
path: indexPath,
|
|
198
|
+
exists: await fileExists(indexPath),
|
|
199
|
+
},
|
|
200
|
+
graph: {
|
|
201
|
+
path: graphPath,
|
|
202
|
+
exists: await fileExists(graphPath),
|
|
203
|
+
},
|
|
204
|
+
writeback: {
|
|
205
|
+
queuePath,
|
|
206
|
+
pendingCount: await countPendingWritebacks(homeDir, contextRoot),
|
|
207
|
+
proposalDir,
|
|
208
|
+
proposalCount: await countActiveProposals(homeDir, contextRoot),
|
|
209
|
+
},
|
|
210
|
+
issues,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function printStatus(status: FacultStatus) {
|
|
215
|
+
console.log(`fclt ${status.packageVersion}`);
|
|
216
|
+
console.log(`cwd: ${status.cwd}`);
|
|
217
|
+
console.log(`global root: ${status.globalRoot}`);
|
|
218
|
+
console.log(`context root: ${status.contextRoot}`);
|
|
219
|
+
console.log(`project root: ${status.projectRoot ?? "(none)"}`);
|
|
220
|
+
console.log(`machine state: ${status.machineStateDir}`);
|
|
221
|
+
console.log(`managed tools: ${status.managedTools.join(", ") || "(none)"}`);
|
|
222
|
+
console.log(
|
|
223
|
+
`index: ${status.index.exists ? "present" : "missing"} (${status.index.path})`
|
|
224
|
+
);
|
|
225
|
+
console.log(
|
|
226
|
+
`graph: ${status.graph.exists ? "present" : "missing"} (${status.graph.path})`
|
|
227
|
+
);
|
|
228
|
+
console.log(
|
|
229
|
+
`writeback: ${status.writeback.pendingCount} queued, ${status.writeback.proposalCount} proposals`
|
|
230
|
+
);
|
|
231
|
+
if (status.issues.length > 0) {
|
|
232
|
+
console.log("issues:");
|
|
233
|
+
for (const issue of status.issues) {
|
|
234
|
+
console.log(`- [${issue.severity}] ${issue.code}: ${issue.message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function statusCommand(argv: string[]) {
|
|
240
|
+
const parsed = parseCliContextArgs(argv);
|
|
241
|
+
if (
|
|
242
|
+
parsed.argv.includes("--help") ||
|
|
243
|
+
parsed.argv.includes("-h") ||
|
|
244
|
+
parsed.argv[0] === "help"
|
|
245
|
+
) {
|
|
246
|
+
console.log(`fclt status
|
|
247
|
+
|
|
248
|
+
Usage:
|
|
249
|
+
fclt status [--json] [--global|--project|--root <path>]
|
|
250
|
+
|
|
251
|
+
Print the active canonical root, managed-tool state, generated index/graph state,
|
|
252
|
+
writeback counts, and high-signal sync risks.
|
|
253
|
+
`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const status = await buildStatus({
|
|
258
|
+
rootArg: parsed.rootArg,
|
|
259
|
+
scope: parsed.scope,
|
|
260
|
+
cwd: process.cwd(),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (parsed.argv.includes("--json")) {
|
|
264
|
+
console.log(JSON.stringify(status, null, 2));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
printStatus(status);
|
|
268
|
+
}
|