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 +28 -122
- package/UPGRADING.md +167 -0
- package/bin/openclew.js +46 -18
- package/lib/checkout.js +26 -18
- package/lib/index-gen.js +100 -25
- package/lib/init.js +70 -37
- package/lib/inject.js +16 -7
- package/lib/mcp-server.js +313 -0
- package/lib/new-doc.js +15 -7
- package/lib/new-log.js +5 -5
- package/lib/search.js +242 -0
- package/lib/status.js +151 -0
- package/lib/templates.js +270 -94
- package/package.json +24 -3
- package/skills/openclew-checkpoint/SKILL.md +36 -0
- package/skills/openclew-init/SKILL.md +49 -0
- package/skills/openclew-search/SKILL.md +45 -0
- package/templates/FORMAT.md +299 -0
- package/templates/log.md +5 -8
- package/templates/onboarding/flow.md +59 -0
- package/templates/onboarding/scaffold_index.md +31 -0
- package/templates/{living.md → refdoc.md} +5 -9
- package/hooks/generate-index.py +0 -157
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,
|
|
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
|
|
130
|
-
|
|
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
|
|
|
@@ -150,26 +150,37 @@ fi`;
|
|
|
150
150
|
return true;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
function
|
|
154
|
-
const
|
|
155
|
-
const
|
|
153
|
+
function updateGitignore() {
|
|
154
|
+
const gitignorePath = path.join(PROJECT_ROOT, ".gitignore");
|
|
155
|
+
const entry = "doc/log/";
|
|
156
156
|
|
|
157
|
-
if (fs.existsSync(
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
if (fs.existsSync(gitignorePath)) {
|
|
158
|
+
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
159
|
+
if (content.includes(entry)) {
|
|
160
|
+
console.log(" .gitignore already ignores doc/log/");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
fs.appendFileSync(gitignorePath, `\n${entry}\n`, "utf-8");
|
|
164
|
+
console.log(" Added doc/log/ to .gitignore");
|
|
165
|
+
} else {
|
|
166
|
+
fs.writeFileSync(gitignorePath, `${entry}\n`, "utf-8");
|
|
167
|
+
console.log(" Created .gitignore with doc/log/");
|
|
160
168
|
}
|
|
169
|
+
}
|
|
161
170
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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)");
|
|
165
177
|
return true;
|
|
166
178
|
}
|
|
167
|
-
|
|
168
|
-
console.log(" generate-index.py not found in package — skipping");
|
|
179
|
+
console.log(" No legacy Python script to clean up");
|
|
169
180
|
return false;
|
|
170
181
|
}
|
|
171
182
|
|
|
172
|
-
function createDocs() {
|
|
183
|
+
function createDocs(entryPointPath) {
|
|
173
184
|
// Guide — always created
|
|
174
185
|
const guidePath = path.join(DOC_DIR, "_USING_OPENCLEW.md");
|
|
175
186
|
if (!fs.existsSync(guidePath)) {
|
|
@@ -179,11 +190,30 @@ function createDocs() {
|
|
|
179
190
|
console.log(" doc/_USING_OPENCLEW.md already exists");
|
|
180
191
|
}
|
|
181
192
|
|
|
182
|
-
//
|
|
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
|
|
183
203
|
const examplePath = path.join(DOC_DIR, "_ARCHITECTURE.md");
|
|
184
204
|
if (!fs.existsSync(examplePath)) {
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
}
|
|
187
217
|
} else {
|
|
188
218
|
console.log(" doc/_ARCHITECTURE.md already exists");
|
|
189
219
|
}
|
|
@@ -199,15 +229,14 @@ function createDocs() {
|
|
|
199
229
|
}
|
|
200
230
|
|
|
201
231
|
function runIndexGenerator() {
|
|
202
|
-
|
|
203
|
-
if (!fs.existsSync(indexScript)) return;
|
|
232
|
+
if (!fs.existsSync(DOC_DIR)) return;
|
|
204
233
|
|
|
205
234
|
try {
|
|
206
|
-
const {
|
|
207
|
-
|
|
208
|
-
console.log(
|
|
209
|
-
} catch {
|
|
210
|
-
console.log(
|
|
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}`);
|
|
211
240
|
}
|
|
212
241
|
}
|
|
213
242
|
|
|
@@ -218,12 +247,16 @@ async function main() {
|
|
|
218
247
|
console.log("1. Project structure");
|
|
219
248
|
createDirs();
|
|
220
249
|
|
|
221
|
-
// Step 2:
|
|
222
|
-
console.log("\n2.
|
|
223
|
-
|
|
250
|
+
// Step 2: Gitignore
|
|
251
|
+
console.log("\n2. Gitignore");
|
|
252
|
+
updateGitignore();
|
|
253
|
+
|
|
254
|
+
// Step 3: Cleanup legacy Python (if upgrading from older version)
|
|
255
|
+
console.log("\n3. Index generator");
|
|
256
|
+
cleanupLegacyPython();
|
|
224
257
|
|
|
225
|
-
// Step
|
|
226
|
-
console.log("\
|
|
258
|
+
// Step 4: Entry point
|
|
259
|
+
console.log("\n4. Entry point");
|
|
227
260
|
const entryPoint = await resolveEntryPoint();
|
|
228
261
|
|
|
229
262
|
if (entryPoint) {
|
|
@@ -241,20 +274,20 @@ async function main() {
|
|
|
241
274
|
writeConfig({ entryPoint: null }, PROJECT_ROOT);
|
|
242
275
|
}
|
|
243
276
|
|
|
244
|
-
// Step
|
|
245
|
-
console.log("\
|
|
277
|
+
// Step 5: Pre-commit hook
|
|
278
|
+
console.log("\n5. Pre-commit hook");
|
|
246
279
|
if (noHook) {
|
|
247
280
|
console.log(" Skipping (--no-hook)");
|
|
248
281
|
} else {
|
|
249
282
|
installPreCommitHook();
|
|
250
283
|
}
|
|
251
284
|
|
|
252
|
-
// Step
|
|
253
|
-
console.log("\
|
|
254
|
-
createDocs();
|
|
285
|
+
// Step 6: Docs
|
|
286
|
+
console.log("\n6. Docs");
|
|
287
|
+
createDocs(entryPoint ? entryPoint.fullPath : null);
|
|
255
288
|
|
|
256
|
-
// Step
|
|
257
|
-
console.log("\
|
|
289
|
+
// Step 7: Generate index
|
|
290
|
+
console.log("\n7. Index");
|
|
258
291
|
runIndexGenerator();
|
|
259
292
|
|
|
260
293
|
// Done
|
package/lib/inject.js
CHANGED
|
@@ -7,17 +7,26 @@ const fs = require("fs");
|
|
|
7
7
|
const OPENCLEW_BLOCK = `
|
|
8
8
|
## Project knowledge (openclew)
|
|
9
9
|
|
|
10
|
-
This
|
|
10
|
+
This project uses \`doc/\` as its knowledge base. Before starting any task:
|
|
11
11
|
|
|
12
|
-
**
|
|
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
|
-
|
|
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
|
+
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
|
-
|
|
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
|
-
|
|
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
|
+
- "checkout" → \`npx openclew checkout\` (end-of-session summary + log)
|
|
27
|
+
- "new doc about X" → \`npx openclew new "X"\` (create refdoc)
|
|
28
|
+
- "search X" → \`npx openclew search "X"\` (search docs)
|
|
29
|
+
- "doc status" → \`npx openclew status\` (health dashboard)
|
|
21
30
|
`.trim();
|
|
22
31
|
|
|
23
32
|
const MARKER_START = "<!-- openclew_START -->";
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclew MCP server — Model Context Protocol over stdio.
|
|
3
|
+
*
|
|
4
|
+
* Exposes openclew docs as MCP tools so AI agents (Claude Code, Cursor, etc.)
|
|
5
|
+
* can search and read project documentation natively.
|
|
6
|
+
*
|
|
7
|
+
* Tools:
|
|
8
|
+
* - search_docs(query) Search docs by keyword (L1/metadata)
|
|
9
|
+
* - read_doc(path, level?) Read a doc at specified level (L1/L2/L3/full)
|
|
10
|
+
* - list_docs(kind?) List all docs with L1 metadata
|
|
11
|
+
*
|
|
12
|
+
* Protocol: MCP 2024-11-05 over stdio (JSON-RPC line-delimited)
|
|
13
|
+
* Zero dependencies — Node 16+ standard library only.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const readline = require("readline");
|
|
19
|
+
const { searchDocs, collectDocs, parseFile } = require("./search");
|
|
20
|
+
|
|
21
|
+
const PROJECT_ROOT = process.cwd();
|
|
22
|
+
const DOC_DIR = path.join(PROJECT_ROOT, "doc");
|
|
23
|
+
|
|
24
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function ocVersion() {
|
|
27
|
+
try {
|
|
28
|
+
return require(path.join(__dirname, "..", "package.json")).version;
|
|
29
|
+
} catch {
|
|
30
|
+
return "0.0.0";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function extractLevel(content, level) {
|
|
35
|
+
if (level === "full") return content;
|
|
36
|
+
|
|
37
|
+
const markers = {
|
|
38
|
+
L1: [/<!--\s*L1_START\s*-->/, /<!--\s*L1_END\s*-->/],
|
|
39
|
+
L2: [/<!--\s*L2_START\s*-->/, /<!--\s*L2_END\s*-->/],
|
|
40
|
+
L3: [/<!--\s*L3_START\s*-->/, /<!--\s*L3_END\s*-->/],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const key = level.toUpperCase();
|
|
44
|
+
if (!markers[key]) return content;
|
|
45
|
+
|
|
46
|
+
const [startRe, endRe] = markers[key];
|
|
47
|
+
const startMatch = content.match(startRe);
|
|
48
|
+
const endMatch = content.match(endRe);
|
|
49
|
+
if (!startMatch || !endMatch) return `No ${key} block found in this document.`;
|
|
50
|
+
|
|
51
|
+
const startIdx = startMatch.index + startMatch[0].length;
|
|
52
|
+
const endIdx = endMatch.index;
|
|
53
|
+
return content.slice(startIdx, endIdx).trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── MCP Tool implementations ────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function toolSearchDocs(params) {
|
|
59
|
+
const query = params.query;
|
|
60
|
+
if (!query) return { error: "Missing required parameter: query" };
|
|
61
|
+
if (!fs.existsSync(DOC_DIR)) return { error: "No doc/ directory found." };
|
|
62
|
+
|
|
63
|
+
const results = searchDocs(DOC_DIR, query);
|
|
64
|
+
return results.map((r) => ({
|
|
65
|
+
path: path.relative(PROJECT_ROOT, r.filepath),
|
|
66
|
+
kind: r.kind,
|
|
67
|
+
subject: r.meta.subject || r.filename,
|
|
68
|
+
doc_brief: r.meta.doc_brief || "",
|
|
69
|
+
status: r.meta.status || "",
|
|
70
|
+
category: r.meta.category || "",
|
|
71
|
+
score: r.score,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toolReadDoc(params) {
|
|
76
|
+
const docPath = params.path;
|
|
77
|
+
if (!docPath) return { error: "Missing required parameter: path" };
|
|
78
|
+
|
|
79
|
+
const absPath = path.resolve(PROJECT_ROOT, docPath);
|
|
80
|
+
// Security: ensure path is within project
|
|
81
|
+
if (!absPath.startsWith(PROJECT_ROOT)) return { error: "Path outside project." };
|
|
82
|
+
if (!fs.existsSync(absPath)) return { error: `File not found: ${docPath}` };
|
|
83
|
+
|
|
84
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
85
|
+
const level = params.level || "L2";
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
path: docPath,
|
|
89
|
+
level: level,
|
|
90
|
+
content: extractLevel(content, level),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toolListDocs(params) {
|
|
95
|
+
if (!fs.existsSync(DOC_DIR)) return { error: "No doc/ directory found." };
|
|
96
|
+
|
|
97
|
+
const docs = collectDocs(DOC_DIR);
|
|
98
|
+
const kind = params.kind; // "refdoc", "log", or undefined (all)
|
|
99
|
+
|
|
100
|
+
return docs
|
|
101
|
+
.filter((d) => !kind || d.kind === kind)
|
|
102
|
+
.map((d) => ({
|
|
103
|
+
path: path.relative(PROJECT_ROOT, d.filepath),
|
|
104
|
+
kind: d.kind,
|
|
105
|
+
subject: d.meta.subject || d.filename,
|
|
106
|
+
doc_brief: d.meta.doc_brief || "",
|
|
107
|
+
status: d.meta.status || "",
|
|
108
|
+
category: d.meta.category || "",
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── MCP Protocol ────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
const TOOLS = [
|
|
115
|
+
{
|
|
116
|
+
name: "search_docs",
|
|
117
|
+
description:
|
|
118
|
+
"Search project documentation by keyword. Searches subject, doc_brief, category, keywords, type, and status fields. Returns results sorted by relevance.",
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {
|
|
122
|
+
query: {
|
|
123
|
+
type: "string",
|
|
124
|
+
description: "Search query (space-separated terms)",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
required: ["query"],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "read_doc",
|
|
132
|
+
description:
|
|
133
|
+
"Read a project document at a specified level. L1 = subject + brief (~40 tokens). L2 = summary + key points. L3 = full technical details. full = entire file.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
path: {
|
|
138
|
+
type: "string",
|
|
139
|
+
description: "Relative path to the document (e.g. doc/_ARCHITECTURE.md)",
|
|
140
|
+
},
|
|
141
|
+
level: {
|
|
142
|
+
type: "string",
|
|
143
|
+
enum: ["L1", "L2", "L3", "full"],
|
|
144
|
+
description: "Level of detail to return (default: L2)",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
required: ["path"],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "list_docs",
|
|
152
|
+
description:
|
|
153
|
+
"List all project documents with their L1 metadata (subject, brief, status, category).",
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
kind: {
|
|
158
|
+
type: "string",
|
|
159
|
+
enum: ["refdoc", "log"],
|
|
160
|
+
description: "Filter by document type. Omit to list all.",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const TOOL_HANDLERS = {
|
|
168
|
+
search_docs: toolSearchDocs,
|
|
169
|
+
read_doc: toolReadDoc,
|
|
170
|
+
list_docs: toolListDocs,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
function handleMessage(msg) {
|
|
174
|
+
const { method, id, params } = msg;
|
|
175
|
+
|
|
176
|
+
switch (method) {
|
|
177
|
+
case "initialize":
|
|
178
|
+
return {
|
|
179
|
+
jsonrpc: "2.0",
|
|
180
|
+
id,
|
|
181
|
+
result: {
|
|
182
|
+
protocolVersion: "2024-11-05",
|
|
183
|
+
capabilities: { tools: {} },
|
|
184
|
+
serverInfo: {
|
|
185
|
+
name: "openclew",
|
|
186
|
+
version: ocVersion(),
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
case "notifications/initialized":
|
|
192
|
+
return null; // No response for notifications
|
|
193
|
+
|
|
194
|
+
case "tools/list":
|
|
195
|
+
return {
|
|
196
|
+
jsonrpc: "2.0",
|
|
197
|
+
id,
|
|
198
|
+
result: { tools: TOOLS },
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
case "tools/call": {
|
|
202
|
+
const toolName = params && params.name;
|
|
203
|
+
const handler = TOOL_HANDLERS[toolName];
|
|
204
|
+
if (!handler) {
|
|
205
|
+
return {
|
|
206
|
+
jsonrpc: "2.0",
|
|
207
|
+
id,
|
|
208
|
+
error: { code: -32601, message: `Unknown tool: ${toolName}` },
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const toolArgs = params.arguments || {};
|
|
213
|
+
const result = handler(toolArgs);
|
|
214
|
+
|
|
215
|
+
// If result has an error field, return as tool error content
|
|
216
|
+
if (result && result.error) {
|
|
217
|
+
return {
|
|
218
|
+
jsonrpc: "2.0",
|
|
219
|
+
id,
|
|
220
|
+
result: {
|
|
221
|
+
content: [{ type: "text", text: result.error }],
|
|
222
|
+
isError: true,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
jsonrpc: "2.0",
|
|
229
|
+
id,
|
|
230
|
+
result: {
|
|
231
|
+
content: [
|
|
232
|
+
{
|
|
233
|
+
type: "text",
|
|
234
|
+
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
default:
|
|
242
|
+
if (id !== undefined) {
|
|
243
|
+
return {
|
|
244
|
+
jsonrpc: "2.0",
|
|
245
|
+
id,
|
|
246
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return null; // Ignore unknown notifications
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── stdio transport ─────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
function run() {
|
|
256
|
+
// Check if running as CLI help
|
|
257
|
+
const args = process.argv.slice(2);
|
|
258
|
+
const cmdIndex = args.indexOf("mcp");
|
|
259
|
+
const extraArgs = cmdIndex >= 0 ? args.slice(cmdIndex + 1) : args.slice(1);
|
|
260
|
+
|
|
261
|
+
if (extraArgs.includes("--help") || extraArgs.includes("-h")) {
|
|
262
|
+
console.log("openclew MCP server — Model Context Protocol over stdio");
|
|
263
|
+
console.log("");
|
|
264
|
+
console.log("Usage: openclew mcp");
|
|
265
|
+
console.log("");
|
|
266
|
+
console.log("Starts an MCP server on stdin/stdout for AI agent integration.");
|
|
267
|
+
console.log("Configure in your AI tool's MCP settings:");
|
|
268
|
+
console.log("");
|
|
269
|
+
console.log(' { "command": "npx", "args": ["openclew", "mcp"] }');
|
|
270
|
+
console.log("");
|
|
271
|
+
console.log("Tools exposed:");
|
|
272
|
+
console.log(" search_docs(query) Search docs by keyword");
|
|
273
|
+
console.log(" read_doc(path, level?) Read doc at L1/L2/L3/full");
|
|
274
|
+
console.log(" list_docs(kind?) List all docs with L1 metadata");
|
|
275
|
+
process.exit(0);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
279
|
+
|
|
280
|
+
rl.on("line", (line) => {
|
|
281
|
+
const trimmed = line.trim();
|
|
282
|
+
if (!trimmed) return;
|
|
283
|
+
|
|
284
|
+
let msg;
|
|
285
|
+
try {
|
|
286
|
+
msg = JSON.parse(trimmed);
|
|
287
|
+
} catch {
|
|
288
|
+
const err = {
|
|
289
|
+
jsonrpc: "2.0",
|
|
290
|
+
id: null,
|
|
291
|
+
error: { code: -32700, message: "Parse error" },
|
|
292
|
+
};
|
|
293
|
+
process.stdout.write(JSON.stringify(err) + "\n");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const response = handleMessage(msg);
|
|
298
|
+
if (response) {
|
|
299
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
rl.on("close", () => process.exit(0));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Export for tests
|
|
307
|
+
module.exports = { handleMessage, extractLevel, TOOLS };
|
|
308
|
+
|
|
309
|
+
// Run as CLI (invoked via dispatcher or directly)
|
|
310
|
+
const calledAsMcp = process.argv.includes("mcp");
|
|
311
|
+
if (require.main === module || calledAsMcp) {
|
|
312
|
+
run();
|
|
313
|
+
}
|
package/lib/new-doc.js
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* openclew new <title> — create a new
|
|
2
|
+
* openclew new <title> — create a new refdoc.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
|
-
const {
|
|
7
|
+
const { refdocContent, slugify } = require("./templates");
|
|
8
8
|
const { readConfig } = require("./config");
|
|
9
9
|
|
|
10
10
|
const args = process.argv.slice(2);
|
|
11
|
-
//
|
|
12
|
-
const
|
|
13
|
-
const
|
|
11
|
+
// Support both "add ref <title>" and legacy "new <title>"
|
|
12
|
+
const refIndex = args.indexOf("ref");
|
|
13
|
+
const newIndex = args.indexOf("new");
|
|
14
|
+
let titleArgs;
|
|
15
|
+
if (refIndex >= 0) {
|
|
16
|
+
titleArgs = args.slice(refIndex + 1);
|
|
17
|
+
} else if (newIndex >= 0) {
|
|
18
|
+
titleArgs = args.slice(newIndex + 1);
|
|
19
|
+
} else {
|
|
20
|
+
titleArgs = args.slice(1);
|
|
21
|
+
}
|
|
14
22
|
const title = titleArgs.join(" ");
|
|
15
23
|
|
|
16
24
|
if (!title) {
|
|
17
|
-
console.error('Usage: openclew
|
|
25
|
+
console.error('Usage: openclew add ref "Title of the document"');
|
|
18
26
|
process.exit(1);
|
|
19
27
|
}
|
|
20
28
|
|
|
@@ -37,7 +45,7 @@ if (fs.existsSync(filepath)) {
|
|
|
37
45
|
process.exit(1);
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
fs.writeFileSync(filepath,
|
|
48
|
+
fs.writeFileSync(filepath, refdocContent(title), "utf-8");
|
|
41
49
|
console.log(`Created doc/${filename}`);
|
|
42
50
|
console.log("");
|
|
43
51
|
console.log("Next: open the file and fill in:");
|
package/lib/new-log.js
CHANGED
|
@@ -8,13 +8,13 @@ const { logContent, slugifyLog, today } = require("./templates");
|
|
|
8
8
|
const { readConfig } = require("./config");
|
|
9
9
|
|
|
10
10
|
const args = process.argv.slice(2);
|
|
11
|
-
//
|
|
12
|
-
const
|
|
13
|
-
const titleArgs =
|
|
11
|
+
// Support both "add log <title>" and legacy "log <title>"
|
|
12
|
+
const logIndex = args.lastIndexOf("log");
|
|
13
|
+
const titleArgs = logIndex >= 0 ? args.slice(logIndex + 1) : args.slice(1);
|
|
14
14
|
const title = titleArgs.join(" ");
|
|
15
15
|
|
|
16
16
|
if (!title) {
|
|
17
|
-
console.error('Usage: openclew log "Title of the log"');
|
|
17
|
+
console.error('Usage: openclew add log "Title of the log"');
|
|
18
18
|
process.exit(1);
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -42,7 +42,7 @@ fs.writeFileSync(filepath, logContent(title), "utf-8");
|
|
|
42
42
|
console.log(`Created doc/log/${filename}`);
|
|
43
43
|
console.log("");
|
|
44
44
|
console.log("Next: open the file and fill in:");
|
|
45
|
-
console.log(" L1 —
|
|
45
|
+
console.log(" L1 — subject + doc_brief (what happened in 1-2 sentences)");
|
|
46
46
|
console.log(" L2 — problem + solution (the facts, frozen after this session)");
|
|
47
47
|
console.log("");
|
|
48
48
|
console.log("Logs are immutable — once written, never modified.");
|