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/src/index.ts CHANGED
@@ -10,6 +10,16 @@ import {
10
10
  parseCliContextArgs,
11
11
  resolveCliContextRoot,
12
12
  } from "./cli-context";
13
+ import {
14
+ renderBadge,
15
+ renderBullets,
16
+ renderCatalog,
17
+ renderCode,
18
+ renderJsonBlock,
19
+ renderKeyValue,
20
+ renderPage,
21
+ renderTable,
22
+ } from "./cli-ui";
13
23
  import { consolidateCommand } from "./consolidate";
14
24
  import { doctorCommand } from "./doctor";
15
25
  import { disableCommand, enableCommand } from "./enable-disable";
@@ -96,182 +106,232 @@ interface GraphCommandOptions {
96
106
  }
97
107
 
98
108
  function printHelp() {
99
- console.log(`fclt — manage canonical AI capabilities, sync surfaces, and evolution state
100
-
101
- Usage:
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
-
138
- Commands:
139
- scan Scan common config locations (Cursor, Claude, Claude Desktop, etc.)
140
- audit Security audits (interactive by default; use --non-interactive for scripts)
141
- migrate Copy/move a legacy canonical store to the current canonical root
142
- doctor Inspect and repair local fclt state
143
- consolidate Deduplicate and copy skills + MCP configs (interactive or --auto)
144
- index Build a queryable index from the canonical store (see FACULT_ROOT_DIR)
145
- list List indexed skills, MCP servers, agents, snippets, or instructions
146
- show Show a single indexed entry, including file contents
147
- find Search local indexed capabilities across asset types
148
- graph Inspect explicit capability graph nodes and dependencies
149
- ai Record, group, and evolve AI writeback into reviewable proposals
150
- adapters List registered tool adapters
151
- trust Mark a skill or MCP server as trusted (annotation only)
152
- untrust Remove trusted annotation
153
- manage Back up tool config and enter managed mode
154
- unmanage Restore backups and exit managed mode
155
- managed List tools in managed mode
156
- enable Enable skills or MCP servers for tools
157
- disable Disable skills or MCP servers for tools
158
- sync Sync managed tools with canonical configs
159
- autosync Install/manage a background autosync service
160
- search Search remote indices (builtin + provider aliases + configured)
161
- install Install an item from a remote index
162
- update Check/apply updates for remotely installed items
163
- self-update Update fclt itself based on install method
164
- verify-source Verify source trust and manifest integrity/signature status
165
- sources Manage source trust policy for remote indices
166
- templates Scaffold DX-first templates (skills/instructions/MCP/snippets/automations)
167
- snippets Sync reusable snippet blocks into config files
168
-
169
- Options:
170
- --json Print full JSON (ScanResult or list output)
171
- --show-duplicates Print duplicates for skills, MCP servers, and hook assets
172
- --tui Render scan output in an interactive TUI (skills list)
173
- --from Add one or more additional scan roots (repeatable): --from ~/dev
174
- --from-ignore (scan) Ignore directories by basename under --from roots (repeatable)
175
- --from-no-default-ignore (scan) Disable the default ignore list for --from scans
176
- --from-max-visits (scan) Max directories visited per --from root before truncating
177
- --from-max-results (scan) Max discovered paths per --from root before truncating
178
- --non-interactive (audit) Run static/agent audit non-interactively (for scripts)
179
- --severity Minimum severity to include in audit output (low|medium|high|critical)
180
- --rules Path to an audit rules YAML file (default: ~/.ai/.facult/audit-rules.yaml)
181
- --with (audit) Agent tool: claude|codex
182
- --max-items (audit) Max items to send to the agent (n|all)
183
- --force Re-copy items already consolidated OR rebuild index from scratch
184
- --auto Auto-resolve consolidate conflicts: keep-newest, keep-current, keep-incoming
185
- --enabled-for Filter list to entries enabled for a specific tool
186
- --untrusted Filter list to entries that are not trusted
187
- --flagged Filter list to entries flagged by audit
188
- --pending Filter list to entries pending audit
189
- --for Comma-separated list of tools for enable/disable
190
- --dry-run Show what sync would change
191
- --as Install/scaffold target name override
192
- --limit Max results for search
193
- --apply Apply updates (update command)
194
- --self (update) run self-update flow instead of remote item updates
195
- --strict-source-trust Enforce trust-only remote install/update actions
196
- --show-secrets (show) Print raw secret values (unsafe)
197
- --root Select a canonical .ai root explicitly
198
- --global Force the global canonical root
199
- --project Force the nearest repo-local .ai root
200
- --scope Capability view scope: merged|global|project
201
- --source Filter to builtin|global|project asset provenance
202
- `);
109
+ console.log(
110
+ renderPage({
111
+ title: "fclt",
112
+ subtitle:
113
+ "Manage canonical AI capability, rendered tool surfaces, and evolution state.",
114
+ sections: [
115
+ {
116
+ title: "Usage",
117
+ lines: renderBullets([
118
+ `${renderCode("fclt list")} defaults to ${renderCode("skills")} when you do not specify a type.`,
119
+ `${renderCode("fclt graph <asset>")} is shorthand for ${renderCode("fclt graph show <asset>")}.`,
120
+ `${renderCode("fclt templates init ...")} is the main entry for scaffolding new canonical capability.`,
121
+ ]),
122
+ },
123
+ {
124
+ title: "Core Commands",
125
+ lines: renderTable({
126
+ headers: ["Command", "Purpose"],
127
+ rows: [
128
+ ["scan", "Scan local tool configs and discovered assets"],
129
+ [
130
+ "audit",
131
+ "Run security audits with interactive or scripted flows",
132
+ ],
133
+ [
134
+ "consolidate",
135
+ "Import existing skills and MCP configs into canonical state",
136
+ ],
137
+ ["index", "Rebuild the generated capability index"],
138
+ [
139
+ "list",
140
+ "List indexed skills, MCP, agents, snippets, or instructions",
141
+ ],
142
+ ["show", "Inspect one indexed asset and its source contents"],
143
+ ["find", "Search indexed capability across asset types"],
144
+ ["graph", "Inspect capability graph nodes, deps, and dependents"],
145
+ [
146
+ "templates",
147
+ "Scaffold skills, MCP, agents, snippets, and automations",
148
+ ],
149
+ ["search/install/update", "Work with remote capability indices"],
150
+ [
151
+ "manage/sync",
152
+ "Enter managed mode and render tool-native output",
153
+ ],
154
+ ["ai", "Capture writeback and evolve canonical assets"],
155
+ ],
156
+ }),
157
+ },
158
+ {
159
+ title: "Common Options",
160
+ lines: renderTable({
161
+ headers: ["Option", "Meaning"],
162
+ rows: [
163
+ [
164
+ "--json",
165
+ "Machine-readable output instead of formatted terminal UI",
166
+ ],
167
+ ["--dry-run", "Show intended writes without mutating files"],
168
+ [
169
+ "--root / --global / --project",
170
+ "Pick the canonical root explicitly",
171
+ ],
172
+ [
173
+ "--scope / --source",
174
+ "Narrow merged views by scope or provenance",
175
+ ],
176
+ [
177
+ "--non-interactive / --yes",
178
+ "Suppress prompts where the command supports inferred defaults",
179
+ ],
180
+ ],
181
+ }),
182
+ },
183
+ {
184
+ title: "Examples",
185
+ lines: renderBullets([
186
+ renderCode("fclt list"),
187
+ renderCode("fclt graph skills:capability-evolution"),
188
+ renderCode("fclt templates init skill review-checklist"),
189
+ renderCode("fclt templates init agent writeback-curator"),
190
+ ]),
191
+ },
192
+ ],
193
+ })
194
+ );
203
195
  }
204
196
 
205
197
  function printListHelp() {
206
- console.log(`fclt list — list indexed entries from the canonical store
207
-
208
- Usage:
209
- fclt list [skills|mcp|agents|snippets|instructions] [options]
210
-
211
- Options:
212
- --enabled-for TOOL Only include entries enabled for a tool
213
- --untrusted Only include entries that are not trusted
214
- --flagged Only include entries flagged by audit
215
- --pending Only include entries pending audit
216
- --root PATH Select a canonical .ai root explicitly
217
- --global Force the global canonical root
218
- --project Force the nearest repo-local .ai root
219
- --scope SCOPE merged|global|project (default: merged)
220
- --source KIND builtin|global|project
221
- --json Print JSON array
222
- `);
198
+ console.log(
199
+ renderPage({
200
+ title: "fclt list",
201
+ subtitle: "List indexed entries from the canonical store.",
202
+ sections: [
203
+ {
204
+ title: "Usage",
205
+ lines: renderBullets([
206
+ renderCode(
207
+ "fclt list [skills|mcp|agents|snippets|instructions] [options]"
208
+ ),
209
+ renderCode("fclt list"),
210
+ ]),
211
+ },
212
+ {
213
+ title: "Options",
214
+ lines: renderTable({
215
+ headers: ["Option", "Meaning"],
216
+ rows: [
217
+ [
218
+ "--enabled-for TOOL",
219
+ "Only include entries enabled for one tool",
220
+ ],
221
+ ["--untrusted", "Only include entries without trust approval"],
222
+ ["--flagged", "Only include entries flagged by audit"],
223
+ ["--pending", "Only include entries still pending audit"],
224
+ ["--root / --global / --project", "Choose the canonical root"],
225
+ ["--scope", "merged, global, or project"],
226
+ ["--source", "builtin, global, or project provenance"],
227
+ ["--json", "Print the raw JSON array"],
228
+ ],
229
+ }),
230
+ },
231
+ ],
232
+ })
233
+ );
223
234
  }
224
235
 
225
236
  function printShowHelp() {
226
- console.log(`fclt show — show a single indexed entry (and file contents)
227
-
228
- Usage:
229
- fclt show <name>
230
- fclt show mcp:<name> [--show-secrets]
231
- fclt show instruction:<name>
232
-
233
- Options:
234
- --show-secrets (mcp) Print raw secret values (unsafe)
235
- --root PATH Select a canonical .ai root explicitly
236
- --global Force the global canonical root
237
- --project Force the nearest repo-local .ai root
238
- --scope SCOPE merged|global|project (default: merged)
239
- --source KIND builtin|global|project
240
- `);
237
+ console.log(
238
+ renderPage({
239
+ title: "fclt show",
240
+ subtitle: "Inspect one indexed entry and the source file behind it.",
241
+ sections: [
242
+ {
243
+ title: "Usage",
244
+ lines: renderBullets([
245
+ renderCode("fclt show <name>"),
246
+ renderCode("fclt show mcp:<name> [--show-secrets]"),
247
+ renderCode("fclt show instruction:<name>"),
248
+ ]),
249
+ },
250
+ {
251
+ title: "Options",
252
+ lines: renderTable({
253
+ headers: ["Option", "Meaning"],
254
+ rows: [
255
+ [
256
+ "--show-secrets",
257
+ "For MCP configs, print raw secrets instead of redacting",
258
+ ],
259
+ ["--root / --global / --project", "Choose the canonical root"],
260
+ ["--scope", "merged, global, or project"],
261
+ ["--source", "builtin, global, or project provenance"],
262
+ ],
263
+ }),
264
+ },
265
+ ],
266
+ })
267
+ );
241
268
  }
242
269
 
243
270
  function printFindHelp() {
244
- console.log(`fclt find — search local indexed capabilities across asset types
245
-
246
- Usage:
247
- fclt find <query> [--json]
248
-
249
- Options:
250
- --root PATH Select a canonical .ai root explicitly
251
- --global Force the global canonical root
252
- --project Force the nearest repo-local .ai root
253
- --scope SCOPE merged|global|project (default: merged)
254
- --source KIND builtin|global|project
255
- --json Print JSON array
256
- `);
271
+ console.log(
272
+ renderPage({
273
+ title: "fclt find",
274
+ subtitle:
275
+ "Search indexed capability across skills, MCP, agents, snippets, and instructions.",
276
+ sections: [
277
+ {
278
+ title: "Usage",
279
+ lines: renderBullets([renderCode("fclt find <query> [--json]")]),
280
+ },
281
+ {
282
+ title: "Options",
283
+ lines: renderTable({
284
+ headers: ["Option", "Meaning"],
285
+ rows: [
286
+ ["--root / --global / --project", "Choose the canonical root"],
287
+ ["--scope", "merged, global, or project"],
288
+ ["--source", "builtin, global, or project provenance"],
289
+ ["--json", "Print the raw JSON array"],
290
+ ],
291
+ }),
292
+ },
293
+ ],
294
+ })
295
+ );
257
296
  }
258
297
 
259
298
  function printGraphHelp() {
260
- console.log(`fclt graph — inspect explicit capability graph nodes and relations
261
-
262
- Usage:
263
- fclt graph show <asset> [--json]
264
- fclt graph deps <asset> [--json]
265
- fclt graph dependents <asset> [--json]
266
-
267
- Options:
268
- --root PATH Select a canonical .ai root explicitly
269
- --global Force the global canonical root
270
- --project Force the nearest repo-local .ai root
271
- --scope SCOPE merged|global|project (default: merged)
272
- --source KIND builtin|global|project
273
- --json Print JSON
274
- `);
299
+ console.log(
300
+ renderPage({
301
+ title: "fclt graph",
302
+ subtitle: "Inspect explicit capability graph nodes and relations.",
303
+ sections: [
304
+ {
305
+ title: "Usage",
306
+ lines: renderBullets([
307
+ renderCode("fclt graph <asset> [--json]"),
308
+ renderCode("fclt graph show <asset> [--json]"),
309
+ renderCode("fclt graph deps <asset> [--json]"),
310
+ renderCode("fclt graph dependents <asset> [--json]"),
311
+ ]),
312
+ },
313
+ {
314
+ title: "Notes",
315
+ lines: renderBullets([
316
+ `${renderCode("fclt graph <asset>")} defaults to ${renderCode("show")}.`,
317
+ "Selectors can be canonical refs, names, or graph node ids.",
318
+ ]),
319
+ },
320
+ {
321
+ title: "Options",
322
+ lines: renderTable({
323
+ headers: ["Option", "Meaning"],
324
+ rows: [
325
+ ["--root / --global / --project", "Choose the canonical root"],
326
+ ["--scope", "merged, global, or project"],
327
+ ["--source", "builtin, global, or project provenance"],
328
+ ["--json", "Print raw graph JSON"],
329
+ ],
330
+ }),
331
+ },
332
+ ],
333
+ })
334
+ );
275
335
  }
276
336
 
277
337
  function parseListKind(argv: string[]): { kind: ListKind; startIndex: number } {
@@ -371,15 +431,16 @@ export function parseFindArgs(argv: string[]): FindCommandOptions {
371
431
  return { text, json };
372
432
  }
373
433
 
374
- function parseGraphArgs(argv: string[]): GraphCommandOptions {
375
- const [kind, ...rest] = argv;
376
- if (!(kind === "show" || kind === "deps" || kind === "dependents")) {
377
- throw new Error(`Unknown graph command: ${kind ?? "<missing>"}`);
378
- }
434
+ export function parseGraphArgs(argv: string[]): GraphCommandOptions {
435
+ const [first, ...rest] = argv;
436
+ const hasExplicitKind =
437
+ first === "show" || first === "deps" || first === "dependents";
438
+ const kind: GraphCommandKind = hasExplicitKind ? first : "show";
439
+ const args = hasExplicitKind ? rest : argv;
379
440
 
380
441
  let json = false;
381
442
  let target: string | null = null;
382
- for (const arg of rest) {
443
+ for (const arg of args) {
383
444
  if (!arg) {
384
445
  continue;
385
446
  }
@@ -409,6 +470,68 @@ function scopeFilterForMode(
409
470
  return scopeMode === "project" ? "project" : undefined;
410
471
  }
411
472
 
473
+ function sourceLabel(entry: { sourceKind?: string; scope?: string }): string {
474
+ const source = entry.sourceKind?.trim();
475
+ const scope = entry.scope?.trim();
476
+ if (source && scope) {
477
+ return `${source}/${scope}`;
478
+ }
479
+ return source ?? scope ?? "merged";
480
+ }
481
+
482
+ function describeFilters(filters: QueryFilters): string {
483
+ const parts: string[] = [];
484
+
485
+ if (filters.enabledFor) {
486
+ parts.push(`enabled for ${filters.enabledFor}`);
487
+ }
488
+ if (filters.untrusted) {
489
+ parts.push("untrusted only");
490
+ }
491
+ if (filters.flagged) {
492
+ parts.push("flagged only");
493
+ }
494
+ if (filters.pending) {
495
+ parts.push("pending only");
496
+ }
497
+ if (filters.sourceKind) {
498
+ parts.push(`source ${filters.sourceKind}`);
499
+ }
500
+ if (filters.scope) {
501
+ parts.push(`scope ${filters.scope}`);
502
+ }
503
+
504
+ return parts.join(" • ");
505
+ }
506
+
507
+ function trustBadge(trusted?: boolean): string {
508
+ return trusted
509
+ ? renderBadge("trusted", "success")
510
+ : renderBadge("untrusted", "warn");
511
+ }
512
+
513
+ function auditBadge(status?: string): string {
514
+ const normalized = (status ?? "pending").trim().toLowerCase();
515
+ if (normalized === "passed") {
516
+ return renderBadge("audit passed", "success");
517
+ }
518
+ if (normalized === "flagged") {
519
+ return renderBadge("audit flagged", "danger");
520
+ }
521
+ return renderBadge("audit pending", "warn");
522
+ }
523
+
524
+ function displayDescription(value?: string): string {
525
+ const normalized = value
526
+ ?.trim()
527
+ .replaceAll('\\"', '"')
528
+ .replace(INLINE_NAME_DESCRIPTION_RE, "");
529
+ if (!normalized || normalized === ">") {
530
+ return "No description.";
531
+ }
532
+ return normalized;
533
+ }
534
+
412
535
  function resolveContextualOptions(
413
536
  argv: string[],
414
537
  opts?: { allowSource?: boolean }
@@ -504,33 +627,70 @@ async function listCommand(argv: string[]) {
504
627
  return;
505
628
  }
506
629
 
507
- for (const entry of entries) {
630
+ if (entries.length === 0) {
631
+ console.log(
632
+ renderPage({
633
+ title: `fclt list ${opts.kind}`,
634
+ subtitle: "No matching entries.",
635
+ sections: [
636
+ {
637
+ title: "Next Steps",
638
+ lines: renderBullets([
639
+ renderCode("fclt index --force"),
640
+ renderCode("fclt templates list"),
641
+ ]),
642
+ },
643
+ ],
644
+ footer: describeFilters(opts.filters)
645
+ ? [describeFilters(opts.filters)]
646
+ : undefined,
647
+ })
648
+ );
649
+ return;
650
+ }
651
+
652
+ const items = entries.map((entry) => {
508
653
  if (opts.kind === "skills") {
509
654
  const skill = entry as SkillEntry;
510
- const desc = skill.description ? `\t${skill.description}` : "";
511
- const meta = skill as SkillEntry & {
512
- trusted?: boolean;
513
- auditStatus?: string;
655
+ return {
656
+ title: skill.name,
657
+ meta: sourceLabel(skill),
658
+ badges: [trustBadge(skill.trusted), auditBadge(skill.auditStatus)],
659
+ description: displayDescription(skill.description),
514
660
  };
515
- const trustedLabel = meta.trusted === true ? "trusted" : "untrusted";
516
- const auditLabel = (meta.auditStatus ?? "pending").trim().toLowerCase();
517
- console.log(
518
- `${skill.name}${desc}\t[${trustedLabel}; audit=${auditLabel}]${formatSourceMeta(skill)}`
519
- );
520
- } else if (opts.kind === "mcp") {
521
- const meta = entry as McpEntry & {
522
- trusted?: boolean;
523
- auditStatus?: string;
661
+ }
662
+
663
+ if (opts.kind === "mcp") {
664
+ const server = entry as McpEntry;
665
+ return {
666
+ title: server.name,
667
+ meta: sourceLabel(server),
668
+ badges: [trustBadge(server.trusted), auditBadge(server.auditStatus)],
669
+ description:
670
+ Array.isArray(server.enabledFor) && server.enabledFor.length > 0
671
+ ? `Enabled for ${server.enabledFor.join(", ")}.`
672
+ : "No enabled-for restrictions recorded.",
524
673
  };
525
- const trustedLabel = meta.trusted === true ? "trusted" : "untrusted";
526
- const auditLabel = (meta.auditStatus ?? "pending").trim().toLowerCase();
527
- console.log(
528
- `${entry.name}\t[${trustedLabel}; audit=${auditLabel}]${formatSourceMeta(entry)}`
529
- );
530
- } else {
531
- console.log(`${entry.name}${formatSourceMeta(entry)}`);
532
674
  }
533
- }
675
+
676
+ const detailEntry = entry as AgentEntry | SnippetEntry | InstructionEntry;
677
+ return {
678
+ title: entry.name,
679
+ meta: sourceLabel(entry),
680
+ description: displayDescription(detailEntry.description),
681
+ };
682
+ });
683
+
684
+ console.log(
685
+ renderPage({
686
+ title: `fclt list ${opts.kind}`,
687
+ subtitle: `${entries.length} matching entr${entries.length === 1 ? "y" : "ies"}`,
688
+ sections: [{ title: "Entries", lines: renderCatalog(items) }],
689
+ footer: describeFilters(opts.filters)
690
+ ? [describeFilters(opts.filters)]
691
+ : undefined,
692
+ })
693
+ );
534
694
  }
535
695
 
536
696
  async function readEntryContents(entryPath: string): Promise<string> {
@@ -589,35 +749,55 @@ async function findCommand(argv: string[]) {
589
749
  return;
590
750
  }
591
751
 
592
- for (const entry of matches) {
593
- const desc = entry.description ? `\t${entry.description}` : "";
594
- console.log(`${entry.kind}:${entry.name}${desc}${formatSourceMeta(entry)}`);
752
+ if (matches.length === 0) {
753
+ console.log(
754
+ renderPage({
755
+ title: "fclt find",
756
+ subtitle: `No matches for "${opts.text}".`,
757
+ sections: [
758
+ {
759
+ title: "Try",
760
+ lines: renderBullets([
761
+ renderCode("fclt list"),
762
+ renderCode("fclt index --force"),
763
+ ]),
764
+ },
765
+ ],
766
+ })
767
+ );
768
+ return;
595
769
  }
770
+
771
+ console.log(
772
+ renderPage({
773
+ title: "fclt find",
774
+ subtitle: `${matches.length} match${matches.length === 1 ? "" : "es"} for "${opts.text}"`,
775
+ sections: [
776
+ {
777
+ title: "Results",
778
+ lines: renderCatalog(
779
+ matches.map((entry) => ({
780
+ title: `${entry.kind}:${entry.name}`,
781
+ meta: sourceLabel(entry),
782
+ description: displayDescription(entry.description),
783
+ }))
784
+ ),
785
+ },
786
+ ],
787
+ })
788
+ );
596
789
  }
597
790
 
598
791
  const SECRET_KEY_RE = /(TOKEN|KEY|SECRET|PASSWORD|PASS|BEARER)/i;
599
792
  const SECRETY_STRING_RE =
600
793
  /\b(sk-[A-Za-z0-9]{10,}|ghp_[A-Za-z0-9]{10,}|github_pat_[A-Za-z0-9_]{10,})\b/g;
794
+ const INLINE_NAME_DESCRIPTION_RE = /^name:\s+\S+\s+description:\s*/i;
795
+ const TRAILING_NEWLINE_RE = /\n$/;
601
796
 
602
797
  function redactPossibleSecrets(value: string): string {
603
798
  return value.replace(SECRETY_STRING_RE, "<redacted>");
604
799
  }
605
800
 
606
- function formatSourceMeta(entry: {
607
- sourceKind?: string;
608
- scope?: string;
609
- }): string {
610
- const source = entry.sourceKind?.trim();
611
- const scope = entry.scope?.trim();
612
- if (!(source || scope)) {
613
- return "";
614
- }
615
- if (source && scope) {
616
- return `\t[${source}/${scope}]`;
617
- }
618
- return `\t[${source ?? scope}]`;
619
- }
620
-
621
801
  function sanitizeForDisplay(value: unknown): unknown {
622
802
  if (typeof value === "string") {
623
803
  return redactPossibleSecrets(value);
@@ -796,10 +976,28 @@ async function showCommand(argv: string[]) {
796
976
  }
797
977
  }
798
978
 
799
- console.log(`${kind}:${entry.name}`);
800
- console.log(JSON.stringify(displayEntry, null, 2));
801
- console.log("\n---\n");
802
- console.log(displayContents);
979
+ console.log(
980
+ renderPage({
981
+ title: `fclt show ${kind}:${entry.name}`,
982
+ subtitle: contentPath,
983
+ sections: [
984
+ {
985
+ title: "Metadata",
986
+ lines: renderJsonBlock(displayEntry),
987
+ },
988
+ {
989
+ title: "Contents",
990
+ lines: displayContents.replace(TRAILING_NEWLINE_RE, "").split("\n"),
991
+ },
992
+ ],
993
+ footer:
994
+ kind === "mcp" && !showSecrets
995
+ ? [
996
+ "Secrets are redacted. Re-run with --show-secrets only when you need raw values.",
997
+ ]
998
+ : undefined,
999
+ })
1000
+ );
803
1001
  }
804
1002
 
805
1003
  async function graphCommand(argv: string[]) {
@@ -858,29 +1056,75 @@ async function graphCommand(argv: string[]) {
858
1056
  }
859
1057
 
860
1058
  if (opts.kind === "show") {
861
- console.log(node.id);
862
- console.log(JSON.stringify(node, null, 2));
863
- console.log("\nDependencies:");
864
- for (const dep of deps) {
865
- console.log(
866
- `- ${dep.edge.kind}\t${dep.node.id}\t(${dep.edge.locator})`
867
- );
868
- }
869
- console.log("\nDependents:");
870
- for (const dependent of dependents) {
871
- console.log(
872
- `- ${dependent.edge.kind}\t${dependent.node.id}\t(${dependent.edge.locator})`
873
- );
874
- }
1059
+ console.log(
1060
+ renderPage({
1061
+ title: `fclt graph ${node.id}`,
1062
+ subtitle: `${node.kind} ${sourceLabel(node)}`,
1063
+ sections: [
1064
+ {
1065
+ title: "Node",
1066
+ lines: renderKeyValue([
1067
+ ["id", node.id],
1068
+ ["kind", node.kind],
1069
+ ["name", node.name],
1070
+ ["path", node.path ?? "—"],
1071
+ ["canonicalRef", node.canonicalRef ?? "—"],
1072
+ ]),
1073
+ },
1074
+ {
1075
+ title: "Dependencies",
1076
+ lines:
1077
+ deps.length > 0
1078
+ ? renderCatalog(
1079
+ deps.map((dep) => ({
1080
+ title: dep.node.id,
1081
+ meta: dep.edge.kind,
1082
+ details: [dep.edge.locator],
1083
+ }))
1084
+ )
1085
+ : ["No dependencies."],
1086
+ },
1087
+ {
1088
+ title: "Dependents",
1089
+ lines:
1090
+ dependents.length > 0
1091
+ ? renderCatalog(
1092
+ dependents.map((dependent) => ({
1093
+ title: dependent.node.id,
1094
+ meta: dependent.edge.kind,
1095
+ details: [dependent.edge.locator],
1096
+ }))
1097
+ )
1098
+ : ["No dependents."],
1099
+ },
1100
+ ],
1101
+ })
1102
+ );
875
1103
  return;
876
1104
  }
877
1105
 
878
1106
  const relations = opts.kind === "deps" ? deps : dependents;
879
- for (const relation of relations) {
880
- console.log(
881
- `${relation.edge.kind}\t${relation.node.id}\t(${relation.edge.locator})`
882
- );
883
- }
1107
+ console.log(
1108
+ renderPage({
1109
+ title: `fclt graph ${opts.kind}`,
1110
+ subtitle: `${relations.length} relation${relations.length === 1 ? "" : "s"} for ${node.id}`,
1111
+ sections: [
1112
+ {
1113
+ title: opts.kind === "deps" ? "Dependencies" : "Dependents",
1114
+ lines:
1115
+ relations.length > 0
1116
+ ? renderCatalog(
1117
+ relations.map((relation) => ({
1118
+ title: relation.node.id,
1119
+ meta: relation.edge.kind,
1120
+ details: [relation.edge.locator],
1121
+ }))
1122
+ )
1123
+ : ["No relations found."],
1124
+ },
1125
+ ],
1126
+ })
1127
+ );
884
1128
  } catch (err) {
885
1129
  console.error(err instanceof Error ? err.message : String(err));
886
1130
  process.exitCode = 1;
@@ -890,19 +1134,47 @@ async function graphCommand(argv: string[]) {
890
1134
  function adaptersCommand(argv: string[]) {
891
1135
  if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
892
1136
  console.log(
893
- "fclt adapters — list registered tool adapters\n\nUsage:\n fclt adapters\n"
1137
+ renderPage({
1138
+ title: "fclt adapters",
1139
+ subtitle: "List registered tool adapters.",
1140
+ sections: [
1141
+ {
1142
+ title: "Usage",
1143
+ lines: renderBullets([renderCode("fclt adapters")]),
1144
+ },
1145
+ ],
1146
+ })
894
1147
  );
895
1148
  return;
896
1149
  }
897
1150
  const adapters = getAllAdapters();
898
1151
  if (!adapters.length) {
899
- console.log("No adapters registered.");
1152
+ console.log(
1153
+ renderPage({
1154
+ title: "fclt adapters",
1155
+ subtitle: "No adapters registered.",
1156
+ sections: [],
1157
+ })
1158
+ );
900
1159
  return;
901
1160
  }
902
- for (const adapter of adapters) {
903
- const versions = adapter.versions.join(", ");
904
- console.log(`${adapter.id}\t${versions}`);
905
- }
1161
+ console.log(
1162
+ renderPage({
1163
+ title: "fclt adapters",
1164
+ subtitle: `${adapters.length} registered adapter${adapters.length === 1 ? "" : "s"}`,
1165
+ sections: [
1166
+ {
1167
+ title: "Adapters",
1168
+ lines: renderCatalog(
1169
+ adapters.map((adapter) => ({
1170
+ title: adapter.id,
1171
+ description: `Versions: ${adapter.versions.join(", ")}`,
1172
+ }))
1173
+ ),
1174
+ },
1175
+ ],
1176
+ })
1177
+ );
906
1178
  }
907
1179
 
908
1180
  async function main(argv: string[]) {