@beignet/cli 0.0.43 → 0.0.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +61 -8
- package/dist/choices.d.ts +4 -0
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +9 -0
- package/dist/choices.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +8 -4
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -13
- package/dist/index.js.map +1 -1
- package/dist/inspect.js +78 -21
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +4 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +354 -3
- package/dist/mcp.js.map +1 -1
- package/dist/operational-lifecycle.d.ts +12 -0
- package/dist/operational-lifecycle.d.ts.map +1 -0
- package/dist/operational-lifecycle.js +39 -0
- package/dist/operational-lifecycle.js.map +1 -0
- package/dist/operational-path.d.ts +3 -0
- package/dist/operational-path.d.ts.map +1 -0
- package/dist/operational-path.js +26 -0
- package/dist/operational-path.js.map +1 -0
- package/dist/operational-process.d.ts +78 -0
- package/dist/operational-process.d.ts.map +1 -0
- package/dist/operational-process.js +228 -0
- package/dist/operational-process.js.map +1 -0
- package/dist/operational-runner.d.ts +2 -0
- package/dist/operational-runner.d.ts.map +1 -0
- package/dist/operational-runner.js +168 -0
- package/dist/operational-runner.js.map +1 -0
- package/dist/outbox.d.ts +14 -6
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +140 -124
- package/dist/outbox.js.map +1 -1
- package/dist/schedule.d.ts +2 -1
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +79 -81
- package/dist/schedule.js.map +1 -1
- package/dist/task.d.ts +2 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +57 -61
- package/dist/task.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +21 -8
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +2 -0
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +9 -2
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shadcn.d.ts.map +1 -1
- package/dist/templates/shadcn.js +3 -1
- package/dist/templates/shadcn.js.map +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +19 -6
- package/src/choices.ts +19 -0
- package/src/db.ts +8 -4
- package/src/index.ts +19 -16
- package/src/inspect.ts +114 -26
- package/src/lib.ts +27 -1
- package/src/mcp.ts +457 -2
- package/src/operational-lifecycle.ts +56 -0
- package/src/operational-path.ts +35 -0
- package/src/operational-process.ts +454 -0
- package/src/operational-runner.ts +221 -0
- package/src/outbox.ts +154 -128
- package/src/schedule.ts +84 -89
- package/src/task.ts +58 -62
- package/src/templates/agents.ts +21 -8
- package/src/templates/base.ts +2 -0
- package/src/templates/server.ts +9 -2
- package/src/templates/shadcn.ts +3 -1
package/src/mcp.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { open, realpath } from "node:fs/promises";
|
|
1
2
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
McpServer,
|
|
5
|
+
ResourceTemplate,
|
|
6
|
+
} from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
8
|
import { z } from "zod";
|
|
5
9
|
import { mapApp, projectAppMap } from "./app-map.js";
|
|
@@ -10,6 +14,8 @@ import {
|
|
|
10
14
|
databaseSchemaDialectChoices,
|
|
11
15
|
databaseSchemaTableChoices,
|
|
12
16
|
doctorFixOperationIds,
|
|
17
|
+
outboxMessageKindChoices,
|
|
18
|
+
outboxMessageStatusChoices,
|
|
13
19
|
type ProviderPresetName,
|
|
14
20
|
providerPresetChoices,
|
|
15
21
|
} from "./choices.js";
|
|
@@ -54,6 +60,12 @@ import {
|
|
|
54
60
|
makeUpload,
|
|
55
61
|
makeUseCase,
|
|
56
62
|
} from "./make.js";
|
|
63
|
+
import {
|
|
64
|
+
defaultOperationalCommandTimeoutMs,
|
|
65
|
+
defaultOperationalResultMaxBytes,
|
|
66
|
+
isAppRelativeOperationalModulePath,
|
|
67
|
+
runIsolatedOperationalCommand,
|
|
68
|
+
} from "./operational-process.js";
|
|
57
69
|
import {
|
|
58
70
|
type AddProviderPresetResult,
|
|
59
71
|
addProviderPreset,
|
|
@@ -92,6 +104,8 @@ const makeArtifactChoices = [
|
|
|
92
104
|
] as const;
|
|
93
105
|
|
|
94
106
|
const defaultMcpCheckTimeoutMs = 10 * 60 * 1_000;
|
|
107
|
+
const maxMcpOperationalTimeoutMs = 60 * 60 * 1_000;
|
|
108
|
+
const maxMcpGuidanceBytes = 64 * 1024;
|
|
95
109
|
|
|
96
110
|
type MakeArtifact = (typeof makeArtifactChoices)[number];
|
|
97
111
|
|
|
@@ -185,6 +199,123 @@ const providerAddInputSchema = {
|
|
|
185
199
|
|
|
186
200
|
const providerPresetDescription = providerPresetChoices.join(", ");
|
|
187
201
|
|
|
202
|
+
const operationalTimeoutSchema = z
|
|
203
|
+
.number()
|
|
204
|
+
.int()
|
|
205
|
+
.min(1_000)
|
|
206
|
+
.max(maxMcpOperationalTimeoutMs)
|
|
207
|
+
.optional()
|
|
208
|
+
.describe(
|
|
209
|
+
"Maximum operation duration in milliseconds. Defaults to 600000 (10 minutes).",
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const operationalModuleSchema = z
|
|
213
|
+
.string()
|
|
214
|
+
.min(1)
|
|
215
|
+
.refine(isAppRelativeOperationalModulePath, {
|
|
216
|
+
message: "Registry modules must stay inside the Beignet app directory.",
|
|
217
|
+
})
|
|
218
|
+
.optional()
|
|
219
|
+
.describe("Override the configured app-relative central registry module.");
|
|
220
|
+
|
|
221
|
+
const taskRunInputSchema = {
|
|
222
|
+
name: z.string().min(1).describe("Registered task name."),
|
|
223
|
+
input: z.unknown().optional().describe("Input validated by the task schema."),
|
|
224
|
+
tenant: z
|
|
225
|
+
.string()
|
|
226
|
+
.min(1)
|
|
227
|
+
.optional()
|
|
228
|
+
.describe(
|
|
229
|
+
"Tenant id or slug passed to createTaskContext, separate from task input.",
|
|
230
|
+
),
|
|
231
|
+
module: operationalModuleSchema,
|
|
232
|
+
timeoutMs: operationalTimeoutSchema,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const scheduleRunInputSchema = {
|
|
236
|
+
name: z.string().min(1).describe("Registered schedule name."),
|
|
237
|
+
payload: z
|
|
238
|
+
.unknown()
|
|
239
|
+
.optional()
|
|
240
|
+
.describe("Payload validated by the schedule schema."),
|
|
241
|
+
runId: z.string().min(1).optional().describe("Provider or app run ID."),
|
|
242
|
+
attempt: z
|
|
243
|
+
.number()
|
|
244
|
+
.int()
|
|
245
|
+
.positive()
|
|
246
|
+
.optional()
|
|
247
|
+
.describe("One-based provider attempt number."),
|
|
248
|
+
scheduledAt: z
|
|
249
|
+
.string()
|
|
250
|
+
.min(1)
|
|
251
|
+
.optional()
|
|
252
|
+
.describe("Provider scheduled timestamp."),
|
|
253
|
+
triggeredAt: z
|
|
254
|
+
.string()
|
|
255
|
+
.min(1)
|
|
256
|
+
.optional()
|
|
257
|
+
.describe("Schedule trigger timestamp."),
|
|
258
|
+
source: z
|
|
259
|
+
.string()
|
|
260
|
+
.min(1)
|
|
261
|
+
.optional()
|
|
262
|
+
.describe("Provider or app source label."),
|
|
263
|
+
module: operationalModuleSchema,
|
|
264
|
+
timeoutMs: operationalTimeoutSchema,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const outboxInspectInputSchema = z.discriminatedUnion("operation", [
|
|
268
|
+
z.object({
|
|
269
|
+
operation: z.literal("list"),
|
|
270
|
+
status: z.enum(outboxMessageStatusChoices).optional(),
|
|
271
|
+
kind: z.enum(outboxMessageKindChoices).optional(),
|
|
272
|
+
name: z.string().min(1).optional(),
|
|
273
|
+
limit: z.number().int().positive().optional(),
|
|
274
|
+
module: operationalModuleSchema,
|
|
275
|
+
timeoutMs: operationalTimeoutSchema,
|
|
276
|
+
}),
|
|
277
|
+
z.object({
|
|
278
|
+
operation: z.literal("show"),
|
|
279
|
+
id: z.string().min(1),
|
|
280
|
+
module: operationalModuleSchema,
|
|
281
|
+
timeoutMs: operationalTimeoutSchema,
|
|
282
|
+
}),
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
const outboxRunInputSchema = z.discriminatedUnion("operation", [
|
|
286
|
+
z.object({
|
|
287
|
+
operation: z.literal("drain"),
|
|
288
|
+
batchSize: z.number().int().positive().optional(),
|
|
289
|
+
module: operationalModuleSchema,
|
|
290
|
+
timeoutMs: operationalTimeoutSchema,
|
|
291
|
+
}),
|
|
292
|
+
z.object({
|
|
293
|
+
operation: z.literal("requeue"),
|
|
294
|
+
id: z.string().min(1),
|
|
295
|
+
availableAt: z.string().min(1).optional(),
|
|
296
|
+
resetAttempts: z.boolean().optional(),
|
|
297
|
+
module: operationalModuleSchema,
|
|
298
|
+
timeoutMs: operationalTimeoutSchema,
|
|
299
|
+
}),
|
|
300
|
+
z.object({
|
|
301
|
+
operation: z.literal("purge"),
|
|
302
|
+
before: z.string().min(1).optional(),
|
|
303
|
+
all: z.boolean().optional(),
|
|
304
|
+
limit: z.number().int().positive().optional(),
|
|
305
|
+
dryRun: z.boolean().optional(),
|
|
306
|
+
module: operationalModuleSchema,
|
|
307
|
+
timeoutMs: operationalTimeoutSchema,
|
|
308
|
+
}),
|
|
309
|
+
z.object({
|
|
310
|
+
operation: z.literal("prune"),
|
|
311
|
+
before: z.string().min(1),
|
|
312
|
+
limit: z.number().int().positive().optional(),
|
|
313
|
+
dryRun: z.boolean().optional(),
|
|
314
|
+
module: operationalModuleSchema,
|
|
315
|
+
timeoutMs: operationalTimeoutSchema,
|
|
316
|
+
}),
|
|
317
|
+
]);
|
|
318
|
+
|
|
188
319
|
type MakeToolInput = {
|
|
189
320
|
artifact: MakeArtifact;
|
|
190
321
|
name?: string;
|
|
@@ -219,6 +350,17 @@ function jsonResult(value: unknown): McpToolResult {
|
|
|
219
350
|
};
|
|
220
351
|
}
|
|
221
352
|
|
|
353
|
+
function operationalJsonResult(value: unknown): McpToolResult {
|
|
354
|
+
const formatted = JSON.stringify(value, null, 2);
|
|
355
|
+
const text =
|
|
356
|
+
Buffer.byteLength(formatted) <= defaultOperationalResultMaxBytes
|
|
357
|
+
? formatted
|
|
358
|
+
: JSON.stringify(value);
|
|
359
|
+
return {
|
|
360
|
+
content: [{ type: "text", text }],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
222
364
|
function errorResult(error: unknown): McpToolResult {
|
|
223
365
|
return {
|
|
224
366
|
isError: true,
|
|
@@ -350,7 +492,8 @@ async function runProviderAddTool(
|
|
|
350
492
|
}
|
|
351
493
|
|
|
352
494
|
/**
|
|
353
|
-
* Build the Beignet MCP server with app
|
|
495
|
+
* Build the Beignet MCP server with app context, validation, generation, and
|
|
496
|
+
* operational tools.
|
|
354
497
|
* Every tool resolves against the app directory captured at launch.
|
|
355
498
|
*/
|
|
356
499
|
export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
|
|
@@ -360,6 +503,80 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
|
|
|
360
503
|
version: options.version,
|
|
361
504
|
});
|
|
362
505
|
|
|
506
|
+
server.registerResource(
|
|
507
|
+
"app-guidance",
|
|
508
|
+
"beignet://app/guidance",
|
|
509
|
+
{
|
|
510
|
+
title: "Beignet app guidance",
|
|
511
|
+
description:
|
|
512
|
+
"The app-local AGENTS.md instructions for working in this Beignet app.",
|
|
513
|
+
mimeType: "text/markdown",
|
|
514
|
+
},
|
|
515
|
+
async (uri) => ({
|
|
516
|
+
contents: [
|
|
517
|
+
{
|
|
518
|
+
uri: uri.href,
|
|
519
|
+
mimeType: "text/markdown",
|
|
520
|
+
text: await readAppGuidance(cwd),
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
}),
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
const featureResourceTemplate = new ResourceTemplate(
|
|
527
|
+
"beignet://app/features/{feature}",
|
|
528
|
+
{
|
|
529
|
+
list: async () => {
|
|
530
|
+
const result = await mapApp({ cwd, strict: true });
|
|
531
|
+
return {
|
|
532
|
+
resources: appMapFeatureNames(result).map((feature) => ({
|
|
533
|
+
name: `${feature} app map`,
|
|
534
|
+
title: `Beignet feature: ${feature}`,
|
|
535
|
+
uri: `beignet://app/features/${encodeURIComponent(feature)}`,
|
|
536
|
+
description:
|
|
537
|
+
"Source-backed feature projection with direct relationships and diagnostics.",
|
|
538
|
+
mimeType: "application/json",
|
|
539
|
+
})),
|
|
540
|
+
};
|
|
541
|
+
},
|
|
542
|
+
complete: {
|
|
543
|
+
feature: async (value) => {
|
|
544
|
+
const result = await mapApp({ cwd, strict: true });
|
|
545
|
+
return appMapFeatureNames(result).filter((feature) =>
|
|
546
|
+
feature.startsWith(value),
|
|
547
|
+
);
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
server.registerResource(
|
|
554
|
+
"feature-app-map",
|
|
555
|
+
featureResourceTemplate,
|
|
556
|
+
{
|
|
557
|
+
title: "Beignet feature app map",
|
|
558
|
+
description:
|
|
559
|
+
"A focused source-backed app-map projection for one feature.",
|
|
560
|
+
mimeType: "application/json",
|
|
561
|
+
},
|
|
562
|
+
async (uri, variables) => {
|
|
563
|
+
const feature = resourceVariable(variables.feature, "feature");
|
|
564
|
+
const result = await mapApp({ cwd, strict: true });
|
|
565
|
+
if (!appMapFeatureNames(result).includes(feature)) {
|
|
566
|
+
throw new Error(`Unknown Beignet feature "${feature}".`);
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
contents: [
|
|
570
|
+
{
|
|
571
|
+
uri: uri.href,
|
|
572
|
+
mimeType: "application/json",
|
|
573
|
+
text: JSON.stringify(projectAppMap(result, { feature }), null, 2),
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
};
|
|
577
|
+
},
|
|
578
|
+
);
|
|
579
|
+
|
|
363
580
|
server.registerTool(
|
|
364
581
|
"app_map",
|
|
365
582
|
{
|
|
@@ -561,6 +778,171 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
|
|
|
561
778
|
),
|
|
562
779
|
);
|
|
563
780
|
|
|
781
|
+
server.registerTool(
|
|
782
|
+
"task_run",
|
|
783
|
+
{
|
|
784
|
+
description:
|
|
785
|
+
"Run one registered app-owned operational task in an isolated process and return exactly the versioned beignet task run <name> --json report. The task input is schema-validated, output is bounded, and cancellation or timeout stops the complete process tree.",
|
|
786
|
+
inputSchema: taskRunInputSchema,
|
|
787
|
+
annotations: {
|
|
788
|
+
readOnlyHint: false,
|
|
789
|
+
destructiveHint: true,
|
|
790
|
+
idempotentHint: false,
|
|
791
|
+
openWorldHint: true,
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
async (input, extra) =>
|
|
795
|
+
safeToolResult(async () =>
|
|
796
|
+
operationalJsonResult(
|
|
797
|
+
await runIsolatedOperationalCommand({
|
|
798
|
+
cwd,
|
|
799
|
+
request: {
|
|
800
|
+
command: "task:run",
|
|
801
|
+
name: input.name,
|
|
802
|
+
input: input.input,
|
|
803
|
+
tenant: input.tenant,
|
|
804
|
+
modulePath: input.module,
|
|
805
|
+
},
|
|
806
|
+
signal: extra.signal,
|
|
807
|
+
timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
|
|
808
|
+
}),
|
|
809
|
+
),
|
|
810
|
+
),
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
server.registerTool(
|
|
814
|
+
"schedule_run",
|
|
815
|
+
{
|
|
816
|
+
description:
|
|
817
|
+
"Run one registered app-owned schedule in an isolated process and return exactly the versioned beignet schedule run <name> --json report. Payload and run metadata match the CLI; cancellation or timeout stops the complete process tree.",
|
|
818
|
+
inputSchema: scheduleRunInputSchema,
|
|
819
|
+
annotations: {
|
|
820
|
+
readOnlyHint: false,
|
|
821
|
+
destructiveHint: true,
|
|
822
|
+
idempotentHint: false,
|
|
823
|
+
openWorldHint: true,
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
async (input, extra) =>
|
|
827
|
+
safeToolResult(async () =>
|
|
828
|
+
operationalJsonResult(
|
|
829
|
+
await runIsolatedOperationalCommand({
|
|
830
|
+
cwd,
|
|
831
|
+
request: {
|
|
832
|
+
command: "schedule:run",
|
|
833
|
+
name: input.name,
|
|
834
|
+
payload: input.payload,
|
|
835
|
+
id: input.runId,
|
|
836
|
+
attempt: input.attempt,
|
|
837
|
+
scheduledAt: input.scheduledAt,
|
|
838
|
+
triggeredAt: input.triggeredAt,
|
|
839
|
+
source: input.source,
|
|
840
|
+
modulePath: input.module,
|
|
841
|
+
},
|
|
842
|
+
signal: extra.signal,
|
|
843
|
+
timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
|
|
844
|
+
}),
|
|
845
|
+
),
|
|
846
|
+
),
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
server.registerTool(
|
|
850
|
+
"outbox_inspect",
|
|
851
|
+
{
|
|
852
|
+
description:
|
|
853
|
+
"Read outbox messages with list or show and return exactly the matching versioned beignet outbox <operation> --json report. Runs in an isolated process with bounded structured output, cancellation, and a timeout.",
|
|
854
|
+
inputSchema: outboxInspectInputSchema,
|
|
855
|
+
annotations: {
|
|
856
|
+
readOnlyHint: true,
|
|
857
|
+
destructiveHint: false,
|
|
858
|
+
idempotentHint: true,
|
|
859
|
+
openWorldHint: true,
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
async (input, extra) =>
|
|
863
|
+
safeToolResult(async () =>
|
|
864
|
+
operationalJsonResult(
|
|
865
|
+
await runIsolatedOperationalCommand({
|
|
866
|
+
cwd,
|
|
867
|
+
request:
|
|
868
|
+
input.operation === "list"
|
|
869
|
+
? {
|
|
870
|
+
command: "outbox:list",
|
|
871
|
+
status: input.status,
|
|
872
|
+
kind: input.kind,
|
|
873
|
+
name: input.name,
|
|
874
|
+
limit: input.limit,
|
|
875
|
+
modulePath: input.module,
|
|
876
|
+
}
|
|
877
|
+
: {
|
|
878
|
+
command: "outbox:show",
|
|
879
|
+
id: input.id,
|
|
880
|
+
modulePath: input.module,
|
|
881
|
+
},
|
|
882
|
+
signal: extra.signal,
|
|
883
|
+
timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
|
|
884
|
+
}),
|
|
885
|
+
),
|
|
886
|
+
),
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
server.registerTool(
|
|
890
|
+
"outbox_run",
|
|
891
|
+
{
|
|
892
|
+
description:
|
|
893
|
+
"Run a state-changing outbox drain, requeue, purge, or prune operation and return exactly the matching versioned beignet outbox <operation> --json report. Purge and prune support dryRun. Runs in an isolated process with bounded structured output, cancellation, and a timeout.",
|
|
894
|
+
inputSchema: outboxRunInputSchema,
|
|
895
|
+
annotations: {
|
|
896
|
+
readOnlyHint: false,
|
|
897
|
+
destructiveHint: true,
|
|
898
|
+
idempotentHint: false,
|
|
899
|
+
openWorldHint: true,
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
async (input, extra) =>
|
|
903
|
+
safeToolResult(async () => {
|
|
904
|
+
const request =
|
|
905
|
+
input.operation === "drain"
|
|
906
|
+
? {
|
|
907
|
+
command: "outbox:drain" as const,
|
|
908
|
+
batchSize: input.batchSize,
|
|
909
|
+
modulePath: input.module,
|
|
910
|
+
}
|
|
911
|
+
: input.operation === "requeue"
|
|
912
|
+
? {
|
|
913
|
+
command: "outbox:requeue" as const,
|
|
914
|
+
id: input.id,
|
|
915
|
+
availableAt: input.availableAt,
|
|
916
|
+
resetAttempts: input.resetAttempts,
|
|
917
|
+
modulePath: input.module,
|
|
918
|
+
}
|
|
919
|
+
: input.operation === "purge"
|
|
920
|
+
? {
|
|
921
|
+
command: "outbox:purge" as const,
|
|
922
|
+
before: input.before,
|
|
923
|
+
all: input.all,
|
|
924
|
+
limit: input.limit,
|
|
925
|
+
dryRun: input.dryRun,
|
|
926
|
+
modulePath: input.module,
|
|
927
|
+
}
|
|
928
|
+
: {
|
|
929
|
+
command: "outbox:prune" as const,
|
|
930
|
+
before: input.before,
|
|
931
|
+
limit: input.limit,
|
|
932
|
+
dryRun: input.dryRun,
|
|
933
|
+
modulePath: input.module,
|
|
934
|
+
};
|
|
935
|
+
return operationalJsonResult(
|
|
936
|
+
await runIsolatedOperationalCommand({
|
|
937
|
+
cwd,
|
|
938
|
+
request,
|
|
939
|
+
signal: extra.signal,
|
|
940
|
+
timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
|
|
941
|
+
}),
|
|
942
|
+
);
|
|
943
|
+
}),
|
|
944
|
+
);
|
|
945
|
+
|
|
564
946
|
server.registerTool(
|
|
565
947
|
"routes",
|
|
566
948
|
{
|
|
@@ -729,6 +1111,79 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
|
|
|
729
1111
|
return server;
|
|
730
1112
|
}
|
|
731
1113
|
|
|
1114
|
+
async function readAppGuidance(cwd: string): Promise<string> {
|
|
1115
|
+
const guidancePath = await resolveAppGuidancePath(cwd);
|
|
1116
|
+
if (!guidancePath) {
|
|
1117
|
+
return "# Beignet app guidance\n\nNo app-local AGENTS.md was found. Use the app_map and explain tools before editing, prefer Beignet generators, and run check after changes.\n";
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
let file: Awaited<ReturnType<typeof open>>;
|
|
1121
|
+
file = await open(guidancePath, "r");
|
|
1122
|
+
|
|
1123
|
+
try {
|
|
1124
|
+
const source = Buffer.alloc(maxMcpGuidanceBytes + 1);
|
|
1125
|
+
let bytesRead = 0;
|
|
1126
|
+
while (bytesRead < source.byteLength) {
|
|
1127
|
+
const chunk = await file.read(
|
|
1128
|
+
source,
|
|
1129
|
+
bytesRead,
|
|
1130
|
+
source.byteLength - bytesRead,
|
|
1131
|
+
bytesRead,
|
|
1132
|
+
);
|
|
1133
|
+
if (chunk.bytesRead === 0) break;
|
|
1134
|
+
bytesRead += chunk.bytesRead;
|
|
1135
|
+
}
|
|
1136
|
+
if (bytesRead <= maxMcpGuidanceBytes) {
|
|
1137
|
+
return source.subarray(0, bytesRead).toString("utf8");
|
|
1138
|
+
}
|
|
1139
|
+
return `${source.subarray(0, maxMcpGuidanceBytes).toString("utf8")}\n\n[Guidance truncated at ${maxMcpGuidanceBytes} bytes. Read AGENTS.md directly for the complete file.]\n`;
|
|
1140
|
+
} finally {
|
|
1141
|
+
await file.close();
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
async function resolveAppGuidancePath(
|
|
1146
|
+
cwd: string,
|
|
1147
|
+
): Promise<string | undefined> {
|
|
1148
|
+
let guidancePath: string;
|
|
1149
|
+
try {
|
|
1150
|
+
guidancePath = await realpath(path.join(cwd, "AGENTS.md"));
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
1153
|
+
throw error;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const appPath = await realpath(cwd);
|
|
1157
|
+
const relativePath = path.relative(appPath, guidancePath);
|
|
1158
|
+
if (
|
|
1159
|
+
relativePath === ".." ||
|
|
1160
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
1161
|
+
path.isAbsolute(relativePath)
|
|
1162
|
+
) {
|
|
1163
|
+
throw new Error("App guidance must stay inside the Beignet app directory.");
|
|
1164
|
+
}
|
|
1165
|
+
return guidancePath;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function appMapFeatureNames(
|
|
1169
|
+
result: Awaited<ReturnType<typeof mapApp>>,
|
|
1170
|
+
): string[] {
|
|
1171
|
+
return result.nodes
|
|
1172
|
+
.filter((node) => node.kind === "feature")
|
|
1173
|
+
.map((node) => node.name)
|
|
1174
|
+
.sort();
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function resourceVariable(
|
|
1178
|
+
value: string | string[] | undefined,
|
|
1179
|
+
name: string,
|
|
1180
|
+
): string {
|
|
1181
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1182
|
+
throw new Error(`MCP resource variable "${name}" must be one value.`);
|
|
1183
|
+
}
|
|
1184
|
+
return value;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
732
1187
|
/**
|
|
733
1188
|
* Run the Beignet MCP server over stdio until the client disconnects.
|
|
734
1189
|
* Stdout belongs to the transport; diagnostics must go to stderr.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export class OperationalCleanupAfterCompletionError extends Error {
|
|
2
|
+
readonly operationCompleted = true;
|
|
3
|
+
|
|
4
|
+
constructor(operation: string, cleanup: string, cause: unknown) {
|
|
5
|
+
const cleanupError =
|
|
6
|
+
cause instanceof Error ? cause : new Error(String(cause));
|
|
7
|
+
super(
|
|
8
|
+
`${operation} completed, but ${cleanup} failed: ${cleanupError.message}`,
|
|
9
|
+
{ cause: cleanupError },
|
|
10
|
+
);
|
|
11
|
+
this.name = "OperationalCleanupAfterCompletionError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isOperationalCleanupAfterCompletionError(
|
|
16
|
+
error: unknown,
|
|
17
|
+
): error is OperationalCleanupAfterCompletionError {
|
|
18
|
+
return error instanceof OperationalCleanupAfterCompletionError;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function runWithOperationalCleanup<Result>(options: {
|
|
22
|
+
operation: string;
|
|
23
|
+
cleanupLabel: string;
|
|
24
|
+
run: () => Promise<Result>;
|
|
25
|
+
cleanup: () => Promise<void>;
|
|
26
|
+
}): Promise<Result> {
|
|
27
|
+
let operationFailed = false;
|
|
28
|
+
let operationError: unknown;
|
|
29
|
+
let result!: Result;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
result = await options.run();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
operationFailed = true;
|
|
35
|
+
operationError = error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let cleanupFailed = false;
|
|
39
|
+
let cleanupError: unknown;
|
|
40
|
+
try {
|
|
41
|
+
await options.cleanup();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
cleanupFailed = true;
|
|
44
|
+
cleanupError = error;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (operationFailed) throw operationError;
|
|
48
|
+
if (cleanupFailed) {
|
|
49
|
+
throw new OperationalCleanupAfterCompletionError(
|
|
50
|
+
options.operation,
|
|
51
|
+
options.cleanupLabel,
|
|
52
|
+
cleanupError,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function isAppRelativeOperationalModulePath(value: string): boolean {
|
|
5
|
+
if (path.isAbsolute(value) || path.win32.isAbsolute(value)) return false;
|
|
6
|
+
const normalized = path.posix.normalize(value.replaceAll("\\", "/"));
|
|
7
|
+
return normalized !== ".." && !normalized.startsWith("../");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function resolveAppOperationalModulePath(
|
|
11
|
+
cwd: string,
|
|
12
|
+
modulePath: string,
|
|
13
|
+
): Promise<string> {
|
|
14
|
+
if (!isAppRelativeOperationalModulePath(modulePath)) {
|
|
15
|
+
throw operationalModuleOutsideAppError();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const appPath = await realpath(cwd);
|
|
19
|
+
const resolvedModulePath = await realpath(path.resolve(cwd, modulePath));
|
|
20
|
+
const relativePath = path.relative(appPath, resolvedModulePath);
|
|
21
|
+
if (
|
|
22
|
+
relativePath === ".." ||
|
|
23
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
24
|
+
path.isAbsolute(relativePath)
|
|
25
|
+
) {
|
|
26
|
+
throw operationalModuleOutsideAppError();
|
|
27
|
+
}
|
|
28
|
+
return resolvedModulePath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function operationalModuleOutsideAppError(): Error {
|
|
32
|
+
return new Error(
|
|
33
|
+
"Registry modules must stay inside the Beignet app directory.",
|
|
34
|
+
);
|
|
35
|
+
}
|