@beignet/cli 0.0.50 → 0.0.51
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 +12 -0
- package/README.md +13 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -3
- package/dist/index.js.map +1 -1
- package/dist/inspect.js +122 -25
- package/dist/inspect.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +0 -2
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +22 -4
- package/dist/mcp.js.map +1 -1
- package/dist/operational-process.d.ts +1 -0
- package/dist/operational-process.d.ts.map +1 -1
- package/dist/operational-process.js.map +1 -1
- package/dist/operational-runner.js +1 -0
- package/dist/operational-runner.js.map +1 -1
- package/dist/outbox.d.ts +1 -0
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +58 -12
- package/dist/outbox.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +7 -2
- package/dist/templates/agents.js.map +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +5 -3
- package/src/index.ts +45 -3
- package/src/inspect.ts +164 -28
- package/src/make.ts +0 -2
- package/src/mcp.ts +33 -10
- package/src/operational-process.ts +1 -0
- package/src/operational-runner.ts +1 -0
- package/src/outbox.ts +63 -12
- package/src/templates/agents.ts +7 -2
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type CommandContext,
|
|
9
9
|
type FlagParametersForType,
|
|
10
10
|
proposeCompletions,
|
|
11
|
+
type RouteMap,
|
|
11
12
|
run,
|
|
12
13
|
type StricliDynamicCommandContext,
|
|
13
14
|
type StricliProcess,
|
|
@@ -180,6 +181,7 @@ type OutboxDrainFlags = {
|
|
|
180
181
|
json?: boolean;
|
|
181
182
|
module?: string;
|
|
182
183
|
batchSize?: number;
|
|
184
|
+
concurrency?: number;
|
|
183
185
|
cwd?: string;
|
|
184
186
|
};
|
|
185
187
|
|
|
@@ -486,7 +488,13 @@ const outboxDrainFlagParameters = {
|
|
|
486
488
|
kind: "parsed",
|
|
487
489
|
parse: parsePositiveInteger,
|
|
488
490
|
optional: true,
|
|
489
|
-
brief: "Maximum messages to
|
|
491
|
+
brief: "Maximum eligible messages to handle in one drain pass.",
|
|
492
|
+
},
|
|
493
|
+
concurrency: {
|
|
494
|
+
kind: "parsed",
|
|
495
|
+
parse: parsePositiveInteger,
|
|
496
|
+
optional: true,
|
|
497
|
+
brief: "Maximum concurrent deliveries; values above one are unordered.",
|
|
490
498
|
},
|
|
491
499
|
cwd: cwdFlag,
|
|
492
500
|
} satisfies FlagParametersForType<OutboxDrainFlags, CliContext>;
|
|
@@ -1597,6 +1605,7 @@ const outboxDrainCommand = buildCommand<OutboxDrainFlags, [], CliContext>({
|
|
|
1597
1605
|
cwd: flags.cwd,
|
|
1598
1606
|
modulePath: flags.module,
|
|
1599
1607
|
batchSize: flags.batchSize,
|
|
1608
|
+
concurrency: flags.concurrency,
|
|
1600
1609
|
});
|
|
1601
1610
|
|
|
1602
1611
|
writeOutput(
|
|
@@ -1605,6 +1614,9 @@ const outboxDrainCommand = buildCommand<OutboxDrainFlags, [], CliContext>({
|
|
|
1605
1614
|
? JSON.stringify(result, null, 2)
|
|
1606
1615
|
: outboxDrainNextSteps(result),
|
|
1607
1616
|
);
|
|
1617
|
+
if (result.result.settlementFailed > 0 || result.result.leaseLost > 0) {
|
|
1618
|
+
this.process.exitCode = 1;
|
|
1619
|
+
}
|
|
1608
1620
|
};
|
|
1609
1621
|
},
|
|
1610
1622
|
});
|
|
@@ -2448,6 +2460,27 @@ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
|
|
|
2448
2460
|
},
|
|
2449
2461
|
});
|
|
2450
2462
|
|
|
2463
|
+
const completionProposalCommandPath = ["completion", "propose"] as const;
|
|
2464
|
+
|
|
2465
|
+
function commandPathsFromRouteMap(
|
|
2466
|
+
routeMap: RouteMap<CliContext>,
|
|
2467
|
+
prefix: readonly string[] = [],
|
|
2468
|
+
): string[][] {
|
|
2469
|
+
return routeMap.getAllEntries().flatMap((entry) => {
|
|
2470
|
+
const path = [...prefix, entry.name["convert-camel-to-kebab"]];
|
|
2471
|
+
if ("getAllEntries" in entry.target) {
|
|
2472
|
+
return commandPathsFromRouteMap(entry.target, path);
|
|
2473
|
+
}
|
|
2474
|
+
return [path];
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
/** Every canonical CLI command path, derived from runtime routing definitions. */
|
|
2479
|
+
export const cliCommandPaths: readonly (readonly string[])[] = [
|
|
2480
|
+
...commandPathsFromRouteMap(rootRoutes),
|
|
2481
|
+
completionProposalCommandPath,
|
|
2482
|
+
].sort((left, right) => left.join(" ").localeCompare(right.join(" ")));
|
|
2483
|
+
|
|
2451
2484
|
const cli = buildApplication(rootRoutes, {
|
|
2452
2485
|
name: "beignet",
|
|
2453
2486
|
versionInfo: {
|
|
@@ -2541,6 +2574,9 @@ type OutboxDrainNextStepsResult = {
|
|
|
2541
2574
|
delivered: number;
|
|
2542
2575
|
retried: number;
|
|
2543
2576
|
deadLettered: number;
|
|
2577
|
+
abandonedDeadLettered: number;
|
|
2578
|
+
settlementFailed: number;
|
|
2579
|
+
leaseLost: number;
|
|
2544
2580
|
};
|
|
2545
2581
|
};
|
|
2546
2582
|
|
|
@@ -2608,7 +2644,10 @@ Result:
|
|
|
2608
2644
|
claimed: ${result.result.claimed}
|
|
2609
2645
|
delivered: ${result.result.delivered}
|
|
2610
2646
|
retried: ${result.result.retried}
|
|
2611
|
-
deadLettered: ${result.result.deadLettered}
|
|
2647
|
+
deadLettered: ${result.result.deadLettered}
|
|
2648
|
+
abandonedDeadLettered: ${result.result.abandonedDeadLettered}
|
|
2649
|
+
settlementFailed: ${result.result.settlementFailed}
|
|
2650
|
+
leaseLost: ${result.result.leaseLost}`;
|
|
2612
2651
|
}
|
|
2613
2652
|
|
|
2614
2653
|
function formatOutboxDate(value: Date | string | null | undefined): string {
|
|
@@ -3053,7 +3092,10 @@ export async function main(
|
|
|
3053
3092
|
// Shell completion scripts call `beignet completion propose <words...>`.
|
|
3054
3093
|
// Handle it before stricli parses the inputs: the trailing words are a
|
|
3055
3094
|
// partial command line, not flags for this CLI.
|
|
3056
|
-
if (
|
|
3095
|
+
if (
|
|
3096
|
+
inputs[0] === completionProposalCommandPath[0] &&
|
|
3097
|
+
inputs[1] === completionProposalCommandPath[1]
|
|
3098
|
+
) {
|
|
3057
3099
|
await writeCompletionProposals(inputs.slice(2), context);
|
|
3058
3100
|
return;
|
|
3059
3101
|
}
|
package/src/inspect.ts
CHANGED
|
@@ -1576,36 +1576,57 @@ function parseRouteExports(
|
|
|
1576
1576
|
config: ResolvedBeignetConfig,
|
|
1577
1577
|
): RouteExport[] {
|
|
1578
1578
|
const exports: RouteExport[] = [];
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1579
|
+
const sourceFile = ts.createSourceFile(
|
|
1580
|
+
handlerFile,
|
|
1581
|
+
source,
|
|
1582
|
+
ts.ScriptTarget.Latest,
|
|
1583
|
+
true,
|
|
1584
|
+
ts.ScriptKind.TS,
|
|
1585
|
+
);
|
|
1586
|
+
const imports = parseRouteNamedImports(sourceFile, config, handlerFile);
|
|
1584
1587
|
|
|
1585
|
-
for (const
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1588
|
+
for (const statement of sourceFile.statements) {
|
|
1589
|
+
if (
|
|
1590
|
+
!ts.isVariableStatement(statement) ||
|
|
1591
|
+
!statement.modifiers?.some(
|
|
1592
|
+
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
|
|
1593
|
+
)
|
|
1594
|
+
) {
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1590
1597
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1599
|
+
if (
|
|
1600
|
+
!declaration.initializer ||
|
|
1601
|
+
!ts.isObjectBindingPattern(declaration.name) ||
|
|
1602
|
+
!isNamedRouteFactoryCall(declaration.initializer, "createApiRoute")
|
|
1603
|
+
) {
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
for (const element of declaration.name.elements) {
|
|
1608
|
+
if (!ts.isIdentifier(element.name)) continue;
|
|
1609
|
+
const method = element.name.text;
|
|
1610
|
+
if (!isHttpMethod(method)) continue;
|
|
1611
|
+
exports.push({
|
|
1612
|
+
method,
|
|
1613
|
+
handlerFile,
|
|
1614
|
+
contractRef: routePath,
|
|
1615
|
+
catchAllPrefix: catchAllRoutePrefix(routePath),
|
|
1616
|
+
source: "next-route",
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1598
1619
|
}
|
|
1599
1620
|
}
|
|
1600
1621
|
|
|
1601
|
-
for (const
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1622
|
+
for (const { exportName, declaration } of exportedVariableDeclarations(
|
|
1623
|
+
sourceFile,
|
|
1624
|
+
)) {
|
|
1625
|
+
if (!isHttpMethod(exportName) || !declaration.initializer) continue;
|
|
1626
|
+
const method = exportName;
|
|
1627
|
+
const localName = routeLocalContractIdentifier(declaration.initializer);
|
|
1606
1628
|
|
|
1607
|
-
if (
|
|
1608
|
-
const localName = contractMatch[1];
|
|
1629
|
+
if (localName) {
|
|
1609
1630
|
const imported = imports.get(localName);
|
|
1610
1631
|
exports.push({
|
|
1611
1632
|
method,
|
|
@@ -1617,7 +1638,7 @@ function parseRouteExports(
|
|
|
1617
1638
|
continue;
|
|
1618
1639
|
}
|
|
1619
1640
|
|
|
1620
|
-
if (
|
|
1641
|
+
if (isServerApiExpression(declaration.initializer)) {
|
|
1621
1642
|
exports.push({
|
|
1622
1643
|
method,
|
|
1623
1644
|
handlerFile,
|
|
@@ -1631,6 +1652,98 @@ function parseRouteExports(
|
|
|
1631
1652
|
return exports;
|
|
1632
1653
|
}
|
|
1633
1654
|
|
|
1655
|
+
function parseRouteNamedImports(
|
|
1656
|
+
sourceFile: ts.SourceFile,
|
|
1657
|
+
config: ResolvedBeignetConfig,
|
|
1658
|
+
handlerFile: string,
|
|
1659
|
+
): Map<string, { importedName: string; contractFile?: string }> {
|
|
1660
|
+
const imports = new Map<
|
|
1661
|
+
string,
|
|
1662
|
+
{ importedName: string; contractFile?: string }
|
|
1663
|
+
>();
|
|
1664
|
+
|
|
1665
|
+
for (const statement of sourceFile.statements) {
|
|
1666
|
+
if (
|
|
1667
|
+
!ts.isImportDeclaration(statement) ||
|
|
1668
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
1669
|
+
statement.importClause?.isTypeOnly ||
|
|
1670
|
+
!statement.importClause?.namedBindings ||
|
|
1671
|
+
!ts.isNamedImports(statement.importClause.namedBindings)
|
|
1672
|
+
) {
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
const contractFile = contractFileFromImport(
|
|
1677
|
+
statement.moduleSpecifier.text,
|
|
1678
|
+
config,
|
|
1679
|
+
handlerFile,
|
|
1680
|
+
);
|
|
1681
|
+
for (const element of statement.importClause.namedBindings.elements) {
|
|
1682
|
+
if (element.isTypeOnly) continue;
|
|
1683
|
+
imports.set(element.name.text, {
|
|
1684
|
+
importedName: element.propertyName?.text ?? element.name.text,
|
|
1685
|
+
contractFile,
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
return imports;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function isNamedRouteFactoryCall(
|
|
1694
|
+
expression: ts.Expression,
|
|
1695
|
+
name: string,
|
|
1696
|
+
): boolean {
|
|
1697
|
+
const unwrapped = unwrapContractExpression(expression);
|
|
1698
|
+
if (!ts.isCallExpression(unwrapped)) return false;
|
|
1699
|
+
const callee = unwrapContractExpression(unwrapped.expression);
|
|
1700
|
+
return ts.isIdentifier(callee) && callee.text === name;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function routeLocalContractIdentifier(
|
|
1704
|
+
expression: ts.Expression,
|
|
1705
|
+
): string | undefined {
|
|
1706
|
+
const handleCall = unwrapContractExpression(expression);
|
|
1707
|
+
if (!ts.isCallExpression(handleCall)) return undefined;
|
|
1708
|
+
const handleAccess = unwrapContractExpression(handleCall.expression);
|
|
1709
|
+
if (
|
|
1710
|
+
!ts.isPropertyAccessExpression(handleAccess) ||
|
|
1711
|
+
handleAccess.name.text !== "handle"
|
|
1712
|
+
) {
|
|
1713
|
+
return undefined;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
const routeCall = unwrapContractExpression(handleAccess.expression);
|
|
1717
|
+
if (!ts.isCallExpression(routeCall)) return undefined;
|
|
1718
|
+
const routeAccess = unwrapContractExpression(routeCall.expression);
|
|
1719
|
+
if (
|
|
1720
|
+
!ts.isPropertyAccessExpression(routeAccess) ||
|
|
1721
|
+
routeAccess.name.text !== "route" ||
|
|
1722
|
+
!ts.isIdentifier(routeAccess.expression) ||
|
|
1723
|
+
routeAccess.expression.text !== "server"
|
|
1724
|
+
) {
|
|
1725
|
+
return undefined;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
const contract = routeCall.arguments[0];
|
|
1729
|
+
const unwrappedContract = contract
|
|
1730
|
+
? unwrapContractExpression(contract)
|
|
1731
|
+
: undefined;
|
|
1732
|
+
return unwrappedContract && ts.isIdentifier(unwrappedContract)
|
|
1733
|
+
? unwrappedContract.text
|
|
1734
|
+
: undefined;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function isServerApiExpression(expression: ts.Expression): boolean {
|
|
1738
|
+
const unwrapped = unwrapContractExpression(expression);
|
|
1739
|
+
return (
|
|
1740
|
+
ts.isPropertyAccessExpression(unwrapped) &&
|
|
1741
|
+
ts.isIdentifier(unwrapped.expression) &&
|
|
1742
|
+
unwrapped.expression.text === "server" &&
|
|
1743
|
+
unwrapped.name.text === "api"
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1634
1747
|
function catchAllRoutePrefix(routePath: string): string | undefined {
|
|
1635
1748
|
const segments = routePath.split("/").filter(Boolean);
|
|
1636
1749
|
const catchAllIndex = segments.findIndex((segment) => segment.endsWith("*"));
|
|
@@ -1762,6 +1875,29 @@ function contractFileFromImport(
|
|
|
1762
1875
|
config: ResolvedBeignetConfig,
|
|
1763
1876
|
importerFile?: string,
|
|
1764
1877
|
): string | undefined {
|
|
1878
|
+
const resolveCandidate = (candidate: string) => {
|
|
1879
|
+
const extension = path.extname(candidate).toLowerCase();
|
|
1880
|
+
if (!extension) return `${candidate}.ts`;
|
|
1881
|
+
|
|
1882
|
+
let sourceExtension: string | undefined;
|
|
1883
|
+
switch (extension) {
|
|
1884
|
+
case ".js":
|
|
1885
|
+
sourceExtension = ".ts";
|
|
1886
|
+
break;
|
|
1887
|
+
case ".jsx":
|
|
1888
|
+
sourceExtension = ".tsx";
|
|
1889
|
+
break;
|
|
1890
|
+
case ".mjs":
|
|
1891
|
+
sourceExtension = ".mts";
|
|
1892
|
+
break;
|
|
1893
|
+
case ".cjs":
|
|
1894
|
+
sourceExtension = ".cts";
|
|
1895
|
+
break;
|
|
1896
|
+
}
|
|
1897
|
+
return sourceExtension
|
|
1898
|
+
? `${candidate.slice(0, -extension.length)}${sourceExtension}`
|
|
1899
|
+
: candidate;
|
|
1900
|
+
};
|
|
1765
1901
|
const contractsPath = directoryPath(config.paths.contracts);
|
|
1766
1902
|
const aliasPrefix = `@/${contractsPath}/`;
|
|
1767
1903
|
const aliasExact = `@/${contractsPath}`;
|
|
@@ -1772,18 +1908,18 @@ function contractFileFromImport(
|
|
|
1772
1908
|
return `${contractsPath}/index.ts`;
|
|
1773
1909
|
}
|
|
1774
1910
|
if (sourcePath.startsWith(aliasPrefix)) {
|
|
1775
|
-
return
|
|
1911
|
+
return resolveCandidate(sourcePath.slice("@/".length));
|
|
1776
1912
|
}
|
|
1777
1913
|
|
|
1778
1914
|
if (sourcePath.startsWith(relativePrefix)) {
|
|
1779
|
-
return
|
|
1915
|
+
return resolveCandidate(sourcePath);
|
|
1780
1916
|
}
|
|
1781
1917
|
|
|
1782
1918
|
if (importerFile && sourcePath.startsWith(".")) {
|
|
1783
1919
|
const resolved = normalizePath(
|
|
1784
1920
|
path.join(path.dirname(importerFile), sourcePath),
|
|
1785
1921
|
);
|
|
1786
|
-
return
|
|
1922
|
+
return resolveCandidate(resolved);
|
|
1787
1923
|
}
|
|
1788
1924
|
|
|
1789
1925
|
return undefined;
|
package/src/make.ts
CHANGED
|
@@ -2225,7 +2225,6 @@ async function updateOutboxDrainProvider(
|
|
|
2225
2225
|
\t\t\tdefer: after,
|
|
2226
2226
|
\t\t\tcreateContext: () => createServiceContext(undefined),
|
|
2227
2227
|
\t\t\tregistry: async () => (await import("${outboxModule}")).outboxRegistry,
|
|
2228
|
-
\t\t\tbatchSize: 100,
|
|
2229
2228
|
\t\t});
|
|
2230
2229
|
|
|
2231
2230
|
\t\treturn {
|
|
@@ -5921,7 +5920,6 @@ export const { GET, POST } = createOutboxDrainRoute({
|
|
|
5921
5920
|
\tserver: getServer,
|
|
5922
5921
|
\tregistry: outboxRegistry,
|
|
5923
5922
|
\tsecret: env.CRON_SECRET,
|
|
5924
|
-
\tbatchSize: 100,
|
|
5925
5923
|
});
|
|
5926
5924
|
`;
|
|
5927
5925
|
}
|
package/src/mcp.ts
CHANGED
|
@@ -285,6 +285,7 @@ const outboxRunInputSchema = z.discriminatedUnion("operation", [
|
|
|
285
285
|
z.object({
|
|
286
286
|
operation: z.literal("drain"),
|
|
287
287
|
batchSize: z.number().int().positive().optional(),
|
|
288
|
+
concurrency: z.number().int().positive().optional(),
|
|
288
289
|
module: operationalModuleSchema,
|
|
289
290
|
timeoutMs: operationalTimeoutSchema,
|
|
290
291
|
}),
|
|
@@ -349,17 +350,36 @@ function jsonResult(value: unknown): McpToolResult {
|
|
|
349
350
|
};
|
|
350
351
|
}
|
|
351
352
|
|
|
352
|
-
function operationalJsonResult(
|
|
353
|
+
function operationalJsonResult(
|
|
354
|
+
value: unknown,
|
|
355
|
+
options: { isError?: boolean } = {},
|
|
356
|
+
): McpToolResult {
|
|
353
357
|
const formatted = JSON.stringify(value, null, 2);
|
|
354
358
|
const text =
|
|
355
359
|
Buffer.byteLength(formatted) <= defaultOperationalResultMaxBytes
|
|
356
360
|
? formatted
|
|
357
361
|
: JSON.stringify(value);
|
|
358
362
|
return {
|
|
363
|
+
...(options.isError ? { isError: true } : {}),
|
|
359
364
|
content: [{ type: "text", text }],
|
|
360
365
|
};
|
|
361
366
|
}
|
|
362
367
|
|
|
368
|
+
function isUncertainOutboxDrainReport(value: unknown): boolean {
|
|
369
|
+
if (typeof value !== "object" || value === null || !("result" in value)) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
const result = value.result;
|
|
373
|
+
if (typeof result !== "object" || result === null) return false;
|
|
374
|
+
const settlementFailed =
|
|
375
|
+
"settlementFailed" in result ? result.settlementFailed : undefined;
|
|
376
|
+
const leaseLost = "leaseLost" in result ? result.leaseLost : undefined;
|
|
377
|
+
return (
|
|
378
|
+
(typeof settlementFailed === "number" && settlementFailed > 0) ||
|
|
379
|
+
(typeof leaseLost === "number" && leaseLost > 0)
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
363
383
|
function errorResult(error: unknown): McpToolResult {
|
|
364
384
|
return {
|
|
365
385
|
isError: true,
|
|
@@ -972,7 +992,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
|
|
|
972
992
|
"outbox_run",
|
|
973
993
|
{
|
|
974
994
|
description:
|
|
975
|
-
"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.",
|
|
995
|
+
"Run a state-changing outbox drain, requeue, purge, or prune operation and return exactly the matching versioned beignet outbox <operation> --json report. Drain supports bounded unordered concurrency and returns an error-bearing report when settlement or lease state is uncertain. Purge and prune support dryRun. Runs in an isolated process with bounded structured output, cancellation, and a timeout.",
|
|
976
996
|
inputSchema: outboxRunInputSchema,
|
|
977
997
|
annotations: {
|
|
978
998
|
readOnlyHint: false,
|
|
@@ -988,6 +1008,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
|
|
|
988
1008
|
? {
|
|
989
1009
|
command: "outbox:drain" as const,
|
|
990
1010
|
batchSize: input.batchSize,
|
|
1011
|
+
concurrency: input.concurrency,
|
|
991
1012
|
modulePath: input.module,
|
|
992
1013
|
}
|
|
993
1014
|
: input.operation === "requeue"
|
|
@@ -1014,14 +1035,16 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
|
|
|
1014
1035
|
dryRun: input.dryRun,
|
|
1015
1036
|
modulePath: input.module,
|
|
1016
1037
|
};
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1038
|
+
const result = await runIsolatedOperationalCommand({
|
|
1039
|
+
cwd,
|
|
1040
|
+
request,
|
|
1041
|
+
signal: ctx.mcpReq.signal,
|
|
1042
|
+
timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
|
|
1043
|
+
});
|
|
1044
|
+
return operationalJsonResult(result, {
|
|
1045
|
+
isError:
|
|
1046
|
+
input.operation === "drain" && isUncertainOutboxDrainReport(result),
|
|
1047
|
+
});
|
|
1025
1048
|
}),
|
|
1026
1049
|
);
|
|
1027
1050
|
|
package/src/outbox.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
OutboxMessageStatus,
|
|
11
11
|
OutboxRegistry,
|
|
12
12
|
} from "@beignet/core/outbox";
|
|
13
|
+
import type { TracingPort } from "@beignet/core/tracing";
|
|
13
14
|
import { createJiti } from "jiti";
|
|
14
15
|
import { loadBeignetConfig, normalizePath } from "./config.js";
|
|
15
16
|
import {
|
|
@@ -26,6 +27,7 @@ export type RunOutboxDrainOptions = {
|
|
|
26
27
|
cwd?: string;
|
|
27
28
|
modulePath?: string;
|
|
28
29
|
batchSize?: number;
|
|
30
|
+
concurrency?: number;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -175,6 +177,7 @@ type OutboxDrainPorts = {
|
|
|
175
177
|
logger?: OutboxDrainLogger;
|
|
176
178
|
devtools?: OutboxDrainInstrumentation;
|
|
177
179
|
instrumentation?: OutboxDrainInstrumentation;
|
|
180
|
+
tracing?: TracingPort;
|
|
178
181
|
errorReporter?: ErrorReporterPort;
|
|
179
182
|
};
|
|
180
183
|
|
|
@@ -187,6 +190,7 @@ type OutboxDrainContext = {
|
|
|
187
190
|
type OutboxDrainContextArgs = {
|
|
188
191
|
registry: OutboxRegistry;
|
|
189
192
|
batchSize?: number;
|
|
193
|
+
concurrency?: number;
|
|
190
194
|
};
|
|
191
195
|
|
|
192
196
|
type OutboxAdminOperation = "list" | "show" | "requeue" | "purge" | "prune";
|
|
@@ -219,7 +223,11 @@ export async function runOutboxDrain(
|
|
|
219
223
|
const config = await loadBeignetConfig(cwd);
|
|
220
224
|
const modulePath = normalizePath(options.modulePath ?? config.paths.outbox);
|
|
221
225
|
const batchSize = options.batchSize;
|
|
226
|
+
const concurrency = options.concurrency;
|
|
222
227
|
if (batchSize !== undefined) assertPositiveInteger("batchSize", batchSize);
|
|
228
|
+
if (concurrency !== undefined) {
|
|
229
|
+
assertPositiveInteger("concurrency", concurrency);
|
|
230
|
+
}
|
|
223
231
|
|
|
224
232
|
const startedAt = performance.now();
|
|
225
233
|
const outboxModule = await loadOutboxModule(cwd, modulePath);
|
|
@@ -227,7 +235,7 @@ export async function runOutboxDrain(
|
|
|
227
235
|
outboxModule.outboxRegistry,
|
|
228
236
|
modulePath,
|
|
229
237
|
);
|
|
230
|
-
const contextArgs = { registry, batchSize };
|
|
238
|
+
const contextArgs = { registry, batchSize, concurrency };
|
|
231
239
|
const rawContext = outboxModule.createOutboxDrainContext
|
|
232
240
|
? await outboxModule.createOutboxDrainContext(contextArgs)
|
|
233
241
|
: undefined;
|
|
@@ -247,7 +255,11 @@ export async function runOutboxDrain(
|
|
|
247
255
|
eventBus: ctx.ports.eventBus,
|
|
248
256
|
jobs: ctx.ports.jobs,
|
|
249
257
|
batchSize,
|
|
250
|
-
|
|
258
|
+
concurrency,
|
|
259
|
+
instrumentation: {
|
|
260
|
+
instrumentation: ctx.ports.instrumentation ?? ctx.ports.devtools,
|
|
261
|
+
tracing: ctx.ports.tracing,
|
|
262
|
+
},
|
|
251
263
|
instrumentationContext: {
|
|
252
264
|
requestId: ctx.requestId,
|
|
253
265
|
traceId: ctx.traceId,
|
|
@@ -288,27 +300,65 @@ export async function runOutboxDrain(
|
|
|
288
300
|
},
|
|
289
301
|
});
|
|
290
302
|
},
|
|
291
|
-
|
|
303
|
+
onLeaseError(failure) {
|
|
304
|
+
const outcome =
|
|
305
|
+
failure.state === "lost"
|
|
306
|
+
? "leaseLost"
|
|
307
|
+
: failure.state === "recovered"
|
|
308
|
+
? "leaseRecovered"
|
|
309
|
+
: "leaseDegraded";
|
|
292
310
|
return reportOperationalFailure({
|
|
293
311
|
ctx,
|
|
294
|
-
error:
|
|
312
|
+
error: failure.error,
|
|
313
|
+
reportOptions: {
|
|
314
|
+
level: failure.state === "lost" ? "error" : "warning",
|
|
315
|
+
mechanism: "beignet.outbox.cli",
|
|
316
|
+
handled: failure.state !== "lost",
|
|
317
|
+
tags: {
|
|
318
|
+
"beignet.kind": "outbox",
|
|
319
|
+
"beignet.outbox.kind": failure.message.kind,
|
|
320
|
+
"beignet.outbox.name": failure.message.name,
|
|
321
|
+
"beignet.outbox.outcome": outcome,
|
|
322
|
+
},
|
|
323
|
+
contexts: {
|
|
324
|
+
outbox: {
|
|
325
|
+
messageId: failure.message.id,
|
|
326
|
+
kind: failure.message.kind,
|
|
327
|
+
name: failure.message.name,
|
|
328
|
+
attempts: failure.message.attempts,
|
|
329
|
+
maxAttempts: failure.message.maxAttempts,
|
|
330
|
+
operation: failure.operation,
|
|
331
|
+
state: failure.state,
|
|
332
|
+
confirmedLost: failure.confirmedLost,
|
|
333
|
+
outcome,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
},
|
|
339
|
+
onSettlementError(failure) {
|
|
340
|
+
return reportOperationalFailure({
|
|
341
|
+
ctx,
|
|
342
|
+
error: failure.error,
|
|
295
343
|
reportOptions: {
|
|
296
344
|
level: "error",
|
|
297
345
|
mechanism: "beignet.outbox.cli",
|
|
298
346
|
handled: false,
|
|
299
347
|
tags: {
|
|
300
348
|
"beignet.kind": "outbox",
|
|
301
|
-
"beignet.outbox.kind": message.kind,
|
|
302
|
-
"beignet.outbox.name": message.name,
|
|
349
|
+
"beignet.outbox.kind": failure.message.kind,
|
|
350
|
+
"beignet.outbox.name": failure.message.name,
|
|
303
351
|
"beignet.outbox.outcome": "settlementFailed",
|
|
304
352
|
},
|
|
305
353
|
contexts: {
|
|
306
354
|
outbox: {
|
|
307
|
-
messageId: message.id,
|
|
308
|
-
kind: message.kind,
|
|
309
|
-
name: message.name,
|
|
310
|
-
attempts: message.attempts,
|
|
311
|
-
maxAttempts: message.maxAttempts,
|
|
355
|
+
messageId: failure.message.id,
|
|
356
|
+
kind: failure.message.kind,
|
|
357
|
+
name: failure.message.name,
|
|
358
|
+
attempts: failure.message.attempts,
|
|
359
|
+
maxAttempts: failure.message.maxAttempts,
|
|
360
|
+
operation: failure.operation,
|
|
361
|
+
deliverySucceeded: failure.deliverySucceeded,
|
|
312
362
|
outcome: "settlementFailed",
|
|
313
363
|
},
|
|
314
364
|
},
|
|
@@ -620,6 +670,7 @@ function isOutboxPort(value: unknown): value is DrainOutboxOptions["outbox"] {
|
|
|
620
670
|
isRecord(value) &&
|
|
621
671
|
typeof value.enqueue === "function" &&
|
|
622
672
|
typeof value.claimBatch === "function" &&
|
|
673
|
+
typeof value.renewClaim === "function" &&
|
|
623
674
|
typeof value.markDelivered === "function" &&
|
|
624
675
|
typeof value.markFailed === "function"
|
|
625
676
|
);
|
|
@@ -677,7 +728,7 @@ async function recordOutboxDrain(
|
|
|
677
728
|
watcher: "outbox",
|
|
678
729
|
name: "outbox.drain",
|
|
679
730
|
label: "Outbox drain",
|
|
680
|
-
summary: `${result.delivered} delivered, ${result.retried} retried, ${result.deadLettered} dead-lettered`,
|
|
731
|
+
summary: `${result.delivered} delivered, ${result.retried} retried, ${result.deadLettered} dead-lettered, ${result.settlementFailed} settlement-uncertain, ${result.leaseLost} lease-lost`,
|
|
681
732
|
requestId: ctx.requestId,
|
|
682
733
|
traceId: ctx.traceId,
|
|
683
734
|
details: result,
|
package/src/templates/agents.ts
CHANGED
|
@@ -107,7 +107,9 @@ re-exports and \`db\` for \`generate\`, \`migrate\`, \`seed\`, or \`reset\`
|
|
|
107
107
|
instead of falling back to a shell. Use the read-only \`db_status\` tool before
|
|
108
108
|
deploying. Use \`task_run\`, \`schedule_run\`,
|
|
109
109
|
\`outbox_inspect\`, and \`outbox_run\` for registered operational workflows
|
|
110
|
-
and outbox recovery.
|
|
110
|
+
and outbox recovery. Treat a drain with nonzero \`settlementFailed\` or
|
|
111
|
+
\`leaseLost\` as an operational failure. Parallel delivery is opt-in and does
|
|
112
|
+
not preserve message order.
|
|
111
113
|
|
|
112
114
|
## The framework already solves these
|
|
113
115
|
|
|
@@ -242,7 +244,10 @@ and reports the script without executing it; it does not simulate SQL or data
|
|
|
242
244
|
changes. Use \`task_run\` and \`schedule_run\` for registered operational
|
|
243
245
|
workflows, \`outbox_inspect\` for read-only \`list\` and \`show\`, and
|
|
244
246
|
\`outbox_run\` for \`drain\`, \`requeue\`, \`purge\`, or \`prune\`; purge and
|
|
245
|
-
prune support \`dryRun\`.
|
|
247
|
+
prune support \`dryRun\`. A drain with nonzero \`settlementFailed\` or
|
|
248
|
+
\`leaseLost\` remains a complete JSON report but is an error result; inspect
|
|
249
|
+
storage health and clock synchronization before retrying. Operational commands
|
|
250
|
+
run in isolated process trees
|
|
246
251
|
with bounded results, cancellation, and timeouts. Optional \`module\` overrides
|
|
247
252
|
must remain inside the app root where the MCP server started. Cancellation and
|
|
248
253
|
timeouts cannot roll back side effects that already completed. Inspect app
|