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/src/index.ts CHANGED
@@ -96,50 +96,50 @@ interface GraphCommandOptions {
96
96
  }
97
97
 
98
98
  function printHelp() {
99
- console.log(`facultinspect local agent configs for skills + MCP servers
99
+ console.log(`fcltmanage canonical AI capabilities, sync surfaces, and evolution state
100
100
 
101
101
  Usage:
102
- facult scan [--json] [--show-duplicates] [--tui] [--from <path>]
103
- facult audit [--from <path>]
104
- facult audit --non-interactive [name|mcp:<name>] [--severity <level>] [--rules <path>] [--from <path>] [--json]
105
- facult audit --non-interactive [name|mcp:<name>] --with <claude|codex> [--from <path>] [--max-items <n|all>] [--json]
106
- facult migrate [--from <path>] [--dry-run] [--move] [--write-config]
107
- facult doctor [--repair]
108
- facult consolidate [--force] [--auto <mode>] [scan options]
109
- facult index [--force]
110
- facult list [skills|mcp|agents|snippets|instructions] [--enabled-for TOOL] [--untrusted] [--flagged] [--pending] [--json]
111
- facult show <name>
112
- facult show mcp:<name> [--show-secrets]
113
- facult show instruction:<name>
114
- facult find <query> [--json]
115
- facult graph <show|deps|dependents> <asset> [--json]
116
- facult ai <writeback|evolve> [args...]
117
- facult adapters
118
- facult trust <name> [moreNames...]
119
- facult untrust <name> [moreNames...]
120
- facult manage <tool>
121
- facult unmanage <tool>
122
- facult managed
123
- facult enable <name> [moreNames...] [--for <tools>]
124
- facult disable <name> [moreNames...] [--for <tools>]
125
- facult sync [tool] [--dry-run]
126
- facult autosync <cmd> [args...]
127
- facult search <query> [--index <name>] [--limit <n>]
128
- facult install <index:item> [--as <name>] [--dry-run] [--force] [--strict-source-trust]
129
- facult update [--apply] [--strict-source-trust]
130
- facult update --self [--version <x.y.z|latest>] [--dry-run]
131
- facult self-update [--version <x.y.z|latest>] [--dry-run]
132
- facult verify-source <name> [--json]
133
- facult sources <cmd> [args...]
134
- facult templates <cmd> [args...]
135
- facult snippets <cmd> [args...]
136
- facult --show-duplicates
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 facult state
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 facult itself based on install method
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(`facult list — list indexed entries from the canonical store
206
+ console.log(`fclt list — list indexed entries from the canonical store
207
207
 
208
208
  Usage:
209
- facult list [skills|mcp|agents|snippets|instructions] [options]
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(`facult show — show a single indexed entry (and file contents)
226
+ console.log(`fclt show — show a single indexed entry (and file contents)
227
227
 
228
228
  Usage:
229
- facult show <name>
230
- facult show mcp:<name> [--show-secrets]
231
- facult show instruction:<name>
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(`facult find — search local indexed capabilities across asset types
244
+ console.log(`fclt find — search local indexed capabilities across asset types
245
245
 
246
246
  Usage:
247
- facult find <query> [--json]
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(`facult graph — inspect explicit capability graph nodes and relations
260
+ console.log(`fclt graph — inspect explicit capability graph nodes and relations
261
261
 
262
262
  Usage:
263
- facult graph show <asset> [--json]
264
- facult graph deps <asset> [--json]
265
- facult graph dependents <asset> [--json]
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
- "facult adapters — list registered tool adapters\n\nUsage:\n facult adapters\n"
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 `facult --show-duplicates` as shorthand for `facult scan --show-duplicates`.
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 p = managedStatePathForRoot(home, rootDir);
303
- if (!(await fileExists(p))) {
304
- return { version: MANAGED_VERSION, tools: {} };
305
- }
306
- try {
307
- const txt = await Bun.file(p).text();
308
- const data = JSON.parse(txt) as Partial<ManagedState> | null;
309
- if (data?.version === MANAGED_VERSION && data.tools) {
310
- return { version: MANAGED_VERSION, tools: data.tools };
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 "facult manage ${tool} --dry-run" to review the plan, then rerun with "--adopt-existing"`,
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(`facult manage — enter managed mode for a tool (backup + symlinks + MCP generation)
3020
+ console.log(`fclt manage — enter managed mode for a tool (backup + symlinks + MCP generation)
3005
3021
 
3006
3022
  Usage:
3007
- facult manage <tool> [--dry-run] [--adopt-existing] [--existing-conflicts keep-canonical|keep-existing] [--builtin-conflicts overwrite] [--root PATH|--global|--project]
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(`facult unmanage — exit managed mode for a tool (restore backups)
3108
+ console.log(`fclt unmanage — exit managed mode for a tool (restore backups)
3093
3109
 
3094
3110
  Usage:
3095
- facult unmanage <tool> [--root PATH|--global|--project]
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(`facult managed — list tools currently in managed mode
3143
+ console.log(`fclt managed — list tools currently in managed mode
3128
3144
 
3129
3145
  Usage:
3130
- facult managed [--root PATH|--global|--project]
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(`facult sync — sync managed tools with canonical state
3173
+ console.log(`fclt sync — sync managed tools with canonical state
3158
3174
 
3159
3175
  Usage:
3160
- facult sync [tool] [--dry-run] [--builtin-conflicts overwrite] [--root PATH|--global|--project]
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(`facult migrate — migrate a legacy canonical store to the facult path
17
+ console.log(`fclt migrate — migrate a legacy canonical store to the fclt path
17
18
 
18
19
  Usage:
19
- facult migrate [--from <path>] [--dry-run] [--move] [--write-config]
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 ~/agents/.facult (default, safe)
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 ~/agents/.facult
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 = join(home, "agents", ".facult");
192
- const stateDir = join(home, ".facult");
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 facult store: ${dest}`
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(stateDir, { recursive: true });
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 facult stores the consolidated "canonical" skill + MCP state
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 `facult scan --from <path>`).
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 facult store if it contains something we'd create.
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 facultStateDir(home: string = defaultHomeDir()): string {
136
- return join(home, ".facult");
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
- const rootDir = args?.rootDir;
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
- const slug = rootDir ? projectSlugFromAiRoot(rootDir, home) : null;
202
- return slug
203
- ? join(facultStateDir(home), "ai", "projects", slug)
204
- : join(facultStateDir(home), "ai", "global");
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(facultStateDir(home), "config.json");
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 p = facultConfigPath(home);
251
- if (!(isSafePathString(p) && fileExists(p))) {
252
- return null;
253
- }
300
+ const candidates = [
301
+ facultConfigPath(home),
302
+ join(legacyExternalFacultStateDir(home), "config.json"),
303
+ ];
254
304
 
255
- try {
256
- const txt = readFileSync(p, "utf8");
257
- const parsed = parseJsonLenient(txt) as unknown;
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
- const scanFromMaxVisitsRaw = (parsed as Record<string, unknown>)
289
- .scanFromMaxVisits;
290
- const scanFromMaxVisits =
291
- typeof scanFromMaxVisitsRaw === "number" &&
292
- Number.isFinite(scanFromMaxVisitsRaw) &&
293
- scanFromMaxVisitsRaw > 0
294
- ? Math.floor(scanFromMaxVisitsRaw)
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
- const scanFromMaxResultsRaw = (parsed as Record<string, unknown>)
298
- .scanFromMaxResults;
299
- const scanFromMaxResults =
300
- typeof scanFromMaxResultsRaw === "number" &&
301
- Number.isFinite(scanFromMaxResultsRaw) &&
302
- scanFromMaxResultsRaw > 0
303
- ? Math.floor(scanFromMaxResultsRaw)
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
- return {
307
- rootDir,
308
- scanFrom,
309
- scanFromIgnore,
310
- scanFromNoDefaultIgnore,
311
- scanFromMaxVisits,
312
- scanFromMaxResults,
313
- };
314
- } catch {
315
- return null;
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 facult root directory.
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, ".facult", "quarantine", stamp);
161
+ args.destDir ?? join(facultStateDir(home), "quarantine", stamp);
161
162
 
162
163
  const entries: QuarantineEntry[] = [];
163
164