openclew 0.2.1 → 0.4.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/LICENSE +190 -21
- package/README.md +40 -3
- package/UPGRADING.md +167 -0
- package/bin/openclew.js +49 -18
- package/commands/oc-checkout.md +134 -0
- package/commands/oc-init.md +27 -0
- package/commands/oc-peek.md +41 -0
- package/commands/oc-search.md +25 -0
- package/commands/oc-status.md +20 -0
- package/lib/checkout.js +2 -4
- package/lib/index-gen.js +100 -25
- package/lib/init.js +103 -31
- package/lib/inject.js +17 -7
- package/lib/mcp-server.js +313 -0
- package/lib/new-doc.js +12 -4
- package/lib/new-log.js +4 -4
- package/lib/peek.js +166 -0
- package/lib/search.js +242 -0
- package/lib/status.js +151 -0
- package/lib/templates.js +193 -13
- package/package.json +15 -3
- package/skills/oc-checkpoint/SKILL.md +36 -0
- package/skills/oc-init/SKILL.md +49 -0
- package/skills/oc-search/SKILL.md +45 -0
- package/templates/FORMAT.md +300 -0
- package/templates/log.md +1 -1
- package/templates/onboarding/flow.md +59 -0
- package/templates/onboarding/scaffold_index.md +31 -0
- package/templates/refdoc.md +1 -1
- package/hooks/generate-index.py +0 -226
package/hooks/generate-index.py
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
openclew index generator.
|
|
4
|
-
|
|
5
|
-
Scans doc/_*.md (refdocs) and doc/log/*.md (logs),
|
|
6
|
-
parses metadata line + L1 blocks, and generates doc/_INDEX.md.
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python generate-index.py # from project root
|
|
10
|
-
python generate-index.py /path/to/doc # custom doc directory
|
|
11
|
-
|
|
12
|
-
Idempotent: running twice produces the same output.
|
|
13
|
-
Zero dependencies: Python 3.8+ standard library only.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
import os
|
|
17
|
-
import re
|
|
18
|
-
import sys
|
|
19
|
-
from datetime import datetime
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def find_doc_dir():
|
|
24
|
-
"""Find the doc/ directory."""
|
|
25
|
-
if len(sys.argv) > 1:
|
|
26
|
-
doc_dir = Path(sys.argv[1])
|
|
27
|
-
else:
|
|
28
|
-
doc_dir = Path("doc")
|
|
29
|
-
|
|
30
|
-
if not doc_dir.is_dir():
|
|
31
|
-
print(f"No '{doc_dir}' directory found. Nothing to index.")
|
|
32
|
-
sys.exit(0)
|
|
33
|
-
|
|
34
|
-
return doc_dir
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def parse_metadata_line(content):
|
|
38
|
-
"""Extract metadata from the first line (before L1_START).
|
|
39
|
-
|
|
40
|
-
Format: openclew@VERSION · key: value · key: value · ...
|
|
41
|
-
"""
|
|
42
|
-
meta = {}
|
|
43
|
-
first_line = content.split("\n", 1)[0].strip()
|
|
44
|
-
if not first_line.startswith("openclew@"):
|
|
45
|
-
return meta
|
|
46
|
-
|
|
47
|
-
parts = first_line.split(" · ")
|
|
48
|
-
for part in parts:
|
|
49
|
-
part = part.strip()
|
|
50
|
-
if part.startswith("openclew@"):
|
|
51
|
-
meta["version"] = part.split("@", 1)[1]
|
|
52
|
-
continue
|
|
53
|
-
if ":" in part:
|
|
54
|
-
key, _, value = part.partition(":")
|
|
55
|
-
meta[key.strip().lower()] = value.strip()
|
|
56
|
-
|
|
57
|
-
return meta
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def parse_l1(content):
|
|
61
|
-
"""Extract L1 fields (subject, doc_brief) from L1_START/L1_END block."""
|
|
62
|
-
meta = {}
|
|
63
|
-
match = re.search(
|
|
64
|
-
r"<!--\s*L1_START\s*-->(.+?)<!--\s*L1_END\s*-->",
|
|
65
|
-
content,
|
|
66
|
-
re.DOTALL,
|
|
67
|
-
)
|
|
68
|
-
if not match:
|
|
69
|
-
return meta
|
|
70
|
-
|
|
71
|
-
block = match.group(1)
|
|
72
|
-
|
|
73
|
-
# Extract **subject:** value
|
|
74
|
-
subject_match = re.search(r"\*\*subject:\*\*\s*(.+)", block)
|
|
75
|
-
if subject_match:
|
|
76
|
-
meta["subject"] = subject_match.group(1).strip()
|
|
77
|
-
|
|
78
|
-
# Extract **doc_brief:** value
|
|
79
|
-
brief_match = re.search(r"\*\*doc_brief:\*\*\s*(.+)", block)
|
|
80
|
-
if brief_match:
|
|
81
|
-
meta["doc_brief"] = brief_match.group(1).strip()
|
|
82
|
-
|
|
83
|
-
return meta
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def parse_l1_legacy(content):
|
|
87
|
-
"""Fallback parser for old format (key: value lines inside L1 block)."""
|
|
88
|
-
meta = {}
|
|
89
|
-
match = re.search(
|
|
90
|
-
r"<!--\s*L1_START\s*-->(.+?)<!--\s*L1_END\s*-->",
|
|
91
|
-
content,
|
|
92
|
-
re.DOTALL,
|
|
93
|
-
)
|
|
94
|
-
if not match:
|
|
95
|
-
return meta
|
|
96
|
-
|
|
97
|
-
block = match.group(1)
|
|
98
|
-
for line in block.splitlines():
|
|
99
|
-
line = line.strip()
|
|
100
|
-
if line.startswith("#") or not line:
|
|
101
|
-
continue
|
|
102
|
-
if ":" in line:
|
|
103
|
-
key, _, value = line.partition(":")
|
|
104
|
-
meta[key.strip().lower()] = value.strip()
|
|
105
|
-
|
|
106
|
-
return meta
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def parse_file(filepath):
|
|
110
|
-
"""Parse a file and return combined metadata + L1 fields."""
|
|
111
|
-
try:
|
|
112
|
-
content = filepath.read_text(encoding="utf-8")
|
|
113
|
-
except (OSError, UnicodeDecodeError):
|
|
114
|
-
return None
|
|
115
|
-
|
|
116
|
-
# Try new format first
|
|
117
|
-
meta_line = parse_metadata_line(content)
|
|
118
|
-
l1 = parse_l1(content)
|
|
119
|
-
|
|
120
|
-
if l1.get("subject"):
|
|
121
|
-
# New format
|
|
122
|
-
meta_line.update(l1)
|
|
123
|
-
return meta_line
|
|
124
|
-
|
|
125
|
-
# Fallback to legacy format
|
|
126
|
-
legacy = parse_l1_legacy(content)
|
|
127
|
-
if legacy:
|
|
128
|
-
meta_line.update(legacy)
|
|
129
|
-
return meta_line
|
|
130
|
-
|
|
131
|
-
return None
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def collect_docs(doc_dir):
|
|
135
|
-
"""Collect refdocs and logs with their metadata."""
|
|
136
|
-
refdocs = []
|
|
137
|
-
logs = []
|
|
138
|
-
|
|
139
|
-
# Refdocs: doc/_*.md
|
|
140
|
-
for f in sorted(doc_dir.glob("_*.md")):
|
|
141
|
-
if f.name == "_INDEX.md":
|
|
142
|
-
continue
|
|
143
|
-
meta = parse_file(f)
|
|
144
|
-
if meta:
|
|
145
|
-
refdocs.append((f, meta))
|
|
146
|
-
|
|
147
|
-
# Log docs: doc/log/*.md
|
|
148
|
-
log_dir = doc_dir / "log"
|
|
149
|
-
if log_dir.is_dir():
|
|
150
|
-
for f in sorted(log_dir.glob("*.md"), reverse=True):
|
|
151
|
-
meta = parse_file(f)
|
|
152
|
-
if meta:
|
|
153
|
-
logs.append((f, meta))
|
|
154
|
-
|
|
155
|
-
return refdocs, logs
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def generate_index(doc_dir, refdocs, logs):
|
|
159
|
-
"""Generate _INDEX.md content."""
|
|
160
|
-
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
161
|
-
lines = [
|
|
162
|
-
f"# Project Knowledge Index",
|
|
163
|
-
f"",
|
|
164
|
-
f"> Auto-generated by [openclew](https://github.com/openclew/openclew) on {now}.",
|
|
165
|
-
f"> Do not edit manually — rebuilt from L1 metadata on every commit.",
|
|
166
|
-
f"",
|
|
167
|
-
]
|
|
168
|
-
|
|
169
|
-
# Refdocs section
|
|
170
|
-
lines.append("## Refdocs")
|
|
171
|
-
lines.append("")
|
|
172
|
-
if refdocs:
|
|
173
|
-
lines.append("| Document | Subject | Status | Category |")
|
|
174
|
-
lines.append("|----------|---------|--------|----------|")
|
|
175
|
-
for f, meta in refdocs:
|
|
176
|
-
name = f.name
|
|
177
|
-
subject = meta.get("subject", "—")
|
|
178
|
-
status = meta.get("status", "—")
|
|
179
|
-
category = meta.get("category", "—")
|
|
180
|
-
rel_path = f.relative_to(doc_dir.parent)
|
|
181
|
-
lines.append(f"| [{name}]({rel_path}) | {subject} | {status} | {category} |")
|
|
182
|
-
else:
|
|
183
|
-
lines.append("_No refdocs yet. Create one with `templates/refdoc.md`._")
|
|
184
|
-
lines.append("")
|
|
185
|
-
|
|
186
|
-
# Logs section (last 20)
|
|
187
|
-
lines.append("## Recent logs")
|
|
188
|
-
lines.append("")
|
|
189
|
-
display_logs = logs[:20]
|
|
190
|
-
if display_logs:
|
|
191
|
-
lines.append("| Date | Subject | Status | Category |")
|
|
192
|
-
lines.append("|------|---------|--------|----------|")
|
|
193
|
-
for f, meta in display_logs:
|
|
194
|
-
date = meta.get("date", f.stem[:10])
|
|
195
|
-
subject = meta.get("subject", "—")
|
|
196
|
-
status = meta.get("status", "—")
|
|
197
|
-
category = meta.get("category", "—")
|
|
198
|
-
rel_path = f.relative_to(doc_dir.parent)
|
|
199
|
-
lines.append(f"| {date} | [{subject}]({rel_path}) | {status} | {category} |")
|
|
200
|
-
if len(logs) > 20:
|
|
201
|
-
lines.append(f"")
|
|
202
|
-
lines.append(f"_{len(logs) - 20} older logs not shown._")
|
|
203
|
-
else:
|
|
204
|
-
lines.append("_No logs yet. Create one with `templates/log.md`._")
|
|
205
|
-
lines.append("")
|
|
206
|
-
|
|
207
|
-
# Stats
|
|
208
|
-
lines.append("---")
|
|
209
|
-
lines.append(f"**{len(refdocs)}** refdocs, **{len(logs)}** logs.")
|
|
210
|
-
lines.append("")
|
|
211
|
-
|
|
212
|
-
return "\n".join(lines)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def main():
|
|
216
|
-
doc_dir = find_doc_dir()
|
|
217
|
-
refdocs, logs = collect_docs(doc_dir)
|
|
218
|
-
index_content = generate_index(doc_dir, refdocs, logs)
|
|
219
|
-
|
|
220
|
-
index_path = doc_dir / "_INDEX.md"
|
|
221
|
-
index_path.write_text(index_content, encoding="utf-8")
|
|
222
|
-
print(f"Generated {index_path} ({len(refdocs)} refdocs, {len(logs)} logs)")
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if __name__ == "__main__":
|
|
226
|
-
main()
|