facult 1.2.1 → 2.0.1
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 +215 -207
- package/bin/facult.cjs +1 -268
- package/bin/fclt.cjs +264 -0
- package/package.json +6 -3
- package/src/ai-state.ts +49 -1
- package/src/ai.ts +23 -23
- package/src/audit/agent.ts +3 -3
- package/src/audit/index.ts +11 -11
- package/src/audit/static.ts +4 -4
- package/src/audit/tui.ts +14 -14
- package/src/autosync.ts +129 -42
- package/src/consolidate.ts +41 -36
- package/src/doctor.ts +236 -20
- package/src/enable-disable.ts +7 -7
- package/src/graph-query.ts +1 -1
- package/src/index-builder.ts +2 -2
- package/src/index.ts +53 -53
- package/src/manage.ts +36 -20
- package/src/migrate.ts +9 -9
- package/src/paths.ts +132 -74
- package/src/quarantine.ts +2 -1
- package/src/query.ts +4 -4
- package/src/remote-source-policy.ts +8 -8
- package/src/remote-sources.ts +18 -4
- package/src/remote.ts +26 -26
- package/src/scan.ts +15 -13
- package/src/schema.ts +1 -1
- package/src/self-update.ts +71 -25
- package/src/snippets-cli.ts +6 -6
- package/src/source-trust.ts +50 -42
- package/src/trust-list.ts +19 -10
- package/src/trust.ts +8 -8
- package/src/tui.ts +1 -1
package/src/index.ts
CHANGED
|
@@ -96,50 +96,50 @@ interface GraphCommandOptions {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function printHelp() {
|
|
99
|
-
console.log(`
|
|
99
|
+
console.log(`fclt — manage canonical AI capabilities, sync surfaces, and evolution state
|
|
100
100
|
|
|
101
101
|
Usage:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
102
|
+
fclt scan [--json] [--show-duplicates] [--tui] [--from <path>]
|
|
103
|
+
fclt audit [--from <path>]
|
|
104
|
+
fclt audit --non-interactive [name|mcp:<name>] [--severity <level>] [--rules <path>] [--from <path>] [--json]
|
|
105
|
+
fclt audit --non-interactive [name|mcp:<name>] --with <claude|codex> [--from <path>] [--max-items <n|all>] [--json]
|
|
106
|
+
fclt migrate [--from <path>] [--dry-run] [--move] [--write-config]
|
|
107
|
+
fclt doctor [--repair]
|
|
108
|
+
fclt consolidate [--force] [--auto <mode>] [scan options]
|
|
109
|
+
fclt index [--force]
|
|
110
|
+
fclt list [skills|mcp|agents|snippets|instructions] [--enabled-for TOOL] [--untrusted] [--flagged] [--pending] [--json]
|
|
111
|
+
fclt show <name>
|
|
112
|
+
fclt show mcp:<name> [--show-secrets]
|
|
113
|
+
fclt show instruction:<name>
|
|
114
|
+
fclt find <query> [--json]
|
|
115
|
+
fclt graph <show|deps|dependents> <asset> [--json]
|
|
116
|
+
fclt ai <writeback|evolve> [args...]
|
|
117
|
+
fclt adapters
|
|
118
|
+
fclt trust <name> [moreNames...]
|
|
119
|
+
fclt untrust <name> [moreNames...]
|
|
120
|
+
fclt manage <tool>
|
|
121
|
+
fclt unmanage <tool>
|
|
122
|
+
fclt managed
|
|
123
|
+
fclt enable <name> [moreNames...] [--for <tools>]
|
|
124
|
+
fclt disable <name> [moreNames...] [--for <tools>]
|
|
125
|
+
fclt sync [tool] [--dry-run]
|
|
126
|
+
fclt autosync <cmd> [args...]
|
|
127
|
+
fclt search <query> [--index <name>] [--limit <n>]
|
|
128
|
+
fclt install <index:item> [--as <name>] [--dry-run] [--force] [--strict-source-trust]
|
|
129
|
+
fclt update [--apply] [--strict-source-trust]
|
|
130
|
+
fclt update --self [--version <x.y.z|latest>] [--dry-run]
|
|
131
|
+
fclt self-update [--version <x.y.z|latest>] [--dry-run]
|
|
132
|
+
fclt verify-source <name> [--json]
|
|
133
|
+
fclt sources <cmd> [args...]
|
|
134
|
+
fclt templates <cmd> [args...]
|
|
135
|
+
fclt snippets <cmd> [args...]
|
|
136
|
+
fclt --show-duplicates
|
|
137
137
|
|
|
138
138
|
Commands:
|
|
139
139
|
scan Scan common config locations (Cursor, Claude, Claude Desktop, etc.)
|
|
140
140
|
audit Security audits (interactive by default; use --non-interactive for scripts)
|
|
141
141
|
migrate Copy/move a legacy canonical store to the current canonical root
|
|
142
|
-
doctor Inspect and repair local
|
|
142
|
+
doctor Inspect and repair local fclt state
|
|
143
143
|
consolidate Deduplicate and copy skills + MCP configs (interactive or --auto)
|
|
144
144
|
index Build a queryable index from the canonical store (see FACULT_ROOT_DIR)
|
|
145
145
|
list List indexed skills, MCP servers, agents, snippets, or instructions
|
|
@@ -160,7 +160,7 @@ Commands:
|
|
|
160
160
|
search Search remote indices (builtin + provider aliases + configured)
|
|
161
161
|
install Install an item from a remote index
|
|
162
162
|
update Check/apply updates for remotely installed items
|
|
163
|
-
self-update Update
|
|
163
|
+
self-update Update fclt itself based on install method
|
|
164
164
|
verify-source Verify source trust and manifest integrity/signature status
|
|
165
165
|
sources Manage source trust policy for remote indices
|
|
166
166
|
templates Scaffold DX-first templates (skills/instructions/MCP/snippets)
|
|
@@ -177,7 +177,7 @@ Options:
|
|
|
177
177
|
--from-max-results (scan) Max discovered paths per --from root before truncating
|
|
178
178
|
--non-interactive (audit) Run static/agent audit non-interactively (for scripts)
|
|
179
179
|
--severity Minimum severity to include in audit output (low|medium|high|critical)
|
|
180
|
-
--rules Path to an audit rules YAML file (default: ~/.facult/audit-rules.yaml)
|
|
180
|
+
--rules Path to an audit rules YAML file (default: ~/.ai/.facult/audit-rules.yaml)
|
|
181
181
|
--with (audit) Agent tool: claude|codex
|
|
182
182
|
--max-items (audit) Max items to send to the agent (n|all)
|
|
183
183
|
--force Re-copy items already consolidated OR rebuild index from scratch
|
|
@@ -203,10 +203,10 @@ Options:
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
function printListHelp() {
|
|
206
|
-
console.log(`
|
|
206
|
+
console.log(`fclt list — list indexed entries from the canonical store
|
|
207
207
|
|
|
208
208
|
Usage:
|
|
209
|
-
|
|
209
|
+
fclt list [skills|mcp|agents|snippets|instructions] [options]
|
|
210
210
|
|
|
211
211
|
Options:
|
|
212
212
|
--enabled-for TOOL Only include entries enabled for a tool
|
|
@@ -223,12 +223,12 @@ Options:
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
function printShowHelp() {
|
|
226
|
-
console.log(`
|
|
226
|
+
console.log(`fclt show — show a single indexed entry (and file contents)
|
|
227
227
|
|
|
228
228
|
Usage:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
fclt show <name>
|
|
230
|
+
fclt show mcp:<name> [--show-secrets]
|
|
231
|
+
fclt show instruction:<name>
|
|
232
232
|
|
|
233
233
|
Options:
|
|
234
234
|
--show-secrets (mcp) Print raw secret values (unsafe)
|
|
@@ -241,10 +241,10 @@ Options:
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
function printFindHelp() {
|
|
244
|
-
console.log(`
|
|
244
|
+
console.log(`fclt find — search local indexed capabilities across asset types
|
|
245
245
|
|
|
246
246
|
Usage:
|
|
247
|
-
|
|
247
|
+
fclt find <query> [--json]
|
|
248
248
|
|
|
249
249
|
Options:
|
|
250
250
|
--root PATH Select a canonical .ai root explicitly
|
|
@@ -257,12 +257,12 @@ Options:
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
function printGraphHelp() {
|
|
260
|
-
console.log(`
|
|
260
|
+
console.log(`fclt graph — inspect explicit capability graph nodes and relations
|
|
261
261
|
|
|
262
262
|
Usage:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
fclt graph show <asset> [--json]
|
|
264
|
+
fclt graph deps <asset> [--json]
|
|
265
|
+
fclt graph dependents <asset> [--json]
|
|
266
266
|
|
|
267
267
|
Options:
|
|
268
268
|
--root PATH Select a canonical .ai root explicitly
|
|
@@ -890,7 +890,7 @@ async function graphCommand(argv: string[]) {
|
|
|
890
890
|
function adaptersCommand(argv: string[]) {
|
|
891
891
|
if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
|
|
892
892
|
console.log(
|
|
893
|
-
"
|
|
893
|
+
"fclt adapters — list registered tool adapters\n\nUsage:\n fclt adapters\n"
|
|
894
894
|
);
|
|
895
895
|
return;
|
|
896
896
|
}
|
|
@@ -912,7 +912,7 @@ async function main(argv: string[]) {
|
|
|
912
912
|
return;
|
|
913
913
|
}
|
|
914
914
|
|
|
915
|
-
// Convenience: allow `
|
|
915
|
+
// Convenience: allow `fclt --show-duplicates` as shorthand for `fclt scan --show-duplicates`.
|
|
916
916
|
if (cmd === "--show-duplicates") {
|
|
917
917
|
await scanCommand([cmd, ...rest]);
|
|
918
918
|
return;
|
package/src/manage.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import {
|
|
35
35
|
facultGeneratedStateDir,
|
|
36
36
|
facultRootDir,
|
|
37
|
+
legacyFacultStateDirForRoot,
|
|
37
38
|
projectRootFromAiRoot,
|
|
38
39
|
} from "./paths";
|
|
39
40
|
|
|
@@ -295,22 +296,37 @@ export function managedStatePathForRoot(
|
|
|
295
296
|
return join(facultGeneratedStateDir({ home, rootDir }), "managed.json");
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
function legacyManagedStatePathForRoot(
|
|
300
|
+
home: string = homedir(),
|
|
301
|
+
rootDir?: string
|
|
302
|
+
): string {
|
|
303
|
+
return join(
|
|
304
|
+
legacyFacultStateDirForRoot(rootDir ?? facultRootDir(home), home),
|
|
305
|
+
"managed.json"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
298
309
|
export async function loadManagedState(
|
|
299
310
|
home: string = homedir(),
|
|
300
311
|
rootDir?: string
|
|
301
312
|
): Promise<ManagedState> {
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
313
|
+
const candidates = [
|
|
314
|
+
managedStatePathForRoot(home, rootDir),
|
|
315
|
+
legacyManagedStatePathForRoot(home, rootDir),
|
|
316
|
+
];
|
|
317
|
+
for (const p of candidates) {
|
|
318
|
+
if (!(await fileExists(p))) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
const txt = await Bun.file(p).text();
|
|
323
|
+
const data = JSON.parse(txt) as Partial<ManagedState> | null;
|
|
324
|
+
if (data?.version === MANAGED_VERSION && data.tools) {
|
|
325
|
+
return { version: MANAGED_VERSION, tools: data.tools };
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
// fallthrough
|
|
311
329
|
}
|
|
312
|
-
} catch {
|
|
313
|
-
// fallthrough
|
|
314
330
|
}
|
|
315
331
|
return { version: MANAGED_VERSION, tools: {} };
|
|
316
332
|
}
|
|
@@ -1830,7 +1846,7 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
1830
1846
|
.map((item) => formatManagedItem(item))
|
|
1831
1847
|
.join(", ")}`
|
|
1832
1848
|
: null,
|
|
1833
|
-
`Run "
|
|
1849
|
+
`Run "fclt manage ${tool} --dry-run" to review the plan, then rerun with "--adopt-existing"`,
|
|
1834
1850
|
existingImportPlan.conflicts.length > 0
|
|
1835
1851
|
? ' and "--existing-conflicts keep-canonical|keep-existing".'
|
|
1836
1852
|
: ".",
|
|
@@ -3001,10 +3017,10 @@ export async function manageCommand(argv: string[]) {
|
|
|
3001
3017
|
const parsed = parseCliContextArgs(argv);
|
|
3002
3018
|
const args = [...parsed.argv];
|
|
3003
3019
|
if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
|
|
3004
|
-
console.log(`
|
|
3020
|
+
console.log(`fclt manage — enter managed mode for a tool (backup + symlinks + MCP generation)
|
|
3005
3021
|
|
|
3006
3022
|
Usage:
|
|
3007
|
-
|
|
3023
|
+
fclt manage <tool> [--dry-run] [--adopt-existing] [--existing-conflicts keep-canonical|keep-existing] [--builtin-conflicts overwrite] [--root PATH|--global|--project]
|
|
3008
3024
|
`);
|
|
3009
3025
|
return;
|
|
3010
3026
|
}
|
|
@@ -3089,10 +3105,10 @@ export async function unmanageCommand(argv: string[]) {
|
|
|
3089
3105
|
parsed.argv.includes("-h") ||
|
|
3090
3106
|
parsed.argv[0] === "help"
|
|
3091
3107
|
) {
|
|
3092
|
-
console.log(`
|
|
3108
|
+
console.log(`fclt unmanage — exit managed mode for a tool (restore backups)
|
|
3093
3109
|
|
|
3094
3110
|
Usage:
|
|
3095
|
-
|
|
3111
|
+
fclt unmanage <tool> [--root PATH|--global|--project]
|
|
3096
3112
|
`);
|
|
3097
3113
|
return;
|
|
3098
3114
|
}
|
|
@@ -3124,10 +3140,10 @@ export async function managedCommand(argv: string[] = []) {
|
|
|
3124
3140
|
parsed.argv.includes("-h") ||
|
|
3125
3141
|
parsed.argv[0] === "help"
|
|
3126
3142
|
) {
|
|
3127
|
-
console.log(`
|
|
3143
|
+
console.log(`fclt managed — list tools currently in managed mode
|
|
3128
3144
|
|
|
3129
3145
|
Usage:
|
|
3130
|
-
|
|
3146
|
+
fclt managed [--root PATH|--global|--project]
|
|
3131
3147
|
`);
|
|
3132
3148
|
return;
|
|
3133
3149
|
}
|
|
@@ -3154,10 +3170,10 @@ export async function syncCommand(argv: string[]) {
|
|
|
3154
3170
|
parsed.argv.includes("-h") ||
|
|
3155
3171
|
parsed.argv[0] === "help"
|
|
3156
3172
|
) {
|
|
3157
|
-
console.log(`
|
|
3173
|
+
console.log(`fclt sync — sync managed tools with canonical state
|
|
3158
3174
|
|
|
3159
3175
|
Usage:
|
|
3160
|
-
|
|
3176
|
+
fclt sync [tool] [--dry-run] [--builtin-conflicts overwrite] [--root PATH|--global|--project]
|
|
3161
3177
|
|
|
3162
3178
|
Options:
|
|
3163
3179
|
--dry-run Show what would change
|
package/src/migrate.ts
CHANGED
|
@@ -11,23 +11,24 @@ import {
|
|
|
11
11
|
} from "node:fs/promises";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { basename, dirname, join, resolve } from "node:path";
|
|
14
|
+
import { facultConfigPath, preferredGlobalAiRoot } from "./paths";
|
|
14
15
|
|
|
15
16
|
function printHelp() {
|
|
16
|
-
console.log(`
|
|
17
|
+
console.log(`fclt migrate — migrate a legacy canonical store to the fclt path
|
|
17
18
|
|
|
18
19
|
Usage:
|
|
19
|
-
|
|
20
|
+
fclt migrate [--from <path>] [--dry-run] [--move] [--write-config]
|
|
20
21
|
|
|
21
22
|
What it does:
|
|
22
23
|
- Auto-detects a legacy store under ~/agents/ (or use --from)
|
|
23
|
-
- Copies it to
|
|
24
|
+
- Copies it to ~/.ai (default, safe)
|
|
24
25
|
- Or moves it with --move (destructive; removes the legacy directory)
|
|
25
26
|
|
|
26
27
|
Options:
|
|
27
28
|
--from Path to a legacy store root directory
|
|
28
29
|
--dry-run Print what would happen without changing anything
|
|
29
30
|
--move Rename legacy dir to the new location instead of copying
|
|
30
|
-
--write-config Write ~/.facult/config.json to pin rootDir to
|
|
31
|
+
--write-config Write ~/.ai/.facult/config.json to pin rootDir to ~/.ai
|
|
31
32
|
`);
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -188,9 +189,8 @@ export async function migrateCommand(argv: string[]) {
|
|
|
188
189
|
const writeConfig = argv.includes("--write-config");
|
|
189
190
|
|
|
190
191
|
const home = homedir();
|
|
191
|
-
const dest =
|
|
192
|
-
const
|
|
193
|
-
const configPath = join(stateDir, "config.json");
|
|
192
|
+
const dest = preferredGlobalAiRoot(home);
|
|
193
|
+
const configPath = facultConfigPath(home);
|
|
194
194
|
|
|
195
195
|
let legacy: string | null = null;
|
|
196
196
|
try {
|
|
@@ -239,7 +239,7 @@ export async function migrateCommand(argv: string[]) {
|
|
|
239
239
|
return;
|
|
240
240
|
}
|
|
241
241
|
console.error(
|
|
242
|
-
`Destination exists but does not look like a
|
|
242
|
+
`Destination exists but does not look like a fclt store: ${dest}`
|
|
243
243
|
);
|
|
244
244
|
process.exitCode = 1;
|
|
245
245
|
return;
|
|
@@ -264,7 +264,7 @@ export async function migrateCommand(argv: string[]) {
|
|
|
264
264
|
if (dryRun) {
|
|
265
265
|
console.log(`[dry-run] Would write ${configPath} with rootDir=${dest}`);
|
|
266
266
|
} else {
|
|
267
|
-
await mkdir(
|
|
267
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
268
268
|
await Bun.write(configPath, JSON.stringify({ rootDir: dest }, null, 2));
|
|
269
269
|
console.log(`Wrote ${configPath}`);
|
|
270
270
|
}
|
package/src/paths.ts
CHANGED
|
@@ -7,13 +7,13 @@ export interface FacultConfig {
|
|
|
7
7
|
/**
|
|
8
8
|
* Override the canonical root directory.
|
|
9
9
|
*
|
|
10
|
-
* This is where
|
|
10
|
+
* This is where fclt stores the consolidated "canonical" skill + MCP state
|
|
11
11
|
* (skills/, mcp/, snippets/, index.json, ...).
|
|
12
12
|
*/
|
|
13
13
|
rootDir?: string;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Default scan roots (equivalent to passing `
|
|
16
|
+
* Default scan roots (equivalent to passing `fclt scan --from <path>`).
|
|
17
17
|
* Example: ["~", "~/dev", "~/work"]
|
|
18
18
|
*/
|
|
19
19
|
scanFrom?: string[];
|
|
@@ -83,6 +83,22 @@ function legacyPreferredRoot(home: string): string {
|
|
|
83
83
|
return join(home, "agents", ".facult");
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
export function preferredGlobalAiRoot(home: string = defaultHomeDir()): string {
|
|
87
|
+
return join(home, ".ai");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function preferredGlobalFacultStateDir(
|
|
91
|
+
home: string = defaultHomeDir()
|
|
92
|
+
): string {
|
|
93
|
+
return join(preferredGlobalAiRoot(home), ".facult");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function legacyExternalFacultStateDir(
|
|
97
|
+
home: string = defaultHomeDir()
|
|
98
|
+
): string {
|
|
99
|
+
return join(home, ".facult");
|
|
100
|
+
}
|
|
101
|
+
|
|
86
102
|
function isLegacyConfiguredRoot(root: string, home: string): boolean {
|
|
87
103
|
return resolve(root) === resolve(legacyPreferredRoot(home));
|
|
88
104
|
}
|
|
@@ -91,7 +107,7 @@ function looksLikeFacultRoot(root: string): boolean {
|
|
|
91
107
|
if (!dirExists(root)) {
|
|
92
108
|
return false;
|
|
93
109
|
}
|
|
94
|
-
// Heuristic: treat as a
|
|
110
|
+
// Heuristic: treat as a fclt store if it contains something we'd create.
|
|
95
111
|
return (
|
|
96
112
|
dirExists(join(root, "rules")) ||
|
|
97
113
|
dirExists(join(root, "instructions")) ||
|
|
@@ -132,8 +148,42 @@ function detectLegacyStoreUnderAgents(home: string): string | null {
|
|
|
132
148
|
return candidates.length === 1 ? (candidates[0] ?? null) : null;
|
|
133
149
|
}
|
|
134
150
|
|
|
135
|
-
export function
|
|
136
|
-
|
|
151
|
+
export function legacyFacultStateDirForRoot(
|
|
152
|
+
rootDir: string,
|
|
153
|
+
home: string = defaultHomeDir()
|
|
154
|
+
): string {
|
|
155
|
+
const projectRoot = projectRootFromAiRoot(rootDir, home);
|
|
156
|
+
return projectRoot
|
|
157
|
+
? join(projectRoot, ".facult")
|
|
158
|
+
: legacyExternalFacultStateDir(home);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function shouldUsePreferredGlobalStateDir(
|
|
162
|
+
rootDir: string,
|
|
163
|
+
home: string
|
|
164
|
+
): boolean {
|
|
165
|
+
const resolved = resolve(rootDir);
|
|
166
|
+
if (projectRootFromAiRoot(resolved, home)) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (resolved === resolve(preferredGlobalAiRoot(home))) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
if (resolved === resolve(legacyPreferredRoot(home))) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
return resolved.startsWith(`${resolve(join(home, "agents"))}/`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function facultStateDir(
|
|
179
|
+
home: string = defaultHomeDir(),
|
|
180
|
+
rootDir?: string
|
|
181
|
+
): string {
|
|
182
|
+
const resolvedRoot = rootDir ?? facultRootDir(home);
|
|
183
|
+
if (shouldUsePreferredGlobalStateDir(resolvedRoot, home)) {
|
|
184
|
+
return preferredGlobalFacultStateDir(home);
|
|
185
|
+
}
|
|
186
|
+
return join(resolvedRoot, ".facult");
|
|
137
187
|
}
|
|
138
188
|
|
|
139
189
|
export function projectRootFromAiRoot(
|
|
@@ -168,9 +218,7 @@ export function facultGeneratedStateDir(args?: {
|
|
|
168
218
|
rootDir?: string;
|
|
169
219
|
}): string {
|
|
170
220
|
const home = args?.home ?? defaultHomeDir();
|
|
171
|
-
|
|
172
|
-
const projectRoot = rootDir ? projectRootFromAiRoot(rootDir, home) : null;
|
|
173
|
-
return projectRoot ? join(projectRoot, ".facult") : facultStateDir(home);
|
|
221
|
+
return facultStateDir(home, args?.rootDir);
|
|
174
222
|
}
|
|
175
223
|
|
|
176
224
|
export function facultAiStateDir(
|
|
@@ -198,10 +246,12 @@ export function facultAiRuntimeScopeDir(
|
|
|
198
246
|
home: string = defaultHomeDir(),
|
|
199
247
|
rootDir?: string
|
|
200
248
|
): string {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
249
|
+
return join(
|
|
250
|
+
facultAiStateDir(home, rootDir),
|
|
251
|
+
projectRootFromAiRoot(rootDir ?? facultRootDir(home), home)
|
|
252
|
+
? "project"
|
|
253
|
+
: "global"
|
|
254
|
+
);
|
|
205
255
|
}
|
|
206
256
|
|
|
207
257
|
export function facultAiJournalPath(
|
|
@@ -241,87 +291,95 @@ export function facultAiDraftDir(
|
|
|
241
291
|
}
|
|
242
292
|
|
|
243
293
|
export function facultConfigPath(home: string = defaultHomeDir()): string {
|
|
244
|
-
return join(
|
|
294
|
+
return join(preferredGlobalFacultStateDir(home), "config.json");
|
|
245
295
|
}
|
|
246
296
|
|
|
247
297
|
export function readFacultConfig(
|
|
248
298
|
home: string = defaultHomeDir()
|
|
249
299
|
): FacultConfig | null {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
300
|
+
const candidates = [
|
|
301
|
+
facultConfigPath(home),
|
|
302
|
+
join(legacyExternalFacultStateDir(home), "config.json"),
|
|
303
|
+
];
|
|
254
304
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (!isPlainObject(parsed)) {
|
|
259
|
-
return null;
|
|
305
|
+
for (const p of candidates) {
|
|
306
|
+
if (!(isSafePathString(p) && fileExists(p))) {
|
|
307
|
+
continue;
|
|
260
308
|
}
|
|
261
|
-
const rootDir =
|
|
262
|
-
typeof parsed.rootDir === "string" ? parsed.rootDir : undefined;
|
|
263
|
-
|
|
264
|
-
const scanFromRaw = (parsed as Record<string, unknown>).scanFrom;
|
|
265
|
-
const scanFrom = Array.isArray(scanFromRaw)
|
|
266
|
-
? scanFromRaw
|
|
267
|
-
.filter((v) => typeof v === "string")
|
|
268
|
-
.map((v) => v.trim())
|
|
269
|
-
.filter(Boolean)
|
|
270
|
-
: undefined;
|
|
271
|
-
|
|
272
|
-
const scanFromIgnoreRaw = (parsed as Record<string, unknown>)
|
|
273
|
-
.scanFromIgnore;
|
|
274
|
-
const scanFromIgnore = Array.isArray(scanFromIgnoreRaw)
|
|
275
|
-
? scanFromIgnoreRaw
|
|
276
|
-
.filter((v) => typeof v === "string")
|
|
277
|
-
.map((v) => v.trim())
|
|
278
|
-
.filter(Boolean)
|
|
279
|
-
: undefined;
|
|
280
|
-
|
|
281
|
-
const scanFromNoDefaultIgnore =
|
|
282
|
-
typeof (parsed as Record<string, unknown>).scanFromNoDefaultIgnore ===
|
|
283
|
-
"boolean"
|
|
284
|
-
? ((parsed as Record<string, unknown>)
|
|
285
|
-
.scanFromNoDefaultIgnore as boolean)
|
|
286
|
-
: undefined;
|
|
287
309
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
310
|
+
try {
|
|
311
|
+
const txt = readFileSync(p, "utf8");
|
|
312
|
+
const parsed = parseJsonLenient(txt) as unknown;
|
|
313
|
+
if (!isPlainObject(parsed)) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const rootDir =
|
|
317
|
+
typeof parsed.rootDir === "string" ? parsed.rootDir : undefined;
|
|
318
|
+
|
|
319
|
+
const scanFromRaw = (parsed as Record<string, unknown>).scanFrom;
|
|
320
|
+
const scanFrom = Array.isArray(scanFromRaw)
|
|
321
|
+
? scanFromRaw
|
|
322
|
+
.filter((v) => typeof v === "string")
|
|
323
|
+
.map((v) => v.trim())
|
|
324
|
+
.filter(Boolean)
|
|
295
325
|
: undefined;
|
|
296
326
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
327
|
+
const scanFromIgnoreRaw = (parsed as Record<string, unknown>)
|
|
328
|
+
.scanFromIgnore;
|
|
329
|
+
const scanFromIgnore = Array.isArray(scanFromIgnoreRaw)
|
|
330
|
+
? scanFromIgnoreRaw
|
|
331
|
+
.filter((v) => typeof v === "string")
|
|
332
|
+
.map((v) => v.trim())
|
|
333
|
+
.filter(Boolean)
|
|
304
334
|
: undefined;
|
|
305
335
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
336
|
+
const scanFromNoDefaultIgnore =
|
|
337
|
+
typeof (parsed as Record<string, unknown>).scanFromNoDefaultIgnore ===
|
|
338
|
+
"boolean"
|
|
339
|
+
? ((parsed as Record<string, unknown>)
|
|
340
|
+
.scanFromNoDefaultIgnore as boolean)
|
|
341
|
+
: undefined;
|
|
342
|
+
|
|
343
|
+
const scanFromMaxVisitsRaw = (parsed as Record<string, unknown>)
|
|
344
|
+
.scanFromMaxVisits;
|
|
345
|
+
const scanFromMaxVisits =
|
|
346
|
+
typeof scanFromMaxVisitsRaw === "number" &&
|
|
347
|
+
Number.isFinite(scanFromMaxVisitsRaw) &&
|
|
348
|
+
scanFromMaxVisitsRaw > 0
|
|
349
|
+
? Math.floor(scanFromMaxVisitsRaw)
|
|
350
|
+
: undefined;
|
|
351
|
+
|
|
352
|
+
const scanFromMaxResultsRaw = (parsed as Record<string, unknown>)
|
|
353
|
+
.scanFromMaxResults;
|
|
354
|
+
const scanFromMaxResults =
|
|
355
|
+
typeof scanFromMaxResultsRaw === "number" &&
|
|
356
|
+
Number.isFinite(scanFromMaxResultsRaw) &&
|
|
357
|
+
scanFromMaxResultsRaw > 0
|
|
358
|
+
? Math.floor(scanFromMaxResultsRaw)
|
|
359
|
+
: undefined;
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
rootDir,
|
|
363
|
+
scanFrom,
|
|
364
|
+
scanFromIgnore,
|
|
365
|
+
scanFromNoDefaultIgnore,
|
|
366
|
+
scanFromMaxVisits,
|
|
367
|
+
scanFromMaxResults,
|
|
368
|
+
};
|
|
369
|
+
} catch {
|
|
370
|
+
// Ignore invalid config files and continue to the next fallback path.
|
|
371
|
+
}
|
|
316
372
|
}
|
|
373
|
+
|
|
374
|
+
return null;
|
|
317
375
|
}
|
|
318
376
|
|
|
319
377
|
/**
|
|
320
|
-
* Return the canonical
|
|
378
|
+
* Return the canonical fclt root directory.
|
|
321
379
|
*
|
|
322
380
|
* Precedence:
|
|
323
381
|
* 1) `FACULT_ROOT_DIR` env var
|
|
324
|
-
* 2) `~/.facult/config.json` { "rootDir": "..." }
|
|
382
|
+
* 2) `~/.ai/.facult/config.json` { "rootDir": "..." } (falls back to legacy `~/.facult/config.json`)
|
|
325
383
|
* 3) `~/.ai` (if it looks like a store)
|
|
326
384
|
* 4) `~/agents/.facult` (if it looks like a store)
|
|
327
385
|
* 5) a legacy store under `~/agents/` (if it looks like a store)
|
package/src/quarantine.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "node:fs/promises";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
15
|
+
import { facultStateDir } from "./paths";
|
|
15
16
|
|
|
16
17
|
export type QuarantineMode = "move" | "copy";
|
|
17
18
|
|
|
@@ -157,7 +158,7 @@ export async function quarantineItems(args: {
|
|
|
157
158
|
const ts = args.timestamp ?? new Date().toISOString();
|
|
158
159
|
const stamp = ts.replace(/[:.]/g, "-");
|
|
159
160
|
const quarantineDir =
|
|
160
|
-
args.destDir ?? join(home, "
|
|
161
|
+
args.destDir ?? join(facultStateDir(home), "quarantine", stamp);
|
|
161
162
|
|
|
162
163
|
const entries: QuarantineEntry[] = [];
|
|
163
164
|
|