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 CHANGED
@@ -43,6 +43,7 @@ Recommended global install:
43
43
  brew tap hack-dance/tap
44
44
  brew install hack-dance/tap/fclt
45
45
  fclt --help
46
+ fclt --version
46
47
  ```
47
48
 
48
49
  Package-manager install:
@@ -89,10 +90,26 @@ fclt self-update --version 0.0.1
89
90
 
90
91
  ```bash
91
92
  fclt scan --show-duplicates
93
+ fclt status
94
+ fclt inventory --json
92
95
  ```
93
96
 
94
97
  `scan` is read-only. It inspects local configs and reports what `fclt` found without changing files.
95
98
 
99
+ `status` reports the active canonical root, managed-tool state, generated index/graph state, writeback/proposal queue state, and high-signal sync risks.
100
+
101
+ `inventory` is the stable machine-readable discovery surface for agent harnesses. It returns a JSON catalog of discovered MCP servers, skills, and instruction/rule assets across known tool configs and configured scan roots. MCP definitions are redacted by default, including env values, inline `KEY=value` args, bearer tokens, and secret-looking URL query params, but include safe auth metadata such as env keys, env references, and whether inline secret values were found.
102
+
103
+ Useful inventory slices:
104
+
105
+ ```bash
106
+ fclt inventory --json --global
107
+ fclt inventory --json --project
108
+ fclt inventory --json --tool codex
109
+ ```
110
+
111
+ Use `mcpCapabilities` for the de-duplicated agent-facing MCP view. Use `mcpServers` when you need raw per-source occurrences for diagnostics.
112
+
96
113
  If you want a repo-local `.ai`:
97
114
 
98
115
  ```bash
@@ -137,6 +154,8 @@ If you run these commands inside a repo that has `<repo>/.ai`, `fclt` targets th
137
154
 
138
155
  ```bash
139
156
  fclt list skills
157
+ fclt inventory --json
158
+ fclt status --json
140
159
  fclt show instruction:WRITING
141
160
  fclt show mcp:github
142
161
  fclt find verification
@@ -344,13 +363,14 @@ version = 1
344
363
  [project_sync.codex]
345
364
  skills = ["hack-cli", "hack-tickets"]
346
365
  agents = ["review-operator"]
366
+ automations = ["project-check"]
347
367
  mcp_servers = ["github"]
348
368
  global_docs = true
349
369
  tool_rules = true
350
370
  tool_config = true
351
371
  ```
352
372
 
353
- That policy applies to project-managed tool renders, including assets inherited from the merged global index. If you want a global skill inside a repo-local managed Codex output, name it explicitly here. `fclt doctor --repair` can materialize repo-local project assets into `config.local.toml` for already-managed project roots.
373
+ That policy applies to project-managed tool renders, including assets inherited from the merged global index. If you want a global skill or shared Codex automation inside project-managed output, name it explicitly here. `fclt doctor --repair` can materialize repo-local project assets into `config.local.toml` for already-managed project roots.
354
374
 
355
375
  ### Snippets
356
376
 
@@ -513,6 +533,7 @@ Recommended security flow:
513
533
  - Inventory and discovery
514
534
  ```bash
515
535
  fclt scan [--from <path>] [--json] [--show-duplicates]
536
+ fclt inventory [--from <path>] [--json] [--show-secrets]
516
537
  fclt list [skills|mcp|agents|snippets|instructions] [--enabled-for <tool>] [--untrusted] [--flagged] [--pending]
517
538
  fclt show <name>
518
539
  fclt show instruction:<name>
@@ -603,7 +624,7 @@ When Codex is in managed mode, canonical automation sources live under:
603
624
  - `~/.ai/automations/<name>/...` for global automation state
604
625
  - `<repo>/.ai/automations/<name>/...` for project-scoped canonical state
605
626
 
606
- Managed sync renders those canonical automation directories into the shared live Codex automation store at `~/.codex/automations/` and only removes automation files that were previously rendered by the same canonical root.
627
+ Managed sync renders global canonical automation directories into the shared live Codex automation store at `~/.codex/automations/` and only removes automation files that were previously rendered by the same canonical root. Project-scoped automation sources are default-deny; add their names to `[project_sync.codex].automations` before project managed sync can render them into that shared live store.
607
628
 
608
629
  Example project automation:
609
630
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "facult",
3
- "version": "2.7.4",
3
+ "version": "2.8.0",
4
4
  "description": "Manage canonical AI capabilities, sync surfaces, and evolution state.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -12,7 +12,7 @@ export const codexAdapter: ToolAdapter = {
12
12
  mcp: "~/.codex/mcp.json",
13
13
  skills: ["~/.agents/skills", "~/.codex/skills"],
14
14
  agents: "~/.codex/agents",
15
- config: "~/.config/openai/codex.json",
15
+ config: "~/.codex/config.toml",
16
16
  }),
17
17
  parseMcp: (config) => parseMcpConfig(config),
18
18
  generateMcp: (canonical) => generateMcpConfig(canonical, "mcpServers"),
package/src/ai.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { appendFile, mkdir, readdir, readFile } from "node:fs/promises";
2
- import { basename, dirname, join } from "node:path";
3
+ import { basename, dirname, join, relative, resolve } from "node:path";
3
4
  import { ensureAiGraphPath } from "./ai-state";
4
5
  import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
5
6
  import type { AssetScope, GraphNodeKind } from "./graph";
@@ -242,7 +243,17 @@ function canonicalRefToPath(args: {
242
243
  return join(facultRootDir(args.homeDir), args.ref.slice("@ai/".length));
243
244
  }
244
245
  if (args.ref.startsWith("@project/")) {
245
- return join(args.rootDir, args.ref.slice("@project/".length));
246
+ const relPath = args.ref.slice("@project/".length);
247
+ const canonicalPath = join(args.rootDir, relPath);
248
+ const projectRoot = projectRootFromAiRoot(args.rootDir, args.homeDir);
249
+ if (projectRoot) {
250
+ const projectPath = join(projectRoot, relPath);
251
+ if (existsSync(canonicalPath)) {
252
+ return canonicalPath;
253
+ }
254
+ return existsSync(projectPath) ? projectPath : canonicalPath;
255
+ }
256
+ return canonicalPath;
246
257
  }
247
258
  return null;
248
259
  }
@@ -337,7 +348,27 @@ async function resolveAssetSelection(args: {
337
348
  });
338
349
  const node = resolveGraphNode(graph, args.asset);
339
350
  if (!node) {
340
- throw new Error(`Asset not found in graph: ${args.asset}`);
351
+ const projectRoot = projectRootFromAiRoot(args.rootDir, args.homeDir);
352
+ if (projectRoot) {
353
+ const resolvedPath = resolve(projectRoot, args.asset);
354
+ const relPath = relative(projectRoot, resolvedPath);
355
+ if (
356
+ relPath &&
357
+ !relPath.startsWith("..") &&
358
+ !relPath.includes("\0") &&
359
+ (await fileExists(resolvedPath))
360
+ ) {
361
+ const normalizedRef = relPath.replaceAll("\\", "/");
362
+ return {
363
+ assetRef: `@project/${normalizedRef}`,
364
+ assetId: `file:project:${normalizedRef}`,
365
+ assetType: "file",
366
+ };
367
+ }
368
+ }
369
+ throw new Error(
370
+ `Asset not found in graph: ${args.asset}. Run "fclt graph show <selector>" to check indexed assets, or use a project-relative file path that exists.`
371
+ );
341
372
  }
342
373
  return {
343
374
  assetRef: node.canonicalRef ?? node.id,
@@ -1514,6 +1545,11 @@ async function writebackCommand(argv: string[]) {
1514
1545
  return;
1515
1546
  }
1516
1547
 
1548
+ if (parsed.argv.includes("--help") || parsed.argv.includes("-h")) {
1549
+ console.log(writebackHelp());
1550
+ return;
1551
+ }
1552
+
1517
1553
  const rootDir = resolveCliContextRoot({
1518
1554
  rootArg: parsed.rootArg,
1519
1555
  scope: parsed.scope,
@@ -1625,6 +1661,11 @@ async function evolveCommand(argv: string[]) {
1625
1661
  return;
1626
1662
  }
1627
1663
 
1664
+ if (parsed.argv.includes("--help") || parsed.argv.includes("-h")) {
1665
+ console.log(evolveHelp());
1666
+ return;
1667
+ }
1668
+
1628
1669
  const rootDir = resolveCliContextRoot({
1629
1670
  rootArg: parsed.rootArg,
1630
1671
  scope: parsed.scope,
package/src/doctor.ts CHANGED
@@ -103,6 +103,51 @@ async function pathExists(pathValue: string): Promise<boolean> {
103
103
  }
104
104
  }
105
105
 
106
+ async function hasCanonicalSource(rootDir: string): Promise<boolean> {
107
+ const fileCandidates = [
108
+ "config.toml",
109
+ "config.local.toml",
110
+ "AGENTS.global.md",
111
+ "AGENTS.override.global.md",
112
+ ];
113
+ for (const relPath of fileCandidates) {
114
+ if (await pathExists(join(rootDir, relPath))) {
115
+ return true;
116
+ }
117
+ }
118
+
119
+ const dirCandidates = [
120
+ "agents",
121
+ "automations",
122
+ "instructions",
123
+ "mcp",
124
+ "rules",
125
+ "skills",
126
+ "snippets",
127
+ "tools",
128
+ ];
129
+ for (const relPath of dirCandidates) {
130
+ const entries = await readdir(join(rootDir, relPath)).catch(
131
+ () => [] as string[]
132
+ );
133
+ if (entries.some((entry) => !entry.startsWith("."))) {
134
+ return true;
135
+ }
136
+ }
137
+
138
+ return false;
139
+ }
140
+
141
+ async function isGeneratedOnlyProjectRoot(args: {
142
+ home: string;
143
+ rootDir: string;
144
+ }): Promise<boolean> {
145
+ if (projectRootFromAiRoot(args.rootDir, args.home) == null) {
146
+ return false;
147
+ }
148
+ return !(await hasCanonicalSource(args.rootDir));
149
+ }
150
+
106
151
  async function hashFile(pathValue: string): Promise<string> {
107
152
  const data = await readFile(pathValue);
108
153
  return createHash("sha256").update(data).digest("hex");
@@ -344,6 +389,23 @@ async function listProjectAgentNames(rootDir: string): Promise<string[]> {
344
389
  .sort((a, b) => a.localeCompare(b));
345
390
  }
346
391
 
392
+ async function listProjectAutomationNames(rootDir: string): Promise<string[]> {
393
+ const automationsDir = join(rootDir, "automations");
394
+ const entries = await readdir(automationsDir, { withFileTypes: true }).catch(
395
+ () => [] as import("node:fs").Dirent[]
396
+ );
397
+ const names: string[] = [];
398
+ for (const entry of entries) {
399
+ if (!entry.isDirectory() || entry.name.startsWith(".")) {
400
+ continue;
401
+ }
402
+ if (await pathExists(join(automationsDir, entry.name, "automation.toml"))) {
403
+ names.push(entry.name);
404
+ }
405
+ }
406
+ return names.sort((a, b) => a.localeCompare(b));
407
+ }
408
+
347
409
  async function listProjectMcpNames(rootDir: string): Promise<string[]> {
348
410
  const trackedPaths = [
349
411
  join(rootDir, "mcp", "servers.json"),
@@ -404,6 +466,7 @@ async function planProjectSyncPolicyRepair(args: {
404
466
  {
405
467
  skills?: string[];
406
468
  agents?: string[];
469
+ automations?: string[];
407
470
  mcpServers?: string[];
408
471
  globalDocs?: boolean;
409
472
  toolRules?: boolean;
@@ -426,18 +489,21 @@ async function planProjectSyncPolicyRepair(args: {
426
489
  const configuredTools = new Set(
427
490
  await loadConfiguredProjectSyncTools({ rootDir: args.rootDir })
428
491
  );
429
- const [skills, agents, mcpServers, globalDocs] = await Promise.all([
430
- listProjectSkillNames(args.rootDir),
431
- listProjectAgentNames(args.rootDir),
432
- listProjectMcpNames(args.rootDir),
433
- hasProjectGlobalDocs(args.rootDir),
434
- ]);
492
+ const [skills, agents, automations, mcpServers, globalDocs] =
493
+ await Promise.all([
494
+ listProjectSkillNames(args.rootDir),
495
+ listProjectAgentNames(args.rootDir),
496
+ listProjectAutomationNames(args.rootDir),
497
+ listProjectMcpNames(args.rootDir),
498
+ hasProjectGlobalDocs(args.rootDir),
499
+ ]);
435
500
 
436
501
  const toolPolicies: Record<
437
502
  string,
438
503
  {
439
504
  skills?: string[];
440
505
  agents?: string[];
506
+ automations?: string[];
441
507
  mcpServers?: string[];
442
508
  globalDocs?: boolean;
443
509
  toolRules?: boolean;
@@ -457,6 +523,7 @@ async function planProjectSyncPolicyRepair(args: {
457
523
  if (
458
524
  skills.length === 0 &&
459
525
  agents.length === 0 &&
526
+ automations.length === 0 &&
460
527
  mcpServers.length === 0 &&
461
528
  !globalDocs &&
462
529
  !toolRules &&
@@ -468,6 +535,7 @@ async function planProjectSyncPolicyRepair(args: {
468
535
  toolPolicies[tool] = {
469
536
  ...(skills.length > 0 ? { skills } : {}),
470
537
  ...(agents.length > 0 ? { agents } : {}),
538
+ ...(automations.length > 0 ? { automations } : {}),
471
539
  ...(mcpServers.length > 0 ? { mcpServers } : {}),
472
540
  ...(globalDocs ? { globalDocs: true } : {}),
473
541
  ...(toolRules ? { toolRules: true } : {}),
@@ -627,6 +695,13 @@ export async function doctorCommand(argv: string[]) {
627
695
  `Project sync is still implicit for managed tools (${projectSyncRepairTools.join(", ")}). Run \`fclt doctor --repair\` to write explicit [project_sync.<tool>] entries.`
628
696
  );
629
697
  }
698
+ if (await isGeneratedOnlyProjectRoot({ home, rootDir })) {
699
+ console.log(
700
+ "Project .ai root contains generated state only. Canonical project source is missing, so managed project sync should be treated as unsafe until source is initialized, restored, or management is detached."
701
+ );
702
+ process.exitCode = 1;
703
+ return;
704
+ }
630
705
 
631
706
  if (result.source === "generated") {
632
707
  console.log("AI index is healthy.");
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  } from "./graph-query";
26
26
  import type {
27
27
  AgentEntry,
28
+ AutomationEntry,
28
29
  FacultIndex,
29
30
  InstructionEntry,
30
31
  McpEntry,
@@ -43,12 +44,19 @@ import {
43
44
  } from "./query";
44
45
  import { parseJsonLenient } from "./util/json";
45
46
 
46
- type ListKind = "skills" | "mcp" | "agents" | "snippets" | "instructions";
47
+ type ListKind =
48
+ | "skills"
49
+ | "mcp"
50
+ | "agents"
51
+ | "automations"
52
+ | "snippets"
53
+ | "instructions";
47
54
 
48
55
  const LIST_KINDS: ListKind[] = [
49
56
  "skills",
50
57
  "mcp",
51
58
  "agents",
59
+ "automations",
52
60
  "snippets",
53
61
  "instructions",
54
62
  ];
@@ -65,6 +73,7 @@ export interface FindCommandOptions {
65
73
  }
66
74
 
67
75
  type GraphCommandKind = "show" | "deps" | "dependents";
76
+ type ShowKind = ListKind | "mcp";
68
77
 
69
78
  interface ContextualCommandOptions {
70
79
  rootArg?: string;
@@ -99,6 +108,14 @@ function printHelp() {
99
108
  headers: ["Command", "Purpose"],
100
109
  rows: [
101
110
  ["scan", "Scan local tool configs and discovered assets"],
111
+ [
112
+ "inventory",
113
+ "Print a JSON inventory of usable skills, instructions, and MCP servers",
114
+ ],
115
+ [
116
+ "status",
117
+ "Show active roots, managed tools, graph/index, and sync risks",
118
+ ],
102
119
  [
103
120
  "audit",
104
121
  "Run security audits with interactive or scripted flows",
@@ -177,7 +194,7 @@ function printListHelp() {
177
194
  title: "Usage",
178
195
  lines: renderBullets([
179
196
  renderCode(
180
- "fclt list [skills|mcp|agents|snippets|instructions] [options]"
197
+ "fclt list [skills|mcp|agents|automations|snippets|instructions] [options]"
181
198
  ),
182
199
  renderCode("fclt list"),
183
200
  ]),
@@ -494,6 +511,32 @@ function auditBadge(status?: string): string {
494
511
  return renderBadge("audit pending", "warn");
495
512
  }
496
513
 
514
+ function showKindForToken(token: string): ShowKind | null {
515
+ switch (token) {
516
+ case "agent":
517
+ case "agents":
518
+ return "agents";
519
+ case "automation":
520
+ case "automations":
521
+ return "automations";
522
+ case "instruction":
523
+ case "instructions":
524
+ return "instructions";
525
+ case "mcp":
526
+ case "mcp-server":
527
+ case "mcp-servers":
528
+ return "mcp";
529
+ case "skill":
530
+ case "skills":
531
+ return "skills";
532
+ case "snippet":
533
+ case "snippets":
534
+ return "snippets";
535
+ default:
536
+ return null;
537
+ }
538
+ }
539
+
497
540
  function displayDescription(value?: string): string {
498
541
  const normalized = value
499
542
  ?.trim()
@@ -571,6 +614,7 @@ async function listCommand(argv: string[]) {
571
614
  | SkillEntry[]
572
615
  | McpEntry[]
573
616
  | AgentEntry[]
617
+ | AutomationEntry[]
574
618
  | SnippetEntry[]
575
619
  | InstructionEntry[] = [];
576
620
 
@@ -584,6 +628,20 @@ async function listCommand(argv: string[]) {
584
628
  case "agents":
585
629
  entries = filterAgents(index.agents ?? {}, opts.filters);
586
630
  break;
631
+ case "automations":
632
+ entries = Object.values(index.automations ?? {}).filter((entry) => {
633
+ if (
634
+ opts.filters.sourceKind &&
635
+ entry.sourceKind !== opts.filters.sourceKind
636
+ ) {
637
+ return false;
638
+ }
639
+ if (opts.filters.scope && entry.scope !== opts.filters.scope) {
640
+ return false;
641
+ }
642
+ return true;
643
+ });
644
+ break;
587
645
  case "snippets":
588
646
  entries = filterSnippets(index.snippets ?? {}, opts.filters);
589
647
  break;
@@ -646,11 +704,17 @@ async function listCommand(argv: string[]) {
646
704
  };
647
705
  }
648
706
 
649
- const detailEntry = entry as AgentEntry | SnippetEntry | InstructionEntry;
707
+ const detailEntry = entry as
708
+ | AgentEntry
709
+ | AutomationEntry
710
+ | SnippetEntry
711
+ | InstructionEntry;
650
712
  return {
651
713
  title: entry.name,
652
714
  meta: sourceLabel(entry),
653
- description: displayDescription(detailEntry.description),
715
+ description: displayDescription(
716
+ "description" in detailEntry ? detailEntry.description : undefined
717
+ ),
654
718
  };
655
719
  });
656
720
 
@@ -834,45 +898,59 @@ async function showCommand(argv: string[]) {
834
898
  return;
835
899
  }
836
900
 
901
+ const rootDir = resolveCliContextRoot({
902
+ rootArg: context.rootArg,
903
+ scope: context.scopeMode,
904
+ cwd: process.cwd(),
905
+ });
906
+
837
907
  let index: FacultIndex;
838
908
  try {
839
- index = await loadIndex({
840
- rootDir: resolveCliContextRoot({
841
- rootArg: context.rootArg,
842
- scope: context.scopeMode,
843
- cwd: process.cwd(),
844
- }),
845
- });
909
+ index = await loadIndex({ rootDir });
846
910
  } catch (err) {
847
911
  console.error(err instanceof Error ? err.message : String(err));
848
912
  process.exitCode = 1;
849
913
  return;
850
914
  }
851
915
 
852
- let kind: ListKind | "mcp" = "skills";
916
+ let kind: ShowKind = "skills";
853
917
  let name = raw;
918
+ const colonIndex = raw.indexOf(":");
919
+ if (colonIndex > 0) {
920
+ const tokenKind = showKindForToken(raw.slice(0, colonIndex));
921
+ if (tokenKind) {
922
+ kind = tokenKind;
923
+ name = raw.slice(colonIndex + 1);
924
+ }
925
+ }
854
926
 
855
- if (raw.startsWith("mcp:")) {
856
- kind = "mcp";
857
- name = raw.slice("mcp:".length);
858
- } else if (raw.startsWith("instruction:")) {
859
- kind = "instructions";
860
- name = raw.slice("instruction:".length);
861
- } else if (raw.startsWith("instructions:")) {
862
- kind = "instructions";
863
- name = raw.slice("instructions:".length);
927
+ try {
928
+ const graph = await loadGraph({ rootDir });
929
+ const node = resolveGraphNode(graph, raw, {
930
+ sourceKind: context.sourceKind,
931
+ scope: scopeFilterForMode(context.scopeMode),
932
+ });
933
+ const graphKind = node ? showKindForToken(node.kind) : null;
934
+ if (node && graphKind) {
935
+ kind = graphKind;
936
+ name = node.name;
937
+ }
938
+ } catch {
939
+ // A missing or stale graph should not make basic index-backed show fail.
864
940
  }
865
941
 
866
942
  let entry:
867
943
  | SkillEntry
868
944
  | McpEntry
869
945
  | AgentEntry
946
+ | AutomationEntry
870
947
  | SnippetEntry
871
948
  | InstructionEntry
872
949
  | null = null;
873
950
  const skill = index.skills[name];
874
951
  const mcpServer = index.mcp?.servers?.[name];
875
952
  const agent = index.agents?.[name];
953
+ const automation = index.automations?.[name];
876
954
  const snippet = index.snippets?.[name];
877
955
  const instruction = index.instructions?.[name];
878
956
  const matchesContext = (candidate: {
@@ -896,6 +974,15 @@ async function showCommand(argv: string[]) {
896
974
  } else if (kind === "skills" && agent && matchesContext(agent)) {
897
975
  kind = "agents";
898
976
  entry = agent;
977
+ } else if (
978
+ kind === "automations" &&
979
+ automation &&
980
+ matchesContext(automation)
981
+ ) {
982
+ entry = automation;
983
+ } else if (kind === "skills" && automation && matchesContext(automation)) {
984
+ kind = "automations";
985
+ entry = automation;
899
986
  } else if (kind === "skills" && snippet && matchesContext(snippet)) {
900
987
  kind = "snippets";
901
988
  entry = snippet;
@@ -1105,6 +1192,7 @@ async function graphCommand(argv: string[]) {
1105
1192
  }
1106
1193
 
1107
1194
  async function adaptersCommand(argv: string[]) {
1195
+ const json = argv.includes("--json");
1108
1196
  if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
1109
1197
  console.log(
1110
1198
  renderPage({
@@ -1122,6 +1210,21 @@ async function adaptersCommand(argv: string[]) {
1122
1210
  }
1123
1211
  const { getAllAdapters } = await import("./adapters");
1124
1212
  const adapters = getAllAdapters();
1213
+ if (json) {
1214
+ console.log(
1215
+ JSON.stringify(
1216
+ adapters.map((adapter) => ({
1217
+ id: adapter.id,
1218
+ name: adapter.name,
1219
+ versions: adapter.versions,
1220
+ defaultPaths: adapter.getDefaultPaths?.() ?? {},
1221
+ })),
1222
+ null,
1223
+ 2
1224
+ )
1225
+ );
1226
+ return;
1227
+ }
1125
1228
  if (!adapters.length) {
1126
1229
  console.log(
1127
1230
  renderPage({
@@ -1158,6 +1261,12 @@ async function main(argv: string[]) {
1158
1261
  return;
1159
1262
  }
1160
1263
 
1264
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
1265
+ const { packageVersion } = await import("./status");
1266
+ console.log(await packageVersion());
1267
+ return;
1268
+ }
1269
+
1161
1270
  // Convenience: allow `fclt --show-duplicates` as shorthand for `fclt scan --show-duplicates`.
1162
1271
  if (cmd === "--show-duplicates") {
1163
1272
  const { scanCommand } = await import("./scan");
@@ -1169,6 +1278,14 @@ async function main(argv: string[]) {
1169
1278
  case "scan":
1170
1279
  await import("./scan").then(({ scanCommand }) => scanCommand(rest));
1171
1280
  return;
1281
+ case "inventory":
1282
+ await import("./inventory").then(({ inventoryCommand }) =>
1283
+ inventoryCommand(rest)
1284
+ );
1285
+ return;
1286
+ case "status":
1287
+ await import("./status").then(({ statusCommand }) => statusCommand(rest));
1288
+ return;
1172
1289
  case "audit":
1173
1290
  await import("./audit").then(({ auditCommand }) => auditCommand(rest));
1174
1291
  return;