svarog-forge 2.2.0__tar.gz → 2.2.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: svarog-forge
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: Forge. The smithy itself — AI-powered project forge from idea to deployment.
5
5
  Project-URL: Homepage, https://github.com/SvarogForge/forge
6
6
  Project-URL: Repository, https://github.com/SvarogForge/forge
@@ -0,0 +1,6 @@
1
+ """⚒️ Forge — AI smithy for Hermes Agent."""
2
+
3
+ from forge.cli import main, status, log, run_security_audit, display_audit_results
4
+
5
+ __version__ = "2.2.2"
6
+ __all__ = ["main", "status", "log", "run_security_audit", "display_audit_results"]
@@ -1,224 +1,223 @@
1
- #!/usr/bin/env python3
2
- """
3
- ⚒️ Forge — AI smithy orchestrator.
4
-
5
- Part of SvarogForge (github.com/SvarogForge/forge).
6
- For Hermes Agent (hermes-agent.nousresearch.com).
7
-
8
- Usage:
9
- python forge.py --plan "Telegram bot for tracking GitHub stars"
10
- python forge.py --plan "..." --scout-only
11
- python forge.py --resume crucible
12
- python forge.py --status
13
- python forge.py --audit # Security audit on current directory
14
- python forge.py --audit --plan "..." # Full cycle with security audit
15
- """
16
-
17
- import argparse
18
- import json
19
- import subprocess
20
- import sys
21
- from datetime import datetime
22
-
23
-
24
- VERSION = "2.2.0"
25
-
26
-
27
- def log(phase: str, message: str, level: str = "INFO"):
28
- """Structured logging for forge pipeline."""
29
- ts = datetime.now().strftime("%H:%M:%S")
30
- print(f"[{ts}] [{level}] [{phase}] {message}")
31
-
32
-
33
- def status():
34
- """Show current forge pipeline status."""
35
- print(f"⚒️ Forge v{VERSION}")
36
- print(" Pipeline: ready")
37
- print(" Phases: scout → gate → assemble → 🛡️audit → crucible → 🛡️audit → deliver → 🛡️audit")
38
- print(" Use: python forge.py --plan 'your idea'")
39
- print(" Security: python forge.py --audit")
40
-
41
-
42
- def run_security_audit() -> dict:
43
- """Run full security audit: files + commit messages."""
44
- # File security scan
45
- try:
46
- result = subprocess.run(
47
- [sys.executable, "forge-verify.py", "--security", "--json"],
48
- capture_output=True, text=True, timeout=60
49
- )
50
- if result.returncode == 0:
51
- scan_results = json.loads(result.stdout)
52
- else:
53
- try:
54
- scan_results = json.loads(result.stdout)
55
- except json.JSONDecodeError:
56
- scan_results = {
57
- "findings": [],
58
- "error": f"Security audit failed: {result.stderr[:200]}",
59
- "total_findings": 0,
60
- }
61
- except FileNotFoundError:
62
- scan_results = {"findings": [{"risk": "🔴", "label": "🚫 forge-verify.py not found", "file": "N/A", "line": 0, "match": "Missing forge-verify.py"}], "total_findings": 1}
63
- except subprocess.TimeoutExpired:
64
- scan_results = {"findings": [{"risk": "🟡", "label": "⏱️ Security audit timed out", "file": "N/A", "line": 0, "match": "Timeout >60s"}], "total_findings": 1}
65
-
66
- # Commit message scan
67
- try:
68
- commit_result = subprocess.run(
69
- [sys.executable, "forge-verify.py", "--commits", "--json"],
70
- input="\n".join(_get_recent_commits()),
71
- capture_output=True, text=True, timeout=30
72
- )
73
- if commit_result.returncode == 0:
74
- commit_data = json.loads(commit_result.stdout)
75
- else:
76
- try:
77
- commit_data = json.loads(commit_result.stdout)
78
- except json.JSONDecodeError:
79
- commit_data = {"findings": [], "total_messages": 0, "total_findings": 0}
80
- except Exception:
81
- commit_data = {"findings": [], "total_messages": 0, "total_findings": 0}
82
-
83
- # Merge findings
84
- all_findings = scan_results.get("findings", [])
85
- for cf in commit_data.get("findings", []):
86
- cf["file"] = "commit_message"
87
- cf["line"] = 0
88
- all_findings.append(cf)
89
-
90
- scan_results["findings"] = all_findings
91
- scan_results["total_findings"] = len(all_findings)
92
- scan_results["commit_messages_scanned"] = commit_data.get("total_messages", 0)
93
- return scan_results
94
-
95
-
96
- def _get_recent_commits(count: int = 10) -> list[str]:
97
- """Get recent commit subjects for scanning."""
98
- try:
99
- result = subprocess.run(
100
- ["git", "log", f"--max-count={count}", "--format=%s"],
101
- capture_output=True, text=True, timeout=10
102
- )
103
- return result.stdout.strip().splitlines()
104
- except Exception:
105
- return []
106
-
107
-
108
- def display_audit_results(results: dict):
109
- """Display security audit results."""
110
- findings = results.get("findings", [])
111
- commit_count = results.get("commit_messages_scanned", 0)
112
-
113
- if not findings:
114
- msg = f" 🛡️ Security Audit: ✅ PASSED — clean"
115
- if commit_count:
116
- msg += f" (scanned {commit_count} commits)"
117
- print(f"\n{msg}")
118
- return True
119
-
120
- red = [f for f in findings if f.get("risk") == "🔴"]
121
- yellow = [f for f in findings if f.get("risk") == "🟡"]
122
-
123
- if red:
124
- print(f"\n 🛡️ Security Audit: ❌ FAILED — {len(red)} blocking issue(s)")
125
- for f in red:
126
- label = "commit" if f.get("file") == "commit_message" else f"{f['file']}:{f['line']}"
127
- print(f" 🔴 {label} {f['match']}")
128
- return False
129
- elif yellow:
130
- print(f"\n 🛡️ Security Audit: ⚠️ WARNINGS — {len(yellow)} medium issue(s)")
131
- for f in yellow:
132
- label = "commit" if f.get("file") == "commit_message" else f"{f['file']}:{f['line']}"
133
- print(f" 🟡 {label} {f['match']}")
134
- if f.get("commit_msg"):
135
- print(f" ↳ {f['commit_msg']}")
136
- if commit_count:
137
- print(f" (scanned {commit_count} commits)")
138
- return True
139
- else:
140
- msg = f" 🛡️ Security Audit: ✅ PASSED — clean"
141
- if commit_count:
142
- msg += f" (scanned {commit_count} commits)"
143
- print(f"\n{msg}")
144
- return True
145
-
146
-
147
- def main():
148
- parser = argparse.ArgumentParser(
149
- description="⚒️ Forge — AI smithy orchestrator",
150
- formatter_class=argparse.RawDescriptionHelpFormatter,
151
- epilog="""
152
- Examples:
153
- python forge.py --plan "Telegram bot for tracking GitHub stars"
154
- python forge.py --plan "..." --scout-only
155
- python forge.py --resume crucible
156
- python forge.py --status
157
- python forge.py --audit
158
- python forge.py --audit --json
159
- """,
160
- )
161
- parser.add_argument("--plan", type=str, help="Project idea to forge")
162
- parser.add_argument("--scout-only", action="store_true", help="Only run market research")
163
- parser.add_argument("--resume", type=str, help="Resume from phase: gate, assemble, crucible, deliver")
164
- parser.add_argument("--status", action="store_true", help="Show pipeline status")
165
- parser.add_argument("--version", action="store_true", help="Show version")
166
- parser.add_argument("--audit", action="store_true", help="Run security audit on current directory")
167
- parser.add_argument("--json", action="store_true", help="JSON output (with --audit)")
168
-
169
- args = parser.parse_args()
170
-
171
- if args.version:
172
- print(f"Forge v{VERSION}")
173
- sys.exit(0)
174
-
175
- # Security audit mode
176
- if args.audit and not args.plan and not args.resume:
177
- log("AUDIT", "Running security audit...")
178
- results = run_security_audit()
179
- if args.json:
180
- print(json.dumps(results, indent=2, ensure_ascii=False))
181
- else:
182
- passed = display_audit_results(results)
183
- sys.exit(0 if passed else 1)
184
-
185
- if args.status or not any([args.plan, args.resume, args.audit, args.version]):
186
- status()
187
- sys.exit(0)
188
-
189
- idea = args.plan or "Unknown"
190
-
191
- log("START", f"⚒️ Forge starting for: {idea}")
192
-
193
- if args.scout_only:
194
- log("SCOUT", f"Market research for: {idea}")
195
- print(json.dumps({
196
- "phase": "scout",
197
- "idea": idea,
198
- "message": "Run delegate_task for 3 parallel scouts",
199
- "next": "review reports → gate"
200
- }, indent=2))
201
- elif args.resume:
202
- log("RESUME", f"Resuming from phase: {args.resume}")
203
- else:
204
- log("SCOUT", "Phase 1: Dispatching Talaria scouts...")
205
- pipeline = {
206
- "scout": "3 parallel scouts (competitors, demand, architecture)",
207
- "gate": "User approval required",
208
- "assemble": "5 sub-agents + hands-on fixes",
209
- "security_audit_1": "🛡️ After assembly secrets, personal data, NSFW, Discord",
210
- "crucible": "Verification checklist + pre-release security audit",
211
- "security_audit_2": "🛡️ Before release — repo contents, CVE, README-reality",
212
- "deliver": "Release + documentation",
213
- "security_audit_3": "🛡️ After release — release notes, PyPI, CI logs",
214
- }
215
- print(json.dumps({
216
- "pipeline": pipeline,
217
- "idea": idea,
218
- "version": VERSION,
219
- "next_step": "Run 'python forge.py --audit' for security scan"
220
- }, indent=2, ensure_ascii=False))
221
-
222
-
223
- if __name__ == "__main__":
224
- main()
1
+ """CLI entry point for Forge."""
2
+ import argparse
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ from datetime import datetime
7
+
8
+
9
+ VERSION = "2.2.2"
10
+
11
+
12
+ def log(phase: str, message: str, level: str = "INFO"):
13
+ """Structured logging for forge pipeline."""
14
+ ts = datetime.now().strftime("%H:%M:%S")
15
+ print(f"[{ts}] [{level}] [{phase}] {message}")
16
+
17
+
18
+ def status():
19
+ """Show current forge pipeline status."""
20
+ print(f"⚒️ Forge v{VERSION}")
21
+ print(" Pipeline: ready")
22
+ print(" Phases: scout → gate → assemble → 🛡️audit → crucible → 🛡️audit → deliver → 🛡️audit")
23
+ print(" Use: python forge.py --plan 'your idea'")
24
+ print(" Security: python forge.py --audit")
25
+
26
+
27
+ def run_security_audit() -> dict:
28
+ """Run full security audit: files + commit messages."""
29
+ # Try forge-verify.py next to package, or on PATH
30
+ verify_script = find_verify_script()
31
+
32
+ if verify_script:
33
+ try:
34
+ result = subprocess.run(
35
+ [sys.executable, verify_script, "--security", "--json"],
36
+ capture_output=True, text=True, timeout=60
37
+ )
38
+ if result.returncode == 0:
39
+ scan_results = json.loads(result.stdout)
40
+ else:
41
+ try:
42
+ scan_results = json.loads(result.stdout)
43
+ except json.JSONDecodeError:
44
+ scan_results = {
45
+ "findings": [],
46
+ "error": f"Security audit failed: {result.stderr[:200]}",
47
+ "total_findings": 0,
48
+ }
49
+ except subprocess.TimeoutExpired:
50
+ scan_results = {"findings": [{"risk": "🟡", "label": "⏱️ Security audit timed out", "file": "N/A", "line": 0, "match": "Timeout >60s"}], "total_findings": 1}
51
+ else:
52
+ scan_results = {"findings": [], "total_findings": 0}
53
+
54
+ # Commit message scan
55
+ try:
56
+ result = subprocess.run(
57
+ ["git", "log", "--max-count=10", "--format=%s"],
58
+ capture_output=True, text=True, timeout=10
59
+ )
60
+ commits = result.stdout.strip().splitlines()
61
+ except Exception:
62
+ commits = []
63
+
64
+ if verify_script:
65
+ try:
66
+ commit_result = subprocess.run(
67
+ [sys.executable, verify_script, "--commits", "--json"],
68
+ input="\n".join(commits),
69
+ capture_output=True, text=True, timeout=30
70
+ )
71
+ if commit_result.returncode == 0:
72
+ commit_data = json.loads(commit_result.stdout)
73
+ else:
74
+ try:
75
+ commit_data = json.loads(commit_result.stdout)
76
+ except json.JSONDecodeError:
77
+ commit_data = {"findings": [], "total_messages": 0, "total_findings": 0}
78
+ except Exception:
79
+ commit_data = {"findings": [], "total_messages": 0, "total_findings": 0}
80
+ else:
81
+ commit_data = {"findings": [], "total_messages": 0, "total_findings": 0}
82
+
83
+ all_findings = scan_results.get("findings", [])
84
+ for cf in commit_data.get("findings", []):
85
+ cf["file"] = "commit_message"
86
+ cf["line"] = 0
87
+ all_findings.append(cf)
88
+
89
+ scan_results["findings"] = all_findings
90
+ scan_results["total_findings"] = len(all_findings)
91
+ scan_results["commit_messages_scanned"] = commit_data.get("total_messages", 0)
92
+ return scan_results
93
+
94
+
95
+ def find_verify_script():
96
+ """Locate forge-verify.py."""
97
+ # Check next to the installed package
98
+ import os
99
+ pkg_dir = os.path.dirname(__file__)
100
+ for candidate in [
101
+ os.path.join(os.path.dirname(pkg_dir), "forge-verify.py"),
102
+ os.path.join(os.getcwd(), "forge-verify.py"),
103
+ ]:
104
+ if os.path.exists(candidate):
105
+ return candidate
106
+ return None
107
+
108
+
109
+ def display_audit_results(results: dict):
110
+ """Display security audit results."""
111
+ findings = results.get("findings", [])
112
+ commit_count = results.get("commit_messages_scanned", 0)
113
+
114
+ if not findings:
115
+ msg = f" 🛡️ Security Audit: ✅ PASSED — clean"
116
+ if commit_count:
117
+ msg += f" (scanned {commit_count} commits)"
118
+ print(f"\n{msg}")
119
+ return True
120
+
121
+ red = [f for f in findings if f.get("risk") == "🔴"]
122
+ yellow = [f for f in findings if f.get("risk") == "🟡"]
123
+
124
+ if red:
125
+ print(f"\n 🛡️ Security Audit: ❌ FAILED — {len(red)} blocking issue(s)")
126
+ for f in red:
127
+ label = "commit" if f.get("file") == "commit_message" else f"{f['file']}:{f['line']}"
128
+ print(f" 🔴 {label} — {f['match']}")
129
+ return False
130
+ elif yellow:
131
+ print(f"\n 🛡️ Security Audit: ⚠️ WARNINGS — {len(yellow)} medium issue(s)")
132
+ for f in yellow:
133
+ label = "commit" if f.get("file") == "commit_message" else f"{f['file']}:{f['line']}"
134
+ print(f" 🟡 {label} — {f['match']}")
135
+ if f.get("commit_msg"):
136
+ print(f" ↳ {f['commit_msg']}")
137
+ if commit_count:
138
+ print(f" (scanned {commit_count} commits)")
139
+ return True
140
+ else:
141
+ msg = f" 🛡️ Security Audit: ✅ PASSED — clean"
142
+ if commit_count:
143
+ msg += f" (scanned {commit_count} commits)"
144
+ print(f"\n{msg}")
145
+ return True
146
+
147
+
148
+ def main():
149
+ parser = argparse.ArgumentParser(
150
+ description="⚒️ Forge — AI smithy orchestrator",
151
+ formatter_class=argparse.RawDescriptionHelpFormatter,
152
+ epilog="""Examples:
153
+ forge --plan "Telegram bot for tracking GitHub stars"
154
+ forge --plan "..." --scout-only
155
+ forge --resume crucible
156
+ forge --status
157
+ forge --version
158
+ forge --audit
159
+ forge --audit --json
160
+ """,
161
+ )
162
+ parser.add_argument("--plan", type=str, help="Project idea to forge")
163
+ parser.add_argument("--scout-only", action="store_true", help="Only run market research")
164
+ parser.add_argument("--resume", type=str, help="Resume from phase: gate, assemble, crucible, deliver")
165
+ parser.add_argument("--status", action="store_true", help="Show pipeline status")
166
+ parser.add_argument("--version", action="store_true", help="Show version")
167
+ parser.add_argument("--audit", action="store_true", help="Run security audit on current directory")
168
+ parser.add_argument("--json", action="store_true", help="JSON output (with --audit)")
169
+
170
+ args = parser.parse_args()
171
+
172
+ if args.version:
173
+ print(f"Forge v{VERSION}")
174
+ sys.exit(0)
175
+
176
+ if args.audit and not args.plan and not args.resume:
177
+ log("AUDIT", "Running security audit...")
178
+ results = run_security_audit()
179
+ if args.json:
180
+ print(json.dumps(results, indent=2, ensure_ascii=False))
181
+ else:
182
+ passed = display_audit_results(results)
183
+ sys.exit(0 if passed else 1)
184
+
185
+ if args.status or not any([args.plan, args.resume, args.audit, args.version]):
186
+ status()
187
+ sys.exit(0)
188
+
189
+ idea = args.plan or "Unknown"
190
+ log("START", f"⚒️ Forge starting for: {idea}")
191
+
192
+ if args.scout_only:
193
+ log("SCOUT", f"Market research for: {idea}")
194
+ print(json.dumps({
195
+ "phase": "scout",
196
+ "idea": idea,
197
+ "message": "Run delegate_task for 3 parallel scouts",
198
+ "next": "review reports gate"
199
+ }, indent=2))
200
+ elif args.resume:
201
+ log("RESUME", f"Resuming from phase: {args.resume}")
202
+ else:
203
+ log("SCOUT", "Phase 1: Dispatching Talaria scouts...")
204
+ pipeline = {
205
+ "scout": "3 parallel scouts (competitors, demand, architecture)",
206
+ "gate": "User approval required",
207
+ "assemble": "5 sub-agents + hands-on fixes",
208
+ "security_audit_1": "🛡️ After assembly secrets, personal data, NSFW, Discord",
209
+ "crucible": "Verification checklist + pre-release security audit",
210
+ "security_audit_2": "🛡️ Before release repo contents, CVE, README-reality",
211
+ "deliver": "Release + documentation",
212
+ "security_audit_3": "🛡️ After release — release notes, PyPI, CI logs",
213
+ }
214
+ print(json.dumps({
215
+ "pipeline": pipeline,
216
+ "idea": idea,
217
+ "version": VERSION,
218
+ "next_step": "Run 'forge --audit' for security scan"
219
+ }, indent=2, ensure_ascii=False))
220
+
221
+
222
+ if __name__ == "__main__":
223
+ main()
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ⚒️ Forge — AI smithy orchestrator.
4
+
5
+ Part of SvarogForge (github.com/SvarogForge/forge).
6
+ For Hermes Agent (hermes-agent.nousresearch.com).
7
+
8
+ Usage:
9
+ python forge.py --plan "Telegram bot for tracking GitHub stars"
10
+ python forge.py --plan "..." --scout-only
11
+ python forge.py --resume crucible
12
+ python forge.py --status
13
+ python forge.py --audit # Security audit on current directory
14
+ python forge.py --audit --plan "..." # Full cycle with security audit
15
+ """
16
+
17
+ from forge.cli import main
18
+
19
+ if __name__ == "__main__":
20
+ main()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "svarog-forge"
7
- version = "2.2.0"
7
+ version = "2.2.2"
8
8
  description = "Forge. The smithy itself — AI-powered project forge from idea to deployment."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,3 +0,0 @@
1
- """⚒️ Forge — AI smithy for Hermes Agent."""
2
-
3
- __version__ = "2.2.0"
@@ -1,12 +0,0 @@
1
- """CLI entry point for Forge — imported via pip-installed svarog-forge package."""
2
- import sys
3
- import os
4
-
5
- # Allow importing forge.py from the package root
6
- sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
7
-
8
- from forge import main # noqa: E402
9
-
10
-
11
- if __name__ == "__main__":
12
- main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes