threadroot 0.1.2 → 0.1.4

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/dist/index.js CHANGED
@@ -5,11 +5,11 @@ import { Command } from "commander";
5
5
 
6
6
  // src/core/bootstrap.ts
7
7
  import { stat as stat9 } from "fs/promises";
8
- import path22 from "path";
8
+ import path23 from "path";
9
9
 
10
10
  // src/core/doctor.ts
11
11
  import { stat as stat5 } from "fs/promises";
12
- import path14 from "path";
12
+ import path15 from "path";
13
13
 
14
14
  // src/core/compile/index.ts
15
15
  import { readFile, stat } from "fs/promises";
@@ -1799,12 +1799,15 @@ function codexAgentsBlock() {
1799
1799
  ""
1800
1800
  ].join("\n");
1801
1801
  }
1802
- function codexMcpBlock() {
1802
+ function tomlString(value) {
1803
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1804
+ }
1805
+ function codexMcpBlock(entry) {
1803
1806
  return [
1804
1807
  CODEX_MCP_BEGIN,
1805
1808
  "[mcp_servers.threadroot]",
1806
- 'command = "threadroot"',
1807
- 'args = ["mcp"]',
1809
+ `command = ${tomlString(entry.command)}`,
1810
+ `args = [${entry.args.map(tomlString).join(", ")}]`,
1808
1811
  CODEX_MCP_END,
1809
1812
  ""
1810
1813
  ].join("\n");
@@ -1897,7 +1900,7 @@ async function setupCodexAgents(home, mode) {
1897
1900
  await writeFile4(filePath, desired, "utf8");
1898
1901
  return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status };
1899
1902
  }
1900
- async function setupCodexMcp(home, mode) {
1903
+ async function setupCodexMcp(home, mode, entry) {
1901
1904
  const filePath = codexConfigPath(home);
1902
1905
  const shown = displayPath(home, filePath);
1903
1906
  const existing = await readMaybe(filePath) ?? "";
@@ -1911,7 +1914,7 @@ async function setupCodexMcp(home, mode) {
1911
1914
  message: "Existing unmanaged [mcp_servers.threadroot] table found. Leaving it untouched."
1912
1915
  };
1913
1916
  }
1914
- const desired = upsertManagedBlock(existing, codexMcpBlock(), CODEX_MCP_BEGIN, CODEX_MCP_END);
1917
+ const desired = upsertManagedBlock(existing, codexMcpBlock(entry), CODEX_MCP_BEGIN, CODEX_MCP_END);
1915
1918
  if (mode === "check") {
1916
1919
  return {
1917
1920
  kind: "codex-mcp",
@@ -1950,7 +1953,7 @@ async function setupGlobal(options = {}) {
1950
1953
  if (providerIds.includes("codex")) {
1951
1954
  entries.push(await setupCodexAgents(home, mode));
1952
1955
  if (options.mcp) {
1953
- entries.push(await setupCodexMcp(home, mode));
1956
+ entries.push(await setupCodexMcp(home, mode, options.mcpEntry ?? { command: "threadroot", args: ["mcp"] }));
1954
1957
  }
1955
1958
  }
1956
1959
  return { entries };
@@ -2390,6 +2393,390 @@ async function checkToolHealth(repoRoot, tool) {
2390
2393
  return { status: "ok", tool: tool.name, result };
2391
2394
  }
2392
2395
 
2396
+ // src/core/mcp-check.ts
2397
+ import { spawn as spawn2 } from "child_process";
2398
+ import { access, mkdtemp, readFile as readFile8, rm as rm2, writeFile as writeFile6 } from "fs/promises";
2399
+ import { constants, realpathSync } from "fs";
2400
+ import { homedir as homedir2, tmpdir } from "os";
2401
+ import path14 from "path";
2402
+
2403
+ // src/core/version.ts
2404
+ var THREADROOT_VERSION = "0.1.4";
2405
+
2406
+ // src/core/mcp-check.ts
2407
+ var REQUIRED_MCP_TOOLS = [
2408
+ "context",
2409
+ "skills_list",
2410
+ "skills_get",
2411
+ "tools_list",
2412
+ "tools_check",
2413
+ "tools_run",
2414
+ "tools_create",
2415
+ "tools_detect",
2416
+ "connections_list",
2417
+ "connections_check",
2418
+ "memory_read",
2419
+ "memory_append",
2420
+ "status",
2421
+ "doctor"
2422
+ ];
2423
+ function codexConfigPath2(home = homedir2()) {
2424
+ return path14.join(home, ".codex", "config.toml");
2425
+ }
2426
+ function mcpEntryForCurrentProcess() {
2427
+ return mcpEntryForScriptPath(process.argv[1]);
2428
+ }
2429
+ function mcpEntryForScriptPath(rawScriptPath) {
2430
+ const scriptPath = currentScriptPath(rawScriptPath);
2431
+ if (scriptPath && isNpxPackagePath(scriptPath)) {
2432
+ return { command: "npx", args: ["--yes", `threadroot@${THREADROOT_VERSION}`, "mcp"] };
2433
+ }
2434
+ if (scriptPath && path14.basename(scriptPath) === "index.js" && scriptPath.includes(`${path14.sep}dist${path14.sep}`)) {
2435
+ return { command: process.execPath, args: [scriptPath, "mcp"] };
2436
+ }
2437
+ return { command: "threadroot", args: ["mcp"] };
2438
+ }
2439
+ function currentScriptPath(scriptPath) {
2440
+ if (!scriptPath) {
2441
+ return void 0;
2442
+ }
2443
+ try {
2444
+ return realpathSync(scriptPath);
2445
+ } catch {
2446
+ return scriptPath;
2447
+ }
2448
+ }
2449
+ function isNpxPackagePath(scriptPath) {
2450
+ const normalized = scriptPath.split(path14.sep).join("/");
2451
+ return normalized.includes("/.npm/_npx/") && normalized.includes("/node_modules/threadroot/");
2452
+ }
2453
+ async function readCodexThreadrootMcpEntry(home = homedir2()) {
2454
+ let raw;
2455
+ try {
2456
+ raw = await readFile8(codexConfigPath2(home), "utf8");
2457
+ } catch (error) {
2458
+ if (error.code === "ENOENT") {
2459
+ return void 0;
2460
+ }
2461
+ throw error;
2462
+ }
2463
+ const table = raw.match(/(?:^|\n)\[mcp_servers\.threadroot\]\s*\n(?<body>[\s\S]*?)(?=\n\[|$)/);
2464
+ if (!table?.groups?.body) {
2465
+ return void 0;
2466
+ }
2467
+ const command = matchTomlString(table.groups.body, "command");
2468
+ const args = matchTomlArray(table.groups.body, "args");
2469
+ if (!command || !args) {
2470
+ return void 0;
2471
+ }
2472
+ return { command, args };
2473
+ }
2474
+ function matchTomlString(body, key) {
2475
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2476
+ const match = body.match(new RegExp(`^${escaped}\\s*=\\s*"(?<value>(?:[^"\\\\]|\\\\.)*)"\\s*$`, "m"));
2477
+ return match?.groups?.value?.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
2478
+ }
2479
+ function matchTomlArray(body, key) {
2480
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2481
+ const match = body.match(new RegExp(`^${escaped}\\s*=\\s*\\[(?<value>.*)\\]\\s*$`, "m"));
2482
+ if (!match?.groups?.value) {
2483
+ return void 0;
2484
+ }
2485
+ const values = [];
2486
+ const pattern = /"((?:[^"\\]|\\.)*)"/g;
2487
+ let value;
2488
+ while (value = pattern.exec(match.groups.value)) {
2489
+ values.push(value[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\"));
2490
+ }
2491
+ return values;
2492
+ }
2493
+ async function commandExists2(command) {
2494
+ if (path14.isAbsolute(command) || command.includes(path14.sep)) {
2495
+ try {
2496
+ await access(command, constants.X_OK);
2497
+ return true;
2498
+ } catch {
2499
+ return false;
2500
+ }
2501
+ }
2502
+ const paths = (process.env.PATH ?? "").split(path14.delimiter).filter(Boolean);
2503
+ for (const dir of paths) {
2504
+ try {
2505
+ await access(path14.join(dir, command), constants.X_OK);
2506
+ return true;
2507
+ } catch {
2508
+ }
2509
+ }
2510
+ return false;
2511
+ }
2512
+ async function checkCodexMcp(input2) {
2513
+ const configPath = codexConfigPath2(input2.home);
2514
+ const entry = await readCodexThreadrootMcpEntry(input2.home);
2515
+ if (!entry) {
2516
+ return {
2517
+ status: "warning",
2518
+ configPath,
2519
+ tools: [],
2520
+ messages: ["No Codex Threadroot MCP config found."]
2521
+ };
2522
+ }
2523
+ if (!await commandExists2(entry.command)) {
2524
+ return {
2525
+ status: "error",
2526
+ configPath,
2527
+ entry,
2528
+ tools: [],
2529
+ messages: [`MCP command is not executable or not on PATH: ${entry.command}`]
2530
+ };
2531
+ }
2532
+ try {
2533
+ const handshake = await runMcpHandshake(entry, input2.repoRoot, input2.timeoutMs ?? 4e3);
2534
+ const toolNames = handshake.tools;
2535
+ const missing = REQUIRED_MCP_TOOLS.filter((tool) => !toolNames.includes(tool));
2536
+ if (missing.length > 0) {
2537
+ return {
2538
+ status: "error",
2539
+ configPath,
2540
+ entry,
2541
+ serverInfo: handshake.serverInfo,
2542
+ tools: toolNames,
2543
+ messages: [`MCP server is missing required tool(s): ${missing.join(", ")}`]
2544
+ };
2545
+ }
2546
+ return {
2547
+ status: "ok",
2548
+ configPath,
2549
+ entry,
2550
+ serverInfo: handshake.serverInfo,
2551
+ tools: toolNames,
2552
+ messages: ["MCP server initialized and returned the expected Threadroot tools."]
2553
+ };
2554
+ } catch (error) {
2555
+ return {
2556
+ status: "error",
2557
+ configPath,
2558
+ entry,
2559
+ tools: [],
2560
+ messages: [`MCP handshake failed: ${error instanceof Error ? error.message : String(error)}`]
2561
+ };
2562
+ }
2563
+ }
2564
+ function runMcpHandshake(entry, repoRoot, timeoutMs) {
2565
+ if (process.platform !== "win32") {
2566
+ return runOneShotMcpHandshake(entry, repoRoot, timeoutMs);
2567
+ }
2568
+ return new Promise((resolve, reject) => {
2569
+ const child = spawn2(entry.command, entry.args, {
2570
+ cwd: repoRoot,
2571
+ stdio: ["pipe", "pipe", "pipe"],
2572
+ env: process.env
2573
+ });
2574
+ let settled = false;
2575
+ const finish = (result) => {
2576
+ if (settled) return;
2577
+ settled = true;
2578
+ clearTimeout(timer);
2579
+ child.kill();
2580
+ resolve(result);
2581
+ };
2582
+ const fail = (error) => {
2583
+ if (settled) return;
2584
+ settled = true;
2585
+ clearTimeout(timer);
2586
+ child.kill();
2587
+ reject(error);
2588
+ };
2589
+ const timer = setTimeout(() => {
2590
+ fail(new Error(`Timed out after ${timeoutMs}ms`));
2591
+ }, timeoutMs);
2592
+ let stdout = "";
2593
+ let stderr = "";
2594
+ let initialized = false;
2595
+ let serverInfo;
2596
+ child.stdout.setEncoding("utf8");
2597
+ child.stderr.setEncoding("utf8");
2598
+ child.stdout.on("data", (chunk) => {
2599
+ stdout += chunk;
2600
+ const lines = stdout.split("\n");
2601
+ stdout = lines.pop() ?? "";
2602
+ for (const line of lines) {
2603
+ if (!line.trim()) {
2604
+ continue;
2605
+ }
2606
+ let message;
2607
+ try {
2608
+ message = JSON.parse(line);
2609
+ } catch (error) {
2610
+ fail(new Error(`Invalid JSON-RPC response: ${error instanceof Error ? error.message : String(error)}`));
2611
+ return;
2612
+ }
2613
+ if (message.error) {
2614
+ fail(new Error(message.error.message ?? "MCP server returned an error"));
2615
+ return;
2616
+ }
2617
+ if (message.id === 1) {
2618
+ initialized = true;
2619
+ serverInfo = message.result?.serverInfo;
2620
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}
2621
+ `);
2622
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" })}
2623
+ `);
2624
+ }
2625
+ if (message.id === 2) {
2626
+ finish({
2627
+ serverInfo,
2628
+ tools: (message.result?.tools ?? []).map((tool) => tool.name)
2629
+ });
2630
+ }
2631
+ }
2632
+ });
2633
+ child.stderr.on("data", (chunk) => {
2634
+ stderr += chunk;
2635
+ });
2636
+ child.on("error", (error) => {
2637
+ fail(error);
2638
+ });
2639
+ child.on("close", (code) => {
2640
+ if (!settled && !initialized && code !== null) {
2641
+ fail(new Error(`Server exited before initialize completed (exit ${code})${stderr ? `: ${stderr}` : ""}`));
2642
+ }
2643
+ });
2644
+ child.stdin.write(
2645
+ `${JSON.stringify({
2646
+ jsonrpc: "2.0",
2647
+ id: 1,
2648
+ method: "initialize",
2649
+ params: {
2650
+ protocolVersion: "2024-11-05",
2651
+ capabilities: {},
2652
+ clientInfo: { name: "threadroot-check", version: THREADROOT_VERSION }
2653
+ }
2654
+ })}
2655
+ `
2656
+ );
2657
+ });
2658
+ }
2659
+ async function runOneShotMcpHandshake(entry, repoRoot, timeoutMs) {
2660
+ const tempDir = await mkdtemp(path14.join(tmpdir(), "threadroot-mcp-check-"));
2661
+ const inputPath = path14.join(tempDir, "input.jsonl");
2662
+ const stdoutPath = path14.join(tempDir, "stdout.jsonl");
2663
+ const stderrPath = path14.join(tempDir, "stderr.txt");
2664
+ try {
2665
+ await writeFile6(
2666
+ inputPath,
2667
+ [
2668
+ JSON.stringify({
2669
+ jsonrpc: "2.0",
2670
+ id: 1,
2671
+ method: "initialize",
2672
+ params: {
2673
+ protocolVersion: "2024-11-05",
2674
+ capabilities: {},
2675
+ clientInfo: { name: "threadroot-check", version: THREADROOT_VERSION }
2676
+ }
2677
+ }),
2678
+ JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
2679
+ JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }),
2680
+ ""
2681
+ ].join("\n"),
2682
+ "utf8"
2683
+ );
2684
+ const commandLine = [
2685
+ shellQuote3(entry.command),
2686
+ ...entry.args.map(shellQuote3),
2687
+ "<",
2688
+ shellQuote3(inputPath),
2689
+ ">",
2690
+ shellQuote3(stdoutPath),
2691
+ "2>",
2692
+ shellQuote3(stderrPath)
2693
+ ].join(" ");
2694
+ await runShell(commandLine, repoRoot, timeoutMs);
2695
+ const [stdout, stderr] = await Promise.all([readFile8(stdoutPath, "utf8"), readOptional(stderrPath)]);
2696
+ if (stderr.trim()) {
2697
+ throw new Error(stderr.trim());
2698
+ }
2699
+ return parseHandshakeOutput(stdout);
2700
+ } finally {
2701
+ await rm2(tempDir, { recursive: true, force: true });
2702
+ }
2703
+ }
2704
+ function runShell(commandLine, repoRoot, timeoutMs) {
2705
+ return new Promise((resolve, reject) => {
2706
+ const child = spawn2("sh", ["-c", commandLine], {
2707
+ cwd: repoRoot,
2708
+ stdio: "ignore",
2709
+ env: process.env
2710
+ });
2711
+ let settled = false;
2712
+ const fail = (error) => {
2713
+ if (settled) return;
2714
+ settled = true;
2715
+ clearTimeout(timer);
2716
+ child.kill();
2717
+ reject(error);
2718
+ };
2719
+ const timer = setTimeout(() => fail(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
2720
+ child.on("error", fail);
2721
+ child.on("close", async (code) => {
2722
+ if (settled) return;
2723
+ settled = true;
2724
+ clearTimeout(timer);
2725
+ if (code === 0) {
2726
+ resolve();
2727
+ return;
2728
+ }
2729
+ reject(new Error(`Server exited with status ${code ?? "unknown"}`));
2730
+ });
2731
+ });
2732
+ }
2733
+ function parseHandshakeOutput(stdout) {
2734
+ let initialized = false;
2735
+ let serverInfo;
2736
+ let tools2;
2737
+ for (const line of stdout.split("\n")) {
2738
+ if (!line.trim()) {
2739
+ continue;
2740
+ }
2741
+ let message;
2742
+ try {
2743
+ message = JSON.parse(line);
2744
+ } catch (error) {
2745
+ throw new Error(`Invalid JSON-RPC response: ${error instanceof Error ? error.message : String(error)}`);
2746
+ }
2747
+ if (message.error) {
2748
+ throw new Error(message.error.message ?? "MCP server returned an error");
2749
+ }
2750
+ if (message.id === 1) {
2751
+ initialized = true;
2752
+ serverInfo = message.result?.serverInfo;
2753
+ }
2754
+ if (message.id === 2) {
2755
+ tools2 = (message.result?.tools ?? []).map((tool) => tool.name);
2756
+ }
2757
+ }
2758
+ if (!initialized) {
2759
+ throw new Error("Server did not return an initialize response");
2760
+ }
2761
+ if (!tools2) {
2762
+ throw new Error("Server did not return a tools/list response");
2763
+ }
2764
+ return { serverInfo, tools: tools2 };
2765
+ }
2766
+ async function readOptional(filePath) {
2767
+ try {
2768
+ return await readFile8(filePath, "utf8");
2769
+ } catch (error) {
2770
+ if (error.code === "ENOENT") {
2771
+ return "";
2772
+ }
2773
+ throw error;
2774
+ }
2775
+ }
2776
+ function shellQuote3(value) {
2777
+ return `'${value.replace(/'/g, "'\\''")}'`;
2778
+ }
2779
+
2393
2780
  // src/core/doctor.ts
2394
2781
  async function exists3(filePath) {
2395
2782
  try {
@@ -2405,9 +2792,23 @@ async function exists3(filePath) {
2405
2792
  function finding2(severity, code, message, pathValue) {
2406
2793
  return pathValue ? { severity, code, message, path: pathValue } : { severity, code, message };
2407
2794
  }
2408
- async function mcpConfigHints(repoRoot) {
2795
+ async function mcpConfigHints(repoRoot, home) {
2796
+ const codexMcp = await checkCodexMcp({ repoRoot, home, timeoutMs: 2500 });
2797
+ if (codexMcp.status === "ok") {
2798
+ return [];
2799
+ }
2800
+ if (codexMcp.status === "error") {
2801
+ return [
2802
+ finding2(
2803
+ "warning",
2804
+ "codex_mcp_unhealthy",
2805
+ `Codex Threadroot MCP is configured but failed verification: ${codexMcp.messages.join(" ")}`,
2806
+ codexMcp.configPath
2807
+ )
2808
+ ];
2809
+ }
2409
2810
  const configs = [".vscode/mcp.json", ".cursor/mcp.json", ".mcp.json"];
2410
- const present = await Promise.all(configs.map((config) => exists3(path14.join(repoRoot, config))));
2811
+ const present = await Promise.all(configs.map((config) => exists3(path15.join(repoRoot, config))));
2411
2812
  if (present.some(Boolean)) {
2412
2813
  return [];
2413
2814
  }
@@ -2581,7 +2982,7 @@ async function doctor(repoRoot, options = {}) {
2581
2982
  )
2582
2983
  );
2583
2984
  }
2584
- const scriptsDir = path14.join(path14.dirname(skill.sourcePath), "scripts");
2985
+ const scriptsDir = path15.join(path15.dirname(skill.sourcePath), "scripts");
2585
2986
  if (await exists3(scriptsDir)) {
2586
2987
  findings.push(
2587
2988
  finding2(
@@ -2594,7 +2995,7 @@ async function doctor(repoRoot, options = {}) {
2594
2995
  }
2595
2996
  }
2596
2997
  findings.push(...await globalSetupHints(options.home));
2597
- findings.push(...await mcpConfigHints(repoRoot));
2998
+ findings.push(...await mcpConfigHints(repoRoot, options.home));
2598
2999
  return summarize(findings);
2599
3000
  }
2600
3001
  function summarize(findings) {
@@ -2605,11 +3006,11 @@ function summarize(findings) {
2605
3006
  }
2606
3007
 
2607
3008
  // src/core/expose.ts
2608
- import { mkdir as mkdir6, readFile as readFile8, rm as rm2, stat as stat6, writeFile as writeFile6 } from "fs/promises";
2609
- import path15 from "path";
3009
+ import { mkdir as mkdir6, readFile as readFile9, rm as rm3, stat as stat6, writeFile as writeFile7 } from "fs/promises";
3010
+ import path16 from "path";
2610
3011
  async function readMaybe2(filePath) {
2611
3012
  try {
2612
- return await readFile8(filePath, "utf8");
3013
+ return await readFile9(filePath, "utf8");
2613
3014
  } catch (error) {
2614
3015
  if (error.code === "ENOENT") {
2615
3016
  return void 0;
@@ -2618,10 +3019,10 @@ async function readMaybe2(filePath) {
2618
3019
  }
2619
3020
  }
2620
3021
  function projectSkillPath(repoRoot, provider) {
2621
- return path15.join(repoRoot, provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
3022
+ return path16.join(repoRoot, provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
2622
3023
  }
2623
3024
  function relSkillPath(provider) {
2624
- return path15.join(provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
3025
+ return path16.join(provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
2625
3026
  }
2626
3027
  async function exposeOne(repoRoot, provider, mode, force) {
2627
3028
  const relativePath = relSkillPath(provider);
@@ -2653,7 +3054,7 @@ async function exposeOne(repoRoot, provider, mode, force) {
2653
3054
  message: "Existing skill is not Threadroot-managed."
2654
3055
  };
2655
3056
  }
2656
- await rm2(path15.dirname(absolutePath), { recursive: true, force: true });
3057
+ await rm3(path16.dirname(absolutePath), { recursive: true, force: true });
2657
3058
  return { agent: provider.id, label: provider.label, path: relativePath, status: "removed" };
2658
3059
  }
2659
3060
  if (existing === desired) {
@@ -2672,8 +3073,8 @@ async function exposeOne(repoRoot, provider, mode, force) {
2672
3073
  if (mode === "dry-run") {
2673
3074
  return { agent: provider.id, label: provider.label, path: relativePath, status };
2674
3075
  }
2675
- await mkdir6(path15.dirname(absolutePath), { recursive: true });
2676
- await writeFile6(absolutePath, desired, "utf8");
3076
+ await mkdir6(path16.dirname(absolutePath), { recursive: true });
3077
+ await writeFile7(absolutePath, desired, "utf8");
2677
3078
  return { agent: provider.id, label: provider.label, path: relativePath, status };
2678
3079
  }
2679
3080
  async function exposeProject(repoRoot, options = {}) {
@@ -2687,18 +3088,18 @@ async function exposeProject(repoRoot, options = {}) {
2687
3088
  }
2688
3089
 
2689
3090
  // src/core/init/index.ts
2690
- import { mkdir as mkdir9, stat as stat8, writeFile as writeFile8 } from "fs/promises";
2691
- import path21 from "path";
3091
+ import { mkdir as mkdir9, stat as stat8, writeFile as writeFile9 } from "fs/promises";
3092
+ import path22 from "path";
2692
3093
 
2693
3094
  // src/core/compile/write.ts
2694
- import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
2695
- import path16 from "path";
3095
+ import { mkdir as mkdir7, writeFile as writeFile8 } from "fs/promises";
3096
+ import path17 from "path";
2696
3097
  async function writeCompiled(repoRoot, files) {
2697
3098
  await Promise.all(
2698
3099
  files.map(async (file) => {
2699
- const absolute = path16.join(repoRoot, file.path);
2700
- await mkdir7(path16.dirname(absolute), { recursive: true });
2701
- await writeFile7(absolute, file.content, "utf8");
3100
+ const absolute = path17.join(repoRoot, file.path);
3101
+ await mkdir7(path17.dirname(absolute), { recursive: true });
3102
+ await writeFile8(absolute, file.content, "utf8");
2702
3103
  })
2703
3104
  );
2704
3105
  return files.map((file) => file.path);
@@ -2714,7 +3115,7 @@ async function runCompile(repoRoot, options = {}) {
2714
3115
 
2715
3116
  // src/core/scan/package.ts
2716
3117
  import fs from "fs/promises";
2717
- import path17 from "path";
3118
+ import path18 from "path";
2718
3119
 
2719
3120
  // src/core/scan/rules.ts
2720
3121
  var ignoredDirectories = /* @__PURE__ */ new Set([
@@ -2732,13 +3133,13 @@ var ignoredDirectories = /* @__PURE__ */ new Set([
2732
3133
  // src/core/scan/package.ts
2733
3134
  async function readJson(repoRoot, relativePath) {
2734
3135
  try {
2735
- return JSON.parse(await fs.readFile(path17.join(repoRoot, relativePath), "utf8"));
3136
+ return JSON.parse(await fs.readFile(path18.join(repoRoot, relativePath), "utf8"));
2736
3137
  } catch {
2737
3138
  return void 0;
2738
3139
  }
2739
3140
  }
2740
3141
  function inferProfile(files, packageJson) {
2741
- if (files.some((file) => path17.basename(file) === "dbt_project.yml" || path17.basename(file) === "dbt_project.yaml")) {
3142
+ if (files.some((file) => path18.basename(file) === "dbt_project.yml" || path18.basename(file) === "dbt_project.yaml")) {
2742
3143
  return "dbt";
2743
3144
  }
2744
3145
  const packageMeta = packageJson && typeof packageJson === "object" ? packageJson : void 0;
@@ -2763,9 +3164,9 @@ function inferProfile(files, packageJson) {
2763
3164
 
2764
3165
  // src/core/scan/walk.ts
2765
3166
  import fs2 from "fs/promises";
2766
- import path18 from "path";
3167
+ import path19 from "path";
2767
3168
  function toPosix(relativePath) {
2768
- return relativePath.split(path18.sep).join("/");
3169
+ return relativePath.split(path19.sep).join("/");
2769
3170
  }
2770
3171
  async function walkRepo(repoRoot, directory = repoRoot) {
2771
3172
  let entries;
@@ -2776,8 +3177,8 @@ async function walkRepo(repoRoot, directory = repoRoot) {
2776
3177
  }
2777
3178
  const files = [];
2778
3179
  for (const entry of entries) {
2779
- const absolutePath = path18.join(directory, entry.name);
2780
- const relativePath = toPosix(path18.relative(repoRoot, absolutePath));
3180
+ const absolutePath = path19.join(directory, entry.name);
3181
+ const relativePath = toPosix(path19.relative(repoRoot, absolutePath));
2781
3182
  if (entry.isDirectory()) {
2782
3183
  if (!ignoredDirectories.has(entry.name)) {
2783
3184
  files.push(...await walkRepo(repoRoot, absolutePath));
@@ -2796,16 +3197,16 @@ import { stringify as stringifyYaml4 } from "yaml";
2796
3197
 
2797
3198
  // src/core/init/builtins.ts
2798
3199
  import { cp, mkdir as mkdir8, readdir as readdir3, stat as stat7 } from "fs/promises";
2799
- import path19 from "path";
3200
+ import path20 from "path";
2800
3201
  import { fileURLToPath } from "url";
2801
- var DIST_DIR = path19.dirname(fileURLToPath(import.meta.url));
2802
- var PACKAGE_ROOT_FROM_BUNDLE = path19.resolve(DIST_DIR, "..");
2803
- var PACKAGE_ROOT_FROM_DIST = path19.resolve(DIST_DIR, "../../..");
2804
- var PACKAGE_ROOT_FROM_SRC = path19.resolve(DIST_DIR, "../../../..");
3202
+ var DIST_DIR = path20.dirname(fileURLToPath(import.meta.url));
3203
+ var PACKAGE_ROOT_FROM_BUNDLE = path20.resolve(DIST_DIR, "..");
3204
+ var PACKAGE_ROOT_FROM_DIST = path20.resolve(DIST_DIR, "../../..");
3205
+ var PACKAGE_ROOT_FROM_SRC = path20.resolve(DIST_DIR, "../../../..");
2805
3206
  var SKILL_PACK_CANDIDATES = [
2806
- path19.join(PACKAGE_ROOT_FROM_BUNDLE, "skills"),
2807
- path19.join(PACKAGE_ROOT_FROM_DIST, "skills"),
2808
- path19.join(PACKAGE_ROOT_FROM_SRC, "skills")
3207
+ path20.join(PACKAGE_ROOT_FROM_BUNDLE, "skills"),
3208
+ path20.join(PACKAGE_ROOT_FROM_DIST, "skills"),
3209
+ path20.join(PACKAGE_ROOT_FROM_SRC, "skills")
2809
3210
  ];
2810
3211
  var PROJECT_MEMORY_TEMPLATE = [
2811
3212
  "# Project",
@@ -2842,13 +3243,13 @@ async function writeBuiltinSkills(repoRoot) {
2842
3243
  if (!entry.isDirectory()) {
2843
3244
  continue;
2844
3245
  }
2845
- const sourceSkill = path19.join(sourceDir, entry.name);
2846
- const sourceSkillFile = path19.join(sourceSkill, "SKILL.md");
3246
+ const sourceSkill = path20.join(sourceDir, entry.name);
3247
+ const sourceSkillFile = path20.join(sourceSkill, "SKILL.md");
2847
3248
  if (!await exists4(sourceSkill) || !await stat7(sourceSkillFile).then((info) => info.isFile()).catch(() => false)) {
2848
3249
  continue;
2849
3250
  }
2850
- const targetSkill = path19.join(targetDir, entry.name);
2851
- const targetSkillFile = path19.join(targetSkill, "SKILL.md");
3251
+ const targetSkill = path20.join(targetDir, entry.name);
3252
+ const targetSkillFile = path20.join(targetSkill, "SKILL.md");
2852
3253
  try {
2853
3254
  await cp(sourceSkill, targetSkill, { recursive: true, force: false, errorOnExist: true });
2854
3255
  written.push(targetSkillFile);
@@ -2862,14 +3263,14 @@ async function writeBuiltinSkills(repoRoot) {
2862
3263
  }
2863
3264
 
2864
3265
  // src/core/init/import.ts
2865
- import { readFile as readFile9, readdir as readdir4 } from "fs/promises";
2866
- import path20 from "path";
3266
+ import { readFile as readFile10, readdir as readdir4 } from "fs/promises";
3267
+ import path21 from "path";
2867
3268
  var PROSE_PRECEDENCE = ["AGENTS.md", "CLAUDE.md", ".github/copilot-instructions.md"];
2868
3269
  var CURSOR_RULES_DIR = ".cursor/rules";
2869
3270
  var NAME_RE4 = /^[a-z0-9][a-z0-9-]*$/;
2870
3271
  async function readIfExists2(filePath) {
2871
3272
  try {
2872
- return await readFile9(filePath, "utf8");
3273
+ return await readFile10(filePath, "utf8");
2873
3274
  } catch (error) {
2874
3275
  if (error.code === "ENOENT") {
2875
3276
  return void 0;
@@ -2913,7 +3314,7 @@ function novelSections(canonical, other) {
2913
3314
  });
2914
3315
  }
2915
3316
  async function listCursorRules(repoRoot) {
2916
- const dir = path20.join(repoRoot, CURSOR_RULES_DIR);
3317
+ const dir = path21.join(repoRoot, CURSOR_RULES_DIR);
2917
3318
  let entries;
2918
3319
  try {
2919
3320
  entries = await readdir4(dir);
@@ -2927,7 +3328,7 @@ async function listCursorRules(repoRoot) {
2927
3328
  return Promise.all(
2928
3329
  files.map(async (name) => ({
2929
3330
  file: `${CURSOR_RULES_DIR}/${name}`,
2930
- content: await readFile9(path20.join(dir, name), "utf8")
3331
+ content: await readFile10(path21.join(dir, name), "utf8")
2931
3332
  }))
2932
3333
  );
2933
3334
  }
@@ -2942,7 +3343,7 @@ function globsToApplyTo(value) {
2942
3343
  return void 0;
2943
3344
  }
2944
3345
  function ruleName(fileName) {
2945
- const base = path20.basename(fileName, ".mdc").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
3346
+ const base = path21.basename(fileName, ".mdc").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
2946
3347
  return NAME_RE4.test(base) ? base : "imported-rule";
2947
3348
  }
2948
3349
  async function importVendorFiles(repoRoot, options = {}) {
@@ -2953,7 +3354,7 @@ async function importVendorFiles(repoRoot, options = {}) {
2953
3354
  if (!wanted(file)) {
2954
3355
  continue;
2955
3356
  }
2956
- const content = await readIfExists2(path20.join(repoRoot, file));
3357
+ const content = await readIfExists2(path21.join(repoRoot, file));
2957
3358
  if (content && content.trim()) {
2958
3359
  prose.push({ file, content });
2959
3360
  }
@@ -3028,7 +3429,7 @@ async function detectName(repoRoot) {
3028
3429
  if (packageJson && typeof packageJson.name === "string" && packageJson.name.trim()) {
3029
3430
  return packageJson.name.trim();
3030
3431
  }
3031
- return path21.basename(repoRoot);
3432
+ return path22.basename(repoRoot);
3032
3433
  }
3033
3434
  async function writeManifest(repoRoot, manifest) {
3034
3435
  const body = {
@@ -3041,14 +3442,14 @@ async function writeManifest(repoRoot, manifest) {
3041
3442
  body.tools = { allow: manifest.tools.allow };
3042
3443
  }
3043
3444
  await mkdir9(projectHarnessDir(repoRoot), { recursive: true });
3044
- await writeFile8(projectManifestPath(repoRoot), stringifyYaml4(body), "utf8");
3445
+ await writeFile9(projectManifestPath(repoRoot), stringifyYaml4(body), "utf8");
3045
3446
  }
3046
3447
  async function writeProjectMemory(repoRoot) {
3047
3448
  const dir = projectObjectDir(repoRoot, "memory");
3048
3449
  await mkdir9(dir, { recursive: true });
3049
- const filePath = path21.join(dir, "project.md");
3450
+ const filePath = path22.join(dir, "project.md");
3050
3451
  try {
3051
- await writeFile8(filePath, `${PROJECT_MEMORY_TEMPLATE}
3452
+ await writeFile9(filePath, `${PROJECT_MEMORY_TEMPLATE}
3052
3453
  `, { encoding: "utf8", flag: "wx" });
3053
3454
  return [filePath];
3054
3455
  } catch (error) {
@@ -3066,13 +3467,13 @@ async function writeImportedRules(repoRoot, report) {
3066
3467
  await mkdir9(dir, { recursive: true });
3067
3468
  const written = [];
3068
3469
  for (const rule of report.importedRules) {
3069
- const filePath = path21.join(dir, `${rule.name}.md`);
3470
+ const filePath = path22.join(dir, `${rule.name}.md`);
3070
3471
  const data = { name: rule.name, scope: "project" };
3071
3472
  if (rule.applyTo) {
3072
3473
  data.applyTo = rule.applyTo;
3073
3474
  }
3074
3475
  try {
3075
- await writeFile8(filePath, serializeFrontmatter(data, rule.body), { encoding: "utf8", flag: "wx" });
3476
+ await writeFile9(filePath, serializeFrontmatter(data, rule.body), { encoding: "utf8", flag: "wx" });
3076
3477
  written.push(filePath);
3077
3478
  } catch (error) {
3078
3479
  if (error.code !== "EEXIST") {
@@ -3111,7 +3512,7 @@ async function writeStarterTools(repoRoot, profile, force) {
3111
3512
  async function initHarness(repoRoot, options = {}) {
3112
3513
  if (!options.force && await pathExists(projectManifestPath(repoRoot))) {
3113
3514
  throw new InitError(
3114
- `A harness already exists at ${path21.join(".threadroot", "harness.yaml")}. Re-run with --force to overwrite.`
3515
+ `A harness already exists at ${path22.join(".threadroot", "harness.yaml")}. Re-run with --force to overwrite.`
3115
3516
  );
3116
3517
  }
3117
3518
  const profile = await detectProfile(repoRoot, options.profile);
@@ -3133,7 +3534,7 @@ async function initHarness(repoRoot, options = {}) {
3133
3534
  if (options.import !== false) {
3134
3535
  report = await importVendorFiles(repoRoot, { include: options.importFiles });
3135
3536
  if (report.canonicalBody.trim()) {
3136
- await writeFile8(path21.join(repoRoot, AGENTS_FILE2), `${report.canonicalBody.trim()}
3537
+ await writeFile9(path22.join(repoRoot, AGENTS_FILE2), `${report.canonicalBody.trim()}
3137
3538
  `, "utf8");
3138
3539
  }
3139
3540
  rules = await writeImportedRules(repoRoot, report);
@@ -3177,7 +3578,7 @@ async function harnessStatus(repoRoot, options = {}) {
3177
3578
  // src/core/bootstrap.ts
3178
3579
  var DEFAULT_TASK = "start this project";
3179
3580
  async function harnessExists(repoRoot) {
3180
- return pathExists2(path22.join(repoRoot, ".threadroot", "harness.yaml"));
3581
+ return pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
3181
3582
  }
3182
3583
  async function pathExists2(target) {
3183
3584
  try {
@@ -3207,7 +3608,8 @@ async function bootstrapProject(repoRoot, options = {}) {
3207
3608
  agents: options.agents ?? "all",
3208
3609
  mode,
3209
3610
  home: options.home,
3210
- mcp: options.mcp
3611
+ mcp: options.mcp,
3612
+ mcpEntry: options.mcpEntry
3211
3613
  });
3212
3614
  } else {
3213
3615
  notes.push("Skipped global setup because --no-global was set.");
@@ -3220,14 +3622,14 @@ async function bootstrapProject(repoRoot, options = {}) {
3220
3622
  home: options.home
3221
3623
  });
3222
3624
  } else {
3223
- notes.push(`Would initialize local-only harness at ${path22.join(".threadroot", "harness.yaml")}.`);
3625
+ notes.push(`Would initialize local-only harness at ${path23.join(".threadroot", "harness.yaml")}.`);
3224
3626
  }
3225
3627
  } else if (existed) {
3226
3628
  notes.push("Existing harness detected; bootstrap will not reinitialize it.");
3227
3629
  } else {
3228
3630
  notes.push("Skipped project initialization because --no-init was set.");
3229
3631
  }
3230
- const hasHarnessAfterInit = existed || Boolean(init) || await pathExists2(path22.join(repoRoot, ".threadroot", "harness.yaml"));
3632
+ const hasHarnessAfterInit = existed || Boolean(init) || await pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
3231
3633
  if (options.expose) {
3232
3634
  exposed = await exposeProject(repoRoot, {
3233
3635
  agents: options.expose,
@@ -3237,6 +3639,10 @@ async function bootstrapProject(repoRoot, options = {}) {
3237
3639
  let status;
3238
3640
  let doctorReport;
3239
3641
  let context;
3642
+ let mcpCheck;
3643
+ if (options.mcp && write2) {
3644
+ mcpCheck = await checkCodexMcp({ repoRoot, home: options.home });
3645
+ }
3240
3646
  if (hasHarnessAfterInit) {
3241
3647
  status = await harnessStatus(repoRoot, { home: options.home });
3242
3648
  doctorReport = await doctor(repoRoot, { home: options.home });
@@ -3259,6 +3665,7 @@ async function bootstrapProject(repoRoot, options = {}) {
3259
3665
  status,
3260
3666
  doctor: doctorReport,
3261
3667
  context,
3668
+ mcpCheck,
3262
3669
  notes
3263
3670
  };
3264
3671
  }
@@ -3341,6 +3748,18 @@ function printCommandMap() {
3341
3748
  console.log("- `threadroot tools list|check` and `threadroot run <tool>` - use explicit local tools");
3342
3749
  console.log('- `threadroot remember "<note>"` - save durable handoff/project memory');
3343
3750
  }
3751
+ function printMcpCheck(report) {
3752
+ if (!report) {
3753
+ return;
3754
+ }
3755
+ console.log(`mcp check: ${report.status}`);
3756
+ if (report.entry) {
3757
+ console.log(`mcp server: ${report.entry.command} ${report.entry.args.join(" ")}`.trim());
3758
+ }
3759
+ for (const message of report.messages) {
3760
+ console.log(`- ${message}`);
3761
+ }
3762
+ }
3344
3763
  function printBootstrapReport(report) {
3345
3764
  console.log(`Threadroot bootstrap: ${report.mode === "write" ? "complete" : "plan"}`);
3346
3765
  if (report.setup) {
@@ -3363,6 +3782,7 @@ function printBootstrapReport(report) {
3363
3782
  }
3364
3783
  }
3365
3784
  printStatus(report.status);
3785
+ printMcpCheck(report.mcpCheck);
3366
3786
  printDoctor(report.doctor);
3367
3787
  printContext(report.context);
3368
3788
  printCommandMap();
@@ -3396,7 +3816,8 @@ async function runBootstrap(repoRoot, options) {
3396
3816
  noGlobal: options.global === false,
3397
3817
  noInit: options.init === false,
3398
3818
  import: options.import,
3399
- profile: options.profile ? profileIdSchema.parse(options.profile) : void 0
3819
+ profile: options.profile ? profileIdSchema.parse(options.profile) : void 0,
3820
+ mcpEntry: options.mcp ? mcpEntryForCurrentProcess() : void 0
3400
3821
  });
3401
3822
  printBootstrapReport(report);
3402
3823
  if (report.mode === "write" && report.doctor && !report.doctor.ok) {
@@ -3468,7 +3889,7 @@ async function runContext(repoRoot, task) {
3468
3889
 
3469
3890
  // src/commands/diff.ts
3470
3891
  import fs3 from "fs/promises";
3471
- import path23 from "path";
3892
+ import path24 from "path";
3472
3893
  async function readIfExists3(filePath) {
3473
3894
  try {
3474
3895
  return await fs3.readFile(filePath, "utf8");
@@ -3527,7 +3948,7 @@ async function runDiff(repoRoot) {
3527
3948
  const files = await compile(repoRoot, harness);
3528
3949
  let changed = 0;
3529
3950
  for (const file of files) {
3530
- const existing = await readIfExists3(path23.join(repoRoot, file.path));
3951
+ const existing = await readIfExists3(path24.join(repoRoot, file.path));
3531
3952
  if (existing === void 0) {
3532
3953
  changed += 1;
3533
3954
  console.log(`+ ${file.path} (new)`);
@@ -3642,13 +4063,13 @@ async function runInit(repoRoot, options) {
3642
4063
  }
3643
4064
 
3644
4065
  // src/commands/install.ts
3645
- import path26 from "path";
4066
+ import path27 from "path";
3646
4067
 
3647
4068
  // src/core/install/fetch.ts
3648
4069
  import { execFile } from "child_process";
3649
- import { mkdtemp, rm as rm3 } from "fs/promises";
4070
+ import { mkdtemp as mkdtemp2, rm as rm4 } from "fs/promises";
3650
4071
  import os2 from "os";
3651
- import path24 from "path";
4072
+ import path25 from "path";
3652
4073
  import { promisify } from "util";
3653
4074
  var run = promisify(execFile);
3654
4075
  function cloneUrl(ref) {
@@ -3669,8 +4090,8 @@ async function git(cwd, args) {
3669
4090
  }
3670
4091
  async function fetchGitSource(ref) {
3671
4092
  const url = cloneUrl(ref);
3672
- const dir = await mkdtemp(path24.join(os2.tmpdir(), "threadroot-fetch-"));
3673
- const cleanup = () => rm3(dir, { recursive: true, force: true });
4093
+ const dir = await mkdtemp2(path25.join(os2.tmpdir(), "threadroot-fetch-"));
4094
+ const cleanup = () => rm4(dir, { recursive: true, force: true });
3674
4095
  try {
3675
4096
  if (ref.ref) {
3676
4097
  try {
@@ -3691,9 +4112,9 @@ async function fetchGitSource(ref) {
3691
4112
  }
3692
4113
 
3693
4114
  // src/core/install/install.ts
3694
- import { cp as cp2, mkdir as mkdir10, readFile as readFile10, readdir as readdir5, stat as stat10, writeFile as writeFile9 } from "fs/promises";
4115
+ import { cp as cp2, mkdir as mkdir10, readFile as readFile11, readdir as readdir5, stat as stat10, writeFile as writeFile10 } from "fs/promises";
3695
4116
  import { createHash as createHash2 } from "crypto";
3696
- import path25 from "path";
4117
+ import path26 from "path";
3697
4118
  var NAME_RE5 = /^[a-z0-9][a-z0-9-]*$/;
3698
4119
  var KIND_DIR = {
3699
4120
  skill: "skills",
@@ -3705,8 +4126,8 @@ function objectExt(kind) {
3705
4126
  return kind === "tool" || kind === "connection" ? ".yaml" : ".md";
3706
4127
  }
3707
4128
  function safeRepoPath(objectPath) {
3708
- const normalized = path25.normalize(objectPath);
3709
- if (path25.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path25.sep}`)) {
4129
+ const normalized = path26.normalize(objectPath);
4130
+ if (path26.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path26.sep}`)) {
3710
4131
  throw new Error(`Unsafe object path: ${objectPath}`);
3711
4132
  }
3712
4133
  return normalized;
@@ -3720,7 +4141,7 @@ function inferKind(objectPath, override) {
3720
4141
  if (segments.includes("tools")) return "tool";
3721
4142
  if (segments.includes("connections")) return "connection";
3722
4143
  if (segments.includes("rules")) return "rule";
3723
- const ext = path25.extname(objectPath).toLowerCase();
4144
+ const ext = path26.extname(objectPath).toLowerCase();
3724
4145
  if (ext === ".yaml" || ext === ".yml") return "tool";
3725
4146
  if (ext === ".md") return "skill";
3726
4147
  throw new Error(
@@ -3728,7 +4149,7 @@ function inferKind(objectPath, override) {
3728
4149
  );
3729
4150
  }
3730
4151
  function deriveName(objectPath) {
3731
- const base = path25.basename(objectPath, path25.extname(objectPath));
4152
+ const base = path26.basename(objectPath, path26.extname(objectPath));
3732
4153
  if (!NAME_RE5.test(base)) {
3733
4154
  throw new Error(`Invalid object name \`${base}\` (use lowercase letters, digits, and dashes).`);
3734
4155
  }
@@ -3740,8 +4161,8 @@ async function hashDirectory(root) {
3740
4161
  async function walk(dir) {
3741
4162
  const entries = (await readdir5(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
3742
4163
  for (const entry of entries) {
3743
- const full = path25.join(dir, entry.name);
3744
- const rel = path25.relative(root, full).split(path25.sep).join("/");
4164
+ const full = path26.join(dir, entry.name);
4165
+ const rel = path26.relative(root, full).split(path26.sep).join("/");
3745
4166
  if (entry.isSymbolicLink()) {
3746
4167
  throw new Error(`Refusing to install skill directory with symlink: ${rel}`);
3747
4168
  }
@@ -3752,7 +4173,7 @@ async function hashDirectory(root) {
3752
4173
  if (entry.isFile()) {
3753
4174
  hash.update(`file:${rel}
3754
4175
  `);
3755
- hash.update(await readFile10(full));
4176
+ hash.update(await readFile11(full));
3756
4177
  hash.update("\n");
3757
4178
  }
3758
4179
  }
@@ -3761,8 +4182,8 @@ async function hashDirectory(root) {
3761
4182
  return hash.digest("hex");
3762
4183
  }
3763
4184
  async function validateSkillDirectory2(sourcePath, expectedName) {
3764
- const skillPath = path25.join(sourcePath, "SKILL.md");
3765
- const parsed = parseFrontmatter(await readFile10(skillPath, "utf8"));
4185
+ const skillPath = path26.join(sourcePath, "SKILL.md");
4186
+ const parsed = parseFrontmatter(await readFile11(skillPath, "utf8"));
3766
4187
  const result = skillFrontmatterSchema.safeParse(parsed.data);
3767
4188
  if (!result.success) {
3768
4189
  const detail = result.error.issues.map((issue) => issue.message).join("; ");
@@ -3791,7 +4212,7 @@ async function installObject(repoRoot, rawSource, options = {}) {
3791
4212
  objectPath = safeRepoPath(within);
3792
4213
  refLabel = ref.ref;
3793
4214
  const fetched = await fetchGitSource(ref);
3794
- sourcePath = path25.join(fetched.dir, objectPath);
4215
+ sourcePath = path26.join(fetched.dir, objectPath);
3795
4216
  resolved = fetched.sha;
3796
4217
  cleanup = fetched.cleanup;
3797
4218
  } else {
@@ -3812,14 +4233,14 @@ async function installObject(repoRoot, rawSource, options = {}) {
3812
4233
  throw new Error("Only skill objects may be installed from a directory.");
3813
4234
  }
3814
4235
  integrity = `sha256:${await validateSkillDirectory2(sourcePath, name)}`;
3815
- destPath = path25.join(destDir, name);
4236
+ destPath = path26.join(destDir, name);
3816
4237
  await mkdir10(destDir, { recursive: true });
3817
4238
  await cp2(sourcePath, destPath, { recursive: true, force: true });
3818
4239
  } else {
3819
- const content = await readFile10(sourcePath, "utf8");
3820
- destPath = path25.join(destDir, `${name}${objectExt(kind)}`);
4240
+ const content = await readFile11(sourcePath, "utf8");
4241
+ destPath = path26.join(destDir, `${name}${objectExt(kind)}`);
3821
4242
  await mkdir10(destDir, { recursive: true });
3822
- await writeFile9(destPath, content, "utf8");
4243
+ await writeFile10(destPath, content, "utf8");
3823
4244
  integrity = `sha256:${hashContent(content)}`;
3824
4245
  }
3825
4246
  const entry = {
@@ -3871,7 +4292,7 @@ async function runInstall(repoRoot, source, options) {
3871
4292
  if (installed.kind === "skill" && installed.entry.sourceKind !== "local") {
3872
4293
  console.log(" note: inspect external skills before trusting bundled scripts, assets, or allowed tools.");
3873
4294
  if (scope === "project") {
3874
- console.log(` inspect: threadroot skills inspect ${path26.relative(repoRoot, installed.path)}`);
4295
+ console.log(` inspect: threadroot skills inspect ${path27.relative(repoRoot, installed.path)}`);
3875
4296
  }
3876
4297
  }
3877
4298
  } catch (error) {
@@ -4162,8 +4583,9 @@ async function handleMessage(repoRoot, request) {
4162
4583
  if (request.method === "initialize") {
4163
4584
  return resultResponse(request, {
4164
4585
  protocolVersion: "2024-11-05",
4165
- serverInfo: { name: "threadroot", version: "0.1.2" },
4166
- capabilities: { tools: {} }
4586
+ serverInfo: { name: "threadroot", version: THREADROOT_VERSION },
4587
+ capabilities: { tools: {} },
4588
+ instructions: "Threadroot exposes the repository's AI agent harness. Call `context` before broad coding work, `doctor` for health and trust checks, inspect skills/tools before risky actions, and use `memory_append` for durable handoffs."
4167
4589
  });
4168
4590
  }
4169
4591
  if (request.method === "notifications/initialized") {
@@ -4385,11 +4807,11 @@ function agentNotes(agent) {
4385
4807
  }
4386
4808
 
4387
4809
  // src/core/mcp-config.ts
4388
- import { mkdir as mkdir11, readFile as readFile11, writeFile as writeFile10 } from "fs/promises";
4389
- import path27 from "path";
4810
+ import { mkdir as mkdir11, readFile as readFile12, writeFile as writeFile11 } from "fs/promises";
4811
+ import path28 from "path";
4390
4812
  var TARGETS = [
4391
- { agent: "copilot", file: path27.join(".vscode", "mcp.json"), key: "servers" },
4392
- { agent: "cursor", file: path27.join(".cursor", "mcp.json"), key: "mcpServers" },
4813
+ { agent: "copilot", file: path28.join(".vscode", "mcp.json"), key: "servers" },
4814
+ { agent: "cursor", file: path28.join(".cursor", "mcp.json"), key: "mcpServers" },
4393
4815
  { agent: "claude", file: ".mcp.json", key: "mcpServers" }
4394
4816
  ];
4395
4817
  function mcpServerEntry(command, scriptPath) {
@@ -4398,7 +4820,7 @@ function mcpServerEntry(command, scriptPath) {
4398
4820
  async function mergeConfig(filePath, key, entry) {
4399
4821
  let config = {};
4400
4822
  try {
4401
- const raw = await readFile11(filePath, "utf8");
4823
+ const raw = await readFile12(filePath, "utf8");
4402
4824
  const parsed = JSON.parse(raw);
4403
4825
  if (parsed && typeof parsed === "object") {
4404
4826
  config = parsed;
@@ -4411,8 +4833,8 @@ async function mergeConfig(filePath, key, entry) {
4411
4833
  const servers = config[key] && typeof config[key] === "object" ? config[key] : {};
4412
4834
  servers.threadroot = { ...entry };
4413
4835
  config[key] = servers;
4414
- await mkdir11(path27.dirname(filePath), { recursive: true });
4415
- await writeFile10(filePath, `${JSON.stringify(config, null, 2)}
4836
+ await mkdir11(path28.dirname(filePath), { recursive: true });
4837
+ await writeFile11(filePath, `${JSON.stringify(config, null, 2)}
4416
4838
  `, "utf8");
4417
4839
  }
4418
4840
  async function writeProjectMcpConfigs(input2) {
@@ -4420,7 +4842,7 @@ async function writeProjectMcpConfigs(input2) {
4420
4842
  const targets = agents ? TARGETS.filter((target) => agents.includes(target.agent)) : TARGETS;
4421
4843
  const written = [];
4422
4844
  for (const target of targets) {
4423
- const filePath = path27.join(input2.repoRoot, target.file);
4845
+ const filePath = path28.join(input2.repoRoot, target.file);
4424
4846
  await mergeConfig(filePath, target.key, input2.entry);
4425
4847
  written.push(target.file);
4426
4848
  }
@@ -4454,6 +4876,24 @@ async function runMcpSetup(repoRoot, options) {
4454
4876
  }
4455
4877
  console.log(mcpSetupGuide({ repoRoot, agent: options.agent }));
4456
4878
  }
4879
+ async function runMcpCheck(repoRoot, options) {
4880
+ const timeoutMs = options.timeout ? Number.parseInt(options.timeout, 10) : void 0;
4881
+ const report = await checkCodexMcp({ repoRoot, timeoutMs });
4882
+ console.log(`Threadroot MCP check: ${report.status}`);
4883
+ console.log(`config: ${report.configPath}`);
4884
+ if (report.entry) {
4885
+ console.log(`server: ${report.entry.command} ${report.entry.args.join(" ")}`.trim());
4886
+ }
4887
+ for (const message of report.messages) {
4888
+ console.log(`- ${message}`);
4889
+ }
4890
+ if (report.tools.length > 0) {
4891
+ console.log(`tools: ${report.tools.join(", ")}`);
4892
+ }
4893
+ if (report.status === "error") {
4894
+ process.exitCode = 1;
4895
+ }
4896
+ }
4457
4897
 
4458
4898
  // src/commands/memory.ts
4459
4899
  async function runMemoryRead(repoRoot, type) {
@@ -4540,7 +4980,8 @@ async function runSetup(_repoRoot, options) {
4540
4980
  agents: options.agent,
4541
4981
  mode,
4542
4982
  force: options.force,
4543
- mcp: options.mcp
4983
+ mcp: options.mcp,
4984
+ mcpEntry: options.mcp ? mcpEntryForCurrentProcess() : void 0
4544
4985
  });
4545
4986
  const title = mode === "dry-run" ? "Global setup plan" : mode === "check" ? "Global setup check" : mode === "undo" ? "Global setup undo" : "Global setup complete";
4546
4987
  console.log(`${title}:`);
@@ -4548,6 +4989,19 @@ async function runSetup(_repoRoot, options) {
4548
4989
  const suffix = entry.message ? ` - ${entry.message}` : "";
4549
4990
  console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
4550
4991
  }
4992
+ if (options.mcp && options.global && mode === "write") {
4993
+ const check = await checkCodexMcp({ repoRoot: _repoRoot });
4994
+ console.log(`MCP verification: ${check.status}`);
4995
+ if (check.entry) {
4996
+ console.log(`MCP server: ${check.entry.command} ${check.entry.args.join(" ")}`.trim());
4997
+ }
4998
+ for (const message of check.messages) {
4999
+ console.log(`- ${message}`);
5000
+ }
5001
+ if (check.status === "error") {
5002
+ process.exitCode = 1;
5003
+ }
5004
+ }
4551
5005
  if (mode === "write") {
4552
5006
  console.log("Reload or restart open agent sessions so new global skills/config are discovered.");
4553
5007
  }
@@ -4586,8 +5040,8 @@ async function runStatus(repoRoot) {
4586
5040
  }
4587
5041
 
4588
5042
  // src/core/packs/index.ts
4589
- import { cp as cp3, mkdir as mkdir12, readFile as readFile12, readdir as readdir6, stat as stat11 } from "fs/promises";
4590
- import path28 from "path";
5043
+ import { cp as cp3, mkdir as mkdir12, readFile as readFile13, readdir as readdir6, stat as stat11 } from "fs/promises";
5044
+ import path29 from "path";
4591
5045
  import { fileURLToPath as fileURLToPath2 } from "url";
4592
5046
  import { parse as parseYaml3 } from "yaml";
4593
5047
  import { z as z5 } from "zod";
@@ -4600,14 +5054,14 @@ var packManifestSchema = z5.object({
4600
5054
  rules: z5.array(z5.string()).default([]),
4601
5055
  connections: z5.array(z5.string()).default([])
4602
5056
  });
4603
- var DIST_DIR2 = path28.dirname(fileURLToPath2(import.meta.url));
4604
- var PACKAGE_ROOT_FROM_BUNDLE2 = path28.resolve(DIST_DIR2, "..");
4605
- var PACKAGE_ROOT_FROM_DIST2 = path28.resolve(DIST_DIR2, "../../..");
4606
- var PACKAGE_ROOT_FROM_SRC2 = path28.resolve(DIST_DIR2, "../../../..");
5057
+ var DIST_DIR2 = path29.dirname(fileURLToPath2(import.meta.url));
5058
+ var PACKAGE_ROOT_FROM_BUNDLE2 = path29.resolve(DIST_DIR2, "..");
5059
+ var PACKAGE_ROOT_FROM_DIST2 = path29.resolve(DIST_DIR2, "../../..");
5060
+ var PACKAGE_ROOT_FROM_SRC2 = path29.resolve(DIST_DIR2, "../../../..");
4607
5061
  var PACK_CANDIDATES = [
4608
- path28.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
4609
- path28.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
4610
- path28.join(PACKAGE_ROOT_FROM_SRC2, "packs")
5062
+ path29.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
5063
+ path29.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
5064
+ path29.join(PACKAGE_ROOT_FROM_SRC2, "packs")
4611
5065
  ];
4612
5066
  async function exists5(target) {
4613
5067
  try {
@@ -4639,22 +5093,22 @@ async function isPackRoot(candidate) {
4639
5093
  return false;
4640
5094
  }
4641
5095
  for (const entry of entries) {
4642
- if (entry.isDirectory() && await exists5(path28.join(candidate, entry.name, "pack.yaml"))) {
5096
+ if (entry.isDirectory() && await exists5(path29.join(candidate, entry.name, "pack.yaml"))) {
4643
5097
  return true;
4644
5098
  }
4645
5099
  }
4646
5100
  return false;
4647
5101
  }
4648
5102
  function safeRelative(ref) {
4649
- const normalized = path28.normalize(ref);
4650
- if (path28.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path28.sep}`)) {
5103
+ const normalized = path29.normalize(ref);
5104
+ if (path29.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path29.sep}`)) {
4651
5105
  throw new Error(`Unsafe pack reference: ${ref}`);
4652
5106
  }
4653
5107
  return normalized;
4654
5108
  }
4655
5109
  async function readPackManifest(packDir) {
4656
- const file = path28.join(packDir, "pack.yaml");
4657
- const parsed = packManifestSchema.safeParse(parseYaml3(await readFile12(file, "utf8")));
5110
+ const file = path29.join(packDir, "pack.yaml");
5111
+ const parsed = packManifestSchema.safeParse(parseYaml3(await readFile13(file, "utf8")));
4658
5112
  if (!parsed.success) {
4659
5113
  const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
4660
5114
  throw new Error(`Invalid pack manifest ${file}: ${detail}`);
@@ -4662,7 +5116,7 @@ async function readPackManifest(packDir) {
4662
5116
  return parsed.data;
4663
5117
  }
4664
5118
  async function packDirFor(repoRoot, nameOrPath) {
4665
- if (path28.isAbsolute(nameOrPath)) {
5119
+ if (path29.isAbsolute(nameOrPath)) {
4666
5120
  return nameOrPath;
4667
5121
  }
4668
5122
  if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
@@ -4670,14 +5124,14 @@ async function packDirFor(repoRoot, nameOrPath) {
4670
5124
  }
4671
5125
  const bundled = await bundledPacksDir();
4672
5126
  if (bundled) {
4673
- return path28.join(bundled, nameOrPath);
5127
+ return path29.join(bundled, nameOrPath);
4674
5128
  }
4675
- return toRepoPath(repoRoot, path28.join("packs", nameOrPath));
5129
+ return toRepoPath(repoRoot, path29.join("packs", nameOrPath));
4676
5130
  }
4677
5131
  async function directFiles(dir, ext) {
4678
5132
  try {
4679
5133
  const entries = await readdir6(dir, { withFileTypes: true });
4680
- return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path28.join(dir, entry.name)).sort();
5134
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path29.join(dir, entry.name)).sort();
4681
5135
  } catch (error) {
4682
5136
  if (error.code === "ENOENT") {
4683
5137
  return [];
@@ -4690,11 +5144,11 @@ async function skillEntries(dir) {
4690
5144
  const entries = await readdir6(dir, { withFileTypes: true });
4691
5145
  const result = [];
4692
5146
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
4693
- const full = path28.join(dir, entry.name);
5147
+ const full = path29.join(dir, entry.name);
4694
5148
  if (entry.isFile() && entry.name.endsWith(".md")) {
4695
5149
  result.push(full);
4696
5150
  }
4697
- if (entry.isDirectory() && await exists5(path28.join(full, "SKILL.md"))) {
5151
+ if (entry.isDirectory() && await exists5(path29.join(full, "SKILL.md"))) {
4698
5152
  result.push(full);
4699
5153
  }
4700
5154
  }
@@ -4709,34 +5163,34 @@ async function skillEntries(dir) {
4709
5163
  async function collectObjects(packDir, manifest) {
4710
5164
  async function resolveRef(ref) {
4711
5165
  const safe = safeRelative(ref);
4712
- const local = path28.resolve(packDir, safe);
5166
+ const local = path29.resolve(packDir, safe);
4713
5167
  if (await exists5(local)) {
4714
5168
  return local;
4715
5169
  }
4716
- return path28.resolve(packDir, "..", "..", safe);
5170
+ return path29.resolve(packDir, "..", "..", safe);
4717
5171
  }
4718
5172
  return {
4719
5173
  skills: [
4720
5174
  ...await Promise.all(manifest.skills.map(resolveRef)),
4721
- ...await skillEntries(path28.join(packDir, "skills"))
5175
+ ...await skillEntries(path29.join(packDir, "skills"))
4722
5176
  ],
4723
5177
  tools: [
4724
5178
  ...await Promise.all(manifest.tools.map(resolveRef)),
4725
- ...await directFiles(path28.join(packDir, "tools"), ".yaml")
5179
+ ...await directFiles(path29.join(packDir, "tools"), ".yaml")
4726
5180
  ],
4727
5181
  rules: [
4728
5182
  ...await Promise.all(manifest.rules.map(resolveRef)),
4729
- ...await directFiles(path28.join(packDir, "rules"), ".md")
5183
+ ...await directFiles(path29.join(packDir, "rules"), ".md")
4730
5184
  ],
4731
5185
  connections: [
4732
5186
  ...await Promise.all(manifest.connections.map(resolveRef)),
4733
- ...await directFiles(path28.join(packDir, "connections"), ".yaml")
5187
+ ...await directFiles(path29.join(packDir, "connections"), ".yaml")
4734
5188
  ]
4735
5189
  };
4736
5190
  }
4737
5191
  function baseName(source) {
4738
- const parsed = path28.basename(source) === "SKILL.md" ? path28.dirname(source) : source;
4739
- return path28.basename(parsed, path28.extname(parsed));
5192
+ const parsed = path29.basename(source) === "SKILL.md" ? path29.dirname(source) : source;
5193
+ return path29.basename(parsed, path29.extname(parsed));
4740
5194
  }
4741
5195
  async function listPacks(repoRoot) {
4742
5196
  const dirs = [toRepoPath(repoRoot, "packs"), await bundledPacksDir()].filter((dir) => Boolean(dir));
@@ -4753,8 +5207,8 @@ async function listPacks(repoRoot) {
4753
5207
  if (!entry.isDirectory() || seen.has(entry.name)) {
4754
5208
  continue;
4755
5209
  }
4756
- const packDir = path28.join(root, entry.name);
4757
- if (!await exists5(path28.join(packDir, "pack.yaml"))) {
5210
+ const packDir = path29.join(root, entry.name);
5211
+ if (!await exists5(path29.join(packDir, "pack.yaml"))) {
4758
5212
  continue;
4759
5213
  }
4760
5214
  seen.add(entry.name);
@@ -4778,14 +5232,14 @@ async function inspectPack(repoRoot, nameOrPath) {
4778
5232
  };
4779
5233
  }
4780
5234
  async function validateProse(file, kind) {
4781
- const target = path28.basename(file) === "SKILL.md" ? file : file;
4782
- const content = await readFile12(target, "utf8");
5235
+ const target = path29.basename(file) === "SKILL.md" ? file : file;
5236
+ const content = await readFile13(target, "utf8");
4783
5237
  const parsed = parseFrontmatter(content);
4784
5238
  const schema = kind === "skill" ? skillFrontmatterSchema : ruleFrontmatterSchema;
4785
5239
  schema.parse(parsed.data);
4786
5240
  }
4787
5241
  async function validateYaml(file, kind) {
4788
- const content = await readFile12(file, "utf8");
5242
+ const content = await readFile13(file, "utf8");
4789
5243
  const schema = kind === "tool" ? toolManifestSchema : connectionManifestSchema;
4790
5244
  schema.parse(parseYaml3(content));
4791
5245
  }
@@ -4796,7 +5250,7 @@ async function validatePack(repoRoot, nameOrPath) {
4796
5250
  const manifest = await readPackManifest(packDir);
4797
5251
  const objects = await collectObjects(packDir, manifest);
4798
5252
  for (const skill of objects.skills) {
4799
- await validateProse(path28.basename(skill) === "SKILL.md" ? skill : path28.join(skill, "SKILL.md"), "skill");
5253
+ await validateProse(path29.basename(skill) === "SKILL.md" ? skill : path29.join(skill, "SKILL.md"), "skill");
4800
5254
  }
4801
5255
  for (const rule of objects.rules) await validateProse(rule, "rule");
4802
5256
  for (const tool of objects.tools) await validateYaml(tool, "tool");
@@ -4812,7 +5266,7 @@ async function validatePack(repoRoot, nameOrPath) {
4812
5266
  async function copyObject(source, destDir) {
4813
5267
  const info = await stat11(source);
4814
5268
  const name = baseName(source);
4815
- const dest = info.isDirectory() ? path28.join(destDir, name) : path28.join(destDir, path28.basename(source));
5269
+ const dest = info.isDirectory() ? path29.join(destDir, name) : path29.join(destDir, path29.basename(source));
4816
5270
  await mkdir12(destDir, { recursive: true });
4817
5271
  await cp3(source, dest, { recursive: true, force: true });
4818
5272
  return dest;
@@ -5101,7 +5555,7 @@ async function runConnectionsCheck(repoRoot) {
5101
5555
  // src/cli.ts
5102
5556
  function createProgram(repoRoot = process.cwd()) {
5103
5557
  const program = new Command();
5104
- program.name("threadroot").description("Git for your AI agent harness: one command to bootstrap, one .threadroot source.").version("0.1.2");
5558
+ program.name("threadroot").description("Git for your AI agent harness: one command to bootstrap, one .threadroot source.").version(THREADROOT_VERSION);
5105
5559
  program.command("bootstrap").description("Plan or apply first-run Threadroot setup for this machine and repository.").option("-y, --yes", "Apply the setup plan. Without --yes, bootstrap prints a dry-run plan.").option("--dry-run", "Print the setup plan without writing files.").option("--agent <list>", "Provider(s): codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--task <task>", "Task used for the initial context slice.").option("--mcp", "Also add Threadroot MCP to Codex global config when Codex is selected.").option("--expose <list>", "Also write project provider skill shims: codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--no-global", "Skip one-time machine-level agent setup.").option("--no-init", "Skip project harness initialization.").option("--no-import", "Skip importing existing vendor files during init.").option("--profile <profile>", "Override the detected project profile during init.").action((options) => runBootstrap(repoRoot, options));
5106
5560
  program.command("start").argument("[task]", "Task to prepare context for.").option("--task <task>", "Task to prepare context for.").description("Start a focused Threadroot agent session: doctor, status, context, and command map.").action((task, options) => runStart(repoRoot, task, options));
5107
5561
  program.command("init").description("Scaffold a local-only Threadroot harness and import existing vendor files once.").option("--force", "Re-initialize over an existing harness.").option("--no-import", "Skip importing existing vendor files (blank-slate init).").option("--profile <profile>", "Override the detected project profile.").option("--adapters <list>", "Comma-separated adapters: agents,claude,copilot,cursor.").option("--expose <list>", "Comma-separated provider skill shims to write: codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").action((options) => runInit(repoRoot, options));
@@ -5139,6 +5593,7 @@ function createProgram(repoRoot = process.cwd()) {
5139
5593
  skills.command("validate").option("--path <path>", "Validate a repo-relative skill file, skill directory, or skill collection.").description("Validate skill frontmatter, naming, trigger descriptions, and progressive-disclosure hygiene.").action((options) => runSkillsValidate(repoRoot, options));
5140
5594
  const mcp = program.command("mcp").description("Run or configure the local Threadroot MCP server.");
5141
5595
  mcp.action(() => runMcp(repoRoot));
5596
+ mcp.command("check").option("--timeout <ms>", "Handshake timeout in milliseconds.").description("Verify Codex MCP config and the Threadroot stdio server handshake.").action((options) => runMcpCheck(repoRoot, options));
5142
5597
  mcp.command("setup").option("--agent <agent>", "all, generic, codex, copilot, cursor, or claude.").option("--write", "Write project-local MCP config files for the agents.").description("Print MCP config snippets and a pasteable agent bootstrap prompt.").action((options) => runMcpSetup(repoRoot, options));
5143
5598
  return program;
5144
5599
  }