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.
@@ -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, path } objects.
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 };