openclew 0.1.0 → 0.2.1
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/README.md +24 -122
- package/bin/openclew.js +14 -2
- package/hooks/generate-index.py +95 -26
- package/lib/checkout.js +296 -0
- package/lib/config.js +34 -0
- package/lib/detect.js +38 -3
- package/lib/init.js +157 -74
- package/lib/inject.js +9 -5
- package/lib/new-doc.js +14 -4
- package/lib/new-log.js +12 -1
- package/lib/templates.js +280 -22
- package/package.json +1 -1
- package/templates/log.md +5 -8
- package/templates/{permanent.md → refdoc.md} +5 -9
package/README.md
CHANGED
|
@@ -70,152 +70,54 @@ L1 answers "should I read this?" L2 answers "what do I need to know?" L3 is ther
|
|
|
70
70
|
|
|
71
71
|
| Type | Location | Role | Mutability |
|
|
72
72
|
|------|----------|------|------------|
|
|
73
|
-
| **
|
|
73
|
+
| **Refdoc** | `doc/_SUBJECT.md` | Reference knowledge (architecture, conventions, decisions) | Updated over time |
|
|
74
74
|
| **Log** | `doc/log/YYYY-MM-DD_subject.md` | Frozen facts (what happened, what was decided) | Never modified |
|
|
75
75
|
|
|
76
|
-
**
|
|
76
|
+
**Refdocs** are your project's brain — they evolve as the project evolves.
|
|
77
77
|
**Logs** are your project's journal — immutable records of what happened and why.
|
|
78
78
|
|
|
79
|
-
Together, they form the thread. The
|
|
79
|
+
Together, they form the thread. The refdocs tell you where you are. The logs tell you how you got here.
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
|
83
|
-
## Quick start (
|
|
83
|
+
## Quick start (2 minutes)
|
|
84
84
|
|
|
85
|
-
### 1.
|
|
85
|
+
### 1. Install
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
|
|
88
|
+
npx openclew init
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
This:
|
|
92
|
+
- Creates `doc/` with a guide, an example doc, and an example log
|
|
93
|
+
- Detects your instruction file (CLAUDE.md, .cursorrules, AGENTS.md...)
|
|
94
|
+
- Injects a block that teaches your agent about the doc structure
|
|
95
|
+
- Installs a pre-commit hook that auto-generates `doc/_INDEX.md`
|
|
92
96
|
|
|
93
|
-
|
|
97
|
+
### 2. Start a session with your agent
|
|
94
98
|
|
|
95
|
-
|
|
96
|
-
<summary><b>templates/permanent.md</b> — for living knowledge</summary>
|
|
97
|
-
|
|
98
|
-
```markdown
|
|
99
|
-
<!-- L1_START -->
|
|
100
|
-
# L1 - Metadata
|
|
101
|
-
type: Reference | Architecture | Guide | Analysis
|
|
102
|
-
subject: Short title (< 60 chars)
|
|
103
|
-
created: YYYY-MM-DD
|
|
104
|
-
updated: YYYY-MM-DD
|
|
105
|
-
short_story: 1-2 sentences. What this doc covers and what it concludes.
|
|
106
|
-
status: Active | Stable | Archived
|
|
107
|
-
category: Main domain (e.g. Auth, API, Database, UI...)
|
|
108
|
-
keywords: [tag1, tag2, tag3]
|
|
109
|
-
<!-- L1_END -->
|
|
110
|
-
|
|
111
|
-
---
|
|
112
|
-
|
|
113
|
-
<!-- L2_START -->
|
|
114
|
-
# L2 - Summary
|
|
115
|
-
|
|
116
|
-
## Objective
|
|
117
|
-
<!-- Why this document exists -->
|
|
99
|
+
Ask it:
|
|
118
100
|
|
|
119
|
-
|
|
120
|
-
<!-- 3-5 essential takeaways -->
|
|
101
|
+
> Read doc/_USING_OPENCLEW.md and document our architecture.
|
|
121
102
|
|
|
122
|
-
|
|
123
|
-
<!-- Recommended approach or pattern -->
|
|
124
|
-
<!-- L2_END -->
|
|
103
|
+
Your agent reads the guide, understands the L1/L2/L3 format, and creates `doc/_ARCHITECTURE.md` with your project's actual architecture.
|
|
125
104
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<!-- L3_START -->
|
|
129
|
-
# L3 - Details
|
|
130
|
-
|
|
131
|
-
<!-- Full technical content: examples, code, references... -->
|
|
105
|
+
### 3. There is no step 3
|
|
132
106
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
| Date | Change |
|
|
136
|
-
|------|--------|
|
|
137
|
-
| YYYY-MM-DD | Initial creation |
|
|
138
|
-
<!-- L3_END -->
|
|
139
|
-
```
|
|
107
|
+
Next session, your agent reads the index, finds the doc, has the context. No re-explanation needed. As your project evolves, your agent creates and updates docs during sessions — refdocs for ongoing knowledge, logs for frozen facts.
|
|
140
108
|
|
|
141
|
-
|
|
109
|
+
The index auto-regenerates on every commit. Never edit it manually.
|
|
142
110
|
|
|
143
111
|
<details>
|
|
144
|
-
<summary><b>
|
|
145
|
-
|
|
146
|
-
```markdown
|
|
147
|
-
<!-- L1_START -->
|
|
148
|
-
# L1 - Metadata
|
|
149
|
-
date: YYYY-MM-DD
|
|
150
|
-
type: Bug | Feature | Refactor | Doc | Deploy
|
|
151
|
-
subject: Short title (< 60 chars)
|
|
152
|
-
short_story: 1-2 sentences. What happened and what was the outcome.
|
|
153
|
-
status: Done | In progress | Abandoned
|
|
154
|
-
category: Main domain
|
|
155
|
-
keywords: [tag1, tag2, tag3]
|
|
156
|
-
<!-- L1_END -->
|
|
157
|
-
|
|
158
|
-
---
|
|
159
|
-
|
|
160
|
-
<!-- L2_START -->
|
|
161
|
-
# L2 - Summary
|
|
112
|
+
<summary><b>Manual setup</b> — if you prefer not to use the CLI</summary>
|
|
162
113
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
<!-- How it was resolved -->
|
|
168
|
-
<!-- L2_END -->
|
|
169
|
-
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
<!-- L3_START -->
|
|
173
|
-
# L3 - Details
|
|
174
|
-
|
|
175
|
-
<!-- Technical details: code changes, debugging steps, references... -->
|
|
176
|
-
<!-- L3_END -->
|
|
177
|
-
```
|
|
114
|
+
1. Create `doc/` and `doc/log/`
|
|
115
|
+
2. Copy templates from [`templates/`](templates/) (refdoc.md, log.md)
|
|
116
|
+
3. Add the openclew block to your instruction file (see `doc/_USING_OPENCLEW.md` after init for the exact format)
|
|
117
|
+
4. Copy [`hooks/generate-index.py`](hooks/generate-index.py) and wire it as a pre-commit hook
|
|
178
118
|
|
|
179
119
|
</details>
|
|
180
120
|
|
|
181
|
-
### 3. Write your first doc
|
|
182
|
-
|
|
183
|
-
```bash
|
|
184
|
-
cp templates/permanent.md doc/_ARCHITECTURE.md
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
Edit it — describe your project's architecture. Fill in L1 (metadata), L2 (summary), skip L3 if you don't need it yet.
|
|
188
|
-
|
|
189
|
-
### 4. Point your agent to it
|
|
190
|
-
|
|
191
|
-
Add this to your `CLAUDE.md`, `.cursorrules`, or `AGENTS.md`:
|
|
192
|
-
|
|
193
|
-
```markdown
|
|
194
|
-
## Project knowledge
|
|
195
|
-
|
|
196
|
-
Documentation lives in `doc/`. Each doc has 3 levels (L1/L2/L3).
|
|
197
|
-
- Read L1 first to decide if you need more
|
|
198
|
-
- Permanent docs: `doc/_*.md` (living knowledge, updated)
|
|
199
|
-
- Logs: `doc/log/YYYY-MM-DD_*.md` (frozen facts, never modified)
|
|
200
|
-
- Index: `doc/_INDEX.md` (auto-generated, start here)
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### 5. Auto-generate the index (optional)
|
|
204
|
-
|
|
205
|
-
Copy [`hooks/generate-index.py`](hooks/generate-index.py) to your project and add it as a pre-commit hook:
|
|
206
|
-
|
|
207
|
-
```bash
|
|
208
|
-
# Option A: git hook
|
|
209
|
-
cp hooks/generate-index.py .git/hooks/generate-index.py
|
|
210
|
-
echo 'python .git/hooks/generate-index.py && git add doc/_INDEX.md' >> .git/hooks/pre-commit
|
|
211
|
-
chmod +x .git/hooks/pre-commit
|
|
212
|
-
|
|
213
|
-
# Option B: pre-commit framework
|
|
214
|
-
# See hooks/README.md for .pre-commit-config.yaml setup
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
The index auto-regenerates on every commit. Never edit it manually.
|
|
218
|
-
|
|
219
121
|
---
|
|
220
122
|
|
|
221
123
|
## How it works in practice
|
|
@@ -263,7 +165,7 @@ doc/
|
|
|
263
165
|
- **Shared knowledge** — Same docs for humans and AI. One source, multiple readers.
|
|
264
166
|
- **SSOT** (Single Source of Truth) — Each piece of information lives in one place.
|
|
265
167
|
- **Logs are immutable** — Once written, never modified. Frozen facts.
|
|
266
|
-
- **
|
|
168
|
+
- **Refdocs evolve** — They evolve as the project evolves.
|
|
267
169
|
- **Index is auto-generated** — Never edit `_INDEX.md` manually.
|
|
268
170
|
|
|
269
171
|
---
|
package/bin/openclew.js
CHANGED
|
@@ -10,14 +10,25 @@ openclew — Long Life Memory for LLMs
|
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
12
|
openclew init Set up openclew in the current project
|
|
13
|
-
openclew new <title> Create a
|
|
14
|
-
openclew log <title> Create a
|
|
13
|
+
openclew new <title> Create a refdoc (evolves with the project)
|
|
14
|
+
openclew log <title> Create a session log (frozen facts)
|
|
15
|
+
openclew checkout End-of-session summary + log creation
|
|
15
16
|
openclew index Regenerate doc/_INDEX.md
|
|
16
17
|
openclew help Show this help
|
|
17
18
|
|
|
18
19
|
Options:
|
|
19
20
|
--no-hook Skip pre-commit hook installation (init)
|
|
20
21
|
--no-inject Skip instruction file injection (init)
|
|
22
|
+
|
|
23
|
+
Getting started:
|
|
24
|
+
npx openclew init 1. Set up doc/ + guide + examples + git hook
|
|
25
|
+
# Edit doc/_ARCHITECTURE.md 2. Replace the example with your project's architecture
|
|
26
|
+
openclew new "API design" 3. Create your own refdocs
|
|
27
|
+
git commit 4. Index auto-regenerates on commit
|
|
28
|
+
|
|
29
|
+
Docs have 3 levels: L1 (metadata) → L2 (summary) → L3 (details).
|
|
30
|
+
Agents read L1 to decide what's relevant, then L2 for context.
|
|
31
|
+
More at: https://github.com/openclew/openclew
|
|
21
32
|
`.trim();
|
|
22
33
|
|
|
23
34
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
@@ -29,6 +40,7 @@ const commands = {
|
|
|
29
40
|
init: () => require("../lib/init"),
|
|
30
41
|
new: () => require("../lib/new-doc"),
|
|
31
42
|
log: () => require("../lib/new-log"),
|
|
43
|
+
checkout: () => require("../lib/checkout"),
|
|
32
44
|
index: () => require("../lib/index-gen"),
|
|
33
45
|
};
|
|
34
46
|
|
package/hooks/generate-index.py
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"""
|
|
3
3
|
openclew index generator.
|
|
4
4
|
|
|
5
|
-
Scans doc/_*.md (
|
|
6
|
-
parses L1
|
|
5
|
+
Scans doc/_*.md (refdocs) and doc/log/*.md (logs),
|
|
6
|
+
parses metadata line + L1 blocks, and generates doc/_INDEX.md.
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
9
|
python generate-index.py # from project root
|
|
@@ -34,23 +34,67 @@ def find_doc_dir():
|
|
|
34
34
|
return doc_dir
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def
|
|
38
|
-
"""Extract
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
43
58
|
|
|
59
|
+
|
|
60
|
+
def parse_l1(content):
|
|
61
|
+
"""Extract L1 fields (subject, doc_brief) from L1_START/L1_END block."""
|
|
62
|
+
meta = {}
|
|
44
63
|
match = re.search(
|
|
45
64
|
r"<!--\s*L1_START\s*-->(.+?)<!--\s*L1_END\s*-->",
|
|
46
65
|
content,
|
|
47
66
|
re.DOTALL,
|
|
48
67
|
)
|
|
49
68
|
if not match:
|
|
50
|
-
return
|
|
69
|
+
return meta
|
|
51
70
|
|
|
52
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)."""
|
|
53
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)
|
|
54
98
|
for line in block.splitlines():
|
|
55
99
|
line = line.strip()
|
|
56
100
|
if line.startswith("#") or not line:
|
|
@@ -62,31 +106,56 @@ def parse_l1(filepath):
|
|
|
62
106
|
return meta
|
|
63
107
|
|
|
64
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
|
+
|
|
65
134
|
def collect_docs(doc_dir):
|
|
66
|
-
"""Collect
|
|
67
|
-
|
|
135
|
+
"""Collect refdocs and logs with their metadata."""
|
|
136
|
+
refdocs = []
|
|
68
137
|
logs = []
|
|
69
138
|
|
|
70
|
-
#
|
|
139
|
+
# Refdocs: doc/_*.md
|
|
71
140
|
for f in sorted(doc_dir.glob("_*.md")):
|
|
72
141
|
if f.name == "_INDEX.md":
|
|
73
142
|
continue
|
|
74
|
-
meta =
|
|
143
|
+
meta = parse_file(f)
|
|
75
144
|
if meta:
|
|
76
|
-
|
|
145
|
+
refdocs.append((f, meta))
|
|
77
146
|
|
|
78
147
|
# Log docs: doc/log/*.md
|
|
79
148
|
log_dir = doc_dir / "log"
|
|
80
149
|
if log_dir.is_dir():
|
|
81
150
|
for f in sorted(log_dir.glob("*.md"), reverse=True):
|
|
82
|
-
meta =
|
|
151
|
+
meta = parse_file(f)
|
|
83
152
|
if meta:
|
|
84
153
|
logs.append((f, meta))
|
|
85
154
|
|
|
86
|
-
return
|
|
155
|
+
return refdocs, logs
|
|
87
156
|
|
|
88
157
|
|
|
89
|
-
def generate_index(doc_dir,
|
|
158
|
+
def generate_index(doc_dir, refdocs, logs):
|
|
90
159
|
"""Generate _INDEX.md content."""
|
|
91
160
|
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
92
161
|
lines = [
|
|
@@ -97,13 +166,13 @@ def generate_index(doc_dir, permanents, logs):
|
|
|
97
166
|
f"",
|
|
98
167
|
]
|
|
99
168
|
|
|
100
|
-
#
|
|
101
|
-
lines.append("##
|
|
169
|
+
# Refdocs section
|
|
170
|
+
lines.append("## Refdocs")
|
|
102
171
|
lines.append("")
|
|
103
|
-
if
|
|
172
|
+
if refdocs:
|
|
104
173
|
lines.append("| Document | Subject | Status | Category |")
|
|
105
174
|
lines.append("|----------|---------|--------|----------|")
|
|
106
|
-
for f, meta in
|
|
175
|
+
for f, meta in refdocs:
|
|
107
176
|
name = f.name
|
|
108
177
|
subject = meta.get("subject", "—")
|
|
109
178
|
status = meta.get("status", "—")
|
|
@@ -111,7 +180,7 @@ def generate_index(doc_dir, permanents, logs):
|
|
|
111
180
|
rel_path = f.relative_to(doc_dir.parent)
|
|
112
181
|
lines.append(f"| [{name}]({rel_path}) | {subject} | {status} | {category} |")
|
|
113
182
|
else:
|
|
114
|
-
lines.append("_No
|
|
183
|
+
lines.append("_No refdocs yet. Create one with `templates/refdoc.md`._")
|
|
115
184
|
lines.append("")
|
|
116
185
|
|
|
117
186
|
# Logs section (last 20)
|
|
@@ -137,7 +206,7 @@ def generate_index(doc_dir, permanents, logs):
|
|
|
137
206
|
|
|
138
207
|
# Stats
|
|
139
208
|
lines.append("---")
|
|
140
|
-
lines.append(f"**{len(
|
|
209
|
+
lines.append(f"**{len(refdocs)}** refdocs, **{len(logs)}** logs.")
|
|
141
210
|
lines.append("")
|
|
142
211
|
|
|
143
212
|
return "\n".join(lines)
|
|
@@ -145,12 +214,12 @@ def generate_index(doc_dir, permanents, logs):
|
|
|
145
214
|
|
|
146
215
|
def main():
|
|
147
216
|
doc_dir = find_doc_dir()
|
|
148
|
-
|
|
149
|
-
index_content = generate_index(doc_dir,
|
|
217
|
+
refdocs, logs = collect_docs(doc_dir)
|
|
218
|
+
index_content = generate_index(doc_dir, refdocs, logs)
|
|
150
219
|
|
|
151
220
|
index_path = doc_dir / "_INDEX.md"
|
|
152
221
|
index_path.write_text(index_content, encoding="utf-8")
|
|
153
|
-
print(f"Generated {index_path} ({len(
|
|
222
|
+
print(f"Generated {index_path} ({len(refdocs)} refdocs, {len(logs)} logs)")
|
|
154
223
|
|
|
155
224
|
|
|
156
225
|
if __name__ == "__main__":
|