threadroot 0.1.4 → 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 +17 -0
- package/INTEGRATION.md +256 -0
- package/README.md +36 -21
- package/dist/index.js +933 -555
- 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) {
|
|
@@ -2401,7 +2466,7 @@ import { homedir as homedir2, tmpdir } from "os";
|
|
|
2401
2466
|
import path14 from "path";
|
|
2402
2467
|
|
|
2403
2468
|
// src/core/version.ts
|
|
2404
|
-
var THREADROOT_VERSION = "0.1.
|
|
2469
|
+
var THREADROOT_VERSION = "0.1.5";
|
|
2405
2470
|
|
|
2406
2471
|
// src/core/mcp-check.ts
|
|
2407
2472
|
var REQUIRED_MCP_TOOLS = [
|
|
@@ -3278,7 +3343,7 @@ async function readIfExists2(filePath) {
|
|
|
3278
3343
|
throw error;
|
|
3279
3344
|
}
|
|
3280
3345
|
}
|
|
3281
|
-
function
|
|
3346
|
+
function normalize2(text) {
|
|
3282
3347
|
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
3283
3348
|
}
|
|
3284
3349
|
function splitSections(markdown) {
|
|
@@ -3294,7 +3359,7 @@ function splitSections(markdown) {
|
|
|
3294
3359
|
for (const line of markdown.split(/\r?\n/)) {
|
|
3295
3360
|
if (/^#{1,6}\s/.test(line)) {
|
|
3296
3361
|
flush();
|
|
3297
|
-
heading =
|
|
3362
|
+
heading = normalize2(line.replace(/^#+\s*/, ""));
|
|
3298
3363
|
buffer = [line];
|
|
3299
3364
|
} else {
|
|
3300
3365
|
buffer.push(line);
|
|
@@ -3304,13 +3369,13 @@ function splitSections(markdown) {
|
|
|
3304
3369
|
return sections;
|
|
3305
3370
|
}
|
|
3306
3371
|
function novelSections(canonical, other) {
|
|
3307
|
-
const haystack =
|
|
3372
|
+
const haystack = normalize2(canonical);
|
|
3308
3373
|
const seenHeadings = new Set(splitSections(canonical).map((section) => section.heading).filter(Boolean));
|
|
3309
3374
|
return splitSections(other).filter((section) => {
|
|
3310
3375
|
if (section.heading && seenHeadings.has(section.heading)) {
|
|
3311
3376
|
return false;
|
|
3312
3377
|
}
|
|
3313
|
-
return !haystack.includes(
|
|
3378
|
+
return !haystack.includes(normalize2(section.text));
|
|
3314
3379
|
});
|
|
3315
3380
|
}
|
|
3316
3381
|
async function listCursorRules(repoRoot) {
|
|
@@ -3544,43 +3609,32 @@ async function initHarness(repoRoot, options = {}) {
|
|
|
3544
3609
|
return { name, profile, adapters, skills, tools: tools2, memory, rules, import: report, compiled: written, exposed };
|
|
3545
3610
|
}
|
|
3546
3611
|
|
|
3547
|
-
// src/core/
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
},
|
|
3574
|
-
drift
|
|
3575
|
-
};
|
|
3576
|
-
}
|
|
3577
|
-
|
|
3578
|
-
// src/core/bootstrap.ts
|
|
3579
|
-
var DEFAULT_TASK = "start this project";
|
|
3580
|
-
async function harnessExists(repoRoot) {
|
|
3581
|
-
return pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
|
|
3582
|
-
}
|
|
3583
|
-
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) {
|
|
3584
3638
|
try {
|
|
3585
3639
|
await stat9(target);
|
|
3586
3640
|
return true;
|
|
@@ -3591,155 +3645,530 @@ async function pathExists2(target) {
|
|
|
3591
3645
|
throw error;
|
|
3592
3646
|
}
|
|
3593
3647
|
}
|
|
3594
|
-
function
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
const task = options.task?.trim() || DEFAULT_TASK;
|
|
3599
|
-
const mode = modeFor(options);
|
|
3600
|
-
const write2 = mode === "write";
|
|
3601
|
-
const notes = [];
|
|
3602
|
-
const existed = await harnessExists(repoRoot);
|
|
3603
|
-
let setup;
|
|
3604
|
-
let init;
|
|
3605
|
-
let exposed;
|
|
3606
|
-
if (!options.noGlobal) {
|
|
3607
|
-
setup = await setupGlobal({
|
|
3608
|
-
agents: options.agents ?? "all",
|
|
3609
|
-
mode,
|
|
3610
|
-
home: options.home,
|
|
3611
|
-
mcp: options.mcp,
|
|
3612
|
-
mcpEntry: options.mcpEntry
|
|
3613
|
-
});
|
|
3614
|
-
} else {
|
|
3615
|
-
notes.push("Skipped global setup because --no-global was set.");
|
|
3616
|
-
}
|
|
3617
|
-
if (!existed && !options.noInit) {
|
|
3618
|
-
if (write2) {
|
|
3619
|
-
init = await initHarness(repoRoot, {
|
|
3620
|
-
import: options.import,
|
|
3621
|
-
profile: options.profile,
|
|
3622
|
-
home: options.home
|
|
3623
|
-
});
|
|
3624
|
-
} else {
|
|
3625
|
-
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;
|
|
3626
3652
|
}
|
|
3627
|
-
} else if (existed) {
|
|
3628
|
-
notes.push("Existing harness detected; bootstrap will not reinitialize it.");
|
|
3629
|
-
} else {
|
|
3630
|
-
notes.push("Skipped project initialization because --no-init was set.");
|
|
3631
|
-
}
|
|
3632
|
-
const hasHarnessAfterInit = existed || Boolean(init) || await pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
|
|
3633
|
-
if (options.expose) {
|
|
3634
|
-
exposed = await exposeProject(repoRoot, {
|
|
3635
|
-
agents: options.expose,
|
|
3636
|
-
mode
|
|
3637
|
-
});
|
|
3638
3653
|
}
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
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;
|
|
3645
3665
|
}
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
if (status.exists) {
|
|
3650
|
-
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;
|
|
3651
3669
|
}
|
|
3652
|
-
} else {
|
|
3653
|
-
notes.push("Skipped doctor/status/context because no harness exists yet.");
|
|
3654
3670
|
}
|
|
3655
|
-
|
|
3656
|
-
|
|
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}`);
|
|
3657
3677
|
}
|
|
3658
|
-
return
|
|
3659
|
-
mode: write2 ? "write" : "plan",
|
|
3660
|
-
task,
|
|
3661
|
-
harnessExisted: existed,
|
|
3662
|
-
setup,
|
|
3663
|
-
init,
|
|
3664
|
-
expose: exposed,
|
|
3665
|
-
status,
|
|
3666
|
-
doctor: doctorReport,
|
|
3667
|
-
context,
|
|
3668
|
-
mcpCheck,
|
|
3669
|
-
notes
|
|
3670
|
-
};
|
|
3678
|
+
return normalized;
|
|
3671
3679
|
}
|
|
3672
|
-
async function
|
|
3673
|
-
const
|
|
3674
|
-
const
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
task,
|
|
3679
|
-
status,
|
|
3680
|
-
notes: ["No harness found. Run `threadroot bootstrap --yes` first."]
|
|
3681
|
-
};
|
|
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}`);
|
|
3682
3686
|
}
|
|
3683
|
-
|
|
3684
|
-
const context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
|
|
3685
|
-
return { task, status, doctor: doctorReport, context, notes };
|
|
3687
|
+
return parsed.data;
|
|
3686
3688
|
}
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
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;
|
|
3692
3702
|
}
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
for (const finding3 of report.findings.slice(0, 8)) {
|
|
3696
|
-
const label = finding3.severity === "info" ? "hint" : finding3.severity;
|
|
3697
|
-
const suffix = finding3.path ? ` (${finding3.path})` : "";
|
|
3698
|
-
console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
|
|
3703
|
+
if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
|
|
3704
|
+
return toRepoPath(repoRoot, nameOrPath);
|
|
3699
3705
|
}
|
|
3700
|
-
|
|
3701
|
-
|
|
3706
|
+
const bundled = await bundledPacksDir();
|
|
3707
|
+
if (bundled) {
|
|
3708
|
+
return path23.join(bundled, nameOrPath);
|
|
3702
3709
|
}
|
|
3710
|
+
return toRepoPath(repoRoot, path23.join("packs", nameOrPath));
|
|
3703
3711
|
}
|
|
3704
|
-
function
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
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;
|
|
3711
3721
|
}
|
|
3712
|
-
console.log(`harness: ${status.manifest.name} (${status.manifest.profile})`);
|
|
3713
|
-
console.log(`adapters: ${status.manifest.adapters.length > 0 ? status.manifest.adapters.join(", ") : "none (local-only)"}`);
|
|
3714
|
-
console.log(
|
|
3715
|
-
`objects: ${status.counts.skills} skills, ${status.counts.rules} rules, ${status.counts.tools} tools, ${status.counts.memory} memory`
|
|
3716
|
-
);
|
|
3717
3722
|
}
|
|
3718
|
-
function
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
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
|
+
}
|
|
3728
3735
|
}
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
console.log("available tools:");
|
|
3734
|
-
for (const tool of context.tools.slice(0, 8)) {
|
|
3735
|
-
console.log(`- ${tool.name} (${tool.risk}) - ${tool.description}`);
|
|
3736
|
+
return result;
|
|
3737
|
+
} catch (error) {
|
|
3738
|
+
if (error.code === "ENOENT") {
|
|
3739
|
+
return [];
|
|
3736
3740
|
}
|
|
3737
|
-
|
|
3738
|
-
if (context.memory.length > 0) {
|
|
3739
|
-
console.log(`memory: ${context.memory.map((entry) => entry.type).join(", ")}`);
|
|
3741
|
+
throw error;
|
|
3740
3742
|
}
|
|
3741
3743
|
}
|
|
3742
|
-
function
|
|
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:");
|
|
4163
|
+
for (const tool of context.tools.slice(0, 8)) {
|
|
4164
|
+
console.log(`- ${tool.name} (${tool.risk}) - ${tool.description}`);
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
if (context.memory.length > 0) {
|
|
4168
|
+
console.log(`memory: ${context.memory.map((entry) => entry.type).join(", ")}`);
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
function printCommandMap() {
|
|
3743
4172
|
console.log("agent command map:");
|
|
3744
4173
|
console.log('- `threadroot start "<task>"` - begin a focused agent session');
|
|
3745
4174
|
console.log('- `threadroot context "<task>"` - get relevant skills, tools, rules, and memory');
|
|
@@ -3781,6 +4210,9 @@ function printBootstrapReport(report) {
|
|
|
3781
4210
|
console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
|
|
3782
4211
|
}
|
|
3783
4212
|
}
|
|
4213
|
+
if (report.packs && report.packs.length > 0) {
|
|
4214
|
+
console.log(`packs: ${report.packs.map((pack) => pack.name).join(", ")}`);
|
|
4215
|
+
}
|
|
3784
4216
|
printStatus(report.status);
|
|
3785
4217
|
printMcpCheck(report.mcpCheck);
|
|
3786
4218
|
printDoctor(report.doctor);
|
|
@@ -3813,13 +4245,18 @@ async function runBootstrap(repoRoot, options) {
|
|
|
3813
4245
|
task: options.task,
|
|
3814
4246
|
mcp: options.mcp,
|
|
3815
4247
|
expose: options.expose,
|
|
4248
|
+
packs: options.packs,
|
|
3816
4249
|
noGlobal: options.global === false,
|
|
3817
4250
|
noInit: options.init === false,
|
|
3818
4251
|
import: options.import,
|
|
3819
4252
|
profile: options.profile ? profileIdSchema.parse(options.profile) : void 0,
|
|
3820
4253
|
mcpEntry: options.mcp ? mcpEntryForCurrentProcess() : void 0
|
|
3821
4254
|
});
|
|
3822
|
-
|
|
4255
|
+
if (options.json) {
|
|
4256
|
+
printJson(report);
|
|
4257
|
+
} else {
|
|
4258
|
+
printBootstrapReport(report);
|
|
4259
|
+
}
|
|
3823
4260
|
if (report.mode === "write" && report.doctor && !report.doctor.ok) {
|
|
3824
4261
|
process.exitCode = 1;
|
|
3825
4262
|
}
|
|
@@ -3846,17 +4283,25 @@ async function runCompileCommand(repoRoot, options) {
|
|
|
3846
4283
|
}
|
|
3847
4284
|
|
|
3848
4285
|
// src/commands/context.ts
|
|
3849
|
-
async function runContext(repoRoot, task) {
|
|
4286
|
+
async function runContext(repoRoot, task, options = {}) {
|
|
3850
4287
|
let context;
|
|
3851
4288
|
try {
|
|
3852
4289
|
context = await assembleContext(repoRoot, task);
|
|
3853
4290
|
} catch (error) {
|
|
3854
4291
|
if (error instanceof HarnessError) {
|
|
3855
|
-
|
|
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
|
+
}
|
|
3856
4297
|
return;
|
|
3857
4298
|
}
|
|
3858
4299
|
throw error;
|
|
3859
4300
|
}
|
|
4301
|
+
if (options.json) {
|
|
4302
|
+
printJson(context);
|
|
4303
|
+
return;
|
|
4304
|
+
}
|
|
3860
4305
|
console.log(`task: ${context.task}`);
|
|
3861
4306
|
if (context.skills.length > 0) {
|
|
3862
4307
|
console.log("\nskills:");
|
|
@@ -3889,7 +4334,7 @@ async function runContext(repoRoot, task) {
|
|
|
3889
4334
|
|
|
3890
4335
|
// src/commands/diff.ts
|
|
3891
4336
|
import fs3 from "fs/promises";
|
|
3892
|
-
import
|
|
4337
|
+
import path25 from "path";
|
|
3893
4338
|
async function readIfExists3(filePath) {
|
|
3894
4339
|
try {
|
|
3895
4340
|
return await fs3.readFile(filePath, "utf8");
|
|
@@ -3948,7 +4393,7 @@ async function runDiff(repoRoot) {
|
|
|
3948
4393
|
const files = await compile(repoRoot, harness);
|
|
3949
4394
|
let changed = 0;
|
|
3950
4395
|
for (const file of files) {
|
|
3951
|
-
const existing = await readIfExists3(
|
|
4396
|
+
const existing = await readIfExists3(path25.join(repoRoot, file.path));
|
|
3952
4397
|
if (existing === void 0) {
|
|
3953
4398
|
changed += 1;
|
|
3954
4399
|
console.log(`+ ${file.path} (new)`);
|
|
@@ -3969,8 +4414,15 @@ async function runDiff(repoRoot) {
|
|
|
3969
4414
|
}
|
|
3970
4415
|
|
|
3971
4416
|
// src/commands/doctor.ts
|
|
3972
|
-
async function runDoctor(repoRoot) {
|
|
4417
|
+
async function runDoctor(repoRoot, options = {}) {
|
|
3973
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
|
+
}
|
|
3974
4426
|
const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
|
|
3975
4427
|
const hints = report.findings.filter((finding3) => finding3.severity === "info");
|
|
3976
4428
|
if (actionable.length === 0) {
|
|
@@ -4063,13 +4515,13 @@ async function runInit(repoRoot, options) {
|
|
|
4063
4515
|
}
|
|
4064
4516
|
|
|
4065
4517
|
// src/commands/install.ts
|
|
4066
|
-
import
|
|
4518
|
+
import path28 from "path";
|
|
4067
4519
|
|
|
4068
4520
|
// src/core/install/fetch.ts
|
|
4069
4521
|
import { execFile } from "child_process";
|
|
4070
4522
|
import { mkdtemp as mkdtemp2, rm as rm4 } from "fs/promises";
|
|
4071
4523
|
import os2 from "os";
|
|
4072
|
-
import
|
|
4524
|
+
import path26 from "path";
|
|
4073
4525
|
import { promisify } from "util";
|
|
4074
4526
|
var run = promisify(execFile);
|
|
4075
4527
|
function cloneUrl(ref) {
|
|
@@ -4090,7 +4542,7 @@ async function git(cwd, args) {
|
|
|
4090
4542
|
}
|
|
4091
4543
|
async function fetchGitSource(ref) {
|
|
4092
4544
|
const url = cloneUrl(ref);
|
|
4093
|
-
const dir = await mkdtemp2(
|
|
4545
|
+
const dir = await mkdtemp2(path26.join(os2.tmpdir(), "threadroot-fetch-"));
|
|
4094
4546
|
const cleanup = () => rm4(dir, { recursive: true, force: true });
|
|
4095
4547
|
try {
|
|
4096
4548
|
if (ref.ref) {
|
|
@@ -4112,9 +4564,9 @@ async function fetchGitSource(ref) {
|
|
|
4112
4564
|
}
|
|
4113
4565
|
|
|
4114
4566
|
// src/core/install/install.ts
|
|
4115
|
-
import { cp as
|
|
4116
|
-
import { createHash as
|
|
4117
|
-
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";
|
|
4118
4570
|
var NAME_RE5 = /^[a-z0-9][a-z0-9-]*$/;
|
|
4119
4571
|
var KIND_DIR = {
|
|
4120
4572
|
skill: "skills",
|
|
@@ -4126,8 +4578,8 @@ function objectExt(kind) {
|
|
|
4126
4578
|
return kind === "tool" || kind === "connection" ? ".yaml" : ".md";
|
|
4127
4579
|
}
|
|
4128
4580
|
function safeRepoPath(objectPath) {
|
|
4129
|
-
const normalized =
|
|
4130
|
-
if (
|
|
4581
|
+
const normalized = path27.normalize(objectPath);
|
|
4582
|
+
if (path27.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path27.sep}`)) {
|
|
4131
4583
|
throw new Error(`Unsafe object path: ${objectPath}`);
|
|
4132
4584
|
}
|
|
4133
4585
|
return normalized;
|
|
@@ -4141,7 +4593,7 @@ function inferKind(objectPath, override) {
|
|
|
4141
4593
|
if (segments.includes("tools")) return "tool";
|
|
4142
4594
|
if (segments.includes("connections")) return "connection";
|
|
4143
4595
|
if (segments.includes("rules")) return "rule";
|
|
4144
|
-
const ext =
|
|
4596
|
+
const ext = path27.extname(objectPath).toLowerCase();
|
|
4145
4597
|
if (ext === ".yaml" || ext === ".yml") return "tool";
|
|
4146
4598
|
if (ext === ".md") return "skill";
|
|
4147
4599
|
throw new Error(
|
|
@@ -4149,20 +4601,20 @@ function inferKind(objectPath, override) {
|
|
|
4149
4601
|
);
|
|
4150
4602
|
}
|
|
4151
4603
|
function deriveName(objectPath) {
|
|
4152
|
-
const base =
|
|
4604
|
+
const base = path27.basename(objectPath, path27.extname(objectPath));
|
|
4153
4605
|
if (!NAME_RE5.test(base)) {
|
|
4154
4606
|
throw new Error(`Invalid object name \`${base}\` (use lowercase letters, digits, and dashes).`);
|
|
4155
4607
|
}
|
|
4156
4608
|
return base;
|
|
4157
4609
|
}
|
|
4158
|
-
async function
|
|
4159
|
-
const hash =
|
|
4610
|
+
async function hashDirectory2(root) {
|
|
4611
|
+
const hash = createHash3("sha256");
|
|
4160
4612
|
hash.update("threadroot-directory-v1\n");
|
|
4161
4613
|
async function walk(dir) {
|
|
4162
|
-
const entries = (await
|
|
4614
|
+
const entries = (await readdir6(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
4163
4615
|
for (const entry of entries) {
|
|
4164
|
-
const full =
|
|
4165
|
-
const rel =
|
|
4616
|
+
const full = path27.join(dir, entry.name);
|
|
4617
|
+
const rel = path27.relative(root, full).split(path27.sep).join("/");
|
|
4166
4618
|
if (entry.isSymbolicLink()) {
|
|
4167
4619
|
throw new Error(`Refusing to install skill directory with symlink: ${rel}`);
|
|
4168
4620
|
}
|
|
@@ -4173,7 +4625,7 @@ async function hashDirectory(root) {
|
|
|
4173
4625
|
if (entry.isFile()) {
|
|
4174
4626
|
hash.update(`file:${rel}
|
|
4175
4627
|
`);
|
|
4176
|
-
hash.update(await
|
|
4628
|
+
hash.update(await readFile12(full));
|
|
4177
4629
|
hash.update("\n");
|
|
4178
4630
|
}
|
|
4179
4631
|
}
|
|
@@ -4182,8 +4634,8 @@ async function hashDirectory(root) {
|
|
|
4182
4634
|
return hash.digest("hex");
|
|
4183
4635
|
}
|
|
4184
4636
|
async function validateSkillDirectory2(sourcePath, expectedName) {
|
|
4185
|
-
const skillPath =
|
|
4186
|
-
const parsed = parseFrontmatter(await
|
|
4637
|
+
const skillPath = path27.join(sourcePath, "SKILL.md");
|
|
4638
|
+
const parsed = parseFrontmatter(await readFile12(skillPath, "utf8"));
|
|
4187
4639
|
const result = skillFrontmatterSchema.safeParse(parsed.data);
|
|
4188
4640
|
if (!result.success) {
|
|
4189
4641
|
const detail = result.error.issues.map((issue) => issue.message).join("; ");
|
|
@@ -4192,7 +4644,7 @@ async function validateSkillDirectory2(sourcePath, expectedName) {
|
|
|
4192
4644
|
if (result.data.name !== expectedName) {
|
|
4193
4645
|
throw new Error(`Skill directory name \`${expectedName}\` must match SKILL.md name \`${result.data.name}\`.`);
|
|
4194
4646
|
}
|
|
4195
|
-
return await
|
|
4647
|
+
return await hashDirectory2(sourcePath);
|
|
4196
4648
|
}
|
|
4197
4649
|
async function installObject(repoRoot, rawSource, options = {}) {
|
|
4198
4650
|
const ref = parseSourceRef(rawSource);
|
|
@@ -4212,7 +4664,7 @@ async function installObject(repoRoot, rawSource, options = {}) {
|
|
|
4212
4664
|
objectPath = safeRepoPath(within);
|
|
4213
4665
|
refLabel = ref.ref;
|
|
4214
4666
|
const fetched = await fetchGitSource(ref);
|
|
4215
|
-
sourcePath =
|
|
4667
|
+
sourcePath = path27.join(fetched.dir, objectPath);
|
|
4216
4668
|
resolved = fetched.sha;
|
|
4217
4669
|
cleanup = fetched.cleanup;
|
|
4218
4670
|
} else {
|
|
@@ -4227,19 +4679,19 @@ async function installObject(repoRoot, rawSource, options = {}) {
|
|
|
4227
4679
|
const destDir = scope === "user" ? userObjectDir(dirKey, options.home) : projectObjectDir(repoRoot, dirKey);
|
|
4228
4680
|
let destPath;
|
|
4229
4681
|
let integrity;
|
|
4230
|
-
const info = await
|
|
4682
|
+
const info = await stat11(sourcePath);
|
|
4231
4683
|
if (info.isDirectory()) {
|
|
4232
4684
|
if (kind !== "skill") {
|
|
4233
4685
|
throw new Error("Only skill objects may be installed from a directory.");
|
|
4234
4686
|
}
|
|
4235
4687
|
integrity = `sha256:${await validateSkillDirectory2(sourcePath, name)}`;
|
|
4236
|
-
destPath =
|
|
4237
|
-
await
|
|
4238
|
-
await
|
|
4688
|
+
destPath = path27.join(destDir, name);
|
|
4689
|
+
await mkdir11(destDir, { recursive: true });
|
|
4690
|
+
await cp3(sourcePath, destPath, { recursive: true, force: true });
|
|
4239
4691
|
} else {
|
|
4240
|
-
const content = await
|
|
4241
|
-
destPath =
|
|
4242
|
-
await
|
|
4692
|
+
const content = await readFile12(sourcePath, "utf8");
|
|
4693
|
+
destPath = path27.join(destDir, `${name}${objectExt(kind)}`);
|
|
4694
|
+
await mkdir11(destDir, { recursive: true });
|
|
4243
4695
|
await writeFile10(destPath, content, "utf8");
|
|
4244
4696
|
integrity = `sha256:${hashContent(content)}`;
|
|
4245
4697
|
}
|
|
@@ -4292,7 +4744,7 @@ async function runInstall(repoRoot, source, options) {
|
|
|
4292
4744
|
if (installed.kind === "skill" && installed.entry.sourceKind !== "local") {
|
|
4293
4745
|
console.log(" note: inspect external skills before trusting bundled scripts, assets, or allowed tools.");
|
|
4294
4746
|
if (scope === "project") {
|
|
4295
|
-
console.log(` inspect: threadroot skills inspect ${
|
|
4747
|
+
console.log(` inspect: threadroot skills inspect ${path28.relative(repoRoot, installed.path)}`);
|
|
4296
4748
|
}
|
|
4297
4749
|
}
|
|
4298
4750
|
} catch (error) {
|
|
@@ -4304,7 +4756,7 @@ async function runInstall(repoRoot, source, options) {
|
|
|
4304
4756
|
// src/mcp/server.ts
|
|
4305
4757
|
import readline from "readline";
|
|
4306
4758
|
import { stdin as input, stdout as output } from "process";
|
|
4307
|
-
import { z as
|
|
4759
|
+
import { z as z5 } from "zod";
|
|
4308
4760
|
function defineTool(spec) {
|
|
4309
4761
|
return spec;
|
|
4310
4762
|
}
|
|
@@ -4316,7 +4768,7 @@ var toolRegistry = [
|
|
|
4316
4768
|
{ task: { type: "string", description: "The coding task to assemble context for." } },
|
|
4317
4769
|
["task"]
|
|
4318
4770
|
),
|
|
4319
|
-
args:
|
|
4771
|
+
args: z5.object({ task: z5.string().min(1) }),
|
|
4320
4772
|
run: async (repoRoot, args) => {
|
|
4321
4773
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4322
4774
|
if (!harness) {
|
|
@@ -4329,7 +4781,7 @@ var toolRegistry = [
|
|
|
4329
4781
|
name: "skills_list",
|
|
4330
4782
|
description: "List the skills defined in this repo's harness (name, when, tags).",
|
|
4331
4783
|
inputSchema: objectSchema({}),
|
|
4332
|
-
args:
|
|
4784
|
+
args: z5.object({}),
|
|
4333
4785
|
run: async (repoRoot) => {
|
|
4334
4786
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4335
4787
|
if (!harness) {
|
|
@@ -4349,7 +4801,7 @@ var toolRegistry = [
|
|
|
4349
4801
|
name: "skills_get",
|
|
4350
4802
|
description: "Return a harness skill's full body and metadata by name.",
|
|
4351
4803
|
inputSchema: objectSchema({ name: { type: "string", description: "Skill name." } }, ["name"]),
|
|
4352
|
-
args:
|
|
4804
|
+
args: z5.object({ name: z5.string().min(1) }),
|
|
4353
4805
|
run: async (repoRoot, args) => {
|
|
4354
4806
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4355
4807
|
const skill = harness?.skills.find((entry) => entry.name === args.name);
|
|
@@ -4361,9 +4813,9 @@ var toolRegistry = [
|
|
|
4361
4813
|
}),
|
|
4362
4814
|
defineTool({
|
|
4363
4815
|
name: "tools_list",
|
|
4364
|
-
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).",
|
|
4365
4817
|
inputSchema: objectSchema({}),
|
|
4366
|
-
args:
|
|
4818
|
+
args: z5.object({}),
|
|
4367
4819
|
run: async (repoRoot) => {
|
|
4368
4820
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4369
4821
|
if (!harness) {
|
|
@@ -4374,7 +4826,10 @@ var toolRegistry = [
|
|
|
4374
4826
|
name: tool.name,
|
|
4375
4827
|
description: tool.manifest.description,
|
|
4376
4828
|
scope: tool.manifest.scope,
|
|
4829
|
+
risk: tool.manifest.risk,
|
|
4377
4830
|
confirm: tool.manifest.confirm,
|
|
4831
|
+
connection: tool.manifest.connection,
|
|
4832
|
+
healthcheck: Boolean(tool.manifest.healthcheck),
|
|
4378
4833
|
kind: tool.manifest.run ? "shell" : "script",
|
|
4379
4834
|
input: tool.manifest.input
|
|
4380
4835
|
}))
|
|
@@ -4385,7 +4840,7 @@ var toolRegistry = [
|
|
|
4385
4840
|
name: "tools_check",
|
|
4386
4841
|
description: "Run configured harness tool healthchecks without running primary tool actions.",
|
|
4387
4842
|
inputSchema: objectSchema({}),
|
|
4388
|
-
args:
|
|
4843
|
+
args: z5.object({}),
|
|
4389
4844
|
run: async (repoRoot) => {
|
|
4390
4845
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4391
4846
|
if (!harness) {
|
|
@@ -4396,28 +4851,30 @@ var toolRegistry = [
|
|
|
4396
4851
|
}),
|
|
4397
4852
|
defineTool({
|
|
4398
4853
|
name: "tools_run",
|
|
4399
|
-
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.",
|
|
4400
4855
|
inputSchema: objectSchema(
|
|
4401
4856
|
{
|
|
4402
4857
|
name: { type: "string", description: "Tool name." },
|
|
4403
|
-
input: { type: "object", description: "Tool inputs as key/value pairs.", additionalProperties: true }
|
|
4404
|
-
confirm: { type: "boolean", description: "Confirm running a tool that requires confirmation." }
|
|
4858
|
+
input: { type: "object", description: "Tool inputs as key/value pairs.", additionalProperties: true }
|
|
4405
4859
|
},
|
|
4406
4860
|
["name"]
|
|
4407
4861
|
),
|
|
4408
|
-
args:
|
|
4409
|
-
name:
|
|
4410
|
-
input:
|
|
4411
|
-
confirm: z4.boolean().optional()
|
|
4862
|
+
args: z5.object({
|
|
4863
|
+
name: z5.string().min(1),
|
|
4864
|
+
input: z5.record(z5.unknown()).optional()
|
|
4412
4865
|
}),
|
|
4413
4866
|
run: async (repoRoot, args) => {
|
|
4414
4867
|
const outcome = await runTool(repoRoot, {
|
|
4415
4868
|
name: args.name,
|
|
4416
4869
|
input: args.input,
|
|
4417
|
-
confirmed:
|
|
4870
|
+
confirmed: false
|
|
4418
4871
|
});
|
|
4419
4872
|
if (outcome.status === "blocked") {
|
|
4420
|
-
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
|
+
};
|
|
4421
4878
|
}
|
|
4422
4879
|
const { result } = outcome;
|
|
4423
4880
|
return {
|
|
@@ -4440,6 +4897,9 @@ var toolRegistry = [
|
|
|
4440
4897
|
description: { type: "string", description: "What the tool does." },
|
|
4441
4898
|
run: { type: "string", description: "Shell command (use {{param}} for inputs)." },
|
|
4442
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." },
|
|
4443
4903
|
confirm: { type: "boolean", description: "Ask before running. Defaults to true for agents." },
|
|
4444
4904
|
scope: { type: "string", enum: ["user", "project"], description: "Tool scope." },
|
|
4445
4905
|
input: {
|
|
@@ -4450,14 +4910,17 @@ var toolRegistry = [
|
|
|
4450
4910
|
},
|
|
4451
4911
|
["name", "description"]
|
|
4452
4912
|
),
|
|
4453
|
-
args:
|
|
4454
|
-
name:
|
|
4455
|
-
description:
|
|
4456
|
-
run:
|
|
4457
|
-
script:
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
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()
|
|
4461
4924
|
}),
|
|
4462
4925
|
run: async (repoRoot, args) => {
|
|
4463
4926
|
const created = await createTool(
|
|
@@ -4467,6 +4930,9 @@ var toolRegistry = [
|
|
|
4467
4930
|
description: args.description,
|
|
4468
4931
|
run: args.run,
|
|
4469
4932
|
script: args.script,
|
|
4933
|
+
risk: args.risk,
|
|
4934
|
+
connection: args.connection,
|
|
4935
|
+
healthcheck: args.healthcheck ? { run: args.healthcheck, expectExitCode: 0 } : void 0,
|
|
4470
4936
|
confirm: args.confirm,
|
|
4471
4937
|
scope: args.scope,
|
|
4472
4938
|
input: args.input
|
|
@@ -4480,7 +4946,7 @@ var toolRegistry = [
|
|
|
4480
4946
|
name: "tools_detect",
|
|
4481
4947
|
description: "Propose starter tools from the repo's existing command surface (scripts, Make/just targets).",
|
|
4482
4948
|
inputSchema: objectSchema({}),
|
|
4483
|
-
args:
|
|
4949
|
+
args: z5.object({}),
|
|
4484
4950
|
run: async (repoRoot) => {
|
|
4485
4951
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4486
4952
|
const profile = harness?.manifest.profile ?? "empty";
|
|
@@ -4491,7 +4957,7 @@ var toolRegistry = [
|
|
|
4491
4957
|
name: "connections_list",
|
|
4492
4958
|
description: "List local CLI connections defined in this repo's harness.",
|
|
4493
4959
|
inputSchema: objectSchema({}),
|
|
4494
|
-
args:
|
|
4960
|
+
args: z5.object({}),
|
|
4495
4961
|
run: async (repoRoot) => {
|
|
4496
4962
|
const harness = await loadHarnessOrNull(repoRoot);
|
|
4497
4963
|
if (!harness) {
|
|
@@ -4514,7 +4980,7 @@ var toolRegistry = [
|
|
|
4514
4980
|
name: "connections_check",
|
|
4515
4981
|
description: "Check local CLI connections and their configured healthchecks.",
|
|
4516
4982
|
inputSchema: objectSchema({}),
|
|
4517
|
-
args:
|
|
4983
|
+
args: z5.object({}),
|
|
4518
4984
|
run: (repoRoot) => checkConnections(repoRoot)
|
|
4519
4985
|
}),
|
|
4520
4986
|
defineTool({
|
|
@@ -4523,7 +4989,7 @@ var toolRegistry = [
|
|
|
4523
4989
|
inputSchema: objectSchema({
|
|
4524
4990
|
type: { type: "string", description: "Memory type (project, repo-map, current-focus, handoff, pitfalls)." }
|
|
4525
4991
|
}),
|
|
4526
|
-
args:
|
|
4992
|
+
args: z5.object({ type: z5.string().optional() }),
|
|
4527
4993
|
run: async (repoRoot, args) => {
|
|
4528
4994
|
if (args.type) {
|
|
4529
4995
|
return { type: args.type, body: await readMemory(repoRoot, args.type) };
|
|
@@ -4542,21 +5008,21 @@ var toolRegistry = [
|
|
|
4542
5008
|
},
|
|
4543
5009
|
["type", "note"]
|
|
4544
5010
|
),
|
|
4545
|
-
args:
|
|
5011
|
+
args: z5.object({ type: z5.string().min(1), note: z5.string().min(1) }),
|
|
4546
5012
|
run: (repoRoot, args) => appendMemory(repoRoot, args.type, args.note)
|
|
4547
5013
|
}),
|
|
4548
5014
|
defineTool({
|
|
4549
5015
|
name: "status",
|
|
4550
5016
|
description: "Return harness state: manifest, object counts, and drift between canonical and compiled outputs.",
|
|
4551
5017
|
inputSchema: objectSchema({}),
|
|
4552
|
-
args:
|
|
5018
|
+
args: z5.object({}),
|
|
4553
5019
|
run: (repoRoot) => harnessStatus(repoRoot)
|
|
4554
5020
|
}),
|
|
4555
5021
|
defineTool({
|
|
4556
5022
|
name: "doctor",
|
|
4557
5023
|
description: "Check harness validity, compiled output health, MCP hints, and tool trust.",
|
|
4558
5024
|
inputSchema: objectSchema({}),
|
|
4559
|
-
args:
|
|
5025
|
+
args: z5.object({}),
|
|
4560
5026
|
run: (repoRoot) => doctor(repoRoot)
|
|
4561
5027
|
})
|
|
4562
5028
|
];
|
|
@@ -4598,7 +5064,8 @@ async function handleMessage(repoRoot, request) {
|
|
|
4598
5064
|
const params = request.params;
|
|
4599
5065
|
const result = await callTool(repoRoot, params?.name, params?.arguments ?? {});
|
|
4600
5066
|
return resultResponse(request, {
|
|
4601
|
-
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
|
|
4602
5069
|
});
|
|
4603
5070
|
}
|
|
4604
5071
|
return errorResponse(request, -32601, `Unknown method: ${request.method ?? "<missing>"}`);
|
|
@@ -4737,10 +5204,11 @@ Rules:
|
|
|
4737
5204
|
Steps:
|
|
4738
5205
|
1. Check whether Threadroot is available with \`threadroot --version\`.
|
|
4739
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\`.
|
|
4740
|
-
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"\`.
|
|
4741
|
-
4.
|
|
4742
|
-
5.
|
|
4743
|
-
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.
|
|
4744
5212
|
|
|
4745
5213
|
Final response:
|
|
4746
5214
|
Say exactly:
|
|
@@ -4807,11 +5275,11 @@ function agentNotes(agent) {
|
|
|
4807
5275
|
}
|
|
4808
5276
|
|
|
4809
5277
|
// src/core/mcp-config.ts
|
|
4810
|
-
import { mkdir as
|
|
4811
|
-
import
|
|
5278
|
+
import { mkdir as mkdir12, readFile as readFile13, writeFile as writeFile11 } from "fs/promises";
|
|
5279
|
+
import path29 from "path";
|
|
4812
5280
|
var TARGETS = [
|
|
4813
|
-
{ agent: "copilot", file:
|
|
4814
|
-
{ 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" },
|
|
4815
5283
|
{ agent: "claude", file: ".mcp.json", key: "mcpServers" }
|
|
4816
5284
|
];
|
|
4817
5285
|
function mcpServerEntry(command, scriptPath) {
|
|
@@ -4820,7 +5288,7 @@ function mcpServerEntry(command, scriptPath) {
|
|
|
4820
5288
|
async function mergeConfig(filePath, key, entry) {
|
|
4821
5289
|
let config = {};
|
|
4822
5290
|
try {
|
|
4823
|
-
const raw = await
|
|
5291
|
+
const raw = await readFile13(filePath, "utf8");
|
|
4824
5292
|
const parsed = JSON.parse(raw);
|
|
4825
5293
|
if (parsed && typeof parsed === "object") {
|
|
4826
5294
|
config = parsed;
|
|
@@ -4833,7 +5301,7 @@ async function mergeConfig(filePath, key, entry) {
|
|
|
4833
5301
|
const servers = config[key] && typeof config[key] === "object" ? config[key] : {};
|
|
4834
5302
|
servers.threadroot = { ...entry };
|
|
4835
5303
|
config[key] = servers;
|
|
4836
|
-
await
|
|
5304
|
+
await mkdir12(path29.dirname(filePath), { recursive: true });
|
|
4837
5305
|
await writeFile11(filePath, `${JSON.stringify(config, null, 2)}
|
|
4838
5306
|
`, "utf8");
|
|
4839
5307
|
}
|
|
@@ -4842,7 +5310,7 @@ async function writeProjectMcpConfigs(input2) {
|
|
|
4842
5310
|
const targets = agents ? TARGETS.filter((target) => agents.includes(target.agent)) : TARGETS;
|
|
4843
5311
|
const written = [];
|
|
4844
5312
|
for (const target of targets) {
|
|
4845
|
-
const filePath =
|
|
5313
|
+
const filePath = path29.join(input2.repoRoot, target.file);
|
|
4846
5314
|
await mergeConfig(filePath, target.key, input2.entry);
|
|
4847
5315
|
written.push(target.file);
|
|
4848
5316
|
}
|
|
@@ -4865,6 +5333,10 @@ async function runMcpSetup(repoRoot, options) {
|
|
|
4865
5333
|
const scriptPath = process.argv[1];
|
|
4866
5334
|
const entry = mcpServerEntry(command, scriptPath);
|
|
4867
5335
|
const result = await writeProjectMcpConfigs({ repoRoot, entry });
|
|
5336
|
+
if (options.json) {
|
|
5337
|
+
printJson(result);
|
|
5338
|
+
return;
|
|
5339
|
+
}
|
|
4868
5340
|
console.log("Wrote project MCP config:");
|
|
4869
5341
|
for (const file of result.written) {
|
|
4870
5342
|
console.log(`- ${file}`);
|
|
@@ -4874,11 +5346,23 @@ async function runMcpSetup(repoRoot, options) {
|
|
|
4874
5346
|
}
|
|
4875
5347
|
return;
|
|
4876
5348
|
}
|
|
4877
|
-
|
|
5349
|
+
const guide = mcpSetupGuide({ repoRoot, agent: options.agent });
|
|
5350
|
+
if (options.json) {
|
|
5351
|
+
printJson({ guide });
|
|
5352
|
+
} else {
|
|
5353
|
+
console.log(guide);
|
|
5354
|
+
}
|
|
4878
5355
|
}
|
|
4879
5356
|
async function runMcpCheck(repoRoot, options) {
|
|
4880
5357
|
const timeoutMs = options.timeout ? Number.parseInt(options.timeout, 10) : void 0;
|
|
4881
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
|
+
}
|
|
4882
5366
|
console.log(`Threadroot MCP check: ${report.status}`);
|
|
4883
5367
|
console.log(`config: ${report.configPath}`);
|
|
4884
5368
|
if (report.entry) {
|
|
@@ -4913,9 +5397,21 @@ async function runRemember(repoRoot, note, options = {}) {
|
|
|
4913
5397
|
}
|
|
4914
5398
|
|
|
4915
5399
|
// src/commands/skills.ts
|
|
4916
|
-
async function runSkillsList(repoRoot) {
|
|
5400
|
+
async function runSkillsList(repoRoot, options = {}) {
|
|
4917
5401
|
try {
|
|
4918
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
|
+
}
|
|
4919
5415
|
if (harness.skills.length === 0) {
|
|
4920
5416
|
console.log("No skills defined. Add folder skills under `.threadroot/skills/<name>/SKILL.md`.");
|
|
4921
5417
|
return;
|
|
@@ -4925,7 +5421,11 @@ async function runSkillsList(repoRoot) {
|
|
|
4925
5421
|
}
|
|
4926
5422
|
} catch (error) {
|
|
4927
5423
|
if (error instanceof HarnessError) {
|
|
4928
|
-
|
|
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
|
+
}
|
|
4929
5429
|
return;
|
|
4930
5430
|
}
|
|
4931
5431
|
throw error;
|
|
@@ -4933,6 +5433,13 @@ async function runSkillsList(repoRoot) {
|
|
|
4933
5433
|
}
|
|
4934
5434
|
async function runSkillsValidate(repoRoot, options = {}) {
|
|
4935
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
|
+
}
|
|
4936
5443
|
if (report.findings.length === 0) {
|
|
4937
5444
|
console.log("Skills valid.");
|
|
4938
5445
|
return;
|
|
@@ -4947,8 +5454,12 @@ async function runSkillsValidate(repoRoot, options = {}) {
|
|
|
4947
5454
|
process.exitCode = 1;
|
|
4948
5455
|
}
|
|
4949
5456
|
}
|
|
4950
|
-
async function runSkillsInspect(repoRoot, targetPath) {
|
|
5457
|
+
async function runSkillsInspect(repoRoot, targetPath, options = {}) {
|
|
4951
5458
|
const inspection = await inspectSkillPath(toRepoPath(repoRoot, targetPath));
|
|
5459
|
+
if (options.json) {
|
|
5460
|
+
printJson(inspection);
|
|
5461
|
+
return;
|
|
5462
|
+
}
|
|
4952
5463
|
console.log(`${inspection.name}`);
|
|
4953
5464
|
console.log(`description: ${inspection.description}`);
|
|
4954
5465
|
console.log(`path: ${inspection.path}`);
|
|
@@ -5010,15 +5521,23 @@ async function runSetup(_repoRoot, options) {
|
|
|
5010
5521
|
// src/commands/start.ts
|
|
5011
5522
|
async function runStart(repoRoot, task, options) {
|
|
5012
5523
|
const report = await startSession(repoRoot, { task: task ?? options.task });
|
|
5013
|
-
|
|
5524
|
+
if (options.json) {
|
|
5525
|
+
printJson(report);
|
|
5526
|
+
} else {
|
|
5527
|
+
printStartReport(report);
|
|
5528
|
+
}
|
|
5014
5529
|
if (!report.status.exists || report.doctor && !report.doctor.ok) {
|
|
5015
5530
|
process.exitCode = 1;
|
|
5016
5531
|
}
|
|
5017
5532
|
}
|
|
5018
5533
|
|
|
5019
5534
|
// src/commands/status.ts
|
|
5020
|
-
async function runStatus(repoRoot) {
|
|
5535
|
+
async function runStatus(repoRoot, options = {}) {
|
|
5021
5536
|
const status = await harnessStatus(repoRoot);
|
|
5537
|
+
if (options.json) {
|
|
5538
|
+
printJson(status);
|
|
5539
|
+
return;
|
|
5540
|
+
}
|
|
5022
5541
|
if (!status.exists) {
|
|
5023
5542
|
console.log("No harness found. Run `tr init` first.");
|
|
5024
5543
|
return;
|
|
@@ -5039,261 +5558,16 @@ async function runStatus(repoRoot) {
|
|
|
5039
5558
|
}
|
|
5040
5559
|
}
|
|
5041
5560
|
|
|
5042
|
-
// src/core/packs/index.ts
|
|
5043
|
-
import { cp as cp3, mkdir as mkdir12, readFile as readFile13, readdir as readdir6, stat as stat11 } from "fs/promises";
|
|
5044
|
-
import path29 from "path";
|
|
5045
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5046
|
-
import { parse as parseYaml3 } from "yaml";
|
|
5047
|
-
import { z as z5 } from "zod";
|
|
5048
|
-
var packManifestSchema = z5.object({
|
|
5049
|
-
name: z5.string().min(1),
|
|
5050
|
-
version: z5.literal(1),
|
|
5051
|
-
description: z5.string().min(1),
|
|
5052
|
-
skills: z5.array(z5.string()).default([]),
|
|
5053
|
-
tools: z5.array(z5.string()).default([]),
|
|
5054
|
-
rules: z5.array(z5.string()).default([]),
|
|
5055
|
-
connections: z5.array(z5.string()).default([])
|
|
5056
|
-
});
|
|
5057
|
-
var DIST_DIR2 = path29.dirname(fileURLToPath2(import.meta.url));
|
|
5058
|
-
var PACKAGE_ROOT_FROM_BUNDLE2 = path29.resolve(DIST_DIR2, "..");
|
|
5059
|
-
var PACKAGE_ROOT_FROM_DIST2 = path29.resolve(DIST_DIR2, "../../..");
|
|
5060
|
-
var PACKAGE_ROOT_FROM_SRC2 = path29.resolve(DIST_DIR2, "../../../..");
|
|
5061
|
-
var PACK_CANDIDATES = [
|
|
5062
|
-
path29.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
|
|
5063
|
-
path29.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
|
|
5064
|
-
path29.join(PACKAGE_ROOT_FROM_SRC2, "packs")
|
|
5065
|
-
];
|
|
5066
|
-
async function exists5(target) {
|
|
5067
|
-
try {
|
|
5068
|
-
await stat11(target);
|
|
5069
|
-
return true;
|
|
5070
|
-
} catch (error) {
|
|
5071
|
-
if (error.code === "ENOENT") {
|
|
5072
|
-
return false;
|
|
5073
|
-
}
|
|
5074
|
-
throw error;
|
|
5075
|
-
}
|
|
5076
|
-
}
|
|
5077
|
-
async function firstExisting(candidates) {
|
|
5078
|
-
for (const candidate of candidates) {
|
|
5079
|
-
if (await isPackRoot(candidate)) {
|
|
5080
|
-
return candidate;
|
|
5081
|
-
}
|
|
5082
|
-
}
|
|
5083
|
-
return void 0;
|
|
5084
|
-
}
|
|
5085
|
-
async function bundledPacksDir() {
|
|
5086
|
-
return firstExisting(PACK_CANDIDATES);
|
|
5087
|
-
}
|
|
5088
|
-
async function isPackRoot(candidate) {
|
|
5089
|
-
let entries;
|
|
5090
|
-
try {
|
|
5091
|
-
entries = await readdir6(candidate, { withFileTypes: true });
|
|
5092
|
-
} catch {
|
|
5093
|
-
return false;
|
|
5094
|
-
}
|
|
5095
|
-
for (const entry of entries) {
|
|
5096
|
-
if (entry.isDirectory() && await exists5(path29.join(candidate, entry.name, "pack.yaml"))) {
|
|
5097
|
-
return true;
|
|
5098
|
-
}
|
|
5099
|
-
}
|
|
5100
|
-
return false;
|
|
5101
|
-
}
|
|
5102
|
-
function safeRelative(ref) {
|
|
5103
|
-
const normalized = path29.normalize(ref);
|
|
5104
|
-
if (path29.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path29.sep}`)) {
|
|
5105
|
-
throw new Error(`Unsafe pack reference: ${ref}`);
|
|
5106
|
-
}
|
|
5107
|
-
return normalized;
|
|
5108
|
-
}
|
|
5109
|
-
async function readPackManifest(packDir) {
|
|
5110
|
-
const file = path29.join(packDir, "pack.yaml");
|
|
5111
|
-
const parsed = packManifestSchema.safeParse(parseYaml3(await readFile13(file, "utf8")));
|
|
5112
|
-
if (!parsed.success) {
|
|
5113
|
-
const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
|
|
5114
|
-
throw new Error(`Invalid pack manifest ${file}: ${detail}`);
|
|
5115
|
-
}
|
|
5116
|
-
return parsed.data;
|
|
5117
|
-
}
|
|
5118
|
-
async function packDirFor(repoRoot, nameOrPath) {
|
|
5119
|
-
if (path29.isAbsolute(nameOrPath)) {
|
|
5120
|
-
return nameOrPath;
|
|
5121
|
-
}
|
|
5122
|
-
if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
|
|
5123
|
-
return toRepoPath(repoRoot, nameOrPath);
|
|
5124
|
-
}
|
|
5125
|
-
const bundled = await bundledPacksDir();
|
|
5126
|
-
if (bundled) {
|
|
5127
|
-
return path29.join(bundled, nameOrPath);
|
|
5128
|
-
}
|
|
5129
|
-
return toRepoPath(repoRoot, path29.join("packs", nameOrPath));
|
|
5130
|
-
}
|
|
5131
|
-
async function directFiles(dir, ext) {
|
|
5132
|
-
try {
|
|
5133
|
-
const entries = await readdir6(dir, { withFileTypes: true });
|
|
5134
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path29.join(dir, entry.name)).sort();
|
|
5135
|
-
} catch (error) {
|
|
5136
|
-
if (error.code === "ENOENT") {
|
|
5137
|
-
return [];
|
|
5138
|
-
}
|
|
5139
|
-
throw error;
|
|
5140
|
-
}
|
|
5141
|
-
}
|
|
5142
|
-
async function skillEntries(dir) {
|
|
5143
|
-
try {
|
|
5144
|
-
const entries = await readdir6(dir, { withFileTypes: true });
|
|
5145
|
-
const result = [];
|
|
5146
|
-
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
5147
|
-
const full = path29.join(dir, entry.name);
|
|
5148
|
-
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
5149
|
-
result.push(full);
|
|
5150
|
-
}
|
|
5151
|
-
if (entry.isDirectory() && await exists5(path29.join(full, "SKILL.md"))) {
|
|
5152
|
-
result.push(full);
|
|
5153
|
-
}
|
|
5154
|
-
}
|
|
5155
|
-
return result;
|
|
5156
|
-
} catch (error) {
|
|
5157
|
-
if (error.code === "ENOENT") {
|
|
5158
|
-
return [];
|
|
5159
|
-
}
|
|
5160
|
-
throw error;
|
|
5161
|
-
}
|
|
5162
|
-
}
|
|
5163
|
-
async function collectObjects(packDir, manifest) {
|
|
5164
|
-
async function resolveRef(ref) {
|
|
5165
|
-
const safe = safeRelative(ref);
|
|
5166
|
-
const local = path29.resolve(packDir, safe);
|
|
5167
|
-
if (await exists5(local)) {
|
|
5168
|
-
return local;
|
|
5169
|
-
}
|
|
5170
|
-
return path29.resolve(packDir, "..", "..", safe);
|
|
5171
|
-
}
|
|
5172
|
-
return {
|
|
5173
|
-
skills: [
|
|
5174
|
-
...await Promise.all(manifest.skills.map(resolveRef)),
|
|
5175
|
-
...await skillEntries(path29.join(packDir, "skills"))
|
|
5176
|
-
],
|
|
5177
|
-
tools: [
|
|
5178
|
-
...await Promise.all(manifest.tools.map(resolveRef)),
|
|
5179
|
-
...await directFiles(path29.join(packDir, "tools"), ".yaml")
|
|
5180
|
-
],
|
|
5181
|
-
rules: [
|
|
5182
|
-
...await Promise.all(manifest.rules.map(resolveRef)),
|
|
5183
|
-
...await directFiles(path29.join(packDir, "rules"), ".md")
|
|
5184
|
-
],
|
|
5185
|
-
connections: [
|
|
5186
|
-
...await Promise.all(manifest.connections.map(resolveRef)),
|
|
5187
|
-
...await directFiles(path29.join(packDir, "connections"), ".yaml")
|
|
5188
|
-
]
|
|
5189
|
-
};
|
|
5190
|
-
}
|
|
5191
|
-
function baseName(source) {
|
|
5192
|
-
const parsed = path29.basename(source) === "SKILL.md" ? path29.dirname(source) : source;
|
|
5193
|
-
return path29.basename(parsed, path29.extname(parsed));
|
|
5194
|
-
}
|
|
5195
|
-
async function listPacks(repoRoot) {
|
|
5196
|
-
const dirs = [toRepoPath(repoRoot, "packs"), await bundledPacksDir()].filter((dir) => Boolean(dir));
|
|
5197
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5198
|
-
const packs = [];
|
|
5199
|
-
for (const root of dirs) {
|
|
5200
|
-
let entries;
|
|
5201
|
-
try {
|
|
5202
|
-
entries = await readdir6(root, { withFileTypes: true });
|
|
5203
|
-
} catch {
|
|
5204
|
-
continue;
|
|
5205
|
-
}
|
|
5206
|
-
for (const entry of entries) {
|
|
5207
|
-
if (!entry.isDirectory() || seen.has(entry.name)) {
|
|
5208
|
-
continue;
|
|
5209
|
-
}
|
|
5210
|
-
const packDir = path29.join(root, entry.name);
|
|
5211
|
-
if (!await exists5(path29.join(packDir, "pack.yaml"))) {
|
|
5212
|
-
continue;
|
|
5213
|
-
}
|
|
5214
|
-
seen.add(entry.name);
|
|
5215
|
-
packs.push(await inspectPack(repoRoot, packDir));
|
|
5216
|
-
}
|
|
5217
|
-
}
|
|
5218
|
-
return packs.sort((a, b) => a.name.localeCompare(b.name));
|
|
5219
|
-
}
|
|
5220
|
-
async function inspectPack(repoRoot, nameOrPath) {
|
|
5221
|
-
const packDir = await packDirFor(repoRoot, nameOrPath);
|
|
5222
|
-
const manifest = await readPackManifest(packDir);
|
|
5223
|
-
const objects = await collectObjects(packDir, manifest);
|
|
5224
|
-
return {
|
|
5225
|
-
name: manifest.name,
|
|
5226
|
-
description: manifest.description,
|
|
5227
|
-
path: packDir,
|
|
5228
|
-
skills: objects.skills.map(baseName),
|
|
5229
|
-
tools: objects.tools.map(baseName),
|
|
5230
|
-
rules: objects.rules.map(baseName),
|
|
5231
|
-
connections: objects.connections.map(baseName)
|
|
5232
|
-
};
|
|
5233
|
-
}
|
|
5234
|
-
async function validateProse(file, kind) {
|
|
5235
|
-
const target = path29.basename(file) === "SKILL.md" ? file : file;
|
|
5236
|
-
const content = await readFile13(target, "utf8");
|
|
5237
|
-
const parsed = parseFrontmatter(content);
|
|
5238
|
-
const schema = kind === "skill" ? skillFrontmatterSchema : ruleFrontmatterSchema;
|
|
5239
|
-
schema.parse(parsed.data);
|
|
5240
|
-
}
|
|
5241
|
-
async function validateYaml(file, kind) {
|
|
5242
|
-
const content = await readFile13(file, "utf8");
|
|
5243
|
-
const schema = kind === "tool" ? toolManifestSchema : connectionManifestSchema;
|
|
5244
|
-
schema.parse(parseYaml3(content));
|
|
5245
|
-
}
|
|
5246
|
-
async function validatePack(repoRoot, nameOrPath) {
|
|
5247
|
-
const findings = [];
|
|
5248
|
-
try {
|
|
5249
|
-
const packDir = await packDirFor(repoRoot, nameOrPath);
|
|
5250
|
-
const manifest = await readPackManifest(packDir);
|
|
5251
|
-
const objects = await collectObjects(packDir, manifest);
|
|
5252
|
-
for (const skill of objects.skills) {
|
|
5253
|
-
await validateProse(path29.basename(skill) === "SKILL.md" ? skill : path29.join(skill, "SKILL.md"), "skill");
|
|
5254
|
-
}
|
|
5255
|
-
for (const rule of objects.rules) await validateProse(rule, "rule");
|
|
5256
|
-
for (const tool of objects.tools) await validateYaml(tool, "tool");
|
|
5257
|
-
for (const connection of objects.connections) await validateYaml(connection, "connection");
|
|
5258
|
-
if (Object.values(objects).every((items) => items.length === 0)) {
|
|
5259
|
-
findings.push({ severity: "warning", message: "Pack does not include any objects." });
|
|
5260
|
-
}
|
|
5261
|
-
} catch (error) {
|
|
5262
|
-
findings.push({ severity: "error", message: error instanceof Error ? error.message : String(error) });
|
|
5263
|
-
}
|
|
5264
|
-
return { ok: !findings.some((finding3) => finding3.severity === "error"), findings };
|
|
5265
|
-
}
|
|
5266
|
-
async function copyObject(source, destDir) {
|
|
5267
|
-
const info = await stat11(source);
|
|
5268
|
-
const name = baseName(source);
|
|
5269
|
-
const dest = info.isDirectory() ? path29.join(destDir, name) : path29.join(destDir, path29.basename(source));
|
|
5270
|
-
await mkdir12(destDir, { recursive: true });
|
|
5271
|
-
await cp3(source, dest, { recursive: true, force: true });
|
|
5272
|
-
return dest;
|
|
5273
|
-
}
|
|
5274
|
-
async function installPack(repoRoot, nameOrPath) {
|
|
5275
|
-
const validation = await validatePack(repoRoot, nameOrPath);
|
|
5276
|
-
if (!validation.ok) {
|
|
5277
|
-
throw new Error(validation.findings.map((finding3) => finding3.message).join("; "));
|
|
5278
|
-
}
|
|
5279
|
-
const packDir = await packDirFor(repoRoot, nameOrPath);
|
|
5280
|
-
const manifest = await readPackManifest(packDir);
|
|
5281
|
-
const objects = await collectObjects(packDir, manifest);
|
|
5282
|
-
await Promise.all([
|
|
5283
|
-
...objects.skills.map((source) => copyObject(source, projectObjectDir(repoRoot, "skills"))),
|
|
5284
|
-
...objects.tools.map((source) => copyObject(source, projectObjectDir(repoRoot, "tools"))),
|
|
5285
|
-
...objects.rules.map((source) => copyObject(source, projectObjectDir(repoRoot, "rules"))),
|
|
5286
|
-
...objects.connections.map((source) => copyObject(source, projectObjectDir(repoRoot, "connections")))
|
|
5287
|
-
]);
|
|
5288
|
-
return inspectPack(repoRoot, packDir);
|
|
5289
|
-
}
|
|
5290
|
-
|
|
5291
5561
|
// src/commands/packs.ts
|
|
5292
5562
|
function printList(label, values) {
|
|
5293
5563
|
console.log(`${label}: ${values.length > 0 ? values.join(", ") : "none"}`);
|
|
5294
5564
|
}
|
|
5295
|
-
async function runPacksList(repoRoot) {
|
|
5565
|
+
async function runPacksList(repoRoot, options = {}) {
|
|
5296
5566
|
const packs = await listPacks(repoRoot);
|
|
5567
|
+
if (options.json) {
|
|
5568
|
+
printJson({ packs });
|
|
5569
|
+
return;
|
|
5570
|
+
}
|
|
5297
5571
|
if (packs.length === 0) {
|
|
5298
5572
|
console.log("No packs found.");
|
|
5299
5573
|
return;
|
|
@@ -5302,8 +5576,12 @@ async function runPacksList(repoRoot) {
|
|
|
5302
5576
|
console.log(`${pack.name} - ${pack.description}`);
|
|
5303
5577
|
}
|
|
5304
5578
|
}
|
|
5305
|
-
async function runPacksInspect(repoRoot, nameOrPath) {
|
|
5579
|
+
async function runPacksInspect(repoRoot, nameOrPath, options = {}) {
|
|
5306
5580
|
const pack = await inspectPack(repoRoot, nameOrPath);
|
|
5581
|
+
if (options.json) {
|
|
5582
|
+
printJson(pack);
|
|
5583
|
+
return;
|
|
5584
|
+
}
|
|
5307
5585
|
console.log(pack.name);
|
|
5308
5586
|
console.log(`description: ${pack.description}`);
|
|
5309
5587
|
console.log(`path: ${pack.path}`);
|
|
@@ -5312,8 +5590,15 @@ async function runPacksInspect(repoRoot, nameOrPath) {
|
|
|
5312
5590
|
printList("rules", pack.rules);
|
|
5313
5591
|
printList("connections", pack.connections);
|
|
5314
5592
|
}
|
|
5315
|
-
async function runPacksValidate(repoRoot, nameOrPath) {
|
|
5593
|
+
async function runPacksValidate(repoRoot, nameOrPath, options = {}) {
|
|
5316
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
|
+
}
|
|
5317
5602
|
if (report.findings.length === 0) {
|
|
5318
5603
|
console.log("Pack valid.");
|
|
5319
5604
|
return;
|
|
@@ -5325,8 +5610,12 @@ async function runPacksValidate(repoRoot, nameOrPath) {
|
|
|
5325
5610
|
process.exitCode = 1;
|
|
5326
5611
|
}
|
|
5327
5612
|
}
|
|
5328
|
-
async function runPacksInstall(repoRoot, nameOrPath) {
|
|
5613
|
+
async function runPacksInstall(repoRoot, nameOrPath, options = {}) {
|
|
5329
5614
|
const pack = await installPack(repoRoot, nameOrPath);
|
|
5615
|
+
if (options.json) {
|
|
5616
|
+
printJson(pack);
|
|
5617
|
+
return;
|
|
5618
|
+
}
|
|
5330
5619
|
console.log(`Installed pack \`${pack.name}\`.`);
|
|
5331
5620
|
printList("skills", pack.skills);
|
|
5332
5621
|
printList("tools", pack.tools);
|
|
@@ -5346,17 +5635,37 @@ function parseInputs(pairs = []) {
|
|
|
5346
5635
|
}
|
|
5347
5636
|
return input2;
|
|
5348
5637
|
}
|
|
5349
|
-
async function runToolsList(repoRoot) {
|
|
5638
|
+
async function runToolsList(repoRoot, options = {}) {
|
|
5350
5639
|
let harness;
|
|
5351
5640
|
try {
|
|
5352
5641
|
harness = await resolveHarness(repoRoot);
|
|
5353
5642
|
} catch (error) {
|
|
5354
5643
|
if (error instanceof HarnessError) {
|
|
5355
|
-
|
|
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
|
+
}
|
|
5356
5649
|
return;
|
|
5357
5650
|
}
|
|
5358
5651
|
throw error;
|
|
5359
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
|
+
}
|
|
5360
5669
|
if (harness.tools.length === 0) {
|
|
5361
5670
|
console.log("No tools defined. Add one with `tr tools add` or `tr tools detect`.");
|
|
5362
5671
|
return;
|
|
@@ -5380,6 +5689,11 @@ async function runToolRun(repoRoot, name, options) {
|
|
|
5380
5689
|
timeoutMs: options.timeout ? Number(options.timeout) : void 0
|
|
5381
5690
|
});
|
|
5382
5691
|
if (outcome.status === "blocked") {
|
|
5692
|
+
if (options.json) {
|
|
5693
|
+
printJson(outcome);
|
|
5694
|
+
process.exitCode = 1;
|
|
5695
|
+
return;
|
|
5696
|
+
}
|
|
5383
5697
|
if (outcome.reason === "needs-confirmation") {
|
|
5384
5698
|
console.log(`${outcome.message} Re-run with --yes to confirm.`);
|
|
5385
5699
|
} else {
|
|
@@ -5389,6 +5703,13 @@ async function runToolRun(repoRoot, name, options) {
|
|
|
5389
5703
|
return;
|
|
5390
5704
|
}
|
|
5391
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
|
+
}
|
|
5392
5713
|
if (result.stdout) {
|
|
5393
5714
|
process.stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}
|
|
5394
5715
|
`);
|
|
@@ -5426,7 +5747,11 @@ async function runToolsAdd(repoRoot, name, options) {
|
|
|
5426
5747
|
},
|
|
5427
5748
|
{ actor: "human", force: options.force }
|
|
5428
5749
|
);
|
|
5429
|
-
|
|
5750
|
+
if (options.json) {
|
|
5751
|
+
printJson(created);
|
|
5752
|
+
} else {
|
|
5753
|
+
console.log(`Created ${created.scope} tool \`${name}\` at ${created.path}.`);
|
|
5754
|
+
}
|
|
5430
5755
|
}
|
|
5431
5756
|
function deriveNameFromCommand(command) {
|
|
5432
5757
|
const first = command.trim().split(/\s+/)[0] ?? "tool";
|
|
@@ -5446,29 +5771,42 @@ async function runToolsCreate(repoRoot, options) {
|
|
|
5446
5771
|
confirm: options.confirm ?? options.risk === "high"
|
|
5447
5772
|
});
|
|
5448
5773
|
}
|
|
5449
|
-
async function runToolsCheck(repoRoot) {
|
|
5774
|
+
async function runToolsCheck(repoRoot, options = {}) {
|
|
5450
5775
|
const harness = await resolveHarness(repoRoot);
|
|
5451
5776
|
if (harness.tools.length === 0) {
|
|
5452
|
-
|
|
5777
|
+
if (options.json) {
|
|
5778
|
+
printJson({ checks: [] });
|
|
5779
|
+
} else {
|
|
5780
|
+
console.log("No tools defined.");
|
|
5781
|
+
}
|
|
5453
5782
|
return;
|
|
5454
5783
|
}
|
|
5455
5784
|
let failures = 0;
|
|
5785
|
+
const checks = [];
|
|
5456
5786
|
for (const tool of harness.tools) {
|
|
5457
5787
|
const check = await checkToolHealth(repoRoot, tool);
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
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") {
|
|
5463
5799
|
failures += 1;
|
|
5464
|
-
console.log(`${tool.name}: error - ${check.message}`);
|
|
5465
5800
|
}
|
|
5466
5801
|
}
|
|
5802
|
+
if (options.json) {
|
|
5803
|
+
printJson({ checks });
|
|
5804
|
+
}
|
|
5467
5805
|
if (failures > 0) {
|
|
5468
5806
|
process.exitCode = 1;
|
|
5469
5807
|
}
|
|
5470
5808
|
}
|
|
5471
|
-
async function runToolsDetect(repoRoot) {
|
|
5809
|
+
async function runToolsDetect(repoRoot, options = {}) {
|
|
5472
5810
|
let profile;
|
|
5473
5811
|
try {
|
|
5474
5812
|
profile = (await resolveHarness(repoRoot)).manifest.profile;
|
|
@@ -5478,6 +5816,10 @@ async function runToolsDetect(repoRoot) {
|
|
|
5478
5816
|
}
|
|
5479
5817
|
}
|
|
5480
5818
|
const candidates = await detectToolCandidates(repoRoot, profile ?? "empty");
|
|
5819
|
+
if (options.json) {
|
|
5820
|
+
printJson({ candidates });
|
|
5821
|
+
return;
|
|
5822
|
+
}
|
|
5481
5823
|
if (candidates.length === 0) {
|
|
5482
5824
|
console.log("No starter tools detected.");
|
|
5483
5825
|
return;
|
|
@@ -5491,17 +5833,40 @@ async function runToolsDetect(repoRoot) {
|
|
|
5491
5833
|
}
|
|
5492
5834
|
|
|
5493
5835
|
// src/commands/connections.ts
|
|
5494
|
-
|
|
5836
|
+
function parseList(value) {
|
|
5837
|
+
return (value ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
5838
|
+
}
|
|
5839
|
+
async function runConnectionsList(repoRoot, options = {}) {
|
|
5495
5840
|
let harness;
|
|
5496
5841
|
try {
|
|
5497
5842
|
harness = await resolveHarness(repoRoot);
|
|
5498
5843
|
} catch (error) {
|
|
5499
5844
|
if (error instanceof HarnessError) {
|
|
5500
|
-
|
|
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
|
+
}
|
|
5501
5850
|
return;
|
|
5502
5851
|
}
|
|
5503
5852
|
throw error;
|
|
5504
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
|
+
}
|
|
5505
5870
|
if (harness.connections.length === 0) {
|
|
5506
5871
|
console.log("No connections defined. Add one with `tr connections add`.");
|
|
5507
5872
|
return;
|
|
@@ -5528,14 +5893,27 @@ async function runConnectionsAdd(repoRoot, name, options) {
|
|
|
5528
5893
|
risk: options.risk,
|
|
5529
5894
|
confirm: options.confirm,
|
|
5530
5895
|
healthcheck: options.healthcheck,
|
|
5896
|
+
allow: parseList(options.allow),
|
|
5897
|
+
deny: parseList(options.deny),
|
|
5531
5898
|
scope: options.scope
|
|
5532
5899
|
},
|
|
5533
5900
|
{ force: options.force }
|
|
5534
5901
|
);
|
|
5535
|
-
|
|
5902
|
+
if (options.json) {
|
|
5903
|
+
printJson(created);
|
|
5904
|
+
} else {
|
|
5905
|
+
console.log(`Created ${created.manifest.scope} connection \`${name}\` at ${created.path}.`);
|
|
5906
|
+
}
|
|
5536
5907
|
}
|
|
5537
|
-
async function runConnectionsCheck(repoRoot) {
|
|
5908
|
+
async function runConnectionsCheck(repoRoot, options = {}) {
|
|
5538
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
|
+
}
|
|
5539
5917
|
if (checks.length === 0) {
|
|
5540
5918
|
console.log("No connections defined.");
|
|
5541
5919
|
return;
|
|
@@ -5556,45 +5934,45 @@ async function runConnectionsCheck(repoRoot) {
|
|
|
5556
5934
|
function createProgram(repoRoot = process.cwd()) {
|
|
5557
5935
|
const program = new Command();
|
|
5558
5936
|
program.name("threadroot").description("Git for your AI agent harness: one command to bootstrap, one .threadroot source.").version(THREADROOT_VERSION);
|
|
5559
|
-
program.command("bootstrap").description("Plan or apply first-run Threadroot setup for this machine and repository.").option("-y, --yes", "Apply the setup plan. Without --yes, bootstrap prints a dry-run plan.").option("--dry-run", "Print the setup plan without writing files.").option("--agent <list>", "Provider(s): codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--task <task>", "Task used for the initial context slice.").option("--mcp", "Also add Threadroot MCP to Codex global config when Codex is selected.").option("--expose <list>", "Also write project provider skill shims: codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--no-global", "Skip one-time machine-level agent setup.").option("--no-init", "Skip project harness initialization.").option("--no-import", "Skip importing existing vendor files during init.").option("--profile <profile>", "Override the detected project profile during init.").action((options) => runBootstrap(repoRoot, options));
|
|
5560
|
-
program.command("start").argument("[task]", "Task to prepare context for.").option("--task <task>", "Task to prepare context for.").description("Start a focused Threadroot agent session: doctor, status, context, and command map.").action((task, options) => runStart(repoRoot, task, options));
|
|
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));
|
|
5561
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));
|
|
5562
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));
|
|
5563
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));
|
|
5564
|
-
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));
|
|
5565
5943
|
program.command("diff").description("Show the diff between the canonical harness and each compiled vendor file.").action(() => runDiff(repoRoot));
|
|
5566
|
-
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));
|
|
5567
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));
|
|
5568
|
-
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));
|
|
5569
|
-
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));
|
|
5570
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));
|
|
5571
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));
|
|
5572
5950
|
const memory = program.command("memory").description("Read and append durable harness memory.");
|
|
5573
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));
|
|
5574
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));
|
|
5575
5953
|
const tools2 = program.command("tools").description("Manage executable harness tools.");
|
|
5576
|
-
tools2.command("list").description("List harness tools.").action(() => runToolsList(repoRoot));
|
|
5577
|
-
tools2.command("check").description("Run configured tool healthchecks.").action(() => runToolsCheck(repoRoot));
|
|
5578
|
-
tools2.command("detect").description("Propose starter tools from the repo's existing command surface.").action(() => runToolsDetect(repoRoot));
|
|
5579
|
-
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));
|
|
5580
|
-
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));
|
|
5581
5959
|
const connections = program.command("connections").description("Manage local CLI connections.");
|
|
5582
|
-
connections.command("list").description("List harness connections.").action(() => runConnectionsList(repoRoot));
|
|
5583
|
-
connections.command("check").description("Run configured connection healthchecks.").action(() => runConnectionsCheck(repoRoot));
|
|
5584
|
-
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));
|
|
5585
5963
|
const packs = program.command("packs").description("Inspect, validate, and install capability packs.");
|
|
5586
|
-
packs.command("list").description("List built-in and repo-local packs.").action(() => runPacksList(repoRoot));
|
|
5587
|
-
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));
|
|
5588
|
-
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));
|
|
5589
|
-
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));
|
|
5590
5968
|
const skills = program.command("skills").description("Inspect and validate harness skills.");
|
|
5591
|
-
skills.command("list").description("List harness skills.").action(() => runSkillsList(repoRoot));
|
|
5592
|
-
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));
|
|
5593
|
-
skills.command("validate").option("--path <path>", "Validate a repo-relative skill file, skill directory, or skill collection.").description("Validate skill frontmatter, naming, trigger descriptions, and progressive-disclosure hygiene.").action((options) => runSkillsValidate(repoRoot, options));
|
|
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));
|
|
5594
5972
|
const mcp = program.command("mcp").description("Run or configure the local Threadroot MCP server.");
|
|
5595
5973
|
mcp.action(() => runMcp(repoRoot));
|
|
5596
|
-
mcp.command("check").option("--timeout <ms>", "Handshake timeout in milliseconds.").description("Verify Codex MCP config and the Threadroot stdio server handshake.").action((options) => runMcpCheck(repoRoot, options));
|
|
5597
|
-
mcp.command("setup").option("--agent <agent>", "all, generic, codex, copilot, cursor, or claude.").option("--write", "Write project-local MCP config files for the agents.").description("Print MCP config snippets and a pasteable agent bootstrap prompt.").action((options) => runMcpSetup(repoRoot, options));
|
|
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));
|
|
5598
5976
|
return program;
|
|
5599
5977
|
}
|
|
5600
5978
|
|