gd-specflow 0.2.0__py3-none-any.whl

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.
cli.py ADDED
@@ -0,0 +1,577 @@
1
+ """SpecFlow CLI — local-only, no-auth, localhost-default.
2
+
3
+ A thin client over existing mcp_server service code. Intended for users who
4
+ cannot use the MCP server (Cursor / Claude Code). All backend interaction
5
+ reuses SpecFlowBackendService; all path resolution reuses session.py.
6
+
7
+ Entry point: specflow <command> [options]
8
+ Also runnable as: python -m cli
9
+
10
+ Commands:
11
+ run-generation Upload specs and start code generation
12
+ check-status Poll the status of the current generation
13
+ retry-generation Retry a failed generation
14
+ download-outputs Download and extract completed generation outputs
15
+ clear-workspace Free a CLEANING workspace set early
16
+ sessions List active generation sessions
17
+
18
+ Local-only invariant: refuses to connect to non-localhost URLs unless --force
19
+ is passed. No API key is ever sent in local mode.
20
+ """
21
+
22
+ import argparse
23
+ import asyncio
24
+ import datetime
25
+ import json
26
+ import logging
27
+ import os
28
+ import sys
29
+ from pathlib import Path
30
+ from typing import Any
31
+ from urllib.parse import urlparse
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Config resolution
37
+ # ---------------------------------------------------------------------------
38
+
39
+ _MCP_CONFIG_FILENAME = ".specflow-local/mcp-config.json"
40
+ _DEFAULT_BACKEND_URL = "http://127.0.0.1:8000"
41
+ _LOCALHOST_HOSTS = {"localhost", "127.0.0.1"}
42
+
43
+
44
+ def _load_mcp_config(root: Path) -> dict[str, Any]:
45
+ """Load .specflow-local/mcp-config.json from the project root, if present."""
46
+ config_path = root / _MCP_CONFIG_FILENAME
47
+ try:
48
+ if config_path.exists():
49
+ data = json.loads(config_path.read_text())
50
+ env_block = (
51
+ data.get("mcpServers", {})
52
+ .get("specflow", {})
53
+ .get("env", {})
54
+ )
55
+ return env_block if isinstance(env_block, dict) else {}
56
+ except Exception as exc:
57
+ logger.debug("Could not read mcp-config.json: %s", exc)
58
+ return {}
59
+
60
+
61
+ def resolve_backend_config(
62
+ backend_url_flag: str | None,
63
+ user_email_flag: str | None,
64
+ workspace_count_flag: int | None,
65
+ root: Path,
66
+ ) -> tuple[str, str | None, int | None]:
67
+ """Resolve backend URL, user email, and workspace count.
68
+
69
+ Priority: CLI flag → env var → mcp-config.json → default.
70
+
71
+ Returns (backend_url, user_email, workspace_count).
72
+ """
73
+ mcp_env = _load_mcp_config(root)
74
+
75
+ backend_url = (
76
+ backend_url_flag
77
+ or os.getenv("BACKEND_URL")
78
+ or mcp_env.get("BACKEND_URL")
79
+ or _DEFAULT_BACKEND_URL
80
+ )
81
+ user_email = (
82
+ user_email_flag
83
+ or os.getenv("USER_EMAIL")
84
+ or mcp_env.get("USER_EMAIL")
85
+ or None
86
+ )
87
+
88
+ workspace_count: int | None = None
89
+ if workspace_count_flag is not None:
90
+ workspace_count = workspace_count_flag
91
+ else:
92
+ wc_env = os.getenv("WORKSPACE_COUNT") or mcp_env.get("WORKSPACE_COUNT")
93
+ if wc_env is not None:
94
+ try:
95
+ parsed = int(wc_env)
96
+ if parsed in (1, 2, 3):
97
+ workspace_count = parsed
98
+ except (ValueError, TypeError):
99
+ pass
100
+
101
+ return backend_url, user_email, workspace_count
102
+
103
+
104
+ def _is_localhost(url: str) -> bool:
105
+ """Return True if the URL host is localhost or 127.0.0.1."""
106
+ try:
107
+ host = urlparse(url).hostname or ""
108
+ return host.lower() in _LOCALHOST_HOSTS
109
+ except Exception:
110
+ return False
111
+
112
+
113
+ def check_localhost_guard(backend_url: str, force: bool) -> None:
114
+ """Print a warning and exit non-zero if backend_url is not localhost and --force not set."""
115
+ if force or _is_localhost(backend_url):
116
+ return
117
+ print(
118
+ f"ERROR: The CLI is local-only (no API key is sent).\n"
119
+ f" Resolved BACKEND_URL: {backend_url}\n"
120
+ f" This URL is not localhost — it will NOT authenticate against a remote backend.\n"
121
+ f" Use the MCP server for remote deployments.\n"
122
+ f" Pass --force to bypass this check.",
123
+ file=sys.stderr,
124
+ )
125
+ sys.exit(1)
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Environment setup — must be called before importing service modules
130
+ # ---------------------------------------------------------------------------
131
+
132
+
133
+ def _configure_env(backend_url: str, user_email: str | None) -> None:
134
+ """Push resolved config into the environment so service singletons pick it up."""
135
+ os.environ["BACKEND_URL"] = backend_url
136
+ if user_email:
137
+ os.environ["USER_EMAIL"] = user_email
138
+ else:
139
+ os.environ.pop("USER_EMAIL", None)
140
+ # Never set SPECFLOW_API_KEY in local mode — service omits X-API-Key when unset.
141
+ os.environ.pop("SPECFLOW_API_KEY", None)
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # Root resolution
146
+ # ---------------------------------------------------------------------------
147
+
148
+
149
+ def resolve_root(root_path_arg: str | None) -> Path:
150
+ """Return absolute project root. Defaults to cwd; --root-path overrides."""
151
+ if root_path_arg:
152
+ return Path(root_path_arg).expanduser().resolve()
153
+ return Path.cwd().resolve()
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # Command implementations (async)
158
+ # ---------------------------------------------------------------------------
159
+
160
+
161
+ async def cmd_run_generation(args: argparse.Namespace) -> int:
162
+ """run-generation: upload specs and start generation."""
163
+ from services.session import set_project_root, resolve_generation_id, write_session
164
+ from services.generation_orchestrator import GenerationOrchestrator
165
+ from services.run_generation_precheck import precheck as run_precheck
166
+ from services.tool_helpers import check_status_safe, is_generation_in_progress
167
+ from services.cli_service import fetch_pool_status, render_capacity_message
168
+
169
+ root = resolve_root(args.root_path)
170
+ print(f"Using project root: {root}")
171
+ set_project_root(root)
172
+
173
+ spec_dir = root / args.spec_dir
174
+ src_dir = root / args.src_dir
175
+ outputs_dir = args.outputs_dir
176
+
177
+ # Pre-run notice (FR-8, LV4.2)
178
+ print(
179
+ f"\nNote: outputs land in {root / outputs_dir}/{{generation_id}}/... and are\n"
180
+ f"archived in the artifact store — nothing is lost on workspace recycle.\n"
181
+ f"Retrieve them any time with: specflow download-outputs\n"
182
+ )
183
+
184
+ rejection = run_precheck(root, args.spec_dir, outputs_dir)
185
+ if rejection is not None:
186
+ print(f"ERROR: {rejection.to_dict().get('error', rejection.code.value)}", file=sys.stderr)
187
+ return 1
188
+
189
+ generation_id = resolve_generation_id(None, root)
190
+ if generation_id:
191
+ status_data = await check_status_safe(generation_id)
192
+ if is_generation_in_progress(status_data):
193
+ print(
194
+ "ERROR: A generation is already running. "
195
+ "Wait for the email notification before starting another one.",
196
+ file=sys.stderr,
197
+ )
198
+ return 1
199
+
200
+ workspace_count = args.workspace_count # may be None
201
+
202
+ try:
203
+ response_data = await GenerationOrchestrator.run_generation(
204
+ spec_dir=spec_dir,
205
+ src_dir=src_dir,
206
+ outputs_dir=outputs_dir,
207
+ generation_id=generation_id,
208
+ workspace_count=workspace_count,
209
+ )
210
+ except Exception as exc:
211
+ error_msg = str(exc)
212
+ # Capacity UX (LV4.1): if allocation failed, show cleaning sets
213
+ if "no" in error_msg.lower() and "workspace" in error_msg.lower():
214
+ try:
215
+ pool = await fetch_pool_status()
216
+ cleaning = pool.get("cleaning_sets") or []
217
+ if cleaning:
218
+ print(render_capacity_message(cleaning))
219
+ return 1
220
+ except Exception:
221
+ pass
222
+ print(f"ERROR: {error_msg}", file=sys.stderr)
223
+ return 1
224
+
225
+ new_id = response_data.get("generation_id")
226
+ if new_id:
227
+ write_session(new_id, root)
228
+
229
+ print(json.dumps(response_data, indent=2))
230
+ print(
231
+ "\nFiles uploaded and generation started (usually 2-8 hours).\n"
232
+ "Run `specflow check-status` to poll progress, or `specflow sessions --watch` for\n"
233
+ "continuous monitoring with a desktop notification on completion.\n"
234
+ "(Email/Slack notifications fire only if configured in .env.)"
235
+ )
236
+ return 0
237
+
238
+
239
+ async def cmd_check_status(args: argparse.Namespace) -> int:
240
+ """check-status: poll status of the current generation."""
241
+ from services.session import set_project_root, resolve_generation_id
242
+ from services.specflow_backend import call_backend_endpoint
243
+
244
+ root = resolve_root(args.root_path)
245
+ print(f"Using project root: {root}")
246
+ set_project_root(root)
247
+
248
+ generation_id = resolve_generation_id(None, root)
249
+ if not generation_id:
250
+ print("No active generation in this project. Run `specflow run-generation` to start one.")
251
+ return 0
252
+
253
+ response_text = await call_backend_endpoint(
254
+ endpoint=f"/api/v1/generation-sessions/{generation_id}/status",
255
+ method="GET",
256
+ timeout_seconds=30,
257
+ )
258
+ data = json.loads(response_text)
259
+ print(json.dumps(data, indent=2))
260
+ return 0
261
+
262
+
263
+ async def cmd_retry_generation(args: argparse.Namespace) -> int:
264
+ """retry-generation: retry a failed generation."""
265
+ from services.session import set_project_root, resolve_generation_id
266
+ from services.specflow_backend import call_backend_endpoint
267
+ from services.tool_helpers import check_status_safe, is_generation_in_progress
268
+
269
+ root = resolve_root(args.root_path)
270
+ print(f"Using project root: {root}")
271
+ set_project_root(root)
272
+
273
+ generation_id = resolve_generation_id(None, root)
274
+ if not generation_id:
275
+ print("No previous generation found. Run `specflow run-generation` to start one.")
276
+ return 0
277
+
278
+ status_data = await check_status_safe(generation_id)
279
+ if is_generation_in_progress(status_data):
280
+ print(
281
+ "ERROR: A generation is already running. Wait for it to finish before retrying.",
282
+ file=sys.stderr,
283
+ )
284
+ return 1
285
+
286
+ response_text = await call_backend_endpoint(
287
+ endpoint=f"/api/v1/generation-sessions/{generation_id}/retry",
288
+ method="POST",
289
+ timeout_seconds=30,
290
+ )
291
+ data = json.loads(response_text)
292
+ print(json.dumps(data, indent=2))
293
+ print("\nRetry queued. Generation will resume from the last checkpoint on the same workspaces.")
294
+ return 0
295
+
296
+
297
+ async def cmd_download_outputs(args: argparse.Namespace) -> int:
298
+ """download-outputs: download and extract completed generation outputs."""
299
+ from services.session import set_project_root, resolve_generation_id
300
+ from services.cli_service import download_and_extract_outputs
301
+
302
+ root = resolve_root(args.root_path)
303
+ print(f"Using project root: {root}")
304
+ set_project_root(root)
305
+
306
+ generation_id = resolve_generation_id(args.generation_id, root)
307
+ if not generation_id:
308
+ print("ERROR: No generation_id found. Pass --generation-id or run run-generation first.", file=sys.stderr)
309
+ return 1
310
+
311
+ outputs_dir = args.outputs_dir
312
+ try:
313
+ result = await download_and_extract_outputs(generation_id, outputs_dir)
314
+ except Exception as exc:
315
+ print(f"ERROR: Couldn't download outputs: {exc}", file=sys.stderr)
316
+ return 1
317
+
318
+ if result.get("status") == "success":
319
+ dest = result.get("outputs_dir", "")
320
+ count = result.get("files_extracted", 0)
321
+ print(f"Downloaded {count} files to: {dest}")
322
+ else:
323
+ print(json.dumps(result, indent=2))
324
+ return 0
325
+
326
+
327
+ def _render_sessions_table(sessions: list[dict]) -> None:
328
+ """Print a sessions table to stdout."""
329
+ if not sessions:
330
+ print("No active generation sessions.")
331
+ return
332
+ header = f"{'GENERATION ID':<40} {'STATUS':<16} {'CHECKPOINT'}"
333
+ print(header)
334
+ print("-" * len(header))
335
+ for s in sessions:
336
+ print(f"{s['generation_id']:<40} {s['status']:<16} {s.get('checkpoint', '')}")
337
+
338
+
339
+ async def cmd_sessions(args: argparse.Namespace) -> int:
340
+ """sessions: list active generation sessions (no new backend route)."""
341
+ from services.cli_service import fetch_sessions, notify_desktop, _TERMINAL_STATUSES
342
+
343
+ watch = getattr(args, "watch", False)
344
+ interval = getattr(args, "interval", 15)
345
+
346
+ if not watch:
347
+ sessions = await fetch_sessions()
348
+ _render_sessions_table(sessions)
349
+ return 0
350
+
351
+ # --watch: poll, refresh screen, notify on terminal state
352
+ notified: set[str] = set()
353
+ print(f"Watching sessions (every {interval}s) — Ctrl+C to stop\n")
354
+ while True:
355
+ try:
356
+ sessions = await fetch_sessions()
357
+ except Exception as exc:
358
+ print(f"Error fetching sessions: {exc}")
359
+ else:
360
+ # Clear terminal and reprint
361
+ print("\033[2J\033[H", end="", flush=True)
362
+ now = datetime.datetime.now().strftime("%H:%M:%S")
363
+ print(f"SpecFlow sessions — every {interval}s — {now} (Ctrl+C to stop)\n")
364
+ _render_sessions_table(sessions)
365
+
366
+ for s in sessions:
367
+ gid = s["generation_id"]
368
+ status = s["status"]
369
+ if status in _TERMINAL_STATUSES and gid not in notified:
370
+ notified.add(gid)
371
+ title = (
372
+ "SpecFlow: generation completed"
373
+ if status == "completed"
374
+ else "SpecFlow: generation failed"
375
+ )
376
+ msg = f"{gid[:12]}... {status}"
377
+ notify_desktop(title=title, message=msg)
378
+ print(f"\n[Desktop notification sent: {gid} → {status}]")
379
+
380
+ try:
381
+ await asyncio.sleep(interval)
382
+ except asyncio.CancelledError:
383
+ break
384
+ return 0
385
+
386
+
387
+ async def cmd_clear_workspace(args: argparse.Namespace) -> int:
388
+ """clear-workspace --set N: clear all 3 members of a CLEANING workspace set."""
389
+ from services.cli_service import clear_workspace_set
390
+
391
+ set_number = args.set
392
+ if not args.yes:
393
+ answer = input(f"Clear all 3 workspaces in set {set_number}? This cannot be undone. [y/N] ")
394
+ if answer.strip().lower() not in ("y", "yes"):
395
+ print("Aborted.")
396
+ return 0
397
+
398
+ results = await clear_workspace_set(set_number)
399
+ all_ok = True
400
+ for r in results:
401
+ status = "OK" if r["success"] else "FAIL"
402
+ print(f" {r['workspace_id']}: {status} — {r['message']}")
403
+ if not r["success"]:
404
+ all_ok = False
405
+
406
+ if all_ok:
407
+ print(f"\nSet {set_number} cleared. Workspaces are available for the next generation.")
408
+ else:
409
+ print("\nSome workspaces failed to clear. See errors above.", file=sys.stderr)
410
+ return 1
411
+ return 0
412
+
413
+
414
+ # ---------------------------------------------------------------------------
415
+ # Argument parser
416
+ # ---------------------------------------------------------------------------
417
+
418
+
419
+ def _build_parser() -> argparse.ArgumentParser:
420
+ parser = argparse.ArgumentParser(
421
+ prog="specflow",
422
+ description=(
423
+ "SpecFlow CLI — local-only tool for managing code-generation sessions.\n"
424
+ "Use the MCP server for remote/hosted deployments."
425
+ ),
426
+ )
427
+ parser.add_argument(
428
+ "--backend-url",
429
+ default=None,
430
+ help="Backend URL (default: BACKEND_URL env or http://127.0.0.1:8000)",
431
+ )
432
+ parser.add_argument(
433
+ "--user-email",
434
+ default=None,
435
+ help="User email (default: USER_EMAIL env)",
436
+ )
437
+ parser.add_argument(
438
+ "--root-path",
439
+ default=None,
440
+ help="Project root directory (default: current working directory)",
441
+ )
442
+ parser.add_argument(
443
+ "--force",
444
+ action="store_true",
445
+ help="Bypass the localhost guard for non-local backend URLs",
446
+ )
447
+
448
+ subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
449
+ subparsers.required = True
450
+
451
+ # run-generation
452
+ p_run = subparsers.add_parser("run-generation", help="Upload specs and start code generation")
453
+ p_run.add_argument("--spec-dir", default="specs", help="Spec directory (default: specs)")
454
+ p_run.add_argument("--outputs-dir", default="docs", help="Outputs directory (default: docs)")
455
+ p_run.add_argument("--src-dir", default="src", help="Source directory (default: src)")
456
+ p_run.add_argument(
457
+ "--workspace-count",
458
+ type=int,
459
+ choices=[1, 2, 3],
460
+ default=None,
461
+ dest="workspace_count",
462
+ help="Number of active workspaces (1-3; default from env/config)",
463
+ )
464
+
465
+ # check-status
466
+ subparsers.add_parser("check-status", help="Check progress of a running generation")
467
+
468
+ # retry-generation
469
+ subparsers.add_parser("retry-generation", help="Retry a failed generation")
470
+
471
+ # download-outputs
472
+ p_dl = subparsers.add_parser(
473
+ "download-outputs", help="Download and extract completed generation outputs"
474
+ )
475
+ p_dl.add_argument(
476
+ "--generation-id",
477
+ default=None,
478
+ dest="generation_id",
479
+ help="Generation ID (default: from specflow_session.json)",
480
+ )
481
+ p_dl.add_argument(
482
+ "--outputs-dir",
483
+ default="docs",
484
+ help="Local directory to extract outputs into (default: docs)",
485
+ )
486
+
487
+ # clear-workspace
488
+ p_clear = subparsers.add_parser(
489
+ "clear-workspace",
490
+ help="Free a CLEANING workspace set early (all 3 members)",
491
+ )
492
+ p_clear.add_argument("--set", type=int, required=True, dest="set", help="Set number to clear")
493
+ p_clear.add_argument("--yes", action="store_true", help="Skip confirmation prompt")
494
+
495
+ # sessions
496
+ p_sessions = subparsers.add_parser("sessions", help="List active generation sessions")
497
+ p_sessions.add_argument(
498
+ "--watch",
499
+ action="store_true",
500
+ help="Poll continuously and send a desktop notification on completion",
501
+ )
502
+ p_sessions.add_argument(
503
+ "--interval",
504
+ type=int,
505
+ default=15,
506
+ metavar="SECONDS",
507
+ help="Polling interval in seconds for --watch mode (default: 15)",
508
+ )
509
+
510
+ return parser
511
+
512
+
513
+ # ---------------------------------------------------------------------------
514
+ # Entry point
515
+ # ---------------------------------------------------------------------------
516
+
517
+ _COMMAND_MAP = {
518
+ "run-generation": cmd_run_generation,
519
+ "check-status": cmd_check_status,
520
+ "retry-generation": cmd_retry_generation,
521
+ "download-outputs": cmd_download_outputs,
522
+ "clear-workspace": cmd_clear_workspace,
523
+ "sessions": cmd_sessions,
524
+ }
525
+
526
+ # Commands that operate on files and print the project root
527
+ _FILE_OPERATING_COMMANDS = {
528
+ "run-generation",
529
+ "check-status",
530
+ "retry-generation",
531
+ "download-outputs",
532
+ }
533
+
534
+
535
+ def main() -> None:
536
+ """CLI entry point."""
537
+ logging.basicConfig(
538
+ level=os.getenv("LOG_LEVEL", "WARNING").upper(),
539
+ format="%(levelname)s - %(message)s",
540
+ )
541
+
542
+ parser = _build_parser()
543
+ args = parser.parse_args()
544
+
545
+ # Determine root early so config resolution can read mcp-config.json
546
+ root = resolve_root(getattr(args, "root_path", None))
547
+
548
+ # Resolve backend config (flag → env → mcp-config.json → default)
549
+ workspace_count_flag = getattr(args, "workspace_count", None)
550
+ backend_url, user_email, workspace_count = resolve_backend_config(
551
+ backend_url_flag=getattr(args, "backend_url", None),
552
+ user_email_flag=getattr(args, "user_email", None),
553
+ workspace_count_flag=workspace_count_flag,
554
+ root=root,
555
+ )
556
+
557
+ # Propagate resolved workspace_count back to args so cmd_run_generation picks it up
558
+ if not hasattr(args, "workspace_count") or args.workspace_count is None:
559
+ args.workspace_count = workspace_count
560
+
561
+ # Localhost guard
562
+ check_localhost_guard(backend_url, force=args.force)
563
+
564
+ # Push config into env before importing service singletons
565
+ _configure_env(backend_url, user_email)
566
+
567
+ handler = _COMMAND_MAP[args.command]
568
+ try:
569
+ exit_code = asyncio.run(handler(args))
570
+ except KeyboardInterrupt:
571
+ print("\nStopped.")
572
+ exit_code = 0
573
+ sys.exit(exit_code)
574
+
575
+
576
+ if __name__ == "__main__":
577
+ main()
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: gd-specflow
3
+ Version: 0.2.0
4
+ Summary: SpecFlow MCP Server and CLI — spec-driven AI code generation
5
+ Author-email: Adam Wrobel <awrobel@griddynamics.com>, Artsiom Kozak <akozak@griddynamics.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/griddynamics/gd-specflow
8
+ Project-URL: Repository, https://github.com/griddynamics/gd-specflow
9
+ Keywords: mcp,ai,code-generation,specflow,llm,claude
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Topic :: Software Development :: Code Generators
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: ==3.13.*
18
+ Requires-Dist: fastmcp>=0.2.0
19
+ Requires-Dist: httpx>=0.26.0
20
+ Requires-Dist: pydantic>=2.5.3
21
+ Requires-Dist: python-dotenv>=1.0.0
22
+ Requires-Dist: PyMuPDF>=1.25.0
23
+ Requires-Dist: python-docx>=1.1.0
24
+ Requires-Dist: python-pptx>=1.0.0
25
+ Requires-Dist: openpyxl>=3.1.0
26
+ Requires-Dist: plyer>=2.1.0
@@ -0,0 +1,31 @@
1
+ cli.py,sha256=NxcMAHEnM0umpq5_ZbODCLyAkT7bEtjpUERZ5WsFvAk,20270
2
+ server.py,sha256=sx3b1f8Wjy4Ct3ggjYzvCE7ftJ8z30TDt_-WWSTeSws,24713
3
+ schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ schemas/gain_json.py,sha256=7TBwMGpkhXWhgBGRuLV0kZ7XRyGDHnp-7XZubP6ebio,1809
5
+ schemas/generation_workflow_enums.py,sha256=a45hLJbgmeebwGUllmulOudLcfrc0C3ngpwrzMur3dM,855
6
+ schemas/specification.py,sha256=MBa8Hn4dlKAKzlEBqtMfhfQwh1dBCQ1tlRPel1471UY,777
7
+ services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ services/archive_builder.py,sha256=fW7ACDKIPGugvjktkJC4pBmY8LKjO9gBjYgg_kha2bA,6498
9
+ services/bundled_skills.py,sha256=WxzknWX7_7Ad3D5tYCaJseePXV2TU9NvntU8MoYWVHo,2790
10
+ services/cli_service.py,sha256=TMF4hP57gCY8jlo6zybgDlHkwcVOHiHqnZOnZrlAiKg,9131
11
+ services/document_reader.py,sha256=myET7r57vH4ibm0Xmc9SDGh30SN9NAFXzx1y0mi0mmU,13926
12
+ services/file_sync.py,sha256=s_Ad12T_Rk_HIy3RQWLJvh1Y0Nzlhov21-Yf5ynoQLI,8269
13
+ services/file_sync_orchestrator.py,sha256=VIG0oasX8EAYbuZynyiVR0VWjeioghsFzxhJvIAPYkQ,5561
14
+ services/generation_orchestrator.py,sha256=00SozQNhQzonZFBAdK6wjIlYXgIDd9QV9prsA-md6ow,9049
15
+ services/llm_tiers.py,sha256=Z2RAAlhETCi_efSyTHU1-Y3UOcMjVg3RcwtLTidNVG8,1368
16
+ services/run_generation_precheck.py,sha256=gxz2elZR-TvDrU1FTyeMyAZDSGqcRxyLTl_1IUOUZzY,8309
17
+ services/server_instructions.py,sha256=DsotO4oHoY5eBA9ofzKazlKQD7QLQU4OW7QjiNTkiZw,4261
18
+ services/session.py,sha256=9zHIdjqC_FiJO4hcN8AY3Mra3vHZrba2lvdipE7dDJE,7761
19
+ services/specflow_backend.py,sha256=kNtlXrNjRkJnr272rzb6uvOGNu3Ng2vG-PY0SIF6zEk,7965
20
+ services/tool_helpers.py,sha256=s5yxVPDjlNC6zGzW-elcWMdHTj6ihJJ5PjiFBcDEss4,4096
21
+ services/user_response.py,sha256=tQ2txv8TIyBL09IMjyJtfxFoPVnI8VE8nH8pLf13D4w,1235
22
+ services/skills/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ services/skills/specflow-analysis/SKILL.md,sha256=e-gB_kDWFX8uQgajthPxps4nwe0VTevk-m-pK1_VGCY,27872
24
+ services/skills/specflow-compare-variants/SKILL.md,sha256=Qip8lKxiub_uy9M18P_qjOWeyL2y8wgT7_FVTgJt8hk,9719
25
+ services/skills/specflow-diagnose/SKILL.md,sha256=uYyZtyZ_qenZv_9avky9kQAUzucJy5Oe2JV5ygSeM3A,7224
26
+ services/skills/specflow-planning/SKILL.md,sha256=MA6WJ-BXgpuxWVUlz2lgehp1TCXKxfwyuX0K9o6IRF4,14015
27
+ gd_specflow-0.2.0.dist-info/METADATA,sha256=XqmLJMmHZcakYWN9qA3UUfVjVk79yGSD4tdOQhU9LqY,1098
28
+ gd_specflow-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
29
+ gd_specflow-0.2.0.dist-info/entry_points.txt,sha256=l5523E6i-dE7vtK3WoTorbZTXkSQbzrD1mLy9JB3xJk,65
30
+ gd_specflow-0.2.0.dist-info/top_level.txt,sha256=FdqXan9EmfUaoz3P8q-vS939F4pSAY5H03ABRdnwZ_Y,28
31
+ gd_specflow-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ specflow = cli:main
3
+ specflow-mcp = server:main
@@ -0,0 +1,4 @@
1
+ cli
2
+ schemas
3
+ server
4
+ services
schemas/__init__.py ADDED
File without changes