cherry-works 0.1.0 → 0.2.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/README.md +61 -9
- package/dist/cw.js +2 -1
- package/dist/main.js +1452 -429
- package/dist/portal/main.js +2 -2
- package/dist/portal/styles.css +1 -1
- package/package.json +2 -1
package/dist/main.js
CHANGED
|
@@ -6,7 +6,7 @@ import { pathToFileURL as pathToFileURL2 } from "url";
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "cherry-works",
|
|
9
|
-
version: "0.1
|
|
9
|
+
version: "0.2.1",
|
|
10
10
|
description: "The charter engine: resolves a charter and compiles it into what a coding agent reads.",
|
|
11
11
|
keywords: [
|
|
12
12
|
"claude-code",
|
|
@@ -93,6 +93,7 @@ var package_default = {
|
|
|
93
93
|
dependencies: {
|
|
94
94
|
"@hono/node-server": "^2.1.1",
|
|
95
95
|
"@hono/zod-openapi": "^1.6.3",
|
|
96
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
96
97
|
hono: "^4.13.7",
|
|
97
98
|
picomatch: "^4.0.7",
|
|
98
99
|
prompts: "^2.4.2",
|
|
@@ -105,8 +106,8 @@ var package_default = {
|
|
|
105
106
|
// src/driver/cli/Commander.ts
|
|
106
107
|
import yargs from "yargs";
|
|
107
108
|
|
|
108
|
-
// src/hexagon/domain/models/
|
|
109
|
-
var
|
|
109
|
+
// src/hexagon/domain/models/DomainFault.ts
|
|
110
|
+
var DomainFault = class extends Error {
|
|
110
111
|
constructor(message, fix, severity = "error") {
|
|
111
112
|
super(message);
|
|
112
113
|
this.fix = fix;
|
|
@@ -116,17 +117,17 @@ var Fault = class extends Error {
|
|
|
116
117
|
fix;
|
|
117
118
|
severity;
|
|
118
119
|
};
|
|
119
|
-
var CharterRootFault = class extends
|
|
120
|
+
var CharterRootFault = class extends DomainFault {
|
|
120
121
|
};
|
|
121
|
-
var SettingsFault = class extends
|
|
122
|
+
var SettingsFault = class extends DomainFault {
|
|
122
123
|
};
|
|
123
|
-
var CharterPrimitiveFault = class extends
|
|
124
|
+
var CharterPrimitiveFault = class extends DomainFault {
|
|
124
125
|
};
|
|
125
|
-
var TestSuiteFault = class extends
|
|
126
|
+
var TestSuiteFault = class extends DomainFault {
|
|
126
127
|
};
|
|
127
|
-
var TestCaseFault = class extends
|
|
128
|
+
var TestCaseFault = class extends DomainFault {
|
|
128
129
|
};
|
|
129
|
-
var VendorFault = class extends
|
|
130
|
+
var VendorFault = class extends DomainFault {
|
|
130
131
|
};
|
|
131
132
|
function throwAggregateError(faults2) {
|
|
132
133
|
throw new AggregateError(faults2, faults2.map((one) => one.message).join(" "));
|
|
@@ -197,14 +198,32 @@ function matches(glob, path) {
|
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
// src/hexagon/domain/models/charter/primitive/Primitive.ts
|
|
200
|
-
import { z as
|
|
201
|
+
import { z as z5 } from "zod";
|
|
201
202
|
|
|
202
203
|
// src/hexagon/domain/models/charter/primitive/BasePrimitive.ts
|
|
203
204
|
import { z } from "zod";
|
|
204
205
|
|
|
205
206
|
// src/hexagon/domain/models/helper.ts
|
|
206
207
|
function formatFrontmatterValue(value) {
|
|
207
|
-
|
|
208
|
+
if (Array.isArray(value)) return `[${value.map((one) => JSON.stringify(one)).join(", ")}]`;
|
|
209
|
+
const line = String(value);
|
|
210
|
+
return YAML_READS_OTHERWISE.test(line) ? JSON.stringify(line) : line;
|
|
211
|
+
}
|
|
212
|
+
var YAML_READS_OTHERWISE = /^[-?:,[\]{}#&*!|>'"%@`]|: | #|:$|^\s|\s$|^$|^(true|false|yes|no|on|off|null|~)$|^[-+]?(\d[\d_]*)?\.?\d+([eE][-+]?\d+)?$/i;
|
|
213
|
+
function shortenStringsOf(strings2) {
|
|
214
|
+
const shortStrings = {};
|
|
215
|
+
const takenShortStrings = /* @__PURE__ */ new Set();
|
|
216
|
+
for (const string of [...new Set(strings2)].sort()) {
|
|
217
|
+
const firstSegment = string.slice(string.indexOf(":") + 1).split("/")[0];
|
|
218
|
+
let hash = 2166136261;
|
|
219
|
+
for (const char of string) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619);
|
|
220
|
+
const hashedString = `${firstSegment}_${(hash >>> 0).toString(16).padStart(8, "0").slice(0, 4)}`;
|
|
221
|
+
let shortString = hashedString;
|
|
222
|
+
for (let count = 2; takenShortStrings.has(shortString); count++) shortString = `${hashedString}_${count}`;
|
|
223
|
+
takenShortStrings.add(shortString);
|
|
224
|
+
shortStrings[string] = shortString;
|
|
225
|
+
}
|
|
226
|
+
return shortStrings;
|
|
208
227
|
}
|
|
209
228
|
|
|
210
229
|
// src/hexagon/domain/models/charter/primitive/BasePrimitive.ts
|
|
@@ -212,7 +231,12 @@ var DELIMITER = "---";
|
|
|
212
231
|
var goodLine = z.string().regex(/\S/);
|
|
213
232
|
var goodArray = z.array(goodLine).min(1).readonly();
|
|
214
233
|
var CommonHeaders = z.object({
|
|
215
|
-
|
|
234
|
+
/** Slugs joined by `/`, so identities can be grouped by team or domain
|
|
235
|
+
* (FR-141). The `/` puts the file in folders and nothing else: nothing is
|
|
236
|
+
* read from where a file sits. At most 40 characters, so every name a host
|
|
237
|
+
* is given for it — an mcp's tools among them, `mcp__cw__mcp-<id>__<tool>` —
|
|
238
|
+
* stays within what the host takes (FR-157). */
|
|
239
|
+
id: z.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/).refine((id) => id.length <= 40, { message: `"id" is at most 40 characters.` }),
|
|
216
240
|
/** All an agent reads of a primitive before opening its body. */
|
|
217
241
|
description: goodLine,
|
|
218
242
|
tags: z.array(goodLine).readonly().optional(),
|
|
@@ -256,14 +280,24 @@ var BasePrimitive = class {
|
|
|
256
280
|
* The kind leads, since it is what decides how the rest is read (FR-003). A
|
|
257
281
|
* list is written the way the parser gives it back, so an author opening the
|
|
258
282
|
* file finds what they declared rather than a second spelling of it.
|
|
283
|
+
*
|
|
284
|
+
* Written as its author wrote it unless the compiler says otherwise (FR-139):
|
|
285
|
+
* `mixins` lend their bodies, before this one's own so it reads as the point
|
|
286
|
+
* and theirs as the setting — nothing is merged and no header moves (FR-006);
|
|
287
|
+
* and `idReplacer` rewrites the body as a host reads it, a place named as
|
|
288
|
+
* `mcp:<id>` as the host calls its tools (FR-147).
|
|
259
289
|
*/
|
|
260
|
-
toMarkdown(
|
|
290
|
+
toMarkdown({
|
|
291
|
+
mixins = [],
|
|
292
|
+
idReplacer = (body) => body
|
|
293
|
+
} = {}) {
|
|
294
|
+
const body = [...mixins.map((mixin) => mixin.body), this.body].filter((one) => one.trim() !== "").join("\n\n");
|
|
261
295
|
return [
|
|
262
296
|
DELIMITER,
|
|
263
297
|
...Object.entries({ kind: this.kind, ...this.headers }).map(([field, value]) => `${field}: ${formatFrontmatterValue(value)}`),
|
|
264
298
|
DELIMITER,
|
|
265
299
|
"",
|
|
266
|
-
|
|
300
|
+
idReplacer(body)
|
|
267
301
|
].join("\n").replace(/\n*$/, "\n");
|
|
268
302
|
}
|
|
269
303
|
/**
|
|
@@ -284,10 +318,11 @@ function headersOf(sampledKind, headerSchema, record) {
|
|
|
284
318
|
const parsed = headerSchema.safeParse(record);
|
|
285
319
|
if (parsed.success) return parsed.data;
|
|
286
320
|
const { kind, sample } = sampledKind;
|
|
321
|
+
const refinementMessages = parsed.error.issues.filter((issue) => issue.code === "custom").map((issue) => issue.message);
|
|
287
322
|
const sampleLines = Object.entries({ kind, ...sample }).map(([field, value]) => `${field}: ${formatFrontmatterValue(value)}`);
|
|
288
323
|
return throwAggregateError([
|
|
289
324
|
new CharterPrimitiveFault(
|
|
290
|
-
`These headers are not what a ${kind} holds.`,
|
|
325
|
+
refinementMessages.length > 0 ? refinementMessages.join(" ") : `These headers are not what a ${kind} holds.`,
|
|
291
326
|
`Write them the way this ${kind} does:
|
|
292
327
|
${sampleLines.join("\n")}`
|
|
293
328
|
)
|
|
@@ -312,12 +347,13 @@ var AgentPrimitive = class _AgentPrimitive extends BasePrimitive {
|
|
|
312
347
|
/** When a reader of this charter is to open this kind at all, said
|
|
313
348
|
* where the kind's contract is: the neutral surface lists one
|
|
314
349
|
* line per kind and none of them is written down twice (FR-002). */
|
|
315
|
-
static activatesWhen = "it is spawned by identity, holding the `tools` it lists and nothing else";
|
|
316
|
-
/** An agent as an author writes one: what a refused header is fixed with.
|
|
350
|
+
static activatesWhen = "it is spawned by identity, holding the `tools` it lists and nothing else \u2014 a place as `mcp:<id>`, or one of its tools as `mcp:<id>:<tool>`";
|
|
351
|
+
/** An agent as an author writes one: what a refused header is fixed with.
|
|
352
|
+
* It holds one tool of a place the way an author writes it (FR-156). */
|
|
317
353
|
static sample = {
|
|
318
354
|
id: "reviewer",
|
|
319
355
|
description: "Review a diff and name what is wrong with it.",
|
|
320
|
-
tools: ["Read", "Grep"]
|
|
356
|
+
tools: ["Read", "Grep", "mcp:mfbs/billing:search_code"]
|
|
321
357
|
};
|
|
322
358
|
/** One agent, or every fault its headers have (FR-004). */
|
|
323
359
|
static of(record, body) {
|
|
@@ -325,34 +361,6 @@ var AgentPrimitive = class _AgentPrimitive extends BasePrimitive {
|
|
|
325
361
|
}
|
|
326
362
|
};
|
|
327
363
|
|
|
328
|
-
// src/hexagon/domain/models/charter/primitive/CommandPrimitive.ts
|
|
329
|
-
var CommandPrimitive = class _CommandPrimitive extends BasePrimitive {
|
|
330
|
-
static kind = "command";
|
|
331
|
-
kind = _CommandPrimitive.kind;
|
|
332
|
-
/** What this kind requires beyond the common headers: nothing. Declared all
|
|
333
|
-
* the same, since what a file is scaffolded with is read off this (FR-039). */
|
|
334
|
-
static requires = {};
|
|
335
|
-
/** The schema its headers are read by: what `headersOf` refuses a file
|
|
336
|
-
* against, and what says the shape of every header this kind takes, required
|
|
337
|
-
* or not (FR-004). */
|
|
338
|
-
static schema = CommonHeaders;
|
|
339
|
-
/** When a reader of this charter is to open this kind at all, said
|
|
340
|
-
* where the kind's contract is: the neutral surface lists one
|
|
341
|
-
* line per kind and none of them is written down twice (FR-002). */
|
|
342
|
-
static activatesWhen = "it is asked for by name, or a request matches what it describes itself as being for";
|
|
343
|
-
/** A command as an author writes one: what a refused header is fixed with. */
|
|
344
|
-
static sample = {
|
|
345
|
-
id: "release",
|
|
346
|
-
description: "Cut a release from the main branch."
|
|
347
|
-
};
|
|
348
|
-
/** One command, or every fault its headers have. Nothing beyond what every
|
|
349
|
-
* primitive declares: a command is asked for by name, and the one line it
|
|
350
|
-
* describes itself with is what decides that it is the one being asked for. */
|
|
351
|
-
static of(record, body) {
|
|
352
|
-
return new _CommandPrimitive(headersOf(_CommandPrimitive, CommonHeaders, record), body);
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
|
|
356
364
|
// src/hexagon/domain/models/charter/primitive/CorpusPrimitive.ts
|
|
357
365
|
var CorpusHeaders = CommonHeaders.extend({});
|
|
358
366
|
var CorpusPrimitive = class _CorpusPrimitive extends BasePrimitive {
|
|
@@ -410,11 +418,109 @@ var GuidePrimitive = class _GuidePrimitive extends BasePrimitive {
|
|
|
410
418
|
}
|
|
411
419
|
};
|
|
412
420
|
|
|
413
|
-
// src/hexagon/domain/models/charter/primitive/
|
|
421
|
+
// src/hexagon/domain/models/charter/primitive/McpPrimitive.ts
|
|
414
422
|
import { z as z2 } from "zod";
|
|
423
|
+
var MCP_AUTHS = ["oauth", "token"];
|
|
424
|
+
function isSecureEndpoint(endpoint) {
|
|
425
|
+
if (!URL.canParse(endpoint)) return false;
|
|
426
|
+
const endpointURL = new URL(endpoint);
|
|
427
|
+
return endpointURL.protocol === "https:" || endpointURL.protocol === "http:" && ["127.0.0.1", "localhost"].includes(endpointURL.hostname);
|
|
428
|
+
}
|
|
429
|
+
var MCP_MENTION = /\bmcp:([a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*)/g;
|
|
430
|
+
var MCP_TOOL_REFERENCE = /^(mcp:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*)(?::([^:\s]+))?$/;
|
|
431
|
+
var McpHeaders = CommonHeaders.extend({
|
|
432
|
+
/** Reached over HTTP. Plain `http` only to this machine, for a server under
|
|
433
|
+
* test: anything else would send a developer's credential in the clear. */
|
|
434
|
+
endpoint: goodLine.refine(isSecureEndpoint, {
|
|
435
|
+
message: `"endpoint" is an https:// URL, or http:// to 127.0.0.1 or localhost for a server under test.`
|
|
436
|
+
}).optional(),
|
|
437
|
+
/** Started as a local process, spoken to over its standard input and output. */
|
|
438
|
+
command: goodLine.optional(),
|
|
439
|
+
args: z2.array(goodLine).readonly().optional(),
|
|
440
|
+
auth: z2.array(z2.enum(MCP_AUTHS)).min(1).readonly().optional(),
|
|
441
|
+
/** The environment variable a command reads its token from. */
|
|
442
|
+
tokenEnv: z2.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).optional(),
|
|
443
|
+
/** The place inside that server: a repository, a database, a channel. */
|
|
444
|
+
path: goodLine.optional(),
|
|
445
|
+
/** The server's tools the agent may use there, by the server's own names. */
|
|
446
|
+
tools: goodArray
|
|
447
|
+
}).superRefine((headers, context) => {
|
|
448
|
+
const addFault = (message) => context.addIssue({ code: "custom", message });
|
|
449
|
+
const { endpoint, command, args, auth: auth2, tokenEnv } = headers;
|
|
450
|
+
if (endpoint !== void 0 && command !== void 0)
|
|
451
|
+
return addFault(`This declares both "endpoint" and "command"; an mcp is one server, reached one way.`);
|
|
452
|
+
if (endpoint === void 0 && command === void 0)
|
|
453
|
+
return addFault(`This declares neither "endpoint" nor "command", so there is no server to reach.`);
|
|
454
|
+
if (endpoint !== void 0) {
|
|
455
|
+
if (auth2 === void 0) addFault(`An endpoint says how a developer signs in to it: "auth" of oauth, token or both.`);
|
|
456
|
+
if (args !== void 0) addFault(`"args" are a command's; an endpoint takes none.`);
|
|
457
|
+
if (tokenEnv !== void 0) addFault(`"tokenEnv" is where a command reads its token; an endpoint takes none.`);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const takesToken = auth2 !== void 0;
|
|
461
|
+
if (takesToken && (auth2.length !== 1 || auth2[0] !== "token"))
|
|
462
|
+
addFault(`A command signs in by token alone: "auth: [token]", or no "auth" for one that takes none.`);
|
|
463
|
+
if (takesToken && tokenEnv === void 0) addFault(`A command taking a token names where it reads it: "tokenEnv".`);
|
|
464
|
+
if (!takesToken && tokenEnv !== void 0) addFault(`"tokenEnv" is read only by a command taking a token: add "auth: [token]", or drop it.`);
|
|
465
|
+
});
|
|
466
|
+
var McpPrimitive = class _McpPrimitive extends BasePrimitive {
|
|
467
|
+
static kind = "mcp";
|
|
468
|
+
kind = _McpPrimitive.kind;
|
|
469
|
+
/** What this kind requires beyond the common headers. `endpoint` or
|
|
470
|
+
* `command` is required too, but which depends on the shape, so the schema's
|
|
471
|
+
* refinement says it rather than this (FR-004, FR-039). */
|
|
472
|
+
static requires = { tools: "list" };
|
|
473
|
+
/** The schema its headers are read by: what `headersOf` refuses a file
|
|
474
|
+
* against, and what says the shape of every header this kind takes, required
|
|
475
|
+
* or not (FR-004). */
|
|
476
|
+
static schema = McpHeaders;
|
|
477
|
+
/** When a reader of this charter is to open this kind at all, said
|
|
478
|
+
* where the kind's contract is: the neutral surface lists one
|
|
479
|
+
* line per kind and none of them is written down twice (FR-002). */
|
|
480
|
+
static activatesWhen = "a primitive's body names it as `mcp:<id>` \u2014 a place outside the repository, reached through `cw mcp serve`";
|
|
481
|
+
/** An mcp reached at an endpoint, as an author writes one: what a refused
|
|
482
|
+
* header is fixed with. */
|
|
483
|
+
static sample = {
|
|
484
|
+
id: "mfbs/billing",
|
|
485
|
+
description: "Billing service code and pull requests.",
|
|
486
|
+
endpoint: "https://api.githubcopilot.com/mcp/",
|
|
487
|
+
path: "moneyforward/billing-service",
|
|
488
|
+
auth: ["oauth", "token"],
|
|
489
|
+
tools: ["get_file_contents", "search_code", "list_pull_requests"]
|
|
490
|
+
};
|
|
491
|
+
/** The same place started as a local process: what a refused file declaring
|
|
492
|
+
* `command` is held up against, since that is the shape it was nearer. */
|
|
493
|
+
static commandSample = {
|
|
494
|
+
id: "mfbs/billing",
|
|
495
|
+
description: "Billing service code and pull requests.",
|
|
496
|
+
command: "npx",
|
|
497
|
+
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
498
|
+
tokenEnv: "GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
499
|
+
auth: ["token"],
|
|
500
|
+
path: "moneyforward/billing-service",
|
|
501
|
+
tools: ["get_file_contents", "search_code", "list_pull_requests"]
|
|
502
|
+
};
|
|
503
|
+
/** What a developer signs in to, and what places are told apart by beside
|
|
504
|
+
* their `path`: the endpoint, or the command followed by each of its
|
|
505
|
+
* arguments. The refinement has already held that one of the two is here
|
|
506
|
+
* (FR-142). */
|
|
507
|
+
get address() {
|
|
508
|
+
const { endpoint, command, args = [] } = this.headers;
|
|
509
|
+
return endpoint ?? [command, ...args].join(" ");
|
|
510
|
+
}
|
|
511
|
+
/** One mcp, or every fault its headers have, shown the sample of the shape
|
|
512
|
+
* it declared (FR-004, FR-142). */
|
|
513
|
+
static of(record, body) {
|
|
514
|
+
const nearerSample = record.command === void 0 ? _McpPrimitive.sample : _McpPrimitive.commandSample;
|
|
515
|
+
return new _McpPrimitive(headersOf({ kind: _McpPrimitive.kind, sample: nearerSample }, McpHeaders, record), body);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// src/hexagon/domain/models/charter/primitive/MixinPrimitive.ts
|
|
520
|
+
import { z as z3 } from "zod";
|
|
415
521
|
var MixinHeaders = CommonHeaders.extend({
|
|
416
522
|
/** Never: reading refuses a mixin that declares one. */
|
|
417
|
-
mixins:
|
|
523
|
+
mixins: z3.undefined().optional()
|
|
418
524
|
});
|
|
419
525
|
var MixinPrimitive = class _MixinPrimitive extends BasePrimitive {
|
|
420
526
|
static kind = "mixin";
|
|
@@ -460,7 +566,7 @@ var PlaybookPrimitive = class _PlaybookPrimitive extends BasePrimitive {
|
|
|
460
566
|
/** When a reader of this charter is to open this kind at all, said
|
|
461
567
|
* where the kind's contract is: the neutral surface lists one
|
|
462
568
|
* line per kind and none of them is written down twice (FR-002). */
|
|
463
|
-
static activatesWhen = "one of its `triggers` matches; the body is a sequence of
|
|
569
|
+
static activatesWhen = "one of its `triggers` matches; the body is a sequence of skills";
|
|
464
570
|
/** A playbook as an author writes one: what a refused header is fixed with. */
|
|
465
571
|
static sample = {
|
|
466
572
|
id: "ship-a-feature",
|
|
@@ -512,7 +618,7 @@ var PosturePrimitive = class _PosturePrimitive extends BasePrimitive {
|
|
|
512
618
|
};
|
|
513
619
|
|
|
514
620
|
// src/hexagon/domain/models/charter/primitive/SensorPrimitive.ts
|
|
515
|
-
import { z as
|
|
621
|
+
import { z as z4 } from "zod";
|
|
516
622
|
var SIGNALS = [
|
|
517
623
|
"PreToolUse",
|
|
518
624
|
"PostToolUse",
|
|
@@ -526,7 +632,7 @@ var SIGNALS = [
|
|
|
526
632
|
];
|
|
527
633
|
var SensorHeaders = CommonHeaders.extend({
|
|
528
634
|
/** What raises it: one of the events every host of this engine speaks in. */
|
|
529
|
-
signal:
|
|
635
|
+
signal: z4.enum(SIGNALS),
|
|
530
636
|
/** What is run when it does. A sensor that names no command is a sensor
|
|
531
637
|
* nothing can fire, so this is required. */
|
|
532
638
|
run: goodLine
|
|
@@ -599,16 +705,16 @@ var SkillPrimitive = class _SkillPrimitive extends BasePrimitive {
|
|
|
599
705
|
var PRIMITIVE_CLASSES = [
|
|
600
706
|
GuidePrimitive,
|
|
601
707
|
SensorPrimitive,
|
|
602
|
-
CommandPrimitive,
|
|
603
708
|
SkillPrimitive,
|
|
604
709
|
PlaybookPrimitive,
|
|
605
710
|
AgentPrimitive,
|
|
606
711
|
PosturePrimitive,
|
|
607
712
|
CorpusPrimitive,
|
|
608
|
-
MixinPrimitive
|
|
713
|
+
MixinPrimitive,
|
|
714
|
+
McpPrimitive
|
|
609
715
|
];
|
|
610
716
|
var KINDS = Object.freeze(PRIMITIVE_CLASSES.map((one) => one.kind));
|
|
611
|
-
var PRIMITIVE_IDENTITY_SCHEMA =
|
|
717
|
+
var PRIMITIVE_IDENTITY_SCHEMA = z5.string().regex(/^[a-z0-9-]+:[a-z0-9-]+(\/[a-z0-9-]+)*$/).transform((identity) => identity);
|
|
612
718
|
function identityOf(kindAndId) {
|
|
613
719
|
if (typeof kindAndId !== "string") return `${kindAndId.kind}:${kindAndId.id}`;
|
|
614
720
|
const parsed = PRIMITIVE_IDENTITY_SCHEMA.safeParse(kindAndId);
|
|
@@ -650,13 +756,13 @@ function primitiveHeadersOf(kind) {
|
|
|
650
756
|
const required = { ...BasePrimitive.requires, ...kindClass.requires };
|
|
651
757
|
return Object.entries(kindClass.schema.shape).filter(([field]) => field !== "id").map(([field, type]) => {
|
|
652
758
|
let heldType = type;
|
|
653
|
-
while (!(heldType instanceof
|
|
759
|
+
while (!(heldType instanceof z5.ZodArray) && typeof heldType?.unwrap === "function")
|
|
654
760
|
heldType = heldType.unwrap();
|
|
655
761
|
return new PrimitiveHeader(
|
|
656
762
|
field,
|
|
657
|
-
heldType instanceof
|
|
763
|
+
heldType instanceof z5.ZodArray ? "list" : "line",
|
|
658
764
|
field in required,
|
|
659
|
-
heldType instanceof
|
|
765
|
+
heldType instanceof z5.ZodEnum ? heldType.options : void 0
|
|
660
766
|
);
|
|
661
767
|
});
|
|
662
768
|
}
|
|
@@ -731,11 +837,16 @@ var ScopedPrimitive = class {
|
|
|
731
837
|
this.scope = scope;
|
|
732
838
|
this.file = file;
|
|
733
839
|
this.primitive = primitive;
|
|
840
|
+
this.normIdentity = identity.replace(/[:/]/g, "-");
|
|
734
841
|
}
|
|
735
842
|
identity;
|
|
736
843
|
scope;
|
|
737
844
|
file;
|
|
738
845
|
primitive;
|
|
846
|
+
/** The identity as every name given to a host writes it, `:` and `/` as `-`:
|
|
847
|
+
* `guide-mfbs-no-any` for `guide:mfbs/no-any` (FR-141). Made here, once, so
|
|
848
|
+
* nothing that names a host's file or a place's tools spells it again. */
|
|
849
|
+
normIdentity;
|
|
739
850
|
/** Whether a word someone searched for is anywhere a reader would look for
|
|
740
851
|
* it: the identity, the kind, what it is for, when its kind comes up, its
|
|
741
852
|
* file, or any header it declared — ignoring case, since whoever typed it
|
|
@@ -772,26 +883,6 @@ var CharterRoot = class {
|
|
|
772
883
|
this.primitives.filter(({ primitive }) => primitive.kind === "mixin").map((one) => [one.primitive.headers.id, one.primitive])
|
|
773
884
|
);
|
|
774
885
|
}
|
|
775
|
-
/**
|
|
776
|
-
* What one primitive says once its mixins are in it: the body of every mixin
|
|
777
|
-
* it pulls in, in the order it named them, and then its own (FR-006).
|
|
778
|
-
*
|
|
779
|
-
* The one place a mixin is applied at all. Nothing is merged and no header
|
|
780
|
-
* moves — a mixin lends text, so what comes back is text, and the host's own
|
|
781
|
-
* words come last so they read as the point and the lent ones as the setting.
|
|
782
|
-
*
|
|
783
|
-
* Asked of the charter because the charter is what already read the mixins
|
|
784
|
-
* and knows which identity a name means across layers. A mixin nothing
|
|
785
|
-
* answers to is left out rather than raised: `allFaultsByFiles` already names
|
|
786
|
-
* that file, and a build refused for it never reaches here (FR-009).
|
|
787
|
-
*/
|
|
788
|
-
bodyOf(one) {
|
|
789
|
-
const mixins = this.mixins;
|
|
790
|
-
return [
|
|
791
|
-
...(one.primitive.headers.mixins ?? []).map((named) => mixins.get(named)?.body),
|
|
792
|
-
one.primitive.body
|
|
793
|
-
].filter((body) => body !== void 0 && body.trim() !== "").join("\n\n");
|
|
794
|
-
}
|
|
795
886
|
/**
|
|
796
887
|
* Every primitive under the identity the whole charter names it by (FR-017).
|
|
797
888
|
*
|
|
@@ -864,20 +955,36 @@ var CharterRoot = class {
|
|
|
864
955
|
* rationale no corpus answers to, which is a warning rather than an error:
|
|
865
956
|
* the rule holds without its reasoning, and the author is told the reasoning
|
|
866
957
|
* is gone (FR-005). A corpus nobody cites and a mixin nobody pulls in are
|
|
867
|
-
* warnings too: nothing breaks, and nothing reads them either (FR-014).
|
|
958
|
+
* warnings too: nothing breaks, and nothing reads them either (FR-014). So
|
|
959
|
+
* are a body naming an mcp nothing holds (FR-143) and an mcp no body names
|
|
960
|
+
* (FR-144). An agent holding an mcp nothing holds, or a tool it does not
|
|
961
|
+
* declare, is an error: the role would be promised a tool it cannot have
|
|
962
|
+
* (FR-156).
|
|
868
963
|
*/
|
|
869
964
|
get compositeFaultsByFiles() {
|
|
870
965
|
const everyPrimitive = this.primitives;
|
|
871
966
|
const mixinsByIdentity = this.mixins;
|
|
872
967
|
const corpusIdentities = this.corpora;
|
|
968
|
+
const toolsByMcpIdentity = new Map(
|
|
969
|
+
everyPrimitive.flatMap(({ identity, primitive }) => primitive instanceof McpPrimitive ? [[identity, primitive.headers.tools]] : [])
|
|
970
|
+
);
|
|
971
|
+
const mcpIdentities = new Set(toolsByMcpIdentity.keys());
|
|
972
|
+
const mcpsNamedBy = (one) => [...one.primitive.body.matchAll(MCP_MENTION)].map(([, id]) => `mcp:${id}`);
|
|
973
|
+
const namedMcps = new Set(
|
|
974
|
+
everyPrimitive.flatMap((one) => [
|
|
975
|
+
...mcpsNamedBy(one),
|
|
976
|
+
...one.primitive instanceof AgentPrimitive ? one.primitive.headers.tools.flatMap((tool) => tool.match(MCP_TOOL_REFERENCE)?.[1] ?? []) : []
|
|
977
|
+
])
|
|
978
|
+
);
|
|
873
979
|
const fileByIdentity = /* @__PURE__ */ new Map();
|
|
980
|
+
const identityByNormalizedIdentity = /* @__PURE__ */ new Map();
|
|
874
981
|
const faultsByFiles = {};
|
|
875
982
|
const addFault = (file, found) => {
|
|
876
983
|
faultsByFiles[file] = [...faultsByFiles[file] ?? [], found];
|
|
877
984
|
};
|
|
878
985
|
for (const one of everyPrimitive) {
|
|
879
986
|
const { file, primitive } = one;
|
|
880
|
-
const { identity } = one;
|
|
987
|
+
const { identity, normIdentity } = one;
|
|
881
988
|
const declaredIn = fileByIdentity.get(identity);
|
|
882
989
|
if (declaredIn === void 0) fileByIdentity.set(identity, file);
|
|
883
990
|
else
|
|
@@ -888,6 +995,16 @@ var CharterRoot = class {
|
|
|
888
995
|
`Give this one an id of its own, or delete it if the other says the same thing. A vendor claiming an id you authored is one to raise with whoever publishes it. An id ${BUILTIN_PATH_PREFIX} claims is the engine's own and nobody can change it, so the one to rename is yours.`
|
|
889
996
|
)
|
|
890
997
|
);
|
|
998
|
+
const normalizedIdentityClaimedBy = identityByNormalizedIdentity.get(normIdentity);
|
|
999
|
+
if (normalizedIdentityClaimedBy === void 0) identityByNormalizedIdentity.set(normIdentity, identity);
|
|
1000
|
+
else if (normalizedIdentityClaimedBy !== identity)
|
|
1001
|
+
addFault(
|
|
1002
|
+
file,
|
|
1003
|
+
new CharterRootFault(
|
|
1004
|
+
`"${identity}" and "${normalizedIdentityClaimedBy}" are both named "${normIdentity}" in an agent's files, where "/" is written as "-". Only one of them would be written there.`,
|
|
1005
|
+
`Give this one an id that stays its own once "/" is written as "-".`
|
|
1006
|
+
)
|
|
1007
|
+
);
|
|
891
1008
|
for (const namedMixin of primitive.headers.mixins ?? []) {
|
|
892
1009
|
const mixin = mixinsByIdentity.get(namedMixin);
|
|
893
1010
|
if (mixin === void 0) {
|
|
@@ -921,6 +1038,45 @@ var CharterRoot = class {
|
|
|
921
1038
|
)
|
|
922
1039
|
);
|
|
923
1040
|
}
|
|
1041
|
+
for (const namedMcp of new Set(mcpsNamedBy(one)))
|
|
1042
|
+
if (!mcpIdentities.has(namedMcp))
|
|
1043
|
+
addFault(
|
|
1044
|
+
file,
|
|
1045
|
+
new CharterRootFault(
|
|
1046
|
+
`This names "${namedMcp}", and this charter holds no mcp of that identity. The agent would be told of a place it cannot reach.`,
|
|
1047
|
+
`Author that mcp, or correct the name. An mcp is named as "mcp:<id>", whichever layer authored it.`,
|
|
1048
|
+
"warn"
|
|
1049
|
+
)
|
|
1050
|
+
);
|
|
1051
|
+
if (primitive instanceof AgentPrimitive)
|
|
1052
|
+
for (const tool of primitive.headers.tools.filter((one2) => one2.startsWith("mcp:"))) {
|
|
1053
|
+
const [, mcpIdentity, mcpTool] = tool.match(MCP_TOOL_REFERENCE) ?? [];
|
|
1054
|
+
const declaredTools = mcpIdentity === void 0 ? void 0 : toolsByMcpIdentity.get(mcpIdentity);
|
|
1055
|
+
if (mcpIdentity === void 0)
|
|
1056
|
+
addFault(
|
|
1057
|
+
file,
|
|
1058
|
+
new CharterRootFault(
|
|
1059
|
+
`"${tool}" under "tools" is not a place this role can hold.`,
|
|
1060
|
+
`Write a place as "mcp:<id>" for every tool it declares, or "mcp:<id>:<tool>" for one.`
|
|
1061
|
+
)
|
|
1062
|
+
);
|
|
1063
|
+
else if (declaredTools === void 0)
|
|
1064
|
+
addFault(
|
|
1065
|
+
file,
|
|
1066
|
+
new CharterRootFault(
|
|
1067
|
+
`"${tool}" under "tools" names "${mcpIdentity}", and this charter holds no mcp of that identity.`,
|
|
1068
|
+
`Author that mcp, or correct the name. An mcp is named as "mcp:<id>", whichever layer authored it.`
|
|
1069
|
+
)
|
|
1070
|
+
);
|
|
1071
|
+
else if (mcpTool !== void 0 && !declaredTools.includes(mcpTool))
|
|
1072
|
+
addFault(
|
|
1073
|
+
file,
|
|
1074
|
+
new CharterRootFault(
|
|
1075
|
+
`"${tool}" under "tools" names a tool "${mcpIdentity}" does not declare; it declares ${declaredTools.join(", ")}.`,
|
|
1076
|
+
`Name one of those, or add "${mcpTool}" to the tools of "${mcpIdentity}".`
|
|
1077
|
+
)
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
924
1080
|
if (primitive.kind === "corpus" && this.citersOf(one).length === 0)
|
|
925
1081
|
addFault(
|
|
926
1082
|
file,
|
|
@@ -939,6 +1095,32 @@ var CharterRoot = class {
|
|
|
939
1095
|
"warn"
|
|
940
1096
|
)
|
|
941
1097
|
);
|
|
1098
|
+
if (primitive.kind === "mcp" && !namedMcps.has(identity))
|
|
1099
|
+
addFault(
|
|
1100
|
+
file,
|
|
1101
|
+
new CharterRootFault(
|
|
1102
|
+
`No primitive names "${identity}" in its body, so nothing an agent reads leads to it.`,
|
|
1103
|
+
`Name it where it is used, as "${identity}", or delete it.`,
|
|
1104
|
+
"warn"
|
|
1105
|
+
)
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
const mcpPrimitives = everyPrimitive.flatMap(
|
|
1109
|
+
({ identity, file, primitive }) => primitive.kind === McpPrimitive.kind ? [{ identity, file, primitive }] : []
|
|
1110
|
+
).sort((one, another) => one.identity < another.identity ? -1 : 1);
|
|
1111
|
+
for (const { file, primitive } of mcpPrimitives) {
|
|
1112
|
+
const { tokenEnv } = primitive.headers;
|
|
1113
|
+
const conflictingMcp = mcpPrimitives.find(
|
|
1114
|
+
(other) => other.primitive.address === primitive.address && other.primitive.headers.tokenEnv !== void 0 && other.primitive.headers.tokenEnv !== tokenEnv
|
|
1115
|
+
);
|
|
1116
|
+
if (tokenEnv !== void 0 && conflictingMcp !== void 0)
|
|
1117
|
+
addFault(
|
|
1118
|
+
file,
|
|
1119
|
+
new CharterRootFault(
|
|
1120
|
+
`This hands "${primitive.address}" its token in "${tokenEnv}", and "${conflictingMcp.identity}" in "${conflictingMcp.primitive.headers.tokenEnv}". One process reads its token from one variable.`,
|
|
1121
|
+
`Name the variable that command reads in both, or drop "tokenEnv" and "auth" from one of them.`
|
|
1122
|
+
)
|
|
1123
|
+
);
|
|
942
1124
|
}
|
|
943
1125
|
return new FaultsByFile(faultsByFiles);
|
|
944
1126
|
}
|
|
@@ -970,7 +1152,7 @@ function charterRootOf({
|
|
|
970
1152
|
function folderURL(at) {
|
|
971
1153
|
const url = new URL(at);
|
|
972
1154
|
if (!url.pathname.endsWith("/"))
|
|
973
|
-
throw new
|
|
1155
|
+
throw new DomainFault(
|
|
974
1156
|
`"${url.href}" names a file, and a folder is what is wanted here.`,
|
|
975
1157
|
`Write it with the separator a folder ends in, as in "${url.href}/".`
|
|
976
1158
|
);
|
|
@@ -990,6 +1172,7 @@ function outFolderIn(repo) {
|
|
|
990
1172
|
}
|
|
991
1173
|
var CLAUDE_DIRECTORY = ".claude";
|
|
992
1174
|
var CLAUDE_ENTRY_FILE = "CLAUDE.md";
|
|
1175
|
+
var CLAUDE_MCP_CONFIG_FILE = ".mcp.json";
|
|
993
1176
|
function agentProviderFolderIn(repo, provider) {
|
|
994
1177
|
switch (provider) {
|
|
995
1178
|
case "claude":
|
|
@@ -1025,7 +1208,7 @@ function parseKVParams(typed, paramName) {
|
|
|
1025
1208
|
for (const one of typed) {
|
|
1026
1209
|
const at = one.indexOf("=");
|
|
1027
1210
|
if (at <= 0)
|
|
1028
|
-
throw new
|
|
1211
|
+
throw new DomainFault(
|
|
1029
1212
|
`"--${paramName} ${one}" names nothing to put a value under.`,
|
|
1030
1213
|
`Type each one as --${paramName} name=value, once per name.`
|
|
1031
1214
|
);
|
|
@@ -1042,7 +1225,7 @@ function lines({ data: { severity, message, fix } }) {
|
|
|
1042
1225
|
async function editInEditor(cwd, file) {
|
|
1043
1226
|
const editorCommand = process.env.VISUAL || process.env.EDITOR;
|
|
1044
1227
|
if (!editorCommand)
|
|
1045
|
-
throw new
|
|
1228
|
+
throw new DomainFault("Neither $VISUAL nor $EDITOR is set, so there is no editor to open it in.", 'Set one, as in "export EDITOR=vim".');
|
|
1046
1229
|
const exitCode = await new Promise(
|
|
1047
1230
|
(resolve, reject) => spawn("sh", ["-c", `${editorCommand} "$1"`, editorCommand, `${cwd}/${file}`], { stdio: "inherit" }).once("error", reject).once("exit", resolve)
|
|
1048
1231
|
);
|
|
@@ -1077,7 +1260,7 @@ var AddCommand = class {
|
|
|
1077
1260
|
Object.entries(parseKVParams(header, "header")).map(([field, values]) => {
|
|
1078
1261
|
if (lists.has(field)) return [field, values];
|
|
1079
1262
|
if (values.length > 1)
|
|
1080
|
-
throw new
|
|
1263
|
+
throw new DomainFault(
|
|
1081
1264
|
`"${field}" holds one line, and was typed ${values.length} times.`,
|
|
1082
1265
|
`Type "--header ${field}=..." once.`
|
|
1083
1266
|
);
|
|
@@ -1114,7 +1297,7 @@ var AddCommand = class {
|
|
|
1114
1297
|
*/
|
|
1115
1298
|
async #answered(required, kind) {
|
|
1116
1299
|
if (!process.stdin.isTTY)
|
|
1117
|
-
throw new
|
|
1300
|
+
throw new DomainFault(
|
|
1118
1301
|
`A ${kind} requires ${required.map((one) => `"${one.field}"`).join(", ")}, and nobody is at the terminal to answer.`,
|
|
1119
1302
|
`Run "cw add" where you can answer, or author the file under the charter yourself.`
|
|
1120
1303
|
);
|
|
@@ -1130,7 +1313,7 @@ var AddCommand = class {
|
|
|
1130
1313
|
)
|
|
1131
1314
|
);
|
|
1132
1315
|
if (required.some((one) => answers[one.field] === void 0))
|
|
1133
|
-
throw new
|
|
1316
|
+
throw new DomainFault("This was stopped before it had its answers, so nothing was written.", 'Run "cw add" again.');
|
|
1134
1317
|
return answers;
|
|
1135
1318
|
}
|
|
1136
1319
|
};
|
|
@@ -1249,7 +1432,7 @@ var EditCommand = class {
|
|
|
1249
1432
|
async run({ cwd, charterAuthoringApp: charterAuthoringApp2 }, { identity }) {
|
|
1250
1433
|
const { scopedPrimitive: scopedPrimitive2 } = (await charterAuthoringApp2.open(identity ?? "")).data;
|
|
1251
1434
|
if (scopedPrimitive2.data.scope === BUILTIN_SCOPE)
|
|
1252
|
-
throw new
|
|
1435
|
+
throw new DomainFault(
|
|
1253
1436
|
`${scopedPrimitive2.data.identity} is built into cw, and there is no file of it in this repository to edit.`,
|
|
1254
1437
|
`To differ from it, run "cw add" for a primitive of your own under an identity of its own.`
|
|
1255
1438
|
);
|
|
@@ -1330,13 +1513,26 @@ var InitCommand = class {
|
|
|
1330
1513
|
options = OPTIONS4;
|
|
1331
1514
|
async run({ charterAuthoringApp: charterAuthoringApp2 }, { agent }) {
|
|
1332
1515
|
const agents = await whichAgents(charterAuthoringApp2, agent);
|
|
1333
|
-
await charterAuthoringApp2.init({ agents });
|
|
1516
|
+
const planSummaryDTO2 = await charterAuthoringApp2.init({ agents });
|
|
1517
|
+
const setUp = ["Set up this repository under a charter.", `Compiling for: ${agents.join(", ")}.`];
|
|
1518
|
+
if (planSummaryDTO2.type === "FaultsByFile") {
|
|
1519
|
+
const noOfFiles = Object.keys(planSummaryDTO2.data.files).length;
|
|
1520
|
+
return {
|
|
1521
|
+
code: EXIT_FAILURE,
|
|
1522
|
+
result: [...setUp, ""].join("\n"),
|
|
1523
|
+
problem: `Nothing was built: ${noOfFiles} file${noOfFiles === 1 ? " has" : "s have"} errors.
|
|
1524
|
+
Run "cw doctor" to see what is wrong with them.
|
|
1525
|
+
`
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
const { added, edited } = planSummaryDTO2.data;
|
|
1529
|
+
const wrote = added.length + edited.length;
|
|
1334
1530
|
return {
|
|
1335
1531
|
code: EXIT_OK,
|
|
1336
1532
|
result: [
|
|
1337
|
-
|
|
1338
|
-
`
|
|
1339
|
-
'Author your first rule under .cw/charter/guide/, then run "cw build".',
|
|
1533
|
+
...setUp,
|
|
1534
|
+
`Built ${wrote} file${wrote === 1 ? "" : "s"}: ${added.length} added, ${edited.length} changed.`,
|
|
1535
|
+
'Author your first rule under .cw/charter/guide/, or ask your agent to write one, then run "cw build".',
|
|
1340
1536
|
""
|
|
1341
1537
|
].join("\n")
|
|
1342
1538
|
};
|
|
@@ -1345,20 +1541,21 @@ var InitCommand = class {
|
|
|
1345
1541
|
async function whichAgents(charterAuthoringApp2, typed) {
|
|
1346
1542
|
if (typed !== void 0) {
|
|
1347
1543
|
if (!isAgentProvider(typed))
|
|
1348
|
-
throw new
|
|
1544
|
+
throw new DomainFault(
|
|
1349
1545
|
`This engine compiles for no agent called "${typed}".`,
|
|
1350
1546
|
`Name any of: ${AGENT_PROVIDERS.join(", ")}.`
|
|
1351
1547
|
);
|
|
1352
1548
|
return [typed];
|
|
1353
1549
|
}
|
|
1354
1550
|
const agentsInSettings = (await charterAuthoringApp2.settings()).data.agents.filter(isAgentProvider);
|
|
1551
|
+
const defaultAgents = agentsInSettings.length > 0 ? agentsInSettings : AGENT_PROVIDERS.length === 1 ? AGENT_PROVIDERS : [];
|
|
1355
1552
|
if (!process.stdin.isTTY) {
|
|
1356
|
-
if (
|
|
1357
|
-
throw new
|
|
1553
|
+
if (defaultAgents.length === 0)
|
|
1554
|
+
throw new DomainFault(
|
|
1358
1555
|
"Nobody is at the terminal to choose an agent, and this repository has none chosen yet.",
|
|
1359
1556
|
`Run "cw init --agent <name>", naming one of: ${AGENT_PROVIDERS.join(", ")}.`
|
|
1360
1557
|
);
|
|
1361
|
-
return
|
|
1558
|
+
return defaultAgents;
|
|
1362
1559
|
}
|
|
1363
1560
|
const { chosen } = await prompts2({
|
|
1364
1561
|
type: "multiselect",
|
|
@@ -1367,14 +1564,15 @@ async function whichAgents(charterAuthoringApp2, typed) {
|
|
|
1367
1564
|
// What the settings already name arrives ticked, so a return keeps what this
|
|
1368
1565
|
// repository is already running under and a second run is rechoosing rather
|
|
1369
1566
|
// than starting over (FR-038). Nothing named yet — a repository being set up
|
|
1370
|
-
// for the first time — arrives with
|
|
1371
|
-
|
|
1567
|
+
// for the first time — arrives with the one agent ticked where this engine
|
|
1568
|
+
// compiles for one, and with nothing ticked otherwise (FR-055).
|
|
1569
|
+
choices: AGENT_PROVIDERS.map((one) => ({ title: one, value: one, selected: defaultAgents.includes(one) })),
|
|
1372
1570
|
// Ticking none of them is not an answer: a charter compiles for at least one
|
|
1373
1571
|
// agent, so the prompt holds the submit until one is ticked (FR-033).
|
|
1374
1572
|
min: 1
|
|
1375
1573
|
});
|
|
1376
1574
|
if (chosen === void 0)
|
|
1377
|
-
throw new
|
|
1575
|
+
throw new DomainFault("Setup was stopped before it had its answers.", 'Run "cw init" again.');
|
|
1378
1576
|
return chosen;
|
|
1379
1577
|
}
|
|
1380
1578
|
|
|
@@ -1472,6 +1670,133 @@ function inFull(entry) {
|
|
|
1472
1670
|
return [surveyed(entry), ` ${file}`, ...declared.length === 0 ? [] : [` ${declared.join(", ")}`]].join("\n");
|
|
1473
1671
|
}
|
|
1474
1672
|
|
|
1673
|
+
// src/driver/cli/commands/McpAuthCommand.ts
|
|
1674
|
+
import prompts3 from "prompts";
|
|
1675
|
+
var OPTIONS6 = {
|
|
1676
|
+
identity: {
|
|
1677
|
+
type: "string",
|
|
1678
|
+
describe: 'One mcp whose address to sign in to again, whatever you were signed in as: "mcp:github/billing"'
|
|
1679
|
+
},
|
|
1680
|
+
status: {
|
|
1681
|
+
type: "boolean",
|
|
1682
|
+
default: false,
|
|
1683
|
+
describe: "List each address, the mcps at it and whether you are signed in there, and change nothing"
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
var McpAuthCommand = class {
|
|
1687
|
+
name = "mcp auth [identity]";
|
|
1688
|
+
summary = "Sign in, as yourself, to every place the charter reaches";
|
|
1689
|
+
options = OPTIONS6;
|
|
1690
|
+
async run({ mcpConnectingApp: mcpConnectingApp2 }, { identity, status }) {
|
|
1691
|
+
const signInStatuses = (await mcpConnectingApp2.signInStatus()).map((one) => one.data);
|
|
1692
|
+
if (status)
|
|
1693
|
+
return {
|
|
1694
|
+
code: EXIT_OK,
|
|
1695
|
+
result: signInStatuses.length === 0 ? "No place the last build listed takes a sign-in.\n" : signInStatuses.map(
|
|
1696
|
+
({ address, identities, signedIn, method }) => [address, ` ${identities.join(", ")}`, ` ${signedIn ? `signed in by ${method}` : "not signed in"}`, ""].join("\n")
|
|
1697
|
+
).join("")
|
|
1698
|
+
};
|
|
1699
|
+
let unsignedStatuses;
|
|
1700
|
+
if (identity === void 0) unsignedStatuses = signInStatuses.filter((one) => !one.signedIn);
|
|
1701
|
+
else {
|
|
1702
|
+
const identityStatus = signInStatuses.find((one) => one.identities.includes(identity));
|
|
1703
|
+
if (identityStatus === void 0)
|
|
1704
|
+
throw new DomainFault(
|
|
1705
|
+
`No place the last build listed is declared by ${identity}.`,
|
|
1706
|
+
'Run "cw mcp auth --status" to see every mcp, or "cw build" if the charter changed.'
|
|
1707
|
+
);
|
|
1708
|
+
unsignedStatuses = [identityStatus];
|
|
1709
|
+
}
|
|
1710
|
+
if (unsignedStatuses.length === 0)
|
|
1711
|
+
return {
|
|
1712
|
+
code: EXIT_OK,
|
|
1713
|
+
result: signInStatuses.length === 0 ? "No place the last build listed takes a sign-in.\n" : `You are signed in at every address: ${signInStatuses.length}.
|
|
1714
|
+
`
|
|
1715
|
+
};
|
|
1716
|
+
if (!process.stdin.isTTY)
|
|
1717
|
+
return {
|
|
1718
|
+
code: EXIT_FAILURE,
|
|
1719
|
+
problem: [
|
|
1720
|
+
"Not signed in at:",
|
|
1721
|
+
...unsignedStatuses.map(({ address }) => ` ${address}`),
|
|
1722
|
+
' Run "cw mcp auth" at a terminal to sign in; nothing is asked without one.',
|
|
1723
|
+
""
|
|
1724
|
+
].join("\n")
|
|
1725
|
+
};
|
|
1726
|
+
for (const { address, identities, auth: auth2 } of unsignedStatuses) {
|
|
1727
|
+
process.stderr.write(`${address}
|
|
1728
|
+
${identities.join(", ")}
|
|
1729
|
+
`);
|
|
1730
|
+
const { method } = auth2.length > 1 ? await prompts3({
|
|
1731
|
+
type: "select",
|
|
1732
|
+
name: "method",
|
|
1733
|
+
message: "Sign in how?",
|
|
1734
|
+
choices: auth2.map((one) => ({ title: one === "oauth" ? "in the browser (OAuth)" : "with a token", value: one }))
|
|
1735
|
+
}) : { method: auth2[0] };
|
|
1736
|
+
if (method === "oauth")
|
|
1737
|
+
await mcpConnectingApp2.signInWithOAuth(
|
|
1738
|
+
address,
|
|
1739
|
+
(authorizationUrl) => process.stderr.write(` Open this address to sign in, then come back here:
|
|
1740
|
+
${authorizationUrl}
|
|
1741
|
+
`)
|
|
1742
|
+
);
|
|
1743
|
+
else if (method === "token") {
|
|
1744
|
+
const { token } = await prompts3({ type: "invisible", name: "token", message: "Token?" });
|
|
1745
|
+
if (token === void 0) throwInterruptFault(address);
|
|
1746
|
+
await mcpConnectingApp2.signInWithToken(address, token);
|
|
1747
|
+
} else throwInterruptFault(address);
|
|
1748
|
+
}
|
|
1749
|
+
return {
|
|
1750
|
+
code: EXIT_OK,
|
|
1751
|
+
result: `Signed in at ${unsignedStatuses.map(({ address }) => address).join(", ")}.
|
|
1752
|
+
`
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
function throwInterruptFault(address) {
|
|
1757
|
+
throw new DomainFault(`This was stopped before signing in at ${address}; what was signed in before it is kept.`, 'Run "cw mcp auth" again.');
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/driver/cli/commands/McpServeCommand.ts
|
|
1761
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1762
|
+
|
|
1763
|
+
// src/driver/mcp/server.ts
|
|
1764
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1765
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
1766
|
+
async function startMcpServer(mcpConnectingApp2, transport, version = "0.0.0") {
|
|
1767
|
+
const server = new Server({ name: "cw", version }, { capabilities: { tools: {} } });
|
|
1768
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1769
|
+
tools: (await mcpConnectingApp2.served()).data.tools.map(({ data }) => data)
|
|
1770
|
+
}));
|
|
1771
|
+
server.setRequestHandler(
|
|
1772
|
+
CallToolRequestSchema,
|
|
1773
|
+
async ({ params }) => (await mcpConnectingApp2.call(params.name, params.arguments ?? {})).data
|
|
1774
|
+
);
|
|
1775
|
+
const serverClosed = new Promise((resolve) => server.onclose = resolve);
|
|
1776
|
+
await server.connect(transport);
|
|
1777
|
+
return { serverClosed };
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// src/driver/cli/commands/McpServeCommand.ts
|
|
1781
|
+
var McpServeCommand = class {
|
|
1782
|
+
name = "mcp serve";
|
|
1783
|
+
summary = "Serve every place the charter reaches to your agent, over stdio";
|
|
1784
|
+
async run({ mcpConnectingApp: mcpConnectingApp2, version }) {
|
|
1785
|
+
const servedTools2 = await mcpConnectingApp2.served();
|
|
1786
|
+
for (const problem of servedTools2.data.problems) process.stderr.write(`${problem}
|
|
1787
|
+
`);
|
|
1788
|
+
const transport = new StdioServerTransport();
|
|
1789
|
+
const { serverClosed } = await startMcpServer(mcpConnectingApp2, transport, version);
|
|
1790
|
+
const closeTransport = () => void transport.close();
|
|
1791
|
+
process.stdin.once("end", closeTransport);
|
|
1792
|
+
process.once("SIGTERM", closeTransport);
|
|
1793
|
+
process.once("SIGINT", closeTransport);
|
|
1794
|
+
await serverClosed;
|
|
1795
|
+
await mcpConnectingApp2.stopServing();
|
|
1796
|
+
return { code: EXIT_OK };
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
|
|
1475
1800
|
// src/driver/portal/server.ts
|
|
1476
1801
|
import { randomBytes } from "crypto";
|
|
1477
1802
|
import { access } from "fs/promises";
|
|
@@ -1484,87 +1809,87 @@ import { serveStatic } from "@hono/node-server/serve-static";
|
|
|
1484
1809
|
// src/driver/portal/routes.ts
|
|
1485
1810
|
import { createRoute, OpenAPIHono } from "@hono/zod-openapi";
|
|
1486
1811
|
import { HTTPException } from "hono/http-exception";
|
|
1487
|
-
import { z as
|
|
1812
|
+
import { z as z11 } from "zod";
|
|
1488
1813
|
|
|
1489
1814
|
// src/hexagon/port/driver/dtos/dto.ts
|
|
1490
|
-
import { z as
|
|
1491
|
-
var strings =
|
|
1492
|
-
var dto = (type, data) =>
|
|
1815
|
+
import { z as z6 } from "zod";
|
|
1816
|
+
var strings = z6.array(z6.string()).readonly();
|
|
1817
|
+
var dto = (type, data) => z6.object({ type: z6.literal(type), data });
|
|
1493
1818
|
function byType(...schemas) {
|
|
1494
1819
|
return Object.fromEntries(schemas.map((one) => [one.shape.type.value, one]));
|
|
1495
1820
|
}
|
|
1496
1821
|
|
|
1497
1822
|
// src/hexagon/port/driver/dtos/data.ts
|
|
1498
|
-
import { z as
|
|
1823
|
+
import { z as z7 } from "zod";
|
|
1499
1824
|
var fault = dto(
|
|
1500
1825
|
"Fault",
|
|
1501
|
-
|
|
1502
|
-
message:
|
|
1503
|
-
fix:
|
|
1504
|
-
severity:
|
|
1826
|
+
z7.object({
|
|
1827
|
+
message: z7.string(),
|
|
1828
|
+
fix: z7.string(),
|
|
1829
|
+
severity: z7.enum(["error", "warn"])
|
|
1505
1830
|
})
|
|
1506
1831
|
);
|
|
1507
1832
|
var faultsByFile = dto(
|
|
1508
1833
|
"FaultsByFile",
|
|
1509
|
-
|
|
1510
|
-
files:
|
|
1834
|
+
z7.object({
|
|
1835
|
+
files: z7.record(z7.string(), z7.array(fault).readonly())
|
|
1511
1836
|
})
|
|
1512
1837
|
);
|
|
1513
1838
|
var faults = dto(
|
|
1514
1839
|
"Faults",
|
|
1515
|
-
|
|
1516
|
-
faults:
|
|
1840
|
+
z7.object({
|
|
1841
|
+
faults: z7.array(fault).readonly()
|
|
1517
1842
|
})
|
|
1518
1843
|
);
|
|
1519
|
-
var primitiveKinds = dto("PrimitiveKinds",
|
|
1844
|
+
var primitiveKinds = dto("PrimitiveKinds", z7.record(z7.string(), z7.string()));
|
|
1520
1845
|
var catalogueEntry = dto(
|
|
1521
1846
|
"CatalogueEntry",
|
|
1522
|
-
|
|
1523
|
-
identity:
|
|
1524
|
-
kind:
|
|
1525
|
-
id:
|
|
1526
|
-
description:
|
|
1527
|
-
file:
|
|
1847
|
+
z7.object({
|
|
1848
|
+
identity: z7.string(),
|
|
1849
|
+
kind: z7.string(),
|
|
1850
|
+
id: z7.string(),
|
|
1851
|
+
description: z7.string(),
|
|
1852
|
+
file: z7.string(),
|
|
1528
1853
|
tags: strings.optional(),
|
|
1529
1854
|
globs: strings.optional(),
|
|
1530
|
-
rationale:
|
|
1855
|
+
rationale: z7.string().optional(),
|
|
1531
1856
|
mixins: strings.optional()
|
|
1532
1857
|
})
|
|
1533
1858
|
);
|
|
1534
1859
|
var catalogue = dto(
|
|
1535
1860
|
"Catalogue",
|
|
1536
|
-
|
|
1537
|
-
entries:
|
|
1861
|
+
z7.object({
|
|
1862
|
+
entries: z7.array(catalogueEntry).readonly()
|
|
1538
1863
|
})
|
|
1539
1864
|
);
|
|
1540
1865
|
var scopedPrimitive = dto(
|
|
1541
1866
|
"ScopedPrimitive",
|
|
1542
|
-
|
|
1543
|
-
identity:
|
|
1544
|
-
kind:
|
|
1545
|
-
description:
|
|
1546
|
-
file:
|
|
1547
|
-
scope:
|
|
1548
|
-
headers:
|
|
1867
|
+
z7.object({
|
|
1868
|
+
identity: z7.string(),
|
|
1869
|
+
kind: z7.string(),
|
|
1870
|
+
description: z7.string(),
|
|
1871
|
+
file: z7.string(),
|
|
1872
|
+
scope: z7.enum(["repo", "vendor", "builtin"]),
|
|
1873
|
+
headers: z7.record(z7.string(), z7.union([z7.string(), strings]))
|
|
1549
1874
|
})
|
|
1550
1875
|
);
|
|
1551
1876
|
var scopedPrimitives = dto(
|
|
1552
1877
|
"ScopedPrimitives",
|
|
1553
|
-
|
|
1554
|
-
primitives:
|
|
1878
|
+
z7.object({
|
|
1879
|
+
primitives: z7.array(scopedPrimitive).readonly()
|
|
1555
1880
|
})
|
|
1556
1881
|
);
|
|
1557
1882
|
var primitiveSnapshot = dto(
|
|
1558
1883
|
"PrimitiveSnapshot",
|
|
1559
|
-
|
|
1884
|
+
z7.object({
|
|
1560
1885
|
scopedPrimitive,
|
|
1561
|
-
body:
|
|
1562
|
-
revision:
|
|
1886
|
+
body: z7.string(),
|
|
1887
|
+
revision: z7.string()
|
|
1563
1888
|
})
|
|
1564
1889
|
);
|
|
1565
1890
|
var planSummary = dto(
|
|
1566
1891
|
"PlanSummary",
|
|
1567
|
-
|
|
1892
|
+
z7.object({
|
|
1568
1893
|
added: strings,
|
|
1569
1894
|
edited: strings,
|
|
1570
1895
|
deleted: strings,
|
|
@@ -1573,63 +1898,89 @@ var planSummary = dto(
|
|
|
1573
1898
|
);
|
|
1574
1899
|
var primitiveHeader = dto(
|
|
1575
1900
|
"PrimitiveHeader",
|
|
1576
|
-
|
|
1577
|
-
field:
|
|
1578
|
-
shape:
|
|
1579
|
-
required:
|
|
1901
|
+
z7.object({
|
|
1902
|
+
field: z7.string(),
|
|
1903
|
+
shape: z7.enum(["line", "list"]),
|
|
1904
|
+
required: z7.boolean(),
|
|
1580
1905
|
allowedValues: strings.optional()
|
|
1581
1906
|
})
|
|
1582
1907
|
);
|
|
1583
1908
|
var primitiveRequirements = dto(
|
|
1584
1909
|
"PrimitiveRequirements",
|
|
1585
|
-
|
|
1586
|
-
headers:
|
|
1587
|
-
sample:
|
|
1910
|
+
z7.object({
|
|
1911
|
+
headers: z7.array(primitiveHeader).readonly(),
|
|
1912
|
+
sample: z7.string()
|
|
1588
1913
|
})
|
|
1589
1914
|
);
|
|
1590
|
-
var testCasesByFile = dto("TestCasesByFile",
|
|
1915
|
+
var testCasesByFile = dto("TestCasesByFile", z7.record(z7.string(), strings));
|
|
1591
1916
|
var testCaseReport = dto(
|
|
1592
1917
|
"TestCaseReport",
|
|
1593
|
-
|
|
1594
|
-
suiteName:
|
|
1595
|
-
situation:
|
|
1596
|
-
passed:
|
|
1918
|
+
z7.object({
|
|
1919
|
+
suiteName: z7.string(),
|
|
1920
|
+
situation: z7.string(),
|
|
1921
|
+
passed: z7.boolean(),
|
|
1597
1922
|
unmet: fault.optional()
|
|
1598
1923
|
})
|
|
1599
1924
|
);
|
|
1600
1925
|
var testRunReport = dto(
|
|
1601
1926
|
"TestRunReport",
|
|
1602
|
-
|
|
1603
|
-
testCaseReports:
|
|
1927
|
+
z7.object({
|
|
1928
|
+
testCaseReports: z7.array(testCaseReport).readonly()
|
|
1604
1929
|
})
|
|
1605
1930
|
);
|
|
1606
1931
|
var testCase = dto(
|
|
1607
1932
|
"TestCase",
|
|
1608
|
-
|
|
1609
|
-
situation:
|
|
1610
|
-
expectation:
|
|
1611
|
-
identity:
|
|
1933
|
+
z7.object({
|
|
1934
|
+
situation: z7.string(),
|
|
1935
|
+
expectation: z7.string(),
|
|
1936
|
+
identity: z7.string().optional()
|
|
1612
1937
|
})
|
|
1613
1938
|
);
|
|
1614
1939
|
var testSuite = dto(
|
|
1615
1940
|
"TestSuite",
|
|
1616
|
-
|
|
1617
|
-
name:
|
|
1618
|
-
text:
|
|
1619
|
-
description:
|
|
1620
|
-
cases:
|
|
1941
|
+
z7.object({
|
|
1942
|
+
name: z7.string(),
|
|
1943
|
+
text: z7.string().optional(),
|
|
1944
|
+
description: z7.string().optional(),
|
|
1945
|
+
cases: z7.array(testCase).readonly(),
|
|
1621
1946
|
fault: fault.optional()
|
|
1622
1947
|
})
|
|
1623
1948
|
);
|
|
1624
1949
|
var testSuites = dto(
|
|
1625
1950
|
"TestSuites",
|
|
1626
|
-
|
|
1627
|
-
testSuites:
|
|
1951
|
+
z7.object({
|
|
1952
|
+
testSuites: z7.array(testSuite).readonly()
|
|
1953
|
+
})
|
|
1954
|
+
);
|
|
1955
|
+
var signInStatus = dto(
|
|
1956
|
+
"SignInStatus",
|
|
1957
|
+
z7.object({
|
|
1958
|
+
address: z7.string(),
|
|
1959
|
+
identities: strings,
|
|
1960
|
+
auth: z7.array(z7.enum(["oauth", "token"])).readonly(),
|
|
1961
|
+
signedIn: z7.boolean(),
|
|
1962
|
+
method: z7.enum(["oauth", "token"]).optional()
|
|
1963
|
+
})
|
|
1964
|
+
);
|
|
1965
|
+
var servedTool = dto(
|
|
1966
|
+
"ServedTool",
|
|
1967
|
+
z7.object({
|
|
1968
|
+
name: z7.string(),
|
|
1969
|
+
description: z7.string(),
|
|
1970
|
+
inputSchema: z7.record(z7.string(), z7.unknown())
|
|
1971
|
+
})
|
|
1972
|
+
);
|
|
1973
|
+
var servedTools = dto(
|
|
1974
|
+
"ServedTools",
|
|
1975
|
+
z7.object({
|
|
1976
|
+
tools: z7.array(servedTool).readonly(),
|
|
1977
|
+
problems: strings
|
|
1628
1978
|
})
|
|
1629
1979
|
);
|
|
1980
|
+
var toolAnswer = dto("ToolAnswer", z7.record(z7.string(), z7.unknown()));
|
|
1630
1981
|
var workspaceSettings = dto(
|
|
1631
1982
|
"WorkspaceSettings",
|
|
1632
|
-
|
|
1983
|
+
z7.object({
|
|
1633
1984
|
agents: strings
|
|
1634
1985
|
})
|
|
1635
1986
|
);
|
|
@@ -1646,59 +1997,63 @@ var DataDTOs = byType(
|
|
|
1646
1997
|
primitiveSnapshot,
|
|
1647
1998
|
scopedPrimitive,
|
|
1648
1999
|
scopedPrimitives,
|
|
2000
|
+
servedTool,
|
|
2001
|
+
servedTools,
|
|
2002
|
+
signInStatus,
|
|
1649
2003
|
testCase,
|
|
1650
2004
|
testCaseReport,
|
|
1651
2005
|
testCasesByFile,
|
|
1652
2006
|
testRunReport,
|
|
1653
2007
|
testSuite,
|
|
1654
2008
|
testSuites,
|
|
2009
|
+
toolAnswer,
|
|
1655
2010
|
workspaceSettings
|
|
1656
2011
|
);
|
|
1657
2012
|
|
|
1658
2013
|
// src/hexagon/port/driver/dtos/outcome.ts
|
|
1659
|
-
import { z as
|
|
2014
|
+
import { z as z8 } from "zod";
|
|
1660
2015
|
var doctorOutcome = dto(
|
|
1661
2016
|
"DoctorOutcome",
|
|
1662
|
-
|
|
2017
|
+
z8.object({
|
|
1663
2018
|
agents: strings,
|
|
1664
|
-
errorCount:
|
|
1665
|
-
warnCount:
|
|
1666
|
-
pendingCount:
|
|
2019
|
+
errorCount: z8.number(),
|
|
2020
|
+
warnCount: z8.number(),
|
|
2021
|
+
pendingCount: z8.number().nullable(),
|
|
1667
2022
|
faultsByFile: DataDTOs.FaultsByFile,
|
|
1668
2023
|
driftedVendors: strings,
|
|
1669
|
-
problemCount:
|
|
2024
|
+
problemCount: z8.number()
|
|
1670
2025
|
})
|
|
1671
2026
|
);
|
|
1672
2027
|
var explanationOutcome = dto(
|
|
1673
2028
|
"ExplanationOutcome",
|
|
1674
|
-
|
|
2029
|
+
z8.object({
|
|
1675
2030
|
scopedPrimitive: DataDTOs.ScopedPrimitive,
|
|
1676
|
-
activatesWhen:
|
|
1677
|
-
useMixins:
|
|
2031
|
+
activatesWhen: z8.string(),
|
|
2032
|
+
useMixins: z8.array(DataDTOs.ScopedPrimitive).readonly(),
|
|
1678
2033
|
rationale: DataDTOs.ScopedPrimitive.optional(),
|
|
1679
|
-
hosts:
|
|
1680
|
-
citers:
|
|
2034
|
+
hosts: z8.array(DataDTOs.ScopedPrimitive).readonly(),
|
|
2035
|
+
citers: z8.array(DataDTOs.ScopedPrimitive).readonly(),
|
|
1681
2036
|
testCasesByFile: DataDTOs.TestCasesByFile
|
|
1682
2037
|
})
|
|
1683
2038
|
);
|
|
1684
2039
|
var OutcomeDTOs = byType(doctorOutcome, explanationOutcome);
|
|
1685
2040
|
|
|
1686
2041
|
// src/hexagon/domain/models/test/TestSuite.ts
|
|
1687
|
-
import { z as
|
|
2042
|
+
import { z as z10 } from "zod";
|
|
1688
2043
|
|
|
1689
2044
|
// src/hexagon/domain/models/test/TestCase.ts
|
|
1690
|
-
import { z as
|
|
1691
|
-
var TEST_CASE_SCHEMA =
|
|
1692
|
-
|
|
1693
|
-
do:
|
|
1694
|
-
expect:
|
|
1695
|
-
|
|
1696
|
-
|
|
2045
|
+
import { z as z9 } from "zod";
|
|
2046
|
+
var TEST_CASE_SCHEMA = z9.union([
|
|
2047
|
+
z9.strictObject({
|
|
2048
|
+
do: z9.strictObject({ touchFile: z9.string().min(1) }),
|
|
2049
|
+
expect: z9.union([
|
|
2050
|
+
z9.strictObject({ activate: PRIMITIVE_IDENTITY_SCHEMA }),
|
|
2051
|
+
z9.strictObject({ allow: z9.boolean() })
|
|
1697
2052
|
])
|
|
1698
2053
|
}),
|
|
1699
|
-
|
|
1700
|
-
when:
|
|
1701
|
-
expect:
|
|
2054
|
+
z9.strictObject({
|
|
2055
|
+
when: z9.enum(SIGNALS),
|
|
2056
|
+
expect: z9.strictObject({ run: PRIMITIVE_IDENTITY_SCHEMA })
|
|
1702
2057
|
})
|
|
1703
2058
|
]);
|
|
1704
2059
|
var TestCase = class {
|
|
@@ -1740,9 +2095,9 @@ var TestCase = class {
|
|
|
1740
2095
|
};
|
|
1741
2096
|
|
|
1742
2097
|
// src/hexagon/domain/models/test/TestSuite.ts
|
|
1743
|
-
var TEST_SUITE_SCHEMA =
|
|
1744
|
-
description:
|
|
1745
|
-
cases:
|
|
2098
|
+
var TEST_SUITE_SCHEMA = z10.strictObject({
|
|
2099
|
+
description: z10.string().min(1).optional(),
|
|
2100
|
+
cases: z10.array(TEST_CASE_SCHEMA.transform((written) => new TestCase(written))).min(1)
|
|
1746
2101
|
});
|
|
1747
2102
|
var TestSuite = class {
|
|
1748
2103
|
constructor(description, cases, body) {
|
|
@@ -1809,7 +2164,7 @@ function testRootOf(testSuiteFiles) {
|
|
|
1809
2164
|
try {
|
|
1810
2165
|
suitesByFile[path] = testSuiteOf(contents);
|
|
1811
2166
|
} catch (raised) {
|
|
1812
|
-
if (!(raised instanceof
|
|
2167
|
+
if (!(raised instanceof DomainFault)) throw raised;
|
|
1813
2168
|
faultsByFiles[path] = [raised];
|
|
1814
2169
|
}
|
|
1815
2170
|
}
|
|
@@ -1962,7 +2317,7 @@ var json = (schema, description) => ({
|
|
|
1962
2317
|
function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
1963
2318
|
const app = new OpenAPIHono();
|
|
1964
2319
|
app.onError((raised, c) => {
|
|
1965
|
-
if (raised instanceof
|
|
2320
|
+
if (raised instanceof DomainFault) return c.json(faultDTO(raised), 422);
|
|
1966
2321
|
if (raised instanceof HTTPException) return raised.getResponse();
|
|
1967
2322
|
console.error(raised);
|
|
1968
2323
|
return c.text(raised.message, 500);
|
|
@@ -1978,10 +2333,10 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
1978
2333
|
createRoute({
|
|
1979
2334
|
method: "get",
|
|
1980
2335
|
path: "/charter/root/primitives",
|
|
1981
|
-
request: { query:
|
|
2336
|
+
request: { query: z11.object({ matching: z11.string().optional() }) },
|
|
1982
2337
|
responses: {
|
|
1983
2338
|
200: json(DataDTOs.ScopedPrimitives, "Every primitive of every layer, or those mentioning the word"),
|
|
1984
|
-
422: json(
|
|
2339
|
+
422: json(z11.union([DataDTOs.FaultsByFile, DataDTOs.Fault]), "The charter does not hold")
|
|
1985
2340
|
}
|
|
1986
2341
|
}),
|
|
1987
2342
|
async (c) => {
|
|
@@ -1992,10 +2347,10 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
1992
2347
|
createRoute({
|
|
1993
2348
|
method: "get",
|
|
1994
2349
|
path: "/charter/root/primitives/{identity}/explanation",
|
|
1995
|
-
request: { params:
|
|
2350
|
+
request: { params: z11.object({ identity: z11.string() }) },
|
|
1996
2351
|
responses: {
|
|
1997
2352
|
200: json(OutcomeDTOs.ExplanationOutcome, "What the engine says of one primitive"),
|
|
1998
|
-
422: json(
|
|
2353
|
+
422: json(z11.union([DataDTOs.FaultsByFile, DataDTOs.Fault]), "The charter does not hold, or holds nothing of it")
|
|
1999
2354
|
}
|
|
2000
2355
|
}),
|
|
2001
2356
|
async (c) => {
|
|
@@ -2009,11 +2364,11 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2009
2364
|
path: "/charter/root/primitives",
|
|
2010
2365
|
request: {
|
|
2011
2366
|
body: json(
|
|
2012
|
-
|
|
2013
|
-
kind:
|
|
2014
|
-
id:
|
|
2015
|
-
headers:
|
|
2016
|
-
body:
|
|
2367
|
+
z11.object({
|
|
2368
|
+
kind: z11.string(),
|
|
2369
|
+
id: z11.string(),
|
|
2370
|
+
headers: z11.record(z11.string(), z11.union([z11.string(), z11.array(z11.string())])),
|
|
2371
|
+
body: z11.string()
|
|
2017
2372
|
}),
|
|
2018
2373
|
"A new primitive: its kind and id, a line or a list under each header, and its body"
|
|
2019
2374
|
)
|
|
@@ -2021,26 +2376,26 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2021
2376
|
responses: {
|
|
2022
2377
|
201: {
|
|
2023
2378
|
...json(DataDTOs.ScopedPrimitive, "The primitive written"),
|
|
2024
|
-
headers:
|
|
2379
|
+
headers: z11.object({ Location: z11.string() })
|
|
2025
2380
|
},
|
|
2026
|
-
422: json(
|
|
2381
|
+
422: json(z11.union([DataDTOs.Faults, DataDTOs.Fault]), "The answers the kind refused, or an identity already claimed")
|
|
2027
2382
|
}
|
|
2028
2383
|
}),
|
|
2029
2384
|
async (c) => {
|
|
2030
2385
|
const { kind, id, headers, body } = c.req.valid("json");
|
|
2031
2386
|
const scopedPrimitiveDTO2 = await charterAuthoringApp2.add(kind, id, headers, body);
|
|
2032
2387
|
if (scopedPrimitiveDTO2.type === "Faults") return c.json(scopedPrimitiveDTO2, 422);
|
|
2033
|
-
return c.json(scopedPrimitiveDTO2, 201, { Location: `/api/charter/root/primitives/${scopedPrimitiveDTO2.data.identity}` });
|
|
2388
|
+
return c.json(scopedPrimitiveDTO2, 201, { Location: `/api/charter/root/primitives/${encodeURIComponent(scopedPrimitiveDTO2.data.identity)}` });
|
|
2034
2389
|
}
|
|
2035
2390
|
).openapi(
|
|
2036
2391
|
createRoute({
|
|
2037
2392
|
method: "get",
|
|
2038
2393
|
path: "/charter/root/primitives/{identity}",
|
|
2039
|
-
request: { params:
|
|
2394
|
+
request: { params: z11.object({ identity: z11.string() }) },
|
|
2040
2395
|
responses: {
|
|
2041
2396
|
200: {
|
|
2042
2397
|
...json(DataDTOs.PrimitiveSnapshot, "One primitive as the charter read it"),
|
|
2043
|
-
headers:
|
|
2398
|
+
headers: z11.object({ ETag: z11.string() })
|
|
2044
2399
|
},
|
|
2045
2400
|
422: json(DataDTOs.Fault, "The charter holds nothing of it")
|
|
2046
2401
|
}
|
|
@@ -2055,12 +2410,12 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2055
2410
|
method: "put",
|
|
2056
2411
|
path: "/charter/root/primitives/{identity}",
|
|
2057
2412
|
request: {
|
|
2058
|
-
params:
|
|
2059
|
-
headers:
|
|
2413
|
+
params: z11.object({ identity: z11.string() }),
|
|
2414
|
+
headers: z11.object({ "if-match": z11.string().optional() }),
|
|
2060
2415
|
body: json(
|
|
2061
|
-
|
|
2062
|
-
headers:
|
|
2063
|
-
body:
|
|
2416
|
+
z11.object({
|
|
2417
|
+
headers: z11.record(z11.string(), z11.union([z11.string(), z11.array(z11.string())])),
|
|
2418
|
+
body: z11.string()
|
|
2064
2419
|
}),
|
|
2065
2420
|
"A line or a list under each header, and the body, written over the primitive's own"
|
|
2066
2421
|
)
|
|
@@ -2068,7 +2423,7 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2068
2423
|
responses: {
|
|
2069
2424
|
200: json(DataDTOs.ScopedPrimitive, "The primitive written"),
|
|
2070
2425
|
412: json(DataDTOs.Fault, "The file changed on disk after it was opened"),
|
|
2071
|
-
422: json(
|
|
2426
|
+
422: json(z11.union([DataDTOs.Faults, DataDTOs.Fault]), "The answers the kind refused, or a primitive this repository did not author")
|
|
2072
2427
|
}
|
|
2073
2428
|
}),
|
|
2074
2429
|
async (c) => {
|
|
@@ -2077,7 +2432,7 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2077
2432
|
if (c.req.valid("header")["if-match"] !== `"${revision}"`)
|
|
2078
2433
|
return c.json(
|
|
2079
2434
|
faultDTO(
|
|
2080
|
-
new
|
|
2435
|
+
new DomainFault(
|
|
2081
2436
|
`${scopedPrimitive2.data.file} changed on disk after it was opened, and saving would write over that change.`,
|
|
2082
2437
|
"Open it again to see what changed, then make your edit there."
|
|
2083
2438
|
)
|
|
@@ -2092,7 +2447,7 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2092
2447
|
createRoute({
|
|
2093
2448
|
method: "delete",
|
|
2094
2449
|
path: "/charter/root/primitives/{identity}",
|
|
2095
|
-
request: { params:
|
|
2450
|
+
request: { params: z11.object({ identity: z11.string() }) },
|
|
2096
2451
|
responses: {
|
|
2097
2452
|
204: { description: "The file is gone" },
|
|
2098
2453
|
422: json(DataDTOs.Fault, "A primitive this repository did not author, or none at all")
|
|
@@ -2114,7 +2469,7 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2114
2469
|
createRoute({
|
|
2115
2470
|
method: "get",
|
|
2116
2471
|
path: "/definitions/kinds/{kind}/requirements",
|
|
2117
|
-
request: { params:
|
|
2472
|
+
request: { params: z11.object({ kind: z11.string() }) },
|
|
2118
2473
|
responses: {
|
|
2119
2474
|
200: json(DataDTOs.PrimitiveRequirements, "Every header the kind takes, and its sample"),
|
|
2120
2475
|
422: json(DataDTOs.Fault, "A word that is no kind")
|
|
@@ -2167,8 +2522,8 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2167
2522
|
path: "/test-suites",
|
|
2168
2523
|
responses: {
|
|
2169
2524
|
201: {
|
|
2170
|
-
...json(
|
|
2171
|
-
headers:
|
|
2525
|
+
...json(z11.string(), "The name of the test file written"),
|
|
2526
|
+
headers: z11.object({ Location: z11.string() })
|
|
2172
2527
|
}
|
|
2173
2528
|
}
|
|
2174
2529
|
}),
|
|
@@ -2181,11 +2536,11 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2181
2536
|
method: "put",
|
|
2182
2537
|
path: "/test-suites/{name}",
|
|
2183
2538
|
request: {
|
|
2184
|
-
params:
|
|
2185
|
-
body: json(
|
|
2539
|
+
params: z11.object({ name: z11.string() }),
|
|
2540
|
+
body: json(z11.object({ text: z11.string() }), "The whole text the test file is written over with")
|
|
2186
2541
|
},
|
|
2187
2542
|
responses: {
|
|
2188
|
-
200: json(
|
|
2543
|
+
200: json(z11.string(), "The name of the test file written"),
|
|
2189
2544
|
422: json(DataDTOs.Fault, "Text that is not a suite, with its sample, or a name that is no test file")
|
|
2190
2545
|
}
|
|
2191
2546
|
}),
|
|
@@ -2194,7 +2549,7 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2194
2549
|
createRoute({
|
|
2195
2550
|
method: "delete",
|
|
2196
2551
|
path: "/test-suites/{name}",
|
|
2197
|
-
request: { params:
|
|
2552
|
+
request: { params: z11.object({ name: z11.string() }) },
|
|
2198
2553
|
responses: {
|
|
2199
2554
|
204: { description: "The test file is gone" },
|
|
2200
2555
|
422: json(DataDTOs.Fault, "A name that is no test file")
|
|
@@ -2221,7 +2576,7 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2221
2576
|
createRoute({
|
|
2222
2577
|
method: "get",
|
|
2223
2578
|
path: "/vendors",
|
|
2224
|
-
responses: { 200: json(
|
|
2579
|
+
responses: { 200: json(z11.array(z11.string()).readonly(), "Every folder a vendor source was installed as") }
|
|
2225
2580
|
}),
|
|
2226
2581
|
async (c) => c.json(await charterVendoringApp2.installed(), 200)
|
|
2227
2582
|
).openapi(
|
|
@@ -2230,7 +2585,7 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2230
2585
|
path: "/vendors",
|
|
2231
2586
|
request: {
|
|
2232
2587
|
body: json(
|
|
2233
|
-
|
|
2588
|
+
z11.object({ source: z11.string(), version: z11.string().optional() }),
|
|
2234
2589
|
"A source as git fetches it, and the version to pin it to where one is named"
|
|
2235
2590
|
)
|
|
2236
2591
|
},
|
|
@@ -2248,7 +2603,7 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2248
2603
|
createRoute({
|
|
2249
2604
|
method: "delete",
|
|
2250
2605
|
path: "/vendors/{name}",
|
|
2251
|
-
request: { params:
|
|
2606
|
+
request: { params: z11.object({ name: z11.string() }) },
|
|
2252
2607
|
responses: {
|
|
2253
2608
|
204: { description: "The folder is gone, and that it is gone is committed" },
|
|
2254
2609
|
422: json(DataDTOs.Fault, "No repository, work in hand, or what git refused")
|
|
@@ -2265,7 +2620,7 @@ function api(charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2) {
|
|
|
2265
2620
|
var DEFAULT_PORT = 9927;
|
|
2266
2621
|
async function startPortal(page, charterAuthoringApp2, charterVendoringApp2, testAuthoringApp2, port = DEFAULT_PORT) {
|
|
2267
2622
|
await access(new URL("index.html", page)).catch(() => {
|
|
2268
|
-
throw new
|
|
2623
|
+
throw new DomainFault("The portal's page is not built.", 'Run "pnpm build", then start the portal again.');
|
|
2269
2624
|
});
|
|
2270
2625
|
const token = randomBytes(32).toString("base64url");
|
|
2271
2626
|
for (let next = port; ; next++) {
|
|
@@ -2300,7 +2655,7 @@ function listen(app, port) {
|
|
|
2300
2655
|
}
|
|
2301
2656
|
|
|
2302
2657
|
// src/driver/cli/commands/PortalCommand.ts
|
|
2303
|
-
var
|
|
2658
|
+
var OPTIONS7 = {
|
|
2304
2659
|
port: {
|
|
2305
2660
|
type: "number",
|
|
2306
2661
|
describe: "Which port to serve on, the next free one when it is taken",
|
|
@@ -2310,7 +2665,7 @@ var OPTIONS6 = {
|
|
|
2310
2665
|
var PortalCommand = class {
|
|
2311
2666
|
name = "portal";
|
|
2312
2667
|
summary = "Serve this repository's charter to a browser on this machine";
|
|
2313
|
-
options =
|
|
2668
|
+
options = OPTIONS7;
|
|
2314
2669
|
async run({ charterAuthoringApp: charterAuthoringApp2, charterVendoringApp: charterVendoringApp2, testAuthoringApp: testAuthoringApp2 }, { port }) {
|
|
2315
2670
|
await charterAuthoringApp2.ensureRepoReady();
|
|
2316
2671
|
const { address } = await startPortal(
|
|
@@ -2327,8 +2682,8 @@ Press Ctrl+C to stop it.
|
|
|
2327
2682
|
};
|
|
2328
2683
|
|
|
2329
2684
|
// src/driver/cli/commands/RemoveCommand.ts
|
|
2330
|
-
import
|
|
2331
|
-
var
|
|
2685
|
+
import prompts4 from "prompts";
|
|
2686
|
+
var OPTIONS8 = {
|
|
2332
2687
|
identity: {
|
|
2333
2688
|
type: "string",
|
|
2334
2689
|
describe: 'What the primitive is named by across the whole charter: "guide:no-any"'
|
|
@@ -2343,16 +2698,16 @@ var OPTIONS7 = {
|
|
|
2343
2698
|
var RemoveCommand = class {
|
|
2344
2699
|
name = "remove <identity>";
|
|
2345
2700
|
summary = "Delete the file of one primitive this repository authored";
|
|
2346
|
-
options =
|
|
2701
|
+
options = OPTIONS8;
|
|
2347
2702
|
async run({ charterAuthoringApp: charterAuthoringApp2 }, { identity, yes }) {
|
|
2348
2703
|
const { scopedPrimitive: scopedPrimitive2 } = (await charterAuthoringApp2.open(identity ?? "")).data;
|
|
2349
2704
|
if (!yes && scopedPrimitive2.data.scope === REPO_SCOPE) {
|
|
2350
2705
|
if (!process.stdin.isTTY)
|
|
2351
|
-
throw new
|
|
2706
|
+
throw new DomainFault(
|
|
2352
2707
|
`Removing ${scopedPrimitive2.data.file} is asked first, and nobody is at the terminal to answer.`,
|
|
2353
2708
|
`Run "cw remove ${scopedPrimitive2.data.identity} --yes" to remove it without asking.`
|
|
2354
2709
|
);
|
|
2355
|
-
const { confirmed } = await
|
|
2710
|
+
const { confirmed } = await prompts4({
|
|
2356
2711
|
type: "confirm",
|
|
2357
2712
|
name: "confirmed",
|
|
2358
2713
|
message: `Remove ${scopedPrimitive2.data.file}?`,
|
|
@@ -2386,7 +2741,7 @@ var SuiteAddCommand = class {
|
|
|
2386
2741
|
};
|
|
2387
2742
|
|
|
2388
2743
|
// src/driver/cli/commands/SuiteEditCommand.ts
|
|
2389
|
-
var
|
|
2744
|
+
var OPTIONS9 = {
|
|
2390
2745
|
name: {
|
|
2391
2746
|
type: "string",
|
|
2392
2747
|
describe: `The test file's name under ${TEST_DIRECTORY}/: "untitled-1.json"`
|
|
@@ -2395,11 +2750,11 @@ var OPTIONS8 = {
|
|
|
2395
2750
|
var SuiteEditCommand = class {
|
|
2396
2751
|
name = "suite edit <name>";
|
|
2397
2752
|
summary = "Open one test file in your editor";
|
|
2398
|
-
options =
|
|
2753
|
+
options = OPTIONS9;
|
|
2399
2754
|
async run({ cwd, testAuthoringApp: testAuthoringApp2 }, { name }) {
|
|
2400
2755
|
const { testSuites: testSuites2 } = (await testAuthoringApp2.suites()).data;
|
|
2401
2756
|
if (!testSuites2.some((one) => one.data.name === name))
|
|
2402
|
-
throw new
|
|
2757
|
+
throw new DomainFault(`${TEST_DIRECTORY}/ holds no test file called "${name ?? ""}".`, 'Run "cw suite add" to write a new one.');
|
|
2403
2758
|
const file = `${TEST_DIRECTORY}/${name}`;
|
|
2404
2759
|
const problem = await editInEditor(cwd, file);
|
|
2405
2760
|
if (problem !== void 0) return { code: EXIT_FAILURE, problem };
|
|
@@ -2408,7 +2763,7 @@ var SuiteEditCommand = class {
|
|
|
2408
2763
|
};
|
|
2409
2764
|
|
|
2410
2765
|
// src/driver/cli/commands/SuiteRemoveCommand.ts
|
|
2411
|
-
var
|
|
2766
|
+
var OPTIONS10 = {
|
|
2412
2767
|
name: {
|
|
2413
2768
|
type: "string",
|
|
2414
2769
|
describe: `The test file's name under ${TEST_DIRECTORY}/: "untitled-1.json"`
|
|
@@ -2417,7 +2772,7 @@ var OPTIONS9 = {
|
|
|
2417
2772
|
var SuiteRemoveCommand = class {
|
|
2418
2773
|
name = "suite remove <name>";
|
|
2419
2774
|
summary = "Delete one test file";
|
|
2420
|
-
options =
|
|
2775
|
+
options = OPTIONS10;
|
|
2421
2776
|
async run({ testAuthoringApp: testAuthoringApp2 }, { name }) {
|
|
2422
2777
|
const removedName = await testAuthoringApp2.removeSuite(name ?? "");
|
|
2423
2778
|
return { code: EXIT_OK, result: `Removed ${TEST_DIRECTORY}/${removedName}.
|
|
@@ -2464,7 +2819,7 @@ function lines2(one) {
|
|
|
2464
2819
|
}
|
|
2465
2820
|
|
|
2466
2821
|
// src/driver/cli/commands/VendorAddCommand.ts
|
|
2467
|
-
var
|
|
2822
|
+
var OPTIONS11 = {
|
|
2468
2823
|
source: {
|
|
2469
2824
|
type: "string",
|
|
2470
2825
|
describe: "What git clones the charter from: an address over any transport, or a path on disk"
|
|
@@ -2477,7 +2832,7 @@ var OPTIONS10 = {
|
|
|
2477
2832
|
var VendorAddCommand = class {
|
|
2478
2833
|
name = "vendor add <source>";
|
|
2479
2834
|
summary = "Install a charter from a git source, or bring an installed one up to date";
|
|
2480
|
-
options =
|
|
2835
|
+
options = OPTIONS11;
|
|
2481
2836
|
async run({ charterVendoringApp: charterVendoringApp2 }, { source, ref }) {
|
|
2482
2837
|
const folder = await charterVendoringApp2.add(source ?? "", ref);
|
|
2483
2838
|
return {
|
|
@@ -2505,7 +2860,7 @@ var VendorListCommand = class {
|
|
|
2505
2860
|
};
|
|
2506
2861
|
|
|
2507
2862
|
// src/driver/cli/commands/VendorRemoveCommand.ts
|
|
2508
|
-
var
|
|
2863
|
+
var OPTIONS12 = {
|
|
2509
2864
|
name: {
|
|
2510
2865
|
type: "string",
|
|
2511
2866
|
describe: "The folder this charter was installed as, as it reads under .cw/vendor/"
|
|
@@ -2514,7 +2869,7 @@ var OPTIONS11 = {
|
|
|
2514
2869
|
var VendorRemoveCommand = class {
|
|
2515
2870
|
name = "vendor remove <name>";
|
|
2516
2871
|
summary = "Take an installed charter away";
|
|
2517
|
-
options =
|
|
2872
|
+
options = OPTIONS12;
|
|
2518
2873
|
async run({ charterVendoringApp: charterVendoringApp2 }, { name }) {
|
|
2519
2874
|
const folder = await charterVendoringApp2.remove(name ?? "");
|
|
2520
2875
|
return {
|
|
@@ -2546,7 +2901,9 @@ var COMMANDS = [
|
|
|
2546
2901
|
new VendorAddCommand(),
|
|
2547
2902
|
new VendorRemoveCommand(),
|
|
2548
2903
|
new VendorListCommand(),
|
|
2549
|
-
new PortalCommand()
|
|
2904
|
+
new PortalCommand(),
|
|
2905
|
+
new McpAuthCommand(),
|
|
2906
|
+
new McpServeCommand()
|
|
2550
2907
|
];
|
|
2551
2908
|
|
|
2552
2909
|
// src/driver/cli/Commander.ts
|
|
@@ -2603,7 +2960,7 @@ var Commander = class {
|
|
|
2603
2960
|
try {
|
|
2604
2961
|
collect(await command.run(this.#context, options));
|
|
2605
2962
|
} catch (raised) {
|
|
2606
|
-
if (!(raised instanceof
|
|
2963
|
+
if (!(raised instanceof DomainFault)) throw raised;
|
|
2607
2964
|
collect({ code: EXIT_FAILURE, problem: `${raised.message}
|
|
2608
2965
|
${raised.fix}
|
|
2609
2966
|
` });
|
|
@@ -2642,7 +2999,6 @@ var CwAuthorSkill = class extends SkillPrimitive {
|
|
|
2642
2999
|
"write a rule",
|
|
2643
3000
|
"add a standard",
|
|
2644
3001
|
"write a skill",
|
|
2645
|
-
"add a command",
|
|
2646
3002
|
"author a primitive of the charter"
|
|
2647
3003
|
]
|
|
2648
3004
|
},
|
|
@@ -2707,7 +3063,7 @@ async function writeCharter(repo, primitive, fileReader2, fileWriter2) {
|
|
|
2707
3063
|
const path = `${CHARTER_DIRECTORY}/${under2}`;
|
|
2708
3064
|
const file = new URL(under2, charterFolderIn(repo));
|
|
2709
3065
|
if (await fileReader2.readIfThere(file) !== void 0)
|
|
2710
|
-
throw new
|
|
3066
|
+
throw new DomainFault(
|
|
2711
3067
|
`${path} is already there, and writing this one would write over what it holds.`,
|
|
2712
3068
|
`Open it, or run this again with an identity this charter has not got.`
|
|
2713
3069
|
);
|
|
@@ -2716,9 +3072,9 @@ async function writeCharter(repo, primitive, fileReader2, fileWriter2) {
|
|
|
2716
3072
|
}
|
|
2717
3073
|
|
|
2718
3074
|
// src/hexagon/domain/models/Settings.ts
|
|
2719
|
-
import { z as
|
|
2720
|
-
var SETTINGS =
|
|
2721
|
-
agents:
|
|
3075
|
+
import { z as z12 } from "zod";
|
|
3076
|
+
var SETTINGS = z12.object({
|
|
3077
|
+
agents: z12.array(z12.enum(AGENT_PROVIDERS))
|
|
2722
3078
|
});
|
|
2723
3079
|
var WorkspaceSettings = class {
|
|
2724
3080
|
constructor(agents) {
|
|
@@ -2811,12 +3167,12 @@ async function loadVendorNames(repo, fileReaders) {
|
|
|
2811
3167
|
}
|
|
2812
3168
|
async function ensureVCSReady(repo, vcs2, because) {
|
|
2813
3169
|
if (!await vcs2.isInstalled(repo))
|
|
2814
|
-
throw new
|
|
3170
|
+
throw new DomainFault(
|
|
2815
3171
|
"This folder is not inside a git repository, and a vendor lives in one.",
|
|
2816
3172
|
'Run "git init" here, or run this again where your repository is.'
|
|
2817
3173
|
);
|
|
2818
3174
|
if (!await vcs2.isClean(repo))
|
|
2819
|
-
throw new
|
|
3175
|
+
throw new DomainFault(
|
|
2820
3176
|
`This repository has work in hand, and ${because}.`,
|
|
2821
3177
|
"Commit or stash what you are holding, then run this again."
|
|
2822
3178
|
);
|
|
@@ -2966,19 +3322,7 @@ async function contentHashOf(content) {
|
|
|
2966
3322
|
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2967
3323
|
}
|
|
2968
3324
|
|
|
2969
|
-
// src/hexagon/domain/models/output/
|
|
2970
|
-
var GENERATED_STAMP = "Generated by cherry-works";
|
|
2971
|
-
function stamp(document) {
|
|
2972
|
-
return `${document.replace(/\n+$/, "")}
|
|
2973
|
-
|
|
2974
|
-
<!-- ${GENERATED_STAMP}. Do not edit; edit the charter and build again. -->
|
|
2975
|
-
`;
|
|
2976
|
-
}
|
|
2977
|
-
function isStamped(contents) {
|
|
2978
|
-
return contents.includes(GENERATED_STAMP);
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
|
-
// src/hexagon/domain/models/output/CharterOutput.ts
|
|
3325
|
+
// src/hexagon/domain/models/output/common/Catalogue.ts
|
|
2982
3326
|
var Catalogue = class _Catalogue {
|
|
2983
3327
|
constructor(entries) {
|
|
2984
3328
|
this.entries = entries;
|
|
@@ -3020,6 +3364,20 @@ var Catalogue = class _Catalogue {
|
|
|
3020
3364
|
return this.entries.map(({ identity, kind, id, description }) => ({ identity, kind, id, description }));
|
|
3021
3365
|
}
|
|
3022
3366
|
};
|
|
3367
|
+
|
|
3368
|
+
// src/hexagon/domain/models/output/StampedDocument.ts
|
|
3369
|
+
var GENERATED_STAMP = "Generated by cherry-works";
|
|
3370
|
+
function stamp(document) {
|
|
3371
|
+
return `${document.replace(/\n+$/, "")}
|
|
3372
|
+
|
|
3373
|
+
<!-- ${GENERATED_STAMP}. Do not edit; edit the charter and build again. -->
|
|
3374
|
+
`;
|
|
3375
|
+
}
|
|
3376
|
+
function isStamped(contents) {
|
|
3377
|
+
return contents.includes(GENERATED_STAMP);
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
// src/hexagon/domain/models/output/common/CharterMd.ts
|
|
3023
3381
|
var CharterMd = class _CharterMd {
|
|
3024
3382
|
constructor(body) {
|
|
3025
3383
|
this.body = body;
|
|
@@ -3050,34 +3408,26 @@ var CharterMd = class _CharterMd {
|
|
|
3050
3408
|
}
|
|
3051
3409
|
};
|
|
3052
3410
|
|
|
3053
|
-
// src/hexagon/domain/models/output/
|
|
3054
|
-
var
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
"Stop",
|
|
3060
|
-
"SubagentStop",
|
|
3061
|
-
"PreCompact",
|
|
3062
|
-
"SessionStart",
|
|
3063
|
-
"SessionEnd"
|
|
3064
|
-
];
|
|
3065
|
-
function isClaudeHookEvent(name) {
|
|
3066
|
-
return CLAUDE_HOOK_EVENTS.includes(name);
|
|
3067
|
-
}
|
|
3068
|
-
var ClaudeSettingsComponent = class _ClaudeSettingsComponent {
|
|
3069
|
-
constructor(settings) {
|
|
3070
|
-
this.settings = settings;
|
|
3411
|
+
// src/hexagon/domain/models/output/common/CompiledPrimitive.ts
|
|
3412
|
+
var CompiledPrimitive = class _CompiledPrimitive {
|
|
3413
|
+
constructor(identity, file, document) {
|
|
3414
|
+
this.identity = identity;
|
|
3415
|
+
this.file = file;
|
|
3416
|
+
this.document = document;
|
|
3071
3417
|
}
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3418
|
+
identity;
|
|
3419
|
+
file;
|
|
3420
|
+
document;
|
|
3421
|
+
static of(one, document) {
|
|
3422
|
+
return new _CompiledPrimitive(one.identity, `${OUT_DIRECTORY}/${one.primitive.kind}/${one.primitive.headers.id}.md`, document);
|
|
3423
|
+
}
|
|
3424
|
+
/** Generated, and the charter's alone: written whole over whatever is there,
|
|
3425
|
+
* and gone with the primitive it was compiled from. */
|
|
3426
|
+
projection = "replace";
|
|
3427
|
+
/** This document as a reader opens it, with the stamp saying where to change
|
|
3428
|
+
* it instead. */
|
|
3429
|
+
toStampedDocument() {
|
|
3430
|
+
return stamp(this.document);
|
|
3081
3431
|
}
|
|
3082
3432
|
};
|
|
3083
3433
|
|
|
@@ -3128,15 +3478,6 @@ var ClaudeAgentComponent = class _ClaudeAgentComponent extends DocumentBasedComp
|
|
|
3128
3478
|
}
|
|
3129
3479
|
};
|
|
3130
3480
|
|
|
3131
|
-
// src/hexagon/domain/models/output/providers/claude/document-based-components/ClaudeCommandComponent.ts
|
|
3132
|
-
var ClaudeCommandComponent = class _ClaudeCommandComponent extends DocumentBasedComponent {
|
|
3133
|
-
static kind = "command";
|
|
3134
|
-
kind = _ClaudeCommandComponent.kind;
|
|
3135
|
-
static of(name, headers, body) {
|
|
3136
|
-
return new _ClaudeCommandComponent(name, headers, body);
|
|
3137
|
-
}
|
|
3138
|
-
};
|
|
3139
|
-
|
|
3140
3481
|
// src/hexagon/domain/models/output/providers/claude/document-based-components/ClaudeRuleComponent.ts
|
|
3141
3482
|
var ClaudeRuleComponent = class _ClaudeRuleComponent extends DocumentBasedComponent {
|
|
3142
3483
|
static kind = "rule";
|
|
@@ -3146,6 +3487,57 @@ var ClaudeRuleComponent = class _ClaudeRuleComponent extends DocumentBasedCompon
|
|
|
3146
3487
|
}
|
|
3147
3488
|
};
|
|
3148
3489
|
|
|
3490
|
+
// src/hexagon/domain/models/output/providers/claude/ClaudeMcpConfigComponent.ts
|
|
3491
|
+
var ClaudeMcpConfigComponent = class _ClaudeMcpConfigComponent {
|
|
3492
|
+
static kind = "mcp-config";
|
|
3493
|
+
kind = _ClaudeMcpConfigComponent.kind;
|
|
3494
|
+
/** What this host names the server, and so what it puts before each tool it
|
|
3495
|
+
* serves: `mcp__cw__<tool>`. */
|
|
3496
|
+
static cwMcpName = "cw";
|
|
3497
|
+
/** That file is the repository's too — every server it wrote itself is
|
|
3498
|
+
* there — so the charter's one entry is written into it, and it is never
|
|
3499
|
+
* taken away, as the settings are not (FR-146). */
|
|
3500
|
+
projection = "mergeJSON";
|
|
3501
|
+
/** The one entry the charter speaks for, under `mcpServers`. */
|
|
3502
|
+
mcpServers = { [_ClaudeMcpConfigComponent.cwMcpName]: { command: "cw", args: ["mcp", "serve"] } };
|
|
3503
|
+
static of() {
|
|
3504
|
+
return new _ClaudeMcpConfigComponent();
|
|
3505
|
+
}
|
|
3506
|
+
constructor() {
|
|
3507
|
+
}
|
|
3508
|
+
};
|
|
3509
|
+
|
|
3510
|
+
// src/hexagon/domain/models/output/providers/claude/ClaudeSettingsComponent.ts
|
|
3511
|
+
var CLAUDE_HOOK_EVENTS = [
|
|
3512
|
+
"PreToolUse",
|
|
3513
|
+
"PostToolUse",
|
|
3514
|
+
"UserPromptSubmit",
|
|
3515
|
+
"Notification",
|
|
3516
|
+
"Stop",
|
|
3517
|
+
"SubagentStop",
|
|
3518
|
+
"PreCompact",
|
|
3519
|
+
"SessionStart",
|
|
3520
|
+
"SessionEnd"
|
|
3521
|
+
];
|
|
3522
|
+
function isClaudeHookEvent(name) {
|
|
3523
|
+
return CLAUDE_HOOK_EVENTS.includes(name);
|
|
3524
|
+
}
|
|
3525
|
+
var ClaudeSettingsComponent = class _ClaudeSettingsComponent {
|
|
3526
|
+
constructor(settings) {
|
|
3527
|
+
this.settings = settings;
|
|
3528
|
+
}
|
|
3529
|
+
settings;
|
|
3530
|
+
static kind = "settings";
|
|
3531
|
+
kind = _ClaudeSettingsComponent.kind;
|
|
3532
|
+
/** The settings file this host reads is shared with whatever the repository set
|
|
3533
|
+
* for itself, so what is put down there is written into what is there rather
|
|
3534
|
+
* than in place of it (FR-018). */
|
|
3535
|
+
projection = "mergeJSON";
|
|
3536
|
+
static of(settings) {
|
|
3537
|
+
return new _ClaudeSettingsComponent(settings);
|
|
3538
|
+
}
|
|
3539
|
+
};
|
|
3540
|
+
|
|
3149
3541
|
// src/hexagon/domain/models/output/providers/claude/document-based-components/ClaudeSkillComponent.ts
|
|
3150
3542
|
var ClaudeSkillComponent = class _ClaudeSkillComponent extends DocumentBasedComponent {
|
|
3151
3543
|
static kind = "skill";
|
|
@@ -3155,50 +3547,74 @@ var ClaudeSkillComponent = class _ClaudeSkillComponent extends DocumentBasedComp
|
|
|
3155
3547
|
}
|
|
3156
3548
|
};
|
|
3157
3549
|
|
|
3158
|
-
// src/hexagon/domain/
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
}
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
}
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3550
|
+
// src/hexagon/domain/models/output/providers/claude/ClaudeComponent.ts
|
|
3551
|
+
var CLAUDE_COMPONENT_CLASSES = [
|
|
3552
|
+
ClaudeAgentComponent,
|
|
3553
|
+
ClaudeSkillComponent,
|
|
3554
|
+
ClaudeSettingsComponent,
|
|
3555
|
+
ClaudeMcpConfigComponent,
|
|
3556
|
+
ClaudeRuleComponent
|
|
3557
|
+
];
|
|
3558
|
+
var CLAUDE_COMPONENT_KINDS = Object.freeze(CLAUDE_COMPONENT_CLASSES.map((one) => one.kind));
|
|
3559
|
+
function claudeDocumentComponentOf(sc, compiledFile, agentTools) {
|
|
3560
|
+
const name = sc.normIdentity;
|
|
3561
|
+
const compiledFromClaudeFolder = `../../${compiledFile}`;
|
|
3562
|
+
const pointerTo = (path) => `Read and follow @${path}.
|
|
3563
|
+
`;
|
|
3564
|
+
switch (sc.primitive.kind) {
|
|
3565
|
+
// A guide is a rule this host loads into context whole: when a file it names
|
|
3566
|
+
// is touched, which is what `paths` on a rule does, and at the start of every
|
|
3567
|
+
// session where it names none (FR-013). Under the identity it is changed by,
|
|
3568
|
+
// so a reader who wants it changed is sent to the primitive rather than
|
|
3569
|
+
// editing what the next build overwrites (FR-017, FR-020).
|
|
3570
|
+
case GuidePrimitive.kind: {
|
|
3571
|
+
const { globs = [] } = sc.primitive.headers;
|
|
3572
|
+
return ClaudeRuleComponent.of(name, globs.length === 0 ? {} : { paths: globs }, `@${compiledFromClaudeFolder}
|
|
3573
|
+
`);
|
|
3574
|
+
}
|
|
3575
|
+
case AgentPrimitive.kind: {
|
|
3576
|
+
const { tools } = sc.primitive.headers;
|
|
3577
|
+
return ClaudeAgentComponent.of(name, { description: sc.primitive.description(), tools: (agentTools ?? tools).join(", ") }, pointerTo(compiledFromClaudeFolder));
|
|
3578
|
+
}
|
|
3579
|
+
// One kind of this host for the two the charter loads when the request calls
|
|
3580
|
+
// for them: what decides that the body is worth opening is the description,
|
|
3581
|
+
// because that line is all this host reads before deciding (FR-013), and
|
|
3582
|
+
// composing it is the primitive's.
|
|
3583
|
+
case SkillPrimitive.kind:
|
|
3584
|
+
case PlaybookPrimitive.kind:
|
|
3585
|
+
return ClaudeSkillComponent.of(name, { description: sc.primitive.description() }, pointerTo(`../${compiledFromClaudeFolder}`));
|
|
3586
|
+
default:
|
|
3587
|
+
return void 0;
|
|
3189
3588
|
}
|
|
3190
3589
|
}
|
|
3191
|
-
|
|
3590
|
+
|
|
3591
|
+
// src/hexagon/domain/services/providers/claude.ts
|
|
3592
|
+
function compileForClaude(charter, compiledPrimitives, shortStrings) {
|
|
3593
|
+
const isSettingComponent = (one) => one.primitive.kind === PosturePrimitive.kind || one.primitive.kind === SensorPrimitive.kind;
|
|
3192
3594
|
const asSettings = charter.primitives.filter(isSettingComponent);
|
|
3193
3595
|
const asDocuments = charter.primitives.filter((one) => !isSettingComponent(one));
|
|
3596
|
+
const toolsByMcpIdentity = new Map(
|
|
3597
|
+
charter.primitives.flatMap(({ identity, primitive }) => primitive instanceof McpPrimitive ? [[identity, primitive.headers.tools]] : [])
|
|
3598
|
+
);
|
|
3599
|
+
const agentToolsOf = (agent) => agent.headers.tools.flatMap((tool) => {
|
|
3600
|
+
const [, mcpIdentity, mcpTool] = tool.match(MCP_TOOL_REFERENCE) ?? [];
|
|
3601
|
+
if (mcpIdentity === void 0) return [tool];
|
|
3602
|
+
return (mcpTool === void 0 ? toolsByMcpIdentity.get(mcpIdentity) ?? [] : [mcpTool]).map(
|
|
3603
|
+
(oneTool) => `mcp__${ClaudeMcpConfigComponent.cwMcpName}__${shortStrings[mcpIdentity]}__${oneTool}`
|
|
3604
|
+
);
|
|
3605
|
+
});
|
|
3194
3606
|
return [
|
|
3195
3607
|
...asSettings.length === 0 ? [] : [ClaudeSettingsComponent.of(claudeSettingsOf(asSettings))],
|
|
3196
|
-
|
|
3608
|
+
ClaudeMcpConfigComponent.of(),
|
|
3609
|
+
...asDocuments.flatMap(
|
|
3610
|
+
(one) => claudeDocumentComponentOf(
|
|
3611
|
+
one,
|
|
3612
|
+
compiledPrimitives.find((compiled) => compiled.identity === one.identity).file,
|
|
3613
|
+
one.primitive instanceof AgentPrimitive ? agentToolsOf(one.primitive) : void 0
|
|
3614
|
+
) ?? []
|
|
3615
|
+
)
|
|
3197
3616
|
];
|
|
3198
3617
|
}
|
|
3199
|
-
function isSettingComponent(one) {
|
|
3200
|
-
return one.primitive.kind === PosturePrimitive.kind || one.primitive.kind === SensorPrimitive.kind;
|
|
3201
|
-
}
|
|
3202
3618
|
function claudeSettingsOf(ones) {
|
|
3203
3619
|
const allow = /* @__PURE__ */ new Set();
|
|
3204
3620
|
const deny = /* @__PURE__ */ new Set();
|
|
@@ -3220,46 +3636,126 @@ function claudeSettingsOf(ones) {
|
|
|
3220
3636
|
...Object.keys(hooks).length === 0 ? {} : { hooks }
|
|
3221
3637
|
};
|
|
3222
3638
|
}
|
|
3223
|
-
function
|
|
3224
|
-
const
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
//
|
|
3229
|
-
//
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
//
|
|
3242
|
-
//
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
//
|
|
3251
|
-
//
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3639
|
+
function claudeComponentProjection(one) {
|
|
3640
|
+
const projectionPolicy = one.projection;
|
|
3641
|
+
switch (one.kind) {
|
|
3642
|
+
case ClaudeAgentComponent.kind:
|
|
3643
|
+
return { path: `${CLAUDE_DIRECTORY}/agents/${one.name}.md`, contents: one.toStampedDocument(), projectionPolicy };
|
|
3644
|
+
// A skill is a directory holding one file of that name, which is where this
|
|
3645
|
+
// host looks for it.
|
|
3646
|
+
case ClaudeSkillComponent.kind:
|
|
3647
|
+
return {
|
|
3648
|
+
path: `${CLAUDE_DIRECTORY}/skills/${one.name}/SKILL.md`,
|
|
3649
|
+
contents: one.toStampedDocument(),
|
|
3650
|
+
projectionPolicy
|
|
3651
|
+
};
|
|
3652
|
+
// A rule of its own file, under the directory this host loads every one of:
|
|
3653
|
+
// at the start of a session where it declares no `paths`, and when one of
|
|
3654
|
+
// them is touched where it does.
|
|
3655
|
+
case ClaudeRuleComponent.kind:
|
|
3656
|
+
return { path: `${CLAUDE_DIRECTORY}/rules/${one.name}.md`, contents: one.toStampedDocument(), projectionPolicy };
|
|
3657
|
+
// Every posture and every sensor of the charter lands in the one file this
|
|
3658
|
+
// host reads its settings from.
|
|
3659
|
+
case ClaudeSettingsComponent.kind:
|
|
3660
|
+
return {
|
|
3661
|
+
path: `${CLAUDE_DIRECTORY}/settings.json`,
|
|
3662
|
+
contents: `${JSON.stringify(one.settings, void 0, 2)}
|
|
3663
|
+
`,
|
|
3664
|
+
projectionPolicy
|
|
3665
|
+
};
|
|
3666
|
+
// The one server reaching every place, in the file this host starts a
|
|
3667
|
+
// project's servers from (FR-146).
|
|
3668
|
+
case ClaudeMcpConfigComponent.kind:
|
|
3669
|
+
return {
|
|
3670
|
+
path: CLAUDE_MCP_CONFIG_FILE,
|
|
3671
|
+
contents: `${JSON.stringify({ mcpServers: one.mcpServers }, void 0, 2)}
|
|
3672
|
+
`,
|
|
3673
|
+
projectionPolicy
|
|
3674
|
+
};
|
|
3259
3675
|
}
|
|
3260
3676
|
}
|
|
3261
|
-
|
|
3262
|
-
|
|
3677
|
+
|
|
3678
|
+
// src/hexagon/domain/services/compileService.ts
|
|
3679
|
+
function compile(charter, agents) {
|
|
3680
|
+
const shortStrings = shortenStringsOf(
|
|
3681
|
+
charter.primitives.filter(({ primitive }) => primitive.kind === McpPrimitive.kind).map((one) => one.identity)
|
|
3682
|
+
);
|
|
3683
|
+
const compiledPrimitives = charter.primitives.map(
|
|
3684
|
+
(one) => CompiledPrimitive.of(
|
|
3685
|
+
one,
|
|
3686
|
+
one.primitive.toMarkdown({
|
|
3687
|
+
// A mixin nothing answers to is left out: the charter already names
|
|
3688
|
+
// that file as an error, and a build refused for it never reaches
|
|
3689
|
+
// here (FR-009).
|
|
3690
|
+
mixins: charter.mixinsOf(one).flatMap(({ primitive }) => primitive.kind === MixinPrimitive.kind ? [primitive] : []),
|
|
3691
|
+
// Each place the body names as `mcp:<id>` written as the name its
|
|
3692
|
+
// tools are served under, `<name>__<tool>`, which every host's name for
|
|
3693
|
+
// them holds whatever it puts before it (FR-147). A name no mcp
|
|
3694
|
+
// answers to has none and is left as written; the charter has warned
|
|
3695
|
+
// about it (FR-143).
|
|
3696
|
+
idReplacer: (body) => body.replace(MCP_MENTION, (identity) => shortStrings[identity] ?? identity)
|
|
3697
|
+
})
|
|
3698
|
+
)
|
|
3699
|
+
);
|
|
3700
|
+
return {
|
|
3701
|
+
// What an agent opens from the catalogue is the primitive as it compiled,
|
|
3702
|
+
// not the file its author wrote: the whole body in one place, under the
|
|
3703
|
+
// output folder whichever layer brought it (FR-140). The catalogue is where
|
|
3704
|
+
// this path is said; the projection reads it from there.
|
|
3705
|
+
catalogue: catalogueOf(charter, (one) => compiledPrimitives.find((compiled) => compiled.identity === one.identity).file),
|
|
3706
|
+
charterMd: CharterMd.of(PRIMITIVE_CLASSES),
|
|
3707
|
+
compiledPrimitives,
|
|
3708
|
+
mcpOrigins: mcpOriginsOf(charter, shortStrings),
|
|
3709
|
+
providerComponents: agents.flatMap((agent) => compileForAgent(agent, charter, compiledPrimitives, shortStrings))
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
function catalogueOf(charter, fileOf) {
|
|
3713
|
+
return Catalogue.of(
|
|
3714
|
+
charter.primitives.map((one) => {
|
|
3715
|
+
const { id, description, tags, globs, rationale, mixins } = one.primitive.headers;
|
|
3716
|
+
return {
|
|
3717
|
+
identity: one.identity,
|
|
3718
|
+
kind: one.primitive.kind,
|
|
3719
|
+
id,
|
|
3720
|
+
description,
|
|
3721
|
+
file: fileOf(one),
|
|
3722
|
+
...tags === void 0 ? {} : { tags },
|
|
3723
|
+
...globs === void 0 ? {} : { globs },
|
|
3724
|
+
...rationale === void 0 ? {} : { rationale },
|
|
3725
|
+
...mixins === void 0 ? {} : { mixins }
|
|
3726
|
+
};
|
|
3727
|
+
})
|
|
3728
|
+
);
|
|
3729
|
+
}
|
|
3730
|
+
function mcpOriginsOf(charter, shortStrings) {
|
|
3731
|
+
const mcpsByKey = /* @__PURE__ */ new Map();
|
|
3732
|
+
for (const { identity, primitive } of [...charter.primitives].sort((one, another) => one.identity < another.identity ? -1 : 1)) {
|
|
3733
|
+
if (primitive.kind !== McpPrimitive.kind) continue;
|
|
3734
|
+
const key = `${primitive.address}
|
|
3735
|
+
${primitive.headers.path ?? ""}`;
|
|
3736
|
+
mcpsByKey.set(key, [...mcpsByKey.get(key) ?? [], { identity, primitive }]);
|
|
3737
|
+
}
|
|
3738
|
+
return [...mcpsByKey.entries()].sort(([oneKey], [anotherKey]) => oneKey < anotherKey ? -1 : 1).map(([, mcpsAtOrigin]) => {
|
|
3739
|
+
const [firstMcp] = mcpsAtOrigin;
|
|
3740
|
+
const { endpoint, command, args = [], path } = firstMcp.primitive.headers;
|
|
3741
|
+
const tokenEnv = mcpsAtOrigin.find((one) => one.primitive.headers.tokenEnv !== void 0)?.primitive.headers.tokenEnv;
|
|
3742
|
+
const auths = new Set(mcpsAtOrigin.flatMap((one) => one.primitive.headers.auth ?? []));
|
|
3743
|
+
return {
|
|
3744
|
+
identities: mcpsAtOrigin.map((one) => one.identity),
|
|
3745
|
+
names: Object.fromEntries(mcpsAtOrigin.map((one) => [one.identity, shortStrings[one.identity]])),
|
|
3746
|
+
address: firstMcp.primitive.address,
|
|
3747
|
+
...endpoint === void 0 ? {} : { endpoint },
|
|
3748
|
+
...command === void 0 ? {} : { command: { command, args, ...tokenEnv === void 0 ? {} : { tokenEnv } } },
|
|
3749
|
+
...path === void 0 ? {} : { path },
|
|
3750
|
+
auth: MCP_AUTHS.filter((auth2) => auths.has(auth2))
|
|
3751
|
+
};
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
3754
|
+
function compileForAgent(agent, charter, compiledPrimitives, shortStrings) {
|
|
3755
|
+
switch (agent) {
|
|
3756
|
+
case "claude":
|
|
3757
|
+
return compileForClaude(charter, compiledPrimitives, shortStrings);
|
|
3758
|
+
}
|
|
3263
3759
|
}
|
|
3264
3760
|
|
|
3265
3761
|
// src/hexagon/domain/services/projectionService.ts
|
|
@@ -3282,13 +3778,27 @@ function charterOutputProjection(output, agentProviders) {
|
|
|
3282
3778
|
`,
|
|
3283
3779
|
projectionPolicy: output.catalogue.projection
|
|
3284
3780
|
},
|
|
3781
|
+
// Indented for the reason the full listing is: committed, and read in
|
|
3782
|
+
// diffs when a place moves (SC-038).
|
|
3783
|
+
{
|
|
3784
|
+
path: `${OUT_DIRECTORY}/mcp-origins.json`,
|
|
3785
|
+
contents: `${JSON.stringify({ origins: output.mcpOrigins }, void 0, 2)}
|
|
3786
|
+
`,
|
|
3787
|
+
projectionPolicy: "replace"
|
|
3788
|
+
},
|
|
3285
3789
|
{
|
|
3286
3790
|
path: `${OUT_DIRECTORY}/CHARTER.md`,
|
|
3287
3791
|
contents: output.charterMd.toStampedDocument(),
|
|
3288
3792
|
projectionPolicy: output.charterMd.projection
|
|
3289
3793
|
},
|
|
3794
|
+
// Each primitive lands where the catalogue sends an agent for it, so the
|
|
3795
|
+
// file named and the file written are one (FR-140).
|
|
3796
|
+
...output.catalogue.full.flatMap(({ identity, file }) => {
|
|
3797
|
+
const compiledPrimitive = output.compiledPrimitives.find((one) => one.identity === identity);
|
|
3798
|
+
return compiledPrimitive === void 0 ? [] : [{ path: file, contents: compiledPrimitive.toStampedDocument(), projectionPolicy: compiledPrimitive.projection }];
|
|
3799
|
+
}),
|
|
3290
3800
|
...agentProviders.map(entryFileProjection),
|
|
3291
|
-
...output.providerComponents.map(
|
|
3801
|
+
...output.providerComponents.map(claudeComponentProjection)
|
|
3292
3802
|
];
|
|
3293
3803
|
}
|
|
3294
3804
|
function entryFileProjection(provider) {
|
|
@@ -3308,37 +3818,6 @@ ${[
|
|
|
3308
3818
|
}
|
|
3309
3819
|
return { path, contents, projectionPolicy: "upsertWithMarker" };
|
|
3310
3820
|
}
|
|
3311
|
-
function componentProjection(one) {
|
|
3312
|
-
const projectionPolicy = one.projection;
|
|
3313
|
-
switch (one.kind) {
|
|
3314
|
-
case ClaudeCommandComponent.kind:
|
|
3315
|
-
return { path: `${CLAUDE_DIRECTORY}/commands/${one.name}.md`, contents: one.toStampedDocument(), projectionPolicy };
|
|
3316
|
-
case ClaudeAgentComponent.kind:
|
|
3317
|
-
return { path: `${CLAUDE_DIRECTORY}/agents/${one.name}.md`, contents: one.toStampedDocument(), projectionPolicy };
|
|
3318
|
-
// A skill is a directory holding one file of that name, which is where this
|
|
3319
|
-
// host looks for it.
|
|
3320
|
-
case ClaudeSkillComponent.kind:
|
|
3321
|
-
return {
|
|
3322
|
-
path: `${CLAUDE_DIRECTORY}/skills/${one.name}/SKILL.md`,
|
|
3323
|
-
contents: one.toStampedDocument(),
|
|
3324
|
-
projectionPolicy
|
|
3325
|
-
};
|
|
3326
|
-
// A rule of its own file, under the directory this host loads every one of:
|
|
3327
|
-
// at the start of a session where it declares no `paths`, and when one of
|
|
3328
|
-
// them is touched where it does.
|
|
3329
|
-
case ClaudeRuleComponent.kind:
|
|
3330
|
-
return { path: `${CLAUDE_DIRECTORY}/rules/${one.name}.md`, contents: one.toStampedDocument(), projectionPolicy };
|
|
3331
|
-
// Every posture and every sensor of the charter lands in the one file this
|
|
3332
|
-
// host reads its settings from.
|
|
3333
|
-
case ClaudeSettingsComponent.kind:
|
|
3334
|
-
return {
|
|
3335
|
-
path: `${CLAUDE_DIRECTORY}/settings.json`,
|
|
3336
|
-
contents: `${JSON.stringify(one.settings, void 0, 2)}
|
|
3337
|
-
`,
|
|
3338
|
-
projectionPolicy
|
|
3339
|
-
};
|
|
3340
|
-
}
|
|
3341
|
-
}
|
|
3342
3821
|
|
|
3343
3822
|
// src/hexagon/service/buildService.ts
|
|
3344
3823
|
async function plan(repo, charter, agentProviders, fileReaders) {
|
|
@@ -3360,10 +3839,19 @@ async function planForProjection(repo, output, agentProviders, fileReaders) {
|
|
|
3360
3839
|
break;
|
|
3361
3840
|
// What the repository set for itself is read and written back: the fields
|
|
3362
3841
|
// this charter speaks for are put over the top of it, and every other one
|
|
3363
|
-
// is theirs and comes through untouched (FR-018).
|
|
3842
|
+
// is theirs and comes through untouched (FR-018). Field by field at every
|
|
3843
|
+
// depth, so an entry of the repository's beside the charter's under one
|
|
3844
|
+
// key — a server of its own under `mcpServers` — stays (FR-146); a list
|
|
3845
|
+
// or a value is the charter's whole, so what it no longer asks for goes.
|
|
3364
3846
|
case "mergeJSON": {
|
|
3365
|
-
const
|
|
3366
|
-
|
|
3847
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3848
|
+
const mergedJSON = (already, written) => isPlainObject(already) && isPlainObject(written) ? Object.fromEntries(
|
|
3849
|
+
[.../* @__PURE__ */ new Set([...Object.keys(already), ...Object.keys(written)])].map((field) => [
|
|
3850
|
+
field,
|
|
3851
|
+
field in written ? mergedJSON(already[field], written[field]) : already[field]
|
|
3852
|
+
])
|
|
3853
|
+
) : written;
|
|
3854
|
+
contents = `${JSON.stringify(mergedJSON(JSON.parse(held?.trim() || "{}"), JSON.parse(one.contents)), void 0, 2)}
|
|
3367
3855
|
`;
|
|
3368
3856
|
break;
|
|
3369
3857
|
}
|
|
@@ -3426,7 +3914,7 @@ async function executePlan(cleanupPlan, projectionPlan, fileWriter2) {
|
|
|
3426
3914
|
await Promise.all(cleanupPlan.map((one) => fileWriter2.delete(one.file)));
|
|
3427
3915
|
for (const one of projectionPlan) await fileWriter2.write(one.file, one.contents ?? "");
|
|
3428
3916
|
} catch (raised) {
|
|
3429
|
-
throw new
|
|
3917
|
+
throw new DomainFault(
|
|
3430
3918
|
`This build could not be written: ${raised instanceof Error ? raised.message : String(raised)}`,
|
|
3431
3919
|
"Check that the files this build writes can be written, then build again."
|
|
3432
3920
|
);
|
|
@@ -3499,11 +3987,10 @@ var CharterAuthoring = class {
|
|
|
3499
3987
|
* What the charter holds, listed, narrowed to one kind where one is named
|
|
3500
3988
|
* (FR-011, FR-012).
|
|
3501
3989
|
*
|
|
3502
|
-
*
|
|
3503
|
-
*
|
|
3504
|
-
*
|
|
3505
|
-
*
|
|
3506
|
-
* none still lists what it holds.
|
|
3990
|
+
* Read off the charter, not off what it compiles to: a person is shown the
|
|
3991
|
+
* file each primitive was authored in, which is the one they would edit and
|
|
3992
|
+
* what says which layer it came from, where the catalogue a build writes sends
|
|
3993
|
+
* an agent to the compiled one (FR-140).
|
|
3507
3994
|
*
|
|
3508
3995
|
* Whether the word narrowing it is a kind at all is asked here, of the
|
|
3509
3996
|
* domain that holds the set. A caller passes on what was typed and knows
|
|
@@ -3514,10 +4001,10 @@ var CharterAuthoring = class {
|
|
|
3514
4001
|
*/
|
|
3515
4002
|
async list(kind) {
|
|
3516
4003
|
if (kind !== void 0 && !isKind(kind))
|
|
3517
|
-
throw new
|
|
4004
|
+
throw new DomainFault(`"${kind}" is not a kind the charter knows.`, `Ask for one the charter knows: ${KINDS.join(", ")}.`);
|
|
3518
4005
|
const [charter, , faultsByFiles] = await this.#read();
|
|
3519
4006
|
if (charter === void 0) return faultsByFileDTO(faultsByFiles.errors(), this.#repoPath);
|
|
3520
|
-
const
|
|
4007
|
+
const catalogue2 = catalogueOf(charter, (one) => one.file);
|
|
3521
4008
|
return catalogueDTO(kind === void 0 ? catalogue2 : catalogue2.filterByKind(kind));
|
|
3522
4009
|
}
|
|
3523
4010
|
/** Every primitive the charter read, or those mentioning a word, each as the
|
|
@@ -3552,7 +4039,7 @@ var CharterAuthoring = class {
|
|
|
3552
4039
|
if (charter === void 0) return faultsByFileDTO(faultsByFiles.errors(), this.#repoPath);
|
|
3553
4040
|
const declared = charter.primitiveById.get(primitiveIdentity);
|
|
3554
4041
|
if (declared === void 0)
|
|
3555
|
-
throw new
|
|
4042
|
+
throw new DomainFault(
|
|
3556
4043
|
`This charter holds no "${identity}".`,
|
|
3557
4044
|
'Run "cw list --min" to see every identity it does hold.'
|
|
3558
4045
|
);
|
|
@@ -3692,7 +4179,7 @@ var CharterAuthoring = class {
|
|
|
3692
4179
|
const charter = await loadCharterRoot(this.#repoPath, this.#fileReader, this.#yamlParser);
|
|
3693
4180
|
const claimingPrimitive = charter.primitiveById.get(identityOf({ kind, id }));
|
|
3694
4181
|
if (claimingPrimitive !== void 0)
|
|
3695
|
-
throw new
|
|
4182
|
+
throw new DomainFault(
|
|
3696
4183
|
`"${identityOf({ kind, id })}" is already there, declared by ${claimingPrimitive.file}, and one identity names one primitive in the whole charter.`,
|
|
3697
4184
|
`Open ${claimingPrimitive.file}, or run this again with an id this charter has not got.`
|
|
3698
4185
|
);
|
|
@@ -3729,12 +4216,12 @@ var CharterAuthoring = class {
|
|
|
3729
4216
|
async rewrite(identity, headers, body, revision) {
|
|
3730
4217
|
const scopedPrimitive2 = await this.#scopedPrimitiveOf(identity);
|
|
3731
4218
|
if (!RepoScopedPrimitive.isSatisfiedBy(scopedPrimitive2))
|
|
3732
|
-
throw new
|
|
4219
|
+
throw new DomainFault(
|
|
3733
4220
|
`${identity} was not authored in this repository, and ${scopedPrimitive2.file} is read-only here.`,
|
|
3734
4221
|
`To differ from it, author a primitive of your own under an identity of its own.`
|
|
3735
4222
|
);
|
|
3736
4223
|
if (await contentHashOf(scopedPrimitive2.primitive.toMarkdown()) !== revision)
|
|
3737
|
-
throw new
|
|
4224
|
+
throw new DomainFault(
|
|
3738
4225
|
`${scopedPrimitive2.file} changed on disk after it was opened, and saving would write over that change.`,
|
|
3739
4226
|
`Open it again to see what changed, then make your edit there.`
|
|
3740
4227
|
);
|
|
@@ -3762,7 +4249,7 @@ var CharterAuthoring = class {
|
|
|
3762
4249
|
async remove(identity) {
|
|
3763
4250
|
const scopedPrimitive2 = await this.#scopedPrimitiveOf(identity);
|
|
3764
4251
|
if (!RepoScopedPrimitive.isSatisfiedBy(scopedPrimitive2))
|
|
3765
|
-
throw new
|
|
4252
|
+
throw new DomainFault(
|
|
3766
4253
|
`${identity} was not authored in this repository, and ${scopedPrimitive2.file} is read-only here.`,
|
|
3767
4254
|
`To differ from it, author a primitive of your own under an identity of its own.`
|
|
3768
4255
|
);
|
|
@@ -3777,7 +4264,7 @@ var CharterAuthoring = class {
|
|
|
3777
4264
|
const charter = await loadCharterRoot(this.#repoPath, this.#fileReader, this.#yamlParser);
|
|
3778
4265
|
const scopedPrimitive2 = charter.primitiveById.get(primitiveIdentity);
|
|
3779
4266
|
if (scopedPrimitive2 === void 0)
|
|
3780
|
-
throw new
|
|
4267
|
+
throw new DomainFault(
|
|
3781
4268
|
`This charter holds no "${identity}".`,
|
|
3782
4269
|
'Run "cw list --min" to see every identity it does hold.'
|
|
3783
4270
|
);
|
|
@@ -3788,7 +4275,7 @@ var CharterAuthoring = class {
|
|
|
3788
4275
|
* every kind there is. */
|
|
3789
4276
|
#kindOf(kind) {
|
|
3790
4277
|
if (!isKind(kind))
|
|
3791
|
-
throw new
|
|
4278
|
+
throw new DomainFault(`"${kind}" is not a kind the charter knows.`, `Ask for one the charter knows: ${KINDS.join(", ")}.`);
|
|
3792
4279
|
return kind;
|
|
3793
4280
|
}
|
|
3794
4281
|
/** What this repository configured itself with, read the one way settings are
|
|
@@ -3804,12 +4291,12 @@ var CharterAuthoring = class {
|
|
|
3804
4291
|
*/
|
|
3805
4292
|
async ensureRepoReady() {
|
|
3806
4293
|
if (!await this.#vcs.isInstalled(this.#repoPath))
|
|
3807
|
-
throw new
|
|
4294
|
+
throw new DomainFault(
|
|
3808
4295
|
"This folder is not inside a git repository, and a charter is authored inside one.",
|
|
3809
4296
|
'Run "git init" here, then "cw init", or run this again where your repository is.'
|
|
3810
4297
|
);
|
|
3811
4298
|
if (await this.#fileReader.readIfThere(settingsFileIn(this.#repoPath)) === void 0)
|
|
3812
|
-
throw new
|
|
4299
|
+
throw new DomainFault("This repository was never set up under a charter.", 'Run "cw init" first.');
|
|
3813
4300
|
}
|
|
3814
4301
|
/**
|
|
3815
4302
|
* One repository, set up under a charter (FR-032, FR-037).
|
|
@@ -3823,10 +4310,10 @@ var CharterAuthoring = class {
|
|
|
3823
4310
|
* agent it runs reads (FR-033).
|
|
3824
4311
|
*
|
|
3825
4312
|
* Then the two things setup owns — what the repository configured itself
|
|
3826
|
-
* with, and a directory per kind for a charter to be authored in — and
|
|
3827
|
-
*
|
|
3828
|
-
*
|
|
3829
|
-
* raised.
|
|
4313
|
+
* with, and a directory per kind for a charter to be authored in — and a
|
|
4314
|
+
* build over them, so the skill the engine brings is in the agent's hands
|
|
4315
|
+
* before anything is authored (FR-057, FR-096). What comes back is what that
|
|
4316
|
+
* build did, or what refused it; what stops setup itself is raised.
|
|
3830
4317
|
*
|
|
3831
4318
|
* Nothing is written into an ignore file: everything under the workspace is
|
|
3832
4319
|
* committed, what a vendor installed included, so a checkout of this
|
|
@@ -3839,12 +4326,12 @@ var CharterAuthoring = class {
|
|
|
3839
4326
|
*/
|
|
3840
4327
|
async init({ agents }) {
|
|
3841
4328
|
if (!await this.#vcs.isInstalled(this.#repoPath))
|
|
3842
|
-
throw new
|
|
4329
|
+
throw new DomainFault(
|
|
3843
4330
|
"This folder is not inside a git repository, and a charter is authored inside one.",
|
|
3844
4331
|
'Run "git init" here, or run this again where your repository is.'
|
|
3845
4332
|
);
|
|
3846
4333
|
if (agents.length === 0)
|
|
3847
|
-
throw new
|
|
4334
|
+
throw new DomainFault(
|
|
3848
4335
|
"Setup was given no agent to compile for, and a charter compiles for at least one.",
|
|
3849
4336
|
`Choose at least one of: ${AGENT_PROVIDERS.join(", ")}.`
|
|
3850
4337
|
);
|
|
@@ -3852,7 +4339,7 @@ var CharterAuthoring = class {
|
|
|
3852
4339
|
`);
|
|
3853
4340
|
const kindFolders = KINDS.map((kind) => `${CHARTER_DIRECTORY}/${kind}/.gitkeep`);
|
|
3854
4341
|
for (const path of kindFolders) await this.#fileWriter.write(new URL(path, this.#repoPath), "");
|
|
3855
|
-
return
|
|
4342
|
+
return this.build();
|
|
3856
4343
|
}
|
|
3857
4344
|
};
|
|
3858
4345
|
|
|
@@ -3903,6 +4390,275 @@ var TestAuthoring = class {
|
|
|
3903
4390
|
}
|
|
3904
4391
|
};
|
|
3905
4392
|
|
|
4393
|
+
// src/hexagon/service/credentialRepo.ts
|
|
4394
|
+
async function readCredential(secrets2, address) {
|
|
4395
|
+
const secret = await secrets2.readSecret(address);
|
|
4396
|
+
if (secret === void 0) return void 0;
|
|
4397
|
+
try {
|
|
4398
|
+
const credential = JSON.parse(secret);
|
|
4399
|
+
return typeof credential?.accessToken === "string" ? credential : void 0;
|
|
4400
|
+
} catch {
|
|
4401
|
+
return void 0;
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
async function writeCredential(secrets2, credential) {
|
|
4405
|
+
await secrets2.writeSecret(credential.address, JSON.stringify(credential));
|
|
4406
|
+
}
|
|
4407
|
+
async function credentialFor(secrets2, authorizing2, origin, { refused = false } = {}) {
|
|
4408
|
+
if (origin.endpoint !== void 0 && !isSecureEndpoint(origin.endpoint))
|
|
4409
|
+
throw new DomainFault(`${origin.address} is plain http to another machine, so no credential is sent there.`, 'Declare its endpoint as https://, then run "cw build".');
|
|
4410
|
+
const signInAgain = `Run "cw mcp auth ${origin.identities[0] ?? ""}" at a terminal to sign in there.`;
|
|
4411
|
+
const credential = await readCredential(secrets2, origin.address);
|
|
4412
|
+
if (credential === void 0) throw new DomainFault(`You are not signed in to ${origin.address}.`, signInAgain);
|
|
4413
|
+
const expired = refused || credential.expiresAt !== void 0 && Date.parse(credential.expiresAt) <= Date.now();
|
|
4414
|
+
if (!expired) return credential.accessToken;
|
|
4415
|
+
const { address, method, client, ...tokens } = credential;
|
|
4416
|
+
const renewedGrant = origin.endpoint !== void 0 && client !== void 0 && tokens.refreshToken !== void 0 ? await authorizing2.renew(origin.endpoint, { ...tokens, client }) : void 0;
|
|
4417
|
+
if (renewedGrant === void 0)
|
|
4418
|
+
throw new DomainFault(`Your sign-in to ${address} has expired and could not be renewed.`, signInAgain);
|
|
4419
|
+
await writeCredential(secrets2, { address, method, ...renewedGrant });
|
|
4420
|
+
return renewedGrant.accessToken;
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
// src/hexagon/service/mcpOriginsRepo.ts
|
|
4424
|
+
async function loadMcpOrigins(repo, fileReader2) {
|
|
4425
|
+
const file = `${OUT_DIRECTORY}/mcp-origins.json`;
|
|
4426
|
+
const contents = await fileReader2.readIfThere(new URL("mcp-origins.json", outFolderIn(repo)));
|
|
4427
|
+
if (contents === void 0)
|
|
4428
|
+
throw new DomainFault(`There is no ${file}, so no place has been listed yet.`, 'Run "cw build" first.');
|
|
4429
|
+
try {
|
|
4430
|
+
const mcpOriginsJson = JSON.parse(contents);
|
|
4431
|
+
const origins = mcpOriginsJson.origins;
|
|
4432
|
+
if (Array.isArray(origins)) return origins;
|
|
4433
|
+
} catch {
|
|
4434
|
+
}
|
|
4435
|
+
throw new DomainFault(`${file} does not hold a list of places.`, 'Run "cw build" to write it again.');
|
|
4436
|
+
}
|
|
4437
|
+
async function loadMcpTools(repo, fileReader2, yamlParser2) {
|
|
4438
|
+
const contents = await fileReader2.readIfThere(new URL("catalog.json", outFolderIn(repo)));
|
|
4439
|
+
let catalogEntries = [];
|
|
4440
|
+
try {
|
|
4441
|
+
const catalogJson = JSON.parse(contents ?? "[]");
|
|
4442
|
+
if (Array.isArray(catalogJson)) catalogEntries = catalogJson;
|
|
4443
|
+
} catch {
|
|
4444
|
+
}
|
|
4445
|
+
const toolsByIdentity = /* @__PURE__ */ new Map();
|
|
4446
|
+
for (const { identity, kind, file } of catalogEntries) {
|
|
4447
|
+
if (kind !== McpPrimitive.kind || typeof identity !== "string" || typeof file !== "string") continue;
|
|
4448
|
+
const compiledDocument = await fileReader2.readIfThere(new URL(file, repo));
|
|
4449
|
+
if (compiledDocument === void 0) continue;
|
|
4450
|
+
try {
|
|
4451
|
+
const primitive = primitiveOf(compiledDocument, yamlParser2);
|
|
4452
|
+
if (primitive instanceof McpPrimitive) toolsByIdentity.set(identity, primitive.headers.tools);
|
|
4453
|
+
} catch {
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
return toolsByIdentity;
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
// src/hexagon/port/zdriven/DrivenFault.ts
|
|
4460
|
+
var DrivenFault = class extends DomainFault {
|
|
4461
|
+
};
|
|
4462
|
+
|
|
4463
|
+
// src/hexagon/port/zdriven/ForCallingMcpServers.ts
|
|
4464
|
+
var UnauthorizedMcpFault = class extends DrivenFault {
|
|
4465
|
+
};
|
|
4466
|
+
|
|
4467
|
+
// src/hexagon/application/McpConnecting.ts
|
|
4468
|
+
var CONNECT_TIMEOUT_MS = 1e4;
|
|
4469
|
+
var errorAnswerOf = (text) => ({
|
|
4470
|
+
type: "ToolAnswer",
|
|
4471
|
+
data: { content: [{ type: "text", text }], isError: true }
|
|
4472
|
+
});
|
|
4473
|
+
var McpConnecting = class {
|
|
4474
|
+
#repoPath;
|
|
4475
|
+
#fileReader;
|
|
4476
|
+
#secrets;
|
|
4477
|
+
#authorizing;
|
|
4478
|
+
#yamlParser;
|
|
4479
|
+
#mcpServers;
|
|
4480
|
+
#mcpServing;
|
|
4481
|
+
constructor(repoPath2, fileReader2, secrets2, authorizing2, yamlParser2, mcpServers2) {
|
|
4482
|
+
this.#repoPath = repoPath2;
|
|
4483
|
+
this.#fileReader = fileReader2;
|
|
4484
|
+
this.#secrets = secrets2;
|
|
4485
|
+
this.#authorizing = authorizing2;
|
|
4486
|
+
this.#yamlParser = yamlParser2;
|
|
4487
|
+
this.#mcpServers = mcpServers2;
|
|
4488
|
+
}
|
|
4489
|
+
async signInStatus() {
|
|
4490
|
+
return Promise.all(
|
|
4491
|
+
(await this.#signInAddresses()).map(async ({ address, identities, auth: auth2 }) => {
|
|
4492
|
+
const credential = await readCredential(this.#secrets, address);
|
|
4493
|
+
return {
|
|
4494
|
+
type: "SignInStatus",
|
|
4495
|
+
data: { address, identities, auth: auth2, signedIn: credential !== void 0, ...credential && { method: credential.method } }
|
|
4496
|
+
};
|
|
4497
|
+
})
|
|
4498
|
+
);
|
|
4499
|
+
}
|
|
4500
|
+
async signInWithToken(address, token) {
|
|
4501
|
+
await this.#addressTaking(address, "token");
|
|
4502
|
+
if (token.trim() === "") throw new DomainFault(`No token was given for ${address}, so nothing was kept.`, 'Run "cw mcp auth" again and paste the token.');
|
|
4503
|
+
await writeCredential(this.#secrets, { address, method: "token", accessToken: token });
|
|
4504
|
+
}
|
|
4505
|
+
async signInWithOAuth(address, showAuthorizationUrl) {
|
|
4506
|
+
await this.#addressTaking(address, "oauth");
|
|
4507
|
+
const oauthGrant = await this.#authorizing.authorize(address, showAuthorizationUrl);
|
|
4508
|
+
await writeCredential(this.#secrets, { address, method: "oauth", ...oauthGrant });
|
|
4509
|
+
}
|
|
4510
|
+
async served() {
|
|
4511
|
+
return (await this.#serving()).servedTools;
|
|
4512
|
+
}
|
|
4513
|
+
async call(name, args) {
|
|
4514
|
+
const { toolRoutes, unreachableTools } = await this.#serving();
|
|
4515
|
+
const toolRoute = toolRoutes.get(name);
|
|
4516
|
+
if (toolRoute === void 0)
|
|
4517
|
+
return errorAnswerOf(unreachableTools.get(name) ?? `"${name}" is not a tool this server serves; call one it listed.`);
|
|
4518
|
+
const { origin, identity, upstream, connection } = toolRoute;
|
|
4519
|
+
const signInAgain = `${origin.address} refused your credential. Run "cw mcp auth ${identity}" at a terminal to sign in there again.`;
|
|
4520
|
+
const accessTokenOf = (refused) => origin.endpoint === void 0 ? Promise.resolve(void 0) : credentialFor(this.#secrets, this.#authorizing, origin, { refused });
|
|
4521
|
+
try {
|
|
4522
|
+
try {
|
|
4523
|
+
return { type: "ToolAnswer", data: await connection.callTool(upstream, args, await accessTokenOf(false)) };
|
|
4524
|
+
} catch (raised) {
|
|
4525
|
+
if (!(raised instanceof UnauthorizedMcpFault)) throw raised;
|
|
4526
|
+
const renewedToken = await accessTokenOf(true).catch(() => void 0);
|
|
4527
|
+
if (renewedToken === void 0) return errorAnswerOf(signInAgain);
|
|
4528
|
+
return { type: "ToolAnswer", data: await connection.callTool(upstream, args, renewedToken) };
|
|
4529
|
+
}
|
|
4530
|
+
} catch (raised) {
|
|
4531
|
+
if (raised instanceof UnauthorizedMcpFault) return errorAnswerOf(signInAgain);
|
|
4532
|
+
if (raised instanceof DomainFault) return errorAnswerOf(`${raised.message} ${raised.fix}`);
|
|
4533
|
+
throw raised;
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
async stopServing() {
|
|
4537
|
+
if (this.#mcpServing === void 0) return;
|
|
4538
|
+
const { connections } = await this.#mcpServing;
|
|
4539
|
+
await Promise.all(connections.map((connection) => connection.close()));
|
|
4540
|
+
}
|
|
4541
|
+
/**
|
|
4542
|
+
* Every place the last build listed, reached at once and once per run
|
|
4543
|
+
* (FR-152). What each lists is kept where one of its identities
|
|
4544
|
+
* declares it, renamed `<prefix>__<tool>`; a place down or not signed in to,
|
|
4545
|
+
* and a declared tool its place lacks, is said and left out, and the rest
|
|
4546
|
+
* are served (FR-153, FR-154, SC-039).
|
|
4547
|
+
*/
|
|
4548
|
+
#serving() {
|
|
4549
|
+
this.#mcpServing ??= (async () => {
|
|
4550
|
+
const origins = await loadMcpOrigins(this.#repoPath, this.#fileReader);
|
|
4551
|
+
const toolsByIdentity = await loadMcpTools(this.#repoPath, this.#fileReader, this.#yamlParser);
|
|
4552
|
+
const problems = [];
|
|
4553
|
+
const servedTools2 = [];
|
|
4554
|
+
const toolRoutes = /* @__PURE__ */ new Map();
|
|
4555
|
+
const unreachableTools = /* @__PURE__ */ new Map();
|
|
4556
|
+
const connections = [];
|
|
4557
|
+
const reachedPlaces = await Promise.all(
|
|
4558
|
+
origins.map(async (origin) => {
|
|
4559
|
+
try {
|
|
4560
|
+
const accessToken = origin.auth.length > 0 ? await credentialFor(this.#secrets, this.#authorizing, origin) : void 0;
|
|
4561
|
+
return { origin, connection: await this.#mcpServers.connect(origin, accessToken, CONNECT_TIMEOUT_MS) };
|
|
4562
|
+
} catch (raised) {
|
|
4563
|
+
if (!(raised instanceof DomainFault)) throw raised;
|
|
4564
|
+
return { origin, reason: `${raised.message} ${raised.fix}` };
|
|
4565
|
+
}
|
|
4566
|
+
})
|
|
4567
|
+
);
|
|
4568
|
+
for (const { origin, connection, reason } of reachedPlaces) {
|
|
4569
|
+
if (connection !== void 0) connections.push(connection);
|
|
4570
|
+
else problems.push(`${origin.identities.join(", ")}: ${reason}`);
|
|
4571
|
+
for (const identity of origin.identities) {
|
|
4572
|
+
const declaredTools = toolsByIdentity.get(identity);
|
|
4573
|
+
if (declaredTools === void 0) {
|
|
4574
|
+
problems.push(`${identity} has no compiled document under .cw/out/, so none of its tools is served. Run "cw build".`);
|
|
4575
|
+
continue;
|
|
4576
|
+
}
|
|
4577
|
+
const identityName = origin.names?.[identity];
|
|
4578
|
+
if (identityName === void 0) {
|
|
4579
|
+
problems.push(`${identity} has no served name in .cw/out/mcp-origins.json, so none of its tools is served. Run "cw build" again.`);
|
|
4580
|
+
continue;
|
|
4581
|
+
}
|
|
4582
|
+
for (const tool of declaredTools) {
|
|
4583
|
+
const servedName = `${identityName}__${tool}`;
|
|
4584
|
+
if (connection === void 0) {
|
|
4585
|
+
unreachableTools.set(servedName, reason ?? "");
|
|
4586
|
+
continue;
|
|
4587
|
+
}
|
|
4588
|
+
const upstreamTool = connection.tools.find(({ name }) => name === tool);
|
|
4589
|
+
if (upstreamTool === void 0) {
|
|
4590
|
+
problems.push(`${identity} declares "${tool}", which ${origin.address} does not have; it is not served.`);
|
|
4591
|
+
continue;
|
|
4592
|
+
}
|
|
4593
|
+
const placeName = origin.path === void 0 ? identity : `${identity} \u2014 ${origin.path}`;
|
|
4594
|
+
servedTools2.push({
|
|
4595
|
+
type: "ServedTool",
|
|
4596
|
+
data: {
|
|
4597
|
+
name: servedName,
|
|
4598
|
+
description: `[${placeName}] ${upstreamTool.description ?? ""}`.trimEnd(),
|
|
4599
|
+
inputSchema: upstreamTool.inputSchema
|
|
4600
|
+
}
|
|
4601
|
+
});
|
|
4602
|
+
toolRoutes.set(servedName, { origin, identity, upstream: tool, connection });
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
return {
|
|
4607
|
+
servedTools: {
|
|
4608
|
+
type: "ServedTools",
|
|
4609
|
+
data: { tools: servedTools2.sort((one, other) => one.data.name.localeCompare(other.data.name)), problems }
|
|
4610
|
+
},
|
|
4611
|
+
toolRoutes,
|
|
4612
|
+
unreachableTools,
|
|
4613
|
+
connections
|
|
4614
|
+
};
|
|
4615
|
+
})();
|
|
4616
|
+
return this.#mcpServing;
|
|
4617
|
+
}
|
|
4618
|
+
/**
|
|
4619
|
+
* Every address a developer signs in to, once, ordered by address (plan
|
|
4620
|
+
* §19.5): one address is one account at one service, whatever path each
|
|
4621
|
+
* place there names. A local command taking no token signs in to nothing and
|
|
4622
|
+
* is left out.
|
|
4623
|
+
*/
|
|
4624
|
+
async #signInAddresses() {
|
|
4625
|
+
const origins = await loadMcpOrigins(this.#repoPath, this.#fileReader);
|
|
4626
|
+
const addresses = /* @__PURE__ */ new Map();
|
|
4627
|
+
for (const { address, endpoint, identities, auth: auth2 } of origins.filter((one) => one.auth.length > 0)) {
|
|
4628
|
+
const signInAddress = addresses.get(address) ?? { ...endpoint !== void 0 && { endpoint }, identities: /* @__PURE__ */ new Set(), auth: /* @__PURE__ */ new Set() };
|
|
4629
|
+
identities.forEach((identity) => signInAddress.identities.add(identity));
|
|
4630
|
+
auth2.forEach((method) => signInAddress.auth.add(method));
|
|
4631
|
+
addresses.set(address, signInAddress);
|
|
4632
|
+
}
|
|
4633
|
+
return [...addresses].sort(([one], [other]) => one.localeCompare(other)).map(([address, { endpoint, identities, auth: auth2 }]) => ({
|
|
4634
|
+
address,
|
|
4635
|
+
...endpoint !== void 0 && { endpoint },
|
|
4636
|
+
identities: [...identities].sort(),
|
|
4637
|
+
auth: ["oauth", "token"].filter((method) => auth2.has(method))
|
|
4638
|
+
}));
|
|
4639
|
+
}
|
|
4640
|
+
/** Refused where no place is at this address, or none there allows signing
|
|
4641
|
+
* in this way: a credential nothing would use is not kept. Refused too where
|
|
4642
|
+
* the address is plain `http` to another machine, whatever the build said:
|
|
4643
|
+
* the list is a committed file, and a credential sent there travels in the
|
|
4644
|
+
* clear. */
|
|
4645
|
+
async #addressTaking(address, method) {
|
|
4646
|
+
const signInAddress = (await this.#signInAddresses()).find((one) => one.address === address);
|
|
4647
|
+
if (signInAddress === void 0)
|
|
4648
|
+
throw new DomainFault(`No place the last build listed is at ${address}.`, 'Run "cw mcp auth --status" to see every address, or "cw build" if the charter changed.');
|
|
4649
|
+
if (signInAddress.endpoint !== void 0 && !isSecureEndpoint(signInAddress.endpoint))
|
|
4650
|
+
throw new DomainFault(
|
|
4651
|
+
`${address} is plain http to another machine, so no credential is sent there.`,
|
|
4652
|
+
'Declare its endpoint as https://, then run "cw build".'
|
|
4653
|
+
);
|
|
4654
|
+
if (!signInAddress.auth.includes(method))
|
|
4655
|
+
throw new DomainFault(
|
|
4656
|
+
`${address} is not signed in to by ${method}; it allows ${signInAddress.auth.join(" or ")}.`,
|
|
4657
|
+
`Sign in there by ${signInAddress.auth.join(" or ")}.`
|
|
4658
|
+
);
|
|
4659
|
+
}
|
|
4660
|
+
};
|
|
4661
|
+
|
|
3906
4662
|
// src/zdriven/FileReaders.ts
|
|
3907
4663
|
import { readdir, readFile } from "fs/promises";
|
|
3908
4664
|
var FileReaders = class {
|
|
@@ -4053,7 +4809,7 @@ var Git = class {
|
|
|
4053
4809
|
{ cwd }
|
|
4054
4810
|
);
|
|
4055
4811
|
} catch (raised) {
|
|
4056
|
-
throw new
|
|
4812
|
+
throw new DrivenFault(
|
|
4057
4813
|
`${raised}`,
|
|
4058
4814
|
version === void 0 ? "Check the address, and that you can reach it from here." : `Check the address, and that "${version}" is a tag or branch it has.`
|
|
4059
4815
|
);
|
|
@@ -4068,19 +4824,286 @@ var Git = class {
|
|
|
4068
4824
|
await run("git", ["rm", "-r", "--quiet", "--", subFolder], { cwd });
|
|
4069
4825
|
await run("git", ["commit", "-qm", `Remove ${subFolder}`], { cwd });
|
|
4070
4826
|
} catch (raised) {
|
|
4071
|
-
throw new
|
|
4827
|
+
throw new DrivenFault(`${raised}`, `Check that "${subFolder}" is a folder this repository holds.`);
|
|
4072
4828
|
}
|
|
4073
4829
|
}
|
|
4074
4830
|
};
|
|
4075
4831
|
|
|
4832
|
+
// src/zdriven/OsSecrets.ts
|
|
4833
|
+
import { spawn as spawn2 } from "child_process";
|
|
4834
|
+
var SERVICE = "cherry-works";
|
|
4835
|
+
var MACOS_NOT_FOUND = 44;
|
|
4836
|
+
function commandOutput(command, args, stdin = "") {
|
|
4837
|
+
return new Promise((resolve) => {
|
|
4838
|
+
const child = spawn2(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
4839
|
+
let stdout = "";
|
|
4840
|
+
let stderr = "";
|
|
4841
|
+
child.stdout.on("data", (chunk) => stdout += chunk);
|
|
4842
|
+
child.stderr.on("data", (chunk) => stderr += chunk);
|
|
4843
|
+
child.on("error", (raised) => resolve({ code: -1, stdout, stderr: `${raised.message}` }));
|
|
4844
|
+
child.on("close", (code) => resolve({ code: code ?? -1, stdout, stderr }));
|
|
4845
|
+
child.stdin.on("error", () => {
|
|
4846
|
+
});
|
|
4847
|
+
child.stdin.end(stdin);
|
|
4848
|
+
});
|
|
4849
|
+
}
|
|
4850
|
+
var OsSecrets = class {
|
|
4851
|
+
/** Which system's store to use: this process's, unless a test names
|
|
4852
|
+
* another. */
|
|
4853
|
+
constructor(platform = process.platform) {
|
|
4854
|
+
this.platform = platform;
|
|
4855
|
+
}
|
|
4856
|
+
platform;
|
|
4857
|
+
async readSecret(key) {
|
|
4858
|
+
this.refuseUnsupportedPlatform();
|
|
4859
|
+
const commandOutputOfRead = this.platform === "darwin" ? await commandOutput("security", ["find-generic-password", "-s", SERVICE, "-a", key, "-w"]) : await commandOutput("secret-tool", ["lookup", "service", SERVICE, "account", key]);
|
|
4860
|
+
if (commandOutputOfRead.code === 0) return commandOutputOfRead.stdout.replace(/\n$/, "");
|
|
4861
|
+
const nothingThere = this.platform === "darwin" ? commandOutputOfRead.code === MACOS_NOT_FOUND : commandOutputOfRead.stderr.trim() === "";
|
|
4862
|
+
if (nothingThere) return void 0;
|
|
4863
|
+
this.throwFault("read", key, commandOutputOfRead.stderr);
|
|
4864
|
+
}
|
|
4865
|
+
async writeSecret(key, secret) {
|
|
4866
|
+
this.refuseUnsupportedPlatform();
|
|
4867
|
+
if (this.platform === "darwin" && /["\\\n]/.test(key)) {
|
|
4868
|
+
throw new DrivenFault(`The key "${key}" cannot be kept in the keychain.`, "Name the place by an address without quotes, backslashes or line breaks.");
|
|
4869
|
+
}
|
|
4870
|
+
const commandOutputOfWrite = this.platform === "darwin" ? await commandOutput(
|
|
4871
|
+
"security",
|
|
4872
|
+
["-i"],
|
|
4873
|
+
`add-generic-password -U -s ${SERVICE} -a "${key}" -X ${Buffer.from(secret).toString("hex")}
|
|
4874
|
+
`
|
|
4875
|
+
) : await commandOutput(
|
|
4876
|
+
"secret-tool",
|
|
4877
|
+
["store", `--label=${SERVICE} ${key}`, "service", SERVICE, "account", key],
|
|
4878
|
+
secret
|
|
4879
|
+
);
|
|
4880
|
+
if (commandOutputOfWrite.code !== 0) this.throwFault("write", key);
|
|
4881
|
+
}
|
|
4882
|
+
async removeSecret(key) {
|
|
4883
|
+
this.refuseUnsupportedPlatform();
|
|
4884
|
+
const commandOutputOfRemove = this.platform === "darwin" ? await commandOutput("security", ["delete-generic-password", "-s", SERVICE, "-a", key]) : await commandOutput("secret-tool", ["clear", "service", SERVICE, "account", key]);
|
|
4885
|
+
if (commandOutputOfRemove.code === 0) return;
|
|
4886
|
+
const nothingThere = this.platform === "darwin" ? commandOutputOfRemove.code === MACOS_NOT_FOUND : commandOutputOfRemove.stderr.trim() === "";
|
|
4887
|
+
if (!nothingThere) this.throwFault("remove", key, commandOutputOfRemove.stderr);
|
|
4888
|
+
}
|
|
4889
|
+
/** macOS and Linux have a store this knows; anything else is refused
|
|
4890
|
+
* before a command runs. */
|
|
4891
|
+
refuseUnsupportedPlatform() {
|
|
4892
|
+
if (this.platform !== "darwin" && this.platform !== "linux") {
|
|
4893
|
+
throw new DrivenFault(
|
|
4894
|
+
`Keeping a secret is not supported on "${this.platform}".`,
|
|
4895
|
+
"Sign in from macOS or Linux, whose credential stores cw uses."
|
|
4896
|
+
);
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
throwFault(operation, key, stderr = "") {
|
|
4900
|
+
const storeName = this.platform === "darwin" ? "The keychain" : "The secret service";
|
|
4901
|
+
throw new DrivenFault(
|
|
4902
|
+
`${storeName} refused to ${operation} the secret for "${key}".${stderr.trim() === "" ? "" : ` It said: ${stderr.trim()}`}`,
|
|
4903
|
+
this.platform === "darwin" ? "Check that the login keychain is unlocked." : "Check that secret-tool is installed and a secret service is running."
|
|
4904
|
+
);
|
|
4905
|
+
}
|
|
4906
|
+
};
|
|
4907
|
+
|
|
4908
|
+
// src/zdriven/OAuth.ts
|
|
4909
|
+
import { randomUUID } from "crypto";
|
|
4910
|
+
import { createServer } from "http";
|
|
4911
|
+
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
4912
|
+
import { OAuthError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
|
|
4913
|
+
var CALLBACK_TIMEOUT_MS = 5 * 6e4;
|
|
4914
|
+
var OAuth = class {
|
|
4915
|
+
#callbackTimeoutMs;
|
|
4916
|
+
constructor(callbackTimeoutMs = CALLBACK_TIMEOUT_MS) {
|
|
4917
|
+
this.#callbackTimeoutMs = callbackTimeoutMs;
|
|
4918
|
+
}
|
|
4919
|
+
async authorize(endpoint, showAuthorizationUrl) {
|
|
4920
|
+
const state = randomUUID();
|
|
4921
|
+
const callbackServer = createServer();
|
|
4922
|
+
await new Promise((resolve, reject) => callbackServer.once("error", reject).listen(0, "127.0.0.1", resolve));
|
|
4923
|
+
const redirectUrl = `http://127.0.0.1:${callbackServer.address().port}/callback`;
|
|
4924
|
+
let callbackTimeout;
|
|
4925
|
+
const authorizationCode = new Promise((resolve, reject) => {
|
|
4926
|
+
callbackTimeout = setTimeout(
|
|
4927
|
+
() => reject(new DrivenFault(`Nobody came back from signing in to ${endpoint} in time.`, 'Run "cw mcp auth" again, and open the address it prints.')),
|
|
4928
|
+
this.#callbackTimeoutMs
|
|
4929
|
+
);
|
|
4930
|
+
callbackServer.on("request", (request, response) => {
|
|
4931
|
+
const callbackUrl = new URL(request.url ?? "/", redirectUrl);
|
|
4932
|
+
if (callbackUrl.pathname !== "/callback") return void response.writeHead(404).end();
|
|
4933
|
+
const callbackCode = callbackUrl.searchParams.get("code");
|
|
4934
|
+
const signedIn = callbackCode !== null && callbackUrl.searchParams.get("state") === state;
|
|
4935
|
+
response.writeHead(200, { "content-type": "text/plain; charset=utf-8" }).end(signedIn ? "Signed in. Go back to the terminal.\n" : "Not signed in. Go back to the terminal.\n");
|
|
4936
|
+
if (signedIn) return resolve(callbackCode);
|
|
4937
|
+
const callbackError = callbackUrl.searchParams.get("error");
|
|
4938
|
+
reject(new DrivenFault(`Signing in to ${endpoint} came back without an authorization${callbackError ? `: ${callbackError}` : ""}.`, 'Run "cw mcp auth" again.'));
|
|
4939
|
+
});
|
|
4940
|
+
});
|
|
4941
|
+
authorizationCode.catch(() => void 0);
|
|
4942
|
+
try {
|
|
4943
|
+
const oauthSession = {};
|
|
4944
|
+
const provider = this.#providerOver(oauthSession, redirectUrl, (authorizationUrl) => showAuthorizationUrl(authorizationUrl.href), state);
|
|
4945
|
+
if (await auth(provider, { serverUrl: endpoint }) === "REDIRECT")
|
|
4946
|
+
await auth(provider, { serverUrl: endpoint, authorizationCode: await authorizationCode });
|
|
4947
|
+
return oauthGrantOf(endpoint, oauthSession);
|
|
4948
|
+
} catch (raised) {
|
|
4949
|
+
if (raised instanceof DrivenFault) throw raised;
|
|
4950
|
+
throw new DrivenFault(
|
|
4951
|
+
`Signing in to ${endpoint} failed: ${raised instanceof Error ? raised.message : String(raised)}`,
|
|
4952
|
+
'Check the address is reachable and offers an OAuth sign-in, then run "cw mcp auth" again.'
|
|
4953
|
+
);
|
|
4954
|
+
} finally {
|
|
4955
|
+
clearTimeout(callbackTimeout);
|
|
4956
|
+
callbackServer.closeAllConnections();
|
|
4957
|
+
callbackServer.close();
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
async renew(endpoint, oauthGrant) {
|
|
4961
|
+
const { clientId, clientSecret, issuer } = oauthGrant.client;
|
|
4962
|
+
const oauthSession = {
|
|
4963
|
+
clientInformation: { client_id: clientId, ...clientSecret !== void 0 && { client_secret: clientSecret }, issuer },
|
|
4964
|
+
tokens: {
|
|
4965
|
+
access_token: oauthGrant.accessToken,
|
|
4966
|
+
token_type: "Bearer",
|
|
4967
|
+
...oauthGrant.refreshToken !== void 0 && { refresh_token: oauthGrant.refreshToken },
|
|
4968
|
+
issuer
|
|
4969
|
+
}
|
|
4970
|
+
};
|
|
4971
|
+
const provider = this.#providerOver(oauthSession, "http://127.0.0.1/callback", () => void 0);
|
|
4972
|
+
try {
|
|
4973
|
+
if (await auth(provider, { serverUrl: endpoint }) === "REDIRECT") return void 0;
|
|
4974
|
+
} catch (raised) {
|
|
4975
|
+
if (raised instanceof OAuthError) return void 0;
|
|
4976
|
+
throw new DrivenFault(
|
|
4977
|
+
`Renewing the sign-in to ${endpoint} failed: ${raised instanceof Error ? raised.message : String(raised)}`,
|
|
4978
|
+
"Check the address is reachable, then try again."
|
|
4979
|
+
);
|
|
4980
|
+
}
|
|
4981
|
+
const renewedGrant = oauthGrantOf(endpoint, oauthSession);
|
|
4982
|
+
return renewedGrant.refreshToken !== void 0 || oauthGrant.refreshToken === void 0 ? renewedGrant : { ...renewedGrant, refreshToken: oauthGrant.refreshToken };
|
|
4983
|
+
}
|
|
4984
|
+
/**
|
|
4985
|
+
* What the SDK's flow is run with: `cw` described as a public client that
|
|
4986
|
+
* signs in at `redirectUrl`, and everything the flow saves kept in this
|
|
4987
|
+
* session. Registering is offered to a sign-in only, where a `state` is
|
|
4988
|
+
* checked on the way back; a renewal presents what the grant was issued to.
|
|
4989
|
+
*/
|
|
4990
|
+
#providerOver(oauthSession, redirectUrl, redirectToAuthorization, state) {
|
|
4991
|
+
return {
|
|
4992
|
+
redirectUrl,
|
|
4993
|
+
clientMetadata: {
|
|
4994
|
+
client_name: "cherry-works",
|
|
4995
|
+
redirect_uris: [redirectUrl],
|
|
4996
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
4997
|
+
response_types: ["code"],
|
|
4998
|
+
token_endpoint_auth_method: "none"
|
|
4999
|
+
},
|
|
5000
|
+
...state !== void 0 && {
|
|
5001
|
+
state: () => state,
|
|
5002
|
+
saveClientInformation: (clientInformation) => {
|
|
5003
|
+
oauthSession.clientInformation = clientInformation;
|
|
5004
|
+
}
|
|
5005
|
+
},
|
|
5006
|
+
clientInformation: () => oauthSession.clientInformation,
|
|
5007
|
+
tokens: () => oauthSession.tokens,
|
|
5008
|
+
saveTokens: (tokens) => {
|
|
5009
|
+
oauthSession.tokens = tokens;
|
|
5010
|
+
},
|
|
5011
|
+
redirectToAuthorization,
|
|
5012
|
+
saveCodeVerifier: (codeVerifier) => {
|
|
5013
|
+
oauthSession.codeVerifier = codeVerifier;
|
|
5014
|
+
},
|
|
5015
|
+
codeVerifier: () => oauthSession.codeVerifier ?? ""
|
|
5016
|
+
};
|
|
5017
|
+
}
|
|
5018
|
+
};
|
|
5019
|
+
function oauthGrantOf(endpoint, { clientInformation, tokens }) {
|
|
5020
|
+
if (clientInformation === void 0 || tokens === void 0)
|
|
5021
|
+
throw new DrivenFault(`Signing in to ${endpoint} ended without a token.`, 'Run "cw mcp auth" again.');
|
|
5022
|
+
return {
|
|
5023
|
+
accessToken: tokens.access_token,
|
|
5024
|
+
...tokens.refresh_token !== void 0 && { refreshToken: tokens.refresh_token },
|
|
5025
|
+
...tokens.expires_in !== void 0 && { expiresAt: new Date(Date.now() + tokens.expires_in * 1e3).toISOString() },
|
|
5026
|
+
client: {
|
|
5027
|
+
clientId: clientInformation.client_id,
|
|
5028
|
+
...clientInformation.client_secret !== void 0 && { clientSecret: clientInformation.client_secret },
|
|
5029
|
+
issuer: clientInformation.issuer ?? tokens.issuer ?? endpoint
|
|
5030
|
+
}
|
|
5031
|
+
};
|
|
5032
|
+
}
|
|
5033
|
+
|
|
5034
|
+
// src/zdriven/McpClients.ts
|
|
5035
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5036
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5037
|
+
import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
5038
|
+
var McpClients = class {
|
|
5039
|
+
async connect({ address, endpoint, command }, accessToken, timeoutMs) {
|
|
5040
|
+
let bearerToken = accessToken;
|
|
5041
|
+
const transport = endpoint !== void 0 ? new StreamableHTTPClientTransport(new URL(endpoint), {
|
|
5042
|
+
fetch: (url, init) => {
|
|
5043
|
+
const headers = new Headers(init?.headers);
|
|
5044
|
+
if (bearerToken !== void 0) headers.set("authorization", `Bearer ${bearerToken}`);
|
|
5045
|
+
return fetch(url, { ...init, headers });
|
|
5046
|
+
}
|
|
5047
|
+
}) : new StdioClientTransport({
|
|
5048
|
+
command: command?.command ?? "",
|
|
5049
|
+
args: [...command?.args ?? []],
|
|
5050
|
+
env: {
|
|
5051
|
+
...Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== void 0)),
|
|
5052
|
+
...command?.tokenEnv !== void 0 && accessToken !== void 0 && { [command.tokenEnv]: accessToken }
|
|
5053
|
+
},
|
|
5054
|
+
stderr: "inherit"
|
|
5055
|
+
});
|
|
5056
|
+
const client = new Client({ name: "cherry-works", version: "1" });
|
|
5057
|
+
let connectTimeout;
|
|
5058
|
+
try {
|
|
5059
|
+
const { tools } = await Promise.race([
|
|
5060
|
+
client.connect(transport).then(() => client.listTools()),
|
|
5061
|
+
new Promise((_resolve, reject) => {
|
|
5062
|
+
connectTimeout = setTimeout(
|
|
5063
|
+
() => reject(new DrivenFault(`${address} did not answer within ${timeoutMs / 1e3} seconds.`, "Check the place is up, then start the server again.")),
|
|
5064
|
+
timeoutMs
|
|
5065
|
+
);
|
|
5066
|
+
})
|
|
5067
|
+
]);
|
|
5068
|
+
return {
|
|
5069
|
+
tools: tools.map(({ name, description, inputSchema }) => ({ name, ...description !== void 0 && { description }, inputSchema })),
|
|
5070
|
+
callTool: async (name, args, callToken) => {
|
|
5071
|
+
if (endpoint !== void 0) bearerToken = callToken;
|
|
5072
|
+
try {
|
|
5073
|
+
return await client.callTool({ name, arguments: { ...args } });
|
|
5074
|
+
} catch (raised) {
|
|
5075
|
+
throw faultOf(raised, `Calling "${name}" at ${address} failed`, "Check the place is up, then try again.");
|
|
5076
|
+
}
|
|
5077
|
+
},
|
|
5078
|
+
close: () => client.close()
|
|
5079
|
+
};
|
|
5080
|
+
} catch (raised) {
|
|
5081
|
+
await client.close().catch(() => void 0);
|
|
5082
|
+
throw faultOf(raised, `${address} could not be reached`, "Check the place is up, or its command installed, then start the server again.");
|
|
5083
|
+
} finally {
|
|
5084
|
+
clearTimeout(connectTimeout);
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
};
|
|
5088
|
+
function faultOf(raised, failed, fix) {
|
|
5089
|
+
if (raised instanceof DrivenFault) return raised;
|
|
5090
|
+
if (raised instanceof StreamableHTTPError && raised.code === 401)
|
|
5091
|
+
return new UnauthorizedMcpFault(`${failed}: the place refused your credential.`, 'Run "cw mcp auth" at a terminal to sign in there again.');
|
|
5092
|
+
return new DrivenFault(`${failed}: ${raised instanceof Error ? raised.message : String(raised)}`, fix);
|
|
5093
|
+
}
|
|
5094
|
+
|
|
4076
5095
|
// main.ts
|
|
4077
5096
|
var fileReader = new FileReaders();
|
|
4078
5097
|
var yamlParser = new YamlParser();
|
|
4079
5098
|
var fileWriter = new FileOutput();
|
|
4080
5099
|
var vcs = new Git(fileReader);
|
|
5100
|
+
var secrets = new OsSecrets();
|
|
5101
|
+
var authorizing = new OAuth();
|
|
5102
|
+
var mcpServers = new McpClients();
|
|
4081
5103
|
var repoPath = pathToFileURL2(`${process.cwd()}/`);
|
|
4082
5104
|
var charterAuthoringApp = new CharterAuthoring(repoPath, fileReader, yamlParser, fileWriter, vcs);
|
|
4083
5105
|
var charterVendoringApp = new CharterVendoring(repoPath, fileReader, vcs);
|
|
4084
5106
|
var testAuthoringApp = new TestAuthoring(repoPath, fileReader, fileWriter);
|
|
4085
|
-
var
|
|
5107
|
+
var mcpConnectingApp = new McpConnecting(repoPath, fileReader, secrets, authorizing, yamlParser, mcpServers);
|
|
5108
|
+
var cli = new Commander({ cwd: process.cwd(), version: package_default.version, charterAuthoringApp, charterVendoringApp, testAuthoringApp, mcpConnectingApp });
|
|
4086
5109
|
process.exitCode = await cli.run(process.argv.slice(2));
|