跳到主要内容

灵感与待办整理

收藏0

把零散想法、口述笔记和待办整理成项目、行动清单与待确认问题,保留原意,不擅自执行。

让 AI 安装

github最新版本 0.1.1更新于 2026-10-21下载 3 次2 个版本

scripts/workspace_inventory.py

7.8 KB
1#!/usr/bin/env python3
2"""workspace_inventory.py — Glob+Grep helper for capture's Section 3 (Connections).
3
4Stdlib-only. Given a working directory + a list of dump-derived keywords,
5returns a structured inventory that the capture skill can use to surface
6real workspace connections (never fabricated).
7
8What it returns:
9 1. Per-keyword filename matches (Glob-style)
10 2. Per-keyword content matches (line-grep across source files)
11 3. Top-level folder structure (max-depth 2)
12
13What it does NOT do:
14 - Score relevance (capture skill applies judgment on top)
15 - Fabricate matches (only real Glob/Grep results)
16 - Make LLM calls
17
18Usage:
19 python workspace_inventory.py --root . --keywords "auth,login,2fa"
20 python workspace_inventory.py --root . --keywords "skill,megaprompt" --output json
21 python workspace_inventory.py --sample
22"""
23
24import argparse
25import json
26import re
27import sys
28from pathlib import Path
29from typing import Any, Dict, List, Set
30
31
32DEFAULT_SOURCE_EXTENSIONS = {
33 ".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".java", ".kt", ".rb",
34 ".cs", ".rs", ".swift", ".php", ".scala", ".clj", ".ex", ".exs",
35 ".md", ".mdx", ".rst", ".txt", ".json", ".yaml", ".yml", ".toml",
36}
37DEFAULT_EXCLUDE_DIRS = {
38 "node_modules", ".git", "dist", "build", "target",
39 ".venv", "venv", "__pycache__", ".next", ".cache",
40}
41MAX_FILENAME_MATCHES_PER_KEYWORD = 20
42MAX_CONTENT_MATCHES_PER_KEYWORD = 10
43MAX_FOLDER_DEPTH = 2
44
45
46SAMPLE_TREE: Dict[str, str] = {
47 "src/auth/login.ts": "// auth login flow handler\nexport function login() {}\n",
48 "src/auth/2fa.ts": "// 2fa stub\nexport function setup2FA() {}\n",
49 "src/users/profile.ts": "// user profile\nexport function getProfile() {}\n",
50 "docs/auth-bugs.md": "# Auth bugs\n\n- The login race condition is back.\n",
51 "tests/auth.test.ts": "// auth tests\ndescribe('auth', () => {});\n",
52 "README.md": "# project\n\nAuth + login + users.\n",
53}
54
55
56def collect_files(root: Path, source_extensions: Set[str], exclude_dirs: Set[str]) -> List[Path]:
57 found: List[Path] = []
58 for p in root.rglob("*"):
59 if p.is_dir():
60 continue
61 if any(part in exclude_dirs for part in p.parts):
62 continue
63 if p.suffix.lower() in source_extensions:
64 found.append(p)
65 return found
66
67
68def filename_matches(files: List[Path], keyword: str) -> List[str]:
69 kw = keyword.lower()
70 out: List[str] = []
71 for p in files:
72 if kw in p.name.lower():
73 out.append(str(p))
74 if len(out) >= MAX_FILENAME_MATCHES_PER_KEYWORD:
75 break
76 return out
77
78
79def content_matches(files: List[Path], keyword: str) -> List[Dict[str, Any]]:
80 pattern = re.compile(re.escape(keyword), re.IGNORECASE)
81 out: List[Dict[str, Any]] = []
82 for p in files:
83 try:
84 text = p.read_text(encoding="utf-8", errors="ignore")
85 except OSError:
86 continue
87 for line_no, line in enumerate(text.splitlines(), start=1):
88 if pattern.search(line):
89 out.append({
90 "file": str(p),
91 "line": line_no,
92 "snippet": line.strip()[:120],
93 })
94 if len(out) >= MAX_CONTENT_MATCHES_PER_KEYWORD:
95 return out
96 return out
97
98
99def folder_structure(root: Path, max_depth: int, exclude_dirs: Set[str]) -> List[str]:
100 out: List[str] = []
101 root = root.resolve()
102 for p in root.rglob("*"):
103 if not p.is_dir():
104 continue
105 if any(part in exclude_dirs for part in p.parts):
106 continue
107 try:
108 rel = p.relative_to(root)
109 except ValueError:
110 continue
111 depth = len(rel.parts)
112 if 0 < depth <= max_depth:
113 out.append(str(rel))
114 return sorted(out)
115
116
117def inventory(
118 root: Path,
119 keywords: List[str],
120 source_extensions: Set[str],
121 exclude_dirs: Set[str],
122) -> Dict[str, Any]:
123 files = collect_files(root, source_extensions, exclude_dirs)
124 per_keyword: Dict[str, Dict[str, Any]] = {}
125 for kw in keywords:
126 per_keyword[kw] = {
127 "filename_matches": filename_matches(files, kw),
128 "content_matches": content_matches(files, kw),
129 }
130 return {
131 "root": str(root.resolve()),
132 "files_scanned": len(files),
133 "folder_structure": folder_structure(root, MAX_FOLDER_DEPTH, exclude_dirs),
134 "per_keyword": per_keyword,
135 }
136
137
138def render_human(result: Dict[str, Any]) -> str:
139 out: List[str] = []
140 out.append(f"Workspace inventory for: {result['root']}")
141 out.append(f" Files scanned: {result['files_scanned']}")
142 out.append("")
143 out.append("Folder structure (max depth 2):")
144 for f in result["folder_structure"][:30]:
145 out.append(f" - {f}/")
146 if len(result["folder_structure"]) > 30:
147 out.append(f" ... + {len(result['folder_structure']) - 30} more")
148 out.append("")
149 for kw, hits in result["per_keyword"].items():
150 out.append(f"Keyword: '{kw}'")
151 if hits["filename_matches"]:
152 out.append(f" Filename matches ({len(hits['filename_matches'])}):")
153 for f in hits["filename_matches"]:
154 out.append(f" - {f}")
155 else:
156 out.append(" Filename matches: (none)")
157 if hits["content_matches"]:
158 out.append(f" Content matches ({len(hits['content_matches'])}):")
159 for m in hits["content_matches"]:
160 out.append(f" - {m['file']}:{m['line']} {m['snippet']}")
161 else:
162 out.append(" Content matches: (none)")
163 out.append("")
164 return "\n".join(out)
165
166
167def run_sample(keywords: List[str]) -> Dict[str, Any]:
168 import tempfile
169 with tempfile.TemporaryDirectory() as td:
170 root = Path(td)
171 for rel, content in SAMPLE_TREE.items():
172 p = root / rel
173 p.parent.mkdir(parents=True, exist_ok=True)
174 p.write_text(content, encoding="utf-8")
175 return inventory(root, keywords, DEFAULT_SOURCE_EXTENSIONS, DEFAULT_EXCLUDE_DIRS)
176
177
178def main(argv: List[str]) -> int:
179 parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
180 parser.add_argument("--root", help="Root directory to inventory")
181 parser.add_argument("--keywords", help="Comma-separated keywords to search for")
182 parser.add_argument("--extensions", help="Comma-separated source extensions (default: common)")
183 parser.add_argument("--sample", action="store_true", help="Inventory the embedded sample tree")
184 parser.add_argument("--output", choices=["human", "json"], default="human")
185 args = parser.parse_args(argv)
186
187 if args.sample:
188 sample_keywords = ["auth", "login", "2fa"] if not args.keywords else [k.strip() for k in args.keywords.split(",") if k.strip()]
189 result = run_sample(sample_keywords)
190 elif args.root and args.keywords:
191 root = Path(args.root)
192 if not root.exists():
193 print(f"error: {args.root} not found", file=sys.stderr)
194 return 2
195 kws = [k.strip() for k in args.keywords.split(",") if k.strip()]
196 if not kws:
197 print("error: --keywords must list at least one keyword", file=sys.stderr)
198 return 2
199 if args.extensions:
200 exts = {e.strip() if e.strip().startswith(".") else "." + e.strip() for e in args.extensions.split(",")}
201 else:
202 exts = DEFAULT_SOURCE_EXTENSIONS
203 result = inventory(root, kws, exts, DEFAULT_EXCLUDE_DIRS)
204 else:
205 parser.print_help()
206 return 0
207
208 if args.output == "json":
209 print(json.dumps(result, indent=2))
210 else:
211 print(render_human(result))
212 return 0
213
214
215if __name__ == "__main__":
216 sys.exit(main(sys.argv[1:]))

文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。