dasa-sradha-kit 5.0.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 (54) hide show
  1. package/.agent/.shared/infinite-memory.md +19 -0
  2. package/.agent/.shared/max-power-core.md +27 -0
  3. package/.agent/ARCHITECTURE.md +104 -0
  4. package/.agent/agents/dasa-dharma.md +21 -0
  5. package/.agent/agents/dasa-dwipa.md +21 -0
  6. package/.agent/agents/dasa-indra.md +21 -0
  7. package/.agent/agents/dasa-kala.md +21 -0
  8. package/.agent/agents/dasa-mpu.md +21 -0
  9. package/.agent/agents/dasa-nala.md +21 -0
  10. package/.agent/agents/dasa-patih.md +21 -0
  11. package/.agent/agents/dasa-rsi.md +25 -0
  12. package/.agent/agents/dasa-sastra.md +21 -0
  13. package/.agent/agents/dasa-widya.md +21 -0
  14. package/.agent/rules/GEMINI.md +183 -0
  15. package/.agent/scripts/api_validator.py +70 -0
  16. package/.agent/scripts/arch_mapper.py +101 -0
  17. package/.agent/scripts/compact_memory.py +68 -0
  18. package/.agent/scripts/complexity_scorer.py +82 -0
  19. package/.agent/scripts/context_mapper.py +91 -0
  20. package/.agent/scripts/design_engine.py +108 -0
  21. package/.agent/scripts/design_memory_sync.py +87 -0
  22. package/.agent/scripts/lint_fixer.py +79 -0
  23. package/.agent/scripts/qa_gate.py +84 -0
  24. package/.agent/scripts/security_scan.py +82 -0
  25. package/.agent/scripts/semantic-scan.py +56 -0
  26. package/.agent/scripts/skill_search.py +91 -0
  27. package/.agent/scripts/status_parser.py +78 -0
  28. package/.agent/scripts/test_runner.py +98 -0
  29. package/.agent/scripts/validate_env.py +71 -0
  30. package/.agent/scripts/web_scraper.py +86 -0
  31. package/.agent/scripts/workspace-mapper.py +58 -0
  32. package/.agent/skills/.gitkeep +0 -0
  33. package/.agent/workflows/dasa-api.md +42 -0
  34. package/.agent/workflows/dasa-assimilate.md +44 -0
  35. package/.agent/workflows/dasa-commit.md +46 -0
  36. package/.agent/workflows/dasa-docs.md +46 -0
  37. package/.agent/workflows/dasa-e2e.md +41 -0
  38. package/.agent/workflows/dasa-feature.md +46 -0
  39. package/.agent/workflows/dasa-fix.md +37 -0
  40. package/.agent/workflows/dasa-init.md +29 -0
  41. package/.agent/workflows/dasa-plan.md +56 -0
  42. package/.agent/workflows/dasa-pr.md +47 -0
  43. package/.agent/workflows/dasa-refactor.md +44 -0
  44. package/.agent/workflows/dasa-seed.md +44 -0
  45. package/.agent/workflows/dasa-start-work.md +51 -0
  46. package/.agent/workflows/dasa-status.md +58 -0
  47. package/.agent/workflows/dasa-sync.md +39 -0
  48. package/.agent/workflows/dasa-uninstall.md +30 -0
  49. package/CHANGELOG.md +94 -0
  50. package/LICENSE +21 -0
  51. package/README.md +135 -0
  52. package/bin/cli.js +218 -0
  53. package/bin/dasa-cli.js +100 -0
  54. package/package.json +37 -0
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dasa Indra: The native QA Gate (qa_gate.py)
4
+ Assimilates ~800 patterns from `engineering-failures-bible`.
5
+ Provides native python text scanning against common language-specific pitfalls
6
+ before a task can be marked complete.
7
+ """
8
+
9
+ import sys
10
+ import os
11
+ import re
12
+ import argparse
13
+ from pathlib import Path
14
+
15
+ # Embedded Failure Patterns (TOON Heuristics)
16
+ FAILURES = {
17
+ "01_Memory": [
18
+ (r"new\s+[a-zA-Z]+\(.*\)\s*;(?!.*\bdelete\b)", "Unmatched 'new' allocation without 'delete' (C++/Leaks)"),
19
+ (r"fmt\.Sprintf\(\s*\"%s\"", "String concatenation in loops instead of strings.Builder (Go/Memory)"),
20
+ (r"\.collect::\<Vec<\w+>\>\(\)", "Unbounded heavy .collect() instead of iterators (Rust/Memory)")
21
+ ],
22
+ "02_Concurrency": [
23
+ (r"sync\.Mutex.*defer\s+.*Unlock", "Missing careful Lock bounds via defer (Go/Deadlock risk)"),
24
+ (r"std::sync::Mutex.*\.unwrap\(\)", "Poisoning panic risk on Mutex.unwrap() (Rust/Concurrency)"),
25
+ (r"Promise\.all\(.*\.(map|forEach)\(async", "Unbounded concurrent Promise execution. Use Promise.allSettled or batching (Node/EventLoop)")
26
+ ],
27
+ "03_Security": [
28
+ (r"SELECT\s+.*\s+FROM\s+.*\s+WHERE\s+.*=\s*\S+\s*\+", "Raw string concatenation in SQL queries (SQL Injection)"),
29
+ (r"eval\(", "Use of eval() detected (RCE vulnerability)"),
30
+ (r"dangerouslySetInnerHTML", "Potential XSS detected in React/Next.js component")
31
+ ]
32
+ }
33
+
34
+ def scan_file(filepath: Path) -> list:
35
+ issues = []
36
+ try:
37
+ content = filepath.read_text(encoding="utf-8")
38
+ lines = content.splitlines()
39
+ for idx, line in enumerate(lines, 1):
40
+ for domain, patterns in FAILURES.items():
41
+ for regex, desc in patterns:
42
+ if re.search(regex, line):
43
+ issues.append(f"[FAIL] {domain} | {filepath.name}:{idx} -> {desc}")
44
+ except Exception as e:
45
+ print(f"Skipping {filepath} ({e})")
46
+ return issues
47
+
48
+ def main():
49
+ parser = argparse.ArgumentParser(description="Dasa Indra QA Gate Scanner")
50
+ parser.add_argument("target", help="Directory or file to scan")
51
+ args = parser.parse_args()
52
+
53
+ target_path = Path(args.target)
54
+ if not target_path.exists():
55
+ print(f"Error: Target {target_path} does not exist.")
56
+ sys.exit(1)
57
+
58
+ print(f"🕵️‍♂️ Dasa Indra: Initiating Engineering Failure Scan on {target_path}...")
59
+
60
+ total_issues = []
61
+ if target_path.is_file():
62
+ total_issues.extend(scan_file(target_path))
63
+ else:
64
+ for root, _, files in os.walk(target_path):
65
+ if ".git" in root or "node_modules" in root or "target" in root:
66
+ continue
67
+ for file in files:
68
+ ext = Path(file).suffix
69
+ if ext in [".js", ".ts", ".go", ".rs", ".java", ".php", ".py", ".cpp"]:
70
+ full_path = Path(root) / file
71
+ total_issues.extend(scan_file(full_path))
72
+
73
+ if total_issues:
74
+ print("\n❌ ENGINEERING FAILURES DETECTED:")
75
+ for issue in total_issues:
76
+ print(f" {issue}")
77
+ print("\n>> Dasa Nala is BLOCKED from completing this task. Fix the issues first.")
78
+ sys.exit(1)
79
+ else:
80
+ print("\n✅ QA Gate Passed. No critical engineering failures detected.")
81
+ sys.exit(0)
82
+
83
+ if __name__ == "__main__":
84
+ main()
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dasa Dharma: Secret Guardian (security_scan.py)
4
+ Scans git diffs or specific files for leaked API keys, tokens, and secrets.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import subprocess
10
+ import re
11
+
12
+ SECRET_PATTERNS = {
13
+ "AWS Access Key": r"AKIA[0-9A-Z]{16}",
14
+ "Stripe API Key": r"sk_(test|live)_[0-9a-zA-Z]{24}",
15
+ "Google API Key": r"AIza[0-9A-Za-z-_]{35}",
16
+ "Generic Bearer/Token": r"(?i)(bearer|token|api_key|secret)['\"\s:=]+[A-Za-z0-9\-_]{20,}",
17
+ "Private Key": r"-----BEGIN (RSA|OPENSSH|DSA|EC|PGP) PRIVATE KEY-----"
18
+ }
19
+
20
+ def get_git_diff():
21
+ """Get the current staged and unstaged git changes."""
22
+ try:
23
+ # Get staged changes
24
+ staged = subprocess.check_output(["git", "diff", "--cached"]).decode('utf-8')
25
+ # Get unstaged changes
26
+ unstaged = subprocess.check_output(["git", "diff"]).decode('utf-8')
27
+ return staged + "\n" + unstaged
28
+ except subprocess.CalledProcessError:
29
+ print("🟡 [Dharma Guardian] Not a git repository or no commits yet. Skipping diff scan.")
30
+ return ""
31
+ except Exception as e:
32
+ print(f"🔴 [Dharma Guardian] Error executing git diff: {e}")
33
+ return ""
34
+
35
+ def scan_diff(diff_text):
36
+ """Scan the diff text for secret patterns."""
37
+ leaks = []
38
+
39
+ # Only scan added/modified lines in diff (+ but not +++)
40
+ diff_lines = [line for line in diff_text.split('\n') if line.startswith('+') and not line.startswith('+++')]
41
+
42
+ for line in diff_lines:
43
+ for name, pattern in SECRET_PATTERNS.items():
44
+ if re.search(pattern, line):
45
+ leaks.append((name, line.strip()))
46
+
47
+ return leaks
48
+
49
+ def main():
50
+ print("🛡️ [Dasa Dharma] Initializing Secret Guardian Scan...")
51
+
52
+ # 1. Scan .env files (preventing accidental .env commits)
53
+ try:
54
+ git_status = subprocess.check_output(["git", "status", "-s"]).decode('utf-8')
55
+ status_lines = git_status.split('\n')
56
+ for line in status_lines:
57
+ if '.env' in line and not '.example' in line:
58
+ print(f"🔴 [Dharma Guardian] FATAL: You are attempting to commit a raw .env file!\nLine: {line.strip()}")
59
+ sys.exit(1)
60
+ except subprocess.CalledProcessError:
61
+ pass # Not a git repo
62
+
63
+ # 2. Scan Git Diff for hardcoded secrets
64
+ diff_text = get_git_diff()
65
+ if not diff_text:
66
+ print("🟢 [Dharma Guardian] No changes to scan. Pass.")
67
+ sys.exit(0)
68
+
69
+ leaks = scan_diff(diff_text)
70
+
71
+ if leaks:
72
+ print("\n🔴 [Dharma Guardian] FATAL: Potential Secret Leaks Detected in `git diff`:")
73
+ for name, line in leaks:
74
+ print(f" - [{name}] Found in line: {line[:80]}...")
75
+ print("\nHALTING COMMIT. Remove hardcoded secrets and use environment variables.")
76
+ sys.exit(1)
77
+
78
+ print("🟢 [Dharma Guardian] Security Audit Passed. No obvious secrets leaked.")
79
+ sys.exit(0)
80
+
81
+ if __name__ == "__main__":
82
+ main()
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ semantic-scan.py — Cross-platform semantic search wrapper (uses osgrep)
4
+ Persona: Dasa Dwipa (The Scout)
5
+
6
+ Usage:
7
+ python .agent/scripts/semantic-scan.py "JWT authentication middleware"
8
+ python .agent/scripts/semantic-scan.py "where is the login form handled"
9
+ """
10
+
11
+ import sys
12
+ import subprocess
13
+ import shutil
14
+ import os
15
+
16
+ def check_osgrep():
17
+ return shutil.which("osgrep") is not None
18
+
19
+ def run_semantic_search(query: str) -> int:
20
+ if not check_osgrep():
21
+ print("[!] osgrep is not installed. Install it with: npm install -g osgrep")
22
+ print(f"[!] Falling back to grep for: {query}")
23
+ result = subprocess.run(
24
+ ["grep", "-r", "--include=*.ts", "--include=*.py", "--include=*.js",
25
+ "--include=*.php", "-n", query, "."],
26
+ capture_output=True, text=True
27
+ )
28
+ if result.stdout:
29
+ print(result.stdout[:3000])
30
+ return result.returncode
31
+
32
+ # osgrep available
33
+ try:
34
+ result = subprocess.run(
35
+ ["osgrep", "search", query],
36
+ capture_output=True, text=True, timeout=30
37
+ )
38
+ print(result.stdout)
39
+ if result.stderr:
40
+ print(result.stderr, file=sys.stderr)
41
+ return result.returncode
42
+ except subprocess.TimeoutExpired:
43
+ print("[x] osgrep search timed out after 30s")
44
+ return 1
45
+ except FileNotFoundError:
46
+ print("[x] osgrep not found in PATH")
47
+ return 1
48
+
49
+ if __name__ == "__main__":
50
+ if len(sys.argv) < 2:
51
+ print("Usage: python semantic-scan.py <query>")
52
+ sys.exit(1)
53
+
54
+ query = " ".join(sys.argv[1:])
55
+ print(f"[+] Semantic search: {query}")
56
+ sys.exit(run_semantic_search(query))
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dasa Dwipa: The Local Skill Indexer (skill_search.py)
4
+ A zero-dependency semantic search to find skills locally without any cloud services.
5
+ Scans both `.agent/skills/` and `~/.gemini/antigravity/skills/`.
6
+ """
7
+
8
+ import sys
9
+ import os
10
+ import re
11
+ from pathlib import Path
12
+
13
+ def extract_yaml_frontmatter(content):
14
+ match = re.search(r"^---\n(.*?)\n---", content, re.DOTALL)
15
+ if not match:
16
+ return {}
17
+
18
+ yaml_text = match.group(1)
19
+ metadata = {}
20
+ for line in yaml_text.split("\n"):
21
+ if ":" in line:
22
+ key, val = line.split(":", 1)
23
+ metadata[key.strip()] = val.strip().strip("'\"")
24
+ return metadata
25
+
26
+ def parse_skills_in_directory(dir_path: Path):
27
+ skills = []
28
+ if not dir_path.exists() or not dir_path.is_dir():
29
+ return skills
30
+
31
+ for root, _, files in os.walk(dir_path):
32
+ for file in files:
33
+ if file == "SKILL.md":
34
+ skill_path = Path(root) / file
35
+ try:
36
+ content = skill_path.read_text(encoding="utf-8")
37
+ meta = extract_yaml_frontmatter(content)
38
+ if "name" in meta and "description" in meta:
39
+ skills.append({
40
+ "name": meta["name"],
41
+ "description": meta["description"],
42
+ "path": str(skill_path.parent)
43
+ })
44
+ except Exception:
45
+ pass
46
+ return skills
47
+
48
+ def score_skill(skill, query_words):
49
+ text_corpus = (skill["name"] + " " + skill["description"]).lower()
50
+ score = 0
51
+ for word in query_words:
52
+ if word in text_corpus:
53
+ score += 1
54
+ return score
55
+
56
+ def main():
57
+ if len(sys.argv) < 2:
58
+ print("Usage: skill_search.py <query>")
59
+ sys.exit(1)
60
+
61
+ query = " ".join(sys.argv[1:]).lower()
62
+ query_words = set(re.findall(r'\w+', query))
63
+
64
+ global_dir = Path.home() / ".gemini" / "antigravity" / "skills"
65
+ local_dir = Path(os.getcwd()) / ".agent" / "skills"
66
+
67
+ all_skills = parse_skills_in_directory(global_dir) + parse_skills_in_directory(local_dir)
68
+
69
+ if not all_skills:
70
+ print("No skills found on the local machine.")
71
+ sys.exit(0)
72
+
73
+ for skill in all_skills:
74
+ skill["score"] = score_skill(skill, query_words)
75
+
76
+ ranked_skills = sorted(all_skills, key=lambda x: x["score"], reverse=True)
77
+
78
+ print(f"🔍 Dasa Dwipa: Ranked Skills for '{query}'\n")
79
+ found_any = False
80
+ for skill in ranked_skills[:3]:
81
+ if skill["score"] > 0:
82
+ found_any = True
83
+ print(f"✨ {skill['name']} (Score: {skill['score']})")
84
+ print(f" Desc: {skill['description']}")
85
+ print(f" Path: {skill['path']}\n")
86
+
87
+ if not found_any:
88
+ print("No relevant skills matched your query. Try broadening your terms.")
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dasa Kala: The Reporter (status_parser.py)
4
+ Merges data from task.md and git diff --stat to output a 3-line JSON summary.
5
+ Prevents Kala from wasting context reading entire task checklists.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import subprocess
11
+ import json
12
+
13
+ def get_task_stats():
14
+ """Read task.md and count checkboxes to determine progress."""
15
+ task_path = ".artifacts/task.md"
16
+ if not os.path.exists(task_path):
17
+ # Fallback to older location
18
+ task_path = ".agent/task.toon"
19
+ if not os.path.exists(task_path):
20
+ return {"total": 0, "completed": 0, "in_progress": 0}
21
+
22
+ total = 0
23
+ completed = 0
24
+ in_progress = 0
25
+
26
+ with open(task_path, "r") as f:
27
+ for line in f.readlines():
28
+ line = line.strip()
29
+ if line.startswith("- [ ]"):
30
+ total += 1
31
+ elif line.startswith("- [x]") or line.startswith("- [X]"):
32
+ total += 1
33
+ completed += 1
34
+ elif line.startswith("- [/]"):
35
+ total += 1
36
+ in_progress += 1
37
+
38
+ return {"total": total, "completed": completed, "in_progress": in_progress}
39
+
40
+ def get_git_stats():
41
+ """Get high-level git diff stats without the actual diff content."""
42
+ try:
43
+ # Get purely the stat line e.g., "3 files changed, 50 insertions(+), 10 deletions(-)"
44
+ stat = subprocess.check_output(["git", "diff", "--shortstat"]).decode('utf-8').strip()
45
+ if not stat:
46
+ stat = subprocess.check_output(["git", "diff", "--cached", "--shortstat"]).decode('utf-8').strip()
47
+ return stat if stat else "Working tree clean"
48
+ except Exception:
49
+ return "Unknown git status"
50
+
51
+ def main():
52
+ print("🛡️ [Dasa Kala] Initializing Project Status Reporter...")
53
+
54
+ tasks = get_task_stats()
55
+ git_stat = get_git_stats()
56
+
57
+ pct = 0
58
+ if tasks["total"] > 0:
59
+ pct = round((tasks["completed"] / tasks["total"]) * 100)
60
+
61
+ summary = {
62
+ "progress_percent": pct,
63
+ "tasks": f"{tasks['completed']}/{tasks['total']} ({tasks['in_progress']} active)",
64
+ "uncommitted_code": git_stat
65
+ }
66
+
67
+ # Write to a tiny file Kala can instantly parse
68
+ os.makedirs(".artifacts", exist_ok=True)
69
+ out_path = ".artifacts/status_summary.json"
70
+
71
+ with open(out_path, "w") as f:
72
+ json.dump(summary, f, indent=2)
73
+
74
+ print(f"🟢 [Kala Reporter] Status parsed. JSON Summary generated at {out_path}.")
75
+ sys.exit(0)
76
+
77
+ if __name__ == "__main__":
78
+ main()
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dasa Indra: Universal Test Watcher (test_runner.py)
4
+ A lightweight wrapper that detects the framework, runs tests, and compresses
5
+ massive test console outputs into a concise TOON summary to save tokens.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import subprocess
11
+ import json
12
+ from datetime import datetime
13
+
14
+ def detect_framework():
15
+ """Detect the testing framework based on workspace files."""
16
+ if os.path.exists("package.json"):
17
+ with open("package.json", "r") as f:
18
+ content = f.read()
19
+ if '"jest"' in content:
20
+ return "npm test", "Jest"
21
+ if '"vitest"' in content:
22
+ return "npm run test", "Vitest"
23
+
24
+ if os.path.exists("pytest.ini") or os.path.exists("setup.py") or os.path.exists("requirements.txt"):
25
+ return "pytest", "PyTest"
26
+
27
+ if os.path.exists("go.mod"):
28
+ return "go test ./...", "Go Test"
29
+
30
+ return None, None
31
+
32
+ def generate_toon_report(framework, output, code):
33
+ """Compress the raw test output into a clean TOON structure."""
34
+ lines = output.split('\n')
35
+ errors = [line for line in lines if 'FAIL' in line or 'Error' in line or 'ERR!' in line]
36
+
37
+ # We only take the last 50 lines to prevent token bloat
38
+ tail = "\n".join(lines[-50:])
39
+
40
+ status = "SUCCESS" if code == 0 else "FAILED"
41
+
42
+ report = f"""# Test Execution Report
43
+ Framework: {framework}
44
+ Status: {status}
45
+ Timestamp: {datetime.now().isoformat()}
46
+
47
+ ## Summary Tail
48
+ ```text
49
+ {tail}
50
+ ```
51
+ """
52
+ if errors and code != 0:
53
+ report += "\n## Detected Failures\n```text\n" + "\n".join(errors[:20]) + "\n```\n"
54
+
55
+ return report
56
+
57
+ def main():
58
+ print("🛡️ [Dasa Indra] Initializing Universal Test Watcher...")
59
+
60
+ cmd, framework = detect_framework()
61
+
62
+ if not cmd:
63
+ print("🟡 [Indra Watcher] No recognized testing framework found in root. Skipping tests.")
64
+ sys.exit(0)
65
+
66
+ print(f"⚡ [Indra Watcher] Detected {framework}. Running: `{cmd}`")
67
+
68
+ try:
69
+ # Run tests and capture both stdout and stderr
70
+ result = subprocess.run(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
71
+ output = result.stdout
72
+ code = result.returncode
73
+ except FileNotFoundError:
74
+ print(f"🔴 [Indra Watcher] Testing executable for '{cmd}' not found.")
75
+ sys.exit(1)
76
+ except Exception as e:
77
+ print(f"🔴 [Indra Watcher] Unexpected error executing tests: {e}")
78
+ sys.exit(1)
79
+
80
+ # Generate the highly compressed TOON output
81
+ report = generate_toon_report(framework, output, code)
82
+
83
+ # We write this compressed report to a file so the AI can read it efficiently
84
+ # instead of bloating the chat context with 10,000 lines of Jest output.
85
+ os.makedirs(".artifacts", exist_ok=True)
86
+ report_path = ".artifacts/test_report.toon"
87
+ with open(report_path, "w") as f:
88
+ f.write(report)
89
+
90
+ if code != 0:
91
+ print(f"🔴 [Indra Watcher] Tests FAILED. Details written to {report_path}")
92
+ sys.exit(1)
93
+ else:
94
+ print(f"🟢 [Indra Watcher] All tests passed! Summary written to {report_path}")
95
+ sys.exit(0)
96
+
97
+ if __name__ == "__main__":
98
+ main()
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dasa Patih: Environment Gatekeeper (validate_env.py)
4
+ Validates the local environment against dasa.config.toon requirements before execution.
5
+ Ensures Python, Node, and Go (if required) are installed.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import subprocess
11
+ import json
12
+
13
+ def check_command(cmd):
14
+ """Check if a command exists in the system PATH."""
15
+ try:
16
+ subprocess.run([cmd, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
17
+ return True
18
+ except (subprocess.CalledProcessError, FileNotFoundError):
19
+ return False
20
+
21
+ def check_env_file():
22
+ """Verify standard .env files exist if a .env.example is present."""
23
+ if os.path.exists(".env.example") and not os.path.exists(".env"):
24
+ print("🔴 [Patih Gatekeeper] WARNING: .env.example exists, but .env is missing. The agent might fail without environment variables.")
25
+ return False
26
+ return True
27
+
28
+ def parse_config():
29
+ """Parse dasa.config.toon for workspace paths if it exists."""
30
+ config_path = ".agent/dasa.config.toon"
31
+ if not os.path.exists(config_path):
32
+ return {}
33
+
34
+ try:
35
+ with open(config_path, "r") as f:
36
+ return json.load(f)
37
+ except Exception as e:
38
+ print(f"🔴 [Patih Gatekeeper] ERROR: Could not parse {config_path}: {e}")
39
+ return {}
40
+
41
+ def main():
42
+ print("🛡️ [Dasa Patih] Initializing Environment Gatekeeper...")
43
+
44
+ config = parse_config()
45
+ workspaces = config.get("workspaces", {"root": "./"})
46
+
47
+ # 1. Check Workspaces
48
+ for name, path in workspaces.items():
49
+ if not os.path.exists(path):
50
+ print(f"🔴 [Patih Gatekeeper] ERROR: Configured workspace '{name}' path '{path}' does not exist.")
51
+ sys.exit(1)
52
+
53
+ # 2. Check Dependencies
54
+ deps = {
55
+ "node": check_command("node"),
56
+ "npm": check_command("npm"),
57
+ "python": check_command("python3")
58
+ }
59
+
60
+ missing = [cmd for cmd, exists in deps.items() if not exists]
61
+ if missing:
62
+ print(f"🔴 [Patih Gatekeeper] WARNING: Missing standard runtime environments: {', '.join(missing)}")
63
+
64
+ # 3. Check ENV
65
+ check_env_file()
66
+
67
+ print("🟢 [Patih Gatekeeper] Environment Validation Passed. Ready for execution.")
68
+ sys.exit(0)
69
+
70
+ if __name__ == "__main__":
71
+ main()
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dasa Widya: The Extractor (web_scraper.py)
4
+ Natively fetches URL content and strips all HTML, inline CSS, and JavaScript.
5
+ Outputs pure markdown text to prevent massive token waste when Widya researchers.
6
+ Zero extra dependencies required.
7
+ """
8
+
9
+ import urllib.request
10
+ import re
11
+ import sys
12
+
13
+ def fetch_html(url):
14
+ """Fetch raw HTML from a URL."""
15
+ try:
16
+ req = urllib.request.Request(
17
+ url,
18
+ data=None,
19
+ headers={
20
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
21
+ }
22
+ )
23
+ with urllib.request.urlopen(req, timeout=10) as response:
24
+ return response.read().decode('utf-8', errors='ignore')
25
+ except Exception as e:
26
+ print(f"🔴 [Widya Extractor] Error fetching {url}: {e}")
27
+ return None
28
+
29
+ def extract_text(html):
30
+ """Strip all HTML tags and noise, returning clean text."""
31
+ if not html:
32
+ return ""
33
+
34
+ # 1. Remove script and style blocks completely
35
+ text = re.sub(r'<script.*?</script>', '', html, flags=re.IGNORECASE | re.DOTALL)
36
+ text = re.sub(r'<style.*?</style>', '', text, flags=re.IGNORECASE | re.DOTALL)
37
+
38
+ # 2. Replace common block elements with newlines for formatting
39
+ text = re.sub(r'</?(p|div|br|h[1-6]|li|tr|table|ul|ol|header|footer|nav)[^>]*>', '\n', text, flags=re.IGNORECASE)
40
+
41
+ # 3. Strip all remaining HTML tags
42
+ text = re.sub(r'<[^>]+>', '', text)
43
+
44
+ # 4. Clean up whitespace and empty lines
45
+ lines = [line.strip() for line in text.split('\n')]
46
+ cleaned_lines = [line for line in lines if line]
47
+
48
+ return '\n\n'.join(cleaned_lines)
49
+
50
+ def main():
51
+ if len(sys.argv) < 2:
52
+ print("Usage: python3 web_scraper.py <URL>")
53
+ sys.exit(1)
54
+
55
+ url = sys.argv[1]
56
+
57
+ # Basic validation
58
+ if not url.startswith('http'):
59
+ url = 'https://' + url
60
+
61
+ print(f"🛡️ [Dasa Widya] Extracting clean text from: {url}")
62
+
63
+ html = fetch_html(url)
64
+ if not html:
65
+ sys.exit(1)
66
+
67
+ clean_text = extract_text(html)
68
+
69
+ # Ensure artifacts directory exists
70
+ import os
71
+ os.makedirs(".artifacts", exist_ok=True)
72
+ out_path = ".artifacts/research_extraction.toon"
73
+
74
+ with open(out_path, "w") as f:
75
+ # Truncate to roughly 15,000 chars to ensure we don't blow out context
76
+ content = clean_text[:15000]
77
+ if len(clean_text) > 15000:
78
+ content += "\n\n... [CONTENT TRUNCATED FOR TOKEN SAFETY] ..."
79
+ f.write(content)
80
+
81
+ print(f"🟢 [Widya Extractor] Successfully stripped HTML noise.")
82
+ print(f"Clean markdown saved to {out_path} ({len(clean_text)} chars).")
83
+ sys.exit(0)
84
+
85
+ if __name__ == "__main__":
86
+ main()