openclew 0.2.1 → 0.4.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.
@@ -0,0 +1,27 @@
1
+ <!-- openclew-managed -->
2
+ # oc-init — Set up openclew in the current project
3
+
4
+ Initialize structured documentation so AI agents and humans navigate project knowledge efficiently.
5
+
6
+ **Usage:** `/oc-init`
7
+
8
+ ## Sequence
9
+
10
+ 1. Run `npx openclew init`
11
+ 2. Display what was created
12
+ 3. Read the generated guide (`doc/_USING_OPENCLEW.md`) to understand the setup
13
+ 4. Propose creating a first architecture doc:
14
+ - "Want me to create `doc/_ARCHITECTURE.md` based on the current project structure?"
15
+ - If yes: analyze the project (main dirs, stack, key files) and fill in the template
16
+
17
+ ## After setup
18
+
19
+ The agent will now consult `doc/_INDEX.md` before starting tasks. Available commands:
20
+
21
+ - `/oc-search <query>` — Search existing docs
22
+ - `/oc-status` — Health dashboard
23
+ - `/oc-checkout` — End-of-session summary
24
+
25
+ To add knowledge manually:
26
+ - `npx openclew add ref "Title"` — Create a reference doc
27
+ - `npx openclew add log "Title"` — Create a session log
@@ -0,0 +1,41 @@
1
+ <!-- openclew-managed -->
2
+ # oc-peek — Discover project knowledge before working
3
+
4
+ First reflex before exploring a project: shows the instruction file and lists all available docs.
5
+
6
+ **Usage:** `/oc-peek` (no arguments, uses the current project)
7
+
8
+ ## Sequence
9
+
10
+ ### Step 1: Run the CLI
11
+
12
+ ```bash
13
+ npx openclew peek
14
+ ```
15
+
16
+ This lists:
17
+ - The instruction file (CLAUDE.md / AGENTS.md) path
18
+ - All refdocs in `doc/` with their subjects
19
+ - All `_*.md` files outside `doc/` (prompts, scripts, etc.)
20
+ - Subdirectories of `doc/`
21
+
22
+ ### Step 2: Display the instruction file
23
+
24
+ Read the instruction file (CLAUDE.md or AGENTS.md) and display it **in full** — do not summarize.
25
+
26
+ ### Step 3: Display the refdoc list
27
+
28
+ Display the CLI output as-is. This is a **listing only** — do not read the refdocs at this stage.
29
+
30
+ ## Rules
31
+
32
+ - **Do not summarize** the instruction file — display it integrally
33
+ - **Do not read** the refdocs — list file names only
34
+ - If no instruction file exists: indicate `(no instruction file found)`
35
+ - If no `doc/` directory exists: indicate `(no doc/ directory)`
36
+
37
+ ## Related commands
38
+
39
+ - `/oc-search <query>` — Search docs by keyword
40
+ - `/oc-status` — Documentation health dashboard
41
+ - `/oc-checkout` — End-of-session summary
@@ -0,0 +1,25 @@
1
+ <!-- openclew-managed -->
2
+ # oc-search — Search project documentation
3
+
4
+ Search your project's knowledge base by keyword.
5
+
6
+ **Usage:** `/oc-search <query>`
7
+
8
+ ## Sequence
9
+
10
+ 1. Run `npx openclew search "$ARGUMENTS"` to get results
11
+ 2. Display results to the user
12
+ 3. If results found: propose to read the most relevant doc(s) at L2 level
13
+ 4. If no results: suggest alternative keywords or propose creating a new doc
14
+
15
+ ## Reading results
16
+
17
+ After finding a doc, read it progressively:
18
+ - **L1** (between `L1_START`/`L1_END`) — subject + brief, ~40 tokens. "Should I read this?"
19
+ - **L2** (between `L2_START`/`L2_END`) — summary, essential context
20
+ - **L3** (between `L3_START`/`L3_END`) — full details, only when deep-diving
21
+
22
+ ## Related commands
23
+
24
+ - `/oc-status` — Health dashboard (missing briefs, stale docs)
25
+ - `/oc-checkout` — End-of-session summary
@@ -0,0 +1,20 @@
1
+ <!-- openclew-managed -->
2
+ # oc-status — Documentation health dashboard
3
+
4
+ Display the health status of project documentation.
5
+
6
+ **Usage:** `/oc-status`
7
+
8
+ ## Sequence
9
+
10
+ 1. Run `npx openclew status` to get the dashboard
11
+ 2. Display results to the user
12
+ 3. If issues found, propose actions:
13
+ - **Missing doc_brief**: "These docs have empty briefs — want me to fill them in?"
14
+ - **Stale docs**: "These docs haven't been updated in a while — want me to review them?"
15
+ - **No recent logs**: "No log created recently — want me to create one for this session?"
16
+
17
+ ## Related commands
18
+
19
+ - `/oc-search <query>` — Search docs by keyword
20
+ - `/oc-checkout` — End-of-session summary (also creates logs)
package/lib/checkout.js CHANGED
@@ -253,11 +253,9 @@ ${commitList}
253
253
  }
254
254
 
255
255
  function regenerateIndex() {
256
- const indexScript = path.join(DOC_DIR, "generate-index.py");
257
- if (!fs.existsSync(indexScript)) return;
258
-
259
256
  try {
260
- execSync(`python3 "${indexScript}" "${DOC_DIR}"`, { stdio: "pipe" });
257
+ const { writeIndex } = require("./index-gen");
258
+ writeIndex(DOC_DIR);
261
259
  console.log(" 📋 Regenerated doc/_INDEX.md");
262
260
  } catch {
263
261
  // Silent — index will be regenerated on next commit anyway
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 };
package/lib/init.js CHANGED
@@ -15,7 +15,7 @@ const readline = require("readline");
15
15
  const { detectInstructionFiles, findAgentsMdCaseInsensitive } = require("./detect");
16
16
  const { inject, isAlreadyInjected } = require("./inject");
17
17
  const { writeConfig } = require("./config");
18
- const { guideContent, exampleRefdocContent, exampleLogContent, today } = require("./templates");
18
+ const { guideContent, frameworkIntegrationContent, exampleRefdocContent, exampleLogContent, today } = require("./templates");
19
19
 
20
20
  const PROJECT_ROOT = process.cwd();
21
21
  const DOC_DIR = path.join(PROJECT_ROOT, "doc");
@@ -126,8 +126,8 @@ function installPreCommitHook() {
126
126
  }
127
127
 
128
128
  const preCommitPath = path.join(hooksDir, "pre-commit");
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"
129
+ const indexScript = `if command -v npx >/dev/null 2>&1; then
130
+ npx --yes openclew index 2>/dev/null || echo "openclew: index generation failed"
131
131
  git add doc/_INDEX.md 2>/dev/null
132
132
  fi`;
133
133
 
@@ -168,26 +168,19 @@ function updateGitignore() {
168
168
  }
169
169
  }
170
170
 
171
- function copyGenerateIndex() {
172
- const src = path.join(__dirname, "..", "hooks", "generate-index.py");
173
- const dst = path.join(DOC_DIR, "generate-index.py");
174
-
175
- if (fs.existsSync(dst)) {
176
- console.log(" doc/generate-index.py already exists");
177
- return false;
178
- }
179
-
180
- if (fs.existsSync(src)) {
181
- fs.copyFileSync(src, dst);
182
- console.log(" Copied generate-index.py to doc/");
171
+ function cleanupLegacyPython() {
172
+ // Remove legacy generate-index.py if present (replaced by JS-native index-gen)
173
+ const legacyScript = path.join(DOC_DIR, "generate-index.py");
174
+ if (fs.existsSync(legacyScript)) {
175
+ fs.unlinkSync(legacyScript);
176
+ console.log(" Removed legacy doc/generate-index.py (now JS-native)");
183
177
  return true;
184
178
  }
185
-
186
- console.log(" generate-index.py not found in package — skipping");
179
+ console.log(" No legacy Python script to clean up");
187
180
  return false;
188
181
  }
189
182
 
190
- function createDocs() {
183
+ function createDocs(entryPointPath) {
191
184
  // Guide — always created
192
185
  const guidePath = path.join(DOC_DIR, "_USING_OPENCLEW.md");
193
186
  if (!fs.existsSync(guidePath)) {
@@ -197,11 +190,30 @@ function createDocs() {
197
190
  console.log(" doc/_USING_OPENCLEW.md already exists");
198
191
  }
199
192
 
200
- // Example refdoc
193
+ // Framework integration guide
194
+ const frameworkPath = path.join(DOC_DIR, "_OPENCLEW_FRAMEWORK_INTEGRATION.md");
195
+ if (!fs.existsSync(frameworkPath)) {
196
+ fs.writeFileSync(frameworkPath, frameworkIntegrationContent(), "utf-8");
197
+ console.log(" Created doc/_OPENCLEW_FRAMEWORK_INTEGRATION.md (framework integration guide)");
198
+ } else {
199
+ console.log(" doc/_OPENCLEW_FRAMEWORK_INTEGRATION.md already exists");
200
+ }
201
+
202
+ // Architecture refdoc — seeded from existing instruction file if available
201
203
  const examplePath = path.join(DOC_DIR, "_ARCHITECTURE.md");
202
204
  if (!fs.existsSync(examplePath)) {
203
- fs.writeFileSync(examplePath, exampleRefdocContent(), "utf-8");
204
- console.log(" Created doc/_ARCHITECTURE.md (example refdoc)");
205
+ let existingInstructions = null;
206
+ if (entryPointPath && fs.existsSync(entryPointPath)) {
207
+ try {
208
+ existingInstructions = fs.readFileSync(entryPointPath, "utf-8");
209
+ } catch {}
210
+ }
211
+ fs.writeFileSync(examplePath, exampleRefdocContent(existingInstructions), "utf-8");
212
+ if (existingInstructions) {
213
+ console.log(" Created doc/_ARCHITECTURE.md (seeded from instruction file)");
214
+ } else {
215
+ console.log(" Created doc/_ARCHITECTURE.md (template)");
216
+ }
205
217
  } else {
206
218
  console.log(" doc/_ARCHITECTURE.md already exists");
207
219
  }
@@ -217,15 +229,66 @@ function createDocs() {
217
229
  }
218
230
 
219
231
  function runIndexGenerator() {
220
- const indexScript = path.join(DOC_DIR, "generate-index.py");
221
- if (!fs.existsSync(indexScript)) return;
232
+ if (!fs.existsSync(DOC_DIR)) return;
222
233
 
223
234
  try {
224
- const { execSync } = require("child_process");
225
- execSync(`python3 "${indexScript}" "${DOC_DIR}"`, { stdio: "pipe" });
226
- console.log(" Generated doc/_INDEX.md");
227
- } catch {
228
- console.log(" Could not generate index (python3 not available)");
235
+ const { writeIndex } = require("./index-gen");
236
+ const { refdocs, logs } = writeIndex(DOC_DIR);
237
+ console.log(` Generated doc/_INDEX.md (${refdocs} refdocs, ${logs} logs)`);
238
+ } catch (err) {
239
+ console.log(` Could not generate index: ${err.message}`);
240
+ }
241
+ }
242
+
243
+ function installSlashCommands() {
244
+ const home = process.env.HOME || process.env.USERPROFILE;
245
+ if (!home) {
246
+ console.log(" Cannot determine home directory — skipping");
247
+ return;
248
+ }
249
+
250
+ const claudeCommandsDir = path.join(home, ".claude", "commands");
251
+ if (!fs.existsSync(path.join(home, ".claude"))) {
252
+ console.log(" No ~/.claude/ found (Claude Code not installed) — skipping");
253
+ return;
254
+ }
255
+
256
+ if (!fs.existsSync(claudeCommandsDir)) {
257
+ fs.mkdirSync(claudeCommandsDir, { recursive: true });
258
+ }
259
+
260
+ // Find commands/ dir relative to this package
261
+ const pkgCommandsDir = path.join(__dirname, "..", "commands");
262
+ if (!fs.existsSync(pkgCommandsDir)) {
263
+ console.log(" No commands/ directory in package — skipping");
264
+ return;
265
+ }
266
+
267
+ const MARKER = "<!-- openclew-managed -->";
268
+ const files = fs.readdirSync(pkgCommandsDir).filter((f) => f.endsWith(".md"));
269
+ let installed = 0;
270
+
271
+ for (const file of files) {
272
+ const dest = path.join(claudeCommandsDir, file);
273
+
274
+ // Only overwrite if file has the managed marker (or doesn't exist)
275
+ if (fs.existsSync(dest)) {
276
+ const existing = fs.readFileSync(dest, "utf-8");
277
+ if (!existing.includes(MARKER)) {
278
+ console.log(` Skipped ${file} (user-modified)`);
279
+ continue;
280
+ }
281
+ }
282
+
283
+ fs.copyFileSync(path.join(pkgCommandsDir, file), dest);
284
+ installed++;
285
+ }
286
+
287
+ if (installed > 0) {
288
+ console.log(` Installed ${installed} slash command(s) → ~/.claude/commands/`);
289
+ console.log(" Available: /oc-checkout, /oc-search, /oc-init, /oc-status");
290
+ } else {
291
+ console.log(" Slash commands already up to date");
229
292
  }
230
293
  }
231
294
 
@@ -240,9 +303,9 @@ async function main() {
240
303
  console.log("\n2. Gitignore");
241
304
  updateGitignore();
242
305
 
243
- // Step 3: Copy index generator
306
+ // Step 3: Cleanup legacy Python (if upgrading from older version)
244
307
  console.log("\n3. Index generator");
245
- copyGenerateIndex();
308
+ cleanupLegacyPython();
246
309
 
247
310
  // Step 4: Entry point
248
311
  console.log("\n4. Entry point");
@@ -273,12 +336,21 @@ async function main() {
273
336
 
274
337
  // Step 6: Docs
275
338
  console.log("\n6. Docs");
276
- createDocs();
339
+ createDocs(entryPoint ? entryPoint.fullPath : null);
277
340
 
278
341
  // Step 7: Generate index
279
342
  console.log("\n7. Index");
280
343
  runIndexGenerator();
281
344
 
345
+ // Step 8: Install Claude Code slash commands
346
+ const noCommands = args.includes("--no-commands");
347
+ console.log("\n8. Slash commands");
348
+ if (noCommands) {
349
+ console.log(" Skipping (--no-commands)");
350
+ } else {
351
+ installSlashCommands();
352
+ }
353
+
282
354
  // Done
283
355
  console.log("\n─── Ready ───\n");
284
356
  if (entryPoint) {
package/lib/inject.js CHANGED
@@ -7,17 +7,27 @@ const fs = require("fs");
7
7
  const OPENCLEW_BLOCK = `
8
8
  ## Project knowledge (openclew)
9
9
 
10
- This file is the **entry point** for project documentation.
10
+ This project uses \`doc/\` as its knowledge base. Before starting any task:
11
11
 
12
- **Doc-first rule:** before any task, read \`doc/_INDEX.md\` to find docs related to the task. Read them before exploring code.
12
+ 1. **Read \`doc/_INDEX.md\`** it lists all available docs with a one-line summary each
13
+ 2. **Pick your reference doc(s)** — choose one or more docs relevant to what you're about to do
14
+ 3. **Read them** (L1 for relevance, L2 for context) — then start working
15
+ 4. **No matching doc?** — propose creating a refdoc with \`npx openclew new "Title"\` before starting
13
16
 
14
- Two types of docs in \`doc/\`:
15
- - **Refdocs** (\`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
+ If a doc contains placeholder comments (\`<!-- ... -->\`), fill them in based on what you observe in the code. This is expected — the docs are meant to be written by you.
17
18
 
18
- Each doc has 3 levels: **L1** (metadata — read first to decide relevance) → **L2** (summary) → **L3** (full details, only when needed).
19
+ Two types of docs:
20
+ - **Refdocs** (\`doc/_*.md\`) — architecture, conventions, decisions (evolve over time)
21
+ - **Logs** (\`doc/log/YYYY-MM-DD_*.md\`) — frozen facts from past sessions
19
22
 
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\`.
23
+ Each doc has 3 levels: **L1** (subject + brief 1 line) **L2** (summary) **L3** (full details, only when needed).
24
+
25
+ **Session commands** (user asks in chat, you run):
26
+ - "peek" → \`npx openclew peek\` (list instruction file + all refdocs)
27
+ - "checkout" → \`npx openclew checkout\` (end-of-session summary + log)
28
+ - "new doc about X" → \`npx openclew new "X"\` (create refdoc)
29
+ - "search X" → \`npx openclew search "X"\` (search docs)
30
+ - "doc status" → \`npx openclew status\` (health dashboard)
21
31
  `.trim();
22
32
 
23
33
  const MARKER_START = "<!-- openclew_START -->";