create-snowline-agents 1.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.
- package/AGENTS_TEMPLATE.md +116 -0
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/auto_scaffolder/SKILL.md +23 -0
- package/auto_scaffolder/scaffolder.py +115 -0
- package/bin/install.js +71 -0
- package/clean_sweeper/SKILL.md +23 -0
- package/clean_sweeper/sweeper.py +98 -0
- package/context_mapper/SKILL.md +21 -0
- package/context_mapper/context_mapper.py +84 -0
- package/crash_decoder/SKILL.md +23 -0
- package/crash_decoder/decoder.py +66 -0
- package/db_extractor/SKILL.md +32 -0
- package/db_extractor/scripts/extractor.py +138 -0
- package/deep_analyzer/SKILL.md +21 -0
- package/deep_analyzer/analyzer.py +97 -0
- package/import_fixer/SKILL.md +24 -0
- package/import_fixer/fixer.py +111 -0
- package/install.ps1 +68 -0
- package/install.sh +58 -0
- package/package.json +31 -0
- package/plan_tracker/PLAN_TEMPLATE.md +20 -0
- package/project_guardian/SKILL.md +26 -0
- package/project_guardian/guardian.py +206 -0
- package/run_all.py +115 -0
- package/selective_reader/SKILL.md +27 -0
- package/selective_reader/reader.py +67 -0
- package/smart_replace/SKILL.md +35 -0
- package/smart_replace/replace_text.py +113 -0
- package/smart_search/SKILL.md +27 -0
- package/smart_search/code_finder.py +103 -0
- package/smart_tree/SKILL.md +33 -0
- package/smart_tree/scripts/tree_viewer.py +72 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
# Force UTF-8 encoding for Windows terminal
|
|
9
|
+
if sys.stdout.encoding.lower() != 'utf-8':
|
|
10
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
11
|
+
|
|
12
|
+
DEFAULT_EXCLUDES = {'.git', 'node_modules', '.history', 'vendor', 'dist', 'build', 'quarantine', '.backup_replace', '.agents'}
|
|
13
|
+
MAX_FILE_SIZE = 500 * 1024 # 500 KB
|
|
14
|
+
|
|
15
|
+
def find_project_root(start_path):
|
|
16
|
+
current = os.path.abspath(start_path)
|
|
17
|
+
while True:
|
|
18
|
+
if os.path.exists(os.path.join(current, 'package.json')) or os.path.exists(os.path.join(current, '.git')):
|
|
19
|
+
return current
|
|
20
|
+
parent = os.path.dirname(current)
|
|
21
|
+
if parent == current:
|
|
22
|
+
return os.path.abspath(start_path)
|
|
23
|
+
current = parent
|
|
24
|
+
|
|
25
|
+
def get_args():
|
|
26
|
+
parser = argparse.ArgumentParser(description="Smart Replace (Pure Python Edition)")
|
|
27
|
+
parser.add_argument("target_dir", help="Target directory (absolute path)")
|
|
28
|
+
parser.add_argument("search_string", help="String or pattern to search for")
|
|
29
|
+
parser.add_argument("replace_string", help="String to replace with")
|
|
30
|
+
parser.add_argument("--ext", help="Comma-separated extensions to include (e.g. .js,.jsx)", default="")
|
|
31
|
+
parser.add_argument("--regex", action="store_true", help="Treat search_string as a regular expression")
|
|
32
|
+
parser.add_argument("--whole-word", action="store_true", help="Match whole words only")
|
|
33
|
+
parser.add_argument("--apply", action="store_true", help="Actually modify the files")
|
|
34
|
+
return parser.parse_args()
|
|
35
|
+
|
|
36
|
+
def backup_file(filepath, backup_dir):
|
|
37
|
+
rel_path = os.path.relpath(filepath, os.getcwd())
|
|
38
|
+
backup_path = os.path.join(backup_dir, rel_path)
|
|
39
|
+
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
|
|
40
|
+
shutil.copy2(filepath, backup_path)
|
|
41
|
+
return backup_path
|
|
42
|
+
|
|
43
|
+
def main():
|
|
44
|
+
args = get_args()
|
|
45
|
+
|
|
46
|
+
if not os.path.exists(args.target_dir):
|
|
47
|
+
print(f"[FAIL] Target directory not found: {args.target_dir}")
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
exts = [e.strip() for e in args.ext.split(',')] if args.ext else []
|
|
51
|
+
|
|
52
|
+
pattern_str = args.search_string
|
|
53
|
+
if not args.regex:
|
|
54
|
+
pattern_str = re.escape(pattern_str)
|
|
55
|
+
if args.whole_word:
|
|
56
|
+
pattern_str = r'\b' + pattern_str + r'\b'
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
regex = re.compile(pattern_str)
|
|
60
|
+
except re.error as e:
|
|
61
|
+
print(f"[FAIL] Invalid regex: {e}")
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
|
|
64
|
+
backup_dir = None
|
|
65
|
+
if args.apply:
|
|
66
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
67
|
+
project_root = find_project_root(args.target_dir)
|
|
68
|
+
backup_dir = os.path.join(project_root, '.backup_replace', timestamp)
|
|
69
|
+
|
|
70
|
+
match_count = 0
|
|
71
|
+
file_count = 0
|
|
72
|
+
scanned_files = 0
|
|
73
|
+
|
|
74
|
+
for root, dirs, files in os.walk(args.target_dir):
|
|
75
|
+
dirs[:] = [d for d in dirs if d not in DEFAULT_EXCLUDES]
|
|
76
|
+
for file in files:
|
|
77
|
+
if exts and not any(file.endswith(e) for e in exts):
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
filepath = os.path.join(root, file)
|
|
81
|
+
if os.path.getsize(filepath) > MAX_FILE_SIZE:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
scanned_files += 1
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
88
|
+
content = f.read()
|
|
89
|
+
except UnicodeDecodeError:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
if regex.search(content):
|
|
93
|
+
file_count += 1
|
|
94
|
+
new_content, count = regex.subn(args.replace_string, content)
|
|
95
|
+
match_count += count
|
|
96
|
+
|
|
97
|
+
rel_path = os.path.relpath(filepath, args.target_dir)
|
|
98
|
+
print(f"[WARN] Found {count} matches in {rel_path}")
|
|
99
|
+
|
|
100
|
+
if args.apply:
|
|
101
|
+
backup_file(filepath, backup_dir)
|
|
102
|
+
with open(filepath, 'w', encoding='utf-8') as f:
|
|
103
|
+
f.write(new_content)
|
|
104
|
+
|
|
105
|
+
print(f"\n[OK] Scan selesai ({scanned_files} file dipindai). Menemukan {match_count} kecocokan di {file_count} file.")
|
|
106
|
+
if not args.apply:
|
|
107
|
+
print("\nš” PROMPT UNTUK AI (Copy-Paste ini):")
|
|
108
|
+
print('"Berdasarkan hasil dry-run di atas, tolong jalankan ulang perintah tersebut dengan menambahkan flag --apply untuk menerapkan perubahannya secara aman."')
|
|
109
|
+
else:
|
|
110
|
+
print(f"\n[INFO] Perubahan telah diterapkan ke {file_count} file. Backup tersimpan di: {backup_dir}")
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
main()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Smart Code Reference Finder
|
|
3
|
+
description: Use this skill when the user asks you to find WHERE a specific function, component, class, or variable is used across a project. This skill uses a Python script to extract the code blocks surrounding the keyword, providing you with full context (e.g. what props are passed, what variables surround it) instead of just single raw lines like standard grep.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Instructions for AI Agent
|
|
7
|
+
|
|
8
|
+
When analyzing a codebase to find references, standard `grep_search` is often insufficient because it only returns the exact line containing the keyword, omitting critical surrounding logic.
|
|
9
|
+
|
|
10
|
+
**When to use this skill:**
|
|
11
|
+
- The user asks to find where a component/function is used.
|
|
12
|
+
- You need to know *how* a specific function is invoked across multiple files (what props/arguments are passed).
|
|
13
|
+
- You are refactoring a core module and need to see all its dependent implementations in context.
|
|
14
|
+
|
|
15
|
+
**How to use:**
|
|
16
|
+
**Command to run:**
|
|
17
|
+
```powershell
|
|
18
|
+
python .agents/skills/smart_search/code_finder.py "<absolute_target_directory>" "<keyword>" --ext "<optional_extensions>"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Parameters:**
|
|
22
|
+
- `<absolute_target_directory>`: The absolute path to the directory you want to scan (e.g., `/path/to/your/src`).
|
|
23
|
+
- `<keyword>`: The exact code string to search for (e.g., `MyComponent`).
|
|
24
|
+
- `--ext`: (Optional) Comma-separated file extensions to limit the search (e.g., `.jsx,.js,.php`).
|
|
25
|
+
|
|
26
|
+
**Expected Output:**
|
|
27
|
+
The script will output beautifully formatted Markdown text directly into standard output. It groups results by file and shows blocks of code (with 5 lines of context above and below the match). The exact line containing the match will be highlighted with a `>` arrow. Read this output carefully to understand the implementation context before deciding on your next coding action.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
if sys.stdout.encoding != 'utf-8':
|
|
6
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
7
|
+
|
|
8
|
+
MAX_FILE_SIZE = 500 * 1024 # 500 KB
|
|
9
|
+
DEFAULT_EXCLUDES = {'node_modules', '.git', 'vendor', 'build', 'dist', '.idea', '.vscode', '.history', '.backup_replace', '.agents'}
|
|
10
|
+
|
|
11
|
+
def search_files(directory, keyword, extensions):
|
|
12
|
+
results = []
|
|
13
|
+
scanned = 0
|
|
14
|
+
skipped = 0
|
|
15
|
+
|
|
16
|
+
for root, dirs, files in os.walk(directory):
|
|
17
|
+
dirs[:] = [d for d in dirs if d not in DEFAULT_EXCLUDES]
|
|
18
|
+
|
|
19
|
+
for file in files:
|
|
20
|
+
if extensions and not any(file.endswith(ext) for ext in extensions):
|
|
21
|
+
continue
|
|
22
|
+
|
|
23
|
+
filepath = os.path.join(root, file)
|
|
24
|
+
if os.path.getsize(filepath) > MAX_FILE_SIZE:
|
|
25
|
+
skipped += 1
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
scanned += 1
|
|
29
|
+
try:
|
|
30
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
31
|
+
lines = f.readlines()
|
|
32
|
+
except Exception:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
matches = []
|
|
36
|
+
for i, line in enumerate(lines):
|
|
37
|
+
if keyword in line:
|
|
38
|
+
matches.append(i)
|
|
39
|
+
|
|
40
|
+
if matches:
|
|
41
|
+
context_lines = 5
|
|
42
|
+
blocks = []
|
|
43
|
+
current_block = None
|
|
44
|
+
|
|
45
|
+
for m in matches:
|
|
46
|
+
start = max(0, m - context_lines)
|
|
47
|
+
end = min(len(lines), m + context_lines + 1)
|
|
48
|
+
|
|
49
|
+
if current_block and start <= current_block['end']:
|
|
50
|
+
current_block['end'] = max(current_block['end'], end)
|
|
51
|
+
current_block['matches'].append(m)
|
|
52
|
+
else:
|
|
53
|
+
if current_block:
|
|
54
|
+
blocks.append(current_block)
|
|
55
|
+
current_block = {'start': start, 'end': end, 'matches': [m]}
|
|
56
|
+
|
|
57
|
+
if current_block:
|
|
58
|
+
blocks.append(current_block)
|
|
59
|
+
|
|
60
|
+
results.append({'file': filepath, 'blocks': blocks, 'lines': lines})
|
|
61
|
+
|
|
62
|
+
return results, scanned, skipped
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
parser = argparse.ArgumentParser(description="Smart Code Finder - Find code with context (Token Efficient)")
|
|
66
|
+
parser.add_argument("target_dir", help="Directory to scan")
|
|
67
|
+
parser.add_argument("keyword", help="Search keyword (e.g., 'function_name')")
|
|
68
|
+
parser.add_argument("--ext", help="Comma-separated extensions to filter (e.g., .js,.jsx)", default="")
|
|
69
|
+
args = parser.parse_args()
|
|
70
|
+
|
|
71
|
+
extensions = [ext.strip() for ext in args.ext.split(",")] if args.ext else []
|
|
72
|
+
|
|
73
|
+
results, scanned, skipped = search_files(args.target_dir, args.keyword, extensions)
|
|
74
|
+
|
|
75
|
+
if not results:
|
|
76
|
+
print(f"[OK] Keyword '{args.keyword}' not found in {scanned} files.")
|
|
77
|
+
sys.exit(0)
|
|
78
|
+
|
|
79
|
+
print(f"š SEARCH RESULTS: '{args.keyword}'")
|
|
80
|
+
print("=" * 60)
|
|
81
|
+
|
|
82
|
+
total_matches = 0
|
|
83
|
+
for r in results:
|
|
84
|
+
rel_path = os.path.relpath(r['file'], args.target_dir)
|
|
85
|
+
print(f"\n[WARN] Found in: {rel_path}")
|
|
86
|
+
print("-" * 60)
|
|
87
|
+
|
|
88
|
+
for block in r['blocks']:
|
|
89
|
+
for i in range(block['start'], block['end']):
|
|
90
|
+
line_str = r['lines'][i].rstrip()
|
|
91
|
+
prefix = ">>" if i in block['matches'] else " "
|
|
92
|
+
print(f"{i+1:5d} | {prefix} {line_str}")
|
|
93
|
+
if i in block['matches']:
|
|
94
|
+
total_matches += 1
|
|
95
|
+
print("-" * 30)
|
|
96
|
+
|
|
97
|
+
print("\n" + "=" * 60)
|
|
98
|
+
print(f"[OK] Selesai: {total_matches} kecocokan di {len(results)} file (dari {scanned} dipindai, {skipped} dilewati karena >500KB).")
|
|
99
|
+
print("\nš” PROMPT UNTUK AI (Copy-Paste ini):")
|
|
100
|
+
print('"Tolong baca cuplikan kode di atas. Jika Anda perlu mengubah kode tersebut, gunakan tool replace_file_content pada file yang disebutkan di atas."')
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Smart Directory Mapper (Tree Viewer)
|
|
3
|
+
description: Use this skill when you need to understand the project structure, locate files, or map out a directory tree WITHOUT consuming a huge amount of tokens. This script generates a compact, `.gitignore`-aware visual tree of a directory, making it vastly superior to the default `list_dir` tool which returns verbose JSON output.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Smart Directory Mapper (Tree Viewer)
|
|
7
|
+
|
|
8
|
+
When exploring a new project or trying to locate where certain feature files might be stored, DO NOT use the default `list_dir` tool on large folders, as it will flood your context window with raw JSON and token-heavy metadata.
|
|
9
|
+
|
|
10
|
+
Instead, ALWAYS use this `smart_tree` tool.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
Run the script using Python. You can provide an optional `max_depth` argument to control how deep the tree goes (default is 3).
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python .agents/skills/smart_tree/scripts/tree_viewer.py <directory_path> [max_depth]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Examples:
|
|
20
|
+
Map the entire project root up to 2 levels deep:
|
|
21
|
+
```bash
|
|
22
|
+
python .agents/skills/smart_tree/scripts/tree_viewer.py . 2
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Map a specific component folder up to 5 levels deep:
|
|
26
|
+
```bash
|
|
27
|
+
python .agents/skills/smart_tree/scripts/tree_viewer.py src/view/admin 5
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Advantages
|
|
31
|
+
1. **Token Efficient**: Outputs a clean visual hierarchy (`āāā`, `āāā`) instead of raw JSON.
|
|
32
|
+
2. **Noise Reduction**: Automatically parses `.gitignore` and hard-ignores junk directories (`node_modules`, `vendor`, `.git`, `.agents`, `dist`) so you don't get overwhelmed with irrelevant files.
|
|
33
|
+
3. **Controlled Depth**: The `max_depth` parameter prevents you from accidentally reading an infinitely deep directory structure.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import fnmatch
|
|
4
|
+
|
|
5
|
+
def parse_gitignore(dir_path):
|
|
6
|
+
ignore_patterns = [
|
|
7
|
+
'.git', '.agents', 'node_modules', 'vendor', '__pycache__',
|
|
8
|
+
'.DS_Store', 'dist', 'build', '.idea', '.vscode'
|
|
9
|
+
]
|
|
10
|
+
gitignore_path = os.path.join(dir_path, '.gitignore')
|
|
11
|
+
if os.path.exists(gitignore_path):
|
|
12
|
+
with open(gitignore_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
13
|
+
for line in f:
|
|
14
|
+
line = line.strip()
|
|
15
|
+
if line and not line.startswith('#'):
|
|
16
|
+
# Remove trailing slash for directory patterns to match effectively
|
|
17
|
+
if line.endswith('/'):
|
|
18
|
+
line = line[:-1]
|
|
19
|
+
ignore_patterns.append(line)
|
|
20
|
+
return ignore_patterns
|
|
21
|
+
|
|
22
|
+
def is_ignored(name, ignore_patterns):
|
|
23
|
+
for pattern in ignore_patterns:
|
|
24
|
+
if fnmatch.fnmatch(name, pattern) or fnmatch.fnmatch(name, pattern + '/*'):
|
|
25
|
+
return True
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
def generate_tree(dir_path, prefix="", depth=0, max_depth=3, ignore_patterns=None):
|
|
29
|
+
if depth > max_depth:
|
|
30
|
+
print(f"{prefix}āāā ... (max depth reached)")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
if ignore_patterns is None:
|
|
34
|
+
ignore_patterns = parse_gitignore(dir_path)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
entries = sorted(os.listdir(dir_path))
|
|
38
|
+
except PermissionError:
|
|
39
|
+
print(f"{prefix}āāā [Permission Denied]")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# Filter out ignored entries
|
|
43
|
+
entries = [e for e in entries if not is_ignored(e, ignore_patterns)]
|
|
44
|
+
|
|
45
|
+
entries_count = len(entries)
|
|
46
|
+
for i, entry in enumerate(entries):
|
|
47
|
+
is_last = (i == entries_count - 1)
|
|
48
|
+
entry_path = os.path.join(dir_path, entry)
|
|
49
|
+
|
|
50
|
+
connector = "āāā " if is_last else "āāā "
|
|
51
|
+
print(f"{prefix}{connector}{entry}")
|
|
52
|
+
|
|
53
|
+
if os.path.isdir(entry_path):
|
|
54
|
+
extension = " " if is_last else "ā "
|
|
55
|
+
generate_tree(entry_path, prefix + extension, depth + 1, max_depth, ignore_patterns)
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
59
|
+
if len(sys.argv) < 2:
|
|
60
|
+
print("Usage: python tree_viewer.py <directory_path> [max_depth]")
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
target_dir = sys.argv[1]
|
|
64
|
+
max_depth = int(sys.argv[2]) if len(sys.argv) > 2 else 3
|
|
65
|
+
|
|
66
|
+
if not os.path.isdir(target_dir):
|
|
67
|
+
print(f"Error: Directory '{target_dir}' does not exist.")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
print(f"Directory Tree for: {os.path.abspath(target_dir)} (Max Depth: {max_depth})")
|
|
71
|
+
print(".")
|
|
72
|
+
generate_tree(target_dir, max_depth=max_depth)
|