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 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
- return [
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 body = renderCursorRuleBody({ baseDir: input.citadelBaseDir });
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
- function maesterEntry(launch) {
1860
- return { command: launch.command, args: [...launch.args] };
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
- rebuilt[key] = maesterEntry(launch);
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
- rebuilt.maester = maesterEntry(launch);
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
- function maesterBlock(launch) {
1932
- return { command: launch.command, args: [...launch.args] };
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 = maesterBlock(launch);
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.4.2"};
2646
+ version: "0.5.0"};
2522
2647
  var PACKAGE_VERSION = package_default.version;
2523
2648
 
2524
2649
  // src/core/skill/version.ts