openclew 0.1.0 → 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/README.md +7 -7
- package/bin/openclew.js +14 -2
- package/hooks/generate-index.py +16 -16
- package/lib/checkout.js +288 -0
- package/lib/config.js +34 -0
- package/lib/detect.js +38 -3
- package/lib/init.js +131 -70
- package/lib/inject.js +9 -5
- package/lib/new-doc.js +14 -4
- package/lib/new-log.js +12 -1
- package/lib/templates.js +265 -3
- package/package.json +1 -1
- /package/templates/{permanent.md → living.md} +0 -0
package/README.md
CHANGED
|
@@ -70,13 +70,13 @@ 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
|
+
| **Living** | `doc/_SUBJECT.md` | Living 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
|
+
**Living docs** 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 living docs tell you where you are. The logs tell you how you got here.
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
|
@@ -93,7 +93,7 @@ mkdir -p doc/log
|
|
|
93
93
|
Download from [`templates/`](templates/) or create manually:
|
|
94
94
|
|
|
95
95
|
<details>
|
|
96
|
-
<summary><b>templates/
|
|
96
|
+
<summary><b>templates/living.md</b> — for living knowledge</summary>
|
|
97
97
|
|
|
98
98
|
```markdown
|
|
99
99
|
<!-- L1_START -->
|
|
@@ -181,7 +181,7 @@ keywords: [tag1, tag2, tag3]
|
|
|
181
181
|
### 3. Write your first doc
|
|
182
182
|
|
|
183
183
|
```bash
|
|
184
|
-
cp templates/
|
|
184
|
+
cp templates/living.md doc/_ARCHITECTURE.md
|
|
185
185
|
```
|
|
186
186
|
|
|
187
187
|
Edit it — describe your project's architecture. Fill in L1 (metadata), L2 (summary), skip L3 if you don't need it yet.
|
|
@@ -195,7 +195,7 @@ Add this to your `CLAUDE.md`, `.cursorrules`, or `AGENTS.md`:
|
|
|
195
195
|
|
|
196
196
|
Documentation lives in `doc/`. Each doc has 3 levels (L1/L2/L3).
|
|
197
197
|
- Read L1 first to decide if you need more
|
|
198
|
-
-
|
|
198
|
+
- Living docs: `doc/_*.md` (living knowledge, updated)
|
|
199
199
|
- Logs: `doc/log/YYYY-MM-DD_*.md` (frozen facts, never modified)
|
|
200
200
|
- Index: `doc/_INDEX.md` (auto-generated, start here)
|
|
201
201
|
```
|
|
@@ -263,7 +263,7 @@ doc/
|
|
|
263
263
|
- **Shared knowledge** — Same docs for humans and AI. One source, multiple readers.
|
|
264
264
|
- **SSOT** (Single Source of Truth) — Each piece of information lives in one place.
|
|
265
265
|
- **Logs are immutable** — Once written, never modified. Frozen facts.
|
|
266
|
-
- **
|
|
266
|
+
- **Living docs evolve** — They evolve as the project evolves.
|
|
267
267
|
- **Index is auto-generated** — Never edit `_INDEX.md` manually.
|
|
268
268
|
|
|
269
269
|
---
|
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 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
|
|
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 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
|
|
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,7 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
openclew index generator.
|
|
4
4
|
|
|
5
|
-
Scans doc/_*.md (
|
|
5
|
+
Scans doc/_*.md (living docs) and doc/log/*.md (logs),
|
|
6
6
|
parses L1 metadata blocks, and generates doc/_INDEX.md.
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
@@ -63,17 +63,17 @@ def parse_l1(filepath):
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def collect_docs(doc_dir):
|
|
66
|
-
"""Collect
|
|
67
|
-
|
|
66
|
+
"""Collect living docs and logs with their L1 metadata."""
|
|
67
|
+
living_docs = []
|
|
68
68
|
logs = []
|
|
69
69
|
|
|
70
|
-
#
|
|
70
|
+
# Living docs: doc/_*.md
|
|
71
71
|
for f in sorted(doc_dir.glob("_*.md")):
|
|
72
72
|
if f.name == "_INDEX.md":
|
|
73
73
|
continue
|
|
74
74
|
meta = parse_l1(f)
|
|
75
75
|
if meta:
|
|
76
|
-
|
|
76
|
+
living_docs.append((f, meta))
|
|
77
77
|
|
|
78
78
|
# Log docs: doc/log/*.md
|
|
79
79
|
log_dir = doc_dir / "log"
|
|
@@ -83,10 +83,10 @@ def collect_docs(doc_dir):
|
|
|
83
83
|
if meta:
|
|
84
84
|
logs.append((f, meta))
|
|
85
85
|
|
|
86
|
-
return
|
|
86
|
+
return living_docs, logs
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
def generate_index(doc_dir,
|
|
89
|
+
def generate_index(doc_dir, living_docs, logs):
|
|
90
90
|
"""Generate _INDEX.md content."""
|
|
91
91
|
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
92
92
|
lines = [
|
|
@@ -97,13 +97,13 @@ def generate_index(doc_dir, permanents, logs):
|
|
|
97
97
|
f"",
|
|
98
98
|
]
|
|
99
99
|
|
|
100
|
-
#
|
|
101
|
-
lines.append("##
|
|
100
|
+
# Living docs section
|
|
101
|
+
lines.append("## Living docs")
|
|
102
102
|
lines.append("")
|
|
103
|
-
if
|
|
103
|
+
if living_docs:
|
|
104
104
|
lines.append("| Document | Subject | Status | Category |")
|
|
105
105
|
lines.append("|----------|---------|--------|----------|")
|
|
106
|
-
for f, meta in
|
|
106
|
+
for f, meta in living_docs:
|
|
107
107
|
name = f.name
|
|
108
108
|
subject = meta.get("subject", "—")
|
|
109
109
|
status = meta.get("status", "—")
|
|
@@ -111,7 +111,7 @@ def generate_index(doc_dir, permanents, logs):
|
|
|
111
111
|
rel_path = f.relative_to(doc_dir.parent)
|
|
112
112
|
lines.append(f"| [{name}]({rel_path}) | {subject} | {status} | {category} |")
|
|
113
113
|
else:
|
|
114
|
-
lines.append("_No
|
|
114
|
+
lines.append("_No living docs yet. Create one with `templates/living.md`._")
|
|
115
115
|
lines.append("")
|
|
116
116
|
|
|
117
117
|
# Logs section (last 20)
|
|
@@ -137,7 +137,7 @@ def generate_index(doc_dir, permanents, logs):
|
|
|
137
137
|
|
|
138
138
|
# Stats
|
|
139
139
|
lines.append("---")
|
|
140
|
-
lines.append(f"**{len(
|
|
140
|
+
lines.append(f"**{len(living_docs)}** living docs, **{len(logs)}** logs.")
|
|
141
141
|
lines.append("")
|
|
142
142
|
|
|
143
143
|
return "\n".join(lines)
|
|
@@ -145,12 +145,12 @@ def generate_index(doc_dir, permanents, logs):
|
|
|
145
145
|
|
|
146
146
|
def main():
|
|
147
147
|
doc_dir = find_doc_dir()
|
|
148
|
-
|
|
149
|
-
index_content = generate_index(doc_dir,
|
|
148
|
+
living_docs, logs = collect_docs(doc_dir)
|
|
149
|
+
index_content = generate_index(doc_dir, living_docs, logs)
|
|
150
150
|
|
|
151
151
|
index_path = doc_dir / "_INDEX.md"
|
|
152
152
|
index_path.write_text(index_content, encoding="utf-8")
|
|
153
|
-
print(f"Generated {index_path} ({len(
|
|
153
|
+
print(f"Generated {index_path} ({len(living_docs)} living docs, {len(logs)} logs)")
|
|
154
154
|
|
|
155
155
|
|
|
156
156
|
if __name__ == "__main__":
|
package/lib/checkout.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclew checkout — end-of-session summary + log creation.
|
|
3
|
+
*
|
|
4
|
+
* 1. Collect git activity (today's commits, uncommitted changes)
|
|
5
|
+
* 2. Display summary table
|
|
6
|
+
* 3. Create a session log pre-filled with the activity
|
|
7
|
+
* 4. Regenerate the index
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const { execSync } = require("child_process");
|
|
13
|
+
const { logContent, slugifyLog, today } = require("./templates");
|
|
14
|
+
const { readConfig } = require("./config");
|
|
15
|
+
|
|
16
|
+
const PROJECT_ROOT = process.cwd();
|
|
17
|
+
const DOC_DIR = path.join(PROJECT_ROOT, "doc");
|
|
18
|
+
const LOG_DIR = path.join(DOC_DIR, "log");
|
|
19
|
+
|
|
20
|
+
function run(cmd) {
|
|
21
|
+
try {
|
|
22
|
+
return execSync(cmd, { cwd: PROJECT_ROOT, encoding: "utf-8" }).trim();
|
|
23
|
+
} catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function collectGitActivity() {
|
|
29
|
+
const date = today();
|
|
30
|
+
|
|
31
|
+
// Today's commits
|
|
32
|
+
const commitLog = run(
|
|
33
|
+
`git log --since="${date} 00:00" --format="%h %s" --no-merges`
|
|
34
|
+
);
|
|
35
|
+
const commits = commitLog
|
|
36
|
+
? commitLog.split("\n").filter((l) => l.trim())
|
|
37
|
+
: [];
|
|
38
|
+
|
|
39
|
+
// Uncommitted changes
|
|
40
|
+
const status = run("git status --porcelain");
|
|
41
|
+
const uncommitted = status ? status.split("\n").filter((l) => l.trim()) : [];
|
|
42
|
+
|
|
43
|
+
// Files changed today (committed)
|
|
44
|
+
const changedFiles = run(
|
|
45
|
+
`git diff --name-only HEAD~${Math.max(commits.length, 1)}..HEAD 2>/dev/null`
|
|
46
|
+
);
|
|
47
|
+
const files = changedFiles
|
|
48
|
+
? changedFiles.split("\n").filter((l) => l.trim())
|
|
49
|
+
: [];
|
|
50
|
+
|
|
51
|
+
// Today's logs already created
|
|
52
|
+
const existingLogs = fs.existsSync(LOG_DIR)
|
|
53
|
+
? fs.readdirSync(LOG_DIR).filter((f) => f.startsWith(date))
|
|
54
|
+
: [];
|
|
55
|
+
|
|
56
|
+
// Living docs
|
|
57
|
+
const livingDocs = fs.existsSync(DOC_DIR)
|
|
58
|
+
? fs.readdirSync(DOC_DIR).filter((f) => f.startsWith("_") && f !== "_INDEX.md" && f.endsWith(".md"))
|
|
59
|
+
: [];
|
|
60
|
+
|
|
61
|
+
return { date, commits, uncommitted, files, existingLogs, livingDocs };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extractActions(commits) {
|
|
65
|
+
// Group commits by type (feat, fix, refactor, docs, etc.)
|
|
66
|
+
return commits.map((c) => {
|
|
67
|
+
const match = c.match(/^([a-f0-9]+)\s+(\w+)(?:\(([^)]*)\))?:\s*(.+)$/);
|
|
68
|
+
if (match) {
|
|
69
|
+
return {
|
|
70
|
+
hash: match[1],
|
|
71
|
+
type: match[2],
|
|
72
|
+
scope: match[3] || "",
|
|
73
|
+
desc: match[4],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Non-conventional commit
|
|
77
|
+
const parts = c.match(/^([a-f0-9]+)\s+(.+)$/);
|
|
78
|
+
return {
|
|
79
|
+
hash: parts ? parts[1] : "",
|
|
80
|
+
type: "other",
|
|
81
|
+
scope: "",
|
|
82
|
+
desc: parts ? parts[2] : c,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function typeLabel(type) {
|
|
88
|
+
const labels = {
|
|
89
|
+
feat: "Feature",
|
|
90
|
+
fix: "Fix",
|
|
91
|
+
refactor: "Refactor",
|
|
92
|
+
docs: "Doc",
|
|
93
|
+
test: "Test",
|
|
94
|
+
build: "Build",
|
|
95
|
+
chore: "Chore",
|
|
96
|
+
};
|
|
97
|
+
return labels[type] || type.charAt(0).toUpperCase() + type.slice(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function displaySummary(activity) {
|
|
101
|
+
const { date, commits, uncommitted, existingLogs, livingDocs } = activity;
|
|
102
|
+
const actions = extractActions(commits);
|
|
103
|
+
|
|
104
|
+
console.log(`\nopenclew checkout — ${date}\n`);
|
|
105
|
+
|
|
106
|
+
if (actions.length === 0 && uncommitted.length === 0) {
|
|
107
|
+
console.log(" Nothing to report — no commits or changes today.");
|
|
108
|
+
console.log("");
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Summary table
|
|
113
|
+
if (actions.length > 0) {
|
|
114
|
+
console.log(" Commits today:");
|
|
115
|
+
console.log(
|
|
116
|
+
" ┌─────┬──────────────────────────────────────────────┬─────┐"
|
|
117
|
+
);
|
|
118
|
+
console.log(
|
|
119
|
+
" │ Sta │ Action │ Com │"
|
|
120
|
+
);
|
|
121
|
+
console.log(
|
|
122
|
+
" ├─────┼──────────────────────────────────────────────┼─────┤"
|
|
123
|
+
);
|
|
124
|
+
for (const a of actions) {
|
|
125
|
+
const label = `${typeLabel(a.type)} : ${a.desc}`;
|
|
126
|
+
const truncated = label.length > 44 ? label.slice(0, 43) + "…" : label;
|
|
127
|
+
const padded = truncated.padEnd(44);
|
|
128
|
+
console.log(` │ ✅ │ ${padded} │ 🟢 │`);
|
|
129
|
+
}
|
|
130
|
+
console.log(
|
|
131
|
+
" └─────┴──────────────────────────────────────────────┴─────┘"
|
|
132
|
+
);
|
|
133
|
+
console.log("");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (uncommitted.length > 0) {
|
|
137
|
+
console.log(` Uncommitted changes: ${uncommitted.length} file(s)`);
|
|
138
|
+
for (const line of uncommitted.slice(0, 10)) {
|
|
139
|
+
console.log(` ${line}`);
|
|
140
|
+
}
|
|
141
|
+
if (uncommitted.length > 10) {
|
|
142
|
+
console.log(` ... and ${uncommitted.length - 10} more`);
|
|
143
|
+
}
|
|
144
|
+
console.log("");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Documentation status
|
|
148
|
+
if (existingLogs.length > 0) {
|
|
149
|
+
console.log(` 📗 Today's logs: ${existingLogs.join(", ")}`);
|
|
150
|
+
} else {
|
|
151
|
+
console.log(" 📕 No log created today");
|
|
152
|
+
}
|
|
153
|
+
console.log("");
|
|
154
|
+
|
|
155
|
+
// Living docs reminder
|
|
156
|
+
if (livingDocs.length > 0) {
|
|
157
|
+
console.log(" 📚 Living docs — check if any need updating:");
|
|
158
|
+
for (const doc of livingDocs) {
|
|
159
|
+
console.log(` ${doc}`);
|
|
160
|
+
}
|
|
161
|
+
console.log("");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return actions;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function generateSessionLog(activity, actions) {
|
|
168
|
+
const { date } = activity;
|
|
169
|
+
|
|
170
|
+
// Build a descriptive title from actions
|
|
171
|
+
let sessionTitle;
|
|
172
|
+
if (actions.length === 1) {
|
|
173
|
+
sessionTitle = actions[0].desc;
|
|
174
|
+
} else if (actions.length > 1) {
|
|
175
|
+
const types = [...new Set(actions.map((a) => typeLabel(a.type)))];
|
|
176
|
+
sessionTitle = types.join(" + ") + " session";
|
|
177
|
+
} else {
|
|
178
|
+
sessionTitle = "Work session";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Build pre-filled log content
|
|
182
|
+
const keywords = [
|
|
183
|
+
...new Set(actions.map((a) => a.scope).filter(Boolean)),
|
|
184
|
+
];
|
|
185
|
+
const keywordsStr =
|
|
186
|
+
keywords.length > 0 ? `[${keywords.join(", ")}]` : "[]";
|
|
187
|
+
|
|
188
|
+
const commitList = actions
|
|
189
|
+
.map((a) => `- ${typeLabel(a.type)}: ${a.desc} (${a.hash})`)
|
|
190
|
+
.join("\n");
|
|
191
|
+
|
|
192
|
+
const content = `<!-- L1_START -->
|
|
193
|
+
# L1 - Metadata
|
|
194
|
+
date: ${date}
|
|
195
|
+
type: ${actions.length === 1 ? actions[0].type === "fix" ? "Bug" : "Feature" : "Feature"}
|
|
196
|
+
subject: ${sessionTitle}
|
|
197
|
+
short_story: ${actions.map((a) => a.desc).join(". ")}.
|
|
198
|
+
status: Done
|
|
199
|
+
category:
|
|
200
|
+
keywords: ${keywordsStr}
|
|
201
|
+
<!-- L1_END -->
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
<!-- L2_START -->
|
|
206
|
+
# L2 - Summary
|
|
207
|
+
|
|
208
|
+
## Objective
|
|
209
|
+
<!-- Why this work was undertaken -->
|
|
210
|
+
|
|
211
|
+
## What was done
|
|
212
|
+
${commitList}
|
|
213
|
+
|
|
214
|
+
## Result
|
|
215
|
+
<!-- Outcome — what works now that didn't before -->
|
|
216
|
+
<!-- L2_END -->
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
<!-- L3_START -->
|
|
221
|
+
# L3 - Details
|
|
222
|
+
|
|
223
|
+
<!-- Technical details, code changes, debugging steps... -->
|
|
224
|
+
<!-- L3_END -->
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
const slug = slugifyLog(sessionTitle);
|
|
228
|
+
const filename = `${date}_${slug}.md`;
|
|
229
|
+
const filepath = path.join(LOG_DIR, filename);
|
|
230
|
+
|
|
231
|
+
if (fs.existsSync(filepath)) {
|
|
232
|
+
console.log(` Log already exists: doc/log/${filename}`);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
237
|
+
console.log(" No doc/log/ directory. Run 'openclew init' first.");
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fs.writeFileSync(filepath, content, "utf-8");
|
|
242
|
+
console.log(` 📝 Created doc/log/${filename}`);
|
|
243
|
+
console.log(" Pre-filled with today's commits. Edit to add context.");
|
|
244
|
+
return filename;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function regenerateIndex() {
|
|
248
|
+
const indexScript = path.join(DOC_DIR, "generate-index.py");
|
|
249
|
+
if (!fs.existsSync(indexScript)) return;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
execSync(`python3 "${indexScript}" "${DOC_DIR}"`, { stdio: "pipe" });
|
|
253
|
+
console.log(" 📋 Regenerated doc/_INDEX.md");
|
|
254
|
+
} catch {
|
|
255
|
+
// Silent — index will be regenerated on next commit anyway
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function main() {
|
|
260
|
+
if (!fs.existsSync(DOC_DIR)) {
|
|
261
|
+
console.error("No doc/ directory found. Run 'openclew init' first.");
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!readConfig(PROJECT_ROOT)) {
|
|
266
|
+
console.warn("Warning: no .openclew.json found. Run 'openclew init' first.\n");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const activity = collectGitActivity();
|
|
270
|
+
const actions = displaySummary(activity);
|
|
271
|
+
|
|
272
|
+
if (!actions || actions.length === 0) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Create session log
|
|
277
|
+
console.log("─── Log ───");
|
|
278
|
+
const created = generateSessionLog(activity, actions);
|
|
279
|
+
|
|
280
|
+
// Regenerate index
|
|
281
|
+
if (created) {
|
|
282
|
+
regenerateIndex();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.log("");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
main();
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read/write .openclew.json config at project root.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
const CONFIG_FILE = ".openclew.json";
|
|
9
|
+
|
|
10
|
+
function configPath(projectRoot) {
|
|
11
|
+
return path.join(projectRoot || process.cwd(), CONFIG_FILE);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readConfig(projectRoot) {
|
|
15
|
+
const p = configPath(projectRoot);
|
|
16
|
+
if (!fs.existsSync(p)) return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeConfig(config, projectRoot) {
|
|
25
|
+
const p = configPath(projectRoot);
|
|
26
|
+
fs.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getEntryPoint(projectRoot) {
|
|
30
|
+
const config = readConfig(projectRoot);
|
|
31
|
+
return config && config.entryPoint ? config.entryPoint : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { readConfig, writeConfig, getEntryPoint, CONFIG_FILE };
|
package/lib/detect.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Detect existing AI instruction files in the project root.
|
|
3
|
-
* Returns an array of { tool, file,
|
|
3
|
+
* Returns an array of { tool, file, fullPath, isDir } objects.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const fs = require("fs");
|
|
@@ -15,25 +15,60 @@ const INSTRUCTION_FILES = [
|
|
|
15
15
|
{ tool: "Windsurf", file: ".windsurf/rules" },
|
|
16
16
|
{ tool: "Cline", file: ".clinerules" },
|
|
17
17
|
{ tool: "Codex / Gemini", file: "AGENTS.md" },
|
|
18
|
+
{ tool: "Antigravity", file: ".antigravity/rules.md" },
|
|
19
|
+
{ tool: "Gemini CLI", file: ".gemini/GEMINI.md" },
|
|
18
20
|
{ tool: "Aider", file: "CONVENTIONS.md" },
|
|
19
21
|
];
|
|
20
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Find AGENTS.md case-insensitively in projectRoot.
|
|
25
|
+
* Returns the actual filename (e.g. "agents.md", "Agents.md") or null.
|
|
26
|
+
*/
|
|
27
|
+
function findAgentsMdCaseInsensitive(projectRoot) {
|
|
28
|
+
try {
|
|
29
|
+
const entries = fs.readdirSync(projectRoot);
|
|
30
|
+
const match = entries.find(
|
|
31
|
+
(e) => e.toLowerCase() === "agents.md" && fs.statSync(path.join(projectRoot, e)).isFile()
|
|
32
|
+
);
|
|
33
|
+
return match || null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
function detectInstructionFiles(projectRoot) {
|
|
22
40
|
const found = [];
|
|
41
|
+
const seenLower = new Set();
|
|
42
|
+
|
|
23
43
|
for (const entry of INSTRUCTION_FILES) {
|
|
44
|
+
// Skip AGENTS.md in the static list — handled by case-insensitive scan
|
|
45
|
+
if (entry.file.toLowerCase() === "agents.md") continue;
|
|
46
|
+
|
|
24
47
|
const fullPath = path.join(projectRoot, entry.file);
|
|
25
48
|
if (fs.existsSync(fullPath)) {
|
|
26
49
|
const stat = fs.statSync(fullPath);
|
|
27
|
-
// For directories (.cursor/rules, .windsurf/rules), note it but don't inject
|
|
28
50
|
found.push({
|
|
29
51
|
tool: entry.tool,
|
|
30
52
|
file: entry.file,
|
|
31
53
|
fullPath,
|
|
32
54
|
isDir: stat.isDirectory(),
|
|
33
55
|
});
|
|
56
|
+
seenLower.add(entry.file.toLowerCase());
|
|
34
57
|
}
|
|
35
58
|
}
|
|
59
|
+
|
|
60
|
+
// Case-insensitive AGENTS.md detection
|
|
61
|
+
const agentsFile = findAgentsMdCaseInsensitive(projectRoot);
|
|
62
|
+
if (agentsFile && !seenLower.has(agentsFile.toLowerCase())) {
|
|
63
|
+
found.push({
|
|
64
|
+
tool: "Codex / Gemini",
|
|
65
|
+
file: agentsFile,
|
|
66
|
+
fullPath: path.join(projectRoot, agentsFile),
|
|
67
|
+
isDir: false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
36
71
|
return found;
|
|
37
72
|
}
|
|
38
73
|
|
|
39
|
-
module.exports = { detectInstructionFiles, INSTRUCTION_FILES };
|
|
74
|
+
module.exports = { detectInstructionFiles, findAgentsMdCaseInsensitive, INSTRUCTION_FILES };
|
package/lib/init.js
CHANGED
|
@@ -2,17 +2,20 @@
|
|
|
2
2
|
* openclew init — set up openclew in the current project.
|
|
3
3
|
*
|
|
4
4
|
* 1. Create doc/ and doc/log/
|
|
5
|
-
* 2. Detect
|
|
6
|
-
* 3.
|
|
5
|
+
* 2. Detect entry point (AGENTS.md case-insensitive by default)
|
|
6
|
+
* 3. Inject openclew block into entry point
|
|
7
7
|
* 4. Install pre-commit hook for index generation
|
|
8
|
-
* 5.
|
|
8
|
+
* 5. Create guide + example docs
|
|
9
|
+
* 6. Generate initial _INDEX.md
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const fs = require("fs");
|
|
12
13
|
const path = require("path");
|
|
13
14
|
const readline = require("readline");
|
|
14
|
-
const { detectInstructionFiles } = require("./detect");
|
|
15
|
+
const { detectInstructionFiles, findAgentsMdCaseInsensitive } = require("./detect");
|
|
15
16
|
const { inject, isAlreadyInjected } = require("./inject");
|
|
17
|
+
const { writeConfig } = require("./config");
|
|
18
|
+
const { guideContent, exampleLivingDocContent, exampleLogContent, today } = require("./templates");
|
|
16
19
|
|
|
17
20
|
const PROJECT_ROOT = process.cwd();
|
|
18
21
|
const DOC_DIR = path.join(PROJECT_ROOT, "doc");
|
|
@@ -31,17 +34,15 @@ function ask(question) {
|
|
|
31
34
|
return new Promise((resolve) => {
|
|
32
35
|
rl.question(question, (answer) => {
|
|
33
36
|
rl.close();
|
|
34
|
-
resolve(answer.trim()
|
|
37
|
+
resolve(answer.trim());
|
|
35
38
|
});
|
|
36
39
|
});
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
function createDirs() {
|
|
40
|
-
let created = false;
|
|
41
43
|
if (!fs.existsSync(DOC_DIR)) {
|
|
42
44
|
fs.mkdirSync(DOC_DIR, { recursive: true });
|
|
43
45
|
console.log(" Created doc/");
|
|
44
|
-
created = true;
|
|
45
46
|
} else {
|
|
46
47
|
console.log(" doc/ already exists");
|
|
47
48
|
}
|
|
@@ -49,12 +50,68 @@ function createDirs() {
|
|
|
49
50
|
if (!fs.existsSync(LOG_DIR)) {
|
|
50
51
|
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
51
52
|
console.log(" Created doc/log/");
|
|
52
|
-
created = true;
|
|
53
53
|
} else {
|
|
54
54
|
console.log(" doc/log/ already exists");
|
|
55
55
|
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the entry point file.
|
|
60
|
+
*
|
|
61
|
+
* Priority:
|
|
62
|
+
* 1. AGENTS.md (case-insensitive) — default, universal
|
|
63
|
+
* 2. Other detected instruction files — user picks one
|
|
64
|
+
* 3. Create AGENTS.md — if nothing exists
|
|
65
|
+
*/
|
|
66
|
+
async function resolveEntryPoint() {
|
|
67
|
+
if (noInject) {
|
|
68
|
+
console.log(" Skipping entry point setup (--no-inject)");
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 1. Check for AGENTS.md (case-insensitive)
|
|
73
|
+
const agentsFile = findAgentsMdCaseInsensitive(PROJECT_ROOT);
|
|
74
|
+
if (agentsFile) {
|
|
75
|
+
if (!process.stdin.isTTY) {
|
|
76
|
+
// Non-interactive: accept AGENTS.md by default
|
|
77
|
+
console.log(` Using ${agentsFile} (non-interactive)`);
|
|
78
|
+
return { file: agentsFile, fullPath: path.join(PROJECT_ROOT, agentsFile), created: false };
|
|
79
|
+
}
|
|
80
|
+
const answer = await ask(` Found ${agentsFile} — use as entry point? [Y/n] `);
|
|
81
|
+
if (answer === "" || answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
82
|
+
return { file: agentsFile, fullPath: path.join(PROJECT_ROOT, agentsFile), created: false };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2. Detect other instruction files
|
|
87
|
+
const others = detectInstructionFiles(PROJECT_ROOT).filter(
|
|
88
|
+
(f) => !f.isDir && f.file.toLowerCase() !== "agents.md"
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (others.length > 0) {
|
|
92
|
+
console.log(" Detected instruction files:");
|
|
93
|
+
others.forEach((f, i) => console.log(` ${i + 1}. ${f.file} (${f.tool})`));
|
|
94
|
+
console.log(` ${others.length + 1}. Create new AGENTS.md`);
|
|
95
|
+
|
|
96
|
+
if (!process.stdin.isTTY) {
|
|
97
|
+
// Non-interactive: default to first detected file
|
|
98
|
+
console.log(` Using ${others[0].file} (non-interactive)`);
|
|
99
|
+
return { file: others[0].file, fullPath: others[0].fullPath, created: false };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const choice = await ask(` Choose entry point [1-${others.length + 1}]: `);
|
|
103
|
+
const idx = parseInt(choice, 10) - 1;
|
|
104
|
+
|
|
105
|
+
if (idx >= 0 && idx < others.length) {
|
|
106
|
+
return { file: others[idx].file, fullPath: others[idx].fullPath, created: false };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
56
109
|
|
|
57
|
-
|
|
110
|
+
// 3. Create AGENTS.md
|
|
111
|
+
const agentsPath = path.join(PROJECT_ROOT, "AGENTS.md");
|
|
112
|
+
fs.writeFileSync(agentsPath, `# ${path.basename(PROJECT_ROOT)}\n\nProject instructions for AI agents.\n`, "utf-8");
|
|
113
|
+
console.log(" Created AGENTS.md");
|
|
114
|
+
return { file: "AGENTS.md", fullPath: agentsPath, created: true };
|
|
58
115
|
}
|
|
59
116
|
|
|
60
117
|
function installPreCommitHook() {
|
|
@@ -69,21 +126,10 @@ function installPreCommitHook() {
|
|
|
69
126
|
}
|
|
70
127
|
|
|
71
128
|
const preCommitPath = path.join(hooksDir, "pre-commit");
|
|
72
|
-
const indexScript = `
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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`;
|
|
129
|
+
const indexScript = `if [ -f doc/generate-index.py ]; then
|
|
130
|
+
python3 doc/generate-index.py doc 2>/dev/null || echo "openclew: index generation failed"
|
|
131
|
+
git add doc/_INDEX.md 2>/dev/null
|
|
132
|
+
fi`;
|
|
87
133
|
|
|
88
134
|
const MARKER = "# openclew-index";
|
|
89
135
|
|
|
@@ -93,19 +139,10 @@ git add doc/_INDEX.md 2>/dev/null`;
|
|
|
93
139
|
console.log(" Pre-commit hook already contains openclew index generation");
|
|
94
140
|
return false;
|
|
95
141
|
}
|
|
96
|
-
|
|
97
|
-
fs.appendFileSync(
|
|
98
|
-
preCommitPath,
|
|
99
|
-
`\n\n${MARKER}\n${indexScript}\n`,
|
|
100
|
-
"utf-8"
|
|
101
|
-
);
|
|
142
|
+
fs.appendFileSync(preCommitPath, `\n\n${MARKER}\n${indexScript}\n`, "utf-8");
|
|
102
143
|
console.log(" Appended openclew index generation to existing pre-commit hook");
|
|
103
144
|
} else {
|
|
104
|
-
fs.writeFileSync(
|
|
105
|
-
preCommitPath,
|
|
106
|
-
`#!/bin/sh\n\n${MARKER}\n${indexScript}\n`,
|
|
107
|
-
"utf-8"
|
|
108
|
-
);
|
|
145
|
+
fs.writeFileSync(preCommitPath, `#!/bin/sh\n\n${MARKER}\n${indexScript}\n`, "utf-8");
|
|
109
146
|
fs.chmodSync(preCommitPath, "755");
|
|
110
147
|
console.log(" Created pre-commit hook for index generation");
|
|
111
148
|
}
|
|
@@ -132,6 +169,35 @@ function copyGenerateIndex() {
|
|
|
132
169
|
return false;
|
|
133
170
|
}
|
|
134
171
|
|
|
172
|
+
function createDocs() {
|
|
173
|
+
// Guide — always created
|
|
174
|
+
const guidePath = path.join(DOC_DIR, "_USING_OPENCLEW.md");
|
|
175
|
+
if (!fs.existsSync(guidePath)) {
|
|
176
|
+
fs.writeFileSync(guidePath, guideContent(), "utf-8");
|
|
177
|
+
console.log(" Created doc/_USING_OPENCLEW.md (guide)");
|
|
178
|
+
} else {
|
|
179
|
+
console.log(" doc/_USING_OPENCLEW.md already exists");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Example living doc
|
|
183
|
+
const examplePath = path.join(DOC_DIR, "_ARCHITECTURE.md");
|
|
184
|
+
if (!fs.existsSync(examplePath)) {
|
|
185
|
+
fs.writeFileSync(examplePath, exampleLivingDocContent(), "utf-8");
|
|
186
|
+
console.log(" Created doc/_ARCHITECTURE.md (example living doc)");
|
|
187
|
+
} else {
|
|
188
|
+
console.log(" doc/_ARCHITECTURE.md already exists");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Example log
|
|
192
|
+
const logPath = path.join(LOG_DIR, `${today()}_setup-openclew.md`);
|
|
193
|
+
if (!fs.existsSync(logPath)) {
|
|
194
|
+
fs.writeFileSync(logPath, exampleLogContent(), "utf-8");
|
|
195
|
+
console.log(` Created doc/log/${today()}_setup-openclew.md (example log)`);
|
|
196
|
+
} else {
|
|
197
|
+
console.log(` doc/log/${today()}_setup-openclew.md already exists`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
135
201
|
function runIndexGenerator() {
|
|
136
202
|
const indexScript = path.join(DOC_DIR, "generate-index.py");
|
|
137
203
|
if (!fs.existsSync(indexScript)) return;
|
|
@@ -156,38 +222,23 @@ async function main() {
|
|
|
156
222
|
console.log("\n2. Index generator");
|
|
157
223
|
copyGenerateIndex();
|
|
158
224
|
|
|
159
|
-
// Step 3:
|
|
160
|
-
console.log("\n3.
|
|
161
|
-
const
|
|
225
|
+
// Step 3: Entry point
|
|
226
|
+
console.log("\n3. Entry point");
|
|
227
|
+
const entryPoint = await resolveEntryPoint();
|
|
162
228
|
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
}
|
|
229
|
+
if (entryPoint) {
|
|
230
|
+
if (isAlreadyInjected(entryPoint.fullPath)) {
|
|
231
|
+
console.log(` ${entryPoint.file} already has openclew block`);
|
|
232
|
+
} else {
|
|
233
|
+
inject(entryPoint.fullPath);
|
|
234
|
+
console.log(` Injected openclew block into ${entryPoint.file}`);
|
|
190
235
|
}
|
|
236
|
+
|
|
237
|
+
writeConfig({ entryPoint: entryPoint.file }, PROJECT_ROOT);
|
|
238
|
+
console.log(` Saved entry point → .openclew.json`);
|
|
239
|
+
} else {
|
|
240
|
+
// --no-inject: still create config to mark init was done
|
|
241
|
+
writeConfig({ entryPoint: null }, PROJECT_ROOT);
|
|
191
242
|
}
|
|
192
243
|
|
|
193
244
|
// Step 4: Pre-commit hook
|
|
@@ -198,14 +249,24 @@ async function main() {
|
|
|
198
249
|
installPreCommitHook();
|
|
199
250
|
}
|
|
200
251
|
|
|
201
|
-
// Step 5:
|
|
202
|
-
console.log("\n5.
|
|
252
|
+
// Step 5: Docs
|
|
253
|
+
console.log("\n5. Docs");
|
|
254
|
+
createDocs();
|
|
255
|
+
|
|
256
|
+
// Step 6: Generate index
|
|
257
|
+
console.log("\n6. Index");
|
|
203
258
|
runIndexGenerator();
|
|
204
259
|
|
|
205
260
|
// Done
|
|
206
|
-
console.log("\
|
|
207
|
-
|
|
208
|
-
|
|
261
|
+
console.log("\n─── Ready ───\n");
|
|
262
|
+
if (entryPoint) {
|
|
263
|
+
console.log(` Entry point: ${entryPoint.file}`);
|
|
264
|
+
}
|
|
265
|
+
console.log(" Guide: doc/_USING_OPENCLEW.md");
|
|
266
|
+
console.log("");
|
|
267
|
+
console.log(" Start a session with your agent now.");
|
|
268
|
+
console.log(' Ask it: "Read doc/_USING_OPENCLEW.md and document our architecture."');
|
|
269
|
+
console.log(" That's it — openclew works from here.");
|
|
209
270
|
console.log("");
|
|
210
271
|
}
|
|
211
272
|
|
package/lib/inject.js
CHANGED
|
@@ -7,13 +7,17 @@ const fs = require("fs");
|
|
|
7
7
|
const OPENCLEW_BLOCK = `
|
|
8
8
|
## Project knowledge (openclew)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
This file is the **entry point** for project documentation.
|
|
11
11
|
|
|
12
|
-
**
|
|
12
|
+
**Doc-first rule:** before any task, read \`doc/_INDEX.md\` to find docs related to the task. Read them before exploring code.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
14
|
+
Two types of docs in \`doc/\`:
|
|
15
|
+
- **Living docs** (\`doc/_*.md\`) — evolve with the project (architecture, conventions, decisions)
|
|
16
|
+
- **Logs** (\`doc/log/YYYY-MM-DD_*.md\`) — frozen facts from a session, never modified after
|
|
17
|
+
|
|
18
|
+
Each doc has 3 levels: **L1** (metadata — read first to decide relevance) → **L2** (summary) → **L3** (full details, only when needed).
|
|
19
|
+
|
|
20
|
+
**Creating docs:** when a decision, convention, or significant event needs to be captured, create the file directly following the format in \`doc/_USING_OPENCLEW.md\`.
|
|
17
21
|
`.trim();
|
|
18
22
|
|
|
19
23
|
const MARKER_START = "<!-- openclew_START -->";
|
package/lib/new-doc.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* openclew new <title> — create a new
|
|
2
|
+
* openclew new <title> — create a new living doc.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
|
-
const {
|
|
7
|
+
const { livingContent, slugify } = require("./templates");
|
|
8
|
+
const { readConfig } = require("./config");
|
|
8
9
|
|
|
9
10
|
const args = process.argv.slice(2);
|
|
10
11
|
// Remove "new" command from args
|
|
@@ -17,11 +18,15 @@ if (!title) {
|
|
|
17
18
|
process.exit(1);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const
|
|
21
|
+
const projectRoot = process.cwd();
|
|
22
|
+
const docDir = path.join(projectRoot, "doc");
|
|
21
23
|
if (!fs.existsSync(docDir)) {
|
|
22
24
|
console.error("No doc/ directory found. Run 'openclew init' first.");
|
|
23
25
|
process.exit(1);
|
|
24
26
|
}
|
|
27
|
+
if (!readConfig(projectRoot)) {
|
|
28
|
+
console.warn("Warning: no .openclew.json found. Run 'openclew init' first.");
|
|
29
|
+
}
|
|
25
30
|
|
|
26
31
|
const slug = slugify(title);
|
|
27
32
|
const filename = `_${slug}.md`;
|
|
@@ -32,5 +37,10 @@ if (fs.existsSync(filepath)) {
|
|
|
32
37
|
process.exit(1);
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
fs.writeFileSync(filepath,
|
|
40
|
+
fs.writeFileSync(filepath, livingContent(title), "utf-8");
|
|
36
41
|
console.log(`Created doc/${filename}`);
|
|
42
|
+
console.log("");
|
|
43
|
+
console.log("Next: open the file and fill in:");
|
|
44
|
+
console.log(" L1 — subject, status, keywords (so the index can find it)");
|
|
45
|
+
console.log(" L2 — objective + key points (what agents and humans need to know)");
|
|
46
|
+
console.log(" L3 — full details (only when needed)");
|
package/lib/new-log.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const { logContent, slugifyLog, today } = require("./templates");
|
|
8
|
+
const { readConfig } = require("./config");
|
|
8
9
|
|
|
9
10
|
const args = process.argv.slice(2);
|
|
10
11
|
// Remove "log" command from args
|
|
@@ -17,11 +18,15 @@ if (!title) {
|
|
|
17
18
|
process.exit(1);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const
|
|
21
|
+
const projectRoot = process.cwd();
|
|
22
|
+
const logDir = path.join(projectRoot, "doc", "log");
|
|
21
23
|
if (!fs.existsSync(logDir)) {
|
|
22
24
|
console.error("No doc/log/ directory found. Run 'openclew init' first.");
|
|
23
25
|
process.exit(1);
|
|
24
26
|
}
|
|
27
|
+
if (!readConfig(projectRoot)) {
|
|
28
|
+
console.warn("Warning: no .openclew.json found. Run 'openclew init' first.");
|
|
29
|
+
}
|
|
25
30
|
|
|
26
31
|
const slug = slugifyLog(title);
|
|
27
32
|
const date = today();
|
|
@@ -35,3 +40,9 @@ if (fs.existsSync(filepath)) {
|
|
|
35
40
|
|
|
36
41
|
fs.writeFileSync(filepath, logContent(title), "utf-8");
|
|
37
42
|
console.log(`Created doc/log/${filename}`);
|
|
43
|
+
console.log("");
|
|
44
|
+
console.log("Next: open the file and fill in:");
|
|
45
|
+
console.log(" L1 — type, status, short_story (what happened in 1-2 sentences)");
|
|
46
|
+
console.log(" L2 — problem + solution (the facts, frozen after this session)");
|
|
47
|
+
console.log("");
|
|
48
|
+
console.log("Logs are immutable — once written, never modified.");
|
package/lib/templates.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Template content for
|
|
2
|
+
* Template content for living docs and logs.
|
|
3
3
|
* Embedded here so the CLI works standalone without needing to locate template files.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -21,7 +21,7 @@ function slugifyLog(title) {
|
|
|
21
21
|
.replace(/^-|-$/g, "");
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function
|
|
24
|
+
function livingContent(title) {
|
|
25
25
|
const date = today();
|
|
26
26
|
return `<!-- L1_START -->
|
|
27
27
|
# L1 - Metadata
|
|
@@ -103,4 +103,266 @@ keywords: []
|
|
|
103
103
|
`;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Guide doc — always created by init.
|
|
108
|
+
* This is what agents read to understand openclew.
|
|
109
|
+
*/
|
|
110
|
+
function guideContent() {
|
|
111
|
+
const date = today();
|
|
112
|
+
return `<!-- L1_START -->
|
|
113
|
+
# L1 - Metadata
|
|
114
|
+
type: Guide
|
|
115
|
+
subject: How openclew works
|
|
116
|
+
created: ${date}
|
|
117
|
+
updated: ${date}
|
|
118
|
+
short_story: How openclew structures project knowledge in 3 levels (L1/L2/L3) so AI agents and humans navigate efficiently.
|
|
119
|
+
status: Active
|
|
120
|
+
category: Documentation
|
|
121
|
+
keywords: [openclew, L1, L2, L3, index, living-doc, log]
|
|
122
|
+
<!-- L1_END -->
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
<!-- L2_START -->
|
|
127
|
+
# L2 - Summary
|
|
128
|
+
|
|
129
|
+
## What is openclew?
|
|
130
|
+
|
|
131
|
+
openclew gives your project a structured memory that both humans and AI agents can navigate efficiently.
|
|
132
|
+
|
|
133
|
+
## Doc-first rule
|
|
134
|
+
|
|
135
|
+
Before starting any task, read \`doc/_INDEX.md\` to find docs related to the task. Read them before exploring code. This avoids reinventing what's already documented.
|
|
136
|
+
|
|
137
|
+
## Two types of docs
|
|
138
|
+
|
|
139
|
+
**Living docs** (\`doc/_*.md\`): knowledge that evolves with the project.
|
|
140
|
+
Architecture decisions, conventions, known pitfalls — anything that stays relevant over time.
|
|
141
|
+
Naming: \`doc/_UPPER_SNAKE_CASE.md\` (e.g. \`doc/_AUTH_DESIGN.md\`)
|
|
142
|
+
|
|
143
|
+
**Logs** (\`doc/log/YYYY-MM-DD_*.md\`): frozen facts from a work session.
|
|
144
|
+
What happened, what was decided, what was tried. Never modified after the session.
|
|
145
|
+
Naming: \`doc/log/YYYY-MM-DD_lowercase-slug.md\` (e.g. \`doc/log/2026-01-15_setup-auth.md\`)
|
|
146
|
+
|
|
147
|
+
## Three levels per doc
|
|
148
|
+
|
|
149
|
+
Every doc has 3 levels. Read only what you need:
|
|
150
|
+
|
|
151
|
+
- **L1 — Metadata** (~40 tokens): subject, keywords, status. Read this first to decide if the doc is relevant.
|
|
152
|
+
- **L2 — Summary**: the essential context — objective, key points, decisions.
|
|
153
|
+
- **L3 — Details**: full technical content. Only read when deep-diving.
|
|
154
|
+
|
|
155
|
+
## Index
|
|
156
|
+
|
|
157
|
+
\`doc/_INDEX.md\` is auto-generated from L1 metadata on every git commit (via a pre-commit hook).
|
|
158
|
+
Never edit it manually. To force a rebuild: \`openclew index\`
|
|
159
|
+
<!-- L2_END -->
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
<!-- L3_START -->
|
|
164
|
+
# L3 - Details
|
|
165
|
+
|
|
166
|
+
## Creating a living doc
|
|
167
|
+
|
|
168
|
+
Create \`doc/_TITLE.md\` (uppercase snake_case) with this structure:
|
|
169
|
+
|
|
170
|
+
\`\`\`
|
|
171
|
+
<!-- L1_START -->
|
|
172
|
+
# L1 - Metadata
|
|
173
|
+
type: Reference
|
|
174
|
+
subject: Title
|
|
175
|
+
created: YYYY-MM-DD
|
|
176
|
+
updated: YYYY-MM-DD
|
|
177
|
+
short_story:
|
|
178
|
+
status: Active
|
|
179
|
+
category:
|
|
180
|
+
keywords: []
|
|
181
|
+
<!-- L1_END -->
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
<!-- L2_START -->
|
|
186
|
+
# L2 - Summary
|
|
187
|
+
## Objective
|
|
188
|
+
## Key points
|
|
189
|
+
<!-- L2_END -->
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
<!-- L3_START -->
|
|
194
|
+
# L3 - Details
|
|
195
|
+
<!-- L3_END -->
|
|
196
|
+
\`\`\`
|
|
197
|
+
|
|
198
|
+
**When to create one:**
|
|
199
|
+
- A decision was made that others need to know (architecture, convention, API design)
|
|
200
|
+
- A pattern or pitfall keeps coming up
|
|
201
|
+
- You want an agent to know something at the start of every session
|
|
202
|
+
|
|
203
|
+
## Creating a log
|
|
204
|
+
|
|
205
|
+
Create \`doc/log/YYYY-MM-DD_slug.md\` (lowercase, hyphens) with this structure:
|
|
206
|
+
|
|
207
|
+
\`\`\`
|
|
208
|
+
<!-- L1_START -->
|
|
209
|
+
# L1 - Metadata
|
|
210
|
+
date: YYYY-MM-DD
|
|
211
|
+
type: Feature
|
|
212
|
+
subject: Title
|
|
213
|
+
short_story:
|
|
214
|
+
status: In progress
|
|
215
|
+
category:
|
|
216
|
+
keywords: []
|
|
217
|
+
<!-- L1_END -->
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
<!-- L2_START -->
|
|
222
|
+
# L2 - Summary
|
|
223
|
+
## Objective
|
|
224
|
+
## Problem
|
|
225
|
+
## Solution
|
|
226
|
+
<!-- L2_END -->
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
<!-- L3_START -->
|
|
231
|
+
# L3 - Details
|
|
232
|
+
<!-- L3_END -->
|
|
233
|
+
\`\`\`
|
|
234
|
+
|
|
235
|
+
**When to create one:**
|
|
236
|
+
- End of a work session (what was done, what's left)
|
|
237
|
+
- A bug was investigated and resolved
|
|
238
|
+
- A spike or experiment was conducted
|
|
239
|
+
|
|
240
|
+
Logs are immutable — once the session ends, the log is never modified.
|
|
241
|
+
|
|
242
|
+
## How agents should use this
|
|
243
|
+
|
|
244
|
+
1. At session start: read the entry point file
|
|
245
|
+
2. Before any task: read \`doc/_INDEX.md\`, scan L1 metadata, identify relevant docs
|
|
246
|
+
3. Read L2 of relevant docs for context
|
|
247
|
+
4. Only read L3 when you need implementation details
|
|
248
|
+
5. After significant work: create or update living docs and logs directly
|
|
249
|
+
|
|
250
|
+
The index (\`doc/_INDEX.md\`) auto-regenerates on every git commit. To force a rebuild: \`openclew index\`
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Changelog
|
|
255
|
+
|
|
256
|
+
| Date | Change |
|
|
257
|
+
|------|--------|
|
|
258
|
+
| ${date} | Created by openclew init |
|
|
259
|
+
<!-- L3_END -->
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Example living doc — shows what a filled-in doc looks like.
|
|
265
|
+
*/
|
|
266
|
+
function exampleLivingDocContent() {
|
|
267
|
+
const date = today();
|
|
268
|
+
return `<!-- L1_START -->
|
|
269
|
+
# L1 - Metadata
|
|
270
|
+
type: Reference
|
|
271
|
+
subject: Architecture overview
|
|
272
|
+
created: ${date}
|
|
273
|
+
updated: ${date}
|
|
274
|
+
short_story: High-level architecture of the project — components, data flow, key decisions.
|
|
275
|
+
status: Active
|
|
276
|
+
category: Architecture
|
|
277
|
+
keywords: [architecture, overview, components]
|
|
278
|
+
<!-- L1_END -->
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
<!-- L2_START -->
|
|
283
|
+
# L2 - Summary
|
|
284
|
+
|
|
285
|
+
## Objective
|
|
286
|
+
Document the high-level architecture so new contributors and AI agents understand the system quickly.
|
|
287
|
+
|
|
288
|
+
## Key points
|
|
289
|
+
- Replace this with your actual architecture
|
|
290
|
+
- Describe the main components and how they interact
|
|
291
|
+
- Note key technical decisions and their rationale
|
|
292
|
+
<!-- L2_END -->
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
<!-- L3_START -->
|
|
297
|
+
# L3 - Details
|
|
298
|
+
|
|
299
|
+
<!-- Replace this with your actual architecture details -->
|
|
300
|
+
|
|
301
|
+
This is an example living doc created by \`openclew init\`.
|
|
302
|
+
Edit it to document your project's architecture, or delete it and create your own.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Changelog
|
|
307
|
+
|
|
308
|
+
| Date | Change |
|
|
309
|
+
|------|--------|
|
|
310
|
+
| ${date} | Created by openclew init (example) |
|
|
311
|
+
<!-- L3_END -->
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Example log — shows what a filled-in log looks like.
|
|
317
|
+
*/
|
|
318
|
+
function exampleLogContent() {
|
|
319
|
+
const date = today();
|
|
320
|
+
return `<!-- L1_START -->
|
|
321
|
+
# L1 - Metadata
|
|
322
|
+
date: ${date}
|
|
323
|
+
type: Feature
|
|
324
|
+
subject: Set up openclew
|
|
325
|
+
short_story: Initialized openclew for structured project knowledge. Created doc/ structure, git hook, guide, and example docs.
|
|
326
|
+
status: Done
|
|
327
|
+
category: Tooling
|
|
328
|
+
keywords: [openclew, setup, documentation]
|
|
329
|
+
<!-- L1_END -->
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
<!-- L2_START -->
|
|
334
|
+
# L2 - Summary
|
|
335
|
+
|
|
336
|
+
## Objective
|
|
337
|
+
Set up structured documentation so AI agents and new contributors can navigate project knowledge efficiently.
|
|
338
|
+
|
|
339
|
+
## Problem
|
|
340
|
+
Project knowledge was scattered — README, inline comments, tribal knowledge. Each new AI session started from zero.
|
|
341
|
+
|
|
342
|
+
## Solution
|
|
343
|
+
Installed openclew. Every doc now has L1 (metadata for triage), L2 (summary for context), L3 (details when needed).
|
|
344
|
+
The index auto-regenerates on each commit via a git hook.
|
|
345
|
+
<!-- L2_END -->
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
<!-- L3_START -->
|
|
350
|
+
# L3 - Details
|
|
351
|
+
|
|
352
|
+
This log was created by \`openclew init\`.
|
|
353
|
+
It shows what a filled-in log looks like. Logs are immutable — once the session ends, the log is frozen.
|
|
354
|
+
For evolving knowledge, use living docs (\`doc/_*.md\`).
|
|
355
|
+
<!-- L3_END -->
|
|
356
|
+
`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
module.exports = {
|
|
360
|
+
livingContent,
|
|
361
|
+
logContent,
|
|
362
|
+
guideContent,
|
|
363
|
+
exampleLivingDocContent,
|
|
364
|
+
exampleLogContent,
|
|
365
|
+
slugify,
|
|
366
|
+
slugifyLog,
|
|
367
|
+
today,
|
|
368
|
+
};
|
package/package.json
CHANGED
|
File without changes
|