@stackwright-pro/mcp 0.2.0-alpha.0 → 0.2.0-alpha.10

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/dist/server.js CHANGED
@@ -111,8 +111,8 @@ function registerDataExplorerTools(server2) {
111
111
  lines.push(" endpoints:");
112
112
  if (result.filter.include && result.filter.include.length > 0) {
113
113
  lines.push(" include:");
114
- for (const path4 of result.filter.include) {
115
- lines.push(` - ${path4}`);
114
+ for (const path3 of result.filter.include) {
115
+ lines.push(` - ${path3}`);
116
116
  }
117
117
  }
118
118
  if (result.filter.exclude && result.filter.exclude.length > 0) {
@@ -142,6 +142,7 @@ function registerDataExplorerTools(server2) {
142
142
 
143
143
  // src/tools/security.ts
144
144
  var import_zod2 = require("zod");
145
+ var import_crypto = require("crypto");
145
146
  var import_fs = __toESM(require("fs"));
146
147
  var import_path = __toESM(require("path"));
147
148
  function registerSecurityTools(server2) {
@@ -219,8 +220,7 @@ This spec is not on the approved-specs allowlist. Add it using stackwright_pro_a
219
220
  if (!specPath.startsWith("http://") && !specPath.startsWith("https://")) {
220
221
  if (import_fs.default.existsSync(specPath)) {
221
222
  const specContent = import_fs.default.readFileSync(specPath, "utf8");
222
- const crypto = require("crypto");
223
- const sha256 = crypto.createHash("sha256").update(specContent).digest("hex");
223
+ const sha256 = (0, import_crypto.createHash)("sha256").update(specContent).digest("hex");
224
224
  if (sha256 !== matchingSpec.sha256) {
225
225
  return {
226
226
  content: [
@@ -271,8 +271,7 @@ Status: Valid (${allowlist.length} specs on allowlist)`
271
271
  sha256 = "<computed-at-build>";
272
272
  } else if (import_fs.default.existsSync(url)) {
273
273
  const specContent = import_fs.default.readFileSync(url, "utf8");
274
- const crypto = require("crypto");
275
- sha256 = crypto.createHash("sha256").update(specContent).digest("hex");
274
+ sha256 = (0, import_crypto.createHash)("sha256").update(specContent).digest("hex");
276
275
  } else {
277
276
  return {
278
277
  content: [
@@ -647,297 +646,165 @@ ${yaml}
647
646
 
648
647
  // src/tools/clarification.ts
649
648
  var import_zod5 = require("zod");
650
- var import_child_process = require("child_process");
651
- var import_os = require("os");
652
- var import_path2 = require("path");
653
- var import_crypto = require("crypto");
654
- var import_fs2 = require("fs");
655
- var activeServers = /* @__PURE__ */ new Map();
656
- async function startPythonServer(sessionId) {
657
- const socketPath = (0, import_path2.join)((0, import_os.tmpdir)(), `otter-raft-${(0, import_crypto.randomUUID)()}.sock`);
658
- const port = 8765 + Math.floor(Math.random() * 100);
659
- return new Promise((resolve, reject) => {
660
- const pythonPath = process.platform === "win32" ? "python" : "python3";
661
- const packageRoot = findPythonPackageRoot();
662
- const proc = (0, import_child_process.spawn)(
663
- pythonPath,
664
- ["-m", "stackwright_pro.raft.server", "--socket", socketPath, "--port", String(port)],
665
- {
666
- stdio: ["pipe", "pipe", "pipe"],
667
- env: {
668
- ...process.env,
669
- PYTHONPATH: packageRoot
670
- }
671
- }
672
- );
673
- activeServers.set(sessionId, proc);
674
- let startupOutput = "";
675
- let started = false;
676
- proc.stdout?.on("data", (data) => {
677
- startupOutput += data.toString();
678
- if (startupOutput.includes("Server ready") || startupOutput.includes("HTTP server")) {
679
- started = true;
680
- resolve({ port, socketPath });
681
- }
682
- });
683
- proc.stderr?.on("data", (data) => {
684
- if (!startupOutput.includes("Starting")) {
685
- console.error("[Python Clarification]", data.toString().trim());
686
- }
687
- });
688
- proc.on("error", (err) => {
689
- if (!started) {
690
- activeServers.delete(sessionId);
691
- reject(new Error(`Failed to start Python server: ${err.message}`));
692
- }
693
- });
694
- proc.on("exit", (code) => {
695
- activeServers.delete(sessionId);
696
- if ((0, import_fs2.existsSync)(socketPath)) {
697
- try {
698
- (0, import_fs2.unlinkSync)(socketPath);
699
- } catch {
700
- }
701
- }
702
- });
703
- setTimeout(() => {
704
- if (!started) {
705
- proc.kill();
706
- activeServers.delete(sessionId);
707
- reject(new Error("Python server startup timeout"));
708
- }
709
- }, 1e4);
710
- });
711
- }
712
- async function stopPythonServer(sessionId) {
713
- const proc = activeServers.get(sessionId);
714
- if (proc) {
715
- proc.kill("SIGTERM");
716
- activeServers.delete(sessionId);
717
- }
718
- }
719
- async function httpRequest(host, port, path4, body) {
720
- const http = await import("http");
721
- return new Promise((resolve, reject) => {
722
- const url = new URL(path4, `http://${host}:${port}`);
723
- const reqOptions = {
724
- hostname: host,
725
- port,
726
- path: url.pathname,
727
- method: body ? "POST" : "GET",
728
- headers: {
729
- "Content-Type": "application/json"
730
- }
649
+ var CONTRADICTION_PATTERNS = [
650
+ {
651
+ keywords: ["minimal", "clean", "simple"],
652
+ conflicts: ["vibrant", "rich", "content-heavy", "playful"]
653
+ },
654
+ {
655
+ keywords: ["dark", "dark mode"],
656
+ conflicts: ["light mode only", "light"]
657
+ },
658
+ {
659
+ keywords: ["enterprise", "professional", "corporate"],
660
+ conflicts: ["playful", "casual", "fun"]
661
+ },
662
+ {
663
+ keywords: ["accessible", "wcag", "section 508"],
664
+ conflicts: ["compact", "dense", "small text"]
665
+ },
666
+ {
667
+ keywords: ["government", "defense", "federal"],
668
+ conflicts: ["public", "no auth", "consumer"]
669
+ }
670
+ ];
671
+ function handleClarify(input) {
672
+ const { question_type, question, choices, priority, target_field, context } = input;
673
+ const base = {
674
+ action: "ask_user",
675
+ targetField: target_field
676
+ };
677
+ if (question_type === "closed_choice" && choices && choices.length > 0) {
678
+ const header = truncateHeader(question);
679
+ base.adaptedQuestion = {
680
+ question,
681
+ header,
682
+ options: choices.map((c) => ({ label: c }))
731
683
  };
732
- const req = http.request(reqOptions, (res) => {
733
- let data = "";
734
- res.on("data", (chunk) => {
735
- data += chunk.toString();
736
- });
737
- res.on("end", () => {
738
- try {
739
- const parsed = JSON.parse(data);
740
- if (res.statusCode && res.statusCode >= 400) {
741
- reject(new Error(parsed.detail || parsed.error || `HTTP ${res.statusCode}`));
742
- } else {
743
- resolve(parsed);
744
- }
745
- } catch (e) {
746
- reject(new Error(`Failed to parse response: ${data}`));
747
- }
748
- });
749
- });
750
- req.on("error", reject);
751
- if (body) {
752
- req.write(JSON.stringify(body));
753
- }
754
- req.end();
755
- });
684
+ } else {
685
+ const contextPrefix = context ? `Context: ${context}
686
+
687
+ ` : "";
688
+ base.prompt = `${contextPrefix}${question}`;
689
+ }
690
+ if (priority === "optional") {
691
+ base.suggestedDefault = inferDefault(question_type, choices);
692
+ }
693
+ return base;
756
694
  }
757
- function findPythonPackageRoot() {
758
- const candidates = [
759
- (0, import_path2.join)(__dirname, "../../python/src"),
760
- (0, import_path2.join)(__dirname, "../../../python/src"),
761
- (0, import_path2.join)(process.cwd(), "python/src")
762
- ];
763
- for (const candidate of candidates) {
764
- if ((0, import_fs2.existsSync)(candidate)) {
765
- return candidate;
695
+ function handleDetectConflict(input) {
696
+ const preferenceLower = input.stated_preference.toLowerCase();
697
+ const valuesLower = Object.values(input.selected_values).map((v) => v.toLowerCase());
698
+ for (const pattern of CONTRADICTION_PATTERNS) {
699
+ const preferenceMatch = pattern.keywords.some((kw) => preferenceLower.includes(kw));
700
+ if (!preferenceMatch) continue;
701
+ const conflictingValues = valuesLower.filter(
702
+ (val) => pattern.conflicts.some((c) => val.includes(c))
703
+ );
704
+ if (conflictingValues.length > 0) {
705
+ const matchedKeywords = pattern.keywords.filter((kw) => preferenceLower.includes(kw));
706
+ const primaryKeyword = matchedKeywords[0] ?? "preference";
707
+ return {
708
+ conflict: true,
709
+ description: `Stated preference includes "${matchedKeywords.join(", ")}" but selections contain "${conflictingValues.join(", ")}" \u2014 these are contradictory.`,
710
+ resolution_options: [
711
+ `Align selections with the "${primaryKeyword}" preference`,
712
+ `Revise stated preference to match current selections`,
713
+ "Ask user which direction they actually want"
714
+ ]
715
+ };
766
716
  }
767
717
  }
768
- return process.cwd();
718
+ return { conflict: false };
719
+ }
720
+ function truncateHeader(text) {
721
+ const MAX = 50;
722
+ if (text.length <= MAX) return text;
723
+ return text.slice(0, MAX - 1) + "\u2026";
724
+ }
725
+ function inferDefault(questionType, choices) {
726
+ if (questionType === "closed_choice" && choices && choices.length > 0) {
727
+ return choices[0] ?? "(first choice)";
728
+ }
729
+ return "(use sensible project default)";
769
730
  }
770
731
  function registerClarificationTools(server2) {
771
732
  server2.tool(
772
733
  "stackwright_pro_clarify",
773
- "Ask the user for clarification when an otter encounters ambiguity. This is for MID-EXECUTION questions, not upfront question collection. Use this when the otter needs user input to proceed. Returns the user's decision which should be used to continue execution.",
734
+ "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).",
774
735
  {
775
736
  context: import_zod5.z.string().optional().describe("Context about what the otter is trying to do"),
776
- question_type: import_zod5.z.enum(["closed_choice", "open_text", "conditional", "multi_step", "reconciliation"]).describe("Type of question being asked"),
737
+ question_type: import_zod5.z.enum(["closed_choice", "open_text"]).describe("Type of question being asked"),
777
738
  question: import_zod5.z.string().describe("The clarification question to ask the user"),
778
- choices: import_zod5.z.array(import_zod5.z.string()).optional().describe("Options for closed_choice or multi_step questions"),
779
- priority: import_zod5.z.enum(["blocking", "preferred", "optional"]).optional().describe("How critical is this clarification? Default: preferred"),
739
+ choices: import_zod5.z.array(import_zod5.z.string()).optional().describe("Options for closed_choice questions"),
740
+ priority: import_zod5.z.enum(["blocking", "preferred", "optional"]).optional().default("preferred").describe("How critical is this clarification? Default: preferred"),
780
741
  target_field: import_zod5.z.string().optional().describe("What field/config does this clarify?")
781
742
  },
782
- async ({ context, question_type, question, choices, priority = "preferred", target_field }) => {
783
- const sessionId = `mcp_${(0, import_crypto.randomUUID)().slice(0, 8)}`;
784
- try {
785
- const { port } = await startPythonServer(sessionId);
786
- await httpRequest(port === 8765 ? "127.0.0.1" : "127.0.0.1", port, "/sessions", {});
787
- if (context) {
788
- await httpRequest(
789
- port === 8765 ? "127.0.0.1" : "127.0.0.1",
790
- port,
791
- `/sessions/${sessionId}/context`,
792
- { context: { purpose: context } }
793
- );
794
- }
795
- const request = {
796
- ...context !== void 0 && { context },
797
- question_type,
798
- question,
799
- ...choices !== void 0 && { choices },
800
- priority,
801
- ...target_field !== void 0 && { target_field }
802
- };
803
- const response = await httpRequest(
804
- port === 8765 ? "127.0.0.1" : "127.0.0.1",
805
- port,
806
- "/clarify",
807
- { request }
808
- );
809
- await stopPythonServer(sessionId);
810
- const decision = response.decision;
811
- const value = decision.value;
812
- const source = decision.source;
813
- const explicit = decision.explicit ? "explicitly" : "via fallback";
814
- if (response.fallback_used) {
815
- return {
816
- content: [
817
- {
818
- type: "text",
819
- text: `\u26A0\uFE0F Clarification fallback used: ${response.fallback_reason || "No user input available"}
820
-
821
- Default value used: ${JSON.stringify(value)}
822
-
823
- \u{1F4A1} Consider following up with the user later if this default isn't appropriate.`
824
- }
825
- ]
826
- };
827
- }
828
- return {
829
- content: [
830
- {
831
- type: "text",
832
- text: `\u2705 User clarified (${source}): ${JSON.stringify(value)}
833
-
834
- Use this value to continue execution.`
835
- }
836
- ]
837
- };
838
- } catch (error) {
839
- await stopPythonServer(sessionId);
840
- return {
841
- content: [
842
- {
843
- type: "text",
844
- text: `\u274C Clarification failed: ${error instanceof Error ? error.message : "Unknown error"}
845
-
846
- Cannot proceed without user input. Consider:
847
- 1. Using a reasonable default
848
- 2. Asking the user directly in your response
849
- 3. Skipping this step if optional`
850
- }
851
- ],
852
- isError: true
853
- };
854
- }
743
+ async ({ context, question_type, question, choices, priority, target_field }) => {
744
+ const result = handleClarify({
745
+ context: context ?? void 0,
746
+ question_type,
747
+ question,
748
+ choices: choices ?? void 0,
749
+ priority: priority ?? "preferred",
750
+ target_field: target_field ?? void 0
751
+ });
752
+ return {
753
+ content: [
754
+ {
755
+ type: "text",
756
+ text: `\u{1F9A6} Clarification needed${target_field ? ` for "${target_field}"` : ""}. Present the following to the user. ` + (result.adaptedQuestion ? "Pass the adaptedQuestion to ask_user_question directly." : "Ask the user the prompt below.")
757
+ },
758
+ {
759
+ type: "text",
760
+ text: JSON.stringify(result)
761
+ }
762
+ ]
763
+ };
855
764
  }
856
765
  );
857
766
  server2.tool(
858
767
  "stackwright_pro_detect_conflict",
859
- "Detect when a user's stated preference conflicts with their selected choices. Use this to identify when users might be indecisive or misunderstood a question. Returns conflict details and resolution options.",
768
+ "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.",
860
769
  {
861
770
  stated_preference: import_zod5.z.string().describe("What the user said they wanted"),
862
- selected_values: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.string()).describe("What the user actually selected")
771
+ selected_values: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.string()).describe("What the user actually selected (field \u2192 value)")
863
772
  },
864
773
  async ({ stated_preference, selected_values }) => {
865
- const sessionId = `mcp_${(0, import_crypto.randomUUID)().slice(0, 8)}`;
866
- try {
867
- const { port } = await startPythonServer(sessionId);
868
- const response = await httpRequest(
869
- port === 8765 ? "127.0.0.1" : "127.0.0.1",
870
- port,
871
- "/conflict",
872
- { stated_preference, selected_values }
873
- );
874
- await stopPythonServer(sessionId);
875
- if (response.conflict) {
876
- const data = response.data;
877
- return {
878
- content: [
879
- {
880
- type: "text",
881
- text: `\u26A0\uFE0F CONFLICT DETECTED
774
+ const result = handleDetectConflict({ stated_preference, selected_values });
775
+ if (result.conflict) {
776
+ return {
777
+ content: [
778
+ {
779
+ type: "text",
780
+ text: `\u26A0\uFE0F CONFLICT DETECTED
882
781
 
883
782
  User stated: "${stated_preference}"
884
783
  But selected: ${JSON.stringify(selected_values)}
885
784
 
886
- Conflict: ${data.description}
785
+ ${result.description}
887
786
 
888
- Resolution options: ${data.options?.join(", ") || "Ask user to clarify"}
787
+ Resolution options:
788
+ ${result.resolution_options?.map((o) => ` \u2022 ${o}`).join("\n")}
889
789
 
890
790
  \u{1F4A1} Consider asking the user to reconcile this conflict.`
891
- }
892
- ]
893
- };
894
- }
895
- return {
896
- content: [
791
+ },
897
792
  {
898
793
  type: "text",
899
- text: `\u2705 No conflict detected between stated preference and selections.`
794
+ text: JSON.stringify(result)
900
795
  }
901
796
  ]
902
797
  };
903
- } catch (error) {
904
- await stopPythonServer(sessionId);
905
- return {
906
- content: [
907
- {
908
- type: "text",
909
- text: `\u274C Conflict detection failed: ${error instanceof Error ? error.message : "Unknown error"}`
910
- }
911
- ],
912
- isError: true
913
- };
914
798
  }
915
- }
916
- );
917
- server2.tool(
918
- "stackwright_pro_get_defaults",
919
- "Get the current clarification defaults from config. Use this to understand what fallback values will be used if user doesn't provide input.",
920
- {
921
- config_path: import_zod5.z.string().optional().describe("Path to config file. Default: .stackwright/clarification.yaml")
922
- },
923
- async ({ config_path }) => {
924
799
  return {
925
800
  content: [
926
801
  {
927
802
  type: "text",
928
- text: `\u{1F4CB} Clarification defaults
929
-
930
- Configuration is loaded from:
931
- - Environment: CLARIFICATION_* variables
932
- - Config file: ${config_path || ".stackwright/clarification.yaml"}
933
- - CLI args: --clarify-* flags
934
-
935
- Default behaviors:
936
- - allow_dont_know: true (users can skip)
937
- - default_timeout: 120 seconds
938
- - channel_priority: [tui, cli_args, config, defaults]
939
-
940
- \u{1F4A1} Set CLARIFICATION_DEFAULT_<FIELD>=value to change defaults.`
803
+ text: "\u2705 No conflict detected between stated preference and selections."
804
+ },
805
+ {
806
+ type: "text",
807
+ text: JSON.stringify(result)
941
808
  }
942
809
  ]
943
810
  };
@@ -947,38 +814,48 @@ Default behaviors:
947
814
 
948
815
  // src/tools/packages.ts
949
816
  var import_zod6 = require("zod");
950
- var import_fs3 = require("fs");
951
- var import_child_process2 = require("child_process");
952
- var import_path3 = __toESM(require("path"));
817
+ var import_fs2 = require("fs");
818
+ var import_child_process = require("child_process");
819
+ var import_path2 = __toESM(require("path"));
820
+ var BASELINE_DEPS = {
821
+ "@stackwright-pro/mcp": "latest",
822
+ "@stackwright-pro/otters": "latest",
823
+ "@stackwright-pro/openapi": "latest",
824
+ "@stackwright-pro/auth": "latest",
825
+ "@stackwright-pro/auth-nextjs": "latest",
826
+ zod: "^3.23.0"
827
+ };
828
+ var BASELINE_DEV_DEPS = {
829
+ "@stoplight/prism-cli": "^5.14.2"
830
+ };
953
831
  function registerPackageTools(server2) {
954
832
  server2.tool(
955
833
  "stackwright_pro_setup_packages",
956
- "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.",
834
+ "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.",
957
835
  {
958
836
  // FIX 3 (B-new-1): Zod v4 requires two-arg z.record(keySchema, valueSchema)
959
837
  packages: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).describe(
960
838
  'Dependencies to add. Record<packageName, version>. e.g. { "@stackwright-pro/auth": "latest" }'
961
839
  ),
962
- devPackages: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional().describe(
963
- "devDependencies to add. Same format as packages."
964
- ),
965
- scripts: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional().describe(
966
- "npm scripts to add. Only adds if key does not already exist."
967
- ),
840
+ devPackages: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional().describe("devDependencies to add. Same format as packages."),
841
+ scripts: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional().describe("npm scripts to add. Only adds if key does not already exist."),
968
842
  targetDir: import_zod6.z.string().optional().describe(
969
843
  "Project directory containing package.json. Defaults to process.cwd(). Must be an absolute path within the current working directory."
970
844
  ),
971
- runInstall: import_zod6.z.boolean().optional().default(true).describe(
972
- "Run pnpm install after writing package.json. Defaults to true."
845
+ runInstall: import_zod6.z.boolean().optional().default(true).describe("Run pnpm install after writing package.json. Defaults to true."),
846
+ includeBaseline: import_zod6.z.boolean().optional().default(false).describe(
847
+ "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."
973
848
  )
974
849
  },
975
- async ({ packages, devPackages, scripts, targetDir, runInstall }) => {
850
+ async ({ packages, devPackages, scripts, targetDir, runInstall, includeBaseline }) => {
851
+ const mergedPackages = includeBaseline ? { ...BASELINE_DEPS, ...packages } : { ...packages };
852
+ const mergedDevPackages = includeBaseline ? { ...BASELINE_DEV_DEPS, ...devPackages ?? {} } : devPackages;
976
853
  const result = setupPackages({
977
- packages,
978
- devPackages,
979
- scripts,
980
- targetDir,
981
- runInstall
854
+ packages: mergedPackages,
855
+ runInstall,
856
+ ...mergedDevPackages !== void 0 ? { devPackages: mergedDevPackages } : {},
857
+ ...scripts !== void 0 ? { scripts } : {},
858
+ ...targetDir !== void 0 ? { targetDir } : {}
982
859
  });
983
860
  const statusLine = result.success ? `\u2705 package.json updated: ${result.packageJsonPath}` : `\u274C Failed: ${result.error}`;
984
861
  const lines = [
@@ -1014,8 +891,8 @@ function setupPackages(opts) {
1014
891
  };
1015
892
  try {
1016
893
  const cwd = process.cwd();
1017
- const resolvedTarget = opts.targetDir ? import_path3.default.resolve(opts.targetDir) : cwd;
1018
- const cwdWithSep = cwd.endsWith(import_path3.default.sep) ? cwd : cwd + import_path3.default.sep;
894
+ const resolvedTarget = opts.targetDir ? import_path2.default.resolve(opts.targetDir) : cwd;
895
+ const cwdWithSep = cwd.endsWith(import_path2.default.sep) ? cwd : cwd + import_path2.default.sep;
1019
896
  if (resolvedTarget !== cwd && !resolvedTarget.startsWith(cwdWithSep)) {
1020
897
  return {
1021
898
  success: false,
@@ -1028,8 +905,8 @@ function setupPackages(opts) {
1028
905
  error: `Path traversal rejected: target directory is outside the allowed working directory`
1029
906
  };
1030
907
  }
1031
- const preResolvePackageJsonPath = import_path3.default.join(resolvedTarget, "package.json");
1032
- if (!(0, import_fs3.existsSync)(preResolvePackageJsonPath)) {
908
+ const preResolvePackageJsonPath = import_path2.default.join(resolvedTarget, "package.json");
909
+ if (!(0, import_fs2.existsSync)(preResolvePackageJsonPath)) {
1033
910
  return {
1034
911
  success: false,
1035
912
  ...emptyResult,
@@ -1038,7 +915,7 @@ function setupPackages(opts) {
1038
915
  }
1039
916
  let realTarget;
1040
917
  try {
1041
- realTarget = (0, import_fs3.realpathSync)(resolvedTarget);
918
+ realTarget = (0, import_fs2.realpathSync)(resolvedTarget);
1042
919
  } catch {
1043
920
  return {
1044
921
  success: false,
@@ -1050,8 +927,8 @@ function setupPackages(opts) {
1050
927
  error: `Could not resolve real path of target directory`
1051
928
  };
1052
929
  }
1053
- const realCwd = (0, import_fs3.realpathSync)(cwd);
1054
- const realCwdWithSep = realCwd.endsWith(import_path3.default.sep) ? realCwd : realCwd + import_path3.default.sep;
930
+ const realCwd = (0, import_fs2.realpathSync)(cwd);
931
+ const realCwdWithSep = realCwd.endsWith(import_path2.default.sep) ? realCwd : realCwd + import_path2.default.sep;
1055
932
  if (realTarget !== realCwd && !realTarget.startsWith(realCwdWithSep)) {
1056
933
  return {
1057
934
  success: false,
@@ -1064,8 +941,8 @@ function setupPackages(opts) {
1064
941
  error: `Path traversal rejected: target directory resolved to a location outside the allowed working directory`
1065
942
  };
1066
943
  }
1067
- const realPackageJsonPath = import_path3.default.join(realTarget, "package.json");
1068
- const pkgStat = (0, import_fs3.lstatSync)(realPackageJsonPath);
944
+ const realPackageJsonPath = import_path2.default.join(realTarget, "package.json");
945
+ const pkgStat = (0, import_fs2.lstatSync)(realPackageJsonPath);
1069
946
  if (pkgStat.isSymbolicLink()) {
1070
947
  return {
1071
948
  ...emptyResult,
@@ -1124,7 +1001,7 @@ function setupPackages(opts) {
1124
1001
  }
1125
1002
  }
1126
1003
  }
1127
- const raw = (0, import_fs3.readFileSync)(realPackageJsonPath, "utf8");
1004
+ const raw = (0, import_fs2.readFileSync)(realPackageJsonPath, "utf8");
1128
1005
  const PackageJsonSchema = import_zod6.z.object({
1129
1006
  // Zod v4: z.record(keySchema, valueSchema) — two-arg form required
1130
1007
  dependencies: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional(),
@@ -1178,12 +1055,12 @@ function setupPackages(opts) {
1178
1055
  }
1179
1056
  }
1180
1057
  }
1181
- (0, import_fs3.writeFileSync)(realPackageJsonPath, JSON.stringify(parsed, null, 2) + "\n");
1058
+ (0, import_fs2.writeFileSync)(realPackageJsonPath, JSON.stringify(parsed, null, 2) + "\n");
1182
1059
  let installed = false;
1183
1060
  let installError;
1184
1061
  if (opts.runInstall) {
1185
1062
  try {
1186
- (0, import_child_process2.execSync)("pnpm install", { cwd: realTarget, stdio: "pipe", timeout: 6e4 });
1063
+ (0, import_child_process.execSync)("pnpm install", { cwd: realTarget, stdio: "pipe", timeout: 6e4 });
1187
1064
  installed = true;
1188
1065
  } catch (err) {
1189
1066
  installed = false;
@@ -1201,17 +1078,2426 @@ function setupPackages(opts) {
1201
1078
  ...installError !== void 0 ? { installError } : {}
1202
1079
  };
1203
1080
  } catch (err) {
1204
- return { ...emptyResult, success: false, error: err instanceof Error ? err.message : String(err) };
1081
+ return {
1082
+ ...emptyResult,
1083
+ success: false,
1084
+ error: err instanceof Error ? err.message : String(err)
1085
+ };
1205
1086
  }
1206
1087
  }
1207
1088
 
1208
- // src/server.ts
1209
- var import_fs4 = require("fs");
1210
- var import_path4 = __toESM(require("path"));
1211
- var packageJson = JSON.parse((0, import_fs4.readFileSync)(import_path4.default.join(__dirname, "package.json"), "utf8"));
1089
+ // src/tools/questions.ts
1090
+ var import_promises = require("fs/promises");
1091
+ var import_node_path = require("path");
1092
+ var import_zod7 = require("zod");
1093
+
1094
+ // src/question-adapter.ts
1095
+ function truncate(str, maxLength) {
1096
+ if (str.length <= maxLength) return str;
1097
+ return str.substring(0, maxLength - 1) + "\u2026";
1098
+ }
1099
+ function generateHeader(id) {
1100
+ const parts = id.split("-");
1101
+ if (parts.length >= 2) {
1102
+ const prefix = parts[0].toUpperCase().substring(0, 4);
1103
+ const num = parts[1];
1104
+ return truncate(`${prefix}-${num}`, 12);
1105
+ }
1106
+ return truncate(
1107
+ id.toUpperCase().replace(/[^A-Z0-9]/g, "").substring(0, 12),
1108
+ 12
1109
+ );
1110
+ }
1111
+ function generateDefaultOptions(type) {
1112
+ switch (type) {
1113
+ case "confirm":
1114
+ return [
1115
+ { label: "Yes", description: "Enable or confirm this option" },
1116
+ { label: "No", description: "Disable or decline this option" }
1117
+ ];
1118
+ case "text":
1119
+ return [
1120
+ { label: "Specify", description: "I will provide a specific value" },
1121
+ { label: "Skip", description: "Use default or skip this question" }
1122
+ ];
1123
+ default:
1124
+ return [
1125
+ { label: "Option 1", description: "First option" },
1126
+ { label: "Option 2", description: "Second option" }
1127
+ ];
1128
+ }
1129
+ }
1130
+ function adaptQuestion(q) {
1131
+ const header = generateHeader(q.id);
1132
+ const multiSelect = q.type === "multi-select";
1133
+ let options;
1134
+ if (q.options && q.options.length >= 2) {
1135
+ options = q.options.map((opt) => ({
1136
+ label: truncate(opt.label, 50),
1137
+ description: opt.value !== opt.label ? opt.value : void 0
1138
+ }));
1139
+ } else if (q.options && q.options.length === 1) {
1140
+ options = [
1141
+ ...q.options.map((opt) => ({ label: truncate(opt.label, 50), description: opt.value })),
1142
+ { label: "Other", description: "Specify a different value" }
1143
+ ];
1144
+ } else {
1145
+ options = generateDefaultOptions(q.type);
1146
+ }
1147
+ if (options.length < 2) {
1148
+ options.push({ label: "Other", description: "Alternative option" });
1149
+ }
1150
+ options = options.slice(0, 6);
1151
+ return {
1152
+ question: q.question + (q.help ? `
1153
+
1154
+ ${q.help}` : ""),
1155
+ header,
1156
+ multi_select: multiSelect,
1157
+ options
1158
+ };
1159
+ }
1160
+ function adaptQuestions(questions, answers = {}) {
1161
+ const adapted = [];
1162
+ for (const q of questions) {
1163
+ if (q.dependsOn) {
1164
+ const dependsAnswer = answers[q.dependsOn.questionId];
1165
+ if (dependsAnswer === void 0) {
1166
+ continue;
1167
+ }
1168
+ const expectedValues = Array.isArray(q.dependsOn.value) ? q.dependsOn.value : [q.dependsOn.value];
1169
+ const answerValue = Array.isArray(dependsAnswer) ? dependsAnswer[0] : dependsAnswer;
1170
+ if (!expectedValues.includes(answerValue)) {
1171
+ continue;
1172
+ }
1173
+ }
1174
+ adapted.push(adaptQuestion(q));
1175
+ }
1176
+ return adapted;
1177
+ }
1178
+ function parseLLMQuestionsResponse(text) {
1179
+ let jsonStr = text;
1180
+ jsonStr = jsonStr.replace(/```(?:json|javascript)?\s*/gi, "");
1181
+ jsonStr = jsonStr.replace(/```\s*$/gm, "");
1182
+ const firstBrace = jsonStr.indexOf("{");
1183
+ const firstBracket = jsonStr.indexOf("[");
1184
+ let start = -1;
1185
+ if (firstBrace !== -1 && firstBracket !== -1) {
1186
+ start = Math.min(firstBrace, firstBracket);
1187
+ } else if (firstBrace !== -1) {
1188
+ start = firstBrace;
1189
+ } else if (firstBracket !== -1) {
1190
+ start = firstBracket;
1191
+ }
1192
+ if (start === -1) {
1193
+ throw new Error("No JSON found in response");
1194
+ }
1195
+ jsonStr = jsonStr.substring(start);
1196
+ const lastBrace = jsonStr.lastIndexOf("}");
1197
+ const lastBracket = jsonStr.lastIndexOf("]");
1198
+ const end = Math.max(lastBrace, lastBracket);
1199
+ if (end === -1) {
1200
+ throw new Error("Invalid JSON structure");
1201
+ }
1202
+ jsonStr = jsonStr.substring(0, end + 1);
1203
+ jsonStr = jsonStr.replace(/,(\s*[}\]])/g, "$1");
1204
+ jsonStr = jsonStr.replace(/'/g, '"');
1205
+ const parsed = JSON.parse(jsonStr);
1206
+ let questions;
1207
+ if (Array.isArray(parsed)) {
1208
+ questions = parsed;
1209
+ } else if (parsed.questions && Array.isArray(parsed.questions)) {
1210
+ questions = parsed.questions;
1211
+ } else if (parsed.data && Array.isArray(parsed.data.questions)) {
1212
+ questions = parsed.data.questions;
1213
+ } else {
1214
+ throw new Error("No questions array found in response");
1215
+ }
1216
+ function sanitize(obj) {
1217
+ const sanitized = {};
1218
+ for (const key of Object.keys(obj)) {
1219
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
1220
+ continue;
1221
+ }
1222
+ const val = obj[key];
1223
+ if (val && typeof val === "object" && !Array.isArray(val)) {
1224
+ sanitized[key] = sanitize(val);
1225
+ } else if (Array.isArray(val)) {
1226
+ sanitized[key] = val.map(
1227
+ (item) => item && typeof item === "object" && !Array.isArray(item) ? sanitize(item) : item
1228
+ );
1229
+ } else {
1230
+ sanitized[key] = val;
1231
+ }
1232
+ }
1233
+ return sanitized;
1234
+ }
1235
+ questions = questions.map((q) => {
1236
+ if (q && typeof q === "object") {
1237
+ return sanitize(q);
1238
+ }
1239
+ return q;
1240
+ });
1241
+ return questions;
1242
+ }
1243
+ function answersToManifestFormat(answers, questions) {
1244
+ const result = {};
1245
+ for (const answer of answers) {
1246
+ const headerLower = answer.question_header.toLowerCase();
1247
+ const question = questions.find((q) => {
1248
+ const qHeader = generateHeader(q.id).toLowerCase();
1249
+ return qHeader === headerLower || q.id.toLowerCase().includes(headerLower);
1250
+ });
1251
+ if (!question) {
1252
+ const matched = questions.find((q) => {
1253
+ const qHeader = generateHeader(q.id).toLowerCase();
1254
+ return qHeader.startsWith(headerLower.split("-")[0]);
1255
+ });
1256
+ if (matched) {
1257
+ result[matched.id] = answer.selected_options[0] || "";
1258
+ }
1259
+ continue;
1260
+ }
1261
+ if (question.type === "multi-select" || answer.selected_options.length > 1) {
1262
+ result[question.id] = answer.selected_options;
1263
+ } else if (question.type === "confirm") {
1264
+ result[question.id] = answer.selected_options[0] === "Yes";
1265
+ } else {
1266
+ if (answer.other_text) {
1267
+ result[question.id] = answer.other_text;
1268
+ } else if (answer.selected_options.length > 0) {
1269
+ const option = question.options?.find((o) => o.label === answer.selected_options[0]);
1270
+ result[question.id] = option?.value ?? answer.selected_options[0];
1271
+ }
1272
+ }
1273
+ }
1274
+ return result;
1275
+ }
1276
+
1277
+ // src/tools/questions.ts
1278
+ var ManifestQuestionSchema = import_zod7.z.object({
1279
+ id: import_zod7.z.string(),
1280
+ question: import_zod7.z.string(),
1281
+ type: import_zod7.z.enum(["text", "select", "multi-select", "confirm"]),
1282
+ required: import_zod7.z.boolean().optional(),
1283
+ options: import_zod7.z.array(
1284
+ import_zod7.z.object({
1285
+ label: import_zod7.z.string(),
1286
+ value: import_zod7.z.string()
1287
+ })
1288
+ ).optional(),
1289
+ default: import_zod7.z.union([import_zod7.z.string(), import_zod7.z.boolean(), import_zod7.z.array(import_zod7.z.string())]).optional(),
1290
+ help: import_zod7.z.string().optional(),
1291
+ dependsOn: import_zod7.z.object({
1292
+ questionId: import_zod7.z.string(),
1293
+ value: import_zod7.z.union([import_zod7.z.string(), import_zod7.z.array(import_zod7.z.string())])
1294
+ }).optional()
1295
+ });
1296
+ function registerQuestionTools(server2) {
1297
+ server2.tool(
1298
+ "stackwright_pro_present_phase_questions",
1299
+ "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.",
1300
+ {
1301
+ phase: import_zod7.z.string().describe('Phase name for display context, e.g. "designer", "api", "auth"'),
1302
+ questions: import_zod7.z.array(ManifestQuestionSchema).optional().describe(
1303
+ "Questions in Question Manifest format. If omitted, questions are read from .stackwright/question-manifest.json using the phase name."
1304
+ ),
1305
+ answers: import_zod7.z.record(import_zod7.z.union([import_zod7.z.string(), import_zod7.z.array(import_zod7.z.string()), import_zod7.z.boolean()])).optional().describe("Previously collected answers used to resolve dependsOn conditions")
1306
+ },
1307
+ async ({ phase, questions, answers }) => {
1308
+ let resolvedQuestions;
1309
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1310
+ if (!SAFE_PHASE.test(phase)) {
1311
+ return {
1312
+ content: [
1313
+ {
1314
+ type: "text",
1315
+ text: JSON.stringify({
1316
+ error: `Invalid phase name: "${phase.slice(0, 50)}". Must be lowercase alphanumeric with hyphens, max 31 chars.`
1317
+ })
1318
+ }
1319
+ ],
1320
+ isError: true
1321
+ };
1322
+ }
1323
+ if (questions && questions.length > 0) {
1324
+ resolvedQuestions = questions;
1325
+ } else {
1326
+ const phaseQuestionPath = (0, import_node_path.join)(process.cwd(), ".stackwright", "questions", `${phase}.json`);
1327
+ try {
1328
+ const raw = await (0, import_promises.readFile)(phaseQuestionPath, "utf-8");
1329
+ const phaseData = JSON.parse(raw);
1330
+ resolvedQuestions = phaseData.questions ?? [];
1331
+ } catch {
1332
+ try {
1333
+ const manifestPath = (0, import_node_path.join)(process.cwd(), ".stackwright", "question-manifest.json");
1334
+ const raw = await (0, import_promises.readFile)(manifestPath, "utf-8");
1335
+ const manifest = JSON.parse(raw);
1336
+ const phaseData = manifest.phases.find((p) => p.phase === phase);
1337
+ resolvedQuestions = phaseData?.questions ?? [];
1338
+ } catch (err) {
1339
+ const msg = err instanceof Error ? err.message : String(err);
1340
+ return {
1341
+ content: [
1342
+ {
1343
+ type: "text",
1344
+ text: JSON.stringify({
1345
+ error: `Could not read question manifest for phase "${phase}": ${msg}`,
1346
+ hint: "Write the manifest first, or pass questions directly to this tool."
1347
+ })
1348
+ }
1349
+ ],
1350
+ isError: true
1351
+ };
1352
+ }
1353
+ }
1354
+ }
1355
+ const adapted = adaptQuestions(resolvedQuestions, answers ?? {});
1356
+ if (adapted.length === 0) {
1357
+ return {
1358
+ content: [
1359
+ {
1360
+ type: "text",
1361
+ text: JSON.stringify({
1362
+ phase,
1363
+ skipped: true,
1364
+ reason: "No questions to present (all filtered by dependsOn conditions)",
1365
+ answers: []
1366
+ })
1367
+ }
1368
+ ],
1369
+ isError: false
1370
+ };
1371
+ }
1372
+ return {
1373
+ content: [
1374
+ {
1375
+ type: "text",
1376
+ text: `Adapted ${adapted.length} questions for phase "${phase}". Pass the JSON array below DIRECTLY to ask_user_question as the "questions" parameter. Do NOT JSON.stringify() it. Do NOT wrap it in an object. Pass the parsed array value as-is.`
1377
+ },
1378
+ {
1379
+ type: "text",
1380
+ text: JSON.stringify(adapted)
1381
+ }
1382
+ ],
1383
+ isError: false
1384
+ };
1385
+ }
1386
+ );
1387
+ }
1388
+
1389
+ // src/tools/orchestration.ts
1390
+ var import_zod8 = require("zod");
1391
+ var import_fs3 = require("fs");
1392
+ var import_path3 = require("path");
1393
+ var OTTER_NAME_TO_PHASE = [
1394
+ ["designer", "designer"],
1395
+ ["theme", "theme"],
1396
+ ["api", "api"],
1397
+ ["auth", "auth"],
1398
+ ["dashboard", "dashboard"],
1399
+ ["data", "data"],
1400
+ ["page", "pages"],
1401
+ ["workflow", "workflow"]
1402
+ ];
1403
+ var PHASE_TO_OTTER = {
1404
+ designer: "stackwright-pro-designer-otter",
1405
+ theme: "stackwright-pro-theme-otter",
1406
+ api: "stackwright-pro-api-otter",
1407
+ auth: "stackwright-pro-auth-otter",
1408
+ pages: "stackwright-pro-page-otter",
1409
+ dashboard: "stackwright-pro-dashboard-otter",
1410
+ data: "stackwright-pro-data-otter",
1411
+ workflow: "stackwright-pro-workflow-otter"
1412
+ };
1413
+ function detectPhase(otterName) {
1414
+ const lower = otterName.toLowerCase();
1415
+ for (const [keyword, phase] of OTTER_NAME_TO_PHASE) {
1416
+ if (lower.includes(keyword)) return phase;
1417
+ }
1418
+ return lower.replace(/^stackwright-pro-/, "").replace(/-otter$/, "");
1419
+ }
1420
+ function handleParseOtterResponse(input) {
1421
+ const phase = detectPhase(input.otterName);
1422
+ try {
1423
+ const questions = parseLLMQuestionsResponse(input.responseText);
1424
+ return {
1425
+ result: { phase, otter: input.otterName, questions },
1426
+ isError: false
1427
+ };
1428
+ } catch (err) {
1429
+ const message = err instanceof Error ? err.message : String(err);
1430
+ return {
1431
+ result: {
1432
+ error: true,
1433
+ otterName: input.otterName,
1434
+ phase,
1435
+ questions: [],
1436
+ parseError: message
1437
+ },
1438
+ isError: true
1439
+ };
1440
+ }
1441
+ }
1442
+ function handleSaveManifest(input) {
1443
+ const cwd = input._cwd ?? process.cwd();
1444
+ const dir = (0, import_path3.join)(cwd, ".stackwright");
1445
+ const filePath = (0, import_path3.join)(dir, "question-manifest.json");
1446
+ try {
1447
+ (0, import_fs3.mkdirSync)(dir, { recursive: true });
1448
+ if ((0, import_fs3.existsSync)(filePath)) {
1449
+ const stat = (0, import_fs3.lstatSync)(filePath);
1450
+ if (stat.isSymbolicLink()) {
1451
+ const message = `Refusing to write to symlink: ${filePath}`;
1452
+ return {
1453
+ text: JSON.stringify({ success: false, error: message }),
1454
+ isError: true
1455
+ };
1456
+ }
1457
+ }
1458
+ const manifest = {
1459
+ version: "1.0",
1460
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1461
+ phases: input.phases
1462
+ };
1463
+ (0, import_fs3.writeFileSync)(filePath, JSON.stringify(manifest, null, 2) + "\n");
1464
+ return {
1465
+ text: JSON.stringify({ success: true, path: filePath, phaseCount: input.phases.length }),
1466
+ isError: false
1467
+ };
1468
+ } catch (err) {
1469
+ const message = err instanceof Error ? err.message : String(err);
1470
+ return {
1471
+ text: JSON.stringify({ success: false, error: message }),
1472
+ isError: true
1473
+ };
1474
+ }
1475
+ }
1476
+ function handleSavePhaseAnswers(input) {
1477
+ const cwd = input._cwd ?? process.cwd();
1478
+ const dir = (0, import_path3.join)(cwd, ".stackwright", "answers");
1479
+ const filePath = (0, import_path3.join)(dir, `${input.phase}.json`);
1480
+ try {
1481
+ (0, import_fs3.mkdirSync)(dir, { recursive: true });
1482
+ let answers;
1483
+ if (input.questions && input.questions.length > 0) {
1484
+ answers = answersToManifestFormat(input.rawAnswers, input.questions);
1485
+ } else {
1486
+ answers = Object.fromEntries(
1487
+ input.rawAnswers.map((a) => [a.question_header, a.selected_options[0] ?? ""])
1488
+ );
1489
+ }
1490
+ const payload = {
1491
+ version: "1.0",
1492
+ phase: input.phase,
1493
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
1494
+ answers
1495
+ };
1496
+ if ((0, import_fs3.existsSync)(filePath)) {
1497
+ const stat = (0, import_fs3.lstatSync)(filePath);
1498
+ if (stat.isSymbolicLink()) {
1499
+ const message = `Refusing to write to symlink: ${filePath}`;
1500
+ return {
1501
+ text: JSON.stringify({ success: false, error: message }),
1502
+ isError: true
1503
+ };
1504
+ }
1505
+ }
1506
+ (0, import_fs3.writeFileSync)(filePath, JSON.stringify(payload, null, 2) + "\n");
1507
+ return {
1508
+ text: JSON.stringify({
1509
+ success: true,
1510
+ path: filePath,
1511
+ answersCount: Object.keys(answers).length
1512
+ }),
1513
+ isError: false
1514
+ };
1515
+ } catch (err) {
1516
+ const message = err instanceof Error ? err.message : String(err);
1517
+ return {
1518
+ text: JSON.stringify({ success: false, error: message }),
1519
+ isError: true
1520
+ };
1521
+ }
1522
+ }
1523
+ function handleReadPhaseAnswers(input) {
1524
+ const cwd = input._cwd ?? process.cwd();
1525
+ const filePath = (0, import_path3.join)(cwd, ".stackwright", "answers", `${input.phase}.json`);
1526
+ if (!(0, import_fs3.existsSync)(filePath)) {
1527
+ return {
1528
+ text: JSON.stringify({ missing: true, phase: input.phase }),
1529
+ isError: false
1530
+ };
1531
+ }
1532
+ try {
1533
+ const raw = (0, import_fs3.readFileSync)(filePath, "utf8");
1534
+ const parsed = JSON.parse(raw);
1535
+ return { text: JSON.stringify(parsed), isError: false };
1536
+ } catch (err) {
1537
+ const message = err instanceof Error ? err.message : String(err);
1538
+ return {
1539
+ text: JSON.stringify({ error: true, phase: input.phase, readError: message }),
1540
+ isError: true
1541
+ };
1542
+ }
1543
+ }
1544
+ function handleGetOtterName(input) {
1545
+ const normalised = input.phase.toLowerCase().trim();
1546
+ const otterName = PHASE_TO_OTTER[normalised];
1547
+ if (!otterName) {
1548
+ return {
1549
+ text: JSON.stringify({ error: true, message: `Unknown phase: ${input.phase}` }),
1550
+ isError: true
1551
+ };
1552
+ }
1553
+ return {
1554
+ text: JSON.stringify({ phase: normalised, otterName }),
1555
+ isError: false
1556
+ };
1557
+ }
1558
+ function registerOrchestrationTools(server2) {
1559
+ server2.tool(
1560
+ "stackwright_pro_parse_otter_response",
1561
+ "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.",
1562
+ {
1563
+ otterName: import_zod8.z.string().describe('The agent name, e.g. "stackwright-pro-api-otter"'),
1564
+ responseText: import_zod8.z.string().describe("Raw text response from the otter's QUESTION_COLLECTION_MODE invocation")
1565
+ },
1566
+ async ({ otterName, responseText }) => {
1567
+ const { result, isError } = handleParseOtterResponse({ otterName, responseText });
1568
+ return {
1569
+ content: [{ type: "text", text: JSON.stringify(result) }],
1570
+ isError
1571
+ };
1572
+ }
1573
+ );
1574
+ server2.tool(
1575
+ "stackwright_pro_save_manifest",
1576
+ "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.",
1577
+ {
1578
+ phases: import_zod8.z.array(
1579
+ import_zod8.z.object({
1580
+ phase: import_zod8.z.string(),
1581
+ otter: import_zod8.z.string(),
1582
+ questions: import_zod8.z.array(import_zod8.z.any()),
1583
+ requiredPackages: import_zod8.z.object({
1584
+ dependencies: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.string()).optional(),
1585
+ devPackages: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.string()).optional()
1586
+ }).optional()
1587
+ })
1588
+ ).describe("Array of parsed phase objects from stackwright_pro_parse_otter_response")
1589
+ },
1590
+ async ({ phases }) => {
1591
+ const { text, isError } = handleSaveManifest({ phases });
1592
+ return {
1593
+ content: [{ type: "text", text }],
1594
+ isError
1595
+ };
1596
+ }
1597
+ );
1598
+ server2.tool(
1599
+ "stackwright_pro_save_phase_answers",
1600
+ "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.",
1601
+ {
1602
+ phase: import_zod8.z.string().describe('Phase name, e.g. "designer"'),
1603
+ rawAnswers: import_zod8.z.array(
1604
+ import_zod8.z.object({
1605
+ question_header: import_zod8.z.string(),
1606
+ selected_options: import_zod8.z.array(import_zod8.z.string()),
1607
+ other_text: import_zod8.z.string().nullable().optional()
1608
+ })
1609
+ ).describe("Answers as returned by ask_user_question"),
1610
+ questions: import_zod8.z.array(import_zod8.z.any()).optional().describe("Original manifest questions for label\u2192value reverse-mapping")
1611
+ },
1612
+ async ({ phase, rawAnswers, questions }) => {
1613
+ const { text, isError } = handleSavePhaseAnswers({
1614
+ phase,
1615
+ rawAnswers,
1616
+ ...questions && questions.length > 0 ? { questions } : {}
1617
+ });
1618
+ return {
1619
+ content: [{ type: "text", text }],
1620
+ isError
1621
+ };
1622
+ }
1623
+ );
1624
+ server2.tool(
1625
+ "stackwright_pro_read_phase_answers",
1626
+ "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.",
1627
+ {
1628
+ phase: import_zod8.z.string().describe('Phase name, e.g. "designer"')
1629
+ },
1630
+ async ({ phase }) => {
1631
+ const { text, isError } = handleReadPhaseAnswers({ phase });
1632
+ return {
1633
+ content: [{ type: "text", text }],
1634
+ isError
1635
+ };
1636
+ }
1637
+ );
1638
+ server2.tool(
1639
+ "stackwright_pro_get_otter_name",
1640
+ "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.",
1641
+ {
1642
+ phase: import_zod8.z.string().describe('Phase name, e.g. "designer", "api", "pages"')
1643
+ },
1644
+ async ({ phase }) => {
1645
+ const { text, isError } = handleGetOtterName({ phase });
1646
+ return {
1647
+ content: [{ type: "text", text }],
1648
+ isError
1649
+ };
1650
+ }
1651
+ );
1652
+ }
1653
+
1654
+ // src/tools/pipeline.ts
1655
+ var import_zod9 = require("zod");
1656
+ var import_fs4 = require("fs");
1657
+ var import_path4 = require("path");
1658
+ var PHASE_ORDER = [
1659
+ "designer",
1660
+ "theme",
1661
+ "api",
1662
+ "auth",
1663
+ "data",
1664
+ "pages",
1665
+ "dashboard",
1666
+ "workflow"
1667
+ ];
1668
+ var PHASE_DEPENDENCIES = {
1669
+ designer: [],
1670
+ theme: ["designer"],
1671
+ api: [],
1672
+ auth: [],
1673
+ data: ["api"],
1674
+ pages: ["designer", "theme", "api", "data", "auth"],
1675
+ dashboard: ["designer", "theme", "api", "data"],
1676
+ workflow: ["auth"]
1677
+ };
1678
+ var PHASE_ARTIFACT = {
1679
+ designer: "design-language.json",
1680
+ theme: "theme-tokens.json",
1681
+ api: "api-config.json",
1682
+ auth: "auth-config.json",
1683
+ data: "data-config.json",
1684
+ pages: "pages-manifest.json",
1685
+ dashboard: "dashboard-manifest.json",
1686
+ workflow: "workflow-config.json"
1687
+ };
1688
+ var PHASE_TO_OTTER2 = {
1689
+ designer: "stackwright-pro-designer-otter",
1690
+ theme: "stackwright-pro-theme-otter",
1691
+ api: "stackwright-pro-api-otter",
1692
+ auth: "stackwright-pro-auth-otter",
1693
+ data: "stackwright-pro-data-otter",
1694
+ pages: "stackwright-pro-page-otter",
1695
+ dashboard: "stackwright-pro-dashboard-otter",
1696
+ workflow: "stackwright-pro-workflow-otter"
1697
+ };
1698
+ function isValidPhase(phase) {
1699
+ return PHASE_ORDER.includes(phase);
1700
+ }
1701
+ function defaultPhaseStatus() {
1702
+ return {
1703
+ questionsCollected: false,
1704
+ answered: false,
1705
+ executed: false,
1706
+ artifactWritten: false,
1707
+ retryCount: 0
1708
+ };
1709
+ }
1710
+ function createDefaultState() {
1711
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1712
+ const phases = {};
1713
+ for (const p of PHASE_ORDER) {
1714
+ phases[p] = defaultPhaseStatus();
1715
+ }
1716
+ return {
1717
+ version: "1.0",
1718
+ currentPhase: PHASE_ORDER[0],
1719
+ status: "setup",
1720
+ phases,
1721
+ startedAt: now,
1722
+ updatedAt: now
1723
+ };
1724
+ }
1725
+ function statePath(cwd) {
1726
+ return (0, import_path4.join)(cwd, ".stackwright", "pipeline-state.json");
1727
+ }
1728
+ function readState(cwd) {
1729
+ const p = statePath(cwd);
1730
+ if (!(0, import_fs4.existsSync)(p)) return createDefaultState();
1731
+ const raw = JSON.parse(safeReadSync(p));
1732
+ if (typeof raw !== "object" || raw === null || raw.version !== "1.0") {
1733
+ return createDefaultState();
1734
+ }
1735
+ return raw;
1736
+ }
1737
+ function safeWriteSync(filePath, content) {
1738
+ if ((0, import_fs4.existsSync)(filePath)) {
1739
+ const stat = (0, import_fs4.lstatSync)(filePath);
1740
+ if (stat.isSymbolicLink()) {
1741
+ throw new Error(`Refusing to write to symlink: ${filePath}`);
1742
+ }
1743
+ }
1744
+ (0, import_fs4.writeFileSync)(filePath, content);
1745
+ }
1746
+ function safeReadSync(filePath) {
1747
+ if ((0, import_fs4.existsSync)(filePath)) {
1748
+ const stat = (0, import_fs4.lstatSync)(filePath);
1749
+ if (stat.isSymbolicLink()) {
1750
+ throw new Error(`Refusing to read symlink: ${filePath}`);
1751
+ }
1752
+ }
1753
+ return (0, import_fs4.readFileSync)(filePath, "utf-8");
1754
+ }
1755
+ function writeState(cwd, state) {
1756
+ const dir = (0, import_path4.join)(cwd, ".stackwright");
1757
+ (0, import_fs4.mkdirSync)(dir, { recursive: true });
1758
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1759
+ safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
1760
+ }
1761
+ function extractJsonFromResponse(text) {
1762
+ let cleaned = text;
1763
+ cleaned = cleaned.replace(/```(?:json)?\s*/gi, "");
1764
+ cleaned = cleaned.replace(/```\s*$/gm, "");
1765
+ const firstBrace = cleaned.indexOf("{");
1766
+ if (firstBrace === -1) throw new Error("No JSON object found in response");
1767
+ cleaned = cleaned.substring(firstBrace);
1768
+ const lastBrace = cleaned.lastIndexOf("}");
1769
+ if (lastBrace === -1) throw new Error("Unclosed JSON object in response");
1770
+ cleaned = cleaned.substring(0, lastBrace + 1);
1771
+ cleaned = cleaned.replace(/,(\s*[}\]])/g, "$1");
1772
+ return JSON.parse(cleaned);
1773
+ }
1774
+ function handleGetPipelineState(_cwd) {
1775
+ const cwd = _cwd ?? process.cwd();
1776
+ try {
1777
+ const state = readState(cwd);
1778
+ return { text: JSON.stringify(state), isError: false };
1779
+ } catch (err) {
1780
+ return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
1781
+ }
1782
+ }
1783
+ function handleSetPipelineState(input) {
1784
+ const cwd = input._cwd ?? process.cwd();
1785
+ if (input.phase && !isValidPhase(input.phase)) {
1786
+ return {
1787
+ text: JSON.stringify({
1788
+ error: true,
1789
+ message: `Invalid phase: ${input.phase}. Valid phases are: ${PHASE_ORDER.join(", ")}`
1790
+ }),
1791
+ isError: true
1792
+ };
1793
+ }
1794
+ const VALID_FIELDS = ["questionsCollected", "answered", "executed", "artifactWritten"];
1795
+ if (input.field && !VALID_FIELDS.includes(input.field)) {
1796
+ return {
1797
+ text: JSON.stringify({
1798
+ error: true,
1799
+ message: `Invalid field: ${input.field}. Valid fields are: ${VALID_FIELDS.join(", ")}`
1800
+ }),
1801
+ isError: true
1802
+ };
1803
+ }
1804
+ try {
1805
+ const state = readState(cwd);
1806
+ if (input.status) {
1807
+ state.status = input.status;
1808
+ }
1809
+ if (input.phase) {
1810
+ const phase = input.phase;
1811
+ if (!state.phases[phase]) {
1812
+ state.phases[phase] = defaultPhaseStatus();
1813
+ }
1814
+ const phaseState = state.phases[phase];
1815
+ if (input.field && input.value !== void 0) {
1816
+ phaseState[input.field] = input.value;
1817
+ }
1818
+ if (input.incrementRetry) {
1819
+ phaseState.retryCount += 1;
1820
+ }
1821
+ state.currentPhase = phase;
1822
+ }
1823
+ writeState(cwd, state);
1824
+ return { text: JSON.stringify(state), isError: false };
1825
+ } catch (err) {
1826
+ return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
1827
+ }
1828
+ }
1829
+ function handleCheckExecutionReady(_cwd) {
1830
+ const cwd = _cwd ?? process.cwd();
1831
+ try {
1832
+ const answersDir = (0, import_path4.join)(cwd, ".stackwright", "answers");
1833
+ const answeredPhases = [];
1834
+ const missingPhases = [];
1835
+ for (const phase of PHASE_ORDER) {
1836
+ const answerFile = (0, import_path4.join)(answersDir, `${phase}.json`);
1837
+ if ((0, import_fs4.existsSync)(answerFile)) {
1838
+ try {
1839
+ const raw = safeReadSync(answerFile);
1840
+ const parsed = JSON.parse(raw);
1841
+ if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
1842
+ missingPhases.push(phase);
1843
+ continue;
1844
+ }
1845
+ answeredPhases.push(phase);
1846
+ } catch {
1847
+ missingPhases.push(phase);
1848
+ }
1849
+ } else {
1850
+ missingPhases.push(phase);
1851
+ }
1852
+ }
1853
+ return {
1854
+ text: JSON.stringify({
1855
+ ready: missingPhases.length === 0,
1856
+ answeredPhases,
1857
+ missingPhases,
1858
+ totalPhases: PHASE_ORDER.length
1859
+ }),
1860
+ isError: false
1861
+ };
1862
+ } catch (err) {
1863
+ return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
1864
+ }
1865
+ }
1866
+ function handleListArtifacts(_cwd) {
1867
+ const cwd = _cwd ?? process.cwd();
1868
+ try {
1869
+ const artifactsDir = (0, import_path4.join)(cwd, ".stackwright", "artifacts");
1870
+ const artifacts = [];
1871
+ let completedCount = 0;
1872
+ for (const phase of PHASE_ORDER) {
1873
+ const expectedFile = PHASE_ARTIFACT[phase];
1874
+ const fullPath = (0, import_path4.join)(artifactsDir, expectedFile);
1875
+ const exists = (0, import_fs4.existsSync)(fullPath);
1876
+ if (exists) completedCount++;
1877
+ artifacts.push({ phase, expectedFile, exists, path: fullPath });
1878
+ }
1879
+ return {
1880
+ text: JSON.stringify({ artifacts, completedCount, totalCount: PHASE_ORDER.length }),
1881
+ isError: false
1882
+ };
1883
+ } catch (err) {
1884
+ return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
1885
+ }
1886
+ }
1887
+ function handleWritePhaseQuestions(input) {
1888
+ const cwd = input._cwd ?? process.cwd();
1889
+ const { phase, responseText } = input;
1890
+ if (!isValidPhase(phase)) {
1891
+ return {
1892
+ text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
1893
+ isError: true
1894
+ };
1895
+ }
1896
+ try {
1897
+ const questions = parseLLMQuestionsResponse(responseText);
1898
+ let requiredPackages = {
1899
+ dependencies: {},
1900
+ devPackages: {}
1901
+ };
1902
+ try {
1903
+ const fullParsed = extractJsonFromResponse(responseText);
1904
+ if (fullParsed.requiredPackages && typeof fullParsed.requiredPackages === "object") {
1905
+ const rp = fullParsed.requiredPackages;
1906
+ requiredPackages = {
1907
+ dependencies: rp.dependencies ?? {},
1908
+ devPackages: rp.devPackages ?? {}
1909
+ };
1910
+ }
1911
+ } catch {
1912
+ }
1913
+ const questionsDir = (0, import_path4.join)(cwd, ".stackwright", "questions");
1914
+ (0, import_fs4.mkdirSync)(questionsDir, { recursive: true });
1915
+ const filePath = (0, import_path4.join)(questionsDir, `${phase}.json`);
1916
+ const payload = {
1917
+ version: "1.0",
1918
+ phase,
1919
+ otter: PHASE_TO_OTTER2[phase],
1920
+ collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
1921
+ questions,
1922
+ requiredPackages
1923
+ };
1924
+ safeWriteSync(filePath, JSON.stringify(payload, null, 2) + "\n");
1925
+ const state = readState(cwd);
1926
+ if (!state.phases[phase]) state.phases[phase] = defaultPhaseStatus();
1927
+ const ps = state.phases[phase];
1928
+ ps.questionsCollected = true;
1929
+ writeState(cwd, state);
1930
+ return {
1931
+ text: JSON.stringify({
1932
+ success: true,
1933
+ phase,
1934
+ questionCount: questions.length,
1935
+ requiredPackages,
1936
+ path: filePath
1937
+ }),
1938
+ isError: false
1939
+ };
1940
+ } catch (err) {
1941
+ const message = err instanceof Error ? err.message : String(err);
1942
+ return { text: JSON.stringify({ error: true, phase, message }), isError: true };
1943
+ }
1944
+ }
1945
+ function handleBuildSpecialistPrompt(input) {
1946
+ const cwd = input._cwd ?? process.cwd();
1947
+ const { phase } = input;
1948
+ if (!isValidPhase(phase)) {
1949
+ return {
1950
+ text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
1951
+ isError: true
1952
+ };
1953
+ }
1954
+ try {
1955
+ const answersPath = (0, import_path4.join)(cwd, ".stackwright", "answers", `${phase}.json`);
1956
+ let answers = {};
1957
+ if ((0, import_fs4.existsSync)(answersPath)) {
1958
+ answers = JSON.parse(safeReadSync(answersPath));
1959
+ }
1960
+ const deps = PHASE_DEPENDENCIES[phase];
1961
+ const artifactSections = [];
1962
+ const missingDependencies = [];
1963
+ for (const dep of deps) {
1964
+ const artifactFile = PHASE_ARTIFACT[dep];
1965
+ const artifactPath = (0, import_path4.join)(cwd, ".stackwright", "artifacts", artifactFile);
1966
+ if ((0, import_fs4.existsSync)(artifactPath)) {
1967
+ const content = JSON.parse(safeReadSync(artifactPath));
1968
+ const expectedOtter = PHASE_TO_OTTER2[dep];
1969
+ const artifactOtter = content["generatedBy"];
1970
+ if (!artifactOtter) {
1971
+ missingDependencies.push(dep);
1972
+ artifactSections.push(
1973
+ `[${artifactFile}]:
1974
+ (integrity check failed: missing generatedBy field)`
1975
+ );
1976
+ } else if (artifactOtter !== expectedOtter) {
1977
+ missingDependencies.push(dep);
1978
+ artifactSections.push(
1979
+ `[${artifactFile}]:
1980
+ (integrity check failed: artifact claims generatedBy="${artifactOtter}" but expected="${expectedOtter}")`
1981
+ );
1982
+ } else {
1983
+ artifactSections.push(`[${artifactFile}]:
1984
+ ${JSON.stringify(content, null, 2)}`);
1985
+ }
1986
+ } else {
1987
+ missingDependencies.push(dep);
1988
+ artifactSections.push(`[${artifactFile}]:
1989
+ (not yet available)`);
1990
+ }
1991
+ }
1992
+ const parts = ["ANSWERS:", JSON.stringify(answers, null, 2)];
1993
+ if (artifactSections.length > 0) {
1994
+ parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
1995
+ }
1996
+ parts.push("", "Execute using these answers and the upstream artifacts provided.");
1997
+ const prompt = parts.join("\n");
1998
+ const dependenciesSatisfied = missingDependencies.length === 0;
1999
+ return {
2000
+ text: JSON.stringify({
2001
+ otterName: PHASE_TO_OTTER2[phase],
2002
+ phase,
2003
+ prompt,
2004
+ dependenciesSatisfied,
2005
+ missingDependencies
2006
+ }),
2007
+ isError: false
2008
+ };
2009
+ } catch (err) {
2010
+ const message = err instanceof Error ? err.message : String(err);
2011
+ return { text: JSON.stringify({ error: true, phase, message }), isError: true };
2012
+ }
2013
+ }
2014
+ var OFF_SCRIPT_PATTERNS = [
2015
+ {
2016
+ pattern: /```(?:ts|tsx|js|jsx|python|bash|sh|sql|ruby|go|rust|java|csharp|c\+\+)\b/,
2017
+ label: "code fence"
2018
+ },
2019
+ { pattern: /\bimport\s+[\w{]/, label: "import statement" },
2020
+ { pattern: /\bexport\s+(?:const|function|default|class)\b/, label: "export statement" },
2021
+ { pattern: /\brequire\s*\(/, label: "require() call" },
2022
+ { pattern: /\beval\s*\(/, label: "eval() call" },
2023
+ { pattern: /^#!/m, label: "shebang" },
2024
+ { pattern: /<script[\s>]/i, label: "script tag" },
2025
+ { pattern: /\.(ts|tsx|js|jsx)\b.*\bfile\b/i, label: "code file reference" }
2026
+ ];
2027
+ var PHASE_REQUIRED_KEYS = {
2028
+ designer: ["designLanguage", "themeTokenSeeds"],
2029
+ theme: ["tokens"],
2030
+ api: ["entities"],
2031
+ auth: ["version", "generatedBy"],
2032
+ data: ["version", "generatedBy"],
2033
+ pages: ["version", "generatedBy"],
2034
+ dashboard: ["version", "generatedBy"],
2035
+ workflow: ["version", "generatedBy"]
2036
+ };
2037
+ function handleValidateArtifact(input) {
2038
+ const cwd = input._cwd ?? process.cwd();
2039
+ const { phase, responseText } = input;
2040
+ if (!isValidPhase(phase)) {
2041
+ return {
2042
+ text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
2043
+ isError: true
2044
+ };
2045
+ }
2046
+ for (const { pattern, label } of OFF_SCRIPT_PATTERNS) {
2047
+ if (pattern.test(responseText)) {
2048
+ const result = {
2049
+ valid: false,
2050
+ phase,
2051
+ violation: "off-script",
2052
+ retryPrompt: `You returned code output (detected: ${label}). Return ONLY a JSON artifact \u2014 no code, no files.`
2053
+ };
2054
+ return { text: JSON.stringify(result), isError: false };
2055
+ }
2056
+ }
2057
+ let artifact;
2058
+ try {
2059
+ artifact = extractJsonFromResponse(responseText);
2060
+ } catch {
2061
+ const result = {
2062
+ valid: false,
2063
+ phase,
2064
+ violation: "invalid-json",
2065
+ retryPrompt: "Your response did not contain valid JSON. Return a single JSON object with no surrounding text."
2066
+ };
2067
+ return { text: JSON.stringify(result), isError: false };
2068
+ }
2069
+ if (!artifact.version || !artifact.generatedBy) {
2070
+ const result = {
2071
+ valid: false,
2072
+ phase,
2073
+ violation: "missing-fields",
2074
+ retryPrompt: 'Your JSON artifact is missing required fields. Every artifact MUST include "version" and "generatedBy" at the top level.'
2075
+ };
2076
+ return { text: JSON.stringify(result), isError: false };
2077
+ }
2078
+ const requiredKeys = PHASE_REQUIRED_KEYS[phase];
2079
+ const missingKeys = requiredKeys.filter((k) => !(k in artifact));
2080
+ if (missingKeys.length > 0) {
2081
+ const result = {
2082
+ valid: false,
2083
+ phase,
2084
+ violation: "schema-mismatch",
2085
+ retryPrompt: `Your ${phase} artifact is missing required keys: ${missingKeys.join(", ")}. Include them and try again.`
2086
+ };
2087
+ return { text: JSON.stringify(result), isError: false };
2088
+ }
2089
+ try {
2090
+ const artifactsDir = (0, import_path4.join)(cwd, ".stackwright", "artifacts");
2091
+ (0, import_fs4.mkdirSync)(artifactsDir, { recursive: true });
2092
+ const artifactFile = PHASE_ARTIFACT[phase];
2093
+ const artifactPath = (0, import_path4.join)(artifactsDir, artifactFile);
2094
+ safeWriteSync(artifactPath, JSON.stringify(artifact, null, 2) + "\n");
2095
+ const state = readState(cwd);
2096
+ if (!state.phases[phase]) state.phases[phase] = defaultPhaseStatus();
2097
+ const ps = state.phases[phase];
2098
+ ps.artifactWritten = true;
2099
+ writeState(cwd, state);
2100
+ const topKeys = Object.keys(artifact).slice(0, 5).join(", ");
2101
+ const result = {
2102
+ valid: true,
2103
+ phase,
2104
+ artifactPath,
2105
+ summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})`
2106
+ };
2107
+ return { text: JSON.stringify(result), isError: false };
2108
+ } catch (err) {
2109
+ const message = err instanceof Error ? err.message : String(err);
2110
+ return { text: JSON.stringify({ error: true, phase, message }), isError: true };
2111
+ }
2112
+ }
2113
+ function registerPipelineTools(server2) {
2114
+ const DESC = "Writes state to .stackwright/ \u2014 the filesystem is the state machine.";
2115
+ const res = (r) => ({
2116
+ content: [{ type: "text", text: r.text }],
2117
+ isError: r.isError
2118
+ });
2119
+ server2.tool(
2120
+ "stackwright_pro_get_pipeline_state",
2121
+ `Read pipeline state from .stackwright/pipeline-state.json. ${DESC}`,
2122
+ {},
2123
+ async () => res(handleGetPipelineState())
2124
+ );
2125
+ server2.tool(
2126
+ "stackwright_pro_set_pipeline_state",
2127
+ `Atomic read\u2192modify\u2192write pipeline state. ${DESC}`,
2128
+ {
2129
+ phase: import_zod9.z.string().optional().describe('Phase to update, e.g. "designer"'),
2130
+ field: import_zod9.z.enum(["questionsCollected", "answered", "executed", "artifactWritten"]).optional().describe("Boolean field to set"),
2131
+ value: import_zod9.z.boolean().optional().describe("Value for the field"),
2132
+ status: import_zod9.z.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
2133
+ incrementRetry: import_zod9.z.boolean().optional().describe("Bump retryCount by 1")
2134
+ },
2135
+ async (args) => res(
2136
+ handleSetPipelineState({
2137
+ ...args.phase != null ? { phase: args.phase } : {},
2138
+ ...args.field != null ? { field: args.field } : {},
2139
+ ...args.value != null ? { value: args.value } : {},
2140
+ ...args.status != null ? { status: args.status } : {},
2141
+ ...args.incrementRetry != null ? { incrementRetry: args.incrementRetry } : {}
2142
+ })
2143
+ )
2144
+ );
2145
+ server2.tool(
2146
+ "stackwright_pro_check_execution_ready",
2147
+ `Check all phases have answer files in .stackwright/answers/. ${DESC}`,
2148
+ {},
2149
+ async () => res(handleCheckExecutionReady())
2150
+ );
2151
+ server2.tool(
2152
+ "stackwright_pro_list_artifacts",
2153
+ `List phase artifacts in .stackwright/artifacts/ with completedCount/totalCount. ${DESC}`,
2154
+ {},
2155
+ async () => res(handleListArtifacts())
2156
+ );
2157
+ server2.tool(
2158
+ "stackwright_pro_write_phase_questions",
2159
+ `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. ${DESC}`,
2160
+ {
2161
+ phase: import_zod9.z.string().describe('Phase name, e.g. "designer"'),
2162
+ responseText: import_zod9.z.string().describe("Raw LLM response from QUESTION_COLLECTION_MODE")
2163
+ },
2164
+ async ({ phase, responseText }) => res(handleWritePhaseQuestions({ phase, responseText }))
2165
+ );
2166
+ server2.tool(
2167
+ "stackwright_pro_build_specialist_prompt",
2168
+ `Assemble execution prompt from answers + upstream artifacts. Foreman passes verbatim. ${DESC}`,
2169
+ { phase: import_zod9.z.string().describe('Phase to build prompt for, e.g. "pages"') },
2170
+ async ({ phase }) => res(handleBuildSpecialistPrompt({ phase }))
2171
+ );
2172
+ server2.tool(
2173
+ "stackwright_pro_validate_artifact",
2174
+ `Validate specialist response + write artifact to .stackwright/artifacts/. Returns retryPrompt on failure. ${DESC}`,
2175
+ {
2176
+ phase: import_zod9.z.string().describe('Phase that produced this artifact, e.g. "designer"'),
2177
+ responseText: import_zod9.z.string().describe("Raw response text from the specialist otter")
2178
+ },
2179
+ async ({ phase, responseText }) => res(handleValidateArtifact({ phase, responseText }))
2180
+ );
2181
+ }
2182
+
2183
+ // src/tools/safe-write.ts
2184
+ var import_zod10 = require("zod");
2185
+ var import_fs5 = require("fs");
2186
+ var import_path5 = require("path");
2187
+ var OTTER_WRITE_ALLOWLISTS = {
2188
+ "stackwright-pro-designer-otter": [
2189
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Design language artifact" }
2190
+ ],
2191
+ "stackwright-pro-theme-otter": [
2192
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Theme tokens artifact" }
2193
+ ],
2194
+ "stackwright-pro-auth-otter": [
2195
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Auth config artifact" },
2196
+ { prefix: "config/", suffix: ".yml", description: "Auth YAML config" },
2197
+ { prefix: "config/", suffix: ".yaml", description: "Auth YAML config" },
2198
+ {
2199
+ prefix: ".env",
2200
+ suffix: "",
2201
+ description: "Dotenv files (.env, .env.local, .env.production, etc.)"
2202
+ }
2203
+ ],
2204
+ "stackwright-pro-data-otter": [
2205
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Data config artifact" },
2206
+ { prefix: "stackwright.yml", suffix: "", description: "Stackwright config" }
2207
+ ],
2208
+ "stackwright-pro-page-otter": [
2209
+ { prefix: "pages/", suffix: "/content.yml", description: "Page content YAML" },
2210
+ { prefix: "pages/", suffix: "/content.yaml", description: "Page content YAML" },
2211
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Pages manifest" }
2212
+ ],
2213
+ "stackwright-pro-dashboard-otter": [
2214
+ { prefix: "pages/", suffix: "/content.yml", description: "Dashboard content YAML" },
2215
+ { prefix: "pages/", suffix: "/content.yaml", description: "Dashboard content YAML" },
2216
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Dashboard manifest" }
2217
+ ],
2218
+ "stackwright-pro-workflow-otter": [
2219
+ { prefix: "workflows/", suffix: ".yml", description: "Workflow definition" },
2220
+ { prefix: "workflows/", suffix: ".yaml", description: "Workflow definition" },
2221
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Workflow config" }
2222
+ ],
2223
+ "stackwright-pro-api-otter": [
2224
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "API config artifact" }
2225
+ ]
2226
+ };
2227
+ var PROTECTED_PATH_PREFIXES = [
2228
+ ".stackwright/pipeline-state.json",
2229
+ ".stackwright/questions/",
2230
+ ".stackwright/answers/"
2231
+ ];
2232
+ function checkPathAllowed(callerOtter, filePath) {
2233
+ const normalized = (0, import_path5.normalize)(filePath);
2234
+ if (normalized.includes("..")) {
2235
+ return { allowed: false, error: 'Path traversal detected: ".." segments are not allowed' };
2236
+ }
2237
+ if ((0, import_path5.isAbsolute)(normalized)) {
2238
+ return {
2239
+ allowed: false,
2240
+ error: "Absolute paths are not allowed \u2014 use paths relative to project root"
2241
+ };
2242
+ }
2243
+ if (callerOtter === "stackwright-pro-foreman-otter") {
2244
+ return {
2245
+ allowed: false,
2246
+ error: "The foreman otter coordinates \u2014 it does not write files directly"
2247
+ };
2248
+ }
2249
+ const allowlist = OTTER_WRITE_ALLOWLISTS[callerOtter];
2250
+ if (!allowlist) {
2251
+ return {
2252
+ allowed: false,
2253
+ error: `Unknown otter: "${callerOtter}" is not in the write allowlist`
2254
+ };
2255
+ }
2256
+ for (const protectedPrefix of PROTECTED_PATH_PREFIXES) {
2257
+ if (normalized === protectedPrefix || normalized.startsWith(protectedPrefix)) {
2258
+ return {
2259
+ allowed: false,
2260
+ error: `Path "${normalized}" is managed by dedicated sink tools, not safe_write`
2261
+ };
2262
+ }
2263
+ }
2264
+ for (const rule of allowlist) {
2265
+ const prefixMatch = normalized.startsWith(rule.prefix);
2266
+ const suffixMatch = rule.suffix === "" || normalized.endsWith(rule.suffix);
2267
+ if (prefixMatch && suffixMatch) {
2268
+ if (rule.prefix === ".env" && rule.suffix === "") {
2269
+ if (!/^\.env(\.[a-zA-Z0-9]{3,})*$/.test(normalized)) {
2270
+ continue;
2271
+ }
2272
+ }
2273
+ return { allowed: true, rule: rule.description };
2274
+ }
2275
+ }
2276
+ return {
2277
+ allowed: false,
2278
+ error: `Path "${normalized}" does not match any allowed write pattern for ${callerOtter}`
2279
+ };
2280
+ }
2281
+ function validateJsonContent(content) {
2282
+ try {
2283
+ JSON.parse(content);
2284
+ return null;
2285
+ } catch (err) {
2286
+ return `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`;
2287
+ }
2288
+ }
2289
+ function validateYamlContent(content) {
2290
+ const trimmed = content.trimStart();
2291
+ const anchorRefPattern = /\[(\*[\w]+)\]/g;
2292
+ const matches = [...content.matchAll(anchorRefPattern)];
2293
+ if (matches.length >= 20) {
2294
+ return "YAML entity expansion pattern detected \u2014 too many anchor references";
2295
+ }
2296
+ const anchorDefPattern = /&([\w]+)/g;
2297
+ const defs = [...content.matchAll(anchorDefPattern)];
2298
+ if (defs.length >= 10) {
2299
+ const anchorNameCounts = /* @__PURE__ */ new Map();
2300
+ for (const [, name] of defs) {
2301
+ const count = (anchorNameCounts.get(name) ?? 0) + 1;
2302
+ anchorNameCounts.set(name, count);
2303
+ if (count >= 10) {
2304
+ return "YAML entity expansion: repeated anchor definitions detected";
2305
+ }
2306
+ }
2307
+ }
2308
+ if (trimmed.startsWith("import ") || trimmed.startsWith("import{")) {
2309
+ return 'Content starts with "import" \u2014 this looks like code, not YAML';
2310
+ }
2311
+ if (trimmed.startsWith("export ") || trimmed.startsWith("export{")) {
2312
+ return 'Content starts with "export" \u2014 this looks like code, not YAML';
2313
+ }
2314
+ if (trimmed.startsWith("#!"))
2315
+ return "Content starts with shebang \u2014 this looks like a script, not YAML";
2316
+ if (/!!(?:python|ruby|perl|js|java)/i.test(content))
2317
+ return "YAML deserialization attack tags detected";
2318
+ if (trimmed.startsWith("<") && !trimmed.startsWith("<<"))
2319
+ return "Content looks like markup, not YAML";
2320
+ return null;
2321
+ }
2322
+ function validateEnvContent(content) {
2323
+ const lines = content.split("\n");
2324
+ for (let i = 0; i < lines.length; i++) {
2325
+ const raw = lines[i];
2326
+ if (raw === void 0) continue;
2327
+ const line = raw.trim();
2328
+ if (line === "" || line.startsWith("#")) continue;
2329
+ if (!/^[A-Za-z_][A-Za-z0-9_]*=/.test(line)) {
2330
+ return `Line ${i + 1} is not a valid env entry (expected KEY=value): "${line}"`;
2331
+ }
2332
+ }
2333
+ return null;
2334
+ }
2335
+ function validateContent(filePath, content) {
2336
+ if (filePath.endsWith(".json")) return validateJsonContent(content);
2337
+ if (filePath.endsWith(".yml") || filePath.endsWith(".yaml")) return validateYamlContent(content);
2338
+ if (filePath === ".env" || filePath.startsWith(".env")) return validateEnvContent(content);
2339
+ return null;
2340
+ }
2341
+ function handleSafeWrite(input) {
2342
+ const cwd = input._cwd ?? process.cwd();
2343
+ const { callerOtter, filePath, content, createDirectories = true } = input;
2344
+ const check = checkPathAllowed(callerOtter, filePath);
2345
+ if (!check.allowed) {
2346
+ const allowlist = OTTER_WRITE_ALLOWLISTS[callerOtter] ?? [];
2347
+ const allowedPaths = allowlist.map((r) => `${r.prefix}*${r.suffix} (${r.description})`);
2348
+ const result = {
2349
+ success: false,
2350
+ error: check.error ?? "Path not allowed",
2351
+ callerOtter,
2352
+ attemptedPath: filePath,
2353
+ allowedPaths
2354
+ };
2355
+ return { text: JSON.stringify(result), isError: true };
2356
+ }
2357
+ const normalized = (0, import_path5.normalize)(filePath);
2358
+ const fullPath = (0, import_path5.join)(cwd, normalized);
2359
+ if ((0, import_fs5.existsSync)(fullPath)) {
2360
+ try {
2361
+ const stat = (0, import_fs5.lstatSync)(fullPath);
2362
+ if (stat.isSymbolicLink()) {
2363
+ const result = {
2364
+ success: false,
2365
+ error: "Target path is a symlink \u2014 refusing to write through symlinks for security",
2366
+ callerOtter,
2367
+ attemptedPath: filePath,
2368
+ allowedPaths: []
2369
+ };
2370
+ return { text: JSON.stringify(result), isError: true };
2371
+ }
2372
+ } catch {
2373
+ }
2374
+ }
2375
+ const contentError = validateContent(normalized, content);
2376
+ if (contentError) {
2377
+ const result = {
2378
+ success: false,
2379
+ error: `Content validation failed: ${contentError}`,
2380
+ callerOtter,
2381
+ attemptedPath: filePath,
2382
+ allowedPaths: []
2383
+ };
2384
+ return { text: JSON.stringify(result), isError: true };
2385
+ }
2386
+ try {
2387
+ if (createDirectories) {
2388
+ (0, import_fs5.mkdirSync)((0, import_path5.dirname)(fullPath), { recursive: true });
2389
+ }
2390
+ (0, import_fs5.writeFileSync)(fullPath, content, { encoding: "utf-8" });
2391
+ const result = {
2392
+ success: true,
2393
+ path: normalized,
2394
+ bytesWritten: Buffer.byteLength(content, "utf-8"),
2395
+ allowRule: check.rule ?? "unknown"
2396
+ };
2397
+ return { text: JSON.stringify(result), isError: false };
2398
+ } catch (err) {
2399
+ const message = err instanceof Error ? err.message : String(err);
2400
+ const result = {
2401
+ success: false,
2402
+ error: `Write failed: ${message}`,
2403
+ callerOtter,
2404
+ attemptedPath: filePath,
2405
+ allowedPaths: []
2406
+ };
2407
+ return { text: JSON.stringify(result), isError: true };
2408
+ }
2409
+ }
2410
+ function registerSafeWriteTools(server2) {
2411
+ const DESC = "Controlled file-write chokepoint. Every write from specialist otters goes through this tool with per-otter path allowlists. The LLM cannot write to arbitrary filesystem paths.";
2412
+ server2.tool(
2413
+ "stackwright_pro_safe_write",
2414
+ DESC,
2415
+ {
2416
+ callerOtter: import_zod10.z.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
2417
+ filePath: import_zod10.z.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
2418
+ content: import_zod10.z.string().describe("File content to write"),
2419
+ createDirectories: import_zod10.z.boolean().optional().describe("Create parent directories if they don't exist. Default: true")
2420
+ },
2421
+ async ({ callerOtter, filePath, content, createDirectories }) => {
2422
+ const result = handleSafeWrite({
2423
+ callerOtter,
2424
+ filePath,
2425
+ content,
2426
+ ...createDirectories != null ? { createDirectories } : {}
2427
+ });
2428
+ return { content: [{ type: "text", text: result.text }], isError: result.isError };
2429
+ }
2430
+ );
2431
+ }
2432
+
2433
+ // src/tools/auth.ts
2434
+ var import_zod11 = require("zod");
2435
+ var import_fs6 = require("fs");
2436
+ var import_path6 = require("path");
2437
+ function buildHierarchy(roles) {
2438
+ const h = {};
2439
+ for (let i = 0; i < roles.length - 1; i++) {
2440
+ h[roles[i]] = roles.slice(i + 1);
2441
+ }
2442
+ return h;
2443
+ }
2444
+ function hierarchyToYaml(hierarchy, indent) {
2445
+ const entries = Object.entries(hierarchy);
2446
+ if (entries.length === 0) return `${indent}{}`;
2447
+ return entries.map(([role, subs]) => `${indent}${role}: [${subs.join(", ")}]`).join("\n");
2448
+ }
2449
+ function rolesToYaml(roles, indent) {
2450
+ return roles.map((r) => `${indent}- ${r}`).join("\n");
2451
+ }
2452
+ function routesToYaml(routes, defaultRole, indent) {
2453
+ return routes.map((r) => `${indent}- pattern: ${r}
2454
+ ${indent} requiredRole: ${defaultRole}`).join("\n");
2455
+ }
2456
+ function upsertAuthBlock(existing, authYaml) {
2457
+ const lines = existing.split("\n");
2458
+ let authStart = -1;
2459
+ let authEnd = lines.length;
2460
+ for (let i = 0; i < lines.length; i++) {
2461
+ if (/^auth:/.test(lines[i])) {
2462
+ authStart = i;
2463
+ } else if (authStart >= 0 && i > authStart && /^\S/.test(lines[i]) && lines[i].trim() !== "") {
2464
+ authEnd = i;
2465
+ break;
2466
+ }
2467
+ }
2468
+ if (authStart < 0) {
2469
+ return existing.trimEnd() + "\n" + authYaml + "\n";
2470
+ }
2471
+ const before = lines.slice(0, authStart);
2472
+ const after = lines.slice(authEnd);
2473
+ return [...before, ...authYaml.trimEnd().split("\n"), ...after.length ? after : []].join("\n");
2474
+ }
2475
+ function generateMiddlewareContent(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
2476
+ const rbacBlock = ` rbac: {
2477
+ roles: ${JSON.stringify(roles)},
2478
+ defaultRole: '${defaultRole}',
2479
+ hierarchy: ${JSON.stringify(hierarchy, null, 4)},
2480
+ },`;
2481
+ const auditBlock = ` audit: {
2482
+ enabled: ${auditEnabled},
2483
+ retentionDays: ${auditRetentionDays},
2484
+ },`;
2485
+ const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
2486
+ const configBlock = `export const config = {
2487
+ matcher: ${JSON.stringify(protectedRoutes)},
2488
+ };`;
2489
+ if (method === "cac") {
2490
+ const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
2491
+ const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
2492
+ const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
2493
+ const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
2494
+ return `// middleware.ts \u2014 generated by @stackwright-pro/auth
2495
+ // \u26A0\uFE0F SECURITY REVIEW REQUIRED \u2014 CAC/PKI certificate validation
2496
+ // DoD security officer review required before production deployment.
2497
+ // Verify: CA bundle completeness, EDIPI lookup endpoint, OCSP accessibility.
2498
+ import { createProMiddleware } from '@stackwright-pro/auth-nextjs';
2499
+
2500
+ export const middleware = createProMiddleware({
2501
+ method: 'cac',
2502
+ cac: {
2503
+ caBundle: process.env.CAC_CA_BUNDLE ?? '${caBundle}',
2504
+ edipiLookup: '${edipiLookup}',
2505
+ ocspEndpoint: process.env.CAC_OCSP_ENDPOINT ?? '${ocspEndpoint}',
2506
+ certHeader: '${certHeader}',
2507
+ },
2508
+ ${rbacBlock}
2509
+ ${auditBlock}
2510
+ ${routesBlock}
2511
+ });
2512
+
2513
+ ${configBlock}
2514
+ `;
2515
+ }
2516
+ if (method === "oidc") {
2517
+ const scopes2 = params.oidcScopes ?? "openid profile email";
2518
+ const roleClaim = params.oidcRoleClaim ?? "roles";
2519
+ return `// middleware.ts \u2014 generated by @stackwright-pro/auth-nextjs
2520
+ import { createProMiddleware } from '@stackwright-pro/auth-nextjs';
2521
+
2522
+ export const middleware = createProMiddleware({
2523
+ method: 'oidc',
2524
+ oidc: {
2525
+ discoveryUrl: process.env.OIDC_DISCOVERY_URL!,
2526
+ clientId: process.env.OIDC_CLIENT_ID!,
2527
+ clientSecret: process.env.OIDC_CLIENT_SECRET!,
2528
+ scopes: '${scopes2}',
2529
+ roleClaim: '${roleClaim}',
2530
+ },
2531
+ ${rbacBlock}
2532
+ ${auditBlock}
2533
+ ${routesBlock}
2534
+ });
2535
+
2536
+ ${configBlock}
2537
+ `;
2538
+ }
2539
+ const scopes = params.oauth2Scopes ?? "read write";
2540
+ return `// middleware.ts \u2014 generated by @stackwright-pro/auth-nextjs
2541
+ import { createProMiddleware } from '@stackwright-pro/auth-nextjs';
2542
+
2543
+ export const middleware = createProMiddleware({
2544
+ method: 'oauth2',
2545
+ oauth2: {
2546
+ authorizationUrl: process.env.OAUTH2_AUTH_URL!,
2547
+ tokenUrl: process.env.OAUTH2_TOKEN_URL!,
2548
+ clientId: process.env.OAUTH2_CLIENT_ID!,
2549
+ clientSecret: process.env.OAUTH2_CLIENT_SECRET!,
2550
+ scopes: '${scopes}',
2551
+ },
2552
+ ${rbacBlock}
2553
+ ${auditBlock}
2554
+ ${routesBlock}
2555
+ });
2556
+
2557
+ ${configBlock}
2558
+ `;
2559
+ }
2560
+ function generateEnvBlock(method, params) {
2561
+ if (method === "cac") {
2562
+ return `# Authentication (CAC/PKI \u2014 DoD)
2563
+ # \u26A0\uFE0F SECURITY REVIEW REQUIRED before production deployment
2564
+ CAC_CA_BUNDLE=./certs/dod-ca-bundle.pem
2565
+ CAC_OCSP_ENDPOINT=https://ocsp.disa.mil
2566
+ `;
2567
+ }
2568
+ if (method === "oidc") {
2569
+ const label = params.provider ?? "OIDC";
2570
+ const discoveryUrl = params.oidcDiscoveryUrl ?? "https://your-provider/.well-known/openid-configuration";
2571
+ return `# Authentication (OIDC \u2014 ${label})
2572
+ OIDC_DISCOVERY_URL=${discoveryUrl}
2573
+ OIDC_CLIENT_ID=your-client-id
2574
+ OIDC_CLIENT_SECRET=your-client-secret
2575
+ `;
2576
+ }
2577
+ const authUrl = params.oauth2AuthUrl ?? "https://your-auth-server/authorize";
2578
+ const tokenUrl = params.oauth2TokenUrl ?? "https://your-auth-server/token";
2579
+ return `# Authentication (OAuth2)
2580
+ OAUTH2_AUTH_URL=${authUrl}
2581
+ OAUTH2_TOKEN_URL=${tokenUrl}
2582
+ OAUTH2_CLIENT_ID=your-client-id
2583
+ OAUTH2_CLIENT_SECRET=your-client-secret
2584
+ `;
2585
+ }
2586
+ function generateYamlBlock(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
2587
+ const rbacSection = ` rbac:
2588
+ roles:
2589
+ ${rolesToYaml(roles, " ")}
2590
+ defaultRole: ${defaultRole}
2591
+ hierarchy:
2592
+ ${hierarchyToYaml(hierarchy, " ")}`;
2593
+ const auditSection = ` audit:
2594
+ enabled: ${auditEnabled}
2595
+ retentionDays: ${auditRetentionDays}`;
2596
+ const routesSection = ` protectedRoutes:
2597
+ ${routesToYaml(protectedRoutes, " ", defaultRole)}`.replace(/\n\s+,/g, "");
2598
+ const routeLines = protectedRoutes.map((r) => ` - pattern: ${r}
2599
+ requiredRole: ${defaultRole}`).join("\n");
2600
+ const providerLine = params.provider ? ` provider: ${params.provider}
2601
+ ` : "";
2602
+ if (method === "cac") {
2603
+ const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
2604
+ const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
2605
+ const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
2606
+ const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
2607
+ return `auth:
2608
+ method: cac
2609
+ ${providerLine} middleware: ./middleware.ts
2610
+ cac:
2611
+ caBundle: \${CAC_CA_BUNDLE}
2612
+ edipiLookup: ${edipiLookup}
2613
+ ocspEndpoint: \${CAC_OCSP_ENDPOINT}
2614
+ certHeader: ${certHeader}
2615
+ ${rbacSection}
2616
+ protectedRoutes:
2617
+ ${routeLines}
2618
+ ${auditSection}
2619
+ `;
2620
+ }
2621
+ if (method === "oidc") {
2622
+ const scopes2 = params.oidcScopes ?? "openid profile email";
2623
+ const roleClaim = params.oidcRoleClaim ?? "roles";
2624
+ return `auth:
2625
+ method: oidc
2626
+ ${providerLine} middleware: ./middleware.ts
2627
+ oidc:
2628
+ discoveryUrl: \${OIDC_DISCOVERY_URL}
2629
+ clientId: \${OIDC_CLIENT_ID}
2630
+ clientSecret: \${OIDC_CLIENT_SECRET}
2631
+ scopes: ${scopes2}
2632
+ roleClaim: ${roleClaim}
2633
+ ${rbacSection}
2634
+ protectedRoutes:
2635
+ ${routeLines}
2636
+ ${auditSection}
2637
+ `;
2638
+ }
2639
+ const scopes = params.oauth2Scopes ?? "read write";
2640
+ return `auth:
2641
+ method: oauth2
2642
+ ${providerLine} middleware: ./middleware.ts
2643
+ oauth2:
2644
+ authorizationUrl: \${OAUTH2_AUTH_URL}
2645
+ tokenUrl: \${OAUTH2_TOKEN_URL}
2646
+ clientId: \${OAUTH2_CLIENT_ID}
2647
+ clientSecret: \${OAUTH2_CLIENT_SECRET}
2648
+ scopes: ${scopes}
2649
+ ${rbacSection}
2650
+ protectedRoutes:
2651
+ ${routeLines}
2652
+ ${auditSection}
2653
+ `;
2654
+ }
2655
+ async function configureAuthHandler(params, cwd) {
2656
+ const {
2657
+ method,
2658
+ provider,
2659
+ rbacRoles = ["SUPER_ADMIN", "ADMIN", "ANALYST"],
2660
+ auditEnabled = true,
2661
+ auditRetentionDays = 90,
2662
+ protectedRoutes = ["/dashboard/:path*"]
2663
+ } = params;
2664
+ const roles = rbacRoles;
2665
+ const defaultRole = params.rbacDefaultRole ?? roles[roles.length - 1];
2666
+ const hierarchy = buildHierarchy(roles);
2667
+ if (method === "none") {
2668
+ return {
2669
+ content: [
2670
+ {
2671
+ type: "text",
2672
+ text: JSON.stringify({
2673
+ success: true,
2674
+ method: "none",
2675
+ provider: null,
2676
+ rbacRoles: roles,
2677
+ rbacDefaultRole: defaultRole,
2678
+ protectedRoutesCount: protectedRoutes.length,
2679
+ filesWritten: [],
2680
+ securityWarning: null
2681
+ })
2682
+ }
2683
+ ]
2684
+ };
2685
+ }
2686
+ const filesWritten = [];
2687
+ try {
2688
+ const middlewareContent = generateMiddlewareContent(
2689
+ method,
2690
+ params,
2691
+ roles,
2692
+ defaultRole,
2693
+ hierarchy,
2694
+ auditEnabled,
2695
+ auditRetentionDays,
2696
+ protectedRoutes
2697
+ );
2698
+ (0, import_fs6.writeFileSync)((0, import_path6.join)(cwd, "middleware.ts"), middlewareContent, "utf8");
2699
+ filesWritten.push("middleware.ts");
2700
+ } catch (err) {
2701
+ const msg = err instanceof Error ? err.message : String(err);
2702
+ return {
2703
+ content: [
2704
+ {
2705
+ type: "text",
2706
+ text: JSON.stringify({ success: false, error: `Failed writing middleware.ts: ${msg}` })
2707
+ }
2708
+ ],
2709
+ isError: true
2710
+ };
2711
+ }
2712
+ try {
2713
+ const envBlock = generateEnvBlock(method, params);
2714
+ const envPath = (0, import_path6.join)(cwd, ".env.example");
2715
+ if ((0, import_fs6.existsSync)(envPath)) {
2716
+ const existing = (0, import_fs6.readFileSync)(envPath, "utf8");
2717
+ (0, import_fs6.writeFileSync)(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
2718
+ } else {
2719
+ (0, import_fs6.writeFileSync)(envPath, envBlock, "utf8");
2720
+ }
2721
+ filesWritten.push(".env.example");
2722
+ } catch (err) {
2723
+ const msg = err instanceof Error ? err.message : String(err);
2724
+ return {
2725
+ content: [
2726
+ {
2727
+ type: "text",
2728
+ text: JSON.stringify({ success: false, error: `Failed writing .env.example: ${msg}` })
2729
+ }
2730
+ ],
2731
+ isError: true
2732
+ };
2733
+ }
2734
+ try {
2735
+ const authYaml = generateYamlBlock(
2736
+ method,
2737
+ params,
2738
+ roles,
2739
+ defaultRole,
2740
+ hierarchy,
2741
+ auditEnabled,
2742
+ auditRetentionDays,
2743
+ protectedRoutes
2744
+ );
2745
+ const ymlPath = (0, import_path6.join)(cwd, "stackwright.yml");
2746
+ if (!(0, import_fs6.existsSync)(ymlPath)) {
2747
+ (0, import_fs6.writeFileSync)(ymlPath, authYaml, "utf8");
2748
+ } else {
2749
+ const existing = (0, import_fs6.readFileSync)(ymlPath, "utf8");
2750
+ (0, import_fs6.writeFileSync)(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
2751
+ }
2752
+ filesWritten.push("stackwright.yml");
2753
+ } catch (err) {
2754
+ const msg = err instanceof Error ? err.message : String(err);
2755
+ return {
2756
+ content: [
2757
+ {
2758
+ type: "text",
2759
+ text: JSON.stringify({ success: false, error: `Failed writing stackwright.yml: ${msg}` })
2760
+ }
2761
+ ],
2762
+ isError: true
2763
+ };
2764
+ }
2765
+ const securityWarning = method === "cac" ? "SECURITY REVIEW REQUIRED \u2014 CAC certificate chain must be verified before production deployment" : null;
2766
+ return {
2767
+ content: [
2768
+ {
2769
+ type: "text",
2770
+ text: JSON.stringify({
2771
+ success: true,
2772
+ method,
2773
+ provider: provider ?? null,
2774
+ rbacRoles: roles,
2775
+ rbacDefaultRole: defaultRole,
2776
+ protectedRoutesCount: protectedRoutes.length,
2777
+ filesWritten,
2778
+ securityWarning
2779
+ })
2780
+ }
2781
+ ]
2782
+ };
2783
+ }
2784
+ function registerAuthTools(server2) {
2785
+ server2.tool(
2786
+ "stackwright_pro_configure_auth",
2787
+ "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.",
2788
+ {
2789
+ method: import_zod11.z.enum(["cac", "oidc", "oauth2", "none"]),
2790
+ provider: import_zod11.z.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
2791
+ // CAC
2792
+ cacCaBundle: import_zod11.z.string().optional(),
2793
+ cacEdipiLookup: import_zod11.z.string().optional(),
2794
+ cacOcspEndpoint: import_zod11.z.string().optional(),
2795
+ cacCertHeader: import_zod11.z.string().optional(),
2796
+ // OIDC
2797
+ oidcDiscoveryUrl: import_zod11.z.string().optional(),
2798
+ oidcClientId: import_zod11.z.string().optional(),
2799
+ oidcClientSecret: import_zod11.z.string().optional(),
2800
+ oidcScopes: import_zod11.z.string().optional(),
2801
+ oidcRoleClaim: import_zod11.z.string().optional(),
2802
+ // OAuth2
2803
+ oauth2AuthUrl: import_zod11.z.string().optional(),
2804
+ oauth2TokenUrl: import_zod11.z.string().optional(),
2805
+ oauth2ClientId: import_zod11.z.string().optional(),
2806
+ oauth2ClientSecret: import_zod11.z.string().optional(),
2807
+ oauth2Scopes: import_zod11.z.string().optional(),
2808
+ // RBAC
2809
+ rbacRoles: import_zod11.z.array(import_zod11.z.string()).optional(),
2810
+ rbacDefaultRole: import_zod11.z.string().optional(),
2811
+ // Audit
2812
+ auditEnabled: import_zod11.z.boolean().optional(),
2813
+ auditRetentionDays: import_zod11.z.number().int().positive().optional(),
2814
+ // Routes
2815
+ protectedRoutes: import_zod11.z.array(import_zod11.z.string()).optional(),
2816
+ // Injection for tests
2817
+ _cwd: import_zod11.z.string().optional()
2818
+ },
2819
+ async (params) => {
2820
+ const cwd = params._cwd ?? process.cwd();
2821
+ return configureAuthHandler(params, cwd);
2822
+ }
2823
+ );
2824
+ }
2825
+
2826
+ // src/integrity.ts
2827
+ var import_crypto2 = require("crypto");
2828
+ var import_fs7 = require("fs");
2829
+ var import_path7 = require("path");
2830
+ var _checksums = /* @__PURE__ */ new Map([
2831
+ [
2832
+ "stackwright-pro-api-otter.json",
2833
+ "f1cc9edf2dd1df3ebcea1d0ab33d17a358faaf8aa97ee232cd7994042f2eac0d"
2834
+ ],
2835
+ [
2836
+ "stackwright-pro-auth-otter.json",
2837
+ "a19e06c503209a8a35fe321d30448623545b36b48c47a6ec064d13406ad1f725"
2838
+ ],
2839
+ [
2840
+ "stackwright-pro-dashboard-otter.json",
2841
+ "b3cb3d7554f2e9eed3b57d5e0e3bf85d6ba5b4db5d3af5514391cf0575fcc001"
2842
+ ],
2843
+ [
2844
+ "stackwright-pro-data-otter.json",
2845
+ "bfacb87ae82867472a75982215554336a105a658d6cd3dd2c8b819fa1e11d7ac"
2846
+ ],
2847
+ [
2848
+ "stackwright-pro-designer-otter.json",
2849
+ "c58fa7c7ead9e6398074e1c7ce3f31a8ef4eb3679f5fa18cc03cae3a87878c88"
2850
+ ],
2851
+ [
2852
+ "stackwright-pro-foreman-otter.json",
2853
+ "27f4dfd4676246c9fea14fd1de4f148928eeaf06e58b08240761fd3e663dd540"
2854
+ ],
2855
+ [
2856
+ "stackwright-pro-page-otter.json",
2857
+ "65bec3a3a0dda6b7591bba2de9399f1e3a4fb99cfe1075342f4f4be98d917b67"
2858
+ ],
2859
+ [
2860
+ "stackwright-pro-theme-otter.json",
2861
+ "64ffaeeceacd739922788a1d074f6feaffc3f91d09706c2c104f0c0281677732"
2862
+ ],
2863
+ [
2864
+ "stackwright-pro-workflow-otter.json",
2865
+ "0eec9d6a731678cf547c2a7b0b6fc338ca143c35501365a1e4e5dd2779dd5510"
2866
+ ]
2867
+ ]);
2868
+ Object.freeze(_checksums);
2869
+ var CANONICAL_CHECKSUMS = _checksums;
2870
+ var SHA256_HEX_RE = /^[0-9a-f]{64}$/;
2871
+ for (const [name, digest] of CANONICAL_CHECKSUMS) {
2872
+ if (!SHA256_HEX_RE.test(digest)) {
2873
+ throw new Error(
2874
+ `Malformed SHA-256 in CANONICAL_CHECKSUMS for "${name}": expected 64 hex chars, got ${digest.length}: "${digest}"`
2875
+ );
2876
+ }
2877
+ }
2878
+ var MAX_OTTER_BYTES = 1 * 1024 * 1024;
2879
+ function computeSha256(data) {
2880
+ return (0, import_crypto2.createHash)("sha256").update(data).digest("hex");
2881
+ }
2882
+ function safeEqual(a, b) {
2883
+ if (a.length !== b.length) return false;
2884
+ return (0, import_crypto2.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
2885
+ }
2886
+ function verifyOtterFile(filePath) {
2887
+ const filename = (0, import_path7.basename)(filePath);
2888
+ const expected = CANONICAL_CHECKSUMS.get(filename);
2889
+ if (expected === void 0) {
2890
+ return { verified: false, filename, error: `Unknown otter file: not in canonical set` };
2891
+ }
2892
+ let stat;
2893
+ try {
2894
+ stat = (0, import_fs7.lstatSync)(filePath);
2895
+ } catch (err) {
2896
+ const msg = err instanceof Error ? err.message : String(err);
2897
+ return { verified: false, filename, error: `Cannot stat file: ${msg}` };
2898
+ }
2899
+ if (stat.isSymbolicLink()) {
2900
+ return { verified: false, filename, error: "Refusing to verify symlink" };
2901
+ }
2902
+ const size = stat.size;
2903
+ if (size > MAX_OTTER_BYTES) {
2904
+ return {
2905
+ verified: false,
2906
+ filename,
2907
+ error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`
2908
+ };
2909
+ }
2910
+ let raw;
2911
+ try {
2912
+ raw = (0, import_fs7.readFileSync)(filePath);
2913
+ } catch (err) {
2914
+ const msg = err instanceof Error ? err.message : String(err);
2915
+ return { verified: false, filename, error: `Cannot read file: ${msg}` };
2916
+ }
2917
+ if (raw.length > MAX_OTTER_BYTES) {
2918
+ return {
2919
+ verified: false,
2920
+ filename,
2921
+ error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`
2922
+ };
2923
+ }
2924
+ const actual = computeSha256(raw);
2925
+ if (!safeEqual(actual, expected)) {
2926
+ return {
2927
+ verified: false,
2928
+ filename,
2929
+ error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}\u2026, got ${actual.substring(0, 8)}\u2026`
2930
+ };
2931
+ }
2932
+ try {
2933
+ const decoder = new TextDecoder("utf-8", { fatal: true });
2934
+ decoder.decode(raw);
2935
+ } catch {
2936
+ return {
2937
+ verified: false,
2938
+ filename,
2939
+ error: "File is not valid UTF-8 \u2014 may be corrupted or contain binary injection"
2940
+ };
2941
+ }
2942
+ return { verified: true, filename };
2943
+ }
2944
+ function verifyAllOtters(otterDir) {
2945
+ const verified = [];
2946
+ const failed = [];
2947
+ const unknown = [];
2948
+ let entries;
2949
+ try {
2950
+ entries = (0, import_fs7.readdirSync)(otterDir);
2951
+ } catch (err) {
2952
+ const msg = err instanceof Error ? err.message : String(err);
2953
+ return {
2954
+ verified: [],
2955
+ failed: [{ filename: "<directory>", error: `Cannot read directory: ${msg}` }],
2956
+ unknown: []
2957
+ };
2958
+ }
2959
+ const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
2960
+ for (const filename of otterFiles) {
2961
+ const filePath = (0, import_path7.join)(otterDir, filename);
2962
+ try {
2963
+ if ((0, import_fs7.lstatSync)(filePath).isSymbolicLink()) {
2964
+ failed.push({ filename, error: "Skipped: symlink" });
2965
+ continue;
2966
+ }
2967
+ } catch {
2968
+ }
2969
+ const result = verifyOtterFile(filePath);
2970
+ if (result.verified) {
2971
+ verified.push(result.filename);
2972
+ } else if (result.error?.startsWith("Unknown otter file")) {
2973
+ unknown.push(result.filename);
2974
+ } else {
2975
+ failed.push({ filename: result.filename, error: result.error ?? "Unknown error" });
2976
+ }
2977
+ }
2978
+ for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {
2979
+ if (!otterFiles.includes(canonicalName)) {
2980
+ failed.push({ filename: canonicalName, error: "Missing from directory" });
2981
+ }
2982
+ }
2983
+ return { verified, failed, unknown };
2984
+ }
2985
+ var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packages/otters/src/"];
2986
+ function resolveOtterDir() {
2987
+ const cwd = process.cwd();
2988
+ for (const relative of DEFAULT_SEARCH_PATHS) {
2989
+ const candidate = (0, import_path7.join)(cwd, relative);
2990
+ try {
2991
+ (0, import_fs7.lstatSync)(candidate);
2992
+ return candidate;
2993
+ } catch {
2994
+ }
2995
+ }
2996
+ return null;
2997
+ }
2998
+ function registerIntegrityTools(server2) {
2999
+ server2.tool(
3000
+ "stackwright_pro_verify_otter_integrity",
3001
+ "Verify SHA-256 integrity of all Pro otter agent definitions. Call this at startup before discovering otters. Auto-discovers the otter directory from known paths. Returns verified/failed/unknown lists.",
3002
+ {},
3003
+ async () => {
3004
+ const resolved = resolveOtterDir();
3005
+ if (!resolved) {
3006
+ return {
3007
+ content: [
3008
+ {
3009
+ type: "text",
3010
+ text: JSON.stringify({
3011
+ error: true,
3012
+ message: "Could not locate otter directory. Searched: " + DEFAULT_SEARCH_PATHS.join(", ")
3013
+ })
3014
+ }
3015
+ ],
3016
+ isError: true
3017
+ };
3018
+ }
3019
+ const result = verifyAllOtters(resolved);
3020
+ const allGood = result.failed.length === 0 && result.unknown.length === 0;
3021
+ return {
3022
+ content: [
3023
+ {
3024
+ type: "text",
3025
+ text: JSON.stringify({
3026
+ otterDir: resolved,
3027
+ totalCanonical: CANONICAL_CHECKSUMS.size,
3028
+ verifiedCount: result.verified.length,
3029
+ failedCount: result.failed.length,
3030
+ unknownCount: result.unknown.length,
3031
+ verified: result.verified,
3032
+ failed: result.failed,
3033
+ unknown: result.unknown,
3034
+ warning: result.failed.length > 0 ? "SHA-256 mismatches detected (non-blocking). PKI-signed manifest support coming soon." : void 0
3035
+ })
3036
+ }
3037
+ ],
3038
+ isError: false
3039
+ };
3040
+ }
3041
+ );
3042
+ }
3043
+
3044
+ // src/tools/domain.ts
3045
+ var import_zod12 = require("zod");
3046
+ var import_fs8 = require("fs");
3047
+ var import_path8 = require("path");
3048
+ function handleListCollections(input) {
3049
+ const cwd = input._cwd ?? process.cwd();
3050
+ const sources = [
3051
+ {
3052
+ path: (0, import_path8.join)(cwd, ".stackwright", "artifacts", "data-config.json"),
3053
+ source: "data-config.json",
3054
+ parse: (raw) => {
3055
+ const parsed = JSON.parse(raw);
3056
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3057
+ return [];
3058
+ }
3059
+ return extractCollectionsFromArtifact(parsed);
3060
+ }
3061
+ },
3062
+ {
3063
+ path: (0, import_path8.join)(cwd, "stackwright.yml"),
3064
+ source: "stackwright.yml",
3065
+ parse: extractCollectionsFromYaml
3066
+ }
3067
+ ];
3068
+ for (const { path: path3, source, parse } of sources) {
3069
+ if (!(0, import_fs8.existsSync)(path3)) continue;
3070
+ try {
3071
+ const collections = parse((0, import_fs8.readFileSync)(path3, "utf8"));
3072
+ return {
3073
+ text: JSON.stringify({ collections, source, collectionCount: collections.length }),
3074
+ isError: false
3075
+ };
3076
+ } catch {
3077
+ }
3078
+ }
3079
+ return {
3080
+ text: JSON.stringify({
3081
+ collections: [],
3082
+ source: "none",
3083
+ collectionCount: 0,
3084
+ hint: "Run API Otter and Data Otter first"
3085
+ }),
3086
+ isError: false
3087
+ };
3088
+ }
3089
+ function extractCollectionsFromArtifact(raw) {
3090
+ const collections = [];
3091
+ const integrations = raw.integrations ?? raw.collections ?? [];
3092
+ if (!Array.isArray(integrations)) return collections;
3093
+ for (const item of integrations) {
3094
+ if (!item || typeof item !== "object") continue;
3095
+ const obj = item;
3096
+ if (typeof obj.name === "string" && typeof obj.endpoint === "string") {
3097
+ collections.push(makeCollectionInfo(obj.name, obj.endpoint, obj.type));
3098
+ continue;
3099
+ }
3100
+ if (!Array.isArray(obj.collections)) continue;
3101
+ for (const col of obj.collections) {
3102
+ if (!col || typeof col !== "object") continue;
3103
+ const c = col;
3104
+ if (typeof c.name === "string" && typeof c.endpoint === "string") {
3105
+ collections.push(makeCollectionInfo(c.name, c.endpoint, c.type ?? obj.type));
3106
+ }
3107
+ }
3108
+ }
3109
+ return collections;
3110
+ }
3111
+ function makeCollectionInfo(name, endpoint, type) {
3112
+ return { name, endpoint, ...typeof type === "string" ? { type } : {} };
3113
+ }
3114
+ function extractCollectionsFromYaml(yamlText) {
3115
+ const collections = [];
3116
+ const lines = yamlText.split("\n");
3117
+ let inIntegrations = false;
3118
+ let currentName = null;
3119
+ let currentEndpoint = null;
3120
+ let currentType = null;
3121
+ for (const line of lines) {
3122
+ if (line.length > 1e3) continue;
3123
+ if (/^integrations:\s*$/.test(line)) {
3124
+ inIntegrations = true;
3125
+ continue;
3126
+ }
3127
+ if (inIntegrations && /^[a-z]/.test(line) && !line.startsWith(" ")) {
3128
+ inIntegrations = false;
3129
+ }
3130
+ if (!inIntegrations) continue;
3131
+ const stripQuotes = (s) => s.trim().replace(/^['"]|['"]$/g, "");
3132
+ const typeMatch = line.match(/^\s+type:\s*(.+)$/);
3133
+ if (typeMatch?.[1]) currentType = stripQuotes(typeMatch[1]);
3134
+ const nameMatch = line.match(/^[\s-]+name:\s*(.+)$/);
3135
+ if (nameMatch?.[1]) {
3136
+ if (currentName && currentEndpoint) {
3137
+ collections.push(makeCollectionInfo(currentName, currentEndpoint, currentType));
3138
+ }
3139
+ currentName = stripQuotes(nameMatch[1]);
3140
+ currentEndpoint = null;
3141
+ }
3142
+ const endpointMatch = line.match(/^\s+endpoint:\s*(.+)$/);
3143
+ if (endpointMatch?.[1]) currentEndpoint = stripQuotes(endpointMatch[1]);
3144
+ }
3145
+ if (currentName && currentEndpoint) {
3146
+ collections.push(makeCollectionInfo(currentName, currentEndpoint, currentType));
3147
+ }
3148
+ return collections;
3149
+ }
3150
+ var DATA_STRATEGIES = {
3151
+ "pulse-fast": {
3152
+ strategy: "pulse-fast",
3153
+ mechanism: "Client-side polling via @stackwright-pro/pulse",
3154
+ mechanismPackage: "@stackwright-pro/pulse",
3155
+ pulse: true,
3156
+ requiredPackages: { "@stackwright-pro/pulse": "latest", "@tanstack/react-query": "^5.0.0" },
3157
+ handoffFlags: ["PULSE_MODE=true"],
3158
+ description: "Real-time updates every few seconds. Uses client-side polling. Dashboard Otter should use *_pulse component variants."
3159
+ },
3160
+ "isr-fast": {
3161
+ strategy: "isr-fast",
3162
+ mechanism: "Next.js ISR",
3163
+ revalidateSeconds: 60,
3164
+ pulse: false,
3165
+ requiredPackages: {},
3166
+ handoffFlags: [],
3167
+ description: "Near real-time with 60-second ISR revalidation. Good for dashboards that need minute-level freshness."
3168
+ },
3169
+ "isr-standard": {
3170
+ strategy: "isr-standard",
3171
+ mechanism: "Next.js ISR",
3172
+ revalidateSeconds: 3600,
3173
+ pulse: false,
3174
+ requiredPackages: {},
3175
+ handoffFlags: [],
3176
+ description: "Standard hourly revalidation. Good for most API-backed pages."
3177
+ },
3178
+ "isr-slow": {
3179
+ strategy: "isr-slow",
3180
+ mechanism: "Next.js ISR",
3181
+ revalidateSeconds: 86400,
3182
+ pulse: false,
3183
+ requiredPackages: {},
3184
+ handoffFlags: [],
3185
+ description: "Daily revalidation. Good for infrequently changing data."
3186
+ }
3187
+ };
3188
+ function handleResolveDataStrategy(input) {
3189
+ const key = input.strategy.trim().toLowerCase();
3190
+ const match = DATA_STRATEGIES[key];
3191
+ if (!match) {
3192
+ const validKeys = Object.keys(DATA_STRATEGIES).join(", ");
3193
+ return {
3194
+ text: JSON.stringify({
3195
+ error: true,
3196
+ message: `Unknown strategy: "${input.strategy}". Valid strategies: ${validKeys}`,
3197
+ validStrategies: Object.keys(DATA_STRATEGIES)
3198
+ }),
3199
+ isError: true
3200
+ };
3201
+ }
3202
+ const configureIsrCall = match.pulse ? null : {
3203
+ tool: "stackwright_pro_configure_isr_batch",
3204
+ args: {
3205
+ collections: [{ name: "$COLLECTION", revalidateSeconds: match.revalidateSeconds }]
3206
+ }
3207
+ };
3208
+ return {
3209
+ text: JSON.stringify({ ...match, configureIsrCall }),
3210
+ isError: false
3211
+ };
3212
+ }
3213
+ function fail(errors) {
3214
+ return { text: JSON.stringify({ valid: false, errors, warnings: [] }), isError: true };
3215
+ }
3216
+ function handleValidateWorkflow(input) {
3217
+ const cwd = input._cwd ?? process.cwd();
3218
+ let raw;
3219
+ if (input.workflow && Object.keys(input.workflow).length > 0) {
3220
+ raw = input.workflow;
3221
+ } else {
3222
+ const artifactPath = (0, import_path8.join)(cwd, ".stackwright", "artifacts", "workflow-config.json");
3223
+ if (!(0, import_fs8.existsSync)(artifactPath)) {
3224
+ return fail([
3225
+ {
3226
+ code: "NO_WORKFLOW",
3227
+ message: "No workflow provided and .stackwright/artifacts/workflow-config.json not found. Pass a workflow object or run the workflow otter first."
3228
+ }
3229
+ ]);
3230
+ }
3231
+ try {
3232
+ raw = JSON.parse((0, import_fs8.readFileSync)(artifactPath, "utf8"));
3233
+ } catch (err) {
3234
+ return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
3235
+ }
3236
+ }
3237
+ const workflow = raw.workflow && typeof raw.workflow === "object" ? raw.workflow : raw;
3238
+ const errors = [];
3239
+ const warnings = [];
3240
+ if (typeof workflow.id !== "string" || !workflow.id) {
3241
+ errors.push({ code: "MISSING_ID", message: "workflow.id is required", path: "workflow.id" });
3242
+ } else if (!/^[a-z0-9-]+$/.test(workflow.id)) {
3243
+ errors.push({
3244
+ code: "INVALID_ID",
3245
+ message: `workflow.id "${workflow.id}" must match ^[a-z0-9-]+$`,
3246
+ path: "workflow.id"
3247
+ });
3248
+ }
3249
+ if (typeof workflow.label !== "string" || !workflow.label) {
3250
+ errors.push({
3251
+ code: "MISSING_LABEL",
3252
+ message: "workflow.label is required",
3253
+ path: "workflow.label"
3254
+ });
3255
+ }
3256
+ const steps = workflow.steps;
3257
+ if (!Array.isArray(steps)) {
3258
+ errors.push({
3259
+ code: "MISSING_STEPS",
3260
+ message: "workflow.steps must be an array",
3261
+ path: "workflow.steps"
3262
+ });
3263
+ return { text: JSON.stringify({ valid: false, errors, warnings }), isError: false };
3264
+ }
3265
+ if (steps.length < 2) {
3266
+ errors.push({
3267
+ code: "TOO_FEW_STEPS",
3268
+ message: "A workflow must have at least 2 steps",
3269
+ path: "workflow.steps"
3270
+ });
3271
+ }
3272
+ const stepIds = /* @__PURE__ */ new Set();
3273
+ const duplicateIds = [];
3274
+ for (const step of steps) {
3275
+ if (!step || typeof step !== "object") continue;
3276
+ const id = step.id;
3277
+ if (typeof id !== "string" || !id) {
3278
+ errors.push({
3279
+ code: "MISSING_STEP_ID",
3280
+ message: "Every step must have an id",
3281
+ path: "workflow.steps"
3282
+ });
3283
+ continue;
3284
+ }
3285
+ if (!/^[a-z0-9_]+$/.test(id)) {
3286
+ errors.push({
3287
+ code: "INVALID_STEP_ID",
3288
+ message: `Step ID "${id}" must match ^[a-z0-9_]+$`,
3289
+ path: `workflow.steps[${id}].id`
3290
+ });
3291
+ }
3292
+ if (stepIds.has(id)) duplicateIds.push(id);
3293
+ stepIds.add(id);
3294
+ }
3295
+ if (duplicateIds.length > 0) {
3296
+ errors.push({
3297
+ code: "DUPLICATE_STEP_IDS",
3298
+ message: `Duplicate step IDs: ${duplicateIds.join(", ")}`,
3299
+ path: "workflow.steps"
3300
+ });
3301
+ }
3302
+ const initialStep = workflow.initial_step;
3303
+ if (typeof initialStep !== "string" || !initialStep) {
3304
+ errors.push({
3305
+ code: "MISSING_INITIAL_STEP",
3306
+ message: "workflow.initial_step is required",
3307
+ path: "workflow.initial_step"
3308
+ });
3309
+ } else if (!stepIds.has(initialStep)) {
3310
+ errors.push({
3311
+ code: "INVALID_INITIAL_STEP",
3312
+ message: `initial_step "${initialStep}" does not reference an existing step. Valid: ${[...stepIds].join(", ")}`,
3313
+ path: "workflow.initial_step"
3314
+ });
3315
+ }
3316
+ for (const step of steps) {
3317
+ if (!step || typeof step !== "object") continue;
3318
+ const s = step;
3319
+ const stepId = String(s.id ?? "??");
3320
+ validateTransitionTargets(s, stepId, stepIds, errors);
3321
+ collectServiceWarnings(s, stepId, warnings);
3322
+ }
3323
+ if (typeof workflow.persistence === "string" && workflow.persistence.startsWith("service:")) {
3324
+ warnings.push({
3325
+ code: "WARN_SERVICE_REFERENCE",
3326
+ message: 'service: reference at "workflow.persistence" requires @stackwright-pro/services. Prism mock fallback will be used until services layer is configured.',
3327
+ path: "workflow.persistence"
3328
+ });
3329
+ }
3330
+ const hasTerminal = steps.some((step) => {
3331
+ if (!step || typeof step !== "object") return false;
3332
+ return step.type === "terminal";
3333
+ });
3334
+ if (!hasTerminal) {
3335
+ errors.push({
3336
+ code: "NO_TERMINAL_STATE",
3337
+ message: "Workflow must have at least one step with type: terminal",
3338
+ path: "workflow.steps"
3339
+ });
3340
+ }
3341
+ return { text: JSON.stringify({ valid: errors.length === 0, errors, warnings }), isError: false };
3342
+ }
3343
+ function validateTransitionTargets(step, stepId, stepIds, errors) {
3344
+ const check = (target, path3) => {
3345
+ if (typeof target === "string" && !stepIds.has(target)) {
3346
+ errors.push({
3347
+ code: "ORPHANED_TRANSITION",
3348
+ message: `Step "${stepId}" transitions to "${target}" which does not exist`,
3349
+ path: path3
3350
+ });
3351
+ }
3352
+ };
3353
+ const onSubmit = step.on_submit;
3354
+ if (onSubmit && typeof onSubmit === "object") {
3355
+ check(onSubmit.transition, `workflow.steps[${stepId}].on_submit.transition`);
3356
+ }
3357
+ if (Array.isArray(step.actions)) {
3358
+ for (const action of step.actions) {
3359
+ if (!action || typeof action !== "object") continue;
3360
+ const a = action;
3361
+ check(a.transition, `workflow.steps[${stepId}].actions[${a.id ?? "??"}].transition`);
3362
+ }
3363
+ }
3364
+ if (Array.isArray(step.conditions)) {
3365
+ for (const cond of step.conditions) {
3366
+ if (!cond || typeof cond !== "object") continue;
3367
+ const then = cond.then;
3368
+ if (then && typeof then === "object") {
3369
+ check(then.transition, `workflow.steps[${stepId}].conditions`);
3370
+ }
3371
+ }
3372
+ }
3373
+ if (Array.isArray(step.show_fields_from)) {
3374
+ for (const ref of step.show_fields_from)
3375
+ check(ref, `workflow.steps[${stepId}].show_fields_from`);
3376
+ }
3377
+ const display = step.display;
3378
+ if (display && typeof display === "object") {
3379
+ check(display.source_step, `workflow.steps[${stepId}].display.source_step`);
3380
+ if (Array.isArray(display.source_steps)) {
3381
+ for (const ref of display.source_steps)
3382
+ check(ref, `workflow.steps[${stepId}].display.source_steps`);
3383
+ }
3384
+ }
3385
+ }
3386
+ function collectServiceWarnings(step, stepId, warnings) {
3387
+ const isService = (val) => typeof val === "string" && val.startsWith("service:");
3388
+ const warn = (path3) => {
3389
+ warnings.push({
3390
+ code: "WARN_SERVICE_REFERENCE",
3391
+ message: `service: reference at "${path3}" requires @stackwright-pro/services. Prism mock fallback will be used until services layer is configured.`,
3392
+ path: path3
3393
+ });
3394
+ };
3395
+ const onSubmit = step.on_submit;
3396
+ if (onSubmit && typeof onSubmit === "object" && isService(onSubmit.action))
3397
+ warn(`${stepId}.on_submit.action`);
3398
+ const onEnter = step.on_enter;
3399
+ if (onEnter && typeof onEnter === "object" && isService(onEnter.action))
3400
+ warn(`${stepId}.on_enter.action`);
3401
+ if (Array.isArray(step.actions)) {
3402
+ for (const action of step.actions) {
3403
+ if (!action || typeof action !== "object") continue;
3404
+ const a = action;
3405
+ if (isService(a.action)) warn(`${stepId}.actions.${a.id ?? "??"}.action`);
3406
+ }
3407
+ }
3408
+ if (Array.isArray(step.fields)) {
3409
+ for (const field of step.fields) {
3410
+ if (!field || typeof field !== "object") continue;
3411
+ const f = field;
3412
+ if (isService(f.data_source)) warn(`${stepId}.fields.${f.name ?? "??"}.data_source`);
3413
+ }
3414
+ }
3415
+ }
3416
+ function registerDomainTools(server2) {
3417
+ const res = (r) => ({
3418
+ content: [{ type: "text", text: r.text }],
3419
+ isError: r.isError
3420
+ });
3421
+ server2.tool(
3422
+ "stackwright_pro_list_collections",
3423
+ "List API-backed collections available for page generation. Reads from Data Otter artifact (.stackwright/artifacts/data-config.json) or stackwright.yml. Call this before generating pages that bind to data.",
3424
+ {},
3425
+ async () => res(handleListCollections({}))
3426
+ );
3427
+ server2.tool(
3428
+ "stackwright_pro_resolve_data_strategy",
3429
+ "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.",
3430
+ {
3431
+ strategy: import_zod12.z.string().describe(
3432
+ 'The data-1 answer value: "pulse-fast", "isr-fast", "isr-standard", or "isr-slow"'
3433
+ )
3434
+ },
3435
+ async ({ strategy }) => res(handleResolveDataStrategy({ strategy }))
3436
+ );
3437
+ server2.tool(
3438
+ "stackwright_pro_validate_workflow",
3439
+ "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.",
3440
+ {
3441
+ workflow: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown()).optional().describe(
3442
+ "Parsed workflow object. If omitted, reads from .stackwright/artifacts/workflow-config.json"
3443
+ )
3444
+ },
3445
+ async ({ workflow }) => res(handleValidateWorkflow(workflow ? { workflow } : {}))
3446
+ );
3447
+ }
3448
+
3449
+ // package.json
3450
+ var package_default = {
3451
+ dependencies: {
3452
+ "@modelcontextprotocol/sdk": "^1.10.0",
3453
+ "@stackwright-pro/cli-data-explorer": "workspace:*",
3454
+ zod: "^4.3.6"
3455
+ },
3456
+ devDependencies: {
3457
+ "@types/node": "^24.1.0",
3458
+ tsup: "^8.5.0",
3459
+ typescript: "^5.8.3",
3460
+ vitest: "^4.0.18"
3461
+ },
3462
+ scripts: {
3463
+ prepublishOnly: "node scripts/verify-integrity-sync.js",
3464
+ build: "tsup",
3465
+ dev: "tsup --watch",
3466
+ start: "node dist/server.js",
3467
+ test: "vitest run",
3468
+ "test:coverage": "vitest run --coverage"
3469
+ },
3470
+ name: "@stackwright-pro/mcp",
3471
+ version: "0.2.0-alpha.10",
3472
+ description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
3473
+ license: "PROPRIETARY",
3474
+ main: "./dist/server.js",
3475
+ module: "./dist/server.mjs",
3476
+ types: "./dist/server.d.ts",
3477
+ exports: {
3478
+ ".": {
3479
+ types: "./dist/server.d.ts",
3480
+ import: "./dist/server.mjs",
3481
+ require: "./dist/server.js"
3482
+ },
3483
+ "./integrity": {
3484
+ types: "./dist/integrity.d.ts",
3485
+ import: "./dist/integrity.mjs",
3486
+ require: "./dist/integrity.js"
3487
+ }
3488
+ },
3489
+ files: [
3490
+ "dist"
3491
+ ],
3492
+ publishConfig: {
3493
+ access: "public"
3494
+ }
3495
+ };
3496
+
3497
+ // src/server.ts
1212
3498
  var server = new import_mcp.McpServer({
1213
3499
  name: "stackwright-pro",
1214
- version: packageJson.version
3500
+ version: package_default.version
1215
3501
  });
1216
3502
  registerDataExplorerTools(server);
1217
3503
  registerSecurityTools(server);
@@ -1219,6 +3505,13 @@ registerIsrTools(server);
1219
3505
  registerDashboardTools(server);
1220
3506
  registerClarificationTools(server);
1221
3507
  registerPackageTools(server);
3508
+ registerQuestionTools(server);
3509
+ registerOrchestrationTools(server);
3510
+ registerPipelineTools(server);
3511
+ registerSafeWriteTools(server);
3512
+ registerAuthTools(server);
3513
+ registerIntegrityTools(server);
3514
+ registerDomainTools(server);
1222
3515
  async function main() {
1223
3516
  const transport = new import_stdio.StdioServerTransport();
1224
3517
  await server.connect(transport);