openclew 0.0.1 → 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.
- package/LICENSE +21 -0
- package/README.md +299 -0
- package/bin/openclew.js +41 -0
- package/hooks/generate-index.py +157 -0
- package/lib/detect.js +39 -0
- package/lib/index-gen.js +40 -0
- package/lib/init.js +212 -0
- package/lib/inject.js +38 -0
- package/lib/new-doc.js +36 -0
- package/lib/new-log.js +37 -0
- package/lib/templates.js +106 -0
- package/package.json +27 -2
- package/templates/log.md +36 -0
- package/templates/permanent.md +45 -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
|
+
| **Permanent** | `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
|
+
**Permanents** 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 permanent 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/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 -->
|
|
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/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
|
+
---
|
|
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
|
+
- **Permanents are living** — 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,41 @@
|
|
|
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 new permanent doc
|
|
14
|
+
openclew log <title> Create a new session log
|
|
15
|
+
openclew index Regenerate doc/_INDEX.md
|
|
16
|
+
openclew help Show this help
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--no-hook Skip pre-commit hook installation (init)
|
|
20
|
+
--no-inject Skip instruction file injection (init)
|
|
21
|
+
`.trim();
|
|
22
|
+
|
|
23
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
24
|
+
console.log(USAGE);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const commands = {
|
|
29
|
+
init: () => require("../lib/init"),
|
|
30
|
+
new: () => require("../lib/new-doc"),
|
|
31
|
+
log: () => require("../lib/new-log"),
|
|
32
|
+
index: () => require("../lib/index-gen"),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (!commands[command]) {
|
|
36
|
+
console.error(`Unknown command: ${command}`);
|
|
37
|
+
console.error(`Run 'openclew help' for usage.`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
commands[command]();
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
openclew index generator.
|
|
4
|
+
|
|
5
|
+
Scans doc/_*.md (permanents) 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 permanents and logs with their L1 metadata."""
|
|
67
|
+
permanents = []
|
|
68
|
+
logs = []
|
|
69
|
+
|
|
70
|
+
# Permanent 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
|
+
permanents.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 permanents, logs
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def generate_index(doc_dir, permanents, 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
|
+
# Permanents section
|
|
101
|
+
lines.append("## Permanent docs")
|
|
102
|
+
lines.append("")
|
|
103
|
+
if permanents:
|
|
104
|
+
lines.append("| Document | Subject | Status | Category |")
|
|
105
|
+
lines.append("|----------|---------|--------|----------|")
|
|
106
|
+
for f, meta in permanents:
|
|
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 permanent docs yet. Create one with `templates/permanent.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(permanents)}** permanent 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
|
+
permanents, logs = collect_docs(doc_dir)
|
|
149
|
+
index_content = generate_index(doc_dir, permanents, 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(permanents)} permanents, {len(logs)} logs)")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
main()
|
package/lib/detect.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect existing AI instruction files in the project root.
|
|
3
|
+
* Returns an array of { tool, file, path } objects.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
const INSTRUCTION_FILES = [
|
|
10
|
+
{ tool: "Claude Code", file: "CLAUDE.md" },
|
|
11
|
+
{ tool: "Cursor", file: ".cursorrules" },
|
|
12
|
+
{ tool: "Cursor", file: ".cursor/rules" },
|
|
13
|
+
{ tool: "GitHub Copilot", file: ".github/copilot-instructions.md" },
|
|
14
|
+
{ tool: "Windsurf", file: ".windsurfrules" },
|
|
15
|
+
{ tool: "Windsurf", file: ".windsurf/rules" },
|
|
16
|
+
{ tool: "Cline", file: ".clinerules" },
|
|
17
|
+
{ tool: "Codex / Gemini", file: "AGENTS.md" },
|
|
18
|
+
{ tool: "Aider", file: "CONVENTIONS.md" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function detectInstructionFiles(projectRoot) {
|
|
22
|
+
const found = [];
|
|
23
|
+
for (const entry of INSTRUCTION_FILES) {
|
|
24
|
+
const fullPath = path.join(projectRoot, entry.file);
|
|
25
|
+
if (fs.existsSync(fullPath)) {
|
|
26
|
+
const stat = fs.statSync(fullPath);
|
|
27
|
+
// For directories (.cursor/rules, .windsurf/rules), note it but don't inject
|
|
28
|
+
found.push({
|
|
29
|
+
tool: entry.tool,
|
|
30
|
+
file: entry.file,
|
|
31
|
+
fullPath,
|
|
32
|
+
isDir: stat.isDirectory(),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { detectInstructionFiles, INSTRUCTION_FILES };
|
package/lib/index-gen.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclew index — regenerate doc/_INDEX.md
|
|
3
|
+
*
|
|
4
|
+
* Wraps hooks/generate-index.py. Falls back to a JS implementation
|
|
5
|
+
* if Python is not available.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { execSync } = require("child_process");
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
|
|
12
|
+
const docDir = path.join(process.cwd(), "doc");
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(docDir)) {
|
|
15
|
+
console.error("No doc/ directory found. Run 'openclew init' first.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Try local generate-index.py first
|
|
20
|
+
const localScript = path.join(docDir, "generate-index.py");
|
|
21
|
+
const packageScript = path.join(__dirname, "..", "hooks", "generate-index.py");
|
|
22
|
+
const script = fs.existsSync(localScript) ? localScript : packageScript;
|
|
23
|
+
|
|
24
|
+
if (fs.existsSync(script)) {
|
|
25
|
+
try {
|
|
26
|
+
const output = execSync(`python3 "${script}" "${docDir}"`, {
|
|
27
|
+
encoding: "utf-8",
|
|
28
|
+
});
|
|
29
|
+
console.log(output.trim());
|
|
30
|
+
process.exit(0);
|
|
31
|
+
} catch {
|
|
32
|
+
console.error(
|
|
33
|
+
"python3 not available. Install Python 3.8+ or regenerate manually."
|
|
34
|
+
);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.error("generate-index.py not found. Run 'openclew init' first.");
|
|
40
|
+
process.exit(1);
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclew init — set up openclew in the current project.
|
|
3
|
+
*
|
|
4
|
+
* 1. Create doc/ and doc/log/
|
|
5
|
+
* 2. Detect existing instruction files
|
|
6
|
+
* 3. Propose to inject openclew block
|
|
7
|
+
* 4. Install pre-commit hook for index generation
|
|
8
|
+
* 5. Generate initial _INDEX.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const readline = require("readline");
|
|
14
|
+
const { detectInstructionFiles } = require("./detect");
|
|
15
|
+
const { inject, isAlreadyInjected } = require("./inject");
|
|
16
|
+
|
|
17
|
+
const PROJECT_ROOT = process.cwd();
|
|
18
|
+
const DOC_DIR = path.join(PROJECT_ROOT, "doc");
|
|
19
|
+
const LOG_DIR = path.join(DOC_DIR, "log");
|
|
20
|
+
const GIT_DIR = path.join(PROJECT_ROOT, ".git");
|
|
21
|
+
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
const noHook = args.includes("--no-hook");
|
|
24
|
+
const noInject = args.includes("--no-inject");
|
|
25
|
+
|
|
26
|
+
function ask(question) {
|
|
27
|
+
const rl = readline.createInterface({
|
|
28
|
+
input: process.stdin,
|
|
29
|
+
output: process.stdout,
|
|
30
|
+
});
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
rl.question(question, (answer) => {
|
|
33
|
+
rl.close();
|
|
34
|
+
resolve(answer.trim().toLowerCase());
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createDirs() {
|
|
40
|
+
let created = false;
|
|
41
|
+
if (!fs.existsSync(DOC_DIR)) {
|
|
42
|
+
fs.mkdirSync(DOC_DIR, { recursive: true });
|
|
43
|
+
console.log(" Created doc/");
|
|
44
|
+
created = true;
|
|
45
|
+
} else {
|
|
46
|
+
console.log(" doc/ already exists");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
50
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
51
|
+
console.log(" Created doc/log/");
|
|
52
|
+
created = true;
|
|
53
|
+
} else {
|
|
54
|
+
console.log(" doc/log/ already exists");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return created;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function installPreCommitHook() {
|
|
61
|
+
if (!fs.existsSync(GIT_DIR)) {
|
|
62
|
+
console.log(" No .git/ found — skipping hook installation");
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const hooksDir = path.join(GIT_DIR, "hooks");
|
|
67
|
+
if (!fs.existsSync(hooksDir)) {
|
|
68
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const preCommitPath = path.join(hooksDir, "pre-commit");
|
|
72
|
+
const indexScript = `python3 -c "
|
|
73
|
+
import subprocess, sys
|
|
74
|
+
try:
|
|
75
|
+
from pathlib import Path
|
|
76
|
+
# Try local hook first, then npx-installed
|
|
77
|
+
local = Path('hooks/generate-index.py')
|
|
78
|
+
if local.exists():
|
|
79
|
+
exec(local.read_text())
|
|
80
|
+
else:
|
|
81
|
+
# Fallback: run via npx
|
|
82
|
+
subprocess.run([sys.executable, '-c', 'from openclew import generate_index; generate_index()'], check=True)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f'openclew index generation skipped: {e}')
|
|
85
|
+
" 2>/dev/null
|
|
86
|
+
git add doc/_INDEX.md 2>/dev/null`;
|
|
87
|
+
|
|
88
|
+
const MARKER = "# openclew-index";
|
|
89
|
+
|
|
90
|
+
if (fs.existsSync(preCommitPath)) {
|
|
91
|
+
const existing = fs.readFileSync(preCommitPath, "utf-8");
|
|
92
|
+
if (existing.includes(MARKER)) {
|
|
93
|
+
console.log(" Pre-commit hook already contains openclew index generation");
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
// Append to existing hook
|
|
97
|
+
fs.appendFileSync(
|
|
98
|
+
preCommitPath,
|
|
99
|
+
`\n\n${MARKER}\n${indexScript}\n`,
|
|
100
|
+
"utf-8"
|
|
101
|
+
);
|
|
102
|
+
console.log(" Appended openclew index generation to existing pre-commit hook");
|
|
103
|
+
} else {
|
|
104
|
+
fs.writeFileSync(
|
|
105
|
+
preCommitPath,
|
|
106
|
+
`#!/bin/sh\n\n${MARKER}\n${indexScript}\n`,
|
|
107
|
+
"utf-8"
|
|
108
|
+
);
|
|
109
|
+
fs.chmodSync(preCommitPath, "755");
|
|
110
|
+
console.log(" Created pre-commit hook for index generation");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function copyGenerateIndex() {
|
|
117
|
+
const src = path.join(__dirname, "..", "hooks", "generate-index.py");
|
|
118
|
+
const dst = path.join(DOC_DIR, "generate-index.py");
|
|
119
|
+
|
|
120
|
+
if (fs.existsSync(dst)) {
|
|
121
|
+
console.log(" doc/generate-index.py already exists");
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (fs.existsSync(src)) {
|
|
126
|
+
fs.copyFileSync(src, dst);
|
|
127
|
+
console.log(" Copied generate-index.py to doc/");
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(" generate-index.py not found in package — skipping");
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function runIndexGenerator() {
|
|
136
|
+
const indexScript = path.join(DOC_DIR, "generate-index.py");
|
|
137
|
+
if (!fs.existsSync(indexScript)) return;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const { execSync } = require("child_process");
|
|
141
|
+
execSync(`python3 "${indexScript}" "${DOC_DIR}"`, { stdio: "pipe" });
|
|
142
|
+
console.log(" Generated doc/_INDEX.md");
|
|
143
|
+
} catch {
|
|
144
|
+
console.log(" Could not generate index (python3 not available)");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function main() {
|
|
149
|
+
console.log("\nopenclew init\n");
|
|
150
|
+
|
|
151
|
+
// Step 1: Create directories
|
|
152
|
+
console.log("1. Project structure");
|
|
153
|
+
createDirs();
|
|
154
|
+
|
|
155
|
+
// Step 2: Copy index generator
|
|
156
|
+
console.log("\n2. Index generator");
|
|
157
|
+
copyGenerateIndex();
|
|
158
|
+
|
|
159
|
+
// Step 3: Detect and inject into instruction files
|
|
160
|
+
console.log("\n3. Instruction files");
|
|
161
|
+
const found = detectInstructionFiles(PROJECT_ROOT);
|
|
162
|
+
|
|
163
|
+
if (found.length === 0) {
|
|
164
|
+
console.log(" No instruction file detected (CLAUDE.md, .cursorrules, AGENTS.md...)");
|
|
165
|
+
console.log(" You can manually add the openclew block later. See: openclew help");
|
|
166
|
+
} else if (noInject) {
|
|
167
|
+
console.log(" Detected:", found.map((f) => f.file).join(", "));
|
|
168
|
+
console.log(" Skipping injection (--no-inject)");
|
|
169
|
+
} else {
|
|
170
|
+
for (const entry of found) {
|
|
171
|
+
if (entry.isDir) {
|
|
172
|
+
console.log(` Found ${entry.file}/ (directory) — manual setup needed`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isAlreadyInjected(entry.fullPath)) {
|
|
177
|
+
console.log(` ${entry.file} already has openclew block`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const answer = await ask(
|
|
182
|
+
` Inject openclew block into ${entry.file}? [Y/n] `
|
|
183
|
+
);
|
|
184
|
+
if (answer === "" || answer === "y" || answer === "yes") {
|
|
185
|
+
inject(entry.fullPath);
|
|
186
|
+
console.log(` Injected into ${entry.file}`);
|
|
187
|
+
} else {
|
|
188
|
+
console.log(` Skipped ${entry.file}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Step 4: Pre-commit hook
|
|
194
|
+
console.log("\n4. Pre-commit hook");
|
|
195
|
+
if (noHook) {
|
|
196
|
+
console.log(" Skipping (--no-hook)");
|
|
197
|
+
} else {
|
|
198
|
+
installPreCommitHook();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Step 5: Generate initial index
|
|
202
|
+
console.log("\n5. Index");
|
|
203
|
+
runIndexGenerator();
|
|
204
|
+
|
|
205
|
+
// Done
|
|
206
|
+
console.log("\nDone. Next steps:");
|
|
207
|
+
console.log(" openclew new \"Architecture decisions\" — create your first doc");
|
|
208
|
+
console.log(" openclew log \"Setup auth\" — create a session log");
|
|
209
|
+
console.log("");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
main();
|
package/lib/inject.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inject openclew block into an existing instruction file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
|
|
7
|
+
const OPENCLEW_BLOCK = `
|
|
8
|
+
## Project knowledge (openclew)
|
|
9
|
+
|
|
10
|
+
Structured documentation lives in \`doc/\`. Each doc has 3 levels (L1/L2/L3).
|
|
11
|
+
|
|
12
|
+
**Before any task:** read \`doc/_INDEX.md\`, identify docs related to the task, read them.
|
|
13
|
+
|
|
14
|
+
- Permanent docs: \`doc/_*.md\` — living knowledge (architecture, conventions, decisions)
|
|
15
|
+
- Logs: \`doc/log/YYYY-MM-DD_*.md\` — frozen facts (what happened, never modified)
|
|
16
|
+
- Read L1 first to decide if you need L2/L3
|
|
17
|
+
`.trim();
|
|
18
|
+
|
|
19
|
+
const MARKER_START = "<!-- openclew_START -->";
|
|
20
|
+
const MARKER_END = "<!-- openclew_END -->";
|
|
21
|
+
|
|
22
|
+
function isAlreadyInjected(filePath) {
|
|
23
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
24
|
+
return content.includes(MARKER_START);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function inject(filePath) {
|
|
28
|
+
if (isAlreadyInjected(filePath)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
33
|
+
const block = `\n\n${MARKER_START}\n${OPENCLEW_BLOCK}\n${MARKER_END}\n`;
|
|
34
|
+
fs.writeFileSync(filePath, content + block, "utf-8");
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { inject, isAlreadyInjected, OPENCLEW_BLOCK, MARKER_START };
|
package/lib/new-doc.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclew new <title> — create a new permanent doc.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { permanentContent, slugify } = require("./templates");
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
// Remove "new" command from args
|
|
11
|
+
const cmdIndex = args.indexOf("new");
|
|
12
|
+
const titleArgs = cmdIndex >= 0 ? args.slice(cmdIndex + 1) : args.slice(1);
|
|
13
|
+
const title = titleArgs.join(" ");
|
|
14
|
+
|
|
15
|
+
if (!title) {
|
|
16
|
+
console.error('Usage: openclew new "Title of the document"');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const docDir = path.join(process.cwd(), "doc");
|
|
21
|
+
if (!fs.existsSync(docDir)) {
|
|
22
|
+
console.error("No doc/ directory found. Run 'openclew init' first.");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const slug = slugify(title);
|
|
27
|
+
const filename = `_${slug}.md`;
|
|
28
|
+
const filepath = path.join(docDir, filename);
|
|
29
|
+
|
|
30
|
+
if (fs.existsSync(filepath)) {
|
|
31
|
+
console.error(`File already exists: doc/${filename}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fs.writeFileSync(filepath, permanentContent(title), "utf-8");
|
|
36
|
+
console.log(`Created doc/${filename}`);
|
package/lib/new-log.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclew log <title> — create a new session log.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { logContent, slugifyLog, today } = require("./templates");
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
// Remove "log" command from args
|
|
11
|
+
const cmdIndex = args.indexOf("log");
|
|
12
|
+
const titleArgs = cmdIndex >= 0 ? args.slice(cmdIndex + 1) : args.slice(1);
|
|
13
|
+
const title = titleArgs.join(" ");
|
|
14
|
+
|
|
15
|
+
if (!title) {
|
|
16
|
+
console.error('Usage: openclew log "Title of the log"');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const logDir = path.join(process.cwd(), "doc", "log");
|
|
21
|
+
if (!fs.existsSync(logDir)) {
|
|
22
|
+
console.error("No doc/log/ directory found. Run 'openclew init' first.");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const slug = slugifyLog(title);
|
|
27
|
+
const date = today();
|
|
28
|
+
const filename = `${date}_${slug}.md`;
|
|
29
|
+
const filepath = path.join(logDir, filename);
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(filepath)) {
|
|
32
|
+
console.error(`File already exists: doc/log/${filename}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fs.writeFileSync(filepath, logContent(title), "utf-8");
|
|
37
|
+
console.log(`Created doc/log/${filename}`);
|
package/lib/templates.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template content for permanent docs and logs.
|
|
3
|
+
* Embedded here so the CLI works standalone without needing to locate template files.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function today() {
|
|
7
|
+
return new Date().toISOString().slice(0, 10);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function slugify(title) {
|
|
11
|
+
return title
|
|
12
|
+
.toUpperCase()
|
|
13
|
+
.replace(/[^A-Z0-9]+/g, "_")
|
|
14
|
+
.replace(/^_|_$/g, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function slugifyLog(title) {
|
|
18
|
+
return title
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
21
|
+
.replace(/^-|-$/g, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function permanentContent(title) {
|
|
25
|
+
const date = today();
|
|
26
|
+
return `<!-- L1_START -->
|
|
27
|
+
# L1 - Metadata
|
|
28
|
+
type: Reference
|
|
29
|
+
subject: ${title}
|
|
30
|
+
created: ${date}
|
|
31
|
+
updated: ${date}
|
|
32
|
+
short_story:
|
|
33
|
+
status: Active
|
|
34
|
+
category:
|
|
35
|
+
keywords: []
|
|
36
|
+
<!-- L1_END -->
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<!-- L2_START -->
|
|
41
|
+
# L2 - Summary
|
|
42
|
+
|
|
43
|
+
## Objective
|
|
44
|
+
<!-- Why this document exists -->
|
|
45
|
+
|
|
46
|
+
## Key points
|
|
47
|
+
<!-- 3-5 essential takeaways -->
|
|
48
|
+
<!-- L2_END -->
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
<!-- L3_START -->
|
|
53
|
+
# L3 - Details
|
|
54
|
+
|
|
55
|
+
<!-- Full technical content -->
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Changelog
|
|
60
|
+
|
|
61
|
+
| Date | Change |
|
|
62
|
+
|------|--------|
|
|
63
|
+
| ${date} | Initial creation |
|
|
64
|
+
<!-- L3_END -->
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function logContent(title) {
|
|
69
|
+
const date = today();
|
|
70
|
+
return `<!-- L1_START -->
|
|
71
|
+
# L1 - Metadata
|
|
72
|
+
date: ${date}
|
|
73
|
+
type: Feature
|
|
74
|
+
subject: ${title}
|
|
75
|
+
short_story:
|
|
76
|
+
status: In progress
|
|
77
|
+
category:
|
|
78
|
+
keywords: []
|
|
79
|
+
<!-- L1_END -->
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
<!-- L2_START -->
|
|
84
|
+
# L2 - Summary
|
|
85
|
+
|
|
86
|
+
## Objective
|
|
87
|
+
<!-- Why this work was undertaken -->
|
|
88
|
+
|
|
89
|
+
## Problem
|
|
90
|
+
<!-- What was observed -->
|
|
91
|
+
|
|
92
|
+
## Solution
|
|
93
|
+
<!-- How it was resolved -->
|
|
94
|
+
<!-- L2_END -->
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
<!-- L3_START -->
|
|
99
|
+
# L3 - Details
|
|
100
|
+
|
|
101
|
+
<!-- Technical details, code changes, debugging steps... -->
|
|
102
|
+
<!-- L3_END -->
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { permanentContent, logContent, slugify, slugifyLog, today };
|
package/package.json
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclew",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Long Life Memory for LLMs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Long Life Memory for LLMs — structured project knowledge for AI agents and humans",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openclew": "./bin/openclew.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"templates/",
|
|
13
|
+
"hooks/",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ai",
|
|
19
|
+
"llm",
|
|
20
|
+
"documentation",
|
|
21
|
+
"knowledge",
|
|
22
|
+
"memory",
|
|
23
|
+
"claude",
|
|
24
|
+
"cursor",
|
|
25
|
+
"copilot",
|
|
26
|
+
"agents"
|
|
27
|
+
],
|
|
6
28
|
"repository": {
|
|
7
29
|
"type": "git",
|
|
8
30
|
"url": "https://github.com/openclew/openclew"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=16"
|
|
9
34
|
}
|
|
10
35
|
}
|
package/templates/log.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!-- L1_START -->
|
|
2
|
+
# L1 - Metadata
|
|
3
|
+
date: YYYY-MM-DD
|
|
4
|
+
type: Bug | Feature | Refactor | Doc | Deploy
|
|
5
|
+
subject: Short title (< 60 chars)
|
|
6
|
+
short_story: 1-2 sentences. What happened and what was the outcome. Include conclusions, not just process.
|
|
7
|
+
status: Done | In progress | Abandoned
|
|
8
|
+
category: Main domain (e.g. Auth, API, Database, UI...)
|
|
9
|
+
keywords: [tag1, tag2, tag3]
|
|
10
|
+
<!-- L1_END -->
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<!-- L2_START -->
|
|
15
|
+
# L2 - Summary
|
|
16
|
+
|
|
17
|
+
## Objective
|
|
18
|
+
<!-- Why this work was undertaken -->
|
|
19
|
+
|
|
20
|
+
## Problem
|
|
21
|
+
<!-- What was observed, context of discovery -->
|
|
22
|
+
|
|
23
|
+
## Solution
|
|
24
|
+
<!-- How it was resolved -->
|
|
25
|
+
|
|
26
|
+
## Watch out
|
|
27
|
+
<!-- Side effects, edge cases, things to know -->
|
|
28
|
+
<!-- L2_END -->
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<!-- L3_START -->
|
|
33
|
+
# L3 - Details
|
|
34
|
+
|
|
35
|
+
<!-- Technical details: code changes, debugging steps, commands, references... -->
|
|
36
|
+
<!-- L3_END -->
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!-- L1_START -->
|
|
2
|
+
# L1 - Metadata
|
|
3
|
+
type: Reference | Architecture | Guide | Analysis
|
|
4
|
+
subject: Short title (< 60 chars)
|
|
5
|
+
created: YYYY-MM-DD
|
|
6
|
+
updated: YYYY-MM-DD
|
|
7
|
+
short_story: 1-2 sentences. What this doc covers and what it concludes. Must be enough to decide if you need to read further.
|
|
8
|
+
status: Active | Stable | Archived
|
|
9
|
+
category: Main domain (e.g. Auth, API, Database, UI...)
|
|
10
|
+
keywords: [tag1, tag2, tag3]
|
|
11
|
+
<!-- L1_END -->
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<!-- L2_START -->
|
|
16
|
+
# L2 - Summary
|
|
17
|
+
|
|
18
|
+
## Objective
|
|
19
|
+
<!-- Why this document exists, what need it covers -->
|
|
20
|
+
|
|
21
|
+
## Key points
|
|
22
|
+
<!-- 3-5 essential takeaways -->
|
|
23
|
+
|
|
24
|
+
## Solution
|
|
25
|
+
<!-- Recommended approach or pattern -->
|
|
26
|
+
|
|
27
|
+
## Watch out
|
|
28
|
+
<!-- Common pitfalls, edge cases, things to be aware of -->
|
|
29
|
+
<!-- L2_END -->
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<!-- L3_START -->
|
|
34
|
+
# L3 - Details
|
|
35
|
+
|
|
36
|
+
<!-- Full technical content: code examples, diagrams, references, deep dives... -->
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Changelog
|
|
41
|
+
|
|
42
|
+
| Date | Change |
|
|
43
|
+
|------|--------|
|
|
44
|
+
| YYYY-MM-DD | Initial creation |
|
|
45
|
+
<!-- L3_END -->
|