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/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
|
-
|
|
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
|
|
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
|
-
|
|
2174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2246
|
-
|
|
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
|
|
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.
|
|
2462
|
+
version: "0.5.0"};
|
|
2338
2463
|
var PACKAGE_VERSION = package_default.version;
|
|
2339
2464
|
|
|
2340
2465
|
// src/core/skill/version.ts
|