@jahanxu/code-flow 0.1.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 (30) hide show
  1. package/package.json +13 -0
  2. package/src/adapters/claude/settings.local.json +26 -0
  3. package/src/adapters/claude/skills/cf-init.md +13 -0
  4. package/src/adapters/claude/skills/cf-inject.md +12 -0
  5. package/src/adapters/claude/skills/cf-learn.md +11 -0
  6. package/src/adapters/claude/skills/cf-scan.md +12 -0
  7. package/src/adapters/claude/skills/cf-stats.md +11 -0
  8. package/src/adapters/claude/skills/cf-validate.md +12 -0
  9. package/src/adapters/codex/AGENTS.md +3 -0
  10. package/src/adapters/cursor/cursorrules +1 -0
  11. package/src/cli.js +105 -0
  12. package/src/core/code-flow/config.yml +97 -0
  13. package/src/core/code-flow/scripts/cf_core.py +129 -0
  14. package/src/core/code-flow/scripts/cf_init.py +829 -0
  15. package/src/core/code-flow/scripts/cf_inject.py +150 -0
  16. package/src/core/code-flow/scripts/cf_inject_hook.py +107 -0
  17. package/src/core/code-flow/scripts/cf_learn.py +202 -0
  18. package/src/core/code-flow/scripts/cf_scan.py +157 -0
  19. package/src/core/code-flow/scripts/cf_session_hook.py +16 -0
  20. package/src/core/code-flow/scripts/cf_stats.py +108 -0
  21. package/src/core/code-flow/scripts/cf_validate.py +340 -0
  22. package/src/core/code-flow/specs/backend/code-quality-performance.md +13 -0
  23. package/src/core/code-flow/specs/backend/database.md +13 -0
  24. package/src/core/code-flow/specs/backend/directory-structure.md +13 -0
  25. package/src/core/code-flow/specs/backend/logging.md +13 -0
  26. package/src/core/code-flow/specs/backend/platform-rules.md +13 -0
  27. package/src/core/code-flow/specs/frontend/component-specs.md +14 -0
  28. package/src/core/code-flow/specs/frontend/directory-structure.md +14 -0
  29. package/src/core/code-flow/specs/frontend/quality-standards.md +15 -0
  30. package/src/core/code-flow/validation.yml +30 -0
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import os
4
+ import sys
5
+ import fnmatch
6
+
7
+ from cf_core import (
8
+ assemble_context,
9
+ load_config,
10
+ load_inject_state,
11
+ match_domains,
12
+ read_specs,
13
+ save_inject_state,
14
+ select_specs,
15
+ )
16
+
17
+
18
+ def match_details(rel_path: str, mapping: dict) -> dict:
19
+ details = {}
20
+ for domain, cfg in (mapping or {}).items():
21
+ patterns = cfg.get("patterns") or []
22
+ for pattern in patterns:
23
+ if fnmatch.fnmatch(rel_path, pattern):
24
+ details[domain] = pattern
25
+ break
26
+ return details
27
+
28
+
29
+ def main() -> None:
30
+ project_root = os.getcwd()
31
+ config = load_config(project_root)
32
+ if not config:
33
+ print(json.dumps({"error": "config_missing"}, ensure_ascii=False))
34
+ return
35
+
36
+ mapping = config.get("path_mapping") or {}
37
+ available_domains = sorted(mapping.keys())
38
+ args = sys.argv[1:]
39
+ list_specs = "--list-specs" in args
40
+ list_domain = ""
41
+ if list_specs:
42
+ for arg in args:
43
+ if arg.startswith("--domain="):
44
+ list_domain = arg.split("=", 1)[1]
45
+ if list_domain and list_domain in mapping:
46
+ specs = (mapping.get(list_domain) or {}).get("specs") or []
47
+ print(json.dumps({"domain": list_domain, "specs": specs}, ensure_ascii=False))
48
+ return
49
+ print(json.dumps({"error": "domain_not_found", "available_domains": available_domains}, ensure_ascii=False))
50
+ return
51
+ if not args:
52
+ state_path = os.path.join(project_root, ".code-flow", ".inject-state")
53
+ recent_target = ""
54
+ try:
55
+ with open(state_path, "r", encoding="utf-8") as file:
56
+ data = json.load(file)
57
+ recent_target = data.get("last_file", "")
58
+ except Exception:
59
+ recent_target = ""
60
+
61
+ if not recent_target:
62
+ print(
63
+ json.dumps(
64
+ {
65
+ "error": "missing_target",
66
+ "available_domains": available_domains,
67
+ "usage": "cf-inject <domain|file_path>",
68
+ },
69
+ ensure_ascii=False,
70
+ )
71
+ )
72
+ return
73
+ target = recent_target
74
+ else:
75
+ target = args[0]
76
+ match_info = {}
77
+ if target in mapping:
78
+ domains = [target]
79
+ else:
80
+ abs_path = target
81
+ if not os.path.isabs(abs_path):
82
+ abs_path = os.path.join(project_root, target)
83
+ rel_path = os.path.relpath(abs_path, project_root)
84
+ domains = match_domains(rel_path, mapping)
85
+ match_info = match_details(rel_path, mapping)
86
+
87
+ if not domains:
88
+ print(
89
+ json.dumps(
90
+ {
91
+ "error": "domain_not_found",
92
+ "target": target,
93
+ "available_domains": available_domains,
94
+ },
95
+ ensure_ascii=False,
96
+ )
97
+ )
98
+ return
99
+
100
+ specs = []
101
+ priorities = {}
102
+ for domain in domains:
103
+ domain_cfg = mapping.get(domain) or {}
104
+ priorities.update(domain_cfg.get("spec_priority") or {})
105
+ specs.extend(read_specs(project_root, domain, domain_cfg))
106
+
107
+ if not specs:
108
+ print(json.dumps({"error": "specs_empty", "domains": domains}, ensure_ascii=False))
109
+ return
110
+
111
+ budget_cfg = config.get("budget") or {}
112
+ budget = budget_cfg.get("l1_max", 1700)
113
+ try:
114
+ budget = int(budget)
115
+ except Exception:
116
+ budget = 1700
117
+
118
+ selected = select_specs(specs, budget, priorities)
119
+ if not selected:
120
+ print(json.dumps({"error": "budget_exceeded", "budget": budget}, ensure_ascii=False))
121
+ return
122
+
123
+ included_domains = {spec.get("domain") for spec in selected if spec.get("domain")}
124
+ state = load_inject_state(project_root)
125
+ injected_domains = state.get("injected_domains") or []
126
+ if not isinstance(injected_domains, list):
127
+ injected_domains = []
128
+ injected_domains = {d for d in injected_domains if isinstance(d, str)}
129
+ state_payload = {"injected_domains": sorted(injected_domains | included_domains)}
130
+ state_payload["last_file"] = target
131
+ save_inject_state(project_root, state_payload)
132
+
133
+ total_tokens = sum(spec.get("tokens", 0) for spec in selected)
134
+ output = {
135
+ "domains": sorted(included_domains),
136
+ "selected_specs": [
137
+ {"path": spec["path"], "tokens": spec["tokens"]} for spec in selected
138
+ ],
139
+ "total_tokens": total_tokens,
140
+ "budget": budget,
141
+ "context": assemble_context(selected, "## Active Specs"),
142
+ }
143
+ if os.environ.get("CF_DEBUG") == "1":
144
+ output["match_info"] = match_info
145
+ output["target"] = target
146
+ print(json.dumps(output, ensure_ascii=False))
147
+
148
+
149
+ if __name__ == "__main__":
150
+ main()
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import os
4
+ import sys
5
+
6
+ from cf_core import (
7
+ assemble_context,
8
+ is_code_file,
9
+ load_config,
10
+ load_inject_state,
11
+ match_domains,
12
+ read_specs,
13
+ save_inject_state,
14
+ select_specs,
15
+ )
16
+
17
+
18
+ def main() -> None:
19
+ try:
20
+ raw = sys.stdin.read()
21
+ if not raw.strip():
22
+ return
23
+ data = json.loads(raw)
24
+ tool_name = data.get("tool_name", "")
25
+ tool_input = data.get("tool_input") or {}
26
+ file_path = tool_input.get("file_path", "")
27
+ if tool_name not in {"Edit", "Write", "MultiEdit"}:
28
+ return
29
+ if not isinstance(file_path, str) or not file_path:
30
+ return
31
+
32
+ project_root = os.getcwd()
33
+ abs_path = file_path
34
+ if not os.path.isabs(abs_path):
35
+ abs_path = os.path.join(project_root, file_path)
36
+ rel_path = os.path.relpath(abs_path, project_root)
37
+
38
+ config = load_config(project_root)
39
+ if not config:
40
+ return
41
+ inject_config = config.get("inject") or {}
42
+ if inject_config.get("auto") is False:
43
+ return
44
+ if not is_code_file(rel_path, inject_config):
45
+ return
46
+
47
+ mapping = config.get("path_mapping") or {}
48
+ domains = match_domains(rel_path, mapping)
49
+ if not domains:
50
+ return
51
+
52
+ state = load_inject_state(project_root)
53
+ injected_domains = state.get("injected_domains") or []
54
+ if not isinstance(injected_domains, list):
55
+ injected_domains = []
56
+ injected_domains = {d for d in injected_domains if isinstance(d, str)}
57
+ new_domains = [domain for domain in domains if domain not in injected_domains]
58
+ if not new_domains:
59
+ state_payload = {"injected_domains": sorted(injected_domains)}
60
+ state_payload["last_file"] = abs_path
61
+ save_inject_state(project_root, state_payload)
62
+ return
63
+
64
+ specs = []
65
+ priorities = {}
66
+ for domain in new_domains:
67
+ domain_cfg = mapping.get(domain) or {}
68
+ priorities.update(domain_cfg.get("spec_priority") or {})
69
+ specs.extend(read_specs(project_root, domain, domain_cfg))
70
+
71
+ if not specs:
72
+ return
73
+
74
+ budget_cfg = config.get("budget") or {}
75
+ budget = budget_cfg.get("l1_max", 1700)
76
+ try:
77
+ budget = int(budget)
78
+ except Exception:
79
+ budget = 1700
80
+
81
+ selected = select_specs(specs, budget, priorities)
82
+ if not selected:
83
+ return
84
+
85
+ included_domains = {spec.get("domain") for spec in selected if spec.get("domain")}
86
+ state_payload = {"injected_domains": sorted(injected_domains | included_domains)}
87
+ state_payload["last_file"] = abs_path
88
+ save_inject_state(project_root, state_payload)
89
+
90
+ payload = {
91
+ "hookSpecificOutput": {
92
+ "hookEventName": "PreToolUse",
93
+ "additionalContext": assemble_context(selected, "## Active Specs (auto-injected)"),
94
+ }
95
+ }
96
+ if os.environ.get("CF_DEBUG") == "1":
97
+ payload["debug"] = {
98
+ "target": abs_path,
99
+ "domains": sorted(new_domains),
100
+ }
101
+ sys.stdout.write(json.dumps(payload))
102
+ except Exception:
103
+ return
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import datetime
4
+ import json
5
+ import os
6
+ import sys
7
+
8
+
9
+ def load_config(project_root: str) -> dict:
10
+ config_path = os.path.join(project_root, ".code-flow", "config.yml")
11
+ if not os.path.exists(config_path):
12
+ return {}
13
+ try:
14
+ import yaml
15
+ except Exception:
16
+ return {}
17
+ try:
18
+ with open(config_path, "r", encoding="utf-8") as file:
19
+ data = yaml.safe_load(file)
20
+ return data or {}
21
+ except Exception:
22
+ return {}
23
+
24
+
25
+ def read_text(path: str) -> str:
26
+ try:
27
+ with open(path, "r", encoding="utf-8") as file:
28
+ return file.read()
29
+ except Exception:
30
+ return ""
31
+
32
+
33
+ def write_text(path: str, content: str) -> bool:
34
+ try:
35
+ with open(path, "w", encoding="utf-8") as file:
36
+ file.write(content)
37
+ return True
38
+ except Exception:
39
+ return False
40
+
41
+
42
+ def estimate_tokens(text: str) -> int:
43
+ return len(text) // 4
44
+
45
+
46
+ def find_target_spec(domain: str, config: dict, file_arg: str) -> tuple:
47
+ mapping = config.get("path_mapping") or {}
48
+ domain_cfg = mapping.get(domain) or {}
49
+ specs = domain_cfg.get("specs") or []
50
+ if not specs:
51
+ return "", specs
52
+ if file_arg:
53
+ if file_arg in specs:
54
+ return file_arg, specs
55
+ return "", specs
56
+ if len(specs) == 1:
57
+ return specs[0], specs
58
+ return "", specs
59
+
60
+
61
+ def insert_learning(content: str, entry: str) -> str:
62
+ text = content.rstrip()
63
+ if not text:
64
+ return f"## Learnings\n{entry}\n"
65
+
66
+ lines = text.splitlines()
67
+ learn_index = None
68
+ for index, line in enumerate(lines):
69
+ if line.strip() == "## Learnings":
70
+ learn_index = index
71
+ break
72
+
73
+ if learn_index is None:
74
+ return f"{text}\n\n## Learnings\n{entry}\n"
75
+
76
+ insert_at = len(lines)
77
+ for idx in range(learn_index + 1, len(lines)):
78
+ if lines[idx].startswith("## "):
79
+ insert_at = idx
80
+ break
81
+
82
+ lines.insert(insert_at, entry)
83
+ return "\n".join(lines).rstrip() + "\n"
84
+
85
+
86
+ def main() -> None:
87
+ parser = argparse.ArgumentParser()
88
+ parser.add_argument("--scope", required=True, choices=["global", "frontend", "backend"])
89
+ parser.add_argument("--content", required=True)
90
+ parser.add_argument("--file", default="")
91
+ parser.add_argument("--dry-run", action="store_true")
92
+ args = parser.parse_args()
93
+
94
+ project_root = os.getcwd()
95
+ config = load_config(project_root)
96
+
97
+ date_str = datetime.date.today().isoformat()
98
+ entry = f"- [{date_str}] {args.content.strip()}"
99
+
100
+ if args.scope == "global":
101
+ target_path = os.path.join(project_root, "CLAUDE.md")
102
+ content = read_text(target_path)
103
+ updated = insert_learning(content, entry)
104
+ if args.dry_run:
105
+ tokens = estimate_tokens(updated)
106
+ print(
107
+ json.dumps(
108
+ {
109
+ "status": "dry_run",
110
+ "target": "CLAUDE.md",
111
+ "entry": entry,
112
+ "tokens": tokens,
113
+ "warning": "L0 超出预算" if tokens > 800 else "",
114
+ },
115
+ ensure_ascii=False,
116
+ )
117
+ )
118
+ return
119
+ if not write_text(target_path, updated):
120
+ print(json.dumps({"error": "write_failed", "target": "CLAUDE.md"}, ensure_ascii=False))
121
+ return
122
+ tokens = estimate_tokens(updated)
123
+ print(
124
+ json.dumps(
125
+ {
126
+ "status": "ok",
127
+ "target": "CLAUDE.md",
128
+ "entry": entry,
129
+ "tokens": tokens,
130
+ "warning": "L0 超出预算" if tokens > 800 else "",
131
+ },
132
+ ensure_ascii=False,
133
+ )
134
+ )
135
+ return
136
+
137
+ if not config:
138
+ print(json.dumps({"error": "config_missing"}, ensure_ascii=False))
139
+ return
140
+
141
+ spec_rel, specs = find_target_spec(args.scope, config, args.file)
142
+ if not spec_rel:
143
+ print(
144
+ json.dumps(
145
+ {
146
+ "error": "spec_not_selected",
147
+ "available_specs": specs,
148
+ },
149
+ ensure_ascii=False,
150
+ )
151
+ )
152
+ return
153
+
154
+ target_path = os.path.join(project_root, ".code-flow", "specs", spec_rel)
155
+ content = read_text(target_path)
156
+ if not content:
157
+ print(
158
+ json.dumps(
159
+ {"error": "spec_missing_or_empty", "target": spec_rel},
160
+ ensure_ascii=False,
161
+ )
162
+ )
163
+ return
164
+
165
+ updated = insert_learning(content, entry)
166
+ tokens = estimate_tokens(updated)
167
+ warning = ""
168
+ if tokens > 500:
169
+ warning = "单文件超过 500 tokens,建议精简"
170
+ if args.dry_run:
171
+ print(
172
+ json.dumps(
173
+ {
174
+ "status": "dry_run",
175
+ "target": spec_rel,
176
+ "entry": entry,
177
+ "tokens": tokens,
178
+ "warning": warning,
179
+ },
180
+ ensure_ascii=False,
181
+ )
182
+ )
183
+ return
184
+ if not write_text(target_path, updated):
185
+ print(json.dumps({"error": "write_failed", "target": spec_rel}, ensure_ascii=False))
186
+ return
187
+ print(
188
+ json.dumps(
189
+ {
190
+ "status": "ok",
191
+ "target": spec_rel,
192
+ "entry": entry,
193
+ "tokens": tokens,
194
+ "warning": warning,
195
+ },
196
+ ensure_ascii=False,
197
+ )
198
+ )
199
+
200
+
201
+ if __name__ == "__main__":
202
+ main()
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import os
4
+ import re
5
+ import sys
6
+
7
+ from cf_core import estimate_tokens, load_config
8
+
9
+
10
+ PATH_PATTERN = re.compile(r"(?:[\w./-]+/)+[\w.-]+\.[A-Za-z0-9]+")
11
+
12
+
13
+ def read_text(path: str) -> str:
14
+ try:
15
+ with open(path, "r", encoding="utf-8") as file:
16
+ return file.read().strip()
17
+ except Exception:
18
+ return ""
19
+
20
+
21
+ def normalize_line(line: str) -> str:
22
+ return " ".join(line.strip().split())
23
+
24
+
25
+ def find_redundant_lines(specs: list) -> dict:
26
+ line_map = {}
27
+ for spec in specs:
28
+ for raw in spec["content"].splitlines():
29
+ line = normalize_line(raw)
30
+ if not line:
31
+ continue
32
+ if line.startswith("#") or line == "---":
33
+ continue
34
+ if len(line) < 6:
35
+ continue
36
+ line_map.setdefault(line, set()).add(spec["path"])
37
+ redundant = {line: paths for line, paths in line_map.items() if len(paths) >= 3}
38
+ return redundant
39
+
40
+
41
+ def find_missing_paths(text: str, project_root: str) -> list:
42
+ missing = []
43
+ for token in set(PATH_PATTERN.findall(text)):
44
+ if token.startswith("http://") or token.startswith("https://"):
45
+ continue
46
+ abs_path = token if os.path.isabs(token) else os.path.join(project_root, token)
47
+ if not os.path.exists(abs_path):
48
+ missing.append(token)
49
+ return sorted(missing)
50
+
51
+
52
+ def main() -> None:
53
+ project_root = os.getcwd()
54
+ files = []
55
+ total_tokens = 0
56
+ json_output = "--json" in sys.argv
57
+ only_issues = "--only-issues" in sys.argv
58
+ limit = None
59
+ for arg in sys.argv[1:]:
60
+ if arg.startswith("--limit="):
61
+ try:
62
+ limit = int(arg.split("=", 1)[1])
63
+ except Exception:
64
+ limit = None
65
+
66
+ claude_path = os.path.join(project_root, "CLAUDE.md")
67
+ if os.path.exists(claude_path):
68
+ content = read_text(claude_path)
69
+ tokens = estimate_tokens(content)
70
+ total_tokens += tokens
71
+ files.append({"path": "CLAUDE.md", "tokens": tokens, "issues": []})
72
+
73
+ specs_root = os.path.join(project_root, ".code-flow", "specs")
74
+ specs = []
75
+ if os.path.isdir(specs_root):
76
+ for root, _, filenames in os.walk(specs_root):
77
+ for name in filenames:
78
+ if not name.endswith(".md"):
79
+ continue
80
+ full_path = os.path.join(root, name)
81
+ content = read_text(full_path)
82
+ if not content:
83
+ continue
84
+ tokens = estimate_tokens(content)
85
+ total_tokens += tokens
86
+ rel = os.path.relpath(full_path, specs_root)
87
+ rel_path = os.path.join("specs", rel).replace(os.sep, "/")
88
+ spec_entry = {
89
+ "path": rel_path,
90
+ "tokens": tokens,
91
+ "issues": [],
92
+ "content": content,
93
+ }
94
+ specs.append(spec_entry)
95
+
96
+ redundant_map = find_redundant_lines(specs)
97
+
98
+ for spec in specs:
99
+ issues = []
100
+ if spec["tokens"] > 500:
101
+ issues.append(f"冗长: {spec['tokens']} tokens")
102
+
103
+ missing_paths = find_missing_paths(spec["content"], project_root)
104
+ for path in missing_paths[:3]:
105
+ issues.append(f"过时: 路径不存在 {path}")
106
+
107
+ redundant_lines = [line for line, paths in redundant_map.items() if spec["path"] in paths]
108
+ for line in redundant_lines[:3]:
109
+ issues.append(f"冗余: '{line}' 出现于 {len(redundant_map[line])} 个文件")
110
+
111
+ files.append({"path": spec["path"], "tokens": spec["tokens"], "issues": issues})
112
+
113
+ for entry in files:
114
+ if total_tokens <= 0:
115
+ entry["percent"] = "0%"
116
+ else:
117
+ entry["percent"] = f"{round(entry['tokens'] * 100 / total_tokens)}%"
118
+
119
+ config = load_config(project_root)
120
+ budget = (config.get("budget") or {}).get("total", 2500)
121
+ try:
122
+ budget = int(budget)
123
+ except Exception:
124
+ budget = 2500
125
+
126
+ output = {
127
+ "files": files,
128
+ "total_tokens": total_tokens,
129
+ "budget": budget,
130
+ "warnings": [],
131
+ }
132
+ if limit is not None:
133
+ files = files[:limit]
134
+
135
+ if only_issues:
136
+ files = [entry for entry in files if entry.get("issues")]
137
+
138
+ if json_output:
139
+ output = {
140
+ "files": files,
141
+ "total_tokens": total_tokens,
142
+ "budget": budget,
143
+ "warnings": [],
144
+ }
145
+ print(json.dumps(output, ensure_ascii=False))
146
+ return
147
+
148
+ print("FILE | TOKENS | PERCENT | ISSUES")
149
+ for entry in files:
150
+ issues = entry.get("issues") or []
151
+ issue_text = " / ".join(issues) if issues else "-"
152
+ print(f"{entry['path']} | {entry['tokens']} | {entry['percent']} | {issue_text}")
153
+ print("TOTAL:", f"{total_tokens} / {budget}")
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env python3
2
+ import os
3
+
4
+
5
+ def main() -> None:
6
+ try:
7
+ project_root = os.getcwd()
8
+ state_path = os.path.join(project_root, ".code-flow", ".inject-state")
9
+ if os.path.exists(state_path):
10
+ os.remove(state_path)
11
+ except Exception:
12
+ return
13
+
14
+
15
+ if __name__ == "__main__":
16
+ main()