@stackwright-pro/mcp 0.2.0-alpha.16 → 0.2.0-alpha.18
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/integrity.js +9 -9
- package/dist/integrity.js.map +1 -1
- package/dist/integrity.mjs +9 -9
- package/dist/integrity.mjs.map +1 -1
- package/dist/server.js +308 -31
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +344 -67
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
package/dist/server.mjs
CHANGED
|
@@ -1110,7 +1110,8 @@ function setupPackages(opts) {
|
|
|
1110
1110
|
}
|
|
1111
1111
|
|
|
1112
1112
|
// src/tools/questions.ts
|
|
1113
|
-
import { readFile } from "fs/promises";
|
|
1113
|
+
import { readFile, writeFile } from "fs/promises";
|
|
1114
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, mkdirSync, renameSync } from "fs";
|
|
1114
1115
|
import { join } from "path";
|
|
1115
1116
|
import { z as z8 } from "zod";
|
|
1116
1117
|
|
|
@@ -1383,12 +1384,12 @@ function registerQuestionTools(server2) {
|
|
|
1383
1384
|
content: [
|
|
1384
1385
|
{
|
|
1385
1386
|
type: "text",
|
|
1386
|
-
text:
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1387
|
+
text: `Phase "${phase}" has no questions to present. Do NOT call ask_user_question. Call stackwright_pro_save_phase_answers({ phase: "${phase}", rawAnswers: [] }) directly, then proceed to the execution step for this phase.`
|
|
1388
|
+
},
|
|
1389
|
+
{
|
|
1390
|
+
type: "text",
|
|
1391
|
+
// Empty array — second block always present so the foreman's two-block contract holds
|
|
1392
|
+
text: JSON.stringify([])
|
|
1392
1393
|
}
|
|
1393
1394
|
],
|
|
1394
1395
|
isError: false
|
|
@@ -1409,11 +1410,149 @@ function registerQuestionTools(server2) {
|
|
|
1409
1410
|
};
|
|
1410
1411
|
}
|
|
1411
1412
|
);
|
|
1413
|
+
server2.tool(
|
|
1414
|
+
"stackwright_pro_get_next_question",
|
|
1415
|
+
"Returns the next unanswered question for a phase as a plain JSON object \u2014 one question at a time. Returns { done: true } when all questions are answered or the phase has no questions. Call this in a loop: present the question in plain chat, get user reply, call record_answer, repeat.",
|
|
1416
|
+
{ phase: z8.string().describe('Phase name e.g. "designer", "api", "auth"') },
|
|
1417
|
+
async ({ phase }) => {
|
|
1418
|
+
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
1419
|
+
if (!SAFE_PHASE.test(phase)) {
|
|
1420
|
+
return {
|
|
1421
|
+
content: [
|
|
1422
|
+
{
|
|
1423
|
+
type: "text",
|
|
1424
|
+
text: JSON.stringify({ error: `Invalid phase name: "${phase.slice(0, 50)}"` })
|
|
1425
|
+
}
|
|
1426
|
+
],
|
|
1427
|
+
isError: true
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
const cwd = process.cwd();
|
|
1431
|
+
const questionsPath = join(cwd, ".stackwright", "questions", `${phase}.json`);
|
|
1432
|
+
let questions = [];
|
|
1433
|
+
try {
|
|
1434
|
+
const raw = await readFile(questionsPath, "utf-8");
|
|
1435
|
+
const parsed = JSON.parse(raw);
|
|
1436
|
+
questions = parsed.questions ?? [];
|
|
1437
|
+
} catch {
|
|
1438
|
+
return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
|
|
1439
|
+
}
|
|
1440
|
+
if (questions.length === 0) {
|
|
1441
|
+
return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
|
|
1442
|
+
}
|
|
1443
|
+
const answersPath = join(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
1444
|
+
let answeredIds = /* @__PURE__ */ new Set();
|
|
1445
|
+
try {
|
|
1446
|
+
const raw = await readFile(answersPath, "utf-8");
|
|
1447
|
+
const parsed = JSON.parse(raw);
|
|
1448
|
+
answeredIds = new Set(Object.keys(parsed.answers ?? {}));
|
|
1449
|
+
} catch {
|
|
1450
|
+
}
|
|
1451
|
+
const next = questions.find((q) => !answeredIds.has(q.id));
|
|
1452
|
+
if (!next) {
|
|
1453
|
+
return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
|
|
1454
|
+
}
|
|
1455
|
+
return {
|
|
1456
|
+
content: [
|
|
1457
|
+
{
|
|
1458
|
+
type: "text",
|
|
1459
|
+
text: JSON.stringify({
|
|
1460
|
+
done: false,
|
|
1461
|
+
questionId: next.id,
|
|
1462
|
+
question: next.question,
|
|
1463
|
+
type: next.type,
|
|
1464
|
+
options: next.options ?? null,
|
|
1465
|
+
help: next.help ?? null,
|
|
1466
|
+
index: questions.indexOf(next) + 1,
|
|
1467
|
+
total: questions.length
|
|
1468
|
+
})
|
|
1469
|
+
}
|
|
1470
|
+
]
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
);
|
|
1474
|
+
server2.tool(
|
|
1475
|
+
"stackwright_pro_record_answer",
|
|
1476
|
+
"Records a single answer to a phase question. Appends to .stackwright/answers/{phase}.json incrementally. Idempotent \u2014 calling twice for the same questionId overwrites. Call after receiving each user reply in the conversational question loop.",
|
|
1477
|
+
{
|
|
1478
|
+
phase: z8.string().describe('Phase name e.g. "designer"'),
|
|
1479
|
+
questionId: z8.string().describe('The question ID from get_next_question, e.g. "designer-1"'),
|
|
1480
|
+
answer: z8.string().describe("The user's free-text answer")
|
|
1481
|
+
},
|
|
1482
|
+
async ({ phase, questionId, answer }) => {
|
|
1483
|
+
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
1484
|
+
if (!SAFE_PHASE.test(phase)) {
|
|
1485
|
+
return {
|
|
1486
|
+
content: [
|
|
1487
|
+
{ type: "text", text: JSON.stringify({ error: "Invalid phase name" }) }
|
|
1488
|
+
],
|
|
1489
|
+
isError: true
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/i.test(questionId)) {
|
|
1493
|
+
return {
|
|
1494
|
+
content: [
|
|
1495
|
+
{ type: "text", text: JSON.stringify({ error: "Invalid questionId" }) }
|
|
1496
|
+
],
|
|
1497
|
+
isError: true
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
const safeAnswer = answer.slice(0, 2e3);
|
|
1501
|
+
const cwd = process.cwd();
|
|
1502
|
+
const answersDir = join(cwd, ".stackwright", "answers");
|
|
1503
|
+
const answersPath = join(answersDir, `${phase}.json`);
|
|
1504
|
+
if (existsSync2(answersDir) && lstatSync2(answersDir).isSymbolicLink()) {
|
|
1505
|
+
return {
|
|
1506
|
+
content: [
|
|
1507
|
+
{
|
|
1508
|
+
type: "text",
|
|
1509
|
+
text: JSON.stringify({ error: "answers dir is a symlink \u2014 refusing to write" })
|
|
1510
|
+
}
|
|
1511
|
+
],
|
|
1512
|
+
isError: true
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
mkdirSync(answersDir, { recursive: true });
|
|
1516
|
+
let existing = {
|
|
1517
|
+
version: "1.0",
|
|
1518
|
+
phase,
|
|
1519
|
+
answers: {}
|
|
1520
|
+
};
|
|
1521
|
+
try {
|
|
1522
|
+
if (existsSync2(answersPath)) {
|
|
1523
|
+
if (lstatSync2(answersPath).isSymbolicLink()) {
|
|
1524
|
+
return {
|
|
1525
|
+
content: [
|
|
1526
|
+
{
|
|
1527
|
+
type: "text",
|
|
1528
|
+
text: JSON.stringify({ error: "answers file is a symlink \u2014 refusing to write" })
|
|
1529
|
+
}
|
|
1530
|
+
],
|
|
1531
|
+
isError: true
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
const raw = await readFile(answersPath, "utf-8");
|
|
1535
|
+
existing = JSON.parse(raw);
|
|
1536
|
+
}
|
|
1537
|
+
} catch {
|
|
1538
|
+
}
|
|
1539
|
+
existing.answers[questionId] = safeAnswer;
|
|
1540
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1541
|
+
const tmp = `${answersPath}.tmp`;
|
|
1542
|
+
await writeFile(tmp, JSON.stringify(existing, null, 2), "utf-8");
|
|
1543
|
+
renameSync(tmp, answersPath);
|
|
1544
|
+
return {
|
|
1545
|
+
content: [
|
|
1546
|
+
{ type: "text", text: JSON.stringify({ recorded: true, phase, questionId }) }
|
|
1547
|
+
]
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
);
|
|
1412
1551
|
}
|
|
1413
1552
|
|
|
1414
1553
|
// src/tools/orchestration.ts
|
|
1415
1554
|
import { z as z9 } from "zod";
|
|
1416
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as
|
|
1555
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, lstatSync as lstatSync3 } from "fs";
|
|
1417
1556
|
import { join as join2 } from "path";
|
|
1418
1557
|
var OTTER_NAME_TO_PHASE = [
|
|
1419
1558
|
["designer", "designer"],
|
|
@@ -1469,9 +1608,9 @@ function handleSaveManifest(input) {
|
|
|
1469
1608
|
const dir = join2(cwd, ".stackwright");
|
|
1470
1609
|
const filePath = join2(dir, "question-manifest.json");
|
|
1471
1610
|
try {
|
|
1472
|
-
|
|
1473
|
-
if (
|
|
1474
|
-
const stat =
|
|
1611
|
+
mkdirSync2(dir, { recursive: true });
|
|
1612
|
+
if (existsSync3(filePath)) {
|
|
1613
|
+
const stat = lstatSync3(filePath);
|
|
1475
1614
|
if (stat.isSymbolicLink()) {
|
|
1476
1615
|
const message = `Refusing to write to symlink: ${filePath}`;
|
|
1477
1616
|
return {
|
|
@@ -1503,7 +1642,7 @@ function handleSavePhaseAnswers(input) {
|
|
|
1503
1642
|
const dir = join2(cwd, ".stackwright", "answers");
|
|
1504
1643
|
const filePath = join2(dir, `${input.phase}.json`);
|
|
1505
1644
|
try {
|
|
1506
|
-
|
|
1645
|
+
mkdirSync2(dir, { recursive: true });
|
|
1507
1646
|
let answers;
|
|
1508
1647
|
if (input.questions && input.questions.length > 0) {
|
|
1509
1648
|
answers = answersToManifestFormat(input.rawAnswers, input.questions);
|
|
@@ -1518,8 +1657,8 @@ function handleSavePhaseAnswers(input) {
|
|
|
1518
1657
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1519
1658
|
answers
|
|
1520
1659
|
};
|
|
1521
|
-
if (
|
|
1522
|
-
const stat =
|
|
1660
|
+
if (existsSync3(filePath)) {
|
|
1661
|
+
const stat = lstatSync3(filePath);
|
|
1523
1662
|
if (stat.isSymbolicLink()) {
|
|
1524
1663
|
const message = `Refusing to write to symlink: ${filePath}`;
|
|
1525
1664
|
return {
|
|
@@ -1548,7 +1687,7 @@ function handleSavePhaseAnswers(input) {
|
|
|
1548
1687
|
function handleReadPhaseAnswers(input) {
|
|
1549
1688
|
const cwd = input._cwd ?? process.cwd();
|
|
1550
1689
|
const filePath = join2(cwd, ".stackwright", "answers", `${input.phase}.json`);
|
|
1551
|
-
if (!
|
|
1690
|
+
if (!existsSync3(filePath)) {
|
|
1552
1691
|
return {
|
|
1553
1692
|
text: JSON.stringify({ missing: true, phase: input.phase }),
|
|
1554
1693
|
isError: false
|
|
@@ -1580,7 +1719,57 @@ function handleGetOtterName(input) {
|
|
|
1580
1719
|
isError: false
|
|
1581
1720
|
};
|
|
1582
1721
|
}
|
|
1722
|
+
function handleSaveBuildContext(input) {
|
|
1723
|
+
const cwd = input._cwd ?? process.cwd();
|
|
1724
|
+
const dir = join2(cwd, ".stackwright");
|
|
1725
|
+
const filePath = join2(dir, "build-context.json");
|
|
1726
|
+
try {
|
|
1727
|
+
mkdirSync2(dir, { recursive: true });
|
|
1728
|
+
if (existsSync3(filePath)) {
|
|
1729
|
+
const stat = lstatSync3(filePath);
|
|
1730
|
+
if (stat.isSymbolicLink()) {
|
|
1731
|
+
return {
|
|
1732
|
+
text: JSON.stringify({
|
|
1733
|
+
success: false,
|
|
1734
|
+
error: `Refusing to write to symlink: ${filePath}`
|
|
1735
|
+
}),
|
|
1736
|
+
isError: true
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
const payload = {
|
|
1741
|
+
version: "1.0",
|
|
1742
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1743
|
+
buildContext: input.buildContext
|
|
1744
|
+
};
|
|
1745
|
+
writeFileSync2(filePath, JSON.stringify(payload, null, 2) + "\n");
|
|
1746
|
+
return {
|
|
1747
|
+
text: JSON.stringify({ success: true, path: filePath }),
|
|
1748
|
+
isError: false
|
|
1749
|
+
};
|
|
1750
|
+
} catch (err) {
|
|
1751
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1752
|
+
return {
|
|
1753
|
+
text: JSON.stringify({ success: false, error: message }),
|
|
1754
|
+
isError: true
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1583
1758
|
function registerOrchestrationTools(server2) {
|
|
1759
|
+
server2.tool(
|
|
1760
|
+
"stackwright_pro_save_build_context",
|
|
1761
|
+
`Save the user's initial build description to .stackwright/build-context.json. Call this once at startup after the user answers the opening "what are you building" question. The saved context is automatically prepended to specialist prompts by stackwright_pro_build_specialist_prompt.`,
|
|
1762
|
+
{
|
|
1763
|
+
buildContext: z9.string().describe("Free-text description of what the user wants to build")
|
|
1764
|
+
},
|
|
1765
|
+
async ({ buildContext }) => {
|
|
1766
|
+
const { text, isError } = handleSaveBuildContext({ buildContext });
|
|
1767
|
+
return {
|
|
1768
|
+
content: [{ type: "text", text }],
|
|
1769
|
+
isError
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
);
|
|
1584
1773
|
server2.tool(
|
|
1585
1774
|
"stackwright_pro_parse_otter_response",
|
|
1586
1775
|
"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.",
|
|
@@ -1686,7 +1875,7 @@ function registerOrchestrationTools(server2) {
|
|
|
1686
1875
|
|
|
1687
1876
|
// src/tools/pipeline.ts
|
|
1688
1877
|
import { z as z10 } from "zod";
|
|
1689
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as
|
|
1878
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3, lstatSync as lstatSync4 } from "fs";
|
|
1690
1879
|
import { join as join3 } from "path";
|
|
1691
1880
|
var PHASE_ORDER = [
|
|
1692
1881
|
"designer",
|
|
@@ -1760,7 +1949,7 @@ function statePath(cwd) {
|
|
|
1760
1949
|
}
|
|
1761
1950
|
function readState(cwd) {
|
|
1762
1951
|
const p = statePath(cwd);
|
|
1763
|
-
if (!
|
|
1952
|
+
if (!existsSync4(p)) return createDefaultState();
|
|
1764
1953
|
const raw = JSON.parse(safeReadSync(p));
|
|
1765
1954
|
if (typeof raw !== "object" || raw === null || raw.version !== "1.0") {
|
|
1766
1955
|
return createDefaultState();
|
|
@@ -1768,8 +1957,8 @@ function readState(cwd) {
|
|
|
1768
1957
|
return raw;
|
|
1769
1958
|
}
|
|
1770
1959
|
function safeWriteSync(filePath, content) {
|
|
1771
|
-
if (
|
|
1772
|
-
const stat =
|
|
1960
|
+
if (existsSync4(filePath)) {
|
|
1961
|
+
const stat = lstatSync4(filePath);
|
|
1773
1962
|
if (stat.isSymbolicLink()) {
|
|
1774
1963
|
throw new Error(`Refusing to write to symlink: ${filePath}`);
|
|
1775
1964
|
}
|
|
@@ -1777,8 +1966,8 @@ function safeWriteSync(filePath, content) {
|
|
|
1777
1966
|
writeFileSync3(filePath, content);
|
|
1778
1967
|
}
|
|
1779
1968
|
function safeReadSync(filePath) {
|
|
1780
|
-
if (
|
|
1781
|
-
const stat =
|
|
1969
|
+
if (existsSync4(filePath)) {
|
|
1970
|
+
const stat = lstatSync4(filePath);
|
|
1782
1971
|
if (stat.isSymbolicLink()) {
|
|
1783
1972
|
throw new Error(`Refusing to read symlink: ${filePath}`);
|
|
1784
1973
|
}
|
|
@@ -1787,7 +1976,7 @@ function safeReadSync(filePath) {
|
|
|
1787
1976
|
}
|
|
1788
1977
|
function writeState(cwd, state) {
|
|
1789
1978
|
const dir = join3(cwd, ".stackwright");
|
|
1790
|
-
|
|
1979
|
+
mkdirSync3(dir, { recursive: true });
|
|
1791
1980
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1792
1981
|
safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
|
|
1793
1982
|
}
|
|
@@ -1859,28 +2048,62 @@ function handleSetPipelineState(input) {
|
|
|
1859
2048
|
return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
|
|
1860
2049
|
}
|
|
1861
2050
|
}
|
|
1862
|
-
function handleCheckExecutionReady(_cwd) {
|
|
2051
|
+
function handleCheckExecutionReady(_cwd, phase) {
|
|
1863
2052
|
const cwd = _cwd ?? process.cwd();
|
|
2053
|
+
if (phase) {
|
|
2054
|
+
if (!isValidPhase(phase)) {
|
|
2055
|
+
return {
|
|
2056
|
+
text: JSON.stringify({
|
|
2057
|
+
error: true,
|
|
2058
|
+
message: `Invalid phase: ${phase}. Valid phases are: ${PHASE_ORDER.join(", ")}`
|
|
2059
|
+
}),
|
|
2060
|
+
isError: true
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
const answerFile = join3(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
2064
|
+
if (!existsSync4(answerFile)) {
|
|
2065
|
+
return {
|
|
2066
|
+
text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
|
|
2067
|
+
isError: false
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
try {
|
|
2071
|
+
const raw = safeReadSync(answerFile);
|
|
2072
|
+
const parsed = JSON.parse(raw);
|
|
2073
|
+
if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
|
|
2074
|
+
return {
|
|
2075
|
+
text: JSON.stringify({ ready: false, phase, reason: "Answer file is malformed" }),
|
|
2076
|
+
isError: false
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
return { text: JSON.stringify({ ready: true, phase }), isError: false };
|
|
2080
|
+
} catch {
|
|
2081
|
+
return {
|
|
2082
|
+
text: JSON.stringify({ ready: false, phase, reason: "Answer file could not be parsed" }),
|
|
2083
|
+
isError: false
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
1864
2087
|
try {
|
|
1865
2088
|
const answersDir = join3(cwd, ".stackwright", "answers");
|
|
1866
2089
|
const answeredPhases = [];
|
|
1867
2090
|
const missingPhases = [];
|
|
1868
|
-
for (const
|
|
1869
|
-
const answerFile = join3(answersDir, `${
|
|
1870
|
-
if (
|
|
2091
|
+
for (const phase2 of PHASE_ORDER) {
|
|
2092
|
+
const answerFile = join3(answersDir, `${phase2}.json`);
|
|
2093
|
+
if (existsSync4(answerFile)) {
|
|
1871
2094
|
try {
|
|
1872
2095
|
const raw = safeReadSync(answerFile);
|
|
1873
2096
|
const parsed = JSON.parse(raw);
|
|
1874
2097
|
if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
|
|
1875
|
-
missingPhases.push(
|
|
2098
|
+
missingPhases.push(phase2);
|
|
1876
2099
|
continue;
|
|
1877
2100
|
}
|
|
1878
|
-
answeredPhases.push(
|
|
2101
|
+
answeredPhases.push(phase2);
|
|
1879
2102
|
} catch {
|
|
1880
|
-
missingPhases.push(
|
|
2103
|
+
missingPhases.push(phase2);
|
|
1881
2104
|
}
|
|
1882
2105
|
} else {
|
|
1883
|
-
missingPhases.push(
|
|
2106
|
+
missingPhases.push(phase2);
|
|
1884
2107
|
}
|
|
1885
2108
|
}
|
|
1886
2109
|
return {
|
|
@@ -1905,7 +2128,7 @@ function handleListArtifacts(_cwd) {
|
|
|
1905
2128
|
for (const phase of PHASE_ORDER) {
|
|
1906
2129
|
const expectedFile = PHASE_ARTIFACT[phase];
|
|
1907
2130
|
const fullPath = join3(artifactsDir, expectedFile);
|
|
1908
|
-
const exists =
|
|
2131
|
+
const exists = existsSync4(fullPath);
|
|
1909
2132
|
if (exists) completedCount++;
|
|
1910
2133
|
artifacts.push({ phase, expectedFile, exists, path: fullPath });
|
|
1911
2134
|
}
|
|
@@ -1944,7 +2167,7 @@ function handleWritePhaseQuestions(input) {
|
|
|
1944
2167
|
} catch {
|
|
1945
2168
|
}
|
|
1946
2169
|
const questionsDir = join3(cwd, ".stackwright", "questions");
|
|
1947
|
-
|
|
2170
|
+
mkdirSync3(questionsDir, { recursive: true });
|
|
1948
2171
|
const filePath = join3(questionsDir, `${phase}.json`);
|
|
1949
2172
|
const payload = {
|
|
1950
2173
|
version: "1.0",
|
|
@@ -1987,16 +2210,27 @@ function handleBuildSpecialistPrompt(input) {
|
|
|
1987
2210
|
try {
|
|
1988
2211
|
const answersPath = join3(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
1989
2212
|
let answers = {};
|
|
1990
|
-
if (
|
|
2213
|
+
if (existsSync4(answersPath)) {
|
|
1991
2214
|
answers = JSON.parse(safeReadSync(answersPath));
|
|
1992
2215
|
}
|
|
2216
|
+
let buildContextText = "";
|
|
2217
|
+
const buildContextPath = join3(cwd, ".stackwright", "build-context.json");
|
|
2218
|
+
if (existsSync4(buildContextPath)) {
|
|
2219
|
+
try {
|
|
2220
|
+
const bcRaw = JSON.parse(safeReadSync(buildContextPath));
|
|
2221
|
+
if (typeof bcRaw.buildContext === "string" && bcRaw.buildContext.trim().length > 0) {
|
|
2222
|
+
buildContextText = bcRaw.buildContext.trim();
|
|
2223
|
+
}
|
|
2224
|
+
} catch {
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
1993
2227
|
const deps = PHASE_DEPENDENCIES[phase];
|
|
1994
2228
|
const artifactSections = [];
|
|
1995
2229
|
const missingDependencies = [];
|
|
1996
2230
|
for (const dep of deps) {
|
|
1997
2231
|
const artifactFile = PHASE_ARTIFACT[dep];
|
|
1998
2232
|
const artifactPath = join3(cwd, ".stackwright", "artifacts", artifactFile);
|
|
1999
|
-
if (
|
|
2233
|
+
if (existsSync4(artifactPath)) {
|
|
2000
2234
|
const content = JSON.parse(safeReadSync(artifactPath));
|
|
2001
2235
|
const expectedOtter = PHASE_TO_OTTER2[dep];
|
|
2002
2236
|
const artifactOtter = content["generatedBy"];
|
|
@@ -2022,7 +2256,11 @@ ${JSON.stringify(content, null, 2)}`);
|
|
|
2022
2256
|
(not yet available)`);
|
|
2023
2257
|
}
|
|
2024
2258
|
}
|
|
2025
|
-
const parts = [
|
|
2259
|
+
const parts = [];
|
|
2260
|
+
if (buildContextText) {
|
|
2261
|
+
parts.push("BUILD_CONTEXT:", buildContextText, "");
|
|
2262
|
+
}
|
|
2263
|
+
parts.push("ANSWERS:", JSON.stringify(answers, null, 2));
|
|
2026
2264
|
if (artifactSections.length > 0) {
|
|
2027
2265
|
parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
|
|
2028
2266
|
}
|
|
@@ -2121,7 +2359,7 @@ function handleValidateArtifact(input) {
|
|
|
2121
2359
|
}
|
|
2122
2360
|
try {
|
|
2123
2361
|
const artifactsDir = join3(cwd, ".stackwright", "artifacts");
|
|
2124
|
-
|
|
2362
|
+
mkdirSync3(artifactsDir, { recursive: true });
|
|
2125
2363
|
const artifactFile = PHASE_ARTIFACT[phase];
|
|
2126
2364
|
const artifactPath = join3(artifactsDir, artifactFile);
|
|
2127
2365
|
safeWriteSync(artifactPath, JSON.stringify(artifact, null, 2) + "\n");
|
|
@@ -2181,9 +2419,11 @@ function registerPipelineTools(server2) {
|
|
|
2181
2419
|
);
|
|
2182
2420
|
server2.tool(
|
|
2183
2421
|
"stackwright_pro_check_execution_ready",
|
|
2184
|
-
`Check all phases have answer files in .stackwright/answers/. ${DESC}`,
|
|
2185
|
-
{
|
|
2186
|
-
|
|
2422
|
+
`Check all phases have answer files in .stackwright/answers/. If phase is provided, check only that phase. ${DESC}`,
|
|
2423
|
+
{
|
|
2424
|
+
phase: z10.string().optional().describe("If provided, check only this phase's readiness. Omit to check all phases.")
|
|
2425
|
+
},
|
|
2426
|
+
async ({ phase }) => res(handleCheckExecutionReady(void 0, phase))
|
|
2187
2427
|
);
|
|
2188
2428
|
server2.tool(
|
|
2189
2429
|
"stackwright_pro_list_artifacts",
|
|
@@ -2193,12 +2433,49 @@ function registerPipelineTools(server2) {
|
|
|
2193
2433
|
);
|
|
2194
2434
|
server2.tool(
|
|
2195
2435
|
"stackwright_pro_write_phase_questions",
|
|
2196
|
-
`Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. ${DESC}`,
|
|
2436
|
+
`Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. Specialists may also call this directly with a parsed questions array. ${DESC}`,
|
|
2197
2437
|
{
|
|
2198
|
-
phase: z10.string().describe('Phase name, e.g. "designer"'),
|
|
2199
|
-
responseText: z10.string().describe("Raw LLM response from QUESTION_COLLECTION_MODE")
|
|
2438
|
+
phase: z10.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
|
|
2439
|
+
responseText: z10.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
|
|
2440
|
+
questions: jsonCoerce(z10.array(z10.any()).optional()).describe(
|
|
2441
|
+
"Questions array for direct specialist write"
|
|
2442
|
+
)
|
|
2200
2443
|
},
|
|
2201
|
-
async ({ phase, responseText }) =>
|
|
2444
|
+
async ({ phase, responseText, questions }) => {
|
|
2445
|
+
if (phase && questions && Array.isArray(questions)) {
|
|
2446
|
+
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
2447
|
+
if (!SAFE_PHASE.test(phase)) {
|
|
2448
|
+
return {
|
|
2449
|
+
content: [
|
|
2450
|
+
{ type: "text", text: JSON.stringify({ error: `Invalid phase name` }) }
|
|
2451
|
+
],
|
|
2452
|
+
isError: true
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
const questionsDir = join3(process.cwd(), ".stackwright", "questions");
|
|
2456
|
+
mkdirSync3(questionsDir, { recursive: true });
|
|
2457
|
+
const outPath = join3(questionsDir, `${phase}.json`);
|
|
2458
|
+
writeFileSync3(
|
|
2459
|
+
outPath,
|
|
2460
|
+
JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
|
|
2461
|
+
);
|
|
2462
|
+
return {
|
|
2463
|
+
content: [
|
|
2464
|
+
{
|
|
2465
|
+
type: "text",
|
|
2466
|
+
text: JSON.stringify({
|
|
2467
|
+
written: true,
|
|
2468
|
+
phase,
|
|
2469
|
+
count: questions.length
|
|
2470
|
+
})
|
|
2471
|
+
}
|
|
2472
|
+
]
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
return res(
|
|
2476
|
+
handleWritePhaseQuestions({ phase: phase ?? "", responseText: responseText ?? "" })
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2202
2479
|
);
|
|
2203
2480
|
server2.tool(
|
|
2204
2481
|
"stackwright_pro_build_specialist_prompt",
|
|
@@ -2219,7 +2496,7 @@ function registerPipelineTools(server2) {
|
|
|
2219
2496
|
|
|
2220
2497
|
// src/tools/safe-write.ts
|
|
2221
2498
|
import { z as z11 } from "zod";
|
|
2222
|
-
import { writeFileSync as writeFileSync4, existsSync as
|
|
2499
|
+
import { writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, lstatSync as lstatSync5 } from "fs";
|
|
2223
2500
|
import { normalize, isAbsolute, dirname, join as join4 } from "path";
|
|
2224
2501
|
var OTTER_WRITE_ALLOWLISTS = {
|
|
2225
2502
|
"stackwright-pro-designer-otter": [
|
|
@@ -2393,9 +2670,9 @@ function handleSafeWrite(input) {
|
|
|
2393
2670
|
}
|
|
2394
2671
|
const normalized = normalize(filePath);
|
|
2395
2672
|
const fullPath = join4(cwd, normalized);
|
|
2396
|
-
if (
|
|
2673
|
+
if (existsSync5(fullPath)) {
|
|
2397
2674
|
try {
|
|
2398
|
-
const stat =
|
|
2675
|
+
const stat = lstatSync5(fullPath);
|
|
2399
2676
|
if (stat.isSymbolicLink()) {
|
|
2400
2677
|
const result = {
|
|
2401
2678
|
success: false,
|
|
@@ -2422,7 +2699,7 @@ function handleSafeWrite(input) {
|
|
|
2422
2699
|
}
|
|
2423
2700
|
try {
|
|
2424
2701
|
if (createDirectories) {
|
|
2425
|
-
|
|
2702
|
+
mkdirSync4(dirname(fullPath), { recursive: true });
|
|
2426
2703
|
}
|
|
2427
2704
|
writeFileSync4(fullPath, content, { encoding: "utf-8" });
|
|
2428
2705
|
const result = {
|
|
@@ -2469,7 +2746,7 @@ function registerSafeWriteTools(server2) {
|
|
|
2469
2746
|
|
|
2470
2747
|
// src/tools/auth.ts
|
|
2471
2748
|
import { z as z12 } from "zod";
|
|
2472
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as
|
|
2749
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
|
|
2473
2750
|
import { join as join5 } from "path";
|
|
2474
2751
|
function buildHierarchy(roles) {
|
|
2475
2752
|
const h = {};
|
|
@@ -2749,7 +3026,7 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2749
3026
|
try {
|
|
2750
3027
|
const envBlock = generateEnvBlock(method, params);
|
|
2751
3028
|
const envPath = join5(cwd, ".env.example");
|
|
2752
|
-
if (
|
|
3029
|
+
if (existsSync6(envPath)) {
|
|
2753
3030
|
const existing = readFileSync4(envPath, "utf8");
|
|
2754
3031
|
writeFileSync5(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
|
|
2755
3032
|
} else {
|
|
@@ -2780,7 +3057,7 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2780
3057
|
protectedRoutes
|
|
2781
3058
|
);
|
|
2782
3059
|
const ymlPath = join5(cwd, "stackwright.yml");
|
|
2783
|
-
if (!
|
|
3060
|
+
if (!existsSync6(ymlPath)) {
|
|
2784
3061
|
writeFileSync5(ymlPath, authYaml, "utf8");
|
|
2785
3062
|
} else {
|
|
2786
3063
|
const existing = readFileSync4(ymlPath, "utf8");
|
|
@@ -2862,44 +3139,44 @@ function registerAuthTools(server2) {
|
|
|
2862
3139
|
|
|
2863
3140
|
// src/integrity.ts
|
|
2864
3141
|
import { createHash as createHash2, timingSafeEqual } from "crypto";
|
|
2865
|
-
import { readFileSync as readFileSync5, readdirSync, lstatSync as
|
|
3142
|
+
import { readFileSync as readFileSync5, readdirSync, lstatSync as lstatSync6 } from "fs";
|
|
2866
3143
|
import { join as join6, basename } from "path";
|
|
2867
3144
|
var _checksums = /* @__PURE__ */ new Map([
|
|
2868
3145
|
[
|
|
2869
3146
|
"stackwright-pro-api-otter.json",
|
|
2870
|
-
"
|
|
3147
|
+
"0ac26d85a5ad35b072a58965e1d5e090dd5c5f16dc14e68c452c3e99fcbb5510"
|
|
2871
3148
|
],
|
|
2872
3149
|
[
|
|
2873
3150
|
"stackwright-pro-auth-otter.json",
|
|
2874
|
-
"
|
|
3151
|
+
"d789b71f196659d5745ebfca87a7bda60a1bb63cfeccd17b4a273ac1e29bb08d"
|
|
2875
3152
|
],
|
|
2876
3153
|
[
|
|
2877
3154
|
"stackwright-pro-dashboard-otter.json",
|
|
2878
|
-
"
|
|
3155
|
+
"600e8597429c353e5b886f316731be84a86cd8b93617bf046e3cbf390b31a431"
|
|
2879
3156
|
],
|
|
2880
3157
|
[
|
|
2881
3158
|
"stackwright-pro-data-otter.json",
|
|
2882
|
-
"
|
|
3159
|
+
"b2946e3da3b53282c122d150e6db86b0cb89d2edba2a94a7666b26d27051be96"
|
|
2883
3160
|
],
|
|
2884
3161
|
[
|
|
2885
3162
|
"stackwright-pro-designer-otter.json",
|
|
2886
|
-
"
|
|
3163
|
+
"f4dbff5149051c77be1645de5ee12c0bd7d590c687a0b2d86737b915a5a6d5f0"
|
|
2887
3164
|
],
|
|
2888
3165
|
[
|
|
2889
3166
|
"stackwright-pro-foreman-otter.json",
|
|
2890
|
-
"
|
|
3167
|
+
"dc86d6f234d073ead2ccb179c45f911add901ebd3ebf541824addc124df639ee"
|
|
2891
3168
|
],
|
|
2892
3169
|
[
|
|
2893
3170
|
"stackwright-pro-page-otter.json",
|
|
2894
|
-
"
|
|
3171
|
+
"d75a71afa489478a6874abfbaa05baa46dcb2c16e4a5108f50f8187c9f67da60"
|
|
2895
3172
|
],
|
|
2896
3173
|
[
|
|
2897
3174
|
"stackwright-pro-theme-otter.json",
|
|
2898
|
-
"
|
|
3175
|
+
"a303ec6c045420f2c916583e3f6efcda469e9610fedfc84a508ed8a8a75866bc"
|
|
2899
3176
|
],
|
|
2900
3177
|
[
|
|
2901
3178
|
"stackwright-pro-workflow-otter.json",
|
|
2902
|
-
"
|
|
3179
|
+
"16da6c109d0b5ee60d0a14e009dbeab02a7bbac3b0947795769da053565b9821"
|
|
2903
3180
|
]
|
|
2904
3181
|
]);
|
|
2905
3182
|
Object.freeze(_checksums);
|
|
@@ -2928,7 +3205,7 @@ function verifyOtterFile(filePath) {
|
|
|
2928
3205
|
}
|
|
2929
3206
|
let stat;
|
|
2930
3207
|
try {
|
|
2931
|
-
stat =
|
|
3208
|
+
stat = lstatSync6(filePath);
|
|
2932
3209
|
} catch (err) {
|
|
2933
3210
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2934
3211
|
return { verified: false, filename, error: `Cannot stat file: ${msg}` };
|
|
@@ -2997,7 +3274,7 @@ function verifyAllOtters(otterDir) {
|
|
|
2997
3274
|
for (const filename of otterFiles) {
|
|
2998
3275
|
const filePath = join6(otterDir, filename);
|
|
2999
3276
|
try {
|
|
3000
|
-
if (
|
|
3277
|
+
if (lstatSync6(filePath).isSymbolicLink()) {
|
|
3001
3278
|
failed.push({ filename, error: "Skipped: symlink" });
|
|
3002
3279
|
continue;
|
|
3003
3280
|
}
|
|
@@ -3025,7 +3302,7 @@ function resolveOtterDir() {
|
|
|
3025
3302
|
for (const relative of DEFAULT_SEARCH_PATHS) {
|
|
3026
3303
|
const candidate = join6(cwd, relative);
|
|
3027
3304
|
try {
|
|
3028
|
-
|
|
3305
|
+
lstatSync6(candidate);
|
|
3029
3306
|
return candidate;
|
|
3030
3307
|
} catch {
|
|
3031
3308
|
}
|
|
@@ -3080,7 +3357,7 @@ function registerIntegrityTools(server2) {
|
|
|
3080
3357
|
|
|
3081
3358
|
// src/tools/domain.ts
|
|
3082
3359
|
import { z as z13 } from "zod";
|
|
3083
|
-
import { readFileSync as readFileSync6, existsSync as
|
|
3360
|
+
import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
|
|
3084
3361
|
import { join as join7 } from "path";
|
|
3085
3362
|
function handleListCollections(input) {
|
|
3086
3363
|
const cwd = input._cwd ?? process.cwd();
|
|
@@ -3103,7 +3380,7 @@ function handleListCollections(input) {
|
|
|
3103
3380
|
}
|
|
3104
3381
|
];
|
|
3105
3382
|
for (const { path: path3, source, parse } of sources) {
|
|
3106
|
-
if (!
|
|
3383
|
+
if (!existsSync7(path3)) continue;
|
|
3107
3384
|
try {
|
|
3108
3385
|
const collections = parse(readFileSync6(path3, "utf8"));
|
|
3109
3386
|
return {
|
|
@@ -3257,7 +3534,7 @@ function handleValidateWorkflow(input) {
|
|
|
3257
3534
|
raw = input.workflow;
|
|
3258
3535
|
} else {
|
|
3259
3536
|
const artifactPath = join7(cwd, ".stackwright", "artifacts", "workflow-config.json");
|
|
3260
|
-
if (!
|
|
3537
|
+
if (!existsSync7(artifactPath)) {
|
|
3261
3538
|
return fail([
|
|
3262
3539
|
{
|
|
3263
3540
|
code: "NO_WORKFLOW",
|
|
@@ -3505,7 +3782,7 @@ var package_default = {
|
|
|
3505
3782
|
"test:coverage": "vitest run --coverage"
|
|
3506
3783
|
},
|
|
3507
3784
|
name: "@stackwright-pro/mcp",
|
|
3508
|
-
version: "0.2.0-alpha.
|
|
3785
|
+
version: "0.2.0-alpha.18",
|
|
3509
3786
|
description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
|
|
3510
3787
|
license: "PROPRIETARY",
|
|
3511
3788
|
main: "./dist/server.js",
|