openclew 0.2.0 → 0.3.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 CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/openclew/openclew/main/assets/logo.png" alt="openclew" width="200">
3
+ </p>
4
+
1
5
  # openclew
2
6
 
3
7
  > Long Life Memory for LLMs
@@ -70,152 +74,54 @@ L1 answers "should I read this?" L2 answers "what do I need to know?" L3 is ther
70
74
 
71
75
  | Type | Location | Role | Mutability |
72
76
  |------|----------|------|------------|
73
- | **Living** | `doc/_SUBJECT.md` | Living knowledge (architecture, conventions, decisions) | Updated over time |
77
+ | **Refdoc** | `doc/_SUBJECT.md` | Reference knowledge (architecture, conventions, decisions) | Updated over time |
74
78
  | **Log** | `doc/log/YYYY-MM-DD_subject.md` | Frozen facts (what happened, what was decided) | Never modified |
75
79
 
76
- **Living docs** are your project's brain — they evolve as the project evolves.
80
+ **Refdocs** are your project's brain — they evolve as the project evolves.
77
81
  **Logs** are your project's journal — immutable records of what happened and why.
78
82
 
79
- Together, they form the thread. The living docs tell you where you are. The logs tell you how you got here.
83
+ Together, they form the thread. The refdocs tell you where you are. The logs tell you how you got here.
80
84
 
81
85
  ---
82
86
 
83
- ## Quick start (5 minutes)
87
+ ## Quick start (2 minutes)
84
88
 
85
- ### 1. Create the structure
89
+ ### 1. Install
86
90
 
87
91
  ```bash
88
- mkdir -p doc/log
92
+ npx openclew init
89
93
  ```
90
94
 
91
- ### 2. Copy the templates
92
-
93
- Download from [`templates/`](templates/) or create manually:
94
-
95
- <details>
96
- <summary><b>templates/living.md</b> — for living knowledge</summary>
97
-
98
- ```markdown
99
- <!-- L1_START -->
100
- # L1 - Metadata
101
- type: Reference | Architecture | Guide | Analysis
102
- subject: Short title (< 60 chars)
103
- created: YYYY-MM-DD
104
- updated: YYYY-MM-DD
105
- short_story: 1-2 sentences. What this doc covers and what it concludes.
106
- status: Active | Stable | Archived
107
- category: Main domain (e.g. Auth, API, Database, UI...)
108
- keywords: [tag1, tag2, tag3]
109
- <!-- L1_END -->
110
-
111
- ---
112
-
113
- <!-- L2_START -->
114
- # L2 - Summary
115
-
116
- ## Objective
117
- <!-- Why this document exists -->
95
+ This:
96
+ - Creates `doc/` with a guide, an example doc, and an example log
97
+ - Detects your instruction file (CLAUDE.md, .cursorrules, AGENTS.md...)
98
+ - Injects a block that teaches your agent about the doc structure
99
+ - Installs a pre-commit hook that auto-generates `doc/_INDEX.md`
118
100
 
119
- ## Key points
120
- <!-- 3-5 essential takeaways -->
101
+ ### 2. Start a session with your agent
121
102
 
122
- ## Solution
123
- <!-- Recommended approach or pattern -->
124
- <!-- L2_END -->
125
-
126
- ---
103
+ Ask it:
127
104
 
128
- <!-- L3_START -->
129
- # L3 - Details
105
+ > Read doc/_USING_OPENCLEW.md and document our architecture.
130
106
 
131
- <!-- Full technical content: examples, code, references... -->
107
+ Your agent reads the guide, understands the L1/L2/L3 format, and creates `doc/_ARCHITECTURE.md` with your project's actual architecture.
132
108
 
133
- ## Changelog
109
+ ### 3. There is no step 3
134
110
 
135
- | Date | Change |
136
- |------|--------|
137
- | YYYY-MM-DD | Initial creation |
138
- <!-- L3_END -->
139
- ```
111
+ Next session, your agent reads the index, finds the doc, has the context. No re-explanation needed. As your project evolves, your agent creates and updates docs during sessions — refdocs for ongoing knowledge, logs for frozen facts.
140
112
 
141
- </details>
113
+ The index auto-regenerates on every commit. Never edit it manually.
142
114
 
143
115
  <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
- ---
116
+ <summary><b>Manual setup</b> — if you prefer not to use the CLI</summary>
171
117
 
172
- <!-- L3_START -->
173
- # L3 - Details
174
-
175
- <!-- Technical details: code changes, debugging steps, references... -->
176
- <!-- L3_END -->
177
- ```
118
+ 1. Create `doc/` and `doc/log/`
119
+ 2. Copy templates from [`templates/`](templates/) (refdoc.md, log.md)
120
+ 3. Add the openclew block to your instruction file (see `doc/_USING_OPENCLEW.md` after init for the exact format)
121
+ 4. Run `openclew index` to generate `doc/_INDEX.md` (or wire it as a pre-commit hook)
178
122
 
179
123
  </details>
180
124
 
181
- ### 3. Write your first doc
182
-
183
- ```bash
184
- cp templates/living.md doc/_ARCHITECTURE.md
185
- ```
186
-
187
- Edit it — describe your project's architecture. Fill in L1 (metadata), L2 (summary), skip L3 if you don't need it yet.
188
-
189
- ### 4. Point your agent to it
190
-
191
- Add this to your `CLAUDE.md`, `.cursorrules`, or `AGENTS.md`:
192
-
193
- ```markdown
194
- ## Project knowledge
195
-
196
- Documentation lives in `doc/`. Each doc has 3 levels (L1/L2/L3).
197
- - Read L1 first to decide if you need more
198
- - Living docs: `doc/_*.md` (living knowledge, updated)
199
- - Logs: `doc/log/YYYY-MM-DD_*.md` (frozen facts, never modified)
200
- - Index: `doc/_INDEX.md` (auto-generated, start here)
201
- ```
202
-
203
- ### 5. Auto-generate the index (optional)
204
-
205
- Copy [`hooks/generate-index.py`](hooks/generate-index.py) to your project and add it as a pre-commit hook:
206
-
207
- ```bash
208
- # Option A: git hook
209
- cp hooks/generate-index.py .git/hooks/generate-index.py
210
- echo 'python .git/hooks/generate-index.py && git add doc/_INDEX.md' >> .git/hooks/pre-commit
211
- chmod +x .git/hooks/pre-commit
212
-
213
- # Option B: pre-commit framework
214
- # See hooks/README.md for .pre-commit-config.yaml setup
215
- ```
216
-
217
- The index auto-regenerates on every commit. Never edit it manually.
218
-
219
125
  ---
220
126
 
221
127
  ## How it works in practice
@@ -263,7 +169,7 @@ doc/
263
169
  - **Shared knowledge** — Same docs for humans and AI. One source, multiple readers.
264
170
  - **SSOT** (Single Source of Truth) — Each piece of information lives in one place.
265
171
  - **Logs are immutable** — Once written, never modified. Frozen facts.
266
- - **Living docs evolve** — They evolve as the project evolves.
172
+ - **Refdocs evolve** — They evolve as the project evolves.
267
173
  - **Index is auto-generated** — Never edit `_INDEX.md` manually.
268
174
 
269
175
  ---
package/UPGRADING.md ADDED
@@ -0,0 +1,167 @@
1
+ # Upgrading openclew
2
+
3
+ openclew evolves. When the format changes, your existing docs still work — parsers
4
+ are backward-compatible. But new features expect the current format.
5
+
6
+ `openclew migrate` bridges the gap.
7
+
8
+ ---
9
+
10
+ ## Quick version
11
+
12
+ ```bash
13
+ npm install -g openclew@latest # or: npx openclew@latest
14
+ openclew status # shows legacy doc count
15
+ openclew migrate # dry-run: what would change
16
+ openclew migrate --write # apply
17
+ git diff # review
18
+ git add doc/ && git commit -m "chore: migrate docs to openclew format"
19
+ ```
20
+
21
+ ---
22
+
23
+ ## When to upgrade
24
+
25
+ After updating openclew, run `openclew status`. If it reports legacy docs,
26
+ run `openclew migrate` to see what needs changing.
27
+
28
+ You can also check directly:
29
+
30
+ ```bash
31
+ openclew migrate
32
+ # → 12 to migrate, 45 already current, 0 errors (57 total)
33
+ ```
34
+
35
+ No output = nothing to do.
36
+
37
+ ---
38
+
39
+ ## How it works
40
+
41
+ `migrate` converts docs from older formats to the current openclew format.
42
+ It is **safe by default**:
43
+
44
+ - **Dry-run first** — shows what would change without touching files
45
+ - **`--write` to apply** — only modifies files when you explicitly ask
46
+ - **Git-friendly** — files are tracked, `git diff` shows exactly what changed
47
+ - **Idempotent** — running it twice produces the same result
48
+ - **Skips current docs** — only touches files that need updating
49
+
50
+ ### What it converts
51
+
52
+ | Before | After |
53
+ |--------|-------|
54
+ | `R.AlphA.Doc@7.0.0` (line 1) | `openclew@0.3.0 · created: ... · type: ... · ...` |
55
+ | `subject: Title` (plain L1) | `**subject:** Title` (bold L1) |
56
+ | `summary: ...` | `**doc_brief:** ...` |
57
+ | `# 📋 L1 · Métadonnées` | _(removed — metadata is on line 1)_ |
58
+ | `# 📝 L2 · Résumé` | `# L2 - Summary` |
59
+ | `# 🔧 L3 · Détails` | `# L3 - Details` |
60
+ | YAML frontmatter (`---`) | Replaced by line 1 + L1 block |
61
+ | `status: Vivant` | `status: Active` |
62
+ | `status: Terminé` | `status: Done` |
63
+
64
+ ### What it does NOT change
65
+
66
+ - **L2/L3 body content** — only headers are normalized, your content is untouched
67
+ - **Sub-headers** — `## Objective`, `## Key points` etc. are preserved as-is
68
+ - **`related_docs` paths** — kept on line 1 but not repointed if you move files
69
+ - **Files already in openclew format** — skipped entirely
70
+ - **`_INDEX.md`** — auto-generated, never touched
71
+
72
+ ---
73
+
74
+ ## Step by step
75
+
76
+ ### 1. Update openclew
77
+
78
+ ```bash
79
+ npm install -g openclew@latest
80
+ ```
81
+
82
+ ### 2. Check your docs
83
+
84
+ ```bash
85
+ openclew status
86
+ ```
87
+
88
+ Look for the "Legacy format" line. If it says 0, you're done.
89
+
90
+ ### 3. Preview changes
91
+
92
+ ```bash
93
+ openclew migrate
94
+ ```
95
+
96
+ This lists every file that would be converted. No files are modified.
97
+
98
+ ### 4. Apply
99
+
100
+ ```bash
101
+ openclew migrate --write
102
+ ```
103
+
104
+ Each converted file is printed with `✓`.
105
+
106
+ ### 5. Review and commit
107
+
108
+ ```bash
109
+ git diff # inspect the changes
110
+ openclew index # regenerate the index
111
+ git add doc/ && git commit -m "chore: migrate docs to openclew format"
112
+ ```
113
+
114
+ ### 6. Verify
115
+
116
+ ```bash
117
+ openclew status # should show 0 legacy docs
118
+ openclew migrate # should show "0 to migrate"
119
+ ```
120
+
121
+ ---
122
+
123
+ ## After migrating
124
+
125
+ - **New docs** you create with `openclew add ref` / `openclew add log` already
126
+ use the current format. No action needed.
127
+ - **Docs created by AI agents** will follow the format they see in your codebase.
128
+ Once your existing docs are migrated, agents will generate in the new format.
129
+ - **Empty `doc_brief`** — some old docs may have no brief after migration.
130
+ Run `openclew status` to find them and fill them in.
131
+
132
+ ---
133
+
134
+ ## Version-specific notes
135
+
136
+ ### → 0.4.0 (format migration)
137
+
138
+ First migration release. Converts from the legacy format (YAML frontmatter,
139
+ plain `key: value` L1, emoji headers) to the openclew format (condensed line 1,
140
+ bold L1 fields, clean headers).
141
+
142
+ **Scope**: line 1 + L1 block + L2/L3 main headers.
143
+
144
+ **Not in scope**: sub-header emojis, `related_docs` repointing, recursive
145
+ `doc/` subdirectory scanning (refdocs must be in `doc/_*.md`, not `doc/ref/`).
146
+
147
+ ---
148
+
149
+ ## Limitations
150
+
151
+ ### Flat `doc/` structure required
152
+
153
+ All openclew tools (search, index, status, migrate) scan `doc/_*.md` for refdocs
154
+ and `doc/log/*.md` for logs. Subdirectories like `doc/ref/` are not scanned yet.
155
+
156
+ If you plan to reorganize into subdirectories, wait for recursive scan support
157
+ (tracked in the openclew roadmap).
158
+
159
+ ### `related_docs` are not repointed
160
+
161
+ If a doc references `related_docs: [doc/_AUTH.md]` and you rename that file,
162
+ the reference breaks. `migrate` preserves paths as-is — manual update required.
163
+
164
+ ### Parsers are backward-compatible
165
+
166
+ Even without migrating, your docs are still readable by openclew tools.
167
+ Migration improves consistency and enables new features, but is not blocking.
package/bin/openclew.js CHANGED
@@ -9,39 +9,67 @@ const USAGE = `
9
9
  openclew — Long Life Memory for LLMs
10
10
 
11
11
  Usage:
12
- openclew init Set up openclew in the current project
13
- openclew new <title> Create a living doc (evolves with the project)
14
- openclew log <title> Create a session log (frozen facts)
15
- openclew checkout End-of-session summary + log creation
16
- openclew index Regenerate doc/_INDEX.md
17
- openclew help Show this help
12
+ openclew init Set up openclew in your project
13
+ openclew add ref <title> Create a refdoc (evolves with the project)
14
+ openclew add log <title> Create a session log (frozen facts)
15
+ openclew search <query> Search docs by keyword
16
+ openclew checkout End-of-session summary
18
17
 
19
- Options:
20
- --no-hook Skip pre-commit hook installation (init)
21
- --no-inject Skip instruction file injection (init)
18
+ Run 'openclew help --all' for advanced commands.
19
+ More at: https://github.com/openclew/openclew
20
+ `.trim();
22
21
 
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
22
+ const USAGE_ALL = `
23
+ openclew Long Life Memory for LLMs
28
24
 
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
25
+ Usage:
26
+ openclew init Set up openclew in your project
27
+ openclew add ref <title> Create a refdoc (evolves with the project)
28
+ openclew add log <title> Create a session log (frozen facts)
29
+ openclew search <query> Search docs by keyword
30
+ openclew checkout End-of-session summary
31
+
32
+ Advanced:
33
+ openclew status Documentation health dashboard
34
+ openclew index Regenerate doc/_INDEX.md
35
+ openclew mcp Start MCP server (stdio JSON-RPC)
36
+
37
+ Options (init):
38
+ --no-hook Skip pre-commit hook installation
39
+ --no-inject Skip instruction file injection
32
40
  `.trim();
33
41
 
34
42
  if (!command || command === "help" || command === "--help" || command === "-h") {
35
- console.log(USAGE);
43
+ const showAll = args.includes("--all");
44
+ console.log(showAll ? USAGE_ALL : USAGE);
45
+ process.exit(0);
46
+ }
47
+
48
+ // Handle "add ref" / "add log" subcommands
49
+ if (command === "add") {
50
+ const sub = args[1];
51
+ if (sub === "ref") {
52
+ require("../lib/new-doc");
53
+ } else if (sub === "log") {
54
+ require("../lib/new-log");
55
+ } else {
56
+ console.error(`Unknown type: ${sub || "(none)"}`);
57
+ console.error('Usage: openclew add ref <title> or openclew add log <title>');
58
+ process.exit(1);
59
+ }
36
60
  process.exit(0);
37
61
  }
38
62
 
63
+ // Legacy aliases
39
64
  const commands = {
40
65
  init: () => require("../lib/init"),
41
66
  new: () => require("../lib/new-doc"),
42
67
  log: () => require("../lib/new-log"),
43
68
  checkout: () => require("../lib/checkout"),
69
+ search: () => require("../lib/search"),
70
+ status: () => require("../lib/status"),
44
71
  index: () => require("../lib/index-gen"),
72
+ mcp: () => require("../lib/mcp-server"),
45
73
  };
46
74
 
47
75
  if (!commands[command]) {
package/lib/checkout.js CHANGED
@@ -10,9 +10,18 @@
10
10
  const fs = require("fs");
11
11
  const path = require("path");
12
12
  const { execSync } = require("child_process");
13
- const { logContent, slugifyLog, today } = require("./templates");
13
+ const { slugifyLog, today } = require("./templates");
14
14
  const { readConfig } = require("./config");
15
15
 
16
+ function ocVersion() {
17
+ try {
18
+ const pkg = require(path.join(__dirname, "..", "package.json"));
19
+ return pkg.version;
20
+ } catch {
21
+ return "0.0.0";
22
+ }
23
+ }
24
+
16
25
  const PROJECT_ROOT = process.cwd();
17
26
  const DOC_DIR = path.join(PROJECT_ROOT, "doc");
18
27
  const LOG_DIR = path.join(DOC_DIR, "log");
@@ -53,12 +62,12 @@ function collectGitActivity() {
53
62
  ? fs.readdirSync(LOG_DIR).filter((f) => f.startsWith(date))
54
63
  : [];
55
64
 
56
- // Living docs
57
- const livingDocs = fs.existsSync(DOC_DIR)
65
+ // Refdocs
66
+ const refdocs = fs.existsSync(DOC_DIR)
58
67
  ? fs.readdirSync(DOC_DIR).filter((f) => f.startsWith("_") && f !== "_INDEX.md" && f.endsWith(".md"))
59
68
  : [];
60
69
 
61
- return { date, commits, uncommitted, files, existingLogs, livingDocs };
70
+ return { date, commits, uncommitted, files, existingLogs, refdocs };
62
71
  }
63
72
 
64
73
  function extractActions(commits) {
@@ -98,7 +107,7 @@ function typeLabel(type) {
98
107
  }
99
108
 
100
109
  function displaySummary(activity) {
101
- const { date, commits, uncommitted, existingLogs, livingDocs } = activity;
110
+ const { date, commits, uncommitted, existingLogs, refdocs } = activity;
102
111
  const actions = extractActions(commits);
103
112
 
104
113
  console.log(`\nopenclew checkout — ${date}\n`);
@@ -152,10 +161,10 @@ function displaySummary(activity) {
152
161
  }
153
162
  console.log("");
154
163
 
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) {
164
+ // Refdocs reminder
165
+ if (refdocs.length > 0) {
166
+ console.log(" 📚 Refdocs — check if any need updating:");
167
+ for (const doc of refdocs) {
159
168
  console.log(` ${doc}`);
160
169
  }
161
170
  console.log("");
@@ -189,15 +198,14 @@ function generateSessionLog(activity, actions) {
189
198
  .map((a) => `- ${typeLabel(a.type)}: ${a.desc} (${a.hash})`)
190
199
  .join("\n");
191
200
 
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
+ const ver = ocVersion();
202
+ const logType = actions.length === 1 ? actions[0].type === "fix" ? "Bug" : "Feature" : "Feature";
203
+ const content = `openclew@${ver} · date: ${date} · type: ${logType} · status: Done · category: · keywords: ${keywordsStr}
204
+
205
+ <!-- L1_START -->
206
+ **subject:** ${sessionTitle}
207
+
208
+ **doc_brief:** ${actions.map((a) => a.desc).join(". ")}.
201
209
  <!-- L1_END -->
202
210
 
203
211
  ---
package/lib/index-gen.js CHANGED
@@ -1,40 +1,115 @@
1
1
  /**
2
2
  * openclew index — regenerate doc/_INDEX.md
3
3
  *
4
- * Wraps hooks/generate-index.py. Falls back to a JS implementation
5
- * if Python is not available.
4
+ * Pure JS implementation. Reuses parsers from search.js (SSOT).
5
+ * Zero dependencies Node 16+ standard library only.
6
6
  */
7
7
 
8
- const { execSync } = require("child_process");
9
8
  const fs = require("fs");
10
9
  const path = require("path");
10
+ const { collectDocs } = require("./search");
11
11
 
12
- const docDir = path.join(process.cwd(), "doc");
12
+ /**
13
+ * Generate _INDEX.md content from parsed docs.
14
+ *
15
+ * @param {string} docDir - Absolute path to doc/ directory
16
+ * @returns {string} Generated index content
17
+ */
18
+ function generateIndex(docDir) {
19
+ const docs = collectDocs(docDir);
20
+ const refdocs = docs.filter((d) => d.kind === "refdoc");
21
+ const logs = docs.filter((d) => d.kind === "log");
22
+
23
+ const now = new Date();
24
+ const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
25
+
26
+ const lines = [
27
+ "# Project Knowledge Index",
28
+ "",
29
+ `> Auto-generated by [openclew](https://github.com/openclew/openclew) on ${timestamp}.`,
30
+ "> Do not edit manually — rebuilt from L1 metadata on every commit.",
31
+ "",
32
+ ];
33
+
34
+ // Refdocs section
35
+ lines.push("## Refdocs");
36
+ lines.push("");
37
+ if (refdocs.length) {
38
+ lines.push("| Document | Subject | Status | Category |");
39
+ lines.push("|----------|---------|--------|----------|");
40
+ for (const doc of refdocs) {
41
+ const name = path.basename(doc.filepath);
42
+ const subject = doc.meta.subject || "—";
43
+ const status = doc.meta.status || "—";
44
+ const category = doc.meta.category || "—";
45
+ const relPath = path.relative(path.dirname(docDir), doc.filepath);
46
+ lines.push(`| [${name}](${relPath}) | ${subject} | ${status} | ${category} |`);
47
+ }
48
+ } else {
49
+ lines.push("_No refdocs yet. Create one with `npx openclew add ref \"Title\"`._");
50
+ }
51
+ lines.push("");
52
+
53
+ // Logs section (last 20)
54
+ const displayLogs = logs.slice(0, 20);
55
+ lines.push("## Recent logs");
56
+ lines.push("");
57
+ if (displayLogs.length) {
58
+ lines.push("| Date | Subject | Status | Category |");
59
+ lines.push("|------|---------|--------|----------|");
60
+ for (const doc of displayLogs) {
61
+ const date = doc.meta.date || path.basename(doc.filepath).slice(0, 10);
62
+ const subject = doc.meta.subject || "—";
63
+ const status = doc.meta.status || "—";
64
+ const category = doc.meta.category || "—";
65
+ const relPath = path.relative(path.dirname(docDir), doc.filepath);
66
+ lines.push(`| ${date} | [${subject}](${relPath}) | ${status} | ${category} |`);
67
+ }
68
+ if (logs.length > 20) {
69
+ lines.push("");
70
+ lines.push(`_${logs.length - 20} older logs not shown._`);
71
+ }
72
+ } else {
73
+ lines.push("_No logs yet. Create one with `npx openclew add log \"Title\"`._");
74
+ }
75
+ lines.push("");
13
76
 
14
- if (!fs.existsSync(docDir)) {
15
- console.error("No doc/ directory found. Run 'openclew init' first.");
16
- process.exit(1);
77
+ // Stats
78
+ lines.push("---");
79
+ lines.push(`**${refdocs.length}** refdocs, **${logs.length}** logs.`);
80
+ lines.push("");
81
+
82
+ return lines.join("\n");
17
83
  }
18
84
 
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
- );
85
+ /**
86
+ * Write _INDEX.md to disk.
87
+ *
88
+ * @param {string} docDir - Absolute path to doc/ directory
89
+ * @returns {{ refdocs: number, logs: number }} Counts
90
+ */
91
+ function writeIndex(docDir) {
92
+ const content = generateIndex(docDir);
93
+ const indexPath = path.join(docDir, "_INDEX.md");
94
+ fs.writeFileSync(indexPath, content, "utf-8");
95
+
96
+ const docs = collectDocs(docDir);
97
+ const refdocs = docs.filter((d) => d.kind === "refdoc").length;
98
+ const logs = docs.filter((d) => d.kind === "log").length;
99
+ return { refdocs, logs };
100
+ }
101
+
102
+ // CLI runner
103
+ if (require.main === module || process.argv.includes("index")) {
104
+ const docDir = process.argv[2] || path.join(process.cwd(), "doc");
105
+
106
+ if (!fs.existsSync(docDir)) {
107
+ console.error("No doc/ directory found. Run 'openclew init' first.");
35
108
  process.exit(1);
36
109
  }
110
+
111
+ const { refdocs, logs } = writeIndex(docDir);
112
+ console.log(`Generated doc/_INDEX.md (${refdocs} refdocs, ${logs} logs)`);
37
113
  }
38
114
 
39
- console.error("generate-index.py not found. Run 'openclew init' first.");
40
- process.exit(1);
115
+ module.exports = { generateIndex, writeIndex };