lootforge 0.3.0
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 +87 -0
- package/README.md +764 -0
- package/bin/lootforge.js +28 -0
- package/dist/benchmarks/coarseToFineCost.d.ts +21 -0
- package/dist/benchmarks/coarseToFineCost.js +49 -0
- package/dist/benchmarks/coarseToFineCost.js.map +1 -0
- package/dist/checks/boundaryMetrics.d.ts +12 -0
- package/dist/checks/boundaryMetrics.js +102 -0
- package/dist/checks/boundaryMetrics.js.map +1 -0
- package/dist/checks/candidateScore.d.ts +11 -0
- package/dist/checks/candidateScore.js +462 -0
- package/dist/checks/candidateScore.js.map +1 -0
- package/dist/checks/commandParser.d.ts +5 -0
- package/dist/checks/commandParser.js +99 -0
- package/dist/checks/commandParser.js.map +1 -0
- package/dist/checks/consistencyOutliers.d.ts +42 -0
- package/dist/checks/consistencyOutliers.js +156 -0
- package/dist/checks/consistencyOutliers.js.map +1 -0
- package/dist/checks/imageAcceptance.d.ts +67 -0
- package/dist/checks/imageAcceptance.js +967 -0
- package/dist/checks/imageAcceptance.js.map +1 -0
- package/dist/checks/packInvariants.d.ts +56 -0
- package/dist/checks/packInvariants.js +1064 -0
- package/dist/checks/packInvariants.js.map +1 -0
- package/dist/checks/softAdapters.d.ts +25 -0
- package/dist/checks/softAdapters.js +275 -0
- package/dist/checks/softAdapters.js.map +1 -0
- package/dist/checks/vlmGate.d.ts +8 -0
- package/dist/checks/vlmGate.js +200 -0
- package/dist/checks/vlmGate.js.map +1 -0
- package/dist/cli/commands/atlas.d.ts +5 -0
- package/dist/cli/commands/atlas.js +18 -0
- package/dist/cli/commands/atlas.js.map +1 -0
- package/dist/cli/commands/eval.d.ts +6 -0
- package/dist/cli/commands/eval.js +23 -0
- package/dist/cli/commands/eval.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +18 -0
- package/dist/cli/commands/generate.js +66 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/init.d.ts +15 -0
- package/dist/cli/commands/init.js +146 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/package.d.ts +6 -0
- package/dist/cli/commands/package.js +27 -0
- package/dist/cli/commands/package.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +16 -0
- package/dist/cli/commands/plan.js +49 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/process.d.ts +14 -0
- package/dist/cli/commands/process.js +29 -0
- package/dist/cli/commands/process.js.map +1 -0
- package/dist/cli/commands/regenerate.d.ts +29 -0
- package/dist/cli/commands/regenerate.js +244 -0
- package/dist/cli/commands/regenerate.js.map +1 -0
- package/dist/cli/commands/review.d.ts +5 -0
- package/dist/cli/commands/review.js +18 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/commands/select.d.ts +6 -0
- package/dist/cli/commands/select.js +21 -0
- package/dist/cli/commands/select.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +16 -0
- package/dist/cli/commands/serve.js +100 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +17 -0
- package/dist/cli/commands/validate.js +108 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +157 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/parseArgs.d.ts +3 -0
- package/dist/cli/parseArgs.js +37 -0
- package/dist/cli/parseArgs.js.map +1 -0
- package/dist/contracts/stageArtifacts.d.ts +4031 -0
- package/dist/contracts/stageArtifacts.js +663 -0
- package/dist/contracts/stageArtifacts.js.map +1 -0
- package/dist/manifest/load.d.ts +3 -0
- package/dist/manifest/load.js +50 -0
- package/dist/manifest/load.js.map +1 -0
- package/dist/manifest/normalize-palette.d.ts +17 -0
- package/dist/manifest/normalize-palette.js +235 -0
- package/dist/manifest/normalize-palette.js.map +1 -0
- package/dist/manifest/normalize-policy.d.ts +48 -0
- package/dist/manifest/normalize-policy.js +239 -0
- package/dist/manifest/normalize-policy.js.map +1 -0
- package/dist/manifest/normalize-prompt.d.ts +14 -0
- package/dist/manifest/normalize-prompt.js +73 -0
- package/dist/manifest/normalize-prompt.js.map +1 -0
- package/dist/manifest/normalize-target.d.ts +49 -0
- package/dist/manifest/normalize-target.js +542 -0
- package/dist/manifest/normalize-target.js.map +1 -0
- package/dist/manifest/schema.d.ts +7570 -0
- package/dist/manifest/schema.js +373 -0
- package/dist/manifest/schema.js.map +1 -0
- package/dist/manifest/semantic-validation.d.ts +4 -0
- package/dist/manifest/semantic-validation.js +526 -0
- package/dist/manifest/semantic-validation.js.map +1 -0
- package/dist/manifest/types.d.ts +263 -0
- package/dist/manifest/types.js +2 -0
- package/dist/manifest/types.js.map +1 -0
- package/dist/manifest/validate.d.ts +12 -0
- package/dist/manifest/validate.js +221 -0
- package/dist/manifest/validate.js.map +1 -0
- package/dist/output/assetPackManifest.d.ts +19 -0
- package/dist/output/assetPackManifest.js +20 -0
- package/dist/output/assetPackManifest.js.map +1 -0
- package/dist/output/catalog.d.ts +60 -0
- package/dist/output/catalog.js +107 -0
- package/dist/output/catalog.js.map +1 -0
- package/dist/output/contactSheet.d.ts +13 -0
- package/dist/output/contactSheet.js +124 -0
- package/dist/output/contactSheet.js.map +1 -0
- package/dist/output/phaserManifest.d.ts +8 -0
- package/dist/output/phaserManifest.js +25 -0
- package/dist/output/phaserManifest.js.map +1 -0
- package/dist/output/pixiManifest.d.ts +8 -0
- package/dist/output/pixiManifest.js +37 -0
- package/dist/output/pixiManifest.js.map +1 -0
- package/dist/output/provenance.d.ts +121 -0
- package/dist/output/provenance.js +10 -0
- package/dist/output/provenance.js.map +1 -0
- package/dist/output/runtimeManifests.d.ts +21 -0
- package/dist/output/runtimeManifests.js +82 -0
- package/dist/output/runtimeManifests.js.map +1 -0
- package/dist/output/unityImportManifest.d.ts +10 -0
- package/dist/output/unityImportManifest.js +58 -0
- package/dist/output/unityImportManifest.js.map +1 -0
- package/dist/output/zip.d.ts +5 -0
- package/dist/output/zip.js +68 -0
- package/dist/output/zip.js.map +1 -0
- package/dist/pipeline/atlas.d.ts +33 -0
- package/dist/pipeline/atlas.js +286 -0
- package/dist/pipeline/atlas.js.map +1 -0
- package/dist/pipeline/eval.d.ts +104 -0
- package/dist/pipeline/eval.js +246 -0
- package/dist/pipeline/eval.js.map +1 -0
- package/dist/pipeline/generate.d.ts +44 -0
- package/dist/pipeline/generate.js +1088 -0
- package/dist/pipeline/generate.js.map +1 -0
- package/dist/pipeline/package.d.ts +18 -0
- package/dist/pipeline/package.js +218 -0
- package/dist/pipeline/package.js.map +1 -0
- package/dist/pipeline/process.d.ts +15 -0
- package/dist/pipeline/process.js +776 -0
- package/dist/pipeline/process.js.map +1 -0
- package/dist/pipeline/review.d.ts +10 -0
- package/dist/pipeline/review.js +341 -0
- package/dist/pipeline/review.js.map +1 -0
- package/dist/pipeline/seamHeal.d.ts +2 -0
- package/dist/pipeline/seamHeal.js +70 -0
- package/dist/pipeline/seamHeal.js.map +1 -0
- package/dist/pipeline/select.d.ts +39 -0
- package/dist/pipeline/select.js +79 -0
- package/dist/pipeline/select.js.map +1 -0
- package/dist/providers/job.d.ts +29 -0
- package/dist/providers/job.js +113 -0
- package/dist/providers/job.js.map +1 -0
- package/dist/providers/localDiffusion.d.ts +28 -0
- package/dist/providers/localDiffusion.js +235 -0
- package/dist/providers/localDiffusion.js.map +1 -0
- package/dist/providers/nano.d.ts +36 -0
- package/dist/providers/nano.js +402 -0
- package/dist/providers/nano.js.map +1 -0
- package/dist/providers/openai.d.ts +37 -0
- package/dist/providers/openai.js +378 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/policy.d.ts +9 -0
- package/dist/providers/policy.js +192 -0
- package/dist/providers/policy.js.map +1 -0
- package/dist/providers/prompt.d.ts +3 -0
- package/dist/providers/prompt.js +63 -0
- package/dist/providers/prompt.js.map +1 -0
- package/dist/providers/registry.d.ts +24 -0
- package/dist/providers/registry.js +92 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/runtime.d.ts +15 -0
- package/dist/providers/runtime.js +101 -0
- package/dist/providers/runtime.js.map +1 -0
- package/dist/providers/runtimeConfig.d.ts +20 -0
- package/dist/providers/runtimeConfig.js +146 -0
- package/dist/providers/runtimeConfig.js.map +1 -0
- package/dist/providers/types-core.d.ts +514 -0
- package/dist/providers/types-core.js +60 -0
- package/dist/providers/types-core.js.map +1 -0
- package/dist/providers/types.d.ts +4 -0
- package/dist/providers/types.js +5 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/service/generationRequest.d.ts +58 -0
- package/dist/service/generationRequest.js +203 -0
- package/dist/service/generationRequest.js.map +1 -0
- package/dist/service/providerCapabilities.d.ts +40 -0
- package/dist/service/providerCapabilities.js +114 -0
- package/dist/service/providerCapabilities.js.map +1 -0
- package/dist/service/server.d.ts +31 -0
- package/dist/service/server.js +774 -0
- package/dist/service/server.js.map +1 -0
- package/dist/shared/errors.d.ts +13 -0
- package/dist/shared/errors.js +24 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/fs.d.ts +6 -0
- package/dist/shared/fs.js +30 -0
- package/dist/shared/fs.js.map +1 -0
- package/dist/shared/image.d.ts +25 -0
- package/dist/shared/image.js +136 -0
- package/dist/shared/image.js.map +1 -0
- package/dist/shared/paths.d.ts +30 -0
- package/dist/shared/paths.js +103 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/schemas.d.ts +209 -0
- package/dist/shared/schemas.js +93 -0
- package/dist/shared/schemas.js.map +1 -0
- package/dist/shared/typeGuards.d.ts +1 -0
- package/dist/shared/typeGuards.js +4 -0
- package/dist/shared/typeGuards.js.map +1 -0
- package/dist/shared/zod.d.ts +1 -0
- package/dist/shared/zod.js +14 -0
- package/dist/shared/zod.js.map +1 -0
- package/dist/showcase/format.d.ts +9 -0
- package/dist/showcase/format.js +61 -0
- package/dist/showcase/format.js.map +1 -0
- package/dist/showcase/panelRenderer.d.ts +59 -0
- package/dist/showcase/panelRenderer.js +294 -0
- package/dist/showcase/panelRenderer.js.map +1 -0
- package/dist/showcase/releaseConfig.d.ts +233 -0
- package/dist/showcase/releaseConfig.js +75 -0
- package/dist/showcase/releaseConfig.js.map +1 -0
- package/dist/showcase/releaseEvidence.d.ts +25 -0
- package/dist/showcase/releaseEvidence.js +540 -0
- package/dist/showcase/releaseEvidence.js.map +1 -0
- package/dist/showcase/releaseEvidenceSchema.d.ts +1611 -0
- package/dist/showcase/releaseEvidenceSchema.js +165 -0
- package/dist/showcase/releaseEvidenceSchema.js.map +1 -0
- package/dist/showcase/scenarioRenderer.d.ts +19 -0
- package/dist/showcase/scenarioRenderer.js +488 -0
- package/dist/showcase/scenarioRenderer.js.map +1 -0
- package/docs/ADAPTER_CONTRACT.md +141 -0
- package/docs/ENGINE_TARGETING.md +86 -0
- package/docs/MANIFEST_POLICY_COVERAGE.md +130 -0
- package/docs/RELEASE_WORKFLOW.md +117 -0
- package/docs/ROADMAP.md +411 -0
- package/docs/ROADMAP_ISSUES.md +244 -0
- package/docs/SERVICE_MODE.md +137 -0
- package/docs/manifest-schema.md +254 -0
- package/package.json +70 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { CanonicalGenerationRequestError, getCanonicalGenerationRequestContract, runCanonicalGenerationRequest, } from "./generationRequest.js";
|
|
3
|
+
import { getProviderCapabilitiesContract, resolveProviderCapabilityDescriptors, } from "./providerCapabilities.js";
|
|
4
|
+
import { runAtlasCommand } from "../cli/commands/atlas.js";
|
|
5
|
+
import { runEvalCommand } from "../cli/commands/eval.js";
|
|
6
|
+
import { runGenerateCommand } from "../cli/commands/generate.js";
|
|
7
|
+
import { runInitCommand } from "../cli/commands/init.js";
|
|
8
|
+
import { runPackageCommand } from "../cli/commands/package.js";
|
|
9
|
+
import { runPlanCommand } from "../cli/commands/plan.js";
|
|
10
|
+
import { runProcessCommand } from "../cli/commands/process.js";
|
|
11
|
+
import { runRegenerateCommand } from "../cli/commands/regenerate.js";
|
|
12
|
+
import { runReviewCommand } from "../cli/commands/review.js";
|
|
13
|
+
import { runSelectCommand } from "../cli/commands/select.js";
|
|
14
|
+
import { runValidateCommand } from "../cli/commands/validate.js";
|
|
15
|
+
import { isProviderName } from "../providers/types.js";
|
|
16
|
+
import { CliError, getErrorExitCode, getErrorMessage } from "../shared/errors.js";
|
|
17
|
+
import { isRecord } from "../shared/typeGuards.js";
|
|
18
|
+
export const SERVICE_API_VERSION = "v1";
|
|
19
|
+
const API_PREFIX = `/${SERVICE_API_VERSION}`;
|
|
20
|
+
const MAX_REQUEST_BODY_BYTES = 1024 * 1024;
|
|
21
|
+
const DEFAULT_MAX_ACTIVE_JOBS = 2;
|
|
22
|
+
class ServiceRequestError extends Error {
|
|
23
|
+
status;
|
|
24
|
+
code;
|
|
25
|
+
constructor(message, options) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "ServiceRequestError";
|
|
28
|
+
this.status = options.status;
|
|
29
|
+
this.code = options.code;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const TOOLS_ALLOW_EMPTY_PARAMS_WITH_DEFAULT_OUT = new Set([
|
|
33
|
+
"init",
|
|
34
|
+
"generate",
|
|
35
|
+
"regenerate",
|
|
36
|
+
"process",
|
|
37
|
+
"atlas",
|
|
38
|
+
"eval",
|
|
39
|
+
"review",
|
|
40
|
+
"select",
|
|
41
|
+
"package",
|
|
42
|
+
]);
|
|
43
|
+
const SERVICE_TOOLS = [
|
|
44
|
+
{
|
|
45
|
+
name: "init",
|
|
46
|
+
description: "Scaffold assets/imagegen directories and default manifest.",
|
|
47
|
+
params: [stringParam("out", "out", "Output root for init scaffold.")],
|
|
48
|
+
run: runInitCommand,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "plan",
|
|
52
|
+
description: "Validate manifest and write targets/jobs outputs.",
|
|
53
|
+
params: [
|
|
54
|
+
stringParam("manifest", "manifest", "Manifest path."),
|
|
55
|
+
stringParam("out", "out", "Output directory."),
|
|
56
|
+
],
|
|
57
|
+
run: runPlanCommand,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "validate",
|
|
61
|
+
description: "Validate manifest and optionally run image acceptance checks.",
|
|
62
|
+
params: [
|
|
63
|
+
stringParam("manifest", "manifest", "Manifest path."),
|
|
64
|
+
stringParam("out", "out", "Output directory."),
|
|
65
|
+
booleanParam("strict", "strict", "Strict mode for validation failure behavior."),
|
|
66
|
+
booleanParam("checkImages", "check-images", "Run image acceptance checks in addition to manifest validation."),
|
|
67
|
+
stringParam("imagesDir", "images-dir", "Optional processed-images directory override."),
|
|
68
|
+
],
|
|
69
|
+
run: runValidateCommand,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "generate",
|
|
73
|
+
description: "Execute generation pipeline from targets index.",
|
|
74
|
+
params: [
|
|
75
|
+
stringParam("manifest", "manifest", "Optional manifest path."),
|
|
76
|
+
stringParam("out", "out", "Output directory."),
|
|
77
|
+
stringParam("index", "index", "Optional targets-index path override."),
|
|
78
|
+
stringParam("provider", "provider", "Provider selection (openai|nano|local|auto)."),
|
|
79
|
+
stringListParam("ids", "ids", "Optional target ID filter."),
|
|
80
|
+
stringParam("lock", "lock", "Selection lock path."),
|
|
81
|
+
booleanParam("skipLocked", "skip-locked", "Skip lock-approved targets when true."),
|
|
82
|
+
],
|
|
83
|
+
run: runGenerateCommand,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "regenerate",
|
|
87
|
+
description: "Regenerate lock-approved targets through the edit-capable flow.",
|
|
88
|
+
params: [
|
|
89
|
+
stringParam("out", "out", "Output directory."),
|
|
90
|
+
stringParam("index", "index", "Optional targets-index path override."),
|
|
91
|
+
stringParam("provider", "provider", "Provider selection (openai|nano|local|auto)."),
|
|
92
|
+
stringParam("lock", "lock", "Selection lock path."),
|
|
93
|
+
stringListParam("ids", "ids", "Optional target ID filter."),
|
|
94
|
+
booleanParam("edit", "edit", "Enable edit-first regenerate behavior."),
|
|
95
|
+
stringParam("instruction", "instruction", "Optional edit instruction override."),
|
|
96
|
+
booleanParam("preserveComposition", "preserve-composition", "Preserve framing/composition during regenerate edit requests."),
|
|
97
|
+
],
|
|
98
|
+
run: runRegenerateCommand,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "process",
|
|
102
|
+
description: "Post-process generated assets into runtime outputs.",
|
|
103
|
+
params: [
|
|
104
|
+
stringParam("out", "out", "Output directory."),
|
|
105
|
+
stringParam("index", "index", "Optional targets-index path override."),
|
|
106
|
+
booleanParam("strict", "strict", "Strict mode for process acceptance failures."),
|
|
107
|
+
],
|
|
108
|
+
run: runProcessCommand,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "atlas",
|
|
112
|
+
description: "Build atlas outputs and atlas manifest.",
|
|
113
|
+
params: [
|
|
114
|
+
stringParam("out", "out", "Output directory."),
|
|
115
|
+
stringParam("index", "index", "Optional targets-index path override."),
|
|
116
|
+
stringParam("manifest", "manifest", "Optional manifest path."),
|
|
117
|
+
],
|
|
118
|
+
run: runAtlasCommand,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "eval",
|
|
122
|
+
description: "Run hard/soft quality evaluation.",
|
|
123
|
+
params: [
|
|
124
|
+
stringParam("out", "out", "Output directory."),
|
|
125
|
+
stringParam("index", "index", "Optional targets-index path override."),
|
|
126
|
+
stringParam("imagesDir", "images-dir", "Optional processed-images directory override."),
|
|
127
|
+
stringParam("report", "report", "Eval report output path override."),
|
|
128
|
+
booleanParam("strict", "strict", "Strict mode for eval hard-gate failures."),
|
|
129
|
+
],
|
|
130
|
+
run: runEvalCommand,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "review",
|
|
134
|
+
description: "Render review HTML from eval report output.",
|
|
135
|
+
params: [
|
|
136
|
+
stringParam("out", "out", "Output directory."),
|
|
137
|
+
stringParam("eval", "eval", "Eval report path override."),
|
|
138
|
+
stringParam("html", "html", "Review HTML output path override."),
|
|
139
|
+
],
|
|
140
|
+
run: runReviewCommand,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "select",
|
|
144
|
+
description: "Create selection lock from eval + provenance artifacts.",
|
|
145
|
+
params: [
|
|
146
|
+
stringParam("out", "out", "Output directory."),
|
|
147
|
+
stringParam("eval", "eval", "Eval report path override."),
|
|
148
|
+
stringParam("provenance", "provenance", "Provenance run path override."),
|
|
149
|
+
stringParam("lock", "lock", "Selection lock output path override."),
|
|
150
|
+
],
|
|
151
|
+
run: runSelectCommand,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "package",
|
|
155
|
+
description: "Assemble distributable runtime pack artifacts.",
|
|
156
|
+
params: [
|
|
157
|
+
stringParam("out", "out", "Output directory."),
|
|
158
|
+
stringParam("manifest", "manifest", "Manifest path override."),
|
|
159
|
+
stringParam("index", "index", "Optional targets-index path override."),
|
|
160
|
+
booleanParam("strict", "strict", "Strict mode for package-time checks."),
|
|
161
|
+
stringListParam("runtimes", "runtimes", "Runtime targets (phaser,pixi,unity)."),
|
|
162
|
+
],
|
|
163
|
+
run: runPackageCommand,
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
const SERVICE_TOOL_BY_NAME = new Map(SERVICE_TOOLS.map((tool) => [tool.name, tool]));
|
|
167
|
+
export function getServiceToolDescriptors() {
|
|
168
|
+
return SERVICE_TOOLS.map((tool) => ({
|
|
169
|
+
name: tool.name,
|
|
170
|
+
description: tool.description,
|
|
171
|
+
endpoint: `${API_PREFIX}/tools/${tool.name}`,
|
|
172
|
+
alias: `${API_PREFIX}/${tool.name}`,
|
|
173
|
+
params: tool.params.map((param) => ({
|
|
174
|
+
key: param.key,
|
|
175
|
+
type: param.kind,
|
|
176
|
+
description: param.description,
|
|
177
|
+
})),
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
export async function startLootForgeService(options) {
|
|
181
|
+
const executionState = {
|
|
182
|
+
maxActiveJobs: normalizeMaxActiveJobs(options.maxActiveJobs ?? DEFAULT_MAX_ACTIVE_JOBS),
|
|
183
|
+
activeJobs: 0,
|
|
184
|
+
};
|
|
185
|
+
const server = createServer((req, res) => {
|
|
186
|
+
void handleRequest(req, res, options, executionState).catch((error) => {
|
|
187
|
+
console.error("Unhandled request error:", error);
|
|
188
|
+
if (!res.headersSent) {
|
|
189
|
+
res.statusCode = 500;
|
|
190
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
191
|
+
res.end(JSON.stringify({
|
|
192
|
+
ok: false,
|
|
193
|
+
apiVersion: SERVICE_API_VERSION,
|
|
194
|
+
error: { code: "internal_error", message: "Unexpected server error." },
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
await new Promise((resolve, reject) => {
|
|
200
|
+
server.once("error", reject);
|
|
201
|
+
server.listen(options.port, options.host, () => {
|
|
202
|
+
server.off("error", reject);
|
|
203
|
+
resolve();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
const address = server.address();
|
|
207
|
+
const boundPort = typeof address === "object" && address !== null ? address.port : options.port;
|
|
208
|
+
const baseUrlHost = options.host === "0.0.0.0" ? "127.0.0.1" : options.host;
|
|
209
|
+
const service = {
|
|
210
|
+
host: options.host,
|
|
211
|
+
port: boundPort,
|
|
212
|
+
baseUrl: `http://${baseUrlHost}:${boundPort}`,
|
|
213
|
+
maxActiveJobs: executionState.maxActiveJobs,
|
|
214
|
+
close: async () => {
|
|
215
|
+
await closeServer(server);
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
options.onListen?.(service);
|
|
219
|
+
return service;
|
|
220
|
+
}
|
|
221
|
+
async function handleRequest(req, res, options, executionState) {
|
|
222
|
+
const method = req.method ?? "GET";
|
|
223
|
+
const requestUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
224
|
+
const pathname = normalizePathname(requestUrl.pathname);
|
|
225
|
+
if (method === "OPTIONS") {
|
|
226
|
+
writeCorsHeaders(res);
|
|
227
|
+
writeSecurityHeaders(res);
|
|
228
|
+
res.statusCode = 204;
|
|
229
|
+
res.end();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (method === "GET" && pathname === "/") {
|
|
233
|
+
writeJson(res, 200, {
|
|
234
|
+
ok: true,
|
|
235
|
+
apiVersion: SERVICE_API_VERSION,
|
|
236
|
+
endpoints: {
|
|
237
|
+
health: `${API_PREFIX}/health`,
|
|
238
|
+
tools: `${API_PREFIX}/tools`,
|
|
239
|
+
execute: `${API_PREFIX}/tools/:name`,
|
|
240
|
+
aliases: `${API_PREFIX}/:name`,
|
|
241
|
+
generationRequest: `${API_PREFIX}/generation/requests`,
|
|
242
|
+
generationContract: `${API_PREFIX}/contracts/generation-request`,
|
|
243
|
+
providerCapabilities: `${API_PREFIX}/providers/capabilities`,
|
|
244
|
+
providerCapabilitiesContract: `${API_PREFIX}/contracts/provider-capabilities`,
|
|
245
|
+
},
|
|
246
|
+
noAuth: true,
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (method === "GET" && pathname === `${API_PREFIX}/health`) {
|
|
251
|
+
writeJson(res, 200, {
|
|
252
|
+
ok: true,
|
|
253
|
+
apiVersion: SERVICE_API_VERSION,
|
|
254
|
+
service: "lootforge",
|
|
255
|
+
noAuth: true,
|
|
256
|
+
now: new Date().toISOString(),
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (method === "GET" && pathname === `${API_PREFIX}/tools`) {
|
|
261
|
+
writeJson(res, 200, {
|
|
262
|
+
ok: true,
|
|
263
|
+
apiVersion: SERVICE_API_VERSION,
|
|
264
|
+
tools: getServiceToolDescriptors(),
|
|
265
|
+
contracts: {
|
|
266
|
+
generationRequest: getCanonicalGenerationRequestContract(),
|
|
267
|
+
providerCapabilities: getProviderCapabilitiesContract(),
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (method === "GET" && pathname === `${API_PREFIX}/contracts/generation-request`) {
|
|
273
|
+
writeJson(res, 200, {
|
|
274
|
+
ok: true,
|
|
275
|
+
apiVersion: SERVICE_API_VERSION,
|
|
276
|
+
contract: getCanonicalGenerationRequestContract(),
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (method === "GET" && pathname === `${API_PREFIX}/contracts/provider-capabilities`) {
|
|
281
|
+
writeJson(res, 200, {
|
|
282
|
+
ok: true,
|
|
283
|
+
apiVersion: SERVICE_API_VERSION,
|
|
284
|
+
contract: getProviderCapabilitiesContract(),
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (method === "GET" && pathname === `${API_PREFIX}/providers/capabilities`) {
|
|
289
|
+
const providerParamRaw = requestUrl.searchParams.get("provider");
|
|
290
|
+
const providerParam = typeof providerParamRaw === "string" && providerParamRaw.trim()
|
|
291
|
+
? providerParamRaw.trim().toLowerCase()
|
|
292
|
+
: undefined;
|
|
293
|
+
const modelParamRaw = requestUrl.searchParams.get("model");
|
|
294
|
+
const modelParam = typeof modelParamRaw === "string" && modelParamRaw.trim() ? modelParamRaw.trim() : undefined;
|
|
295
|
+
if (providerParam && !isProviderName(providerParam)) {
|
|
296
|
+
writeJson(res, 400, {
|
|
297
|
+
ok: false,
|
|
298
|
+
apiVersion: SERVICE_API_VERSION,
|
|
299
|
+
error: {
|
|
300
|
+
code: "invalid_query_parameter",
|
|
301
|
+
message: `Unknown provider "${providerParam}". Use openai|nano|local.`,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (modelParam && !providerParam) {
|
|
307
|
+
writeJson(res, 400, {
|
|
308
|
+
ok: false,
|
|
309
|
+
apiVersion: SERVICE_API_VERSION,
|
|
310
|
+
error: {
|
|
311
|
+
code: "invalid_query_parameter",
|
|
312
|
+
message: 'Query parameter "model" requires "provider".',
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const capabilities = resolveProviderCapabilityDescriptors({
|
|
318
|
+
...(providerParam ? { provider: providerParam } : {}),
|
|
319
|
+
...(modelParam ? { model: modelParam } : {}),
|
|
320
|
+
});
|
|
321
|
+
writeJson(res, 200, {
|
|
322
|
+
ok: true,
|
|
323
|
+
apiVersion: SERVICE_API_VERSION,
|
|
324
|
+
endpoint: `${API_PREFIX}/providers/capabilities`,
|
|
325
|
+
capabilities,
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (method === "POST" && pathname === `${API_PREFIX}/generation/requests`) {
|
|
330
|
+
if (!tryAcquireExecutionSlot(executionState)) {
|
|
331
|
+
writeServiceBusyResponse(res, {
|
|
332
|
+
operation: "generation_request",
|
|
333
|
+
}, executionState);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
try {
|
|
338
|
+
const body = await readJsonBody(req);
|
|
339
|
+
const result = await runCanonicalGenerationRequest(body, {
|
|
340
|
+
defaultOutDir: options.defaultOutDir,
|
|
341
|
+
});
|
|
342
|
+
writeJson(res, 200, {
|
|
343
|
+
ok: true,
|
|
344
|
+
apiVersion: SERVICE_API_VERSION,
|
|
345
|
+
operation: "generation_request",
|
|
346
|
+
result,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (error instanceof CanonicalGenerationRequestError) {
|
|
351
|
+
writeJson(res, error.status, {
|
|
352
|
+
ok: false,
|
|
353
|
+
apiVersion: SERVICE_API_VERSION,
|
|
354
|
+
operation: "generation_request",
|
|
355
|
+
error: {
|
|
356
|
+
code: error.code,
|
|
357
|
+
message: error.message,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const status = error instanceof Error ? 422 : 500;
|
|
363
|
+
if (status >= 500) {
|
|
364
|
+
console.error(getErrorMessage(error));
|
|
365
|
+
}
|
|
366
|
+
writeJson(res, status, {
|
|
367
|
+
ok: false,
|
|
368
|
+
apiVersion: SERVICE_API_VERSION,
|
|
369
|
+
operation: "generation_request",
|
|
370
|
+
error: {
|
|
371
|
+
code: resolveErrorCode(error),
|
|
372
|
+
message: sanitizeErrorMessage(getErrorMessage(error)),
|
|
373
|
+
exitCode: getErrorExitCode(error, 1),
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
releaseExecutionSlot(executionState);
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (method !== "POST") {
|
|
384
|
+
writeJson(res, 405, {
|
|
385
|
+
ok: false,
|
|
386
|
+
apiVersion: SERVICE_API_VERSION,
|
|
387
|
+
error: {
|
|
388
|
+
code: "method_not_allowed",
|
|
389
|
+
message: `Method ${method} is not allowed for ${pathname}.`,
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const toolName = resolveServiceToolName(pathname);
|
|
395
|
+
if (!toolName) {
|
|
396
|
+
writeJson(res, 404, {
|
|
397
|
+
ok: false,
|
|
398
|
+
apiVersion: SERVICE_API_VERSION,
|
|
399
|
+
error: {
|
|
400
|
+
code: "unknown_endpoint",
|
|
401
|
+
message: `No service endpoint registered for ${pathname}.`,
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const tool = SERVICE_TOOL_BY_NAME.get(toolName);
|
|
407
|
+
if (!tool) {
|
|
408
|
+
writeJson(res, 404, {
|
|
409
|
+
ok: false,
|
|
410
|
+
apiVersion: SERVICE_API_VERSION,
|
|
411
|
+
tool: toolName,
|
|
412
|
+
error: {
|
|
413
|
+
code: "unknown_tool",
|
|
414
|
+
message: `Tool "${toolName}" is not registered.`,
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (!tryAcquireExecutionSlot(executionState)) {
|
|
420
|
+
writeServiceBusyResponse(res, {
|
|
421
|
+
tool: tool.name,
|
|
422
|
+
}, executionState);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
try {
|
|
427
|
+
const body = await readJsonBody(req);
|
|
428
|
+
const payload = decodeToolExecutionPayload(body);
|
|
429
|
+
const argv = buildCommandArgv(tool, payload, options.defaultOutDir);
|
|
430
|
+
const result = await tool.run(argv);
|
|
431
|
+
writeJson(res, 200, {
|
|
432
|
+
ok: true,
|
|
433
|
+
apiVersion: SERVICE_API_VERSION,
|
|
434
|
+
tool: tool.name,
|
|
435
|
+
...(payload.requestId !== undefined ? { requestId: payload.requestId } : {}),
|
|
436
|
+
result,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
if (error instanceof ServiceRequestError) {
|
|
441
|
+
writeJson(res, error.status, {
|
|
442
|
+
ok: false,
|
|
443
|
+
apiVersion: SERVICE_API_VERSION,
|
|
444
|
+
tool: tool.name,
|
|
445
|
+
error: {
|
|
446
|
+
code: error.code,
|
|
447
|
+
message: error.message,
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const status = error instanceof Error ? 422 : 500;
|
|
453
|
+
if (status >= 500) {
|
|
454
|
+
console.error(getErrorMessage(error));
|
|
455
|
+
}
|
|
456
|
+
writeJson(res, status, {
|
|
457
|
+
ok: false,
|
|
458
|
+
apiVersion: SERVICE_API_VERSION,
|
|
459
|
+
tool: tool.name,
|
|
460
|
+
error: {
|
|
461
|
+
code: resolveErrorCode(error),
|
|
462
|
+
message: sanitizeErrorMessage(getErrorMessage(error)),
|
|
463
|
+
exitCode: getErrorExitCode(error, 1),
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
finally {
|
|
469
|
+
releaseExecutionSlot(executionState);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function readJsonBody(req) {
|
|
473
|
+
const chunks = [];
|
|
474
|
+
let totalBytes = 0;
|
|
475
|
+
for await (const chunk of req) {
|
|
476
|
+
const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
477
|
+
totalBytes += bufferChunk.length;
|
|
478
|
+
if (totalBytes > MAX_REQUEST_BODY_BYTES) {
|
|
479
|
+
throw new ServiceRequestError(`Request body exceeds ${MAX_REQUEST_BODY_BYTES} bytes.`, {
|
|
480
|
+
status: 413,
|
|
481
|
+
code: "request_too_large",
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
chunks.push(bufferChunk);
|
|
485
|
+
}
|
|
486
|
+
if (chunks.length === 0) {
|
|
487
|
+
return {};
|
|
488
|
+
}
|
|
489
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
490
|
+
if (!raw) {
|
|
491
|
+
return {};
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
return JSON.parse(raw);
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
throw new ServiceRequestError(`Invalid JSON body: ${getErrorMessage(error)}`, {
|
|
498
|
+
status: 400,
|
|
499
|
+
code: "invalid_json",
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function decodeToolExecutionPayload(value) {
|
|
504
|
+
if (value === undefined || value === null) {
|
|
505
|
+
throw new ServiceRequestError("Tool request body is required.", {
|
|
506
|
+
status: 400,
|
|
507
|
+
code: "invalid_request_body",
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (!isRecord(value)) {
|
|
511
|
+
throw new ServiceRequestError("Request body must be a JSON object.", {
|
|
512
|
+
status: 400,
|
|
513
|
+
code: "invalid_request_body",
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const requestIdValue = value.requestId;
|
|
517
|
+
const requestId = typeof requestIdValue === "string" || typeof requestIdValue === "number"
|
|
518
|
+
? requestIdValue
|
|
519
|
+
: undefined;
|
|
520
|
+
const hasArgs = Object.prototype.hasOwnProperty.call(value, "args");
|
|
521
|
+
const hasParams = Object.prototype.hasOwnProperty.call(value, "params");
|
|
522
|
+
if (hasArgs && hasParams) {
|
|
523
|
+
throw new ServiceRequestError('Provide either "params" or "args", not both.', {
|
|
524
|
+
status: 400,
|
|
525
|
+
code: "invalid_request_body",
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
const argvValue = value.args;
|
|
529
|
+
if (hasArgs) {
|
|
530
|
+
if (!Array.isArray(argvValue) || argvValue.some((item) => typeof item !== "string")) {
|
|
531
|
+
throw new ServiceRequestError('Field "args" must be an array of strings.', {
|
|
532
|
+
status: 400,
|
|
533
|
+
code: "invalid_args_override",
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
if (argvValue.length === 0) {
|
|
537
|
+
throw new ServiceRequestError('Field "args" must include at least one CLI argument.', {
|
|
538
|
+
status: 400,
|
|
539
|
+
code: "invalid_args_override",
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
requestId,
|
|
544
|
+
params: {},
|
|
545
|
+
argvOverride: [...argvValue],
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
const paramsValue = hasParams ? value.params : stripMetaFields(value, new Set(["requestId"]));
|
|
549
|
+
if (paramsValue === undefined || paramsValue === null) {
|
|
550
|
+
throw new ServiceRequestError('Tool request body must include "params" or "args".', {
|
|
551
|
+
status: 400,
|
|
552
|
+
code: "invalid_request_body",
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (!isRecord(paramsValue)) {
|
|
556
|
+
throw new ServiceRequestError('Field "params" must be an object.', {
|
|
557
|
+
status: 400,
|
|
558
|
+
code: "invalid_params",
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
requestId,
|
|
563
|
+
params: paramsValue,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function buildCommandArgv(tool, payload, defaultOutDir) {
|
|
567
|
+
if (payload.argvOverride) {
|
|
568
|
+
return payload.argvOverride;
|
|
569
|
+
}
|
|
570
|
+
const knownKeys = new Set(tool.params.map((param) => param.key));
|
|
571
|
+
for (const key of Object.keys(payload.params)) {
|
|
572
|
+
if (!knownKeys.has(key)) {
|
|
573
|
+
throw new ServiceRequestError(`Unknown parameter "${key}" for tool "${tool.name}".`, {
|
|
574
|
+
status: 400,
|
|
575
|
+
code: "unknown_parameter",
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (Object.keys(payload.params).length === 0 &&
|
|
580
|
+
!(defaultOutDir && TOOLS_ALLOW_EMPTY_PARAMS_WITH_DEFAULT_OUT.has(tool.name))) {
|
|
581
|
+
throw new ServiceRequestError('Tool request body must include "params" with at least one field.', { status: 400, code: "invalid_request_body" });
|
|
582
|
+
}
|
|
583
|
+
const argv = [];
|
|
584
|
+
for (const param of tool.params) {
|
|
585
|
+
const resolvedValue = payload.params[param.key] !== undefined
|
|
586
|
+
? payload.params[param.key]
|
|
587
|
+
: param.key === "out" && defaultOutDir
|
|
588
|
+
? defaultOutDir
|
|
589
|
+
: undefined;
|
|
590
|
+
if (resolvedValue === undefined) {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const normalized = normalizeParamValue(tool.name, param, resolvedValue);
|
|
594
|
+
if (normalized === undefined) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
argv.push(`--${param.flag}`, normalized);
|
|
598
|
+
}
|
|
599
|
+
return argv;
|
|
600
|
+
}
|
|
601
|
+
function normalizeParamValue(toolName, param, value) {
|
|
602
|
+
if (param.kind === "string") {
|
|
603
|
+
if (typeof value !== "string") {
|
|
604
|
+
throw new ServiceRequestError(`Parameter "${param.key}" for tool "${toolName}" must be a string.`, { status: 400, code: "invalid_parameter_type" });
|
|
605
|
+
}
|
|
606
|
+
const trimmed = value.trim();
|
|
607
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
608
|
+
}
|
|
609
|
+
if (param.kind === "boolean") {
|
|
610
|
+
if (typeof value === "boolean") {
|
|
611
|
+
return value ? "true" : "false";
|
|
612
|
+
}
|
|
613
|
+
if (typeof value === "string") {
|
|
614
|
+
const normalized = value.trim().toLowerCase();
|
|
615
|
+
if (["true", "1", "yes", "y"].includes(normalized)) {
|
|
616
|
+
return "true";
|
|
617
|
+
}
|
|
618
|
+
if (["false", "0", "no", "n"].includes(normalized)) {
|
|
619
|
+
return "false";
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
throw new ServiceRequestError(`Parameter "${param.key}" for tool "${toolName}" must be a boolean.`, { status: 400, code: "invalid_parameter_type" });
|
|
623
|
+
}
|
|
624
|
+
if (Array.isArray(value)) {
|
|
625
|
+
const normalizedValues = value
|
|
626
|
+
.map((item) => {
|
|
627
|
+
if (typeof item !== "string") {
|
|
628
|
+
throw new ServiceRequestError(`Parameter "${param.key}" for tool "${toolName}" must be a string array.`, { status: 400, code: "invalid_parameter_type" });
|
|
629
|
+
}
|
|
630
|
+
return item.trim();
|
|
631
|
+
})
|
|
632
|
+
.filter(Boolean);
|
|
633
|
+
return normalizedValues.length > 0 ? normalizedValues.join(",") : undefined;
|
|
634
|
+
}
|
|
635
|
+
if (typeof value === "string") {
|
|
636
|
+
const trimmed = value.trim();
|
|
637
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
638
|
+
}
|
|
639
|
+
throw new ServiceRequestError(`Parameter "${param.key}" for tool "${toolName}" must be a string list.`, { status: 400, code: "invalid_parameter_type" });
|
|
640
|
+
}
|
|
641
|
+
function normalizePathname(pathname) {
|
|
642
|
+
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
643
|
+
return pathname.slice(0, -1);
|
|
644
|
+
}
|
|
645
|
+
return pathname;
|
|
646
|
+
}
|
|
647
|
+
function resolveServiceToolName(pathname) {
|
|
648
|
+
const toolPrefix = `${API_PREFIX}/tools/`;
|
|
649
|
+
if (pathname.startsWith(toolPrefix)) {
|
|
650
|
+
const candidate = pathname.slice(toolPrefix.length);
|
|
651
|
+
return isServiceToolName(candidate) ? candidate : undefined;
|
|
652
|
+
}
|
|
653
|
+
const commandPrefix = `${API_PREFIX}/`;
|
|
654
|
+
if (pathname.startsWith(commandPrefix)) {
|
|
655
|
+
const candidate = pathname.slice(commandPrefix.length);
|
|
656
|
+
if (candidate === "health" || candidate === "tools") {
|
|
657
|
+
return undefined;
|
|
658
|
+
}
|
|
659
|
+
return isServiceToolName(candidate) ? candidate : undefined;
|
|
660
|
+
}
|
|
661
|
+
return undefined;
|
|
662
|
+
}
|
|
663
|
+
function isServiceToolName(value) {
|
|
664
|
+
return SERVICE_TOOL_BY_NAME.has(value);
|
|
665
|
+
}
|
|
666
|
+
function writeSecurityHeaders(res) {
|
|
667
|
+
res.setHeader("x-content-type-options", "nosniff");
|
|
668
|
+
res.setHeader("x-frame-options", "DENY");
|
|
669
|
+
res.setHeader("content-security-policy", "default-src 'none'");
|
|
670
|
+
res.setHeader("cache-control", "no-store");
|
|
671
|
+
}
|
|
672
|
+
function sanitizeErrorMessage(message) {
|
|
673
|
+
return message.replace(/(?:\/(?:Users|home|var|tmp|opt|etc|usr|private)\/|[A-Z]:\\\\)[^\s"']+/g, "[path]");
|
|
674
|
+
}
|
|
675
|
+
function writeJson(res, status, payload) {
|
|
676
|
+
writeCorsHeaders(res);
|
|
677
|
+
writeSecurityHeaders(res);
|
|
678
|
+
res.statusCode = status;
|
|
679
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
680
|
+
res.end(`${JSON.stringify(payload, null, 2)}\n`);
|
|
681
|
+
}
|
|
682
|
+
function writeCorsHeaders(res) {
|
|
683
|
+
// Wildcard origin is intentional: this is a local dev tool with no auth.
|
|
684
|
+
res.setHeader("access-control-allow-origin", "*");
|
|
685
|
+
res.setHeader("access-control-allow-methods", "GET,POST,OPTIONS");
|
|
686
|
+
res.setHeader("access-control-allow-headers", "content-type");
|
|
687
|
+
}
|
|
688
|
+
function resolveErrorCode(error) {
|
|
689
|
+
if (error instanceof CliError) {
|
|
690
|
+
return error.code;
|
|
691
|
+
}
|
|
692
|
+
if (error instanceof Error && typeof error.code === "string") {
|
|
693
|
+
return String(error.code);
|
|
694
|
+
}
|
|
695
|
+
return "tool_execution_failed";
|
|
696
|
+
}
|
|
697
|
+
function normalizeMaxActiveJobs(value) {
|
|
698
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
699
|
+
throw new Error(`Invalid maxActiveJobs value "${String(value)}". Use an integer >= 1.`);
|
|
700
|
+
}
|
|
701
|
+
return value;
|
|
702
|
+
}
|
|
703
|
+
function tryAcquireExecutionSlot(executionState) {
|
|
704
|
+
if (executionState.activeJobs >= executionState.maxActiveJobs) {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
executionState.activeJobs += 1;
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
function releaseExecutionSlot(executionState) {
|
|
711
|
+
if (executionState.activeJobs <= 0) {
|
|
712
|
+
executionState.activeJobs = 0;
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
executionState.activeJobs -= 1;
|
|
716
|
+
}
|
|
717
|
+
function writeServiceBusyResponse(res, context, executionState) {
|
|
718
|
+
writeJson(res, 429, {
|
|
719
|
+
ok: false,
|
|
720
|
+
apiVersion: SERVICE_API_VERSION,
|
|
721
|
+
...context,
|
|
722
|
+
error: {
|
|
723
|
+
code: "service_busy",
|
|
724
|
+
message: `Service is busy (${executionState.activeJobs}/${executionState.maxActiveJobs} active jobs). Retry later.`,
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
async function closeServer(server) {
|
|
729
|
+
await new Promise((resolve, reject) => {
|
|
730
|
+
server.close((error) => {
|
|
731
|
+
if (error) {
|
|
732
|
+
reject(error);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
resolve();
|
|
736
|
+
});
|
|
737
|
+
server.closeAllConnections();
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
function stripMetaFields(value, keys) {
|
|
741
|
+
const stripped = {};
|
|
742
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
743
|
+
if (keys.has(key)) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
stripped[key] = entryValue;
|
|
747
|
+
}
|
|
748
|
+
return stripped;
|
|
749
|
+
}
|
|
750
|
+
function stringParam(key, flag, description) {
|
|
751
|
+
return {
|
|
752
|
+
key,
|
|
753
|
+
flag,
|
|
754
|
+
kind: "string",
|
|
755
|
+
description,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function booleanParam(key, flag, description) {
|
|
759
|
+
return {
|
|
760
|
+
key,
|
|
761
|
+
flag,
|
|
762
|
+
kind: "boolean",
|
|
763
|
+
description,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function stringListParam(key, flag, description) {
|
|
767
|
+
return {
|
|
768
|
+
key,
|
|
769
|
+
flag,
|
|
770
|
+
kind: "string_list",
|
|
771
|
+
description,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
//# sourceMappingURL=server.js.map
|