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 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
- | **Permanent** | `doc/_SUBJECT.md` | Living knowledge (architecture, conventions, decisions) | Updated over time |
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
- **Permanents** are your project's brain — they evolve as the project evolves.
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 permanent docs tell you where you are. The logs tell you how you got here.
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 (5 minutes)
83
+ ## Quick start (2 minutes)
84
84
 
85
- ### 1. Create the structure
85
+ ### 1. Install
86
86
 
87
87
  ```bash
88
- mkdir -p doc/log
88
+ npx openclew init
89
89
  ```
90
90
 
91
- ### 2. Copy the templates
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
- Download from [`templates/`](templates/) or create manually:
97
+ ### 2. Start a session with your agent
94
98
 
95
- <details>
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
- ## Key points
120
- <!-- 3-5 essential takeaways -->
101
+ > Read doc/_USING_OPENCLEW.md and document our architecture.
121
102
 
122
- ## Solution
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
- ## Changelog
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
- </details>
109
+ The index auto-regenerates on every commit. Never edit it manually.
142
110
 
143
111
  <details>
144
- <summary><b>templates/log.md</b> — for frozen facts</summary>
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
- ## Problem
164
- <!-- What was observed -->
165
-
166
- ## Solution
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
- - **Permanents are living** — They evolve as the project evolves.
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 new permanent doc
14
- openclew log <title> Create a new session log
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
 
@@ -2,8 +2,8 @@
2
2
  """
3
3
  openclew index generator.
4
4
 
5
- Scans doc/_*.md (permanents) and doc/log/*.md (logs),
6
- parses L1 metadata blocks, and generates doc/_INDEX.md.
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 parse_l1(filepath):
38
- """Extract L1 metadata from a file."""
39
- try:
40
- content = filepath.read_text(encoding="utf-8")
41
- except (OSError, UnicodeDecodeError):
42
- return None
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 None
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 permanents and logs with their L1 metadata."""
67
- permanents = []
135
+ """Collect refdocs and logs with their metadata."""
136
+ refdocs = []
68
137
  logs = []
69
138
 
70
- # Permanent docs: doc/_*.md
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 = parse_l1(f)
143
+ meta = parse_file(f)
75
144
  if meta:
76
- permanents.append((f, meta))
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 = parse_l1(f)
151
+ meta = parse_file(f)
83
152
  if meta:
84
153
  logs.append((f, meta))
85
154
 
86
- return permanents, logs
155
+ return refdocs, logs
87
156
 
88
157
 
89
- def generate_index(doc_dir, permanents, logs):
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
- # Permanents section
101
- lines.append("## Permanent docs")
169
+ # Refdocs section
170
+ lines.append("## Refdocs")
102
171
  lines.append("")
103
- if permanents:
172
+ if refdocs:
104
173
  lines.append("| Document | Subject | Status | Category |")
105
174
  lines.append("|----------|---------|--------|----------|")
106
- for f, meta in permanents:
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 permanent docs yet. Create one with `templates/permanent.md`._")
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(permanents)}** permanent docs, **{len(logs)}** logs.")
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
- permanents, logs = collect_docs(doc_dir)
149
- index_content = generate_index(doc_dir, permanents, logs)
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(permanents)} permanents, {len(logs)} logs)")
222
+ print(f"Generated {index_path} ({len(refdocs)} refdocs, {len(logs)} logs)")
154
223
 
155
224
 
156
225
  if __name__ == "__main__":