facult 2.5.2 → 2.7.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 +7 -0
- package/package.json +1 -1
- package/src/audit/agent.ts +26 -24
- package/src/audit/fix.ts +875 -0
- package/src/audit/index.ts +51 -2
- package/src/audit/safe.ts +596 -0
- package/src/audit/static.ts +151 -34
- package/src/audit/status.ts +21 -0
- package/src/audit/suppressions.ts +266 -0
- package/src/audit/tui.ts +784 -174
- package/src/audit/update-index.ts +4 -17
- package/src/cli-ui.ts +375 -0
- package/src/consolidate.ts +151 -55
- package/src/global-docs.ts +38 -12
- package/src/index.ts +511 -239
- package/src/manage.ts +51 -51
- package/src/mcp-config.ts +132 -0
- package/src/remote.ts +387 -117
- package/src/trust.ts +119 -11
- package/src/util/git.ts +95 -0
package/src/manage.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { renderCanonicalText } from "./agents";
|
|
|
15
15
|
import { ensureAiIndexPath } from "./ai-state";
|
|
16
16
|
import { builtinSyncDefaultsEnabled, facultBuiltinPackRoot } from "./builtin";
|
|
17
17
|
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
18
|
+
import { renderBullets, renderCode, renderPage } from "./cli-ui";
|
|
18
19
|
import { contentHash, normalizeText } from "./conflicts";
|
|
19
20
|
import {
|
|
20
21
|
globalDocTargetPaths,
|
|
@@ -31,6 +32,11 @@ import {
|
|
|
31
32
|
type FacultIndex,
|
|
32
33
|
type SkillEntry,
|
|
33
34
|
} from "./index-builder";
|
|
35
|
+
import {
|
|
36
|
+
extractServersObject,
|
|
37
|
+
loadCanonicalMcpState,
|
|
38
|
+
stringifyCanonicalMcpServers,
|
|
39
|
+
} from "./mcp-config";
|
|
34
40
|
import {
|
|
35
41
|
facultMachineStateDir,
|
|
36
42
|
facultRootDir,
|
|
@@ -903,24 +909,6 @@ async function loadEnabledSkillNames({
|
|
|
903
909
|
return entries.map((entry) => entry.name);
|
|
904
910
|
}
|
|
905
911
|
|
|
906
|
-
function extractServersObject(parsed: unknown): Record<string, unknown> | null {
|
|
907
|
-
if (!isPlainObject(parsed)) {
|
|
908
|
-
return null;
|
|
909
|
-
}
|
|
910
|
-
const raw = parsed as Record<string, unknown>;
|
|
911
|
-
const servers =
|
|
912
|
-
(raw.servers as Record<string, unknown> | undefined) ??
|
|
913
|
-
(raw.mcpServers as Record<string, unknown> | undefined) ??
|
|
914
|
-
((raw.mcp as Record<string, unknown> | undefined)?.servers as
|
|
915
|
-
| Record<string, unknown>
|
|
916
|
-
| undefined) ??
|
|
917
|
-
null;
|
|
918
|
-
if (servers && isPlainObject(servers)) {
|
|
919
|
-
return servers;
|
|
920
|
-
}
|
|
921
|
-
return null;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
912
|
function canonicalServerToToolConfig(server: unknown): unknown {
|
|
925
913
|
if (!isPlainObject(server)) {
|
|
926
914
|
return server;
|
|
@@ -970,21 +958,11 @@ async function loadCanonicalServers(rootDir: string): Promise<{
|
|
|
970
958
|
servers: Record<string, unknown>;
|
|
971
959
|
sourcePath: string | null;
|
|
972
960
|
}> {
|
|
973
|
-
const
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
return { servers: {}, sourcePath: null };
|
|
979
|
-
}
|
|
980
|
-
try {
|
|
981
|
-
const txt = await Bun.file(preferred).text();
|
|
982
|
-
const parsed = JSON.parse(txt) as unknown;
|
|
983
|
-
const servers = extractServersObject(parsed) ?? {};
|
|
984
|
-
return { servers, sourcePath: preferred };
|
|
985
|
-
} catch {
|
|
986
|
-
return { servers: {}, sourcePath: preferred };
|
|
987
|
-
}
|
|
961
|
+
const loaded = await loadCanonicalMcpState(rootDir);
|
|
962
|
+
const sourcePath = (await fileExists(loaded.trackedPath))
|
|
963
|
+
? loaded.trackedPath
|
|
964
|
+
: null;
|
|
965
|
+
return { servers: loaded.trackedServers, sourcePath };
|
|
988
966
|
}
|
|
989
967
|
|
|
990
968
|
async function ensureEmptyDir(p: string) {
|
|
@@ -1744,12 +1722,6 @@ async function adoptExistingToolConfig(args: {
|
|
|
1744
1722
|
];
|
|
1745
1723
|
}
|
|
1746
1724
|
|
|
1747
|
-
function normalizeCanonicalMcpServers(
|
|
1748
|
-
servers: Record<string, unknown>
|
|
1749
|
-
): string {
|
|
1750
|
-
return JSON.stringify({ servers }, null, 2);
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
1725
|
async function planExistingMcpAdoption(args: {
|
|
1754
1726
|
rootDir: string;
|
|
1755
1727
|
tool: string;
|
|
@@ -1845,7 +1817,7 @@ async function adoptExistingMcpServers(args: {
|
|
|
1845
1817
|
const canonicalPath =
|
|
1846
1818
|
canonical.sourcePath ?? join(args.rootDir, "mcp", "servers.json");
|
|
1847
1819
|
await ensureDir(dirname(canonicalPath));
|
|
1848
|
-
await Bun.write(canonicalPath,
|
|
1820
|
+
await Bun.write(canonicalPath, stringifyCanonicalMcpServers(merged));
|
|
1849
1821
|
return adopted;
|
|
1850
1822
|
}
|
|
1851
1823
|
|
|
@@ -2044,7 +2016,9 @@ async function planMcpWrite({
|
|
|
2044
2016
|
rootDir: string;
|
|
2045
2017
|
tool: string;
|
|
2046
2018
|
}): Promise<{ needsWrite: boolean; contents: string }> {
|
|
2047
|
-
const { servers } = await
|
|
2019
|
+
const { servers } = await loadCanonicalMcpState(rootDir, {
|
|
2020
|
+
includeLocal: true,
|
|
2021
|
+
});
|
|
2048
2022
|
const filtered = filterServersForTool(servers, tool);
|
|
2049
2023
|
const contents = `${JSON.stringify({ mcpServers: filtered }, null, 2)}\n`;
|
|
2050
2024
|
|
|
@@ -2090,7 +2064,9 @@ async function writeToolMcpConfig({
|
|
|
2090
2064
|
rootDir: string;
|
|
2091
2065
|
tool: string;
|
|
2092
2066
|
}) {
|
|
2093
|
-
const { servers } = await
|
|
2067
|
+
const { servers } = await loadCanonicalMcpState(rootDir, {
|
|
2068
|
+
includeLocal: true,
|
|
2069
|
+
});
|
|
2094
2070
|
const filtered = filterServersForTool(servers, tool);
|
|
2095
2071
|
await ensureDir(dirname(mcpConfigPath));
|
|
2096
2072
|
await Bun.write(
|
|
@@ -3667,11 +3643,20 @@ export async function managedCommand(argv: string[] = []) {
|
|
|
3667
3643
|
parsed.argv.includes("-h") ||
|
|
3668
3644
|
parsed.argv[0] === "help"
|
|
3669
3645
|
) {
|
|
3670
|
-
console.log(
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3646
|
+
console.log(
|
|
3647
|
+
renderPage({
|
|
3648
|
+
title: "fclt managed",
|
|
3649
|
+
subtitle: "List tools currently in managed mode.",
|
|
3650
|
+
sections: [
|
|
3651
|
+
{
|
|
3652
|
+
title: "Usage",
|
|
3653
|
+
lines: renderBullets([
|
|
3654
|
+
renderCode("fclt managed [--root PATH|--global|--project]"),
|
|
3655
|
+
]),
|
|
3656
|
+
},
|
|
3657
|
+
],
|
|
3658
|
+
})
|
|
3659
|
+
);
|
|
3675
3660
|
return;
|
|
3676
3661
|
}
|
|
3677
3662
|
const tools = await listManagedTools({
|
|
@@ -3682,12 +3667,27 @@ Usage:
|
|
|
3682
3667
|
}),
|
|
3683
3668
|
});
|
|
3684
3669
|
if (!tools.length) {
|
|
3685
|
-
console.log(
|
|
3670
|
+
console.log(
|
|
3671
|
+
renderPage({
|
|
3672
|
+
title: "fclt managed",
|
|
3673
|
+
subtitle: "No managed tools.",
|
|
3674
|
+
sections: [],
|
|
3675
|
+
})
|
|
3676
|
+
);
|
|
3686
3677
|
return;
|
|
3687
3678
|
}
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3679
|
+
console.log(
|
|
3680
|
+
renderPage({
|
|
3681
|
+
title: "fclt managed",
|
|
3682
|
+
subtitle: `${tools.length} managed tool${tools.length === 1 ? "" : "s"}`,
|
|
3683
|
+
sections: [
|
|
3684
|
+
{
|
|
3685
|
+
title: "Tools",
|
|
3686
|
+
lines: renderBullets(tools),
|
|
3687
|
+
},
|
|
3688
|
+
],
|
|
3689
|
+
})
|
|
3690
|
+
);
|
|
3691
3691
|
}
|
|
3692
3692
|
|
|
3693
3693
|
export async function syncCommand(argv: string[]) {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { basename, dirname, join } from "node:path";
|
|
2
|
+
import { parseJsonLenient } from "./util/json";
|
|
3
|
+
|
|
4
|
+
const INLINE_SECRET_PLACEHOLDER_VALUES = new Set(["<set-me>", "<redacted>"]);
|
|
5
|
+
const INLINE_SECRET_ENV_REF_RE = /^\$\{[^}]+\}$/;
|
|
6
|
+
|
|
7
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
8
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isInlineMcpSecretValue(value: unknown): value is string {
|
|
12
|
+
if (typeof value !== "string") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
if (!trimmed) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (INLINE_SECRET_PLACEHOLDER_VALUES.has(trimmed)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (INLINE_SECRET_ENV_REF_RE.test(trimmed)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function extractServersObject(
|
|
29
|
+
parsed: unknown
|
|
30
|
+
): Record<string, unknown> | null {
|
|
31
|
+
if (!isPlainObject(parsed)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const raw = parsed as Record<string, unknown>;
|
|
35
|
+
const servers =
|
|
36
|
+
(raw.servers as Record<string, unknown> | undefined) ??
|
|
37
|
+
(raw.mcpServers as Record<string, unknown> | undefined) ??
|
|
38
|
+
((raw.mcp as Record<string, unknown> | undefined)?.servers as
|
|
39
|
+
| Record<string, unknown>
|
|
40
|
+
| undefined) ??
|
|
41
|
+
null;
|
|
42
|
+
if (servers && isPlainObject(servers)) {
|
|
43
|
+
return servers;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function mergeJsonObjects(
|
|
49
|
+
base: Record<string, unknown>,
|
|
50
|
+
override: Record<string, unknown>
|
|
51
|
+
): Record<string, unknown> {
|
|
52
|
+
const out: Record<string, unknown> = { ...base };
|
|
53
|
+
for (const [key, value] of Object.entries(override)) {
|
|
54
|
+
const current = out[key];
|
|
55
|
+
if (isPlainObject(current) && isPlainObject(value)) {
|
|
56
|
+
out[key] = mergeJsonObjects(current, value);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
out[key] = value;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function canonicalMcpTrackedPath(rootDir: string): string {
|
|
65
|
+
return join(rootDir, "mcp", "servers.json");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function canonicalMcpPaths(
|
|
69
|
+
rootDir: string,
|
|
70
|
+
trackedPath?: string | null
|
|
71
|
+
): { trackedPath: string; localPath: string } {
|
|
72
|
+
const resolvedTrackedPath = trackedPath ?? canonicalMcpTrackedPath(rootDir);
|
|
73
|
+
const fileName = basename(resolvedTrackedPath);
|
|
74
|
+
const localFileName =
|
|
75
|
+
fileName === "mcp.json" ? "mcp.local.json" : "servers.local.json";
|
|
76
|
+
return {
|
|
77
|
+
trackedPath: resolvedTrackedPath,
|
|
78
|
+
localPath: join(dirname(resolvedTrackedPath), localFileName),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function loadServersFromPath(
|
|
83
|
+
path: string
|
|
84
|
+
): Promise<Record<string, unknown>> {
|
|
85
|
+
const file = Bun.file(path);
|
|
86
|
+
if (!(await file.exists())) {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const parsed = parseJsonLenient(await file.text());
|
|
91
|
+
return extractServersObject(parsed) ?? {};
|
|
92
|
+
} catch {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function loadCanonicalMcpState(
|
|
98
|
+
rootDir: string,
|
|
99
|
+
opts?: { includeLocal?: boolean }
|
|
100
|
+
): Promise<{
|
|
101
|
+
trackedPath: string;
|
|
102
|
+
localPath: string;
|
|
103
|
+
trackedServers: Record<string, unknown>;
|
|
104
|
+
localServers: Record<string, unknown>;
|
|
105
|
+
servers: Record<string, unknown>;
|
|
106
|
+
}> {
|
|
107
|
+
const serversPath = join(rootDir, "mcp", "servers.json");
|
|
108
|
+
const mcpPath = join(rootDir, "mcp", "mcp.json");
|
|
109
|
+
|
|
110
|
+
const trackedPath = (await Bun.file(serversPath).exists())
|
|
111
|
+
? serversPath
|
|
112
|
+
: (await Bun.file(mcpPath).exists())
|
|
113
|
+
? mcpPath
|
|
114
|
+
: serversPath;
|
|
115
|
+
const { localPath } = canonicalMcpPaths(rootDir, trackedPath);
|
|
116
|
+
const trackedServers = await loadServersFromPath(trackedPath);
|
|
117
|
+
const localServers =
|
|
118
|
+
opts?.includeLocal === true ? await loadServersFromPath(localPath) : {};
|
|
119
|
+
return {
|
|
120
|
+
trackedPath,
|
|
121
|
+
localPath,
|
|
122
|
+
trackedServers,
|
|
123
|
+
localServers,
|
|
124
|
+
servers: mergeJsonObjects(trackedServers, localServers),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function stringifyCanonicalMcpServers(
|
|
129
|
+
servers: Record<string, unknown>
|
|
130
|
+
): string {
|
|
131
|
+
return `${JSON.stringify({ servers }, null, 2)}\n`;
|
|
132
|
+
}
|