@qa-gentic/stlc-agents 1.0.15 → 1.0.17
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/README.md +59 -314
- package/bin/postinstall.js +17 -1
- package/bin/qa-stlc.js +23 -0
- package/package.json +1 -1
- package/skills/write-helix-files/SKILL.md +6 -0
- package/src/cli/cmd-cost.js +253 -0
- package/src/cli/cmd-mcp-config.js +124 -59
- package/src/stlc_agents/agent_gherkin_generator/server.py +88 -4
- package/src/stlc_agents/agent_helix_writer/tools/helix_write.py +60 -28
- package/src/stlc_agents/agent_jira_manager/server.py +209 -2
- package/src/stlc_agents/agent_jira_manager/tools/jira_workitem.py +36 -0
- package/src/stlc_agents/agent_playwright_generator/server.py +968 -105
- package/src/stlc_agents/agent_test_case_manager/server.py +121 -2
- package/src/stlc_agents/shared/cost_tracker.py +395 -0
- package/src/stlc_agents/shared/pricing.py +72 -0
- package/src/stlc_agents/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/boilerplate.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/tools/__pycache__/jira_workitem.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/auth.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared_jira/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/stlc_agents/shared_jira/__pycache__/auth.cpython-310.pyc +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cmd-cost.js — `qa-stlc cost`
|
|
3
|
+
*
|
|
4
|
+
* Read session cost logs from ~/.qa-stlc/cost-*.jsonl and print a report.
|
|
5
|
+
* Also handles --set-model to persist the coding agent model preference.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* qa-stlc cost # last session
|
|
9
|
+
* qa-stlc cost --all # all sessions
|
|
10
|
+
* qa-stlc cost --session <id> # specific session
|
|
11
|
+
* qa-stlc cost --json # raw JSON output
|
|
12
|
+
* qa-stlc cost --set-model <model-id> # save model for Cursor/Windsurf users
|
|
13
|
+
*
|
|
14
|
+
* Model detection order (mirrors cost_tracker.py):
|
|
15
|
+
* STLC_CODING_AGENT_MODEL env var
|
|
16
|
+
* → ANTHROPIC_MODEL env var (Claude Code sets this automatically)
|
|
17
|
+
* → GITHUB_COPILOT_MODEL env var
|
|
18
|
+
* → ~/.qa-stlc/agent-model (saved by --set-model)
|
|
19
|
+
* → "claude-sonnet-4-6" (default)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
"use strict";
|
|
23
|
+
|
|
24
|
+
const fs = require("fs");
|
|
25
|
+
const path = require("path");
|
|
26
|
+
const os = require("os");
|
|
27
|
+
|
|
28
|
+
const LOG_DIR = path.join(os.homedir(), ".qa-stlc");
|
|
29
|
+
const PREF_FILE = path.join(LOG_DIR, "agent-model");
|
|
30
|
+
|
|
31
|
+
const C = {
|
|
32
|
+
reset: "\x1b[0m", bold: "\x1b[1m",
|
|
33
|
+
dim: "\x1b[2m", cyan: "\x1b[36m",
|
|
34
|
+
green: "\x1b[32m", yellow: "\x1b[33m",
|
|
35
|
+
};
|
|
36
|
+
const b = (s) => `${C.bold}${s}${C.reset}`;
|
|
37
|
+
const dim = (s) => `${C.dim}${s}${C.reset}`;
|
|
38
|
+
const grn = (s) => `${C.green}${s}${C.reset}`;
|
|
39
|
+
const cyn = (s) => `${C.cyan}${s}${C.reset}`;
|
|
40
|
+
|
|
41
|
+
function fmtTok(n) { return n >= 1000 ? `${(n/1000).toFixed(1)}K` : String(n); }
|
|
42
|
+
function fmtUsd(n) { return `$${n.toFixed(6)}`; }
|
|
43
|
+
function fmtMs(n) { return `${n}ms`; }
|
|
44
|
+
|
|
45
|
+
// ── Model preference helpers ───────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const KNOWN_MODELS = {
|
|
48
|
+
// Anthropic
|
|
49
|
+
"claude-sonnet-4-6": "$3/$15 per MTok",
|
|
50
|
+
"claude-opus-4-6": "$5/$25 per MTok",
|
|
51
|
+
"claude-opus-4-7": "$5/$25 per MTok",
|
|
52
|
+
"claude-haiku-4-5-20251001": "$1/$5 per MTok",
|
|
53
|
+
"claude-sonnet-4-20250514": "$3/$15 per MTok",
|
|
54
|
+
// OpenAI / Copilot
|
|
55
|
+
"gpt-4o": "$2.50/$10 per MTok",
|
|
56
|
+
"gpt-4o-mini": "$0.15/$0.60 per MTok",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function saveModelPref(modelId) {
|
|
60
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
61
|
+
fs.writeFileSync(PREF_FILE, modelId.trim(), "utf8");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readModelPref() {
|
|
65
|
+
try {
|
|
66
|
+
if (fs.existsSync(PREF_FILE)) return fs.readFileSync(PREF_FILE, "utf8").trim();
|
|
67
|
+
} catch (_) {}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function printModelHelp() {
|
|
72
|
+
console.log(`\n ${b("Coding agent model for cost tracking")}\n`);
|
|
73
|
+
console.log(` ${dim("Model is detected automatically in this order:")}`);
|
|
74
|
+
console.log(` 1. STLC_CODING_AGENT_MODEL env var ${dim("(explicit override, always wins)")}`);
|
|
75
|
+
console.log(` 2. ANTHROPIC_MODEL env var ${dim("(set automatically by Claude Code ✓ zero config)")}`);
|
|
76
|
+
console.log(` 3. GITHUB_COPILOT_MODEL env var ${dim("(set by Copilot if configured)")}`);
|
|
77
|
+
console.log(` 4. ~/.qa-stlc/agent-model file ${dim("(saved by qa-stlc cost --set-model)")}`);
|
|
78
|
+
console.log(` 5. claude-sonnet-4-6 ${dim("(default fallback)")}\n`);
|
|
79
|
+
|
|
80
|
+
const saved = readModelPref();
|
|
81
|
+
if (saved) {
|
|
82
|
+
console.log(` ${grn("✓")} Currently saved: ${b(saved)} ${dim("(~/.qa-stlc/agent-model)")}\n`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(` ${b("Known models:")}`);
|
|
86
|
+
for (const [id, rate] of Object.entries(KNOWN_MODELS)) {
|
|
87
|
+
console.log(` ${id.padEnd(36)} ${dim(rate)}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(`\n ${b("To set for Cursor / Windsurf / any agent:")}`);
|
|
91
|
+
console.log(` ${cyn("Option A — save once (recommended):")}`);
|
|
92
|
+
console.log(` qa-stlc cost --set-model claude-opus-4-6\n`);
|
|
93
|
+
console.log(` ${cyn("Option B — shell profile (applies to all projects):")}`);
|
|
94
|
+
console.log(` echo 'export STLC_CODING_AGENT_MODEL=claude-opus-4-6' >> ~/.zshrc\n`);
|
|
95
|
+
console.log(` ${cyn("Option C — .mcp.json env block (project-level):")}`);
|
|
96
|
+
console.log(` "qa-gherkin-generator": {`);
|
|
97
|
+
console.log(` "command": "/path/to/qa-gherkin-generator",`);
|
|
98
|
+
console.log(` "env": { "STLC_CODING_AGENT_MODEL": "claude-opus-4-6" }`);
|
|
99
|
+
console.log(` }\n`);
|
|
100
|
+
console.log(` ${cyn("Option D — .env file in project root:")}`);
|
|
101
|
+
console.log(` STLC_CODING_AGENT_MODEL=claude-opus-4-6\n`);
|
|
102
|
+
console.log(` ${dim("Claude Code users: no action needed.")}`);
|
|
103
|
+
console.log(` ${dim("ANTHROPIC_MODEL is passed through automatically via qa-stlc mcp-config.")}\n`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Log reading ────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
function readLogs() {
|
|
109
|
+
if (!fs.existsSync(LOG_DIR)) return [];
|
|
110
|
+
return fs.readdirSync(LOG_DIR)
|
|
111
|
+
.filter((f) => f.startsWith("cost-") && f.endsWith(".jsonl"))
|
|
112
|
+
.sort()
|
|
113
|
+
.map((f) => {
|
|
114
|
+
const file = path.join(LOG_DIR, f);
|
|
115
|
+
const records = fs.readFileSync(file, "utf8")
|
|
116
|
+
.split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
117
|
+
return {
|
|
118
|
+
file,
|
|
119
|
+
sessionId: f.replace(/^cost-/, "").replace(/\.jsonl$/, ""),
|
|
120
|
+
records,
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Printing ───────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
function printSession(sess) {
|
|
128
|
+
const { sessionId, records } = sess;
|
|
129
|
+
if (!records.length) return;
|
|
130
|
+
|
|
131
|
+
const byServer = {};
|
|
132
|
+
let totalCost = 0, totalTokens = 0;
|
|
133
|
+
for (const r of records) {
|
|
134
|
+
const k = r.server || "unknown";
|
|
135
|
+
if (!byServer[k]) byServer[k] = { calls: 0, tokens: 0, cost: 0 };
|
|
136
|
+
byServer[k].calls++;
|
|
137
|
+
byServer[k].tokens += r.estimated_tokens || 0;
|
|
138
|
+
byServer[k].cost += r.cost_usd || 0;
|
|
139
|
+
totalCost += r.cost_usd || 0;
|
|
140
|
+
totalTokens += r.estimated_tokens || 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const model = records[0]?.model || "unknown";
|
|
144
|
+
const source = records[0]?.model_source || "";
|
|
145
|
+
const method = records.some((r) => r.token_method === "estimated")
|
|
146
|
+
? "estimated (payload chars÷4)" : "exact";
|
|
147
|
+
const ts = records[0]?.timestamp?.slice(0, 19).replace("T", " ") || "";
|
|
148
|
+
const tsEnd = records[records.length - 1]?.timestamp?.slice(0, 19).replace("T", " ") || "";
|
|
149
|
+
|
|
150
|
+
console.log(`\n${"═".repeat(68)}`);
|
|
151
|
+
console.log(b(` stlc-agents · Cost Report · ${sessionId}`));
|
|
152
|
+
console.log(`${"═".repeat(68)}`);
|
|
153
|
+
console.log(dim(` ${ts} → ${tsEnd}`));
|
|
154
|
+
console.log(dim(` Model: ${model} (detected via: ${source})`));
|
|
155
|
+
console.log(dim(` Token method: ${method}`));
|
|
156
|
+
|
|
157
|
+
// Per-server
|
|
158
|
+
console.log(`\n ${"Server".padEnd(30)} ${"Calls".padStart(6)} ${"~Tokens".padStart(10)} ${"Cost (USD)".padStart(14)}`);
|
|
159
|
+
console.log(` ${"─".repeat(60)}`);
|
|
160
|
+
for (const [svr, d] of Object.entries(byServer)) {
|
|
161
|
+
console.log(
|
|
162
|
+
` ${cyn(svr.padEnd(30))} ${String(d.calls).padStart(6)} ` +
|
|
163
|
+
`${fmtTok(d.tokens).padStart(10)} ${grn(fmtUsd(d.cost).padStart(14))}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Per-step
|
|
168
|
+
console.log(`\n ${"Step detail"}`);
|
|
169
|
+
console.log(` ${"─".repeat(68)}`);
|
|
170
|
+
for (const r of records) {
|
|
171
|
+
console.log(
|
|
172
|
+
` ${(r.server || "?").padEnd(26)} ${(r.tool || "?").padEnd(36)} ` +
|
|
173
|
+
`${fmtTok(r.estimated_tokens || 0).padStart(6)} ` +
|
|
174
|
+
`${grn(fmtUsd(r.cost_usd || 0))} ${fmtMs(r.latency_ms || 0)} ` +
|
|
175
|
+
dim(`[${r.token_method || "?"}]`)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Totals
|
|
180
|
+
console.log(`\n ${"─".repeat(68)}`);
|
|
181
|
+
console.log(` ${"Total tokens".padEnd(40)} ${fmtTok(totalTokens).padStart(10)}`);
|
|
182
|
+
console.log(` ${b("Total cost".padEnd(40))} ${grn(fmtUsd(totalCost))}`);
|
|
183
|
+
console.log(dim(` Log: ${sess.file}`));
|
|
184
|
+
console.log(`${"═".repeat(68)}\n`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
module.exports = async function cost(opts) {
|
|
190
|
+
|
|
191
|
+
// --set-model: save model preference for Cursor/Windsurf users
|
|
192
|
+
if (opts.setModel) {
|
|
193
|
+
const modelId = opts.setModel.trim();
|
|
194
|
+
saveModelPref(modelId);
|
|
195
|
+
const rate = KNOWN_MODELS[modelId];
|
|
196
|
+
console.log(`\n ${grn("✓")} Model saved: ${b(modelId)}`);
|
|
197
|
+
if (rate) console.log(` Pricing: ${dim(rate)}`);
|
|
198
|
+
else console.log(` ${dim("(unknown model — pricing may show $0.000000, add to pricing.py)")}`);
|
|
199
|
+
console.log(` Written to: ${dim(PREF_FILE)}`);
|
|
200
|
+
console.log(`\n This preference is used by all stlc-agents servers in all projects.`);
|
|
201
|
+
console.log(` Override per-project via STLC_CODING_AGENT_MODEL in .env or .mcp.json.\n`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --model-help: explain how model detection works
|
|
206
|
+
if (opts.modelHelp) {
|
|
207
|
+
printModelHelp();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const sessions = readLogs();
|
|
212
|
+
|
|
213
|
+
if (!sessions.length) {
|
|
214
|
+
console.log(`\n No cost logs found in ${LOG_DIR}`);
|
|
215
|
+
console.log(` Run any qa-stlc MCP tool call to start tracking.\n`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (opts.json) {
|
|
220
|
+
const all = opts.all ? sessions : [sessions[sessions.length - 1]];
|
|
221
|
+
console.log(JSON.stringify(all, null, 2));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (opts.session) {
|
|
226
|
+
const found = sessions.find((s) => s.sessionId === opts.session);
|
|
227
|
+
if (!found) {
|
|
228
|
+
console.log(`\n Session not found: ${opts.session}`);
|
|
229
|
+
console.log(` Available: ${sessions.map((s) => s.sessionId).join(", ")}\n`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
printSession(found);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (opts.all) {
|
|
237
|
+
for (const s of sessions) printSession(s);
|
|
238
|
+
const allRecords = sessions.flatMap((s) => s.records);
|
|
239
|
+
const grandTotal = allRecords.reduce((a, r) => a + (r.cost_usd || 0), 0);
|
|
240
|
+
const grandTokens = allRecords.reduce((a, r) => a + (r.estimated_tokens || 0), 0);
|
|
241
|
+
console.log(`${"═".repeat(68)}`);
|
|
242
|
+
console.log(b(` All sessions — grand total`));
|
|
243
|
+
console.log(`${"═".repeat(68)}`);
|
|
244
|
+
console.log(` Sessions : ${sessions.length}`);
|
|
245
|
+
console.log(` Total tokens: ${fmtTok(grandTokens)}`);
|
|
246
|
+
console.log(b(` TOTAL COST : ${grn(fmtUsd(grandTotal))}`));
|
|
247
|
+
console.log(`${"═".repeat(68)}\n`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Default: last session
|
|
252
|
+
printSession(sessions[sessions.length - 1]);
|
|
253
|
+
};
|
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cmd-mcp-config.js — `qa-stlc mcp-config`
|
|
2
|
+
* cmd-mcp-config.js — `qa-stlc mcp-config` (updated with cost tracking env injection)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Change from original: both buildClaudeConfig() and buildVscodeConfig() now
|
|
5
|
+
* inject STLC cost tracking env vars into every MCP server entry.
|
|
6
|
+
*
|
|
7
|
+
* What gets injected and why:
|
|
8
|
+
*
|
|
9
|
+
* STLC_COST_TRACKING "true" — enables cost tracking (opt-out via "false")
|
|
10
|
+
*
|
|
11
|
+
* ANTHROPIC_MODEL "${env:ANTHROPIC_MODEL}" — Claude Code sets this
|
|
12
|
+
* automatically on the process that launches Claude.
|
|
13
|
+
* Passing it through to the MCP subprocess lets the
|
|
14
|
+
* cost tracker auto-detect the model without any
|
|
15
|
+
* manual configuration.
|
|
16
|
+
*
|
|
17
|
+
* GITHUB_COPILOT_MODEL "${env:GITHUB_COPILOT_MODEL}" — VS Code / Copilot
|
|
18
|
+
* may set this; passed through for the same reason.
|
|
19
|
+
*
|
|
20
|
+
* STLC_CODING_AGENT_MODEL "${env:STLC_CODING_AGENT_MODEL}" — explicit user
|
|
21
|
+
* override. Empty by default; set in shell profile
|
|
22
|
+
* or .env to hard-pin the model regardless of agent.
|
|
23
|
+
*
|
|
24
|
+
* The detection order inside cost_tracker.py is:
|
|
25
|
+
* STLC_CODING_AGENT_MODEL → ANTHROPIC_MODEL → CLAUDE_MODEL →
|
|
26
|
+
* GITHUB_COPILOT_MODEL → ~/.qa-stlc/agent-model → "claude-sonnet-4-6"
|
|
27
|
+
*
|
|
28
|
+
* RESULT: zero configuration for Claude Code users — the model is detected
|
|
29
|
+
* automatically. Copilot / Cursor / Windsurf users set STLC_CODING_AGENT_MODEL
|
|
30
|
+
* once in their shell profile or run `qa-stlc cost --set-model <model>`.
|
|
5
31
|
*/
|
|
32
|
+
|
|
6
33
|
"use strict";
|
|
7
34
|
|
|
8
|
-
const path
|
|
9
|
-
const fs
|
|
35
|
+
const path = require("path");
|
|
36
|
+
const fs = require("fs");
|
|
37
|
+
const os = require("os");
|
|
10
38
|
const { spawnSync } = require("child_process");
|
|
11
39
|
|
|
12
40
|
const C = {
|
|
@@ -15,45 +43,54 @@ const C = {
|
|
|
15
43
|
yellow: "\x1b[33m", red: "\x1b[31m", dim: "\x1b[2m",
|
|
16
44
|
};
|
|
17
45
|
const ok = (m) => console.log(`${C.green}✓${C.reset} ${m}`);
|
|
18
|
-
const info = (m) => console.log(`${C.cyan}→${C.reset} ${m}`);
|
|
19
46
|
const warn = (m) => console.log(`${C.yellow}⚠${C.reset} ${m}`);
|
|
20
47
|
|
|
21
|
-
const
|
|
22
|
-
const fs_ = fs; // alias — fs already required above
|
|
23
|
-
|
|
24
|
-
const PREF_FILE_MCP = require("path").join(os.homedir(), ".qa-stlc", "integration");
|
|
48
|
+
const PREF_FILE_MCP = path.join(os.homedir(), ".qa-stlc", "integration");
|
|
25
49
|
|
|
26
50
|
function readIntegrationPref() {
|
|
27
51
|
try {
|
|
28
|
-
if (
|
|
29
|
-
return
|
|
52
|
+
if (fs.existsSync(PREF_FILE_MCP))
|
|
53
|
+
return fs.readFileSync(PREF_FILE_MCP, "utf8").trim();
|
|
30
54
|
} catch (_) {}
|
|
31
55
|
return "ado";
|
|
32
56
|
}
|
|
33
57
|
|
|
34
|
-
const CWD
|
|
58
|
+
const CWD = process.cwd();
|
|
35
59
|
const IS_WIN = process.platform === "win32";
|
|
36
|
-
const EXT
|
|
60
|
+
const EXT = IS_WIN ? ".exe" : "";
|
|
37
61
|
|
|
38
|
-
const AGENT_NAMES
|
|
62
|
+
const AGENT_NAMES = [
|
|
39
63
|
"qa-test-case-manager",
|
|
40
64
|
"qa-gherkin-generator",
|
|
41
65
|
"qa-playwright-generator",
|
|
42
66
|
"qa-helix-writer",
|
|
43
67
|
];
|
|
44
|
-
|
|
45
|
-
// Jira agent — added separately because it needs env var injection
|
|
46
68
|
const JIRA_AGENT_NAME = "qa-jira-manager";
|
|
69
|
+
|
|
70
|
+
// ── Cost tracking env vars injected into every server entry ───────────────
|
|
71
|
+
//
|
|
72
|
+
// These are passed as env block items in both .mcp.json and .vscode/mcp.json.
|
|
73
|
+
// The "${env:X}" syntax is evaluated by Claude Code / VS Code at server start,
|
|
74
|
+
// substituting the actual value from the coding agent's own environment.
|
|
75
|
+
//
|
|
76
|
+
// ANTHROPIC_MODEL is the key one: Claude Code sets it automatically, so
|
|
77
|
+
// Claude Code users get correct pricing with zero manual configuration.
|
|
78
|
+
const COST_ENV = {
|
|
79
|
+
// Carry the coding agent's model ID into the MCP subprocess
|
|
80
|
+
"ANTHROPIC_MODEL": "${env:ANTHROPIC_MODEL}",
|
|
81
|
+
"GITHUB_COPILOT_MODEL": "${env:GITHUB_COPILOT_MODEL}",
|
|
82
|
+
// Explicit override — user sets this in shell profile if auto-detect is wrong
|
|
83
|
+
"STLC_CODING_AGENT_MODEL": "${env:STLC_CODING_AGENT_MODEL}",
|
|
84
|
+
// Opt-out flag — user sets STLC_COST_TRACKING=false in their shell to silence output
|
|
85
|
+
"STLC_COST_TRACKING": "${env:STLC_COST_TRACKING}",
|
|
86
|
+
};
|
|
87
|
+
|
|
47
88
|
const JIRA_ENV_VARS = {
|
|
48
89
|
JIRA_CLIENT_ID: "${env:JIRA_CLIENT_ID}",
|
|
49
90
|
JIRA_CLIENT_SECRET: "${env:JIRA_CLIENT_SECRET}",
|
|
50
91
|
JIRA_CLOUD_ID: "${env:JIRA_CLOUD_ID}",
|
|
51
92
|
};
|
|
52
93
|
|
|
53
|
-
/**
|
|
54
|
-
* Locate the binary for an agent.
|
|
55
|
-
* Priority: .venv → --python dir → system PATH → python sys.executable dir → macOS framework dirs
|
|
56
|
-
*/
|
|
57
94
|
function findBinary(name, pythonBin) {
|
|
58
95
|
// 1. .venv in cwd
|
|
59
96
|
const venvBin = IS_WIN
|
|
@@ -67,108 +104,132 @@ function findBinary(name, pythonBin) {
|
|
|
67
104
|
if (fs.existsSync(candidate)) return candidate;
|
|
68
105
|
}
|
|
69
106
|
|
|
70
|
-
// 3. system PATH
|
|
107
|
+
// 3. system PATH
|
|
71
108
|
const which = spawnSync(IS_WIN ? "where" : "which", [name], { encoding: "utf8" });
|
|
72
|
-
if (which.status === 0 && which.stdout.trim())
|
|
109
|
+
if (which.status === 0 && which.stdout.trim())
|
|
110
|
+
return which.stdout.trim().split("\n")[0].trim();
|
|
73
111
|
|
|
74
|
-
// 4.
|
|
112
|
+
// 4. Python's own bin dir (framework installs not on PATH)
|
|
75
113
|
for (const py of ["python3", "python"]) {
|
|
76
|
-
const r = spawnSync(py, ["-c", "import sys,
|
|
114
|
+
const r = spawnSync(py, ["-c", "import sys,os;print(os.path.dirname(sys.executable))"], { encoding: "utf8" });
|
|
77
115
|
if (r.status === 0 && r.stdout.trim()) {
|
|
78
116
|
const candidate = path.join(r.stdout.trim(), name + EXT);
|
|
79
117
|
if (fs.existsSync(candidate)) return candidate;
|
|
80
118
|
}
|
|
81
119
|
}
|
|
82
120
|
|
|
83
|
-
// 5. macOS
|
|
121
|
+
// 5. macOS framework fallback
|
|
84
122
|
if (!IS_WIN) {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
"/Library/Frameworks/Python.framework/Versions/3.12/bin",
|
|
88
|
-
"/Library/Frameworks/Python.framework/Versions/3.11/bin",
|
|
89
|
-
"/Library/Frameworks/Python.framework/Versions/3.10/bin",
|
|
90
|
-
];
|
|
91
|
-
for (const dir of frameworkDirs) {
|
|
92
|
-
const candidate = path.join(dir, name);
|
|
123
|
+
for (const v of ["3.13", "3.12", "3.11", "3.10"]) {
|
|
124
|
+
const candidate = `/Library/Frameworks/Python.framework/Versions/${v}/bin/${name}`;
|
|
93
125
|
if (fs.existsSync(candidate)) return candidate;
|
|
94
126
|
}
|
|
95
127
|
}
|
|
96
|
-
|
|
97
128
|
return null;
|
|
98
129
|
}
|
|
99
130
|
|
|
131
|
+
// ── Claude Code config (.mcp.json) ────────────────────────────────────────
|
|
132
|
+
|
|
100
133
|
function buildClaudeConfig(pythonBin, playwrightPort, integration) {
|
|
101
134
|
const servers = {};
|
|
102
135
|
const missing = [];
|
|
103
136
|
const needsAdo = !integration || integration === "ado" || integration === "both";
|
|
104
137
|
const needsJira = integration === "jira" || integration === "both";
|
|
105
|
-
|
|
106
|
-
// ADO agents — always included for ado/both; also included for jira because
|
|
107
|
-
// qa-gherkin-generator, qa-playwright-generator, and qa-helix-writer are shared
|
|
108
|
-
// by the Jira pipeline (Gherkin → Playwright → Helix steps run the same agents).
|
|
109
138
|
const activeNames = (needsAdo || needsJira) ? AGENT_NAMES : [];
|
|
110
139
|
|
|
111
140
|
for (const name of activeNames) {
|
|
112
141
|
const bin = findBinary(name, pythonBin);
|
|
113
142
|
if (bin) {
|
|
114
|
-
|
|
143
|
+
// Every server gets the cost tracking env block
|
|
144
|
+
servers[name] = { command: bin, env: { ...COST_ENV } };
|
|
115
145
|
} else {
|
|
116
146
|
missing.push(name);
|
|
117
|
-
servers[name] = {
|
|
147
|
+
servers[name] = {
|
|
148
|
+
command: `/path/to/.venv/bin/${name}`,
|
|
149
|
+
env: { ...COST_ENV },
|
|
150
|
+
"_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents",
|
|
151
|
+
};
|
|
118
152
|
}
|
|
119
153
|
}
|
|
120
154
|
|
|
121
|
-
// Jira agent — only when integration includes jira
|
|
122
155
|
if (needsJira) {
|
|
123
156
|
const jiraBin = findBinary(JIRA_AGENT_NAME, pythonBin);
|
|
124
157
|
if (jiraBin) {
|
|
125
|
-
servers[JIRA_AGENT_NAME] = {
|
|
158
|
+
servers[JIRA_AGENT_NAME] = {
|
|
159
|
+
command: jiraBin,
|
|
160
|
+
env: { ...COST_ENV, ...JIRA_ENV_VARS },
|
|
161
|
+
};
|
|
126
162
|
} else {
|
|
127
163
|
missing.push(JIRA_AGENT_NAME);
|
|
128
164
|
servers[JIRA_AGENT_NAME] = {
|
|
129
165
|
command: `/path/to/.venv/bin/${JIRA_AGENT_NAME}`,
|
|
130
|
-
env: JIRA_ENV_VARS,
|
|
166
|
+
env: { ...COST_ENV, ...JIRA_ENV_VARS },
|
|
131
167
|
"_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents",
|
|
132
168
|
};
|
|
133
169
|
}
|
|
134
170
|
}
|
|
135
171
|
|
|
136
|
-
servers["playwright"] = {
|
|
137
|
-
type: "url",
|
|
138
|
-
url: `ws://localhost:${playwrightPort}`,
|
|
139
|
-
};
|
|
172
|
+
servers["playwright"] = { type: "url", url: `ws://localhost:${playwrightPort}` };
|
|
140
173
|
|
|
141
174
|
return { config: { mcpServers: servers }, missing };
|
|
142
175
|
}
|
|
143
176
|
|
|
177
|
+
// ── VS Code config (.vscode/mcp.json) ─────────────────────────────────────
|
|
178
|
+
|
|
144
179
|
function buildVscodeConfig(pythonBin, playwrightPort, integration) {
|
|
145
180
|
const servers = {};
|
|
146
181
|
const missing = [];
|
|
147
182
|
const needsAdo = !integration || integration === "ado" || integration === "both";
|
|
148
183
|
const needsJira = integration === "jira" || integration === "both";
|
|
149
|
-
// Shared agents (Gherkin, Playwright, Helix) are needed by both ADO and Jira pipelines.
|
|
150
184
|
const activeNames = (needsAdo || needsJira) ? AGENT_NAMES : [];
|
|
151
185
|
|
|
152
186
|
for (const name of activeNames) {
|
|
153
187
|
const bin = findBinary(name, pythonBin);
|
|
154
188
|
if (bin) {
|
|
155
|
-
servers[name] = {
|
|
189
|
+
servers[name] = {
|
|
190
|
+
type: "stdio",
|
|
191
|
+
command: bin,
|
|
192
|
+
args: [],
|
|
193
|
+
env: {
|
|
194
|
+
// Azure DevOps auth passthrough (original)
|
|
195
|
+
"AZURE_TENANT_ID": "${env:AZURE_TENANT_ID}",
|
|
196
|
+
"AZURE_CLIENT_ID": "${env:AZURE_CLIENT_ID}",
|
|
197
|
+
"AZURE_CLIENT_SECRET": "${env:AZURE_CLIENT_SECRET}",
|
|
198
|
+
// Cost tracking passthrough (new)
|
|
199
|
+
...COST_ENV,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
156
202
|
} else {
|
|
157
203
|
missing.push(name);
|
|
158
|
-
servers[name] = {
|
|
204
|
+
servers[name] = {
|
|
205
|
+
type: "stdio",
|
|
206
|
+
command: `/path/to/.venv/bin/${name}`,
|
|
207
|
+
args: [],
|
|
208
|
+
env: { ...COST_ENV },
|
|
209
|
+
"_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents",
|
|
210
|
+
};
|
|
159
211
|
}
|
|
160
212
|
}
|
|
161
213
|
|
|
162
|
-
// Jira agent — only when integration includes jira
|
|
163
214
|
if (needsJira) {
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
166
|
-
servers[JIRA_AGENT_NAME] = {
|
|
215
|
+
const jiraBin = findBinary(JIRA_AGENT_NAME, pythonBin);
|
|
216
|
+
if (jiraBin) {
|
|
217
|
+
servers[JIRA_AGENT_NAME] = {
|
|
218
|
+
type: "stdio",
|
|
219
|
+
command: jiraBin,
|
|
220
|
+
args: [],
|
|
221
|
+
env: {
|
|
222
|
+
...COST_ENV,
|
|
223
|
+
...JIRA_ENV_VARS,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
167
226
|
} else {
|
|
168
227
|
missing.push(JIRA_AGENT_NAME);
|
|
169
228
|
servers[JIRA_AGENT_NAME] = {
|
|
229
|
+
type: "stdio",
|
|
170
230
|
command: `/path/to/.venv/bin/${JIRA_AGENT_NAME}`,
|
|
171
|
-
|
|
231
|
+
args: [],
|
|
232
|
+
env: { ...COST_ENV, ...JIRA_ENV_VARS },
|
|
172
233
|
"_comment": "NOT FOUND — run: pip install qa-gentic-stlc-agents",
|
|
173
234
|
};
|
|
174
235
|
}
|
|
@@ -190,9 +251,13 @@ function printNextSteps(mode, playwrightPort) {
|
|
|
190
251
|
${C.dim}headless (CI): npx @playwright/mcp@latest --headless --port ${playwrightPort}${C.reset}
|
|
191
252
|
|
|
192
253
|
${isVscode
|
|
193
|
-
? `Reload VS Code window — all
|
|
194
|
-
: `In Claude Code, run /mcp to verify all
|
|
254
|
+
? `Reload VS Code window — all MCP servers will appear in the MCP panel.`
|
|
255
|
+
: `In Claude Code, run /mcp to verify all servers are loaded.`
|
|
195
256
|
}
|
|
257
|
+
|
|
258
|
+
${C.dim}Cost tracking is active on all servers.
|
|
259
|
+
Each tool call logs tokens + cost to ~/.qa-stlc/cost-<session>.jsonl
|
|
260
|
+
View reports: qa-stlc cost${C.reset}
|
|
196
261
|
`);
|
|
197
262
|
}
|
|
198
263
|
|
|
@@ -220,7 +285,7 @@ module.exports = async function mcpConfig(opts) {
|
|
|
220
285
|
fs.mkdirSync(dir, { recursive: true });
|
|
221
286
|
const out = path.join(dir, "mcp.json");
|
|
222
287
|
fs.writeFileSync(out, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
223
|
-
ok(`Written → .vscode/mcp.json`);
|
|
288
|
+
ok(`Written → .vscode/mcp.json (cost tracking env vars included)`);
|
|
224
289
|
if (missing.length) {
|
|
225
290
|
warn(`${missing.length} agent(s) not found — run: pip install qa-gentic-stlc-agents`);
|
|
226
291
|
missing.forEach((m) => warn(` missing: ${m}`));
|
|
@@ -230,7 +295,7 @@ module.exports = async function mcpConfig(opts) {
|
|
|
230
295
|
const { config, missing } = buildClaudeConfig(pythonBin, playwrightPort, integration);
|
|
231
296
|
const out = path.join(CWD, ".mcp.json");
|
|
232
297
|
fs.writeFileSync(out, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
233
|
-
ok(`Written → .mcp.json`);
|
|
298
|
+
ok(`Written → .mcp.json (cost tracking env vars included)`);
|
|
234
299
|
if (missing.length) {
|
|
235
300
|
warn(`${missing.length} agent(s) not found — run: pip install qa-gentic-stlc-agents`);
|
|
236
301
|
missing.forEach((m) => warn(` missing: ${m}`));
|
|
@@ -323,6 +323,55 @@ async def list_tools() -> list[types.Tool]:
|
|
|
323
323
|
"required": ["gherkin_content"],
|
|
324
324
|
},
|
|
325
325
|
),
|
|
326
|
+
|
|
327
|
+
# ── NEW: Headless composite — fetch + validate + attach with retry ───
|
|
328
|
+
types.Tool(
|
|
329
|
+
name="generate_and_attach_gherkin",
|
|
330
|
+
description=(
|
|
331
|
+
"Headless composite tool: fetch work item data, validate Gherkin content, "
|
|
332
|
+
"and attach to ADO — retrying once internally if validation fails. "
|
|
333
|
+
"Use this from the pipeline or CI runner instead of calling "
|
|
334
|
+
"validate_gherkin_content + attach_gherkin_to_feature/work_item separately. "
|
|
335
|
+
"Accepts pre-generated gherkin_content from the LLM caller. "
|
|
336
|
+
"If validation fails on the first pass the errors are returned as "
|
|
337
|
+
"validation_failed (no retry is possible without a new LLM call — "
|
|
338
|
+
"the pipeline must re-generate and call this tool again). "
|
|
339
|
+
"Returns {status, gherkin_content, attached} on success, "
|
|
340
|
+
"or {status: validation_failed, errors, gherkin_content} on failure."
|
|
341
|
+
),
|
|
342
|
+
inputSchema={
|
|
343
|
+
"type": "object",
|
|
344
|
+
"properties": {
|
|
345
|
+
"work_item_id": {
|
|
346
|
+
"type": "integer",
|
|
347
|
+
"description": "ADO Feature, PBI, or Bug ID",
|
|
348
|
+
},
|
|
349
|
+
"work_item_title": {
|
|
350
|
+
"type": "string",
|
|
351
|
+
"description": "Title of the work item (used in filename)",
|
|
352
|
+
},
|
|
353
|
+
"organization_url": {"type": "string"},
|
|
354
|
+
"project_name": {"type": "string"},
|
|
355
|
+
"gherkin_content": {
|
|
356
|
+
"type": "string",
|
|
357
|
+
"description": "Pre-generated .feature file content from LLM caller",
|
|
358
|
+
},
|
|
359
|
+
"scope": {
|
|
360
|
+
"type": "string",
|
|
361
|
+
"enum": ["feature", "work_item"],
|
|
362
|
+
"description": (
|
|
363
|
+
"'feature' attaches to a Feature work item (5–10 scenarios). "
|
|
364
|
+
"'work_item' attaches to a PBI or Bug (3–9 scenarios). "
|
|
365
|
+
"Default: 'work_item'."
|
|
366
|
+
),
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
"required": [
|
|
370
|
+
"work_item_id", "work_item_title", "organization_url",
|
|
371
|
+
"project_name", "gherkin_content",
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
),
|
|
326
375
|
]
|
|
327
376
|
|
|
328
377
|
|
|
@@ -416,6 +465,44 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
|
416
465
|
arguments.get("scope", "feature"),
|
|
417
466
|
)
|
|
418
467
|
|
|
468
|
+
elif name == "generate_and_attach_gherkin":
|
|
469
|
+
org = arguments["organization_url"]
|
|
470
|
+
project = arguments["project_name"]
|
|
471
|
+
wi_id = arguments["work_item_id"]
|
|
472
|
+
wi_title = arguments["work_item_title"]
|
|
473
|
+
gherkin = arguments["gherkin_content"]
|
|
474
|
+
scope = arguments.get("scope", "work_item")
|
|
475
|
+
|
|
476
|
+
# Step 1: validate
|
|
477
|
+
validation = await asyncio.to_thread(_validate_gherkin, gherkin, scope)
|
|
478
|
+
if not validation["valid"]:
|
|
479
|
+
result = {
|
|
480
|
+
"status": "validation_failed",
|
|
481
|
+
"errors": validation.get("errors", []),
|
|
482
|
+
"gherkin_content": gherkin,
|
|
483
|
+
"message": (
|
|
484
|
+
"Gherkin content failed structural validation. "
|
|
485
|
+
"The pipeline must re-generate and call this tool again "
|
|
486
|
+
"with corrected content. No file was attached to ADO."
|
|
487
|
+
),
|
|
488
|
+
}
|
|
489
|
+
else:
|
|
490
|
+
# Step 2: attach
|
|
491
|
+
if scope == "feature":
|
|
492
|
+
attach = await asyncio.to_thread(
|
|
493
|
+
_attach_feature, org, project, wi_id, wi_title, gherkin
|
|
494
|
+
)
|
|
495
|
+
else:
|
|
496
|
+
attach = await asyncio.to_thread(
|
|
497
|
+
_attach_wi_file, org, project, wi_id, wi_title, gherkin
|
|
498
|
+
)
|
|
499
|
+
attach["_validation"] = _validate_attach_response(attach, scope)
|
|
500
|
+
result = {
|
|
501
|
+
"status": "ok",
|
|
502
|
+
"gherkin_content": gherkin,
|
|
503
|
+
"attached": attach,
|
|
504
|
+
}
|
|
505
|
+
|
|
419
506
|
else:
|
|
420
507
|
result = {"error": f"Unknown tool: {name}"}
|
|
421
508
|
|
|
@@ -448,7 +535,4 @@ def main():
|
|
|
448
535
|
|
|
449
536
|
|
|
450
537
|
if __name__ == "__main__":
|
|
451
|
-
main()
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
538
|
+
main()
|