baller-maester 0.4.2 → 0.5.0
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 +6 -0
- package/dist/cli/main.js +149 -24
- package/dist/cli/main.js.map +1 -1
- package/dist/index.js +149 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-05-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Connector env-var seeding into MCP host registrations.** The Codex CLI writer now emits `env_vars = ["NAME", ...]` inside `[mcp_servers.maester]` (Codex's native shell pass-through). The Claude Code writer now emits an `env` block under `mcpServers.maester` with `"NAME": "${NAME:-}"` placeholders (Claude Code's native `${VAR}` expansion; the `:-` empty-default form prevents `.mcp.json` parse failures when a var is unset). Cursor — which has no working config-file pass-through — relies on its parent-process env inheritance instead; the installed Grand Maester Cursor rule now lists the required env-var names so users know what to export in the shell that launches Cursor. Closes the gap where a connector configured with `auth: { type: "token", envVar: "GITLAB_TOKEN" }` failed every tool call with `missing-env-var` even when the user had exported the variable in their shell.
|
|
14
|
+
- **Union semantics for the maester block.** The framework owns the set of env-var names derived from `citadel.connectors[*].auth.envVar` and persists a small `_maester_managed_env_vars` (Codex) / `_maesterManagedEnv` (Claude Code) marker so the next refresh can distinguish framework-managed entries (which should be stripped when no connector still references them) from user-added entries (which are preserved verbatim across refreshes). De-duped and stable-sorted so output is byte-identical between refreshes. No env-var values are ever written to any file maester produces — names only.
|
|
15
|
+
|
|
10
16
|
## [0.4.2] - 2026-05-21
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/dist/cli/main.js
CHANGED
|
@@ -1643,10 +1643,39 @@ function decideAction(existing, previousVersion, newVersion, newContent) {
|
|
|
1643
1643
|
return "upgraded";
|
|
1644
1644
|
}
|
|
1645
1645
|
|
|
1646
|
+
// src/core/mcp/registrations/env-vars.ts
|
|
1647
|
+
var EMPTY = { managed: [], invalid: [] };
|
|
1648
|
+
function collectConnectorEnvVars(connectors) {
|
|
1649
|
+
if (!connectors || connectors.length === 0) return EMPTY;
|
|
1650
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1651
|
+
const invalid = [];
|
|
1652
|
+
for (const c of connectors) {
|
|
1653
|
+
if (!c.auth || c.auth.type !== "token") continue;
|
|
1654
|
+
const name = c.auth.envVar;
|
|
1655
|
+
if (!ENV_VAR_RE.test(name)) {
|
|
1656
|
+
invalid.push({ connector: c.name, envVar: name });
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
seen.add(name);
|
|
1660
|
+
}
|
|
1661
|
+
return { managed: Array.from(seen).sort(), invalid };
|
|
1662
|
+
}
|
|
1663
|
+
async function loadConnectorEnvVarsBestEffort(repoRoot) {
|
|
1664
|
+
try {
|
|
1665
|
+
const config = await loadCitadelConfig(repoRoot);
|
|
1666
|
+
return collectConnectorEnvVars(config.connectors);
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
const code = err.code;
|
|
1669
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1670
|
+
if (code === "ENOENT" || /No citadel\.yaml/.test(message)) return EMPTY;
|
|
1671
|
+
return { ...EMPTY, loadError: message };
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1646
1675
|
// src/core/skill/templates/shells/cursor.ts
|
|
1647
1676
|
var DESCRIPTION = "Citadel-aware guidance for reading aggregated documentation under the citadel base directory.";
|
|
1648
1677
|
function renderCursorRuleBody(opts) {
|
|
1649
|
-
|
|
1678
|
+
const sections = [
|
|
1650
1679
|
"# Grand Maester (Cursor rule)",
|
|
1651
1680
|
"",
|
|
1652
1681
|
"This rule applies when the user asks about content under the citadel",
|
|
@@ -1659,6 +1688,25 @@ function renderCursorRuleBody(opts) {
|
|
|
1659
1688
|
interpolate3(freshness_awareness_default, opts),
|
|
1660
1689
|
"",
|
|
1661
1690
|
interpolate3(connector_policy_default, opts)
|
|
1691
|
+
];
|
|
1692
|
+
if (opts.requiredEnvVars && opts.requiredEnvVars.length > 0) {
|
|
1693
|
+
sections.push("", renderRequiredEnvVarsNote(opts.requiredEnvVars));
|
|
1694
|
+
}
|
|
1695
|
+
return sections.join("\n");
|
|
1696
|
+
}
|
|
1697
|
+
function renderRequiredEnvVarsNote(envVars) {
|
|
1698
|
+
const sorted = [...envVars].sort();
|
|
1699
|
+
const list = sorted.map((v) => `\`${v}\``).join(", ");
|
|
1700
|
+
return [
|
|
1701
|
+
"## Required environment variables (Cursor)",
|
|
1702
|
+
"",
|
|
1703
|
+
`This citadel exposes connectors that require these env vars: ${list}.`,
|
|
1704
|
+
"",
|
|
1705
|
+
"Cursor inherits env vars from the shell that launches it, so export them in",
|
|
1706
|
+
"that shell (e.g. in your `~/.zshrc` / `~/.bashrc` or by launching Cursor",
|
|
1707
|
+
"from a terminal where they are already set). The maester MCP server reads",
|
|
1708
|
+
"each value at tool-invocation time; if a var is unset, the call returns a",
|
|
1709
|
+
"`missing-env-var` envelope naming the variable."
|
|
1662
1710
|
].join("\n");
|
|
1663
1711
|
}
|
|
1664
1712
|
function renderCursorRuleFile(body, opts) {
|
|
@@ -1691,7 +1739,11 @@ async function writeCursor(input) {
|
|
|
1691
1739
|
await promises.mkdir(path8.dirname(filePath), { recursive: true });
|
|
1692
1740
|
const existing = await readTextOrUndefined3(filePath);
|
|
1693
1741
|
const previousVersion = existing ? extractMarkdownRegion(existing)?.version : void 0;
|
|
1694
|
-
const
|
|
1742
|
+
const envVars = await loadConnectorEnvVarsBestEffort(input.repoRoot);
|
|
1743
|
+
const body = renderCursorRuleBody({
|
|
1744
|
+
baseDir: input.citadelBaseDir,
|
|
1745
|
+
requiredEnvVars: envVars.managed
|
|
1746
|
+
});
|
|
1695
1747
|
const next = existing ? replaceMarkdownRegion(existing, body, input.skillVersion) : `${renderCursorRuleFile(
|
|
1696
1748
|
replaceMarkdownRegion(void 0, body, input.skillVersion).trimEnd(),
|
|
1697
1749
|
{
|
|
@@ -1856,58 +1908,92 @@ function resolveMaesterLaunchCommand() {
|
|
|
1856
1908
|
|
|
1857
1909
|
// src/core/mcp/registrations/claude-code.ts
|
|
1858
1910
|
var MCP_FILE = ".mcp.json";
|
|
1859
|
-
|
|
1860
|
-
|
|
1911
|
+
var MANAGED_MARKER_KEY = "_maesterManagedEnv";
|
|
1912
|
+
function maesterEntry(launch, envObject, managed) {
|
|
1913
|
+
const entry = { command: launch.command, args: [...launch.args] };
|
|
1914
|
+
if (Object.keys(envObject).length > 0) entry.env = envObject;
|
|
1915
|
+
if (managed.length > 0) entry[MANAGED_MARKER_KEY] = [...managed];
|
|
1916
|
+
return entry;
|
|
1861
1917
|
}
|
|
1862
1918
|
async function writeClaudeCodeMcpEntry(repoRoot, options = {}) {
|
|
1863
1919
|
const launch = options.launch ?? resolveMaesterLaunchCommand();
|
|
1864
|
-
return writeJsonMcpFile(path8.join(repoRoot, MCP_FILE), launch);
|
|
1920
|
+
return writeJsonMcpFile(path8.join(repoRoot, MCP_FILE), launch, options.connectorEnvVars ?? []);
|
|
1865
1921
|
}
|
|
1866
|
-
async function writeJsonMcpFile(filePath, launch) {
|
|
1922
|
+
async function writeJsonMcpFile(filePath, launch, connectorEnvVars = []) {
|
|
1867
1923
|
await promises.mkdir(path8.dirname(filePath), { recursive: true });
|
|
1868
1924
|
const existingText = await readOrUndefined(filePath);
|
|
1869
|
-
const newText = renderJsonWithMaesterEntry(existingText, launch);
|
|
1925
|
+
const newText = renderJsonWithMaesterEntry(existingText, launch, connectorEnvVars);
|
|
1870
1926
|
if (existingText === newText) {
|
|
1871
1927
|
return { filePath, action: "unchanged" };
|
|
1872
1928
|
}
|
|
1873
1929
|
await promises.writeFile(filePath, newText, "utf8");
|
|
1874
1930
|
return { filePath, action: "written" };
|
|
1875
1931
|
}
|
|
1876
|
-
function renderJsonWithMaesterEntry(existingText, launch) {
|
|
1932
|
+
function renderJsonWithMaesterEntry(existingText, launch, connectorEnvVars = []) {
|
|
1877
1933
|
const parsed = parseOrEmpty(existingText);
|
|
1878
1934
|
const rebuilt = {};
|
|
1879
1935
|
let placed = false;
|
|
1880
1936
|
for (const [key, value] of Object.entries(parsed)) {
|
|
1881
1937
|
if (key === "mcpServers") {
|
|
1882
|
-
rebuilt[key] = mutateMcpServers(value, launch);
|
|
1938
|
+
rebuilt[key] = mutateMcpServers(value, launch, connectorEnvVars);
|
|
1883
1939
|
placed = true;
|
|
1884
1940
|
} else {
|
|
1885
1941
|
rebuilt[key] = value;
|
|
1886
1942
|
}
|
|
1887
1943
|
}
|
|
1888
1944
|
if (!placed) {
|
|
1889
|
-
rebuilt.mcpServers = mutateMcpServers(void 0, launch);
|
|
1945
|
+
rebuilt.mcpServers = mutateMcpServers(void 0, launch, connectorEnvVars);
|
|
1890
1946
|
}
|
|
1891
1947
|
return `${JSON.stringify(rebuilt, null, 2)}
|
|
1892
1948
|
`;
|
|
1893
1949
|
}
|
|
1894
|
-
function mutateMcpServers(existing, launch) {
|
|
1950
|
+
function mutateMcpServers(existing, launch, connectorEnvVars) {
|
|
1895
1951
|
const map = isPlainObject(existing) ? { ...existing } : {};
|
|
1896
1952
|
const rebuilt = {};
|
|
1953
|
+
const managedSorted = [...connectorEnvVars].sort();
|
|
1897
1954
|
let placed = false;
|
|
1898
1955
|
for (const [key, value] of Object.entries(map)) {
|
|
1899
1956
|
if (key === "maester") {
|
|
1900
|
-
|
|
1957
|
+
const mergedEnv = mergeEnvObject(value, connectorEnvVars);
|
|
1958
|
+
rebuilt[key] = maesterEntry(launch, mergedEnv, managedSorted);
|
|
1901
1959
|
placed = true;
|
|
1902
1960
|
} else {
|
|
1903
1961
|
rebuilt[key] = value;
|
|
1904
1962
|
}
|
|
1905
1963
|
}
|
|
1906
1964
|
if (!placed) {
|
|
1907
|
-
|
|
1965
|
+
const mergedEnv = mergeEnvObject(void 0, connectorEnvVars);
|
|
1966
|
+
rebuilt.maester = maesterEntry(launch, mergedEnv, managedSorted);
|
|
1908
1967
|
}
|
|
1909
1968
|
return rebuilt;
|
|
1910
1969
|
}
|
|
1970
|
+
function mergeEnvObject(existingMaesterEntry, connectorEnvVars) {
|
|
1971
|
+
const result = {};
|
|
1972
|
+
const managed = new Set(connectorEnvVars);
|
|
1973
|
+
const previouslyManaged = readStringArray(
|
|
1974
|
+
isPlainObject(existingMaesterEntry) ? existingMaesterEntry[MANAGED_MARKER_KEY] : void 0
|
|
1975
|
+
);
|
|
1976
|
+
const previouslyManagedSet = new Set(previouslyManaged);
|
|
1977
|
+
const existingEnv = isPlainObject(existingMaesterEntry) ? existingMaesterEntry.env : void 0;
|
|
1978
|
+
if (isPlainObject(existingEnv)) {
|
|
1979
|
+
for (const [k, v] of Object.entries(existingEnv)) {
|
|
1980
|
+
if (managed.has(k)) continue;
|
|
1981
|
+
if (previouslyManagedSet.has(k)) continue;
|
|
1982
|
+
if (typeof v === "string") result[k] = v;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
for (const name of managed) result[name] = `\${${name}:-}`;
|
|
1986
|
+
const sorted = {};
|
|
1987
|
+
for (const k of Object.keys(result).sort()) {
|
|
1988
|
+
const v = result[k];
|
|
1989
|
+
if (v !== void 0) sorted[k] = v;
|
|
1990
|
+
}
|
|
1991
|
+
return sorted;
|
|
1992
|
+
}
|
|
1993
|
+
function readStringArray(value) {
|
|
1994
|
+
if (!Array.isArray(value)) return [];
|
|
1995
|
+
return value.filter((v) => typeof v === "string" && v.length > 0);
|
|
1996
|
+
}
|
|
1911
1997
|
function parseOrEmpty(text2) {
|
|
1912
1998
|
if (!text2 || text2.trim().length === 0) return {};
|
|
1913
1999
|
const parsed = JSON.parse(text2);
|
|
@@ -1928,28 +2014,51 @@ async function readOrUndefined(filePath) {
|
|
|
1928
2014
|
}
|
|
1929
2015
|
}
|
|
1930
2016
|
var CONFIG_FILE = path8.join(".codex", "config.toml");
|
|
1931
|
-
|
|
1932
|
-
|
|
2017
|
+
var MANAGED_MARKER_KEY2 = "_maester_managed_env_vars";
|
|
2018
|
+
function maesterBlock(launch, envVars, managed) {
|
|
2019
|
+
const block = { command: launch.command, args: [...launch.args] };
|
|
2020
|
+
if (envVars.length > 0) block.env_vars = [...envVars];
|
|
2021
|
+
if (managed.length > 0) block[MANAGED_MARKER_KEY2] = [...managed];
|
|
2022
|
+
return block;
|
|
1933
2023
|
}
|
|
1934
2024
|
async function writeCodexMcpEntry(repoRoot, options = {}) {
|
|
1935
2025
|
const filePath = path8.join(repoRoot, CONFIG_FILE);
|
|
1936
2026
|
await promises.mkdir(path8.dirname(filePath), { recursive: true });
|
|
1937
2027
|
const existingText = await readOrUndefined2(filePath);
|
|
1938
2028
|
const launch = options.launch ?? resolveMaesterLaunchCommand();
|
|
1939
|
-
const newText = renderTomlWithMaesterBlock(existingText, launch);
|
|
2029
|
+
const newText = renderTomlWithMaesterBlock(existingText, launch, options.connectorEnvVars ?? []);
|
|
1940
2030
|
if (existingText === newText) {
|
|
1941
2031
|
return { filePath, action: "unchanged" };
|
|
1942
2032
|
}
|
|
1943
2033
|
await promises.writeFile(filePath, newText, "utf8");
|
|
1944
2034
|
return { filePath, action: "written" };
|
|
1945
2035
|
}
|
|
1946
|
-
function renderTomlWithMaesterBlock(existingText, launch) {
|
|
2036
|
+
function renderTomlWithMaesterBlock(existingText, launch, connectorEnvVars = []) {
|
|
1947
2037
|
const parsed = existingText && existingText.trim().length > 0 ? TOML.parse(existingText) : {};
|
|
1948
2038
|
const mcpServers = isJsonMap(parsed.mcp_servers) ? { ...parsed.mcp_servers } : {};
|
|
1949
|
-
mcpServers.maester
|
|
2039
|
+
const existingMaester = isJsonMap(mcpServers.maester) ? mcpServers.maester : void 0;
|
|
2040
|
+
const previouslyManaged = readStringArray2(existingMaester?.[MANAGED_MARKER_KEY2]);
|
|
2041
|
+
const userAdded = userAddedEnvVars(existingMaester?.env_vars, previouslyManaged);
|
|
2042
|
+
const mergedEnvVars = Array.from(/* @__PURE__ */ new Set([...connectorEnvVars, ...userAdded])).sort();
|
|
2043
|
+
mcpServers.maester = maesterBlock(launch, mergedEnvVars, [...connectorEnvVars].sort());
|
|
1950
2044
|
const next = { ...parsed, mcp_servers: mcpServers };
|
|
1951
2045
|
return TOML.stringify(next);
|
|
1952
2046
|
}
|
|
2047
|
+
function userAddedEnvVars(existing, previouslyManaged) {
|
|
2048
|
+
if (!Array.isArray(existing)) return [];
|
|
2049
|
+
const managedSet = new Set(previouslyManaged);
|
|
2050
|
+
const result = [];
|
|
2051
|
+
for (const entry of existing) {
|
|
2052
|
+
if (typeof entry !== "string" || entry.length === 0) continue;
|
|
2053
|
+
if (managedSet.has(entry)) continue;
|
|
2054
|
+
result.push(entry);
|
|
2055
|
+
}
|
|
2056
|
+
return result;
|
|
2057
|
+
}
|
|
2058
|
+
function readStringArray2(value) {
|
|
2059
|
+
if (!Array.isArray(value)) return [];
|
|
2060
|
+
return value.filter((v) => typeof v === "string" && v.length > 0);
|
|
2061
|
+
}
|
|
1953
2062
|
function isJsonMap(value) {
|
|
1954
2063
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1955
2064
|
}
|
|
@@ -1964,7 +2073,7 @@ async function readOrUndefined2(filePath) {
|
|
|
1964
2073
|
var MCP_FILE2 = path8.join(".cursor", "mcp.json");
|
|
1965
2074
|
async function writeCursorMcpEntry(repoRoot, options = {}) {
|
|
1966
2075
|
const launch = options.launch ?? resolveMaesterLaunchCommand();
|
|
1967
|
-
return writeJsonMcpFile(path8.join(repoRoot, MCP_FILE2), launch);
|
|
2076
|
+
return writeJsonMcpFile(path8.join(repoRoot, MCP_FILE2), launch, []);
|
|
1968
2077
|
}
|
|
1969
2078
|
|
|
1970
2079
|
// src/core/mcp/registrations/index.ts
|
|
@@ -1972,13 +2081,15 @@ async function refreshMcpRegistrations(repoRoot, options = {}) {
|
|
|
1972
2081
|
const targets = listSkillTargets().filter(
|
|
1973
2082
|
(t) => isMcpHost(t.id) && (!options.scopeTo || options.scopeTo.includes(t.id))
|
|
1974
2083
|
);
|
|
2084
|
+
const envVars = await loadConnectorEnvVarsBestEffort(repoRoot);
|
|
2085
|
+
surfaceEnvVarDiagnostics(envVars);
|
|
1975
2086
|
const outcomes = [];
|
|
1976
2087
|
for (const target of targets) {
|
|
1977
2088
|
const installedVersion = await target.readInstalledVersion(repoRoot);
|
|
1978
2089
|
if (installedVersion === void 0 && !options.scopeTo?.includes(target.id)) {
|
|
1979
2090
|
continue;
|
|
1980
2091
|
}
|
|
1981
|
-
const outcome = await runWriter(target, repoRoot);
|
|
2092
|
+
const outcome = await runWriter(target, repoRoot, envVars.managed);
|
|
1982
2093
|
outcomes.push(outcome);
|
|
1983
2094
|
}
|
|
1984
2095
|
return outcomes;
|
|
@@ -1986,11 +2097,11 @@ async function refreshMcpRegistrations(repoRoot, options = {}) {
|
|
|
1986
2097
|
function isMcpHost(id) {
|
|
1987
2098
|
return id === "claude-code" || id === "cursor" || id === "codex";
|
|
1988
2099
|
}
|
|
1989
|
-
async function runWriter(target, repoRoot) {
|
|
2100
|
+
async function runWriter(target, repoRoot, connectorEnvVars) {
|
|
1990
2101
|
try {
|
|
1991
2102
|
switch (target.id) {
|
|
1992
2103
|
case "claude-code": {
|
|
1993
|
-
const r = await writeClaudeCodeMcpEntry(repoRoot);
|
|
2104
|
+
const r = await writeClaudeCodeMcpEntry(repoRoot, { connectorEnvVars });
|
|
1994
2105
|
return { host: "claude-code", filePath: r.filePath, action: r.action };
|
|
1995
2106
|
}
|
|
1996
2107
|
case "cursor": {
|
|
@@ -1998,7 +2109,7 @@ async function runWriter(target, repoRoot) {
|
|
|
1998
2109
|
return { host: "cursor", filePath: r.filePath, action: r.action };
|
|
1999
2110
|
}
|
|
2000
2111
|
case "codex": {
|
|
2001
|
-
const r = await writeCodexMcpEntry(repoRoot);
|
|
2112
|
+
const r = await writeCodexMcpEntry(repoRoot, { connectorEnvVars });
|
|
2002
2113
|
return { host: "codex", filePath: r.filePath, action: r.action };
|
|
2003
2114
|
}
|
|
2004
2115
|
default:
|
|
@@ -2017,6 +2128,20 @@ async function runWriter(target, repoRoot) {
|
|
|
2017
2128
|
};
|
|
2018
2129
|
}
|
|
2019
2130
|
}
|
|
2131
|
+
function surfaceEnvVarDiagnostics(envVars) {
|
|
2132
|
+
if (envVars.loadError !== void 0) {
|
|
2133
|
+
process.stderr.write(
|
|
2134
|
+
`maester: warning: citadel.yaml could not be loaded for MCP env-var seeding (${envVars.loadError}); writing entries without connector env vars.
|
|
2135
|
+
`
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
for (const entry of envVars.invalid) {
|
|
2139
|
+
process.stderr.write(
|
|
2140
|
+
`maester: warning: connector '${entry.connector}' declares env-var '${entry.envVar}' which is not a valid name (uppercase letters, digits, underscore, starting with a letter); skipping.
|
|
2141
|
+
`
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2020
2145
|
|
|
2021
2146
|
// src/cli/commands/connector.ts
|
|
2022
2147
|
var EXIT_OK = 0;
|
|
@@ -2518,7 +2643,7 @@ function validateTag(value) {
|
|
|
2518
2643
|
|
|
2519
2644
|
// package.json
|
|
2520
2645
|
var package_default = {
|
|
2521
|
-
version: "0.
|
|
2646
|
+
version: "0.5.0"};
|
|
2522
2647
|
var PACKAGE_VERSION = package_default.version;
|
|
2523
2648
|
|
|
2524
2649
|
// src/core/skill/version.ts
|