@qa-gentic/stlc-agents 1.0.16 → 1.0.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/ORCHESTRATION_RULES.md +283 -0
- package/README.md +246 -321
- package/bin/postinstall.js +26 -2
- package/bin/qa-stlc.js +23 -0
- package/package.json +15 -2
- package/skills/write-helix-files/SKILL.md +6 -0
- package/src/cli/cmd-cost.js +253 -0
- package/src/cli/cmd-init.js +19 -2
- package/src/cli/cmd-mcp-config.js +123 -62
- package/src/cli/cmd-skills.js +21 -4
- 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/install_hook.py +154 -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,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
install_hook.py — stlc_agents.shared.install_hook
|
|
3
|
+
─────────────────────────────────────────────────────
|
|
4
|
+
Called automatically by postinstall.js after `pip install qa-gentic-stlc-agents`.
|
|
5
|
+
Also callable manually: python -m stlc_agents.shared.install_hook
|
|
6
|
+
|
|
7
|
+
Applies the cost tracking patch to all 5 MCP server files by importing
|
|
8
|
+
and running the same logic as scripts/apply_cost_tracking.py, but resolved
|
|
9
|
+
relative to the installed package location (works in site-packages, .venv, etc).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
SERVERS = [
|
|
19
|
+
("agent_gherkin_generator", "qa-gherkin-generator"),
|
|
20
|
+
("agent_test_case_manager", "qa-test-case-manager"),
|
|
21
|
+
("agent_playwright_generator", "qa-playwright-generator"),
|
|
22
|
+
("agent_helix_writer", "qa-helix-writer"),
|
|
23
|
+
("agent_jira_manager", "qa-jira-manager"),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
IMPORT_MARKER = "from stlc_agents.shared.cost_tracker import track"
|
|
27
|
+
TIME_IMPORT = "import time"
|
|
28
|
+
|
|
29
|
+
OLD_RETURN = (
|
|
30
|
+
'return [types.TextContent(type="text", '
|
|
31
|
+
'text=json.dumps(result, indent=2, ensure_ascii=False))]'
|
|
32
|
+
)
|
|
33
|
+
NEW_RETURN = "return track(result, tool_name=name, server={server!r}, t0=t0)"
|
|
34
|
+
|
|
35
|
+
OLD_ERR_BLOCK = """\
|
|
36
|
+
return [types.TextContent(
|
|
37
|
+
type="text",
|
|
38
|
+
text=json.dumps({"error": str(exc), "tool": name}, indent=2),
|
|
39
|
+
)]"""
|
|
40
|
+
|
|
41
|
+
NEW_ERR_BLOCK = """\
|
|
42
|
+
err_result = {{"error": str(exc), "tool": name}}
|
|
43
|
+
return track(err_result, tool_name=name, server={server!r}, t0=t0)"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _root() -> Path:
|
|
47
|
+
"""Resolve the stlc_agents package root regardless of install method."""
|
|
48
|
+
import stlc_agents
|
|
49
|
+
return Path(stlc_agents.__file__).parent
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def patch_server(agent_dir: str, server_name: str, root: Path) -> str:
|
|
53
|
+
"""Patch one server file. Returns 'patched' | 'already_patched' | 'not_found' | 'no_change'."""
|
|
54
|
+
path = root / agent_dir / "server.py"
|
|
55
|
+
if not path.exists():
|
|
56
|
+
return "not_found"
|
|
57
|
+
|
|
58
|
+
src = path.read_text(encoding="utf-8")
|
|
59
|
+
if IMPORT_MARKER in src:
|
|
60
|
+
return "already_patched"
|
|
61
|
+
|
|
62
|
+
original = src
|
|
63
|
+
|
|
64
|
+
# 1. import time
|
|
65
|
+
if TIME_IMPORT not in src:
|
|
66
|
+
src = src.replace("import sys\n", "import sys\nimport time\n", 1)
|
|
67
|
+
|
|
68
|
+
# 2. cost_tracker import — after last `from stlc_agents...` line
|
|
69
|
+
last_match = None
|
|
70
|
+
for m in re.finditer(r"^from stlc_agents\..+\n", src, re.MULTILINE):
|
|
71
|
+
last_match = m
|
|
72
|
+
if last_match:
|
|
73
|
+
pos = last_match.end()
|
|
74
|
+
src = src[:pos] + "from stlc_agents.shared.cost_tracker import track\n" + src[pos:]
|
|
75
|
+
else:
|
|
76
|
+
src = src.replace(
|
|
77
|
+
"from dotenv import load_dotenv\n",
|
|
78
|
+
"from dotenv import load_dotenv\nfrom stlc_agents.shared.cost_tracker import track\n",
|
|
79
|
+
1,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# 3. t0 = time.monotonic() inside call_tool()
|
|
83
|
+
src = src.replace(
|
|
84
|
+
"@app.call_tool()\nasync def call_tool(name: str, arguments: dict)"
|
|
85
|
+
" -> list[types.TextContent]:\n try:\n",
|
|
86
|
+
"@app.call_tool()\nasync def call_tool(name: str, arguments: dict)"
|
|
87
|
+
" -> list[types.TextContent]:\n t0 = time.monotonic()\n try:\n",
|
|
88
|
+
1,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# 4. Replace all result return lines
|
|
92
|
+
new_ret = NEW_RETURN.format(server=server_name)
|
|
93
|
+
for indent in (" ", " ", " "):
|
|
94
|
+
src = src.replace(f"{indent}{OLD_RETURN}", f"{indent}{new_ret}")
|
|
95
|
+
|
|
96
|
+
# 5. Replace error-path block
|
|
97
|
+
src = src.replace(OLD_ERR_BLOCK, NEW_ERR_BLOCK.format(server=server_name))
|
|
98
|
+
|
|
99
|
+
if src == original:
|
|
100
|
+
return "no_change"
|
|
101
|
+
|
|
102
|
+
path.write_text(src, encoding="utf-8")
|
|
103
|
+
return "patched"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def apply_cost_tracking() -> None:
|
|
107
|
+
"""Entry point — called by postinstall.js and the console_script."""
|
|
108
|
+
root = _root()
|
|
109
|
+
|
|
110
|
+
ok = "\x1b[32m✓\x1b[0m"
|
|
111
|
+
skip = "\x1b[33m–\x1b[0m"
|
|
112
|
+
err = "\x1b[31m✗\x1b[0m"
|
|
113
|
+
|
|
114
|
+
print("\n stlc-agents · Activating cost tracking on MCP servers...\n")
|
|
115
|
+
|
|
116
|
+
any_patched = False
|
|
117
|
+
for agent_dir, server_name in SERVERS:
|
|
118
|
+
status = patch_server(agent_dir, server_name, root)
|
|
119
|
+
if status == "patched":
|
|
120
|
+
print(f" {ok} {agent_dir} ({server_name})")
|
|
121
|
+
any_patched = True
|
|
122
|
+
elif status == "already_patched":
|
|
123
|
+
print(f" {skip} {agent_dir} — already active")
|
|
124
|
+
elif status == "not_found":
|
|
125
|
+
print(f" {err} {agent_dir}/server.py — not found (skip)")
|
|
126
|
+
elif status == "no_change":
|
|
127
|
+
print(f" {skip} {agent_dir} — no matching pattern (manual patch needed)")
|
|
128
|
+
|
|
129
|
+
print()
|
|
130
|
+
if any_patched:
|
|
131
|
+
print(" Cost tracking is now active. On every MCP tool call you will see:")
|
|
132
|
+
print(" [stlc-cost] <server> · <tool> ~<N>K tokens $<cost> (session: $<total>)")
|
|
133
|
+
print()
|
|
134
|
+
print(" Session logs: ~/.qa-stlc/cost-<session-id>.jsonl")
|
|
135
|
+
print(" View report: qa-stlc cost")
|
|
136
|
+
print(" View all: qa-stlc cost --all")
|
|
137
|
+
print()
|
|
138
|
+
print(" Environment variables:")
|
|
139
|
+
print(" STLC_COST_TRACKING=false disable output")
|
|
140
|
+
print(" STLC_CODING_AGENT_MODEL=<model> set your agent's model for exact pricing")
|
|
141
|
+
print(" e.g. claude-sonnet-4-6 | claude-opus-4-6 | gpt-4o")
|
|
142
|
+
print(" STLC_COST_LOG_DIR=<path> change log directory")
|
|
143
|
+
else:
|
|
144
|
+
print(" All servers already have cost tracking active.")
|
|
145
|
+
|
|
146
|
+
print()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def main() -> None:
|
|
150
|
+
apply_cost_tracking()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
main()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pricing.py — Model pricing registry for stlc-agents cost tracking.
|
|
3
|
+
|
|
4
|
+
Prices: USD per million tokens (MTok).
|
|
5
|
+
Source: Anthropic official docs, April 2026.
|
|
6
|
+
|
|
7
|
+
Models this repo actually calls:
|
|
8
|
+
- claude-sonnet-4-20250514 (LocatorHealer AI Vision, default)
|
|
9
|
+
- gpt-4o (LocatorHealer AI Vision, copilot provider)
|
|
10
|
+
+ whatever coding agent the user runs (Claude / Copilot / Cursor / Windsurf)
|
|
11
|
+
— the user declares this via STLC_CODING_AGENT_MODEL env var.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ModelPricing:
|
|
21
|
+
model_id: str
|
|
22
|
+
display_name: str
|
|
23
|
+
provider: str
|
|
24
|
+
input_per_mtok: float # USD / 1M input tokens
|
|
25
|
+
output_per_mtok: float # USD / 1M output tokens
|
|
26
|
+
cache_write_per_mtok: float # USD / 1M cache-write tokens
|
|
27
|
+
cache_read_per_mtok: float # USD / 1M cache-read tokens
|
|
28
|
+
|
|
29
|
+
def cost(
|
|
30
|
+
self,
|
|
31
|
+
input_tokens: int = 0,
|
|
32
|
+
output_tokens: int = 0,
|
|
33
|
+
cache_write_tokens: int = 0,
|
|
34
|
+
cache_read_tokens: int = 0,
|
|
35
|
+
) -> float:
|
|
36
|
+
return (
|
|
37
|
+
(input_tokens / 1_000_000) * self.input_per_mtok
|
|
38
|
+
+ (output_tokens / 1_000_000) * self.output_per_mtok
|
|
39
|
+
+ (cache_write_tokens/ 1_000_000) * self.cache_write_per_mtok
|
|
40
|
+
+ (cache_read_tokens / 1_000_000) * self.cache_read_per_mtok
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_REGISTRY: list[ModelPricing] = [
|
|
45
|
+
# ── Anthropic ──────────────────────────────────────────────────────────
|
|
46
|
+
ModelPricing("claude-sonnet-4-20250514", "Claude Sonnet 4", "anthropic", 3.00, 15.00, 3.75, 0.30),
|
|
47
|
+
ModelPricing("claude-sonnet-4-6", "Claude Sonnet 4.6", "anthropic", 3.00, 15.00, 3.75, 0.30),
|
|
48
|
+
ModelPricing("claude-haiku-4-5-20251001","Claude Haiku 4.5", "anthropic", 1.00, 5.00, 1.25, 0.10),
|
|
49
|
+
ModelPricing("claude-opus-4-6", "Claude Opus 4.6", "anthropic", 5.00, 25.00, 6.25, 0.50),
|
|
50
|
+
ModelPricing("claude-opus-4-7", "Claude Opus 4.7", "anthropic", 5.00, 25.00, 6.25, 0.50),
|
|
51
|
+
# ── OpenAI / Copilot ──────────────────────────────────────────────────
|
|
52
|
+
ModelPricing("gpt-4o", "GPT-4o", "openai", 2.50, 10.00, 0.00, 0.00),
|
|
53
|
+
ModelPricing("gpt-4o-mini", "GPT-4o Mini", "openai", 0.15, 0.60, 0.00, 0.00),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
_by_id: dict[str, ModelPricing] = {p.model_id: p for p in _REGISTRY}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_pricing(model_id: str) -> Optional[ModelPricing]:
|
|
60
|
+
"""Exact match first, then longest substring match."""
|
|
61
|
+
key = model_id.lower().strip()
|
|
62
|
+
if key in _by_id:
|
|
63
|
+
return _by_id[key]
|
|
64
|
+
# Substring: "claude-sonnet-4-20250514" ⊇ "sonnet-4"
|
|
65
|
+
for p in _REGISTRY:
|
|
66
|
+
if key in p.model_id or p.model_id in key:
|
|
67
|
+
return p
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def list_models() -> list[ModelPricing]:
|
|
72
|
+
return list(_REGISTRY)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-310.pyc
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-310.pyc
DELETED
|
Binary file
|
package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-310.pyc
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-310.pyc
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|