baller-maester 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/cli/main.js +1843 -462
- package/dist/cli/main.js.map +1 -1
- package/dist/index.d.ts +362 -18
- package/dist/index.js +945 -75
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -8,12 +8,384 @@ import { promisify } from 'util';
|
|
|
8
8
|
import { simpleGit } from 'simple-git';
|
|
9
9
|
import picomatch from 'picomatch';
|
|
10
10
|
import matter from 'gray-matter';
|
|
11
|
+
import TOML from '@iarna/toml';
|
|
12
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
11
13
|
|
|
12
14
|
var __defProp = Object.defineProperty;
|
|
13
15
|
var __export = (target, all) => {
|
|
14
16
|
for (var name in all)
|
|
15
17
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
18
|
};
|
|
19
|
+
|
|
20
|
+
// src/core/connectors/types.ts
|
|
21
|
+
var ENVELOPE_SCHEMA_VERSION = 1;
|
|
22
|
+
var CONNECTOR_ERROR_CODES = [
|
|
23
|
+
"missing-env-var",
|
|
24
|
+
"connector-not-found",
|
|
25
|
+
"unknown-operation",
|
|
26
|
+
"invalid-argument",
|
|
27
|
+
"auth-failed",
|
|
28
|
+
"remote-error",
|
|
29
|
+
"internal-error"
|
|
30
|
+
];
|
|
31
|
+
function defineConnectorOperation(opts) {
|
|
32
|
+
return opts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/core/connectors/errors.ts
|
|
36
|
+
var ConnectorError = class extends Error {
|
|
37
|
+
code;
|
|
38
|
+
details;
|
|
39
|
+
constructor(code, message, details) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "ConnectorError";
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.details = details;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/core/connectors/types/gitlab-issues/errors.ts
|
|
48
|
+
function mapGitLabHttpError(input) {
|
|
49
|
+
const { status, body, headers, host, envVarName, context } = input;
|
|
50
|
+
if (status === 401 || status === 403) {
|
|
51
|
+
return new ConnectorError(
|
|
52
|
+
"auth-failed",
|
|
53
|
+
envVarName ? `GitLab rejected the token from ${envVarName} (HTTP ${status}) on ${host}.` : `GitLab returned HTTP ${status} on ${host}.`,
|
|
54
|
+
{
|
|
55
|
+
status,
|
|
56
|
+
...envVarName ? { envVar: envVarName } : {},
|
|
57
|
+
host
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (status === 404) {
|
|
62
|
+
const message = context.iid !== void 0 ? `Issue ${context.iid} not found in project '${context.project}' on ${host}.` : `Project '${context.project}' not found on ${host}.`;
|
|
63
|
+
return new ConnectorError("remote-error", message, {
|
|
64
|
+
kind: "not-found",
|
|
65
|
+
status,
|
|
66
|
+
project: context.project,
|
|
67
|
+
...context.iid !== void 0 ? { iid: context.iid } : {}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (status === 429) {
|
|
71
|
+
const retryAfter = headers.get("retry-after");
|
|
72
|
+
return new ConnectorError(
|
|
73
|
+
"remote-error",
|
|
74
|
+
`GitLab rate-limited the request (HTTP 429) on ${host}.${retryAfter ? ` Retry after ${retryAfter}s.` : ""}`,
|
|
75
|
+
{
|
|
76
|
+
kind: "rate-limited",
|
|
77
|
+
status,
|
|
78
|
+
...retryAfter ? { retryAfter } : {}
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (status >= 500 && status <= 599) {
|
|
83
|
+
return new ConnectorError("remote-error", `GitLab returned HTTP ${status} on ${host}.`, {
|
|
84
|
+
kind: "transport",
|
|
85
|
+
status
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return new ConnectorError("remote-error", `Unexpected HTTP ${status} from GitLab on ${host}.`, {
|
|
89
|
+
kind: "unexpected",
|
|
90
|
+
status,
|
|
91
|
+
body: truncateBody(body)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function mapTransportError(err, host) {
|
|
95
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
96
|
+
return new ConnectorError("remote-error", `Failed to reach GitLab at ${host}: ${message}`, {
|
|
97
|
+
kind: "transport",
|
|
98
|
+
cause: message
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
var BODY_EXCERPT_MAX = 1024;
|
|
102
|
+
function truncateBody(body) {
|
|
103
|
+
if (body.length <= BODY_EXCERPT_MAX) return body;
|
|
104
|
+
return `${body.slice(0, BODY_EXCERPT_MAX)}\u2026`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/core/connectors/types/gitlab-issues/output.ts
|
|
108
|
+
var GITLAB_ISSUES_DATA_SCHEMA = 1;
|
|
109
|
+
function projectIssue(raw) {
|
|
110
|
+
if (typeof raw !== "object" || raw === null) {
|
|
111
|
+
throw new Error("Expected GitLab issue payload to be a JSON object.");
|
|
112
|
+
}
|
|
113
|
+
const r = raw;
|
|
114
|
+
return {
|
|
115
|
+
iid: requireNumber(r.iid, "iid"),
|
|
116
|
+
id: requireNumber(r.id, "id"),
|
|
117
|
+
title: requireString(r.title, "title"),
|
|
118
|
+
description: optionalString(r.description),
|
|
119
|
+
state: requireString(r.state, "state"),
|
|
120
|
+
labels: projectLabels(r.labels),
|
|
121
|
+
assignees: projectAssignees(r.assignees),
|
|
122
|
+
milestone: projectMilestone(r.milestone),
|
|
123
|
+
web_url: requireString(r.web_url, "web_url"),
|
|
124
|
+
created_at: requireString(r.created_at, "created_at"),
|
|
125
|
+
updated_at: requireString(r.updated_at, "updated_at"),
|
|
126
|
+
closed_at: optionalString(r.closed_at)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function requireNumber(value, field) {
|
|
130
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
131
|
+
throw new Error(`Expected GitLab issue field '${field}' to be a number.`);
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
function requireString(value, field) {
|
|
136
|
+
if (typeof value !== "string") {
|
|
137
|
+
throw new Error(`Expected GitLab issue field '${field}' to be a string.`);
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
function optionalString(value) {
|
|
142
|
+
if (value === null || value === void 0) return null;
|
|
143
|
+
if (typeof value !== "string") return null;
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
function projectLabels(value) {
|
|
147
|
+
if (!Array.isArray(value)) return [];
|
|
148
|
+
return value.filter((v) => typeof v === "string").map((v) => v);
|
|
149
|
+
}
|
|
150
|
+
function projectAssignees(value) {
|
|
151
|
+
if (!Array.isArray(value)) return [];
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const entry of value) {
|
|
154
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
155
|
+
const r = entry;
|
|
156
|
+
const username = typeof r.username === "string" ? r.username : null;
|
|
157
|
+
const name = typeof r.name === "string" ? r.name : null;
|
|
158
|
+
if (username === null || name === null) continue;
|
|
159
|
+
out.push({ username, name });
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
function projectMilestone(value) {
|
|
164
|
+
if (typeof value !== "object" || value === null) return null;
|
|
165
|
+
const r = value;
|
|
166
|
+
const title = typeof r.title === "string" ? r.title : null;
|
|
167
|
+
const state = typeof r.state === "string" ? r.state : null;
|
|
168
|
+
if (title === null || state === null) return null;
|
|
169
|
+
return { title, state };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/core/connectors/types/gitlab-issues/client.ts
|
|
173
|
+
var NUMERIC_ID_RE = /^\d+$/;
|
|
174
|
+
async function listIssues(opts, params) {
|
|
175
|
+
const url = buildIssuesListUrl(opts, params);
|
|
176
|
+
const response = await performRequest(opts, url);
|
|
177
|
+
const issuesRaw = await response.json();
|
|
178
|
+
if (!Array.isArray(issuesRaw)) {
|
|
179
|
+
throw new ConnectorError("remote-error", "GitLab list-issues response was not a JSON array.", {
|
|
180
|
+
kind: "unexpected"
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const issues = issuesRaw.map((raw) => projectIssue(raw));
|
|
184
|
+
return {
|
|
185
|
+
issues,
|
|
186
|
+
totalPages: readIntegerHeader(response.headers, "x-total-pages"),
|
|
187
|
+
total: readIntegerHeader(response.headers, "x-total")
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async function getIssue(opts, iid) {
|
|
191
|
+
const url = buildIssueUrl(opts, iid);
|
|
192
|
+
const response = await performRequest(opts, url, { iid });
|
|
193
|
+
const raw = await response.json();
|
|
194
|
+
return projectIssue(raw);
|
|
195
|
+
}
|
|
196
|
+
function buildIssuesListUrl(opts, params) {
|
|
197
|
+
const url = new URL(`${opts.host}/api/v4/projects/${encodeProjectSegment(opts.project)}/issues`);
|
|
198
|
+
url.searchParams.set("state", params.state);
|
|
199
|
+
url.searchParams.set("page", String(params.page));
|
|
200
|
+
url.searchParams.set("per_page", String(params.per_page));
|
|
201
|
+
if (params.labels !== void 0) url.searchParams.set("labels", params.labels);
|
|
202
|
+
if (params.assignee !== void 0) {
|
|
203
|
+
const param = gitlabAssigneeParam(params.assignee);
|
|
204
|
+
url.searchParams.set(param.key, param.value);
|
|
205
|
+
}
|
|
206
|
+
if (params.milestone !== void 0) url.searchParams.set("milestone", params.milestone);
|
|
207
|
+
if (params.search !== void 0) url.searchParams.set("search", params.search);
|
|
208
|
+
return url;
|
|
209
|
+
}
|
|
210
|
+
function buildIssueUrl(opts, iid) {
|
|
211
|
+
return new URL(
|
|
212
|
+
`${opts.host}/api/v4/projects/${encodeProjectSegment(opts.project)}/issues/${iid}`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
function encodeProjectSegment(project) {
|
|
216
|
+
if (NUMERIC_ID_RE.test(project)) return project;
|
|
217
|
+
return encodeURIComponent(project);
|
|
218
|
+
}
|
|
219
|
+
async function performRequest(opts, url, context = {}) {
|
|
220
|
+
const headers = {
|
|
221
|
+
Accept: "application/json"
|
|
222
|
+
};
|
|
223
|
+
if (opts.token !== void 0) {
|
|
224
|
+
headers["PRIVATE-TOKEN"] = opts.token;
|
|
225
|
+
}
|
|
226
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
227
|
+
let response;
|
|
228
|
+
try {
|
|
229
|
+
response = await fetchImpl(url, { method: "GET", headers });
|
|
230
|
+
} catch (err) {
|
|
231
|
+
throw mapTransportError(err, opts.host);
|
|
232
|
+
}
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
const body = await response.text().catch(() => "");
|
|
235
|
+
throw mapGitLabHttpError({
|
|
236
|
+
status: response.status,
|
|
237
|
+
body,
|
|
238
|
+
headers: response.headers,
|
|
239
|
+
host: opts.host,
|
|
240
|
+
envVarName: opts.envVarName,
|
|
241
|
+
context: {
|
|
242
|
+
project: opts.project,
|
|
243
|
+
...context.iid !== void 0 ? { iid: context.iid } : {}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return response;
|
|
248
|
+
}
|
|
249
|
+
function readIntegerHeader(headers, name) {
|
|
250
|
+
const raw = headers.get(name);
|
|
251
|
+
if (raw === null || raw.length === 0) return null;
|
|
252
|
+
const parsed = Number.parseInt(raw, 10);
|
|
253
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
254
|
+
}
|
|
255
|
+
function gitlabAssigneeParam(value) {
|
|
256
|
+
return { key: "assignee_username", value };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/core/connectors/types/gitlab-issues/operations.ts
|
|
260
|
+
var PER_PAGE_CAP = 100;
|
|
261
|
+
var listIssuesArgsSchema = z.object({
|
|
262
|
+
state: z.enum(["opened", "closed", "all"]).default("opened"),
|
|
263
|
+
labels: z.string().min(1).optional(),
|
|
264
|
+
assignee: z.string().min(1).optional(),
|
|
265
|
+
milestone: z.string().min(1).optional(),
|
|
266
|
+
search: z.string().min(1).optional(),
|
|
267
|
+
page: z.coerce.number().int().positive("page must be a positive integer").default(1),
|
|
268
|
+
per_page: z.coerce.number().int().positive("per_page must be a positive integer").default(20)
|
|
269
|
+
}).strict();
|
|
270
|
+
var getIssueArgsSchema = z.object({
|
|
271
|
+
iid: z.coerce.number().int().positive("iid must be a positive integer")
|
|
272
|
+
}).strict();
|
|
273
|
+
var listIssuesOperation = defineConnectorOperation({
|
|
274
|
+
name: "list-issues",
|
|
275
|
+
argsSchema: listIssuesArgsSchema,
|
|
276
|
+
dataSchemaVersion: GITLAB_ISSUES_DATA_SCHEMA,
|
|
277
|
+
handler: async (args, ctx) => {
|
|
278
|
+
const requestedPerPage = args.per_page;
|
|
279
|
+
const clamped = requestedPerPage > PER_PAGE_CAP;
|
|
280
|
+
const effectivePerPage = clamped ? PER_PAGE_CAP : requestedPerPage;
|
|
281
|
+
const params = {
|
|
282
|
+
state: args.state,
|
|
283
|
+
page: args.page,
|
|
284
|
+
per_page: effectivePerPage,
|
|
285
|
+
...args.labels !== void 0 ? { labels: args.labels } : {},
|
|
286
|
+
...args.assignee !== void 0 ? { assignee: args.assignee } : {},
|
|
287
|
+
...args.milestone !== void 0 ? { milestone: args.milestone } : {},
|
|
288
|
+
...args.search !== void 0 ? { search: args.search } : {}
|
|
289
|
+
};
|
|
290
|
+
const client = clientFromContext(ctx);
|
|
291
|
+
const response = await listIssues(client, params);
|
|
292
|
+
return {
|
|
293
|
+
data: {
|
|
294
|
+
issues: response.issues,
|
|
295
|
+
meta: {
|
|
296
|
+
page: args.page,
|
|
297
|
+
per_page: effectivePerPage,
|
|
298
|
+
total_pages: response.totalPages,
|
|
299
|
+
total: response.total,
|
|
300
|
+
clamped
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
var getIssueOperation = defineConnectorOperation({
|
|
307
|
+
name: "get-issue",
|
|
308
|
+
argsSchema: getIssueArgsSchema,
|
|
309
|
+
dataSchemaVersion: GITLAB_ISSUES_DATA_SCHEMA,
|
|
310
|
+
handler: async (args, ctx) => {
|
|
311
|
+
const client = clientFromContext(ctx);
|
|
312
|
+
const issue = await getIssue(client, args.iid);
|
|
313
|
+
return { data: { issue } };
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
function clientFromContext(ctx) {
|
|
317
|
+
return {
|
|
318
|
+
host: ctx.config.host,
|
|
319
|
+
project: ctx.config.project,
|
|
320
|
+
token: ctx.token,
|
|
321
|
+
envVarName: ctx.auth.type === "token" ? ctx.auth.envVar : void 0
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
var DEFAULT_HOST = "https://gitlab.com";
|
|
325
|
+
function isHttpsUrl(value) {
|
|
326
|
+
if (/\s/.test(value)) return false;
|
|
327
|
+
if (!/^https:\/\/[^/]+/i.test(value)) return false;
|
|
328
|
+
try {
|
|
329
|
+
new URL(value);
|
|
330
|
+
return true;
|
|
331
|
+
} catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function normalizeHost(value) {
|
|
336
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
337
|
+
}
|
|
338
|
+
var GitLabIssuesConfigSchema = z.object({
|
|
339
|
+
host: z.string().refine(isHttpsUrl, "host must be an HTTPS URL (e.g. https://gitlab.com)").transform(normalizeHost).optional().default(DEFAULT_HOST),
|
|
340
|
+
project: z.string().min(1, "project is required").refine((v) => !/\s/.test(v), "project must not contain whitespace"),
|
|
341
|
+
apiVersion: z.literal(4).optional().default(4)
|
|
342
|
+
}).strict();
|
|
343
|
+
|
|
344
|
+
// src/core/connectors/types/gitlab-issues/index.ts
|
|
345
|
+
var GITLAB_ISSUES_TYPE_ID = "gitlab-issues";
|
|
346
|
+
var gitlabIssuesType = {
|
|
347
|
+
id: GITLAB_ISSUES_TYPE_ID,
|
|
348
|
+
label: "GitLab Issues",
|
|
349
|
+
configSchema: GitLabIssuesConfigSchema,
|
|
350
|
+
operations: {
|
|
351
|
+
[listIssuesOperation.name]: listIssuesOperation,
|
|
352
|
+
[getIssueOperation.name]: getIssueOperation
|
|
353
|
+
},
|
|
354
|
+
describeTool: (operation, resolvedConfig) => {
|
|
355
|
+
const host = stripScheme(resolvedConfig.host);
|
|
356
|
+
const project = resolvedConfig.project;
|
|
357
|
+
switch (operation.name) {
|
|
358
|
+
case "list-issues":
|
|
359
|
+
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.`;
|
|
360
|
+
case "get-issue":
|
|
361
|
+
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.`;
|
|
362
|
+
default:
|
|
363
|
+
return `GitLab Issues operation '${operation.name}' for project ${project} on ${host}.`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
function stripScheme(host) {
|
|
368
|
+
return host.replace(/^https?:\/\//i, "");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/core/connectors/registry.ts
|
|
372
|
+
var REGISTRY = /* @__PURE__ */ new Map();
|
|
373
|
+
REGISTRY.set(gitlabIssuesType.id, gitlabIssuesType);
|
|
374
|
+
function listConnectorTypes() {
|
|
375
|
+
return [...REGISTRY.values()];
|
|
376
|
+
}
|
|
377
|
+
function lookupConnectorType(id) {
|
|
378
|
+
return REGISTRY.get(id);
|
|
379
|
+
}
|
|
380
|
+
function hasConnectorType(id) {
|
|
381
|
+
return REGISTRY.has(id);
|
|
382
|
+
}
|
|
383
|
+
function registerConnectorType(type) {
|
|
384
|
+
if (REGISTRY.has(type.id)) {
|
|
385
|
+
throw new Error(`Connector type '${type.id}' is already registered.`);
|
|
386
|
+
}
|
|
387
|
+
REGISTRY.set(type.id, type);
|
|
388
|
+
}
|
|
17
389
|
var STATE_VALUES = ["draft", "canon"];
|
|
18
390
|
var StateSchema = z.enum(STATE_VALUES);
|
|
19
391
|
var DEFAULT_STATE = "draft";
|
|
@@ -78,6 +450,13 @@ var SourceSchema = z.object({
|
|
|
78
450
|
description: z.string().min(1).optional(),
|
|
79
451
|
tags: z.array(z.string().min(1).regex(SLUG_RE, "tags must be slugs")).optional()
|
|
80
452
|
}).strict();
|
|
453
|
+
var ConnectorBaseSchema = z.object({
|
|
454
|
+
name: z.string().min(1).regex(SLUG_RE, "name must be a kebab-case slug starting with a letter or digit"),
|
|
455
|
+
type: z.string().min(1),
|
|
456
|
+
auth: AuthRefSchema.optional(),
|
|
457
|
+
description: z.string().min(1).optional(),
|
|
458
|
+
config: z.unknown().optional()
|
|
459
|
+
}).strict();
|
|
81
460
|
var DEFAULT_BASE_DIR = "citadel";
|
|
82
461
|
function applyCombinedInvariants(data, ctx) {
|
|
83
462
|
if (data.sources.length === 0) {
|
|
@@ -116,11 +495,46 @@ function applyCombinedInvariants(data, ctx) {
|
|
|
116
495
|
destsSeen.set(resolved, { index: i, name: entry.name });
|
|
117
496
|
}
|
|
118
497
|
}
|
|
498
|
+
const connectorNames = /* @__PURE__ */ new Map();
|
|
499
|
+
const connectors = data.connectors ?? [];
|
|
500
|
+
for (let i = 0; i < connectors.length; i++) {
|
|
501
|
+
const entry = connectors[i];
|
|
502
|
+
if (!entry?.name) continue;
|
|
503
|
+
const priorIndex = connectorNames.get(entry.name);
|
|
504
|
+
if (priorIndex !== void 0) {
|
|
505
|
+
ctx.addIssue({
|
|
506
|
+
code: z.ZodIssueCode.custom,
|
|
507
|
+
message: `duplicate connector name '${entry.name}' \u2014 also used by connectors[${priorIndex}]`,
|
|
508
|
+
path: ["connectors", i, "name"]
|
|
509
|
+
});
|
|
510
|
+
} else {
|
|
511
|
+
connectorNames.set(entry.name, i);
|
|
512
|
+
}
|
|
513
|
+
const type = lookupConnectorType(entry.type);
|
|
514
|
+
if (!type) {
|
|
515
|
+
ctx.addIssue({
|
|
516
|
+
code: z.ZodIssueCode.custom,
|
|
517
|
+
message: `unknown connector type '${entry.type}' for connector '${entry.name}'`,
|
|
518
|
+
path: ["connectors", i, "type"]
|
|
519
|
+
});
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const configResult = type.configSchema.safeParse(entry.config ?? {});
|
|
523
|
+
if (!configResult.success) {
|
|
524
|
+
for (const issue of configResult.error.issues) {
|
|
525
|
+
ctx.addIssue({
|
|
526
|
+
...issue,
|
|
527
|
+
path: ["connectors", i, "config", ...issue.path]
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
119
532
|
}
|
|
120
533
|
var CitadelConfigSchema = z.object({
|
|
121
534
|
schemaVersion: z.literal(1),
|
|
122
535
|
baseDir: z.string().min(1).refine(isSafeRelativePath, "baseDir must be a repo-relative path with no '..' segments").optional(),
|
|
123
|
-
sources: z.array(SourceSchema).optional().default([])
|
|
536
|
+
sources: z.array(SourceSchema).optional().default([]),
|
|
537
|
+
connectors: z.array(ConnectorBaseSchema).optional()
|
|
124
538
|
}).strict().superRefine((data, ctx) => {
|
|
125
539
|
applyCombinedInvariants(data, ctx);
|
|
126
540
|
});
|
|
@@ -235,26 +649,26 @@ function defaultDestinationFor(repoRoot, sourceName, baseDir) {
|
|
|
235
649
|
|
|
236
650
|
// src/core/config/loader.ts
|
|
237
651
|
async function loadCitadelConfig(repoRoot) {
|
|
238
|
-
const
|
|
239
|
-
if (!existsSync(
|
|
652
|
+
const path8 = citadelConfigPath(repoRoot);
|
|
653
|
+
if (!existsSync(path8)) {
|
|
240
654
|
throw new ConfigError(
|
|
241
655
|
"No citadel.yaml found at the repository root. Run `npx maester init` to create one.",
|
|
242
|
-
{ filePath:
|
|
656
|
+
{ filePath: path8 }
|
|
243
657
|
);
|
|
244
658
|
}
|
|
245
|
-
const raw = await readFile(
|
|
246
|
-
return parseAndValidate(raw, CitadelConfigSchema,
|
|
659
|
+
const raw = await readFile(path8, "utf8");
|
|
660
|
+
return parseAndValidate(raw, CitadelConfigSchema, path8);
|
|
247
661
|
}
|
|
248
662
|
async function loadMaesterConfig(repoRoot) {
|
|
249
|
-
const
|
|
250
|
-
if (!existsSync(
|
|
663
|
+
const path8 = maesterConfigPath(repoRoot);
|
|
664
|
+
if (!existsSync(path8)) {
|
|
251
665
|
throw new ConfigError(
|
|
252
666
|
"No maester.yaml found at the repository root. Run `npx maester publish` to create one.",
|
|
253
|
-
{ filePath:
|
|
667
|
+
{ filePath: path8 }
|
|
254
668
|
);
|
|
255
669
|
}
|
|
256
|
-
const raw = await readFile(
|
|
257
|
-
return parseAndValidate(raw, MaesterConfigSchema,
|
|
670
|
+
const raw = await readFile(path8, "utf8");
|
|
671
|
+
return parseAndValidate(raw, MaesterConfigSchema, path8);
|
|
258
672
|
}
|
|
259
673
|
function parseAndValidate(raw, schema, filePath) {
|
|
260
674
|
const data = parseYaml(raw, filePath);
|
|
@@ -312,7 +726,7 @@ function resolveAuth(auth, env = process.env) {
|
|
|
312
726
|
`${auth.envVar} is not set. Define it in your shell, .env loader, or CI secret manager before syncing.`
|
|
313
727
|
);
|
|
314
728
|
}
|
|
315
|
-
return { type: "token", value };
|
|
729
|
+
return { type: "token", value, envVar: auth.envVar };
|
|
316
730
|
}
|
|
317
731
|
var execFile = promisify(execFile$1);
|
|
318
732
|
var cachedCapabilities;
|
|
@@ -545,10 +959,10 @@ function manifestError(name, reason) {
|
|
|
545
959
|
);
|
|
546
960
|
}
|
|
547
961
|
async function discoverManifestFromCache(cacheDir) {
|
|
548
|
-
const
|
|
549
|
-
if (!existsSync(
|
|
962
|
+
const path8 = resolve(cacheDir, MAESTER_MANIFEST_FILENAME);
|
|
963
|
+
if (!existsSync(path8)) return { mode: "no-manifest", reason: "absent" };
|
|
550
964
|
try {
|
|
551
|
-
const raw = await readFile(
|
|
965
|
+
const raw = await readFile(path8, "utf8");
|
|
552
966
|
const doc = parseDocument(raw);
|
|
553
967
|
if (doc.errors.length > 0) return { mode: "no-manifest", reason: "invalid" };
|
|
554
968
|
const parsed = MaesterConfigSchema.safeParse(doc.toJS({ maxAliasCount: -1 }));
|
|
@@ -885,17 +1299,17 @@ async function applyState(stagedDir, rules) {
|
|
|
885
1299
|
}
|
|
886
1300
|
var PROVENANCE_FILENAME = ".maester-source.json";
|
|
887
1301
|
async function writeProvenanceMarker(destination, marker) {
|
|
888
|
-
const
|
|
1302
|
+
const path8 = resolve(destination, PROVENANCE_FILENAME);
|
|
889
1303
|
const body = `${JSON.stringify(marker, null, 2)}
|
|
890
1304
|
`;
|
|
891
|
-
await writeFile(
|
|
892
|
-
return
|
|
1305
|
+
await writeFile(path8, body, "utf8");
|
|
1306
|
+
return path8;
|
|
893
1307
|
}
|
|
894
1308
|
async function readProvenanceMarker(destination) {
|
|
895
|
-
const
|
|
896
|
-
if (!existsSync(
|
|
1309
|
+
const path8 = resolve(destination, PROVENANCE_FILENAME);
|
|
1310
|
+
if (!existsSync(path8)) return void 0;
|
|
897
1311
|
try {
|
|
898
|
-
const text = await readFile(
|
|
1312
|
+
const text = await readFile(path8, "utf8");
|
|
899
1313
|
const parsed = JSON.parse(text);
|
|
900
1314
|
if (typeof parsed.sourceName !== "string" || typeof parsed.sourceUrl !== "string" || typeof parsed.commitSha !== "string" || !Array.isArray(parsed.filterSet)) {
|
|
901
1315
|
return void 0;
|
|
@@ -1301,7 +1715,7 @@ function renderManagedRegion(body, version) {
|
|
|
1301
1715
|
${inner}
|
|
1302
1716
|
${END_MARKER_LITERAL}`;
|
|
1303
1717
|
}
|
|
1304
|
-
function replaceJsonMaesterKey(existingText,
|
|
1718
|
+
function replaceJsonMaesterKey(existingText, maesterBlock2) {
|
|
1305
1719
|
const parsed = existingText && existingText.trim().length > 0 ? JSON.parse(existingText) : {};
|
|
1306
1720
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1307
1721
|
throw new Error("Expected .claude/settings.json to be a JSON object at the top level.");
|
|
@@ -1310,14 +1724,14 @@ function replaceJsonMaesterKey(existingText, maesterBlock) {
|
|
|
1310
1724
|
let placed = false;
|
|
1311
1725
|
for (const [key, value] of Object.entries(parsed)) {
|
|
1312
1726
|
if (key === "maester") {
|
|
1313
|
-
rebuilt[key] =
|
|
1727
|
+
rebuilt[key] = maesterBlock2;
|
|
1314
1728
|
placed = true;
|
|
1315
1729
|
} else {
|
|
1316
1730
|
rebuilt[key] = value;
|
|
1317
1731
|
}
|
|
1318
1732
|
}
|
|
1319
1733
|
if (!placed) {
|
|
1320
|
-
rebuilt.maester =
|
|
1734
|
+
rebuilt.maester = maesterBlock2;
|
|
1321
1735
|
}
|
|
1322
1736
|
return `${JSON.stringify(rebuilt, null, 2)}
|
|
1323
1737
|
`;
|
|
@@ -1339,6 +1753,9 @@ function readJsonMaesterKey(existingText) {
|
|
|
1339
1753
|
// src/core/skill/templates/content/citadel-awareness.md
|
|
1340
1754
|
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';
|
|
1341
1755
|
|
|
1756
|
+
// src/core/skill/templates/content/connector-policy.md
|
|
1757
|
+
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";
|
|
1758
|
+
|
|
1342
1759
|
// src/core/skill/templates/content/freshness-awareness.md
|
|
1343
1760
|
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';
|
|
1344
1761
|
|
|
@@ -1358,7 +1775,9 @@ function renderClaudeSkillBody(opts) {
|
|
|
1358
1775
|
"",
|
|
1359
1776
|
interpolate(state_awareness_default, opts),
|
|
1360
1777
|
"",
|
|
1361
|
-
interpolate(freshness_awareness_default, opts)
|
|
1778
|
+
interpolate(freshness_awareness_default, opts),
|
|
1779
|
+
"",
|
|
1780
|
+
interpolate(connector_policy_default, opts)
|
|
1362
1781
|
].join("\n");
|
|
1363
1782
|
}
|
|
1364
1783
|
function renderClaudeSkillFile(body) {
|
|
@@ -1458,53 +1877,66 @@ function combineActions(a, b) {
|
|
|
1458
1877
|
return "unchanged";
|
|
1459
1878
|
}
|
|
1460
1879
|
|
|
1461
|
-
// src/core/skill/templates/shells/
|
|
1462
|
-
var
|
|
1463
|
-
|
|
1464
|
-
This file contains agent instructions for working in this repository. The
|
|
1465
|
-
section between the maester managed-region markers is generated by
|
|
1466
|
-
\`maester skill install\` and refreshed by \`maester skill upgrade\`. Anything
|
|
1467
|
-
you write outside that region is preserved across upgrades.
|
|
1468
|
-
`;
|
|
1469
|
-
function renderAgentsMdBody(opts) {
|
|
1880
|
+
// src/core/skill/templates/shells/codex.ts
|
|
1881
|
+
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.";
|
|
1882
|
+
function renderCodexSkillBody(opts) {
|
|
1470
1883
|
return [
|
|
1471
|
-
"# Grand Maester
|
|
1884
|
+
"# Grand Maester (Codex CLI skill)",
|
|
1472
1885
|
"",
|
|
1473
|
-
"
|
|
1474
|
-
|
|
1475
|
-
"guidance below.",
|
|
1886
|
+
"Use this guidance whenever you read files under the citadel base directory",
|
|
1887
|
+
`(\`${opts.baseDir}/\`) in this repository.`,
|
|
1476
1888
|
"",
|
|
1477
1889
|
interpolate2(citadel_awareness_default, opts),
|
|
1478
1890
|
"",
|
|
1479
1891
|
interpolate2(state_awareness_default, opts),
|
|
1480
1892
|
"",
|
|
1481
|
-
interpolate2(freshness_awareness_default, opts)
|
|
1893
|
+
interpolate2(freshness_awareness_default, opts),
|
|
1894
|
+
"",
|
|
1895
|
+
interpolate2(connector_policy_default, opts)
|
|
1482
1896
|
].join("\n");
|
|
1483
1897
|
}
|
|
1484
|
-
function
|
|
1485
|
-
return
|
|
1898
|
+
function renderCodexSkillFile(body) {
|
|
1899
|
+
return [
|
|
1900
|
+
"---",
|
|
1901
|
+
"name: grand-maester",
|
|
1902
|
+
`description: ${SKILL_FRONTMATTER_DESCRIPTION2}`,
|
|
1903
|
+
"---",
|
|
1904
|
+
"",
|
|
1905
|
+
body
|
|
1906
|
+
].join("\n");
|
|
1486
1907
|
}
|
|
1487
1908
|
function interpolate2(template, opts) {
|
|
1488
1909
|
return template.replace(/\{\{baseDir\}\}/g, opts.baseDir);
|
|
1489
1910
|
}
|
|
1490
1911
|
|
|
1491
|
-
// src/core/skill/targets/
|
|
1492
|
-
var
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1912
|
+
// src/core/skill/targets/codex.ts
|
|
1913
|
+
var SKILL_MD_PATH2 = ".agents/skills/grand-maester/SKILL.md";
|
|
1914
|
+
var codexTarget = {
|
|
1915
|
+
id: "codex",
|
|
1916
|
+
label: "Codex CLI",
|
|
1917
|
+
artifactPaths: [SKILL_MD_PATH2],
|
|
1918
|
+
writerKey: "codex",
|
|
1919
|
+
write: writeCodex,
|
|
1920
|
+
readInstalledVersion: readInstalledVersion2
|
|
1921
|
+
};
|
|
1922
|
+
async function writeCodex(input) {
|
|
1923
|
+
const filePath = path.join(input.repoRoot, SKILL_MD_PATH2);
|
|
1924
|
+
await promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
1925
|
+
const existing = await readTextOrUndefined2(filePath);
|
|
1926
|
+
const previousVersion = existing ? extractMarkdownRegion(existing)?.version : void 0;
|
|
1927
|
+
const body = renderCodexSkillBody({ baseDir: input.citadelBaseDir });
|
|
1928
|
+
const managedRegion = replaceMarkdownRegion(void 0, body, input.skillVersion).trimEnd();
|
|
1929
|
+
const fileContent = existing ? replaceMarkdownRegion(existing, body, input.skillVersion) : `${renderCodexSkillFile(managedRegion)}
|
|
1930
|
+
`;
|
|
1931
|
+
const action = decideAction(existing, previousVersion, input.skillVersion, fileContent);
|
|
1500
1932
|
if (action === "unchanged") {
|
|
1501
1933
|
return previousVersion !== void 0 ? { action, installedVersion: previousVersion } : { action };
|
|
1502
1934
|
}
|
|
1503
|
-
await promises.writeFile(filePath,
|
|
1935
|
+
await promises.writeFile(filePath, fileContent, "utf8");
|
|
1504
1936
|
return { action, installedVersion: input.skillVersion };
|
|
1505
1937
|
}
|
|
1506
|
-
async function
|
|
1507
|
-
const filePath = path.join(repoRoot,
|
|
1938
|
+
async function readInstalledVersion2(repoRoot) {
|
|
1939
|
+
const filePath = path.join(repoRoot, SKILL_MD_PATH2);
|
|
1508
1940
|
const text = await readTextOrUndefined2(filePath);
|
|
1509
1941
|
if (!text) return void 0;
|
|
1510
1942
|
return extractMarkdownRegion(text)?.version;
|
|
@@ -1525,16 +1957,6 @@ function decideAction(existing, previousVersion, newVersion, newContent) {
|
|
|
1525
1957
|
return "upgraded";
|
|
1526
1958
|
}
|
|
1527
1959
|
|
|
1528
|
-
// src/core/skill/targets/codex.ts
|
|
1529
|
-
var codexTarget = {
|
|
1530
|
-
id: "codex",
|
|
1531
|
-
label: "Codex CLI",
|
|
1532
|
-
artifactPaths: [AGENTS_MD_ARTIFACT_PATH],
|
|
1533
|
-
writerKey: "agents-md",
|
|
1534
|
-
write: writeAgentsMd,
|
|
1535
|
-
readInstalledVersion: readAgentsMdInstalledVersion
|
|
1536
|
-
};
|
|
1537
|
-
|
|
1538
1960
|
// src/core/skill/templates/shells/cursor.ts
|
|
1539
1961
|
var DESCRIPTION = "Citadel-aware guidance for reading aggregated documentation under the citadel base directory.";
|
|
1540
1962
|
function renderCursorRuleBody(opts) {
|
|
@@ -1548,7 +1970,9 @@ function renderCursorRuleBody(opts) {
|
|
|
1548
1970
|
"",
|
|
1549
1971
|
interpolate3(state_awareness_default, opts),
|
|
1550
1972
|
"",
|
|
1551
|
-
interpolate3(freshness_awareness_default, opts)
|
|
1973
|
+
interpolate3(freshness_awareness_default, opts),
|
|
1974
|
+
"",
|
|
1975
|
+
interpolate3(connector_policy_default, opts)
|
|
1552
1976
|
].join("\n");
|
|
1553
1977
|
}
|
|
1554
1978
|
function renderCursorRuleFile(body, opts) {
|
|
@@ -1574,7 +1998,7 @@ var cursorTarget = {
|
|
|
1574
1998
|
artifactPaths: [CURSOR_RULE_PATH],
|
|
1575
1999
|
writerKey: "cursor",
|
|
1576
2000
|
write: writeCursor,
|
|
1577
|
-
readInstalledVersion:
|
|
2001
|
+
readInstalledVersion: readInstalledVersion3
|
|
1578
2002
|
};
|
|
1579
2003
|
async function writeCursor(input) {
|
|
1580
2004
|
const filePath = path.join(input.repoRoot, CURSOR_RULE_PATH);
|
|
@@ -1596,7 +2020,7 @@ async function writeCursor(input) {
|
|
|
1596
2020
|
await promises.writeFile(filePath, next, "utf8");
|
|
1597
2021
|
return { action, installedVersion: input.skillVersion };
|
|
1598
2022
|
}
|
|
1599
|
-
async function
|
|
2023
|
+
async function readInstalledVersion3(repoRoot) {
|
|
1600
2024
|
const filePath = path.join(repoRoot, CURSOR_RULE_PATH);
|
|
1601
2025
|
const text = await readTextOrUndefined3(filePath);
|
|
1602
2026
|
if (!text) return void 0;
|
|
@@ -1618,6 +2042,78 @@ function decideAction2(existing, previousVersion, newVersion, newContent) {
|
|
|
1618
2042
|
return "upgraded";
|
|
1619
2043
|
}
|
|
1620
2044
|
|
|
2045
|
+
// src/core/skill/templates/content/connector-policy-fallback.md
|
|
2046
|
+
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 maester connector list\nnpx 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";
|
|
2047
|
+
|
|
2048
|
+
// src/core/skill/templates/shells/agents-md.ts
|
|
2049
|
+
var PREAMBLE = `# AGENTS.md
|
|
2050
|
+
|
|
2051
|
+
This file contains agent instructions for working in this repository. The
|
|
2052
|
+
section between the maester managed-region markers is generated by
|
|
2053
|
+
\`maester skill install\` and refreshed by \`maester skill upgrade\`. Anything
|
|
2054
|
+
you write outside that region is preserved across upgrades.
|
|
2055
|
+
`;
|
|
2056
|
+
function renderAgentsMdBody(opts) {
|
|
2057
|
+
return [
|
|
2058
|
+
"# Grand Maester guidance",
|
|
2059
|
+
"",
|
|
2060
|
+
"This repository is set up to aggregate documentation from remote sources",
|
|
2061
|
+
"into a local citadel. When you reason about citadel content, follow the",
|
|
2062
|
+
"guidance below.",
|
|
2063
|
+
"",
|
|
2064
|
+
interpolate4(citadel_awareness_default, opts),
|
|
2065
|
+
"",
|
|
2066
|
+
interpolate4(state_awareness_default, opts),
|
|
2067
|
+
"",
|
|
2068
|
+
interpolate4(freshness_awareness_default, opts),
|
|
2069
|
+
"",
|
|
2070
|
+
interpolate4(connector_policy_fallback_default, opts)
|
|
2071
|
+
].join("\n");
|
|
2072
|
+
}
|
|
2073
|
+
function agentsMdPreamble() {
|
|
2074
|
+
return PREAMBLE;
|
|
2075
|
+
}
|
|
2076
|
+
function interpolate4(template, opts) {
|
|
2077
|
+
return template.replace(/\{\{baseDir\}\}/g, opts.baseDir);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// src/core/skill/targets/agents-md-writer.ts
|
|
2081
|
+
var AGENTS_MD_ARTIFACT_PATH = "AGENTS.md";
|
|
2082
|
+
async function writeAgentsMd(input) {
|
|
2083
|
+
const filePath = path.join(input.repoRoot, AGENTS_MD_ARTIFACT_PATH);
|
|
2084
|
+
const existingText = await readTextOrUndefined4(filePath);
|
|
2085
|
+
const previousVersion = existingText ? extractMarkdownRegion(existingText)?.version : void 0;
|
|
2086
|
+
const body = renderAgentsMdBody({ baseDir: input.citadelBaseDir });
|
|
2087
|
+
const next = replaceMarkdownRegion(existingText, body, input.skillVersion, agentsMdPreamble());
|
|
2088
|
+
const action = decideAction3(existingText, previousVersion, input.skillVersion, next);
|
|
2089
|
+
if (action === "unchanged") {
|
|
2090
|
+
return previousVersion !== void 0 ? { action, installedVersion: previousVersion } : { action };
|
|
2091
|
+
}
|
|
2092
|
+
await promises.writeFile(filePath, next, "utf8");
|
|
2093
|
+
return { action, installedVersion: input.skillVersion };
|
|
2094
|
+
}
|
|
2095
|
+
async function readAgentsMdInstalledVersion(repoRoot) {
|
|
2096
|
+
const filePath = path.join(repoRoot, AGENTS_MD_ARTIFACT_PATH);
|
|
2097
|
+
const text = await readTextOrUndefined4(filePath);
|
|
2098
|
+
if (!text) return void 0;
|
|
2099
|
+
return extractMarkdownRegion(text)?.version;
|
|
2100
|
+
}
|
|
2101
|
+
async function readTextOrUndefined4(filePath) {
|
|
2102
|
+
try {
|
|
2103
|
+
return await promises.readFile(filePath, "utf8");
|
|
2104
|
+
} catch (err) {
|
|
2105
|
+
if (err.code === "ENOENT") return void 0;
|
|
2106
|
+
throw err;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
function decideAction3(existing, previousVersion, newVersion, newContent) {
|
|
2110
|
+
if (existing === void 0) return "installed";
|
|
2111
|
+
if (existing === newContent) return "unchanged";
|
|
2112
|
+
if (previousVersion === void 0) return "installed";
|
|
2113
|
+
if (previousVersion !== newVersion) return "upgraded";
|
|
2114
|
+
return "upgraded";
|
|
2115
|
+
}
|
|
2116
|
+
|
|
1621
2117
|
// src/core/skill/targets/generic.ts
|
|
1622
2118
|
var genericTarget = {
|
|
1623
2119
|
id: "agents-md",
|
|
@@ -1629,20 +2125,20 @@ var genericTarget = {
|
|
|
1629
2125
|
};
|
|
1630
2126
|
|
|
1631
2127
|
// src/core/skill/targets/index.ts
|
|
1632
|
-
var
|
|
2128
|
+
var REGISTRY2 = [
|
|
1633
2129
|
claudeCodeTarget,
|
|
1634
2130
|
codexTarget,
|
|
1635
2131
|
cursorTarget,
|
|
1636
2132
|
genericTarget
|
|
1637
2133
|
];
|
|
1638
2134
|
function listSkillTargets() {
|
|
1639
|
-
return
|
|
2135
|
+
return REGISTRY2;
|
|
1640
2136
|
}
|
|
1641
2137
|
function getTarget(id) {
|
|
1642
|
-
const found =
|
|
2138
|
+
const found = REGISTRY2.find((t) => t.id === id);
|
|
1643
2139
|
if (!found) {
|
|
1644
2140
|
throw new Error(
|
|
1645
|
-
`Unknown skill target '${id}'. Supported: ${
|
|
2141
|
+
`Unknown skill target '${id}'. Supported: ${REGISTRY2.map((t) => t.id).join(", ")}`
|
|
1646
2142
|
);
|
|
1647
2143
|
}
|
|
1648
2144
|
return found;
|
|
@@ -1667,15 +2163,188 @@ function dedupeTargets(targets) {
|
|
|
1667
2163
|
return [...groups.values()];
|
|
1668
2164
|
}
|
|
1669
2165
|
|
|
2166
|
+
// src/core/mcp/registrations/command.ts
|
|
2167
|
+
function resolveMaesterLaunchCommand() {
|
|
2168
|
+
return { command: "npx", args: ["-y", "baller-maester", "mcp"] };
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// src/core/mcp/registrations/claude-code.ts
|
|
2172
|
+
var MCP_FILE = ".mcp.json";
|
|
2173
|
+
function maesterEntry(launch) {
|
|
2174
|
+
return { command: launch.command, args: [...launch.args] };
|
|
2175
|
+
}
|
|
2176
|
+
async function writeClaudeCodeMcpEntry(repoRoot, options = {}) {
|
|
2177
|
+
const launch = options.launch ?? resolveMaesterLaunchCommand();
|
|
2178
|
+
return writeJsonMcpFile(path.join(repoRoot, MCP_FILE), launch);
|
|
2179
|
+
}
|
|
2180
|
+
async function writeJsonMcpFile(filePath, launch) {
|
|
2181
|
+
await promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
2182
|
+
const existingText = await readOrUndefined(filePath);
|
|
2183
|
+
const newText = renderJsonWithMaesterEntry(existingText, launch);
|
|
2184
|
+
if (existingText === newText) {
|
|
2185
|
+
return { filePath, action: "unchanged" };
|
|
2186
|
+
}
|
|
2187
|
+
await promises.writeFile(filePath, newText, "utf8");
|
|
2188
|
+
return { filePath, action: "written" };
|
|
2189
|
+
}
|
|
2190
|
+
function renderJsonWithMaesterEntry(existingText, launch) {
|
|
2191
|
+
const parsed = parseOrEmpty(existingText);
|
|
2192
|
+
const rebuilt = {};
|
|
2193
|
+
let placed = false;
|
|
2194
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
2195
|
+
if (key === "mcpServers") {
|
|
2196
|
+
rebuilt[key] = mutateMcpServers(value, launch);
|
|
2197
|
+
placed = true;
|
|
2198
|
+
} else {
|
|
2199
|
+
rebuilt[key] = value;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
if (!placed) {
|
|
2203
|
+
rebuilt.mcpServers = mutateMcpServers(void 0, launch);
|
|
2204
|
+
}
|
|
2205
|
+
return `${JSON.stringify(rebuilt, null, 2)}
|
|
2206
|
+
`;
|
|
2207
|
+
}
|
|
2208
|
+
function mutateMcpServers(existing, launch) {
|
|
2209
|
+
const map = isPlainObject(existing) ? { ...existing } : {};
|
|
2210
|
+
const rebuilt = {};
|
|
2211
|
+
let placed = false;
|
|
2212
|
+
for (const [key, value] of Object.entries(map)) {
|
|
2213
|
+
if (key === "maester") {
|
|
2214
|
+
rebuilt[key] = maesterEntry(launch);
|
|
2215
|
+
placed = true;
|
|
2216
|
+
} else {
|
|
2217
|
+
rebuilt[key] = value;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
if (!placed) {
|
|
2221
|
+
rebuilt.maester = maesterEntry(launch);
|
|
2222
|
+
}
|
|
2223
|
+
return rebuilt;
|
|
2224
|
+
}
|
|
2225
|
+
function parseOrEmpty(text) {
|
|
2226
|
+
if (!text || text.trim().length === 0) return {};
|
|
2227
|
+
const parsed = JSON.parse(text);
|
|
2228
|
+
if (!isPlainObject(parsed)) {
|
|
2229
|
+
throw new Error("Expected MCP config to be a JSON object at the top level.");
|
|
2230
|
+
}
|
|
2231
|
+
return parsed;
|
|
2232
|
+
}
|
|
2233
|
+
function isPlainObject(value) {
|
|
2234
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2235
|
+
}
|
|
2236
|
+
async function readOrUndefined(filePath) {
|
|
2237
|
+
try {
|
|
2238
|
+
return await promises.readFile(filePath, "utf8");
|
|
2239
|
+
} catch (err) {
|
|
2240
|
+
if (err.code === "ENOENT") return void 0;
|
|
2241
|
+
throw err;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
var CONFIG_FILE = path.join(".codex", "config.toml");
|
|
2245
|
+
function maesterBlock(repoRoot, launch) {
|
|
2246
|
+
return { command: launch.command, args: [...launch.args], cwd: repoRoot };
|
|
2247
|
+
}
|
|
2248
|
+
async function writeCodexMcpEntry(repoRoot, options = {}) {
|
|
2249
|
+
const filePath = path.join(repoRoot, CONFIG_FILE);
|
|
2250
|
+
await promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
2251
|
+
const existingText = await readOrUndefined2(filePath);
|
|
2252
|
+
const launch = options.launch ?? resolveMaesterLaunchCommand();
|
|
2253
|
+
const newText = renderTomlWithMaesterBlock(existingText, repoRoot, launch);
|
|
2254
|
+
if (existingText === newText) {
|
|
2255
|
+
return { filePath, action: "unchanged" };
|
|
2256
|
+
}
|
|
2257
|
+
await promises.writeFile(filePath, newText, "utf8");
|
|
2258
|
+
return { filePath, action: "written" };
|
|
2259
|
+
}
|
|
2260
|
+
function renderTomlWithMaesterBlock(existingText, repoRoot, launch) {
|
|
2261
|
+
const parsed = existingText && existingText.trim().length > 0 ? TOML.parse(existingText) : {};
|
|
2262
|
+
const mcpServers = isJsonMap(parsed.mcp_servers) ? { ...parsed.mcp_servers } : {};
|
|
2263
|
+
mcpServers.maester = maesterBlock(repoRoot, launch);
|
|
2264
|
+
const next = { ...parsed, mcp_servers: mcpServers };
|
|
2265
|
+
return TOML.stringify(next);
|
|
2266
|
+
}
|
|
2267
|
+
function isJsonMap(value) {
|
|
2268
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2269
|
+
}
|
|
2270
|
+
async function readOrUndefined2(filePath) {
|
|
2271
|
+
try {
|
|
2272
|
+
return await promises.readFile(filePath, "utf8");
|
|
2273
|
+
} catch (err) {
|
|
2274
|
+
if (err.code === "ENOENT") return void 0;
|
|
2275
|
+
throw err;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
var MCP_FILE2 = path.join(".cursor", "mcp.json");
|
|
2279
|
+
async function writeCursorMcpEntry(repoRoot, options = {}) {
|
|
2280
|
+
const launch = options.launch ?? resolveMaesterLaunchCommand();
|
|
2281
|
+
return writeJsonMcpFile(path.join(repoRoot, MCP_FILE2), launch);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// src/core/mcp/registrations/index.ts
|
|
2285
|
+
async function refreshMcpRegistrations(repoRoot, options = {}) {
|
|
2286
|
+
const targets = listSkillTargets().filter(
|
|
2287
|
+
(t) => isMcpHost(t.id) && (!options.scopeTo || options.scopeTo.includes(t.id))
|
|
2288
|
+
);
|
|
2289
|
+
const outcomes = [];
|
|
2290
|
+
for (const target of targets) {
|
|
2291
|
+
const installedVersion = await target.readInstalledVersion(repoRoot);
|
|
2292
|
+
if (installedVersion === void 0 && !options.scopeTo?.includes(target.id)) {
|
|
2293
|
+
continue;
|
|
2294
|
+
}
|
|
2295
|
+
const outcome = await runWriter(target, repoRoot);
|
|
2296
|
+
outcomes.push(outcome);
|
|
2297
|
+
}
|
|
2298
|
+
return outcomes;
|
|
2299
|
+
}
|
|
2300
|
+
function isMcpHost(id) {
|
|
2301
|
+
return id === "claude-code" || id === "cursor" || id === "codex";
|
|
2302
|
+
}
|
|
2303
|
+
async function runWriter(target, repoRoot) {
|
|
2304
|
+
try {
|
|
2305
|
+
switch (target.id) {
|
|
2306
|
+
case "claude-code": {
|
|
2307
|
+
const r = await writeClaudeCodeMcpEntry(repoRoot);
|
|
2308
|
+
return { host: "claude-code", filePath: r.filePath, action: r.action };
|
|
2309
|
+
}
|
|
2310
|
+
case "cursor": {
|
|
2311
|
+
const r = await writeCursorMcpEntry(repoRoot);
|
|
2312
|
+
return { host: "cursor", filePath: r.filePath, action: r.action };
|
|
2313
|
+
}
|
|
2314
|
+
case "codex": {
|
|
2315
|
+
const r = await writeCodexMcpEntry(repoRoot);
|
|
2316
|
+
return { host: "codex", filePath: r.filePath, action: r.action };
|
|
2317
|
+
}
|
|
2318
|
+
default:
|
|
2319
|
+
return {
|
|
2320
|
+
host: target.id,
|
|
2321
|
+
filePath: "",
|
|
2322
|
+
action: "skipped"
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
} catch (err) {
|
|
2326
|
+
return {
|
|
2327
|
+
host: target.id,
|
|
2328
|
+
filePath: "",
|
|
2329
|
+
action: "failed",
|
|
2330
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
1670
2335
|
// package.json
|
|
1671
2336
|
var package_default = {
|
|
1672
|
-
version: "0.
|
|
2337
|
+
version: "0.4.0"};
|
|
1673
2338
|
var PACKAGE_VERSION = package_default.version;
|
|
1674
2339
|
|
|
1675
2340
|
// src/core/skill/version.ts
|
|
1676
2341
|
var SKILL_VERSION = PACKAGE_VERSION;
|
|
1677
2342
|
|
|
1678
2343
|
// src/core/skill/runner.ts
|
|
2344
|
+
var MCP_HOST_IDS = ["claude-code", "cursor", "codex"];
|
|
2345
|
+
function selectMcpHosts(targetIds) {
|
|
2346
|
+
return targetIds.filter((id) => MCP_HOST_IDS.includes(id));
|
|
2347
|
+
}
|
|
1679
2348
|
async function runSkillInstall(repoRoot, opts) {
|
|
1680
2349
|
if (opts.targets.length === 0) {
|
|
1681
2350
|
throw new Error("At least one target id must be supplied.");
|
|
@@ -1703,12 +2372,14 @@ async function runSkillInstall(repoRoot, opts) {
|
|
|
1703
2372
|
});
|
|
1704
2373
|
}
|
|
1705
2374
|
}
|
|
1706
|
-
|
|
2375
|
+
const mcpHosts = selectMcpHosts(opts.targets);
|
|
2376
|
+
const mcpRegistrations = mcpHosts.length > 0 ? await refreshMcpRegistrations(repoRoot, { scopeTo: mcpHosts }) : [];
|
|
2377
|
+
return { outcomes, counts: countOutcomes(outcomes), mcpRegistrations };
|
|
1707
2378
|
}
|
|
1708
2379
|
async function runSkillUpgrade(repoRoot, opts) {
|
|
1709
2380
|
const installedGroups = await findInstalledGroups(repoRoot);
|
|
1710
2381
|
if (installedGroups.length === 0) {
|
|
1711
|
-
return { outcomes: [], counts: countOutcomes([]) };
|
|
2382
|
+
return { outcomes: [], counts: countOutcomes([]), mcpRegistrations: [] };
|
|
1712
2383
|
}
|
|
1713
2384
|
const outcomes = [];
|
|
1714
2385
|
for (const group of installedGroups) {
|
|
@@ -1749,7 +2420,8 @@ async function runSkillUpgrade(repoRoot, opts) {
|
|
|
1749
2420
|
});
|
|
1750
2421
|
}
|
|
1751
2422
|
}
|
|
1752
|
-
|
|
2423
|
+
const mcpRegistrations = opts.check === true ? [] : await refreshMcpRegistrations(repoRoot);
|
|
2424
|
+
return { outcomes, counts: countOutcomes(outcomes), mcpRegistrations };
|
|
1753
2425
|
}
|
|
1754
2426
|
async function runSkillStatus(repoRoot) {
|
|
1755
2427
|
const outcomes = [];
|
|
@@ -1814,6 +2486,204 @@ function countOutcomes(outcomes) {
|
|
|
1814
2486
|
return { installed, upgraded, unchanged, failed };
|
|
1815
2487
|
}
|
|
1816
2488
|
|
|
1817
|
-
|
|
2489
|
+
// src/core/connectors/envelope.ts
|
|
2490
|
+
function buildSuccessEnvelope(input) {
|
|
2491
|
+
return {
|
|
2492
|
+
schema: ENVELOPE_SCHEMA_VERSION,
|
|
2493
|
+
connector: input.connector,
|
|
2494
|
+
operation: input.operation,
|
|
2495
|
+
ok: true,
|
|
2496
|
+
data: { ...input.data, dataSchema: input.dataSchemaVersion }
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
function buildFailureEnvelope(input) {
|
|
2500
|
+
return {
|
|
2501
|
+
schema: ENVELOPE_SCHEMA_VERSION,
|
|
2502
|
+
connector: input.connector,
|
|
2503
|
+
operation: input.operation,
|
|
2504
|
+
ok: false,
|
|
2505
|
+
error: {
|
|
2506
|
+
code: input.code,
|
|
2507
|
+
message: input.message,
|
|
2508
|
+
...input.details ? { details: input.details } : {}
|
|
2509
|
+
}
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
function argsSchemaToJsonSchema(schema) {
|
|
2513
|
+
const raw = zodToJsonSchema(schema, {
|
|
2514
|
+
$refStrategy: "none",
|
|
2515
|
+
target: "jsonSchema7"
|
|
2516
|
+
});
|
|
2517
|
+
const {
|
|
2518
|
+
$schema: _omitSchema,
|
|
2519
|
+
definitions: _omitDefs,
|
|
2520
|
+
...rest
|
|
2521
|
+
} = raw;
|
|
2522
|
+
return rest;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// src/core/connectors/tool-name.ts
|
|
2526
|
+
var VALID_TOOL_NAME_RE = /^[a-z][a-z0-9_]*$/;
|
|
2527
|
+
function toolName(connectorName, operationName) {
|
|
2528
|
+
const left = normalize(connectorName);
|
|
2529
|
+
const right = normalize(operationName);
|
|
2530
|
+
const result = `${left}__${right}`;
|
|
2531
|
+
if (!VALID_TOOL_NAME_RE.test(result)) {
|
|
2532
|
+
throw new Error(
|
|
2533
|
+
`Invalid MCP tool name '${result}' (from connector '${connectorName}', operation '${operationName}'). Names must match /^[a-z][a-z0-9_]*$/ after normalization.`
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2536
|
+
return result;
|
|
2537
|
+
}
|
|
2538
|
+
function normalize(part) {
|
|
2539
|
+
return part.toLowerCase().replace(/-/g, "_");
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
// src/core/connectors/dispatch.ts
|
|
2543
|
+
async function invokeOperation(input) {
|
|
2544
|
+
const { connector, operationName, args, env } = input;
|
|
2545
|
+
const type = lookupConnectorType(connector.type);
|
|
2546
|
+
if (!type) {
|
|
2547
|
+
return buildFailureEnvelope({
|
|
2548
|
+
connector: connector.name,
|
|
2549
|
+
operation: operationName,
|
|
2550
|
+
code: "connector-not-found",
|
|
2551
|
+
message: `No registered connector type '${connector.type}' for connector '${connector.name}'.`
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
const operation = type.operations[operationName];
|
|
2555
|
+
if (!operation) {
|
|
2556
|
+
const known = Object.keys(type.operations).sort();
|
|
2557
|
+
return buildFailureEnvelope({
|
|
2558
|
+
connector: connector.name,
|
|
2559
|
+
operation: operationName,
|
|
2560
|
+
code: "unknown-operation",
|
|
2561
|
+
message: `Connector '${connector.name}' (type '${type.id}') has no operation '${operationName}'. Known operations: ${known.join(", ") || "(none)"}.`
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
let resolvedConfig;
|
|
2565
|
+
try {
|
|
2566
|
+
resolvedConfig = type.configSchema.parse(connector.config);
|
|
2567
|
+
} catch (err) {
|
|
2568
|
+
return buildFailureEnvelope({
|
|
2569
|
+
connector: connector.name,
|
|
2570
|
+
operation: operationName,
|
|
2571
|
+
code: "invalid-argument",
|
|
2572
|
+
message: `Connector '${connector.name}' has invalid per-type config for type '${type.id}'.`,
|
|
2573
|
+
details: { cause: zodIssueDetails(err) }
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
let parsedArgs;
|
|
2577
|
+
try {
|
|
2578
|
+
parsedArgs = operation.argsSchema.parse(args ?? {});
|
|
2579
|
+
} catch (err) {
|
|
2580
|
+
return buildFailureEnvelope({
|
|
2581
|
+
connector: connector.name,
|
|
2582
|
+
operation: operationName,
|
|
2583
|
+
code: "invalid-argument",
|
|
2584
|
+
message: `Invalid arguments for operation '${operationName}'.`,
|
|
2585
|
+
details: { cause: zodIssueDetails(err) }
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
let auth;
|
|
2589
|
+
try {
|
|
2590
|
+
auth = resolveAuth(connector.auth, env ?? process.env);
|
|
2591
|
+
} catch (err) {
|
|
2592
|
+
if (err instanceof AuthError) {
|
|
2593
|
+
return buildFailureEnvelope({
|
|
2594
|
+
connector: connector.name,
|
|
2595
|
+
operation: operationName,
|
|
2596
|
+
code: "missing-env-var",
|
|
2597
|
+
message: `Environment variable '${err.envVar}' is not set; required by connector '${connector.name}'.`,
|
|
2598
|
+
details: { envVar: err.envVar }
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
throw err;
|
|
2602
|
+
}
|
|
2603
|
+
try {
|
|
2604
|
+
const result = await operation.handler(parsedArgs, {
|
|
2605
|
+
config: resolvedConfig,
|
|
2606
|
+
token: auth.type === "token" ? auth.value : void 0,
|
|
2607
|
+
auth
|
|
2608
|
+
});
|
|
2609
|
+
return buildSuccessEnvelope({
|
|
2610
|
+
connector: connector.name,
|
|
2611
|
+
operation: operationName,
|
|
2612
|
+
data: result.data,
|
|
2613
|
+
dataSchemaVersion: operation.dataSchemaVersion
|
|
2614
|
+
});
|
|
2615
|
+
} catch (err) {
|
|
2616
|
+
if (err instanceof ConnectorError) {
|
|
2617
|
+
return buildFailureEnvelope({
|
|
2618
|
+
connector: connector.name,
|
|
2619
|
+
operation: operationName,
|
|
2620
|
+
code: err.code,
|
|
2621
|
+
message: err.message,
|
|
2622
|
+
...err.details ? { details: err.details } : {}
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
process.stderr.write(
|
|
2626
|
+
`[maester] internal error in connector '${connector.name}'.${operationName}: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
2627
|
+
`
|
|
2628
|
+
);
|
|
2629
|
+
return buildFailureEnvelope({
|
|
2630
|
+
connector: connector.name,
|
|
2631
|
+
operation: operationName,
|
|
2632
|
+
code: "internal-error",
|
|
2633
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
function buildToolDescription(connector, operation, resolvedConfig, type) {
|
|
2638
|
+
const typeDescription = type.describeTool(operation, resolvedConfig);
|
|
2639
|
+
const entryDescription = connector.description?.trim();
|
|
2640
|
+
return entryDescription ? `${entryDescription} ${typeDescription}` : typeDescription;
|
|
2641
|
+
}
|
|
2642
|
+
function listConnectorTools(config) {
|
|
2643
|
+
const descriptors = [];
|
|
2644
|
+
const connectors = config.connectors ?? [];
|
|
2645
|
+
for (const connector of connectors) {
|
|
2646
|
+
const type = lookupConnectorType(connector.type);
|
|
2647
|
+
if (!type) {
|
|
2648
|
+
throw new Error(
|
|
2649
|
+
`Connector '${connector.name}' references unregistered type '${connector.type}'. This indicates registry mutation after config load.`
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
const resolvedConfig = type.configSchema.parse(connector.config);
|
|
2653
|
+
for (const operation of Object.values(type.operations)) {
|
|
2654
|
+
descriptors.push({
|
|
2655
|
+
name: toolName(connector.name, operation.name),
|
|
2656
|
+
description: buildToolDescription(connector, operation, resolvedConfig, type),
|
|
2657
|
+
inputSchema: argsSchemaToJsonSchema(operation.argsSchema)
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
return descriptors;
|
|
2662
|
+
}
|
|
2663
|
+
function findOperationByToolName(config, candidateToolName) {
|
|
2664
|
+
const connectors = config.connectors ?? [];
|
|
2665
|
+
for (const connector of connectors) {
|
|
2666
|
+
const type = lookupConnectorType(connector.type);
|
|
2667
|
+
if (!type) continue;
|
|
2668
|
+
for (const operation of Object.values(type.operations)) {
|
|
2669
|
+
if (toolName(connector.name, operation.name) === candidateToolName) {
|
|
2670
|
+
return { connector, operationName: operation.name, type };
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
return void 0;
|
|
2675
|
+
}
|
|
2676
|
+
function zodIssueDetails(err) {
|
|
2677
|
+
if (err instanceof z.ZodError) {
|
|
2678
|
+
return err.issues.map((issue) => ({
|
|
2679
|
+
path: issue.path,
|
|
2680
|
+
message: issue.message,
|
|
2681
|
+
code: issue.code
|
|
2682
|
+
}));
|
|
2683
|
+
}
|
|
2684
|
+
return err instanceof Error ? err.message : String(err);
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
export { AuthError, CONNECTOR_ERROR_CODES, ConfigError, ConnectorError, DestinationBlockedError, ENVELOPE_SCHEMA_VERSION, MaesterError, RefNotFoundError, SKILL_VERSION, buildToolDescription, defineConnectorOperation, findOperationByToolName, hasConnectorType, invokeOperation, listConnectorTools, listConnectorTypes, listSkillTargets, loadCitadelConfig, loadMaesterConfig, lookupConnectorType, registerConnectorType, runSkillInstall, runSkillStatus, runSkillUpgrade, runStatus, runSync };
|
|
1818
2688
|
//# sourceMappingURL=index.js.map
|
|
1819
2689
|
//# sourceMappingURL=index.js.map
|