facult 2.16.0 → 2.17.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
@@ -181,6 +181,7 @@ fclt index
181
181
  Managed mode writes rendered files into a tool home. Use it only when `fclt` should own that rendered surface.
182
182
 
183
183
  ```bash
184
+ fclt setup codex-plugin
184
185
  fclt manage codex --dry-run
185
186
  fclt manage codex --adopt-existing
186
187
  fclt sync codex --dry-run
@@ -377,6 +378,7 @@ fclt index [--force]
377
378
  Managed mode:
378
379
 
379
380
  ```bash
381
+ fclt setup codex-plugin [--dry-run] [--json]
380
382
  fclt manage <tool> [--dry-run] [--adopt-existing]
381
383
  fclt sync [tool] [--dry-run] [--adopt-live]
382
384
  fclt enable <selector> --for codex,claude
@@ -426,7 +428,7 @@ Start with:
426
428
 
427
429
  ### Does fclt run an MCP server?
428
430
 
429
- The core product is still CLI-first. The first-party Codex plugin includes a small stdio MCP wrapper that delegates to the installed `fclt` binary for status, doctor, paths, setup, writeback, and evolution workflows. See [Codex plugin](./docs/codex-plugin.md).
431
+ The core product is still CLI-first. `fclt setup codex-plugin` installs the first-party Codex plugin without putting all of Codex under managed mode. The plugin includes a small stdio MCP wrapper that delegates to the installed `fclt` binary for status, doctor, paths, setup, writeback, and evolution workflows. See [Codex plugin](./docs/codex-plugin.md).
430
432
 
431
433
  ### Does fclt have to manage Codex or Claude files?
432
434
 
@@ -34,15 +34,28 @@ These tools are thin wrappers around CLI commands and return command output. Mut
34
34
 
35
35
  ## Install In Codex
36
36
 
37
- From this repository, the plugin can be rendered into the Codex plugin marketplace by managed sync:
37
+ Use the narrow setup command for normal installs:
38
+
39
+ ```bash
40
+ fclt setup codex-plugin
41
+ ```
42
+
43
+ That updates only the local `fclt` plugin payload under `~/plugins/fclt`, merges the `hack-local` marketplace entry at `~/.agents/plugins/marketplace.json`, and runs `codex plugin add fclt@hack-local --json` when the `codex` command is available. It does not enter managed mode, adopt Codex state, render `~/.codex/AGENTS.md`, or touch existing Codex skills/rules/config.
44
+
45
+ Useful flags:
46
+
47
+ ```bash
48
+ fclt setup codex-plugin --dry-run --json
49
+ fclt setup codex-plugin --no-codex-install
50
+ ```
51
+
52
+ Use managed sync only when you intentionally want `fclt` to render broader Codex tool files:
38
53
 
39
54
  ```bash
40
55
  fclt manage codex --global
41
56
  fclt sync codex --global
42
57
  ```
43
58
 
44
- That writes plugin files under the Codex plugin location and updates the personal marketplace entry. Use managed sync only when you want `fclt` to write Codex tool files.
45
-
46
59
  For local plugin development, run the lightweight checks that ship with the repository:
47
60
 
48
61
  ```bash
@@ -2,6 +2,14 @@
2
2
 
3
3
  Managed mode is optional. Use it when you want `fclt` to write rendered files into a tool home. Do not use it just to inspect or normalize existing tool-native state.
4
4
 
5
+ If you only want the first-party fclt Codex plugin, use the narrow setup path instead:
6
+
7
+ ```bash
8
+ fclt setup codex-plugin
9
+ ```
10
+
11
+ That installs/exposes the bundled plugin without adopting or rendering the rest of Codex state.
12
+
5
13
  Prefer this default workflow:
6
14
 
7
15
  ```bash
package/docs/reference.md CHANGED
@@ -65,6 +65,7 @@ Use these to create or normalize canonical capability in `~/.ai` or `<repo>/.ai`
65
65
  ## Managed mode
66
66
 
67
67
  ```bash
68
+ fclt setup codex-plugin [--dry-run] [--json] [--no-codex-install]
68
69
  fclt manage <tool> [--dry-run] [--adopt-existing]
69
70
  fclt sync [tool] [--dry-run] [--adopt-live]
70
71
  fclt enable <selector> --for codex,claude
@@ -73,7 +74,7 @@ fclt managed
73
74
  fclt unmanage <tool>
74
75
  ```
75
76
 
76
- Managed mode writes rendered output into tool homes. Read [Managed mode](./managed-mode.md) before using it on an existing setup.
77
+ `setup codex-plugin` is the narrow path for exposing the bundled fclt Codex plugin without entering managed mode. Managed mode writes rendered output into tool homes. Read [Managed mode](./managed-mode.md) before using it on an existing setup.
77
78
 
78
79
  ## Writeback and evolution
79
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "facult",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "Manage canonical AI capabilities, sync surfaces, and evolution state.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -142,6 +142,10 @@ function printHelp() {
142
142
  "manage/sync",
143
143
  "Enter managed mode and render tool-native output",
144
144
  ],
145
+ [
146
+ "setup",
147
+ "Install narrow agent integrations without full managed mode",
148
+ ],
145
149
  ["ai", "Capture writeback and evolve canonical assets"],
146
150
  ],
147
151
  }),
@@ -1365,6 +1369,9 @@ async function main(argv: string[]) {
1365
1369
  case "sync":
1366
1370
  await import("./manage").then(({ syncCommand }) => syncCommand(rest));
1367
1371
  return;
1372
+ case "setup":
1373
+ await import("./manage").then(({ setupCommand }) => setupCommand(rest));
1374
+ return;
1368
1375
  case "autosync":
1369
1376
  await import("./autosync").then(({ autosyncCommand }) =>
1370
1377
  autosyncCommand(rest)
package/src/manage.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { spawn } from "node:child_process";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import {
3
4
  appendFile,
@@ -146,6 +147,28 @@ export interface SyncOptions {
146
147
  adoptLive?: boolean;
147
148
  }
148
149
 
150
+ export interface SetupCodexPluginOptions {
151
+ homeDir?: string;
152
+ dryRun?: boolean;
153
+ installInCodex?: boolean;
154
+ codexBin?: string | null;
155
+ }
156
+
157
+ export interface SetupCodexPluginResult {
158
+ pluginDir: string;
159
+ marketplacePath: string;
160
+ changedPaths: string[];
161
+ dryRun: boolean;
162
+ codexInstall: {
163
+ status: "skipped" | "succeeded" | "failed";
164
+ command?: string[];
165
+ stdout?: string;
166
+ stderr?: string;
167
+ code?: number | null;
168
+ reason?: string;
169
+ };
170
+ }
171
+
149
172
  const MANAGED_VERSION = 1 as const;
150
173
 
151
174
  function nowIso(now?: () => Date): string {
@@ -350,6 +373,42 @@ function fcltCodexMarketplaceEntry(): Record<string, unknown> {
350
373
  };
351
374
  }
352
375
 
376
+ function hackLocalCodexMarketplaceText(text: string | null): string {
377
+ const base =
378
+ text == null
379
+ ? {
380
+ name: "hack-local",
381
+ interface: { displayName: "Hack Local Plugins" },
382
+ plugins: [],
383
+ }
384
+ : (JSON.parse(text) as unknown);
385
+ if (!isPlainObject(base)) {
386
+ throw new Error("Codex plugin marketplace must be a JSON object.");
387
+ }
388
+ const plugins = Array.isArray(base.plugins) ? [...base.plugins] : [];
389
+ const nextPlugins = plugins.filter(
390
+ (entry) => !(isPlainObject(entry) && entry.name === FCLT_CODEX_PLUGIN_NAME)
391
+ );
392
+ nextPlugins.push(fcltCodexMarketplaceEntry());
393
+ return normalizeCodexMarketplaceText(
394
+ JSON.stringify(
395
+ {
396
+ ...base,
397
+ name:
398
+ typeof base.name === "string" && base.name.trim()
399
+ ? base.name
400
+ : "hack-local",
401
+ interface: isPlainObject(base.interface)
402
+ ? base.interface
403
+ : { displayName: "Hack Local Plugins" },
404
+ plugins: nextPlugins,
405
+ },
406
+ null,
407
+ 2
408
+ )
409
+ );
410
+ }
411
+
353
412
  function withBuiltinFcltCodexMarketplaceEntry(text: string | null): string {
354
413
  const base =
355
414
  text == null
@@ -4600,6 +4659,116 @@ async function planCodexPluginFileChanges(args: {
4600
4659
  };
4601
4660
  }
4602
4661
 
4662
+ async function runCodexPluginAdd(args: {
4663
+ codexBin: string;
4664
+ cwd: string;
4665
+ }): Promise<SetupCodexPluginResult["codexInstall"]> {
4666
+ const command = [args.codexBin, "plugin", "add", "fclt@hack-local", "--json"];
4667
+ return await new Promise((resolve) => {
4668
+ const child = spawn(args.codexBin, command.slice(1), {
4669
+ cwd: args.cwd,
4670
+ env: process.env,
4671
+ stdio: ["ignore", "pipe", "pipe"],
4672
+ });
4673
+ let stdout = "";
4674
+ let stderr = "";
4675
+ child.stdout.on("data", (chunk) => {
4676
+ stdout += chunk.toString();
4677
+ });
4678
+ child.stderr.on("data", (chunk) => {
4679
+ stderr += chunk.toString();
4680
+ });
4681
+ child.on("error", (error) => {
4682
+ resolve({
4683
+ status: "failed",
4684
+ command,
4685
+ stdout,
4686
+ stderr: error.message,
4687
+ code: null,
4688
+ });
4689
+ });
4690
+ child.on("close", (code) => {
4691
+ resolve({
4692
+ status: code === 0 ? "succeeded" : "failed",
4693
+ command,
4694
+ stdout,
4695
+ stderr,
4696
+ code,
4697
+ });
4698
+ });
4699
+ });
4700
+ }
4701
+
4702
+ export async function setupCodexPlugin(
4703
+ opts: SetupCodexPluginOptions = {}
4704
+ ): Promise<SetupCodexPluginResult> {
4705
+ const home = opts.homeDir ?? homedir();
4706
+ const pluginDir = codexPluginsDir(home);
4707
+ const marketplacePath = codexPluginMarketplacePath(home);
4708
+ const sourceDir = facultBuiltinCodexPluginRoot();
4709
+ const changedPaths: string[] = [];
4710
+ const sourceHash = await hashDirectoryTree(sourceDir);
4711
+ const pluginTargetDir = join(pluginDir, FCLT_CODEX_PLUGIN_NAME);
4712
+ const targetHash = await hashDirectoryTree(pluginTargetDir);
4713
+
4714
+ if (sourceHash !== targetHash) {
4715
+ changedPaths.push(pluginTargetDir);
4716
+ }
4717
+
4718
+ const marketplaceRaw = await readTextOrNull(marketplacePath);
4719
+ let marketplaceText: string;
4720
+ try {
4721
+ marketplaceText = hackLocalCodexMarketplaceText(marketplaceRaw);
4722
+ } catch (error) {
4723
+ throw new Error(
4724
+ `Cannot update Codex plugin marketplace at ${marketplacePath}: ${
4725
+ error instanceof Error ? error.message : String(error)
4726
+ }`
4727
+ );
4728
+ }
4729
+ if (
4730
+ (await readTargetHash(marketplacePath, { normalizeText: false })) !==
4731
+ targetContentHash(marketplaceText, { normalizeText: false })
4732
+ ) {
4733
+ changedPaths.push(marketplacePath);
4734
+ }
4735
+
4736
+ const installInCodex = opts.installInCodex ?? true;
4737
+ let codexInstall: SetupCodexPluginResult["codexInstall"] = {
4738
+ status: "skipped",
4739
+ reason: installInCodex
4740
+ ? "dry-run"
4741
+ : "codex plugin install disabled by --no-codex-install",
4742
+ };
4743
+
4744
+ if (!opts.dryRun) {
4745
+ await rm(pluginTargetDir, { recursive: true, force: true });
4746
+ await ensureDir(pluginDir);
4747
+ await cp(sourceDir, pluginTargetDir, { recursive: true });
4748
+ await ensureDir(dirname(marketplacePath));
4749
+ await Bun.write(marketplacePath, marketplaceText);
4750
+
4751
+ if (installInCodex) {
4752
+ const codexBin =
4753
+ opts.codexBin === undefined ? Bun.which("codex") : opts.codexBin;
4754
+ codexInstall = codexBin
4755
+ ? await runCodexPluginAdd({ codexBin, cwd: home })
4756
+ : {
4757
+ status: "skipped",
4758
+ reason: "codex command not found",
4759
+ };
4760
+ }
4761
+ }
4762
+
4763
+ return {
4764
+ pluginDir: pluginTargetDir,
4765
+ marketplacePath,
4766
+ changedPaths: changedPaths.sort(),
4767
+ dryRun: Boolean(opts.dryRun),
4768
+ codexInstall,
4769
+ };
4770
+ }
4771
+
4603
4772
  async function syncManagedToolEntry({
4604
4773
  homeDir,
4605
4774
  tool,
@@ -5329,6 +5498,65 @@ export async function managedCommand(argv: string[] = []) {
5329
5498
  );
5330
5499
  }
5331
5500
 
5501
+ export async function setupCommand(argv: string[]) {
5502
+ const args = [...argv];
5503
+ if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
5504
+ console.log(`fclt setup — set up narrow integrations without full managed mode
5505
+
5506
+ Usage:
5507
+ fclt setup codex-plugin [--dry-run] [--json] [--no-codex-install]
5508
+
5509
+ Options:
5510
+ --dry-run Show paths that would change without writing files
5511
+ --json Print machine-readable setup result
5512
+ --no-codex-install Only expose the local marketplace entry; do not run codex plugin add
5513
+ `);
5514
+ return;
5515
+ }
5516
+ const target = args.find((arg) => !arg.startsWith("-"));
5517
+ const dryRun = args.includes("--dry-run");
5518
+ const json = args.includes("--json");
5519
+ const installInCodex = !args.includes("--no-codex-install");
5520
+ if (target !== "codex-plugin") {
5521
+ console.error("setup requires a target: codex-plugin");
5522
+ process.exitCode = 1;
5523
+ return;
5524
+ }
5525
+
5526
+ try {
5527
+ const result = await setupCodexPlugin({
5528
+ dryRun,
5529
+ installInCodex,
5530
+ });
5531
+ if (json) {
5532
+ console.log(JSON.stringify(result, null, 2));
5533
+ return;
5534
+ }
5535
+ if (dryRun) {
5536
+ console.log("codex plugin setup dry-run");
5537
+ } else {
5538
+ console.log("codex plugin setup complete");
5539
+ }
5540
+ console.log(`plugin: ${result.pluginDir}`);
5541
+ console.log(`marketplace: ${result.marketplacePath}`);
5542
+ console.log(`changed: ${result.changedPaths.length}`);
5543
+ if (result.codexInstall.status === "succeeded") {
5544
+ console.log("codex install: succeeded");
5545
+ } else if (result.codexInstall.status === "failed") {
5546
+ console.log("codex install: failed");
5547
+ if (result.codexInstall.stderr?.trim()) {
5548
+ console.log(result.codexInstall.stderr.trim());
5549
+ }
5550
+ process.exitCode = 1;
5551
+ } else {
5552
+ console.log(`codex install: skipped (${result.codexInstall.reason})`);
5553
+ }
5554
+ } catch (err) {
5555
+ console.error(err instanceof Error ? err.message : String(err));
5556
+ process.exitCode = 1;
5557
+ }
5558
+ }
5559
+
5332
5560
  export async function syncCommand(argv: string[]) {
5333
5561
  const parsed = parseCliContextArgs(argv);
5334
5562
  if (