@beignet/cli 0.0.48 → 0.0.50

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 (111) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +117 -22
  3. package/dist/analysis/contract-factories.d.ts +17 -0
  4. package/dist/analysis/contract-factories.d.ts.map +1 -0
  5. package/dist/analysis/contract-factories.js +176 -0
  6. package/dist/analysis/contract-factories.js.map +1 -0
  7. package/dist/analysis/layers.d.ts +5 -0
  8. package/dist/analysis/layers.d.ts.map +1 -0
  9. package/dist/analysis/layers.js +172 -0
  10. package/dist/analysis/layers.js.map +1 -0
  11. package/dist/analysis/port-wiring.d.ts +23 -0
  12. package/dist/analysis/port-wiring.d.ts.map +1 -0
  13. package/dist/analysis/port-wiring.js +379 -0
  14. package/dist/analysis/port-wiring.js.map +1 -0
  15. package/dist/analysis/source-index.d.ts +5 -0
  16. package/dist/analysis/source-index.d.ts.map +1 -1
  17. package/dist/analysis/source-index.js +171 -42
  18. package/dist/analysis/source-index.js.map +1 -1
  19. package/dist/analysis/workspace.d.ts +11 -4
  20. package/dist/analysis/workspace.d.ts.map +1 -1
  21. package/dist/analysis/workspace.js +78 -8
  22. package/dist/analysis/workspace.js.map +1 -1
  23. package/dist/app-map-changes.d.ts +103 -0
  24. package/dist/app-map-changes.d.ts.map +1 -0
  25. package/dist/app-map-changes.js +949 -0
  26. package/dist/app-map-changes.js.map +1 -0
  27. package/dist/app-map.d.ts.map +1 -1
  28. package/dist/app-map.js +311 -92
  29. package/dist/app-map.js.map +1 -1
  30. package/dist/check.js +37 -3
  31. package/dist/check.js.map +1 -1
  32. package/dist/explain.js +4 -4
  33. package/dist/explain.js.map +1 -1
  34. package/dist/git-changes.d.ts +30 -0
  35. package/dist/git-changes.d.ts.map +1 -0
  36. package/dist/git-changes.js +367 -0
  37. package/dist/git-changes.js.map +1 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +26 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/inspect.d.ts.map +1 -1
  42. package/dist/inspect.js +706 -186
  43. package/dist/inspect.js.map +1 -1
  44. package/dist/lib.d.ts +3 -0
  45. package/dist/lib.d.ts.map +1 -1
  46. package/dist/lib.js +1 -0
  47. package/dist/lib.js.map +1 -1
  48. package/dist/lint.d.ts.map +1 -1
  49. package/dist/lint.js +309 -487
  50. package/dist/lint.js.map +1 -1
  51. package/dist/make/inbox.d.ts.map +1 -1
  52. package/dist/make/inbox.js +11 -4
  53. package/dist/make/inbox.js.map +1 -1
  54. package/dist/make/payments.js +2 -2
  55. package/dist/make/shared.d.ts.map +1 -1
  56. package/dist/make/shared.js +31 -27
  57. package/dist/make/shared.js.map +1 -1
  58. package/dist/make.d.ts.map +1 -1
  59. package/dist/make.js +144 -8
  60. package/dist/make.js.map +1 -1
  61. package/dist/mcp.d.ts +1 -1
  62. package/dist/mcp.d.ts.map +1 -1
  63. package/dist/mcp.js +43 -21
  64. package/dist/mcp.js.map +1 -1
  65. package/dist/provider-add.d.ts.map +1 -1
  66. package/dist/provider-add.js +142 -7
  67. package/dist/provider-add.js.map +1 -1
  68. package/dist/registry-edits.d.ts +7 -0
  69. package/dist/registry-edits.d.ts.map +1 -1
  70. package/dist/registry-edits.js +237 -53
  71. package/dist/registry-edits.js.map +1 -1
  72. package/dist/templates/agents.d.ts.map +1 -1
  73. package/dist/templates/agents.js +37 -1
  74. package/dist/templates/agents.js.map +1 -1
  75. package/dist/templates/base.d.ts.map +1 -1
  76. package/dist/templates/base.js +13 -2
  77. package/dist/templates/base.js.map +1 -1
  78. package/dist/templates/server.d.ts.map +1 -1
  79. package/dist/templates/server.js +2 -0
  80. package/dist/templates/server.js.map +1 -1
  81. package/dist/templates/shared.d.ts +1 -0
  82. package/dist/templates/shared.d.ts.map +1 -1
  83. package/dist/templates/shared.js +3 -2
  84. package/dist/templates/shared.js.map +1 -1
  85. package/package.json +7 -6
  86. package/skills/app-structure/SKILL.md +71 -7
  87. package/src/analysis/contract-factories.ts +256 -0
  88. package/src/analysis/layers.ts +266 -0
  89. package/src/analysis/port-wiring.ts +506 -0
  90. package/src/analysis/source-index.ts +225 -57
  91. package/src/analysis/workspace.ts +134 -9
  92. package/src/app-map-changes.ts +1462 -0
  93. package/src/app-map.ts +418 -109
  94. package/src/check.ts +51 -3
  95. package/src/explain.ts +4 -4
  96. package/src/git-changes.ts +511 -0
  97. package/src/index.ts +39 -1
  98. package/src/inspect.ts +1005 -238
  99. package/src/lib.ts +21 -0
  100. package/src/lint.ts +393 -643
  101. package/src/make/inbox.ts +11 -4
  102. package/src/make/payments.ts +2 -2
  103. package/src/make/shared.ts +31 -27
  104. package/src/make.ts +225 -8
  105. package/src/mcp.ts +51 -24
  106. package/src/provider-add.ts +186 -7
  107. package/src/registry-edits.ts +313 -63
  108. package/src/templates/agents.ts +37 -1
  109. package/src/templates/base.ts +13 -2
  110. package/src/templates/server.ts +2 -0
  111. package/src/templates/shared.ts +3 -2
package/src/make/inbox.ts CHANGED
@@ -518,9 +518,9 @@ export const inboxNotificationSchema = z.object({
518
518
  });
519
519
 
520
520
  export const listInboxQuerySchema = z.object({
521
- \tlimit: z.coerce.number().int().min(1).max(50).default(20),
521
+ \tlimit: z.number().int().min(1).max(50).default(20),
522
522
  \tcursor: z.string().min(1).optional(),
523
- \tunread: z.coerce.boolean().default(false),
523
+ \tunread: z.boolean().default(false),
524
524
  });
525
525
 
526
526
  export const listInboxOutputSchema = z.object({
@@ -659,7 +659,7 @@ export function inboxContractsFile(config: ResolvedBeignetConfig): string {
659
659
  );
660
660
  const errorsModule = aliasModule(resourceSharedErrorsPath(config));
661
661
 
662
- return `import { defineContractGroup } from "@beignet/core/contracts";
662
+ return `import { defineContractGroup, defineQueryTransport, query } from "@beignet/core/contracts";
663
663
  import {
664
664
  \tinboxNotificationSchema,
665
665
  \tlistInboxOutputSchema,
@@ -674,7 +674,14 @@ const inbox = defineContractGroup().namespace("inbox").prefix("/api/inbox");
674
674
 
675
675
  export const listInbox = inbox
676
676
  \t.get("/")
677
- \t.query(listInboxQuerySchema)
677
+ \t.query(
678
+ \t\tlistInboxQuerySchema,
679
+ \t\tdefineQueryTransport({
680
+ \t\t\tlimit: query.integer(),
681
+ \t\t\tcursor: query.string(),
682
+ \t\t\tunread: query.boolean(),
683
+ \t\t}),
684
+ \t)
678
685
  \t.meta({ auth: "required" })
679
686
  \t.errors({ Unauthorized: errors.Unauthorized })
680
687
  \t.responses({
@@ -862,7 +862,7 @@ export function isBillingAccountActive(
862
862
  export function billingContractsFile(
863
863
  idempotencyScope: "actor" | "actor-tenant" = "actor",
864
864
  ): string {
865
- return `import { defineContractGroup } from "@beignet/core/contracts";
865
+ return `import { defineContractGroup, defineQueryTransport } from "@beignet/core/contracts";
866
866
  import { z } from "zod";
867
867
  import {
868
868
  \tbillingStatusOutputSchema,
@@ -882,7 +882,7 @@ const checkoutBilling = billing.headers(checkoutHeadersSchema);
882
882
 
883
883
  export const getBillingStatus = billing
884
884
  \t.get("/")
885
- \t.query(z.object({}))
885
+ \t.query(z.object({}), defineQueryTransport({}))
886
886
  \t.meta({ auth: "required" })
887
887
  \t.errors({ Unauthorized: errors.Unauthorized })
888
888
  \t.responses({
@@ -453,36 +453,40 @@ export function wireListenersProviderSource(
453
453
  >()({
454
454
  \tname: "app-listeners",
455
455
  \tsetup({ ports, createServiceContext }) {
456
- \t\tconst unregister = registerListeners(ports.eventBus, listeners, {
457
- \t\t\tctx: () =>
458
- \t\t\t\tcreateServiceContext({
459
- \t\t\t\t\tactor: createServiceActor("beignet-listener"),
460
- \t\t\t\t}),
461
- \t\t\tonError(error, listener) {
462
- \t\t\t\tports.logger.error("Event listener failed", {
463
- \t\t\t\t\terror,
464
- \t\t\t\t\tlistenerName: listener.name,
465
- \t\t\t\t});
466
- \t\t\t\tvoid tryReportException({
467
- \t\t\t\t\treporter: ports.errorReporter,
468
- \t\t\t\t\terror,
469
- \t\t\t\t\treportOptions: {
470
- \t\t\t\t\t\tlevel: "error",
471
- \t\t\t\t\t\tmechanism: "beignet.listener",
472
- \t\t\t\t\t\thandled: false,
473
- \t\t\t\t\t\ttags: {
474
- \t\t\t\t\t\t\t"beignet.kind": "listener",
475
- \t\t\t\t\t\t\t"beignet.listener": listener.name,
476
- \t\t\t\t\t\t},
477
- \t\t\t\t\t\tcontexts: { listener: { name: listener.name } },
456
+ \t\tlet registration: ReturnType<typeof registerListeners> | undefined;
457
+
458
+ \t\treturn {
459
+ \t\t\tasync start() {
460
+ \t\t\t\tregistration = registerListeners(ports.eventBus, listeners, {
461
+ \t\t\t\t\tctx: () =>
462
+ \t\t\t\t\t\tcreateServiceContext({
463
+ \t\t\t\t\t\t\tactor: createServiceActor("beignet-listener"),
464
+ \t\t\t\t\t\t}),
465
+ \t\t\t\t\tonError(error, listener) {
466
+ \t\t\t\t\t\tports.logger.error("Event listener failed", {
467
+ \t\t\t\t\t\t\terror,
468
+ \t\t\t\t\t\t\tlistenerName: listener.name,
469
+ \t\t\t\t\t\t});
470
+ \t\t\t\t\t\tvoid tryReportException({
471
+ \t\t\t\t\t\t\treporter: ports.errorReporter,
472
+ \t\t\t\t\t\t\terror,
473
+ \t\t\t\t\t\t\treportOptions: {
474
+ \t\t\t\t\t\t\t\tlevel: "error",
475
+ \t\t\t\t\t\t\t\tmechanism: "beignet.listener",
476
+ \t\t\t\t\t\t\t\thandled: false,
477
+ \t\t\t\t\t\t\t\ttags: {
478
+ \t\t\t\t\t\t\t\t\t"beignet.kind": "listener",
479
+ \t\t\t\t\t\t\t\t\t"beignet.listener": listener.name,
480
+ \t\t\t\t\t\t\t\t},
481
+ \t\t\t\t\t\t\t\tcontexts: { listener: { name: listener.name } },
482
+ \t\t\t\t\t\t\t},
483
+ \t\t\t\t\t\t});
478
484
  \t\t\t\t\t},
479
485
  \t\t\t\t});
486
+ \t\t\t\tawait registration.ready;
480
487
  \t\t\t},
481
- \t\t});
482
-
483
- \t\treturn {
484
- \t\t\tstop() {
485
- \t\t\t\tunregister();
488
+ \t\t\tasync stop() {
489
+ \t\t\t\tawait registration?.unsubscribe();
486
490
  \t\t\t},
487
491
  \t\t};
488
492
  \t},
package/src/make.ts CHANGED
@@ -2,6 +2,8 @@ import { randomBytes } from "node:crypto";
2
2
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import ts from "typescript";
5
+ import { analyzePortWiringFiles } from "./analysis/port-wiring.js";
6
+ import { listProjectFiles } from "./analysis/workspace.js";
5
7
  import type { MakeFeatureAddon, MakeFeatureRecipe } from "./choices.js";
6
8
  import {
7
9
  type BeignetConfig,
@@ -1084,6 +1086,10 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1084
1086
  await assertFeatureArtifactApp(targetDir, config, "job");
1085
1087
  await assertServerRuntimeApp(targetDir, config, "job");
1086
1088
  await updateOutboxPortWiring(targetDir, config, { dryRun: true });
1089
+ const jobDispatcherPlan = await planInlineJobDispatcherWiring(
1090
+ targetDir,
1091
+ config,
1092
+ );
1087
1093
  const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
1088
1094
  force: Boolean(options.force),
1089
1095
  });
@@ -1114,6 +1120,9 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1114
1120
  await applyOutboxDrainWiring(result, targetDir, outboxDrainPlan, {
1115
1121
  dryRun: Boolean(options.dryRun),
1116
1122
  });
1123
+ await applyInlineJobDispatcherWiring(result, targetDir, jobDispatcherPlan, {
1124
+ dryRun: Boolean(options.dryRun),
1125
+ });
1117
1126
  await applyOutboxPortWiring(result, targetDir, config, {
1118
1127
  dryRun: Boolean(options.dryRun),
1119
1128
  });
@@ -1124,6 +1133,201 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1124
1133
  return result;
1125
1134
  }
1126
1135
 
1136
+ type InlineJobDispatcherWiringPlan = {
1137
+ changes: { path: string; before: string; after: string }[];
1138
+ };
1139
+
1140
+ async function planInlineJobDispatcherWiring(
1141
+ targetDir: string,
1142
+ config: ResolvedBeignetConfig,
1143
+ ): Promise<InlineJobDispatcherWiringPlan> {
1144
+ const portsFile = config.paths.ports;
1145
+ const portWiringFile = config.paths.portWiring;
1146
+ const providersFile = providersFilePath(config);
1147
+ const ports = await readFile(path.join(targetDir, portsFile), "utf8");
1148
+ const portWiring = await readFile(
1149
+ path.join(targetDir, portWiringFile),
1150
+ "utf8",
1151
+ );
1152
+ const analysis = await analyzePortWiringFiles({
1153
+ targetDir,
1154
+ files: await listProjectFiles(targetDir),
1155
+ config,
1156
+ });
1157
+ const declaresJobs = analysis.declared.has("jobs");
1158
+ const wiresJobs = analysis.bound.has("jobs") || analysis.deferred.has("jobs");
1159
+
1160
+ if (declaresJobs && wiresJobs) {
1161
+ return { changes: [] };
1162
+ }
1163
+ if (!declaresJobs && analysis.declarationIndeterminate) {
1164
+ throw new Error(
1165
+ `Could not prove whether AppPorts in ${portsFile} declares the jobs port. Preserve the app's custom port type and add jobs: JobDispatcherPort manually before running make job.`,
1166
+ );
1167
+ }
1168
+ if (!wiresJobs && analysis.indeterminate) {
1169
+ throw new Error(
1170
+ `Could not prove how the jobs port is wired in ${portWiringFile}. Preserve the app's custom dispatcher by binding or deferring "jobs" in a statically inspectable definePorts(...) object before running make job.`,
1171
+ );
1172
+ }
1173
+
1174
+ let nextPorts = ports;
1175
+ if (!declaresJobs && !analysis.appPortsInferredFromWiring) {
1176
+ nextPorts = addNamedTypeImport(
1177
+ nextPorts,
1178
+ "JobDispatcherPort",
1179
+ "@beignet/core/ports",
1180
+ );
1181
+ nextPorts = insertTypeProperty(
1182
+ nextPorts,
1183
+ "AppPorts",
1184
+ "jobs: JobDispatcherPort;",
1185
+ `Could not find AppPorts in ${portsFile}. Add jobs: JobDispatcherPort manually and wire a dispatcher, or restore the generated ports file before running make job.`,
1186
+ );
1187
+ }
1188
+
1189
+ if (wiresJobs) {
1190
+ return {
1191
+ changes:
1192
+ ports === nextPorts
1193
+ ? []
1194
+ : [{ path: portsFile, before: ports, after: nextPorts }],
1195
+ };
1196
+ }
1197
+
1198
+ const nextPortWiring = appendDeferredPortKey(portWiring, "jobs");
1199
+ if (nextPortWiring === portWiring) {
1200
+ throw new Error(
1201
+ `Could not find the deferred port list in ${portWiringFile}. Add "jobs" manually and wire a dispatcher, or restore the generated port wiring file before running make job.`,
1202
+ );
1203
+ }
1204
+
1205
+ const providersPath = path.join(targetDir, providersFile);
1206
+ const providers = await readOptionalFile(providersPath);
1207
+ if (providers === undefined) {
1208
+ throw new Error(
1209
+ `Could not find ${providersFile}. Register an inline or provider-backed jobs dispatcher manually, or restore the generated providers file before running make job.`,
1210
+ );
1211
+ }
1212
+ let nextProviders = addNamedImport(
1213
+ providers,
1214
+ "createInlineJobDispatcher",
1215
+ "@beignet/core/jobs",
1216
+ );
1217
+ nextProviders = addNamedImport(
1218
+ nextProviders,
1219
+ "createProvider",
1220
+ "@beignet/core/providers",
1221
+ );
1222
+ nextProviders = addNamedTypeImport(
1223
+ nextProviders,
1224
+ "AppContext",
1225
+ aliasModule(config.paths.appContext),
1226
+ );
1227
+ nextProviders = addNamedTypeImport(
1228
+ nextProviders,
1229
+ "AppPorts",
1230
+ aliasModule(config.paths.ports),
1231
+ );
1232
+ nextProviders = addNamedTypeImport(
1233
+ nextProviders,
1234
+ "AppServiceContextInput",
1235
+ aliasModule(path.join(path.dirname(config.paths.server), "context.ts")),
1236
+ );
1237
+
1238
+ const providerName = "inlineJobsProvider";
1239
+ const generatedProviderMarker = "// beignet:generated-inline-jobs-provider";
1240
+ const providersSourceFile = ts.createSourceFile(
1241
+ providersFile,
1242
+ nextProviders,
1243
+ ts.ScriptTarget.Latest,
1244
+ true,
1245
+ ts.ScriptKind.TS,
1246
+ );
1247
+ const providerDeclaration = providersSourceFile.statements
1248
+ .filter(ts.isVariableStatement)
1249
+ .flatMap((statement) => [...statement.declarationList.declarations])
1250
+ .find(
1251
+ (declaration) =>
1252
+ ts.isIdentifier(declaration.name) &&
1253
+ declaration.name.text === providerName,
1254
+ );
1255
+ const providerDeclarationExists = providerDeclaration !== undefined;
1256
+ const providerIsGenerated = providerDeclaration
1257
+ ? nextProviders
1258
+ .slice(0, providerDeclaration.getStart(providersSourceFile))
1259
+ .trimEnd()
1260
+ .endsWith(generatedProviderMarker)
1261
+ : false;
1262
+ if (providerDeclarationExists && !providerIsGenerated) {
1263
+ throw new Error(
1264
+ `Could not install the generated inline jobs dispatcher because ${providerName} already exists in ${providersFile}. Rename the app-owned declaration or wire its jobs port explicitly before running make job.`,
1265
+ );
1266
+ }
1267
+ if (!providerDeclarationExists) {
1268
+ const definition = `// beignet:generated-inline-jobs-provider
1269
+ const ${providerName} = createProvider<
1270
+ {},
1271
+ AppContext,
1272
+ AppServiceContextInput
1273
+ >()({
1274
+ name: "inline-jobs",
1275
+ setup({ createServiceContext }): { ports: Pick<AppPorts, "jobs"> } {
1276
+ return {
1277
+ ports: {
1278
+ jobs: createInlineJobDispatcher<AppContext>({
1279
+ ctx: () => createServiceContext(undefined),
1280
+ }),
1281
+ },
1282
+ };
1283
+ },
1284
+ });
1285
+ `;
1286
+ const inserted = nextProviders.replace(
1287
+ /\nexport const providers = \[/,
1288
+ `\n${definition}\nexport const providers = [`,
1289
+ );
1290
+ if (inserted === nextProviders) {
1291
+ throw new Error(
1292
+ `Could not find the exported providers array in ${providersFile}. Register an inline or provider-backed jobs dispatcher manually, or restore the generated providers file before running make job.`,
1293
+ );
1294
+ }
1295
+ nextProviders = inserted;
1296
+ }
1297
+ const appended = appendToNamedArray(nextProviders, "providers", providerName);
1298
+ if (appended.kind === "missing") {
1299
+ throw new Error(
1300
+ `Could not find the exported providers array in ${providersFile}. Register ${providerName} manually, or restore the generated providers file before running make job.`,
1301
+ );
1302
+ }
1303
+ if (appended.kind === "updated") nextProviders = appended.source;
1304
+
1305
+ return {
1306
+ changes: [
1307
+ { path: portsFile, before: ports, after: nextPorts },
1308
+ { path: portWiringFile, before: portWiring, after: nextPortWiring },
1309
+ { path: providersFile, before: providers, after: nextProviders },
1310
+ ].filter((change) => change.before !== change.after),
1311
+ };
1312
+ }
1313
+
1314
+ async function applyInlineJobDispatcherWiring(
1315
+ result: MakeResult,
1316
+ targetDir: string,
1317
+ plan: InlineJobDispatcherWiringPlan,
1318
+ options: { dryRun: boolean },
1319
+ ): Promise<void> {
1320
+ for (const change of plan.changes) {
1321
+ if (!options.dryRun) {
1322
+ await writeFile(path.join(targetDir, change.path), change.after);
1323
+ }
1324
+ if (!result.updatedFiles.includes(change.path)) {
1325
+ result.updatedFiles.push(change.path);
1326
+ }
1327
+ if (!result.files.includes(change.path)) result.files.push(change.path);
1328
+ }
1329
+ }
1330
+
1127
1331
  export async function makeTask(
1128
1332
  options: MakeTaskOptions,
1129
1333
  ): Promise<MakeTaskResult> {
@@ -3983,8 +4187,8 @@ export function decode${names.singularPascal}Cursor(cursor: string): ${names.sin
3983
4187
 
3984
4188
  export const List${names.pluralPascal}InputSchema = z
3985
4189
  .object({
3986
- limit: z.coerce.number().int().min(1).max(100).default(20),
3987
- cursor: z.string().nullable().default(null),
4190
+ limit: z.number().int().min(1).max(100).default(20),
4191
+ cursor: z.string().optional().transform((value) => value ?? null),
3988
4192
  name: z.string().trim().min(1).max(120).optional(),
3989
4193
  sortBy: ${names.singularPascal}SortBySchema.default("createdAt"),
3990
4194
  sortDirection: ${names.singularPascal}SortDirectionSchema.default("desc"),
@@ -4840,7 +5044,7 @@ function contractFile(
4840
5044
  mode: ResourceGenerationMode,
4841
5045
  options: ResourceGenerationOptions,
4842
5046
  ): string {
4843
- return `import { defineContractGroup } from "@beignet/core/contracts";
5047
+ return `import { defineContractGroup, defineQueryTransport, query } from "@beignet/core/contracts";
4844
5048
  import { z } from "zod";
4845
5049
  ${mode === "resource" ? `import { errors } from "${aliasModule(resourceSharedErrorsPath(config))}";\n` : ""}import {
4846
5050
  Create${names.singularPascal}InputSchema,
@@ -4863,7 +5067,16 @@ const ${names.pluralCamel} = defineContractGroup()
4863
5067
 
4864
5068
  export const list${names.pluralPascal} = ${names.pluralCamel}
4865
5069
  .get("/api/${names.pluralKebab}")
4866
- .query(List${names.pluralPascal}InputSchema)
5070
+ .query(
5071
+ List${names.pluralPascal}InputSchema,
5072
+ defineQueryTransport({
5073
+ limit: query.integer(),
5074
+ cursor: query.string(),
5075
+ name: query.string(),
5076
+ sortBy: query.string(),
5077
+ sortDirection: query.string(),
5078
+ }),
5079
+ )
4867
5080
  .responses({
4868
5081
  200: List${names.pluralPascal}OutputSchema,
4869
5082
  });
@@ -4908,7 +5121,7 @@ ${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization:
4908
5121
  }
4909
5122
 
4910
5123
  function standaloneContractFile(names: ResourceNames): string {
4911
- return `import { defineContractGroup } from "@beignet/core/contracts";
5124
+ return `import { defineContractGroup, defineQueryTransport, query } from "@beignet/core/contracts";
4912
5125
  import { z } from "zod";
4913
5126
 
4914
5127
  const ErrorResponseSchema = z.object({
@@ -4932,8 +5145,12 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
4932
5145
  .get("/api/${names.pluralKebab}")
4933
5146
  .query(
4934
5147
  z.object({
4935
- limit: z.coerce.number().int().min(1).max(100).default(20),
4936
- offset: z.coerce.number().int().min(0).default(0),
5148
+ limit: z.number().int().min(1).max(100).default(20),
5149
+ offset: z.number().int().min(0).default(0),
5150
+ }),
5151
+ defineQueryTransport({
5152
+ limit: query.integer(),
5153
+ offset: query.integer(),
4937
5154
  }),
4938
5155
  )
4939
5156
  .responses({
@@ -6038,7 +6255,7 @@ ${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
6038
6255
  );
6039
6256
  const secondPage = await tester.run(
6040
6257
  list${names.pluralPascal}UseCase,
6041
- { limit: 2, cursor: firstPage.page.nextCursor },
6258
+ { limit: 2, cursor: firstPage.page.nextCursor! },
6042
6259
  { ctx },
6043
6260
  );
6044
6261
  const nameAscendingPage = await tester.run(
package/src/mcp.ts CHANGED
@@ -1,12 +1,10 @@
1
1
  import { open, realpath } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import {
4
- McpServer,
5
- ResourceTemplate,
6
- } from "@modelcontextprotocol/sdk/server/mcp.js";
7
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
- import { z } from "zod";
3
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
5
+ import { z } from "zod/v4";
9
6
  import { mapApp, projectAppMap } from "./app-map.js";
7
+ import { mapAppChanges } from "./app-map-changes.js";
10
8
  import { appMapNodeKinds } from "./app-map-schema.js";
11
9
  import { checkApp } from "./check.js";
12
10
  import {
@@ -582,8 +580,21 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
582
580
  "app_map",
583
581
  {
584
582
  description:
585
- "Return a deterministic graph of this Beignet app: features, contracts, routes, use cases, authorization, workflows, ports, providers, OpenAPI, tests, dependency edges, and validation findings. Project by feature or node kind to keep agent context focused.",
583
+ "Return a deterministic graph of this Beignet app, or map the current Git change set to bounded, source-backed concepts with changed=true. Changed mode is report-only and exposes unresolved evidence without claiming verification.",
586
584
  inputSchema: {
585
+ changed: z
586
+ .boolean()
587
+ .optional()
588
+ .describe(
589
+ "Map the current Git change set instead of returning the complete app graph.",
590
+ ),
591
+ base: z
592
+ .string()
593
+ .min(1)
594
+ .optional()
595
+ .describe(
596
+ "Include committed changes since the merge base of this already-local Git ref. Requires changed=true; Beignet never fetches it.",
597
+ ),
587
598
  feature: z
588
599
  .string()
589
600
  .optional()
@@ -601,6 +612,22 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
601
612
  },
602
613
  async (input) =>
603
614
  safeToolResult(async () => {
615
+ if (input.base !== undefined && !input.changed) {
616
+ throw new Error("app_map base requires changed=true.");
617
+ }
618
+ if (
619
+ input.changed &&
620
+ (input.feature !== undefined ||
621
+ input.kinds !== undefined ||
622
+ input.includeDiagnostics !== undefined)
623
+ ) {
624
+ throw new Error(
625
+ "app_map changed=true cannot be combined with feature, kinds, or includeDiagnostics.",
626
+ );
627
+ }
628
+ if (input.changed) {
629
+ return jsonResult(await mapAppChanges({ cwd, base: input.base }));
630
+ }
604
631
  const result = await mapApp({ cwd, strict: true });
605
632
  return jsonResult(
606
633
  projectAppMap(result, {
@@ -680,7 +707,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
680
707
  openWorldHint: true,
681
708
  },
682
709
  },
683
- async (input, extra) =>
710
+ async (input, ctx) =>
684
711
  safeToolResult(async () =>
685
712
  jsonResult(
686
713
  await checkApp({
@@ -689,7 +716,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
689
716
  preflightConnect: input.preflightConnect ?? false,
690
717
  connectTimeoutMs: input.connectTimeoutMs,
691
718
  scriptTimeoutMs: input.timeoutMs ?? defaultMcpCheckTimeoutMs,
692
- signal: extra.signal,
719
+ signal: ctx.mcpReq.signal,
693
720
  }),
694
721
  ),
695
722
  ),
@@ -727,7 +754,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
727
754
  openWorldHint: true,
728
755
  },
729
756
  },
730
- async (input, extra) =>
757
+ async (input, ctx) =>
731
758
  safeToolResult(async () =>
732
759
  jsonResult(
733
760
  await runDatabaseCommand({
@@ -735,7 +762,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
735
762
  cwd,
736
763
  captureOutput: true,
737
764
  maxOutputBytes: defaultDatabaseCommandMaxOutputBytes,
738
- signal: extra.signal,
765
+ signal: ctx.mcpReq.signal,
739
766
  timeoutMs: input.timeoutMs ?? defaultDatabaseCommandTimeoutMs,
740
767
  dryRun: input.dryRun,
741
768
  }),
@@ -766,14 +793,14 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
766
793
  openWorldHint: true,
767
794
  },
768
795
  },
769
- async (input, extra) =>
796
+ async (input, ctx) =>
770
797
  safeToolResult(async () =>
771
798
  jsonResult(
772
799
  await runDatabaseStatus({
773
800
  cwd,
774
801
  captureOutput: true,
775
802
  maxOutputBytes: defaultDatabaseCommandMaxOutputBytes,
776
- signal: extra.signal,
803
+ signal: ctx.mcpReq.signal,
777
804
  timeoutMs: input.timeoutMs ?? defaultDatabaseCommandTimeoutMs,
778
805
  }),
779
806
  ),
@@ -818,7 +845,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
818
845
  openWorldHint: false,
819
846
  },
820
847
  },
821
- async (input, extra) =>
848
+ async (input, ctx) =>
822
849
  safeToolResult(async () =>
823
850
  jsonResult(
824
851
  await syncDatabaseSchema({
@@ -827,7 +854,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
827
854
  tables: input.tables,
828
855
  output: input.output,
829
856
  dryRun: input.dryRun,
830
- signal: extra.signal,
857
+ signal: ctx.mcpReq.signal,
831
858
  }),
832
859
  ),
833
860
  ),
@@ -846,7 +873,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
846
873
  openWorldHint: true,
847
874
  },
848
875
  },
849
- async (input, extra) =>
876
+ async (input, ctx) =>
850
877
  safeToolResult(async () =>
851
878
  operationalJsonResult(
852
879
  await runIsolatedOperationalCommand({
@@ -858,7 +885,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
858
885
  tenant: input.tenant,
859
886
  modulePath: input.module,
860
887
  },
861
- signal: extra.signal,
888
+ signal: ctx.mcpReq.signal,
862
889
  timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
863
890
  }),
864
891
  ),
@@ -878,7 +905,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
878
905
  openWorldHint: true,
879
906
  },
880
907
  },
881
- async (input, extra) =>
908
+ async (input, ctx) =>
882
909
  safeToolResult(async () =>
883
910
  operationalJsonResult(
884
911
  await runIsolatedOperationalCommand({
@@ -894,7 +921,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
894
921
  source: input.source,
895
922
  modulePath: input.module,
896
923
  },
897
- signal: extra.signal,
924
+ signal: ctx.mcpReq.signal,
898
925
  timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
899
926
  }),
900
927
  ),
@@ -914,7 +941,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
914
941
  openWorldHint: true,
915
942
  },
916
943
  },
917
- async (input, extra) =>
944
+ async (input, ctx) =>
918
945
  safeToolResult(async () =>
919
946
  operationalJsonResult(
920
947
  await runIsolatedOperationalCommand({
@@ -934,7 +961,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
934
961
  id: input.id,
935
962
  modulePath: input.module,
936
963
  },
937
- signal: extra.signal,
964
+ signal: ctx.mcpReq.signal,
938
965
  timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
939
966
  }),
940
967
  ),
@@ -954,7 +981,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
954
981
  openWorldHint: true,
955
982
  },
956
983
  },
957
- async (input, extra) =>
984
+ async (input, ctx) =>
958
985
  safeToolResult(async () => {
959
986
  const request =
960
987
  input.operation === "drain"
@@ -991,7 +1018,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
991
1018
  await runIsolatedOperationalCommand({
992
1019
  cwd,
993
1020
  request,
994
- signal: extra.signal,
1021
+ signal: ctx.mcpReq.signal,
995
1022
  timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
996
1023
  }),
997
1024
  );
@@ -1169,7 +1196,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
1169
1196
  async function readAppGuidance(cwd: string): Promise<string> {
1170
1197
  const guidancePath = await resolveAppGuidancePath(cwd);
1171
1198
  if (!guidancePath) {
1172
- 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";
1199
+ return "# Beignet app guidance\n\nNo app-local AGENTS.md was found. Use app_map and explain before editing, prefer Beignet generators, use app_map with changed=true to inspect potential consumers after editing in Git, and then run check.\n";
1173
1200
  }
1174
1201
 
1175
1202
  let file: Awaited<ReturnType<typeof open>>;