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/dist/index.js CHANGED
@@ -1957,10 +1957,39 @@ function decideAction(existing, previousVersion, newVersion, newContent) {
1957
1957
  return "upgraded";
1958
1958
  }
1959
1959
 
1960
+ // src/core/mcp/registrations/env-vars.ts
1961
+ var EMPTY = { managed: [], invalid: [] };
1962
+ function collectConnectorEnvVars(connectors) {
1963
+ if (!connectors || connectors.length === 0) return EMPTY;
1964
+ const seen = /* @__PURE__ */ new Set();
1965
+ const invalid = [];
1966
+ for (const c of connectors) {
1967
+ if (!c.auth || c.auth.type !== "token") continue;
1968
+ const name = c.auth.envVar;
1969
+ if (!ENV_VAR_RE.test(name)) {
1970
+ invalid.push({ connector: c.name, envVar: name });
1971
+ continue;
1972
+ }
1973
+ seen.add(name);
1974
+ }
1975
+ return { managed: Array.from(seen).sort(), invalid };
1976
+ }
1977
+ async function loadConnectorEnvVarsBestEffort(repoRoot) {
1978
+ try {
1979
+ const config = await loadCitadelConfig(repoRoot);
1980
+ return collectConnectorEnvVars(config.connectors);
1981
+ } catch (err) {
1982
+ const code = err.code;
1983
+ const message = err instanceof Error ? err.message : String(err);
1984
+ if (code === "ENOENT" || /No citadel\.yaml/.test(message)) return EMPTY;
1985
+ return { ...EMPTY, loadError: message };
1986
+ }
1987
+ }
1988
+
1960
1989
  // src/core/skill/templates/shells/cursor.ts
1961
1990
  var DESCRIPTION = "Citadel-aware guidance for reading aggregated documentation under the citadel base directory.";
1962
1991
  function renderCursorRuleBody(opts) {
1963
- return [
1992
+ const sections = [
1964
1993
  "# Grand Maester (Cursor rule)",
1965
1994
  "",
1966
1995
  "This rule applies when the user asks about content under the citadel",
@@ -1973,6 +2002,25 @@ function renderCursorRuleBody(opts) {
1973
2002
  interpolate3(freshness_awareness_default, opts),
1974
2003
  "",
1975
2004
  interpolate3(connector_policy_default, opts)
2005
+ ];
2006
+ if (opts.requiredEnvVars && opts.requiredEnvVars.length > 0) {
2007
+ sections.push("", renderRequiredEnvVarsNote(opts.requiredEnvVars));
2008
+ }
2009
+ return sections.join("\n");
2010
+ }
2011
+ function renderRequiredEnvVarsNote(envVars) {
2012
+ const sorted = [...envVars].sort();
2013
+ const list = sorted.map((v) => `\`${v}\``).join(", ");
2014
+ return [
2015
+ "## Required environment variables (Cursor)",
2016
+ "",
2017
+ `This citadel exposes connectors that require these env vars: ${list}.`,
2018
+ "",
2019
+ "Cursor inherits env vars from the shell that launches it, so export them in",
2020
+ "that shell (e.g. in your `~/.zshrc` / `~/.bashrc` or by launching Cursor",
2021
+ "from a terminal where they are already set). The maester MCP server reads",
2022
+ "each value at tool-invocation time; if a var is unset, the call returns a",
2023
+ "`missing-env-var` envelope naming the variable."
1976
2024
  ].join("\n");
1977
2025
  }
1978
2026
  function renderCursorRuleFile(body, opts) {
@@ -2005,7 +2053,11 @@ async function writeCursor(input) {
2005
2053
  await promises.mkdir(path.dirname(filePath), { recursive: true });
2006
2054
  const existing = await readTextOrUndefined3(filePath);
2007
2055
  const previousVersion = existing ? extractMarkdownRegion(existing)?.version : void 0;
2008
- const body = renderCursorRuleBody({ baseDir: input.citadelBaseDir });
2056
+ const envVars = await loadConnectorEnvVarsBestEffort(input.repoRoot);
2057
+ const body = renderCursorRuleBody({
2058
+ baseDir: input.citadelBaseDir,
2059
+ requiredEnvVars: envVars.managed
2060
+ });
2009
2061
  const next = existing ? replaceMarkdownRegion(existing, body, input.skillVersion) : `${renderCursorRuleFile(
2010
2062
  replaceMarkdownRegion(void 0, body, input.skillVersion).trimEnd(),
2011
2063
  {
@@ -2170,58 +2222,92 @@ function resolveMaesterLaunchCommand() {
2170
2222
 
2171
2223
  // src/core/mcp/registrations/claude-code.ts
2172
2224
  var MCP_FILE = ".mcp.json";
2173
- function maesterEntry(launch) {
2174
- return { command: launch.command, args: [...launch.args] };
2225
+ var MANAGED_MARKER_KEY = "_maesterManagedEnv";
2226
+ function maesterEntry(launch, envObject, managed) {
2227
+ const entry = { command: launch.command, args: [...launch.args] };
2228
+ if (Object.keys(envObject).length > 0) entry.env = envObject;
2229
+ if (managed.length > 0) entry[MANAGED_MARKER_KEY] = [...managed];
2230
+ return entry;
2175
2231
  }
2176
2232
  async function writeClaudeCodeMcpEntry(repoRoot, options = {}) {
2177
2233
  const launch = options.launch ?? resolveMaesterLaunchCommand();
2178
- return writeJsonMcpFile(path.join(repoRoot, MCP_FILE), launch);
2234
+ return writeJsonMcpFile(path.join(repoRoot, MCP_FILE), launch, options.connectorEnvVars ?? []);
2179
2235
  }
2180
- async function writeJsonMcpFile(filePath, launch) {
2236
+ async function writeJsonMcpFile(filePath, launch, connectorEnvVars = []) {
2181
2237
  await promises.mkdir(path.dirname(filePath), { recursive: true });
2182
2238
  const existingText = await readOrUndefined(filePath);
2183
- const newText = renderJsonWithMaesterEntry(existingText, launch);
2239
+ const newText = renderJsonWithMaesterEntry(existingText, launch, connectorEnvVars);
2184
2240
  if (existingText === newText) {
2185
2241
  return { filePath, action: "unchanged" };
2186
2242
  }
2187
2243
  await promises.writeFile(filePath, newText, "utf8");
2188
2244
  return { filePath, action: "written" };
2189
2245
  }
2190
- function renderJsonWithMaesterEntry(existingText, launch) {
2246
+ function renderJsonWithMaesterEntry(existingText, launch, connectorEnvVars = []) {
2191
2247
  const parsed = parseOrEmpty(existingText);
2192
2248
  const rebuilt = {};
2193
2249
  let placed = false;
2194
2250
  for (const [key, value] of Object.entries(parsed)) {
2195
2251
  if (key === "mcpServers") {
2196
- rebuilt[key] = mutateMcpServers(value, launch);
2252
+ rebuilt[key] = mutateMcpServers(value, launch, connectorEnvVars);
2197
2253
  placed = true;
2198
2254
  } else {
2199
2255
  rebuilt[key] = value;
2200
2256
  }
2201
2257
  }
2202
2258
  if (!placed) {
2203
- rebuilt.mcpServers = mutateMcpServers(void 0, launch);
2259
+ rebuilt.mcpServers = mutateMcpServers(void 0, launch, connectorEnvVars);
2204
2260
  }
2205
2261
  return `${JSON.stringify(rebuilt, null, 2)}
2206
2262
  `;
2207
2263
  }
2208
- function mutateMcpServers(existing, launch) {
2264
+ function mutateMcpServers(existing, launch, connectorEnvVars) {
2209
2265
  const map = isPlainObject(existing) ? { ...existing } : {};
2210
2266
  const rebuilt = {};
2267
+ const managedSorted = [...connectorEnvVars].sort();
2211
2268
  let placed = false;
2212
2269
  for (const [key, value] of Object.entries(map)) {
2213
2270
  if (key === "maester") {
2214
- rebuilt[key] = maesterEntry(launch);
2271
+ const mergedEnv = mergeEnvObject(value, connectorEnvVars);
2272
+ rebuilt[key] = maesterEntry(launch, mergedEnv, managedSorted);
2215
2273
  placed = true;
2216
2274
  } else {
2217
2275
  rebuilt[key] = value;
2218
2276
  }
2219
2277
  }
2220
2278
  if (!placed) {
2221
- rebuilt.maester = maesterEntry(launch);
2279
+ const mergedEnv = mergeEnvObject(void 0, connectorEnvVars);
2280
+ rebuilt.maester = maesterEntry(launch, mergedEnv, managedSorted);
2222
2281
  }
2223
2282
  return rebuilt;
2224
2283
  }
2284
+ function mergeEnvObject(existingMaesterEntry, connectorEnvVars) {
2285
+ const result = {};
2286
+ const managed = new Set(connectorEnvVars);
2287
+ const previouslyManaged = readStringArray(
2288
+ isPlainObject(existingMaesterEntry) ? existingMaesterEntry[MANAGED_MARKER_KEY] : void 0
2289
+ );
2290
+ const previouslyManagedSet = new Set(previouslyManaged);
2291
+ const existingEnv = isPlainObject(existingMaesterEntry) ? existingMaesterEntry.env : void 0;
2292
+ if (isPlainObject(existingEnv)) {
2293
+ for (const [k, v] of Object.entries(existingEnv)) {
2294
+ if (managed.has(k)) continue;
2295
+ if (previouslyManagedSet.has(k)) continue;
2296
+ if (typeof v === "string") result[k] = v;
2297
+ }
2298
+ }
2299
+ for (const name of managed) result[name] = `\${${name}:-}`;
2300
+ const sorted = {};
2301
+ for (const k of Object.keys(result).sort()) {
2302
+ const v = result[k];
2303
+ if (v !== void 0) sorted[k] = v;
2304
+ }
2305
+ return sorted;
2306
+ }
2307
+ function readStringArray(value) {
2308
+ if (!Array.isArray(value)) return [];
2309
+ return value.filter((v) => typeof v === "string" && v.length > 0);
2310
+ }
2225
2311
  function parseOrEmpty(text) {
2226
2312
  if (!text || text.trim().length === 0) return {};
2227
2313
  const parsed = JSON.parse(text);
@@ -2242,28 +2328,51 @@ async function readOrUndefined(filePath) {
2242
2328
  }
2243
2329
  }
2244
2330
  var CONFIG_FILE = path.join(".codex", "config.toml");
2245
- function maesterBlock(launch) {
2246
- return { command: launch.command, args: [...launch.args] };
2331
+ var MANAGED_MARKER_KEY2 = "_maester_managed_env_vars";
2332
+ function maesterBlock(launch, envVars, managed) {
2333
+ const block = { command: launch.command, args: [...launch.args] };
2334
+ if (envVars.length > 0) block.env_vars = [...envVars];
2335
+ if (managed.length > 0) block[MANAGED_MARKER_KEY2] = [...managed];
2336
+ return block;
2247
2337
  }
2248
2338
  async function writeCodexMcpEntry(repoRoot, options = {}) {
2249
2339
  const filePath = path.join(repoRoot, CONFIG_FILE);
2250
2340
  await promises.mkdir(path.dirname(filePath), { recursive: true });
2251
2341
  const existingText = await readOrUndefined2(filePath);
2252
2342
  const launch = options.launch ?? resolveMaesterLaunchCommand();
2253
- const newText = renderTomlWithMaesterBlock(existingText, launch);
2343
+ const newText = renderTomlWithMaesterBlock(existingText, launch, options.connectorEnvVars ?? []);
2254
2344
  if (existingText === newText) {
2255
2345
  return { filePath, action: "unchanged" };
2256
2346
  }
2257
2347
  await promises.writeFile(filePath, newText, "utf8");
2258
2348
  return { filePath, action: "written" };
2259
2349
  }
2260
- function renderTomlWithMaesterBlock(existingText, launch) {
2350
+ function renderTomlWithMaesterBlock(existingText, launch, connectorEnvVars = []) {
2261
2351
  const parsed = existingText && existingText.trim().length > 0 ? TOML.parse(existingText) : {};
2262
2352
  const mcpServers = isJsonMap(parsed.mcp_servers) ? { ...parsed.mcp_servers } : {};
2263
- mcpServers.maester = maesterBlock(launch);
2353
+ const existingMaester = isJsonMap(mcpServers.maester) ? mcpServers.maester : void 0;
2354
+ const previouslyManaged = readStringArray2(existingMaester?.[MANAGED_MARKER_KEY2]);
2355
+ const userAdded = userAddedEnvVars(existingMaester?.env_vars, previouslyManaged);
2356
+ const mergedEnvVars = Array.from(/* @__PURE__ */ new Set([...connectorEnvVars, ...userAdded])).sort();
2357
+ mcpServers.maester = maesterBlock(launch, mergedEnvVars, [...connectorEnvVars].sort());
2264
2358
  const next = { ...parsed, mcp_servers: mcpServers };
2265
2359
  return TOML.stringify(next);
2266
2360
  }
2361
+ function userAddedEnvVars(existing, previouslyManaged) {
2362
+ if (!Array.isArray(existing)) return [];
2363
+ const managedSet = new Set(previouslyManaged);
2364
+ const result = [];
2365
+ for (const entry of existing) {
2366
+ if (typeof entry !== "string" || entry.length === 0) continue;
2367
+ if (managedSet.has(entry)) continue;
2368
+ result.push(entry);
2369
+ }
2370
+ return result;
2371
+ }
2372
+ function readStringArray2(value) {
2373
+ if (!Array.isArray(value)) return [];
2374
+ return value.filter((v) => typeof v === "string" && v.length > 0);
2375
+ }
2267
2376
  function isJsonMap(value) {
2268
2377
  return typeof value === "object" && value !== null && !Array.isArray(value);
2269
2378
  }
@@ -2278,7 +2387,7 @@ async function readOrUndefined2(filePath) {
2278
2387
  var MCP_FILE2 = path.join(".cursor", "mcp.json");
2279
2388
  async function writeCursorMcpEntry(repoRoot, options = {}) {
2280
2389
  const launch = options.launch ?? resolveMaesterLaunchCommand();
2281
- return writeJsonMcpFile(path.join(repoRoot, MCP_FILE2), launch);
2390
+ return writeJsonMcpFile(path.join(repoRoot, MCP_FILE2), launch, []);
2282
2391
  }
2283
2392
 
2284
2393
  // src/core/mcp/registrations/index.ts
@@ -2286,13 +2395,15 @@ async function refreshMcpRegistrations(repoRoot, options = {}) {
2286
2395
  const targets = listSkillTargets().filter(
2287
2396
  (t) => isMcpHost(t.id) && (!options.scopeTo || options.scopeTo.includes(t.id))
2288
2397
  );
2398
+ const envVars = await loadConnectorEnvVarsBestEffort(repoRoot);
2399
+ surfaceEnvVarDiagnostics(envVars);
2289
2400
  const outcomes = [];
2290
2401
  for (const target of targets) {
2291
2402
  const installedVersion = await target.readInstalledVersion(repoRoot);
2292
2403
  if (installedVersion === void 0 && !options.scopeTo?.includes(target.id)) {
2293
2404
  continue;
2294
2405
  }
2295
- const outcome = await runWriter(target, repoRoot);
2406
+ const outcome = await runWriter(target, repoRoot, envVars.managed);
2296
2407
  outcomes.push(outcome);
2297
2408
  }
2298
2409
  return outcomes;
@@ -2300,11 +2411,11 @@ async function refreshMcpRegistrations(repoRoot, options = {}) {
2300
2411
  function isMcpHost(id) {
2301
2412
  return id === "claude-code" || id === "cursor" || id === "codex";
2302
2413
  }
2303
- async function runWriter(target, repoRoot) {
2414
+ async function runWriter(target, repoRoot, connectorEnvVars) {
2304
2415
  try {
2305
2416
  switch (target.id) {
2306
2417
  case "claude-code": {
2307
- const r = await writeClaudeCodeMcpEntry(repoRoot);
2418
+ const r = await writeClaudeCodeMcpEntry(repoRoot, { connectorEnvVars });
2308
2419
  return { host: "claude-code", filePath: r.filePath, action: r.action };
2309
2420
  }
2310
2421
  case "cursor": {
@@ -2312,7 +2423,7 @@ async function runWriter(target, repoRoot) {
2312
2423
  return { host: "cursor", filePath: r.filePath, action: r.action };
2313
2424
  }
2314
2425
  case "codex": {
2315
- const r = await writeCodexMcpEntry(repoRoot);
2426
+ const r = await writeCodexMcpEntry(repoRoot, { connectorEnvVars });
2316
2427
  return { host: "codex", filePath: r.filePath, action: r.action };
2317
2428
  }
2318
2429
  default:
@@ -2331,10 +2442,24 @@ async function runWriter(target, repoRoot) {
2331
2442
  };
2332
2443
  }
2333
2444
  }
2445
+ function surfaceEnvVarDiagnostics(envVars) {
2446
+ if (envVars.loadError !== void 0) {
2447
+ process.stderr.write(
2448
+ `maester: warning: citadel.yaml could not be loaded for MCP env-var seeding (${envVars.loadError}); writing entries without connector env vars.
2449
+ `
2450
+ );
2451
+ }
2452
+ for (const entry of envVars.invalid) {
2453
+ process.stderr.write(
2454
+ `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.
2455
+ `
2456
+ );
2457
+ }
2458
+ }
2334
2459
 
2335
2460
  // package.json
2336
2461
  var package_default = {
2337
- version: "0.4.2"};
2462
+ version: "0.5.0"};
2338
2463
  var PACKAGE_VERSION = package_default.version;
2339
2464
 
2340
2465
  // src/core/skill/version.ts