threadroot 0.1.0 → 0.1.1
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 +15 -0
- package/README.md +64 -17
- package/dist/index.js +721 -156
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ var harnessManifestSchema = z2.object({
|
|
|
36
36
|
name: z2.string().min(1),
|
|
37
37
|
version: z2.literal(1),
|
|
38
38
|
profile: profileIdSchema,
|
|
39
|
-
adapters: z2.array(adapterIdSchema).
|
|
39
|
+
adapters: z2.array(adapterIdSchema).default([]),
|
|
40
40
|
references: z2.array(referenceSchema).default([]),
|
|
41
41
|
memory: z2.object({
|
|
42
42
|
budget: z2.record(memoryTypeSchema, z2.number().int().positive()).default({})
|
|
@@ -1042,13 +1042,13 @@ async function runDiff(repoRoot) {
|
|
|
1042
1042
|
}
|
|
1043
1043
|
}
|
|
1044
1044
|
if (changed === 0) {
|
|
1045
|
-
console.log("No drift:
|
|
1045
|
+
console.log("No drift: optional compiled outputs match the canonical harness.");
|
|
1046
1046
|
}
|
|
1047
1047
|
}
|
|
1048
1048
|
|
|
1049
1049
|
// src/core/doctor.ts
|
|
1050
|
-
import { stat as
|
|
1051
|
-
import
|
|
1050
|
+
import { stat as stat5 } from "fs/promises";
|
|
1051
|
+
import path16 from "path";
|
|
1052
1052
|
|
|
1053
1053
|
// src/core/install/lock.ts
|
|
1054
1054
|
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
@@ -1711,6 +1711,407 @@ async function checkConnections(repoRoot, options = {}) {
|
|
|
1711
1711
|
return Promise.all(harness.connections.map((connection) => checkConnection(repoRoot, connection)));
|
|
1712
1712
|
}
|
|
1713
1713
|
|
|
1714
|
+
// src/core/setup.ts
|
|
1715
|
+
import { mkdir as mkdir5, readFile as readFile6, rm, stat as stat3, writeFile as writeFile5 } from "fs/promises";
|
|
1716
|
+
import { homedir } from "os";
|
|
1717
|
+
import path13 from "path";
|
|
1718
|
+
|
|
1719
|
+
// src/core/agent-providers.ts
|
|
1720
|
+
import path12 from "path";
|
|
1721
|
+
var AGENT_PROVIDER_IDS = [
|
|
1722
|
+
"antigravity",
|
|
1723
|
+
"claude",
|
|
1724
|
+
"codex",
|
|
1725
|
+
"cursor",
|
|
1726
|
+
"gemini",
|
|
1727
|
+
"copilot",
|
|
1728
|
+
"opencode",
|
|
1729
|
+
"windsurf"
|
|
1730
|
+
];
|
|
1731
|
+
var AGENT_PROVIDERS = {
|
|
1732
|
+
antigravity: {
|
|
1733
|
+
id: "antigravity",
|
|
1734
|
+
label: "Antigravity",
|
|
1735
|
+
projectSkillDir: path12.join(".agent", "skills"),
|
|
1736
|
+
globalSkillDir: path12.join(".gemini", "antigravity", "skills")
|
|
1737
|
+
},
|
|
1738
|
+
claude: {
|
|
1739
|
+
id: "claude",
|
|
1740
|
+
label: "Claude Code",
|
|
1741
|
+
projectSkillDir: path12.join(".claude", "skills"),
|
|
1742
|
+
globalSkillDir: path12.join(".claude", "skills")
|
|
1743
|
+
},
|
|
1744
|
+
codex: {
|
|
1745
|
+
id: "codex",
|
|
1746
|
+
label: "Codex",
|
|
1747
|
+
projectSkillDir: path12.join(".agents", "skills"),
|
|
1748
|
+
globalSkillDir: path12.join(".agents", "skills")
|
|
1749
|
+
},
|
|
1750
|
+
cursor: {
|
|
1751
|
+
id: "cursor",
|
|
1752
|
+
label: "Cursor",
|
|
1753
|
+
projectSkillDir: path12.join(".cursor", "skills"),
|
|
1754
|
+
globalSkillDir: path12.join(".cursor", "skills")
|
|
1755
|
+
},
|
|
1756
|
+
gemini: {
|
|
1757
|
+
id: "gemini",
|
|
1758
|
+
label: "Gemini CLI",
|
|
1759
|
+
projectSkillDir: path12.join(".gemini", "skills"),
|
|
1760
|
+
globalSkillDir: path12.join(".gemini", "skills")
|
|
1761
|
+
},
|
|
1762
|
+
copilot: {
|
|
1763
|
+
id: "copilot",
|
|
1764
|
+
label: "GitHub Copilot",
|
|
1765
|
+
projectSkillDir: path12.join(".github", "skills"),
|
|
1766
|
+
globalSkillDir: path12.join(".copilot", "skills")
|
|
1767
|
+
},
|
|
1768
|
+
opencode: {
|
|
1769
|
+
id: "opencode",
|
|
1770
|
+
label: "OpenCode",
|
|
1771
|
+
projectSkillDir: path12.join(".opencode", "skills"),
|
|
1772
|
+
globalSkillDir: path12.join(".config", "opencode", "skills")
|
|
1773
|
+
},
|
|
1774
|
+
windsurf: {
|
|
1775
|
+
id: "windsurf",
|
|
1776
|
+
label: "Windsurf",
|
|
1777
|
+
projectSkillDir: path12.join(".windsurf", "skills"),
|
|
1778
|
+
globalSkillDir: path12.join(".codeium", "windsurf", "skills")
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
var ALIASES = {
|
|
1782
|
+
all: "all",
|
|
1783
|
+
agent: "antigravity",
|
|
1784
|
+
antigravity: "antigravity",
|
|
1785
|
+
claude: "claude",
|
|
1786
|
+
"claude-code": "claude",
|
|
1787
|
+
codex: "codex",
|
|
1788
|
+
cursor: "cursor",
|
|
1789
|
+
gemini: "gemini",
|
|
1790
|
+
"gemini-cli": "gemini",
|
|
1791
|
+
copilot: "copilot",
|
|
1792
|
+
github: "copilot",
|
|
1793
|
+
"github-copilot": "copilot",
|
|
1794
|
+
opencode: "opencode",
|
|
1795
|
+
"open-code": "opencode",
|
|
1796
|
+
windsurf: "windsurf"
|
|
1797
|
+
};
|
|
1798
|
+
function parseAgentProviderList(value, fallback = "all") {
|
|
1799
|
+
const raw = value?.trim();
|
|
1800
|
+
if (!raw) {
|
|
1801
|
+
return fallback === "all" ? [...AGENT_PROVIDER_IDS] : fallback;
|
|
1802
|
+
}
|
|
1803
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1804
|
+
for (const entry of raw.split(",")) {
|
|
1805
|
+
const key = entry.trim().toLowerCase();
|
|
1806
|
+
if (!key) {
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
const resolved = ALIASES[key];
|
|
1810
|
+
if (!resolved) {
|
|
1811
|
+
throw new Error(`Unknown agent provider \`${entry}\`. Supported: ${AGENT_PROVIDER_IDS.join(", ")}, all.`);
|
|
1812
|
+
}
|
|
1813
|
+
if (resolved === "all") {
|
|
1814
|
+
for (const id of AGENT_PROVIDER_IDS) {
|
|
1815
|
+
ids.add(id);
|
|
1816
|
+
}
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
ids.add(resolved);
|
|
1820
|
+
}
|
|
1821
|
+
return [...ids];
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// src/core/managed-block.ts
|
|
1825
|
+
function hasManagedBlock(content, begin, end) {
|
|
1826
|
+
return content.includes(begin) && content.includes(end);
|
|
1827
|
+
}
|
|
1828
|
+
function upsertManagedBlock(content, block, begin, end) {
|
|
1829
|
+
const start = content.indexOf(begin);
|
|
1830
|
+
const finish = content.indexOf(end);
|
|
1831
|
+
const normalizedBlock = block.endsWith("\n") ? block : `${block}
|
|
1832
|
+
`;
|
|
1833
|
+
if (start !== -1 && finish !== -1 && finish > start) {
|
|
1834
|
+
const afterEnd = finish + end.length;
|
|
1835
|
+
return `${content.slice(0, start)}${normalizedBlock}${content.slice(afterEnd).replace(/^\n+/, "")}`;
|
|
1836
|
+
}
|
|
1837
|
+
const prefix = content.trim().length > 0 ? `${content.trimEnd()}
|
|
1838
|
+
|
|
1839
|
+
` : "";
|
|
1840
|
+
return `${prefix}${normalizedBlock}`;
|
|
1841
|
+
}
|
|
1842
|
+
function removeManagedBlock(content, begin, end) {
|
|
1843
|
+
const start = content.indexOf(begin);
|
|
1844
|
+
const finish = content.indexOf(end);
|
|
1845
|
+
if (start === -1 || finish === -1 || finish < start) {
|
|
1846
|
+
return content;
|
|
1847
|
+
}
|
|
1848
|
+
const afterEnd = finish + end.length;
|
|
1849
|
+
return `${content.slice(0, start).trimEnd()}${content.slice(afterEnd).replace(/^\n+/, "\n")}`.trimStart();
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// src/core/threadroot-skill.ts
|
|
1853
|
+
var THREADROOT_SKILL_NAME = "threadroot";
|
|
1854
|
+
var THREADROOT_MANAGED_MARKER = "<!-- threadroot:managed skill -->";
|
|
1855
|
+
function threadrootSkillContent(provider, scope) {
|
|
1856
|
+
const scopeLine = scope === "global" ? "This is a global machine-level skill. Use it only when the current repository contains `.threadroot/` or the user asks for Threadroot." : "This is a project-level skill for the current repository.";
|
|
1857
|
+
return [
|
|
1858
|
+
"---",
|
|
1859
|
+
`name: ${THREADROOT_SKILL_NAME}`,
|
|
1860
|
+
"description: Use when a repository contains .threadroot/ or the user asks to initialize, inspect, repair, or use Threadroot harness context, skills, tools, memory, connections, or agent setup.",
|
|
1861
|
+
"---",
|
|
1862
|
+
"",
|
|
1863
|
+
THREADROOT_MANAGED_MARKER,
|
|
1864
|
+
"",
|
|
1865
|
+
"# Threadroot Harness",
|
|
1866
|
+
"",
|
|
1867
|
+
scopeLine,
|
|
1868
|
+
"",
|
|
1869
|
+
"Threadroot keeps agent-facing project context in `.threadroot/` and exposes it through deterministic CLI commands. Keep broad context out of the chat until it is task-relevant.",
|
|
1870
|
+
"",
|
|
1871
|
+
"## Workflow",
|
|
1872
|
+
"",
|
|
1873
|
+
"1. If `threadroot --version` works, use `threadroot`. Otherwise use `npx --yes threadroot@latest` for one-off commands.",
|
|
1874
|
+
"2. If `.threadroot/harness.yaml` is missing and the user wants setup, run `threadroot init` or `npx --yes threadroot@latest init`.",
|
|
1875
|
+
"3. Before coding in a Threadroot repo, run `threadroot doctor` and resolve errors. Treat warnings as review items, not automatic blockers.",
|
|
1876
|
+
'4. For the current task, run `threadroot context "<task>"` and use the returned skills, rules, tools, memory, and references before doing broad file reads.',
|
|
1877
|
+
"5. Use `threadroot status` to inspect harness state and `threadroot diff` only when compiled adapter outputs are enabled.",
|
|
1878
|
+
"6. Use `threadroot tools list`, `threadroot tools check`, and `threadroot run <tool>` for explicit local capabilities. Confirm risky tools when required.",
|
|
1879
|
+
"7. Do not create provider-specific files unless the user asks. Use `threadroot expose <agent>` when native project skill shims are desired.",
|
|
1880
|
+
"",
|
|
1881
|
+
"## Useful Commands",
|
|
1882
|
+
"",
|
|
1883
|
+
"```bash",
|
|
1884
|
+
"threadroot doctor",
|
|
1885
|
+
"threadroot status",
|
|
1886
|
+
'threadroot context "<task>"',
|
|
1887
|
+
"threadroot skills list",
|
|
1888
|
+
"threadroot tools list",
|
|
1889
|
+
"threadroot packs list",
|
|
1890
|
+
"```",
|
|
1891
|
+
"",
|
|
1892
|
+
"## Boundaries",
|
|
1893
|
+
"",
|
|
1894
|
+
"- `.threadroot/` is the source of truth.",
|
|
1895
|
+
"- Keep generated or exposed provider files thin.",
|
|
1896
|
+
"- Never store secrets in Threadroot. Connections should wrap locally authenticated CLIs.",
|
|
1897
|
+
"- Inspect external skills, scripts, tools, and MCP servers before trusting them.",
|
|
1898
|
+
"",
|
|
1899
|
+
`Provider target: ${provider.label}.`,
|
|
1900
|
+
""
|
|
1901
|
+
].join("\n");
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// src/core/setup.ts
|
|
1905
|
+
var CODEX_AGENTS_BEGIN = "<!-- threadroot:begin global-codex -->";
|
|
1906
|
+
var CODEX_AGENTS_END = "<!-- threadroot:end global-codex -->";
|
|
1907
|
+
var CODEX_MCP_BEGIN = "# threadroot:begin codex-mcp";
|
|
1908
|
+
var CODEX_MCP_END = "# threadroot:end codex-mcp";
|
|
1909
|
+
async function readMaybe(filePath) {
|
|
1910
|
+
try {
|
|
1911
|
+
return await readFile6(filePath, "utf8");
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
if (error.code === "ENOENT") {
|
|
1914
|
+
return void 0;
|
|
1915
|
+
}
|
|
1916
|
+
throw error;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
async function fileExists(filePath) {
|
|
1920
|
+
try {
|
|
1921
|
+
const info = await stat3(filePath);
|
|
1922
|
+
return info.isFile();
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
if (error.code === "ENOENT") {
|
|
1925
|
+
return false;
|
|
1926
|
+
}
|
|
1927
|
+
throw error;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
function displayPath(home, filePath) {
|
|
1931
|
+
const relative = path13.relative(home, filePath);
|
|
1932
|
+
return relative && !relative.startsWith("..") ? path13.join("~", relative) : filePath;
|
|
1933
|
+
}
|
|
1934
|
+
function globalSkillPath(home, provider) {
|
|
1935
|
+
return path13.join(home, provider.globalSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
|
|
1936
|
+
}
|
|
1937
|
+
function codexAgentsPath(home) {
|
|
1938
|
+
return path13.join(home, ".codex", "AGENTS.md");
|
|
1939
|
+
}
|
|
1940
|
+
function codexConfigPath(home) {
|
|
1941
|
+
return path13.join(home, ".codex", "config.toml");
|
|
1942
|
+
}
|
|
1943
|
+
function codexAgentsBlock() {
|
|
1944
|
+
return [
|
|
1945
|
+
CODEX_AGENTS_BEGIN,
|
|
1946
|
+
"## Threadroot",
|
|
1947
|
+
"",
|
|
1948
|
+
"When a repository contains `.threadroot/`, treat it as the source of truth for agent harness context.",
|
|
1949
|
+
'Before coding, prefer `threadroot doctor` and `threadroot context "<task>"` over broad, unfocused file reads.',
|
|
1950
|
+
"Do not create provider-specific project files unless the user asks; use `threadroot expose <agent>` for that.",
|
|
1951
|
+
CODEX_AGENTS_END,
|
|
1952
|
+
""
|
|
1953
|
+
].join("\n");
|
|
1954
|
+
}
|
|
1955
|
+
function codexMcpBlock() {
|
|
1956
|
+
return [
|
|
1957
|
+
CODEX_MCP_BEGIN,
|
|
1958
|
+
"[mcp_servers.threadroot]",
|
|
1959
|
+
'command = "threadroot"',
|
|
1960
|
+
'args = ["mcp"]',
|
|
1961
|
+
CODEX_MCP_END,
|
|
1962
|
+
""
|
|
1963
|
+
].join("\n");
|
|
1964
|
+
}
|
|
1965
|
+
async function setupGlobalSkill(home, provider, mode, force) {
|
|
1966
|
+
const filePath = globalSkillPath(home, provider);
|
|
1967
|
+
const shown = displayPath(home, filePath);
|
|
1968
|
+
const desired = threadrootSkillContent(provider, "global");
|
|
1969
|
+
const existing = await readMaybe(filePath);
|
|
1970
|
+
if (mode === "check") {
|
|
1971
|
+
if (existing === void 0) {
|
|
1972
|
+
return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status: "missing" };
|
|
1973
|
+
}
|
|
1974
|
+
return {
|
|
1975
|
+
kind: "skill",
|
|
1976
|
+
agent: provider.id,
|
|
1977
|
+
label: provider.label,
|
|
1978
|
+
path: shown,
|
|
1979
|
+
status: existing === desired ? "unchanged" : "present",
|
|
1980
|
+
message: existing === desired ? void 0 : "Existing global skill differs from the current Threadroot template."
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
if (mode === "undo") {
|
|
1984
|
+
if (existing === void 0) {
|
|
1985
|
+
return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status: "missing" };
|
|
1986
|
+
}
|
|
1987
|
+
if (!existing.includes(THREADROOT_MANAGED_MARKER)) {
|
|
1988
|
+
return {
|
|
1989
|
+
kind: "skill",
|
|
1990
|
+
agent: provider.id,
|
|
1991
|
+
label: provider.label,
|
|
1992
|
+
path: shown,
|
|
1993
|
+
status: "skipped",
|
|
1994
|
+
message: "Existing skill is not Threadroot-managed."
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
await rm(path13.dirname(filePath), { recursive: true, force: true });
|
|
1998
|
+
return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status: "removed" };
|
|
1999
|
+
}
|
|
2000
|
+
if (existing === desired) {
|
|
2001
|
+
return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status: "unchanged" };
|
|
2002
|
+
}
|
|
2003
|
+
if (existing !== void 0 && !existing.includes(THREADROOT_MANAGED_MARKER) && !force) {
|
|
2004
|
+
return {
|
|
2005
|
+
kind: "skill",
|
|
2006
|
+
agent: provider.id,
|
|
2007
|
+
label: provider.label,
|
|
2008
|
+
path: shown,
|
|
2009
|
+
status: "skipped",
|
|
2010
|
+
message: "Existing skill is not Threadroot-managed. Re-run with --force to replace it."
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
const status = existing === void 0 ? "create" : "update";
|
|
2014
|
+
if (mode === "dry-run") {
|
|
2015
|
+
return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status };
|
|
2016
|
+
}
|
|
2017
|
+
await mkdir5(path13.dirname(filePath), { recursive: true });
|
|
2018
|
+
await writeFile5(filePath, desired, "utf8");
|
|
2019
|
+
return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status };
|
|
2020
|
+
}
|
|
2021
|
+
async function setupCodexAgents(home, mode) {
|
|
2022
|
+
const filePath = codexAgentsPath(home);
|
|
2023
|
+
const shown = displayPath(home, filePath);
|
|
2024
|
+
const existing = await readMaybe(filePath) ?? "";
|
|
2025
|
+
const desired = upsertManagedBlock(existing, codexAgentsBlock(), CODEX_AGENTS_BEGIN, CODEX_AGENTS_END);
|
|
2026
|
+
if (mode === "check") {
|
|
2027
|
+
return {
|
|
2028
|
+
kind: "codex-agents",
|
|
2029
|
+
agent: "codex",
|
|
2030
|
+
label: "Codex global AGENTS.md",
|
|
2031
|
+
path: shown,
|
|
2032
|
+
status: hasManagedBlock(existing, CODEX_AGENTS_BEGIN, CODEX_AGENTS_END) ? "present" : "missing"
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
if (mode === "undo") {
|
|
2036
|
+
if (!hasManagedBlock(existing, CODEX_AGENTS_BEGIN, CODEX_AGENTS_END)) {
|
|
2037
|
+
return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status: "missing" };
|
|
2038
|
+
}
|
|
2039
|
+
await writeFile5(filePath, removeManagedBlock(existing, CODEX_AGENTS_BEGIN, CODEX_AGENTS_END), "utf8");
|
|
2040
|
+
return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status: "removed" };
|
|
2041
|
+
}
|
|
2042
|
+
if (existing === desired) {
|
|
2043
|
+
return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status: "unchanged" };
|
|
2044
|
+
}
|
|
2045
|
+
const status = existing.trim() ? "update" : "create";
|
|
2046
|
+
if (mode === "dry-run") {
|
|
2047
|
+
return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status };
|
|
2048
|
+
}
|
|
2049
|
+
await mkdir5(path13.dirname(filePath), { recursive: true });
|
|
2050
|
+
await writeFile5(filePath, desired, "utf8");
|
|
2051
|
+
return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status };
|
|
2052
|
+
}
|
|
2053
|
+
async function setupCodexMcp(home, mode) {
|
|
2054
|
+
const filePath = codexConfigPath(home);
|
|
2055
|
+
const shown = displayPath(home, filePath);
|
|
2056
|
+
const existing = await readMaybe(filePath) ?? "";
|
|
2057
|
+
if (existing.includes("[mcp_servers.threadroot]") && !hasManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END)) {
|
|
2058
|
+
return {
|
|
2059
|
+
kind: "codex-mcp",
|
|
2060
|
+
agent: "codex",
|
|
2061
|
+
label: "Codex MCP config",
|
|
2062
|
+
path: shown,
|
|
2063
|
+
status: "skipped",
|
|
2064
|
+
message: "Existing unmanaged [mcp_servers.threadroot] table found. Leaving it untouched."
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
const desired = upsertManagedBlock(existing, codexMcpBlock(), CODEX_MCP_BEGIN, CODEX_MCP_END);
|
|
2068
|
+
if (mode === "check") {
|
|
2069
|
+
return {
|
|
2070
|
+
kind: "codex-mcp",
|
|
2071
|
+
agent: "codex",
|
|
2072
|
+
label: "Codex MCP config",
|
|
2073
|
+
path: shown,
|
|
2074
|
+
status: hasManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END) ? "present" : "missing"
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
if (mode === "undo") {
|
|
2078
|
+
if (!hasManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END)) {
|
|
2079
|
+
return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status: "missing" };
|
|
2080
|
+
}
|
|
2081
|
+
await writeFile5(filePath, removeManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END), "utf8");
|
|
2082
|
+
return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status: "removed" };
|
|
2083
|
+
}
|
|
2084
|
+
if (existing === desired) {
|
|
2085
|
+
return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status: "unchanged" };
|
|
2086
|
+
}
|
|
2087
|
+
const status = existing.trim() ? "update" : "create";
|
|
2088
|
+
if (mode === "dry-run") {
|
|
2089
|
+
return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status };
|
|
2090
|
+
}
|
|
2091
|
+
await mkdir5(path13.dirname(filePath), { recursive: true });
|
|
2092
|
+
await writeFile5(filePath, desired, "utf8");
|
|
2093
|
+
return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status };
|
|
2094
|
+
}
|
|
2095
|
+
async function setupGlobal(options = {}) {
|
|
2096
|
+
const home = options.home ?? homedir();
|
|
2097
|
+
const mode = options.mode ?? "write";
|
|
2098
|
+
const providerIds = parseAgentProviderList(options.agents, "all");
|
|
2099
|
+
const entries = [];
|
|
2100
|
+
for (const id of providerIds) {
|
|
2101
|
+
entries.push(await setupGlobalSkill(home, AGENT_PROVIDERS[id], mode, options.force ?? false));
|
|
2102
|
+
}
|
|
2103
|
+
if (providerIds.includes("codex")) {
|
|
2104
|
+
entries.push(await setupCodexAgents(home, mode));
|
|
2105
|
+
if (options.mcp) {
|
|
2106
|
+
entries.push(await setupCodexMcp(home, mode));
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
return { entries };
|
|
2110
|
+
}
|
|
2111
|
+
async function hasGlobalThreadrootSkill(home, agent) {
|
|
2112
|
+
return fileExists(globalSkillPath(home ?? homedir(), AGENT_PROVIDERS[agent]));
|
|
2113
|
+
}
|
|
2114
|
+
|
|
1714
2115
|
// src/core/tools/authorize.ts
|
|
1715
2116
|
function authorizeTool(tool, options) {
|
|
1716
2117
|
const trusted = options.trusted ?? true;
|
|
@@ -1834,8 +2235,8 @@ function inputEnv(values) {
|
|
|
1834
2235
|
}
|
|
1835
2236
|
|
|
1836
2237
|
// src/core/tools/create.ts
|
|
1837
|
-
import { mkdir as
|
|
1838
|
-
import
|
|
2238
|
+
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
2239
|
+
import path14 from "path";
|
|
1839
2240
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
1840
2241
|
var ToolCreateError = class extends Error {
|
|
1841
2242
|
constructor(message) {
|
|
@@ -1852,7 +2253,7 @@ function assertSafeName(name) {
|
|
|
1852
2253
|
}
|
|
1853
2254
|
}
|
|
1854
2255
|
function assertSafeScript(script) {
|
|
1855
|
-
if (
|
|
2256
|
+
if (path14.isAbsolute(script) || script.split(/[\\/]/).includes("..")) {
|
|
1856
2257
|
throw new ToolCreateError(`Script path must be inside the harness directory: ${script}`);
|
|
1857
2258
|
}
|
|
1858
2259
|
}
|
|
@@ -1881,9 +2282,9 @@ async function createTool(repoRoot, input2, options) {
|
|
|
1881
2282
|
throw new ToolCreateError(`Invalid tool definition: ${detail}`);
|
|
1882
2283
|
}
|
|
1883
2284
|
const dir = scope === "project" ? projectObjectDir(repoRoot, "tools") : userObjectDir("tools", options.home);
|
|
1884
|
-
const filePath =
|
|
1885
|
-
await
|
|
1886
|
-
await
|
|
2285
|
+
const filePath = path14.join(dir, `${input2.name}.yaml`);
|
|
2286
|
+
await mkdir6(dir, { recursive: true });
|
|
2287
|
+
await writeFile6(filePath, stringifyYaml3(parsed.data), { encoding: "utf8", flag: options.force ? "w" : "wx" }).catch(
|
|
1887
2288
|
(error) => {
|
|
1888
2289
|
if (error.code === "EEXIST") {
|
|
1889
2290
|
throw new ToolCreateError(`Tool \`${input2.name}\` already exists at ${filePath}. Pass force to overwrite.`);
|
|
@@ -1895,8 +2296,8 @@ async function createTool(repoRoot, input2, options) {
|
|
|
1895
2296
|
}
|
|
1896
2297
|
|
|
1897
2298
|
// src/core/tools/catalog.ts
|
|
1898
|
-
import { readFile as
|
|
1899
|
-
import
|
|
2299
|
+
import { readFile as readFile7, stat as stat4 } from "fs/promises";
|
|
2300
|
+
import path15 from "path";
|
|
1900
2301
|
var NAME_RE3 = /^[a-z0-9][a-z0-9-]*$/;
|
|
1901
2302
|
var DESTRUCTIVE_RE = /\b(migrate|deploy|publish|release|prune|reset|destroy|drop|delete|rm|push|seed|wipe)\b/i;
|
|
1902
2303
|
function looksDestructive(name, command) {
|
|
@@ -1908,20 +2309,20 @@ function sanitize(name) {
|
|
|
1908
2309
|
}
|
|
1909
2310
|
async function exists2(file) {
|
|
1910
2311
|
try {
|
|
1911
|
-
await
|
|
2312
|
+
await stat4(file);
|
|
1912
2313
|
return true;
|
|
1913
2314
|
} catch {
|
|
1914
2315
|
return false;
|
|
1915
2316
|
}
|
|
1916
2317
|
}
|
|
1917
2318
|
async function detectPackageManager(repoRoot) {
|
|
1918
|
-
if (await exists2(
|
|
2319
|
+
if (await exists2(path15.join(repoRoot, "pnpm-lock.yaml"))) {
|
|
1919
2320
|
return "pnpm";
|
|
1920
2321
|
}
|
|
1921
|
-
if (await exists2(
|
|
2322
|
+
if (await exists2(path15.join(repoRoot, "yarn.lock"))) {
|
|
1922
2323
|
return "yarn";
|
|
1923
2324
|
}
|
|
1924
|
-
if (await exists2(
|
|
2325
|
+
if (await exists2(path15.join(repoRoot, "bun.lockb"))) {
|
|
1925
2326
|
return "bun";
|
|
1926
2327
|
}
|
|
1927
2328
|
return "npm";
|
|
@@ -1942,10 +2343,10 @@ function orderScripts(names) {
|
|
|
1942
2343
|
});
|
|
1943
2344
|
}
|
|
1944
2345
|
async function fromPackageJson(repoRoot) {
|
|
1945
|
-
const file =
|
|
2346
|
+
const file = path15.join(repoRoot, "package.json");
|
|
1946
2347
|
let raw;
|
|
1947
2348
|
try {
|
|
1948
|
-
raw = await
|
|
2349
|
+
raw = await readFile7(file, "utf8");
|
|
1949
2350
|
} catch {
|
|
1950
2351
|
return [];
|
|
1951
2352
|
}
|
|
@@ -1979,7 +2380,7 @@ async function fromPackageJson(repoRoot) {
|
|
|
1979
2380
|
async function fromTargets(repoRoot, fileName, runner, source) {
|
|
1980
2381
|
let raw;
|
|
1981
2382
|
try {
|
|
1982
|
-
raw = await
|
|
2383
|
+
raw = await readFile7(path15.join(repoRoot, fileName), "utf8");
|
|
1983
2384
|
} catch {
|
|
1984
2385
|
return [];
|
|
1985
2386
|
}
|
|
@@ -2145,7 +2546,7 @@ async function checkToolHealth(repoRoot, tool) {
|
|
|
2145
2546
|
// src/core/doctor.ts
|
|
2146
2547
|
async function exists3(filePath) {
|
|
2147
2548
|
try {
|
|
2148
|
-
await
|
|
2549
|
+
await stat5(filePath);
|
|
2149
2550
|
return true;
|
|
2150
2551
|
} catch (error) {
|
|
2151
2552
|
if (error.code === "ENOENT") {
|
|
@@ -2157,17 +2558,29 @@ async function exists3(filePath) {
|
|
|
2157
2558
|
function finding2(severity, code, message, pathValue) {
|
|
2158
2559
|
return pathValue ? { severity, code, message, path: pathValue } : { severity, code, message };
|
|
2159
2560
|
}
|
|
2160
|
-
async function
|
|
2561
|
+
async function mcpConfigHints(repoRoot) {
|
|
2161
2562
|
const configs = [".vscode/mcp.json", ".cursor/mcp.json", ".mcp.json"];
|
|
2162
|
-
const present = await Promise.all(configs.map((config) => exists3(
|
|
2563
|
+
const present = await Promise.all(configs.map((config) => exists3(path16.join(repoRoot, config))));
|
|
2163
2564
|
if (present.some(Boolean)) {
|
|
2164
2565
|
return [];
|
|
2165
2566
|
}
|
|
2166
2567
|
return [
|
|
2167
2568
|
finding2(
|
|
2168
|
-
"
|
|
2569
|
+
"info",
|
|
2169
2570
|
"mcp_config_missing",
|
|
2170
|
-
"No project-local MCP config found.
|
|
2571
|
+
"No project-local MCP config found. This is fine for local-only harnesses; run `threadroot mcp setup --write` only when this repo should expose MCP tools to local agents."
|
|
2572
|
+
)
|
|
2573
|
+
];
|
|
2574
|
+
}
|
|
2575
|
+
async function globalSetupHints(home) {
|
|
2576
|
+
if (await hasGlobalThreadrootSkill(home, "codex")) {
|
|
2577
|
+
return [];
|
|
2578
|
+
}
|
|
2579
|
+
return [
|
|
2580
|
+
finding2(
|
|
2581
|
+
"info",
|
|
2582
|
+
"global_setup_missing",
|
|
2583
|
+
"Codex global Threadroot setup was not detected. Run `threadroot setup --global --agent codex` for one-time machine setup."
|
|
2171
2584
|
)
|
|
2172
2585
|
];
|
|
2173
2586
|
}
|
|
@@ -2321,7 +2734,7 @@ async function doctor(repoRoot, options = {}) {
|
|
|
2321
2734
|
)
|
|
2322
2735
|
);
|
|
2323
2736
|
}
|
|
2324
|
-
const scriptsDir =
|
|
2737
|
+
const scriptsDir = path16.join(path16.dirname(skill.sourcePath), "scripts");
|
|
2325
2738
|
if (await exists3(scriptsDir)) {
|
|
2326
2739
|
findings.push(
|
|
2327
2740
|
finding2(
|
|
@@ -2333,25 +2746,35 @@ async function doctor(repoRoot, options = {}) {
|
|
|
2333
2746
|
);
|
|
2334
2747
|
}
|
|
2335
2748
|
}
|
|
2336
|
-
findings.push(...await
|
|
2749
|
+
findings.push(...await globalSetupHints(options.home));
|
|
2750
|
+
findings.push(...await mcpConfigHints(repoRoot));
|
|
2337
2751
|
return summarize(findings);
|
|
2338
2752
|
}
|
|
2339
2753
|
function summarize(findings) {
|
|
2340
2754
|
const errors = findings.filter((entry) => entry.severity === "error").length;
|
|
2341
2755
|
const warnings = findings.filter((entry) => entry.severity === "warning").length;
|
|
2342
|
-
|
|
2756
|
+
const info = findings.filter((entry) => entry.severity === "info").length;
|
|
2757
|
+
return { ok: errors === 0, findings, summary: { errors, warnings, info } };
|
|
2343
2758
|
}
|
|
2344
2759
|
|
|
2345
2760
|
// src/commands/doctor.ts
|
|
2346
2761
|
async function runDoctor(repoRoot) {
|
|
2347
2762
|
const report = await doctor(repoRoot);
|
|
2348
|
-
|
|
2763
|
+
const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
|
|
2764
|
+
const hints = report.findings.filter((finding3) => finding3.severity === "info");
|
|
2765
|
+
if (actionable.length === 0) {
|
|
2349
2766
|
console.log("Threadroot doctor: clean");
|
|
2767
|
+
for (const finding3 of hints) {
|
|
2768
|
+
const suffix = finding3.path ? ` (${finding3.path})` : "";
|
|
2769
|
+
console.log(`- hint ${finding3.code}: ${finding3.message}${suffix}`);
|
|
2770
|
+
}
|
|
2350
2771
|
return;
|
|
2351
2772
|
}
|
|
2352
|
-
console.log(
|
|
2773
|
+
console.log(
|
|
2774
|
+
`Threadroot doctor: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`
|
|
2775
|
+
);
|
|
2353
2776
|
for (const finding3 of report.findings) {
|
|
2354
|
-
const label = finding3.severity === "error" ? "error" : "warning";
|
|
2777
|
+
const label = finding3.severity === "error" ? "error" : finding3.severity === "warning" ? "warning" : "hint";
|
|
2355
2778
|
const suffix = finding3.path ? ` (${finding3.path})` : "";
|
|
2356
2779
|
console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
|
|
2357
2780
|
}
|
|
@@ -2360,13 +2783,117 @@ async function runDoctor(repoRoot) {
|
|
|
2360
2783
|
}
|
|
2361
2784
|
}
|
|
2362
2785
|
|
|
2786
|
+
// src/core/expose.ts
|
|
2787
|
+
import { mkdir as mkdir7, readFile as readFile8, rm as rm2, stat as stat6, writeFile as writeFile7 } from "fs/promises";
|
|
2788
|
+
import path17 from "path";
|
|
2789
|
+
async function readMaybe2(filePath) {
|
|
2790
|
+
try {
|
|
2791
|
+
return await readFile8(filePath, "utf8");
|
|
2792
|
+
} catch (error) {
|
|
2793
|
+
if (error.code === "ENOENT") {
|
|
2794
|
+
return void 0;
|
|
2795
|
+
}
|
|
2796
|
+
throw error;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
function projectSkillPath(repoRoot, provider) {
|
|
2800
|
+
return path17.join(repoRoot, provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
|
|
2801
|
+
}
|
|
2802
|
+
function relSkillPath(provider) {
|
|
2803
|
+
return path17.join(provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
|
|
2804
|
+
}
|
|
2805
|
+
async function exposeOne(repoRoot, provider, mode, force) {
|
|
2806
|
+
const relativePath = relSkillPath(provider);
|
|
2807
|
+
const absolutePath = projectSkillPath(repoRoot, provider);
|
|
2808
|
+
const desired = threadrootSkillContent(provider, "project");
|
|
2809
|
+
const existing = await readMaybe2(absolutePath);
|
|
2810
|
+
if (mode === "check") {
|
|
2811
|
+
if (existing === void 0) {
|
|
2812
|
+
return { agent: provider.id, label: provider.label, path: relativePath, status: "missing" };
|
|
2813
|
+
}
|
|
2814
|
+
return {
|
|
2815
|
+
agent: provider.id,
|
|
2816
|
+
label: provider.label,
|
|
2817
|
+
path: relativePath,
|
|
2818
|
+
status: existing === desired ? "unchanged" : "present",
|
|
2819
|
+
message: existing === desired ? void 0 : "Existing project skill differs from the current Threadroot template."
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
if (mode === "undo") {
|
|
2823
|
+
if (existing === void 0) {
|
|
2824
|
+
return { agent: provider.id, label: provider.label, path: relativePath, status: "missing" };
|
|
2825
|
+
}
|
|
2826
|
+
if (!existing.includes(THREADROOT_MANAGED_MARKER)) {
|
|
2827
|
+
return {
|
|
2828
|
+
agent: provider.id,
|
|
2829
|
+
label: provider.label,
|
|
2830
|
+
path: relativePath,
|
|
2831
|
+
status: "skipped",
|
|
2832
|
+
message: "Existing skill is not Threadroot-managed."
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
await rm2(path17.dirname(absolutePath), { recursive: true, force: true });
|
|
2836
|
+
return { agent: provider.id, label: provider.label, path: relativePath, status: "removed" };
|
|
2837
|
+
}
|
|
2838
|
+
if (existing === desired) {
|
|
2839
|
+
return { agent: provider.id, label: provider.label, path: relativePath, status: "unchanged" };
|
|
2840
|
+
}
|
|
2841
|
+
if (existing !== void 0 && !existing.includes(THREADROOT_MANAGED_MARKER) && !force) {
|
|
2842
|
+
return {
|
|
2843
|
+
agent: provider.id,
|
|
2844
|
+
label: provider.label,
|
|
2845
|
+
path: relativePath,
|
|
2846
|
+
status: "skipped",
|
|
2847
|
+
message: "Existing skill is not Threadroot-managed. Re-run with --force to replace it."
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
const status = existing === void 0 ? "create" : "update";
|
|
2851
|
+
if (mode === "dry-run") {
|
|
2852
|
+
return { agent: provider.id, label: provider.label, path: relativePath, status };
|
|
2853
|
+
}
|
|
2854
|
+
await mkdir7(path17.dirname(absolutePath), { recursive: true });
|
|
2855
|
+
await writeFile7(absolutePath, desired, "utf8");
|
|
2856
|
+
return { agent: provider.id, label: provider.label, path: relativePath, status };
|
|
2857
|
+
}
|
|
2858
|
+
async function exposeProject(repoRoot, options = {}) {
|
|
2859
|
+
const providerIds = parseAgentProviderList(options.agents, ["codex"]);
|
|
2860
|
+
const mode = options.mode ?? "write";
|
|
2861
|
+
const entries = [];
|
|
2862
|
+
for (const id of providerIds) {
|
|
2863
|
+
entries.push(await exposeOne(repoRoot, AGENT_PROVIDERS[id], mode, options.force ?? false));
|
|
2864
|
+
}
|
|
2865
|
+
return { entries };
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// src/commands/expose.ts
|
|
2869
|
+
function modeFromOptions(options) {
|
|
2870
|
+
if (options.undo) return "undo";
|
|
2871
|
+
if (options.check) return "check";
|
|
2872
|
+
if (options.dryRun) return "dry-run";
|
|
2873
|
+
return "write";
|
|
2874
|
+
}
|
|
2875
|
+
async function runExpose(repoRoot, agent, options) {
|
|
2876
|
+
const mode = modeFromOptions(options);
|
|
2877
|
+
const result = await exposeProject(repoRoot, {
|
|
2878
|
+
agents: agent,
|
|
2879
|
+
mode,
|
|
2880
|
+
force: options.force
|
|
2881
|
+
});
|
|
2882
|
+
const verb = mode === "dry-run" ? "Project exposure plan" : mode === "check" ? "Project exposure check" : mode === "undo" ? "Removed project exposure" : "Exposed Threadroot project skills";
|
|
2883
|
+
console.log(`${verb}:`);
|
|
2884
|
+
for (const entry of result.entries) {
|
|
2885
|
+
const suffix = entry.message ? ` - ${entry.message}` : "";
|
|
2886
|
+
console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2363
2890
|
// src/core/init/index.ts
|
|
2364
|
-
import { mkdir as
|
|
2365
|
-
import
|
|
2891
|
+
import { mkdir as mkdir9, stat as stat8, writeFile as writeFile8 } from "fs/promises";
|
|
2892
|
+
import path22 from "path";
|
|
2366
2893
|
|
|
2367
2894
|
// src/core/scan/package.ts
|
|
2368
2895
|
import fs2 from "fs/promises";
|
|
2369
|
-
import
|
|
2896
|
+
import path18 from "path";
|
|
2370
2897
|
|
|
2371
2898
|
// src/core/scan/rules.ts
|
|
2372
2899
|
var ignoredDirectories = /* @__PURE__ */ new Set([
|
|
@@ -2384,13 +2911,13 @@ var ignoredDirectories = /* @__PURE__ */ new Set([
|
|
|
2384
2911
|
// src/core/scan/package.ts
|
|
2385
2912
|
async function readJson(repoRoot, relativePath) {
|
|
2386
2913
|
try {
|
|
2387
|
-
return JSON.parse(await fs2.readFile(
|
|
2914
|
+
return JSON.parse(await fs2.readFile(path18.join(repoRoot, relativePath), "utf8"));
|
|
2388
2915
|
} catch {
|
|
2389
2916
|
return void 0;
|
|
2390
2917
|
}
|
|
2391
2918
|
}
|
|
2392
2919
|
function inferProfile(files, packageJson) {
|
|
2393
|
-
if (files.some((file) =>
|
|
2920
|
+
if (files.some((file) => path18.basename(file) === "dbt_project.yml" || path18.basename(file) === "dbt_project.yaml")) {
|
|
2394
2921
|
return "dbt";
|
|
2395
2922
|
}
|
|
2396
2923
|
const packageMeta = packageJson && typeof packageJson === "object" ? packageJson : void 0;
|
|
@@ -2415,9 +2942,9 @@ function inferProfile(files, packageJson) {
|
|
|
2415
2942
|
|
|
2416
2943
|
// src/core/scan/walk.ts
|
|
2417
2944
|
import fs3 from "fs/promises";
|
|
2418
|
-
import
|
|
2945
|
+
import path19 from "path";
|
|
2419
2946
|
function toPosix(relativePath) {
|
|
2420
|
-
return relativePath.split(
|
|
2947
|
+
return relativePath.split(path19.sep).join("/");
|
|
2421
2948
|
}
|
|
2422
2949
|
async function walkRepo(repoRoot, directory = repoRoot) {
|
|
2423
2950
|
let entries;
|
|
@@ -2428,8 +2955,8 @@ async function walkRepo(repoRoot, directory = repoRoot) {
|
|
|
2428
2955
|
}
|
|
2429
2956
|
const files = [];
|
|
2430
2957
|
for (const entry of entries) {
|
|
2431
|
-
const absolutePath =
|
|
2432
|
-
const relativePath = toPosix(
|
|
2958
|
+
const absolutePath = path19.join(directory, entry.name);
|
|
2959
|
+
const relativePath = toPosix(path19.relative(repoRoot, absolutePath));
|
|
2433
2960
|
if (entry.isDirectory()) {
|
|
2434
2961
|
if (!ignoredDirectories.has(entry.name)) {
|
|
2435
2962
|
files.push(...await walkRepo(repoRoot, absolutePath));
|
|
@@ -2447,17 +2974,17 @@ async function walkRepo(repoRoot, directory = repoRoot) {
|
|
|
2447
2974
|
import { stringify as stringifyYaml4 } from "yaml";
|
|
2448
2975
|
|
|
2449
2976
|
// src/core/init/builtins.ts
|
|
2450
|
-
import { cp, mkdir as
|
|
2451
|
-
import
|
|
2977
|
+
import { cp, mkdir as mkdir8, readdir as readdir3, stat as stat7 } from "fs/promises";
|
|
2978
|
+
import path20 from "path";
|
|
2452
2979
|
import { fileURLToPath } from "url";
|
|
2453
|
-
var DIST_DIR =
|
|
2454
|
-
var PACKAGE_ROOT_FROM_BUNDLE =
|
|
2455
|
-
var PACKAGE_ROOT_FROM_DIST =
|
|
2456
|
-
var PACKAGE_ROOT_FROM_SRC =
|
|
2980
|
+
var DIST_DIR = path20.dirname(fileURLToPath(import.meta.url));
|
|
2981
|
+
var PACKAGE_ROOT_FROM_BUNDLE = path20.resolve(DIST_DIR, "..");
|
|
2982
|
+
var PACKAGE_ROOT_FROM_DIST = path20.resolve(DIST_DIR, "../../..");
|
|
2983
|
+
var PACKAGE_ROOT_FROM_SRC = path20.resolve(DIST_DIR, "../../../..");
|
|
2457
2984
|
var SKILL_PACK_CANDIDATES = [
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2985
|
+
path20.join(PACKAGE_ROOT_FROM_BUNDLE, "skills"),
|
|
2986
|
+
path20.join(PACKAGE_ROOT_FROM_DIST, "skills"),
|
|
2987
|
+
path20.join(PACKAGE_ROOT_FROM_SRC, "skills")
|
|
2461
2988
|
];
|
|
2462
2989
|
var PROJECT_MEMORY_TEMPLATE = [
|
|
2463
2990
|
"# Project",
|
|
@@ -2470,7 +2997,7 @@ var PROJECT_MEMORY_TEMPLATE = [
|
|
|
2470
2997
|
].join("\n");
|
|
2471
2998
|
async function exists4(target) {
|
|
2472
2999
|
try {
|
|
2473
|
-
const info = await
|
|
3000
|
+
const info = await stat7(target);
|
|
2474
3001
|
return info.isDirectory();
|
|
2475
3002
|
} catch {
|
|
2476
3003
|
return false;
|
|
@@ -2487,20 +3014,20 @@ async function bundledSkillsDir() {
|
|
|
2487
3014
|
async function writeBuiltinSkills(repoRoot) {
|
|
2488
3015
|
const sourceDir = await bundledSkillsDir();
|
|
2489
3016
|
const targetDir = projectObjectDir(repoRoot, "skills");
|
|
2490
|
-
await
|
|
3017
|
+
await mkdir8(targetDir, { recursive: true });
|
|
2491
3018
|
const written = [];
|
|
2492
3019
|
const entries = await readdir3(sourceDir, { withFileTypes: true });
|
|
2493
3020
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2494
3021
|
if (!entry.isDirectory()) {
|
|
2495
3022
|
continue;
|
|
2496
3023
|
}
|
|
2497
|
-
const sourceSkill =
|
|
2498
|
-
const sourceSkillFile =
|
|
2499
|
-
if (!await exists4(sourceSkill) || !await
|
|
3024
|
+
const sourceSkill = path20.join(sourceDir, entry.name);
|
|
3025
|
+
const sourceSkillFile = path20.join(sourceSkill, "SKILL.md");
|
|
3026
|
+
if (!await exists4(sourceSkill) || !await stat7(sourceSkillFile).then((info) => info.isFile()).catch(() => false)) {
|
|
2500
3027
|
continue;
|
|
2501
3028
|
}
|
|
2502
|
-
const targetSkill =
|
|
2503
|
-
const targetSkillFile =
|
|
3029
|
+
const targetSkill = path20.join(targetDir, entry.name);
|
|
3030
|
+
const targetSkillFile = path20.join(targetSkill, "SKILL.md");
|
|
2504
3031
|
try {
|
|
2505
3032
|
await cp(sourceSkill, targetSkill, { recursive: true, force: false, errorOnExist: true });
|
|
2506
3033
|
written.push(targetSkillFile);
|
|
@@ -2514,14 +3041,14 @@ async function writeBuiltinSkills(repoRoot) {
|
|
|
2514
3041
|
}
|
|
2515
3042
|
|
|
2516
3043
|
// src/core/init/import.ts
|
|
2517
|
-
import { readFile as
|
|
2518
|
-
import
|
|
3044
|
+
import { readFile as readFile9, readdir as readdir4 } from "fs/promises";
|
|
3045
|
+
import path21 from "path";
|
|
2519
3046
|
var PROSE_PRECEDENCE = ["AGENTS.md", "CLAUDE.md", ".github/copilot-instructions.md"];
|
|
2520
3047
|
var CURSOR_RULES_DIR = ".cursor/rules";
|
|
2521
3048
|
var NAME_RE4 = /^[a-z0-9][a-z0-9-]*$/;
|
|
2522
3049
|
async function readIfExists3(filePath) {
|
|
2523
3050
|
try {
|
|
2524
|
-
return await
|
|
3051
|
+
return await readFile9(filePath, "utf8");
|
|
2525
3052
|
} catch (error) {
|
|
2526
3053
|
if (error.code === "ENOENT") {
|
|
2527
3054
|
return void 0;
|
|
@@ -2565,7 +3092,7 @@ function novelSections(canonical, other) {
|
|
|
2565
3092
|
});
|
|
2566
3093
|
}
|
|
2567
3094
|
async function listCursorRules(repoRoot) {
|
|
2568
|
-
const dir =
|
|
3095
|
+
const dir = path21.join(repoRoot, CURSOR_RULES_DIR);
|
|
2569
3096
|
let entries;
|
|
2570
3097
|
try {
|
|
2571
3098
|
entries = await readdir4(dir);
|
|
@@ -2579,7 +3106,7 @@ async function listCursorRules(repoRoot) {
|
|
|
2579
3106
|
return Promise.all(
|
|
2580
3107
|
files.map(async (name) => ({
|
|
2581
3108
|
file: `${CURSOR_RULES_DIR}/${name}`,
|
|
2582
|
-
content: await
|
|
3109
|
+
content: await readFile9(path21.join(dir, name), "utf8")
|
|
2583
3110
|
}))
|
|
2584
3111
|
);
|
|
2585
3112
|
}
|
|
@@ -2594,7 +3121,7 @@ function globsToApplyTo(value) {
|
|
|
2594
3121
|
return void 0;
|
|
2595
3122
|
}
|
|
2596
3123
|
function ruleName(fileName) {
|
|
2597
|
-
const base =
|
|
3124
|
+
const base = path21.basename(fileName, ".mdc").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2598
3125
|
return NAME_RE4.test(base) ? base : "imported-rule";
|
|
2599
3126
|
}
|
|
2600
3127
|
async function importVendorFiles(repoRoot, options = {}) {
|
|
@@ -2605,7 +3132,7 @@ async function importVendorFiles(repoRoot, options = {}) {
|
|
|
2605
3132
|
if (!wanted(file)) {
|
|
2606
3133
|
continue;
|
|
2607
3134
|
}
|
|
2608
|
-
const content = await readIfExists3(
|
|
3135
|
+
const content = await readIfExists3(path21.join(repoRoot, file));
|
|
2609
3136
|
if (content && content.trim()) {
|
|
2610
3137
|
prose.push({ file, content });
|
|
2611
3138
|
}
|
|
@@ -2650,7 +3177,7 @@ ${novel.map((section) => section.text).join("\n\n")}`.trim();
|
|
|
2650
3177
|
}
|
|
2651
3178
|
|
|
2652
3179
|
// src/core/init/index.ts
|
|
2653
|
-
var DEFAULT_ADAPTERS = [
|
|
3180
|
+
var DEFAULT_ADAPTERS = [];
|
|
2654
3181
|
var AGENTS_FILE2 = "AGENTS.md";
|
|
2655
3182
|
var InitError = class extends Error {
|
|
2656
3183
|
constructor(message) {
|
|
@@ -2660,7 +3187,7 @@ var InitError = class extends Error {
|
|
|
2660
3187
|
};
|
|
2661
3188
|
async function pathExists(target) {
|
|
2662
3189
|
try {
|
|
2663
|
-
await
|
|
3190
|
+
await stat8(target);
|
|
2664
3191
|
return true;
|
|
2665
3192
|
} catch {
|
|
2666
3193
|
return false;
|
|
@@ -2680,7 +3207,7 @@ async function detectName(repoRoot) {
|
|
|
2680
3207
|
if (packageJson && typeof packageJson.name === "string" && packageJson.name.trim()) {
|
|
2681
3208
|
return packageJson.name.trim();
|
|
2682
3209
|
}
|
|
2683
|
-
return
|
|
3210
|
+
return path22.basename(repoRoot);
|
|
2684
3211
|
}
|
|
2685
3212
|
async function writeManifest(repoRoot, manifest) {
|
|
2686
3213
|
const body = {
|
|
@@ -2692,15 +3219,15 @@ async function writeManifest(repoRoot, manifest) {
|
|
|
2692
3219
|
if (manifest.tools.allow.length > 0) {
|
|
2693
3220
|
body.tools = { allow: manifest.tools.allow };
|
|
2694
3221
|
}
|
|
2695
|
-
await
|
|
2696
|
-
await
|
|
3222
|
+
await mkdir9(projectHarnessDir(repoRoot), { recursive: true });
|
|
3223
|
+
await writeFile8(projectManifestPath(repoRoot), stringifyYaml4(body), "utf8");
|
|
2697
3224
|
}
|
|
2698
3225
|
async function writeProjectMemory(repoRoot) {
|
|
2699
3226
|
const dir = projectObjectDir(repoRoot, "memory");
|
|
2700
|
-
await
|
|
2701
|
-
const filePath =
|
|
3227
|
+
await mkdir9(dir, { recursive: true });
|
|
3228
|
+
const filePath = path22.join(dir, "project.md");
|
|
2702
3229
|
try {
|
|
2703
|
-
await
|
|
3230
|
+
await writeFile8(filePath, `${PROJECT_MEMORY_TEMPLATE}
|
|
2704
3231
|
`, { encoding: "utf8", flag: "wx" });
|
|
2705
3232
|
return [filePath];
|
|
2706
3233
|
} catch (error) {
|
|
@@ -2715,16 +3242,16 @@ async function writeImportedRules(repoRoot, report) {
|
|
|
2715
3242
|
return [];
|
|
2716
3243
|
}
|
|
2717
3244
|
const dir = projectObjectDir(repoRoot, "rules");
|
|
2718
|
-
await
|
|
3245
|
+
await mkdir9(dir, { recursive: true });
|
|
2719
3246
|
const written = [];
|
|
2720
3247
|
for (const rule of report.importedRules) {
|
|
2721
|
-
const filePath =
|
|
3248
|
+
const filePath = path22.join(dir, `${rule.name}.md`);
|
|
2722
3249
|
const data = { name: rule.name, scope: "project" };
|
|
2723
3250
|
if (rule.applyTo) {
|
|
2724
3251
|
data.applyTo = rule.applyTo;
|
|
2725
3252
|
}
|
|
2726
3253
|
try {
|
|
2727
|
-
await
|
|
3254
|
+
await writeFile8(filePath, serializeFrontmatter(data, rule.body), { encoding: "utf8", flag: "wx" });
|
|
2728
3255
|
written.push(filePath);
|
|
2729
3256
|
} catch (error) {
|
|
2730
3257
|
if (error.code !== "EEXIST") {
|
|
@@ -2763,7 +3290,7 @@ async function writeStarterTools(repoRoot, profile, force) {
|
|
|
2763
3290
|
async function initHarness(repoRoot, options = {}) {
|
|
2764
3291
|
if (!options.force && await pathExists(projectManifestPath(repoRoot))) {
|
|
2765
3292
|
throw new InitError(
|
|
2766
|
-
`A harness already exists at ${
|
|
3293
|
+
`A harness already exists at ${path22.join(".threadroot", "harness.yaml")}. Re-run with --force to overwrite.`
|
|
2767
3294
|
);
|
|
2768
3295
|
}
|
|
2769
3296
|
const profile = await detectProfile(repoRoot, options.profile);
|
|
@@ -2785,13 +3312,14 @@ async function initHarness(repoRoot, options = {}) {
|
|
|
2785
3312
|
if (options.import !== false) {
|
|
2786
3313
|
report = await importVendorFiles(repoRoot, { include: options.importFiles });
|
|
2787
3314
|
if (report.canonicalBody.trim()) {
|
|
2788
|
-
await
|
|
3315
|
+
await writeFile8(path22.join(repoRoot, AGENTS_FILE2), `${report.canonicalBody.trim()}
|
|
2789
3316
|
`, "utf8");
|
|
2790
3317
|
}
|
|
2791
3318
|
rules = await writeImportedRules(repoRoot, report);
|
|
2792
3319
|
}
|
|
2793
3320
|
const { written } = await runCompile(repoRoot, { home: options.home });
|
|
2794
|
-
|
|
3321
|
+
const exposed = options.expose ? (await exposeProject(repoRoot, { agents: options.expose })).entries.filter((entry) => entry.status !== "missing" && entry.status !== "skipped").map((entry) => entry.path) : [];
|
|
3322
|
+
return { name, profile, adapters, skills, tools: tools2, memory, rules, import: report, compiled: written, exposed };
|
|
2795
3323
|
}
|
|
2796
3324
|
|
|
2797
3325
|
// src/commands/init.ts
|
|
@@ -2806,12 +3334,13 @@ async function runInit(repoRoot, options) {
|
|
|
2806
3334
|
force: options.force,
|
|
2807
3335
|
import: options.import,
|
|
2808
3336
|
profile: options.profile ? profileIdSchema.parse(options.profile) : void 0,
|
|
2809
|
-
adapters: parseAdapters(options.adapters)
|
|
3337
|
+
adapters: parseAdapters(options.adapters),
|
|
3338
|
+
expose: options.expose
|
|
2810
3339
|
};
|
|
2811
3340
|
try {
|
|
2812
3341
|
const report = await initHarness(repoRoot, initOptions);
|
|
2813
3342
|
console.log(`Initialized harness \`${report.name}\` (profile: ${report.profile}).`);
|
|
2814
|
-
console.log(`adapters: ${report.adapters.join(", ")}`);
|
|
3343
|
+
console.log(`adapters: ${report.adapters.length > 0 ? report.adapters.join(", ") : "none (local-only)"}`);
|
|
2815
3344
|
console.log(`skills: ${report.skills.length}, tools: ${report.tools.length}, memory: ${report.memory.length}`);
|
|
2816
3345
|
if (report.import?.canonicalSource) {
|
|
2817
3346
|
console.log(`imported canonical prose from ${report.import.canonicalSource}`);
|
|
@@ -2826,6 +3355,9 @@ async function runInit(repoRoot, options) {
|
|
|
2826
3355
|
console.log(`imported ${report.rules.length} cursor rule(s)`);
|
|
2827
3356
|
}
|
|
2828
3357
|
console.log(`compiled ${report.compiled.length} vendor file(s).`);
|
|
3358
|
+
if (report.exposed.length > 0) {
|
|
3359
|
+
console.log(`exposed ${report.exposed.length} provider skill shim(s).`);
|
|
3360
|
+
}
|
|
2829
3361
|
} catch (error) {
|
|
2830
3362
|
if (error instanceof InitError) {
|
|
2831
3363
|
console.error(error.message);
|
|
@@ -2837,13 +3369,13 @@ async function runInit(repoRoot, options) {
|
|
|
2837
3369
|
}
|
|
2838
3370
|
|
|
2839
3371
|
// src/commands/install.ts
|
|
2840
|
-
import
|
|
3372
|
+
import path25 from "path";
|
|
2841
3373
|
|
|
2842
3374
|
// src/core/install/fetch.ts
|
|
2843
3375
|
import { execFile } from "child_process";
|
|
2844
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
3376
|
+
import { mkdtemp, rm as rm3 } from "fs/promises";
|
|
2845
3377
|
import os2 from "os";
|
|
2846
|
-
import
|
|
3378
|
+
import path23 from "path";
|
|
2847
3379
|
import { promisify } from "util";
|
|
2848
3380
|
var run = promisify(execFile);
|
|
2849
3381
|
function cloneUrl(ref) {
|
|
@@ -2864,8 +3396,8 @@ async function git(cwd, args) {
|
|
|
2864
3396
|
}
|
|
2865
3397
|
async function fetchGitSource(ref) {
|
|
2866
3398
|
const url = cloneUrl(ref);
|
|
2867
|
-
const dir = await mkdtemp(
|
|
2868
|
-
const cleanup = () =>
|
|
3399
|
+
const dir = await mkdtemp(path23.join(os2.tmpdir(), "threadroot-fetch-"));
|
|
3400
|
+
const cleanup = () => rm3(dir, { recursive: true, force: true });
|
|
2869
3401
|
try {
|
|
2870
3402
|
if (ref.ref) {
|
|
2871
3403
|
try {
|
|
@@ -2886,9 +3418,9 @@ async function fetchGitSource(ref) {
|
|
|
2886
3418
|
}
|
|
2887
3419
|
|
|
2888
3420
|
// src/core/install/install.ts
|
|
2889
|
-
import { cp as cp2, mkdir as
|
|
3421
|
+
import { cp as cp2, mkdir as mkdir10, readFile as readFile10, readdir as readdir5, stat as stat9, writeFile as writeFile9 } from "fs/promises";
|
|
2890
3422
|
import { createHash as createHash2 } from "crypto";
|
|
2891
|
-
import
|
|
3423
|
+
import path24 from "path";
|
|
2892
3424
|
var NAME_RE5 = /^[a-z0-9][a-z0-9-]*$/;
|
|
2893
3425
|
var KIND_DIR = {
|
|
2894
3426
|
skill: "skills",
|
|
@@ -2900,8 +3432,8 @@ function objectExt(kind) {
|
|
|
2900
3432
|
return kind === "tool" || kind === "connection" ? ".yaml" : ".md";
|
|
2901
3433
|
}
|
|
2902
3434
|
function safeRepoPath(objectPath) {
|
|
2903
|
-
const normalized =
|
|
2904
|
-
if (
|
|
3435
|
+
const normalized = path24.normalize(objectPath);
|
|
3436
|
+
if (path24.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path24.sep}`)) {
|
|
2905
3437
|
throw new Error(`Unsafe object path: ${objectPath}`);
|
|
2906
3438
|
}
|
|
2907
3439
|
return normalized;
|
|
@@ -2915,7 +3447,7 @@ function inferKind(objectPath, override) {
|
|
|
2915
3447
|
if (segments.includes("tools")) return "tool";
|
|
2916
3448
|
if (segments.includes("connections")) return "connection";
|
|
2917
3449
|
if (segments.includes("rules")) return "rule";
|
|
2918
|
-
const ext =
|
|
3450
|
+
const ext = path24.extname(objectPath).toLowerCase();
|
|
2919
3451
|
if (ext === ".yaml" || ext === ".yml") return "tool";
|
|
2920
3452
|
if (ext === ".md") return "skill";
|
|
2921
3453
|
throw new Error(
|
|
@@ -2923,7 +3455,7 @@ function inferKind(objectPath, override) {
|
|
|
2923
3455
|
);
|
|
2924
3456
|
}
|
|
2925
3457
|
function deriveName(objectPath) {
|
|
2926
|
-
const base =
|
|
3458
|
+
const base = path24.basename(objectPath, path24.extname(objectPath));
|
|
2927
3459
|
if (!NAME_RE5.test(base)) {
|
|
2928
3460
|
throw new Error(`Invalid object name \`${base}\` (use lowercase letters, digits, and dashes).`);
|
|
2929
3461
|
}
|
|
@@ -2935,8 +3467,8 @@ async function hashDirectory(root) {
|
|
|
2935
3467
|
async function walk(dir) {
|
|
2936
3468
|
const entries = (await readdir5(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
2937
3469
|
for (const entry of entries) {
|
|
2938
|
-
const full =
|
|
2939
|
-
const rel =
|
|
3470
|
+
const full = path24.join(dir, entry.name);
|
|
3471
|
+
const rel = path24.relative(root, full).split(path24.sep).join("/");
|
|
2940
3472
|
if (entry.isSymbolicLink()) {
|
|
2941
3473
|
throw new Error(`Refusing to install skill directory with symlink: ${rel}`);
|
|
2942
3474
|
}
|
|
@@ -2947,7 +3479,7 @@ async function hashDirectory(root) {
|
|
|
2947
3479
|
if (entry.isFile()) {
|
|
2948
3480
|
hash.update(`file:${rel}
|
|
2949
3481
|
`);
|
|
2950
|
-
hash.update(await
|
|
3482
|
+
hash.update(await readFile10(full));
|
|
2951
3483
|
hash.update("\n");
|
|
2952
3484
|
}
|
|
2953
3485
|
}
|
|
@@ -2956,8 +3488,8 @@ async function hashDirectory(root) {
|
|
|
2956
3488
|
return hash.digest("hex");
|
|
2957
3489
|
}
|
|
2958
3490
|
async function validateSkillDirectory2(sourcePath, expectedName) {
|
|
2959
|
-
const skillPath =
|
|
2960
|
-
const parsed = parseFrontmatter(await
|
|
3491
|
+
const skillPath = path24.join(sourcePath, "SKILL.md");
|
|
3492
|
+
const parsed = parseFrontmatter(await readFile10(skillPath, "utf8"));
|
|
2961
3493
|
const result = skillFrontmatterSchema.safeParse(parsed.data);
|
|
2962
3494
|
if (!result.success) {
|
|
2963
3495
|
const detail = result.error.issues.map((issue) => issue.message).join("; ");
|
|
@@ -2986,7 +3518,7 @@ async function installObject(repoRoot, rawSource, options = {}) {
|
|
|
2986
3518
|
objectPath = safeRepoPath(within);
|
|
2987
3519
|
refLabel = ref.ref;
|
|
2988
3520
|
const fetched = await fetchGitSource(ref);
|
|
2989
|
-
sourcePath =
|
|
3521
|
+
sourcePath = path24.join(fetched.dir, objectPath);
|
|
2990
3522
|
resolved = fetched.sha;
|
|
2991
3523
|
cleanup = fetched.cleanup;
|
|
2992
3524
|
} else {
|
|
@@ -3001,20 +3533,20 @@ async function installObject(repoRoot, rawSource, options = {}) {
|
|
|
3001
3533
|
const destDir = scope === "user" ? userObjectDir(dirKey, options.home) : projectObjectDir(repoRoot, dirKey);
|
|
3002
3534
|
let destPath;
|
|
3003
3535
|
let integrity;
|
|
3004
|
-
const info = await
|
|
3536
|
+
const info = await stat9(sourcePath);
|
|
3005
3537
|
if (info.isDirectory()) {
|
|
3006
3538
|
if (kind !== "skill") {
|
|
3007
3539
|
throw new Error("Only skill objects may be installed from a directory.");
|
|
3008
3540
|
}
|
|
3009
3541
|
integrity = `sha256:${await validateSkillDirectory2(sourcePath, name)}`;
|
|
3010
|
-
destPath =
|
|
3011
|
-
await
|
|
3542
|
+
destPath = path24.join(destDir, name);
|
|
3543
|
+
await mkdir10(destDir, { recursive: true });
|
|
3012
3544
|
await cp2(sourcePath, destPath, { recursive: true, force: true });
|
|
3013
3545
|
} else {
|
|
3014
|
-
const content = await
|
|
3015
|
-
destPath =
|
|
3016
|
-
await
|
|
3017
|
-
await
|
|
3546
|
+
const content = await readFile10(sourcePath, "utf8");
|
|
3547
|
+
destPath = path24.join(destDir, `${name}${objectExt(kind)}`);
|
|
3548
|
+
await mkdir10(destDir, { recursive: true });
|
|
3549
|
+
await writeFile9(destPath, content, "utf8");
|
|
3018
3550
|
integrity = `sha256:${hashContent(content)}`;
|
|
3019
3551
|
}
|
|
3020
3552
|
const entry = {
|
|
@@ -3066,7 +3598,7 @@ async function runInstall(repoRoot, source, options) {
|
|
|
3066
3598
|
if (installed.kind === "skill" && installed.entry.sourceKind !== "local") {
|
|
3067
3599
|
console.log(" note: inspect external skills before trusting bundled scripts, assets, or allowed tools.");
|
|
3068
3600
|
if (scope === "project") {
|
|
3069
|
-
console.log(` inspect: threadroot skills inspect ${
|
|
3601
|
+
console.log(` inspect: threadroot skills inspect ${path25.relative(repoRoot, installed.path)}`);
|
|
3070
3602
|
}
|
|
3071
3603
|
}
|
|
3072
3604
|
} catch (error) {
|
|
@@ -3390,7 +3922,7 @@ async function handleMessage(repoRoot, request) {
|
|
|
3390
3922
|
if (request.method === "initialize") {
|
|
3391
3923
|
return resultResponse(request, {
|
|
3392
3924
|
protocolVersion: "2024-11-05",
|
|
3393
|
-
serverInfo: { name: "threadroot", version: "0.1.
|
|
3925
|
+
serverInfo: { name: "threadroot", version: "0.1.1" },
|
|
3394
3926
|
capabilities: { tools: {} }
|
|
3395
3927
|
});
|
|
3396
3928
|
}
|
|
@@ -3532,7 +4064,7 @@ Repository:
|
|
|
3532
4064
|
${repoRoot}
|
|
3533
4065
|
|
|
3534
4066
|
Goal:
|
|
3535
|
-
Initialize Threadroot so this repo has portable AI-agent
|
|
4067
|
+
Initialize Threadroot so this repo has a portable AI-agent harness in one canonical \`.threadroot/\` directory.
|
|
3536
4068
|
|
|
3537
4069
|
Rules:
|
|
3538
4070
|
- Prefer deterministic CLI commands.
|
|
@@ -3545,9 +4077,9 @@ Steps:
|
|
|
3545
4077
|
3. Run \`threadroot status\` to check whether a harness already exists.
|
|
3546
4078
|
4. If no harness exists, run \`threadroot init\`. Use \`--no-import\` only when the user explicitly wants a blank-slate harness.
|
|
3547
4079
|
5. Run \`threadroot status\` again.
|
|
3548
|
-
6. If
|
|
4080
|
+
6. If the user asks for provider-native project skill files, run \`threadroot expose <agent>\` or \`threadroot expose all\`.
|
|
3549
4081
|
7. Run \`threadroot context "current task"\` with the user's actual task to find relevant skills, rules, tools, and memory.
|
|
3550
|
-
8. If project-local MCP config is useful,
|
|
4082
|
+
8. If project-local MCP config is useful, ask before running \`threadroot mcp setup --write\`, then tell the user to reload their agent surface.
|
|
3551
4083
|
|
|
3552
4084
|
Final response:
|
|
3553
4085
|
Say exactly:
|
|
@@ -3614,11 +4146,11 @@ function agentNotes(agent) {
|
|
|
3614
4146
|
}
|
|
3615
4147
|
|
|
3616
4148
|
// src/core/mcp-config.ts
|
|
3617
|
-
import { mkdir as
|
|
3618
|
-
import
|
|
4149
|
+
import { mkdir as mkdir11, readFile as readFile11, writeFile as writeFile10 } from "fs/promises";
|
|
4150
|
+
import path26 from "path";
|
|
3619
4151
|
var TARGETS = [
|
|
3620
|
-
{ agent: "copilot", file:
|
|
3621
|
-
{ agent: "cursor", file:
|
|
4152
|
+
{ agent: "copilot", file: path26.join(".vscode", "mcp.json"), key: "servers" },
|
|
4153
|
+
{ agent: "cursor", file: path26.join(".cursor", "mcp.json"), key: "mcpServers" },
|
|
3622
4154
|
{ agent: "claude", file: ".mcp.json", key: "mcpServers" }
|
|
3623
4155
|
];
|
|
3624
4156
|
function mcpServerEntry(command, scriptPath) {
|
|
@@ -3627,7 +4159,7 @@ function mcpServerEntry(command, scriptPath) {
|
|
|
3627
4159
|
async function mergeConfig(filePath, key, entry) {
|
|
3628
4160
|
let config = {};
|
|
3629
4161
|
try {
|
|
3630
|
-
const raw = await
|
|
4162
|
+
const raw = await readFile11(filePath, "utf8");
|
|
3631
4163
|
const parsed = JSON.parse(raw);
|
|
3632
4164
|
if (parsed && typeof parsed === "object") {
|
|
3633
4165
|
config = parsed;
|
|
@@ -3640,8 +4172,8 @@ async function mergeConfig(filePath, key, entry) {
|
|
|
3640
4172
|
const servers = config[key] && typeof config[key] === "object" ? config[key] : {};
|
|
3641
4173
|
servers.threadroot = { ...entry };
|
|
3642
4174
|
config[key] = servers;
|
|
3643
|
-
await
|
|
3644
|
-
await
|
|
4175
|
+
await mkdir11(path26.dirname(filePath), { recursive: true });
|
|
4176
|
+
await writeFile10(filePath, `${JSON.stringify(config, null, 2)}
|
|
3645
4177
|
`, "utf8");
|
|
3646
4178
|
}
|
|
3647
4179
|
async function writeProjectMcpConfigs(input2) {
|
|
@@ -3649,7 +4181,7 @@ async function writeProjectMcpConfigs(input2) {
|
|
|
3649
4181
|
const targets = agents ? TARGETS.filter((target) => agents.includes(target.agent)) : TARGETS;
|
|
3650
4182
|
const written = [];
|
|
3651
4183
|
for (const target of targets) {
|
|
3652
|
-
const filePath =
|
|
4184
|
+
const filePath = path26.join(input2.repoRoot, target.file);
|
|
3653
4185
|
await mergeConfig(filePath, target.key, input2.entry);
|
|
3654
4186
|
written.push(target.file);
|
|
3655
4187
|
}
|
|
@@ -3751,6 +4283,37 @@ async function runSkillsInspect(repoRoot, targetPath) {
|
|
|
3751
4283
|
}
|
|
3752
4284
|
}
|
|
3753
4285
|
|
|
4286
|
+
// src/commands/setup.ts
|
|
4287
|
+
function modeFromOptions2(options) {
|
|
4288
|
+
if (options.undo) return "undo";
|
|
4289
|
+
if (options.check) return "check";
|
|
4290
|
+
if (options.dryRun) return "dry-run";
|
|
4291
|
+
return "write";
|
|
4292
|
+
}
|
|
4293
|
+
async function runSetup(_repoRoot, options) {
|
|
4294
|
+
if (!options.global) {
|
|
4295
|
+
console.error("Only global setup is supported right now. Run `threadroot setup --global`.");
|
|
4296
|
+
process.exitCode = 1;
|
|
4297
|
+
return;
|
|
4298
|
+
}
|
|
4299
|
+
const mode = modeFromOptions2(options);
|
|
4300
|
+
const result = await setupGlobal({
|
|
4301
|
+
agents: options.agent,
|
|
4302
|
+
mode,
|
|
4303
|
+
force: options.force,
|
|
4304
|
+
mcp: options.mcp
|
|
4305
|
+
});
|
|
4306
|
+
const title = mode === "dry-run" ? "Global setup plan" : mode === "check" ? "Global setup check" : mode === "undo" ? "Global setup undo" : "Global setup complete";
|
|
4307
|
+
console.log(`${title}:`);
|
|
4308
|
+
for (const entry of result.entries) {
|
|
4309
|
+
const suffix = entry.message ? ` - ${entry.message}` : "";
|
|
4310
|
+
console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
|
|
4311
|
+
}
|
|
4312
|
+
if (mode === "write") {
|
|
4313
|
+
console.log("Reload or restart open agent sessions so new global skills/config are discovered.");
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
|
|
3754
4317
|
// src/commands/status.ts
|
|
3755
4318
|
async function runStatus(repoRoot) {
|
|
3756
4319
|
const status = await harnessStatus(repoRoot);
|
|
@@ -3759,7 +4322,7 @@ async function runStatus(repoRoot) {
|
|
|
3759
4322
|
return;
|
|
3760
4323
|
}
|
|
3761
4324
|
console.log(`harness: ${status.manifest.name} (${status.manifest.profile})`);
|
|
3762
|
-
console.log(`adapters: ${status.manifest.adapters.join(", ")}`);
|
|
4325
|
+
console.log(`adapters: ${status.manifest.adapters.length > 0 ? status.manifest.adapters.join(", ") : "none (local-only)"}`);
|
|
3763
4326
|
console.log(
|
|
3764
4327
|
`objects: ${status.counts.skills} skills, ${status.counts.rules} rules, ${status.counts.tools} tools, ${status.counts.memory} memory`
|
|
3765
4328
|
);
|
|
@@ -3775,8 +4338,8 @@ async function runStatus(repoRoot) {
|
|
|
3775
4338
|
}
|
|
3776
4339
|
|
|
3777
4340
|
// src/core/packs/index.ts
|
|
3778
|
-
import { cp as cp3, mkdir as
|
|
3779
|
-
import
|
|
4341
|
+
import { cp as cp3, mkdir as mkdir12, readFile as readFile12, readdir as readdir6, stat as stat10 } from "fs/promises";
|
|
4342
|
+
import path27 from "path";
|
|
3780
4343
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3781
4344
|
import { parse as parseYaml3 } from "yaml";
|
|
3782
4345
|
import { z as z5 } from "zod";
|
|
@@ -3789,18 +4352,18 @@ var packManifestSchema = z5.object({
|
|
|
3789
4352
|
rules: z5.array(z5.string()).default([]),
|
|
3790
4353
|
connections: z5.array(z5.string()).default([])
|
|
3791
4354
|
});
|
|
3792
|
-
var DIST_DIR2 =
|
|
3793
|
-
var PACKAGE_ROOT_FROM_BUNDLE2 =
|
|
3794
|
-
var PACKAGE_ROOT_FROM_DIST2 =
|
|
3795
|
-
var PACKAGE_ROOT_FROM_SRC2 =
|
|
4355
|
+
var DIST_DIR2 = path27.dirname(fileURLToPath2(import.meta.url));
|
|
4356
|
+
var PACKAGE_ROOT_FROM_BUNDLE2 = path27.resolve(DIST_DIR2, "..");
|
|
4357
|
+
var PACKAGE_ROOT_FROM_DIST2 = path27.resolve(DIST_DIR2, "../../..");
|
|
4358
|
+
var PACKAGE_ROOT_FROM_SRC2 = path27.resolve(DIST_DIR2, "../../../..");
|
|
3796
4359
|
var PACK_CANDIDATES = [
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
4360
|
+
path27.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
|
|
4361
|
+
path27.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
|
|
4362
|
+
path27.join(PACKAGE_ROOT_FROM_SRC2, "packs")
|
|
3800
4363
|
];
|
|
3801
4364
|
async function exists5(target) {
|
|
3802
4365
|
try {
|
|
3803
|
-
await
|
|
4366
|
+
await stat10(target);
|
|
3804
4367
|
return true;
|
|
3805
4368
|
} catch (error) {
|
|
3806
4369
|
if (error.code === "ENOENT") {
|
|
@@ -3828,22 +4391,22 @@ async function isPackRoot(candidate) {
|
|
|
3828
4391
|
return false;
|
|
3829
4392
|
}
|
|
3830
4393
|
for (const entry of entries) {
|
|
3831
|
-
if (entry.isDirectory() && await exists5(
|
|
4394
|
+
if (entry.isDirectory() && await exists5(path27.join(candidate, entry.name, "pack.yaml"))) {
|
|
3832
4395
|
return true;
|
|
3833
4396
|
}
|
|
3834
4397
|
}
|
|
3835
4398
|
return false;
|
|
3836
4399
|
}
|
|
3837
4400
|
function safeRelative(ref) {
|
|
3838
|
-
const normalized =
|
|
3839
|
-
if (
|
|
4401
|
+
const normalized = path27.normalize(ref);
|
|
4402
|
+
if (path27.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path27.sep}`)) {
|
|
3840
4403
|
throw new Error(`Unsafe pack reference: ${ref}`);
|
|
3841
4404
|
}
|
|
3842
4405
|
return normalized;
|
|
3843
4406
|
}
|
|
3844
4407
|
async function readPackManifest(packDir) {
|
|
3845
|
-
const file =
|
|
3846
|
-
const parsed = packManifestSchema.safeParse(parseYaml3(await
|
|
4408
|
+
const file = path27.join(packDir, "pack.yaml");
|
|
4409
|
+
const parsed = packManifestSchema.safeParse(parseYaml3(await readFile12(file, "utf8")));
|
|
3847
4410
|
if (!parsed.success) {
|
|
3848
4411
|
const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
|
|
3849
4412
|
throw new Error(`Invalid pack manifest ${file}: ${detail}`);
|
|
@@ -3851,7 +4414,7 @@ async function readPackManifest(packDir) {
|
|
|
3851
4414
|
return parsed.data;
|
|
3852
4415
|
}
|
|
3853
4416
|
async function packDirFor(repoRoot, nameOrPath) {
|
|
3854
|
-
if (
|
|
4417
|
+
if (path27.isAbsolute(nameOrPath)) {
|
|
3855
4418
|
return nameOrPath;
|
|
3856
4419
|
}
|
|
3857
4420
|
if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
|
|
@@ -3859,14 +4422,14 @@ async function packDirFor(repoRoot, nameOrPath) {
|
|
|
3859
4422
|
}
|
|
3860
4423
|
const bundled = await bundledPacksDir();
|
|
3861
4424
|
if (bundled) {
|
|
3862
|
-
return
|
|
4425
|
+
return path27.join(bundled, nameOrPath);
|
|
3863
4426
|
}
|
|
3864
|
-
return toRepoPath(repoRoot,
|
|
4427
|
+
return toRepoPath(repoRoot, path27.join("packs", nameOrPath));
|
|
3865
4428
|
}
|
|
3866
4429
|
async function directFiles(dir, ext) {
|
|
3867
4430
|
try {
|
|
3868
4431
|
const entries = await readdir6(dir, { withFileTypes: true });
|
|
3869
|
-
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) =>
|
|
4432
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path27.join(dir, entry.name)).sort();
|
|
3870
4433
|
} catch (error) {
|
|
3871
4434
|
if (error.code === "ENOENT") {
|
|
3872
4435
|
return [];
|
|
@@ -3879,11 +4442,11 @@ async function skillEntries(dir) {
|
|
|
3879
4442
|
const entries = await readdir6(dir, { withFileTypes: true });
|
|
3880
4443
|
const result = [];
|
|
3881
4444
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
3882
|
-
const full =
|
|
4445
|
+
const full = path27.join(dir, entry.name);
|
|
3883
4446
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
3884
4447
|
result.push(full);
|
|
3885
4448
|
}
|
|
3886
|
-
if (entry.isDirectory() && await exists5(
|
|
4449
|
+
if (entry.isDirectory() && await exists5(path27.join(full, "SKILL.md"))) {
|
|
3887
4450
|
result.push(full);
|
|
3888
4451
|
}
|
|
3889
4452
|
}
|
|
@@ -3898,34 +4461,34 @@ async function skillEntries(dir) {
|
|
|
3898
4461
|
async function collectObjects(packDir, manifest) {
|
|
3899
4462
|
async function resolveRef(ref) {
|
|
3900
4463
|
const safe = safeRelative(ref);
|
|
3901
|
-
const local =
|
|
4464
|
+
const local = path27.resolve(packDir, safe);
|
|
3902
4465
|
if (await exists5(local)) {
|
|
3903
4466
|
return local;
|
|
3904
4467
|
}
|
|
3905
|
-
return
|
|
4468
|
+
return path27.resolve(packDir, "..", "..", safe);
|
|
3906
4469
|
}
|
|
3907
4470
|
return {
|
|
3908
4471
|
skills: [
|
|
3909
4472
|
...await Promise.all(manifest.skills.map(resolveRef)),
|
|
3910
|
-
...await skillEntries(
|
|
4473
|
+
...await skillEntries(path27.join(packDir, "skills"))
|
|
3911
4474
|
],
|
|
3912
4475
|
tools: [
|
|
3913
4476
|
...await Promise.all(manifest.tools.map(resolveRef)),
|
|
3914
|
-
...await directFiles(
|
|
4477
|
+
...await directFiles(path27.join(packDir, "tools"), ".yaml")
|
|
3915
4478
|
],
|
|
3916
4479
|
rules: [
|
|
3917
4480
|
...await Promise.all(manifest.rules.map(resolveRef)),
|
|
3918
|
-
...await directFiles(
|
|
4481
|
+
...await directFiles(path27.join(packDir, "rules"), ".md")
|
|
3919
4482
|
],
|
|
3920
4483
|
connections: [
|
|
3921
4484
|
...await Promise.all(manifest.connections.map(resolveRef)),
|
|
3922
|
-
...await directFiles(
|
|
4485
|
+
...await directFiles(path27.join(packDir, "connections"), ".yaml")
|
|
3923
4486
|
]
|
|
3924
4487
|
};
|
|
3925
4488
|
}
|
|
3926
4489
|
function baseName(source) {
|
|
3927
|
-
const parsed =
|
|
3928
|
-
return
|
|
4490
|
+
const parsed = path27.basename(source) === "SKILL.md" ? path27.dirname(source) : source;
|
|
4491
|
+
return path27.basename(parsed, path27.extname(parsed));
|
|
3929
4492
|
}
|
|
3930
4493
|
async function listPacks(repoRoot) {
|
|
3931
4494
|
const dirs = [toRepoPath(repoRoot, "packs"), await bundledPacksDir()].filter((dir) => Boolean(dir));
|
|
@@ -3942,8 +4505,8 @@ async function listPacks(repoRoot) {
|
|
|
3942
4505
|
if (!entry.isDirectory() || seen.has(entry.name)) {
|
|
3943
4506
|
continue;
|
|
3944
4507
|
}
|
|
3945
|
-
const packDir =
|
|
3946
|
-
if (!await exists5(
|
|
4508
|
+
const packDir = path27.join(root, entry.name);
|
|
4509
|
+
if (!await exists5(path27.join(packDir, "pack.yaml"))) {
|
|
3947
4510
|
continue;
|
|
3948
4511
|
}
|
|
3949
4512
|
seen.add(entry.name);
|
|
@@ -3967,14 +4530,14 @@ async function inspectPack(repoRoot, nameOrPath) {
|
|
|
3967
4530
|
};
|
|
3968
4531
|
}
|
|
3969
4532
|
async function validateProse(file, kind) {
|
|
3970
|
-
const target =
|
|
3971
|
-
const content = await
|
|
4533
|
+
const target = path27.basename(file) === "SKILL.md" ? file : file;
|
|
4534
|
+
const content = await readFile12(target, "utf8");
|
|
3972
4535
|
const parsed = parseFrontmatter(content);
|
|
3973
4536
|
const schema = kind === "skill" ? skillFrontmatterSchema : ruleFrontmatterSchema;
|
|
3974
4537
|
schema.parse(parsed.data);
|
|
3975
4538
|
}
|
|
3976
4539
|
async function validateYaml(file, kind) {
|
|
3977
|
-
const content = await
|
|
4540
|
+
const content = await readFile12(file, "utf8");
|
|
3978
4541
|
const schema = kind === "tool" ? toolManifestSchema : connectionManifestSchema;
|
|
3979
4542
|
schema.parse(parseYaml3(content));
|
|
3980
4543
|
}
|
|
@@ -3985,7 +4548,7 @@ async function validatePack(repoRoot, nameOrPath) {
|
|
|
3985
4548
|
const manifest = await readPackManifest(packDir);
|
|
3986
4549
|
const objects = await collectObjects(packDir, manifest);
|
|
3987
4550
|
for (const skill of objects.skills) {
|
|
3988
|
-
await validateProse(
|
|
4551
|
+
await validateProse(path27.basename(skill) === "SKILL.md" ? skill : path27.join(skill, "SKILL.md"), "skill");
|
|
3989
4552
|
}
|
|
3990
4553
|
for (const rule of objects.rules) await validateProse(rule, "rule");
|
|
3991
4554
|
for (const tool of objects.tools) await validateYaml(tool, "tool");
|
|
@@ -3999,10 +4562,10 @@ async function validatePack(repoRoot, nameOrPath) {
|
|
|
3999
4562
|
return { ok: !findings.some((finding3) => finding3.severity === "error"), findings };
|
|
4000
4563
|
}
|
|
4001
4564
|
async function copyObject(source, destDir) {
|
|
4002
|
-
const info = await
|
|
4565
|
+
const info = await stat10(source);
|
|
4003
4566
|
const name = baseName(source);
|
|
4004
|
-
const dest = info.isDirectory() ?
|
|
4005
|
-
await
|
|
4567
|
+
const dest = info.isDirectory() ? path27.join(destDir, name) : path27.join(destDir, path27.basename(source));
|
|
4568
|
+
await mkdir12(destDir, { recursive: true });
|
|
4006
4569
|
await cp3(source, dest, { recursive: true, force: true });
|
|
4007
4570
|
return dest;
|
|
4008
4571
|
}
|
|
@@ -4290,8 +4853,10 @@ async function runConnectionsCheck(repoRoot) {
|
|
|
4290
4853
|
// src/cli.ts
|
|
4291
4854
|
function createProgram(repoRoot = process.cwd()) {
|
|
4292
4855
|
const program = new Command();
|
|
4293
|
-
program.name("threadroot").description("Git for your AI agent harness: one canonical source,
|
|
4294
|
-
program.command("init").description("Scaffold a Threadroot harness
|
|
4856
|
+
program.name("threadroot").description("Git for your AI agent harness: one canonical .threadroot source, optional provider exposure.").version("0.1.1");
|
|
4857
|
+
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));
|
|
4858
|
+
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));
|
|
4859
|
+
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));
|
|
4295
4860
|
program.command("status").description("Show harness state, object counts, and compiled-output drift.").action(() => runStatus(repoRoot));
|
|
4296
4861
|
program.command("diff").description("Show the diff between the canonical harness and each compiled vendor file.").action(() => runDiff(repoRoot));
|
|
4297
4862
|
program.command("doctor").description("Check harness validity, compiled output health, MCP hints, and tool trust.").action(() => runDoctor(repoRoot));
|