@stackwright-pro/mcp 0.2.0-alpha.5 → 0.2.0-alpha.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.
- package/LICENSE +21 -0
- package/dist/integrity.d.mts +29 -1
- package/dist/integrity.d.ts +29 -1
- package/dist/integrity.js +49 -10
- package/dist/integrity.js.map +1 -1
- package/dist/integrity.mjs +48 -10
- package/dist/integrity.mjs.map +1 -1
- package/dist/server.js +1450 -285
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +1468 -287
- package/dist/server.mjs.map +1 -1
- package/dist/tools/type-schemas.d.mts +51 -0
- package/dist/tools/type-schemas.d.ts +51 -0
- package/dist/tools/type-schemas.js +120 -0
- package/dist/tools/type-schemas.js.map +1 -0
- package/dist/tools/type-schemas.mjs +94 -0
- package/dist/tools/type-schemas.mjs.map +1 -0
- package/package.json +17 -8
package/dist/server.mjs
CHANGED
|
@@ -3,15 +3,44 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
|
|
5
5
|
// src/tools/data-explorer.ts
|
|
6
|
-
import { z } from "zod";
|
|
6
|
+
import { z as z2 } from "zod";
|
|
7
7
|
import { listEntities, generateFilter } from "@stackwright-pro/cli-data-explorer";
|
|
8
|
+
|
|
9
|
+
// src/coerce.ts
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
function jsonCoerce(schema) {
|
|
12
|
+
return z.preprocess((v) => {
|
|
13
|
+
if (typeof v === "string") {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(v);
|
|
16
|
+
} catch {
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return v;
|
|
21
|
+
}, schema);
|
|
22
|
+
}
|
|
23
|
+
function boolCoerce(schema) {
|
|
24
|
+
return z.preprocess((v) => v === "true" ? true : v === "false" ? false : v, schema);
|
|
25
|
+
}
|
|
26
|
+
function numCoerce(schema) {
|
|
27
|
+
return z.preprocess((v) => {
|
|
28
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
29
|
+
const n = Number(v);
|
|
30
|
+
if (!Number.isNaN(n)) return n;
|
|
31
|
+
}
|
|
32
|
+
return v;
|
|
33
|
+
}, schema);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/tools/data-explorer.ts
|
|
8
37
|
function registerDataExplorerTools(server2) {
|
|
9
38
|
server2.tool(
|
|
10
39
|
"stackwright_pro_list_entities",
|
|
11
40
|
"List all available API entities from OpenAPI specs or generated Zod schemas. Use this to discover what entities are available before generating endpoint filters. Returns entity names, endpoints, and field counts. Part of the Pro Otter Raft for building API-integrated Stackwright applications.",
|
|
12
41
|
{
|
|
13
|
-
specPath:
|
|
14
|
-
projectRoot:
|
|
42
|
+
specPath: z2.string().optional().describe("Path to OpenAPI spec file (YAML or JSON)"),
|
|
43
|
+
projectRoot: z2.string().optional().describe("Project root directory (auto-detected if omitted)")
|
|
15
44
|
},
|
|
16
45
|
async ({ specPath, projectRoot }) => {
|
|
17
46
|
const result = listEntities({
|
|
@@ -59,9 +88,13 @@ function registerDataExplorerTools(server2) {
|
|
|
59
88
|
"stackwright_pro_generate_filter",
|
|
60
89
|
"Generate endpoint filter configuration from selected entities. Creates include/exclude patterns for stackwright.yml OpenAPI integration. Use this after stackwright_pro_list_entities to select which API endpoints the application needs. Only selected endpoints will generate client code, reducing bundle size and improving security.",
|
|
61
90
|
{
|
|
62
|
-
selectedEntities:
|
|
63
|
-
|
|
64
|
-
|
|
91
|
+
selectedEntities: jsonCoerce(z2.array(z2.string())).describe(
|
|
92
|
+
'Entity slugs to include (e.g., ["equipment", "supplies"])'
|
|
93
|
+
),
|
|
94
|
+
excludePatterns: jsonCoerce(z2.array(z2.string()).optional()).describe(
|
|
95
|
+
'Glob patterns to exclude (e.g., ["/admin/**", "/reports/**"])'
|
|
96
|
+
),
|
|
97
|
+
projectRoot: z2.string().optional().describe("Project root directory")
|
|
65
98
|
},
|
|
66
99
|
async ({ selectedEntities, excludePatterns, projectRoot }) => {
|
|
67
100
|
const result = generateFilter({
|
|
@@ -117,7 +150,7 @@ function registerDataExplorerTools(server2) {
|
|
|
117
150
|
}
|
|
118
151
|
|
|
119
152
|
// src/tools/security.ts
|
|
120
|
-
import { z as
|
|
153
|
+
import { z as z3 } from "zod";
|
|
121
154
|
import { createHash } from "crypto";
|
|
122
155
|
import fs from "fs";
|
|
123
156
|
import path from "path";
|
|
@@ -126,8 +159,8 @@ function registerSecurityTools(server2) {
|
|
|
126
159
|
"stackwright_pro_validate_spec",
|
|
127
160
|
"Validate an OpenAPI spec against the enterprise approved-specs configuration. Checks if the spec URL is on the allowlist and verifies SHA-256 hash integrity. Use this in enterprise environments where only pre-approved API specs are allowed. Fails build if spec is not approved or has been modified.",
|
|
128
161
|
{
|
|
129
|
-
specPath:
|
|
130
|
-
configPath:
|
|
162
|
+
specPath: z3.string().describe("URL or file path to the OpenAPI spec to validate"),
|
|
163
|
+
configPath: z3.string().optional().describe("Path to stackwright.yml with prebuild.security config")
|
|
131
164
|
},
|
|
132
165
|
async ({ specPath, configPath }) => {
|
|
133
166
|
let securityEnabled = false;
|
|
@@ -235,16 +268,15 @@ Status: Valid (${allowlist.length} specs on allowlist)`
|
|
|
235
268
|
"stackwright_pro_add_approved_spec",
|
|
236
269
|
"Add an OpenAPI spec to the approved-specs allowlist in stackwright.yml. Computes the SHA-256 hash of the spec and adds it to the security configuration. Use this when onboarding a new API in enterprise environments.",
|
|
237
270
|
{
|
|
238
|
-
name:
|
|
239
|
-
url:
|
|
240
|
-
configPath:
|
|
271
|
+
name: z3.string().describe("Human-readable name for the spec"),
|
|
272
|
+
url: z3.string().describe("URL or file path to the OpenAPI spec"),
|
|
273
|
+
configPath: z3.string().optional().describe("Path to stackwright.yml")
|
|
241
274
|
},
|
|
242
275
|
async ({ name, url, configPath }) => {
|
|
243
276
|
const configFile = configPath || path.join(process.cwd(), "stackwright.yml");
|
|
244
277
|
let sha256 = "<computed-at-build>";
|
|
245
278
|
try {
|
|
246
279
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
247
|
-
sha256 = "<computed-at-build>";
|
|
248
280
|
} else if (fs.existsSync(url)) {
|
|
249
281
|
const specContent = fs.readFileSync(url, "utf8");
|
|
250
282
|
sha256 = createHash("sha256").update(specContent).digest("hex");
|
|
@@ -291,7 +323,7 @@ SHA-256 computed: ${sha256}`
|
|
|
291
323
|
"stackwright_pro_list_approved_specs",
|
|
292
324
|
"List all specs currently on the approved-specs allowlist. Shows spec names, URLs, and hash prefixes. Use this to audit what APIs are approved in the project.",
|
|
293
325
|
{
|
|
294
|
-
configPath:
|
|
326
|
+
configPath: z3.string().optional().describe("Path to stackwright.yml")
|
|
295
327
|
},
|
|
296
328
|
async ({ configPath }) => {
|
|
297
329
|
const configFile = configPath || path.join(process.cwd(), "stackwright.yml");
|
|
@@ -360,16 +392,18 @@ SHA-256 computed: ${sha256}`
|
|
|
360
392
|
}
|
|
361
393
|
|
|
362
394
|
// src/tools/isr.ts
|
|
363
|
-
import { z as
|
|
395
|
+
import { z as z4 } from "zod";
|
|
364
396
|
function registerIsrTools(server2) {
|
|
365
397
|
server2.tool(
|
|
366
398
|
"stackwright_pro_configure_isr",
|
|
367
399
|
"Configure Incremental Static Regeneration (ISR) for an API-backed collection. ISR allows API data to be cached and refreshed on a schedule, providing real-time data with the performance of static generation. Use this after stackwright_pro_generate_filter to set revalidation intervals.",
|
|
368
400
|
{
|
|
369
|
-
collection:
|
|
370
|
-
revalidateSeconds:
|
|
371
|
-
|
|
372
|
-
|
|
401
|
+
collection: z4.string().describe('Collection name (e.g., "equipment", "supplies")'),
|
|
402
|
+
revalidateSeconds: numCoerce(z4.number().optional()).describe(
|
|
403
|
+
"Revalidation interval in seconds (default: 60)"
|
|
404
|
+
),
|
|
405
|
+
fallback: z4.enum(["blocking", "true", "false"]).optional().describe("Fallback behavior for new pages"),
|
|
406
|
+
configPath: z4.string().optional().describe("Path to stackwright.yml")
|
|
373
407
|
},
|
|
374
408
|
async ({ collection, revalidateSeconds = 60, fallback = "blocking", configPath }) => {
|
|
375
409
|
const revalidate = revalidateSeconds;
|
|
@@ -380,7 +414,7 @@ function registerIsrTools(server2) {
|
|
|
380
414
|
isr:
|
|
381
415
|
revalidate: ${revalidate}
|
|
382
416
|
fallback: ${fallback}`;
|
|
383
|
-
let description
|
|
417
|
+
let description;
|
|
384
418
|
if (revalidate < 60) {
|
|
385
419
|
description = "\u26A1 Very fresh data (revalidate every " + revalidate + "s)";
|
|
386
420
|
} else if (revalidate < 300) {
|
|
@@ -412,14 +446,16 @@ ${yamlSnippet}
|
|
|
412
446
|
"stackwright_pro_configure_isr_batch",
|
|
413
447
|
"Configure ISR for multiple collections at once. More efficient than calling stackwright_pro_configure_isr multiple times. Provide different revalidation intervals based on data freshness requirements.",
|
|
414
448
|
{
|
|
415
|
-
collections:
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
449
|
+
collections: jsonCoerce(
|
|
450
|
+
z4.array(
|
|
451
|
+
z4.object({
|
|
452
|
+
name: z4.string().describe("Collection name"),
|
|
453
|
+
revalidateSeconds: numCoerce(z4.number().optional()).describe("Revalidation interval")
|
|
454
|
+
})
|
|
455
|
+
)
|
|
420
456
|
).describe("Array of collection configurations"),
|
|
421
|
-
defaultFallback:
|
|
422
|
-
configPath:
|
|
457
|
+
defaultFallback: z4.enum(["blocking", "true", "false"]).optional().describe("Default fallback behavior"),
|
|
458
|
+
configPath: z4.string().optional().describe("Path to stackwright.yml")
|
|
423
459
|
},
|
|
424
460
|
async ({ collections, defaultFallback = "blocking", configPath }) => {
|
|
425
461
|
const lines = [`\u2699\uFE0F Batch ISR Configuration:
|
|
@@ -447,17 +483,17 @@ Fallback: ${defaultFallback}`);
|
|
|
447
483
|
}
|
|
448
484
|
|
|
449
485
|
// src/tools/dashboard.ts
|
|
450
|
-
import { z as
|
|
486
|
+
import { z as z5 } from "zod";
|
|
451
487
|
import { listEntities as listEntities2 } from "@stackwright-pro/cli-data-explorer";
|
|
452
488
|
function registerDashboardTools(server2) {
|
|
453
489
|
server2.tool(
|
|
454
490
|
"stackwright_pro_generate_dashboard",
|
|
455
491
|
"Generate a dashboard page configuration for displaying API data. Creates YAML content for a Stackwright page with grid, metric_card, data_table, and collection_list content types. Use this after stackwright_pro_generate_filter to create pages for your API collections.",
|
|
456
492
|
{
|
|
457
|
-
entities:
|
|
458
|
-
layout:
|
|
459
|
-
pageTitle:
|
|
460
|
-
specPath:
|
|
493
|
+
entities: jsonCoerce(z5.array(z5.string())).describe("Entity slugs to include in dashboard"),
|
|
494
|
+
layout: z5.enum(["grid", "table", "mixed"]).optional().describe("Dashboard layout style"),
|
|
495
|
+
pageTitle: z5.string().optional().describe("Page title"),
|
|
496
|
+
specPath: z5.string().optional().describe("Path to OpenAPI spec for entity details")
|
|
461
497
|
},
|
|
462
498
|
async ({ entities, layout = "mixed", pageTitle, specPath }) => {
|
|
463
499
|
let entityDetails = [];
|
|
@@ -543,9 +579,9 @@ ${yaml}
|
|
|
543
579
|
"stackwright_pro_generate_detail_page",
|
|
544
580
|
"Generate a detail view page for a single API entity. Creates YAML content for displaying all fields of an individual record. Use this to create detail pages that complement collection listing pages.",
|
|
545
581
|
{
|
|
546
|
-
entity:
|
|
547
|
-
slugField:
|
|
548
|
-
specPath:
|
|
582
|
+
entity: z5.string().describe('Entity slug (e.g., "equipment")'),
|
|
583
|
+
slugField: z5.string().optional().describe('Field to use as URL slug (default: "id")'),
|
|
584
|
+
specPath: z5.string().optional().describe("Path to OpenAPI spec for field details")
|
|
549
585
|
},
|
|
550
586
|
async ({ entity, slugField = "id", specPath }) => {
|
|
551
587
|
let fields = [];
|
|
@@ -621,7 +657,7 @@ ${yaml}
|
|
|
621
657
|
}
|
|
622
658
|
|
|
623
659
|
// src/tools/clarification.ts
|
|
624
|
-
import { z as
|
|
660
|
+
import { z as z6 } from "zod";
|
|
625
661
|
var CONTRADICTION_PATTERNS = [
|
|
626
662
|
{
|
|
627
663
|
keywords: ["minimal", "clean", "simple"],
|
|
@@ -709,12 +745,14 @@ function registerClarificationTools(server2) {
|
|
|
709
745
|
"stackwright_pro_clarify",
|
|
710
746
|
"Ask the user for clarification when a specialist otter encounters ambiguity. This is for MID-EXECUTION questions, NOT upfront question collection (use the Question Manifest Protocol for that). Returns a structured response for the foreman to present to the user via ask_user_question (closed_choice) or directly (open_text).",
|
|
711
747
|
{
|
|
712
|
-
context:
|
|
713
|
-
question_type:
|
|
714
|
-
question:
|
|
715
|
-
choices:
|
|
716
|
-
|
|
717
|
-
|
|
748
|
+
context: z6.string().optional().describe("Context about what the otter is trying to do"),
|
|
749
|
+
question_type: z6.enum(["closed_choice", "open_text"]).describe("Type of question being asked"),
|
|
750
|
+
question: z6.string().describe("The clarification question to ask the user"),
|
|
751
|
+
choices: jsonCoerce(z6.array(z6.string()).optional()).describe(
|
|
752
|
+
"Options for closed_choice questions"
|
|
753
|
+
),
|
|
754
|
+
priority: z6.enum(["blocking", "preferred", "optional"]).optional().default("preferred").describe("How critical is this clarification? Default: preferred"),
|
|
755
|
+
target_field: z6.string().optional().describe("What field/config does this clarify?")
|
|
718
756
|
},
|
|
719
757
|
async ({ context, question_type, question, choices, priority, target_field }) => {
|
|
720
758
|
const result = handleClarify({
|
|
@@ -743,8 +781,10 @@ function registerClarificationTools(server2) {
|
|
|
743
781
|
"stackwright_pro_detect_conflict",
|
|
744
782
|
"Detect when a user's stated preference conflicts with their selected choices. Uses keyword heuristics against known contradiction patterns (minimal vs vibrant, dark vs light, etc). Returns conflict details and resolution options.",
|
|
745
783
|
{
|
|
746
|
-
stated_preference:
|
|
747
|
-
selected_values:
|
|
784
|
+
stated_preference: z6.string().describe("What the user said they wanted"),
|
|
785
|
+
selected_values: jsonCoerce(z6.record(z6.string(), z6.string())).describe(
|
|
786
|
+
"What the user actually selected (field \u2192 value)"
|
|
787
|
+
)
|
|
748
788
|
},
|
|
749
789
|
async ({ stated_preference, selected_values }) => {
|
|
750
790
|
const result = handleDetectConflict({ stated_preference, selected_values });
|
|
@@ -789,7 +829,7 @@ ${result.resolution_options?.map((o) => ` \u2022 ${o}`).join("\n")}
|
|
|
789
829
|
}
|
|
790
830
|
|
|
791
831
|
// src/tools/packages.ts
|
|
792
|
-
import { z as
|
|
832
|
+
import { z as z7 } from "zod";
|
|
793
833
|
import { readFileSync, writeFileSync, existsSync, realpathSync, lstatSync } from "fs";
|
|
794
834
|
import { execSync } from "child_process";
|
|
795
835
|
import path2 from "path";
|
|
@@ -810,17 +850,23 @@ function registerPackageTools(server2) {
|
|
|
810
850
|
"Ensures pro packages are present in a project's package.json. Safe to call multiple times \u2014 never overwrites existing version pins. Use this to bootstrap dependencies before specialist otters run. Pass includeBaseline: true to automatically include all required @stackwright-pro/* baseline dependencies. Safe to call on existing projects \u2014 never overwrites pinned versions.",
|
|
811
851
|
{
|
|
812
852
|
// FIX 3 (B-new-1): Zod v4 requires two-arg z.record(keySchema, valueSchema)
|
|
813
|
-
packages:
|
|
814
|
-
'Dependencies to add. Record<packageName, version>. e.g. { "@stackwright-pro/auth": "latest" }'
|
|
853
|
+
packages: jsonCoerce(z7.record(z7.string(), z7.string()).optional().default({})).describe(
|
|
854
|
+
'Dependencies to add. Record<packageName, version>. e.g. { "@stackwright-pro/auth": "latest" }. Omit or pass {} when using includeBaseline: true.'
|
|
855
|
+
),
|
|
856
|
+
devPackages: jsonCoerce(z7.record(z7.string(), z7.string()).optional()).describe(
|
|
857
|
+
"devDependencies to add. Same format as packages."
|
|
858
|
+
),
|
|
859
|
+
scripts: jsonCoerce(z7.record(z7.string(), z7.string()).optional()).describe(
|
|
860
|
+
"npm scripts to add. Only adds if key does not already exist."
|
|
815
861
|
),
|
|
816
|
-
|
|
817
|
-
scripts: z6.record(z6.string(), z6.string()).optional().describe("npm scripts to add. Only adds if key does not already exist."),
|
|
818
|
-
targetDir: z6.string().optional().describe(
|
|
862
|
+
targetDir: z7.string().optional().describe(
|
|
819
863
|
"Project directory containing package.json. Defaults to process.cwd(). Must be an absolute path within the current working directory."
|
|
820
864
|
),
|
|
821
|
-
runInstall:
|
|
822
|
-
|
|
823
|
-
|
|
865
|
+
runInstall: boolCoerce(z7.boolean().optional().default(true)).describe(
|
|
866
|
+
"Run pnpm install after writing package.json. Defaults to true. Pass boolean true/false."
|
|
867
|
+
),
|
|
868
|
+
includeBaseline: boolCoerce(z7.boolean().optional().default(false)).describe(
|
|
869
|
+
"When true, automatically merges BASELINE_DEPS and BASELINE_DEV_DEPS before applying packages/devPackages args. Safe to call on existing projects \u2014 never overwrites pinned versions. Pass boolean true/false."
|
|
824
870
|
)
|
|
825
871
|
},
|
|
826
872
|
async ({ packages, devPackages, scripts, targetDir, runInstall, includeBaseline }) => {
|
|
@@ -978,11 +1024,11 @@ function setupPackages(opts) {
|
|
|
978
1024
|
}
|
|
979
1025
|
}
|
|
980
1026
|
const raw = readFileSync(realPackageJsonPath, "utf8");
|
|
981
|
-
const PackageJsonSchema =
|
|
1027
|
+
const PackageJsonSchema = z7.object({
|
|
982
1028
|
// Zod v4: z.record(keySchema, valueSchema) — two-arg form required
|
|
983
|
-
dependencies:
|
|
984
|
-
devDependencies:
|
|
985
|
-
scripts:
|
|
1029
|
+
dependencies: z7.record(z7.string(), z7.string()).optional(),
|
|
1030
|
+
devDependencies: z7.record(z7.string(), z7.string()).optional(),
|
|
1031
|
+
scripts: z7.record(z7.string(), z7.string()).optional()
|
|
986
1032
|
}).passthrough();
|
|
987
1033
|
const schemaResult = PackageJsonSchema.safeParse(JSON.parse(raw));
|
|
988
1034
|
if (!schemaResult.success) {
|
|
@@ -1063,9 +1109,10 @@ function setupPackages(opts) {
|
|
|
1063
1109
|
}
|
|
1064
1110
|
|
|
1065
1111
|
// src/tools/questions.ts
|
|
1066
|
-
import { readFile } from "fs/promises";
|
|
1112
|
+
import { readFile, writeFile } from "fs/promises";
|
|
1113
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, mkdirSync, renameSync } from "fs";
|
|
1067
1114
|
import { join } from "path";
|
|
1068
|
-
import { z as
|
|
1115
|
+
import { z as z8 } from "zod";
|
|
1069
1116
|
|
|
1070
1117
|
// src/question-adapter.ts
|
|
1071
1118
|
function truncate(str, maxLength) {
|
|
@@ -1251,22 +1298,22 @@ function answersToManifestFormat(answers, questions) {
|
|
|
1251
1298
|
}
|
|
1252
1299
|
|
|
1253
1300
|
// src/tools/questions.ts
|
|
1254
|
-
var ManifestQuestionSchema =
|
|
1255
|
-
id:
|
|
1256
|
-
question:
|
|
1257
|
-
type:
|
|
1258
|
-
required:
|
|
1259
|
-
options:
|
|
1260
|
-
|
|
1261
|
-
label:
|
|
1262
|
-
value:
|
|
1301
|
+
var ManifestQuestionSchema = z8.object({
|
|
1302
|
+
id: z8.string(),
|
|
1303
|
+
question: z8.string(),
|
|
1304
|
+
type: z8.enum(["text", "select", "multi-select", "confirm"]),
|
|
1305
|
+
required: z8.boolean().optional(),
|
|
1306
|
+
options: z8.array(
|
|
1307
|
+
z8.object({
|
|
1308
|
+
label: z8.string(),
|
|
1309
|
+
value: z8.string()
|
|
1263
1310
|
})
|
|
1264
1311
|
).optional(),
|
|
1265
|
-
default:
|
|
1266
|
-
help:
|
|
1267
|
-
dependsOn:
|
|
1268
|
-
questionId:
|
|
1269
|
-
value:
|
|
1312
|
+
default: z8.union([z8.string(), z8.boolean(), z8.array(z8.string())]).optional(),
|
|
1313
|
+
help: z8.string().optional(),
|
|
1314
|
+
dependsOn: z8.object({
|
|
1315
|
+
questionId: z8.string(),
|
|
1316
|
+
value: z8.union([z8.string(), z8.array(z8.string())])
|
|
1270
1317
|
}).optional()
|
|
1271
1318
|
});
|
|
1272
1319
|
function registerQuestionTools(server2) {
|
|
@@ -1274,11 +1321,13 @@ function registerQuestionTools(server2) {
|
|
|
1274
1321
|
"stackwright_pro_present_phase_questions",
|
|
1275
1322
|
"Adapt manifest-format questions from a specialist otter and present them to the user via ask_user_question. Pass only the phase name \u2014 this tool reads questions from .stackwright/question-manifest.json automatically. The questions parameter is optional and only needed if the manifest has not been written yet. Use this instead of calling ask_user_question directly \u2014 it handles label truncation (50-char limit), header generation, confirm/text defaults, and correct array formatting automatically. IMPORTANT: This is the ONLY approved way to prepare questions before calling ask_user_question. Never call ask_user_question with raw manifest questions. Never retry ask_user_question validation errors by calling it directly \u2014 always re-call this tool.",
|
|
1276
1323
|
{
|
|
1277
|
-
phase:
|
|
1278
|
-
questions:
|
|
1324
|
+
phase: z8.string().describe('Phase name for display context, e.g. "designer", "api", "auth"'),
|
|
1325
|
+
questions: jsonCoerce(z8.array(ManifestQuestionSchema).optional()).describe(
|
|
1279
1326
|
"Questions in Question Manifest format. If omitted, questions are read from .stackwright/question-manifest.json using the phase name."
|
|
1280
1327
|
),
|
|
1281
|
-
answers:
|
|
1328
|
+
answers: jsonCoerce(
|
|
1329
|
+
z8.record(z8.string(), z8.union([z8.string(), z8.array(z8.string()), z8.boolean()])).optional()
|
|
1330
|
+
).describe("Previously collected answers used to resolve dependsOn conditions")
|
|
1282
1331
|
},
|
|
1283
1332
|
async ({ phase, questions, answers }) => {
|
|
1284
1333
|
let resolvedQuestions;
|
|
@@ -1329,17 +1378,36 @@ function registerQuestionTools(server2) {
|
|
|
1329
1378
|
}
|
|
1330
1379
|
}
|
|
1331
1380
|
const adapted = adaptQuestions(resolvedQuestions, answers ?? {});
|
|
1381
|
+
const labelSchema = z8.string().max(50, "Value should have at most 50 characters");
|
|
1382
|
+
for (const q of adapted) {
|
|
1383
|
+
for (const opt of q.options) {
|
|
1384
|
+
const check = labelSchema.safeParse(opt.label);
|
|
1385
|
+
if (!check.success) {
|
|
1386
|
+
return {
|
|
1387
|
+
content: [
|
|
1388
|
+
{
|
|
1389
|
+
type: "text",
|
|
1390
|
+
text: JSON.stringify({
|
|
1391
|
+
error: `Option label for phase "${phase}" exceeds 50 characters: ${check.error.issues[0]?.message ?? "label too long"}. Truncate option labels to \u226450 characters before calling this tool.`
|
|
1392
|
+
})
|
|
1393
|
+
}
|
|
1394
|
+
],
|
|
1395
|
+
isError: true
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1332
1400
|
if (adapted.length === 0) {
|
|
1333
1401
|
return {
|
|
1334
1402
|
content: [
|
|
1335
1403
|
{
|
|
1336
1404
|
type: "text",
|
|
1337
|
-
text:
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1405
|
+
text: `Phase "${phase}" has no questions to present. Do NOT call ask_user_question. Call stackwright_pro_save_phase_answers({ phase: "${phase}", rawAnswers: [] }) directly, then proceed to the execution step for this phase.`
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
type: "text",
|
|
1409
|
+
// Empty array — second block always present so the foreman's two-block contract holds
|
|
1410
|
+
text: JSON.stringify([])
|
|
1343
1411
|
}
|
|
1344
1412
|
],
|
|
1345
1413
|
isError: false
|
|
@@ -1360,11 +1428,149 @@ function registerQuestionTools(server2) {
|
|
|
1360
1428
|
};
|
|
1361
1429
|
}
|
|
1362
1430
|
);
|
|
1431
|
+
server2.tool(
|
|
1432
|
+
"stackwright_pro_get_next_question",
|
|
1433
|
+
"Returns the next unanswered question for a phase as a plain JSON object \u2014 one question at a time. Returns { done: true } when all questions are answered or the phase has no questions. Call this in a loop: present the question in plain chat, get user reply, call record_answer, repeat.",
|
|
1434
|
+
{ phase: z8.string().describe('Phase name e.g. "designer", "api", "auth"') },
|
|
1435
|
+
async ({ phase }) => {
|
|
1436
|
+
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
1437
|
+
if (!SAFE_PHASE.test(phase)) {
|
|
1438
|
+
return {
|
|
1439
|
+
content: [
|
|
1440
|
+
{
|
|
1441
|
+
type: "text",
|
|
1442
|
+
text: JSON.stringify({ error: `Invalid phase name: "${phase.slice(0, 50)}"` })
|
|
1443
|
+
}
|
|
1444
|
+
],
|
|
1445
|
+
isError: true
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
const cwd = process.cwd();
|
|
1449
|
+
const questionsPath = join(cwd, ".stackwright", "questions", `${phase}.json`);
|
|
1450
|
+
let questions = [];
|
|
1451
|
+
try {
|
|
1452
|
+
const raw = await readFile(questionsPath, "utf-8");
|
|
1453
|
+
const parsed = JSON.parse(raw);
|
|
1454
|
+
questions = parsed.questions ?? [];
|
|
1455
|
+
} catch {
|
|
1456
|
+
return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
|
|
1457
|
+
}
|
|
1458
|
+
if (questions.length === 0) {
|
|
1459
|
+
return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
|
|
1460
|
+
}
|
|
1461
|
+
const answersPath = join(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
1462
|
+
let answeredIds = /* @__PURE__ */ new Set();
|
|
1463
|
+
try {
|
|
1464
|
+
const raw = await readFile(answersPath, "utf-8");
|
|
1465
|
+
const parsed = JSON.parse(raw);
|
|
1466
|
+
answeredIds = new Set(Object.keys(parsed.answers ?? {}));
|
|
1467
|
+
} catch {
|
|
1468
|
+
}
|
|
1469
|
+
const next = questions.find((q) => !answeredIds.has(q.id));
|
|
1470
|
+
if (!next) {
|
|
1471
|
+
return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
|
|
1472
|
+
}
|
|
1473
|
+
return {
|
|
1474
|
+
content: [
|
|
1475
|
+
{
|
|
1476
|
+
type: "text",
|
|
1477
|
+
text: JSON.stringify({
|
|
1478
|
+
done: false,
|
|
1479
|
+
questionId: next.id,
|
|
1480
|
+
question: next.question,
|
|
1481
|
+
type: next.type,
|
|
1482
|
+
options: next.options ?? null,
|
|
1483
|
+
help: next.help ?? null,
|
|
1484
|
+
index: questions.indexOf(next) + 1,
|
|
1485
|
+
total: questions.length
|
|
1486
|
+
})
|
|
1487
|
+
}
|
|
1488
|
+
]
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
);
|
|
1492
|
+
server2.tool(
|
|
1493
|
+
"stackwright_pro_record_answer",
|
|
1494
|
+
"Records a single answer to a phase question. Appends to .stackwright/answers/{phase}.json incrementally. Idempotent \u2014 calling twice for the same questionId overwrites. Call after receiving each user reply in the conversational question loop.",
|
|
1495
|
+
{
|
|
1496
|
+
phase: z8.string().describe('Phase name e.g. "designer"'),
|
|
1497
|
+
questionId: z8.string().describe('The question ID from get_next_question, e.g. "designer-1"'),
|
|
1498
|
+
answer: z8.string().describe("The user's free-text answer")
|
|
1499
|
+
},
|
|
1500
|
+
async ({ phase, questionId, answer }) => {
|
|
1501
|
+
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
1502
|
+
if (!SAFE_PHASE.test(phase)) {
|
|
1503
|
+
return {
|
|
1504
|
+
content: [
|
|
1505
|
+
{ type: "text", text: JSON.stringify({ error: "Invalid phase name" }) }
|
|
1506
|
+
],
|
|
1507
|
+
isError: true
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/i.test(questionId)) {
|
|
1511
|
+
return {
|
|
1512
|
+
content: [
|
|
1513
|
+
{ type: "text", text: JSON.stringify({ error: "Invalid questionId" }) }
|
|
1514
|
+
],
|
|
1515
|
+
isError: true
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
const safeAnswer = answer.slice(0, 2e3);
|
|
1519
|
+
const cwd = process.cwd();
|
|
1520
|
+
const answersDir = join(cwd, ".stackwright", "answers");
|
|
1521
|
+
const answersPath = join(answersDir, `${phase}.json`);
|
|
1522
|
+
if (existsSync2(answersDir) && lstatSync2(answersDir).isSymbolicLink()) {
|
|
1523
|
+
return {
|
|
1524
|
+
content: [
|
|
1525
|
+
{
|
|
1526
|
+
type: "text",
|
|
1527
|
+
text: JSON.stringify({ error: "answers dir is a symlink \u2014 refusing to write" })
|
|
1528
|
+
}
|
|
1529
|
+
],
|
|
1530
|
+
isError: true
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
mkdirSync(answersDir, { recursive: true });
|
|
1534
|
+
let existing = {
|
|
1535
|
+
version: "1.0",
|
|
1536
|
+
phase,
|
|
1537
|
+
answers: {}
|
|
1538
|
+
};
|
|
1539
|
+
try {
|
|
1540
|
+
if (existsSync2(answersPath)) {
|
|
1541
|
+
if (lstatSync2(answersPath).isSymbolicLink()) {
|
|
1542
|
+
return {
|
|
1543
|
+
content: [
|
|
1544
|
+
{
|
|
1545
|
+
type: "text",
|
|
1546
|
+
text: JSON.stringify({ error: "answers file is a symlink \u2014 refusing to write" })
|
|
1547
|
+
}
|
|
1548
|
+
],
|
|
1549
|
+
isError: true
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
const raw = await readFile(answersPath, "utf-8");
|
|
1553
|
+
existing = JSON.parse(raw);
|
|
1554
|
+
}
|
|
1555
|
+
} catch {
|
|
1556
|
+
}
|
|
1557
|
+
existing.answers[questionId] = safeAnswer;
|
|
1558
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1559
|
+
const tmp = `${answersPath}.tmp`;
|
|
1560
|
+
await writeFile(tmp, JSON.stringify(existing, null, 2), "utf-8");
|
|
1561
|
+
renameSync(tmp, answersPath);
|
|
1562
|
+
return {
|
|
1563
|
+
content: [
|
|
1564
|
+
{ type: "text", text: JSON.stringify({ recorded: true, phase, questionId }) }
|
|
1565
|
+
]
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
);
|
|
1363
1569
|
}
|
|
1364
1570
|
|
|
1365
1571
|
// src/tools/orchestration.ts
|
|
1366
|
-
import { z as
|
|
1367
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as
|
|
1572
|
+
import { z as z9 } from "zod";
|
|
1573
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, lstatSync as lstatSync3 } from "fs";
|
|
1368
1574
|
import { join as join2 } from "path";
|
|
1369
1575
|
var OTTER_NAME_TO_PHASE = [
|
|
1370
1576
|
["designer", "designer"],
|
|
@@ -1420,9 +1626,9 @@ function handleSaveManifest(input) {
|
|
|
1420
1626
|
const dir = join2(cwd, ".stackwright");
|
|
1421
1627
|
const filePath = join2(dir, "question-manifest.json");
|
|
1422
1628
|
try {
|
|
1423
|
-
|
|
1424
|
-
if (
|
|
1425
|
-
const stat =
|
|
1629
|
+
mkdirSync2(dir, { recursive: true });
|
|
1630
|
+
if (existsSync3(filePath)) {
|
|
1631
|
+
const stat = lstatSync3(filePath);
|
|
1426
1632
|
if (stat.isSymbolicLink()) {
|
|
1427
1633
|
const message = `Refusing to write to symlink: ${filePath}`;
|
|
1428
1634
|
return {
|
|
@@ -1454,7 +1660,7 @@ function handleSavePhaseAnswers(input) {
|
|
|
1454
1660
|
const dir = join2(cwd, ".stackwright", "answers");
|
|
1455
1661
|
const filePath = join2(dir, `${input.phase}.json`);
|
|
1456
1662
|
try {
|
|
1457
|
-
|
|
1663
|
+
mkdirSync2(dir, { recursive: true });
|
|
1458
1664
|
let answers;
|
|
1459
1665
|
if (input.questions && input.questions.length > 0) {
|
|
1460
1666
|
answers = answersToManifestFormat(input.rawAnswers, input.questions);
|
|
@@ -1469,8 +1675,8 @@ function handleSavePhaseAnswers(input) {
|
|
|
1469
1675
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1470
1676
|
answers
|
|
1471
1677
|
};
|
|
1472
|
-
if (
|
|
1473
|
-
const stat =
|
|
1678
|
+
if (existsSync3(filePath)) {
|
|
1679
|
+
const stat = lstatSync3(filePath);
|
|
1474
1680
|
if (stat.isSymbolicLink()) {
|
|
1475
1681
|
const message = `Refusing to write to symlink: ${filePath}`;
|
|
1476
1682
|
return {
|
|
@@ -1499,7 +1705,7 @@ function handleSavePhaseAnswers(input) {
|
|
|
1499
1705
|
function handleReadPhaseAnswers(input) {
|
|
1500
1706
|
const cwd = input._cwd ?? process.cwd();
|
|
1501
1707
|
const filePath = join2(cwd, ".stackwright", "answers", `${input.phase}.json`);
|
|
1502
|
-
if (!
|
|
1708
|
+
if (!existsSync3(filePath)) {
|
|
1503
1709
|
return {
|
|
1504
1710
|
text: JSON.stringify({ missing: true, phase: input.phase }),
|
|
1505
1711
|
isError: false
|
|
@@ -1531,13 +1737,63 @@ function handleGetOtterName(input) {
|
|
|
1531
1737
|
isError: false
|
|
1532
1738
|
};
|
|
1533
1739
|
}
|
|
1740
|
+
function handleSaveBuildContext(input) {
|
|
1741
|
+
const cwd = input._cwd ?? process.cwd();
|
|
1742
|
+
const dir = join2(cwd, ".stackwright");
|
|
1743
|
+
const filePath = join2(dir, "build-context.json");
|
|
1744
|
+
try {
|
|
1745
|
+
mkdirSync2(dir, { recursive: true });
|
|
1746
|
+
if (existsSync3(filePath)) {
|
|
1747
|
+
const stat = lstatSync3(filePath);
|
|
1748
|
+
if (stat.isSymbolicLink()) {
|
|
1749
|
+
return {
|
|
1750
|
+
text: JSON.stringify({
|
|
1751
|
+
success: false,
|
|
1752
|
+
error: `Refusing to write to symlink: ${filePath}`
|
|
1753
|
+
}),
|
|
1754
|
+
isError: true
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
const payload = {
|
|
1759
|
+
version: "1.0",
|
|
1760
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1761
|
+
buildContext: input.buildContext
|
|
1762
|
+
};
|
|
1763
|
+
writeFileSync2(filePath, JSON.stringify(payload, null, 2) + "\n");
|
|
1764
|
+
return {
|
|
1765
|
+
text: JSON.stringify({ success: true, path: filePath }),
|
|
1766
|
+
isError: false
|
|
1767
|
+
};
|
|
1768
|
+
} catch (err) {
|
|
1769
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1770
|
+
return {
|
|
1771
|
+
text: JSON.stringify({ success: false, error: message }),
|
|
1772
|
+
isError: true
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1534
1776
|
function registerOrchestrationTools(server2) {
|
|
1777
|
+
server2.tool(
|
|
1778
|
+
"stackwright_pro_save_build_context",
|
|
1779
|
+
`Save the user's initial build description to .stackwright/build-context.json. Call this once at startup after the user answers the opening "what are you building" question. The saved context is automatically prepended to specialist prompts by stackwright_pro_build_specialist_prompt.`,
|
|
1780
|
+
{
|
|
1781
|
+
buildContext: z9.string().describe("Free-text description of what the user wants to build")
|
|
1782
|
+
},
|
|
1783
|
+
async ({ buildContext }) => {
|
|
1784
|
+
const { text, isError } = handleSaveBuildContext({ buildContext });
|
|
1785
|
+
return {
|
|
1786
|
+
content: [{ type: "text", text }],
|
|
1787
|
+
isError
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
);
|
|
1535
1791
|
server2.tool(
|
|
1536
1792
|
"stackwright_pro_parse_otter_response",
|
|
1537
1793
|
"Parse and validate a specialist otter's QUESTION_COLLECTION_MODE JSON response. Handles JSON extraction from LLM responses (strips markdown, fixes single quotes, trailing commas). Detects the phase from the otter name. Use this immediately after invoke_agent() to get a validated manifest phase object.",
|
|
1538
1794
|
{
|
|
1539
|
-
otterName:
|
|
1540
|
-
responseText:
|
|
1795
|
+
otterName: z9.string().describe('The agent name, e.g. "stackwright-pro-api-otter"'),
|
|
1796
|
+
responseText: z9.string().describe("Raw text response from the otter's QUESTION_COLLECTION_MODE invocation")
|
|
1541
1797
|
},
|
|
1542
1798
|
async ({ otterName, responseText }) => {
|
|
1543
1799
|
const { result, isError } = handleParseOtterResponse({ otterName, responseText });
|
|
@@ -1551,16 +1807,18 @@ function registerOrchestrationTools(server2) {
|
|
|
1551
1807
|
"stackwright_pro_save_manifest",
|
|
1552
1808
|
"Write the question manifest to .stackwright/question-manifest.json. Call this after collecting and parsing questions from all otters via stackwright_pro_parse_otter_response.",
|
|
1553
1809
|
{
|
|
1554
|
-
phases:
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1810
|
+
phases: jsonCoerce(
|
|
1811
|
+
z9.array(
|
|
1812
|
+
z9.object({
|
|
1813
|
+
phase: z9.string(),
|
|
1814
|
+
otter: z9.string(),
|
|
1815
|
+
questions: z9.array(z9.any()),
|
|
1816
|
+
requiredPackages: z9.object({
|
|
1817
|
+
dependencies: z9.record(z9.string(), z9.string()).optional(),
|
|
1818
|
+
devPackages: z9.record(z9.string(), z9.string()).optional()
|
|
1819
|
+
}).optional()
|
|
1820
|
+
})
|
|
1821
|
+
)
|
|
1564
1822
|
).describe("Array of parsed phase objects from stackwright_pro_parse_otter_response")
|
|
1565
1823
|
},
|
|
1566
1824
|
async ({ phases }) => {
|
|
@@ -1575,15 +1833,21 @@ function registerOrchestrationTools(server2) {
|
|
|
1575
1833
|
"stackwright_pro_save_phase_answers",
|
|
1576
1834
|
"Save user answers for a phase to .stackwright/answers/{phase}.json. Pass rawAnswers directly from ask_user_question and the original manifest questions for label-to-value reverse mapping.",
|
|
1577
1835
|
{
|
|
1578
|
-
phase:
|
|
1579
|
-
rawAnswers:
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1836
|
+
phase: z9.string().describe('Phase name, e.g. "designer"'),
|
|
1837
|
+
rawAnswers: jsonCoerce(
|
|
1838
|
+
z9.array(
|
|
1839
|
+
z9.object({
|
|
1840
|
+
question_header: z9.string(),
|
|
1841
|
+
selected_options: z9.array(z9.string()),
|
|
1842
|
+
other_text: z9.string().nullable().optional()
|
|
1843
|
+
})
|
|
1844
|
+
)
|
|
1845
|
+
).describe(
|
|
1846
|
+
"Answers as returned by ask_user_question \u2014 pass the native array, not a JSON string"
|
|
1847
|
+
),
|
|
1848
|
+
questions: jsonCoerce(z9.array(z9.any()).optional()).describe(
|
|
1849
|
+
"Original manifest questions for label\u2192value reverse-mapping \u2014 pass the native array, not a JSON string"
|
|
1850
|
+
)
|
|
1587
1851
|
},
|
|
1588
1852
|
async ({ phase, rawAnswers, questions }) => {
|
|
1589
1853
|
const { text, isError } = handleSavePhaseAnswers({
|
|
@@ -1601,7 +1865,7 @@ function registerOrchestrationTools(server2) {
|
|
|
1601
1865
|
"stackwright_pro_read_phase_answers",
|
|
1602
1866
|
"Read saved answers for a phase from .stackwright/answers/{phase}.json. Returns { missing: true } when no answers exist yet \u2014 use this to skip phases safely in the execution loop.",
|
|
1603
1867
|
{
|
|
1604
|
-
phase:
|
|
1868
|
+
phase: z9.string().describe('Phase name, e.g. "designer"')
|
|
1605
1869
|
},
|
|
1606
1870
|
async ({ phase }) => {
|
|
1607
1871
|
const { text, isError } = handleReadPhaseAnswers({ phase });
|
|
@@ -1615,7 +1879,7 @@ function registerOrchestrationTools(server2) {
|
|
|
1615
1879
|
"stackwright_pro_get_otter_name",
|
|
1616
1880
|
"Get the agent name for a phase (e.g. 'designer' \u2192 'stackwright-pro-designer-otter'). Use this in the execution loop to invoke the correct specialist otter without hardcoding names in the prompt.",
|
|
1617
1881
|
{
|
|
1618
|
-
phase:
|
|
1882
|
+
phase: z9.string().describe('Phase name, e.g. "designer", "api", "pages"')
|
|
1619
1883
|
},
|
|
1620
1884
|
async ({ phase }) => {
|
|
1621
1885
|
const { text, isError } = handleGetOtterName({ phase });
|
|
@@ -1628,28 +1892,376 @@ function registerOrchestrationTools(server2) {
|
|
|
1628
1892
|
}
|
|
1629
1893
|
|
|
1630
1894
|
// src/tools/pipeline.ts
|
|
1631
|
-
import { z as
|
|
1632
|
-
import { readFileSync as
|
|
1895
|
+
import { z as z11 } from "zod";
|
|
1896
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, lstatSync as lstatSync5 } from "fs";
|
|
1897
|
+
import { join as join4 } from "path";
|
|
1898
|
+
import { createHash as createHash3 } from "crypto";
|
|
1899
|
+
|
|
1900
|
+
// src/artifact-signing.ts
|
|
1901
|
+
import {
|
|
1902
|
+
createHash as createHash2,
|
|
1903
|
+
generateKeyPairSync,
|
|
1904
|
+
createPublicKey,
|
|
1905
|
+
createPrivateKey,
|
|
1906
|
+
sign,
|
|
1907
|
+
verify,
|
|
1908
|
+
timingSafeEqual
|
|
1909
|
+
} from "crypto";
|
|
1910
|
+
import {
|
|
1911
|
+
readFileSync as readFileSync3,
|
|
1912
|
+
writeFileSync as writeFileSync3,
|
|
1913
|
+
existsSync as existsSync4,
|
|
1914
|
+
mkdirSync as mkdirSync3,
|
|
1915
|
+
lstatSync as lstatSync4,
|
|
1916
|
+
unlinkSync,
|
|
1917
|
+
readdirSync
|
|
1918
|
+
} from "fs";
|
|
1633
1919
|
import { join as join3 } from "path";
|
|
1920
|
+
import { z as z10 } from "zod";
|
|
1921
|
+
var ALGORITHM = "ECDSA-P384-SHA384";
|
|
1922
|
+
var KEY_FILE = "pipeline-keys.json";
|
|
1923
|
+
var KEY_DIR = ".stackwright";
|
|
1924
|
+
var SIGNATURE_MANIFEST = "signatures.json";
|
|
1925
|
+
var ARTIFACTS_DIR = ".stackwright/artifacts";
|
|
1926
|
+
function rejectSymlink(filePath, context) {
|
|
1927
|
+
if (!existsSync4(filePath)) return;
|
|
1928
|
+
const stat = lstatSync4(filePath);
|
|
1929
|
+
if (stat.isSymbolicLink()) {
|
|
1930
|
+
throw new Error(`Security: refusing to follow symlink at ${context}: ${filePath}`);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
function computeSha384(data) {
|
|
1934
|
+
return createHash2("sha384").update(data).digest("hex");
|
|
1935
|
+
}
|
|
1936
|
+
function safeDigestEqual(a, b) {
|
|
1937
|
+
if (a.length !== b.length) return false;
|
|
1938
|
+
return timingSafeEqual(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
1939
|
+
}
|
|
1940
|
+
function emptyManifest() {
|
|
1941
|
+
return {
|
|
1942
|
+
version: "1.0",
|
|
1943
|
+
algorithm: ALGORITHM,
|
|
1944
|
+
signatures: {}
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
function initPipelineKeys(cwd) {
|
|
1948
|
+
const keyDir = join3(cwd, KEY_DIR);
|
|
1949
|
+
const keyPath = join3(keyDir, KEY_FILE);
|
|
1950
|
+
rejectSymlink(keyPath, "pipeline-keys.json");
|
|
1951
|
+
mkdirSync3(keyDir, { recursive: true });
|
|
1952
|
+
const { publicKey, privateKey } = generateKeyPairSync("ec", {
|
|
1953
|
+
namedCurve: "P-384"
|
|
1954
|
+
});
|
|
1955
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
1956
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
1957
|
+
const fingerprint = createHash2("sha256").update(publicKeyPem).digest("hex");
|
|
1958
|
+
const keyFile = {
|
|
1959
|
+
version: "1.0",
|
|
1960
|
+
algorithm: ALGORITHM,
|
|
1961
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1962
|
+
publicKeyPem,
|
|
1963
|
+
privateKeyPem
|
|
1964
|
+
};
|
|
1965
|
+
writeFileSync3(keyPath, JSON.stringify(keyFile, null, 2), { encoding: "utf-8" });
|
|
1966
|
+
return { publicKeyPem, fingerprint };
|
|
1967
|
+
}
|
|
1968
|
+
function loadPipelineKeys(cwd) {
|
|
1969
|
+
const keyPath = join3(cwd, KEY_DIR, KEY_FILE);
|
|
1970
|
+
rejectSymlink(keyPath, "pipeline-keys.json");
|
|
1971
|
+
if (!existsSync4(keyPath)) {
|
|
1972
|
+
throw new Error("Pipeline keys not found \u2014 call initPipelineKeys() first");
|
|
1973
|
+
}
|
|
1974
|
+
let raw;
|
|
1975
|
+
try {
|
|
1976
|
+
raw = readFileSync3(keyPath, "utf-8");
|
|
1977
|
+
} catch (err) {
|
|
1978
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1979
|
+
throw new Error(`Cannot read pipeline keys: ${msg}`, { cause: err });
|
|
1980
|
+
}
|
|
1981
|
+
let parsed;
|
|
1982
|
+
try {
|
|
1983
|
+
parsed = JSON.parse(raw);
|
|
1984
|
+
} catch {
|
|
1985
|
+
throw new Error("Pipeline keys file is not valid JSON");
|
|
1986
|
+
}
|
|
1987
|
+
if (typeof parsed.publicKeyPem !== "string" || !parsed.publicKeyPem.includes("-----BEGIN PUBLIC KEY-----")) {
|
|
1988
|
+
throw new Error("Invalid public key PEM in pipeline keys file");
|
|
1989
|
+
}
|
|
1990
|
+
if (typeof parsed.privateKeyPem !== "string" || !parsed.privateKeyPem.includes("-----BEGIN")) {
|
|
1991
|
+
throw new Error("Invalid private key PEM in pipeline keys file");
|
|
1992
|
+
}
|
|
1993
|
+
const publicKey = createPublicKey(parsed.publicKeyPem);
|
|
1994
|
+
const privateKey = createPrivateKey(parsed.privateKeyPem);
|
|
1995
|
+
return { privateKey, publicKey };
|
|
1996
|
+
}
|
|
1997
|
+
function signArtifact(artifactBytes, privateKey) {
|
|
1998
|
+
const digest = computeSha384(artifactBytes);
|
|
1999
|
+
const sig = sign("SHA384", artifactBytes, privateKey);
|
|
2000
|
+
return {
|
|
2001
|
+
digest,
|
|
2002
|
+
signature: sig.toString("base64"),
|
|
2003
|
+
algorithm: ALGORITHM,
|
|
2004
|
+
signedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
function verifyArtifact(artifactBytes, signature, publicKey) {
|
|
2008
|
+
const sigValid = verify(
|
|
2009
|
+
"SHA384",
|
|
2010
|
+
artifactBytes,
|
|
2011
|
+
publicKey,
|
|
2012
|
+
Buffer.from(signature.signature, "base64")
|
|
2013
|
+
);
|
|
2014
|
+
if (!sigValid) return false;
|
|
2015
|
+
const actualDigest = computeSha384(artifactBytes);
|
|
2016
|
+
return safeDigestEqual(actualDigest, signature.digest);
|
|
2017
|
+
}
|
|
2018
|
+
function loadSignatureManifest(cwd) {
|
|
2019
|
+
const manifestPath = join3(cwd, ARTIFACTS_DIR, SIGNATURE_MANIFEST);
|
|
2020
|
+
rejectSymlink(manifestPath, "signatures.json");
|
|
2021
|
+
if (!existsSync4(manifestPath)) return emptyManifest();
|
|
2022
|
+
let raw;
|
|
2023
|
+
try {
|
|
2024
|
+
raw = readFileSync3(manifestPath, "utf-8");
|
|
2025
|
+
} catch (err) {
|
|
2026
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2027
|
+
throw new Error(`Cannot read signature manifest: ${msg}`, { cause: err });
|
|
2028
|
+
}
|
|
2029
|
+
try {
|
|
2030
|
+
const parsed = JSON.parse(raw);
|
|
2031
|
+
if (parsed.version !== "1.0" || typeof parsed.signatures !== "object") {
|
|
2032
|
+
throw new Error("Malformed signature manifest: invalid version or missing signatures object");
|
|
2033
|
+
}
|
|
2034
|
+
return parsed;
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
if (err instanceof Error && err.message.startsWith("Malformed")) throw err;
|
|
2037
|
+
throw new Error("Signature manifest is not valid JSON", { cause: err });
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
function saveArtifactSignature(cwd, artifactFilename, sig, signerOtter) {
|
|
2041
|
+
const artifactsDir = join3(cwd, ARTIFACTS_DIR);
|
|
2042
|
+
const manifestPath = join3(artifactsDir, SIGNATURE_MANIFEST);
|
|
2043
|
+
rejectSymlink(manifestPath, "signatures.json (save)");
|
|
2044
|
+
mkdirSync3(artifactsDir, { recursive: true });
|
|
2045
|
+
const manifest = loadSignatureManifest(cwd);
|
|
2046
|
+
manifest.signatures[artifactFilename] = {
|
|
2047
|
+
...sig,
|
|
2048
|
+
signedBy: signerOtter
|
|
2049
|
+
};
|
|
2050
|
+
writeFileSync3(manifestPath, JSON.stringify(manifest, null, 2), { encoding: "utf-8" });
|
|
2051
|
+
}
|
|
2052
|
+
function getArtifactSignature(cwd, artifactFilename) {
|
|
2053
|
+
const manifest = loadSignatureManifest(cwd);
|
|
2054
|
+
const entry = manifest.signatures[artifactFilename];
|
|
2055
|
+
if (!entry) return null;
|
|
2056
|
+
return {
|
|
2057
|
+
digest: entry.digest,
|
|
2058
|
+
signature: entry.signature,
|
|
2059
|
+
algorithm: entry.algorithm,
|
|
2060
|
+
signedAt: entry.signedAt
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
function emitSignatureAuditEvent(params) {
|
|
2064
|
+
const record = JSON.stringify({
|
|
2065
|
+
level: "AUDIT",
|
|
2066
|
+
event: "ARTIFACT_SIGNATURE_FAIL",
|
|
2067
|
+
timestamp: params.timestamp,
|
|
2068
|
+
source: params.source,
|
|
2069
|
+
artifactFilename: params.artifactFilename,
|
|
2070
|
+
expectedDigest: params.expectedDigest,
|
|
2071
|
+
actualDigest: params.actualDigest,
|
|
2072
|
+
phase: params.phase
|
|
2073
|
+
});
|
|
2074
|
+
process.stderr.write(`ARTIFACT_SIGNATURE_FAIL ${record}
|
|
2075
|
+
`);
|
|
2076
|
+
}
|
|
2077
|
+
function registerArtifactSigningTools(server2) {
|
|
2078
|
+
server2.tool(
|
|
2079
|
+
"stackwright_pro_verify_artifact_signatures",
|
|
2080
|
+
"Verify ECDSA P-384 signatures for all pipeline artifacts in .stackwright/artifacts/. Auto-discovers keys from .stackwright/pipeline-keys.json. Returns per-artifact verification status.",
|
|
2081
|
+
{
|
|
2082
|
+
cwd: z10.string().optional().describe("Project root directory. Defaults to process.cwd().")
|
|
2083
|
+
},
|
|
2084
|
+
async ({ cwd: cwdParam }) => {
|
|
2085
|
+
const cwd = cwdParam ?? process.cwd();
|
|
2086
|
+
let publicKey;
|
|
2087
|
+
try {
|
|
2088
|
+
const keys = loadPipelineKeys(cwd);
|
|
2089
|
+
publicKey = keys.publicKey;
|
|
2090
|
+
} catch (err) {
|
|
2091
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2092
|
+
return {
|
|
2093
|
+
content: [
|
|
2094
|
+
{
|
|
2095
|
+
type: "text",
|
|
2096
|
+
text: JSON.stringify({
|
|
2097
|
+
error: true,
|
|
2098
|
+
message: `Cannot load pipeline keys: ${msg}`
|
|
2099
|
+
})
|
|
2100
|
+
}
|
|
2101
|
+
],
|
|
2102
|
+
isError: true
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
let manifest;
|
|
2106
|
+
try {
|
|
2107
|
+
manifest = loadSignatureManifest(cwd);
|
|
2108
|
+
} catch (err) {
|
|
2109
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2110
|
+
return {
|
|
2111
|
+
content: [
|
|
2112
|
+
{
|
|
2113
|
+
type: "text",
|
|
2114
|
+
text: JSON.stringify({
|
|
2115
|
+
error: true,
|
|
2116
|
+
message: `Cannot load signature manifest: ${msg}`
|
|
2117
|
+
})
|
|
2118
|
+
}
|
|
2119
|
+
],
|
|
2120
|
+
isError: true
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
const artifactsPath = join3(cwd, ARTIFACTS_DIR);
|
|
2124
|
+
let artifactFiles = [];
|
|
2125
|
+
try {
|
|
2126
|
+
if (existsSync4(artifactsPath)) {
|
|
2127
|
+
artifactFiles = readdirSync(artifactsPath).filter(
|
|
2128
|
+
(f) => f.endsWith(".json") && f !== SIGNATURE_MANIFEST
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
} catch {
|
|
2132
|
+
}
|
|
2133
|
+
const results = [];
|
|
2134
|
+
let hasFailure = false;
|
|
2135
|
+
for (const filename of artifactFiles) {
|
|
2136
|
+
const filePath = join3(artifactsPath, filename);
|
|
2137
|
+
try {
|
|
2138
|
+
rejectSymlink(filePath, `artifact ${filename}`);
|
|
2139
|
+
} catch {
|
|
2140
|
+
results.push({
|
|
2141
|
+
filename,
|
|
2142
|
+
verified: false,
|
|
2143
|
+
error: "Refusing to verify symlink"
|
|
2144
|
+
});
|
|
2145
|
+
hasFailure = true;
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
let artifactBytes;
|
|
2149
|
+
try {
|
|
2150
|
+
artifactBytes = readFileSync3(filePath);
|
|
2151
|
+
} catch (err) {
|
|
2152
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2153
|
+
results.push({
|
|
2154
|
+
filename,
|
|
2155
|
+
verified: false,
|
|
2156
|
+
error: `Cannot read artifact: ${msg}`
|
|
2157
|
+
});
|
|
2158
|
+
hasFailure = true;
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
2161
|
+
const entry = manifest.signatures[filename];
|
|
2162
|
+
if (!entry) {
|
|
2163
|
+
results.push({
|
|
2164
|
+
filename,
|
|
2165
|
+
verified: false,
|
|
2166
|
+
error: "No signature found in manifest"
|
|
2167
|
+
});
|
|
2168
|
+
hasFailure = true;
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
const sig = {
|
|
2172
|
+
digest: entry.digest,
|
|
2173
|
+
signature: entry.signature,
|
|
2174
|
+
algorithm: entry.algorithm,
|
|
2175
|
+
signedAt: entry.signedAt
|
|
2176
|
+
};
|
|
2177
|
+
const verified = verifyArtifact(artifactBytes, sig, publicKey);
|
|
2178
|
+
if (!verified) {
|
|
2179
|
+
const actualDigest = computeSha384(artifactBytes);
|
|
2180
|
+
emitSignatureAuditEvent({
|
|
2181
|
+
artifactFilename: filename,
|
|
2182
|
+
expectedDigest: sig.digest,
|
|
2183
|
+
actualDigest,
|
|
2184
|
+
phase: entry.signedBy ?? "unknown",
|
|
2185
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2186
|
+
source: "stackwright_pro_verify_artifact_signatures"
|
|
2187
|
+
});
|
|
2188
|
+
results.push({
|
|
2189
|
+
filename,
|
|
2190
|
+
verified: false,
|
|
2191
|
+
error: `Signature verification failed \u2014 artifact may have been tampered with`,
|
|
2192
|
+
signedBy: entry.signedBy,
|
|
2193
|
+
signedAt: entry.signedAt
|
|
2194
|
+
});
|
|
2195
|
+
hasFailure = true;
|
|
2196
|
+
} else {
|
|
2197
|
+
results.push({
|
|
2198
|
+
filename,
|
|
2199
|
+
verified: true,
|
|
2200
|
+
signedBy: entry.signedBy,
|
|
2201
|
+
signedAt: entry.signedAt
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
for (const manifestFilename of Object.keys(manifest.signatures)) {
|
|
2206
|
+
if (!artifactFiles.includes(manifestFilename)) {
|
|
2207
|
+
results.push({
|
|
2208
|
+
filename: manifestFilename,
|
|
2209
|
+
verified: false,
|
|
2210
|
+
error: "Artifact referenced in manifest but missing from disk"
|
|
2211
|
+
});
|
|
2212
|
+
hasFailure = true;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
const verifiedCount = results.filter((r) => r.verified).length;
|
|
2216
|
+
const failedCount = results.filter((r) => !r.verified).length;
|
|
2217
|
+
return {
|
|
2218
|
+
content: [
|
|
2219
|
+
{
|
|
2220
|
+
type: "text",
|
|
2221
|
+
text: JSON.stringify({
|
|
2222
|
+
totalArtifacts: artifactFiles.length,
|
|
2223
|
+
verifiedCount,
|
|
2224
|
+
failedCount,
|
|
2225
|
+
results,
|
|
2226
|
+
...hasFailure ? {
|
|
2227
|
+
error: "SIGNATURE VERIFICATION FAILED: One or more artifact signatures are invalid. Do not proceed \u2014 artifacts may have been tampered with."
|
|
2228
|
+
} : {}
|
|
2229
|
+
})
|
|
2230
|
+
}
|
|
2231
|
+
],
|
|
2232
|
+
isError: hasFailure
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
// src/tools/pipeline.ts
|
|
2239
|
+
import { WorkflowFileSchema, authConfigSchema } from "@stackwright-pro/types";
|
|
1634
2240
|
var PHASE_ORDER = [
|
|
1635
2241
|
"designer",
|
|
1636
2242
|
"theme",
|
|
1637
2243
|
"api",
|
|
1638
|
-
"auth",
|
|
1639
2244
|
"data",
|
|
2245
|
+
"workflow",
|
|
1640
2246
|
"pages",
|
|
1641
2247
|
"dashboard",
|
|
1642
|
-
"
|
|
2248
|
+
"auth"
|
|
1643
2249
|
];
|
|
1644
2250
|
var PHASE_DEPENDENCIES = {
|
|
1645
2251
|
designer: [],
|
|
1646
2252
|
theme: ["designer"],
|
|
1647
2253
|
api: [],
|
|
1648
|
-
auth: [],
|
|
1649
2254
|
data: ["api"],
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
workflow: [
|
|
2255
|
+
// workflow: no hard deps — uses service: references with Prism mock fallback
|
|
2256
|
+
// when API artifacts aren't available; roles come from user answers
|
|
2257
|
+
workflow: [],
|
|
2258
|
+
// pages: 'api' is transitive through 'data'; auth removed — page-otter has
|
|
2259
|
+
// graceful fallback and reads role names from workflow artifacts instead
|
|
2260
|
+
pages: ["designer", "theme", "data"],
|
|
2261
|
+
dashboard: ["designer", "theme", "data"],
|
|
2262
|
+
// auth is the terminal phase — reads all available artifacts at runtime;
|
|
2263
|
+
// no hard upstream requirements so it always runs and never gets skipped
|
|
2264
|
+
auth: []
|
|
1653
2265
|
};
|
|
1654
2266
|
var PHASE_ARTIFACT = {
|
|
1655
2267
|
designer: "design-language.json",
|
|
@@ -1699,11 +2311,11 @@ function createDefaultState() {
|
|
|
1699
2311
|
};
|
|
1700
2312
|
}
|
|
1701
2313
|
function statePath(cwd) {
|
|
1702
|
-
return
|
|
2314
|
+
return join4(cwd, ".stackwright", "pipeline-state.json");
|
|
1703
2315
|
}
|
|
1704
2316
|
function readState(cwd) {
|
|
1705
2317
|
const p = statePath(cwd);
|
|
1706
|
-
if (!
|
|
2318
|
+
if (!existsSync5(p)) return createDefaultState();
|
|
1707
2319
|
const raw = JSON.parse(safeReadSync(p));
|
|
1708
2320
|
if (typeof raw !== "object" || raw === null || raw.version !== "1.0") {
|
|
1709
2321
|
return createDefaultState();
|
|
@@ -1711,26 +2323,26 @@ function readState(cwd) {
|
|
|
1711
2323
|
return raw;
|
|
1712
2324
|
}
|
|
1713
2325
|
function safeWriteSync(filePath, content) {
|
|
1714
|
-
if (
|
|
1715
|
-
const stat =
|
|
2326
|
+
if (existsSync5(filePath)) {
|
|
2327
|
+
const stat = lstatSync5(filePath);
|
|
1716
2328
|
if (stat.isSymbolicLink()) {
|
|
1717
2329
|
throw new Error(`Refusing to write to symlink: ${filePath}`);
|
|
1718
2330
|
}
|
|
1719
2331
|
}
|
|
1720
|
-
|
|
2332
|
+
writeFileSync4(filePath, content);
|
|
1721
2333
|
}
|
|
1722
2334
|
function safeReadSync(filePath) {
|
|
1723
|
-
if (
|
|
1724
|
-
const stat =
|
|
2335
|
+
if (existsSync5(filePath)) {
|
|
2336
|
+
const stat = lstatSync5(filePath);
|
|
1725
2337
|
if (stat.isSymbolicLink()) {
|
|
1726
2338
|
throw new Error(`Refusing to read symlink: ${filePath}`);
|
|
1727
2339
|
}
|
|
1728
2340
|
}
|
|
1729
|
-
return
|
|
2341
|
+
return readFileSync4(filePath, "utf-8");
|
|
1730
2342
|
}
|
|
1731
2343
|
function writeState(cwd, state) {
|
|
1732
|
-
const dir =
|
|
1733
|
-
|
|
2344
|
+
const dir = join4(cwd, ".stackwright");
|
|
2345
|
+
mkdirSync4(dir, { recursive: true });
|
|
1734
2346
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1735
2347
|
safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
|
|
1736
2348
|
}
|
|
@@ -1751,6 +2363,15 @@ function handleGetPipelineState(_cwd) {
|
|
|
1751
2363
|
const cwd = _cwd ?? process.cwd();
|
|
1752
2364
|
try {
|
|
1753
2365
|
const state = readState(cwd);
|
|
2366
|
+
const keyPath = join4(cwd, ".stackwright", "pipeline-keys.json");
|
|
2367
|
+
if (!existsSync5(keyPath)) {
|
|
2368
|
+
try {
|
|
2369
|
+
const { fingerprint } = initPipelineKeys(cwd);
|
|
2370
|
+
state["signingKeyFingerprint"] = fingerprint;
|
|
2371
|
+
writeState(cwd, state);
|
|
2372
|
+
} catch {
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
1754
2375
|
return { text: JSON.stringify(state), isError: false };
|
|
1755
2376
|
} catch (err) {
|
|
1756
2377
|
return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
|
|
@@ -1802,28 +2423,62 @@ function handleSetPipelineState(input) {
|
|
|
1802
2423
|
return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
|
|
1803
2424
|
}
|
|
1804
2425
|
}
|
|
1805
|
-
function handleCheckExecutionReady(_cwd) {
|
|
2426
|
+
function handleCheckExecutionReady(_cwd, phase) {
|
|
1806
2427
|
const cwd = _cwd ?? process.cwd();
|
|
2428
|
+
if (phase) {
|
|
2429
|
+
if (!isValidPhase(phase)) {
|
|
2430
|
+
return {
|
|
2431
|
+
text: JSON.stringify({
|
|
2432
|
+
error: true,
|
|
2433
|
+
message: `Invalid phase: ${phase}. Valid phases are: ${PHASE_ORDER.join(", ")}`
|
|
2434
|
+
}),
|
|
2435
|
+
isError: true
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
const answerFile = join4(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
2439
|
+
if (!existsSync5(answerFile)) {
|
|
2440
|
+
return {
|
|
2441
|
+
text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
|
|
2442
|
+
isError: false
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
try {
|
|
2446
|
+
const raw = safeReadSync(answerFile);
|
|
2447
|
+
const parsed = JSON.parse(raw);
|
|
2448
|
+
if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
|
|
2449
|
+
return {
|
|
2450
|
+
text: JSON.stringify({ ready: false, phase, reason: "Answer file is malformed" }),
|
|
2451
|
+
isError: false
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
return { text: JSON.stringify({ ready: true, phase }), isError: false };
|
|
2455
|
+
} catch {
|
|
2456
|
+
return {
|
|
2457
|
+
text: JSON.stringify({ ready: false, phase, reason: "Answer file could not be parsed" }),
|
|
2458
|
+
isError: false
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
1807
2462
|
try {
|
|
1808
|
-
const answersDir =
|
|
2463
|
+
const answersDir = join4(cwd, ".stackwright", "answers");
|
|
1809
2464
|
const answeredPhases = [];
|
|
1810
2465
|
const missingPhases = [];
|
|
1811
|
-
for (const
|
|
1812
|
-
const answerFile =
|
|
1813
|
-
if (
|
|
2466
|
+
for (const phase2 of PHASE_ORDER) {
|
|
2467
|
+
const answerFile = join4(answersDir, `${phase2}.json`);
|
|
2468
|
+
if (existsSync5(answerFile)) {
|
|
1814
2469
|
try {
|
|
1815
2470
|
const raw = safeReadSync(answerFile);
|
|
1816
2471
|
const parsed = JSON.parse(raw);
|
|
1817
2472
|
if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
|
|
1818
|
-
missingPhases.push(
|
|
2473
|
+
missingPhases.push(phase2);
|
|
1819
2474
|
continue;
|
|
1820
2475
|
}
|
|
1821
|
-
answeredPhases.push(
|
|
2476
|
+
answeredPhases.push(phase2);
|
|
1822
2477
|
} catch {
|
|
1823
|
-
missingPhases.push(
|
|
2478
|
+
missingPhases.push(phase2);
|
|
1824
2479
|
}
|
|
1825
2480
|
} else {
|
|
1826
|
-
missingPhases.push(
|
|
2481
|
+
missingPhases.push(phase2);
|
|
1827
2482
|
}
|
|
1828
2483
|
}
|
|
1829
2484
|
return {
|
|
@@ -1842,15 +2497,35 @@ function handleCheckExecutionReady(_cwd) {
|
|
|
1842
2497
|
function handleListArtifacts(_cwd) {
|
|
1843
2498
|
const cwd = _cwd ?? process.cwd();
|
|
1844
2499
|
try {
|
|
1845
|
-
const artifactsDir =
|
|
2500
|
+
const artifactsDir = join4(cwd, ".stackwright", "artifacts");
|
|
2501
|
+
let manifest = null;
|
|
2502
|
+
try {
|
|
2503
|
+
manifest = loadSignatureManifest(cwd);
|
|
2504
|
+
} catch {
|
|
2505
|
+
}
|
|
1846
2506
|
const artifacts = [];
|
|
1847
2507
|
let completedCount = 0;
|
|
1848
2508
|
for (const phase of PHASE_ORDER) {
|
|
1849
2509
|
const expectedFile = PHASE_ARTIFACT[phase];
|
|
1850
|
-
const fullPath =
|
|
1851
|
-
const exists =
|
|
2510
|
+
const fullPath = join4(artifactsDir, expectedFile);
|
|
2511
|
+
const exists = existsSync5(fullPath);
|
|
1852
2512
|
if (exists) completedCount++;
|
|
1853
|
-
|
|
2513
|
+
let signed = false;
|
|
2514
|
+
let signatureValid = null;
|
|
2515
|
+
if (exists && manifest) {
|
|
2516
|
+
const entry = manifest.signatures[expectedFile];
|
|
2517
|
+
if (entry) {
|
|
2518
|
+
signed = true;
|
|
2519
|
+
try {
|
|
2520
|
+
const rawBytes = Buffer.from(safeReadSync(fullPath), "utf-8");
|
|
2521
|
+
const { publicKey } = loadPipelineKeys(cwd);
|
|
2522
|
+
signatureValid = verifyArtifact(rawBytes, entry, publicKey);
|
|
2523
|
+
} catch {
|
|
2524
|
+
signatureValid = null;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
artifacts.push({ phase, expectedFile, exists, path: fullPath, signed, signatureValid });
|
|
1854
2529
|
}
|
|
1855
2530
|
return {
|
|
1856
2531
|
text: JSON.stringify({ artifacts, completedCount, totalCount: PHASE_ORDER.length }),
|
|
@@ -1886,9 +2561,9 @@ function handleWritePhaseQuestions(input) {
|
|
|
1886
2561
|
}
|
|
1887
2562
|
} catch {
|
|
1888
2563
|
}
|
|
1889
|
-
const questionsDir =
|
|
1890
|
-
|
|
1891
|
-
const filePath =
|
|
2564
|
+
const questionsDir = join4(cwd, ".stackwright", "questions");
|
|
2565
|
+
mkdirSync4(questionsDir, { recursive: true });
|
|
2566
|
+
const filePath = join4(questionsDir, `${phase}.json`);
|
|
1892
2567
|
const payload = {
|
|
1893
2568
|
version: "1.0",
|
|
1894
2569
|
phase,
|
|
@@ -1928,36 +2603,81 @@ function handleBuildSpecialistPrompt(input) {
|
|
|
1928
2603
|
};
|
|
1929
2604
|
}
|
|
1930
2605
|
try {
|
|
1931
|
-
const answersPath =
|
|
2606
|
+
const answersPath = join4(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
1932
2607
|
let answers = {};
|
|
1933
|
-
if (
|
|
2608
|
+
if (existsSync5(answersPath)) {
|
|
1934
2609
|
answers = JSON.parse(safeReadSync(answersPath));
|
|
1935
2610
|
}
|
|
2611
|
+
let buildContextText = "";
|
|
2612
|
+
const buildContextPath = join4(cwd, ".stackwright", "build-context.json");
|
|
2613
|
+
if (existsSync5(buildContextPath)) {
|
|
2614
|
+
try {
|
|
2615
|
+
const bcRaw = JSON.parse(safeReadSync(buildContextPath));
|
|
2616
|
+
if (typeof bcRaw.buildContext === "string" && bcRaw.buildContext.trim().length > 0) {
|
|
2617
|
+
buildContextText = bcRaw.buildContext.trim();
|
|
2618
|
+
}
|
|
2619
|
+
} catch {
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
1936
2622
|
const deps = PHASE_DEPENDENCIES[phase];
|
|
1937
2623
|
const artifactSections = [];
|
|
1938
2624
|
const missingDependencies = [];
|
|
1939
2625
|
for (const dep of deps) {
|
|
1940
2626
|
const artifactFile = PHASE_ARTIFACT[dep];
|
|
1941
|
-
const artifactPath =
|
|
1942
|
-
if (
|
|
1943
|
-
const
|
|
2627
|
+
const artifactPath = join4(cwd, ".stackwright", "artifacts", artifactFile);
|
|
2628
|
+
if (existsSync5(artifactPath)) {
|
|
2629
|
+
const rawContent = safeReadSync(artifactPath);
|
|
2630
|
+
const rawBytes = Buffer.from(rawContent, "utf-8");
|
|
2631
|
+
const content = JSON.parse(rawContent);
|
|
2632
|
+
let signatureVerified = false;
|
|
2633
|
+
let signatureAvailable = false;
|
|
2634
|
+
try {
|
|
2635
|
+
const sig = getArtifactSignature(cwd, artifactFile);
|
|
2636
|
+
if (sig) {
|
|
2637
|
+
signatureAvailable = true;
|
|
2638
|
+
const { publicKey } = loadPipelineKeys(cwd);
|
|
2639
|
+
signatureVerified = verifyArtifact(rawBytes, sig, publicKey);
|
|
2640
|
+
if (!signatureVerified) {
|
|
2641
|
+
const actualDigest = createHash3("sha384").update(rawBytes).digest("hex");
|
|
2642
|
+
emitSignatureAuditEvent({
|
|
2643
|
+
artifactFilename: artifactFile,
|
|
2644
|
+
expectedDigest: sig.digest,
|
|
2645
|
+
actualDigest,
|
|
2646
|
+
phase: dep,
|
|
2647
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2648
|
+
source: "stackwright_pro_build_specialist_prompt"
|
|
2649
|
+
});
|
|
2650
|
+
missingDependencies.push(dep);
|
|
2651
|
+
artifactSections.push(
|
|
2652
|
+
`[${artifactFile}]:
|
|
2653
|
+
(integrity check failed: ECDSA-P384 signature verification failed \u2014 artifact may have been tampered with)`
|
|
2654
|
+
);
|
|
2655
|
+
continue;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
} catch {
|
|
2659
|
+
}
|
|
1944
2660
|
const expectedOtter = PHASE_TO_OTTER2[dep];
|
|
1945
2661
|
const artifactOtter = content["generatedBy"];
|
|
2662
|
+
const normalizedOtter = artifactOtter?.replace(/-[a-f0-9]{6}$/, "");
|
|
1946
2663
|
if (!artifactOtter) {
|
|
1947
2664
|
missingDependencies.push(dep);
|
|
1948
2665
|
artifactSections.push(
|
|
1949
2666
|
`[${artifactFile}]:
|
|
1950
2667
|
(integrity check failed: missing generatedBy field)`
|
|
1951
2668
|
);
|
|
1952
|
-
} else if (
|
|
2669
|
+
} else if (normalizedOtter !== expectedOtter) {
|
|
1953
2670
|
missingDependencies.push(dep);
|
|
1954
2671
|
artifactSections.push(
|
|
1955
2672
|
`[${artifactFile}]:
|
|
1956
2673
|
(integrity check failed: artifact claims generatedBy="${artifactOtter}" but expected="${expectedOtter}")`
|
|
1957
2674
|
);
|
|
1958
2675
|
} else {
|
|
1959
|
-
|
|
1960
|
-
|
|
2676
|
+
const sigStatus = signatureAvailable ? signatureVerified ? " [signature verified]" : " [signature check skipped]" : " [unsigned]";
|
|
2677
|
+
artifactSections.push(
|
|
2678
|
+
`[${artifactFile}]${sigStatus}:
|
|
2679
|
+
${JSON.stringify(content, null, 2)}`
|
|
2680
|
+
);
|
|
1961
2681
|
}
|
|
1962
2682
|
} else {
|
|
1963
2683
|
missingDependencies.push(dep);
|
|
@@ -1965,10 +2685,17 @@ ${JSON.stringify(content, null, 2)}`);
|
|
|
1965
2685
|
(not yet available)`);
|
|
1966
2686
|
}
|
|
1967
2687
|
}
|
|
1968
|
-
const parts = [
|
|
2688
|
+
const parts = [];
|
|
2689
|
+
if (buildContextText) {
|
|
2690
|
+
parts.push("BUILD_CONTEXT:", buildContextText, "");
|
|
2691
|
+
}
|
|
2692
|
+
parts.push("ANSWERS:", JSON.stringify(answers, null, 2));
|
|
1969
2693
|
if (artifactSections.length > 0) {
|
|
1970
2694
|
parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
|
|
1971
2695
|
}
|
|
2696
|
+
const artifactSchema = PHASE_ARTIFACT_SCHEMA[phase];
|
|
2697
|
+
parts.push("", "REQUIRED_ARTIFACT_SCHEMA:");
|
|
2698
|
+
parts.push(artifactSchema);
|
|
1972
2699
|
parts.push("", "Execute using these answers and the upstream artifacts provided.");
|
|
1973
2700
|
const prompt = parts.join("\n");
|
|
1974
2701
|
const dependenciesSatisfied = missingDependencies.length === 0;
|
|
@@ -2003,45 +2730,231 @@ var OFF_SCRIPT_PATTERNS = [
|
|
|
2003
2730
|
var PHASE_REQUIRED_KEYS = {
|
|
2004
2731
|
designer: ["designLanguage", "themeTokenSeeds"],
|
|
2005
2732
|
theme: ["tokens"],
|
|
2006
|
-
api: ["entities"],
|
|
2733
|
+
api: ["entities", "version", "generatedBy"],
|
|
2007
2734
|
auth: ["version", "generatedBy"],
|
|
2008
|
-
data: ["version", "generatedBy"],
|
|
2735
|
+
data: ["version", "generatedBy", "strategy", "collections"],
|
|
2009
2736
|
pages: ["version", "generatedBy"],
|
|
2010
2737
|
dashboard: ["version", "generatedBy"],
|
|
2011
2738
|
workflow: ["version", "generatedBy"]
|
|
2012
2739
|
};
|
|
2740
|
+
var PHASE_ARTIFACT_SCHEMA = {
|
|
2741
|
+
designer: JSON.stringify(
|
|
2742
|
+
{
|
|
2743
|
+
version: "1.0",
|
|
2744
|
+
generatedBy: "stackwright-pro-designer-otter",
|
|
2745
|
+
application: {
|
|
2746
|
+
type: "<operational|data-explorer|admin|logistics|general>",
|
|
2747
|
+
environment: "<workstation|field|control-room|mixed>",
|
|
2748
|
+
density: "<compact|balanced|spacious>",
|
|
2749
|
+
accessibility: "<wcag-aa|section-508|none>",
|
|
2750
|
+
colorScheme: "<light|dark|both>"
|
|
2751
|
+
},
|
|
2752
|
+
designLanguage: {
|
|
2753
|
+
rationale: "<design rationale>",
|
|
2754
|
+
spacingScale: { base: 8, scale: [0, 4, 8, 16, 24, 32, 48, 64] },
|
|
2755
|
+
colorSemantics: { primary: "#1a365d", accent: "#e53e3e" },
|
|
2756
|
+
typography: {
|
|
2757
|
+
dataFont: "Inter",
|
|
2758
|
+
headingFont: "Inter",
|
|
2759
|
+
monoFont: "monospace",
|
|
2760
|
+
dataSizePx: 12,
|
|
2761
|
+
bodySizePx: 14
|
|
2762
|
+
},
|
|
2763
|
+
contrastRatio: "4.5",
|
|
2764
|
+
borderRadius: "4",
|
|
2765
|
+
shadowElevation: "standard"
|
|
2766
|
+
},
|
|
2767
|
+
themeTokenSeeds: {
|
|
2768
|
+
light: {
|
|
2769
|
+
background: "#ffffff",
|
|
2770
|
+
foreground: "#1a1a1a",
|
|
2771
|
+
primary: "#1a365d",
|
|
2772
|
+
surface: "#f7f7f7",
|
|
2773
|
+
border: "#e2e8f0"
|
|
2774
|
+
},
|
|
2775
|
+
dark: {
|
|
2776
|
+
background: "#1a1a1a",
|
|
2777
|
+
foreground: "#ffffff",
|
|
2778
|
+
primary: "#90cdf4",
|
|
2779
|
+
surface: "#2d2d2d",
|
|
2780
|
+
border: "#4a5568"
|
|
2781
|
+
}
|
|
2782
|
+
},
|
|
2783
|
+
conformsTo: null,
|
|
2784
|
+
operationalNotes: []
|
|
2785
|
+
},
|
|
2786
|
+
null,
|
|
2787
|
+
2
|
|
2788
|
+
),
|
|
2789
|
+
theme: JSON.stringify(
|
|
2790
|
+
{
|
|
2791
|
+
version: "1.0",
|
|
2792
|
+
generatedBy: "stackwright-pro-theme-otter",
|
|
2793
|
+
componentLibrary: "shadcn",
|
|
2794
|
+
colorScheme: "<light|dark|both>",
|
|
2795
|
+
tokens: {
|
|
2796
|
+
colors: { "primary-500": "#1a365d", background: "#ffffff" },
|
|
2797
|
+
spacing: { "spacing-1": "8px", "spacing-2": "16px" },
|
|
2798
|
+
typography: { "font-data": "Inter", "text-sm": "12px" },
|
|
2799
|
+
shape: { "radius-sm": "4px", "radius-md": "8px" },
|
|
2800
|
+
shadows: { "shadow-sm": "0 1px 2px rgba(0,0,0,0.08)" }
|
|
2801
|
+
},
|
|
2802
|
+
cssVariables: {
|
|
2803
|
+
"--background": "0 0% 100%",
|
|
2804
|
+
"--foreground": "222.2 84% 4.9%",
|
|
2805
|
+
"--primary": "222.2 47.4% 11.2%",
|
|
2806
|
+
"--primary-foreground": "210 40% 98%",
|
|
2807
|
+
"--surface": "210 40% 98%",
|
|
2808
|
+
"--border": "214.3 31.8% 91.4%"
|
|
2809
|
+
},
|
|
2810
|
+
dark: { "--background": "222.2 84% 4.9%", "--foreground": "210 40% 98%" }
|
|
2811
|
+
},
|
|
2812
|
+
null,
|
|
2813
|
+
2
|
|
2814
|
+
),
|
|
2815
|
+
api: JSON.stringify(
|
|
2816
|
+
{
|
|
2817
|
+
version: "1.0",
|
|
2818
|
+
generatedBy: "stackwright-pro-api-otter",
|
|
2819
|
+
entities: [
|
|
2820
|
+
{
|
|
2821
|
+
name: "Shipment",
|
|
2822
|
+
endpoint: "/shipments",
|
|
2823
|
+
method: "GET",
|
|
2824
|
+
revalidate: 60,
|
|
2825
|
+
mutationType: null
|
|
2826
|
+
}
|
|
2827
|
+
],
|
|
2828
|
+
auth: { type: "bearer", header: "Authorization", envVar: "API_TOKEN" },
|
|
2829
|
+
baseUrl: "https://api.example.mil/v2",
|
|
2830
|
+
specPath: "./specs/api.yaml"
|
|
2831
|
+
},
|
|
2832
|
+
null,
|
|
2833
|
+
2
|
|
2834
|
+
),
|
|
2835
|
+
data: JSON.stringify(
|
|
2836
|
+
{
|
|
2837
|
+
version: "1.0",
|
|
2838
|
+
generatedBy: "stackwright-pro-data-otter",
|
|
2839
|
+
strategy: "<pulse-fast|isr-fast|isr-standard|isr-slow>",
|
|
2840
|
+
pulseMode: false,
|
|
2841
|
+
collections: [{ name: "equipment", revalidate: 60, pulse: false }],
|
|
2842
|
+
endpoints: { included: ["/equipment/**"], excluded: ["/admin/**"] },
|
|
2843
|
+
requiredPackages: { dependencies: {}, devPackages: {} }
|
|
2844
|
+
},
|
|
2845
|
+
null,
|
|
2846
|
+
2
|
|
2847
|
+
),
|
|
2848
|
+
workflow: JSON.stringify(
|
|
2849
|
+
{
|
|
2850
|
+
version: "1.0",
|
|
2851
|
+
generatedBy: "stackwright-pro-workflow-otter",
|
|
2852
|
+
workflowConfig: {
|
|
2853
|
+
id: "procurement-approval",
|
|
2854
|
+
route: "/procurement",
|
|
2855
|
+
files: ["workflows/procurement-approval.yml"],
|
|
2856
|
+
serviceDependencies: ["service:workflow-state"],
|
|
2857
|
+
warnings: []
|
|
2858
|
+
}
|
|
2859
|
+
},
|
|
2860
|
+
null,
|
|
2861
|
+
2
|
|
2862
|
+
),
|
|
2863
|
+
pages: JSON.stringify(
|
|
2864
|
+
{
|
|
2865
|
+
version: "1.0",
|
|
2866
|
+
generatedBy: "stackwright-pro-page-otter",
|
|
2867
|
+
pages: [
|
|
2868
|
+
{
|
|
2869
|
+
slug: "catalog",
|
|
2870
|
+
type: "collection_listing",
|
|
2871
|
+
collection: "products",
|
|
2872
|
+
themeApplied: true,
|
|
2873
|
+
authRequired: false
|
|
2874
|
+
},
|
|
2875
|
+
{
|
|
2876
|
+
slug: "admin",
|
|
2877
|
+
type: "protected",
|
|
2878
|
+
collection: null,
|
|
2879
|
+
themeApplied: true,
|
|
2880
|
+
authRequired: true
|
|
2881
|
+
}
|
|
2882
|
+
]
|
|
2883
|
+
},
|
|
2884
|
+
null,
|
|
2885
|
+
2
|
|
2886
|
+
),
|
|
2887
|
+
dashboard: JSON.stringify(
|
|
2888
|
+
{
|
|
2889
|
+
version: "1.0",
|
|
2890
|
+
generatedBy: "stackwright-pro-dashboard-otter",
|
|
2891
|
+
pages: [
|
|
2892
|
+
{
|
|
2893
|
+
slug: "dashboard",
|
|
2894
|
+
layout: "<grid|table|mixed>",
|
|
2895
|
+
collections: ["equipment", "supplies"],
|
|
2896
|
+
mode: "<ISR|Pulse>"
|
|
2897
|
+
}
|
|
2898
|
+
]
|
|
2899
|
+
},
|
|
2900
|
+
null,
|
|
2901
|
+
2
|
|
2902
|
+
),
|
|
2903
|
+
auth: JSON.stringify(
|
|
2904
|
+
{
|
|
2905
|
+
version: "1.0",
|
|
2906
|
+
generatedBy: "stackwright-pro-auth-otter",
|
|
2907
|
+
authConfig: {
|
|
2908
|
+
method: "<cac|oidc|oauth2|none>",
|
|
2909
|
+
provider: "<azure-ad|okta|ping|cognito \u2014 if OIDC, else null>",
|
|
2910
|
+
rbacRoles: ["ADMIN", "ANALYST"],
|
|
2911
|
+
rbacDefaultRole: "ANALYST",
|
|
2912
|
+
protectedRoutes: ["/dashboard/:path*", "/procurement/:path*"],
|
|
2913
|
+
auditEnabled: true,
|
|
2914
|
+
auditRetentionDays: 90
|
|
2915
|
+
}
|
|
2916
|
+
},
|
|
2917
|
+
null,
|
|
2918
|
+
2
|
|
2919
|
+
)
|
|
2920
|
+
};
|
|
2013
2921
|
function handleValidateArtifact(input) {
|
|
2014
2922
|
const cwd = input._cwd ?? process.cwd();
|
|
2015
|
-
const { phase, responseText } = input;
|
|
2923
|
+
const { phase, responseText, artifact: directArtifact } = input;
|
|
2016
2924
|
if (!isValidPhase(phase)) {
|
|
2017
2925
|
return {
|
|
2018
2926
|
text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
|
|
2019
2927
|
isError: true
|
|
2020
2928
|
};
|
|
2021
2929
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2930
|
+
let artifact;
|
|
2931
|
+
if (directArtifact) {
|
|
2932
|
+
artifact = directArtifact;
|
|
2933
|
+
} else {
|
|
2934
|
+
const text = responseText ?? "";
|
|
2935
|
+
for (const { pattern, label } of OFF_SCRIPT_PATTERNS) {
|
|
2936
|
+
if (pattern.test(text)) {
|
|
2937
|
+
const result = {
|
|
2938
|
+
valid: false,
|
|
2939
|
+
phase,
|
|
2940
|
+
violation: "off-script",
|
|
2941
|
+
retryPrompt: `You returned code output (detected: ${label}). Return ONLY a JSON artifact \u2014 no code, no files.`
|
|
2942
|
+
};
|
|
2943
|
+
return { text: JSON.stringify(result), isError: false };
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
try {
|
|
2947
|
+
artifact = extractJsonFromResponse(text);
|
|
2948
|
+
} catch {
|
|
2024
2949
|
const result = {
|
|
2025
2950
|
valid: false,
|
|
2026
2951
|
phase,
|
|
2027
|
-
violation: "
|
|
2028
|
-
retryPrompt:
|
|
2952
|
+
violation: "invalid-json",
|
|
2953
|
+
retryPrompt: "Your response did not contain valid JSON. Return a single JSON object with no surrounding text."
|
|
2029
2954
|
};
|
|
2030
2955
|
return { text: JSON.stringify(result), isError: false };
|
|
2031
2956
|
}
|
|
2032
2957
|
}
|
|
2033
|
-
let artifact;
|
|
2034
|
-
try {
|
|
2035
|
-
artifact = extractJsonFromResponse(responseText);
|
|
2036
|
-
} catch {
|
|
2037
|
-
const result = {
|
|
2038
|
-
valid: false,
|
|
2039
|
-
phase,
|
|
2040
|
-
violation: "invalid-json",
|
|
2041
|
-
retryPrompt: "Your response did not contain valid JSON. Return a single JSON object with no surrounding text."
|
|
2042
|
-
};
|
|
2043
|
-
return { text: JSON.stringify(result), isError: false };
|
|
2044
|
-
}
|
|
2045
2958
|
if (!artifact.version || !artifact.generatedBy) {
|
|
2046
2959
|
const result = {
|
|
2047
2960
|
valid: false,
|
|
@@ -2062,12 +2975,58 @@ function handleValidateArtifact(input) {
|
|
|
2062
2975
|
};
|
|
2063
2976
|
return { text: JSON.stringify(result), isError: false };
|
|
2064
2977
|
}
|
|
2978
|
+
const PHASE_ZOD_VALIDATORS = {
|
|
2979
|
+
workflow: (artifact2) => {
|
|
2980
|
+
const workflowConfig = artifact2["workflowConfig"];
|
|
2981
|
+
if (!workflowConfig) return { success: true };
|
|
2982
|
+
const result = WorkflowFileSchema.safeParse(workflowConfig);
|
|
2983
|
+
if (!result.success) {
|
|
2984
|
+
const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
2985
|
+
return { success: false, error: { message: issues } };
|
|
2986
|
+
}
|
|
2987
|
+
return { success: true };
|
|
2988
|
+
},
|
|
2989
|
+
auth: (artifact2) => {
|
|
2990
|
+
const authConfig = artifact2["authConfig"];
|
|
2991
|
+
if (!authConfig) return { success: true };
|
|
2992
|
+
const result = authConfigSchema.safeParse(authConfig);
|
|
2993
|
+
if (!result.success) {
|
|
2994
|
+
const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
2995
|
+
return { success: false, error: { message: issues } };
|
|
2996
|
+
}
|
|
2997
|
+
return { success: true };
|
|
2998
|
+
}
|
|
2999
|
+
};
|
|
3000
|
+
const zodValidator = PHASE_ZOD_VALIDATORS[phase];
|
|
3001
|
+
if (zodValidator) {
|
|
3002
|
+
const zodResult = zodValidator(artifact);
|
|
3003
|
+
if (!zodResult.success) {
|
|
3004
|
+
const result = {
|
|
3005
|
+
valid: false,
|
|
3006
|
+
phase,
|
|
3007
|
+
violation: "schema-mismatch",
|
|
3008
|
+
retryPrompt: `Your artifact failed schema validation: ${zodResult.error?.message}. Fix these fields and return the corrected JSON artifact.`
|
|
3009
|
+
};
|
|
3010
|
+
return { text: JSON.stringify(result), isError: false };
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
2065
3013
|
try {
|
|
2066
|
-
const artifactsDir =
|
|
2067
|
-
|
|
3014
|
+
const artifactsDir = join4(cwd, ".stackwright", "artifacts");
|
|
3015
|
+
mkdirSync4(artifactsDir, { recursive: true });
|
|
2068
3016
|
const artifactFile = PHASE_ARTIFACT[phase];
|
|
2069
|
-
const artifactPath =
|
|
2070
|
-
|
|
3017
|
+
const artifactPath = join4(artifactsDir, artifactFile);
|
|
3018
|
+
const serialized = JSON.stringify(artifact, null, 2) + "\n";
|
|
3019
|
+
const artifactBytes = Buffer.from(serialized, "utf-8");
|
|
3020
|
+
safeWriteSync(artifactPath, serialized);
|
|
3021
|
+
let signed = false;
|
|
3022
|
+
try {
|
|
3023
|
+
const { privateKey } = loadPipelineKeys(cwd);
|
|
3024
|
+
const sig = signArtifact(artifactBytes, privateKey);
|
|
3025
|
+
const signerOtter = PHASE_TO_OTTER2[phase];
|
|
3026
|
+
saveArtifactSignature(cwd, artifactFile, sig, signerOtter);
|
|
3027
|
+
signed = true;
|
|
3028
|
+
} catch {
|
|
3029
|
+
}
|
|
2071
3030
|
const state = readState(cwd);
|
|
2072
3031
|
if (!state.phases[phase]) state.phases[phase] = defaultPhaseStatus();
|
|
2073
3032
|
const ps = state.phases[phase];
|
|
@@ -2078,7 +3037,7 @@ function handleValidateArtifact(input) {
|
|
|
2078
3037
|
valid: true,
|
|
2079
3038
|
phase,
|
|
2080
3039
|
artifactPath,
|
|
2081
|
-
summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})`
|
|
3040
|
+
summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})${signed ? " [signed]" : ""}`
|
|
2082
3041
|
};
|
|
2083
3042
|
return { text: JSON.stringify(result), isError: false };
|
|
2084
3043
|
} catch (err) {
|
|
@@ -2102,11 +3061,15 @@ function registerPipelineTools(server2) {
|
|
|
2102
3061
|
"stackwright_pro_set_pipeline_state",
|
|
2103
3062
|
`Atomic read\u2192modify\u2192write pipeline state. ${DESC}`,
|
|
2104
3063
|
{
|
|
2105
|
-
phase:
|
|
2106
|
-
field:
|
|
2107
|
-
value:
|
|
2108
|
-
|
|
2109
|
-
|
|
3064
|
+
phase: z11.string().optional().describe('Phase to update, e.g. "designer"'),
|
|
3065
|
+
field: z11.enum(["questionsCollected", "answered", "executed", "artifactWritten"]).optional().describe("Boolean field to set"),
|
|
3066
|
+
value: boolCoerce(z11.boolean().optional()).describe(
|
|
3067
|
+
'Value for the field \u2014 must be a JSON boolean (true/false), NOT the string "true"/"false"'
|
|
3068
|
+
),
|
|
3069
|
+
status: z11.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
|
|
3070
|
+
incrementRetry: boolCoerce(z11.boolean().optional()).describe(
|
|
3071
|
+
"Bump retryCount by 1 \u2014 must be a JSON boolean"
|
|
3072
|
+
)
|
|
2110
3073
|
},
|
|
2111
3074
|
async (args) => res(
|
|
2112
3075
|
handleSetPipelineState({
|
|
@@ -2120,9 +3083,11 @@ function registerPipelineTools(server2) {
|
|
|
2120
3083
|
);
|
|
2121
3084
|
server2.tool(
|
|
2122
3085
|
"stackwright_pro_check_execution_ready",
|
|
2123
|
-
`Check all phases have answer files in .stackwright/answers/. ${DESC}`,
|
|
2124
|
-
{
|
|
2125
|
-
|
|
3086
|
+
`Check all phases have answer files in .stackwright/answers/. If phase is provided, check only that phase. ${DESC}`,
|
|
3087
|
+
{
|
|
3088
|
+
phase: z11.string().optional().describe("If provided, check only this phase's readiness. Omit to check all phases.")
|
|
3089
|
+
},
|
|
3090
|
+
async ({ phase }) => res(handleCheckExecutionReady(void 0, phase))
|
|
2126
3091
|
);
|
|
2127
3092
|
server2.tool(
|
|
2128
3093
|
"stackwright_pro_list_artifacts",
|
|
@@ -2132,34 +3097,81 @@ function registerPipelineTools(server2) {
|
|
|
2132
3097
|
);
|
|
2133
3098
|
server2.tool(
|
|
2134
3099
|
"stackwright_pro_write_phase_questions",
|
|
2135
|
-
`Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. ${DESC}`,
|
|
3100
|
+
`Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. Specialists may also call this directly with a parsed questions array. ${DESC}`,
|
|
2136
3101
|
{
|
|
2137
|
-
phase:
|
|
2138
|
-
responseText:
|
|
3102
|
+
phase: z11.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
|
|
3103
|
+
responseText: z11.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
|
|
3104
|
+
questions: jsonCoerce(z11.array(z11.any()).optional()).describe(
|
|
3105
|
+
"Questions array for direct specialist write"
|
|
3106
|
+
)
|
|
2139
3107
|
},
|
|
2140
|
-
async ({ phase, responseText }) =>
|
|
3108
|
+
async ({ phase, responseText, questions }) => {
|
|
3109
|
+
if (phase && questions && Array.isArray(questions)) {
|
|
3110
|
+
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
3111
|
+
if (!SAFE_PHASE.test(phase)) {
|
|
3112
|
+
return {
|
|
3113
|
+
content: [
|
|
3114
|
+
{ type: "text", text: JSON.stringify({ error: `Invalid phase name` }) }
|
|
3115
|
+
],
|
|
3116
|
+
isError: true
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
const questionsDir = join4(process.cwd(), ".stackwright", "questions");
|
|
3120
|
+
mkdirSync4(questionsDir, { recursive: true });
|
|
3121
|
+
const outPath = join4(questionsDir, `${phase}.json`);
|
|
3122
|
+
writeFileSync4(
|
|
3123
|
+
outPath,
|
|
3124
|
+
JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
|
|
3125
|
+
);
|
|
3126
|
+
return {
|
|
3127
|
+
content: [
|
|
3128
|
+
{
|
|
3129
|
+
type: "text",
|
|
3130
|
+
text: JSON.stringify({
|
|
3131
|
+
written: true,
|
|
3132
|
+
phase,
|
|
3133
|
+
count: questions.length
|
|
3134
|
+
})
|
|
3135
|
+
}
|
|
3136
|
+
]
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
return res(
|
|
3140
|
+
handleWritePhaseQuestions({ phase: phase ?? "", responseText: responseText ?? "" })
|
|
3141
|
+
);
|
|
3142
|
+
}
|
|
2141
3143
|
);
|
|
2142
3144
|
server2.tool(
|
|
2143
3145
|
"stackwright_pro_build_specialist_prompt",
|
|
2144
3146
|
`Assemble execution prompt from answers + upstream artifacts. Foreman passes verbatim. ${DESC}`,
|
|
2145
|
-
{ phase:
|
|
3147
|
+
{ phase: z11.string().describe('Phase to build prompt for, e.g. "pages"') },
|
|
2146
3148
|
async ({ phase }) => res(handleBuildSpecialistPrompt({ phase }))
|
|
2147
3149
|
);
|
|
2148
3150
|
server2.tool(
|
|
2149
3151
|
"stackwright_pro_validate_artifact",
|
|
2150
|
-
`Validate
|
|
3152
|
+
`Validate and write artifact to .stackwright/artifacts/. Returns retryPrompt on failure. ${DESC}`,
|
|
2151
3153
|
{
|
|
2152
|
-
phase:
|
|
2153
|
-
responseText:
|
|
3154
|
+
phase: z11.string().describe('Phase that produced this artifact, e.g. "designer"'),
|
|
3155
|
+
responseText: z11.string().optional().describe("Raw response text from the specialist otter (Foreman-mediated path, legacy)"),
|
|
3156
|
+
artifact: jsonCoerce(z11.record(z11.string(), z11.unknown()).optional()).describe(
|
|
3157
|
+
"Artifact object to validate and write directly (specialist direct path \u2014 skips off-script detection and JSON parsing)"
|
|
3158
|
+
)
|
|
2154
3159
|
},
|
|
2155
|
-
async ({ phase, responseText }) =>
|
|
3160
|
+
async ({ phase, responseText, artifact }) => {
|
|
3161
|
+
if (artifact) {
|
|
3162
|
+
return res(
|
|
3163
|
+
handleValidateArtifact({ phase, artifact })
|
|
3164
|
+
);
|
|
3165
|
+
}
|
|
3166
|
+
return res(handleValidateArtifact({ phase, responseText: responseText ?? "" }));
|
|
3167
|
+
}
|
|
2156
3168
|
);
|
|
2157
3169
|
}
|
|
2158
3170
|
|
|
2159
3171
|
// src/tools/safe-write.ts
|
|
2160
|
-
import { z as
|
|
2161
|
-
import { writeFileSync as
|
|
2162
|
-
import { normalize, isAbsolute, dirname, join as
|
|
3172
|
+
import { z as z12 } from "zod";
|
|
3173
|
+
import { writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync5, lstatSync as lstatSync6 } from "fs";
|
|
3174
|
+
import { normalize, isAbsolute, dirname, join as join5 } from "path";
|
|
2163
3175
|
var OTTER_WRITE_ALLOWLISTS = {
|
|
2164
3176
|
"stackwright-pro-designer-otter": [
|
|
2165
3177
|
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Design language artifact" }
|
|
@@ -2202,9 +3214,25 @@ var OTTER_WRITE_ALLOWLISTS = {
|
|
|
2202
3214
|
};
|
|
2203
3215
|
var PROTECTED_PATH_PREFIXES = [
|
|
2204
3216
|
".stackwright/pipeline-state.json",
|
|
3217
|
+
".stackwright/pipeline-keys.json",
|
|
3218
|
+
// ephemeral signing keys
|
|
3219
|
+
".stackwright/artifacts/signatures.json",
|
|
3220
|
+
// artifact signature manifest
|
|
2205
3221
|
".stackwright/questions/",
|
|
2206
3222
|
".stackwright/answers/"
|
|
2207
3223
|
];
|
|
3224
|
+
var MAX_SAFE_WRITE_BYTES_JSON = 512 * 1024;
|
|
3225
|
+
var MAX_SAFE_WRITE_BYTES_YAML = 256 * 1024;
|
|
3226
|
+
var MAX_SAFE_WRITE_BYTES_ENV = 4 * 1024;
|
|
3227
|
+
var MAX_SAFE_WRITE_BYTES_DEFAULT = 256 * 1024;
|
|
3228
|
+
function getMaxBytesForPath(filePath) {
|
|
3229
|
+
if (filePath.endsWith(".json")) return { limit: MAX_SAFE_WRITE_BYTES_JSON, label: "JSON" };
|
|
3230
|
+
if (filePath.endsWith(".yml") || filePath.endsWith(".yaml"))
|
|
3231
|
+
return { limit: MAX_SAFE_WRITE_BYTES_YAML, label: "YAML" };
|
|
3232
|
+
if (filePath === ".env" || /^\.env\.[a-zA-Z0-9]+/.test(filePath))
|
|
3233
|
+
return { limit: MAX_SAFE_WRITE_BYTES_ENV, label: "env" };
|
|
3234
|
+
return { limit: MAX_SAFE_WRITE_BYTES_DEFAULT, label: "default" };
|
|
3235
|
+
}
|
|
2208
3236
|
function checkPathAllowed(callerOtter, filePath) {
|
|
2209
3237
|
const normalized = normalize(filePath);
|
|
2210
3238
|
if (normalized.includes("..")) {
|
|
@@ -2330,11 +3358,23 @@ function handleSafeWrite(input) {
|
|
|
2330
3358
|
};
|
|
2331
3359
|
return { text: JSON.stringify(result), isError: true };
|
|
2332
3360
|
}
|
|
3361
|
+
const contentBytes = Buffer.byteLength(content, "utf-8");
|
|
3362
|
+
const { limit: maxBytes, label: fileTypeLabel } = getMaxBytesForPath(filePath);
|
|
3363
|
+
if (contentBytes > maxBytes) {
|
|
3364
|
+
const result = {
|
|
3365
|
+
success: false,
|
|
3366
|
+
error: `Content size ${contentBytes} bytes exceeds ${fileTypeLabel} limit of ${maxBytes} bytes (${maxBytes / 1024} KB)`,
|
|
3367
|
+
callerOtter,
|
|
3368
|
+
attemptedPath: filePath,
|
|
3369
|
+
allowedPaths: []
|
|
3370
|
+
};
|
|
3371
|
+
return { text: JSON.stringify(result), isError: true };
|
|
3372
|
+
}
|
|
2333
3373
|
const normalized = normalize(filePath);
|
|
2334
|
-
const fullPath =
|
|
2335
|
-
if (
|
|
3374
|
+
const fullPath = join5(cwd, normalized);
|
|
3375
|
+
if (existsSync6(fullPath)) {
|
|
2336
3376
|
try {
|
|
2337
|
-
const stat =
|
|
3377
|
+
const stat = lstatSync6(fullPath);
|
|
2338
3378
|
if (stat.isSymbolicLink()) {
|
|
2339
3379
|
const result = {
|
|
2340
3380
|
success: false,
|
|
@@ -2361,9 +3401,9 @@ function handleSafeWrite(input) {
|
|
|
2361
3401
|
}
|
|
2362
3402
|
try {
|
|
2363
3403
|
if (createDirectories) {
|
|
2364
|
-
|
|
3404
|
+
mkdirSync5(dirname(fullPath), { recursive: true });
|
|
2365
3405
|
}
|
|
2366
|
-
|
|
3406
|
+
writeFileSync5(fullPath, content, { encoding: "utf-8" });
|
|
2367
3407
|
const result = {
|
|
2368
3408
|
success: true,
|
|
2369
3409
|
path: normalized,
|
|
@@ -2389,10 +3429,12 @@ function registerSafeWriteTools(server2) {
|
|
|
2389
3429
|
"stackwright_pro_safe_write",
|
|
2390
3430
|
DESC,
|
|
2391
3431
|
{
|
|
2392
|
-
callerOtter:
|
|
2393
|
-
filePath:
|
|
2394
|
-
content:
|
|
2395
|
-
createDirectories:
|
|
3432
|
+
callerOtter: z12.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
|
|
3433
|
+
filePath: z12.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
|
|
3434
|
+
content: z12.string().describe("File content to write"),
|
|
3435
|
+
createDirectories: boolCoerce(z12.boolean().optional().default(true)).describe(
|
|
3436
|
+
"Create parent directories if they don't exist. Default: true"
|
|
3437
|
+
)
|
|
2396
3438
|
},
|
|
2397
3439
|
async ({ callerOtter, filePath, content, createDirectories }) => {
|
|
2398
3440
|
const result = handleSafeWrite({
|
|
@@ -2407,9 +3449,9 @@ function registerSafeWriteTools(server2) {
|
|
|
2407
3449
|
}
|
|
2408
3450
|
|
|
2409
3451
|
// src/tools/auth.ts
|
|
2410
|
-
import { z as
|
|
2411
|
-
import { readFileSync as
|
|
2412
|
-
import { join as
|
|
3452
|
+
import { z as z13 } from "zod";
|
|
3453
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
|
|
3454
|
+
import { join as join6 } from "path";
|
|
2413
3455
|
function buildHierarchy(roles) {
|
|
2414
3456
|
const h = {};
|
|
2415
3457
|
for (let i = 0; i < roles.length - 1; i++) {
|
|
@@ -2671,7 +3713,7 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2671
3713
|
auditRetentionDays,
|
|
2672
3714
|
protectedRoutes
|
|
2673
3715
|
);
|
|
2674
|
-
|
|
3716
|
+
writeFileSync6(join6(cwd, "middleware.ts"), middlewareContent, "utf8");
|
|
2675
3717
|
filesWritten.push("middleware.ts");
|
|
2676
3718
|
} catch (err) {
|
|
2677
3719
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2687,12 +3729,12 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2687
3729
|
}
|
|
2688
3730
|
try {
|
|
2689
3731
|
const envBlock = generateEnvBlock(method, params);
|
|
2690
|
-
const envPath =
|
|
2691
|
-
if (
|
|
2692
|
-
const existing =
|
|
2693
|
-
|
|
3732
|
+
const envPath = join6(cwd, ".env.example");
|
|
3733
|
+
if (existsSync7(envPath)) {
|
|
3734
|
+
const existing = readFileSync5(envPath, "utf8");
|
|
3735
|
+
writeFileSync6(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
|
|
2694
3736
|
} else {
|
|
2695
|
-
|
|
3737
|
+
writeFileSync6(envPath, envBlock, "utf8");
|
|
2696
3738
|
}
|
|
2697
3739
|
filesWritten.push(".env.example");
|
|
2698
3740
|
} catch (err) {
|
|
@@ -2718,12 +3760,12 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2718
3760
|
auditRetentionDays,
|
|
2719
3761
|
protectedRoutes
|
|
2720
3762
|
);
|
|
2721
|
-
const ymlPath =
|
|
2722
|
-
if (!
|
|
2723
|
-
|
|
3763
|
+
const ymlPath = join6(cwd, "stackwright.yml");
|
|
3764
|
+
if (!existsSync7(ymlPath)) {
|
|
3765
|
+
writeFileSync6(ymlPath, authYaml, "utf8");
|
|
2724
3766
|
} else {
|
|
2725
|
-
const existing =
|
|
2726
|
-
|
|
3767
|
+
const existing = readFileSync5(ymlPath, "utf8");
|
|
3768
|
+
writeFileSync6(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
|
|
2727
3769
|
}
|
|
2728
3770
|
filesWritten.push("stackwright.yml");
|
|
2729
3771
|
} catch (err) {
|
|
@@ -2762,35 +3804,35 @@ function registerAuthTools(server2) {
|
|
|
2762
3804
|
"stackwright_pro_configure_auth",
|
|
2763
3805
|
"Generate authentication middleware and configuration for a Next.js Stackwright application. Writes `middleware.ts` from a secure template, appends/updates the `auth:` section in `stackwright.yml`, and generates `.env.example` with required environment variables. \u26A0\uFE0F For CAC/PKI: generated `middleware.ts` carries a SECURITY REVIEW REQUIRED comment \u2014 certificate chain validation must be verified by a DoD security officer before production deployment. This is the ONLY approved path to generating `middleware.ts`. Never write TypeScript auth files directly.",
|
|
2764
3806
|
{
|
|
2765
|
-
method:
|
|
2766
|
-
provider:
|
|
3807
|
+
method: z13.enum(["cac", "oidc", "oauth2", "none"]),
|
|
3808
|
+
provider: z13.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
|
|
2767
3809
|
// CAC
|
|
2768
|
-
cacCaBundle:
|
|
2769
|
-
cacEdipiLookup:
|
|
2770
|
-
cacOcspEndpoint:
|
|
2771
|
-
cacCertHeader:
|
|
3810
|
+
cacCaBundle: z13.string().optional(),
|
|
3811
|
+
cacEdipiLookup: z13.string().optional(),
|
|
3812
|
+
cacOcspEndpoint: z13.string().optional(),
|
|
3813
|
+
cacCertHeader: z13.string().optional(),
|
|
2772
3814
|
// OIDC
|
|
2773
|
-
oidcDiscoveryUrl:
|
|
2774
|
-
oidcClientId:
|
|
2775
|
-
oidcClientSecret:
|
|
2776
|
-
oidcScopes:
|
|
2777
|
-
oidcRoleClaim:
|
|
3815
|
+
oidcDiscoveryUrl: z13.string().optional(),
|
|
3816
|
+
oidcClientId: z13.string().optional(),
|
|
3817
|
+
oidcClientSecret: z13.string().optional(),
|
|
3818
|
+
oidcScopes: z13.string().optional(),
|
|
3819
|
+
oidcRoleClaim: z13.string().optional(),
|
|
2778
3820
|
// OAuth2
|
|
2779
|
-
oauth2AuthUrl:
|
|
2780
|
-
oauth2TokenUrl:
|
|
2781
|
-
oauth2ClientId:
|
|
2782
|
-
oauth2ClientSecret:
|
|
2783
|
-
oauth2Scopes:
|
|
3821
|
+
oauth2AuthUrl: z13.string().optional(),
|
|
3822
|
+
oauth2TokenUrl: z13.string().optional(),
|
|
3823
|
+
oauth2ClientId: z13.string().optional(),
|
|
3824
|
+
oauth2ClientSecret: z13.string().optional(),
|
|
3825
|
+
oauth2Scopes: z13.string().optional(),
|
|
2784
3826
|
// RBAC
|
|
2785
|
-
rbacRoles:
|
|
2786
|
-
rbacDefaultRole:
|
|
3827
|
+
rbacRoles: jsonCoerce(z13.array(z13.string()).optional()),
|
|
3828
|
+
rbacDefaultRole: z13.string().optional(),
|
|
2787
3829
|
// Audit
|
|
2788
|
-
auditEnabled:
|
|
2789
|
-
auditRetentionDays:
|
|
3830
|
+
auditEnabled: boolCoerce(z13.boolean().optional()),
|
|
3831
|
+
auditRetentionDays: numCoerce(z13.number().int().positive().optional()),
|
|
2790
3832
|
// Routes
|
|
2791
|
-
protectedRoutes:
|
|
3833
|
+
protectedRoutes: jsonCoerce(z13.array(z13.string()).optional()),
|
|
2792
3834
|
// Injection for tests
|
|
2793
|
-
_cwd:
|
|
3835
|
+
_cwd: z13.string().optional()
|
|
2794
3836
|
},
|
|
2795
3837
|
async (params) => {
|
|
2796
3838
|
const cwd = params._cwd ?? process.cwd();
|
|
@@ -2800,45 +3842,45 @@ function registerAuthTools(server2) {
|
|
|
2800
3842
|
}
|
|
2801
3843
|
|
|
2802
3844
|
// src/integrity.ts
|
|
2803
|
-
import { createHash as
|
|
2804
|
-
import { readFileSync as
|
|
2805
|
-
import { join as
|
|
3845
|
+
import { createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
3846
|
+
import { readFileSync as readFileSync6, readdirSync as readdirSync2, lstatSync as lstatSync7 } from "fs";
|
|
3847
|
+
import { join as join7, basename } from "path";
|
|
2806
3848
|
var _checksums = /* @__PURE__ */ new Map([
|
|
2807
3849
|
[
|
|
2808
3850
|
"stackwright-pro-api-otter.json",
|
|
2809
|
-
"
|
|
3851
|
+
"9fbaed0ce6116b82d0289f24432037d04637c89b8e73062ed946e5d49b294734"
|
|
2810
3852
|
],
|
|
2811
3853
|
[
|
|
2812
3854
|
"stackwright-pro-auth-otter.json",
|
|
2813
|
-
"
|
|
3855
|
+
"bf0e66e35d15ba818ba6ff1a007df34975565bacbb35cc0c80151fb1da13e573"
|
|
2814
3856
|
],
|
|
2815
3857
|
[
|
|
2816
3858
|
"stackwright-pro-dashboard-otter.json",
|
|
2817
|
-
"
|
|
3859
|
+
"4f9fcb73c1f168846dbaf7af09f4ca99887339739160de83f739dca693464a23"
|
|
2818
3860
|
],
|
|
2819
3861
|
[
|
|
2820
3862
|
"stackwright-pro-data-otter.json",
|
|
2821
|
-
"
|
|
3863
|
+
"fb3e34977908d36a30eb448f069ecf41bff6c309c0daddf00471bb6f4822d025"
|
|
2822
3864
|
],
|
|
2823
3865
|
[
|
|
2824
3866
|
"stackwright-pro-designer-otter.json",
|
|
2825
|
-
"
|
|
3867
|
+
"af09ac8f06385bdbac63e2820daa2ff7d38b8ff1ff383c161f07e3fb9d9359c5"
|
|
2826
3868
|
],
|
|
2827
3869
|
[
|
|
2828
3870
|
"stackwright-pro-foreman-otter.json",
|
|
2829
|
-
"
|
|
3871
|
+
"582a26766a5bc80ef9f3aef53e82794c7f47f618bd28e4e70583bfc2b569398c"
|
|
2830
3872
|
],
|
|
2831
3873
|
[
|
|
2832
3874
|
"stackwright-pro-page-otter.json",
|
|
2833
|
-
"
|
|
3875
|
+
"1ba5754f1e8ac8918483881d1d29e6f0ced6cead57d6e7557ff70b247dc4b979"
|
|
2834
3876
|
],
|
|
2835
3877
|
[
|
|
2836
3878
|
"stackwright-pro-theme-otter.json",
|
|
2837
|
-
"
|
|
3879
|
+
"b323259ccda02a3397b40df3a5c93286c16c97908535eb89fcf6fa9a2c2c05d4"
|
|
2838
3880
|
],
|
|
2839
3881
|
[
|
|
2840
3882
|
"stackwright-pro-workflow-otter.json",
|
|
2841
|
-
"
|
|
3883
|
+
"c90d6773b2287aa9a640c2715ca0e75f44c13e99fddcfb89ced36603f38930ce"
|
|
2842
3884
|
]
|
|
2843
3885
|
]);
|
|
2844
3886
|
Object.freeze(_checksums);
|
|
@@ -2853,11 +3895,11 @@ for (const [name, digest] of CANONICAL_CHECKSUMS) {
|
|
|
2853
3895
|
}
|
|
2854
3896
|
var MAX_OTTER_BYTES = 1 * 1024 * 1024;
|
|
2855
3897
|
function computeSha256(data) {
|
|
2856
|
-
return
|
|
3898
|
+
return createHash4("sha256").update(data).digest("hex");
|
|
2857
3899
|
}
|
|
2858
3900
|
function safeEqual(a, b) {
|
|
2859
3901
|
if (a.length !== b.length) return false;
|
|
2860
|
-
return
|
|
3902
|
+
return timingSafeEqual2(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
2861
3903
|
}
|
|
2862
3904
|
function verifyOtterFile(filePath) {
|
|
2863
3905
|
const filename = basename(filePath);
|
|
@@ -2867,7 +3909,7 @@ function verifyOtterFile(filePath) {
|
|
|
2867
3909
|
}
|
|
2868
3910
|
let stat;
|
|
2869
3911
|
try {
|
|
2870
|
-
stat =
|
|
3912
|
+
stat = lstatSync7(filePath);
|
|
2871
3913
|
} catch (err) {
|
|
2872
3914
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2873
3915
|
return { verified: false, filename, error: `Cannot stat file: ${msg}` };
|
|
@@ -2885,7 +3927,7 @@ function verifyOtterFile(filePath) {
|
|
|
2885
3927
|
}
|
|
2886
3928
|
let raw;
|
|
2887
3929
|
try {
|
|
2888
|
-
raw =
|
|
3930
|
+
raw = readFileSync6(filePath);
|
|
2889
3931
|
} catch (err) {
|
|
2890
3932
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2891
3933
|
return { verified: false, filename, error: `Cannot read file: ${msg}` };
|
|
@@ -2918,12 +3960,24 @@ function verifyOtterFile(filePath) {
|
|
|
2918
3960
|
return { verified: true, filename };
|
|
2919
3961
|
}
|
|
2920
3962
|
function verifyAllOtters(otterDir) {
|
|
3963
|
+
if (/(?:^|[/\\])\.\.(?:[/\\]|$)/.test(otterDir) || otterDir.includes("..")) {
|
|
3964
|
+
return {
|
|
3965
|
+
verified: [],
|
|
3966
|
+
failed: [
|
|
3967
|
+
{
|
|
3968
|
+
filename: "<directory>",
|
|
3969
|
+
error: `Security: path traversal sequence detected in otter directory parameter`
|
|
3970
|
+
}
|
|
3971
|
+
],
|
|
3972
|
+
unknown: []
|
|
3973
|
+
};
|
|
3974
|
+
}
|
|
2921
3975
|
const verified = [];
|
|
2922
3976
|
const failed = [];
|
|
2923
3977
|
const unknown = [];
|
|
2924
3978
|
let entries;
|
|
2925
3979
|
try {
|
|
2926
|
-
entries =
|
|
3980
|
+
entries = readdirSync2(otterDir);
|
|
2927
3981
|
} catch (err) {
|
|
2928
3982
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2929
3983
|
return {
|
|
@@ -2934,9 +3988,9 @@ function verifyAllOtters(otterDir) {
|
|
|
2934
3988
|
}
|
|
2935
3989
|
const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
|
|
2936
3990
|
for (const filename of otterFiles) {
|
|
2937
|
-
const filePath =
|
|
3991
|
+
const filePath = join7(otterDir, filename);
|
|
2938
3992
|
try {
|
|
2939
|
-
if (
|
|
3993
|
+
if (lstatSync7(filePath).isSymbolicLink()) {
|
|
2940
3994
|
failed.push({ filename, error: "Skipped: symlink" });
|
|
2941
3995
|
continue;
|
|
2942
3996
|
}
|
|
@@ -2962,15 +4016,30 @@ var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packag
|
|
|
2962
4016
|
function resolveOtterDir() {
|
|
2963
4017
|
const cwd = process.cwd();
|
|
2964
4018
|
for (const relative of DEFAULT_SEARCH_PATHS) {
|
|
2965
|
-
const candidate =
|
|
4019
|
+
const candidate = join7(cwd, relative);
|
|
2966
4020
|
try {
|
|
2967
|
-
|
|
4021
|
+
lstatSync7(candidate);
|
|
2968
4022
|
return candidate;
|
|
2969
4023
|
} catch {
|
|
2970
4024
|
}
|
|
2971
4025
|
}
|
|
2972
4026
|
return null;
|
|
2973
4027
|
}
|
|
4028
|
+
function emitIntegrityAuditEvent(params) {
|
|
4029
|
+
const record = JSON.stringify({
|
|
4030
|
+
level: "AUDIT",
|
|
4031
|
+
event: "INTEGRITY_FAIL",
|
|
4032
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4033
|
+
source: "stackwright_pro_verify_otter_integrity",
|
|
4034
|
+
otterDir: params.otterDir,
|
|
4035
|
+
failedCount: params.failed.length,
|
|
4036
|
+
unknownCount: params.unknown.length,
|
|
4037
|
+
failures: params.failed,
|
|
4038
|
+
unknown: params.unknown
|
|
4039
|
+
});
|
|
4040
|
+
process.stderr.write(`INTEGRITY_FAIL ${record}
|
|
4041
|
+
`);
|
|
4042
|
+
}
|
|
2974
4043
|
function registerIntegrityTools(server2) {
|
|
2975
4044
|
server2.tool(
|
|
2976
4045
|
"stackwright_pro_verify_otter_integrity",
|
|
@@ -2994,6 +4063,13 @@ function registerIntegrityTools(server2) {
|
|
|
2994
4063
|
}
|
|
2995
4064
|
const result = verifyAllOtters(resolved);
|
|
2996
4065
|
const allGood = result.failed.length === 0 && result.unknown.length === 0;
|
|
4066
|
+
if (!allGood) {
|
|
4067
|
+
emitIntegrityAuditEvent({
|
|
4068
|
+
otterDir: resolved,
|
|
4069
|
+
failed: result.failed,
|
|
4070
|
+
unknown: result.unknown
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
2997
4073
|
return {
|
|
2998
4074
|
content: [
|
|
2999
4075
|
{
|
|
@@ -3006,7 +4082,10 @@ function registerIntegrityTools(server2) {
|
|
|
3006
4082
|
unknownCount: result.unknown.length,
|
|
3007
4083
|
verified: result.verified,
|
|
3008
4084
|
failed: result.failed,
|
|
3009
|
-
unknown: result.unknown
|
|
4085
|
+
unknown: result.unknown,
|
|
4086
|
+
...allGood ? {} : {
|
|
4087
|
+
error: "INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed \u2014 otter files may have been tampered with."
|
|
4088
|
+
}
|
|
3010
4089
|
})
|
|
3011
4090
|
}
|
|
3012
4091
|
],
|
|
@@ -3017,14 +4096,14 @@ function registerIntegrityTools(server2) {
|
|
|
3017
4096
|
}
|
|
3018
4097
|
|
|
3019
4098
|
// src/tools/domain.ts
|
|
3020
|
-
import { z as
|
|
3021
|
-
import { readFileSync as
|
|
3022
|
-
import { join as
|
|
4099
|
+
import { z as z14 } from "zod";
|
|
4100
|
+
import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
|
|
4101
|
+
import { join as join8 } from "path";
|
|
3023
4102
|
function handleListCollections(input) {
|
|
3024
4103
|
const cwd = input._cwd ?? process.cwd();
|
|
3025
4104
|
const sources = [
|
|
3026
4105
|
{
|
|
3027
|
-
path:
|
|
4106
|
+
path: join8(cwd, ".stackwright", "artifacts", "data-config.json"),
|
|
3028
4107
|
source: "data-config.json",
|
|
3029
4108
|
parse: (raw) => {
|
|
3030
4109
|
const parsed = JSON.parse(raw);
|
|
@@ -3035,15 +4114,15 @@ function handleListCollections(input) {
|
|
|
3035
4114
|
}
|
|
3036
4115
|
},
|
|
3037
4116
|
{
|
|
3038
|
-
path:
|
|
4117
|
+
path: join8(cwd, "stackwright.yml"),
|
|
3039
4118
|
source: "stackwright.yml",
|
|
3040
4119
|
parse: extractCollectionsFromYaml
|
|
3041
4120
|
}
|
|
3042
4121
|
];
|
|
3043
4122
|
for (const { path: path3, source, parse } of sources) {
|
|
3044
|
-
if (!
|
|
4123
|
+
if (!existsSync8(path3)) continue;
|
|
3045
4124
|
try {
|
|
3046
|
-
const collections = parse(
|
|
4125
|
+
const collections = parse(readFileSync7(path3, "utf8"));
|
|
3047
4126
|
return {
|
|
3048
4127
|
text: JSON.stringify({ collections, source, collectionCount: collections.length }),
|
|
3049
4128
|
isError: false
|
|
@@ -3194,8 +4273,8 @@ function handleValidateWorkflow(input) {
|
|
|
3194
4273
|
if (input.workflow && Object.keys(input.workflow).length > 0) {
|
|
3195
4274
|
raw = input.workflow;
|
|
3196
4275
|
} else {
|
|
3197
|
-
const artifactPath =
|
|
3198
|
-
if (!
|
|
4276
|
+
const artifactPath = join8(cwd, ".stackwright", "artifacts", "workflow-config.json");
|
|
4277
|
+
if (!existsSync8(artifactPath)) {
|
|
3199
4278
|
return fail([
|
|
3200
4279
|
{
|
|
3201
4280
|
code: "NO_WORKFLOW",
|
|
@@ -3204,7 +4283,7 @@ function handleValidateWorkflow(input) {
|
|
|
3204
4283
|
]);
|
|
3205
4284
|
}
|
|
3206
4285
|
try {
|
|
3207
|
-
raw = JSON.parse(
|
|
4286
|
+
raw = JSON.parse(readFileSync7(artifactPath, "utf8"));
|
|
3208
4287
|
} catch (err) {
|
|
3209
4288
|
return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
|
|
3210
4289
|
}
|
|
@@ -3403,7 +4482,7 @@ function registerDomainTools(server2) {
|
|
|
3403
4482
|
"stackwright_pro_resolve_data_strategy",
|
|
3404
4483
|
"Look up the data freshness strategy configuration from the user's answer. Returns mechanism, revalidation seconds, required packages, and the exact MCP tool call to make. Replaces the strategy table in the data-otter prompt.",
|
|
3405
4484
|
{
|
|
3406
|
-
strategy:
|
|
4485
|
+
strategy: z14.string().describe(
|
|
3407
4486
|
'The data-1 answer value: "pulse-fast", "isr-fast", "isr-standard", or "isr-slow"'
|
|
3408
4487
|
)
|
|
3409
4488
|
},
|
|
@@ -3413,7 +4492,7 @@ function registerDomainTools(server2) {
|
|
|
3413
4492
|
"stackwright_pro_validate_workflow",
|
|
3414
4493
|
"Validate a workflow definition against the Stackwright workflow schema. Checks step ID uniqueness, transition targets, terminal state existence, and service references. Call this after the workflow otter produces output.",
|
|
3415
4494
|
{
|
|
3416
|
-
workflow:
|
|
4495
|
+
workflow: jsonCoerce(z14.record(z14.string(), z14.unknown()).optional()).describe(
|
|
3417
4496
|
"Parsed workflow object. If omitted, reads from .stackwright/artifacts/workflow-config.json"
|
|
3418
4497
|
)
|
|
3419
4498
|
},
|
|
@@ -3421,20 +4500,112 @@ function registerDomainTools(server2) {
|
|
|
3421
4500
|
);
|
|
3422
4501
|
}
|
|
3423
4502
|
|
|
4503
|
+
// src/tools/type-schemas.ts
|
|
4504
|
+
import { z as z15 } from "zod";
|
|
4505
|
+
function buildTypeSchemaSummary() {
|
|
4506
|
+
return {
|
|
4507
|
+
version: "1.0",
|
|
4508
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4509
|
+
domains: {
|
|
4510
|
+
workflow: {
|
|
4511
|
+
description: "Workflow DSL \u2014 step definitions, auth blocks, field types, conditions",
|
|
4512
|
+
schemas: [
|
|
4513
|
+
"WorkflowFileSchema",
|
|
4514
|
+
"WorkflowDefinitionSchema",
|
|
4515
|
+
"WorkflowStepSchema",
|
|
4516
|
+
"WorkflowStepTypeSchema",
|
|
4517
|
+
"WorkflowFieldSchema",
|
|
4518
|
+
"WorkflowActionSchema",
|
|
4519
|
+
"WorkflowAuthSchema",
|
|
4520
|
+
"WorkflowThemeSchema",
|
|
4521
|
+
"TransitionConditionSchema",
|
|
4522
|
+
"PersistenceSchema"
|
|
4523
|
+
],
|
|
4524
|
+
otter: "stackwright-pro-workflow-otter",
|
|
4525
|
+
artifactKey: "workflowConfig"
|
|
4526
|
+
},
|
|
4527
|
+
auth: {
|
|
4528
|
+
description: "Authentication providers \u2014 PKI/CAC, OIDC, RBAC configuration",
|
|
4529
|
+
schemas: [
|
|
4530
|
+
"authConfigSchema",
|
|
4531
|
+
"pkiConfigSchema",
|
|
4532
|
+
"oidcConfigSchema",
|
|
4533
|
+
"rbacConfigSchema",
|
|
4534
|
+
"componentAuthSchema",
|
|
4535
|
+
"authUserSchema",
|
|
4536
|
+
"authSessionSchema"
|
|
4537
|
+
],
|
|
4538
|
+
otter: "stackwright-pro-auth-otter",
|
|
4539
|
+
artifactKey: "authConfig"
|
|
4540
|
+
},
|
|
4541
|
+
openapi: {
|
|
4542
|
+
description: "OpenAPI spec integration \u2014 collection config, endpoint filters, actions",
|
|
4543
|
+
interfaces: [
|
|
4544
|
+
"OpenAPIConfig",
|
|
4545
|
+
"ActionConfig",
|
|
4546
|
+
"EndpointFilter",
|
|
4547
|
+
"ApprovedSpec",
|
|
4548
|
+
"PrebuildSecurityConfig",
|
|
4549
|
+
"SiteConfig",
|
|
4550
|
+
"ValidationResult"
|
|
4551
|
+
],
|
|
4552
|
+
otter: "stackwright-pro-api-otter",
|
|
4553
|
+
artifactKey: "apiConfig"
|
|
4554
|
+
},
|
|
4555
|
+
pulse: {
|
|
4556
|
+
description: "Real-time data polling \u2014 source-agnostic polling options and states",
|
|
4557
|
+
interfaces: ["PulseOptions", "PulseMeta", "PulseState"],
|
|
4558
|
+
note: "React-bound types (PulseProps, PulseIndicatorProps) remain in @stackwright-pro/pulse"
|
|
4559
|
+
},
|
|
4560
|
+
enterprise: {
|
|
4561
|
+
description: "Enterprise collection access \u2014 multi-tenant provider extension",
|
|
4562
|
+
interfaces: [
|
|
4563
|
+
"EnterpriseCollectionProvider",
|
|
4564
|
+
"CollectionProvider",
|
|
4565
|
+
"CollectionEntry",
|
|
4566
|
+
"CollectionListOptions",
|
|
4567
|
+
"CollectionListResult",
|
|
4568
|
+
"TenantFilter",
|
|
4569
|
+
"Collection"
|
|
4570
|
+
],
|
|
4571
|
+
note: "CollectionProvider, CollectionEntry, CollectionListOptions, and CollectionListResult are re-exported from @stackwright/types (^1.5.0). EnterpriseCollectionProvider, TenantFilter, and Collection are Pro-only extensions."
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
};
|
|
4575
|
+
}
|
|
4576
|
+
function registerTypeSchemasTool(server2) {
|
|
4577
|
+
server2.tool(
|
|
4578
|
+
"stackwright_pro_get_type_schemas",
|
|
4579
|
+
"Returns a structured summary of all canonical @stackwright-pro/types schemas, organized by domain. Use this to determine which otter owns a given schema and what artifact key to expect.",
|
|
4580
|
+
{
|
|
4581
|
+
format: z15.enum(["full", "domains-only"]).optional().default("full").describe("full = complete summary with all fields; domains-only = just domain names")
|
|
4582
|
+
},
|
|
4583
|
+
async ({ format }) => {
|
|
4584
|
+
const summary = buildTypeSchemaSummary();
|
|
4585
|
+
const output = format === "domains-only" ? Object.keys(summary.domains) : summary;
|
|
4586
|
+
return {
|
|
4587
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
|
|
4588
|
+
};
|
|
4589
|
+
}
|
|
4590
|
+
);
|
|
4591
|
+
}
|
|
4592
|
+
|
|
3424
4593
|
// package.json
|
|
3425
4594
|
var package_default = {
|
|
3426
4595
|
dependencies: {
|
|
4596
|
+
"@stackwright-pro/types": "workspace:*",
|
|
3427
4597
|
"@modelcontextprotocol/sdk": "^1.10.0",
|
|
3428
4598
|
"@stackwright-pro/cli-data-explorer": "workspace:*",
|
|
3429
|
-
zod: "^4.3
|
|
4599
|
+
zod: "^4.4.3"
|
|
3430
4600
|
},
|
|
3431
4601
|
devDependencies: {
|
|
3432
|
-
"@types/node": "
|
|
3433
|
-
tsup: "
|
|
3434
|
-
typescript: "
|
|
3435
|
-
vitest: "
|
|
4602
|
+
"@types/node": "catalog:",
|
|
4603
|
+
tsup: "catalog:",
|
|
4604
|
+
typescript: "catalog:",
|
|
4605
|
+
vitest: "catalog:"
|
|
3436
4606
|
},
|
|
3437
4607
|
scripts: {
|
|
4608
|
+
prepublishOnly: "node scripts/verify-integrity-sync.js",
|
|
3438
4609
|
build: "tsup",
|
|
3439
4610
|
dev: "tsup --watch",
|
|
3440
4611
|
start: "node dist/server.js",
|
|
@@ -3442,10 +4613,13 @@ var package_default = {
|
|
|
3442
4613
|
"test:coverage": "vitest run --coverage"
|
|
3443
4614
|
},
|
|
3444
4615
|
name: "@stackwright-pro/mcp",
|
|
3445
|
-
version: "0.2.0-alpha.
|
|
4616
|
+
version: "0.2.0-alpha.49",
|
|
3446
4617
|
description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
|
|
3447
|
-
license: "
|
|
4618
|
+
license: "SEE LICENSE IN LICENSE",
|
|
3448
4619
|
main: "./dist/server.js",
|
|
4620
|
+
bin: {
|
|
4621
|
+
"stackwright-pro-mcp": "./dist/server.js"
|
|
4622
|
+
},
|
|
3449
4623
|
module: "./dist/server.mjs",
|
|
3450
4624
|
types: "./dist/server.d.ts",
|
|
3451
4625
|
exports: {
|
|
@@ -3458,6 +4632,11 @@ var package_default = {
|
|
|
3458
4632
|
types: "./dist/integrity.d.ts",
|
|
3459
4633
|
import: "./dist/integrity.mjs",
|
|
3460
4634
|
require: "./dist/integrity.js"
|
|
4635
|
+
},
|
|
4636
|
+
"./type-schemas": {
|
|
4637
|
+
types: "./dist/tools/type-schemas.d.ts",
|
|
4638
|
+
import: "./dist/tools/type-schemas.mjs",
|
|
4639
|
+
require: "./dist/tools/type-schemas.js"
|
|
3461
4640
|
}
|
|
3462
4641
|
},
|
|
3463
4642
|
files: [
|
|
@@ -3485,7 +4664,9 @@ registerPipelineTools(server);
|
|
|
3485
4664
|
registerSafeWriteTools(server);
|
|
3486
4665
|
registerAuthTools(server);
|
|
3487
4666
|
registerIntegrityTools(server);
|
|
4667
|
+
registerArtifactSigningTools(server);
|
|
3488
4668
|
registerDomainTools(server);
|
|
4669
|
+
registerTypeSchemasTool(server);
|
|
3489
4670
|
async function main() {
|
|
3490
4671
|
const transport = new StdioServerTransport();
|
|
3491
4672
|
await server.connect(transport);
|