@stackwright-pro/mcp 0.2.0-alpha.21 → 0.2.0-alpha.22
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 +313 -33
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +349 -69
- 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,26 +2210,38 @@ 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"];
|
|
2237
|
+
const normalizedOtter = artifactOtter?.replace(/-[a-f0-9]{6}$/, "");
|
|
2003
2238
|
if (!artifactOtter) {
|
|
2004
2239
|
missingDependencies.push(dep);
|
|
2005
2240
|
artifactSections.push(
|
|
2006
2241
|
`[${artifactFile}]:
|
|
2007
2242
|
(integrity check failed: missing generatedBy field)`
|
|
2008
2243
|
);
|
|
2009
|
-
} else if (
|
|
2244
|
+
} else if (normalizedOtter !== expectedOtter) {
|
|
2010
2245
|
missingDependencies.push(dep);
|
|
2011
2246
|
artifactSections.push(
|
|
2012
2247
|
`[${artifactFile}]:
|
|
@@ -2022,7 +2257,11 @@ ${JSON.stringify(content, null, 2)}`);
|
|
|
2022
2257
|
(not yet available)`);
|
|
2023
2258
|
}
|
|
2024
2259
|
}
|
|
2025
|
-
const parts = [
|
|
2260
|
+
const parts = [];
|
|
2261
|
+
if (buildContextText) {
|
|
2262
|
+
parts.push("BUILD_CONTEXT:", buildContextText, "");
|
|
2263
|
+
}
|
|
2264
|
+
parts.push("ANSWERS:", JSON.stringify(answers, null, 2));
|
|
2026
2265
|
if (artifactSections.length > 0) {
|
|
2027
2266
|
parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
|
|
2028
2267
|
}
|
|
@@ -2121,7 +2360,7 @@ function handleValidateArtifact(input) {
|
|
|
2121
2360
|
}
|
|
2122
2361
|
try {
|
|
2123
2362
|
const artifactsDir = join3(cwd, ".stackwright", "artifacts");
|
|
2124
|
-
|
|
2363
|
+
mkdirSync3(artifactsDir, { recursive: true });
|
|
2125
2364
|
const artifactFile = PHASE_ARTIFACT[phase];
|
|
2126
2365
|
const artifactPath = join3(artifactsDir, artifactFile);
|
|
2127
2366
|
safeWriteSync(artifactPath, JSON.stringify(artifact, null, 2) + "\n");
|
|
@@ -2181,9 +2420,11 @@ function registerPipelineTools(server2) {
|
|
|
2181
2420
|
);
|
|
2182
2421
|
server2.tool(
|
|
2183
2422
|
"stackwright_pro_check_execution_ready",
|
|
2184
|
-
`Check all phases have answer files in .stackwright/answers/. ${DESC}`,
|
|
2185
|
-
{
|
|
2186
|
-
|
|
2423
|
+
`Check all phases have answer files in .stackwright/answers/. If phase is provided, check only that phase. ${DESC}`,
|
|
2424
|
+
{
|
|
2425
|
+
phase: z10.string().optional().describe("If provided, check only this phase's readiness. Omit to check all phases.")
|
|
2426
|
+
},
|
|
2427
|
+
async ({ phase }) => res(handleCheckExecutionReady(void 0, phase))
|
|
2187
2428
|
);
|
|
2188
2429
|
server2.tool(
|
|
2189
2430
|
"stackwright_pro_list_artifacts",
|
|
@@ -2193,12 +2434,49 @@ function registerPipelineTools(server2) {
|
|
|
2193
2434
|
);
|
|
2194
2435
|
server2.tool(
|
|
2195
2436
|
"stackwright_pro_write_phase_questions",
|
|
2196
|
-
`Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. ${DESC}`,
|
|
2437
|
+
`Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. Specialists may also call this directly with a parsed questions array. ${DESC}`,
|
|
2197
2438
|
{
|
|
2198
|
-
phase: z10.string().describe('Phase name, e.g. "designer"'),
|
|
2199
|
-
responseText: z10.string().describe("Raw LLM response from QUESTION_COLLECTION_MODE")
|
|
2439
|
+
phase: z10.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
|
|
2440
|
+
responseText: z10.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
|
|
2441
|
+
questions: jsonCoerce(z10.array(z10.any()).optional()).describe(
|
|
2442
|
+
"Questions array for direct specialist write"
|
|
2443
|
+
)
|
|
2200
2444
|
},
|
|
2201
|
-
async ({ phase, responseText }) =>
|
|
2445
|
+
async ({ phase, responseText, questions }) => {
|
|
2446
|
+
if (phase && questions && Array.isArray(questions)) {
|
|
2447
|
+
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
2448
|
+
if (!SAFE_PHASE.test(phase)) {
|
|
2449
|
+
return {
|
|
2450
|
+
content: [
|
|
2451
|
+
{ type: "text", text: JSON.stringify({ error: `Invalid phase name` }) }
|
|
2452
|
+
],
|
|
2453
|
+
isError: true
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
const questionsDir = join3(process.cwd(), ".stackwright", "questions");
|
|
2457
|
+
mkdirSync3(questionsDir, { recursive: true });
|
|
2458
|
+
const outPath = join3(questionsDir, `${phase}.json`);
|
|
2459
|
+
writeFileSync3(
|
|
2460
|
+
outPath,
|
|
2461
|
+
JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
|
|
2462
|
+
);
|
|
2463
|
+
return {
|
|
2464
|
+
content: [
|
|
2465
|
+
{
|
|
2466
|
+
type: "text",
|
|
2467
|
+
text: JSON.stringify({
|
|
2468
|
+
written: true,
|
|
2469
|
+
phase,
|
|
2470
|
+
count: questions.length
|
|
2471
|
+
})
|
|
2472
|
+
}
|
|
2473
|
+
]
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
return res(
|
|
2477
|
+
handleWritePhaseQuestions({ phase: phase ?? "", responseText: responseText ?? "" })
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2202
2480
|
);
|
|
2203
2481
|
server2.tool(
|
|
2204
2482
|
"stackwright_pro_build_specialist_prompt",
|
|
@@ -2219,7 +2497,7 @@ function registerPipelineTools(server2) {
|
|
|
2219
2497
|
|
|
2220
2498
|
// src/tools/safe-write.ts
|
|
2221
2499
|
import { z as z11 } from "zod";
|
|
2222
|
-
import { writeFileSync as writeFileSync4, existsSync as
|
|
2500
|
+
import { writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, lstatSync as lstatSync5 } from "fs";
|
|
2223
2501
|
import { normalize, isAbsolute, dirname, join as join4 } from "path";
|
|
2224
2502
|
var OTTER_WRITE_ALLOWLISTS = {
|
|
2225
2503
|
"stackwright-pro-designer-otter": [
|
|
@@ -2393,9 +2671,9 @@ function handleSafeWrite(input) {
|
|
|
2393
2671
|
}
|
|
2394
2672
|
const normalized = normalize(filePath);
|
|
2395
2673
|
const fullPath = join4(cwd, normalized);
|
|
2396
|
-
if (
|
|
2674
|
+
if (existsSync5(fullPath)) {
|
|
2397
2675
|
try {
|
|
2398
|
-
const stat =
|
|
2676
|
+
const stat = lstatSync5(fullPath);
|
|
2399
2677
|
if (stat.isSymbolicLink()) {
|
|
2400
2678
|
const result = {
|
|
2401
2679
|
success: false,
|
|
@@ -2422,7 +2700,7 @@ function handleSafeWrite(input) {
|
|
|
2422
2700
|
}
|
|
2423
2701
|
try {
|
|
2424
2702
|
if (createDirectories) {
|
|
2425
|
-
|
|
2703
|
+
mkdirSync4(dirname(fullPath), { recursive: true });
|
|
2426
2704
|
}
|
|
2427
2705
|
writeFileSync4(fullPath, content, { encoding: "utf-8" });
|
|
2428
2706
|
const result = {
|
|
@@ -2453,7 +2731,9 @@ function registerSafeWriteTools(server2) {
|
|
|
2453
2731
|
callerOtter: z11.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
|
|
2454
2732
|
filePath: z11.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
|
|
2455
2733
|
content: z11.string().describe("File content to write"),
|
|
2456
|
-
createDirectories: z11.boolean().optional().
|
|
2734
|
+
createDirectories: boolCoerce(z11.boolean().optional().default(true)).describe(
|
|
2735
|
+
"Create parent directories if they don't exist. Default: true"
|
|
2736
|
+
)
|
|
2457
2737
|
},
|
|
2458
2738
|
async ({ callerOtter, filePath, content, createDirectories }) => {
|
|
2459
2739
|
const result = handleSafeWrite({
|
|
@@ -2469,7 +2749,7 @@ function registerSafeWriteTools(server2) {
|
|
|
2469
2749
|
|
|
2470
2750
|
// src/tools/auth.ts
|
|
2471
2751
|
import { z as z12 } from "zod";
|
|
2472
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as
|
|
2752
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
|
|
2473
2753
|
import { join as join5 } from "path";
|
|
2474
2754
|
function buildHierarchy(roles) {
|
|
2475
2755
|
const h = {};
|
|
@@ -2749,7 +3029,7 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2749
3029
|
try {
|
|
2750
3030
|
const envBlock = generateEnvBlock(method, params);
|
|
2751
3031
|
const envPath = join5(cwd, ".env.example");
|
|
2752
|
-
if (
|
|
3032
|
+
if (existsSync6(envPath)) {
|
|
2753
3033
|
const existing = readFileSync4(envPath, "utf8");
|
|
2754
3034
|
writeFileSync5(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
|
|
2755
3035
|
} else {
|
|
@@ -2780,7 +3060,7 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2780
3060
|
protectedRoutes
|
|
2781
3061
|
);
|
|
2782
3062
|
const ymlPath = join5(cwd, "stackwright.yml");
|
|
2783
|
-
if (!
|
|
3063
|
+
if (!existsSync6(ymlPath)) {
|
|
2784
3064
|
writeFileSync5(ymlPath, authYaml, "utf8");
|
|
2785
3065
|
} else {
|
|
2786
3066
|
const existing = readFileSync4(ymlPath, "utf8");
|
|
@@ -2862,44 +3142,44 @@ function registerAuthTools(server2) {
|
|
|
2862
3142
|
|
|
2863
3143
|
// src/integrity.ts
|
|
2864
3144
|
import { createHash as createHash2, timingSafeEqual } from "crypto";
|
|
2865
|
-
import { readFileSync as readFileSync5, readdirSync, lstatSync as
|
|
3145
|
+
import { readFileSync as readFileSync5, readdirSync, lstatSync as lstatSync6 } from "fs";
|
|
2866
3146
|
import { join as join6, basename } from "path";
|
|
2867
3147
|
var _checksums = /* @__PURE__ */ new Map([
|
|
2868
3148
|
[
|
|
2869
3149
|
"stackwright-pro-api-otter.json",
|
|
2870
|
-
"
|
|
3150
|
+
"0ac26d85a5ad35b072a58965e1d5e090dd5c5f16dc14e68c452c3e99fcbb5510"
|
|
2871
3151
|
],
|
|
2872
3152
|
[
|
|
2873
3153
|
"stackwright-pro-auth-otter.json",
|
|
2874
|
-
"
|
|
3154
|
+
"d789b71f196659d5745ebfca87a7bda60a1bb63cfeccd17b4a273ac1e29bb08d"
|
|
2875
3155
|
],
|
|
2876
3156
|
[
|
|
2877
3157
|
"stackwright-pro-dashboard-otter.json",
|
|
2878
|
-
"
|
|
3158
|
+
"600e8597429c353e5b886f316731be84a86cd8b93617bf046e3cbf390b31a431"
|
|
2879
3159
|
],
|
|
2880
3160
|
[
|
|
2881
3161
|
"stackwright-pro-data-otter.json",
|
|
2882
|
-
"
|
|
3162
|
+
"b2946e3da3b53282c122d150e6db86b0cb89d2edba2a94a7666b26d27051be96"
|
|
2883
3163
|
],
|
|
2884
3164
|
[
|
|
2885
3165
|
"stackwright-pro-designer-otter.json",
|
|
2886
|
-
"
|
|
3166
|
+
"f4dbff5149051c77be1645de5ee12c0bd7d590c687a0b2d86737b915a5a6d5f0"
|
|
2887
3167
|
],
|
|
2888
3168
|
[
|
|
2889
3169
|
"stackwright-pro-foreman-otter.json",
|
|
2890
|
-
"
|
|
3170
|
+
"7464523d7288374dc6efa5c213c825ec0616e00cf4f739d8039eb41df812cbc5"
|
|
2891
3171
|
],
|
|
2892
3172
|
[
|
|
2893
3173
|
"stackwright-pro-page-otter.json",
|
|
2894
|
-
"
|
|
3174
|
+
"12aca7b666b3c85c1d96c700a2da7f209604cb75d0f064995e052711ddafd657"
|
|
2895
3175
|
],
|
|
2896
3176
|
[
|
|
2897
3177
|
"stackwright-pro-theme-otter.json",
|
|
2898
|
-
"
|
|
3178
|
+
"a303ec6c045420f2c916583e3f6efcda469e9610fedfc84a508ed8a8a75866bc"
|
|
2899
3179
|
],
|
|
2900
3180
|
[
|
|
2901
3181
|
"stackwright-pro-workflow-otter.json",
|
|
2902
|
-
"
|
|
3182
|
+
"16da6c109d0b5ee60d0a14e009dbeab02a7bbac3b0947795769da053565b9821"
|
|
2903
3183
|
]
|
|
2904
3184
|
]);
|
|
2905
3185
|
Object.freeze(_checksums);
|
|
@@ -2928,7 +3208,7 @@ function verifyOtterFile(filePath) {
|
|
|
2928
3208
|
}
|
|
2929
3209
|
let stat;
|
|
2930
3210
|
try {
|
|
2931
|
-
stat =
|
|
3211
|
+
stat = lstatSync6(filePath);
|
|
2932
3212
|
} catch (err) {
|
|
2933
3213
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2934
3214
|
return { verified: false, filename, error: `Cannot stat file: ${msg}` };
|
|
@@ -2997,7 +3277,7 @@ function verifyAllOtters(otterDir) {
|
|
|
2997
3277
|
for (const filename of otterFiles) {
|
|
2998
3278
|
const filePath = join6(otterDir, filename);
|
|
2999
3279
|
try {
|
|
3000
|
-
if (
|
|
3280
|
+
if (lstatSync6(filePath).isSymbolicLink()) {
|
|
3001
3281
|
failed.push({ filename, error: "Skipped: symlink" });
|
|
3002
3282
|
continue;
|
|
3003
3283
|
}
|
|
@@ -3025,7 +3305,7 @@ function resolveOtterDir() {
|
|
|
3025
3305
|
for (const relative of DEFAULT_SEARCH_PATHS) {
|
|
3026
3306
|
const candidate = join6(cwd, relative);
|
|
3027
3307
|
try {
|
|
3028
|
-
|
|
3308
|
+
lstatSync6(candidate);
|
|
3029
3309
|
return candidate;
|
|
3030
3310
|
} catch {
|
|
3031
3311
|
}
|
|
@@ -3080,7 +3360,7 @@ function registerIntegrityTools(server2) {
|
|
|
3080
3360
|
|
|
3081
3361
|
// src/tools/domain.ts
|
|
3082
3362
|
import { z as z13 } from "zod";
|
|
3083
|
-
import { readFileSync as readFileSync6, existsSync as
|
|
3363
|
+
import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
|
|
3084
3364
|
import { join as join7 } from "path";
|
|
3085
3365
|
function handleListCollections(input) {
|
|
3086
3366
|
const cwd = input._cwd ?? process.cwd();
|
|
@@ -3103,7 +3383,7 @@ function handleListCollections(input) {
|
|
|
3103
3383
|
}
|
|
3104
3384
|
];
|
|
3105
3385
|
for (const { path: path3, source, parse } of sources) {
|
|
3106
|
-
if (!
|
|
3386
|
+
if (!existsSync7(path3)) continue;
|
|
3107
3387
|
try {
|
|
3108
3388
|
const collections = parse(readFileSync6(path3, "utf8"));
|
|
3109
3389
|
return {
|
|
@@ -3257,7 +3537,7 @@ function handleValidateWorkflow(input) {
|
|
|
3257
3537
|
raw = input.workflow;
|
|
3258
3538
|
} else {
|
|
3259
3539
|
const artifactPath = join7(cwd, ".stackwright", "artifacts", "workflow-config.json");
|
|
3260
|
-
if (!
|
|
3540
|
+
if (!existsSync7(artifactPath)) {
|
|
3261
3541
|
return fail([
|
|
3262
3542
|
{
|
|
3263
3543
|
code: "NO_WORKFLOW",
|
|
@@ -3505,7 +3785,7 @@ var package_default = {
|
|
|
3505
3785
|
"test:coverage": "vitest run --coverage"
|
|
3506
3786
|
},
|
|
3507
3787
|
name: "@stackwright-pro/mcp",
|
|
3508
|
-
version: "0.2.0-alpha.
|
|
3788
|
+
version: "0.2.0-alpha.22",
|
|
3509
3789
|
description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
|
|
3510
3790
|
license: "PROPRIETARY",
|
|
3511
3791
|
main: "./dist/server.js",
|