@beignet/cli 0.0.44 → 0.0.46

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +78 -16
  3. package/dist/choices.d.ts +5 -1
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +12 -0
  6. package/dist/choices.js.map +1 -1
  7. package/dist/db.d.ts +4 -0
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +12 -6
  10. package/dist/db.js.map +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +13 -13
  13. package/dist/index.js.map +1 -1
  14. package/dist/inspect.d.ts.map +1 -1
  15. package/dist/inspect.js +598 -62
  16. package/dist/inspect.js.map +1 -1
  17. package/dist/lib.d.ts +4 -1
  18. package/dist/lib.d.ts.map +1 -1
  19. package/dist/lib.js +1 -1
  20. package/dist/lib.js.map +1 -1
  21. package/dist/mcp.d.ts +2 -1
  22. package/dist/mcp.d.ts.map +1 -1
  23. package/dist/mcp.js +356 -5
  24. package/dist/mcp.js.map +1 -1
  25. package/dist/operational-lifecycle.d.ts +12 -0
  26. package/dist/operational-lifecycle.d.ts.map +1 -0
  27. package/dist/operational-lifecycle.js +39 -0
  28. package/dist/operational-lifecycle.js.map +1 -0
  29. package/dist/operational-path.d.ts +3 -0
  30. package/dist/operational-path.d.ts.map +1 -0
  31. package/dist/operational-path.js +26 -0
  32. package/dist/operational-path.js.map +1 -0
  33. package/dist/operational-process.d.ts +78 -0
  34. package/dist/operational-process.d.ts.map +1 -0
  35. package/dist/operational-process.js +228 -0
  36. package/dist/operational-process.js.map +1 -0
  37. package/dist/operational-runner.d.ts +2 -0
  38. package/dist/operational-runner.d.ts.map +1 -0
  39. package/dist/operational-runner.js +168 -0
  40. package/dist/operational-runner.js.map +1 -0
  41. package/dist/outbox.d.ts +14 -6
  42. package/dist/outbox.d.ts.map +1 -1
  43. package/dist/outbox.js +140 -124
  44. package/dist/outbox.js.map +1 -1
  45. package/dist/schedule.d.ts +2 -1
  46. package/dist/schedule.d.ts.map +1 -1
  47. package/dist/schedule.js +79 -81
  48. package/dist/schedule.js.map +1 -1
  49. package/dist/task.d.ts +2 -1
  50. package/dist/task.d.ts.map +1 -1
  51. package/dist/task.js +57 -61
  52. package/dist/task.js.map +1 -1
  53. package/dist/templates/agents.d.ts.map +1 -1
  54. package/dist/templates/agents.js +32 -8
  55. package/dist/templates/agents.js.map +1 -1
  56. package/dist/templates/server.d.ts.map +1 -1
  57. package/dist/templates/server.js +9 -2
  58. package/dist/templates/server.js.map +1 -1
  59. package/dist/templates/shadcn.d.ts.map +1 -1
  60. package/dist/templates/shadcn.js +3 -1
  61. package/dist/templates/shadcn.js.map +1 -1
  62. package/dist/templates/shared.js +1 -1
  63. package/package.json +2 -2
  64. package/skills/app-structure/SKILL.md +26 -7
  65. package/src/choices.ts +22 -0
  66. package/src/db.ts +12 -6
  67. package/src/index.ts +19 -16
  68. package/src/inspect.ts +844 -67
  69. package/src/lib.ts +27 -1
  70. package/src/mcp.ts +459 -4
  71. package/src/operational-lifecycle.ts +56 -0
  72. package/src/operational-path.ts +35 -0
  73. package/src/operational-process.ts +454 -0
  74. package/src/operational-runner.ts +221 -0
  75. package/src/outbox.ts +154 -128
  76. package/src/schedule.ts +84 -89
  77. package/src/task.ts +58 -62
  78. package/src/templates/agents.ts +32 -8
  79. package/src/templates/server.ts +9 -2
  80. package/src/templates/shadcn.ts +3 -1
  81. package/src/templates/shared.ts +1 -1
package/src/lib.ts CHANGED
@@ -103,7 +103,33 @@ export {
103
103
  makeUpload,
104
104
  makeUseCase,
105
105
  } from "./make.js";
106
- export { runOutboxDrain } from "./outbox.js";
106
+ export type {
107
+ RunOutboxDrainOptions,
108
+ RunOutboxDrainResult,
109
+ RunOutboxListOptions,
110
+ RunOutboxListResult,
111
+ RunOutboxPruneOptions,
112
+ RunOutboxPruneResult,
113
+ RunOutboxPurgeOptions,
114
+ RunOutboxPurgeResult,
115
+ RunOutboxRequeueOptions,
116
+ RunOutboxRequeueResult,
117
+ RunOutboxShowOptions,
118
+ RunOutboxShowResult,
119
+ } from "./outbox.js";
120
+ export {
121
+ runOutboxDrain,
122
+ runOutboxList,
123
+ runOutboxPrune,
124
+ runOutboxPurge,
125
+ runOutboxRequeue,
126
+ runOutboxShow,
127
+ } from "./outbox.js";
128
+ export type {
129
+ RunAppScheduleOptions,
130
+ RunAppScheduleResult,
131
+ } from "./schedule.js";
107
132
  export { runAppSchedule } from "./schedule.js";
133
+ export type { RunAppTaskOptions, RunAppTaskResult } from "./task.js";
108
134
  export { runAppTask } from "./task.js";
109
135
  export type { StarterProviderName } from "./templates/index.js";
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 { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
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-map, validation, and generator tools.
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
  {
@@ -613,7 +995,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
613
995
  "doctor_fix_plan",
614
996
  {
615
997
  description:
616
- "Plan every currently available low-risk doctor repair without changing files. Returns stable operation IDs, exact unified patches, file hashes, and a plan ID for guarded doctor_fix application.",
998
+ "Plan every currently available low-risk doctor repair without changing files, including eligible registration drift and managed default provider-table exports. Returns stable operation IDs, exact unified patches, file hashes, and a plan ID for guarded doctor_fix application.",
617
999
  inputSchema: {
618
1000
  strict: z
619
1001
  .boolean()
@@ -639,7 +1021,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
639
1021
  "doctor_fix",
640
1022
  {
641
1023
  description:
642
- "Apply low-risk doctor repairs, then re-check the app. With no planId, preserves the legacy apply-all behavior. Pass the planId returned by doctor_fix_plan for stale-file protection, optionally with fixIds to select repair operations.",
1024
+ "Apply eligible low-risk doctor repairs, then re-check the app; custom or ambiguous code remains diagnostic-only. With no planId, preserves the legacy apply-all behavior. Pass the planId returned by doctor_fix_plan for stale-file protection, optionally with fixIds to select repair operations.",
643
1025
  inputSchema: {
644
1026
  strict: z
645
1027
  .boolean()
@@ -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
+ }