torusguard 1.0.0 → 1.3.0

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.
Files changed (55) hide show
  1. package/.torusguard/.manifest.json +89 -75
  2. package/.torusguard/references/csharp-security.md +41 -0
  3. package/.torusguard/references/go-security.md +41 -0
  4. package/.torusguard/references/java-security.md +40 -0
  5. package/.torusguard/references/polyglot-security-matrix.md +25 -0
  6. package/.torusguard/references/rust-security.md +40 -0
  7. package/.torusguard/rules/custom/.gitkeep +1 -0
  8. package/.torusguard/rules/custom/README.md +30 -0
  9. package/.torusguard/runs/report-latest.html +328 -0
  10. package/.torusguard/schemas/golden-recipe.schema.json +75 -0
  11. package/.torusguard/scripts/diff_guard.py +131 -4
  12. package/.torusguard/scripts/finding_scorer.py +33 -5
  13. package/.torusguard/scripts/html_reporter.py +628 -0
  14. package/.torusguard/scripts/manifest_builder.py +5 -5
  15. package/.torusguard/scripts/memory_engine.py +521 -99
  16. package/.torusguard/scripts/monorepo_detector.py +123 -10
  17. package/.torusguard/scripts/rules_sync.py +321 -0
  18. package/.torusguard/scripts/stack_detect.py +458 -26
  19. package/.torusguard/skills/torusguard/references/csharp-security.md +41 -0
  20. package/.torusguard/skills/torusguard/references/go-security.md +41 -0
  21. package/.torusguard/skills/torusguard/references/java-security.md +40 -0
  22. package/.torusguard/skills/torusguard/references/polyglot-security-matrix.md +25 -0
  23. package/.torusguard/skills/torusguard/references/rust-security.md +40 -0
  24. package/README.md +307 -410
  25. package/bin/torusguard.js +108 -4
  26. package/package.json +1 -1
  27. package/skills/torusguard/bootstrap.py +2 -2
  28. package/skills/torusguard/payload/.manifest.json +89 -75
  29. package/skills/torusguard/payload/references/csharp-security.md +41 -0
  30. package/skills/torusguard/payload/references/go-security.md +41 -0
  31. package/skills/torusguard/payload/references/java-security.md +40 -0
  32. package/skills/torusguard/payload/references/polyglot-security-matrix.md +25 -0
  33. package/skills/torusguard/payload/references/rust-security.md +40 -0
  34. package/skills/torusguard/payload/rules/custom/.gitkeep +1 -0
  35. package/skills/torusguard/payload/rules/custom/README.md +30 -0
  36. package/skills/torusguard/payload/schemas/golden-recipe.schema.json +75 -0
  37. package/skills/torusguard/payload/scripts/diff_guard.py +131 -4
  38. package/skills/torusguard/payload/scripts/finding_scorer.py +33 -5
  39. package/skills/torusguard/payload/scripts/html_reporter.py +628 -0
  40. package/skills/torusguard/payload/scripts/manifest_builder.py +5 -5
  41. package/skills/torusguard/payload/scripts/memory_engine.py +521 -99
  42. package/skills/torusguard/payload/scripts/monorepo_detector.py +123 -10
  43. package/skills/torusguard/payload/scripts/rules_sync.py +321 -0
  44. package/skills/torusguard/payload/scripts/stack_detect.py +458 -26
  45. package/skills/torusguard/payload/skills/torusguard/bootstrap.py +2 -2
  46. package/skills/torusguard/payload/skills/torusguard/references/csharp-security.md +41 -0
  47. package/skills/torusguard/payload/skills/torusguard/references/go-security.md +41 -0
  48. package/skills/torusguard/payload/skills/torusguard/references/java-security.md +40 -0
  49. package/skills/torusguard/payload/skills/torusguard/references/polyglot-security-matrix.md +25 -0
  50. package/skills/torusguard/payload/skills/torusguard/references/rust-security.md +40 -0
  51. package/.torusguard/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  52. package/.torusguard/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
  53. package/.torusguard/scripts/__pycache__/memory_engine.cpython-311.pyc +0 -0
  54. package/.torusguard/scripts/__pycache__/monorepo_detector.cpython-311.pyc +0 -0
  55. package/skills/torusguard/__pycache__/bootstrap.cpython-311.pyc +0 -0
@@ -26,7 +26,7 @@ def detect_package_info(abs_pkg_dir: Path, root: Path) -> Dict[str, Any]:
26
26
  # Check typescript / javascript
27
27
  package_json = pkg_dir / "package.json"
28
28
  if package_json.is_file() or list(pkg_dir.glob("*.ts")) or list(pkg_dir.glob("*.js")):
29
- language = "typescript"
29
+ language = "TypeScript" if (list(pkg_dir.glob("*.ts")) or list(pkg_dir.glob("**/*.ts"))) else "JavaScript"
30
30
  if package_json.is_file():
31
31
  try:
32
32
  with open(package_json, "r", encoding="utf-8") as f:
@@ -35,6 +35,8 @@ def detect_package_info(abs_pkg_dir: Path, root: Path) -> Dict[str, Any]:
35
35
  deps = {**pj.get("dependencies", {}), **pj.get("devDependencies", {})}
36
36
  if "next" in deps:
37
37
  framework = "nextjs"
38
+ elif "@nestjs/core" in deps:
39
+ framework = "nestjs"
38
40
  elif "express" in deps:
39
41
  framework = "express"
40
42
  elif "react" in deps:
@@ -51,7 +53,7 @@ def detect_package_info(abs_pkg_dir: Path, root: Path) -> Dict[str, Any]:
51
53
  reqs = pkg_dir / "requirements.txt"
52
54
  if pyproject.is_file() or reqs.is_file() or list(pkg_dir.glob("*.py")):
53
55
  if language == "unknown":
54
- language = "python"
56
+ language = "Python"
55
57
  content = ""
56
58
  if pyproject.is_file():
57
59
  content += pyproject.read_text(encoding="utf-8", errors="replace").lower()
@@ -61,17 +63,116 @@ def detect_package_info(abs_pkg_dir: Path, root: Path) -> Dict[str, Any]:
61
63
  content += pyf.read_text(encoding="utf-8", errors="replace").lower()
62
64
 
63
65
  if "fastapi" in content or "fastapi" in pkg_dir.name:
64
- framework = "fastapi"
66
+ framework = "FastAPI"
65
67
  elif "django" in content or "django" in pkg_dir.name:
66
- framework = "django"
68
+ framework = "Django"
67
69
  elif "flask" in content or "flask" in pkg_dir.name:
68
- framework = "flask"
70
+ framework = "Flask"
71
+
72
+ # Check Go
73
+ go_mod = pkg_dir / "go.mod"
74
+ if go_mod.is_file() or list(pkg_dir.glob("*.go")):
75
+ if language == "unknown":
76
+ language = "Go"
77
+ if go_mod.is_file():
78
+ try:
79
+ gc = go_mod.read_text(encoding="utf-8", errors="replace")
80
+ if "gin-gonic/gin" in gc:
81
+ framework = "Gin"
82
+ elif "gofiber/fiber" in gc:
83
+ framework = "Fiber"
84
+ elif "go-chi/chi" in gc:
85
+ framework = "Chi"
86
+ except Exception:
87
+ pass
88
+
89
+ # Check Rust
90
+ cargo_toml = pkg_dir / "Cargo.toml"
91
+ if cargo_toml.is_file() or list(pkg_dir.glob("*.rs")):
92
+ if language == "unknown":
93
+ language = "Rust"
94
+ if cargo_toml.is_file():
95
+ try:
96
+ cc = cargo_toml.read_text(encoding="utf-8", errors="replace")
97
+ if "actix-web" in cc:
98
+ framework = "Actix-web"
99
+ elif "axum" in cc:
100
+ framework = "Axum"
101
+ elif "rocket" in cc:
102
+ framework = "Rocket"
103
+ except Exception:
104
+ pass
105
+
106
+ # Check Java / Kotlin
107
+ pom_xml = pkg_dir / "pom.xml"
108
+ b_gradle = pkg_dir / "build.gradle"
109
+ b_gradle_kts = pkg_dir / "build.gradle.kts"
110
+ if pom_xml.is_file() or b_gradle.is_file() or b_gradle_kts.is_file() or list(pkg_dir.glob("**/*.java")) or list(pkg_dir.glob("**/*.kt")):
111
+ if language == "unknown":
112
+ language = "Kotlin" if (b_gradle_kts.is_file() or list(pkg_dir.glob("**/*.kt"))) else "Java"
113
+ jc = ""
114
+ for jf in [pom_xml, b_gradle, b_gradle_kts]:
115
+ if jf.is_file():
116
+ try:
117
+ jc += jf.read_text(encoding="utf-8", errors="replace") + "\n"
118
+ except Exception:
119
+ pass
120
+ if "spring-boot" in jc or "springframework" in jc:
121
+ framework = "Spring Boot"
122
+ elif "quarkus" in jc:
123
+ framework = "Quarkus"
124
+ elif "ktor" in jc:
125
+ framework = "Ktor"
126
+
127
+ # Check C#
128
+ csprojs = list(pkg_dir.glob("*.csproj")) + list(pkg_dir.glob("**/*.csproj"))
129
+ if csprojs or list(pkg_dir.glob("*.sln")) or list(pkg_dir.glob("*.cs")):
130
+ if language == "unknown":
131
+ language = "C#"
132
+ csc = ""
133
+ for cf in csprojs[:5]:
134
+ try:
135
+ csc += cf.read_text(encoding="utf-8", errors="replace") + "\n"
136
+ except Exception:
137
+ pass
138
+ if "Microsoft.NET.Sdk.Web" in csc or "Microsoft.AspNetCore" in csc or "Swashbuckle" in csc:
139
+ framework = "ASP.NET Core"
140
+
141
+ # Check PHP
142
+ composer_json = pkg_dir / "composer.json"
143
+ if composer_json.is_file() or list(pkg_dir.glob("*.php")):
144
+ if language == "unknown":
145
+ language = "PHP"
146
+ if composer_json.is_file():
147
+ try:
148
+ with open(composer_json, "r", encoding="utf-8") as f:
149
+ c_data = json.load(f)
150
+ reqs = {**c_data.get("require", {}), **c_data.get("require-dev", {})}
151
+ if "laravel/framework" in reqs:
152
+ framework = "Laravel"
153
+ elif any("symfony" in k for k in reqs):
154
+ framework = "Symfony"
155
+ except Exception:
156
+ pass
157
+
158
+ # Check Ruby
159
+ gemfile = pkg_dir / "Gemfile"
160
+ if gemfile.is_file() or list(pkg_dir.glob("*.rb")):
161
+ if language == "unknown":
162
+ language = "Ruby"
163
+ if gemfile.is_file():
164
+ try:
165
+ gc = gemfile.read_text(encoding="utf-8", errors="replace")
166
+ if "rails" in gc:
167
+ framework = "Ruby on Rails"
168
+ except Exception:
169
+ pass
69
170
 
70
171
  return {
71
172
  "name": name,
72
173
  "path": rel_path,
73
- "language": language,
74
- "framework": framework
174
+ "language": language.lower(),
175
+ "framework": framework.lower()
75
176
  }
76
177
 
77
178
 
@@ -102,21 +203,33 @@ def scan_workspace(root_dir: str = ".") -> Dict[str, Any]:
102
203
  pass
103
204
 
104
205
  # Scan common monorepo container directories and direct project subfolders
105
- candidate_folders = ["apps", "packages", "services", "libs", "modules"]
206
+ candidate_folders = ["apps", "packages", "services", "src", "libs", "modules"]
207
+ manifest_names = [
208
+ "package.json", "pyproject.toml", "requirements.txt", "setup.py", "Cargo.toml",
209
+ "go.mod", "pom.xml", "build.gradle", "build.gradle.kts", "composer.json",
210
+ "Gemfile", "mix.exs", "pubspec.yaml", "CMakeLists.txt", "main.py", "main.go", "actions.ts"
211
+ ]
106
212
  found_dirs = []
107
213
 
214
+ def has_manifest(d: Path) -> bool:
215
+ if any((d / m).exists() for m in manifest_names):
216
+ return True
217
+ if list(d.glob("*.csproj")) or list(d.glob("*/*.csproj")):
218
+ return True
219
+ return False
220
+
108
221
  for cf in candidate_folders:
109
222
  container = root / cf
110
223
  if container.is_dir():
111
224
  for child in container.iterdir():
112
225
  if child.is_dir() and not child.name.startswith(('.', '_')):
113
- if any((child / m).exists() for m in ["package.json", "pyproject.toml", "requirements.txt", "setup.py", "Cargo.toml", "main.py"]):
226
+ if has_manifest(child):
114
227
  found_dirs.append(child)
115
228
 
116
229
  # Also scan direct subdirectories if not already captured
117
230
  for child in root.iterdir():
118
231
  if child.is_dir() and not child.name.startswith(('.', '_')) and child.name not in candidate_folders:
119
- if any((child / m).exists() for m in ["package.json", "pyproject.toml", "requirements.txt", "setup.py", "Cargo.toml", "main.py", "actions.ts"]):
232
+ if has_manifest(child):
120
233
  if child not in found_dirs:
121
234
  found_dirs.append(child)
122
235
 
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ TorusGuard AI IDE Rules Auto-Sync Engine (v1.2.0)
4
+ Compiles project security memory, golden fix recipes, and active guardrails
5
+ into prompt-optimized rule files for Cursor, Claude Code, Antigravity, and Windsurf.
6
+ Non-destructive: preserves existing user rules via demarcated comment fences.
7
+ Zero external dependencies (Python 3.10+ stdlib).
8
+ """
9
+
10
+ import sys
11
+ import json
12
+ import re
13
+ import argparse
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ # Windows console UTF-8 support
18
+ if sys.stdout and hasattr(sys.stdout, "reconfigure"):
19
+ try:
20
+ getattr(sys.stdout, "reconfigure")(encoding="utf-8", errors="replace")
21
+ except (AttributeError, OSError):
22
+ pass
23
+ if sys.stderr and hasattr(sys.stderr, "reconfigure"):
24
+ try:
25
+ getattr(sys.stderr, "reconfigure")(encoding="utf-8", errors="replace")
26
+ except (AttributeError, OSError):
27
+ pass
28
+
29
+ START_MARKER = "<!-- TORUSGUARD-SECURITY-GUARDRAILS:START -->"
30
+ END_MARKER = "<!-- TORUSGUARD-SECURITY-GUARDRAILS:END -->"
31
+ MAX_PROMPT_TOKENS = 400
32
+
33
+
34
+ def find_project_root(start_dir: str | Path | None = None) -> Path:
35
+ """Detect project root directory."""
36
+ current = Path(start_dir or Path.cwd()).resolve()
37
+ markers = [".git", "package.json", "pyproject.toml", ".torusguard"]
38
+ for m in markers:
39
+ if (current / m).exists():
40
+ return current
41
+ for parent in current.parents:
42
+ for m in markers:
43
+ if (parent / m).exists():
44
+ return parent
45
+ return current
46
+
47
+
48
+ def load_security_context(root_dir: Path) -> dict[str, Any]:
49
+ """Load security profile and patterns from memory subsystem."""
50
+ tg_dir = root_dir / ".torusguard"
51
+ memory_dir = tg_dir / "memory"
52
+
53
+ context_file = memory_dir / "context.json"
54
+ patterns_file = memory_dir / "patterns.json"
55
+ profile_file = memory_dir / "profile.json"
56
+
57
+ context_data: dict[str, Any] = {}
58
+ patterns: list[dict[str, Any]] = []
59
+ profile: dict[str, Any] = {}
60
+
61
+ if context_file.exists():
62
+ try:
63
+ context_data = json.loads(context_file.read_text(encoding="utf-8"))
64
+ except (json.JSONDecodeError, OSError):
65
+ context_data = {}
66
+
67
+ if patterns_file.exists():
68
+ try:
69
+ patterns = json.loads(patterns_file.read_text(encoding="utf-8"))
70
+ except (json.JSONDecodeError, OSError):
71
+ patterns = []
72
+
73
+ if profile_file.exists():
74
+ try:
75
+ profile = json.loads(profile_file.read_text(encoding="utf-8"))
76
+ except (json.JSONDecodeError, OSError):
77
+ profile = {}
78
+
79
+ cfg_file = tg_dir / "config" / "torusguard.json"
80
+ if cfg_file.exists():
81
+ try:
82
+ cfg = json.loads(cfg_file.read_text(encoding="utf-8"))
83
+ det = cfg.get("detected_stack", {})
84
+ if not profile.get("stack"):
85
+ lang = det.get("language")
86
+ fw = det.get("framework")
87
+ if lang and lang != "Unknown":
88
+ stk = [lang]
89
+ if fw and fw != "None":
90
+ stk.append(fw)
91
+ profile["stack"] = stk
92
+ except (json.JSONDecodeError, OSError):
93
+ pass
94
+
95
+ # Dynamic fallback to stack_detect if stack is still unknown
96
+ if not profile.get("stack"):
97
+ try:
98
+ import stack_detect # type: ignore
99
+ det_stk = stack_detect.detect_stack(root_dir)
100
+ lang = det_stk.get("language")
101
+ fw = det_stk.get("framework")
102
+ if lang and lang != "Unknown":
103
+ stk = [lang]
104
+ if fw and fw != "None":
105
+ stk.append(fw)
106
+ profile["stack"] = stk
107
+ except Exception:
108
+ pass
109
+
110
+ return {
111
+ "context": context_data,
112
+ "patterns": patterns,
113
+ "profile": profile
114
+ }
115
+
116
+
117
+ def format_dense_security_rules(sec_data: dict[str, Any]) -> str:
118
+ """
119
+ Format active security memory into a dense, token-budgeted markdown block (<=400 tokens),
120
+ dynamically tailored to the detected language and framework family.
121
+ """
122
+ patterns = sec_data.get("patterns", [])
123
+ profile = sec_data.get("profile", {})
124
+ stack = profile.get("stack", [])
125
+
126
+ golden_recipes: list[dict[str, Any]] = []
127
+ regressions: list[dict[str, Any]] = []
128
+ common_vulns: list[dict[str, Any]] = []
129
+
130
+ for p in patterns:
131
+ ptype = p.get("pattern_type")
132
+ if ptype == "golden_fix_recipe":
133
+ golden_recipes.append(p)
134
+ elif ptype == "regression_watch":
135
+ regressions.append(p)
136
+ elif ptype in ("common_vulnerability", "recurring_fix"):
137
+ common_vulns.append(p)
138
+
139
+ lines: list[str] = [
140
+ START_MARKER,
141
+ "## 🛡️ TorusGuard Active Project Security Invariants",
142
+ "> Auto-synced from `.torusguard/memory/`. Adhere to these guardrails in all generated code.",
143
+ ""
144
+ ]
145
+
146
+ if stack:
147
+ lines.append(f"**Detected Stack:** {', '.join(stack)}")
148
+ lines.append("")
149
+
150
+ # Core Behavioral Invariants (Stack-Adaptive & Token-Optimized)
151
+ stack_lower = " ".join(stack).lower()
152
+ sql_guards = []
153
+ bypass_guards = []
154
+
155
+ if "go" in stack_lower:
156
+ sql_guards.append("`db.Query(..., args)` / GORM")
157
+ bypass_guards.append("`InsecureSkipVerify: true`")
158
+ if "rust" in stack_lower:
159
+ sql_guards.append("`sqlx::query!` / Diesel")
160
+ bypass_guards.append("`unsafe { ... }`")
161
+ if "java" in stack_lower or "kotlin" in stack_lower:
162
+ sql_guards.append("JPA / PreparedStatement")
163
+ bypass_guards.append("`csrf().disable()`")
164
+ if "c#" in stack_lower or "dotnet" in stack_lower:
165
+ sql_guards.append("EF Core LINQ / `FromSqlInterpolated`")
166
+ bypass_guards.append("`[AllowAnonymous]`")
167
+ if "php" in stack_lower:
168
+ sql_guards.append("PDO prepared statements / Eloquent")
169
+ bypass_guards.append("`CURLOPT_SSL_VERIFYPEER => false`")
170
+ if "typescript" in stack_lower or "javascript" in stack_lower or "next" in stack_lower or "express" in stack_lower:
171
+ sql_guards.append("Prisma / Drizzle binders")
172
+ bypass_guards.append("`rejectUnauthorized: false`")
173
+
174
+ if not sql_guards:
175
+ sql_rule = "1. **Zero Raw SQL / String Formats:** Always use parameterized queries (e.g. `%s`, `?`, or ORM bound params)."
176
+ else:
177
+ sql_rule = f"1. **Zero Raw SQL:** Always use parameterized queries ({' / '.join(sql_guards[:2])})."
178
+
179
+ if not bypass_guards:
180
+ bypass_rule = "4. **No Security Bypasses:** Never insert `# nosec`, `verify=False`, `@csrf_exempt`, or `bypass_auth=True`."
181
+ else:
182
+ bypass_rule = f"4. **No Security Bypasses:** Never insert `# nosec`, `verify=False`, or {', '.join(bypass_guards[:2])}."
183
+
184
+ lines.extend([
185
+ "### Universal Guardrails",
186
+ sql_rule,
187
+ "2. **Tenant Isolation:** Enforce tenant/owner filtering on every database lookup (never query without `tenant_id` or owner scope).",
188
+ "3. **Ponytail Bound Rule:** Remediation patches must be minimal (<= 35 additions, <= 25 deletions).",
189
+ bypass_rule,
190
+ ""
191
+ ])
192
+
193
+ # Golden Fix Recipes (Top 2 to preserve tokens)
194
+ if golden_recipes:
195
+ lines.append("### Verified Golden Fix Patterns")
196
+ for rec in golden_recipes[:2]:
197
+ rule = rec.get("rule_id", "TG-SEC")
198
+ desc = rec.get("description", "Verified AST fix")
199
+ r_data = rec.get("recipe_data", {})
200
+ before = r_data.get("before_snippet", "")
201
+ after = r_data.get("after_snippet", "")
202
+ lines.append(f"- **{rule}** ({desc}):")
203
+ if before and after:
204
+ lines.append(f" - ❌ Avoid: `{before[:70]}`")
205
+ lines.append(f" - ✅ Prefer: `{after[:70]}`")
206
+ lines.append("")
207
+
208
+ # High-Risk Regression Watches
209
+ if regressions:
210
+ lines.append("### Active Regression Watches (Do Not Re-Introduce)")
211
+ for reg in regressions[:3]:
212
+ rule = reg.get("rule_id", "TG-REG")
213
+ files = ", ".join(reg.get("affected_files", [])[:2]) or "tracked files"
214
+ lines.append(f"- **{rule}**: Recurrence risk in `{files}`.")
215
+ lines.append("")
216
+
217
+ lines.append(END_MARKER)
218
+ return "\n".join(lines)
219
+
220
+
221
+ def inject_rules_into_file(file_path: Path, rules_block: str) -> bool:
222
+ """
223
+ Non-destructively inject or replace the TorusGuard block in a target rules file.
224
+ Creates parent directories if necessary.
225
+ """
226
+ file_path.parent.mkdir(parents=True, exist_ok=True)
227
+ existing_content = ""
228
+
229
+ if file_path.exists():
230
+ try:
231
+ existing_content = file_path.read_text(encoding="utf-8")
232
+ except OSError:
233
+ existing_content = ""
234
+
235
+ pattern = re.compile(
236
+ rf"{re.escape(START_MARKER)}.*?{re.escape(END_MARKER)}",
237
+ re.DOTALL
238
+ )
239
+
240
+ if pattern.search(existing_content):
241
+ updated_content = pattern.sub(rules_block, existing_content)
242
+ else:
243
+ if existing_content.strip():
244
+ updated_content = existing_content.rstrip() + "\n\n" + rules_block + "\n"
245
+ else:
246
+ updated_content = rules_block + "\n"
247
+
248
+ try:
249
+ file_path.write_text(updated_content, encoding="utf-8")
250
+ return True
251
+ except OSError as err:
252
+ print(f"Error writing rules to {file_path}: {err}", file=sys.stderr)
253
+ return False
254
+
255
+
256
+ def sync_rules(
257
+ target_format: str = "all",
258
+ root_dir: str | Path | None = None
259
+ ) -> dict[str, Any]:
260
+ """
261
+ Synchronize project security invariants into target IDE rules files.
262
+ Supported targets: 'cursor', 'claude', 'agent', 'windsurf', 'all'.
263
+ """
264
+ root = Path(root_dir or find_project_root()).resolve()
265
+ sec_data = load_security_context(root)
266
+ rules_block = format_dense_security_rules(sec_data)
267
+
268
+ target_files: dict[str, Path] = {
269
+ "cursor": root / ".cursorrules",
270
+ "claude": root / "CLAUDE.md",
271
+ "agent": root / ".agent" / "rules" / "torusguard.md",
272
+ "windsurf": root / ".windsurfrules"
273
+ }
274
+
275
+ synced_targets: list[str] = []
276
+ formats_to_sync = list(target_files.keys()) if target_format == "all" else [target_format]
277
+
278
+ for fmt in formats_to_sync:
279
+ if fmt in target_files:
280
+ target_path = target_files[fmt]
281
+ success = inject_rules_into_file(target_path, rules_block)
282
+ if success:
283
+ synced_targets.append(str(target_path.relative_to(root)))
284
+
285
+ # Estimate token overhead (approx 4 chars/token)
286
+ token_est = max(1, len(rules_block) // 4)
287
+
288
+ return {
289
+ "status": "success" if synced_targets else "skipped",
290
+ "synced_files": synced_targets,
291
+ "token_estimate": token_est,
292
+ "within_budget": token_est <= MAX_PROMPT_TOKENS
293
+ }
294
+
295
+
296
+ def main():
297
+ parser = argparse.ArgumentParser(description="TorusGuard AI IDE Rules Auto-Sync Engine")
298
+ parser.add_argument(
299
+ "--format",
300
+ choices=["all", "cursor", "claude", "agent", "windsurf"],
301
+ default="all",
302
+ help="Target AI IDE rules format to sync"
303
+ )
304
+ parser.add_argument("--root", help="Project root override")
305
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
306
+
307
+ args = parser.parse_args()
308
+ root = Path(args.root).resolve() if args.root else None
309
+
310
+ result = sync_rules(target_format=args.format, root_dir=root)
311
+ if args.json:
312
+ print(json.dumps(result, indent=2))
313
+ else:
314
+ print(f"[SUCCESS] Synced TorusGuard security invariants to {len(result['synced_files'])} file(s):")
315
+ for f in result["synced_files"]:
316
+ print(f" - {f}")
317
+ print(f"Token estimate: ~{result['token_estimate']} tokens (budget <= {MAX_PROMPT_TOKENS})")
318
+
319
+
320
+ if __name__ == "__main__":
321
+ main()