openclew 0.1.0 → 0.2.1
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 +24 -122
- package/bin/openclew.js +14 -2
- package/hooks/generate-index.py +95 -26
- package/lib/checkout.js +296 -0
- package/lib/config.js +34 -0
- package/lib/detect.js +38 -3
- package/lib/init.js +157 -74
- package/lib/inject.js +9 -5
- package/lib/new-doc.js +14 -4
- package/lib/new-log.js +12 -1
- package/lib/templates.js +280 -22
- package/package.json +1 -1
- package/templates/log.md +5 -8
- package/templates/{permanent.md → refdoc.md} +5 -9
package/lib/checkout.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
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 { slugifyLog, today } = require("./templates");
|
|
14
|
+
const { readConfig } = require("./config");
|
|
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
|
+
|
|
25
|
+
const PROJECT_ROOT = process.cwd();
|
|
26
|
+
const DOC_DIR = path.join(PROJECT_ROOT, "doc");
|
|
27
|
+
const LOG_DIR = path.join(DOC_DIR, "log");
|
|
28
|
+
|
|
29
|
+
function run(cmd) {
|
|
30
|
+
try {
|
|
31
|
+
return execSync(cmd, { cwd: PROJECT_ROOT, encoding: "utf-8" }).trim();
|
|
32
|
+
} catch {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function collectGitActivity() {
|
|
38
|
+
const date = today();
|
|
39
|
+
|
|
40
|
+
// Today's commits
|
|
41
|
+
const commitLog = run(
|
|
42
|
+
`git log --since="${date} 00:00" --format="%h %s" --no-merges`
|
|
43
|
+
);
|
|
44
|
+
const commits = commitLog
|
|
45
|
+
? commitLog.split("\n").filter((l) => l.trim())
|
|
46
|
+
: [];
|
|
47
|
+
|
|
48
|
+
// Uncommitted changes
|
|
49
|
+
const status = run("git status --porcelain");
|
|
50
|
+
const uncommitted = status ? status.split("\n").filter((l) => l.trim()) : [];
|
|
51
|
+
|
|
52
|
+
// Files changed today (committed)
|
|
53
|
+
const changedFiles = run(
|
|
54
|
+
`git diff --name-only HEAD~${Math.max(commits.length, 1)}..HEAD 2>/dev/null`
|
|
55
|
+
);
|
|
56
|
+
const files = changedFiles
|
|
57
|
+
? changedFiles.split("\n").filter((l) => l.trim())
|
|
58
|
+
: [];
|
|
59
|
+
|
|
60
|
+
// Today's logs already created
|
|
61
|
+
const existingLogs = fs.existsSync(LOG_DIR)
|
|
62
|
+
? fs.readdirSync(LOG_DIR).filter((f) => f.startsWith(date))
|
|
63
|
+
: [];
|
|
64
|
+
|
|
65
|
+
// Refdocs
|
|
66
|
+
const refdocs = fs.existsSync(DOC_DIR)
|
|
67
|
+
? fs.readdirSync(DOC_DIR).filter((f) => f.startsWith("_") && f !== "_INDEX.md" && f.endsWith(".md"))
|
|
68
|
+
: [];
|
|
69
|
+
|
|
70
|
+
return { date, commits, uncommitted, files, existingLogs, refdocs };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractActions(commits) {
|
|
74
|
+
// Group commits by type (feat, fix, refactor, docs, etc.)
|
|
75
|
+
return commits.map((c) => {
|
|
76
|
+
const match = c.match(/^([a-f0-9]+)\s+(\w+)(?:\(([^)]*)\))?:\s*(.+)$/);
|
|
77
|
+
if (match) {
|
|
78
|
+
return {
|
|
79
|
+
hash: match[1],
|
|
80
|
+
type: match[2],
|
|
81
|
+
scope: match[3] || "",
|
|
82
|
+
desc: match[4],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Non-conventional commit
|
|
86
|
+
const parts = c.match(/^([a-f0-9]+)\s+(.+)$/);
|
|
87
|
+
return {
|
|
88
|
+
hash: parts ? parts[1] : "",
|
|
89
|
+
type: "other",
|
|
90
|
+
scope: "",
|
|
91
|
+
desc: parts ? parts[2] : c,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function typeLabel(type) {
|
|
97
|
+
const labels = {
|
|
98
|
+
feat: "Feature",
|
|
99
|
+
fix: "Fix",
|
|
100
|
+
refactor: "Refactor",
|
|
101
|
+
docs: "Doc",
|
|
102
|
+
test: "Test",
|
|
103
|
+
build: "Build",
|
|
104
|
+
chore: "Chore",
|
|
105
|
+
};
|
|
106
|
+
return labels[type] || type.charAt(0).toUpperCase() + type.slice(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function displaySummary(activity) {
|
|
110
|
+
const { date, commits, uncommitted, existingLogs, refdocs } = activity;
|
|
111
|
+
const actions = extractActions(commits);
|
|
112
|
+
|
|
113
|
+
console.log(`\nopenclew checkout — ${date}\n`);
|
|
114
|
+
|
|
115
|
+
if (actions.length === 0 && uncommitted.length === 0) {
|
|
116
|
+
console.log(" Nothing to report — no commits or changes today.");
|
|
117
|
+
console.log("");
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Summary table
|
|
122
|
+
if (actions.length > 0) {
|
|
123
|
+
console.log(" Commits today:");
|
|
124
|
+
console.log(
|
|
125
|
+
" ┌─────┬──────────────────────────────────────────────┬─────┐"
|
|
126
|
+
);
|
|
127
|
+
console.log(
|
|
128
|
+
" │ Sta │ Action │ Com │"
|
|
129
|
+
);
|
|
130
|
+
console.log(
|
|
131
|
+
" ├─────┼──────────────────────────────────────────────┼─────┤"
|
|
132
|
+
);
|
|
133
|
+
for (const a of actions) {
|
|
134
|
+
const label = `${typeLabel(a.type)} : ${a.desc}`;
|
|
135
|
+
const truncated = label.length > 44 ? label.slice(0, 43) + "…" : label;
|
|
136
|
+
const padded = truncated.padEnd(44);
|
|
137
|
+
console.log(` │ ✅ │ ${padded} │ 🟢 │`);
|
|
138
|
+
}
|
|
139
|
+
console.log(
|
|
140
|
+
" └─────┴──────────────────────────────────────────────┴─────┘"
|
|
141
|
+
);
|
|
142
|
+
console.log("");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (uncommitted.length > 0) {
|
|
146
|
+
console.log(` Uncommitted changes: ${uncommitted.length} file(s)`);
|
|
147
|
+
for (const line of uncommitted.slice(0, 10)) {
|
|
148
|
+
console.log(` ${line}`);
|
|
149
|
+
}
|
|
150
|
+
if (uncommitted.length > 10) {
|
|
151
|
+
console.log(` ... and ${uncommitted.length - 10} more`);
|
|
152
|
+
}
|
|
153
|
+
console.log("");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Documentation status
|
|
157
|
+
if (existingLogs.length > 0) {
|
|
158
|
+
console.log(` 📗 Today's logs: ${existingLogs.join(", ")}`);
|
|
159
|
+
} else {
|
|
160
|
+
console.log(" 📕 No log created today");
|
|
161
|
+
}
|
|
162
|
+
console.log("");
|
|
163
|
+
|
|
164
|
+
// Refdocs reminder
|
|
165
|
+
if (refdocs.length > 0) {
|
|
166
|
+
console.log(" 📚 Refdocs — check if any need updating:");
|
|
167
|
+
for (const doc of refdocs) {
|
|
168
|
+
console.log(` ${doc}`);
|
|
169
|
+
}
|
|
170
|
+
console.log("");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return actions;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function generateSessionLog(activity, actions) {
|
|
177
|
+
const { date } = activity;
|
|
178
|
+
|
|
179
|
+
// Build a descriptive title from actions
|
|
180
|
+
let sessionTitle;
|
|
181
|
+
if (actions.length === 1) {
|
|
182
|
+
sessionTitle = actions[0].desc;
|
|
183
|
+
} else if (actions.length > 1) {
|
|
184
|
+
const types = [...new Set(actions.map((a) => typeLabel(a.type)))];
|
|
185
|
+
sessionTitle = types.join(" + ") + " session";
|
|
186
|
+
} else {
|
|
187
|
+
sessionTitle = "Work session";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Build pre-filled log content
|
|
191
|
+
const keywords = [
|
|
192
|
+
...new Set(actions.map((a) => a.scope).filter(Boolean)),
|
|
193
|
+
];
|
|
194
|
+
const keywordsStr =
|
|
195
|
+
keywords.length > 0 ? `[${keywords.join(", ")}]` : "[]";
|
|
196
|
+
|
|
197
|
+
const commitList = actions
|
|
198
|
+
.map((a) => `- ${typeLabel(a.type)}: ${a.desc} (${a.hash})`)
|
|
199
|
+
.join("\n");
|
|
200
|
+
|
|
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(". ")}.
|
|
209
|
+
<!-- L1_END -->
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
<!-- L2_START -->
|
|
214
|
+
# L2 - Summary
|
|
215
|
+
|
|
216
|
+
## Objective
|
|
217
|
+
<!-- Why this work was undertaken -->
|
|
218
|
+
|
|
219
|
+
## What was done
|
|
220
|
+
${commitList}
|
|
221
|
+
|
|
222
|
+
## Result
|
|
223
|
+
<!-- Outcome — what works now that didn't before -->
|
|
224
|
+
<!-- L2_END -->
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
<!-- L3_START -->
|
|
229
|
+
# L3 - Details
|
|
230
|
+
|
|
231
|
+
<!-- Technical details, code changes, debugging steps... -->
|
|
232
|
+
<!-- L3_END -->
|
|
233
|
+
`;
|
|
234
|
+
|
|
235
|
+
const slug = slugifyLog(sessionTitle);
|
|
236
|
+
const filename = `${date}_${slug}.md`;
|
|
237
|
+
const filepath = path.join(LOG_DIR, filename);
|
|
238
|
+
|
|
239
|
+
if (fs.existsSync(filepath)) {
|
|
240
|
+
console.log(` Log already exists: doc/log/${filename}`);
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
245
|
+
console.log(" No doc/log/ directory. Run 'openclew init' first.");
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fs.writeFileSync(filepath, content, "utf-8");
|
|
250
|
+
console.log(` 📝 Created doc/log/${filename}`);
|
|
251
|
+
console.log(" Pre-filled with today's commits. Edit to add context.");
|
|
252
|
+
return filename;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function regenerateIndex() {
|
|
256
|
+
const indexScript = path.join(DOC_DIR, "generate-index.py");
|
|
257
|
+
if (!fs.existsSync(indexScript)) return;
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
execSync(`python3 "${indexScript}" "${DOC_DIR}"`, { stdio: "pipe" });
|
|
261
|
+
console.log(" 📋 Regenerated doc/_INDEX.md");
|
|
262
|
+
} catch {
|
|
263
|
+
// Silent — index will be regenerated on next commit anyway
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function main() {
|
|
268
|
+
if (!fs.existsSync(DOC_DIR)) {
|
|
269
|
+
console.error("No doc/ directory found. Run 'openclew init' first.");
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!readConfig(PROJECT_ROOT)) {
|
|
274
|
+
console.warn("Warning: no .openclew.json found. Run 'openclew init' first.\n");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const activity = collectGitActivity();
|
|
278
|
+
const actions = displaySummary(activity);
|
|
279
|
+
|
|
280
|
+
if (!actions || actions.length === 0) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Create session log
|
|
285
|
+
console.log("─── Log ───");
|
|
286
|
+
const created = generateSessionLog(activity, actions);
|
|
287
|
+
|
|
288
|
+
// Regenerate index
|
|
289
|
+
if (created) {
|
|
290
|
+
regenerateIndex();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.log("");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
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 };
|