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/CHANGELOG.md +19 -0
- package/README.md +17 -4
- package/dist/index.js +592 -137
- package/package.json +1 -1
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
|
|
8
|
+
import path23 from "path";
|
|
9
9
|
|
|
10
10
|
// src/core/doctor.ts
|
|
11
11
|
import { stat as stat5 } from "fs/promises";
|
|
12
|
-
import
|
|
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
|
|
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
|
-
|
|
1807
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
2609
|
-
import
|
|
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
|
|
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
|
|
3022
|
+
return path16.join(repoRoot, provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
|
|
2622
3023
|
}
|
|
2623
3024
|
function relSkillPath(provider) {
|
|
2624
|
-
return
|
|
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
|
|
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(
|
|
2676
|
-
await
|
|
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
|
|
2691
|
-
import
|
|
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
|
|
2695
|
-
import
|
|
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 =
|
|
2700
|
-
await mkdir7(
|
|
2701
|
-
await
|
|
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
|
|
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(
|
|
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) =>
|
|
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
|
|
3167
|
+
import path19 from "path";
|
|
2767
3168
|
function toPosix(relativePath) {
|
|
2768
|
-
return relativePath.split(
|
|
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 =
|
|
2780
|
-
const relativePath = toPosix(
|
|
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
|
|
3200
|
+
import path20 from "path";
|
|
2800
3201
|
import { fileURLToPath } from "url";
|
|
2801
|
-
var DIST_DIR =
|
|
2802
|
-
var PACKAGE_ROOT_FROM_BUNDLE =
|
|
2803
|
-
var PACKAGE_ROOT_FROM_DIST =
|
|
2804
|
-
var PACKAGE_ROOT_FROM_SRC =
|
|
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
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
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 =
|
|
2846
|
-
const sourceSkillFile =
|
|
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 =
|
|
2851
|
-
const targetSkillFile =
|
|
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
|
|
2866
|
-
import
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
3450
|
+
const filePath = path22.join(dir, "project.md");
|
|
3050
3451
|
try {
|
|
3051
|
-
await
|
|
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 =
|
|
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
|
|
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 ${
|
|
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
|
|
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(
|
|
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 ${
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
4070
|
+
import { mkdtemp as mkdtemp2, rm as rm4 } from "fs/promises";
|
|
3650
4071
|
import os2 from "os";
|
|
3651
|
-
import
|
|
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
|
|
3673
|
-
const cleanup = () =>
|
|
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
|
|
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
|
|
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 =
|
|
3709
|
-
if (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3744
|
-
const rel =
|
|
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
|
|
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 =
|
|
3765
|
-
const parsed = parseFrontmatter(await
|
|
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 =
|
|
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 =
|
|
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
|
|
3820
|
-
destPath =
|
|
4240
|
+
const content = await readFile11(sourcePath, "utf8");
|
|
4241
|
+
destPath = path26.join(destDir, `${name}${objectExt(kind)}`);
|
|
3821
4242
|
await mkdir10(destDir, { recursive: true });
|
|
3822
|
-
await
|
|
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 ${
|
|
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:
|
|
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
|
|
4389
|
-
import
|
|
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:
|
|
4392
|
-
{ agent: "cursor", file:
|
|
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
|
|
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(
|
|
4415
|
-
await
|
|
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 =
|
|
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
|
|
4590
|
-
import
|
|
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 =
|
|
4604
|
-
var PACKAGE_ROOT_FROM_BUNDLE2 =
|
|
4605
|
-
var PACKAGE_ROOT_FROM_DIST2 =
|
|
4606
|
-
var PACKAGE_ROOT_FROM_SRC2 =
|
|
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
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
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(
|
|
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 =
|
|
4650
|
-
if (
|
|
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 =
|
|
4657
|
-
const parsed = packManifestSchema.safeParse(parseYaml3(await
|
|
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 (
|
|
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
|
|
5127
|
+
return path29.join(bundled, nameOrPath);
|
|
4674
5128
|
}
|
|
4675
|
-
return toRepoPath(repoRoot,
|
|
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) =>
|
|
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 =
|
|
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(
|
|
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 =
|
|
5166
|
+
const local = path29.resolve(packDir, safe);
|
|
4713
5167
|
if (await exists5(local)) {
|
|
4714
5168
|
return local;
|
|
4715
5169
|
}
|
|
4716
|
-
return
|
|
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(
|
|
5175
|
+
...await skillEntries(path29.join(packDir, "skills"))
|
|
4722
5176
|
],
|
|
4723
5177
|
tools: [
|
|
4724
5178
|
...await Promise.all(manifest.tools.map(resolveRef)),
|
|
4725
|
-
...await directFiles(
|
|
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(
|
|
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(
|
|
5187
|
+
...await directFiles(path29.join(packDir, "connections"), ".yaml")
|
|
4734
5188
|
]
|
|
4735
5189
|
};
|
|
4736
5190
|
}
|
|
4737
5191
|
function baseName(source) {
|
|
4738
|
-
const parsed =
|
|
4739
|
-
return
|
|
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 =
|
|
4757
|
-
if (!await exists5(
|
|
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 =
|
|
4782
|
-
const content = await
|
|
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
|
|
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(
|
|
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() ?
|
|
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(
|
|
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
|
}
|