threadroot 0.1.2 → 0.1.3

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,376 @@ 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
+ var REQUIRED_MCP_TOOLS = [
2403
+ "context",
2404
+ "skills_list",
2405
+ "skills_get",
2406
+ "tools_list",
2407
+ "tools_check",
2408
+ "tools_run",
2409
+ "tools_create",
2410
+ "tools_detect",
2411
+ "connections_list",
2412
+ "connections_check",
2413
+ "memory_read",
2414
+ "memory_append",
2415
+ "status",
2416
+ "doctor"
2417
+ ];
2418
+ function codexConfigPath2(home = homedir2()) {
2419
+ return path14.join(home, ".codex", "config.toml");
2420
+ }
2421
+ function mcpEntryForCurrentProcess() {
2422
+ const scriptPath = currentScriptPath();
2423
+ if (scriptPath && path14.basename(scriptPath) === "index.js" && scriptPath.includes(`${path14.sep}dist${path14.sep}`)) {
2424
+ return { command: process.execPath, args: [scriptPath, "mcp"] };
2425
+ }
2426
+ return { command: "threadroot", args: ["mcp"] };
2427
+ }
2428
+ function currentScriptPath() {
2429
+ const scriptPath = process.argv[1];
2430
+ if (!scriptPath) {
2431
+ return void 0;
2432
+ }
2433
+ try {
2434
+ return realpathSync(scriptPath);
2435
+ } catch {
2436
+ return scriptPath;
2437
+ }
2438
+ }
2439
+ async function readCodexThreadrootMcpEntry(home = homedir2()) {
2440
+ let raw;
2441
+ try {
2442
+ raw = await readFile8(codexConfigPath2(home), "utf8");
2443
+ } catch (error) {
2444
+ if (error.code === "ENOENT") {
2445
+ return void 0;
2446
+ }
2447
+ throw error;
2448
+ }
2449
+ const table = raw.match(/(?:^|\n)\[mcp_servers\.threadroot\]\s*\n(?<body>[\s\S]*?)(?=\n\[|$)/);
2450
+ if (!table?.groups?.body) {
2451
+ return void 0;
2452
+ }
2453
+ const command = matchTomlString(table.groups.body, "command");
2454
+ const args = matchTomlArray(table.groups.body, "args");
2455
+ if (!command || !args) {
2456
+ return void 0;
2457
+ }
2458
+ return { command, args };
2459
+ }
2460
+ function matchTomlString(body, key) {
2461
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2462
+ const match = body.match(new RegExp(`^${escaped}\\s*=\\s*"(?<value>(?:[^"\\\\]|\\\\.)*)"\\s*$`, "m"));
2463
+ return match?.groups?.value?.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
2464
+ }
2465
+ function matchTomlArray(body, key) {
2466
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2467
+ const match = body.match(new RegExp(`^${escaped}\\s*=\\s*\\[(?<value>.*)\\]\\s*$`, "m"));
2468
+ if (!match?.groups?.value) {
2469
+ return void 0;
2470
+ }
2471
+ const values = [];
2472
+ const pattern = /"((?:[^"\\]|\\.)*)"/g;
2473
+ let value;
2474
+ while (value = pattern.exec(match.groups.value)) {
2475
+ values.push(value[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\"));
2476
+ }
2477
+ return values;
2478
+ }
2479
+ async function commandExists2(command) {
2480
+ if (path14.isAbsolute(command) || command.includes(path14.sep)) {
2481
+ try {
2482
+ await access(command, constants.X_OK);
2483
+ return true;
2484
+ } catch {
2485
+ return false;
2486
+ }
2487
+ }
2488
+ const paths = (process.env.PATH ?? "").split(path14.delimiter).filter(Boolean);
2489
+ for (const dir of paths) {
2490
+ try {
2491
+ await access(path14.join(dir, command), constants.X_OK);
2492
+ return true;
2493
+ } catch {
2494
+ }
2495
+ }
2496
+ return false;
2497
+ }
2498
+ async function checkCodexMcp(input2) {
2499
+ const configPath = codexConfigPath2(input2.home);
2500
+ const entry = await readCodexThreadrootMcpEntry(input2.home);
2501
+ if (!entry) {
2502
+ return {
2503
+ status: "warning",
2504
+ configPath,
2505
+ tools: [],
2506
+ messages: ["No Codex Threadroot MCP config found."]
2507
+ };
2508
+ }
2509
+ if (!await commandExists2(entry.command)) {
2510
+ return {
2511
+ status: "error",
2512
+ configPath,
2513
+ entry,
2514
+ tools: [],
2515
+ messages: [`MCP command is not executable or not on PATH: ${entry.command}`]
2516
+ };
2517
+ }
2518
+ try {
2519
+ const handshake = await runMcpHandshake(entry, input2.repoRoot, input2.timeoutMs ?? 4e3);
2520
+ const toolNames = handshake.tools;
2521
+ const missing = REQUIRED_MCP_TOOLS.filter((tool) => !toolNames.includes(tool));
2522
+ if (missing.length > 0) {
2523
+ return {
2524
+ status: "error",
2525
+ configPath,
2526
+ entry,
2527
+ serverInfo: handshake.serverInfo,
2528
+ tools: toolNames,
2529
+ messages: [`MCP server is missing required tool(s): ${missing.join(", ")}`]
2530
+ };
2531
+ }
2532
+ return {
2533
+ status: "ok",
2534
+ configPath,
2535
+ entry,
2536
+ serverInfo: handshake.serverInfo,
2537
+ tools: toolNames,
2538
+ messages: ["MCP server initialized and returned the expected Threadroot tools."]
2539
+ };
2540
+ } catch (error) {
2541
+ return {
2542
+ status: "error",
2543
+ configPath,
2544
+ entry,
2545
+ tools: [],
2546
+ messages: [`MCP handshake failed: ${error instanceof Error ? error.message : String(error)}`]
2547
+ };
2548
+ }
2549
+ }
2550
+ function runMcpHandshake(entry, repoRoot, timeoutMs) {
2551
+ if (process.platform !== "win32") {
2552
+ return runOneShotMcpHandshake(entry, repoRoot, timeoutMs);
2553
+ }
2554
+ return new Promise((resolve, reject) => {
2555
+ const child = spawn2(entry.command, entry.args, {
2556
+ cwd: repoRoot,
2557
+ stdio: ["pipe", "pipe", "pipe"],
2558
+ env: process.env
2559
+ });
2560
+ let settled = false;
2561
+ const finish = (result) => {
2562
+ if (settled) return;
2563
+ settled = true;
2564
+ clearTimeout(timer);
2565
+ child.kill();
2566
+ resolve(result);
2567
+ };
2568
+ const fail = (error) => {
2569
+ if (settled) return;
2570
+ settled = true;
2571
+ clearTimeout(timer);
2572
+ child.kill();
2573
+ reject(error);
2574
+ };
2575
+ const timer = setTimeout(() => {
2576
+ fail(new Error(`Timed out after ${timeoutMs}ms`));
2577
+ }, timeoutMs);
2578
+ let stdout = "";
2579
+ let stderr = "";
2580
+ let initialized = false;
2581
+ let serverInfo;
2582
+ child.stdout.setEncoding("utf8");
2583
+ child.stderr.setEncoding("utf8");
2584
+ child.stdout.on("data", (chunk) => {
2585
+ stdout += chunk;
2586
+ const lines = stdout.split("\n");
2587
+ stdout = lines.pop() ?? "";
2588
+ for (const line of lines) {
2589
+ if (!line.trim()) {
2590
+ continue;
2591
+ }
2592
+ let message;
2593
+ try {
2594
+ message = JSON.parse(line);
2595
+ } catch (error) {
2596
+ fail(new Error(`Invalid JSON-RPC response: ${error instanceof Error ? error.message : String(error)}`));
2597
+ return;
2598
+ }
2599
+ if (message.error) {
2600
+ fail(new Error(message.error.message ?? "MCP server returned an error"));
2601
+ return;
2602
+ }
2603
+ if (message.id === 1) {
2604
+ initialized = true;
2605
+ serverInfo = message.result?.serverInfo;
2606
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}
2607
+ `);
2608
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" })}
2609
+ `);
2610
+ }
2611
+ if (message.id === 2) {
2612
+ finish({
2613
+ serverInfo,
2614
+ tools: (message.result?.tools ?? []).map((tool) => tool.name)
2615
+ });
2616
+ }
2617
+ }
2618
+ });
2619
+ child.stderr.on("data", (chunk) => {
2620
+ stderr += chunk;
2621
+ });
2622
+ child.on("error", (error) => {
2623
+ fail(error);
2624
+ });
2625
+ child.on("close", (code) => {
2626
+ if (!settled && !initialized && code !== null) {
2627
+ fail(new Error(`Server exited before initialize completed (exit ${code})${stderr ? `: ${stderr}` : ""}`));
2628
+ }
2629
+ });
2630
+ child.stdin.write(
2631
+ `${JSON.stringify({
2632
+ jsonrpc: "2.0",
2633
+ id: 1,
2634
+ method: "initialize",
2635
+ params: {
2636
+ protocolVersion: "2024-11-05",
2637
+ capabilities: {},
2638
+ clientInfo: { name: "threadroot-check", version: "0.1.3" }
2639
+ }
2640
+ })}
2641
+ `
2642
+ );
2643
+ });
2644
+ }
2645
+ async function runOneShotMcpHandshake(entry, repoRoot, timeoutMs) {
2646
+ const tempDir = await mkdtemp(path14.join(tmpdir(), "threadroot-mcp-check-"));
2647
+ const inputPath = path14.join(tempDir, "input.jsonl");
2648
+ const stdoutPath = path14.join(tempDir, "stdout.jsonl");
2649
+ const stderrPath = path14.join(tempDir, "stderr.txt");
2650
+ try {
2651
+ await writeFile6(
2652
+ inputPath,
2653
+ [
2654
+ JSON.stringify({
2655
+ jsonrpc: "2.0",
2656
+ id: 1,
2657
+ method: "initialize",
2658
+ params: {
2659
+ protocolVersion: "2024-11-05",
2660
+ capabilities: {},
2661
+ clientInfo: { name: "threadroot-check", version: "0.1.3" }
2662
+ }
2663
+ }),
2664
+ JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
2665
+ JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }),
2666
+ ""
2667
+ ].join("\n"),
2668
+ "utf8"
2669
+ );
2670
+ const commandLine = [
2671
+ shellQuote3(entry.command),
2672
+ ...entry.args.map(shellQuote3),
2673
+ "<",
2674
+ shellQuote3(inputPath),
2675
+ ">",
2676
+ shellQuote3(stdoutPath),
2677
+ "2>",
2678
+ shellQuote3(stderrPath)
2679
+ ].join(" ");
2680
+ await runShell(commandLine, repoRoot, timeoutMs);
2681
+ const [stdout, stderr] = await Promise.all([readFile8(stdoutPath, "utf8"), readOptional(stderrPath)]);
2682
+ if (stderr.trim()) {
2683
+ throw new Error(stderr.trim());
2684
+ }
2685
+ return parseHandshakeOutput(stdout);
2686
+ } finally {
2687
+ await rm2(tempDir, { recursive: true, force: true });
2688
+ }
2689
+ }
2690
+ function runShell(commandLine, repoRoot, timeoutMs) {
2691
+ return new Promise((resolve, reject) => {
2692
+ const child = spawn2("sh", ["-c", commandLine], {
2693
+ cwd: repoRoot,
2694
+ stdio: "ignore",
2695
+ env: process.env
2696
+ });
2697
+ let settled = false;
2698
+ const fail = (error) => {
2699
+ if (settled) return;
2700
+ settled = true;
2701
+ clearTimeout(timer);
2702
+ child.kill();
2703
+ reject(error);
2704
+ };
2705
+ const timer = setTimeout(() => fail(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
2706
+ child.on("error", fail);
2707
+ child.on("close", async (code) => {
2708
+ if (settled) return;
2709
+ settled = true;
2710
+ clearTimeout(timer);
2711
+ if (code === 0) {
2712
+ resolve();
2713
+ return;
2714
+ }
2715
+ reject(new Error(`Server exited with status ${code ?? "unknown"}`));
2716
+ });
2717
+ });
2718
+ }
2719
+ function parseHandshakeOutput(stdout) {
2720
+ let initialized = false;
2721
+ let serverInfo;
2722
+ let tools2;
2723
+ for (const line of stdout.split("\n")) {
2724
+ if (!line.trim()) {
2725
+ continue;
2726
+ }
2727
+ let message;
2728
+ try {
2729
+ message = JSON.parse(line);
2730
+ } catch (error) {
2731
+ throw new Error(`Invalid JSON-RPC response: ${error instanceof Error ? error.message : String(error)}`);
2732
+ }
2733
+ if (message.error) {
2734
+ throw new Error(message.error.message ?? "MCP server returned an error");
2735
+ }
2736
+ if (message.id === 1) {
2737
+ initialized = true;
2738
+ serverInfo = message.result?.serverInfo;
2739
+ }
2740
+ if (message.id === 2) {
2741
+ tools2 = (message.result?.tools ?? []).map((tool) => tool.name);
2742
+ }
2743
+ }
2744
+ if (!initialized) {
2745
+ throw new Error("Server did not return an initialize response");
2746
+ }
2747
+ if (!tools2) {
2748
+ throw new Error("Server did not return a tools/list response");
2749
+ }
2750
+ return { serverInfo, tools: tools2 };
2751
+ }
2752
+ async function readOptional(filePath) {
2753
+ try {
2754
+ return await readFile8(filePath, "utf8");
2755
+ } catch (error) {
2756
+ if (error.code === "ENOENT") {
2757
+ return "";
2758
+ }
2759
+ throw error;
2760
+ }
2761
+ }
2762
+ function shellQuote3(value) {
2763
+ return `'${value.replace(/'/g, "'\\''")}'`;
2764
+ }
2765
+
2393
2766
  // src/core/doctor.ts
2394
2767
  async function exists3(filePath) {
2395
2768
  try {
@@ -2405,9 +2778,23 @@ async function exists3(filePath) {
2405
2778
  function finding2(severity, code, message, pathValue) {
2406
2779
  return pathValue ? { severity, code, message, path: pathValue } : { severity, code, message };
2407
2780
  }
2408
- async function mcpConfigHints(repoRoot) {
2781
+ async function mcpConfigHints(repoRoot, home) {
2782
+ const codexMcp = await checkCodexMcp({ repoRoot, home, timeoutMs: 2500 });
2783
+ if (codexMcp.status === "ok") {
2784
+ return [];
2785
+ }
2786
+ if (codexMcp.status === "error") {
2787
+ return [
2788
+ finding2(
2789
+ "warning",
2790
+ "codex_mcp_unhealthy",
2791
+ `Codex Threadroot MCP is configured but failed verification: ${codexMcp.messages.join(" ")}`,
2792
+ codexMcp.configPath
2793
+ )
2794
+ ];
2795
+ }
2409
2796
  const configs = [".vscode/mcp.json", ".cursor/mcp.json", ".mcp.json"];
2410
- const present = await Promise.all(configs.map((config) => exists3(path14.join(repoRoot, config))));
2797
+ const present = await Promise.all(configs.map((config) => exists3(path15.join(repoRoot, config))));
2411
2798
  if (present.some(Boolean)) {
2412
2799
  return [];
2413
2800
  }
@@ -2581,7 +2968,7 @@ async function doctor(repoRoot, options = {}) {
2581
2968
  )
2582
2969
  );
2583
2970
  }
2584
- const scriptsDir = path14.join(path14.dirname(skill.sourcePath), "scripts");
2971
+ const scriptsDir = path15.join(path15.dirname(skill.sourcePath), "scripts");
2585
2972
  if (await exists3(scriptsDir)) {
2586
2973
  findings.push(
2587
2974
  finding2(
@@ -2594,7 +2981,7 @@ async function doctor(repoRoot, options = {}) {
2594
2981
  }
2595
2982
  }
2596
2983
  findings.push(...await globalSetupHints(options.home));
2597
- findings.push(...await mcpConfigHints(repoRoot));
2984
+ findings.push(...await mcpConfigHints(repoRoot, options.home));
2598
2985
  return summarize(findings);
2599
2986
  }
2600
2987
  function summarize(findings) {
@@ -2605,11 +2992,11 @@ function summarize(findings) {
2605
2992
  }
2606
2993
 
2607
2994
  // 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";
2995
+ import { mkdir as mkdir6, readFile as readFile9, rm as rm3, stat as stat6, writeFile as writeFile7 } from "fs/promises";
2996
+ import path16 from "path";
2610
2997
  async function readMaybe2(filePath) {
2611
2998
  try {
2612
- return await readFile8(filePath, "utf8");
2999
+ return await readFile9(filePath, "utf8");
2613
3000
  } catch (error) {
2614
3001
  if (error.code === "ENOENT") {
2615
3002
  return void 0;
@@ -2618,10 +3005,10 @@ async function readMaybe2(filePath) {
2618
3005
  }
2619
3006
  }
2620
3007
  function projectSkillPath(repoRoot, provider) {
2621
- return path15.join(repoRoot, provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
3008
+ return path16.join(repoRoot, provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
2622
3009
  }
2623
3010
  function relSkillPath(provider) {
2624
- return path15.join(provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
3011
+ return path16.join(provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
2625
3012
  }
2626
3013
  async function exposeOne(repoRoot, provider, mode, force) {
2627
3014
  const relativePath = relSkillPath(provider);
@@ -2653,7 +3040,7 @@ async function exposeOne(repoRoot, provider, mode, force) {
2653
3040
  message: "Existing skill is not Threadroot-managed."
2654
3041
  };
2655
3042
  }
2656
- await rm2(path15.dirname(absolutePath), { recursive: true, force: true });
3043
+ await rm3(path16.dirname(absolutePath), { recursive: true, force: true });
2657
3044
  return { agent: provider.id, label: provider.label, path: relativePath, status: "removed" };
2658
3045
  }
2659
3046
  if (existing === desired) {
@@ -2672,8 +3059,8 @@ async function exposeOne(repoRoot, provider, mode, force) {
2672
3059
  if (mode === "dry-run") {
2673
3060
  return { agent: provider.id, label: provider.label, path: relativePath, status };
2674
3061
  }
2675
- await mkdir6(path15.dirname(absolutePath), { recursive: true });
2676
- await writeFile6(absolutePath, desired, "utf8");
3062
+ await mkdir6(path16.dirname(absolutePath), { recursive: true });
3063
+ await writeFile7(absolutePath, desired, "utf8");
2677
3064
  return { agent: provider.id, label: provider.label, path: relativePath, status };
2678
3065
  }
2679
3066
  async function exposeProject(repoRoot, options = {}) {
@@ -2687,18 +3074,18 @@ async function exposeProject(repoRoot, options = {}) {
2687
3074
  }
2688
3075
 
2689
3076
  // src/core/init/index.ts
2690
- import { mkdir as mkdir9, stat as stat8, writeFile as writeFile8 } from "fs/promises";
2691
- import path21 from "path";
3077
+ import { mkdir as mkdir9, stat as stat8, writeFile as writeFile9 } from "fs/promises";
3078
+ import path22 from "path";
2692
3079
 
2693
3080
  // src/core/compile/write.ts
2694
- import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
2695
- import path16 from "path";
3081
+ import { mkdir as mkdir7, writeFile as writeFile8 } from "fs/promises";
3082
+ import path17 from "path";
2696
3083
  async function writeCompiled(repoRoot, files) {
2697
3084
  await Promise.all(
2698
3085
  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");
3086
+ const absolute = path17.join(repoRoot, file.path);
3087
+ await mkdir7(path17.dirname(absolute), { recursive: true });
3088
+ await writeFile8(absolute, file.content, "utf8");
2702
3089
  })
2703
3090
  );
2704
3091
  return files.map((file) => file.path);
@@ -2714,7 +3101,7 @@ async function runCompile(repoRoot, options = {}) {
2714
3101
 
2715
3102
  // src/core/scan/package.ts
2716
3103
  import fs from "fs/promises";
2717
- import path17 from "path";
3104
+ import path18 from "path";
2718
3105
 
2719
3106
  // src/core/scan/rules.ts
2720
3107
  var ignoredDirectories = /* @__PURE__ */ new Set([
@@ -2732,13 +3119,13 @@ var ignoredDirectories = /* @__PURE__ */ new Set([
2732
3119
  // src/core/scan/package.ts
2733
3120
  async function readJson(repoRoot, relativePath) {
2734
3121
  try {
2735
- return JSON.parse(await fs.readFile(path17.join(repoRoot, relativePath), "utf8"));
3122
+ return JSON.parse(await fs.readFile(path18.join(repoRoot, relativePath), "utf8"));
2736
3123
  } catch {
2737
3124
  return void 0;
2738
3125
  }
2739
3126
  }
2740
3127
  function inferProfile(files, packageJson) {
2741
- if (files.some((file) => path17.basename(file) === "dbt_project.yml" || path17.basename(file) === "dbt_project.yaml")) {
3128
+ if (files.some((file) => path18.basename(file) === "dbt_project.yml" || path18.basename(file) === "dbt_project.yaml")) {
2742
3129
  return "dbt";
2743
3130
  }
2744
3131
  const packageMeta = packageJson && typeof packageJson === "object" ? packageJson : void 0;
@@ -2763,9 +3150,9 @@ function inferProfile(files, packageJson) {
2763
3150
 
2764
3151
  // src/core/scan/walk.ts
2765
3152
  import fs2 from "fs/promises";
2766
- import path18 from "path";
3153
+ import path19 from "path";
2767
3154
  function toPosix(relativePath) {
2768
- return relativePath.split(path18.sep).join("/");
3155
+ return relativePath.split(path19.sep).join("/");
2769
3156
  }
2770
3157
  async function walkRepo(repoRoot, directory = repoRoot) {
2771
3158
  let entries;
@@ -2776,8 +3163,8 @@ async function walkRepo(repoRoot, directory = repoRoot) {
2776
3163
  }
2777
3164
  const files = [];
2778
3165
  for (const entry of entries) {
2779
- const absolutePath = path18.join(directory, entry.name);
2780
- const relativePath = toPosix(path18.relative(repoRoot, absolutePath));
3166
+ const absolutePath = path19.join(directory, entry.name);
3167
+ const relativePath = toPosix(path19.relative(repoRoot, absolutePath));
2781
3168
  if (entry.isDirectory()) {
2782
3169
  if (!ignoredDirectories.has(entry.name)) {
2783
3170
  files.push(...await walkRepo(repoRoot, absolutePath));
@@ -2796,16 +3183,16 @@ import { stringify as stringifyYaml4 } from "yaml";
2796
3183
 
2797
3184
  // src/core/init/builtins.ts
2798
3185
  import { cp, mkdir as mkdir8, readdir as readdir3, stat as stat7 } from "fs/promises";
2799
- import path19 from "path";
3186
+ import path20 from "path";
2800
3187
  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, "../../../..");
3188
+ var DIST_DIR = path20.dirname(fileURLToPath(import.meta.url));
3189
+ var PACKAGE_ROOT_FROM_BUNDLE = path20.resolve(DIST_DIR, "..");
3190
+ var PACKAGE_ROOT_FROM_DIST = path20.resolve(DIST_DIR, "../../..");
3191
+ var PACKAGE_ROOT_FROM_SRC = path20.resolve(DIST_DIR, "../../../..");
2805
3192
  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")
3193
+ path20.join(PACKAGE_ROOT_FROM_BUNDLE, "skills"),
3194
+ path20.join(PACKAGE_ROOT_FROM_DIST, "skills"),
3195
+ path20.join(PACKAGE_ROOT_FROM_SRC, "skills")
2809
3196
  ];
2810
3197
  var PROJECT_MEMORY_TEMPLATE = [
2811
3198
  "# Project",
@@ -2842,13 +3229,13 @@ async function writeBuiltinSkills(repoRoot) {
2842
3229
  if (!entry.isDirectory()) {
2843
3230
  continue;
2844
3231
  }
2845
- const sourceSkill = path19.join(sourceDir, entry.name);
2846
- const sourceSkillFile = path19.join(sourceSkill, "SKILL.md");
3232
+ const sourceSkill = path20.join(sourceDir, entry.name);
3233
+ const sourceSkillFile = path20.join(sourceSkill, "SKILL.md");
2847
3234
  if (!await exists4(sourceSkill) || !await stat7(sourceSkillFile).then((info) => info.isFile()).catch(() => false)) {
2848
3235
  continue;
2849
3236
  }
2850
- const targetSkill = path19.join(targetDir, entry.name);
2851
- const targetSkillFile = path19.join(targetSkill, "SKILL.md");
3237
+ const targetSkill = path20.join(targetDir, entry.name);
3238
+ const targetSkillFile = path20.join(targetSkill, "SKILL.md");
2852
3239
  try {
2853
3240
  await cp(sourceSkill, targetSkill, { recursive: true, force: false, errorOnExist: true });
2854
3241
  written.push(targetSkillFile);
@@ -2862,14 +3249,14 @@ async function writeBuiltinSkills(repoRoot) {
2862
3249
  }
2863
3250
 
2864
3251
  // src/core/init/import.ts
2865
- import { readFile as readFile9, readdir as readdir4 } from "fs/promises";
2866
- import path20 from "path";
3252
+ import { readFile as readFile10, readdir as readdir4 } from "fs/promises";
3253
+ import path21 from "path";
2867
3254
  var PROSE_PRECEDENCE = ["AGENTS.md", "CLAUDE.md", ".github/copilot-instructions.md"];
2868
3255
  var CURSOR_RULES_DIR = ".cursor/rules";
2869
3256
  var NAME_RE4 = /^[a-z0-9][a-z0-9-]*$/;
2870
3257
  async function readIfExists2(filePath) {
2871
3258
  try {
2872
- return await readFile9(filePath, "utf8");
3259
+ return await readFile10(filePath, "utf8");
2873
3260
  } catch (error) {
2874
3261
  if (error.code === "ENOENT") {
2875
3262
  return void 0;
@@ -2913,7 +3300,7 @@ function novelSections(canonical, other) {
2913
3300
  });
2914
3301
  }
2915
3302
  async function listCursorRules(repoRoot) {
2916
- const dir = path20.join(repoRoot, CURSOR_RULES_DIR);
3303
+ const dir = path21.join(repoRoot, CURSOR_RULES_DIR);
2917
3304
  let entries;
2918
3305
  try {
2919
3306
  entries = await readdir4(dir);
@@ -2927,7 +3314,7 @@ async function listCursorRules(repoRoot) {
2927
3314
  return Promise.all(
2928
3315
  files.map(async (name) => ({
2929
3316
  file: `${CURSOR_RULES_DIR}/${name}`,
2930
- content: await readFile9(path20.join(dir, name), "utf8")
3317
+ content: await readFile10(path21.join(dir, name), "utf8")
2931
3318
  }))
2932
3319
  );
2933
3320
  }
@@ -2942,7 +3329,7 @@ function globsToApplyTo(value) {
2942
3329
  return void 0;
2943
3330
  }
2944
3331
  function ruleName(fileName) {
2945
- const base = path20.basename(fileName, ".mdc").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
3332
+ const base = path21.basename(fileName, ".mdc").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
2946
3333
  return NAME_RE4.test(base) ? base : "imported-rule";
2947
3334
  }
2948
3335
  async function importVendorFiles(repoRoot, options = {}) {
@@ -2953,7 +3340,7 @@ async function importVendorFiles(repoRoot, options = {}) {
2953
3340
  if (!wanted(file)) {
2954
3341
  continue;
2955
3342
  }
2956
- const content = await readIfExists2(path20.join(repoRoot, file));
3343
+ const content = await readIfExists2(path21.join(repoRoot, file));
2957
3344
  if (content && content.trim()) {
2958
3345
  prose.push({ file, content });
2959
3346
  }
@@ -3028,7 +3415,7 @@ async function detectName(repoRoot) {
3028
3415
  if (packageJson && typeof packageJson.name === "string" && packageJson.name.trim()) {
3029
3416
  return packageJson.name.trim();
3030
3417
  }
3031
- return path21.basename(repoRoot);
3418
+ return path22.basename(repoRoot);
3032
3419
  }
3033
3420
  async function writeManifest(repoRoot, manifest) {
3034
3421
  const body = {
@@ -3041,14 +3428,14 @@ async function writeManifest(repoRoot, manifest) {
3041
3428
  body.tools = { allow: manifest.tools.allow };
3042
3429
  }
3043
3430
  await mkdir9(projectHarnessDir(repoRoot), { recursive: true });
3044
- await writeFile8(projectManifestPath(repoRoot), stringifyYaml4(body), "utf8");
3431
+ await writeFile9(projectManifestPath(repoRoot), stringifyYaml4(body), "utf8");
3045
3432
  }
3046
3433
  async function writeProjectMemory(repoRoot) {
3047
3434
  const dir = projectObjectDir(repoRoot, "memory");
3048
3435
  await mkdir9(dir, { recursive: true });
3049
- const filePath = path21.join(dir, "project.md");
3436
+ const filePath = path22.join(dir, "project.md");
3050
3437
  try {
3051
- await writeFile8(filePath, `${PROJECT_MEMORY_TEMPLATE}
3438
+ await writeFile9(filePath, `${PROJECT_MEMORY_TEMPLATE}
3052
3439
  `, { encoding: "utf8", flag: "wx" });
3053
3440
  return [filePath];
3054
3441
  } catch (error) {
@@ -3066,13 +3453,13 @@ async function writeImportedRules(repoRoot, report) {
3066
3453
  await mkdir9(dir, { recursive: true });
3067
3454
  const written = [];
3068
3455
  for (const rule of report.importedRules) {
3069
- const filePath = path21.join(dir, `${rule.name}.md`);
3456
+ const filePath = path22.join(dir, `${rule.name}.md`);
3070
3457
  const data = { name: rule.name, scope: "project" };
3071
3458
  if (rule.applyTo) {
3072
3459
  data.applyTo = rule.applyTo;
3073
3460
  }
3074
3461
  try {
3075
- await writeFile8(filePath, serializeFrontmatter(data, rule.body), { encoding: "utf8", flag: "wx" });
3462
+ await writeFile9(filePath, serializeFrontmatter(data, rule.body), { encoding: "utf8", flag: "wx" });
3076
3463
  written.push(filePath);
3077
3464
  } catch (error) {
3078
3465
  if (error.code !== "EEXIST") {
@@ -3111,7 +3498,7 @@ async function writeStarterTools(repoRoot, profile, force) {
3111
3498
  async function initHarness(repoRoot, options = {}) {
3112
3499
  if (!options.force && await pathExists(projectManifestPath(repoRoot))) {
3113
3500
  throw new InitError(
3114
- `A harness already exists at ${path21.join(".threadroot", "harness.yaml")}. Re-run with --force to overwrite.`
3501
+ `A harness already exists at ${path22.join(".threadroot", "harness.yaml")}. Re-run with --force to overwrite.`
3115
3502
  );
3116
3503
  }
3117
3504
  const profile = await detectProfile(repoRoot, options.profile);
@@ -3133,7 +3520,7 @@ async function initHarness(repoRoot, options = {}) {
3133
3520
  if (options.import !== false) {
3134
3521
  report = await importVendorFiles(repoRoot, { include: options.importFiles });
3135
3522
  if (report.canonicalBody.trim()) {
3136
- await writeFile8(path21.join(repoRoot, AGENTS_FILE2), `${report.canonicalBody.trim()}
3523
+ await writeFile9(path22.join(repoRoot, AGENTS_FILE2), `${report.canonicalBody.trim()}
3137
3524
  `, "utf8");
3138
3525
  }
3139
3526
  rules = await writeImportedRules(repoRoot, report);
@@ -3177,7 +3564,7 @@ async function harnessStatus(repoRoot, options = {}) {
3177
3564
  // src/core/bootstrap.ts
3178
3565
  var DEFAULT_TASK = "start this project";
3179
3566
  async function harnessExists(repoRoot) {
3180
- return pathExists2(path22.join(repoRoot, ".threadroot", "harness.yaml"));
3567
+ return pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
3181
3568
  }
3182
3569
  async function pathExists2(target) {
3183
3570
  try {
@@ -3207,7 +3594,8 @@ async function bootstrapProject(repoRoot, options = {}) {
3207
3594
  agents: options.agents ?? "all",
3208
3595
  mode,
3209
3596
  home: options.home,
3210
- mcp: options.mcp
3597
+ mcp: options.mcp,
3598
+ mcpEntry: options.mcpEntry
3211
3599
  });
3212
3600
  } else {
3213
3601
  notes.push("Skipped global setup because --no-global was set.");
@@ -3220,14 +3608,14 @@ async function bootstrapProject(repoRoot, options = {}) {
3220
3608
  home: options.home
3221
3609
  });
3222
3610
  } else {
3223
- notes.push(`Would initialize local-only harness at ${path22.join(".threadroot", "harness.yaml")}.`);
3611
+ notes.push(`Would initialize local-only harness at ${path23.join(".threadroot", "harness.yaml")}.`);
3224
3612
  }
3225
3613
  } else if (existed) {
3226
3614
  notes.push("Existing harness detected; bootstrap will not reinitialize it.");
3227
3615
  } else {
3228
3616
  notes.push("Skipped project initialization because --no-init was set.");
3229
3617
  }
3230
- const hasHarnessAfterInit = existed || Boolean(init) || await pathExists2(path22.join(repoRoot, ".threadroot", "harness.yaml"));
3618
+ const hasHarnessAfterInit = existed || Boolean(init) || await pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
3231
3619
  if (options.expose) {
3232
3620
  exposed = await exposeProject(repoRoot, {
3233
3621
  agents: options.expose,
@@ -3237,6 +3625,10 @@ async function bootstrapProject(repoRoot, options = {}) {
3237
3625
  let status;
3238
3626
  let doctorReport;
3239
3627
  let context;
3628
+ let mcpCheck;
3629
+ if (options.mcp && write2) {
3630
+ mcpCheck = await checkCodexMcp({ repoRoot, home: options.home });
3631
+ }
3240
3632
  if (hasHarnessAfterInit) {
3241
3633
  status = await harnessStatus(repoRoot, { home: options.home });
3242
3634
  doctorReport = await doctor(repoRoot, { home: options.home });
@@ -3259,6 +3651,7 @@ async function bootstrapProject(repoRoot, options = {}) {
3259
3651
  status,
3260
3652
  doctor: doctorReport,
3261
3653
  context,
3654
+ mcpCheck,
3262
3655
  notes
3263
3656
  };
3264
3657
  }
@@ -3341,6 +3734,18 @@ function printCommandMap() {
3341
3734
  console.log("- `threadroot tools list|check` and `threadroot run <tool>` - use explicit local tools");
3342
3735
  console.log('- `threadroot remember "<note>"` - save durable handoff/project memory');
3343
3736
  }
3737
+ function printMcpCheck(report) {
3738
+ if (!report) {
3739
+ return;
3740
+ }
3741
+ console.log(`mcp check: ${report.status}`);
3742
+ if (report.entry) {
3743
+ console.log(`mcp server: ${report.entry.command} ${report.entry.args.join(" ")}`.trim());
3744
+ }
3745
+ for (const message of report.messages) {
3746
+ console.log(`- ${message}`);
3747
+ }
3748
+ }
3344
3749
  function printBootstrapReport(report) {
3345
3750
  console.log(`Threadroot bootstrap: ${report.mode === "write" ? "complete" : "plan"}`);
3346
3751
  if (report.setup) {
@@ -3363,6 +3768,7 @@ function printBootstrapReport(report) {
3363
3768
  }
3364
3769
  }
3365
3770
  printStatus(report.status);
3771
+ printMcpCheck(report.mcpCheck);
3366
3772
  printDoctor(report.doctor);
3367
3773
  printContext(report.context);
3368
3774
  printCommandMap();
@@ -3396,7 +3802,8 @@ async function runBootstrap(repoRoot, options) {
3396
3802
  noGlobal: options.global === false,
3397
3803
  noInit: options.init === false,
3398
3804
  import: options.import,
3399
- profile: options.profile ? profileIdSchema.parse(options.profile) : void 0
3805
+ profile: options.profile ? profileIdSchema.parse(options.profile) : void 0,
3806
+ mcpEntry: options.mcp ? mcpEntryForCurrentProcess() : void 0
3400
3807
  });
3401
3808
  printBootstrapReport(report);
3402
3809
  if (report.mode === "write" && report.doctor && !report.doctor.ok) {
@@ -3468,7 +3875,7 @@ async function runContext(repoRoot, task) {
3468
3875
 
3469
3876
  // src/commands/diff.ts
3470
3877
  import fs3 from "fs/promises";
3471
- import path23 from "path";
3878
+ import path24 from "path";
3472
3879
  async function readIfExists3(filePath) {
3473
3880
  try {
3474
3881
  return await fs3.readFile(filePath, "utf8");
@@ -3527,7 +3934,7 @@ async function runDiff(repoRoot) {
3527
3934
  const files = await compile(repoRoot, harness);
3528
3935
  let changed = 0;
3529
3936
  for (const file of files) {
3530
- const existing = await readIfExists3(path23.join(repoRoot, file.path));
3937
+ const existing = await readIfExists3(path24.join(repoRoot, file.path));
3531
3938
  if (existing === void 0) {
3532
3939
  changed += 1;
3533
3940
  console.log(`+ ${file.path} (new)`);
@@ -3642,13 +4049,13 @@ async function runInit(repoRoot, options) {
3642
4049
  }
3643
4050
 
3644
4051
  // src/commands/install.ts
3645
- import path26 from "path";
4052
+ import path27 from "path";
3646
4053
 
3647
4054
  // src/core/install/fetch.ts
3648
4055
  import { execFile } from "child_process";
3649
- import { mkdtemp, rm as rm3 } from "fs/promises";
4056
+ import { mkdtemp as mkdtemp2, rm as rm4 } from "fs/promises";
3650
4057
  import os2 from "os";
3651
- import path24 from "path";
4058
+ import path25 from "path";
3652
4059
  import { promisify } from "util";
3653
4060
  var run = promisify(execFile);
3654
4061
  function cloneUrl(ref) {
@@ -3669,8 +4076,8 @@ async function git(cwd, args) {
3669
4076
  }
3670
4077
  async function fetchGitSource(ref) {
3671
4078
  const url = cloneUrl(ref);
3672
- const dir = await mkdtemp(path24.join(os2.tmpdir(), "threadroot-fetch-"));
3673
- const cleanup = () => rm3(dir, { recursive: true, force: true });
4079
+ const dir = await mkdtemp2(path25.join(os2.tmpdir(), "threadroot-fetch-"));
4080
+ const cleanup = () => rm4(dir, { recursive: true, force: true });
3674
4081
  try {
3675
4082
  if (ref.ref) {
3676
4083
  try {
@@ -3691,9 +4098,9 @@ async function fetchGitSource(ref) {
3691
4098
  }
3692
4099
 
3693
4100
  // 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";
4101
+ import { cp as cp2, mkdir as mkdir10, readFile as readFile11, readdir as readdir5, stat as stat10, writeFile as writeFile10 } from "fs/promises";
3695
4102
  import { createHash as createHash2 } from "crypto";
3696
- import path25 from "path";
4103
+ import path26 from "path";
3697
4104
  var NAME_RE5 = /^[a-z0-9][a-z0-9-]*$/;
3698
4105
  var KIND_DIR = {
3699
4106
  skill: "skills",
@@ -3705,8 +4112,8 @@ function objectExt(kind) {
3705
4112
  return kind === "tool" || kind === "connection" ? ".yaml" : ".md";
3706
4113
  }
3707
4114
  function safeRepoPath(objectPath) {
3708
- const normalized = path25.normalize(objectPath);
3709
- if (path25.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path25.sep}`)) {
4115
+ const normalized = path26.normalize(objectPath);
4116
+ if (path26.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path26.sep}`)) {
3710
4117
  throw new Error(`Unsafe object path: ${objectPath}`);
3711
4118
  }
3712
4119
  return normalized;
@@ -3720,7 +4127,7 @@ function inferKind(objectPath, override) {
3720
4127
  if (segments.includes("tools")) return "tool";
3721
4128
  if (segments.includes("connections")) return "connection";
3722
4129
  if (segments.includes("rules")) return "rule";
3723
- const ext = path25.extname(objectPath).toLowerCase();
4130
+ const ext = path26.extname(objectPath).toLowerCase();
3724
4131
  if (ext === ".yaml" || ext === ".yml") return "tool";
3725
4132
  if (ext === ".md") return "skill";
3726
4133
  throw new Error(
@@ -3728,7 +4135,7 @@ function inferKind(objectPath, override) {
3728
4135
  );
3729
4136
  }
3730
4137
  function deriveName(objectPath) {
3731
- const base = path25.basename(objectPath, path25.extname(objectPath));
4138
+ const base = path26.basename(objectPath, path26.extname(objectPath));
3732
4139
  if (!NAME_RE5.test(base)) {
3733
4140
  throw new Error(`Invalid object name \`${base}\` (use lowercase letters, digits, and dashes).`);
3734
4141
  }
@@ -3740,8 +4147,8 @@ async function hashDirectory(root) {
3740
4147
  async function walk(dir) {
3741
4148
  const entries = (await readdir5(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
3742
4149
  for (const entry of entries) {
3743
- const full = path25.join(dir, entry.name);
3744
- const rel = path25.relative(root, full).split(path25.sep).join("/");
4150
+ const full = path26.join(dir, entry.name);
4151
+ const rel = path26.relative(root, full).split(path26.sep).join("/");
3745
4152
  if (entry.isSymbolicLink()) {
3746
4153
  throw new Error(`Refusing to install skill directory with symlink: ${rel}`);
3747
4154
  }
@@ -3752,7 +4159,7 @@ async function hashDirectory(root) {
3752
4159
  if (entry.isFile()) {
3753
4160
  hash.update(`file:${rel}
3754
4161
  `);
3755
- hash.update(await readFile10(full));
4162
+ hash.update(await readFile11(full));
3756
4163
  hash.update("\n");
3757
4164
  }
3758
4165
  }
@@ -3761,8 +4168,8 @@ async function hashDirectory(root) {
3761
4168
  return hash.digest("hex");
3762
4169
  }
3763
4170
  async function validateSkillDirectory2(sourcePath, expectedName) {
3764
- const skillPath = path25.join(sourcePath, "SKILL.md");
3765
- const parsed = parseFrontmatter(await readFile10(skillPath, "utf8"));
4171
+ const skillPath = path26.join(sourcePath, "SKILL.md");
4172
+ const parsed = parseFrontmatter(await readFile11(skillPath, "utf8"));
3766
4173
  const result = skillFrontmatterSchema.safeParse(parsed.data);
3767
4174
  if (!result.success) {
3768
4175
  const detail = result.error.issues.map((issue) => issue.message).join("; ");
@@ -3791,7 +4198,7 @@ async function installObject(repoRoot, rawSource, options = {}) {
3791
4198
  objectPath = safeRepoPath(within);
3792
4199
  refLabel = ref.ref;
3793
4200
  const fetched = await fetchGitSource(ref);
3794
- sourcePath = path25.join(fetched.dir, objectPath);
4201
+ sourcePath = path26.join(fetched.dir, objectPath);
3795
4202
  resolved = fetched.sha;
3796
4203
  cleanup = fetched.cleanup;
3797
4204
  } else {
@@ -3812,14 +4219,14 @@ async function installObject(repoRoot, rawSource, options = {}) {
3812
4219
  throw new Error("Only skill objects may be installed from a directory.");
3813
4220
  }
3814
4221
  integrity = `sha256:${await validateSkillDirectory2(sourcePath, name)}`;
3815
- destPath = path25.join(destDir, name);
4222
+ destPath = path26.join(destDir, name);
3816
4223
  await mkdir10(destDir, { recursive: true });
3817
4224
  await cp2(sourcePath, destPath, { recursive: true, force: true });
3818
4225
  } else {
3819
- const content = await readFile10(sourcePath, "utf8");
3820
- destPath = path25.join(destDir, `${name}${objectExt(kind)}`);
4226
+ const content = await readFile11(sourcePath, "utf8");
4227
+ destPath = path26.join(destDir, `${name}${objectExt(kind)}`);
3821
4228
  await mkdir10(destDir, { recursive: true });
3822
- await writeFile9(destPath, content, "utf8");
4229
+ await writeFile10(destPath, content, "utf8");
3823
4230
  integrity = `sha256:${hashContent(content)}`;
3824
4231
  }
3825
4232
  const entry = {
@@ -3871,7 +4278,7 @@ async function runInstall(repoRoot, source, options) {
3871
4278
  if (installed.kind === "skill" && installed.entry.sourceKind !== "local") {
3872
4279
  console.log(" note: inspect external skills before trusting bundled scripts, assets, or allowed tools.");
3873
4280
  if (scope === "project") {
3874
- console.log(` inspect: threadroot skills inspect ${path26.relative(repoRoot, installed.path)}`);
4281
+ console.log(` inspect: threadroot skills inspect ${path27.relative(repoRoot, installed.path)}`);
3875
4282
  }
3876
4283
  }
3877
4284
  } catch (error) {
@@ -4162,8 +4569,9 @@ async function handleMessage(repoRoot, request) {
4162
4569
  if (request.method === "initialize") {
4163
4570
  return resultResponse(request, {
4164
4571
  protocolVersion: "2024-11-05",
4165
- serverInfo: { name: "threadroot", version: "0.1.2" },
4166
- capabilities: { tools: {} }
4572
+ serverInfo: { name: "threadroot", version: "0.1.3" },
4573
+ capabilities: { tools: {} },
4574
+ 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
4575
  });
4168
4576
  }
4169
4577
  if (request.method === "notifications/initialized") {
@@ -4385,11 +4793,11 @@ function agentNotes(agent) {
4385
4793
  }
4386
4794
 
4387
4795
  // src/core/mcp-config.ts
4388
- import { mkdir as mkdir11, readFile as readFile11, writeFile as writeFile10 } from "fs/promises";
4389
- import path27 from "path";
4796
+ import { mkdir as mkdir11, readFile as readFile12, writeFile as writeFile11 } from "fs/promises";
4797
+ import path28 from "path";
4390
4798
  var TARGETS = [
4391
- { agent: "copilot", file: path27.join(".vscode", "mcp.json"), key: "servers" },
4392
- { agent: "cursor", file: path27.join(".cursor", "mcp.json"), key: "mcpServers" },
4799
+ { agent: "copilot", file: path28.join(".vscode", "mcp.json"), key: "servers" },
4800
+ { agent: "cursor", file: path28.join(".cursor", "mcp.json"), key: "mcpServers" },
4393
4801
  { agent: "claude", file: ".mcp.json", key: "mcpServers" }
4394
4802
  ];
4395
4803
  function mcpServerEntry(command, scriptPath) {
@@ -4398,7 +4806,7 @@ function mcpServerEntry(command, scriptPath) {
4398
4806
  async function mergeConfig(filePath, key, entry) {
4399
4807
  let config = {};
4400
4808
  try {
4401
- const raw = await readFile11(filePath, "utf8");
4809
+ const raw = await readFile12(filePath, "utf8");
4402
4810
  const parsed = JSON.parse(raw);
4403
4811
  if (parsed && typeof parsed === "object") {
4404
4812
  config = parsed;
@@ -4411,8 +4819,8 @@ async function mergeConfig(filePath, key, entry) {
4411
4819
  const servers = config[key] && typeof config[key] === "object" ? config[key] : {};
4412
4820
  servers.threadroot = { ...entry };
4413
4821
  config[key] = servers;
4414
- await mkdir11(path27.dirname(filePath), { recursive: true });
4415
- await writeFile10(filePath, `${JSON.stringify(config, null, 2)}
4822
+ await mkdir11(path28.dirname(filePath), { recursive: true });
4823
+ await writeFile11(filePath, `${JSON.stringify(config, null, 2)}
4416
4824
  `, "utf8");
4417
4825
  }
4418
4826
  async function writeProjectMcpConfigs(input2) {
@@ -4420,7 +4828,7 @@ async function writeProjectMcpConfigs(input2) {
4420
4828
  const targets = agents ? TARGETS.filter((target) => agents.includes(target.agent)) : TARGETS;
4421
4829
  const written = [];
4422
4830
  for (const target of targets) {
4423
- const filePath = path27.join(input2.repoRoot, target.file);
4831
+ const filePath = path28.join(input2.repoRoot, target.file);
4424
4832
  await mergeConfig(filePath, target.key, input2.entry);
4425
4833
  written.push(target.file);
4426
4834
  }
@@ -4454,6 +4862,24 @@ async function runMcpSetup(repoRoot, options) {
4454
4862
  }
4455
4863
  console.log(mcpSetupGuide({ repoRoot, agent: options.agent }));
4456
4864
  }
4865
+ async function runMcpCheck(repoRoot, options) {
4866
+ const timeoutMs = options.timeout ? Number.parseInt(options.timeout, 10) : void 0;
4867
+ const report = await checkCodexMcp({ repoRoot, timeoutMs });
4868
+ console.log(`Threadroot MCP check: ${report.status}`);
4869
+ console.log(`config: ${report.configPath}`);
4870
+ if (report.entry) {
4871
+ console.log(`server: ${report.entry.command} ${report.entry.args.join(" ")}`.trim());
4872
+ }
4873
+ for (const message of report.messages) {
4874
+ console.log(`- ${message}`);
4875
+ }
4876
+ if (report.tools.length > 0) {
4877
+ console.log(`tools: ${report.tools.join(", ")}`);
4878
+ }
4879
+ if (report.status === "error") {
4880
+ process.exitCode = 1;
4881
+ }
4882
+ }
4457
4883
 
4458
4884
  // src/commands/memory.ts
4459
4885
  async function runMemoryRead(repoRoot, type) {
@@ -4540,7 +4966,8 @@ async function runSetup(_repoRoot, options) {
4540
4966
  agents: options.agent,
4541
4967
  mode,
4542
4968
  force: options.force,
4543
- mcp: options.mcp
4969
+ mcp: options.mcp,
4970
+ mcpEntry: options.mcp ? mcpEntryForCurrentProcess() : void 0
4544
4971
  });
4545
4972
  const title = mode === "dry-run" ? "Global setup plan" : mode === "check" ? "Global setup check" : mode === "undo" ? "Global setup undo" : "Global setup complete";
4546
4973
  console.log(`${title}:`);
@@ -4548,6 +4975,19 @@ async function runSetup(_repoRoot, options) {
4548
4975
  const suffix = entry.message ? ` - ${entry.message}` : "";
4549
4976
  console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
4550
4977
  }
4978
+ if (options.mcp && options.global && mode === "write") {
4979
+ const check = await checkCodexMcp({ repoRoot: _repoRoot });
4980
+ console.log(`MCP verification: ${check.status}`);
4981
+ if (check.entry) {
4982
+ console.log(`MCP server: ${check.entry.command} ${check.entry.args.join(" ")}`.trim());
4983
+ }
4984
+ for (const message of check.messages) {
4985
+ console.log(`- ${message}`);
4986
+ }
4987
+ if (check.status === "error") {
4988
+ process.exitCode = 1;
4989
+ }
4990
+ }
4551
4991
  if (mode === "write") {
4552
4992
  console.log("Reload or restart open agent sessions so new global skills/config are discovered.");
4553
4993
  }
@@ -4586,8 +5026,8 @@ async function runStatus(repoRoot) {
4586
5026
  }
4587
5027
 
4588
5028
  // 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";
5029
+ import { cp as cp3, mkdir as mkdir12, readFile as readFile13, readdir as readdir6, stat as stat11 } from "fs/promises";
5030
+ import path29 from "path";
4591
5031
  import { fileURLToPath as fileURLToPath2 } from "url";
4592
5032
  import { parse as parseYaml3 } from "yaml";
4593
5033
  import { z as z5 } from "zod";
@@ -4600,14 +5040,14 @@ var packManifestSchema = z5.object({
4600
5040
  rules: z5.array(z5.string()).default([]),
4601
5041
  connections: z5.array(z5.string()).default([])
4602
5042
  });
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, "../../../..");
5043
+ var DIST_DIR2 = path29.dirname(fileURLToPath2(import.meta.url));
5044
+ var PACKAGE_ROOT_FROM_BUNDLE2 = path29.resolve(DIST_DIR2, "..");
5045
+ var PACKAGE_ROOT_FROM_DIST2 = path29.resolve(DIST_DIR2, "../../..");
5046
+ var PACKAGE_ROOT_FROM_SRC2 = path29.resolve(DIST_DIR2, "../../../..");
4607
5047
  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")
5048
+ path29.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
5049
+ path29.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
5050
+ path29.join(PACKAGE_ROOT_FROM_SRC2, "packs")
4611
5051
  ];
4612
5052
  async function exists5(target) {
4613
5053
  try {
@@ -4639,22 +5079,22 @@ async function isPackRoot(candidate) {
4639
5079
  return false;
4640
5080
  }
4641
5081
  for (const entry of entries) {
4642
- if (entry.isDirectory() && await exists5(path28.join(candidate, entry.name, "pack.yaml"))) {
5082
+ if (entry.isDirectory() && await exists5(path29.join(candidate, entry.name, "pack.yaml"))) {
4643
5083
  return true;
4644
5084
  }
4645
5085
  }
4646
5086
  return false;
4647
5087
  }
4648
5088
  function safeRelative(ref) {
4649
- const normalized = path28.normalize(ref);
4650
- if (path28.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path28.sep}`)) {
5089
+ const normalized = path29.normalize(ref);
5090
+ if (path29.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path29.sep}`)) {
4651
5091
  throw new Error(`Unsafe pack reference: ${ref}`);
4652
5092
  }
4653
5093
  return normalized;
4654
5094
  }
4655
5095
  async function readPackManifest(packDir) {
4656
- const file = path28.join(packDir, "pack.yaml");
4657
- const parsed = packManifestSchema.safeParse(parseYaml3(await readFile12(file, "utf8")));
5096
+ const file = path29.join(packDir, "pack.yaml");
5097
+ const parsed = packManifestSchema.safeParse(parseYaml3(await readFile13(file, "utf8")));
4658
5098
  if (!parsed.success) {
4659
5099
  const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
4660
5100
  throw new Error(`Invalid pack manifest ${file}: ${detail}`);
@@ -4662,7 +5102,7 @@ async function readPackManifest(packDir) {
4662
5102
  return parsed.data;
4663
5103
  }
4664
5104
  async function packDirFor(repoRoot, nameOrPath) {
4665
- if (path28.isAbsolute(nameOrPath)) {
5105
+ if (path29.isAbsolute(nameOrPath)) {
4666
5106
  return nameOrPath;
4667
5107
  }
4668
5108
  if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
@@ -4670,14 +5110,14 @@ async function packDirFor(repoRoot, nameOrPath) {
4670
5110
  }
4671
5111
  const bundled = await bundledPacksDir();
4672
5112
  if (bundled) {
4673
- return path28.join(bundled, nameOrPath);
5113
+ return path29.join(bundled, nameOrPath);
4674
5114
  }
4675
- return toRepoPath(repoRoot, path28.join("packs", nameOrPath));
5115
+ return toRepoPath(repoRoot, path29.join("packs", nameOrPath));
4676
5116
  }
4677
5117
  async function directFiles(dir, ext) {
4678
5118
  try {
4679
5119
  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();
5120
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path29.join(dir, entry.name)).sort();
4681
5121
  } catch (error) {
4682
5122
  if (error.code === "ENOENT") {
4683
5123
  return [];
@@ -4690,11 +5130,11 @@ async function skillEntries(dir) {
4690
5130
  const entries = await readdir6(dir, { withFileTypes: true });
4691
5131
  const result = [];
4692
5132
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
4693
- const full = path28.join(dir, entry.name);
5133
+ const full = path29.join(dir, entry.name);
4694
5134
  if (entry.isFile() && entry.name.endsWith(".md")) {
4695
5135
  result.push(full);
4696
5136
  }
4697
- if (entry.isDirectory() && await exists5(path28.join(full, "SKILL.md"))) {
5137
+ if (entry.isDirectory() && await exists5(path29.join(full, "SKILL.md"))) {
4698
5138
  result.push(full);
4699
5139
  }
4700
5140
  }
@@ -4709,34 +5149,34 @@ async function skillEntries(dir) {
4709
5149
  async function collectObjects(packDir, manifest) {
4710
5150
  async function resolveRef(ref) {
4711
5151
  const safe = safeRelative(ref);
4712
- const local = path28.resolve(packDir, safe);
5152
+ const local = path29.resolve(packDir, safe);
4713
5153
  if (await exists5(local)) {
4714
5154
  return local;
4715
5155
  }
4716
- return path28.resolve(packDir, "..", "..", safe);
5156
+ return path29.resolve(packDir, "..", "..", safe);
4717
5157
  }
4718
5158
  return {
4719
5159
  skills: [
4720
5160
  ...await Promise.all(manifest.skills.map(resolveRef)),
4721
- ...await skillEntries(path28.join(packDir, "skills"))
5161
+ ...await skillEntries(path29.join(packDir, "skills"))
4722
5162
  ],
4723
5163
  tools: [
4724
5164
  ...await Promise.all(manifest.tools.map(resolveRef)),
4725
- ...await directFiles(path28.join(packDir, "tools"), ".yaml")
5165
+ ...await directFiles(path29.join(packDir, "tools"), ".yaml")
4726
5166
  ],
4727
5167
  rules: [
4728
5168
  ...await Promise.all(manifest.rules.map(resolveRef)),
4729
- ...await directFiles(path28.join(packDir, "rules"), ".md")
5169
+ ...await directFiles(path29.join(packDir, "rules"), ".md")
4730
5170
  ],
4731
5171
  connections: [
4732
5172
  ...await Promise.all(manifest.connections.map(resolveRef)),
4733
- ...await directFiles(path28.join(packDir, "connections"), ".yaml")
5173
+ ...await directFiles(path29.join(packDir, "connections"), ".yaml")
4734
5174
  ]
4735
5175
  };
4736
5176
  }
4737
5177
  function baseName(source) {
4738
- const parsed = path28.basename(source) === "SKILL.md" ? path28.dirname(source) : source;
4739
- return path28.basename(parsed, path28.extname(parsed));
5178
+ const parsed = path29.basename(source) === "SKILL.md" ? path29.dirname(source) : source;
5179
+ return path29.basename(parsed, path29.extname(parsed));
4740
5180
  }
4741
5181
  async function listPacks(repoRoot) {
4742
5182
  const dirs = [toRepoPath(repoRoot, "packs"), await bundledPacksDir()].filter((dir) => Boolean(dir));
@@ -4753,8 +5193,8 @@ async function listPacks(repoRoot) {
4753
5193
  if (!entry.isDirectory() || seen.has(entry.name)) {
4754
5194
  continue;
4755
5195
  }
4756
- const packDir = path28.join(root, entry.name);
4757
- if (!await exists5(path28.join(packDir, "pack.yaml"))) {
5196
+ const packDir = path29.join(root, entry.name);
5197
+ if (!await exists5(path29.join(packDir, "pack.yaml"))) {
4758
5198
  continue;
4759
5199
  }
4760
5200
  seen.add(entry.name);
@@ -4778,14 +5218,14 @@ async function inspectPack(repoRoot, nameOrPath) {
4778
5218
  };
4779
5219
  }
4780
5220
  async function validateProse(file, kind) {
4781
- const target = path28.basename(file) === "SKILL.md" ? file : file;
4782
- const content = await readFile12(target, "utf8");
5221
+ const target = path29.basename(file) === "SKILL.md" ? file : file;
5222
+ const content = await readFile13(target, "utf8");
4783
5223
  const parsed = parseFrontmatter(content);
4784
5224
  const schema = kind === "skill" ? skillFrontmatterSchema : ruleFrontmatterSchema;
4785
5225
  schema.parse(parsed.data);
4786
5226
  }
4787
5227
  async function validateYaml(file, kind) {
4788
- const content = await readFile12(file, "utf8");
5228
+ const content = await readFile13(file, "utf8");
4789
5229
  const schema = kind === "tool" ? toolManifestSchema : connectionManifestSchema;
4790
5230
  schema.parse(parseYaml3(content));
4791
5231
  }
@@ -4796,7 +5236,7 @@ async function validatePack(repoRoot, nameOrPath) {
4796
5236
  const manifest = await readPackManifest(packDir);
4797
5237
  const objects = await collectObjects(packDir, manifest);
4798
5238
  for (const skill of objects.skills) {
4799
- await validateProse(path28.basename(skill) === "SKILL.md" ? skill : path28.join(skill, "SKILL.md"), "skill");
5239
+ await validateProse(path29.basename(skill) === "SKILL.md" ? skill : path29.join(skill, "SKILL.md"), "skill");
4800
5240
  }
4801
5241
  for (const rule of objects.rules) await validateProse(rule, "rule");
4802
5242
  for (const tool of objects.tools) await validateYaml(tool, "tool");
@@ -4812,7 +5252,7 @@ async function validatePack(repoRoot, nameOrPath) {
4812
5252
  async function copyObject(source, destDir) {
4813
5253
  const info = await stat11(source);
4814
5254
  const name = baseName(source);
4815
- const dest = info.isDirectory() ? path28.join(destDir, name) : path28.join(destDir, path28.basename(source));
5255
+ const dest = info.isDirectory() ? path29.join(destDir, name) : path29.join(destDir, path29.basename(source));
4816
5256
  await mkdir12(destDir, { recursive: true });
4817
5257
  await cp3(source, dest, { recursive: true, force: true });
4818
5258
  return dest;
@@ -5101,7 +5541,7 @@ async function runConnectionsCheck(repoRoot) {
5101
5541
  // src/cli.ts
5102
5542
  function createProgram(repoRoot = process.cwd()) {
5103
5543
  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");
5544
+ program.name("threadroot").description("Git for your AI agent harness: one command to bootstrap, one .threadroot source.").version("0.1.3");
5105
5545
  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
5546
  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
5547
  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 +5579,7 @@ function createProgram(repoRoot = process.cwd()) {
5139
5579
  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
5580
  const mcp = program.command("mcp").description("Run or configure the local Threadroot MCP server.");
5141
5581
  mcp.action(() => runMcp(repoRoot));
5582
+ 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
5583
  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
5584
  return program;
5144
5585
  }