baller-maester 0.3.0 → 0.4.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 +27 -1
- package/README.md +6 -6
- package/dist/cli/main.js +1842 -461
- package/dist/cli/main.js.map +1 -1
- package/dist/index.d.ts +362 -18
- package/dist/index.js +949 -79
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/cli/main.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { existsSync, promises } from 'fs';
|
|
3
|
-
import
|
|
3
|
+
import path8, { resolve, dirname, relative, extname } from 'path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import * as clack from '@clack/prompts';
|
|
6
6
|
import { Chalk } from 'chalk';
|
|
7
7
|
import { readFile, mkdir, writeFile, mkdtemp, rm, readdir, cp, rename } from 'fs/promises';
|
|
8
8
|
import { parseDocument, isMap, stringify } from 'yaml';
|
|
9
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
10
|
+
import TOML from '@iarna/toml';
|
|
11
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
12
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
+
import { createConsola, consola } from 'consola';
|
|
9
15
|
import { globby } from 'globby';
|
|
10
16
|
import { execFile as execFile$1 } from 'child_process';
|
|
11
17
|
import { promisify } from 'util';
|
|
12
18
|
import { simpleGit } from 'simple-git';
|
|
13
|
-
import { createConsola } from 'consola';
|
|
14
19
|
import picomatch from 'picomatch';
|
|
15
20
|
import matter from 'gray-matter';
|
|
16
21
|
|
|
@@ -19,6 +24,361 @@ var __export = (target, all) => {
|
|
|
19
24
|
for (var name in all)
|
|
20
25
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
21
26
|
};
|
|
27
|
+
|
|
28
|
+
// src/core/connectors/types.ts
|
|
29
|
+
var ENVELOPE_SCHEMA_VERSION = 1;
|
|
30
|
+
function defineConnectorOperation(opts) {
|
|
31
|
+
return opts;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/core/connectors/errors.ts
|
|
35
|
+
var ConnectorError = class extends Error {
|
|
36
|
+
code;
|
|
37
|
+
details;
|
|
38
|
+
constructor(code, message, details) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "ConnectorError";
|
|
41
|
+
this.code = code;
|
|
42
|
+
this.details = details;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/core/connectors/types/gitlab-issues/errors.ts
|
|
47
|
+
function mapGitLabHttpError(input) {
|
|
48
|
+
const { status, body, headers, host, envVarName, context } = input;
|
|
49
|
+
if (status === 401 || status === 403) {
|
|
50
|
+
return new ConnectorError(
|
|
51
|
+
"auth-failed",
|
|
52
|
+
envVarName ? `GitLab rejected the token from ${envVarName} (HTTP ${status}) on ${host}.` : `GitLab returned HTTP ${status} on ${host}.`,
|
|
53
|
+
{
|
|
54
|
+
status,
|
|
55
|
+
...envVarName ? { envVar: envVarName } : {},
|
|
56
|
+
host
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (status === 404) {
|
|
61
|
+
const message = context.iid !== void 0 ? `Issue ${context.iid} not found in project '${context.project}' on ${host}.` : `Project '${context.project}' not found on ${host}.`;
|
|
62
|
+
return new ConnectorError("remote-error", message, {
|
|
63
|
+
kind: "not-found",
|
|
64
|
+
status,
|
|
65
|
+
project: context.project,
|
|
66
|
+
...context.iid !== void 0 ? { iid: context.iid } : {}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (status === 429) {
|
|
70
|
+
const retryAfter = headers.get("retry-after");
|
|
71
|
+
return new ConnectorError(
|
|
72
|
+
"remote-error",
|
|
73
|
+
`GitLab rate-limited the request (HTTP 429) on ${host}.${retryAfter ? ` Retry after ${retryAfter}s.` : ""}`,
|
|
74
|
+
{
|
|
75
|
+
kind: "rate-limited",
|
|
76
|
+
status,
|
|
77
|
+
...retryAfter ? { retryAfter } : {}
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (status >= 500 && status <= 599) {
|
|
82
|
+
return new ConnectorError("remote-error", `GitLab returned HTTP ${status} on ${host}.`, {
|
|
83
|
+
kind: "transport",
|
|
84
|
+
status
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return new ConnectorError("remote-error", `Unexpected HTTP ${status} from GitLab on ${host}.`, {
|
|
88
|
+
kind: "unexpected",
|
|
89
|
+
status,
|
|
90
|
+
body: truncateBody(body)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function mapTransportError(err, host) {
|
|
94
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
95
|
+
return new ConnectorError("remote-error", `Failed to reach GitLab at ${host}: ${message}`, {
|
|
96
|
+
kind: "transport",
|
|
97
|
+
cause: message
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
var BODY_EXCERPT_MAX = 1024;
|
|
101
|
+
function truncateBody(body) {
|
|
102
|
+
if (body.length <= BODY_EXCERPT_MAX) return body;
|
|
103
|
+
return `${body.slice(0, BODY_EXCERPT_MAX)}\u2026`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/core/connectors/types/gitlab-issues/output.ts
|
|
107
|
+
var GITLAB_ISSUES_DATA_SCHEMA = 1;
|
|
108
|
+
function projectIssue(raw) {
|
|
109
|
+
if (typeof raw !== "object" || raw === null) {
|
|
110
|
+
throw new Error("Expected GitLab issue payload to be a JSON object.");
|
|
111
|
+
}
|
|
112
|
+
const r = raw;
|
|
113
|
+
return {
|
|
114
|
+
iid: requireNumber(r.iid, "iid"),
|
|
115
|
+
id: requireNumber(r.id, "id"),
|
|
116
|
+
title: requireString(r.title, "title"),
|
|
117
|
+
description: optionalString(r.description),
|
|
118
|
+
state: requireString(r.state, "state"),
|
|
119
|
+
labels: projectLabels(r.labels),
|
|
120
|
+
assignees: projectAssignees(r.assignees),
|
|
121
|
+
milestone: projectMilestone(r.milestone),
|
|
122
|
+
web_url: requireString(r.web_url, "web_url"),
|
|
123
|
+
created_at: requireString(r.created_at, "created_at"),
|
|
124
|
+
updated_at: requireString(r.updated_at, "updated_at"),
|
|
125
|
+
closed_at: optionalString(r.closed_at)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function requireNumber(value, field) {
|
|
129
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
130
|
+
throw new Error(`Expected GitLab issue field '${field}' to be a number.`);
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
function requireString(value, field) {
|
|
135
|
+
if (typeof value !== "string") {
|
|
136
|
+
throw new Error(`Expected GitLab issue field '${field}' to be a string.`);
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
function optionalString(value) {
|
|
141
|
+
if (value === null || value === void 0) return null;
|
|
142
|
+
if (typeof value !== "string") return null;
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
function projectLabels(value) {
|
|
146
|
+
if (!Array.isArray(value)) return [];
|
|
147
|
+
return value.filter((v) => typeof v === "string").map((v) => v);
|
|
148
|
+
}
|
|
149
|
+
function projectAssignees(value) {
|
|
150
|
+
if (!Array.isArray(value)) return [];
|
|
151
|
+
const out = [];
|
|
152
|
+
for (const entry of value) {
|
|
153
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
154
|
+
const r = entry;
|
|
155
|
+
const username = typeof r.username === "string" ? r.username : null;
|
|
156
|
+
const name = typeof r.name === "string" ? r.name : null;
|
|
157
|
+
if (username === null || name === null) continue;
|
|
158
|
+
out.push({ username, name });
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
function projectMilestone(value) {
|
|
163
|
+
if (typeof value !== "object" || value === null) return null;
|
|
164
|
+
const r = value;
|
|
165
|
+
const title = typeof r.title === "string" ? r.title : null;
|
|
166
|
+
const state = typeof r.state === "string" ? r.state : null;
|
|
167
|
+
if (title === null || state === null) return null;
|
|
168
|
+
return { title, state };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/core/connectors/types/gitlab-issues/client.ts
|
|
172
|
+
var NUMERIC_ID_RE = /^\d+$/;
|
|
173
|
+
async function listIssues(opts, params) {
|
|
174
|
+
const url = buildIssuesListUrl(opts, params);
|
|
175
|
+
const response = await performRequest(opts, url);
|
|
176
|
+
const issuesRaw = await response.json();
|
|
177
|
+
if (!Array.isArray(issuesRaw)) {
|
|
178
|
+
throw new ConnectorError("remote-error", "GitLab list-issues response was not a JSON array.", {
|
|
179
|
+
kind: "unexpected"
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const issues = issuesRaw.map((raw) => projectIssue(raw));
|
|
183
|
+
return {
|
|
184
|
+
issues,
|
|
185
|
+
totalPages: readIntegerHeader(response.headers, "x-total-pages"),
|
|
186
|
+
total: readIntegerHeader(response.headers, "x-total")
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
async function getIssue(opts, iid) {
|
|
190
|
+
const url = buildIssueUrl(opts, iid);
|
|
191
|
+
const response = await performRequest(opts, url, { iid });
|
|
192
|
+
const raw = await response.json();
|
|
193
|
+
return projectIssue(raw);
|
|
194
|
+
}
|
|
195
|
+
function buildIssuesListUrl(opts, params) {
|
|
196
|
+
const url = new URL(`${opts.host}/api/v4/projects/${encodeProjectSegment(opts.project)}/issues`);
|
|
197
|
+
url.searchParams.set("state", params.state);
|
|
198
|
+
url.searchParams.set("page", String(params.page));
|
|
199
|
+
url.searchParams.set("per_page", String(params.per_page));
|
|
200
|
+
if (params.labels !== void 0) url.searchParams.set("labels", params.labels);
|
|
201
|
+
if (params.assignee !== void 0) {
|
|
202
|
+
const param = gitlabAssigneeParam(params.assignee);
|
|
203
|
+
url.searchParams.set(param.key, param.value);
|
|
204
|
+
}
|
|
205
|
+
if (params.milestone !== void 0) url.searchParams.set("milestone", params.milestone);
|
|
206
|
+
if (params.search !== void 0) url.searchParams.set("search", params.search);
|
|
207
|
+
return url;
|
|
208
|
+
}
|
|
209
|
+
function buildIssueUrl(opts, iid) {
|
|
210
|
+
return new URL(
|
|
211
|
+
`${opts.host}/api/v4/projects/${encodeProjectSegment(opts.project)}/issues/${iid}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
function encodeProjectSegment(project) {
|
|
215
|
+
if (NUMERIC_ID_RE.test(project)) return project;
|
|
216
|
+
return encodeURIComponent(project);
|
|
217
|
+
}
|
|
218
|
+
async function performRequest(opts, url, context = {}) {
|
|
219
|
+
const headers = {
|
|
220
|
+
Accept: "application/json"
|
|
221
|
+
};
|
|
222
|
+
if (opts.token !== void 0) {
|
|
223
|
+
headers["PRIVATE-TOKEN"] = opts.token;
|
|
224
|
+
}
|
|
225
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
226
|
+
let response;
|
|
227
|
+
try {
|
|
228
|
+
response = await fetchImpl(url, { method: "GET", headers });
|
|
229
|
+
} catch (err) {
|
|
230
|
+
throw mapTransportError(err, opts.host);
|
|
231
|
+
}
|
|
232
|
+
if (!response.ok) {
|
|
233
|
+
const body = await response.text().catch(() => "");
|
|
234
|
+
throw mapGitLabHttpError({
|
|
235
|
+
status: response.status,
|
|
236
|
+
body,
|
|
237
|
+
headers: response.headers,
|
|
238
|
+
host: opts.host,
|
|
239
|
+
envVarName: opts.envVarName,
|
|
240
|
+
context: {
|
|
241
|
+
project: opts.project,
|
|
242
|
+
...context.iid !== void 0 ? { iid: context.iid } : {}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return response;
|
|
247
|
+
}
|
|
248
|
+
function readIntegerHeader(headers, name) {
|
|
249
|
+
const raw = headers.get(name);
|
|
250
|
+
if (raw === null || raw.length === 0) return null;
|
|
251
|
+
const parsed = Number.parseInt(raw, 10);
|
|
252
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
253
|
+
}
|
|
254
|
+
function gitlabAssigneeParam(value) {
|
|
255
|
+
return { key: "assignee_username", value };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/core/connectors/types/gitlab-issues/operations.ts
|
|
259
|
+
var PER_PAGE_CAP = 100;
|
|
260
|
+
var listIssuesArgsSchema = z.object({
|
|
261
|
+
state: z.enum(["opened", "closed", "all"]).default("opened"),
|
|
262
|
+
labels: z.string().min(1).optional(),
|
|
263
|
+
assignee: z.string().min(1).optional(),
|
|
264
|
+
milestone: z.string().min(1).optional(),
|
|
265
|
+
search: z.string().min(1).optional(),
|
|
266
|
+
page: z.coerce.number().int().positive("page must be a positive integer").default(1),
|
|
267
|
+
per_page: z.coerce.number().int().positive("per_page must be a positive integer").default(20)
|
|
268
|
+
}).strict();
|
|
269
|
+
var getIssueArgsSchema = z.object({
|
|
270
|
+
iid: z.coerce.number().int().positive("iid must be a positive integer")
|
|
271
|
+
}).strict();
|
|
272
|
+
var listIssuesOperation = defineConnectorOperation({
|
|
273
|
+
name: "list-issues",
|
|
274
|
+
argsSchema: listIssuesArgsSchema,
|
|
275
|
+
dataSchemaVersion: GITLAB_ISSUES_DATA_SCHEMA,
|
|
276
|
+
handler: async (args, ctx) => {
|
|
277
|
+
const requestedPerPage = args.per_page;
|
|
278
|
+
const clamped = requestedPerPage > PER_PAGE_CAP;
|
|
279
|
+
const effectivePerPage = clamped ? PER_PAGE_CAP : requestedPerPage;
|
|
280
|
+
const params = {
|
|
281
|
+
state: args.state,
|
|
282
|
+
page: args.page,
|
|
283
|
+
per_page: effectivePerPage,
|
|
284
|
+
...args.labels !== void 0 ? { labels: args.labels } : {},
|
|
285
|
+
...args.assignee !== void 0 ? { assignee: args.assignee } : {},
|
|
286
|
+
...args.milestone !== void 0 ? { milestone: args.milestone } : {},
|
|
287
|
+
...args.search !== void 0 ? { search: args.search } : {}
|
|
288
|
+
};
|
|
289
|
+
const client = clientFromContext(ctx);
|
|
290
|
+
const response = await listIssues(client, params);
|
|
291
|
+
return {
|
|
292
|
+
data: {
|
|
293
|
+
issues: response.issues,
|
|
294
|
+
meta: {
|
|
295
|
+
page: args.page,
|
|
296
|
+
per_page: effectivePerPage,
|
|
297
|
+
total_pages: response.totalPages,
|
|
298
|
+
total: response.total,
|
|
299
|
+
clamped
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
var getIssueOperation = defineConnectorOperation({
|
|
306
|
+
name: "get-issue",
|
|
307
|
+
argsSchema: getIssueArgsSchema,
|
|
308
|
+
dataSchemaVersion: GITLAB_ISSUES_DATA_SCHEMA,
|
|
309
|
+
handler: async (args, ctx) => {
|
|
310
|
+
const client = clientFromContext(ctx);
|
|
311
|
+
const issue = await getIssue(client, args.iid);
|
|
312
|
+
return { data: { issue } };
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
function clientFromContext(ctx) {
|
|
316
|
+
return {
|
|
317
|
+
host: ctx.config.host,
|
|
318
|
+
project: ctx.config.project,
|
|
319
|
+
token: ctx.token,
|
|
320
|
+
envVarName: ctx.auth.type === "token" ? ctx.auth.envVar : void 0
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
var DEFAULT_HOST = "https://gitlab.com";
|
|
324
|
+
function isHttpsUrl(value) {
|
|
325
|
+
if (/\s/.test(value)) return false;
|
|
326
|
+
if (!/^https:\/\/[^/]+/i.test(value)) return false;
|
|
327
|
+
try {
|
|
328
|
+
new URL(value);
|
|
329
|
+
return true;
|
|
330
|
+
} catch {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function normalizeHost(value) {
|
|
335
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
336
|
+
}
|
|
337
|
+
var GitLabIssuesConfigSchema = z.object({
|
|
338
|
+
host: z.string().refine(isHttpsUrl, "host must be an HTTPS URL (e.g. https://gitlab.com)").transform(normalizeHost).optional().default(DEFAULT_HOST),
|
|
339
|
+
project: z.string().min(1, "project is required").refine((v) => !/\s/.test(v), "project must not contain whitespace"),
|
|
340
|
+
apiVersion: z.literal(4).optional().default(4)
|
|
341
|
+
}).strict();
|
|
342
|
+
|
|
343
|
+
// src/core/connectors/types/gitlab-issues/index.ts
|
|
344
|
+
var GITLAB_ISSUES_TYPE_ID = "gitlab-issues";
|
|
345
|
+
var gitlabIssuesType = {
|
|
346
|
+
id: GITLAB_ISSUES_TYPE_ID,
|
|
347
|
+
label: "GitLab Issues",
|
|
348
|
+
configSchema: GitLabIssuesConfigSchema,
|
|
349
|
+
operations: {
|
|
350
|
+
[listIssuesOperation.name]: listIssuesOperation,
|
|
351
|
+
[getIssueOperation.name]: getIssueOperation
|
|
352
|
+
},
|
|
353
|
+
describeTool: (operation, resolvedConfig) => {
|
|
354
|
+
const host = stripScheme(resolvedConfig.host);
|
|
355
|
+
const project = resolvedConfig.project;
|
|
356
|
+
switch (operation.name) {
|
|
357
|
+
case "list-issues":
|
|
358
|
+
return `List GitLab issues for project ${project} on ${host}. Supports filtering by state (opened/closed/all), labels, assignee, milestone, free-text search, and page/per_page pagination. Returns at most 100 issues per call.`;
|
|
359
|
+
case "get-issue":
|
|
360
|
+
return `Fetch a single GitLab issue from project ${project} on ${host} by its project-scoped iid. Returns the issue's title, description, state, labels, assignees, milestone, timestamps, and web_url.`;
|
|
361
|
+
default:
|
|
362
|
+
return `GitLab Issues operation '${operation.name}' for project ${project} on ${host}.`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
function stripScheme(host) {
|
|
367
|
+
return host.replace(/^https?:\/\//i, "");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/core/connectors/registry.ts
|
|
371
|
+
var REGISTRY = /* @__PURE__ */ new Map();
|
|
372
|
+
REGISTRY.set(gitlabIssuesType.id, gitlabIssuesType);
|
|
373
|
+
function listConnectorTypes() {
|
|
374
|
+
return [...REGISTRY.values()];
|
|
375
|
+
}
|
|
376
|
+
function lookupConnectorType(id) {
|
|
377
|
+
return REGISTRY.get(id);
|
|
378
|
+
}
|
|
379
|
+
function hasConnectorType(id) {
|
|
380
|
+
return REGISTRY.has(id);
|
|
381
|
+
}
|
|
22
382
|
var STATE_VALUES = ["draft", "canon"];
|
|
23
383
|
var StateSchema = z.enum(STATE_VALUES);
|
|
24
384
|
var DEFAULT_STATE = "draft";
|
|
@@ -83,6 +443,13 @@ var SourceSchema = z.object({
|
|
|
83
443
|
description: z.string().min(1).optional(),
|
|
84
444
|
tags: z.array(z.string().min(1).regex(SLUG_RE, "tags must be slugs")).optional()
|
|
85
445
|
}).strict();
|
|
446
|
+
var ConnectorBaseSchema = z.object({
|
|
447
|
+
name: z.string().min(1).regex(SLUG_RE, "name must be a kebab-case slug starting with a letter or digit"),
|
|
448
|
+
type: z.string().min(1),
|
|
449
|
+
auth: AuthRefSchema.optional(),
|
|
450
|
+
description: z.string().min(1).optional(),
|
|
451
|
+
config: z.unknown().optional()
|
|
452
|
+
}).strict();
|
|
86
453
|
var DEFAULT_BASE_DIR = "citadel";
|
|
87
454
|
function applyCombinedInvariants(data, ctx) {
|
|
88
455
|
if (data.sources.length === 0) {
|
|
@@ -121,11 +488,46 @@ function applyCombinedInvariants(data, ctx) {
|
|
|
121
488
|
destsSeen.set(resolved, { index: i, name: entry.name });
|
|
122
489
|
}
|
|
123
490
|
}
|
|
491
|
+
const connectorNames = /* @__PURE__ */ new Map();
|
|
492
|
+
const connectors = data.connectors ?? [];
|
|
493
|
+
for (let i = 0; i < connectors.length; i++) {
|
|
494
|
+
const entry = connectors[i];
|
|
495
|
+
if (!entry?.name) continue;
|
|
496
|
+
const priorIndex = connectorNames.get(entry.name);
|
|
497
|
+
if (priorIndex !== void 0) {
|
|
498
|
+
ctx.addIssue({
|
|
499
|
+
code: z.ZodIssueCode.custom,
|
|
500
|
+
message: `duplicate connector name '${entry.name}' \u2014 also used by connectors[${priorIndex}]`,
|
|
501
|
+
path: ["connectors", i, "name"]
|
|
502
|
+
});
|
|
503
|
+
} else {
|
|
504
|
+
connectorNames.set(entry.name, i);
|
|
505
|
+
}
|
|
506
|
+
const type = lookupConnectorType(entry.type);
|
|
507
|
+
if (!type) {
|
|
508
|
+
ctx.addIssue({
|
|
509
|
+
code: z.ZodIssueCode.custom,
|
|
510
|
+
message: `unknown connector type '${entry.type}' for connector '${entry.name}'`,
|
|
511
|
+
path: ["connectors", i, "type"]
|
|
512
|
+
});
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const configResult = type.configSchema.safeParse(entry.config ?? {});
|
|
516
|
+
if (!configResult.success) {
|
|
517
|
+
for (const issue of configResult.error.issues) {
|
|
518
|
+
ctx.addIssue({
|
|
519
|
+
...issue,
|
|
520
|
+
path: ["connectors", i, "config", ...issue.path]
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
124
525
|
}
|
|
125
526
|
var CitadelConfigSchema = z.object({
|
|
126
527
|
schemaVersion: z.literal(1),
|
|
127
528
|
baseDir: z.string().min(1).refine(isSafeRelativePath, "baseDir must be a repo-relative path with no '..' segments").optional(),
|
|
128
|
-
sources: z.array(SourceSchema).optional().default([])
|
|
529
|
+
sources: z.array(SourceSchema).optional().default([]),
|
|
530
|
+
connectors: z.array(ConnectorBaseSchema).optional()
|
|
129
531
|
}).strict().superRefine((data, ctx) => {
|
|
130
532
|
applyCombinedInvariants(data, ctx);
|
|
131
533
|
});
|
|
@@ -551,216 +953,411 @@ function readColumns(stream = process.stdout) {
|
|
|
551
953
|
if (typeof stream.columns === "number" && stream.columns > 0) return stream.columns;
|
|
552
954
|
return DEFAULT_FALLBACK;
|
|
553
955
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
# Each source is a git repository. By default, the citadel fetches whatever
|
|
560
|
-
# the source publishes in its own \`maester.yaml\` manifest. If a source does
|
|
561
|
-
# not publish a manifest (or you want to override what gets pulled), declare
|
|
562
|
-
# an \`includes\` list and the citadel will materialize exactly those paths
|
|
563
|
-
# or globs instead.
|
|
564
|
-
#
|
|
565
|
-
# By default, every source is surfaced at \`<baseDir>/<source-name>/\` from the
|
|
566
|
-
# repository root. The optional top-level \`baseDir\` field changes that parent
|
|
567
|
-
# folder once for every source; when omitted, the default is \`citadel\`. A
|
|
568
|
-
# per-source \`destination\` always wins over the configured base.
|
|
569
|
-
#
|
|
570
|
-
# Run \`maester sync\` (or \`npm run maester:sync\`) to refresh every source in
|
|
571
|
-
# one pass. Generated by \`npx maester init\` and safe to commit. Secret
|
|
572
|
-
# values are never stored here \u2014 only the names of environment variables
|
|
573
|
-
# that hold them.
|
|
574
|
-
|
|
575
|
-
`;
|
|
576
|
-
var MAESTER_HEADER = `# maester.yaml
|
|
577
|
-
#
|
|
578
|
-
# This file declares the documents this repository publishes to any citadel
|
|
579
|
-
# that pulls from it. It is a manifest only \u2014 the documents themselves live
|
|
580
|
-
# wherever the \`path\` fields point, and \`maester\` does not modify them.
|
|
581
|
-
#
|
|
582
|
-
# Generated by \`npx maester publish\` and safe to commit.
|
|
583
|
-
|
|
584
|
-
`;
|
|
585
|
-
async function writeCitadelConfig(repoRoot, config) {
|
|
586
|
-
const path5 = citadelConfigPath(repoRoot);
|
|
587
|
-
const ordered = {
|
|
588
|
-
schemaVersion: config.schemaVersion,
|
|
589
|
-
...config.baseDir ? { baseDir: config.baseDir } : {},
|
|
590
|
-
sources: config.sources
|
|
591
|
-
};
|
|
592
|
-
const body = stringify(ordered, { indent: 2, lineWidth: 100, singleQuote: false });
|
|
593
|
-
await writeFile(path5, `${CITADEL_HEADER}${body}`, "utf8");
|
|
594
|
-
return path5;
|
|
595
|
-
}
|
|
596
|
-
async function writeMaesterConfig(repoRoot, config) {
|
|
597
|
-
const path5 = maesterConfigPath(repoRoot);
|
|
598
|
-
const body = stringify(config, { indent: 2, lineWidth: 100, singleQuote: false });
|
|
599
|
-
await writeFile(path5, `${MAESTER_HEADER}${body}`, "utf8");
|
|
600
|
-
return path5;
|
|
956
|
+
function isSafeRepoRelative(value) {
|
|
957
|
+
if (value.length === 0 || /^\s+$/.test(value)) return false;
|
|
958
|
+
if (value.startsWith("/")) return false;
|
|
959
|
+
if (value.split(/[\\/]+/).some((seg) => seg === "..")) return false;
|
|
960
|
+
return true;
|
|
601
961
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
962
|
+
var PublishedDocumentSchema = z.object({
|
|
963
|
+
path: z.string().min(1).refine(
|
|
964
|
+
isSafeRepoRelative,
|
|
965
|
+
"path must be a repo-relative file or glob; no leading '/' and no '..'"
|
|
966
|
+
),
|
|
967
|
+
description: z.string().min(1).optional(),
|
|
968
|
+
category: z.string().min(1).regex(SLUG_RE, "category must be a kebab-case slug").optional(),
|
|
969
|
+
tags: z.array(z.string().min(1).regex(SLUG_RE, "tags must be slugs")).optional(),
|
|
970
|
+
state: StateSchema.optional()
|
|
971
|
+
}).strict();
|
|
972
|
+
var MaesterConfigSchema = z.object({
|
|
973
|
+
schemaVersion: z.literal(1),
|
|
974
|
+
documents: z.array(PublishedDocumentSchema).min(1, "at least one published document must be declared").superRefine((docs, ctx) => {
|
|
975
|
+
const seen = /* @__PURE__ */ new Map();
|
|
976
|
+
for (let i = 0; i < docs.length; i++) {
|
|
977
|
+
const p = docs[i]?.path;
|
|
978
|
+
if (!p) continue;
|
|
979
|
+
const prior = seen.get(p);
|
|
980
|
+
if (prior !== void 0) {
|
|
981
|
+
ctx.addIssue({
|
|
982
|
+
code: z.ZodIssueCode.custom,
|
|
983
|
+
message: `duplicate path '${p}' (also at index ${prior})`,
|
|
984
|
+
path: [i, "path"]
|
|
985
|
+
});
|
|
986
|
+
} else {
|
|
987
|
+
seen.set(p, i);
|
|
988
|
+
}
|
|
621
989
|
}
|
|
990
|
+
})
|
|
991
|
+
}).strict();
|
|
992
|
+
|
|
993
|
+
// src/core/config/loader.ts
|
|
994
|
+
async function loadCitadelConfig(repoRoot) {
|
|
995
|
+
const path9 = citadelConfigPath(repoRoot);
|
|
996
|
+
if (!existsSync(path9)) {
|
|
997
|
+
throw new ConfigError(
|
|
998
|
+
"No citadel.yaml found at the repository root. Run `npx baller-maester init` to create one.",
|
|
999
|
+
{ filePath: path9 }
|
|
1000
|
+
);
|
|
622
1001
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
const needsTrailingNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
627
|
-
const appendBlock = `${needsTrailingNewline ? "\n" : ""}${added.join("\n")}
|
|
628
|
-
`;
|
|
629
|
-
await writeFile(path5, `${existing}${appendBlock}`, "utf8");
|
|
630
|
-
return { added, alreadyPresent };
|
|
1002
|
+
const raw = await readFile(path9, "utf8");
|
|
1003
|
+
return parseAndValidate(raw, CitadelConfigSchema, path9);
|
|
631
1004
|
}
|
|
632
|
-
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
1005
|
+
function parseAndValidate(raw, schema, filePath) {
|
|
1006
|
+
const data = parseYaml(raw, filePath);
|
|
1007
|
+
return runSchema(data, schema, filePath);
|
|
1008
|
+
}
|
|
1009
|
+
function parseYaml(raw, filePath) {
|
|
1010
|
+
const doc = parseDocument(raw, { keepSourceTokens: false });
|
|
1011
|
+
const yamlErrors = doc.errors;
|
|
1012
|
+
if (yamlErrors.length > 0) {
|
|
1013
|
+
const first = yamlErrors[0];
|
|
1014
|
+
const pos = positionFromError(first, raw);
|
|
1015
|
+
throw new ConfigError(`YAML parse error: ${first.message}`, {
|
|
1016
|
+
filePath,
|
|
1017
|
+
line: pos.line,
|
|
1018
|
+
column: pos.column,
|
|
1019
|
+
cause: first
|
|
1020
|
+
});
|
|
643
1021
|
}
|
|
644
|
-
|
|
645
|
-
|
|
1022
|
+
return doc.toJS({ maxAliasCount: -1 });
|
|
1023
|
+
}
|
|
1024
|
+
function runSchema(data, schema, filePath) {
|
|
1025
|
+
const result = schema.safeParse(data);
|
|
1026
|
+
if (!result.success) {
|
|
1027
|
+
const issue = result.error.issues[0];
|
|
1028
|
+
const where = issue?.path?.length ? ` at \`${issue.path.join(".")}\`` : "";
|
|
1029
|
+
throw new ConfigError(`${filePath}: ${issue?.message ?? "validation failed"}${where}`, {
|
|
1030
|
+
filePath,
|
|
1031
|
+
cause: result.error
|
|
1032
|
+
});
|
|
646
1033
|
}
|
|
647
|
-
|
|
648
|
-
parsed.scripts = scripts;
|
|
649
|
-
const serialized = JSON.stringify(parsed, null, 2) + (trailingNewline ? "\n" : "");
|
|
650
|
-
await writeFile(path5, serialized, "utf8");
|
|
651
|
-
return { added: true, reason: "added" };
|
|
1034
|
+
return result.data;
|
|
652
1035
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const gitignore = await appendMissingGitignoreEntries(repoRoot, [`${CACHE_DIR_NAME}/`]);
|
|
664
|
-
const script = await ensureScript(repoRoot, "maester:sync", "maester sync");
|
|
665
|
-
return {
|
|
666
|
-
citadelPath,
|
|
667
|
-
gitignoreAdded: gitignore.added,
|
|
668
|
-
packageJsonScript: script.reason
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
function detectDestinationCollisions(repoRoot, input, baseDirArg) {
|
|
672
|
-
const baseDir = Array.isArray(input) ? baseDirArg : input.baseDir;
|
|
673
|
-
const entries = Array.isArray(input) ? input : input.sources.map((s) => ({ name: s.name, destination: s.destination }));
|
|
674
|
-
const byDest = /* @__PURE__ */ new Map();
|
|
675
|
-
for (const entry of entries) {
|
|
676
|
-
const dest = entry.destination ? resolve(repoRoot, entry.destination) : defaultDestinationFor(repoRoot, entry.name, baseDir);
|
|
677
|
-
const prior = byDest.get(dest);
|
|
678
|
-
if (prior) {
|
|
679
|
-
throw new Error(
|
|
680
|
-
`sources '${entry.name}' and '${prior.name}' both resolve to destination '${dest}'. Set a unique destination for one of them.`
|
|
681
|
-
);
|
|
1036
|
+
function positionFromError(err, raw) {
|
|
1037
|
+
const pos = err.pos;
|
|
1038
|
+
if (!pos) return { line: 1, column: 1 };
|
|
1039
|
+
const offset = pos[0];
|
|
1040
|
+
let line = 1;
|
|
1041
|
+
let lastLineStart = 0;
|
|
1042
|
+
for (let i = 0; i < offset && i < raw.length; i++) {
|
|
1043
|
+
if (raw[i] === "\n") {
|
|
1044
|
+
line++;
|
|
1045
|
+
lastLineStart = i + 1;
|
|
682
1046
|
}
|
|
683
|
-
byDest.set(dest, { name: entry.name });
|
|
684
1047
|
}
|
|
1048
|
+
return { line, column: offset - lastLineStart + 1 };
|
|
685
1049
|
}
|
|
686
1050
|
|
|
687
|
-
// src/core/
|
|
688
|
-
function
|
|
689
|
-
if (!
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1051
|
+
// src/core/auth/resolver.ts
|
|
1052
|
+
function resolveAuth(auth, env = process.env) {
|
|
1053
|
+
if (!auth || auth.type === "none") return { type: "delegated" };
|
|
1054
|
+
const value = env[auth.envVar];
|
|
1055
|
+
if (value === void 0 || value.length === 0) {
|
|
1056
|
+
throw new AuthError(
|
|
1057
|
+
auth.envVar,
|
|
1058
|
+
`${auth.envVar} is not set. Define it in your shell, .env loader, or CI secret manager before syncing.`
|
|
1059
|
+
);
|
|
695
1060
|
}
|
|
696
|
-
return {
|
|
1061
|
+
return { type: "token", value, envVar: auth.envVar };
|
|
697
1062
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
if (value.startsWith("https://") || value.startsWith("ssh://") || value.startsWith("file://")) {
|
|
702
|
-
return { ok: true };
|
|
703
|
-
}
|
|
704
|
-
if (/^git@[^\s:]+:\S+$/.test(value)) return { ok: true };
|
|
1063
|
+
|
|
1064
|
+
// src/core/connectors/envelope.ts
|
|
1065
|
+
function buildSuccessEnvelope(input) {
|
|
705
1066
|
return {
|
|
1067
|
+
schema: ENVELOPE_SCHEMA_VERSION,
|
|
1068
|
+
connector: input.connector,
|
|
1069
|
+
operation: input.operation,
|
|
1070
|
+
ok: true,
|
|
1071
|
+
data: { ...input.data, dataSchema: input.dataSchemaVersion }
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
function buildFailureEnvelope(input) {
|
|
1075
|
+
return {
|
|
1076
|
+
schema: ENVELOPE_SCHEMA_VERSION,
|
|
1077
|
+
connector: input.connector,
|
|
1078
|
+
operation: input.operation,
|
|
706
1079
|
ok: false,
|
|
707
|
-
|
|
1080
|
+
error: {
|
|
1081
|
+
code: input.code,
|
|
1082
|
+
message: input.message,
|
|
1083
|
+
...input.details ? { details: input.details } : {}
|
|
1084
|
+
}
|
|
708
1085
|
};
|
|
709
1086
|
}
|
|
710
|
-
function
|
|
711
|
-
|
|
712
|
-
|
|
1087
|
+
function argsSchemaToJsonSchema(schema) {
|
|
1088
|
+
const raw = zodToJsonSchema(schema, {
|
|
1089
|
+
$refStrategy: "none",
|
|
1090
|
+
target: "jsonSchema7"
|
|
1091
|
+
});
|
|
1092
|
+
const {
|
|
1093
|
+
$schema: _omitSchema,
|
|
1094
|
+
definitions: _omitDefs,
|
|
1095
|
+
...rest
|
|
1096
|
+
} = raw;
|
|
1097
|
+
return rest;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// src/core/connectors/tool-name.ts
|
|
1101
|
+
var VALID_TOOL_NAME_RE = /^[a-z][a-z0-9_]*$/;
|
|
1102
|
+
function toolName(connectorName, operationName) {
|
|
1103
|
+
const left = normalize(connectorName);
|
|
1104
|
+
const right = normalize(operationName);
|
|
1105
|
+
const result = `${left}__${right}`;
|
|
1106
|
+
if (!VALID_TOOL_NAME_RE.test(result)) {
|
|
1107
|
+
throw new Error(
|
|
1108
|
+
`Invalid MCP tool name '${result}' (from connector '${connectorName}', operation '${operationName}'). Names must match /^[a-z][a-z0-9_]*$/ after normalization.`
|
|
1109
|
+
);
|
|
713
1110
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1111
|
+
return result;
|
|
1112
|
+
}
|
|
1113
|
+
function normalize(part) {
|
|
1114
|
+
return part.toLowerCase().replace(/-/g, "_");
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/core/connectors/dispatch.ts
|
|
1118
|
+
async function invokeOperation(input) {
|
|
1119
|
+
const { connector, operationName, args, env } = input;
|
|
1120
|
+
const type = lookupConnectorType(connector.type);
|
|
1121
|
+
if (!type) {
|
|
1122
|
+
return buildFailureEnvelope({
|
|
1123
|
+
connector: connector.name,
|
|
1124
|
+
operation: operationName,
|
|
1125
|
+
code: "connector-not-found",
|
|
1126
|
+
message: `No registered connector type '${connector.type}' for connector '${connector.name}'.`
|
|
1127
|
+
});
|
|
720
1128
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1129
|
+
const operation = type.operations[operationName];
|
|
1130
|
+
if (!operation) {
|
|
1131
|
+
const known = Object.keys(type.operations).sort();
|
|
1132
|
+
return buildFailureEnvelope({
|
|
1133
|
+
connector: connector.name,
|
|
1134
|
+
operation: operationName,
|
|
1135
|
+
code: "unknown-operation",
|
|
1136
|
+
message: `Connector '${connector.name}' (type '${type.id}') has no operation '${operationName}'. Known operations: ${known.join(", ") || "(none)"}.`
|
|
1137
|
+
});
|
|
726
1138
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1139
|
+
let resolvedConfig;
|
|
1140
|
+
try {
|
|
1141
|
+
resolvedConfig = type.configSchema.parse(connector.config);
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
return buildFailureEnvelope({
|
|
1144
|
+
connector: connector.name,
|
|
1145
|
+
operation: operationName,
|
|
1146
|
+
code: "invalid-argument",
|
|
1147
|
+
message: `Connector '${connector.name}' has invalid per-type config for type '${type.id}'.`,
|
|
1148
|
+
details: { cause: zodIssueDetails(err) }
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
let parsedArgs;
|
|
1152
|
+
try {
|
|
1153
|
+
parsedArgs = operation.argsSchema.parse(args ?? {});
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
return buildFailureEnvelope({
|
|
1156
|
+
connector: connector.name,
|
|
1157
|
+
operation: operationName,
|
|
1158
|
+
code: "invalid-argument",
|
|
1159
|
+
message: `Invalid arguments for operation '${operationName}'.`,
|
|
1160
|
+
details: { cause: zodIssueDetails(err) }
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
let auth;
|
|
1164
|
+
try {
|
|
1165
|
+
auth = resolveAuth(connector.auth, env ?? process.env);
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
if (err instanceof AuthError) {
|
|
1168
|
+
return buildFailureEnvelope({
|
|
1169
|
+
connector: connector.name,
|
|
1170
|
+
operation: operationName,
|
|
1171
|
+
code: "missing-env-var",
|
|
1172
|
+
message: `Environment variable '${err.envVar}' is not set; required by connector '${connector.name}'.`,
|
|
1173
|
+
details: { envVar: err.envVar }
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
throw err;
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
const result = await operation.handler(parsedArgs, {
|
|
1180
|
+
config: resolvedConfig,
|
|
1181
|
+
token: auth.type === "token" ? auth.value : void 0,
|
|
1182
|
+
auth
|
|
1183
|
+
});
|
|
1184
|
+
return buildSuccessEnvelope({
|
|
1185
|
+
connector: connector.name,
|
|
1186
|
+
operation: operationName,
|
|
1187
|
+
data: result.data,
|
|
1188
|
+
dataSchemaVersion: operation.dataSchemaVersion
|
|
1189
|
+
});
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
if (err instanceof ConnectorError) {
|
|
1192
|
+
return buildFailureEnvelope({
|
|
1193
|
+
connector: connector.name,
|
|
1194
|
+
operation: operationName,
|
|
1195
|
+
code: err.code,
|
|
1196
|
+
message: err.message,
|
|
1197
|
+
...err.details ? { details: err.details } : {}
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
process.stderr.write(
|
|
1201
|
+
`[maester] internal error in connector '${connector.name}'.${operationName}: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
1202
|
+
`
|
|
1203
|
+
);
|
|
1204
|
+
return buildFailureEnvelope({
|
|
1205
|
+
connector: connector.name,
|
|
1206
|
+
operation: operationName,
|
|
1207
|
+
code: "internal-error",
|
|
1208
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1209
|
+
});
|
|
735
1210
|
}
|
|
736
|
-
return { ok: true };
|
|
737
1211
|
}
|
|
738
|
-
function
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1212
|
+
function buildToolDescription(connector, operation, resolvedConfig, type) {
|
|
1213
|
+
const typeDescription = type.describeTool(operation, resolvedConfig);
|
|
1214
|
+
const entryDescription = connector.description?.trim();
|
|
1215
|
+
return entryDescription ? `${entryDescription} ${typeDescription}` : typeDescription;
|
|
1216
|
+
}
|
|
1217
|
+
function listConnectorTools(config) {
|
|
1218
|
+
const descriptors = [];
|
|
1219
|
+
const connectors = config.connectors ?? [];
|
|
1220
|
+
for (const connector of connectors) {
|
|
1221
|
+
const type = lookupConnectorType(connector.type);
|
|
1222
|
+
if (!type) {
|
|
1223
|
+
throw new Error(
|
|
1224
|
+
`Connector '${connector.name}' references unregistered type '${connector.type}'. This indicates registry mutation after config load.`
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
const resolvedConfig = type.configSchema.parse(connector.config);
|
|
1228
|
+
for (const operation of Object.values(type.operations)) {
|
|
1229
|
+
descriptors.push({
|
|
1230
|
+
name: toolName(connector.name, operation.name),
|
|
1231
|
+
description: buildToolDescription(connector, operation, resolvedConfig, type),
|
|
1232
|
+
inputSchema: argsSchemaToJsonSchema(operation.argsSchema)
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
744
1235
|
}
|
|
745
|
-
return
|
|
1236
|
+
return descriptors;
|
|
746
1237
|
}
|
|
747
|
-
function
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1238
|
+
function findOperationByToolName(config, candidateToolName) {
|
|
1239
|
+
const connectors = config.connectors ?? [];
|
|
1240
|
+
for (const connector of connectors) {
|
|
1241
|
+
const type = lookupConnectorType(connector.type);
|
|
1242
|
+
if (!type) continue;
|
|
1243
|
+
for (const operation of Object.values(type.operations)) {
|
|
1244
|
+
if (toolName(connector.name, operation.name) === candidateToolName) {
|
|
1245
|
+
return { connector, operationName: operation.name, type };
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
752
1248
|
}
|
|
753
|
-
|
|
754
|
-
|
|
1249
|
+
return void 0;
|
|
1250
|
+
}
|
|
1251
|
+
function zodIssueDetails(err) {
|
|
1252
|
+
if (err instanceof z.ZodError) {
|
|
1253
|
+
return err.issues.map((issue) => ({
|
|
1254
|
+
path: issue.path,
|
|
1255
|
+
message: issue.message,
|
|
1256
|
+
code: issue.code
|
|
1257
|
+
}));
|
|
755
1258
|
}
|
|
756
|
-
return
|
|
1259
|
+
return err instanceof Error ? err.message : String(err);
|
|
757
1260
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1261
|
+
var CITADEL_HEADER = `# citadel.yaml
|
|
1262
|
+
#
|
|
1263
|
+
# This file declares the remote knowledge sources this repository pulls into
|
|
1264
|
+
# a local destination directory.
|
|
1265
|
+
#
|
|
1266
|
+
# Each source is a git repository. By default, the citadel fetches whatever
|
|
1267
|
+
# the source publishes in its own \`maester.yaml\` manifest. If a source does
|
|
1268
|
+
# not publish a manifest (or you want to override what gets pulled), declare
|
|
1269
|
+
# an \`includes\` list and the citadel will materialize exactly those paths
|
|
1270
|
+
# or globs instead.
|
|
1271
|
+
#
|
|
1272
|
+
# By default, every source is surfaced at \`<baseDir>/<source-name>/\` from the
|
|
1273
|
+
# repository root. The optional top-level \`baseDir\` field changes that parent
|
|
1274
|
+
# folder once for every source; when omitted, the default is \`citadel\`. A
|
|
1275
|
+
# per-source \`destination\` always wins over the configured base.
|
|
1276
|
+
#
|
|
1277
|
+
# Run \`maester sync\` (or \`npm run maester:sync\`) to refresh every source in
|
|
1278
|
+
# one pass. Generated by \`npx baller-maester init\` and safe to commit. Secret
|
|
1279
|
+
# values are never stored here \u2014 only the names of environment variables
|
|
1280
|
+
# that hold them.
|
|
1281
|
+
|
|
1282
|
+
`;
|
|
1283
|
+
var MAESTER_HEADER = `# maester.yaml
|
|
1284
|
+
#
|
|
1285
|
+
# This file declares the documents this repository publishes to any citadel
|
|
1286
|
+
# that pulls from it. It is a manifest only \u2014 the documents themselves live
|
|
1287
|
+
# wherever the \`path\` fields point, and \`maester\` does not modify them.
|
|
1288
|
+
#
|
|
1289
|
+
# Generated by \`npx baller-maester publish\` and safe to commit.
|
|
1290
|
+
|
|
1291
|
+
`;
|
|
1292
|
+
async function writeCitadelConfig(repoRoot, config) {
|
|
1293
|
+
const path9 = citadelConfigPath(repoRoot);
|
|
1294
|
+
const ordered = {
|
|
1295
|
+
schemaVersion: config.schemaVersion,
|
|
1296
|
+
...config.baseDir ? { baseDir: config.baseDir } : {},
|
|
1297
|
+
sources: config.sources,
|
|
1298
|
+
...config.connectors && config.connectors.length > 0 ? { connectors: config.connectors } : {}
|
|
1299
|
+
};
|
|
1300
|
+
const body = stringify(ordered, { indent: 2, lineWidth: 100, singleQuote: false });
|
|
1301
|
+
await writeFile(path9, `${CITADEL_HEADER}${body}`, "utf8");
|
|
1302
|
+
return path9;
|
|
1303
|
+
}
|
|
1304
|
+
async function writeMaesterConfig(repoRoot, config) {
|
|
1305
|
+
const path9 = maesterConfigPath(repoRoot);
|
|
1306
|
+
const body = stringify(config, { indent: 2, lineWidth: 100, singleQuote: false });
|
|
1307
|
+
await writeFile(path9, `${MAESTER_HEADER}${body}`, "utf8");
|
|
1308
|
+
return path9;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/core/init/connector-writer.ts
|
|
1312
|
+
var ConnectorNotFoundError = class extends MaesterError {
|
|
1313
|
+
connectorName;
|
|
1314
|
+
constructor(name) {
|
|
1315
|
+
super("CONNECTOR_NOT_FOUND", `No connector named '${name}' is configured in citadel.yaml.`);
|
|
1316
|
+
this.name = "ConnectorNotFoundError";
|
|
1317
|
+
this.connectorName = name;
|
|
762
1318
|
}
|
|
763
|
-
|
|
1319
|
+
};
|
|
1320
|
+
var DuplicateConnectorError = class extends MaesterError {
|
|
1321
|
+
connectorName;
|
|
1322
|
+
constructor(name) {
|
|
1323
|
+
super(
|
|
1324
|
+
"DUPLICATE_CONNECTOR",
|
|
1325
|
+
`A connector named '${name}' is already declared in citadel.yaml.`
|
|
1326
|
+
);
|
|
1327
|
+
this.name = "DuplicateConnectorError";
|
|
1328
|
+
this.connectorName = name;
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
async function addConnectorToCitadel(repoRoot, connector) {
|
|
1332
|
+
const config = await loadCitadelConfig(repoRoot);
|
|
1333
|
+
const existing = config.connectors ?? [];
|
|
1334
|
+
if (existing.some((c) => c.name === connector.name)) {
|
|
1335
|
+
throw new DuplicateConnectorError(connector.name);
|
|
1336
|
+
}
|
|
1337
|
+
const next = {
|
|
1338
|
+
...config,
|
|
1339
|
+
connectors: [...existing, connector]
|
|
1340
|
+
};
|
|
1341
|
+
const filePath = await writeCitadelConfig(repoRoot, next);
|
|
1342
|
+
return { filePath, config: next };
|
|
1343
|
+
}
|
|
1344
|
+
async function removeConnectorFromCitadel(repoRoot, name) {
|
|
1345
|
+
const config = await loadCitadelConfig(repoRoot);
|
|
1346
|
+
const existing = config.connectors ?? [];
|
|
1347
|
+
const index = existing.findIndex((c) => c.name === name);
|
|
1348
|
+
if (index === -1) {
|
|
1349
|
+
throw new ConnectorNotFoundError(name);
|
|
1350
|
+
}
|
|
1351
|
+
const next = {
|
|
1352
|
+
...config,
|
|
1353
|
+
connectors: existing.filter((_, i) => i !== index)
|
|
1354
|
+
};
|
|
1355
|
+
const filePath = await writeCitadelConfig(repoRoot, next);
|
|
1356
|
+
return { filePath, config: next };
|
|
1357
|
+
}
|
|
1358
|
+
async function listConnectorsFromCitadel(repoRoot) {
|
|
1359
|
+
const config = await loadCitadelConfig(repoRoot);
|
|
1360
|
+
return [...config.connectors ?? []];
|
|
764
1361
|
}
|
|
765
1362
|
|
|
766
1363
|
// src/core/skill/managed-region.ts
|
|
@@ -804,7 +1401,7 @@ function renderManagedRegion(body, version) {
|
|
|
804
1401
|
${inner}
|
|
805
1402
|
${END_MARKER_LITERAL}`;
|
|
806
1403
|
}
|
|
807
|
-
function replaceJsonMaesterKey(existingText,
|
|
1404
|
+
function replaceJsonMaesterKey(existingText, maesterBlock2) {
|
|
808
1405
|
const parsed = existingText && existingText.trim().length > 0 ? JSON.parse(existingText) : {};
|
|
809
1406
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
810
1407
|
throw new Error("Expected .claude/settings.json to be a JSON object at the top level.");
|
|
@@ -813,14 +1410,14 @@ function replaceJsonMaesterKey(existingText, maesterBlock) {
|
|
|
813
1410
|
let placed = false;
|
|
814
1411
|
for (const [key, value] of Object.entries(parsed)) {
|
|
815
1412
|
if (key === "maester") {
|
|
816
|
-
rebuilt[key] =
|
|
1413
|
+
rebuilt[key] = maesterBlock2;
|
|
817
1414
|
placed = true;
|
|
818
1415
|
} else {
|
|
819
1416
|
rebuilt[key] = value;
|
|
820
1417
|
}
|
|
821
1418
|
}
|
|
822
1419
|
if (!placed) {
|
|
823
|
-
rebuilt.maester =
|
|
1420
|
+
rebuilt.maester = maesterBlock2;
|
|
824
1421
|
}
|
|
825
1422
|
return `${JSON.stringify(rebuilt, null, 2)}
|
|
826
1423
|
`;
|
|
@@ -842,8 +1439,11 @@ function readJsonMaesterKey(existingText) {
|
|
|
842
1439
|
// src/core/skill/templates/content/citadel-awareness.md
|
|
843
1440
|
var citadel_awareness_default = '## Citadel awareness\n\nThis repository is a **citadel** \u2014 it pulls curated documentation from multiple\nremote sources into a single tree, managed by the `maester` CLI.\n\n- The citadel\'s aggregated content lives under `{{baseDir}}/` at the repository\n root. Each direct subdirectory of `{{baseDir}}/` corresponds to one remote\n source declared in `citadel.yaml` (`{{baseDir}}/<source-name>/...`).\n- The configuration that declares those sources is in `citadel.yaml` at the\n repository root. It names each source, the git remote it pulls from, and the\n ref it pins to.\n- When answering questions about anything the citadel covers, prefer citing\n files under `{{baseDir}}/` over external knowledge. Cite the file path\n relative to the repository root so the user can open it.\n- Each materialized file may carry a `state` value in its frontmatter or\n inline (see "State awareness" below). Surface that state alongside any\n citation so the user knows whether the source is canonical or draft.\n';
|
|
844
1441
|
|
|
1442
|
+
// src/core/skill/templates/content/connector-policy.md
|
|
1443
|
+
var connector_policy_default = "## Connector tools (traveling maesters)\n\nThis citadel may expose one or more **traveling maesters** as MCP tools whose\nnames begin with the connector slug (e.g. `team_gl__list_issues`).\n\n- Their output is **live, point-in-time data** from an external service. Cite\n specific identifiers (issue iids, ticket numbers) when surfacing it, do not\n treat it as a stable corpus, and flag the **freshness verdict** in your\n answer when it is not `up-to-date`.\n- The tools' arguments and return shapes are described in MCP `tools/list`;\n do not assume undocumented fields.\n- Connector tool results are JSON envelopes carrying a `dataSchema` version\n alongside the payload \u2014 if your reading of the data depends on a specific\n shape, branch on `dataSchema`.\n";
|
|
1444
|
+
|
|
845
1445
|
// src/core/skill/templates/content/freshness-awareness.md
|
|
846
|
-
var freshness_awareness_default = '## Freshness awareness\n\nCitadel content can drift out of date when remote sources advance after the\nlast sync. The Grand Maester is **auto-sync directed**: when the citadel is\nbehind, refresh it before reading rather than relying on stale content.\n\n**Auto-sync policy**\n\nWhen you discover that the citadel is behind \u2014 either from the Claude Code\npre-read hook\'s `additionalContext` message, or from a manual run of\n`npx maester status` \u2014 follow these steps without prompting the user:\n\n1. Run `npx maester sync` to refresh every configured source.\n - Exits 0 on success.\n - Exits non-zero if any source failed (missing auth env var, ref typo,\n network error). Report the failure to the user and proceed cautiously.\n2. **Invalidate the pre-read hook\'s debounce cache** so the next read\n observes fresh state instead of the stale verdict captured before the\n sync:\n ```\n rm -f .maester/.skill-cache.json\n ```\n3. Proceed with the original read of the citadel file.\n\nYou do not need to ask the user before syncing. `maester sync` is read-only\nagainst the remote sources it pulls from; it only materializes content the\n`citadel.yaml` already declared. The same sync the user would run manually.\n\n**Don\'t loop on failures.** If `maester sync` fails (or the hook reports a\n`failed` verdict from `maester status`), do **not** retry sync repeatedly.\nSurface the failure to the user, proceed with the read, and flag that cited\ncontent may be stale.\n\n**Avoid redundant syncs within a session.** Once you have synced and\ninvalidated the cache, ignore any further "citadel is behind" messages that\narrive before you have done another citadel read \u2014 they are cached signals\ncaptured before your sync completed.\n\n**Manual status check**\n\n```\nnpx maester status\n```\n\nExit codes:\n\n- **`0`** \u2014 every source is up to date.\n- **`1`** \u2014 at least one source is behind (remote advanced, manifest\n changed, or never-synced). Run the auto-sync policy above.\n- **`2`** \u2014 the status check itself failed. Surface to the user; proceed\n with a caveat that staleness cannot be verified.\n\nFor machine-readable output, pass `--json` and parse the NDJSON stream on\nstdout. The final line contains `{ "type": "summary", "upToDate": N,\n"behind": N, "failed": N }`.\n\n**On Claude Code specifically**, a `PreToolUse` hook installed by\n`maester skill install` runs the status check automatically before any\n`Read`, `Glob`, or `Grep` targeting a path under `{{baseDir}}/`. The\nhook debounces (default 300s, override with `MAESTER_SKILL_STATUS_TTL`) so\nthe check does not run more than once per session for routine reads.\n';
|
|
1446
|
+
var freshness_awareness_default = '## Freshness awareness\n\nCitadel content can drift out of date when remote sources advance after the\nlast sync. The Grand Maester is **auto-sync directed**: when the citadel is\nbehind, refresh it before reading rather than relying on stale content.\n\n**Auto-sync policy**\n\nWhen you discover that the citadel is behind \u2014 either from the Claude Code\npre-read hook\'s `additionalContext` message, or from a manual run of\n`npx baller-maester status` \u2014 follow these steps without prompting the user:\n\n1. Run `npx baller-maester sync` to refresh every configured source.\n - Exits 0 on success.\n - Exits non-zero if any source failed (missing auth env var, ref typo,\n network error). Report the failure to the user and proceed cautiously.\n2. **Invalidate the pre-read hook\'s debounce cache** so the next read\n observes fresh state instead of the stale verdict captured before the\n sync:\n ```\n rm -f .maester/.skill-cache.json\n ```\n3. Proceed with the original read of the citadel file.\n\nYou do not need to ask the user before syncing. `maester sync` is read-only\nagainst the remote sources it pulls from; it only materializes content the\n`citadel.yaml` already declared. The same sync the user would run manually.\n\n**Don\'t loop on failures.** If `maester sync` fails (or the hook reports a\n`failed` verdict from `maester status`), do **not** retry sync repeatedly.\nSurface the failure to the user, proceed with the read, and flag that cited\ncontent may be stale.\n\n**Avoid redundant syncs within a session.** Once you have synced and\ninvalidated the cache, ignore any further "citadel is behind" messages that\narrive before you have done another citadel read \u2014 they are cached signals\ncaptured before your sync completed.\n\n**Manual status check**\n\n```\nnpx baller-maester status\n```\n\nExit codes:\n\n- **`0`** \u2014 every source is up to date.\n- **`1`** \u2014 at least one source is behind (remote advanced, manifest\n changed, or never-synced). Run the auto-sync policy above.\n- **`2`** \u2014 the status check itself failed. Surface to the user; proceed\n with a caveat that staleness cannot be verified.\n\nFor machine-readable output, pass `--json` and parse the NDJSON stream on\nstdout. The final line contains `{ "type": "summary", "upToDate": N,\n"behind": N, "failed": N }`.\n\n**On Claude Code specifically**, a `PreToolUse` hook installed by\n`maester skill install` runs the status check automatically before any\n`Read`, `Glob`, or `Grep` targeting a path under `{{baseDir}}/`. The\nhook debounces (default 300s, override with `MAESTER_SKILL_STATUS_TTL`) so\nthe check does not run more than once per session for routine reads.\n';
|
|
847
1447
|
|
|
848
1448
|
// src/core/skill/templates/content/state-awareness.md
|
|
849
1449
|
var state_awareness_default = '## State awareness (canon vs draft)\n\nEvery citadel file may declare a publication state of `canon` (authoritative)\nor `draft` (work-in-progress). The state lives **inline** in the file using\nthe format\'s native convention:\n\n- **Markdown / MDX (`.md`, `.mdx`)** \u2014 `state` field inside YAML frontmatter\n at the top of the file:\n ```\n ---\n state: canon\n ---\n ```\n- **HTML (`.html`, `.htm`)** \u2014 first-line HTML comment:\n `<!-- state: canon -->`\n- **YAML / JSON (`.yaml`, `.yml`, `.json`)** \u2014 a top-level `state` key.\n- **Plain text (`.txt`)** \u2014 `state: canon` as the very first line.\n\nFiles without inline state default to `draft`.\n\n**Policy when answering from the citadel:**\n\n1. **Prefer `canon` files** as the authoritative source of truth. When a\n `canon` file answers the question, cite it and stop there.\n2. **`draft` files are informational only.** Cite them when no `canon`\n alternative exists, but mark the citation explicitly: "(draft \u2014 work in\n progress)" alongside the file path so the user knows the source is not yet\n stable.\n3. **Never mix the two without labeling.** If you draw from both canon and\n draft files in one answer, separate the two and tell the user which fact\n came from which kind of source.\n';
|
|
@@ -861,7 +1461,9 @@ function renderClaudeSkillBody(opts) {
|
|
|
861
1461
|
"",
|
|
862
1462
|
interpolate(state_awareness_default, opts),
|
|
863
1463
|
"",
|
|
864
|
-
interpolate(freshness_awareness_default, opts)
|
|
1464
|
+
interpolate(freshness_awareness_default, opts),
|
|
1465
|
+
"",
|
|
1466
|
+
interpolate(connector_policy_default, opts)
|
|
865
1467
|
].join("\n");
|
|
866
1468
|
}
|
|
867
1469
|
function renderClaudeSkillFile(body) {
|
|
@@ -881,7 +1483,7 @@ function buildClaudeMaesterBlock(version) {
|
|
|
881
1483
|
PreToolUse: [
|
|
882
1484
|
{
|
|
883
1485
|
matcher: "Read|Glob|Grep",
|
|
884
|
-
hooks: [{ type: "command", command: "npx maester skill runtime preread" }]
|
|
1486
|
+
hooks: [{ type: "command", command: "npx -y baller-maester skill runtime preread" }]
|
|
885
1487
|
}
|
|
886
1488
|
]
|
|
887
1489
|
}
|
|
@@ -912,8 +1514,8 @@ async function writeClaudeCode(input) {
|
|
|
912
1514
|
};
|
|
913
1515
|
}
|
|
914
1516
|
async function writeSkillMd(input) {
|
|
915
|
-
const filePath =
|
|
916
|
-
await promises.mkdir(
|
|
1517
|
+
const filePath = path8.join(input.repoRoot, SKILL_MD_PATH);
|
|
1518
|
+
await promises.mkdir(path8.dirname(filePath), { recursive: true });
|
|
917
1519
|
const existing = await readTextOrUndefined(filePath);
|
|
918
1520
|
const previousVersion = existing ? extractMarkdownRegion(existing)?.version : void 0;
|
|
919
1521
|
const body = renderClaudeSkillBody({ baseDir: input.citadelBaseDir });
|
|
@@ -927,8 +1529,8 @@ async function writeSkillMd(input) {
|
|
|
927
1529
|
return { action: "upgraded" };
|
|
928
1530
|
}
|
|
929
1531
|
async function writeSettingsJson(input) {
|
|
930
|
-
const filePath =
|
|
931
|
-
await promises.mkdir(
|
|
1532
|
+
const filePath = path8.join(input.repoRoot, SETTINGS_JSON_PATH);
|
|
1533
|
+
await promises.mkdir(path8.dirname(filePath), { recursive: true });
|
|
932
1534
|
const existing = await readTextOrUndefined(filePath);
|
|
933
1535
|
const previousBlock = readJsonMaesterKey(existing);
|
|
934
1536
|
const previousVersion = typeof previousBlock?.version === "string" ? previousBlock.version : void 0;
|
|
@@ -941,7 +1543,7 @@ async function writeSettingsJson(input) {
|
|
|
941
1543
|
return { action: "upgraded" };
|
|
942
1544
|
}
|
|
943
1545
|
async function readInstalledVersion(repoRoot) {
|
|
944
|
-
const skillPath =
|
|
1546
|
+
const skillPath = path8.join(repoRoot, SKILL_MD_PATH);
|
|
945
1547
|
const text2 = await readTextOrUndefined(skillPath);
|
|
946
1548
|
if (!text2) return void 0;
|
|
947
1549
|
return extractMarkdownRegion(text2)?.version;
|
|
@@ -961,53 +1563,66 @@ function combineActions(a, b) {
|
|
|
961
1563
|
return "unchanged";
|
|
962
1564
|
}
|
|
963
1565
|
|
|
964
|
-
// src/core/skill/templates/shells/
|
|
965
|
-
var
|
|
966
|
-
|
|
967
|
-
This file contains agent instructions for working in this repository. The
|
|
968
|
-
section between the maester managed-region markers is generated by
|
|
969
|
-
\`maester skill install\` and refreshed by \`maester skill upgrade\`. Anything
|
|
970
|
-
you write outside that region is preserved across upgrades.
|
|
971
|
-
`;
|
|
972
|
-
function renderAgentsMdBody(opts) {
|
|
1566
|
+
// src/core/skill/templates/shells/codex.ts
|
|
1567
|
+
var SKILL_FRONTMATTER_DESCRIPTION2 = "Citadel-aware guidance for reading aggregated documentation under the citadel base directory. Prefers canon files over draft and runs maester status before substantial citadel reads.";
|
|
1568
|
+
function renderCodexSkillBody(opts) {
|
|
973
1569
|
return [
|
|
974
|
-
"# Grand Maester
|
|
1570
|
+
"# Grand Maester (Codex CLI skill)",
|
|
975
1571
|
"",
|
|
976
|
-
"
|
|
977
|
-
|
|
978
|
-
"guidance below.",
|
|
1572
|
+
"Use this guidance whenever you read files under the citadel base directory",
|
|
1573
|
+
`(\`${opts.baseDir}/\`) in this repository.`,
|
|
979
1574
|
"",
|
|
980
1575
|
interpolate2(citadel_awareness_default, opts),
|
|
981
1576
|
"",
|
|
982
1577
|
interpolate2(state_awareness_default, opts),
|
|
983
1578
|
"",
|
|
984
|
-
interpolate2(freshness_awareness_default, opts)
|
|
1579
|
+
interpolate2(freshness_awareness_default, opts),
|
|
1580
|
+
"",
|
|
1581
|
+
interpolate2(connector_policy_default, opts)
|
|
985
1582
|
].join("\n");
|
|
986
1583
|
}
|
|
987
|
-
function
|
|
988
|
-
return
|
|
1584
|
+
function renderCodexSkillFile(body) {
|
|
1585
|
+
return [
|
|
1586
|
+
"---",
|
|
1587
|
+
"name: grand-maester",
|
|
1588
|
+
`description: ${SKILL_FRONTMATTER_DESCRIPTION2}`,
|
|
1589
|
+
"---",
|
|
1590
|
+
"",
|
|
1591
|
+
body
|
|
1592
|
+
].join("\n");
|
|
989
1593
|
}
|
|
990
1594
|
function interpolate2(template, opts) {
|
|
991
1595
|
return template.replace(/\{\{baseDir\}\}/g, opts.baseDir);
|
|
992
1596
|
}
|
|
993
1597
|
|
|
994
|
-
// src/core/skill/targets/
|
|
995
|
-
var
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1598
|
+
// src/core/skill/targets/codex.ts
|
|
1599
|
+
var SKILL_MD_PATH2 = ".agents/skills/grand-maester/SKILL.md";
|
|
1600
|
+
var codexTarget = {
|
|
1601
|
+
id: "codex",
|
|
1602
|
+
label: "Codex CLI",
|
|
1603
|
+
artifactPaths: [SKILL_MD_PATH2],
|
|
1604
|
+
writerKey: "codex",
|
|
1605
|
+
write: writeCodex,
|
|
1606
|
+
readInstalledVersion: readInstalledVersion2
|
|
1607
|
+
};
|
|
1608
|
+
async function writeCodex(input) {
|
|
1609
|
+
const filePath = path8.join(input.repoRoot, SKILL_MD_PATH2);
|
|
1610
|
+
await promises.mkdir(path8.dirname(filePath), { recursive: true });
|
|
1611
|
+
const existing = await readTextOrUndefined2(filePath);
|
|
1612
|
+
const previousVersion = existing ? extractMarkdownRegion(existing)?.version : void 0;
|
|
1613
|
+
const body = renderCodexSkillBody({ baseDir: input.citadelBaseDir });
|
|
1614
|
+
const managedRegion = replaceMarkdownRegion(void 0, body, input.skillVersion).trimEnd();
|
|
1615
|
+
const fileContent = existing ? replaceMarkdownRegion(existing, body, input.skillVersion) : `${renderCodexSkillFile(managedRegion)}
|
|
1616
|
+
`;
|
|
1617
|
+
const action = decideAction(existing, previousVersion, input.skillVersion, fileContent);
|
|
1003
1618
|
if (action === "unchanged") {
|
|
1004
1619
|
return previousVersion !== void 0 ? { action, installedVersion: previousVersion } : { action };
|
|
1005
1620
|
}
|
|
1006
|
-
await promises.writeFile(filePath,
|
|
1621
|
+
await promises.writeFile(filePath, fileContent, "utf8");
|
|
1007
1622
|
return { action, installedVersion: input.skillVersion };
|
|
1008
1623
|
}
|
|
1009
|
-
async function
|
|
1010
|
-
const filePath =
|
|
1624
|
+
async function readInstalledVersion2(repoRoot) {
|
|
1625
|
+
const filePath = path8.join(repoRoot, SKILL_MD_PATH2);
|
|
1011
1626
|
const text2 = await readTextOrUndefined2(filePath);
|
|
1012
1627
|
if (!text2) return void 0;
|
|
1013
1628
|
return extractMarkdownRegion(text2)?.version;
|
|
@@ -1028,16 +1643,6 @@ function decideAction(existing, previousVersion, newVersion, newContent) {
|
|
|
1028
1643
|
return "upgraded";
|
|
1029
1644
|
}
|
|
1030
1645
|
|
|
1031
|
-
// src/core/skill/targets/codex.ts
|
|
1032
|
-
var codexTarget = {
|
|
1033
|
-
id: "codex",
|
|
1034
|
-
label: "Codex CLI",
|
|
1035
|
-
artifactPaths: [AGENTS_MD_ARTIFACT_PATH],
|
|
1036
|
-
writerKey: "agents-md",
|
|
1037
|
-
write: writeAgentsMd,
|
|
1038
|
-
readInstalledVersion: readAgentsMdInstalledVersion
|
|
1039
|
-
};
|
|
1040
|
-
|
|
1041
1646
|
// src/core/skill/templates/shells/cursor.ts
|
|
1042
1647
|
var DESCRIPTION = "Citadel-aware guidance for reading aggregated documentation under the citadel base directory.";
|
|
1043
1648
|
function renderCursorRuleBody(opts) {
|
|
@@ -1051,7 +1656,9 @@ function renderCursorRuleBody(opts) {
|
|
|
1051
1656
|
"",
|
|
1052
1657
|
interpolate3(state_awareness_default, opts),
|
|
1053
1658
|
"",
|
|
1054
|
-
interpolate3(freshness_awareness_default, opts)
|
|
1659
|
+
interpolate3(freshness_awareness_default, opts),
|
|
1660
|
+
"",
|
|
1661
|
+
interpolate3(connector_policy_default, opts)
|
|
1055
1662
|
].join("\n");
|
|
1056
1663
|
}
|
|
1057
1664
|
function renderCursorRuleFile(body, opts) {
|
|
@@ -1077,11 +1684,11 @@ var cursorTarget = {
|
|
|
1077
1684
|
artifactPaths: [CURSOR_RULE_PATH],
|
|
1078
1685
|
writerKey: "cursor",
|
|
1079
1686
|
write: writeCursor,
|
|
1080
|
-
readInstalledVersion:
|
|
1687
|
+
readInstalledVersion: readInstalledVersion3
|
|
1081
1688
|
};
|
|
1082
1689
|
async function writeCursor(input) {
|
|
1083
|
-
const filePath =
|
|
1084
|
-
await promises.mkdir(
|
|
1690
|
+
const filePath = path8.join(input.repoRoot, CURSOR_RULE_PATH);
|
|
1691
|
+
await promises.mkdir(path8.dirname(filePath), { recursive: true });
|
|
1085
1692
|
const existing = await readTextOrUndefined3(filePath);
|
|
1086
1693
|
const previousVersion = existing ? extractMarkdownRegion(existing)?.version : void 0;
|
|
1087
1694
|
const body = renderCursorRuleBody({ baseDir: input.citadelBaseDir });
|
|
@@ -1099,86 +1706,829 @@ async function writeCursor(input) {
|
|
|
1099
1706
|
await promises.writeFile(filePath, next, "utf8");
|
|
1100
1707
|
return { action, installedVersion: input.skillVersion };
|
|
1101
1708
|
}
|
|
1102
|
-
async function
|
|
1103
|
-
const filePath =
|
|
1709
|
+
async function readInstalledVersion3(repoRoot) {
|
|
1710
|
+
const filePath = path8.join(repoRoot, CURSOR_RULE_PATH);
|
|
1104
1711
|
const text2 = await readTextOrUndefined3(filePath);
|
|
1105
1712
|
if (!text2) return void 0;
|
|
1106
1713
|
return extractMarkdownRegion(text2)?.version;
|
|
1107
1714
|
}
|
|
1108
|
-
async function readTextOrUndefined3(filePath) {
|
|
1109
|
-
try {
|
|
1110
|
-
return await promises.readFile(filePath, "utf8");
|
|
1111
|
-
} catch (err) {
|
|
1112
|
-
if (err.code === "ENOENT") return void 0;
|
|
1113
|
-
throw err;
|
|
1715
|
+
async function readTextOrUndefined3(filePath) {
|
|
1716
|
+
try {
|
|
1717
|
+
return await promises.readFile(filePath, "utf8");
|
|
1718
|
+
} catch (err) {
|
|
1719
|
+
if (err.code === "ENOENT") return void 0;
|
|
1720
|
+
throw err;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
function decideAction2(existing, previousVersion, newVersion, newContent) {
|
|
1724
|
+
if (existing === void 0) return "installed";
|
|
1725
|
+
if (existing === newContent) return "unchanged";
|
|
1726
|
+
if (previousVersion === void 0) return "installed";
|
|
1727
|
+
if (previousVersion !== newVersion) return "upgraded";
|
|
1728
|
+
return "upgraded";
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/core/skill/templates/content/connector-policy-fallback.md
|
|
1732
|
+
var connector_policy_fallback_default = "## Connector tools (traveling maesters)\n\nThis citadel may expose one or more **traveling maesters** as connectors. Your\nagent platform does not speak MCP, so connector operations are reached via the\nfallback CLI:\n\n```\nnpx baller-maester connector list\nnpx baller-maester connector exec <connector-name> <operation> [--key value]...\n```\n\n- `connector list` prints the configured connectors and the operations they\n expose.\n- `connector exec` invokes an operation and writes a JSON envelope to stdout.\n Exit code `0` is success, `1` is a connector-level failure (auth, remote\n error, invalid args), `2` is an invocation-level error (no such connector,\n no citadel.yaml).\n\nTreat the data the same way as MCP tool output: live, point-in-time, cite\nspecific identifiers, flag freshness when it isn't `up-to-date`, and don't\nassume undocumented fields.\n";
|
|
1733
|
+
|
|
1734
|
+
// src/core/skill/templates/shells/agents-md.ts
|
|
1735
|
+
var PREAMBLE = `# AGENTS.md
|
|
1736
|
+
|
|
1737
|
+
This file contains agent instructions for working in this repository. The
|
|
1738
|
+
section between the maester managed-region markers is generated by
|
|
1739
|
+
\`maester skill install\` and refreshed by \`maester skill upgrade\`. Anything
|
|
1740
|
+
you write outside that region is preserved across upgrades.
|
|
1741
|
+
`;
|
|
1742
|
+
function renderAgentsMdBody(opts) {
|
|
1743
|
+
return [
|
|
1744
|
+
"# Grand Maester guidance",
|
|
1745
|
+
"",
|
|
1746
|
+
"This repository is set up to aggregate documentation from remote sources",
|
|
1747
|
+
"into a local citadel. When you reason about citadel content, follow the",
|
|
1748
|
+
"guidance below.",
|
|
1749
|
+
"",
|
|
1750
|
+
interpolate4(citadel_awareness_default, opts),
|
|
1751
|
+
"",
|
|
1752
|
+
interpolate4(state_awareness_default, opts),
|
|
1753
|
+
"",
|
|
1754
|
+
interpolate4(freshness_awareness_default, opts),
|
|
1755
|
+
"",
|
|
1756
|
+
interpolate4(connector_policy_fallback_default, opts)
|
|
1757
|
+
].join("\n");
|
|
1758
|
+
}
|
|
1759
|
+
function agentsMdPreamble() {
|
|
1760
|
+
return PREAMBLE;
|
|
1761
|
+
}
|
|
1762
|
+
function interpolate4(template, opts) {
|
|
1763
|
+
return template.replace(/\{\{baseDir\}\}/g, opts.baseDir);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// src/core/skill/targets/agents-md-writer.ts
|
|
1767
|
+
var AGENTS_MD_ARTIFACT_PATH = "AGENTS.md";
|
|
1768
|
+
async function writeAgentsMd(input) {
|
|
1769
|
+
const filePath = path8.join(input.repoRoot, AGENTS_MD_ARTIFACT_PATH);
|
|
1770
|
+
const existingText = await readTextOrUndefined4(filePath);
|
|
1771
|
+
const previousVersion = existingText ? extractMarkdownRegion(existingText)?.version : void 0;
|
|
1772
|
+
const body = renderAgentsMdBody({ baseDir: input.citadelBaseDir });
|
|
1773
|
+
const next = replaceMarkdownRegion(existingText, body, input.skillVersion, agentsMdPreamble());
|
|
1774
|
+
const action = decideAction3(existingText, previousVersion, input.skillVersion, next);
|
|
1775
|
+
if (action === "unchanged") {
|
|
1776
|
+
return previousVersion !== void 0 ? { action, installedVersion: previousVersion } : { action };
|
|
1777
|
+
}
|
|
1778
|
+
await promises.writeFile(filePath, next, "utf8");
|
|
1779
|
+
return { action, installedVersion: input.skillVersion };
|
|
1780
|
+
}
|
|
1781
|
+
async function readAgentsMdInstalledVersion(repoRoot) {
|
|
1782
|
+
const filePath = path8.join(repoRoot, AGENTS_MD_ARTIFACT_PATH);
|
|
1783
|
+
const text2 = await readTextOrUndefined4(filePath);
|
|
1784
|
+
if (!text2) return void 0;
|
|
1785
|
+
return extractMarkdownRegion(text2)?.version;
|
|
1786
|
+
}
|
|
1787
|
+
async function readTextOrUndefined4(filePath) {
|
|
1788
|
+
try {
|
|
1789
|
+
return await promises.readFile(filePath, "utf8");
|
|
1790
|
+
} catch (err) {
|
|
1791
|
+
if (err.code === "ENOENT") return void 0;
|
|
1792
|
+
throw err;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
function decideAction3(existing, previousVersion, newVersion, newContent) {
|
|
1796
|
+
if (existing === void 0) return "installed";
|
|
1797
|
+
if (existing === newContent) return "unchanged";
|
|
1798
|
+
if (previousVersion === void 0) return "installed";
|
|
1799
|
+
if (previousVersion !== newVersion) return "upgraded";
|
|
1800
|
+
return "upgraded";
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// src/core/skill/targets/generic.ts
|
|
1804
|
+
var genericTarget = {
|
|
1805
|
+
id: "agents-md",
|
|
1806
|
+
label: "Generic AGENTS.md",
|
|
1807
|
+
artifactPaths: [AGENTS_MD_ARTIFACT_PATH],
|
|
1808
|
+
writerKey: "agents-md",
|
|
1809
|
+
write: writeAgentsMd,
|
|
1810
|
+
readInstalledVersion: readAgentsMdInstalledVersion
|
|
1811
|
+
};
|
|
1812
|
+
|
|
1813
|
+
// src/core/skill/targets/index.ts
|
|
1814
|
+
var REGISTRY2 = [
|
|
1815
|
+
claudeCodeTarget,
|
|
1816
|
+
codexTarget,
|
|
1817
|
+
cursorTarget,
|
|
1818
|
+
genericTarget
|
|
1819
|
+
];
|
|
1820
|
+
function listSkillTargets() {
|
|
1821
|
+
return REGISTRY2;
|
|
1822
|
+
}
|
|
1823
|
+
function getTarget(id) {
|
|
1824
|
+
const found = REGISTRY2.find((t) => t.id === id);
|
|
1825
|
+
if (!found) {
|
|
1826
|
+
throw new Error(
|
|
1827
|
+
`Unknown skill target '${id}'. Supported: ${REGISTRY2.map((t) => t.id).join(", ")}`
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
return found;
|
|
1831
|
+
}
|
|
1832
|
+
function dedupeTargets(targets) {
|
|
1833
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1834
|
+
for (const target of targets) {
|
|
1835
|
+
const existing = groups.get(target.writerKey);
|
|
1836
|
+
if (existing) {
|
|
1837
|
+
existing.ids.push(target.id);
|
|
1838
|
+
existing.labels.push(target.label);
|
|
1839
|
+
} else {
|
|
1840
|
+
groups.set(target.writerKey, {
|
|
1841
|
+
writerKey: target.writerKey,
|
|
1842
|
+
primary: target,
|
|
1843
|
+
ids: [target.id],
|
|
1844
|
+
labels: [target.label],
|
|
1845
|
+
artifactPaths: target.artifactPaths
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return [...groups.values()];
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// src/core/mcp/registrations/command.ts
|
|
1853
|
+
function resolveMaesterLaunchCommand() {
|
|
1854
|
+
return { command: "npx", args: ["-y", "baller-maester", "mcp"] };
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// src/core/mcp/registrations/claude-code.ts
|
|
1858
|
+
var MCP_FILE = ".mcp.json";
|
|
1859
|
+
function maesterEntry(launch) {
|
|
1860
|
+
return { command: launch.command, args: [...launch.args] };
|
|
1861
|
+
}
|
|
1862
|
+
async function writeClaudeCodeMcpEntry(repoRoot, options = {}) {
|
|
1863
|
+
const launch = options.launch ?? resolveMaesterLaunchCommand();
|
|
1864
|
+
return writeJsonMcpFile(path8.join(repoRoot, MCP_FILE), launch);
|
|
1865
|
+
}
|
|
1866
|
+
async function writeJsonMcpFile(filePath, launch) {
|
|
1867
|
+
await promises.mkdir(path8.dirname(filePath), { recursive: true });
|
|
1868
|
+
const existingText = await readOrUndefined(filePath);
|
|
1869
|
+
const newText = renderJsonWithMaesterEntry(existingText, launch);
|
|
1870
|
+
if (existingText === newText) {
|
|
1871
|
+
return { filePath, action: "unchanged" };
|
|
1872
|
+
}
|
|
1873
|
+
await promises.writeFile(filePath, newText, "utf8");
|
|
1874
|
+
return { filePath, action: "written" };
|
|
1875
|
+
}
|
|
1876
|
+
function renderJsonWithMaesterEntry(existingText, launch) {
|
|
1877
|
+
const parsed = parseOrEmpty(existingText);
|
|
1878
|
+
const rebuilt = {};
|
|
1879
|
+
let placed = false;
|
|
1880
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1881
|
+
if (key === "mcpServers") {
|
|
1882
|
+
rebuilt[key] = mutateMcpServers(value, launch);
|
|
1883
|
+
placed = true;
|
|
1884
|
+
} else {
|
|
1885
|
+
rebuilt[key] = value;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
if (!placed) {
|
|
1889
|
+
rebuilt.mcpServers = mutateMcpServers(void 0, launch);
|
|
1890
|
+
}
|
|
1891
|
+
return `${JSON.stringify(rebuilt, null, 2)}
|
|
1892
|
+
`;
|
|
1893
|
+
}
|
|
1894
|
+
function mutateMcpServers(existing, launch) {
|
|
1895
|
+
const map = isPlainObject(existing) ? { ...existing } : {};
|
|
1896
|
+
const rebuilt = {};
|
|
1897
|
+
let placed = false;
|
|
1898
|
+
for (const [key, value] of Object.entries(map)) {
|
|
1899
|
+
if (key === "maester") {
|
|
1900
|
+
rebuilt[key] = maesterEntry(launch);
|
|
1901
|
+
placed = true;
|
|
1902
|
+
} else {
|
|
1903
|
+
rebuilt[key] = value;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
if (!placed) {
|
|
1907
|
+
rebuilt.maester = maesterEntry(launch);
|
|
1908
|
+
}
|
|
1909
|
+
return rebuilt;
|
|
1910
|
+
}
|
|
1911
|
+
function parseOrEmpty(text2) {
|
|
1912
|
+
if (!text2 || text2.trim().length === 0) return {};
|
|
1913
|
+
const parsed = JSON.parse(text2);
|
|
1914
|
+
if (!isPlainObject(parsed)) {
|
|
1915
|
+
throw new Error("Expected MCP config to be a JSON object at the top level.");
|
|
1916
|
+
}
|
|
1917
|
+
return parsed;
|
|
1918
|
+
}
|
|
1919
|
+
function isPlainObject(value) {
|
|
1920
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1921
|
+
}
|
|
1922
|
+
async function readOrUndefined(filePath) {
|
|
1923
|
+
try {
|
|
1924
|
+
return await promises.readFile(filePath, "utf8");
|
|
1925
|
+
} catch (err) {
|
|
1926
|
+
if (err.code === "ENOENT") return void 0;
|
|
1927
|
+
throw err;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
var CONFIG_FILE = path8.join(".codex", "config.toml");
|
|
1931
|
+
function maesterBlock(repoRoot, launch) {
|
|
1932
|
+
return { command: launch.command, args: [...launch.args], cwd: repoRoot };
|
|
1933
|
+
}
|
|
1934
|
+
async function writeCodexMcpEntry(repoRoot, options = {}) {
|
|
1935
|
+
const filePath = path8.join(repoRoot, CONFIG_FILE);
|
|
1936
|
+
await promises.mkdir(path8.dirname(filePath), { recursive: true });
|
|
1937
|
+
const existingText = await readOrUndefined2(filePath);
|
|
1938
|
+
const launch = options.launch ?? resolveMaesterLaunchCommand();
|
|
1939
|
+
const newText = renderTomlWithMaesterBlock(existingText, repoRoot, launch);
|
|
1940
|
+
if (existingText === newText) {
|
|
1941
|
+
return { filePath, action: "unchanged" };
|
|
1942
|
+
}
|
|
1943
|
+
await promises.writeFile(filePath, newText, "utf8");
|
|
1944
|
+
return { filePath, action: "written" };
|
|
1945
|
+
}
|
|
1946
|
+
function renderTomlWithMaesterBlock(existingText, repoRoot, launch) {
|
|
1947
|
+
const parsed = existingText && existingText.trim().length > 0 ? TOML.parse(existingText) : {};
|
|
1948
|
+
const mcpServers = isJsonMap(parsed.mcp_servers) ? { ...parsed.mcp_servers } : {};
|
|
1949
|
+
mcpServers.maester = maesterBlock(repoRoot, launch);
|
|
1950
|
+
const next = { ...parsed, mcp_servers: mcpServers };
|
|
1951
|
+
return TOML.stringify(next);
|
|
1952
|
+
}
|
|
1953
|
+
function isJsonMap(value) {
|
|
1954
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1955
|
+
}
|
|
1956
|
+
async function readOrUndefined2(filePath) {
|
|
1957
|
+
try {
|
|
1958
|
+
return await promises.readFile(filePath, "utf8");
|
|
1959
|
+
} catch (err) {
|
|
1960
|
+
if (err.code === "ENOENT") return void 0;
|
|
1961
|
+
throw err;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
var MCP_FILE2 = path8.join(".cursor", "mcp.json");
|
|
1965
|
+
async function writeCursorMcpEntry(repoRoot, options = {}) {
|
|
1966
|
+
const launch = options.launch ?? resolveMaesterLaunchCommand();
|
|
1967
|
+
return writeJsonMcpFile(path8.join(repoRoot, MCP_FILE2), launch);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/core/mcp/registrations/index.ts
|
|
1971
|
+
async function refreshMcpRegistrations(repoRoot, options = {}) {
|
|
1972
|
+
const targets = listSkillTargets().filter(
|
|
1973
|
+
(t) => isMcpHost(t.id) && (!options.scopeTo || options.scopeTo.includes(t.id))
|
|
1974
|
+
);
|
|
1975
|
+
const outcomes = [];
|
|
1976
|
+
for (const target of targets) {
|
|
1977
|
+
const installedVersion = await target.readInstalledVersion(repoRoot);
|
|
1978
|
+
if (installedVersion === void 0 && !options.scopeTo?.includes(target.id)) {
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
const outcome = await runWriter(target, repoRoot);
|
|
1982
|
+
outcomes.push(outcome);
|
|
1983
|
+
}
|
|
1984
|
+
return outcomes;
|
|
1985
|
+
}
|
|
1986
|
+
function isMcpHost(id) {
|
|
1987
|
+
return id === "claude-code" || id === "cursor" || id === "codex";
|
|
1988
|
+
}
|
|
1989
|
+
async function runWriter(target, repoRoot) {
|
|
1990
|
+
try {
|
|
1991
|
+
switch (target.id) {
|
|
1992
|
+
case "claude-code": {
|
|
1993
|
+
const r = await writeClaudeCodeMcpEntry(repoRoot);
|
|
1994
|
+
return { host: "claude-code", filePath: r.filePath, action: r.action };
|
|
1995
|
+
}
|
|
1996
|
+
case "cursor": {
|
|
1997
|
+
const r = await writeCursorMcpEntry(repoRoot);
|
|
1998
|
+
return { host: "cursor", filePath: r.filePath, action: r.action };
|
|
1999
|
+
}
|
|
2000
|
+
case "codex": {
|
|
2001
|
+
const r = await writeCodexMcpEntry(repoRoot);
|
|
2002
|
+
return { host: "codex", filePath: r.filePath, action: r.action };
|
|
2003
|
+
}
|
|
2004
|
+
default:
|
|
2005
|
+
return {
|
|
2006
|
+
host: target.id,
|
|
2007
|
+
filePath: "",
|
|
2008
|
+
action: "skipped"
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
} catch (err) {
|
|
2012
|
+
return {
|
|
2013
|
+
host: target.id,
|
|
2014
|
+
filePath: "",
|
|
2015
|
+
action: "failed",
|
|
2016
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// src/cli/commands/connector.ts
|
|
2022
|
+
var EXIT_OK = 0;
|
|
2023
|
+
var EXIT_FALLBACK_FAILURE = 1;
|
|
2024
|
+
var EXIT_INVOCATION_ERROR = 2;
|
|
2025
|
+
function registerConnector(program, getContext) {
|
|
2026
|
+
const group2 = program.command("connector").description(
|
|
2027
|
+
"Manage citadel connectors (traveling maesters) and dispatch operations for non-MCP agents."
|
|
2028
|
+
);
|
|
2029
|
+
group2.command("list").description("Print the configured connectors and the tool names the MCP server exposes.").action(async () => {
|
|
2030
|
+
process.exitCode = await runList(getContext());
|
|
2031
|
+
});
|
|
2032
|
+
group2.command("add").description(
|
|
2033
|
+
"Register a new connector with this citadel. Interactive when no flags are passed."
|
|
2034
|
+
).option("--type <type>", "Connector type identifier (e.g. gitlab-issues).").option("--name <name>", "Unique connector name (kebab-case slug).").option("--env-var <name>", "Environment variable that holds the connector's auth token.").option(
|
|
2035
|
+
"--config <json>",
|
|
2036
|
+
"Per-type config as a JSON string. Required when --type is supplied."
|
|
2037
|
+
).option(
|
|
2038
|
+
"--description <text>",
|
|
2039
|
+
"Optional short description (prepended to MCP tool descriptions)."
|
|
2040
|
+
).action(async (options) => {
|
|
2041
|
+
process.exitCode = await runAdd(getContext(), options);
|
|
2042
|
+
});
|
|
2043
|
+
group2.command("remove <name>").description("Remove the named connector from citadel.yaml and refresh per-host MCP entries.").option("--yes", "Skip the confirmation prompt.").action(async (name, options) => {
|
|
2044
|
+
process.exitCode = await runRemove(getContext(), name, options.yes === true);
|
|
2045
|
+
});
|
|
2046
|
+
group2.command("refresh").description(
|
|
2047
|
+
"Re-validate citadel.yaml and refresh per-host MCP registrations. Use this after editing citadel.yaml by hand."
|
|
2048
|
+
).action(async () => {
|
|
2049
|
+
process.exitCode = await runRefresh(getContext());
|
|
2050
|
+
});
|
|
2051
|
+
group2.command("exec <name> <operation> [args...]").description(
|
|
2052
|
+
"Fallback dispatch for non-MCP agent hosts. Invokes the named operation; prints the JSON envelope on stdout."
|
|
2053
|
+
).allowUnknownOption(true).action(async (name, operation, args) => {
|
|
2054
|
+
process.exitCode = await runExec(getContext(), name, operation, args);
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
async function runList(ctx) {
|
|
2058
|
+
let connectors;
|
|
2059
|
+
try {
|
|
2060
|
+
connectors = await listConnectorsFromCitadel(ctx.repoRoot.path);
|
|
2061
|
+
} catch (err) {
|
|
2062
|
+
return handleCitadelLoadError(ctx, err);
|
|
2063
|
+
}
|
|
2064
|
+
if (connectors.length === 0) {
|
|
2065
|
+
ctx.prompts.log.info("No connectors configured. Add one with `maester connector add`.");
|
|
2066
|
+
return EXIT_OK;
|
|
2067
|
+
}
|
|
2068
|
+
for (const c of connectors) {
|
|
2069
|
+
const tools = toolNamesFor(c);
|
|
2070
|
+
const lines = [`\u2022 ${c.name} (type: ${c.type})`];
|
|
2071
|
+
for (const t of tools) {
|
|
2072
|
+
lines.push(` ${t}`);
|
|
2073
|
+
}
|
|
2074
|
+
process.stdout.write(`${lines.join("\n")}
|
|
2075
|
+
`);
|
|
2076
|
+
}
|
|
2077
|
+
return EXIT_OK;
|
|
2078
|
+
}
|
|
2079
|
+
async function runAdd(ctx, options) {
|
|
2080
|
+
const isFlagDriven = Boolean(options.type || options.name);
|
|
2081
|
+
if (isFlagDriven) {
|
|
2082
|
+
return runAddFlagDriven(ctx, options);
|
|
2083
|
+
}
|
|
2084
|
+
return runAddInteractive(ctx);
|
|
2085
|
+
}
|
|
2086
|
+
async function runAddFlagDriven(ctx, options) {
|
|
2087
|
+
if (!options.type) {
|
|
2088
|
+
ctx.logger.error("--type is required when running connector add non-interactively.");
|
|
2089
|
+
return EXIT_INVOCATION_ERROR;
|
|
2090
|
+
}
|
|
2091
|
+
if (!options.name) {
|
|
2092
|
+
ctx.logger.error("--name is required when running connector add non-interactively.");
|
|
2093
|
+
return EXIT_INVOCATION_ERROR;
|
|
2094
|
+
}
|
|
2095
|
+
if (!SLUG_RE.test(options.name)) {
|
|
2096
|
+
ctx.logger.error(`--name must be kebab-case (matched ${SLUG_RE}).`);
|
|
2097
|
+
return EXIT_INVOCATION_ERROR;
|
|
2098
|
+
}
|
|
2099
|
+
if (!hasConnectorType(options.type)) {
|
|
2100
|
+
const known = listConnectorTypes().map((t) => t.id);
|
|
2101
|
+
ctx.logger.error(
|
|
2102
|
+
`Unknown connector type '${options.type}'.${known.length > 0 ? ` Known types: ${known.join(", ")}` : " No types are registered in this build."}`
|
|
2103
|
+
);
|
|
2104
|
+
return EXIT_INVOCATION_ERROR;
|
|
2105
|
+
}
|
|
2106
|
+
if (options.envVar && !ENV_VAR_RE.test(options.envVar)) {
|
|
2107
|
+
ctx.logger.error("--env-var must be UPPER_SNAKE_CASE (matched ^[A-Z][A-Z0-9_]*$).");
|
|
2108
|
+
return EXIT_INVOCATION_ERROR;
|
|
2109
|
+
}
|
|
2110
|
+
let parsedConfig = {};
|
|
2111
|
+
if (options.config) {
|
|
2112
|
+
try {
|
|
2113
|
+
parsedConfig = JSON.parse(options.config);
|
|
2114
|
+
} catch (err) {
|
|
2115
|
+
ctx.logger.error(
|
|
2116
|
+
`--config must be valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
2117
|
+
);
|
|
2118
|
+
return EXIT_INVOCATION_ERROR;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
const connector = {
|
|
2122
|
+
name: options.name,
|
|
2123
|
+
type: options.type,
|
|
2124
|
+
...options.description ? { description: options.description } : {},
|
|
2125
|
+
...options.envVar ? { auth: { type: "token", envVar: options.envVar } } : {},
|
|
2126
|
+
config: parsedConfig
|
|
2127
|
+
};
|
|
2128
|
+
return writeAddAndRefresh(ctx, connector);
|
|
2129
|
+
}
|
|
2130
|
+
async function runAddInteractive(ctx) {
|
|
2131
|
+
const types = listConnectorTypes();
|
|
2132
|
+
if (types.length === 0) {
|
|
2133
|
+
ctx.prompts.log.warning(
|
|
2134
|
+
"No connector types are registered in this build of maester. Cannot add a connector interactively."
|
|
2135
|
+
);
|
|
2136
|
+
return EXIT_INVOCATION_ERROR;
|
|
2137
|
+
}
|
|
2138
|
+
try {
|
|
2139
|
+
const typeId = await ctx.prompts.select({
|
|
2140
|
+
message: "Connector type",
|
|
2141
|
+
options: types.map((t) => ({ value: t.id, label: t.label }))
|
|
2142
|
+
});
|
|
2143
|
+
const name = await ctx.prompts.text({
|
|
2144
|
+
message: "Connector name (unique slug)",
|
|
2145
|
+
validate: (v) => SLUG_RE.test(v) ? void 0 : "Must be a kebab-case slug."
|
|
2146
|
+
});
|
|
2147
|
+
const description = await ctx.prompts.text({
|
|
2148
|
+
message: "Optional description (press enter to skip)",
|
|
2149
|
+
initialValue: ""
|
|
2150
|
+
});
|
|
2151
|
+
const envVar = await ctx.prompts.text({
|
|
2152
|
+
message: "Auth env var name (press enter to skip if no auth required)",
|
|
2153
|
+
initialValue: "",
|
|
2154
|
+
validate: (v) => !v || ENV_VAR_RE.test(v) ? void 0 : "Must be UPPER_SNAKE_CASE."
|
|
2155
|
+
});
|
|
2156
|
+
const configJson = await ctx.prompts.text({
|
|
2157
|
+
message: "Per-type config (JSON)",
|
|
2158
|
+
initialValue: "{}",
|
|
2159
|
+
validate: (v) => {
|
|
2160
|
+
try {
|
|
2161
|
+
JSON.parse(v);
|
|
2162
|
+
return void 0;
|
|
2163
|
+
} catch (e) {
|
|
2164
|
+
return e instanceof Error ? e.message : String(e);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
});
|
|
2168
|
+
const connector = {
|
|
2169
|
+
name,
|
|
2170
|
+
type: typeId,
|
|
2171
|
+
...description ? { description } : {},
|
|
2172
|
+
...envVar ? { auth: { type: "token", envVar } } : {},
|
|
2173
|
+
config: JSON.parse(configJson)
|
|
2174
|
+
};
|
|
2175
|
+
return writeAddAndRefresh(ctx, connector);
|
|
2176
|
+
} catch (err) {
|
|
2177
|
+
if (err instanceof PromptCancelledError) {
|
|
2178
|
+
ctx.prompts.outro("Cancelled \u2014 no connector added.");
|
|
2179
|
+
return EXIT_INVOCATION_ERROR;
|
|
2180
|
+
}
|
|
2181
|
+
throw err;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
async function writeAddAndRefresh(ctx, connector) {
|
|
2185
|
+
try {
|
|
2186
|
+
const result = await addConnectorToCitadel(ctx.repoRoot.path, connector);
|
|
2187
|
+
ctx.prompts.log.success(`Wrote connector '${connector.name}' to ${result.filePath}.`);
|
|
2188
|
+
if (result.config.sources.some((s) => s.name === connector.name)) {
|
|
2189
|
+
ctx.prompts.log.warning(
|
|
2190
|
+
`Connector '${connector.name}' shadows a source with the same name. The two namespaces are separate but reading citadel.yaml may confuse future maintainers.`
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
2193
|
+
const refreshOutcomes = await refreshMcpRegistrations(ctx.repoRoot.path);
|
|
2194
|
+
reportRefresh(ctx, refreshOutcomes);
|
|
2195
|
+
return EXIT_OK;
|
|
2196
|
+
} catch (err) {
|
|
2197
|
+
return handleCitadelLoadError(ctx, err);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
async function runRemove(ctx, name, yes) {
|
|
2201
|
+
if (!yes) {
|
|
2202
|
+
try {
|
|
2203
|
+
const confirmed = await ctx.prompts.confirm({
|
|
2204
|
+
message: `Remove connector '${name}' from citadel.yaml?`,
|
|
2205
|
+
initialValue: false
|
|
2206
|
+
});
|
|
2207
|
+
if (!confirmed) {
|
|
2208
|
+
ctx.prompts.outro("Cancelled \u2014 citadel.yaml not modified.");
|
|
2209
|
+
return EXIT_OK;
|
|
2210
|
+
}
|
|
2211
|
+
} catch (err) {
|
|
2212
|
+
if (err instanceof PromptCancelledError) {
|
|
2213
|
+
ctx.prompts.outro("Cancelled.");
|
|
2214
|
+
return EXIT_OK;
|
|
2215
|
+
}
|
|
2216
|
+
throw err;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
try {
|
|
2220
|
+
const result = await removeConnectorFromCitadel(ctx.repoRoot.path, name);
|
|
2221
|
+
ctx.prompts.log.success(`Removed connector '${name}' from ${result.filePath}.`);
|
|
2222
|
+
const refreshOutcomes = await refreshMcpRegistrations(ctx.repoRoot.path);
|
|
2223
|
+
reportRefresh(ctx, refreshOutcomes);
|
|
2224
|
+
return EXIT_OK;
|
|
2225
|
+
} catch (err) {
|
|
2226
|
+
if (err instanceof ConnectorNotFoundError) {
|
|
2227
|
+
ctx.logger.error(err.message);
|
|
2228
|
+
return EXIT_INVOCATION_ERROR;
|
|
2229
|
+
}
|
|
2230
|
+
return handleCitadelLoadError(ctx, err);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
async function runRefresh(ctx) {
|
|
2234
|
+
let config;
|
|
2235
|
+
try {
|
|
2236
|
+
config = await loadCitadelConfig(ctx.repoRoot.path);
|
|
2237
|
+
} catch (err) {
|
|
2238
|
+
return handleCitadelLoadError(ctx, err);
|
|
2239
|
+
}
|
|
2240
|
+
const count = config.connectors?.length ?? 0;
|
|
2241
|
+
ctx.prompts.log.info(
|
|
2242
|
+
count === 0 ? "citadel.yaml has no connectors. Refreshing per-host MCP entries anyway so the maester server stays registered." : `citadel.yaml lists ${count} connector${count === 1 ? "" : "s"}. Refreshing per-host MCP entries.`
|
|
2243
|
+
);
|
|
2244
|
+
const outcomes = await refreshMcpRegistrations(ctx.repoRoot.path);
|
|
2245
|
+
reportRefresh(ctx, outcomes);
|
|
2246
|
+
return EXIT_OK;
|
|
2247
|
+
}
|
|
2248
|
+
async function runExec(ctx, name, operation, rawArgs) {
|
|
2249
|
+
let config;
|
|
2250
|
+
try {
|
|
2251
|
+
config = await loadCitadelConfig(ctx.repoRoot.path);
|
|
2252
|
+
} catch (err) {
|
|
2253
|
+
return handleCitadelLoadError(ctx, err);
|
|
2254
|
+
}
|
|
2255
|
+
const connector = (config.connectors ?? []).find((c) => c.name === name);
|
|
2256
|
+
if (!connector) {
|
|
2257
|
+
ctx.logger.error(`No connector named '${name}' is configured in citadel.yaml.`);
|
|
2258
|
+
return EXIT_INVOCATION_ERROR;
|
|
2259
|
+
}
|
|
2260
|
+
let args;
|
|
2261
|
+
try {
|
|
2262
|
+
args = parseExecArgs(rawArgs);
|
|
2263
|
+
} catch (err) {
|
|
2264
|
+
ctx.logger.error(err instanceof Error ? err.message : String(err));
|
|
2265
|
+
return EXIT_INVOCATION_ERROR;
|
|
2266
|
+
}
|
|
2267
|
+
const envelope = await invokeOperation({
|
|
2268
|
+
connector,
|
|
2269
|
+
operationName: operation,
|
|
2270
|
+
args
|
|
2271
|
+
});
|
|
2272
|
+
process.stdout.write(`${JSON.stringify(envelope)}
|
|
2273
|
+
`);
|
|
2274
|
+
return envelope.ok ? EXIT_OK : EXIT_FALLBACK_FAILURE;
|
|
2275
|
+
}
|
|
2276
|
+
function parseExecArgs(raw) {
|
|
2277
|
+
const args = {};
|
|
2278
|
+
let i = 0;
|
|
2279
|
+
while (i < raw.length) {
|
|
2280
|
+
const token = raw[i];
|
|
2281
|
+
if (token === void 0) {
|
|
2282
|
+
i += 1;
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
if (!token.startsWith("--")) {
|
|
2286
|
+
throw new Error(`Unexpected positional argument '${token}'. Use --key value form.`);
|
|
2287
|
+
}
|
|
2288
|
+
const eqIndex = token.indexOf("=");
|
|
2289
|
+
let key;
|
|
2290
|
+
let value;
|
|
2291
|
+
if (eqIndex >= 0) {
|
|
2292
|
+
key = token.slice(2, eqIndex);
|
|
2293
|
+
value = token.slice(eqIndex + 1);
|
|
2294
|
+
i += 1;
|
|
2295
|
+
} else {
|
|
2296
|
+
key = token.slice(2);
|
|
2297
|
+
const next = raw[i + 1];
|
|
2298
|
+
if (next === void 0 || next.startsWith("--")) {
|
|
2299
|
+
value = true;
|
|
2300
|
+
i += 1;
|
|
2301
|
+
} else {
|
|
2302
|
+
value = next;
|
|
2303
|
+
i += 2;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
const existing = args[key];
|
|
2307
|
+
if (existing === void 0) {
|
|
2308
|
+
args[key] = value;
|
|
2309
|
+
} else if (Array.isArray(existing)) {
|
|
2310
|
+
existing.push(value);
|
|
2311
|
+
} else {
|
|
2312
|
+
args[key] = [existing, value];
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
return args;
|
|
2316
|
+
}
|
|
2317
|
+
function reportRefresh(ctx, outcomes) {
|
|
2318
|
+
if (outcomes.length === 0) {
|
|
2319
|
+
ctx.prompts.log.info(
|
|
2320
|
+
"No MCP-capable Grand Maester targets installed \u2014 skipping MCP config refresh."
|
|
2321
|
+
);
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
for (const o of outcomes) {
|
|
2325
|
+
if (o.action === "failed") {
|
|
2326
|
+
ctx.prompts.log.error(`MCP refresh failed for ${o.host}${o.error ? `: ${o.error}` : ""}`);
|
|
2327
|
+
} else {
|
|
2328
|
+
ctx.prompts.log.success(`MCP entry ${o.action} \u2192 ${o.filePath}`);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
ctx.prompts.log.info(
|
|
2332
|
+
"Restart your agent session to pick up the new tool surface (most host platforms restart MCP servers automatically when their config changes)."
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
function handleCitadelLoadError(ctx, err) {
|
|
2336
|
+
if (err instanceof ConfigError) {
|
|
2337
|
+
ctx.logger.error(err.message);
|
|
2338
|
+
return EXIT_INVOCATION_ERROR;
|
|
2339
|
+
}
|
|
2340
|
+
if (err instanceof MaesterError) {
|
|
2341
|
+
ctx.logger.error(err.message);
|
|
2342
|
+
return EXIT_INVOCATION_ERROR;
|
|
2343
|
+
}
|
|
2344
|
+
throw err;
|
|
2345
|
+
}
|
|
2346
|
+
function toolNamesFor(connector) {
|
|
2347
|
+
if (!hasConnectorType(connector.type)) {
|
|
2348
|
+
return [`(unregistered type: ${connector.type})`];
|
|
2349
|
+
}
|
|
2350
|
+
const type = listConnectorTypes().find((t) => t.id === connector.type);
|
|
2351
|
+
if (!type) return [];
|
|
2352
|
+
return Object.values(type.operations).map((op) => toolName(connector.name, op.name));
|
|
2353
|
+
}
|
|
2354
|
+
async function appendMissingGitignoreEntries(repoRoot, entries) {
|
|
2355
|
+
const path9 = resolve(repoRoot, ".gitignore");
|
|
2356
|
+
let existing = "";
|
|
2357
|
+
if (existsSync(path9)) {
|
|
2358
|
+
existing = await readFile(path9, "utf8");
|
|
2359
|
+
}
|
|
2360
|
+
const existingLines = new Set(
|
|
2361
|
+
existing.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0)
|
|
2362
|
+
);
|
|
2363
|
+
const added = [];
|
|
2364
|
+
const alreadyPresent = [];
|
|
2365
|
+
for (const entry of entries) {
|
|
2366
|
+
const normalized = entry.trim();
|
|
2367
|
+
if (normalized.length === 0) continue;
|
|
2368
|
+
if (existingLines.has(normalized)) {
|
|
2369
|
+
alreadyPresent.push(normalized);
|
|
2370
|
+
} else {
|
|
2371
|
+
added.push(normalized);
|
|
2372
|
+
existingLines.add(normalized);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
if (added.length === 0) {
|
|
2376
|
+
return { added, alreadyPresent };
|
|
2377
|
+
}
|
|
2378
|
+
const needsTrailingNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
2379
|
+
const appendBlock = `${needsTrailingNewline ? "\n" : ""}${added.join("\n")}
|
|
2380
|
+
`;
|
|
2381
|
+
await writeFile(path9, `${existing}${appendBlock}`, "utf8");
|
|
2382
|
+
return { added, alreadyPresent };
|
|
2383
|
+
}
|
|
2384
|
+
async function ensureScript(repoRoot, scriptName, command) {
|
|
2385
|
+
const path9 = resolve(repoRoot, "package.json");
|
|
2386
|
+
if (!existsSync(path9)) {
|
|
2387
|
+
return { added: false, reason: "no-package-json" };
|
|
2388
|
+
}
|
|
2389
|
+
const raw = await readFile(path9, "utf8");
|
|
2390
|
+
const trailingNewline = raw.endsWith("\n");
|
|
2391
|
+
const parsed = JSON.parse(raw);
|
|
2392
|
+
const scripts = parsed.scripts ?? {};
|
|
2393
|
+
if (scripts[scriptName] === command) {
|
|
2394
|
+
return { added: false, reason: "already-set" };
|
|
2395
|
+
}
|
|
2396
|
+
if (typeof scripts[scriptName] === "string" && scripts[scriptName] !== command) {
|
|
2397
|
+
return { added: false, reason: "already-set" };
|
|
2398
|
+
}
|
|
2399
|
+
scripts[scriptName] = command;
|
|
2400
|
+
parsed.scripts = scripts;
|
|
2401
|
+
const serialized = JSON.stringify(parsed, null, 2) + (trailingNewline ? "\n" : "");
|
|
2402
|
+
await writeFile(path9, serialized, "utf8");
|
|
2403
|
+
return { added: true, reason: "added" };
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
// src/core/init/finalize.ts
|
|
2407
|
+
async function finalizeCitadel(repoRoot, input) {
|
|
2408
|
+
detectDestinationCollisions(repoRoot, input);
|
|
2409
|
+
const config = {
|
|
2410
|
+
schemaVersion: 1,
|
|
2411
|
+
...input.baseDir ? { baseDir: input.baseDir } : {},
|
|
2412
|
+
sources: input.sources,
|
|
2413
|
+
...input.connectors && input.connectors.length > 0 ? { connectors: input.connectors } : {}
|
|
2414
|
+
};
|
|
2415
|
+
const citadelPath = await writeCitadelConfig(repoRoot, config);
|
|
2416
|
+
const gitignore = await appendMissingGitignoreEntries(repoRoot, [`${CACHE_DIR_NAME}/`]);
|
|
2417
|
+
const script = await ensureScript(repoRoot, "maester:sync", "maester sync");
|
|
2418
|
+
return {
|
|
2419
|
+
citadelPath,
|
|
2420
|
+
gitignoreAdded: gitignore.added,
|
|
2421
|
+
packageJsonScript: script.reason
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
function detectDestinationCollisions(repoRoot, input, baseDirArg) {
|
|
2425
|
+
const baseDir = Array.isArray(input) ? baseDirArg : input.baseDir;
|
|
2426
|
+
const entries = Array.isArray(input) ? input : input.sources.map((s) => ({ name: s.name, destination: s.destination }));
|
|
2427
|
+
const byDest = /* @__PURE__ */ new Map();
|
|
2428
|
+
for (const entry of entries) {
|
|
2429
|
+
const dest = entry.destination ? resolve(repoRoot, entry.destination) : defaultDestinationFor(repoRoot, entry.name, baseDir);
|
|
2430
|
+
const prior = byDest.get(dest);
|
|
2431
|
+
if (prior) {
|
|
2432
|
+
throw new Error(
|
|
2433
|
+
`sources '${entry.name}' and '${prior.name}' both resolve to destination '${dest}'. Set a unique destination for one of them.`
|
|
2434
|
+
);
|
|
2435
|
+
}
|
|
2436
|
+
byDest.set(dest, { name: entry.name });
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// src/core/init/validators.ts
|
|
2441
|
+
function validateSourceName(value) {
|
|
2442
|
+
if (!value || value.length === 0) return { ok: false, reason: "Name cannot be empty." };
|
|
2443
|
+
if (!SLUG_RE.test(value)) {
|
|
2444
|
+
return {
|
|
2445
|
+
ok: false,
|
|
2446
|
+
reason: "Name must be a kebab-case slug (lowercase letters, digits, and hyphens)."
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
return { ok: true };
|
|
2450
|
+
}
|
|
2451
|
+
function validateGitUrl(value) {
|
|
2452
|
+
if (!value || value.length === 0) return { ok: false, reason: "URL cannot be empty." };
|
|
2453
|
+
if (/\s/.test(value)) return { ok: false, reason: "URL cannot contain whitespace." };
|
|
2454
|
+
if (value.startsWith("https://") || value.startsWith("ssh://") || value.startsWith("file://")) {
|
|
2455
|
+
return { ok: true };
|
|
2456
|
+
}
|
|
2457
|
+
if (/^git@[^\s:]+:\S+$/.test(value)) return { ok: true };
|
|
2458
|
+
return {
|
|
2459
|
+
ok: false,
|
|
2460
|
+
reason: "URL must start with https://, ssh://, file://, or use the git@host:path form."
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
function validateEnvVarName(value) {
|
|
2464
|
+
if (!value || value.length === 0) {
|
|
2465
|
+
return { ok: false, reason: "Environment variable name cannot be empty." };
|
|
2466
|
+
}
|
|
2467
|
+
if (/\s/.test(value)) return { ok: false, reason: "Whitespace is not allowed." };
|
|
2468
|
+
if (!ENV_VAR_RE.test(value)) {
|
|
2469
|
+
return {
|
|
2470
|
+
ok: false,
|
|
2471
|
+
reason: "Environment variable name must be UPPER_SNAKE_CASE (e.g. MAESTER_DOCS_TOKEN)."
|
|
2472
|
+
};
|
|
1114
2473
|
}
|
|
2474
|
+
if (value.length >= 32 && !value.includes("_")) {
|
|
2475
|
+
return {
|
|
2476
|
+
ok: true,
|
|
2477
|
+
warning: "That looks unusually long and has no underscores \u2014 make sure you entered the NAME of the env var, not its value."
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
return { ok: true };
|
|
1115
2481
|
}
|
|
1116
|
-
function
|
|
1117
|
-
if (
|
|
1118
|
-
if (
|
|
1119
|
-
|
|
1120
|
-
if (
|
|
1121
|
-
|
|
2482
|
+
function validateDestination(value) {
|
|
2483
|
+
if (!value || value.length === 0) return { ok: true };
|
|
2484
|
+
if (value.startsWith("/"))
|
|
2485
|
+
return { ok: false, reason: "Destination must be repo-relative (no leading '/')." };
|
|
2486
|
+
if (value.split(/[\\/]+/).some((seg) => seg === "..")) {
|
|
2487
|
+
return { ok: false, reason: "Destination cannot contain '..' segments." };
|
|
2488
|
+
}
|
|
2489
|
+
return { ok: true };
|
|
1122
2490
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
readInstalledVersion: readAgentsMdInstalledVersion
|
|
1132
|
-
};
|
|
1133
|
-
|
|
1134
|
-
// src/core/skill/targets/index.ts
|
|
1135
|
-
var REGISTRY = [
|
|
1136
|
-
claudeCodeTarget,
|
|
1137
|
-
codexTarget,
|
|
1138
|
-
cursorTarget,
|
|
1139
|
-
genericTarget
|
|
1140
|
-
];
|
|
1141
|
-
function listSkillTargets() {
|
|
1142
|
-
return REGISTRY;
|
|
2491
|
+
function validateBaseDir(value) {
|
|
2492
|
+
if (!value || value.length === 0) return { ok: true };
|
|
2493
|
+
if (value.startsWith("/"))
|
|
2494
|
+
return { ok: false, reason: "Base directory must be repo-relative (no leading '/')." };
|
|
2495
|
+
if (value.split(/[\\/]+/).some((seg) => seg === "..")) {
|
|
2496
|
+
return { ok: false, reason: "Base directory cannot contain '..' segments." };
|
|
2497
|
+
}
|
|
2498
|
+
return { ok: true };
|
|
1143
2499
|
}
|
|
1144
|
-
function
|
|
1145
|
-
|
|
1146
|
-
if (
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
);
|
|
2500
|
+
function validateIncludesEntry(value) {
|
|
2501
|
+
if (!value || value.length === 0) return { ok: false, reason: "Includes entry cannot be empty." };
|
|
2502
|
+
if (/^\s+$/.test(value)) return { ok: false, reason: "Includes entry cannot be whitespace." };
|
|
2503
|
+
if (value.startsWith("/")) {
|
|
2504
|
+
return { ok: false, reason: "Includes entry must be repo-relative (no leading '/')." };
|
|
1150
2505
|
}
|
|
1151
|
-
|
|
2506
|
+
if (value.split(/[\\/]+/).some((seg) => seg === "..")) {
|
|
2507
|
+
return { ok: false, reason: "Includes entry cannot contain '..' segments." };
|
|
2508
|
+
}
|
|
2509
|
+
return { ok: true };
|
|
1152
2510
|
}
|
|
1153
|
-
function
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
if (existing) {
|
|
1158
|
-
existing.ids.push(target.id);
|
|
1159
|
-
existing.labels.push(target.label);
|
|
1160
|
-
} else {
|
|
1161
|
-
groups.set(target.writerKey, {
|
|
1162
|
-
writerKey: target.writerKey,
|
|
1163
|
-
primary: target,
|
|
1164
|
-
ids: [target.id],
|
|
1165
|
-
labels: [target.label],
|
|
1166
|
-
artifactPaths: target.artifactPaths
|
|
1167
|
-
});
|
|
1168
|
-
}
|
|
2511
|
+
function validateTag(value) {
|
|
2512
|
+
if (!value || value.length === 0) return { ok: false, reason: "Tag cannot be empty." };
|
|
2513
|
+
if (!SLUG_RE.test(value)) {
|
|
2514
|
+
return { ok: false, reason: "Tag must be a kebab-case slug." };
|
|
1169
2515
|
}
|
|
1170
|
-
return
|
|
2516
|
+
return { ok: true };
|
|
1171
2517
|
}
|
|
1172
2518
|
|
|
1173
2519
|
// package.json
|
|
1174
2520
|
var package_default = {
|
|
1175
|
-
version: "0.
|
|
2521
|
+
version: "0.4.1"};
|
|
1176
2522
|
var PACKAGE_VERSION = package_default.version;
|
|
1177
2523
|
|
|
1178
2524
|
// src/core/skill/version.ts
|
|
1179
2525
|
var SKILL_VERSION = PACKAGE_VERSION;
|
|
1180
2526
|
|
|
1181
2527
|
// src/core/skill/runner.ts
|
|
2528
|
+
var MCP_HOST_IDS = ["claude-code", "cursor", "codex"];
|
|
2529
|
+
function selectMcpHosts(targetIds) {
|
|
2530
|
+
return targetIds.filter((id) => MCP_HOST_IDS.includes(id));
|
|
2531
|
+
}
|
|
1182
2532
|
async function runSkillInstall(repoRoot, opts) {
|
|
1183
2533
|
if (opts.targets.length === 0) {
|
|
1184
2534
|
throw new Error("At least one target id must be supplied.");
|
|
@@ -1206,12 +2556,14 @@ async function runSkillInstall(repoRoot, opts) {
|
|
|
1206
2556
|
});
|
|
1207
2557
|
}
|
|
1208
2558
|
}
|
|
1209
|
-
|
|
2559
|
+
const mcpHosts = selectMcpHosts(opts.targets);
|
|
2560
|
+
const mcpRegistrations = mcpHosts.length > 0 ? await refreshMcpRegistrations(repoRoot, { scopeTo: mcpHosts }) : [];
|
|
2561
|
+
return { outcomes, counts: countOutcomes(outcomes), mcpRegistrations };
|
|
1210
2562
|
}
|
|
1211
2563
|
async function runSkillUpgrade(repoRoot, opts) {
|
|
1212
2564
|
const installedGroups = await findInstalledGroups(repoRoot);
|
|
1213
2565
|
if (installedGroups.length === 0) {
|
|
1214
|
-
return { outcomes: [], counts: countOutcomes([]) };
|
|
2566
|
+
return { outcomes: [], counts: countOutcomes([]), mcpRegistrations: [] };
|
|
1215
2567
|
}
|
|
1216
2568
|
const outcomes = [];
|
|
1217
2569
|
for (const group2 of installedGroups) {
|
|
@@ -1252,7 +2604,8 @@ async function runSkillUpgrade(repoRoot, opts) {
|
|
|
1252
2604
|
});
|
|
1253
2605
|
}
|
|
1254
2606
|
}
|
|
1255
|
-
|
|
2607
|
+
const mcpRegistrations = opts.check === true ? [] : await refreshMcpRegistrations(repoRoot);
|
|
2608
|
+
return { outcomes, counts: countOutcomes(outcomes), mcpRegistrations };
|
|
1256
2609
|
}
|
|
1257
2610
|
async function runSkillStatus(repoRoot) {
|
|
1258
2611
|
const outcomes = [];
|
|
@@ -1296,25 +2649,25 @@ async function safeWrite(write6, input) {
|
|
|
1296
2649
|
}
|
|
1297
2650
|
async function findInstalledGroups(repoRoot) {
|
|
1298
2651
|
const targets = listSkillTargets();
|
|
1299
|
-
const
|
|
2652
|
+
const installed2 = [];
|
|
1300
2653
|
for (const target of targets) {
|
|
1301
2654
|
const installedVersion = await target.readInstalledVersion(repoRoot);
|
|
1302
|
-
if (installedVersion !== void 0)
|
|
2655
|
+
if (installedVersion !== void 0) installed2.push(target);
|
|
1303
2656
|
}
|
|
1304
|
-
return dedupeTargets(
|
|
2657
|
+
return dedupeTargets(installed2);
|
|
1305
2658
|
}
|
|
1306
2659
|
function countOutcomes(outcomes) {
|
|
1307
|
-
let
|
|
2660
|
+
let installed2 = 0;
|
|
1308
2661
|
let upgraded = 0;
|
|
1309
2662
|
let unchanged = 0;
|
|
1310
2663
|
let failed = 0;
|
|
1311
2664
|
for (const o of outcomes) {
|
|
1312
|
-
if (o.action === "installed")
|
|
2665
|
+
if (o.action === "installed") installed2 += 1;
|
|
1313
2666
|
else if (o.action === "upgraded") upgraded += 1;
|
|
1314
2667
|
else if (o.action === "unchanged") unchanged += 1;
|
|
1315
2668
|
else if (o.action === "failed") failed += 1;
|
|
1316
2669
|
}
|
|
1317
|
-
return { installed, upgraded, unchanged, failed };
|
|
2670
|
+
return { installed: installed2, upgraded, unchanged, failed };
|
|
1318
2671
|
}
|
|
1319
2672
|
|
|
1320
2673
|
// src/cli/commands/init.ts
|
|
@@ -1368,8 +2721,9 @@ async function runInit(ctx) {
|
|
|
1368
2721
|
ctx.prompts.outro("Cancelled due to destination collision. Re-run when resolved.");
|
|
1369
2722
|
return 1;
|
|
1370
2723
|
}
|
|
2724
|
+
const connectors = await collectConnectors(ctx);
|
|
1371
2725
|
const confirmWrite = await ctx.prompts.confirm({
|
|
1372
|
-
message: `Write ${sources.length} source(s) to citadel.yaml?`,
|
|
2726
|
+
message: `Write ${sources.length} source(s)${connectors.length > 0 ? ` and ${connectors.length} connector(s)` : ""} to citadel.yaml?`,
|
|
1373
2727
|
initialValue: true
|
|
1374
2728
|
});
|
|
1375
2729
|
if (!confirmWrite) {
|
|
@@ -1378,7 +2732,8 @@ async function runInit(ctx) {
|
|
|
1378
2732
|
}
|
|
1379
2733
|
const result = await finalizeCitadel(ctx.repoRoot.path, {
|
|
1380
2734
|
sources,
|
|
1381
|
-
...baseDir ? { baseDir } : {}
|
|
2735
|
+
...baseDir ? { baseDir } : {},
|
|
2736
|
+
...connectors.length > 0 ? { connectors } : {}
|
|
1382
2737
|
});
|
|
1383
2738
|
ctx.prompts.log.success(`Wrote ${result.citadelPath}`);
|
|
1384
2739
|
if (result.gitignoreAdded.length > 0) {
|
|
@@ -1400,7 +2755,7 @@ async function runInit(ctx) {
|
|
|
1400
2755
|
ctx.prompts.log.info(`Remember to set these env vars before syncing: ${summary}`);
|
|
1401
2756
|
}
|
|
1402
2757
|
await maybeInstallSkill(ctx, baseDir ?? DEFAULT_BASE_DIR);
|
|
1403
|
-
ctx.prompts.outro("Next: run `npx maester sync` to fetch your sources.");
|
|
2758
|
+
ctx.prompts.outro("Next: run `npx baller-maester sync` to fetch your sources.");
|
|
1404
2759
|
return 0;
|
|
1405
2760
|
} catch (err) {
|
|
1406
2761
|
if (err instanceof PromptCancelledError) {
|
|
@@ -1557,9 +2912,9 @@ async function collectIncludes(ctx) {
|
|
|
1557
2912
|
});
|
|
1558
2913
|
const paths = parseIncludesEntries(raw);
|
|
1559
2914
|
const entries = [];
|
|
1560
|
-
for (const
|
|
2915
|
+
for (const path9 of paths) {
|
|
1561
2916
|
const choice = await ctx.prompts.select({
|
|
1562
|
-
message: `State for '${
|
|
2917
|
+
message: `State for '${path9}'?`,
|
|
1563
2918
|
initialValue: "file-header",
|
|
1564
2919
|
options: [
|
|
1565
2920
|
{ value: "draft", label: "draft", hint: "tag this entry as draft" },
|
|
@@ -1571,14 +2926,14 @@ async function collectIncludes(ctx) {
|
|
|
1571
2926
|
}
|
|
1572
2927
|
]
|
|
1573
2928
|
});
|
|
1574
|
-
entries.push(buildIncludeEntry(
|
|
2929
|
+
entries.push(buildIncludeEntry(path9, choice));
|
|
1575
2930
|
}
|
|
1576
2931
|
return entries;
|
|
1577
2932
|
}
|
|
1578
|
-
function buildIncludeEntry(
|
|
1579
|
-
if (choice === "file-header") return
|
|
2933
|
+
function buildIncludeEntry(path9, choice) {
|
|
2934
|
+
if (choice === "file-header") return path9;
|
|
1580
2935
|
const state = choice;
|
|
1581
|
-
return { path:
|
|
2936
|
+
return { path: path9, state };
|
|
1582
2937
|
}
|
|
1583
2938
|
async function collectAuth(ctx) {
|
|
1584
2939
|
const authType = await ctx.prompts.select({
|
|
@@ -1645,13 +3000,137 @@ function parseIncludesEntries(raw) {
|
|
|
1645
3000
|
function parseTagsEntries(raw) {
|
|
1646
3001
|
return raw.split(/[,\s]+/).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
1647
3002
|
}
|
|
3003
|
+
async function collectConnectors(ctx) {
|
|
3004
|
+
const types = listConnectorTypes();
|
|
3005
|
+
if (types.length === 0) {
|
|
3006
|
+
return [];
|
|
3007
|
+
}
|
|
3008
|
+
ctx.prompts.log.info(
|
|
3009
|
+
`${types.length} connector type(s) available, but interactive registration during init is wired in a follow-on feature. Use \`maester connector add\` after init.`
|
|
3010
|
+
);
|
|
3011
|
+
return [];
|
|
3012
|
+
}
|
|
3013
|
+
var installed = false;
|
|
3014
|
+
function setupStdioForMcp() {
|
|
3015
|
+
if (installed) return;
|
|
3016
|
+
installed = true;
|
|
3017
|
+
const logger = consola;
|
|
3018
|
+
if (typeof logger.setReporters === "function") {
|
|
3019
|
+
logger.setReporters([
|
|
3020
|
+
{
|
|
3021
|
+
log(logObj) {
|
|
3022
|
+
const text2 = formatConsolaLogObj(logObj);
|
|
3023
|
+
process.stderr.write(text2);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
]);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
function formatConsolaLogObj(logObj) {
|
|
3030
|
+
const prefix = logObj.type ? `[${logObj.type}] ` : "";
|
|
3031
|
+
const body = (logObj.args ?? []).map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg)).join(" ");
|
|
3032
|
+
return `${prefix}${body}
|
|
3033
|
+
`;
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
// src/core/mcp/server.ts
|
|
3037
|
+
var MCP_SERVER_NAME = "maester";
|
|
3038
|
+
async function bootMcpServer(repoRoot, serverVersion) {
|
|
3039
|
+
setupStdioForMcp();
|
|
3040
|
+
let citadelConfig;
|
|
3041
|
+
try {
|
|
3042
|
+
citadelConfig = await loadCitadelConfig(repoRoot);
|
|
3043
|
+
} catch (err) {
|
|
3044
|
+
if (err instanceof ConfigError) {
|
|
3045
|
+
return { ok: false, exitCode: 2, message: err.message };
|
|
3046
|
+
}
|
|
3047
|
+
return {
|
|
3048
|
+
ok: false,
|
|
3049
|
+
exitCode: 2,
|
|
3050
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
3053
|
+
let tools;
|
|
3054
|
+
try {
|
|
3055
|
+
tools = listConnectorTools(citadelConfig);
|
|
3056
|
+
} catch (err) {
|
|
3057
|
+
return {
|
|
3058
|
+
ok: false,
|
|
3059
|
+
exitCode: 2,
|
|
3060
|
+
message: err instanceof Error ? `Failed to build connector tool surface: ${err.message}` : String(err)
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
const server = new Server(
|
|
3064
|
+
{ name: MCP_SERVER_NAME, version: serverVersion },
|
|
3065
|
+
{ capabilities: { tools: {} } }
|
|
3066
|
+
);
|
|
3067
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
3068
|
+
tools: tools.map((t) => ({
|
|
3069
|
+
name: t.name,
|
|
3070
|
+
description: t.description,
|
|
3071
|
+
inputSchema: t.inputSchema
|
|
3072
|
+
}))
|
|
3073
|
+
}));
|
|
3074
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3075
|
+
const requestedName = request.params.name;
|
|
3076
|
+
const args = request.params.arguments ?? {};
|
|
3077
|
+
const match = findOperationByToolName(citadelConfig, requestedName);
|
|
3078
|
+
let envelope;
|
|
3079
|
+
if (!match) {
|
|
3080
|
+
const failure = {
|
|
3081
|
+
connector: requestedName.split("__")[0] ?? requestedName,
|
|
3082
|
+
operation: requestedName.split("__").slice(1).join("__") || "(unknown)",
|
|
3083
|
+
code: "unknown-operation",
|
|
3084
|
+
message: `No tool named '${requestedName}' is registered. Run \`maester connector list\` to see configured tools.`
|
|
3085
|
+
};
|
|
3086
|
+
envelope = buildFailureEnvelope(failure);
|
|
3087
|
+
} else {
|
|
3088
|
+
envelope = await invokeOperation({
|
|
3089
|
+
connector: match.connector,
|
|
3090
|
+
operationName: match.operationName,
|
|
3091
|
+
args
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
const text2 = JSON.stringify(envelope);
|
|
3095
|
+
if (envelope.ok) {
|
|
3096
|
+
return {
|
|
3097
|
+
content: [{ type: "text", text: text2 }]
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
return {
|
|
3101
|
+
isError: true,
|
|
3102
|
+
content: [{ type: "text", text: text2 }]
|
|
3103
|
+
};
|
|
3104
|
+
});
|
|
3105
|
+
const transport = new StdioServerTransport();
|
|
3106
|
+
await server.connect(transport);
|
|
3107
|
+
return { ok: true, toolCount: tools.length, server };
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
// src/cli/commands/mcp.ts
|
|
3111
|
+
function registerMcp(program, getContext) {
|
|
3112
|
+
program.command("mcp").description(
|
|
3113
|
+
"Run the stdio MCP server for this citadel. Intended to be spawned by an agent host (Claude Code, Cursor, Codex CLI) via project-level MCP config."
|
|
3114
|
+
).action(async () => {
|
|
3115
|
+
process.exitCode = await runMcpCommand(getContext());
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
async function runMcpCommand(ctx) {
|
|
3119
|
+
const result = await bootMcpServer(ctx.repoRoot.path, PACKAGE_VERSION);
|
|
3120
|
+
if (!result.ok) {
|
|
3121
|
+
process.stderr.write(`maester mcp: ${result.message}
|
|
3122
|
+
`);
|
|
3123
|
+
return result.exitCode;
|
|
3124
|
+
}
|
|
3125
|
+
return 0;
|
|
3126
|
+
}
|
|
1648
3127
|
|
|
1649
3128
|
// src/core/publish/finalize.ts
|
|
1650
3129
|
async function finalizeMaesterManifest(repoRoot, documents) {
|
|
1651
3130
|
detectDuplicatePaths(documents);
|
|
1652
3131
|
const config = { schemaVersion: 1, documents };
|
|
1653
|
-
const
|
|
1654
|
-
return { maesterPath:
|
|
3132
|
+
const path9 = await writeMaesterConfig(repoRoot, config);
|
|
3133
|
+
return { maesterPath: path9, documentCount: documents.length };
|
|
1655
3134
|
}
|
|
1656
3135
|
function detectDuplicatePaths(documents) {
|
|
1657
3136
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -1706,9 +3185,9 @@ function buildPublishedDocumentStateField(choice) {
|
|
|
1706
3185
|
if (choice === "file-header") return {};
|
|
1707
3186
|
return { state: choice };
|
|
1708
3187
|
}
|
|
1709
|
-
async function askDocumentState(ctx,
|
|
3188
|
+
async function askDocumentState(ctx, path9) {
|
|
1710
3189
|
return ctx.prompts.select({
|
|
1711
|
-
message: `State for '${
|
|
3190
|
+
message: `State for '${path9}'?`,
|
|
1712
3191
|
initialValue: "file-header",
|
|
1713
3192
|
options: [
|
|
1714
3193
|
{ value: "draft", label: "draft", hint: "tag this entry as draft" },
|
|
@@ -1796,7 +3275,7 @@ async function runPublish(ctx) {
|
|
|
1796
3275
|
return 0;
|
|
1797
3276
|
}
|
|
1798
3277
|
async function collectOneDocument(ctx, repoRoot, existing) {
|
|
1799
|
-
const
|
|
3278
|
+
const path9 = await ctx.prompts.text({
|
|
1800
3279
|
message: "Path or glob (relative to the repo root)",
|
|
1801
3280
|
placeholder: "docs/runbooks/**/*.md",
|
|
1802
3281
|
validate: (value) => {
|
|
@@ -1809,7 +3288,7 @@ async function collectOneDocument(ctx, repoRoot, existing) {
|
|
|
1809
3288
|
return void 0;
|
|
1810
3289
|
}
|
|
1811
3290
|
});
|
|
1812
|
-
const trimmed =
|
|
3291
|
+
const trimmed = path9.trim();
|
|
1813
3292
|
if (isGlobPath(trimmed)) {
|
|
1814
3293
|
const matches = await globby(trimmed, { cwd: repoRoot, dot: false, gitignore: false });
|
|
1815
3294
|
ctx.prompts.log.info(`Glob matches ${matches.length} file(s) currently.`);
|
|
@@ -1851,126 +3330,19 @@ async function collectOneDocument(ctx, repoRoot, existing) {
|
|
|
1851
3330
|
};
|
|
1852
3331
|
return doc;
|
|
1853
3332
|
}
|
|
1854
|
-
function isSafeRepoRelative(value) {
|
|
1855
|
-
if (value.length === 0 || /^\s+$/.test(value)) return false;
|
|
1856
|
-
if (value.startsWith("/")) return false;
|
|
1857
|
-
if (value.split(/[\\/]+/).some((seg) => seg === "..")) return false;
|
|
1858
|
-
return true;
|
|
1859
|
-
}
|
|
1860
|
-
var PublishedDocumentSchema = z.object({
|
|
1861
|
-
path: z.string().min(1).refine(
|
|
1862
|
-
isSafeRepoRelative,
|
|
1863
|
-
"path must be a repo-relative file or glob; no leading '/' and no '..'"
|
|
1864
|
-
),
|
|
1865
|
-
description: z.string().min(1).optional(),
|
|
1866
|
-
category: z.string().min(1).regex(SLUG_RE, "category must be a kebab-case slug").optional(),
|
|
1867
|
-
tags: z.array(z.string().min(1).regex(SLUG_RE, "tags must be slugs")).optional(),
|
|
1868
|
-
state: StateSchema.optional()
|
|
1869
|
-
}).strict();
|
|
1870
|
-
var MaesterConfigSchema = z.object({
|
|
1871
|
-
schemaVersion: z.literal(1),
|
|
1872
|
-
documents: z.array(PublishedDocumentSchema).min(1, "at least one published document must be declared").superRefine((docs, ctx) => {
|
|
1873
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1874
|
-
for (let i = 0; i < docs.length; i++) {
|
|
1875
|
-
const p = docs[i]?.path;
|
|
1876
|
-
if (!p) continue;
|
|
1877
|
-
const prior = seen.get(p);
|
|
1878
|
-
if (prior !== void 0) {
|
|
1879
|
-
ctx.addIssue({
|
|
1880
|
-
code: z.ZodIssueCode.custom,
|
|
1881
|
-
message: `duplicate path '${p}' (also at index ${prior})`,
|
|
1882
|
-
path: [i, "path"]
|
|
1883
|
-
});
|
|
1884
|
-
} else {
|
|
1885
|
-
seen.set(p, i);
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
})
|
|
1889
|
-
}).strict();
|
|
1890
|
-
|
|
1891
|
-
// src/core/config/loader.ts
|
|
1892
|
-
async function loadCitadelConfig(repoRoot) {
|
|
1893
|
-
const path5 = citadelConfigPath(repoRoot);
|
|
1894
|
-
if (!existsSync(path5)) {
|
|
1895
|
-
throw new ConfigError(
|
|
1896
|
-
"No citadel.yaml found at the repository root. Run `npx maester init` to create one.",
|
|
1897
|
-
{ filePath: path5 }
|
|
1898
|
-
);
|
|
1899
|
-
}
|
|
1900
|
-
const raw = await readFile(path5, "utf8");
|
|
1901
|
-
return parseAndValidate(raw, CitadelConfigSchema, path5);
|
|
1902
|
-
}
|
|
1903
|
-
function parseAndValidate(raw, schema, filePath) {
|
|
1904
|
-
const data = parseYaml(raw, filePath);
|
|
1905
|
-
return runSchema(data, schema, filePath);
|
|
1906
|
-
}
|
|
1907
|
-
function parseYaml(raw, filePath) {
|
|
1908
|
-
const doc = parseDocument(raw, { keepSourceTokens: false });
|
|
1909
|
-
const yamlErrors = doc.errors;
|
|
1910
|
-
if (yamlErrors.length > 0) {
|
|
1911
|
-
const first = yamlErrors[0];
|
|
1912
|
-
const pos = positionFromError(first, raw);
|
|
1913
|
-
throw new ConfigError(`YAML parse error: ${first.message}`, {
|
|
1914
|
-
filePath,
|
|
1915
|
-
line: pos.line,
|
|
1916
|
-
column: pos.column,
|
|
1917
|
-
cause: first
|
|
1918
|
-
});
|
|
1919
|
-
}
|
|
1920
|
-
return doc.toJS({ maxAliasCount: -1 });
|
|
1921
|
-
}
|
|
1922
|
-
function runSchema(data, schema, filePath) {
|
|
1923
|
-
const result = schema.safeParse(data);
|
|
1924
|
-
if (!result.success) {
|
|
1925
|
-
const issue = result.error.issues[0];
|
|
1926
|
-
const where = issue?.path?.length ? ` at \`${issue.path.join(".")}\`` : "";
|
|
1927
|
-
throw new ConfigError(`${filePath}: ${issue?.message ?? "validation failed"}${where}`, {
|
|
1928
|
-
filePath,
|
|
1929
|
-
cause: result.error
|
|
1930
|
-
});
|
|
1931
|
-
}
|
|
1932
|
-
return result.data;
|
|
1933
|
-
}
|
|
1934
|
-
function positionFromError(err, raw) {
|
|
1935
|
-
const pos = err.pos;
|
|
1936
|
-
if (!pos) return { line: 1, column: 1 };
|
|
1937
|
-
const offset = pos[0];
|
|
1938
|
-
let line = 1;
|
|
1939
|
-
let lastLineStart = 0;
|
|
1940
|
-
for (let i = 0; i < offset && i < raw.length; i++) {
|
|
1941
|
-
if (raw[i] === "\n") {
|
|
1942
|
-
line++;
|
|
1943
|
-
lastLineStart = i + 1;
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
return { line, column: offset - lastLineStart + 1 };
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
// src/core/auth/resolver.ts
|
|
1950
|
-
function resolveAuth(auth, env = process.env) {
|
|
1951
|
-
if (!auth || auth.type === "none") return { type: "delegated" };
|
|
1952
|
-
const value = env[auth.envVar];
|
|
1953
|
-
if (value === void 0 || value.length === 0) {
|
|
1954
|
-
throw new AuthError(
|
|
1955
|
-
auth.envVar,
|
|
1956
|
-
`${auth.envVar} is not set. Define it in your shell, .env loader, or CI secret manager before syncing.`
|
|
1957
|
-
);
|
|
1958
|
-
}
|
|
1959
|
-
return { type: "token", value };
|
|
1960
|
-
}
|
|
1961
3333
|
var PROVENANCE_FILENAME = ".maester-source.json";
|
|
1962
3334
|
async function writeProvenanceMarker(destination, marker) {
|
|
1963
|
-
const
|
|
3335
|
+
const path9 = resolve(destination, PROVENANCE_FILENAME);
|
|
1964
3336
|
const body = `${JSON.stringify(marker, null, 2)}
|
|
1965
3337
|
`;
|
|
1966
|
-
await writeFile(
|
|
1967
|
-
return
|
|
3338
|
+
await writeFile(path9, body, "utf8");
|
|
3339
|
+
return path9;
|
|
1968
3340
|
}
|
|
1969
3341
|
async function readProvenanceMarker(destination) {
|
|
1970
|
-
const
|
|
1971
|
-
if (!existsSync(
|
|
3342
|
+
const path9 = resolve(destination, PROVENANCE_FILENAME);
|
|
3343
|
+
if (!existsSync(path9)) return void 0;
|
|
1972
3344
|
try {
|
|
1973
|
-
const text2 = await readFile(
|
|
3345
|
+
const text2 = await readFile(path9, "utf8");
|
|
1974
3346
|
const parsed = JSON.parse(text2);
|
|
1975
3347
|
if (typeof parsed.sourceName !== "string" || typeof parsed.sourceUrl !== "string" || typeof parsed.commitSha !== "string" || !Array.isArray(parsed.filterSet)) {
|
|
1976
3348
|
return void 0;
|
|
@@ -2225,10 +3597,10 @@ function manifestError(name, reason) {
|
|
|
2225
3597
|
);
|
|
2226
3598
|
}
|
|
2227
3599
|
async function discoverManifestFromCache(cacheDir) {
|
|
2228
|
-
const
|
|
2229
|
-
if (!existsSync(
|
|
3600
|
+
const path9 = resolve(cacheDir, MAESTER_MANIFEST_FILENAME);
|
|
3601
|
+
if (!existsSync(path9)) return { mode: "no-manifest", reason: "absent" };
|
|
2230
3602
|
try {
|
|
2231
|
-
const raw = await readFile(
|
|
3603
|
+
const raw = await readFile(path9, "utf8");
|
|
2232
3604
|
const doc = parseDocument(raw);
|
|
2233
3605
|
if (doc.errors.length > 0) return { mode: "no-manifest", reason: "invalid" };
|
|
2234
3606
|
const parsed = MaesterConfigSchema.safeParse(doc.toJS({ maxAliasCount: -1 }));
|
|
@@ -2516,13 +3888,13 @@ function extractTargetPath(envelope, repoRoot) {
|
|
|
2516
3888
|
const raw = input.file_path ?? input.path ?? input.pattern;
|
|
2517
3889
|
if (typeof raw !== "string" || raw.length === 0) return void 0;
|
|
2518
3890
|
const cwd = typeof envelope.cwd === "string" ? envelope.cwd : repoRoot;
|
|
2519
|
-
return
|
|
3891
|
+
return path8.isAbsolute(raw) ? raw : path8.resolve(cwd, raw);
|
|
2520
3892
|
}
|
|
2521
3893
|
function isUnderBaseDir(targetPath, repoRoot, baseDir) {
|
|
2522
|
-
const base =
|
|
2523
|
-
const rel =
|
|
3894
|
+
const base = path8.resolve(repoRoot, baseDir);
|
|
3895
|
+
const rel = path8.relative(base, targetPath);
|
|
2524
3896
|
if (rel === "") return true;
|
|
2525
|
-
return !rel.startsWith("..") && !
|
|
3897
|
+
return !rel.startsWith("..") && !path8.isAbsolute(rel);
|
|
2526
3898
|
}
|
|
2527
3899
|
function readTtlSeconds(env) {
|
|
2528
3900
|
const raw = (env ?? process.env).MAESTER_SKILL_STATUS_TTL;
|
|
@@ -2533,7 +3905,7 @@ function readTtlSeconds(env) {
|
|
|
2533
3905
|
}
|
|
2534
3906
|
async function readCache(repoRoot) {
|
|
2535
3907
|
try {
|
|
2536
|
-
const raw = await promises.readFile(
|
|
3908
|
+
const raw = await promises.readFile(path8.join(repoRoot, CACHE_RELATIVE_PATH), "utf8");
|
|
2537
3909
|
const parsed = JSON.parse(raw);
|
|
2538
3910
|
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
2539
3911
|
const candidate = parsed;
|
|
@@ -2550,8 +3922,8 @@ async function readCache(repoRoot) {
|
|
|
2550
3922
|
}
|
|
2551
3923
|
}
|
|
2552
3924
|
async function writeCache(repoRoot, verdict) {
|
|
2553
|
-
const finalPath =
|
|
2554
|
-
await promises.mkdir(
|
|
3925
|
+
const finalPath = path8.join(repoRoot, CACHE_RELATIVE_PATH);
|
|
3926
|
+
await promises.mkdir(path8.dirname(finalPath), { recursive: true });
|
|
2555
3927
|
const tempPath = `${finalPath}.tmp-${Math.floor(Math.random() * 1e9)}`;
|
|
2556
3928
|
await promises.writeFile(tempPath, `${JSON.stringify(verdict)}
|
|
2557
3929
|
`, "utf8");
|
|
@@ -2574,7 +3946,7 @@ function summarize(result) {
|
|
|
2574
3946
|
return parts.join(", ");
|
|
2575
3947
|
}
|
|
2576
3948
|
function buildHookResponse(verdict) {
|
|
2577
|
-
const detail = verdict.verdict === "failed" ? `Citadel status check failed: ${verdict.summary}. Proceed with the read and flag that cited content may be stale; do not retry sync in a loop.` : `Citadel is behind (${verdict.summary}). Auto-sync policy: run \`npx maester sync\`, then \`rm -f .maester/.skill-cache.json\` to invalidate this hook's cache, then proceed with the read. Do not prompt the user \u2014 sync is read-only against the configured remotes.`;
|
|
3949
|
+
const detail = verdict.verdict === "failed" ? `Citadel status check failed: ${verdict.summary}. Proceed with the read and flag that cited content may be stale; do not retry sync in a loop.` : `Citadel is behind (${verdict.summary}). Auto-sync policy: run \`npx baller-maester sync\`, then \`rm -f .maester/.skill-cache.json\` to invalidate this hook's cache, then proceed with the read. Do not prompt the user \u2014 sync is read-only against the configured remotes.`;
|
|
2578
3950
|
const response = {
|
|
2579
3951
|
hookSpecificOutput: {
|
|
2580
3952
|
hookEventName: "PreToolUse",
|
|
@@ -2586,7 +3958,7 @@ function buildHookResponse(verdict) {
|
|
|
2586
3958
|
}
|
|
2587
3959
|
|
|
2588
3960
|
// src/cli/commands/skill.ts
|
|
2589
|
-
var
|
|
3961
|
+
var EXIT_OK2 = 0;
|
|
2590
3962
|
var EXIT_OUTDATED_OR_BEHIND = 1;
|
|
2591
3963
|
var EXIT_FAILED = 2;
|
|
2592
3964
|
var SUPPORTED_IDS = ["claude-code", "codex", "cursor", "agents-md"];
|
|
@@ -2645,7 +4017,7 @@ async function runSkillInstallCommand(ctx, flagTargets, mode) {
|
|
|
2645
4017
|
}
|
|
2646
4018
|
if (targets.length === 0) {
|
|
2647
4019
|
ctx.logger.warning("No targets selected \u2014 nothing to install.");
|
|
2648
|
-
return
|
|
4020
|
+
return EXIT_OK2;
|
|
2649
4021
|
}
|
|
2650
4022
|
}
|
|
2651
4023
|
let result;
|
|
@@ -2660,7 +4032,7 @@ async function runSkillInstallCommand(ctx, flagTargets, mode) {
|
|
|
2660
4032
|
return EXIT_FAILED;
|
|
2661
4033
|
}
|
|
2662
4034
|
renderInstallResult(ctx, result, mode);
|
|
2663
|
-
return result.counts.failed > 0 ? EXIT_FAILED :
|
|
4035
|
+
return result.counts.failed > 0 ? EXIT_FAILED : EXIT_OK2;
|
|
2664
4036
|
}
|
|
2665
4037
|
async function runSkillUpgradeCommand(ctx, check) {
|
|
2666
4038
|
const baseDir = await loadBaseDir(ctx);
|
|
@@ -2674,7 +4046,7 @@ async function runSkillUpgradeCommand(ctx, check) {
|
|
|
2674
4046
|
}
|
|
2675
4047
|
if (result.outcomes.length === 0) {
|
|
2676
4048
|
ctx.logger.info("No Grand Maester targets are installed. Run `maester skill install` first.");
|
|
2677
|
-
return check ?
|
|
4049
|
+
return check ? EXIT_OK2 : EXIT_OK2;
|
|
2678
4050
|
}
|
|
2679
4051
|
for (const outcome of result.outcomes) {
|
|
2680
4052
|
renderInstallOutcome(ctx, outcome);
|
|
@@ -2696,7 +4068,7 @@ async function runSkillUpgradeCommand(ctx, check) {
|
|
|
2696
4068
|
} else {
|
|
2697
4069
|
ctx.logger.success(`All ${result.outcomes.length} installed target(s) up to date.`);
|
|
2698
4070
|
}
|
|
2699
|
-
return
|
|
4071
|
+
return EXIT_OK2;
|
|
2700
4072
|
}
|
|
2701
4073
|
async function runSkillStatusCommand(ctx) {
|
|
2702
4074
|
const result = await runSkillStatus(ctx.repoRoot.path);
|
|
@@ -2712,13 +4084,13 @@ async function runSkillStatusCommand(ctx) {
|
|
|
2712
4084
|
}
|
|
2713
4085
|
if (result.counts.upToDate + result.counts.outdated === 0) return EXIT_FAILED;
|
|
2714
4086
|
if (result.counts.outdated > 0) return EXIT_OUTDATED_OR_BEHIND;
|
|
2715
|
-
return
|
|
4087
|
+
return EXIT_OK2;
|
|
2716
4088
|
}
|
|
2717
4089
|
async function runRuntimePrereadCommand(ctx) {
|
|
2718
4090
|
const stdin = await readAllStdin();
|
|
2719
4091
|
const out = await runtimePreread(stdin, { repoRoot: ctx.repoRoot.path });
|
|
2720
4092
|
if (out.length > 0) process.stdout.write(out);
|
|
2721
|
-
return
|
|
4093
|
+
return EXIT_OK2;
|
|
2722
4094
|
}
|
|
2723
4095
|
async function runRuntimeStatusSummaryCommand(ctx) {
|
|
2724
4096
|
const { summary, exitCode } = await runtimeStatusSummary({ repoRoot: ctx.repoRoot.path });
|
|
@@ -2763,6 +4135,13 @@ function renderInstallResult(ctx, result, mode) {
|
|
|
2763
4135
|
for (const outcome of result.outcomes) {
|
|
2764
4136
|
renderInstallOutcome(ctx, outcome);
|
|
2765
4137
|
}
|
|
4138
|
+
for (const reg of result.mcpRegistrations) {
|
|
4139
|
+
if (reg.action === "failed") {
|
|
4140
|
+
ctx.logger.error(`MCP refresh failed for ${reg.host}${reg.error ? `: ${reg.error}` : ""}`);
|
|
4141
|
+
} else if (reg.action !== "skipped") {
|
|
4142
|
+
ctx.logger.success(`MCP entry ${reg.action} \u2192 ${reg.filePath}`);
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
2766
4145
|
ctx.logger.blank();
|
|
2767
4146
|
const action = mode === "add-target" ? "Added" : "Installed";
|
|
2768
4147
|
const total = result.counts.installed + result.counts.upgraded + result.counts.unchanged;
|
|
@@ -2919,7 +4298,7 @@ function redactUrl(value) {
|
|
|
2919
4298
|
}
|
|
2920
4299
|
|
|
2921
4300
|
// src/cli/commands/status.ts
|
|
2922
|
-
var
|
|
4301
|
+
var EXIT_OK3 = 0;
|
|
2923
4302
|
var EXIT_BEHIND = 1;
|
|
2924
4303
|
var EXIT_FAILED2 = 2;
|
|
2925
4304
|
function registerStatus(program, getContext) {
|
|
@@ -2967,7 +4346,7 @@ async function runStatusCommand(ctx, scope, concurrency) {
|
|
|
2967
4346
|
}
|
|
2968
4347
|
if (result.counts.failed > 0) return EXIT_FAILED2;
|
|
2969
4348
|
if (result.counts.behind > 0) return EXIT_BEHIND;
|
|
2970
|
-
return
|
|
4349
|
+
return EXIT_OK3;
|
|
2971
4350
|
}
|
|
2972
4351
|
function buildJsonOutcome(outcome) {
|
|
2973
4352
|
if (outcome.verdict === "up-to-date") {
|
|
@@ -3646,11 +5025,11 @@ function formatStateWarning(warning) {
|
|
|
3646
5025
|
return `${warning.file}: inline state '${warning.inline}' overrides rule state '${warning.rule}'`;
|
|
3647
5026
|
}
|
|
3648
5027
|
function getRepoRoot(start = process.cwd()) {
|
|
3649
|
-
const
|
|
5028
|
+
const path9 = resolve(start);
|
|
3650
5029
|
return {
|
|
3651
|
-
path:
|
|
3652
|
-
hasGit: existsSync(resolve(
|
|
3653
|
-
hasPackageJson: existsSync(resolve(
|
|
5030
|
+
path: path9,
|
|
5031
|
+
hasGit: existsSync(resolve(path9, ".git")),
|
|
5032
|
+
hasPackageJson: existsSync(resolve(path9, "package.json"))
|
|
3654
5033
|
};
|
|
3655
5034
|
}
|
|
3656
5035
|
|
|
@@ -3719,7 +5098,7 @@ function maybeRenderHelpVersionBanner(argv) {
|
|
|
3719
5098
|
const wantsVersion = tail.includes("--version") || tail.includes("-V");
|
|
3720
5099
|
if (!wantsHelp && !wantsVersion) return;
|
|
3721
5100
|
const theming = createTheming();
|
|
3722
|
-
const subtitle = wantsVersion ? "v0.1
|
|
5101
|
+
const subtitle = wantsVersion ? "v0.4.1 \xB7 living specs" : "living specs \xB7 v0.4.1";
|
|
3723
5102
|
const banner = bannerForContext(theming, readColumns(), subtitle);
|
|
3724
5103
|
if (banner.length > 0) {
|
|
3725
5104
|
process.stdout.write(`${banner}
|
|
@@ -3737,8 +5116,10 @@ function toExitCode(value) {
|
|
|
3737
5116
|
}
|
|
3738
5117
|
function buildProgram() {
|
|
3739
5118
|
const program = new Command();
|
|
3740
|
-
program.name("maester").description("Aggregate documentation from many sources into one citadel.").version("0.1
|
|
5119
|
+
program.name("maester").description("Aggregate documentation from many sources into one citadel.").version("0.4.1", "-V, --version", "Print the maester version.").option("--verbose", "Show verbose output").option("--quiet", "Suppress all output except errors").option("--json", "Emit machine-readable JSON output (one object per line)").option("--color", "Force colored output (overrides auto-detection)").option("--no-color", "Disable colored output (overrides auto-detection)").option("--theme <theme>", "Theme override: 'dark' or 'light'").option("--no-welcome", "Suppress the first-run welcome banner").enablePositionalOptions(false).allowExcessArguments(false);
|
|
5120
|
+
registerConnector(program, () => buildContext(extractFlags(program.opts())));
|
|
3741
5121
|
registerInit(program, () => buildContext(extractFlags(program.opts())));
|
|
5122
|
+
registerMcp(program, () => buildContext(extractFlags(program.opts())));
|
|
3742
5123
|
registerPublish(program, () => buildContext(extractFlags(program.opts())));
|
|
3743
5124
|
registerSkill(program, () => buildContext(extractFlags(program.opts())));
|
|
3744
5125
|
registerStatus(program, () => buildContext(extractFlags(program.opts())));
|
|
@@ -3773,7 +5154,7 @@ async function runNoArgs(ctx) {
|
|
|
3773
5154
|
const roles = detectRoles(ctx.repoRoot.path);
|
|
3774
5155
|
const showWelcome = !roles.hasCitadel && !roles.hasMaester && !ctx.flags.noWelcome;
|
|
3775
5156
|
if (showWelcome) {
|
|
3776
|
-
const banner = bannerForContext(ctx.theming, readColumns(), "living specs \xB7 v0.1
|
|
5157
|
+
const banner = bannerForContext(ctx.theming, readColumns(), "living specs \xB7 v0.4.1");
|
|
3777
5158
|
if (banner.length > 0) {
|
|
3778
5159
|
process.stdout.write(`${banner}
|
|
3779
5160
|
|