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/CHANGELOG.md +13 -0
- package/README.md +15 -4
- package/dist/index.js +578 -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,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(
|
|
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 =
|
|
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
|
|
2609
|
-
import
|
|
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
|
|
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
|
|
3008
|
+
return path16.join(repoRoot, provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
|
|
2622
3009
|
}
|
|
2623
3010
|
function relSkillPath(provider) {
|
|
2624
|
-
return
|
|
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
|
|
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(
|
|
2676
|
-
await
|
|
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
|
|
2691
|
-
import
|
|
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
|
|
2695
|
-
import
|
|
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 =
|
|
2700
|
-
await mkdir7(
|
|
2701
|
-
await
|
|
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
|
|
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(
|
|
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) =>
|
|
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
|
|
3153
|
+
import path19 from "path";
|
|
2767
3154
|
function toPosix(relativePath) {
|
|
2768
|
-
return relativePath.split(
|
|
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 =
|
|
2780
|
-
const relativePath = toPosix(
|
|
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
|
|
3186
|
+
import path20 from "path";
|
|
2800
3187
|
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 =
|
|
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
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
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 =
|
|
2846
|
-
const sourceSkillFile =
|
|
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 =
|
|
2851
|
-
const targetSkillFile =
|
|
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
|
|
2866
|
-
import
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
3436
|
+
const filePath = path22.join(dir, "project.md");
|
|
3050
3437
|
try {
|
|
3051
|
-
await
|
|
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 =
|
|
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
|
|
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 ${
|
|
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
|
|
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(
|
|
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 ${
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
4056
|
+
import { mkdtemp as mkdtemp2, rm as rm4 } from "fs/promises";
|
|
3650
4057
|
import os2 from "os";
|
|
3651
|
-
import
|
|
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
|
|
3673
|
-
const cleanup = () =>
|
|
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
|
|
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
|
|
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 =
|
|
3709
|
-
if (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3744
|
-
const rel =
|
|
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
|
|
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 =
|
|
3765
|
-
const parsed = parseFrontmatter(await
|
|
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 =
|
|
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 =
|
|
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
|
|
3820
|
-
destPath =
|
|
4226
|
+
const content = await readFile11(sourcePath, "utf8");
|
|
4227
|
+
destPath = path26.join(destDir, `${name}${objectExt(kind)}`);
|
|
3821
4228
|
await mkdir10(destDir, { recursive: true });
|
|
3822
|
-
await
|
|
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 ${
|
|
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.
|
|
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
|
|
4389
|
-
import
|
|
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:
|
|
4392
|
-
{ agent: "cursor", file:
|
|
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
|
|
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(
|
|
4415
|
-
await
|
|
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 =
|
|
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
|
|
4590
|
-
import
|
|
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 =
|
|
4604
|
-
var PACKAGE_ROOT_FROM_BUNDLE2 =
|
|
4605
|
-
var PACKAGE_ROOT_FROM_DIST2 =
|
|
4606
|
-
var PACKAGE_ROOT_FROM_SRC2 =
|
|
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
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
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(
|
|
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 =
|
|
4650
|
-
if (
|
|
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 =
|
|
4657
|
-
const parsed = packManifestSchema.safeParse(parseYaml3(await
|
|
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 (
|
|
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
|
|
5113
|
+
return path29.join(bundled, nameOrPath);
|
|
4674
5114
|
}
|
|
4675
|
-
return toRepoPath(repoRoot,
|
|
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) =>
|
|
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 =
|
|
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(
|
|
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 =
|
|
5152
|
+
const local = path29.resolve(packDir, safe);
|
|
4713
5153
|
if (await exists5(local)) {
|
|
4714
5154
|
return local;
|
|
4715
5155
|
}
|
|
4716
|
-
return
|
|
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(
|
|
5161
|
+
...await skillEntries(path29.join(packDir, "skills"))
|
|
4722
5162
|
],
|
|
4723
5163
|
tools: [
|
|
4724
5164
|
...await Promise.all(manifest.tools.map(resolveRef)),
|
|
4725
|
-
...await directFiles(
|
|
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(
|
|
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(
|
|
5173
|
+
...await directFiles(path29.join(packDir, "connections"), ".yaml")
|
|
4734
5174
|
]
|
|
4735
5175
|
};
|
|
4736
5176
|
}
|
|
4737
5177
|
function baseName(source) {
|
|
4738
|
-
const parsed =
|
|
4739
|
-
return
|
|
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 =
|
|
4757
|
-
if (!await exists5(
|
|
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 =
|
|
4782
|
-
const content = await
|
|
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
|
|
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(
|
|
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() ?
|
|
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.
|
|
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
|
}
|