threadroot 0.1.3 → 0.1.5
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 +23 -0
- package/INTEGRATION.md +256 -0
- package/README.md +42 -25
- package/dist/index.js +946 -554
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/core/bootstrap.ts
|
|
7
|
-
import { stat as
|
|
8
|
-
import
|
|
7
|
+
import { stat as stat10 } from "fs/promises";
|
|
8
|
+
import path24 from "path";
|
|
9
9
|
|
|
10
10
|
// src/core/doctor.ts
|
|
11
11
|
import { stat as stat5 } from "fs/promises";
|
|
@@ -1487,6 +1487,8 @@ async function createConnection(repoRoot, input2, options = {}) {
|
|
|
1487
1487
|
risk: input2.risk ?? "medium",
|
|
1488
1488
|
confirm: input2.confirm ?? input2.risk === "high",
|
|
1489
1489
|
healthcheck: input2.healthcheck ? { run: input2.healthcheck, expectExitCode: 0 } : void 0,
|
|
1490
|
+
allow: input2.allow ?? [],
|
|
1491
|
+
deny: input2.deny ?? [],
|
|
1490
1492
|
scope
|
|
1491
1493
|
};
|
|
1492
1494
|
const parsed = connectionManifestSchema.safeParse(candidate);
|
|
@@ -1716,16 +1718,17 @@ function threadrootSkillContent(provider, scope) {
|
|
|
1716
1718
|
"## Workflow",
|
|
1717
1719
|
"",
|
|
1718
1720
|
"1. If `threadroot --version` works, use `threadroot`. Otherwise use `npx --yes threadroot@latest` for one-off commands.",
|
|
1719
|
-
"2. If `.threadroot/harness.yaml` is missing and the user wants setup, run `threadroot bootstrap --yes` or `npx --yes threadroot@latest bootstrap --yes`.",
|
|
1721
|
+
"2. If `.threadroot/harness.yaml` is missing and the user wants setup, run `threadroot bootstrap --yes --mcp` or `npx --yes threadroot@latest bootstrap --yes --mcp`.",
|
|
1720
1722
|
'3. At the start of a coding session, run `threadroot start "<task>"` to get doctor status, project state, relevant skills, tools, memory, and the command map.',
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
"6.
|
|
1723
|
+
"4. If the project needs curated capabilities, inspect `threadroot packs list` and install relevant packs with `threadroot packs install <pack>` or `threadroot bootstrap --packs <list>` during setup.",
|
|
1724
|
+
'5. For a narrower slice, run `threadroot context "<task>"` and use the returned skills, rules, tools, memory, and references before doing broad file reads.',
|
|
1725
|
+
"6. Use `threadroot tools list`, `threadroot tools check`, and `threadroot run <tool>` for explicit local capabilities. Confirm risky tools only after human review.",
|
|
1726
|
+
"7. Do not create provider-specific files unless the user asks. Use `threadroot expose <agent>` when native project skill shims are desired.",
|
|
1724
1727
|
"",
|
|
1725
1728
|
"## Useful Commands",
|
|
1726
1729
|
"",
|
|
1727
1730
|
"```bash",
|
|
1728
|
-
"threadroot bootstrap --yes",
|
|
1731
|
+
"threadroot bootstrap --yes --mcp",
|
|
1729
1732
|
'threadroot start "<task>"',
|
|
1730
1733
|
"threadroot doctor",
|
|
1731
1734
|
"threadroot status",
|
|
@@ -1997,6 +2000,56 @@ function authorizeTool(tool, options) {
|
|
|
1997
2000
|
return { allowed: true };
|
|
1998
2001
|
}
|
|
1999
2002
|
|
|
2003
|
+
// src/core/tools/connection-policy.ts
|
|
2004
|
+
function normalize(value) {
|
|
2005
|
+
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
2006
|
+
}
|
|
2007
|
+
function commandBody(command, connectionCommand) {
|
|
2008
|
+
const normalized = normalize(command);
|
|
2009
|
+
const prefix = normalize(connectionCommand);
|
|
2010
|
+
if (normalized === prefix) {
|
|
2011
|
+
return "";
|
|
2012
|
+
}
|
|
2013
|
+
if (normalized.startsWith(`${prefix} `)) {
|
|
2014
|
+
return normalized.slice(prefix.length).trim();
|
|
2015
|
+
}
|
|
2016
|
+
return normalized;
|
|
2017
|
+
}
|
|
2018
|
+
function includesPattern(command, pattern) {
|
|
2019
|
+
return command.includes(normalize(pattern));
|
|
2020
|
+
}
|
|
2021
|
+
function authorizeConnectionCommand(connection, command) {
|
|
2022
|
+
const { allow, deny } = connection.manifest;
|
|
2023
|
+
if (allow.length === 0 && deny.length === 0) {
|
|
2024
|
+
return { allowed: true };
|
|
2025
|
+
}
|
|
2026
|
+
if (!command) {
|
|
2027
|
+
return {
|
|
2028
|
+
allowed: false,
|
|
2029
|
+
message: `Connection \`${connection.name}\` defines allow/deny rules, but this tool uses a script that Threadroot cannot policy-check. Use a shell \`run\` tool for connection-backed actions.`
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
const body = commandBody(command, connection.manifest.command);
|
|
2033
|
+
const full = normalize(command);
|
|
2034
|
+
const denied = deny.find((pattern) => includesPattern(body, pattern) || includesPattern(full, pattern));
|
|
2035
|
+
if (denied) {
|
|
2036
|
+
return {
|
|
2037
|
+
allowed: false,
|
|
2038
|
+
message: `Connection \`${connection.name}\` denies command fragment \`${denied}\`.`
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
if (allow.length > 0) {
|
|
2042
|
+
const allowed = allow.some((pattern) => includesPattern(body, pattern) || includesPattern(full, pattern));
|
|
2043
|
+
if (!allowed) {
|
|
2044
|
+
return {
|
|
2045
|
+
allowed: false,
|
|
2046
|
+
message: `Connection \`${connection.name}\` only allows: ${allow.map((pattern) => `\`${pattern}\``).join(", ")}.`
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
return { allowed: true };
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2000
2053
|
// src/core/tools/interpolate.ts
|
|
2001
2054
|
var ToolInputError = class extends Error {
|
|
2002
2055
|
constructor(message) {
|
|
@@ -2373,7 +2426,19 @@ async function runTool(repoRoot, options) {
|
|
|
2373
2426
|
const values = resolveInputs(tool.manifest, options.input);
|
|
2374
2427
|
const env = inputEnv(values);
|
|
2375
2428
|
const execOptions = { cwd: repoRoot, env, timeoutMs: options.timeoutMs, signal: options.signal };
|
|
2376
|
-
const
|
|
2429
|
+
const command = tool.manifest.run ? interpolateRun(tool.manifest.run, values) : void 0;
|
|
2430
|
+
if (connection) {
|
|
2431
|
+
const connectionDecision = authorizeConnectionCommand(connection, command);
|
|
2432
|
+
if (!connectionDecision.allowed) {
|
|
2433
|
+
return {
|
|
2434
|
+
status: "blocked",
|
|
2435
|
+
tool: tool.name,
|
|
2436
|
+
reason: "not-allowed",
|
|
2437
|
+
message: connectionDecision.message
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
const result = command ? await executeShell(command, execOptions) : await executeScript(repoRoot, tool.manifest.script, execOptions);
|
|
2377
2442
|
return { status: "ran", tool: tool.name, result };
|
|
2378
2443
|
}
|
|
2379
2444
|
async function checkToolHealth(repoRoot, tool) {
|
|
@@ -2399,6 +2464,11 @@ import { access, mkdtemp, readFile as readFile8, rm as rm2, writeFile as writeFi
|
|
|
2399
2464
|
import { constants, realpathSync } from "fs";
|
|
2400
2465
|
import { homedir as homedir2, tmpdir } from "os";
|
|
2401
2466
|
import path14 from "path";
|
|
2467
|
+
|
|
2468
|
+
// src/core/version.ts
|
|
2469
|
+
var THREADROOT_VERSION = "0.1.5";
|
|
2470
|
+
|
|
2471
|
+
// src/core/mcp-check.ts
|
|
2402
2472
|
var REQUIRED_MCP_TOOLS = [
|
|
2403
2473
|
"context",
|
|
2404
2474
|
"skills_list",
|
|
@@ -2419,14 +2489,19 @@ function codexConfigPath2(home = homedir2()) {
|
|
|
2419
2489
|
return path14.join(home, ".codex", "config.toml");
|
|
2420
2490
|
}
|
|
2421
2491
|
function mcpEntryForCurrentProcess() {
|
|
2422
|
-
|
|
2492
|
+
return mcpEntryForScriptPath(process.argv[1]);
|
|
2493
|
+
}
|
|
2494
|
+
function mcpEntryForScriptPath(rawScriptPath) {
|
|
2495
|
+
const scriptPath = currentScriptPath(rawScriptPath);
|
|
2496
|
+
if (scriptPath && isNpxPackagePath(scriptPath)) {
|
|
2497
|
+
return { command: "npx", args: ["--yes", `threadroot@${THREADROOT_VERSION}`, "mcp"] };
|
|
2498
|
+
}
|
|
2423
2499
|
if (scriptPath && path14.basename(scriptPath) === "index.js" && scriptPath.includes(`${path14.sep}dist${path14.sep}`)) {
|
|
2424
2500
|
return { command: process.execPath, args: [scriptPath, "mcp"] };
|
|
2425
2501
|
}
|
|
2426
2502
|
return { command: "threadroot", args: ["mcp"] };
|
|
2427
2503
|
}
|
|
2428
|
-
function currentScriptPath() {
|
|
2429
|
-
const scriptPath = process.argv[1];
|
|
2504
|
+
function currentScriptPath(scriptPath) {
|
|
2430
2505
|
if (!scriptPath) {
|
|
2431
2506
|
return void 0;
|
|
2432
2507
|
}
|
|
@@ -2436,6 +2511,10 @@ function currentScriptPath() {
|
|
|
2436
2511
|
return scriptPath;
|
|
2437
2512
|
}
|
|
2438
2513
|
}
|
|
2514
|
+
function isNpxPackagePath(scriptPath) {
|
|
2515
|
+
const normalized = scriptPath.split(path14.sep).join("/");
|
|
2516
|
+
return normalized.includes("/.npm/_npx/") && normalized.includes("/node_modules/threadroot/");
|
|
2517
|
+
}
|
|
2439
2518
|
async function readCodexThreadrootMcpEntry(home = homedir2()) {
|
|
2440
2519
|
let raw;
|
|
2441
2520
|
try {
|
|
@@ -2635,7 +2714,7 @@ function runMcpHandshake(entry, repoRoot, timeoutMs) {
|
|
|
2635
2714
|
params: {
|
|
2636
2715
|
protocolVersion: "2024-11-05",
|
|
2637
2716
|
capabilities: {},
|
|
2638
|
-
clientInfo: { name: "threadroot-check", version:
|
|
2717
|
+
clientInfo: { name: "threadroot-check", version: THREADROOT_VERSION }
|
|
2639
2718
|
}
|
|
2640
2719
|
})}
|
|
2641
2720
|
`
|
|
@@ -2658,7 +2737,7 @@ async function runOneShotMcpHandshake(entry, repoRoot, timeoutMs) {
|
|
|
2658
2737
|
params: {
|
|
2659
2738
|
protocolVersion: "2024-11-05",
|
|
2660
2739
|
capabilities: {},
|
|
2661
|
-
clientInfo: { name: "threadroot-check", version:
|
|
2740
|
+
clientInfo: { name: "threadroot-check", version: THREADROOT_VERSION }
|
|
2662
2741
|
}
|
|
2663
2742
|
}),
|
|
2664
2743
|
JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
|
|
@@ -3264,7 +3343,7 @@ async function readIfExists2(filePath) {
|
|
|
3264
3343
|
throw error;
|
|
3265
3344
|
}
|
|
3266
3345
|
}
|
|
3267
|
-
function
|
|
3346
|
+
function normalize2(text) {
|
|
3268
3347
|
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
3269
3348
|
}
|
|
3270
3349
|
function splitSections(markdown) {
|
|
@@ -3280,7 +3359,7 @@ function splitSections(markdown) {
|
|
|
3280
3359
|
for (const line of markdown.split(/\r?\n/)) {
|
|
3281
3360
|
if (/^#{1,6}\s/.test(line)) {
|
|
3282
3361
|
flush();
|
|
3283
|
-
heading =
|
|
3362
|
+
heading = normalize2(line.replace(/^#+\s*/, ""));
|
|
3284
3363
|
buffer = [line];
|
|
3285
3364
|
} else {
|
|
3286
3365
|
buffer.push(line);
|
|
@@ -3290,13 +3369,13 @@ function splitSections(markdown) {
|
|
|
3290
3369
|
return sections;
|
|
3291
3370
|
}
|
|
3292
3371
|
function novelSections(canonical, other) {
|
|
3293
|
-
const haystack =
|
|
3372
|
+
const haystack = normalize2(canonical);
|
|
3294
3373
|
const seenHeadings = new Set(splitSections(canonical).map((section) => section.heading).filter(Boolean));
|
|
3295
3374
|
return splitSections(other).filter((section) => {
|
|
3296
3375
|
if (section.heading && seenHeadings.has(section.heading)) {
|
|
3297
3376
|
return false;
|
|
3298
3377
|
}
|
|
3299
|
-
return !haystack.includes(
|
|
3378
|
+
return !haystack.includes(normalize2(section.text));
|
|
3300
3379
|
});
|
|
3301
3380
|
}
|
|
3302
3381
|
async function listCursorRules(repoRoot) {
|
|
@@ -3530,43 +3609,32 @@ async function initHarness(repoRoot, options = {}) {
|
|
|
3530
3609
|
return { name, profile, adapters, skills, tools: tools2, memory, rules, import: report, compiled: written, exposed };
|
|
3531
3610
|
}
|
|
3532
3611
|
|
|
3533
|
-
// src/core/
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
},
|
|
3560
|
-
drift
|
|
3561
|
-
};
|
|
3562
|
-
}
|
|
3563
|
-
|
|
3564
|
-
// src/core/bootstrap.ts
|
|
3565
|
-
var DEFAULT_TASK = "start this project";
|
|
3566
|
-
async function harnessExists(repoRoot) {
|
|
3567
|
-
return pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
|
|
3568
|
-
}
|
|
3569
|
-
async function pathExists2(target) {
|
|
3612
|
+
// src/core/packs/index.ts
|
|
3613
|
+
import { createHash as createHash2 } from "crypto";
|
|
3614
|
+
import { cp as cp2, lstat, mkdir as mkdir10, readFile as readFile11, readdir as readdir5, stat as stat9 } from "fs/promises";
|
|
3615
|
+
import path23 from "path";
|
|
3616
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3617
|
+
import { parse as parseYaml3 } from "yaml";
|
|
3618
|
+
import { z as z4 } from "zod";
|
|
3619
|
+
var packManifestSchema = z4.object({
|
|
3620
|
+
name: z4.string().min(1),
|
|
3621
|
+
version: z4.literal(1),
|
|
3622
|
+
description: z4.string().min(1),
|
|
3623
|
+
skills: z4.array(z4.string()).default([]),
|
|
3624
|
+
tools: z4.array(z4.string()).default([]),
|
|
3625
|
+
rules: z4.array(z4.string()).default([]),
|
|
3626
|
+
connections: z4.array(z4.string()).default([])
|
|
3627
|
+
});
|
|
3628
|
+
var DIST_DIR2 = path23.dirname(fileURLToPath2(import.meta.url));
|
|
3629
|
+
var PACKAGE_ROOT_FROM_BUNDLE2 = path23.resolve(DIST_DIR2, "..");
|
|
3630
|
+
var PACKAGE_ROOT_FROM_DIST2 = path23.resolve(DIST_DIR2, "../../..");
|
|
3631
|
+
var PACKAGE_ROOT_FROM_SRC2 = path23.resolve(DIST_DIR2, "../../../..");
|
|
3632
|
+
var PACK_CANDIDATES = [
|
|
3633
|
+
path23.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
|
|
3634
|
+
path23.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
|
|
3635
|
+
path23.join(PACKAGE_ROOT_FROM_SRC2, "packs")
|
|
3636
|
+
];
|
|
3637
|
+
async function exists5(target) {
|
|
3570
3638
|
try {
|
|
3571
3639
|
await stat9(target);
|
|
3572
3640
|
return true;
|
|
@@ -3577,146 +3645,521 @@ async function pathExists2(target) {
|
|
|
3577
3645
|
throw error;
|
|
3578
3646
|
}
|
|
3579
3647
|
}
|
|
3580
|
-
function
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
const task = options.task?.trim() || DEFAULT_TASK;
|
|
3585
|
-
const mode = modeFor(options);
|
|
3586
|
-
const write2 = mode === "write";
|
|
3587
|
-
const notes = [];
|
|
3588
|
-
const existed = await harnessExists(repoRoot);
|
|
3589
|
-
let setup;
|
|
3590
|
-
let init;
|
|
3591
|
-
let exposed;
|
|
3592
|
-
if (!options.noGlobal) {
|
|
3593
|
-
setup = await setupGlobal({
|
|
3594
|
-
agents: options.agents ?? "all",
|
|
3595
|
-
mode,
|
|
3596
|
-
home: options.home,
|
|
3597
|
-
mcp: options.mcp,
|
|
3598
|
-
mcpEntry: options.mcpEntry
|
|
3599
|
-
});
|
|
3600
|
-
} else {
|
|
3601
|
-
notes.push("Skipped global setup because --no-global was set.");
|
|
3602
|
-
}
|
|
3603
|
-
if (!existed && !options.noInit) {
|
|
3604
|
-
if (write2) {
|
|
3605
|
-
init = await initHarness(repoRoot, {
|
|
3606
|
-
import: options.import,
|
|
3607
|
-
profile: options.profile,
|
|
3608
|
-
home: options.home
|
|
3609
|
-
});
|
|
3610
|
-
} else {
|
|
3611
|
-
notes.push(`Would initialize local-only harness at ${path23.join(".threadroot", "harness.yaml")}.`);
|
|
3648
|
+
async function firstExisting(candidates) {
|
|
3649
|
+
for (const candidate of candidates) {
|
|
3650
|
+
if (await isPackRoot(candidate)) {
|
|
3651
|
+
return candidate;
|
|
3612
3652
|
}
|
|
3613
|
-
} else if (existed) {
|
|
3614
|
-
notes.push("Existing harness detected; bootstrap will not reinitialize it.");
|
|
3615
|
-
} else {
|
|
3616
|
-
notes.push("Skipped project initialization because --no-init was set.");
|
|
3617
|
-
}
|
|
3618
|
-
const hasHarnessAfterInit = existed || Boolean(init) || await pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
|
|
3619
|
-
if (options.expose) {
|
|
3620
|
-
exposed = await exposeProject(repoRoot, {
|
|
3621
|
-
agents: options.expose,
|
|
3622
|
-
mode
|
|
3623
|
-
});
|
|
3624
3653
|
}
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3654
|
+
return void 0;
|
|
3655
|
+
}
|
|
3656
|
+
async function bundledPacksDir() {
|
|
3657
|
+
return firstExisting(PACK_CANDIDATES);
|
|
3658
|
+
}
|
|
3659
|
+
async function isPackRoot(candidate) {
|
|
3660
|
+
let entries;
|
|
3661
|
+
try {
|
|
3662
|
+
entries = await readdir5(candidate, { withFileTypes: true });
|
|
3663
|
+
} catch {
|
|
3664
|
+
return false;
|
|
3631
3665
|
}
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
if (status.exists) {
|
|
3636
|
-
context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
|
|
3666
|
+
for (const entry of entries) {
|
|
3667
|
+
if (entry.isDirectory() && await exists5(path23.join(candidate, entry.name, "pack.yaml"))) {
|
|
3668
|
+
return true;
|
|
3637
3669
|
}
|
|
3638
|
-
} else {
|
|
3639
|
-
notes.push("Skipped doctor/status/context because no harness exists yet.");
|
|
3640
3670
|
}
|
|
3641
|
-
|
|
3642
|
-
|
|
3671
|
+
return false;
|
|
3672
|
+
}
|
|
3673
|
+
function safeRelative(ref) {
|
|
3674
|
+
const normalized = path23.normalize(ref);
|
|
3675
|
+
if (path23.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path23.sep}`)) {
|
|
3676
|
+
throw new Error(`Unsafe pack reference: ${ref}`);
|
|
3643
3677
|
}
|
|
3644
|
-
return
|
|
3645
|
-
mode: write2 ? "write" : "plan",
|
|
3646
|
-
task,
|
|
3647
|
-
harnessExisted: existed,
|
|
3648
|
-
setup,
|
|
3649
|
-
init,
|
|
3650
|
-
expose: exposed,
|
|
3651
|
-
status,
|
|
3652
|
-
doctor: doctorReport,
|
|
3653
|
-
context,
|
|
3654
|
-
mcpCheck,
|
|
3655
|
-
notes
|
|
3656
|
-
};
|
|
3678
|
+
return normalized;
|
|
3657
3679
|
}
|
|
3658
|
-
async function
|
|
3659
|
-
const
|
|
3660
|
-
const
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
task,
|
|
3665
|
-
status,
|
|
3666
|
-
notes: ["No harness found. Run `threadroot bootstrap --yes` first."]
|
|
3667
|
-
};
|
|
3680
|
+
async function readPackManifest(packDir) {
|
|
3681
|
+
const file = path23.join(packDir, "pack.yaml");
|
|
3682
|
+
const parsed = packManifestSchema.safeParse(parseYaml3(await readFile11(file, "utf8")));
|
|
3683
|
+
if (!parsed.success) {
|
|
3684
|
+
const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
|
|
3685
|
+
throw new Error(`Invalid pack manifest ${file}: ${detail}`);
|
|
3668
3686
|
}
|
|
3669
|
-
|
|
3670
|
-
const context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
|
|
3671
|
-
return { task, status, doctor: doctorReport, context, notes };
|
|
3687
|
+
return parsed.data;
|
|
3672
3688
|
}
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3689
|
+
async function packDirFor(repoRoot, nameOrPath) {
|
|
3690
|
+
if (path23.isAbsolute(nameOrPath)) {
|
|
3691
|
+
const root = path23.resolve(repoRoot);
|
|
3692
|
+
const target = path23.resolve(nameOrPath);
|
|
3693
|
+
const repoRelative = path23.relative(root, target);
|
|
3694
|
+
const bundled2 = await bundledPacksDir();
|
|
3695
|
+
const bundledRelative = bundled2 ? path23.relative(path23.resolve(bundled2), target) : void 0;
|
|
3696
|
+
const insideRepo = repoRelative !== "" && !repoRelative.startsWith("..") && !path23.isAbsolute(repoRelative);
|
|
3697
|
+
const insideBundled = bundledRelative !== void 0 && bundledRelative !== "" && !bundledRelative.startsWith("..") && !path23.isAbsolute(bundledRelative);
|
|
3698
|
+
if (!insideRepo && !insideBundled) {
|
|
3699
|
+
throw new Error(`Pack path must be repo-relative or a built-in pack name: ${nameOrPath}`);
|
|
3700
|
+
}
|
|
3701
|
+
return nameOrPath;
|
|
3678
3702
|
}
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
for (const finding3 of report.findings.slice(0, 8)) {
|
|
3682
|
-
const label = finding3.severity === "info" ? "hint" : finding3.severity;
|
|
3683
|
-
const suffix = finding3.path ? ` (${finding3.path})` : "";
|
|
3684
|
-
console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
|
|
3703
|
+
if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
|
|
3704
|
+
return toRepoPath(repoRoot, nameOrPath);
|
|
3685
3705
|
}
|
|
3686
|
-
|
|
3687
|
-
|
|
3706
|
+
const bundled = await bundledPacksDir();
|
|
3707
|
+
if (bundled) {
|
|
3708
|
+
return path23.join(bundled, nameOrPath);
|
|
3688
3709
|
}
|
|
3710
|
+
return toRepoPath(repoRoot, path23.join("packs", nameOrPath));
|
|
3689
3711
|
}
|
|
3690
|
-
function
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3712
|
+
async function directFiles(dir, ext) {
|
|
3713
|
+
try {
|
|
3714
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
3715
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path23.join(dir, entry.name)).sort();
|
|
3716
|
+
} catch (error) {
|
|
3717
|
+
if (error.code === "ENOENT") {
|
|
3718
|
+
return [];
|
|
3719
|
+
}
|
|
3720
|
+
throw error;
|
|
3697
3721
|
}
|
|
3698
|
-
console.log(`harness: ${status.manifest.name} (${status.manifest.profile})`);
|
|
3699
|
-
console.log(`adapters: ${status.manifest.adapters.length > 0 ? status.manifest.adapters.join(", ") : "none (local-only)"}`);
|
|
3700
|
-
console.log(
|
|
3701
|
-
`objects: ${status.counts.skills} skills, ${status.counts.rules} rules, ${status.counts.tools} tools, ${status.counts.memory} memory`
|
|
3702
|
-
);
|
|
3703
3722
|
}
|
|
3704
|
-
function
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3723
|
+
async function skillEntries(dir) {
|
|
3724
|
+
try {
|
|
3725
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
3726
|
+
const result = [];
|
|
3727
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
3728
|
+
const full = path23.join(dir, entry.name);
|
|
3729
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
3730
|
+
result.push(full);
|
|
3731
|
+
}
|
|
3732
|
+
if (entry.isDirectory() && await exists5(path23.join(full, "SKILL.md"))) {
|
|
3733
|
+
result.push(full);
|
|
3734
|
+
}
|
|
3714
3735
|
}
|
|
3715
|
-
|
|
3716
|
-
|
|
3736
|
+
return result;
|
|
3737
|
+
} catch (error) {
|
|
3738
|
+
if (error.code === "ENOENT") {
|
|
3739
|
+
return [];
|
|
3740
|
+
}
|
|
3741
|
+
throw error;
|
|
3717
3742
|
}
|
|
3718
|
-
|
|
3719
|
-
|
|
3743
|
+
}
|
|
3744
|
+
async function collectObjects(packDir, manifest) {
|
|
3745
|
+
async function resolveRef(ref) {
|
|
3746
|
+
const safe = safeRelative(ref);
|
|
3747
|
+
const local = path23.resolve(packDir, safe);
|
|
3748
|
+
if (await exists5(local)) {
|
|
3749
|
+
return local;
|
|
3750
|
+
}
|
|
3751
|
+
return path23.resolve(packDir, "..", "..", safe);
|
|
3752
|
+
}
|
|
3753
|
+
return {
|
|
3754
|
+
skills: [
|
|
3755
|
+
...await Promise.all(manifest.skills.map(resolveRef)),
|
|
3756
|
+
...await skillEntries(path23.join(packDir, "skills"))
|
|
3757
|
+
],
|
|
3758
|
+
tools: [
|
|
3759
|
+
...await Promise.all(manifest.tools.map(resolveRef)),
|
|
3760
|
+
...await directFiles(path23.join(packDir, "tools"), ".yaml")
|
|
3761
|
+
],
|
|
3762
|
+
rules: [
|
|
3763
|
+
...await Promise.all(manifest.rules.map(resolveRef)),
|
|
3764
|
+
...await directFiles(path23.join(packDir, "rules"), ".md")
|
|
3765
|
+
],
|
|
3766
|
+
connections: [
|
|
3767
|
+
...await Promise.all(manifest.connections.map(resolveRef)),
|
|
3768
|
+
...await directFiles(path23.join(packDir, "connections"), ".yaml")
|
|
3769
|
+
]
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
function baseName(source) {
|
|
3773
|
+
const parsed = path23.basename(source) === "SKILL.md" ? path23.dirname(source) : source;
|
|
3774
|
+
return path23.basename(parsed, path23.extname(parsed));
|
|
3775
|
+
}
|
|
3776
|
+
async function listPacks(repoRoot) {
|
|
3777
|
+
const dirs = [toRepoPath(repoRoot, "packs"), await bundledPacksDir()].filter((dir) => Boolean(dir));
|
|
3778
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3779
|
+
const packs = [];
|
|
3780
|
+
for (const root of dirs) {
|
|
3781
|
+
let entries;
|
|
3782
|
+
try {
|
|
3783
|
+
entries = await readdir5(root, { withFileTypes: true });
|
|
3784
|
+
} catch {
|
|
3785
|
+
continue;
|
|
3786
|
+
}
|
|
3787
|
+
for (const entry of entries) {
|
|
3788
|
+
if (!entry.isDirectory() || seen.has(entry.name)) {
|
|
3789
|
+
continue;
|
|
3790
|
+
}
|
|
3791
|
+
const packDir = path23.join(root, entry.name);
|
|
3792
|
+
if (!await exists5(path23.join(packDir, "pack.yaml"))) {
|
|
3793
|
+
continue;
|
|
3794
|
+
}
|
|
3795
|
+
seen.add(entry.name);
|
|
3796
|
+
packs.push(await inspectPack(repoRoot, packDir));
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
return packs.sort((a, b) => a.name.localeCompare(b.name));
|
|
3800
|
+
}
|
|
3801
|
+
async function inspectPack(repoRoot, nameOrPath) {
|
|
3802
|
+
const packDir = await packDirFor(repoRoot, nameOrPath);
|
|
3803
|
+
const manifest = await readPackManifest(packDir);
|
|
3804
|
+
const objects = await collectObjects(packDir, manifest);
|
|
3805
|
+
return {
|
|
3806
|
+
name: manifest.name,
|
|
3807
|
+
description: manifest.description,
|
|
3808
|
+
path: packDir,
|
|
3809
|
+
skills: objects.skills.map(baseName),
|
|
3810
|
+
tools: objects.tools.map(baseName),
|
|
3811
|
+
rules: objects.rules.map(baseName),
|
|
3812
|
+
connections: objects.connections.map(baseName)
|
|
3813
|
+
};
|
|
3814
|
+
}
|
|
3815
|
+
async function validateProse(file, kind) {
|
|
3816
|
+
const target = path23.basename(file) === "SKILL.md" ? file : file;
|
|
3817
|
+
const content = await readFile11(target, "utf8");
|
|
3818
|
+
const parsed = parseFrontmatter(content);
|
|
3819
|
+
const schema = kind === "skill" ? skillFrontmatterSchema : ruleFrontmatterSchema;
|
|
3820
|
+
schema.parse(parsed.data);
|
|
3821
|
+
}
|
|
3822
|
+
async function validateYaml(file, kind) {
|
|
3823
|
+
const content = await readFile11(file, "utf8");
|
|
3824
|
+
const schema = kind === "tool" ? toolManifestSchema : connectionManifestSchema;
|
|
3825
|
+
schema.parse(parseYaml3(content));
|
|
3826
|
+
}
|
|
3827
|
+
async function validatePack(repoRoot, nameOrPath) {
|
|
3828
|
+
const findings = [];
|
|
3829
|
+
try {
|
|
3830
|
+
const packDir = await packDirFor(repoRoot, nameOrPath);
|
|
3831
|
+
const manifest = await readPackManifest(packDir);
|
|
3832
|
+
const objects = await collectObjects(packDir, manifest);
|
|
3833
|
+
for (const skill of objects.skills) {
|
|
3834
|
+
await validateProse(path23.basename(skill) === "SKILL.md" ? skill : path23.join(skill, "SKILL.md"), "skill");
|
|
3835
|
+
}
|
|
3836
|
+
for (const rule of objects.rules) await validateProse(rule, "rule");
|
|
3837
|
+
for (const tool of objects.tools) await validateYaml(tool, "tool");
|
|
3838
|
+
for (const connection of objects.connections) await validateYaml(connection, "connection");
|
|
3839
|
+
if (Object.values(objects).every((items) => items.length === 0)) {
|
|
3840
|
+
findings.push({ severity: "warning", message: "Pack does not include any objects." });
|
|
3841
|
+
}
|
|
3842
|
+
} catch (error) {
|
|
3843
|
+
findings.push({ severity: "error", message: error instanceof Error ? error.message : String(error) });
|
|
3844
|
+
}
|
|
3845
|
+
return { ok: !findings.some((finding3) => finding3.severity === "error"), findings };
|
|
3846
|
+
}
|
|
3847
|
+
async function copyObject(source, destDir) {
|
|
3848
|
+
if ((await lstat(source)).isSymbolicLink()) {
|
|
3849
|
+
throw new Error(`Refusing to install pack object symlink: ${source}`);
|
|
3850
|
+
}
|
|
3851
|
+
const info = await stat9(source);
|
|
3852
|
+
const name = baseName(source);
|
|
3853
|
+
const dest = info.isDirectory() ? path23.join(destDir, name) : path23.join(destDir, path23.basename(source));
|
|
3854
|
+
await mkdir10(destDir, { recursive: true });
|
|
3855
|
+
await cp2(source, dest, { recursive: true, force: true });
|
|
3856
|
+
return dest;
|
|
3857
|
+
}
|
|
3858
|
+
async function hashFile(filePath) {
|
|
3859
|
+
return createHash2("sha256").update(await readFile11(filePath)).digest("hex");
|
|
3860
|
+
}
|
|
3861
|
+
async function hashDirectory(root) {
|
|
3862
|
+
const hash = createHash2("sha256");
|
|
3863
|
+
hash.update("threadroot-pack-directory-v1\n");
|
|
3864
|
+
async function walk(dir) {
|
|
3865
|
+
const entries = (await readdir5(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
3866
|
+
for (const entry of entries) {
|
|
3867
|
+
const full = path23.join(dir, entry.name);
|
|
3868
|
+
const rel = path23.relative(root, full).split(path23.sep).join("/");
|
|
3869
|
+
if (entry.isSymbolicLink()) {
|
|
3870
|
+
throw new Error(`Refusing to install pack object with symlink: ${rel}`);
|
|
3871
|
+
}
|
|
3872
|
+
if (entry.isDirectory()) {
|
|
3873
|
+
await walk(full);
|
|
3874
|
+
continue;
|
|
3875
|
+
}
|
|
3876
|
+
if (entry.isFile()) {
|
|
3877
|
+
hash.update(`file:${rel}
|
|
3878
|
+
`);
|
|
3879
|
+
hash.update(await readFile11(full));
|
|
3880
|
+
hash.update("\n");
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
await walk(root);
|
|
3885
|
+
return hash.digest("hex");
|
|
3886
|
+
}
|
|
3887
|
+
async function integrityFor(source) {
|
|
3888
|
+
if ((await lstat(source)).isSymbolicLink()) {
|
|
3889
|
+
throw new Error(`Refusing to hash pack object symlink: ${source}`);
|
|
3890
|
+
}
|
|
3891
|
+
const info = await stat9(source);
|
|
3892
|
+
const digest = info.isDirectory() ? await hashDirectory(source) : await hashFile(source);
|
|
3893
|
+
return `sha256:${digest}`;
|
|
3894
|
+
}
|
|
3895
|
+
function normalizeLockPath(value) {
|
|
3896
|
+
return value.split(path23.sep).join("/");
|
|
3897
|
+
}
|
|
3898
|
+
function inside(root, target) {
|
|
3899
|
+
const relative = path23.relative(path23.resolve(root), path23.resolve(target));
|
|
3900
|
+
if (relative === "" || relative.startsWith("..") || path23.isAbsolute(relative)) {
|
|
3901
|
+
return void 0;
|
|
3902
|
+
}
|
|
3903
|
+
return normalizeLockPath(relative);
|
|
3904
|
+
}
|
|
3905
|
+
function lockObjectPath(packDir, source) {
|
|
3906
|
+
return inside(packDir, source) ?? inside(path23.resolve(packDir, "..", ".."), source) ?? path23.basename(source);
|
|
3907
|
+
}
|
|
3908
|
+
async function lockEntryForPackObject(packDir, manifest, kind, source, installedAt) {
|
|
3909
|
+
return {
|
|
3910
|
+
name: baseName(source),
|
|
3911
|
+
kind,
|
|
3912
|
+
sourceKind: "local",
|
|
3913
|
+
source: `pack:${manifest.name}`,
|
|
3914
|
+
objectPath: lockObjectPath(packDir, source),
|
|
3915
|
+
integrity: await integrityFor(source),
|
|
3916
|
+
installedAt
|
|
3917
|
+
};
|
|
3918
|
+
}
|
|
3919
|
+
async function writePackLockEntries(repoRoot, packDir, manifest, objects) {
|
|
3920
|
+
const installedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3921
|
+
const entries = await Promise.all([
|
|
3922
|
+
...objects.skills.map((source) => lockEntryForPackObject(packDir, manifest, "skill", source, installedAt)),
|
|
3923
|
+
...objects.tools.map((source) => lockEntryForPackObject(packDir, manifest, "tool", source, installedAt)),
|
|
3924
|
+
...objects.rules.map((source) => lockEntryForPackObject(packDir, manifest, "rule", source, installedAt)),
|
|
3925
|
+
...objects.connections.map(
|
|
3926
|
+
(source) => lockEntryForPackObject(packDir, manifest, "connection", source, installedAt)
|
|
3927
|
+
)
|
|
3928
|
+
]);
|
|
3929
|
+
let lock = await readLockFile(projectLockPath(repoRoot));
|
|
3930
|
+
for (const entry of entries) {
|
|
3931
|
+
lock = upsertLockEntry(lock, entry);
|
|
3932
|
+
}
|
|
3933
|
+
await writeLockFile(projectLockPath(repoRoot), lock);
|
|
3934
|
+
}
|
|
3935
|
+
async function installPack(repoRoot, nameOrPath) {
|
|
3936
|
+
const validation = await validatePack(repoRoot, nameOrPath);
|
|
3937
|
+
if (!validation.ok) {
|
|
3938
|
+
throw new Error(validation.findings.map((finding3) => finding3.message).join("; "));
|
|
3939
|
+
}
|
|
3940
|
+
const packDir = await packDirFor(repoRoot, nameOrPath);
|
|
3941
|
+
const manifest = await readPackManifest(packDir);
|
|
3942
|
+
const objects = await collectObjects(packDir, manifest);
|
|
3943
|
+
await Promise.all([
|
|
3944
|
+
...objects.skills.map((source) => copyObject(source, projectObjectDir(repoRoot, "skills"))),
|
|
3945
|
+
...objects.tools.map((source) => copyObject(source, projectObjectDir(repoRoot, "tools"))),
|
|
3946
|
+
...objects.rules.map((source) => copyObject(source, projectObjectDir(repoRoot, "rules"))),
|
|
3947
|
+
...objects.connections.map((source) => copyObject(source, projectObjectDir(repoRoot, "connections")))
|
|
3948
|
+
]);
|
|
3949
|
+
await writePackLockEntries(repoRoot, packDir, manifest, objects);
|
|
3950
|
+
return inspectPack(repoRoot, packDir);
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
// src/core/status.ts
|
|
3954
|
+
async function harnessStatus(repoRoot, options = {}) {
|
|
3955
|
+
let harness;
|
|
3956
|
+
try {
|
|
3957
|
+
harness = await resolveHarness(repoRoot, { home: options.home });
|
|
3958
|
+
} catch (error) {
|
|
3959
|
+
if (error instanceof HarnessError) {
|
|
3960
|
+
return { exists: false };
|
|
3961
|
+
}
|
|
3962
|
+
throw error;
|
|
3963
|
+
}
|
|
3964
|
+
const files = await compile(repoRoot, harness);
|
|
3965
|
+
const drift = await detectDrift(repoRoot, files);
|
|
3966
|
+
return {
|
|
3967
|
+
exists: true,
|
|
3968
|
+
manifest: {
|
|
3969
|
+
name: harness.manifest.name,
|
|
3970
|
+
profile: harness.manifest.profile,
|
|
3971
|
+
adapters: harness.manifest.adapters,
|
|
3972
|
+
toolsAllow: harness.manifest.tools.allow
|
|
3973
|
+
},
|
|
3974
|
+
counts: {
|
|
3975
|
+
skills: harness.skills.length,
|
|
3976
|
+
rules: harness.rules.length,
|
|
3977
|
+
tools: harness.tools.length,
|
|
3978
|
+
memory: harness.memory.length
|
|
3979
|
+
},
|
|
3980
|
+
drift
|
|
3981
|
+
};
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
// src/core/bootstrap.ts
|
|
3985
|
+
var DEFAULT_TASK = "start this project";
|
|
3986
|
+
async function harnessExists(repoRoot) {
|
|
3987
|
+
return pathExists2(path24.join(repoRoot, ".threadroot", "harness.yaml"));
|
|
3988
|
+
}
|
|
3989
|
+
async function pathExists2(target) {
|
|
3990
|
+
try {
|
|
3991
|
+
await stat10(target);
|
|
3992
|
+
return true;
|
|
3993
|
+
} catch (error) {
|
|
3994
|
+
if (error.code === "ENOENT") {
|
|
3995
|
+
return false;
|
|
3996
|
+
}
|
|
3997
|
+
throw error;
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
function modeFor(options) {
|
|
4001
|
+
return options.yes && !options.dryRun ? "write" : "dry-run";
|
|
4002
|
+
}
|
|
4003
|
+
function parseListOption(value) {
|
|
4004
|
+
return (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
4005
|
+
}
|
|
4006
|
+
async function bootstrapProject(repoRoot, options = {}) {
|
|
4007
|
+
const task = options.task?.trim() || DEFAULT_TASK;
|
|
4008
|
+
const mode = modeFor(options);
|
|
4009
|
+
const write2 = mode === "write";
|
|
4010
|
+
const notes = [];
|
|
4011
|
+
const existed = await harnessExists(repoRoot);
|
|
4012
|
+
let setup;
|
|
4013
|
+
let init;
|
|
4014
|
+
let exposed;
|
|
4015
|
+
let packs;
|
|
4016
|
+
if (!options.noGlobal) {
|
|
4017
|
+
setup = await setupGlobal({
|
|
4018
|
+
agents: options.agents ?? "all",
|
|
4019
|
+
mode,
|
|
4020
|
+
home: options.home,
|
|
4021
|
+
mcp: options.mcp,
|
|
4022
|
+
mcpEntry: options.mcpEntry
|
|
4023
|
+
});
|
|
4024
|
+
} else {
|
|
4025
|
+
notes.push("Skipped global setup because --no-global was set.");
|
|
4026
|
+
}
|
|
4027
|
+
if (!existed && !options.noInit) {
|
|
4028
|
+
if (write2) {
|
|
4029
|
+
init = await initHarness(repoRoot, {
|
|
4030
|
+
import: options.import,
|
|
4031
|
+
profile: options.profile,
|
|
4032
|
+
home: options.home
|
|
4033
|
+
});
|
|
4034
|
+
} else {
|
|
4035
|
+
notes.push(`Would initialize local-only harness at ${path24.join(".threadroot", "harness.yaml")}.`);
|
|
4036
|
+
}
|
|
4037
|
+
} else if (existed) {
|
|
4038
|
+
notes.push("Existing harness detected; bootstrap will not reinitialize it.");
|
|
4039
|
+
} else {
|
|
4040
|
+
notes.push("Skipped project initialization because --no-init was set.");
|
|
4041
|
+
}
|
|
4042
|
+
const hasHarnessAfterInit = existed || Boolean(init) || await pathExists2(path24.join(repoRoot, ".threadroot", "harness.yaml"));
|
|
4043
|
+
if (options.expose) {
|
|
4044
|
+
exposed = await exposeProject(repoRoot, {
|
|
4045
|
+
agents: options.expose,
|
|
4046
|
+
mode
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
const packNames = parseListOption(options.packs);
|
|
4050
|
+
if (packNames.length > 0) {
|
|
4051
|
+
if (!hasHarnessAfterInit) {
|
|
4052
|
+
notes.push("Skipped pack installation because no harness exists yet.");
|
|
4053
|
+
} else if (write2) {
|
|
4054
|
+
packs = [];
|
|
4055
|
+
for (const packName of packNames) {
|
|
4056
|
+
packs.push(await installPack(repoRoot, packName));
|
|
4057
|
+
}
|
|
4058
|
+
} else {
|
|
4059
|
+
notes.push(`Would install pack(s): ${packNames.join(", ")}.`);
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
let status;
|
|
4063
|
+
let doctorReport;
|
|
4064
|
+
let context;
|
|
4065
|
+
let mcpCheck;
|
|
4066
|
+
if (options.mcp && write2) {
|
|
4067
|
+
mcpCheck = await checkCodexMcp({ repoRoot, home: options.home });
|
|
4068
|
+
}
|
|
4069
|
+
if (hasHarnessAfterInit) {
|
|
4070
|
+
status = await harnessStatus(repoRoot, { home: options.home });
|
|
4071
|
+
doctorReport = await doctor(repoRoot, { home: options.home });
|
|
4072
|
+
if (status.exists) {
|
|
4073
|
+
context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
|
|
4074
|
+
}
|
|
4075
|
+
} else {
|
|
4076
|
+
notes.push("Skipped doctor/status/context because no harness exists yet.");
|
|
4077
|
+
}
|
|
4078
|
+
if (!write2) {
|
|
4079
|
+
notes.push("Run `threadroot bootstrap --yes` to apply this plan.");
|
|
4080
|
+
}
|
|
4081
|
+
return {
|
|
4082
|
+
mode: write2 ? "write" : "plan",
|
|
4083
|
+
task,
|
|
4084
|
+
harnessExisted: existed,
|
|
4085
|
+
setup,
|
|
4086
|
+
init,
|
|
4087
|
+
expose: exposed,
|
|
4088
|
+
packs,
|
|
4089
|
+
status,
|
|
4090
|
+
doctor: doctorReport,
|
|
4091
|
+
context,
|
|
4092
|
+
mcpCheck,
|
|
4093
|
+
notes
|
|
4094
|
+
};
|
|
4095
|
+
}
|
|
4096
|
+
async function startSession(repoRoot, options = {}) {
|
|
4097
|
+
const task = options.task?.trim() || DEFAULT_TASK;
|
|
4098
|
+
const status = await harnessStatus(repoRoot, { home: options.home });
|
|
4099
|
+
const notes = [];
|
|
4100
|
+
if (!status.exists) {
|
|
4101
|
+
return {
|
|
4102
|
+
task,
|
|
4103
|
+
status,
|
|
4104
|
+
notes: ["No harness found. Run `threadroot bootstrap --yes` first."]
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
const doctorReport = await doctor(repoRoot, { home: options.home });
|
|
4108
|
+
const context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
|
|
4109
|
+
return { task, status, doctor: doctorReport, context, notes };
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
// src/commands/json.ts
|
|
4113
|
+
function printJson(value) {
|
|
4114
|
+
console.log(JSON.stringify(value, null, 2));
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
// src/commands/session-output.ts
|
|
4118
|
+
function printDoctor(report) {
|
|
4119
|
+
if (!report) {
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
4122
|
+
const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
|
|
4123
|
+
console.log(actionable.length === 0 ? "doctor: clean" : `doctor: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`);
|
|
4124
|
+
for (const finding3 of report.findings.slice(0, 8)) {
|
|
4125
|
+
const label = finding3.severity === "info" ? "hint" : finding3.severity;
|
|
4126
|
+
const suffix = finding3.path ? ` (${finding3.path})` : "";
|
|
4127
|
+
console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
|
|
4128
|
+
}
|
|
4129
|
+
if (report.findings.length > 8) {
|
|
4130
|
+
console.log(`- ... ${report.findings.length - 8} more finding(s)`);
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
function printStatus(status) {
|
|
4134
|
+
if (!status) {
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
if (!status.exists) {
|
|
4138
|
+
console.log("harness: missing");
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4141
|
+
console.log(`harness: ${status.manifest.name} (${status.manifest.profile})`);
|
|
4142
|
+
console.log(`adapters: ${status.manifest.adapters.length > 0 ? status.manifest.adapters.join(", ") : "none (local-only)"}`);
|
|
4143
|
+
console.log(
|
|
4144
|
+
`objects: ${status.counts.skills} skills, ${status.counts.rules} rules, ${status.counts.tools} tools, ${status.counts.memory} memory`
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
function printContext(context) {
|
|
4148
|
+
if (!context) {
|
|
4149
|
+
return;
|
|
4150
|
+
}
|
|
4151
|
+
console.log(`task: ${context.task}`);
|
|
4152
|
+
if (context.skills.length > 0) {
|
|
4153
|
+
const skillLabel = context.skills.some((skill) => skill.score > 0) ? "relevant skills:" : "starter skills:";
|
|
4154
|
+
console.log(skillLabel);
|
|
4155
|
+
for (const skill of context.skills.slice(0, 8)) {
|
|
4156
|
+
console.log(`- ${skill.name} - ${skill.when}`);
|
|
4157
|
+
}
|
|
4158
|
+
} else {
|
|
4159
|
+
console.log("relevant skills: none matched; run `threadroot skills list` to inspect all skills.");
|
|
4160
|
+
}
|
|
4161
|
+
if (context.tools.length > 0) {
|
|
4162
|
+
console.log("available tools:");
|
|
3720
4163
|
for (const tool of context.tools.slice(0, 8)) {
|
|
3721
4164
|
console.log(`- ${tool.name} (${tool.risk}) - ${tool.description}`);
|
|
3722
4165
|
}
|
|
@@ -3767,6 +4210,9 @@ function printBootstrapReport(report) {
|
|
|
3767
4210
|
console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
|
|
3768
4211
|
}
|
|
3769
4212
|
}
|
|
4213
|
+
if (report.packs && report.packs.length > 0) {
|
|
4214
|
+
console.log(`packs: ${report.packs.map((pack) => pack.name).join(", ")}`);
|
|
4215
|
+
}
|
|
3770
4216
|
printStatus(report.status);
|
|
3771
4217
|
printMcpCheck(report.mcpCheck);
|
|
3772
4218
|
printDoctor(report.doctor);
|
|
@@ -3799,13 +4245,18 @@ async function runBootstrap(repoRoot, options) {
|
|
|
3799
4245
|
task: options.task,
|
|
3800
4246
|
mcp: options.mcp,
|
|
3801
4247
|
expose: options.expose,
|
|
4248
|
+
packs: options.packs,
|
|
3802
4249
|
noGlobal: options.global === false,
|
|
3803
4250
|
noInit: options.init === false,
|
|
3804
4251
|
import: options.import,
|
|
3805
4252
|
profile: options.profile ? profileIdSchema.parse(options.profile) : void 0,
|
|
3806
4253
|
mcpEntry: options.mcp ? mcpEntryForCurrentProcess() : void 0
|
|
3807
4254
|
});
|
|
3808
|
-
|
|
4255
|
+
if (options.json) {
|
|
4256
|
+
printJson(report);
|
|
4257
|
+
} else {
|
|
4258
|
+
printBootstrapReport(report);
|
|
4259
|
+
}
|
|
3809
4260
|
if (report.mode === "write" && report.doctor && !report.doctor.ok) {
|
|
3810
4261
|
process.exitCode = 1;
|
|
3811
4262
|
}
|
|
@@ -3832,17 +4283,25 @@ async function runCompileCommand(repoRoot, options) {
|
|
|
3832
4283
|
}
|
|
3833
4284
|
|
|
3834
4285
|
// src/commands/context.ts
|
|
3835
|
-
async function runContext(repoRoot, task) {
|
|
4286
|
+
async function runContext(repoRoot, task, options = {}) {
|
|
3836
4287
|
let context;
|
|
3837
4288
|
try {
|
|
3838
4289
|
context = await assembleContext(repoRoot, task);
|
|
3839
4290
|
} catch (error) {
|
|
3840
4291
|
if (error instanceof HarnessError) {
|
|
3841
|
-
|
|
4292
|
+
if (options.json) {
|
|
4293
|
+
printJson({ ok: false, error: "harness_missing", message: "No harness found. Run `tr init` first." });
|
|
4294
|
+
} else {
|
|
4295
|
+
console.log("No harness found. Run `tr init` first.");
|
|
4296
|
+
}
|
|
3842
4297
|
return;
|
|
3843
4298
|
}
|
|
3844
4299
|
throw error;
|
|
3845
4300
|
}
|
|
4301
|
+
if (options.json) {
|
|
4302
|
+
printJson(context);
|
|
4303
|
+
return;
|
|
4304
|
+
}
|
|
3846
4305
|
console.log(`task: ${context.task}`);
|
|
3847
4306
|
if (context.skills.length > 0) {
|
|
3848
4307
|
console.log("\nskills:");
|
|
@@ -3875,7 +4334,7 @@ async function runContext(repoRoot, task) {
|
|
|
3875
4334
|
|
|
3876
4335
|
// src/commands/diff.ts
|
|
3877
4336
|
import fs3 from "fs/promises";
|
|
3878
|
-
import
|
|
4337
|
+
import path25 from "path";
|
|
3879
4338
|
async function readIfExists3(filePath) {
|
|
3880
4339
|
try {
|
|
3881
4340
|
return await fs3.readFile(filePath, "utf8");
|
|
@@ -3934,7 +4393,7 @@ async function runDiff(repoRoot) {
|
|
|
3934
4393
|
const files = await compile(repoRoot, harness);
|
|
3935
4394
|
let changed = 0;
|
|
3936
4395
|
for (const file of files) {
|
|
3937
|
-
const existing = await readIfExists3(
|
|
4396
|
+
const existing = await readIfExists3(path25.join(repoRoot, file.path));
|
|
3938
4397
|
if (existing === void 0) {
|
|
3939
4398
|
changed += 1;
|
|
3940
4399
|
console.log(`+ ${file.path} (new)`);
|
|
@@ -3955,8 +4414,15 @@ async function runDiff(repoRoot) {
|
|
|
3955
4414
|
}
|
|
3956
4415
|
|
|
3957
4416
|
// src/commands/doctor.ts
|
|
3958
|
-
async function runDoctor(repoRoot) {
|
|
4417
|
+
async function runDoctor(repoRoot, options = {}) {
|
|
3959
4418
|
const report = await doctor(repoRoot);
|
|
4419
|
+
if (options.json) {
|
|
4420
|
+
printJson(report);
|
|
4421
|
+
if (!report.ok) {
|
|
4422
|
+
process.exitCode = 1;
|
|
4423
|
+
}
|
|
4424
|
+
return;
|
|
4425
|
+
}
|
|
3960
4426
|
const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
|
|
3961
4427
|
const hints = report.findings.filter((finding3) => finding3.severity === "info");
|
|
3962
4428
|
if (actionable.length === 0) {
|
|
@@ -4049,13 +4515,13 @@ async function runInit(repoRoot, options) {
|
|
|
4049
4515
|
}
|
|
4050
4516
|
|
|
4051
4517
|
// src/commands/install.ts
|
|
4052
|
-
import
|
|
4518
|
+
import path28 from "path";
|
|
4053
4519
|
|
|
4054
4520
|
// src/core/install/fetch.ts
|
|
4055
4521
|
import { execFile } from "child_process";
|
|
4056
4522
|
import { mkdtemp as mkdtemp2, rm as rm4 } from "fs/promises";
|
|
4057
4523
|
import os2 from "os";
|
|
4058
|
-
import
|
|
4524
|
+
import path26 from "path";
|
|
4059
4525
|
import { promisify } from "util";
|
|
4060
4526
|
var run = promisify(execFile);
|
|
4061
4527
|
function cloneUrl(ref) {
|
|
@@ -4076,7 +4542,7 @@ async function git(cwd, args) {
|
|
|
4076
4542
|
}
|
|
4077
4543
|
async function fetchGitSource(ref) {
|
|
4078
4544
|
const url = cloneUrl(ref);
|
|
4079
|
-
const dir = await mkdtemp2(
|
|
4545
|
+
const dir = await mkdtemp2(path26.join(os2.tmpdir(), "threadroot-fetch-"));
|
|
4080
4546
|
const cleanup = () => rm4(dir, { recursive: true, force: true });
|
|
4081
4547
|
try {
|
|
4082
4548
|
if (ref.ref) {
|
|
@@ -4098,9 +4564,9 @@ async function fetchGitSource(ref) {
|
|
|
4098
4564
|
}
|
|
4099
4565
|
|
|
4100
4566
|
// src/core/install/install.ts
|
|
4101
|
-
import { cp as
|
|
4102
|
-
import { createHash as
|
|
4103
|
-
import
|
|
4567
|
+
import { cp as cp3, mkdir as mkdir11, readFile as readFile12, readdir as readdir6, stat as stat11, writeFile as writeFile10 } from "fs/promises";
|
|
4568
|
+
import { createHash as createHash3 } from "crypto";
|
|
4569
|
+
import path27 from "path";
|
|
4104
4570
|
var NAME_RE5 = /^[a-z0-9][a-z0-9-]*$/;
|
|
4105
4571
|
var KIND_DIR = {
|
|
4106
4572
|
skill: "skills",
|
|
@@ -4112,8 +4578,8 @@ function objectExt(kind) {
|
|
|
4112
4578
|
return kind === "tool" || kind === "connection" ? ".yaml" : ".md";
|
|
4113
4579
|
}
|
|
4114
4580
|
function safeRepoPath(objectPath) {
|
|
4115
|
-
const normalized =
|
|
4116
|
-
if (
|
|
4581
|
+
const normalized = path27.normalize(objectPath);
|
|
4582
|
+
if (path27.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path27.sep}`)) {
|
|
4117
4583
|
throw new Error(`Unsafe object path: ${objectPath}`);
|
|
4118
4584
|
}
|
|
4119
4585
|
return normalized;
|
|
@@ -4127,7 +4593,7 @@ function inferKind(objectPath, override) {
|
|
|
4127
4593
|
if (segments.includes("tools")) return "tool";
|
|
4128
4594
|
if (segments.includes("connections")) return "connection";
|
|
4129
4595
|
if (segments.includes("rules")) return "rule";
|
|
4130
|
-
const ext =
|
|
4596
|
+
const ext = path27.extname(objectPath).toLowerCase();
|
|
4131
4597
|
if (ext === ".yaml" || ext === ".yml") return "tool";
|
|
4132
4598
|
if (ext === ".md") return "skill";
|
|
4133
4599
|
throw new Error(
|
|
@@ -4135,20 +4601,20 @@ function inferKind(objectPath, override) {
|
|
|
4135
4601
|
);
|
|
4136
4602
|
}
|
|
4137
4603
|
function deriveName(objectPath) {
|
|
4138
|
-
const base =
|
|
4604
|
+
const base = path27.basename(objectPath, path27.extname(objectPath));
|
|
4139
4605
|
if (!NAME_RE5.test(base)) {
|
|
4140
4606
|
throw new Error(`Invalid object name \`${base}\` (use lowercase letters, digits, and dashes).`);
|
|
4141
4607
|
}
|
|
4142
4608
|
return base;
|
|
4143
4609
|
}
|
|
4144
|
-
async function
|
|
4145
|
-
const hash =
|
|
4610
|
+
async function hashDirectory2(root) {
|
|
4611
|
+
const hash = createHash3("sha256");
|
|
4146
4612
|
hash.update("threadroot-directory-v1\n");
|
|
4147
4613
|
async function walk(dir) {
|
|
4148
|
-
const entries = (await
|
|
4614
|
+
const entries = (await readdir6(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
4149
4615
|
for (const entry of entries) {
|
|
4150
|
-
const full =
|
|
4151
|
-
const rel =
|
|
4616
|
+
const full = path27.join(dir, entry.name);
|
|
4617
|
+
const rel = path27.relative(root, full).split(path27.sep).join("/");
|
|
4152
4618
|
if (entry.isSymbolicLink()) {
|
|
4153
4619
|
throw new Error(`Refusing to install skill directory with symlink: ${rel}`);
|
|
4154
4620
|
}
|
|
@@ -4159,7 +4625,7 @@ async function hashDirectory(root) {
|
|
|
4159
4625
|
if (entry.isFile()) {
|
|
4160
4626
|
hash.update(`file:${rel}
|
|
4161
4627
|
`);
|
|
4162
|
-
hash.update(await
|
|
4628
|
+
hash.update(await readFile12(full));
|
|
4163
4629
|
hash.update("\n");
|
|
4164
4630
|
}
|
|
4165
4631
|
}
|
|
@@ -4168,8 +4634,8 @@ async function hashDirectory(root) {
|
|
|
4168
4634
|
return hash.digest("hex");
|
|
4169
4635
|
}
|
|
4170
4636
|
async function validateSkillDirectory2(sourcePath, expectedName) {
|
|
4171
|
-
const skillPath =
|
|
4172
|
-
const parsed = parseFrontmatter(await
|
|
4637
|
+
const skillPath = path27.join(sourcePath, "SKILL.md");
|
|
4638
|
+
const parsed = parseFrontmatter(await readFile12(skillPath, "utf8"));
|
|
4173
4639
|
const result = skillFrontmatterSchema.safeParse(parsed.data);
|
|
4174
4640
|
if (!result.success) {
|
|
4175
4641
|
const detail = result.error.issues.map((issue) => issue.message).join("; ");
|
|
@@ -4178,7 +4644,7 @@ async function validateSkillDirectory2(sourcePath, expectedName) {
|
|
|
4178
4644
|
if (result.data.name !== expectedName) {
|
|
4179
4645
|
throw new Error(`Skill directory name \`${expectedName}\` must match SKILL.md name \`${result.data.name}\`.`);
|
|
4180
4646
|
}
|
|
4181
|
-
return await
|
|
4647
|
+
return await hashDirectory2(sourcePath);
|
|
4182
4648
|
}
|
|
4183
4649
|
async function installObject(repoRoot, rawSource, options = {}) {
|
|
4184
4650
|
const ref = parseSourceRef(rawSource);
|
|
@@ -4198,7 +4664,7 @@ async function installObject(repoRoot, rawSource, options = {}) {
|
|
|
4198
4664
|
objectPath = safeRepoPath(within);
|
|
4199
4665
|
refLabel = ref.ref;
|
|
4200
4666
|
const fetched = await fetchGitSource(ref);
|
|
4201
|
-
sourcePath =
|
|
4667
|
+
sourcePath = path27.join(fetched.dir, objectPath);
|
|
4202
4668
|
resolved = fetched.sha;
|
|
4203
4669
|
cleanup = fetched.cleanup;
|
|
4204
4670
|
} else {
|
|
@@ -4213,19 +4679,19 @@ async function installObject(repoRoot, rawSource, options = {}) {
|
|
|
4213
4679
|
const destDir = scope === "user" ? userObjectDir(dirKey, options.home) : projectObjectDir(repoRoot, dirKey);
|
|
4214
4680
|
let destPath;
|
|
4215
4681
|
let integrity;
|
|
4216
|
-
const info = await
|
|
4682
|
+
const info = await stat11(sourcePath);
|
|
4217
4683
|
if (info.isDirectory()) {
|
|
4218
4684
|
if (kind !== "skill") {
|
|
4219
4685
|
throw new Error("Only skill objects may be installed from a directory.");
|
|
4220
4686
|
}
|
|
4221
4687
|
integrity = `sha256:${await validateSkillDirectory2(sourcePath, name)}`;
|
|
4222
|
-
destPath =
|
|
4223
|
-
await
|
|
4224
|
-
await
|
|
4688
|
+
destPath = path27.join(destDir, name);
|
|
4689
|
+
await mkdir11(destDir, { recursive: true });
|
|
4690
|
+
await cp3(sourcePath, destPath, { recursive: true, force: true });
|
|
4225
4691
|
} else {
|
|
4226
|
-
const content = await
|
|
4227
|
-
destPath =
|
|
4228
|
-
await
|
|
4692
|
+
const content = await readFile12(sourcePath, "utf8");
|
|
4693
|
+
destPath = path27.join(destDir, `${name}${objectExt(kind)}`);
|
|
4694
|
+
await mkdir11(destDir, { recursive: true });
|
|
4229
4695
|
await writeFile10(destPath, content, "utf8");
|
|
4230
4696
|
integrity = `sha256:${hashContent(content)}`;
|
|
4231
4697
|
}
|
|
@@ -4278,7 +4744,7 @@ async function runInstall(repoRoot, source, options) {
|
|
|
4278
4744
|
if (installed.kind === "skill" && installed.entry.sourceKind !== "local") {
|
|
4279
4745
|
console.log(" note: inspect external skills before trusting bundled scripts, assets, or allowed tools.");
|
|
4280
4746
|
if (scope === "project") {
|
|
4281
|
-
console.log(` inspect: threadroot skills inspect ${
|
|
4747
|
+
console.log(` inspect: threadroot skills inspect ${path28.relative(repoRoot, installed.path)}`);
|
|
4282
4748
|
}
|
|
4283
4749
|
}
|
|
4284
4750
|
} catch (error) {
|
|
@@ -4290,7 +4756,7 @@ async function runInstall(repoRoot, source, options) {
|
|
|
4290
4756
|
// src/mcp/server.ts
|
|
4291
4757
|
import readline from "readline";
|
|
4292
4758
|
import { stdin as input, stdout as output } from "process";
|
|
4293
|
-
import { z as
|
|
4759
|
+
import { z as z5 } from "zod";
|
|
4294
4760
|
function defineTool(spec) {
|
|
4295
4761
|
return spec;
|
|
4296
4762
|
}
|
|
@@ -4302,7 +4768,7 @@ var toolRegistry = [
|
|
|
4302
4768
|
{ task: { type: "string", description: "The coding task to assemble context for." } },
|
|
4303
4769
|
["task"]
|
|
4304
4770
|
),
|
|
4305
|
-
args:
|
|
4771
|
+
args: z5.object({ task: z5.string().min(1) }),
|
|
4306
4772
|
run: async (repoRoot, args) => {
|
|
4307
4773
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4308
4774
|
if (!harness) {
|
|
@@ -4315,7 +4781,7 @@ var toolRegistry = [
|
|
|
4315
4781
|
name: "skills_list",
|
|
4316
4782
|
description: "List the skills defined in this repo's harness (name, when, tags).",
|
|
4317
4783
|
inputSchema: objectSchema({}),
|
|
4318
|
-
args:
|
|
4784
|
+
args: z5.object({}),
|
|
4319
4785
|
run: async (repoRoot) => {
|
|
4320
4786
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4321
4787
|
if (!harness) {
|
|
@@ -4335,7 +4801,7 @@ var toolRegistry = [
|
|
|
4335
4801
|
name: "skills_get",
|
|
4336
4802
|
description: "Return a harness skill's full body and metadata by name.",
|
|
4337
4803
|
inputSchema: objectSchema({ name: { type: "string", description: "Skill name." } }, ["name"]),
|
|
4338
|
-
args:
|
|
4804
|
+
args: z5.object({ name: z5.string().min(1) }),
|
|
4339
4805
|
run: async (repoRoot, args) => {
|
|
4340
4806
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4341
4807
|
const skill = harness?.skills.find((entry) => entry.name === args.name);
|
|
@@ -4347,9 +4813,9 @@ var toolRegistry = [
|
|
|
4347
4813
|
}),
|
|
4348
4814
|
defineTool({
|
|
4349
4815
|
name: "tools_list",
|
|
4350
|
-
description: "List the executable tools defined in this repo's harness (name, inputs, confirm).",
|
|
4816
|
+
description: "List the executable tools defined in this repo's harness (name, inputs, risk, connection, confirm).",
|
|
4351
4817
|
inputSchema: objectSchema({}),
|
|
4352
|
-
args:
|
|
4818
|
+
args: z5.object({}),
|
|
4353
4819
|
run: async (repoRoot) => {
|
|
4354
4820
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4355
4821
|
if (!harness) {
|
|
@@ -4360,7 +4826,10 @@ var toolRegistry = [
|
|
|
4360
4826
|
name: tool.name,
|
|
4361
4827
|
description: tool.manifest.description,
|
|
4362
4828
|
scope: tool.manifest.scope,
|
|
4829
|
+
risk: tool.manifest.risk,
|
|
4363
4830
|
confirm: tool.manifest.confirm,
|
|
4831
|
+
connection: tool.manifest.connection,
|
|
4832
|
+
healthcheck: Boolean(tool.manifest.healthcheck),
|
|
4364
4833
|
kind: tool.manifest.run ? "shell" : "script",
|
|
4365
4834
|
input: tool.manifest.input
|
|
4366
4835
|
}))
|
|
@@ -4371,7 +4840,7 @@ var toolRegistry = [
|
|
|
4371
4840
|
name: "tools_check",
|
|
4372
4841
|
description: "Run configured harness tool healthchecks without running primary tool actions.",
|
|
4373
4842
|
inputSchema: objectSchema({}),
|
|
4374
|
-
args:
|
|
4843
|
+
args: z5.object({}),
|
|
4375
4844
|
run: async (repoRoot) => {
|
|
4376
4845
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4377
4846
|
if (!harness) {
|
|
@@ -4382,28 +4851,30 @@ var toolRegistry = [
|
|
|
4382
4851
|
}),
|
|
4383
4852
|
defineTool({
|
|
4384
4853
|
name: "tools_run",
|
|
4385
|
-
description: "Execute a harness tool locally.
|
|
4854
|
+
description: "Execute a safe harness tool locally. MCP cannot self-confirm risky tools; use `threadroot run <tool> --yes` after human review.",
|
|
4386
4855
|
inputSchema: objectSchema(
|
|
4387
4856
|
{
|
|
4388
4857
|
name: { type: "string", description: "Tool name." },
|
|
4389
|
-
input: { type: "object", description: "Tool inputs as key/value pairs.", additionalProperties: true }
|
|
4390
|
-
confirm: { type: "boolean", description: "Confirm running a tool that requires confirmation." }
|
|
4858
|
+
input: { type: "object", description: "Tool inputs as key/value pairs.", additionalProperties: true }
|
|
4391
4859
|
},
|
|
4392
4860
|
["name"]
|
|
4393
4861
|
),
|
|
4394
|
-
args:
|
|
4395
|
-
name:
|
|
4396
|
-
input:
|
|
4397
|
-
confirm: z4.boolean().optional()
|
|
4862
|
+
args: z5.object({
|
|
4863
|
+
name: z5.string().min(1),
|
|
4864
|
+
input: z5.record(z5.unknown()).optional()
|
|
4398
4865
|
}),
|
|
4399
4866
|
run: async (repoRoot, args) => {
|
|
4400
4867
|
const outcome = await runTool(repoRoot, {
|
|
4401
4868
|
name: args.name,
|
|
4402
4869
|
input: args.input,
|
|
4403
|
-
confirmed:
|
|
4870
|
+
confirmed: false
|
|
4404
4871
|
});
|
|
4405
4872
|
if (outcome.status === "blocked") {
|
|
4406
|
-
return {
|
|
4873
|
+
return {
|
|
4874
|
+
ok: false,
|
|
4875
|
+
blocked: outcome.reason,
|
|
4876
|
+
message: outcome.reason === "needs-confirmation" ? `${outcome.message} Ask the user to run \`threadroot run ${args.name} --yes\` after review.` : outcome.message
|
|
4877
|
+
};
|
|
4407
4878
|
}
|
|
4408
4879
|
const { result } = outcome;
|
|
4409
4880
|
return {
|
|
@@ -4426,6 +4897,9 @@ var toolRegistry = [
|
|
|
4426
4897
|
description: { type: "string", description: "What the tool does." },
|
|
4427
4898
|
run: { type: "string", description: "Shell command (use {{param}} for inputs)." },
|
|
4428
4899
|
script: { type: "string", description: "Harness-relative script path (alternative to run)." },
|
|
4900
|
+
risk: { type: "string", enum: ["low", "medium", "high"], description: "Risk level." },
|
|
4901
|
+
connection: { type: "string", description: "Optional connection dependency." },
|
|
4902
|
+
healthcheck: { type: "string", description: "Command that verifies this tool is available." },
|
|
4429
4903
|
confirm: { type: "boolean", description: "Ask before running. Defaults to true for agents." },
|
|
4430
4904
|
scope: { type: "string", enum: ["user", "project"], description: "Tool scope." },
|
|
4431
4905
|
input: {
|
|
@@ -4436,14 +4910,17 @@ var toolRegistry = [
|
|
|
4436
4910
|
},
|
|
4437
4911
|
["name", "description"]
|
|
4438
4912
|
),
|
|
4439
|
-
args:
|
|
4440
|
-
name:
|
|
4441
|
-
description:
|
|
4442
|
-
run:
|
|
4443
|
-
script:
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4913
|
+
args: z5.object({
|
|
4914
|
+
name: z5.string().min(1),
|
|
4915
|
+
description: z5.string().min(1),
|
|
4916
|
+
run: z5.string().optional(),
|
|
4917
|
+
script: z5.string().optional(),
|
|
4918
|
+
risk: z5.enum(["low", "medium", "high"]).optional(),
|
|
4919
|
+
connection: z5.string().optional(),
|
|
4920
|
+
healthcheck: z5.string().optional(),
|
|
4921
|
+
confirm: z5.boolean().optional(),
|
|
4922
|
+
scope: z5.enum(["user", "project"]).optional(),
|
|
4923
|
+
input: z5.record(z5.unknown()).optional()
|
|
4447
4924
|
}),
|
|
4448
4925
|
run: async (repoRoot, args) => {
|
|
4449
4926
|
const created = await createTool(
|
|
@@ -4453,6 +4930,9 @@ var toolRegistry = [
|
|
|
4453
4930
|
description: args.description,
|
|
4454
4931
|
run: args.run,
|
|
4455
4932
|
script: args.script,
|
|
4933
|
+
risk: args.risk,
|
|
4934
|
+
connection: args.connection,
|
|
4935
|
+
healthcheck: args.healthcheck ? { run: args.healthcheck, expectExitCode: 0 } : void 0,
|
|
4456
4936
|
confirm: args.confirm,
|
|
4457
4937
|
scope: args.scope,
|
|
4458
4938
|
input: args.input
|
|
@@ -4466,7 +4946,7 @@ var toolRegistry = [
|
|
|
4466
4946
|
name: "tools_detect",
|
|
4467
4947
|
description: "Propose starter tools from the repo's existing command surface (scripts, Make/just targets).",
|
|
4468
4948
|
inputSchema: objectSchema({}),
|
|
4469
|
-
args:
|
|
4949
|
+
args: z5.object({}),
|
|
4470
4950
|
run: async (repoRoot) => {
|
|
4471
4951
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4472
4952
|
const profile = harness?.manifest.profile ?? "empty";
|
|
@@ -4477,7 +4957,7 @@ var toolRegistry = [
|
|
|
4477
4957
|
name: "connections_list",
|
|
4478
4958
|
description: "List local CLI connections defined in this repo's harness.",
|
|
4479
4959
|
inputSchema: objectSchema({}),
|
|
4480
|
-
args:
|
|
4960
|
+
args: z5.object({}),
|
|
4481
4961
|
run: async (repoRoot) => {
|
|
4482
4962
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4483
4963
|
if (!harness) {
|
|
@@ -4500,7 +4980,7 @@ var toolRegistry = [
|
|
|
4500
4980
|
name: "connections_check",
|
|
4501
4981
|
description: "Check local CLI connections and their configured healthchecks.",
|
|
4502
4982
|
inputSchema: objectSchema({}),
|
|
4503
|
-
args:
|
|
4983
|
+
args: z5.object({}),
|
|
4504
4984
|
run: (repoRoot) => checkConnections(repoRoot)
|
|
4505
4985
|
}),
|
|
4506
4986
|
defineTool({
|
|
@@ -4509,7 +4989,7 @@ var toolRegistry = [
|
|
|
4509
4989
|
inputSchema: objectSchema({
|
|
4510
4990
|
type: { type: "string", description: "Memory type (project, repo-map, current-focus, handoff, pitfalls)." }
|
|
4511
4991
|
}),
|
|
4512
|
-
args:
|
|
4992
|
+
args: z5.object({ type: z5.string().optional() }),
|
|
4513
4993
|
run: async (repoRoot, args) => {
|
|
4514
4994
|
if (args.type) {
|
|
4515
4995
|
return { type: args.type, body: await readMemory(repoRoot, args.type) };
|
|
@@ -4528,21 +5008,21 @@ var toolRegistry = [
|
|
|
4528
5008
|
},
|
|
4529
5009
|
["type", "note"]
|
|
4530
5010
|
),
|
|
4531
|
-
args:
|
|
5011
|
+
args: z5.object({ type: z5.string().min(1), note: z5.string().min(1) }),
|
|
4532
5012
|
run: (repoRoot, args) => appendMemory(repoRoot, args.type, args.note)
|
|
4533
5013
|
}),
|
|
4534
5014
|
defineTool({
|
|
4535
5015
|
name: "status",
|
|
4536
5016
|
description: "Return harness state: manifest, object counts, and drift between canonical and compiled outputs.",
|
|
4537
5017
|
inputSchema: objectSchema({}),
|
|
4538
|
-
args:
|
|
5018
|
+
args: z5.object({}),
|
|
4539
5019
|
run: (repoRoot) => harnessStatus(repoRoot)
|
|
4540
5020
|
}),
|
|
4541
5021
|
defineTool({
|
|
4542
5022
|
name: "doctor",
|
|
4543
5023
|
description: "Check harness validity, compiled output health, MCP hints, and tool trust.",
|
|
4544
5024
|
inputSchema: objectSchema({}),
|
|
4545
|
-
args:
|
|
5025
|
+
args: z5.object({}),
|
|
4546
5026
|
run: (repoRoot) => doctor(repoRoot)
|
|
4547
5027
|
})
|
|
4548
5028
|
];
|
|
@@ -4569,7 +5049,7 @@ async function handleMessage(repoRoot, request) {
|
|
|
4569
5049
|
if (request.method === "initialize") {
|
|
4570
5050
|
return resultResponse(request, {
|
|
4571
5051
|
protocolVersion: "2024-11-05",
|
|
4572
|
-
serverInfo: { name: "threadroot", version:
|
|
5052
|
+
serverInfo: { name: "threadroot", version: THREADROOT_VERSION },
|
|
4573
5053
|
capabilities: { tools: {} },
|
|
4574
5054
|
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."
|
|
4575
5055
|
});
|
|
@@ -4584,7 +5064,8 @@ async function handleMessage(repoRoot, request) {
|
|
|
4584
5064
|
const params = request.params;
|
|
4585
5065
|
const result = await callTool(repoRoot, params?.name, params?.arguments ?? {});
|
|
4586
5066
|
return resultResponse(request, {
|
|
4587
|
-
content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }]
|
|
5067
|
+
content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }],
|
|
5068
|
+
structuredContent: result
|
|
4588
5069
|
});
|
|
4589
5070
|
}
|
|
4590
5071
|
return errorResponse(request, -32601, `Unknown method: ${request.method ?? "<missing>"}`);
|
|
@@ -4723,10 +5204,11 @@ Rules:
|
|
|
4723
5204
|
Steps:
|
|
4724
5205
|
1. Check whether Threadroot is available with \`threadroot --version\`.
|
|
4725
5206
|
2. If it is not available, try \`npm exec threadroot -- --help\` or \`pnpm dlx threadroot --help\`. If this is a local checkout, use \`${localCommand} --help\`.
|
|
4726
|
-
3. Run \`threadroot bootstrap --yes --agent all --task "current task"\`. If this is a local checkout, run \`${localCommand} bootstrap --yes --agent all --task "current task"\`.
|
|
4727
|
-
4.
|
|
4728
|
-
5.
|
|
4729
|
-
6. If
|
|
5207
|
+
3. Run \`threadroot bootstrap --yes --agent all --mcp --task "current task"\`. If this is a local checkout, run \`${localCommand} bootstrap --yes --agent all --mcp --task "current task"\`.
|
|
5208
|
+
4. If the user named a stack or workflow, add curated packs with \`--packs <comma-separated-pack-list>\` when a matching built-in pack exists.
|
|
5209
|
+
5. Run \`threadroot start "current task"\` with the user's actual task.
|
|
5210
|
+
6. If the user asks for provider-native project skill files, run \`threadroot expose <agent>\` or \`threadroot expose all\`.
|
|
5211
|
+
7. If project-local MCP config is useful, ask before running \`threadroot mcp setup --write\`, then tell the user to reload their agent surface.
|
|
4730
5212
|
|
|
4731
5213
|
Final response:
|
|
4732
5214
|
Say exactly:
|
|
@@ -4793,11 +5275,11 @@ function agentNotes(agent) {
|
|
|
4793
5275
|
}
|
|
4794
5276
|
|
|
4795
5277
|
// src/core/mcp-config.ts
|
|
4796
|
-
import { mkdir as
|
|
4797
|
-
import
|
|
5278
|
+
import { mkdir as mkdir12, readFile as readFile13, writeFile as writeFile11 } from "fs/promises";
|
|
5279
|
+
import path29 from "path";
|
|
4798
5280
|
var TARGETS = [
|
|
4799
|
-
{ agent: "copilot", file:
|
|
4800
|
-
{ agent: "cursor", file:
|
|
5281
|
+
{ agent: "copilot", file: path29.join(".vscode", "mcp.json"), key: "servers" },
|
|
5282
|
+
{ agent: "cursor", file: path29.join(".cursor", "mcp.json"), key: "mcpServers" },
|
|
4801
5283
|
{ agent: "claude", file: ".mcp.json", key: "mcpServers" }
|
|
4802
5284
|
];
|
|
4803
5285
|
function mcpServerEntry(command, scriptPath) {
|
|
@@ -4806,7 +5288,7 @@ function mcpServerEntry(command, scriptPath) {
|
|
|
4806
5288
|
async function mergeConfig(filePath, key, entry) {
|
|
4807
5289
|
let config = {};
|
|
4808
5290
|
try {
|
|
4809
|
-
const raw = await
|
|
5291
|
+
const raw = await readFile13(filePath, "utf8");
|
|
4810
5292
|
const parsed = JSON.parse(raw);
|
|
4811
5293
|
if (parsed && typeof parsed === "object") {
|
|
4812
5294
|
config = parsed;
|
|
@@ -4819,7 +5301,7 @@ async function mergeConfig(filePath, key, entry) {
|
|
|
4819
5301
|
const servers = config[key] && typeof config[key] === "object" ? config[key] : {};
|
|
4820
5302
|
servers.threadroot = { ...entry };
|
|
4821
5303
|
config[key] = servers;
|
|
4822
|
-
await
|
|
5304
|
+
await mkdir12(path29.dirname(filePath), { recursive: true });
|
|
4823
5305
|
await writeFile11(filePath, `${JSON.stringify(config, null, 2)}
|
|
4824
5306
|
`, "utf8");
|
|
4825
5307
|
}
|
|
@@ -4828,7 +5310,7 @@ async function writeProjectMcpConfigs(input2) {
|
|
|
4828
5310
|
const targets = agents ? TARGETS.filter((target) => agents.includes(target.agent)) : TARGETS;
|
|
4829
5311
|
const written = [];
|
|
4830
5312
|
for (const target of targets) {
|
|
4831
|
-
const filePath =
|
|
5313
|
+
const filePath = path29.join(input2.repoRoot, target.file);
|
|
4832
5314
|
await mergeConfig(filePath, target.key, input2.entry);
|
|
4833
5315
|
written.push(target.file);
|
|
4834
5316
|
}
|
|
@@ -4851,6 +5333,10 @@ async function runMcpSetup(repoRoot, options) {
|
|
|
4851
5333
|
const scriptPath = process.argv[1];
|
|
4852
5334
|
const entry = mcpServerEntry(command, scriptPath);
|
|
4853
5335
|
const result = await writeProjectMcpConfigs({ repoRoot, entry });
|
|
5336
|
+
if (options.json) {
|
|
5337
|
+
printJson(result);
|
|
5338
|
+
return;
|
|
5339
|
+
}
|
|
4854
5340
|
console.log("Wrote project MCP config:");
|
|
4855
5341
|
for (const file of result.written) {
|
|
4856
5342
|
console.log(`- ${file}`);
|
|
@@ -4860,11 +5346,23 @@ async function runMcpSetup(repoRoot, options) {
|
|
|
4860
5346
|
}
|
|
4861
5347
|
return;
|
|
4862
5348
|
}
|
|
4863
|
-
|
|
5349
|
+
const guide = mcpSetupGuide({ repoRoot, agent: options.agent });
|
|
5350
|
+
if (options.json) {
|
|
5351
|
+
printJson({ guide });
|
|
5352
|
+
} else {
|
|
5353
|
+
console.log(guide);
|
|
5354
|
+
}
|
|
4864
5355
|
}
|
|
4865
5356
|
async function runMcpCheck(repoRoot, options) {
|
|
4866
5357
|
const timeoutMs = options.timeout ? Number.parseInt(options.timeout, 10) : void 0;
|
|
4867
5358
|
const report = await checkCodexMcp({ repoRoot, timeoutMs });
|
|
5359
|
+
if (options.json) {
|
|
5360
|
+
printJson(report);
|
|
5361
|
+
if (report.status === "error") {
|
|
5362
|
+
process.exitCode = 1;
|
|
5363
|
+
}
|
|
5364
|
+
return;
|
|
5365
|
+
}
|
|
4868
5366
|
console.log(`Threadroot MCP check: ${report.status}`);
|
|
4869
5367
|
console.log(`config: ${report.configPath}`);
|
|
4870
5368
|
if (report.entry) {
|
|
@@ -4899,9 +5397,21 @@ async function runRemember(repoRoot, note, options = {}) {
|
|
|
4899
5397
|
}
|
|
4900
5398
|
|
|
4901
5399
|
// src/commands/skills.ts
|
|
4902
|
-
async function runSkillsList(repoRoot) {
|
|
5400
|
+
async function runSkillsList(repoRoot, options = {}) {
|
|
4903
5401
|
try {
|
|
4904
5402
|
const harness = await resolveHarness(repoRoot);
|
|
5403
|
+
const skills = harness.skills.map((skill) => ({
|
|
5404
|
+
name: skill.name,
|
|
5405
|
+
origin: skill.origin,
|
|
5406
|
+
description: skill.frontmatter.description,
|
|
5407
|
+
when: skill.frontmatter.when,
|
|
5408
|
+
tags: skill.frontmatter.tags,
|
|
5409
|
+
sourcePath: skill.sourcePath
|
|
5410
|
+
}));
|
|
5411
|
+
if (options.json) {
|
|
5412
|
+
printJson({ skills });
|
|
5413
|
+
return;
|
|
5414
|
+
}
|
|
4905
5415
|
if (harness.skills.length === 0) {
|
|
4906
5416
|
console.log("No skills defined. Add folder skills under `.threadroot/skills/<name>/SKILL.md`.");
|
|
4907
5417
|
return;
|
|
@@ -4911,7 +5421,11 @@ async function runSkillsList(repoRoot) {
|
|
|
4911
5421
|
}
|
|
4912
5422
|
} catch (error) {
|
|
4913
5423
|
if (error instanceof HarnessError) {
|
|
4914
|
-
|
|
5424
|
+
if (options.json) {
|
|
5425
|
+
printJson({ skills: [], ok: false, error: "harness_missing", message: "No harness found. Run `tr init` first." });
|
|
5426
|
+
} else {
|
|
5427
|
+
console.log("No harness found. Run `tr init` first.");
|
|
5428
|
+
}
|
|
4915
5429
|
return;
|
|
4916
5430
|
}
|
|
4917
5431
|
throw error;
|
|
@@ -4919,6 +5433,13 @@ async function runSkillsList(repoRoot) {
|
|
|
4919
5433
|
}
|
|
4920
5434
|
async function runSkillsValidate(repoRoot, options = {}) {
|
|
4921
5435
|
const report = options.path ? await validateSkillPath(toRepoPath(repoRoot, options.path)) : await validateSkills(repoRoot);
|
|
5436
|
+
if (options.json) {
|
|
5437
|
+
printJson(report);
|
|
5438
|
+
if (!report.ok) {
|
|
5439
|
+
process.exitCode = 1;
|
|
5440
|
+
}
|
|
5441
|
+
return;
|
|
5442
|
+
}
|
|
4922
5443
|
if (report.findings.length === 0) {
|
|
4923
5444
|
console.log("Skills valid.");
|
|
4924
5445
|
return;
|
|
@@ -4933,8 +5454,12 @@ async function runSkillsValidate(repoRoot, options = {}) {
|
|
|
4933
5454
|
process.exitCode = 1;
|
|
4934
5455
|
}
|
|
4935
5456
|
}
|
|
4936
|
-
async function runSkillsInspect(repoRoot, targetPath) {
|
|
5457
|
+
async function runSkillsInspect(repoRoot, targetPath, options = {}) {
|
|
4937
5458
|
const inspection = await inspectSkillPath(toRepoPath(repoRoot, targetPath));
|
|
5459
|
+
if (options.json) {
|
|
5460
|
+
printJson(inspection);
|
|
5461
|
+
return;
|
|
5462
|
+
}
|
|
4938
5463
|
console.log(`${inspection.name}`);
|
|
4939
5464
|
console.log(`description: ${inspection.description}`);
|
|
4940
5465
|
console.log(`path: ${inspection.path}`);
|
|
@@ -4996,15 +5521,23 @@ async function runSetup(_repoRoot, options) {
|
|
|
4996
5521
|
// src/commands/start.ts
|
|
4997
5522
|
async function runStart(repoRoot, task, options) {
|
|
4998
5523
|
const report = await startSession(repoRoot, { task: task ?? options.task });
|
|
4999
|
-
|
|
5524
|
+
if (options.json) {
|
|
5525
|
+
printJson(report);
|
|
5526
|
+
} else {
|
|
5527
|
+
printStartReport(report);
|
|
5528
|
+
}
|
|
5000
5529
|
if (!report.status.exists || report.doctor && !report.doctor.ok) {
|
|
5001
5530
|
process.exitCode = 1;
|
|
5002
5531
|
}
|
|
5003
5532
|
}
|
|
5004
5533
|
|
|
5005
5534
|
// src/commands/status.ts
|
|
5006
|
-
async function runStatus(repoRoot) {
|
|
5535
|
+
async function runStatus(repoRoot, options = {}) {
|
|
5007
5536
|
const status = await harnessStatus(repoRoot);
|
|
5537
|
+
if (options.json) {
|
|
5538
|
+
printJson(status);
|
|
5539
|
+
return;
|
|
5540
|
+
}
|
|
5008
5541
|
if (!status.exists) {
|
|
5009
5542
|
console.log("No harness found. Run `tr init` first.");
|
|
5010
5543
|
return;
|
|
@@ -5025,261 +5558,16 @@ async function runStatus(repoRoot) {
|
|
|
5025
5558
|
}
|
|
5026
5559
|
}
|
|
5027
5560
|
|
|
5028
|
-
// src/core/packs/index.ts
|
|
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";
|
|
5031
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5032
|
-
import { parse as parseYaml3 } from "yaml";
|
|
5033
|
-
import { z as z5 } from "zod";
|
|
5034
|
-
var packManifestSchema = z5.object({
|
|
5035
|
-
name: z5.string().min(1),
|
|
5036
|
-
version: z5.literal(1),
|
|
5037
|
-
description: z5.string().min(1),
|
|
5038
|
-
skills: z5.array(z5.string()).default([]),
|
|
5039
|
-
tools: z5.array(z5.string()).default([]),
|
|
5040
|
-
rules: z5.array(z5.string()).default([]),
|
|
5041
|
-
connections: z5.array(z5.string()).default([])
|
|
5042
|
-
});
|
|
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, "../../../..");
|
|
5047
|
-
var PACK_CANDIDATES = [
|
|
5048
|
-
path29.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
|
|
5049
|
-
path29.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
|
|
5050
|
-
path29.join(PACKAGE_ROOT_FROM_SRC2, "packs")
|
|
5051
|
-
];
|
|
5052
|
-
async function exists5(target) {
|
|
5053
|
-
try {
|
|
5054
|
-
await stat11(target);
|
|
5055
|
-
return true;
|
|
5056
|
-
} catch (error) {
|
|
5057
|
-
if (error.code === "ENOENT") {
|
|
5058
|
-
return false;
|
|
5059
|
-
}
|
|
5060
|
-
throw error;
|
|
5061
|
-
}
|
|
5062
|
-
}
|
|
5063
|
-
async function firstExisting(candidates) {
|
|
5064
|
-
for (const candidate of candidates) {
|
|
5065
|
-
if (await isPackRoot(candidate)) {
|
|
5066
|
-
return candidate;
|
|
5067
|
-
}
|
|
5068
|
-
}
|
|
5069
|
-
return void 0;
|
|
5070
|
-
}
|
|
5071
|
-
async function bundledPacksDir() {
|
|
5072
|
-
return firstExisting(PACK_CANDIDATES);
|
|
5073
|
-
}
|
|
5074
|
-
async function isPackRoot(candidate) {
|
|
5075
|
-
let entries;
|
|
5076
|
-
try {
|
|
5077
|
-
entries = await readdir6(candidate, { withFileTypes: true });
|
|
5078
|
-
} catch {
|
|
5079
|
-
return false;
|
|
5080
|
-
}
|
|
5081
|
-
for (const entry of entries) {
|
|
5082
|
-
if (entry.isDirectory() && await exists5(path29.join(candidate, entry.name, "pack.yaml"))) {
|
|
5083
|
-
return true;
|
|
5084
|
-
}
|
|
5085
|
-
}
|
|
5086
|
-
return false;
|
|
5087
|
-
}
|
|
5088
|
-
function safeRelative(ref) {
|
|
5089
|
-
const normalized = path29.normalize(ref);
|
|
5090
|
-
if (path29.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path29.sep}`)) {
|
|
5091
|
-
throw new Error(`Unsafe pack reference: ${ref}`);
|
|
5092
|
-
}
|
|
5093
|
-
return normalized;
|
|
5094
|
-
}
|
|
5095
|
-
async function readPackManifest(packDir) {
|
|
5096
|
-
const file = path29.join(packDir, "pack.yaml");
|
|
5097
|
-
const parsed = packManifestSchema.safeParse(parseYaml3(await readFile13(file, "utf8")));
|
|
5098
|
-
if (!parsed.success) {
|
|
5099
|
-
const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
|
|
5100
|
-
throw new Error(`Invalid pack manifest ${file}: ${detail}`);
|
|
5101
|
-
}
|
|
5102
|
-
return parsed.data;
|
|
5103
|
-
}
|
|
5104
|
-
async function packDirFor(repoRoot, nameOrPath) {
|
|
5105
|
-
if (path29.isAbsolute(nameOrPath)) {
|
|
5106
|
-
return nameOrPath;
|
|
5107
|
-
}
|
|
5108
|
-
if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
|
|
5109
|
-
return toRepoPath(repoRoot, nameOrPath);
|
|
5110
|
-
}
|
|
5111
|
-
const bundled = await bundledPacksDir();
|
|
5112
|
-
if (bundled) {
|
|
5113
|
-
return path29.join(bundled, nameOrPath);
|
|
5114
|
-
}
|
|
5115
|
-
return toRepoPath(repoRoot, path29.join("packs", nameOrPath));
|
|
5116
|
-
}
|
|
5117
|
-
async function directFiles(dir, ext) {
|
|
5118
|
-
try {
|
|
5119
|
-
const entries = await readdir6(dir, { withFileTypes: true });
|
|
5120
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path29.join(dir, entry.name)).sort();
|
|
5121
|
-
} catch (error) {
|
|
5122
|
-
if (error.code === "ENOENT") {
|
|
5123
|
-
return [];
|
|
5124
|
-
}
|
|
5125
|
-
throw error;
|
|
5126
|
-
}
|
|
5127
|
-
}
|
|
5128
|
-
async function skillEntries(dir) {
|
|
5129
|
-
try {
|
|
5130
|
-
const entries = await readdir6(dir, { withFileTypes: true });
|
|
5131
|
-
const result = [];
|
|
5132
|
-
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
5133
|
-
const full = path29.join(dir, entry.name);
|
|
5134
|
-
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
5135
|
-
result.push(full);
|
|
5136
|
-
}
|
|
5137
|
-
if (entry.isDirectory() && await exists5(path29.join(full, "SKILL.md"))) {
|
|
5138
|
-
result.push(full);
|
|
5139
|
-
}
|
|
5140
|
-
}
|
|
5141
|
-
return result;
|
|
5142
|
-
} catch (error) {
|
|
5143
|
-
if (error.code === "ENOENT") {
|
|
5144
|
-
return [];
|
|
5145
|
-
}
|
|
5146
|
-
throw error;
|
|
5147
|
-
}
|
|
5148
|
-
}
|
|
5149
|
-
async function collectObjects(packDir, manifest) {
|
|
5150
|
-
async function resolveRef(ref) {
|
|
5151
|
-
const safe = safeRelative(ref);
|
|
5152
|
-
const local = path29.resolve(packDir, safe);
|
|
5153
|
-
if (await exists5(local)) {
|
|
5154
|
-
return local;
|
|
5155
|
-
}
|
|
5156
|
-
return path29.resolve(packDir, "..", "..", safe);
|
|
5157
|
-
}
|
|
5158
|
-
return {
|
|
5159
|
-
skills: [
|
|
5160
|
-
...await Promise.all(manifest.skills.map(resolveRef)),
|
|
5161
|
-
...await skillEntries(path29.join(packDir, "skills"))
|
|
5162
|
-
],
|
|
5163
|
-
tools: [
|
|
5164
|
-
...await Promise.all(manifest.tools.map(resolveRef)),
|
|
5165
|
-
...await directFiles(path29.join(packDir, "tools"), ".yaml")
|
|
5166
|
-
],
|
|
5167
|
-
rules: [
|
|
5168
|
-
...await Promise.all(manifest.rules.map(resolveRef)),
|
|
5169
|
-
...await directFiles(path29.join(packDir, "rules"), ".md")
|
|
5170
|
-
],
|
|
5171
|
-
connections: [
|
|
5172
|
-
...await Promise.all(manifest.connections.map(resolveRef)),
|
|
5173
|
-
...await directFiles(path29.join(packDir, "connections"), ".yaml")
|
|
5174
|
-
]
|
|
5175
|
-
};
|
|
5176
|
-
}
|
|
5177
|
-
function baseName(source) {
|
|
5178
|
-
const parsed = path29.basename(source) === "SKILL.md" ? path29.dirname(source) : source;
|
|
5179
|
-
return path29.basename(parsed, path29.extname(parsed));
|
|
5180
|
-
}
|
|
5181
|
-
async function listPacks(repoRoot) {
|
|
5182
|
-
const dirs = [toRepoPath(repoRoot, "packs"), await bundledPacksDir()].filter((dir) => Boolean(dir));
|
|
5183
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5184
|
-
const packs = [];
|
|
5185
|
-
for (const root of dirs) {
|
|
5186
|
-
let entries;
|
|
5187
|
-
try {
|
|
5188
|
-
entries = await readdir6(root, { withFileTypes: true });
|
|
5189
|
-
} catch {
|
|
5190
|
-
continue;
|
|
5191
|
-
}
|
|
5192
|
-
for (const entry of entries) {
|
|
5193
|
-
if (!entry.isDirectory() || seen.has(entry.name)) {
|
|
5194
|
-
continue;
|
|
5195
|
-
}
|
|
5196
|
-
const packDir = path29.join(root, entry.name);
|
|
5197
|
-
if (!await exists5(path29.join(packDir, "pack.yaml"))) {
|
|
5198
|
-
continue;
|
|
5199
|
-
}
|
|
5200
|
-
seen.add(entry.name);
|
|
5201
|
-
packs.push(await inspectPack(repoRoot, packDir));
|
|
5202
|
-
}
|
|
5203
|
-
}
|
|
5204
|
-
return packs.sort((a, b) => a.name.localeCompare(b.name));
|
|
5205
|
-
}
|
|
5206
|
-
async function inspectPack(repoRoot, nameOrPath) {
|
|
5207
|
-
const packDir = await packDirFor(repoRoot, nameOrPath);
|
|
5208
|
-
const manifest = await readPackManifest(packDir);
|
|
5209
|
-
const objects = await collectObjects(packDir, manifest);
|
|
5210
|
-
return {
|
|
5211
|
-
name: manifest.name,
|
|
5212
|
-
description: manifest.description,
|
|
5213
|
-
path: packDir,
|
|
5214
|
-
skills: objects.skills.map(baseName),
|
|
5215
|
-
tools: objects.tools.map(baseName),
|
|
5216
|
-
rules: objects.rules.map(baseName),
|
|
5217
|
-
connections: objects.connections.map(baseName)
|
|
5218
|
-
};
|
|
5219
|
-
}
|
|
5220
|
-
async function validateProse(file, kind) {
|
|
5221
|
-
const target = path29.basename(file) === "SKILL.md" ? file : file;
|
|
5222
|
-
const content = await readFile13(target, "utf8");
|
|
5223
|
-
const parsed = parseFrontmatter(content);
|
|
5224
|
-
const schema = kind === "skill" ? skillFrontmatterSchema : ruleFrontmatterSchema;
|
|
5225
|
-
schema.parse(parsed.data);
|
|
5226
|
-
}
|
|
5227
|
-
async function validateYaml(file, kind) {
|
|
5228
|
-
const content = await readFile13(file, "utf8");
|
|
5229
|
-
const schema = kind === "tool" ? toolManifestSchema : connectionManifestSchema;
|
|
5230
|
-
schema.parse(parseYaml3(content));
|
|
5231
|
-
}
|
|
5232
|
-
async function validatePack(repoRoot, nameOrPath) {
|
|
5233
|
-
const findings = [];
|
|
5234
|
-
try {
|
|
5235
|
-
const packDir = await packDirFor(repoRoot, nameOrPath);
|
|
5236
|
-
const manifest = await readPackManifest(packDir);
|
|
5237
|
-
const objects = await collectObjects(packDir, manifest);
|
|
5238
|
-
for (const skill of objects.skills) {
|
|
5239
|
-
await validateProse(path29.basename(skill) === "SKILL.md" ? skill : path29.join(skill, "SKILL.md"), "skill");
|
|
5240
|
-
}
|
|
5241
|
-
for (const rule of objects.rules) await validateProse(rule, "rule");
|
|
5242
|
-
for (const tool of objects.tools) await validateYaml(tool, "tool");
|
|
5243
|
-
for (const connection of objects.connections) await validateYaml(connection, "connection");
|
|
5244
|
-
if (Object.values(objects).every((items) => items.length === 0)) {
|
|
5245
|
-
findings.push({ severity: "warning", message: "Pack does not include any objects." });
|
|
5246
|
-
}
|
|
5247
|
-
} catch (error) {
|
|
5248
|
-
findings.push({ severity: "error", message: error instanceof Error ? error.message : String(error) });
|
|
5249
|
-
}
|
|
5250
|
-
return { ok: !findings.some((finding3) => finding3.severity === "error"), findings };
|
|
5251
|
-
}
|
|
5252
|
-
async function copyObject(source, destDir) {
|
|
5253
|
-
const info = await stat11(source);
|
|
5254
|
-
const name = baseName(source);
|
|
5255
|
-
const dest = info.isDirectory() ? path29.join(destDir, name) : path29.join(destDir, path29.basename(source));
|
|
5256
|
-
await mkdir12(destDir, { recursive: true });
|
|
5257
|
-
await cp3(source, dest, { recursive: true, force: true });
|
|
5258
|
-
return dest;
|
|
5259
|
-
}
|
|
5260
|
-
async function installPack(repoRoot, nameOrPath) {
|
|
5261
|
-
const validation = await validatePack(repoRoot, nameOrPath);
|
|
5262
|
-
if (!validation.ok) {
|
|
5263
|
-
throw new Error(validation.findings.map((finding3) => finding3.message).join("; "));
|
|
5264
|
-
}
|
|
5265
|
-
const packDir = await packDirFor(repoRoot, nameOrPath);
|
|
5266
|
-
const manifest = await readPackManifest(packDir);
|
|
5267
|
-
const objects = await collectObjects(packDir, manifest);
|
|
5268
|
-
await Promise.all([
|
|
5269
|
-
...objects.skills.map((source) => copyObject(source, projectObjectDir(repoRoot, "skills"))),
|
|
5270
|
-
...objects.tools.map((source) => copyObject(source, projectObjectDir(repoRoot, "tools"))),
|
|
5271
|
-
...objects.rules.map((source) => copyObject(source, projectObjectDir(repoRoot, "rules"))),
|
|
5272
|
-
...objects.connections.map((source) => copyObject(source, projectObjectDir(repoRoot, "connections")))
|
|
5273
|
-
]);
|
|
5274
|
-
return inspectPack(repoRoot, packDir);
|
|
5275
|
-
}
|
|
5276
|
-
|
|
5277
5561
|
// src/commands/packs.ts
|
|
5278
5562
|
function printList(label, values) {
|
|
5279
5563
|
console.log(`${label}: ${values.length > 0 ? values.join(", ") : "none"}`);
|
|
5280
5564
|
}
|
|
5281
|
-
async function runPacksList(repoRoot) {
|
|
5565
|
+
async function runPacksList(repoRoot, options = {}) {
|
|
5282
5566
|
const packs = await listPacks(repoRoot);
|
|
5567
|
+
if (options.json) {
|
|
5568
|
+
printJson({ packs });
|
|
5569
|
+
return;
|
|
5570
|
+
}
|
|
5283
5571
|
if (packs.length === 0) {
|
|
5284
5572
|
console.log("No packs found.");
|
|
5285
5573
|
return;
|
|
@@ -5288,8 +5576,12 @@ async function runPacksList(repoRoot) {
|
|
|
5288
5576
|
console.log(`${pack.name} - ${pack.description}`);
|
|
5289
5577
|
}
|
|
5290
5578
|
}
|
|
5291
|
-
async function runPacksInspect(repoRoot, nameOrPath) {
|
|
5579
|
+
async function runPacksInspect(repoRoot, nameOrPath, options = {}) {
|
|
5292
5580
|
const pack = await inspectPack(repoRoot, nameOrPath);
|
|
5581
|
+
if (options.json) {
|
|
5582
|
+
printJson(pack);
|
|
5583
|
+
return;
|
|
5584
|
+
}
|
|
5293
5585
|
console.log(pack.name);
|
|
5294
5586
|
console.log(`description: ${pack.description}`);
|
|
5295
5587
|
console.log(`path: ${pack.path}`);
|
|
@@ -5298,8 +5590,15 @@ async function runPacksInspect(repoRoot, nameOrPath) {
|
|
|
5298
5590
|
printList("rules", pack.rules);
|
|
5299
5591
|
printList("connections", pack.connections);
|
|
5300
5592
|
}
|
|
5301
|
-
async function runPacksValidate(repoRoot, nameOrPath) {
|
|
5593
|
+
async function runPacksValidate(repoRoot, nameOrPath, options = {}) {
|
|
5302
5594
|
const report = await validatePack(repoRoot, nameOrPath);
|
|
5595
|
+
if (options.json) {
|
|
5596
|
+
printJson(report);
|
|
5597
|
+
if (!report.ok) {
|
|
5598
|
+
process.exitCode = 1;
|
|
5599
|
+
}
|
|
5600
|
+
return;
|
|
5601
|
+
}
|
|
5303
5602
|
if (report.findings.length === 0) {
|
|
5304
5603
|
console.log("Pack valid.");
|
|
5305
5604
|
return;
|
|
@@ -5311,8 +5610,12 @@ async function runPacksValidate(repoRoot, nameOrPath) {
|
|
|
5311
5610
|
process.exitCode = 1;
|
|
5312
5611
|
}
|
|
5313
5612
|
}
|
|
5314
|
-
async function runPacksInstall(repoRoot, nameOrPath) {
|
|
5613
|
+
async function runPacksInstall(repoRoot, nameOrPath, options = {}) {
|
|
5315
5614
|
const pack = await installPack(repoRoot, nameOrPath);
|
|
5615
|
+
if (options.json) {
|
|
5616
|
+
printJson(pack);
|
|
5617
|
+
return;
|
|
5618
|
+
}
|
|
5316
5619
|
console.log(`Installed pack \`${pack.name}\`.`);
|
|
5317
5620
|
printList("skills", pack.skills);
|
|
5318
5621
|
printList("tools", pack.tools);
|
|
@@ -5332,17 +5635,37 @@ function parseInputs(pairs = []) {
|
|
|
5332
5635
|
}
|
|
5333
5636
|
return input2;
|
|
5334
5637
|
}
|
|
5335
|
-
async function runToolsList(repoRoot) {
|
|
5638
|
+
async function runToolsList(repoRoot, options = {}) {
|
|
5336
5639
|
let harness;
|
|
5337
5640
|
try {
|
|
5338
5641
|
harness = await resolveHarness(repoRoot);
|
|
5339
5642
|
} catch (error) {
|
|
5340
5643
|
if (error instanceof HarnessError) {
|
|
5341
|
-
|
|
5644
|
+
if (options.json) {
|
|
5645
|
+
printJson({ tools: [], ok: false, error: "harness_missing", message: "No harness found. Run `tr init` first." });
|
|
5646
|
+
} else {
|
|
5647
|
+
console.log("No harness found. Run `tr init` first.");
|
|
5648
|
+
}
|
|
5342
5649
|
return;
|
|
5343
5650
|
}
|
|
5344
5651
|
throw error;
|
|
5345
5652
|
}
|
|
5653
|
+
const tools2 = harness.tools.map((tool) => ({
|
|
5654
|
+
name: tool.name,
|
|
5655
|
+
description: tool.manifest.description,
|
|
5656
|
+
origin: tool.origin,
|
|
5657
|
+
scope: tool.manifest.scope,
|
|
5658
|
+
risk: tool.manifest.risk,
|
|
5659
|
+
confirm: tool.manifest.confirm,
|
|
5660
|
+
connection: tool.manifest.connection,
|
|
5661
|
+
healthcheck: Boolean(tool.manifest.healthcheck),
|
|
5662
|
+
kind: tool.manifest.run ? "shell" : "script",
|
|
5663
|
+
input: tool.manifest.input
|
|
5664
|
+
}));
|
|
5665
|
+
if (options.json) {
|
|
5666
|
+
printJson({ tools: tools2 });
|
|
5667
|
+
return;
|
|
5668
|
+
}
|
|
5346
5669
|
if (harness.tools.length === 0) {
|
|
5347
5670
|
console.log("No tools defined. Add one with `tr tools add` or `tr tools detect`.");
|
|
5348
5671
|
return;
|
|
@@ -5366,6 +5689,11 @@ async function runToolRun(repoRoot, name, options) {
|
|
|
5366
5689
|
timeoutMs: options.timeout ? Number(options.timeout) : void 0
|
|
5367
5690
|
});
|
|
5368
5691
|
if (outcome.status === "blocked") {
|
|
5692
|
+
if (options.json) {
|
|
5693
|
+
printJson(outcome);
|
|
5694
|
+
process.exitCode = 1;
|
|
5695
|
+
return;
|
|
5696
|
+
}
|
|
5369
5697
|
if (outcome.reason === "needs-confirmation") {
|
|
5370
5698
|
console.log(`${outcome.message} Re-run with --yes to confirm.`);
|
|
5371
5699
|
} else {
|
|
@@ -5375,6 +5703,13 @@ async function runToolRun(repoRoot, name, options) {
|
|
|
5375
5703
|
return;
|
|
5376
5704
|
}
|
|
5377
5705
|
const { result } = outcome;
|
|
5706
|
+
if (options.json) {
|
|
5707
|
+
printJson(outcome);
|
|
5708
|
+
if (!result.ok) {
|
|
5709
|
+
process.exitCode = result.exitCode ?? 1;
|
|
5710
|
+
}
|
|
5711
|
+
return;
|
|
5712
|
+
}
|
|
5378
5713
|
if (result.stdout) {
|
|
5379
5714
|
process.stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}
|
|
5380
5715
|
`);
|
|
@@ -5412,7 +5747,11 @@ async function runToolsAdd(repoRoot, name, options) {
|
|
|
5412
5747
|
},
|
|
5413
5748
|
{ actor: "human", force: options.force }
|
|
5414
5749
|
);
|
|
5415
|
-
|
|
5750
|
+
if (options.json) {
|
|
5751
|
+
printJson(created);
|
|
5752
|
+
} else {
|
|
5753
|
+
console.log(`Created ${created.scope} tool \`${name}\` at ${created.path}.`);
|
|
5754
|
+
}
|
|
5416
5755
|
}
|
|
5417
5756
|
function deriveNameFromCommand(command) {
|
|
5418
5757
|
const first = command.trim().split(/\s+/)[0] ?? "tool";
|
|
@@ -5432,29 +5771,42 @@ async function runToolsCreate(repoRoot, options) {
|
|
|
5432
5771
|
confirm: options.confirm ?? options.risk === "high"
|
|
5433
5772
|
});
|
|
5434
5773
|
}
|
|
5435
|
-
async function runToolsCheck(repoRoot) {
|
|
5774
|
+
async function runToolsCheck(repoRoot, options = {}) {
|
|
5436
5775
|
const harness = await resolveHarness(repoRoot);
|
|
5437
5776
|
if (harness.tools.length === 0) {
|
|
5438
|
-
|
|
5777
|
+
if (options.json) {
|
|
5778
|
+
printJson({ checks: [] });
|
|
5779
|
+
} else {
|
|
5780
|
+
console.log("No tools defined.");
|
|
5781
|
+
}
|
|
5439
5782
|
return;
|
|
5440
5783
|
}
|
|
5441
5784
|
let failures = 0;
|
|
5785
|
+
const checks = [];
|
|
5442
5786
|
for (const tool of harness.tools) {
|
|
5443
5787
|
const check = await checkToolHealth(repoRoot, tool);
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5788
|
+
checks.push(check);
|
|
5789
|
+
if (!options.json) {
|
|
5790
|
+
if (check.status === "ok") {
|
|
5791
|
+
console.log(`${tool.name}: ok`);
|
|
5792
|
+
} else if (check.status === "skipped") {
|
|
5793
|
+
console.log(`${tool.name}: skipped - ${check.message}`);
|
|
5794
|
+
} else {
|
|
5795
|
+
console.log(`${tool.name}: error - ${check.message}`);
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
if (check.status === "error") {
|
|
5449
5799
|
failures += 1;
|
|
5450
|
-
console.log(`${tool.name}: error - ${check.message}`);
|
|
5451
5800
|
}
|
|
5452
5801
|
}
|
|
5802
|
+
if (options.json) {
|
|
5803
|
+
printJson({ checks });
|
|
5804
|
+
}
|
|
5453
5805
|
if (failures > 0) {
|
|
5454
5806
|
process.exitCode = 1;
|
|
5455
5807
|
}
|
|
5456
5808
|
}
|
|
5457
|
-
async function runToolsDetect(repoRoot) {
|
|
5809
|
+
async function runToolsDetect(repoRoot, options = {}) {
|
|
5458
5810
|
let profile;
|
|
5459
5811
|
try {
|
|
5460
5812
|
profile = (await resolveHarness(repoRoot)).manifest.profile;
|
|
@@ -5464,6 +5816,10 @@ async function runToolsDetect(repoRoot) {
|
|
|
5464
5816
|
}
|
|
5465
5817
|
}
|
|
5466
5818
|
const candidates = await detectToolCandidates(repoRoot, profile ?? "empty");
|
|
5819
|
+
if (options.json) {
|
|
5820
|
+
printJson({ candidates });
|
|
5821
|
+
return;
|
|
5822
|
+
}
|
|
5467
5823
|
if (candidates.length === 0) {
|
|
5468
5824
|
console.log("No starter tools detected.");
|
|
5469
5825
|
return;
|
|
@@ -5477,17 +5833,40 @@ async function runToolsDetect(repoRoot) {
|
|
|
5477
5833
|
}
|
|
5478
5834
|
|
|
5479
5835
|
// src/commands/connections.ts
|
|
5480
|
-
|
|
5836
|
+
function parseList(value) {
|
|
5837
|
+
return (value ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
5838
|
+
}
|
|
5839
|
+
async function runConnectionsList(repoRoot, options = {}) {
|
|
5481
5840
|
let harness;
|
|
5482
5841
|
try {
|
|
5483
5842
|
harness = await resolveHarness(repoRoot);
|
|
5484
5843
|
} catch (error) {
|
|
5485
5844
|
if (error instanceof HarnessError) {
|
|
5486
|
-
|
|
5845
|
+
if (options.json) {
|
|
5846
|
+
printJson({ connections: [], ok: false, error: "harness_missing", message: "No harness found. Run `tr init` first." });
|
|
5847
|
+
} else {
|
|
5848
|
+
console.log("No harness found. Run `tr init` first.");
|
|
5849
|
+
}
|
|
5487
5850
|
return;
|
|
5488
5851
|
}
|
|
5489
5852
|
throw error;
|
|
5490
5853
|
}
|
|
5854
|
+
const connections = harness.connections.map((connection) => ({
|
|
5855
|
+
name: connection.name,
|
|
5856
|
+
origin: connection.origin,
|
|
5857
|
+
provider: connection.manifest.provider,
|
|
5858
|
+
command: connection.manifest.command,
|
|
5859
|
+
profile: connection.manifest.profile,
|
|
5860
|
+
risk: connection.manifest.risk,
|
|
5861
|
+
confirm: connection.manifest.confirm,
|
|
5862
|
+
healthcheck: Boolean(connection.manifest.healthcheck),
|
|
5863
|
+
allow: connection.manifest.allow,
|
|
5864
|
+
deny: connection.manifest.deny
|
|
5865
|
+
}));
|
|
5866
|
+
if (options.json) {
|
|
5867
|
+
printJson({ connections });
|
|
5868
|
+
return;
|
|
5869
|
+
}
|
|
5491
5870
|
if (harness.connections.length === 0) {
|
|
5492
5871
|
console.log("No connections defined. Add one with `tr connections add`.");
|
|
5493
5872
|
return;
|
|
@@ -5514,14 +5893,27 @@ async function runConnectionsAdd(repoRoot, name, options) {
|
|
|
5514
5893
|
risk: options.risk,
|
|
5515
5894
|
confirm: options.confirm,
|
|
5516
5895
|
healthcheck: options.healthcheck,
|
|
5896
|
+
allow: parseList(options.allow),
|
|
5897
|
+
deny: parseList(options.deny),
|
|
5517
5898
|
scope: options.scope
|
|
5518
5899
|
},
|
|
5519
5900
|
{ force: options.force }
|
|
5520
5901
|
);
|
|
5521
|
-
|
|
5902
|
+
if (options.json) {
|
|
5903
|
+
printJson(created);
|
|
5904
|
+
} else {
|
|
5905
|
+
console.log(`Created ${created.manifest.scope} connection \`${name}\` at ${created.path}.`);
|
|
5906
|
+
}
|
|
5522
5907
|
}
|
|
5523
|
-
async function runConnectionsCheck(repoRoot) {
|
|
5908
|
+
async function runConnectionsCheck(repoRoot, options = {}) {
|
|
5524
5909
|
const checks = await checkConnections(repoRoot);
|
|
5910
|
+
if (options.json) {
|
|
5911
|
+
printJson({ checks });
|
|
5912
|
+
if (checks.some((check) => check.status === "error")) {
|
|
5913
|
+
process.exitCode = 1;
|
|
5914
|
+
}
|
|
5915
|
+
return;
|
|
5916
|
+
}
|
|
5525
5917
|
if (checks.length === 0) {
|
|
5526
5918
|
console.log("No connections defined.");
|
|
5527
5919
|
return;
|
|
@@ -5541,46 +5933,46 @@ async function runConnectionsCheck(repoRoot) {
|
|
|
5541
5933
|
// src/cli.ts
|
|
5542
5934
|
function createProgram(repoRoot = process.cwd()) {
|
|
5543
5935
|
const program = new Command();
|
|
5544
|
-
program.name("threadroot").description("Git for your AI agent harness: one command to bootstrap, one .threadroot source.").version(
|
|
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));
|
|
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));
|
|
5936
|
+
program.name("threadroot").description("Git for your AI agent harness: one command to bootstrap, one .threadroot source.").version(THREADROOT_VERSION);
|
|
5937
|
+
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("--packs <list>", "Comma-separated capability packs to install, such as testing,typescript-node.").option("--json", "Print machine-readable JSON.").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));
|
|
5938
|
+
program.command("start").argument("[task]", "Task to prepare context for.").option("--task <task>", "Task to prepare context for.").option("--json", "Print machine-readable JSON.").description("Start a focused Threadroot agent session: doctor, status, context, and command map.").action((task, options) => runStart(repoRoot, task, options));
|
|
5547
5939
|
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));
|
|
5548
5940
|
program.command("expose").argument("[agent]", "Provider(s) to expose: codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--dry-run", "Show project files that would be written.").option("--check", "Check current project exposure state.").option("--undo", "Remove Threadroot-managed project exposure files.").option("--force", "Replace an existing unmanaged threadroot skill.").description("Write thin provider project skills that point agents at `.threadroot/`.").action((agent, options) => runExpose(repoRoot, agent, options));
|
|
5549
5941
|
program.command("setup").option("--global", "Install machine-level Threadroot agent bootstrap skills/config.").option("--agent <list>", "Provider(s): codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--dry-run", "Show global files that would be written.").option("--check", "Check global Threadroot setup state.").option("--undo", "Remove Threadroot-managed global setup files/blocks.").option("--force", "Replace an existing unmanaged threadroot skill.").option("--mcp", "Also add Threadroot MCP to Codex global config when Codex is selected.").description("Set up Threadroot once per machine for supported coding agents.").action((options) => runSetup(repoRoot, options));
|
|
5550
|
-
program.command("status").description("Show harness state, object counts, and compiled-output drift.").action(() => runStatus(repoRoot));
|
|
5942
|
+
program.command("status").description("Show harness state, object counts, and compiled-output drift.").option("--json", "Print machine-readable JSON.").action((options) => runStatus(repoRoot, options));
|
|
5551
5943
|
program.command("diff").description("Show the diff between the canonical harness and each compiled vendor file.").action(() => runDiff(repoRoot));
|
|
5552
|
-
program.command("doctor").description("Check harness validity, compiled output health, MCP hints, and tool trust.").action(() => runDoctor(repoRoot));
|
|
5944
|
+
program.command("doctor").description("Check harness validity, compiled output health, MCP hints, and tool trust.").option("--json", "Print machine-readable JSON.").action((options) => runDoctor(repoRoot, options));
|
|
5553
5945
|
program.command("compile").option("--adapter <adapter>", "Restrict output to one adapter: agents, claude, copilot, or cursor.").description("Compile the canonical harness into vendor files.").action((options) => runCompileCommand(repoRoot, options));
|
|
5554
|
-
program.command("context").argument("<task>", "Task to assemble a relevant harness slice for.").description("Assemble the task-relevant harness slice: skills, rules, tools, and memory.").action((task) => runContext(repoRoot, task));
|
|
5555
|
-
program.command("run").argument("<tool>", "Harness tool name.").option("--input <pair...>", "Tool input as key=value (repeatable).").option("-y, --yes", "Confirm running a tool marked confirm:true.").option("--timeout <ms>", "Override the execution timeout in milliseconds.").description("Execute a harness tool locally.").action((tool, options) => runToolRun(repoRoot, tool, options));
|
|
5946
|
+
program.command("context").argument("<task>", "Task to assemble a relevant harness slice for.").description("Assemble the task-relevant harness slice: skills, rules, tools, and memory.").option("--json", "Print machine-readable JSON.").action((task, options) => runContext(repoRoot, task, options));
|
|
5947
|
+
program.command("run").argument("<tool>", "Harness tool name.").option("--input <pair...>", "Tool input as key=value (repeatable).").option("-y, --yes", "Confirm running a tool marked confirm:true.").option("--timeout <ms>", "Override the execution timeout in milliseconds.").option("--json", "Print machine-readable JSON.").description("Execute a harness tool locally.").action((tool, options) => runToolRun(repoRoot, tool, options));
|
|
5556
5948
|
program.command("install").argument("<source>", "Object source: local path or git (github:owner/repo/path[@ref]).").option("--kind <kind>", "Object kind: skill, tool, rule, or connection (inferred when omitted).").option("--path <path>", "Path to the object within a git source repo.").option("--user", "Install into the user harness (~/.threadroot) instead of the project.").description("Install a harness object from a local path or git source.").action((source, options) => runInstall(repoRoot, source, options));
|
|
5557
5949
|
program.command("remember").argument("<note>", "Durable note to record.").option("--type <type>", "Memory type: project, current-focus, handoff, or pitfalls.").description("Append a durable note to harness memory (defaults to handoff).").action((note, options) => runRemember(repoRoot, note, options));
|
|
5558
5950
|
const memory = program.command("memory").description("Read and append durable harness memory.");
|
|
5559
5951
|
memory.command("read").argument("<type>", "Memory type: project, repo-map, current-focus, handoff, or pitfalls.").description("Print a memory file.").action((type) => runMemoryRead(repoRoot, type));
|
|
5560
5952
|
memory.command("append").argument("<type>", "Memory type: project, repo-map, current-focus, handoff, or pitfalls.").argument("<note>", "Note to append.").description("Append a durable note to memory.").action((type, note) => runMemoryAppend(repoRoot, type, note));
|
|
5561
5953
|
const tools2 = program.command("tools").description("Manage executable harness tools.");
|
|
5562
|
-
tools2.command("list").description("List harness tools.").action(() => runToolsList(repoRoot));
|
|
5563
|
-
tools2.command("check").description("Run configured tool healthchecks.").action(() => runToolsCheck(repoRoot));
|
|
5564
|
-
tools2.command("detect").description("Propose starter tools from the repo's existing command surface.").action(() => runToolsDetect(repoRoot));
|
|
5565
|
-
tools2.command("add").argument("<name>", "Tool name (lowercase, hyphenated).").requiredOption("--description <text>", "What the tool does.").option("--run <command>", "Shell command (use {{param}} for inputs).").option("--script <path>", "Harness-relative script path (alternative to --run).").option("--risk <risk>", "Risk level: low, medium, or high.").option("--connection <name>", "Connection this tool depends on.").option("--healthcheck <command>", "Command that verifies the tool is available.").option("--confirm", "Require confirmation before running.").option("--scope <scope>", "user or project.").option("--force", "Overwrite an existing tool.").description("Author a new harness tool.").action((name, options) => runToolsAdd(repoRoot, name, options));
|
|
5566
|
-
tools2.command("create").option("--from-command <command>", "Create a tool around an existing command.").option("--description <text>", "What the tool does.").option("--risk <risk>", "Risk level: low, medium, or high.").option("--connection <name>", "Connection this tool depends on.").option("--healthcheck <command>", "Command that verifies the tool is available.").option("--confirm", "Require confirmation before running.").option("--scope <scope>", "user or project.").option("--force", "Overwrite an existing tool.").description("Guided safe tool builder.").action((options) => runToolsCreate(repoRoot, options));
|
|
5954
|
+
tools2.command("list").option("--json", "Print machine-readable JSON.").description("List harness tools.").action((options) => runToolsList(repoRoot, options));
|
|
5955
|
+
tools2.command("check").option("--json", "Print machine-readable JSON.").description("Run configured tool healthchecks.").action((options) => runToolsCheck(repoRoot, options));
|
|
5956
|
+
tools2.command("detect").option("--json", "Print machine-readable JSON.").description("Propose starter tools from the repo's existing command surface.").action((options) => runToolsDetect(repoRoot, options));
|
|
5957
|
+
tools2.command("add").argument("<name>", "Tool name (lowercase, hyphenated).").requiredOption("--description <text>", "What the tool does.").option("--run <command>", "Shell command (use {{param}} for inputs).").option("--script <path>", "Harness-relative script path (alternative to --run).").option("--risk <risk>", "Risk level: low, medium, or high.").option("--connection <name>", "Connection this tool depends on.").option("--healthcheck <command>", "Command that verifies the tool is available.").option("--confirm", "Require confirmation before running.").option("--scope <scope>", "user or project.").option("--force", "Overwrite an existing tool.").option("--json", "Print machine-readable JSON.").description("Author a new harness tool.").action((name, options) => runToolsAdd(repoRoot, name, options));
|
|
5958
|
+
tools2.command("create").option("--from-command <command>", "Create a tool around an existing command.").option("--description <text>", "What the tool does.").option("--risk <risk>", "Risk level: low, medium, or high.").option("--connection <name>", "Connection this tool depends on.").option("--healthcheck <command>", "Command that verifies the tool is available.").option("--confirm", "Require confirmation before running.").option("--scope <scope>", "user or project.").option("--force", "Overwrite an existing tool.").option("--json", "Print machine-readable JSON.").description("Guided safe tool builder.").action((options) => runToolsCreate(repoRoot, options));
|
|
5567
5959
|
const connections = program.command("connections").description("Manage local CLI connections.");
|
|
5568
|
-
connections.command("list").description("List harness connections.").action(() => runConnectionsList(repoRoot));
|
|
5569
|
-
connections.command("check").description("Run configured connection healthchecks.").action(() => runConnectionsCheck(repoRoot));
|
|
5570
|
-
connections.command("add").argument("<name>", "Connection name (lowercase, hyphenated).").requiredOption("--provider <provider>", "Provider name, such as aws, github, azure, or snowflake.").requiredOption("--command <command>", "Local CLI command, such as aws, gh, az, or snow.").option("--description <text>", "What this connection is for.").option("--profile <profile>", "Local CLI profile/account label.").option("--risk <risk>", "Risk level: low, medium, or high.").option("--confirm", "Require confirmation before connection-backed tools run.").option("--healthcheck <command>", "Command that verifies the connection works.").option("--scope <scope>", "user or project.").option("--force", "Overwrite an existing connection.").description("Author a local CLI connection manifest.").action((name, options) => runConnectionsAdd(repoRoot, name, options));
|
|
5960
|
+
connections.command("list").option("--json", "Print machine-readable JSON.").description("List harness connections.").action((options) => runConnectionsList(repoRoot, options));
|
|
5961
|
+
connections.command("check").option("--json", "Print machine-readable JSON.").description("Run configured connection healthchecks.").action((options) => runConnectionsCheck(repoRoot, options));
|
|
5962
|
+
connections.command("add").argument("<name>", "Connection name (lowercase, hyphenated).").requiredOption("--provider <provider>", "Provider name, such as aws, github, azure, or snowflake.").requiredOption("--command <command>", "Local CLI command, such as aws, gh, az, or snow.").option("--description <text>", "What this connection is for.").option("--profile <profile>", "Local CLI profile/account label.").option("--risk <risk>", "Risk level: low, medium, or high.").option("--confirm", "Require confirmation before connection-backed tools run.").option("--healthcheck <command>", "Command that verifies the connection works.").option("--allow <patterns>", "Comma-separated allowed command fragments for this connection.").option("--deny <patterns>", "Comma-separated denied command fragments for this connection.").option("--scope <scope>", "user or project.").option("--force", "Overwrite an existing connection.").option("--json", "Print machine-readable JSON.").description("Author a local CLI connection manifest.").action((name, options) => runConnectionsAdd(repoRoot, name, options));
|
|
5571
5963
|
const packs = program.command("packs").description("Inspect, validate, and install capability packs.");
|
|
5572
|
-
packs.command("list").description("List built-in and repo-local packs.").action(() => runPacksList(repoRoot));
|
|
5573
|
-
packs.command("inspect").argument("<name-or-path>", "Built-in pack name or repo-relative pack path.").description("Inspect a capability pack.").action((nameOrPath) => runPacksInspect(repoRoot, nameOrPath));
|
|
5574
|
-
packs.command("validate").argument("<name-or-path>", "Built-in pack name or repo-relative pack path.").description("Validate a capability pack.").action((nameOrPath) => runPacksValidate(repoRoot, nameOrPath));
|
|
5575
|
-
packs.command("install").argument("<name-or-path>", "Built-in pack name or repo-relative pack path.").description("Install a capability pack into the project harness.").action((nameOrPath) => runPacksInstall(repoRoot, nameOrPath));
|
|
5964
|
+
packs.command("list").option("--json", "Print machine-readable JSON.").description("List built-in and repo-local packs.").action((options) => runPacksList(repoRoot, options));
|
|
5965
|
+
packs.command("inspect").argument("<name-or-path>", "Built-in pack name or repo-relative pack path.").option("--json", "Print machine-readable JSON.").description("Inspect a capability pack.").action((nameOrPath, options) => runPacksInspect(repoRoot, nameOrPath, options));
|
|
5966
|
+
packs.command("validate").argument("<name-or-path>", "Built-in pack name or repo-relative pack path.").option("--json", "Print machine-readable JSON.").description("Validate a capability pack.").action((nameOrPath, options) => runPacksValidate(repoRoot, nameOrPath, options));
|
|
5967
|
+
packs.command("install").argument("<name-or-path>", "Built-in pack name or repo-relative pack path.").option("--json", "Print machine-readable JSON.").description("Install a capability pack into the project harness.").action((nameOrPath, options) => runPacksInstall(repoRoot, nameOrPath, options));
|
|
5576
5968
|
const skills = program.command("skills").description("Inspect and validate harness skills.");
|
|
5577
|
-
skills.command("list").description("List harness skills.").action(() => runSkillsList(repoRoot));
|
|
5578
|
-
skills.command("inspect").argument("<path>", "Repo-relative skill file or skill directory.").description("Inspect a skill's metadata, references, scripts, assets, and eval files.").action((targetPath) => runSkillsInspect(repoRoot, targetPath));
|
|
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));
|
|
5969
|
+
skills.command("list").option("--json", "Print machine-readable JSON.").description("List harness skills.").action((options) => runSkillsList(repoRoot, options));
|
|
5970
|
+
skills.command("inspect").argument("<path>", "Repo-relative skill file or skill directory.").option("--json", "Print machine-readable JSON.").description("Inspect a skill's metadata, references, scripts, assets, and eval files.").action((targetPath, options) => runSkillsInspect(repoRoot, targetPath, options));
|
|
5971
|
+
skills.command("validate").option("--path <path>", "Validate a repo-relative skill file, skill directory, or skill collection.").option("--json", "Print machine-readable JSON.").description("Validate skill frontmatter, naming, trigger descriptions, and progressive-disclosure hygiene.").action((options) => runSkillsValidate(repoRoot, options));
|
|
5580
5972
|
const mcp = program.command("mcp").description("Run or configure the local Threadroot MCP server.");
|
|
5581
5973
|
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));
|
|
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));
|
|
5974
|
+
mcp.command("check").option("--timeout <ms>", "Handshake timeout in milliseconds.").option("--json", "Print machine-readable JSON.").description("Verify Codex MCP config and the Threadroot stdio server handshake.").action((options) => runMcpCheck(repoRoot, options));
|
|
5975
|
+
mcp.command("setup").option("--agent <agent>", "all, generic, codex, copilot, cursor, or claude.").option("--write", "Write project-local MCP config files for the agents.").option("--json", "Print machine-readable JSON.").description("Print MCP config snippets and a pasteable agent bootstrap prompt.").action((options) => runMcpSetup(repoRoot, options));
|
|
5584
5976
|
return program;
|
|
5585
5977
|
}
|
|
5586
5978
|
|