openclew 0.0.1 → 0.2.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 +21 -0
- package/README.md +299 -0
- package/bin/openclew.js +53 -0
- package/hooks/generate-index.py +157 -0
- package/lib/checkout.js +288 -0
- package/lib/config.js +34 -0
- package/lib/detect.js +74 -0
- package/lib/index-gen.js +40 -0
- package/lib/init.js +273 -0
- package/lib/inject.js +42 -0
- package/lib/new-doc.js +46 -0
- package/lib/new-log.js +48 -0
- package/lib/templates.js +368 -0
- package/package.json +27 -2
- package/templates/living.md +45 -0
- package/templates/log.md +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 R.AlphA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# openclew
|
|
2
|
+
|
|
3
|
+
> Long Life Memory for LLMs
|
|
4
|
+
|
|
5
|
+
**Your agent forgets. Your project remembers.**
|
|
6
|
+
|
|
7
|
+
In Greek mythology, Ariadne gave Theseus a *clew* — a ball of thread — to find his way out of the Minotaur's labyrinth. That thread is the etymological origin of the word "clue." It wasn't a map. It wasn't a search engine. It was a continuous trail that connected where you've been to where you are.
|
|
8
|
+
|
|
9
|
+
That's what openclew does for your project. Every decision, every architectural choice, every hard-won lesson — laid down as a thread that any reader (human or AI) can follow. Not scattered across wikis, chat logs, and CLAUDE.md files that grow until they're unreadable. One trail. One source of truth.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Why this exists
|
|
14
|
+
|
|
15
|
+
AI agents are powerful, but they're amnesiac. Every new session starts from zero. The usual fixes don't work:
|
|
16
|
+
|
|
17
|
+
| Approach | What goes wrong |
|
|
18
|
+
|----------|----------------|
|
|
19
|
+
| CLAUDE.md / .cursorrules | Grows into an unreadable wall of text. Agent loads everything, wastes tokens on irrelevant context |
|
|
20
|
+
| Agent memory (Claude, Copilot) | Opaque, not versioned, not shareable with the team |
|
|
21
|
+
| Wiki / Notion | Disconnected from the code, goes stale |
|
|
22
|
+
| README.md | Not structured for AI consumption |
|
|
23
|
+
| Nothing | Re-explain everything every session |
|
|
24
|
+
|
|
25
|
+
The deeper problem isn't *storage* — it's **navigation**. A project with 50 documents and 200K tokens of knowledge can't be loaded in full. The real question an agent (or a human) needs to answer is:
|
|
26
|
+
|
|
27
|
+
> **"Should I read this document?"**
|
|
28
|
+
|
|
29
|
+
Not "does this file contain the word `auth`?" — that's pattern matching. The question is about *relevance*. And you can only answer it if documents are designed to be skimmed before they're read.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## The idea: 3 levels of depth
|
|
34
|
+
|
|
35
|
+
Every openclew document has 3 levels. Same file, different depths — for different needs.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
┌─────────────────────────────────────────────┐
|
|
39
|
+
│ L1 — Metadata │
|
|
40
|
+
│ type, subject, status, keywords │
|
|
41
|
+
│ → "Should I read this?" — decidable in │
|
|
42
|
+
│ 2 seconds, ~40 tokens per doc │
|
|
43
|
+
│ → Auto-indexed, machine-parseable │
|
|
44
|
+
├─────────────────────────────────────────────┤
|
|
45
|
+
│ L2 — Summary │
|
|
46
|
+
│ Objective, key points, solution │
|
|
47
|
+
│ → The full picture in 30 seconds │
|
|
48
|
+
│ → Enough for most decisions │
|
|
49
|
+
├─────────────────────────────────────────────┤
|
|
50
|
+
│ L3 — Details │
|
|
51
|
+
│ Code, examples, history, edge cases │
|
|
52
|
+
│ → Deep-dive only when actually needed │
|
|
53
|
+
│ → Most readers never go here │
|
|
54
|
+
└─────────────────────────────────────────────┘
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This isn't just an organizational trick — it's a **token efficiency strategy**. A project with 50 docs:
|
|
58
|
+
|
|
59
|
+
| Strategy | Tokens consumed | Relevance |
|
|
60
|
+
|----------|----------------|-----------|
|
|
61
|
+
| Load everything | ~200K | Mostly noise |
|
|
62
|
+
| Grep for keywords | Variable | Misses context, false positives |
|
|
63
|
+
| **Read all L1s, then L2 of relevant docs** | **~2K + 2-3 docs** | **Precise, contextual** |
|
|
64
|
+
|
|
65
|
+
L1 answers "should I read this?" L2 answers "what do I need to know?" L3 is there when you need the details. Most of the time, you don't.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Two types of docs
|
|
70
|
+
|
|
71
|
+
| Type | Location | Role | Mutability |
|
|
72
|
+
|------|----------|------|------------|
|
|
73
|
+
| **Living** | `doc/_SUBJECT.md` | Living knowledge (architecture, conventions, decisions) | Updated over time |
|
|
74
|
+
| **Log** | `doc/log/YYYY-MM-DD_subject.md` | Frozen facts (what happened, what was decided) | Never modified |
|
|
75
|
+
|
|
76
|
+
**Living docs** are your project's brain — they evolve as the project evolves.
|
|
77
|
+
**Logs** are your project's journal — immutable records of what happened and why.
|
|
78
|
+
|
|
79
|
+
Together, they form the thread. The living docs tell you where you are. The logs tell you how you got here.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Quick start (5 minutes)
|
|
84
|
+
|
|
85
|
+
### 1. Create the structure
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
mkdir -p doc/log
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 2. Copy the templates
|
|
92
|
+
|
|
93
|
+
Download from [`templates/`](templates/) or create manually:
|
|
94
|
+
|
|
95
|
+
<details>
|
|
96
|
+
<summary><b>templates/living.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 -->
|
|
118
|
+
|
|
119
|
+
## Key points
|
|
120
|
+
<!-- 3-5 essential takeaways -->
|
|
121
|
+
|
|
122
|
+
## Solution
|
|
123
|
+
<!-- Recommended approach or pattern -->
|
|
124
|
+
<!-- L2_END -->
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
<!-- L3_START -->
|
|
129
|
+
# L3 - Details
|
|
130
|
+
|
|
131
|
+
<!-- Full technical content: examples, code, references... -->
|
|
132
|
+
|
|
133
|
+
## Changelog
|
|
134
|
+
|
|
135
|
+
| Date | Change |
|
|
136
|
+
|------|--------|
|
|
137
|
+
| YYYY-MM-DD | Initial creation |
|
|
138
|
+
<!-- L3_END -->
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
</details>
|
|
142
|
+
|
|
143
|
+
<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
|
|
162
|
+
|
|
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
|
+
```
|
|
178
|
+
|
|
179
|
+
</details>
|
|
180
|
+
|
|
181
|
+
### 3. Write your first doc
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
cp templates/living.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
|
+
- Living 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
|
+
---
|
|
220
|
+
|
|
221
|
+
## How it works in practice
|
|
222
|
+
|
|
223
|
+
**Session 1** — You're setting up auth:
|
|
224
|
+
```
|
|
225
|
+
doc/
|
|
226
|
+
├── _ARCHITECTURE.md # Your stack, main patterns
|
|
227
|
+
└── log/
|
|
228
|
+
└── 2026-03-07_setup-auth.md # What you did, decisions made
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Session 5** — New agent session, different feature:
|
|
232
|
+
```
|
|
233
|
+
Agent reads doc/_INDEX.md (auto-generated)
|
|
234
|
+
→ Scans all L1s: "Should I read this?"
|
|
235
|
+
→ _ARCHITECTURE.md → yes → reads L2
|
|
236
|
+
→ setup-auth log → relevant → reads L2
|
|
237
|
+
→ Skips the rest
|
|
238
|
+
→ Full context in ~1K tokens instead of 50K
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Session 20** — Your project has grown:
|
|
242
|
+
```
|
|
243
|
+
doc/
|
|
244
|
+
├── _INDEX.md # Auto-generated, 30 entries
|
|
245
|
+
├── _ARCHITECTURE.md # Updated 12 times
|
|
246
|
+
├── _AUTH.md # Extracted when auth got complex
|
|
247
|
+
├── _API_CONVENTIONS.md # Team conventions
|
|
248
|
+
├── _KNOWN_ISSUES.md # Active gotchas
|
|
249
|
+
└── log/
|
|
250
|
+
├── 2026-03-07_setup-auth.md
|
|
251
|
+
├── 2026-03-10_migrate-db.md
|
|
252
|
+
├── 2026-03-15_fix-token-refresh.md
|
|
253
|
+
└── ... (20 more)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
30 docs. The agent scans all L1s in 2 seconds, reads the 3 that matter, and starts working with full context. A new teammate does the same — reads L2s to get up to speed in minutes. Same docs, same truth, different depth.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Principles
|
|
261
|
+
|
|
262
|
+
- **"Should I read this?"** — L1 exists to answer this question. If it can't, the L1 is poorly written.
|
|
263
|
+
- **Shared knowledge** — Same docs for humans and AI. One source, multiple readers.
|
|
264
|
+
- **SSOT** (Single Source of Truth) — Each piece of information lives in one place.
|
|
265
|
+
- **Logs are immutable** — Once written, never modified. Frozen facts.
|
|
266
|
+
- **Living docs evolve** — They evolve as the project evolves.
|
|
267
|
+
- **Index is auto-generated** — Never edit `_INDEX.md` manually.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Works with everything
|
|
272
|
+
|
|
273
|
+
**AI agents:** Claude Code, Cursor, Copilot, Windsurf, Codex, Zed, Kiro, Aider, Cline, Gemini CLI...
|
|
274
|
+
|
|
275
|
+
**Workflow frameworks:** BMAD, Spec Kit, or any methodology — openclew handles knowledge, your framework handles process.
|
|
276
|
+
|
|
277
|
+
**It's just Markdown.** No runtime, no dependencies, no lock-in. Git-versioned, diffable, reviewable in PRs. If you stop using it, the docs are still useful — to humans and agents alike.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Compared to alternatives
|
|
282
|
+
|
|
283
|
+
| Feature | CLAUDE.md | Cline Memory Bank | BMAD | openclew |
|
|
284
|
+
|---------|-----------|-------------------|------|----------|
|
|
285
|
+
| Readable by humans AND agents | partial | partial | yes | **yes** |
|
|
286
|
+
| Levels of depth (L1/L2/L3) | - | - | - | **yes** |
|
|
287
|
+
| "Should I read this?" (L1 triage) | - | - | - | **yes** |
|
|
288
|
+
| Token-efficient navigation | - | - | partial | **yes** |
|
|
289
|
+
| Auto-generated index | - | - | CSV | **yes** |
|
|
290
|
+
| Immutable logs | - | - | - | **yes** |
|
|
291
|
+
| Git-versioned | yes | yes | yes | **yes** |
|
|
292
|
+
| Cross-project | - | - | - | **yes** |
|
|
293
|
+
| Tool-agnostic | Claude only | Cline only | multi | **yes** |
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## License
|
|
298
|
+
|
|
299
|
+
MIT — use it however you want.
|
package/bin/openclew.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { resolve } = require("path");
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const command = args[0];
|
|
7
|
+
|
|
8
|
+
const USAGE = `
|
|
9
|
+
openclew — Long Life Memory for LLMs
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
openclew init Set up openclew in the current project
|
|
13
|
+
openclew new <title> Create a living doc (evolves with the project)
|
|
14
|
+
openclew log <title> Create a session log (frozen facts)
|
|
15
|
+
openclew checkout End-of-session summary + log creation
|
|
16
|
+
openclew index Regenerate doc/_INDEX.md
|
|
17
|
+
openclew help Show this help
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--no-hook Skip pre-commit hook installation (init)
|
|
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 living docs
|
|
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
|
|
32
|
+
`.trim();
|
|
33
|
+
|
|
34
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
35
|
+
console.log(USAGE);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const commands = {
|
|
40
|
+
init: () => require("../lib/init"),
|
|
41
|
+
new: () => require("../lib/new-doc"),
|
|
42
|
+
log: () => require("../lib/new-log"),
|
|
43
|
+
checkout: () => require("../lib/checkout"),
|
|
44
|
+
index: () => require("../lib/index-gen"),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (!commands[command]) {
|
|
48
|
+
console.error(`Unknown command: ${command}`);
|
|
49
|
+
console.error(`Run 'openclew help' for usage.`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
commands[command]();
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
openclew index generator.
|
|
4
|
+
|
|
5
|
+
Scans doc/_*.md (living docs) and doc/log/*.md (logs),
|
|
6
|
+
parses L1 metadata 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_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
|
|
43
|
+
|
|
44
|
+
match = re.search(
|
|
45
|
+
r"<!--\s*L1_START\s*-->(.+?)<!--\s*L1_END\s*-->",
|
|
46
|
+
content,
|
|
47
|
+
re.DOTALL,
|
|
48
|
+
)
|
|
49
|
+
if not match:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
block = match.group(1)
|
|
53
|
+
meta = {}
|
|
54
|
+
for line in block.splitlines():
|
|
55
|
+
line = line.strip()
|
|
56
|
+
if line.startswith("#") or not line:
|
|
57
|
+
continue
|
|
58
|
+
if ":" in line:
|
|
59
|
+
key, _, value = line.partition(":")
|
|
60
|
+
meta[key.strip().lower()] = value.strip()
|
|
61
|
+
|
|
62
|
+
return meta
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def collect_docs(doc_dir):
|
|
66
|
+
"""Collect living docs and logs with their L1 metadata."""
|
|
67
|
+
living_docs = []
|
|
68
|
+
logs = []
|
|
69
|
+
|
|
70
|
+
# Living docs: doc/_*.md
|
|
71
|
+
for f in sorted(doc_dir.glob("_*.md")):
|
|
72
|
+
if f.name == "_INDEX.md":
|
|
73
|
+
continue
|
|
74
|
+
meta = parse_l1(f)
|
|
75
|
+
if meta:
|
|
76
|
+
living_docs.append((f, meta))
|
|
77
|
+
|
|
78
|
+
# Log docs: doc/log/*.md
|
|
79
|
+
log_dir = doc_dir / "log"
|
|
80
|
+
if log_dir.is_dir():
|
|
81
|
+
for f in sorted(log_dir.glob("*.md"), reverse=True):
|
|
82
|
+
meta = parse_l1(f)
|
|
83
|
+
if meta:
|
|
84
|
+
logs.append((f, meta))
|
|
85
|
+
|
|
86
|
+
return living_docs, logs
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def generate_index(doc_dir, living_docs, logs):
|
|
90
|
+
"""Generate _INDEX.md content."""
|
|
91
|
+
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
92
|
+
lines = [
|
|
93
|
+
f"# Project Knowledge Index",
|
|
94
|
+
f"",
|
|
95
|
+
f"> Auto-generated by [openclew](https://github.com/openclew/openclew) on {now}.",
|
|
96
|
+
f"> Do not edit manually — rebuilt from L1 metadata on every commit.",
|
|
97
|
+
f"",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
# Living docs section
|
|
101
|
+
lines.append("## Living docs")
|
|
102
|
+
lines.append("")
|
|
103
|
+
if living_docs:
|
|
104
|
+
lines.append("| Document | Subject | Status | Category |")
|
|
105
|
+
lines.append("|----------|---------|--------|----------|")
|
|
106
|
+
for f, meta in living_docs:
|
|
107
|
+
name = f.name
|
|
108
|
+
subject = meta.get("subject", "—")
|
|
109
|
+
status = meta.get("status", "—")
|
|
110
|
+
category = meta.get("category", "—")
|
|
111
|
+
rel_path = f.relative_to(doc_dir.parent)
|
|
112
|
+
lines.append(f"| [{name}]({rel_path}) | {subject} | {status} | {category} |")
|
|
113
|
+
else:
|
|
114
|
+
lines.append("_No living docs yet. Create one with `templates/living.md`._")
|
|
115
|
+
lines.append("")
|
|
116
|
+
|
|
117
|
+
# Logs section (last 20)
|
|
118
|
+
lines.append("## Recent logs")
|
|
119
|
+
lines.append("")
|
|
120
|
+
display_logs = logs[:20]
|
|
121
|
+
if display_logs:
|
|
122
|
+
lines.append("| Date | Subject | Status | Category |")
|
|
123
|
+
lines.append("|------|---------|--------|----------|")
|
|
124
|
+
for f, meta in display_logs:
|
|
125
|
+
date = meta.get("date", f.stem[:10])
|
|
126
|
+
subject = meta.get("subject", "—")
|
|
127
|
+
status = meta.get("status", "—")
|
|
128
|
+
category = meta.get("category", "—")
|
|
129
|
+
rel_path = f.relative_to(doc_dir.parent)
|
|
130
|
+
lines.append(f"| {date} | [{subject}]({rel_path}) | {status} | {category} |")
|
|
131
|
+
if len(logs) > 20:
|
|
132
|
+
lines.append(f"")
|
|
133
|
+
lines.append(f"_{len(logs) - 20} older logs not shown._")
|
|
134
|
+
else:
|
|
135
|
+
lines.append("_No logs yet. Create one with `templates/log.md`._")
|
|
136
|
+
lines.append("")
|
|
137
|
+
|
|
138
|
+
# Stats
|
|
139
|
+
lines.append("---")
|
|
140
|
+
lines.append(f"**{len(living_docs)}** living docs, **{len(logs)}** logs.")
|
|
141
|
+
lines.append("")
|
|
142
|
+
|
|
143
|
+
return "\n".join(lines)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main():
|
|
147
|
+
doc_dir = find_doc_dir()
|
|
148
|
+
living_docs, logs = collect_docs(doc_dir)
|
|
149
|
+
index_content = generate_index(doc_dir, living_docs, logs)
|
|
150
|
+
|
|
151
|
+
index_path = doc_dir / "_INDEX.md"
|
|
152
|
+
index_path.write_text(index_content, encoding="utf-8")
|
|
153
|
+
print(f"Generated {index_path} ({len(living_docs)} living docs, {len(logs)} logs)")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
main()
|