openwolf 1.0.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.
Files changed (112) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +232 -0
  3. package/dist/bin/openwolf.js +10 -0
  4. package/dist/bin/openwolf.js.map +1 -0
  5. package/dist/dashboard/assets/AISuggestions-DzE-DQkR.js +1 -0
  6. package/dist/dashboard/assets/ActivityTimeline-DGVjujnt.js +1 -0
  7. package/dist/dashboard/assets/AnatomyBrowser-S-2rmYtw.js +1 -0
  8. package/dist/dashboard/assets/BugLog-CG2zDHJc.js +1 -0
  9. package/dist/dashboard/assets/CerebrumViewer-Dlgoy69U.js +1 -0
  10. package/dist/dashboard/assets/CronStatus-DxUF1iW_.js +1 -0
  11. package/dist/dashboard/assets/DesignQC-BGXn_aq8.js +1 -0
  12. package/dist/dashboard/assets/MemoryViewer-CGqkTyvQ.js +1 -0
  13. package/dist/dashboard/assets/ProjectOverview-DlFhu69i.js +1 -0
  14. package/dist/dashboard/assets/TokenUsage-DDsQiVIq.js +68 -0
  15. package/dist/dashboard/assets/index-CzK9GUjV.css +1 -0
  16. package/dist/dashboard/assets/index-PYeNGjkN.js +52 -0
  17. package/dist/dashboard/index.html +16 -0
  18. package/dist/hooks/post-read.js +68 -0
  19. package/dist/hooks/post-write.js +502 -0
  20. package/dist/hooks/pre-read.js +79 -0
  21. package/dist/hooks/pre-write.js +120 -0
  22. package/dist/hooks/session-start.js +76 -0
  23. package/dist/hooks/shared.js +613 -0
  24. package/dist/hooks/stop.js +146 -0
  25. package/dist/src/buglog/bug-matcher.js +3 -0
  26. package/dist/src/buglog/bug-matcher.js.map +1 -0
  27. package/dist/src/buglog/bug-tracker.js +81 -0
  28. package/dist/src/buglog/bug-tracker.js.map +1 -0
  29. package/dist/src/cli/bug-cmd.js +28 -0
  30. package/dist/src/cli/bug-cmd.js.map +1 -0
  31. package/dist/src/cli/cron-cmd.js +106 -0
  32. package/dist/src/cli/cron-cmd.js.map +1 -0
  33. package/dist/src/cli/daemon-cmd.js +177 -0
  34. package/dist/src/cli/daemon-cmd.js.map +1 -0
  35. package/dist/src/cli/dashboard.js +84 -0
  36. package/dist/src/cli/dashboard.js.map +1 -0
  37. package/dist/src/cli/designqc-cmd.js +31 -0
  38. package/dist/src/cli/designqc-cmd.js.map +1 -0
  39. package/dist/src/cli/index.js +149 -0
  40. package/dist/src/cli/index.js.map +1 -0
  41. package/dist/src/cli/init.js +506 -0
  42. package/dist/src/cli/init.js.map +1 -0
  43. package/dist/src/cli/registry.js +93 -0
  44. package/dist/src/cli/registry.js.map +1 -0
  45. package/dist/src/cli/scan.js +39 -0
  46. package/dist/src/cli/scan.js.map +1 -0
  47. package/dist/src/cli/status.js +85 -0
  48. package/dist/src/cli/status.js.map +1 -0
  49. package/dist/src/cli/update.js +414 -0
  50. package/dist/src/cli/update.js.map +1 -0
  51. package/dist/src/daemon/cron-engine.js +300 -0
  52. package/dist/src/daemon/cron-engine.js.map +1 -0
  53. package/dist/src/daemon/file-watcher.js +53 -0
  54. package/dist/src/daemon/file-watcher.js.map +1 -0
  55. package/dist/src/daemon/health.js +23 -0
  56. package/dist/src/daemon/health.js.map +1 -0
  57. package/dist/src/daemon/wolf-daemon.js +294 -0
  58. package/dist/src/daemon/wolf-daemon.js.map +1 -0
  59. package/dist/src/designqc/designqc-capture.js +235 -0
  60. package/dist/src/designqc/designqc-capture.js.map +1 -0
  61. package/dist/src/designqc/designqc-engine.js +141 -0
  62. package/dist/src/designqc/designqc-engine.js.map +1 -0
  63. package/dist/src/designqc/designqc-types.js +5 -0
  64. package/dist/src/designqc/designqc-types.js.map +1 -0
  65. package/dist/src/hooks/post-read.js +69 -0
  66. package/dist/src/hooks/post-read.js.map +1 -0
  67. package/dist/src/hooks/post-write.js +503 -0
  68. package/dist/src/hooks/post-write.js.map +1 -0
  69. package/dist/src/hooks/pre-read.js +80 -0
  70. package/dist/src/hooks/pre-read.js.map +1 -0
  71. package/dist/src/hooks/pre-write.js +121 -0
  72. package/dist/src/hooks/pre-write.js.map +1 -0
  73. package/dist/src/hooks/session-start.js +77 -0
  74. package/dist/src/hooks/session-start.js.map +1 -0
  75. package/dist/src/hooks/shared.js +614 -0
  76. package/dist/src/hooks/shared.js.map +1 -0
  77. package/dist/src/hooks/stop.js +147 -0
  78. package/dist/src/hooks/stop.js.map +1 -0
  79. package/dist/src/scanner/anatomy-scanner.js +260 -0
  80. package/dist/src/scanner/anatomy-scanner.js.map +1 -0
  81. package/dist/src/scanner/description-extractor.js +1007 -0
  82. package/dist/src/scanner/description-extractor.js.map +1 -0
  83. package/dist/src/scanner/project-root.js +42 -0
  84. package/dist/src/scanner/project-root.js.map +1 -0
  85. package/dist/src/tracker/token-estimator.js +20 -0
  86. package/dist/src/tracker/token-estimator.js.map +1 -0
  87. package/dist/src/tracker/token-ledger.js +45 -0
  88. package/dist/src/tracker/token-ledger.js.map +1 -0
  89. package/dist/src/tracker/waste-detector.js +101 -0
  90. package/dist/src/tracker/waste-detector.js.map +1 -0
  91. package/dist/src/utils/fs-safe.js +74 -0
  92. package/dist/src/utils/fs-safe.js.map +1 -0
  93. package/dist/src/utils/logger.js +48 -0
  94. package/dist/src/utils/logger.js.map +1 -0
  95. package/dist/src/utils/paths.js +23 -0
  96. package/dist/src/utils/paths.js.map +1 -0
  97. package/dist/src/utils/platform.js +14 -0
  98. package/dist/src/utils/platform.js.map +1 -0
  99. package/package.json +77 -0
  100. package/src/templates/OPENWOLF.md +135 -0
  101. package/src/templates/anatomy.md +5 -0
  102. package/src/templates/buglog.json +4 -0
  103. package/src/templates/cerebrum.md +22 -0
  104. package/src/templates/claude-md-snippet.md +5 -0
  105. package/src/templates/claude-rules-openwolf.md +15 -0
  106. package/src/templates/config.json +73 -0
  107. package/src/templates/cron-manifest.json +97 -0
  108. package/src/templates/cron-state.json +7 -0
  109. package/src/templates/identity.md +9 -0
  110. package/src/templates/memory.md +4 -0
  111. package/src/templates/reframe-frameworks.md +597 -0
  112. package/src/templates/token-ledger.json +21 -0
@@ -0,0 +1,614 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as crypto from "node:crypto";
4
+ export function getWolfDir() {
5
+ // Prefer CLAUDE_PROJECT_DIR so hooks work even if CWD changes during a session
6
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
7
+ return path.join(projectDir, ".wolf");
8
+ }
9
+ /**
10
+ * Bail out silently if .wolf/ directory doesn't exist in the current project.
11
+ * Call this at the top of every hook to avoid crashes in non-OpenWolf projects.
12
+ */
13
+ export function ensureWolfDir() {
14
+ const wolfDir = getWolfDir();
15
+ if (!fs.existsSync(wolfDir)) {
16
+ process.exit(0);
17
+ }
18
+ }
19
+ export function readJSON(filePath, fallback) {
20
+ try {
21
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
22
+ }
23
+ catch {
24
+ return fallback;
25
+ }
26
+ }
27
+ export function writeJSON(filePath, data) {
28
+ const dir = path.dirname(filePath);
29
+ if (!fs.existsSync(dir))
30
+ fs.mkdirSync(dir, { recursive: true });
31
+ const tmp = filePath + "." + crypto.randomBytes(4).toString("hex") + ".tmp";
32
+ try {
33
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
34
+ fs.renameSync(tmp, filePath);
35
+ }
36
+ catch {
37
+ // On Windows, rename can fail if another process holds a handle.
38
+ // Fall back to direct write and clean up the tmp file.
39
+ try {
40
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
41
+ }
42
+ catch { }
43
+ try {
44
+ fs.unlinkSync(tmp);
45
+ }
46
+ catch { }
47
+ }
48
+ }
49
+ export function readMarkdown(filePath) {
50
+ try {
51
+ return fs.readFileSync(filePath, "utf-8");
52
+ }
53
+ catch {
54
+ return "";
55
+ }
56
+ }
57
+ export function appendMarkdown(filePath, line) {
58
+ const dir = path.dirname(filePath);
59
+ if (!fs.existsSync(dir))
60
+ fs.mkdirSync(dir, { recursive: true });
61
+ fs.appendFileSync(filePath, line, "utf-8");
62
+ }
63
+ export function parseAnatomy(content) {
64
+ const sections = new Map();
65
+ let currentSection = "";
66
+ for (const line of content.split("\n")) {
67
+ const sm = line.match(/^## (.+)/);
68
+ if (sm) {
69
+ currentSection = sm[1].trim();
70
+ if (!sections.has(currentSection))
71
+ sections.set(currentSection, []);
72
+ continue;
73
+ }
74
+ if (!currentSection)
75
+ continue;
76
+ const em = line.match(/^- `([^`]+)`(?:\s+—\s+(.+?))?\s*\(~(\d+)\s+tok\)$/);
77
+ if (em) {
78
+ sections.get(currentSection).push({
79
+ file: em[1],
80
+ description: em[2] || "",
81
+ tokens: parseInt(em[3], 10),
82
+ });
83
+ }
84
+ }
85
+ return sections;
86
+ }
87
+ export function serializeAnatomy(sections, metadata) {
88
+ const lines = [
89
+ "# anatomy.md",
90
+ "",
91
+ `> Auto-maintained by OpenWolf. Last scanned: ${metadata.lastScanned}`,
92
+ `> Files: ${metadata.fileCount} tracked | Anatomy hits: ${metadata.hits} | Misses: ${metadata.misses}`,
93
+ "",
94
+ ];
95
+ const keys = [...sections.keys()].sort();
96
+ for (const key of keys) {
97
+ lines.push(`## ${key}`);
98
+ lines.push("");
99
+ const entries = sections.get(key).sort((a, b) => a.file.localeCompare(b.file));
100
+ for (const e of entries) {
101
+ const desc = e.description ? ` — ${e.description}` : "";
102
+ lines.push(`- \`${e.file}\`${desc} (~${e.tokens} tok)`);
103
+ }
104
+ lines.push("");
105
+ }
106
+ return lines.join("\n");
107
+ }
108
+ export function extractDescription(filePath) {
109
+ const MAX_DESC = 150;
110
+ const basename = path.basename(filePath);
111
+ const ext = path.extname(basename).toLowerCase();
112
+ const known = {
113
+ "package.json": "Node.js package manifest",
114
+ "tsconfig.json": "TypeScript configuration",
115
+ ".gitignore": "Git ignore rules",
116
+ "README.md": "Project documentation",
117
+ "composer.json": "PHP package manifest",
118
+ "requirements.txt": "Python dependencies",
119
+ "schema.sql": "Database schema",
120
+ "Dockerfile": "Docker container definition",
121
+ "docker-compose.yml": "Docker Compose services",
122
+ "Cargo.toml": "Rust package manifest",
123
+ "go.mod": "Go module definition",
124
+ "Gemfile": "Ruby dependencies",
125
+ "pubspec.yaml": "Dart/Flutter package manifest",
126
+ };
127
+ if (known[basename])
128
+ return known[basename];
129
+ let content;
130
+ try {
131
+ const fd = fs.openSync(filePath, "r");
132
+ const buf = Buffer.alloc(12288); // 12KB
133
+ const n = fs.readSync(fd, buf, 0, 12288, 0);
134
+ fs.closeSync(fd);
135
+ content = buf.subarray(0, n).toString("utf-8");
136
+ }
137
+ catch {
138
+ return "";
139
+ }
140
+ if (!content.trim())
141
+ return "";
142
+ const cap = (s) => s.length <= MAX_DESC ? s : s.slice(0, MAX_DESC - 3) + "...";
143
+ // Markdown heading
144
+ if (ext === ".md" || ext === ".mdx") {
145
+ const m = content.match(/^#{1,2}\s+(.+)$/m);
146
+ if (m)
147
+ return cap(m[1].trim());
148
+ }
149
+ // HTML title
150
+ if (ext === ".html" || ext === ".htm") {
151
+ const m = content.match(/<title[^>]*>([^<]+)<\/title>/i);
152
+ if (m)
153
+ return cap(m[1].trim());
154
+ }
155
+ // JSDoc / PHPDoc / Javadoc — first meaningful line
156
+ const jm = content.match(/\/\*\*\s*\n?\s*\*?\s*(.+)/);
157
+ if (jm) {
158
+ const l = jm[1].replace(/\*\/$/, "").trim();
159
+ if (l && !l.startsWith("@") && l.length > 5)
160
+ return cap(l);
161
+ }
162
+ // Python docstring
163
+ if (ext === ".py") {
164
+ const dm = content.match(/^(?:#[^\n]*\n)*\s*(?:"""(.+?)"""|'''(.+?)''')/s);
165
+ if (dm) {
166
+ const first = (dm[1] || dm[2]).split("\n")[0].trim();
167
+ if (first && first.length > 3)
168
+ return cap(first);
169
+ }
170
+ }
171
+ // Rust doc comments
172
+ if (ext === ".rs") {
173
+ const lines = content.split("\n");
174
+ for (const line of lines.slice(0, 20)) {
175
+ const m = line.match(/^\s*(?:\/\/\/|\/\/!)\s*(.+)/);
176
+ if (m && m[1].length > 5)
177
+ return cap(m[1].trim());
178
+ }
179
+ }
180
+ // Go package comment
181
+ if (ext === ".go") {
182
+ const m = content.match(/\/\/\s*Package\s+\w+\s+(.*)/);
183
+ if (m)
184
+ return cap(m[1].trim());
185
+ }
186
+ // C# XML doc
187
+ if (ext === ".cs") {
188
+ const m = content.match(/<summary>\s*([\s\S]*?)\s*<\/summary>/);
189
+ if (m) {
190
+ const text = m[1].replace(/\/\/\/\s*/g, "").replace(/\s+/g, " ").trim();
191
+ if (text.length > 5)
192
+ return cap(text);
193
+ }
194
+ }
195
+ // Elixir @moduledoc
196
+ if (ext === ".ex" || ext === ".exs") {
197
+ const m = content.match(/@moduledoc\s+"""\s*\n\s*(.*)/);
198
+ if (m)
199
+ return cap(m[1].trim());
200
+ }
201
+ // Header comment (skip generic ones)
202
+ const hdrLines = content.split("\n");
203
+ for (const line of hdrLines.slice(0, 15)) {
204
+ const t = line.trim();
205
+ if (!t || t === "<?php" || t.startsWith("#!") || t.startsWith("namespace") || t.startsWith("use ") || t.startsWith("import ") || t.startsWith("from ") || t.startsWith("require") || t.startsWith("module "))
206
+ continue;
207
+ const cm = t.match(/^(?:\/\/|#|--)\s*(.+)/);
208
+ if (cm) {
209
+ const text = cm[1].trim();
210
+ const lower = text.toLowerCase();
211
+ if (text.length > 5 && !lower.startsWith("copyright") && !lower.startsWith("license") && !lower.startsWith("@") && !lower.startsWith("strict") && !lower.startsWith("generated") && !lower.startsWith("eslint-") && !lower.startsWith("nolint")) {
212
+ return cap(text);
213
+ }
214
+ }
215
+ if (!t.startsWith("//") && !t.startsWith("#") && !t.startsWith("/*") && !t.startsWith("*") && !t.startsWith("--"))
216
+ break;
217
+ }
218
+ // ─── PHP / Laravel ───────────────────────────────────────
219
+ if (ext === ".php") {
220
+ if (basename.endsWith(".blade.php")) {
221
+ const ext2 = content.match(/@extends\(\s*['"]([^'"]+)['"]\s*\)/);
222
+ const sections = (content.match(/@section\(\s*['"](\w+)['"]/g) || []).map(s => s.match(/['"](\w+)['"]/)?.[1]).filter(Boolean);
223
+ const parts = [];
224
+ if (ext2)
225
+ parts.push(`extends ${ext2[1]}`);
226
+ if (sections.length)
227
+ parts.push(`sections: ${sections.join(", ")}`);
228
+ return cap(parts.length ? `Blade: ${parts.join(", ")}` : "Blade template");
229
+ }
230
+ const classM = content.match(/class\s+(\w+)(?:\s+extends\s+(\w+))?/);
231
+ const className = classM?.[1] || "";
232
+ const parent = classM?.[2] || "";
233
+ const pubMethods = (content.match(/public\s+function\s+(\w+)/g) || [])
234
+ .map(m => m.match(/public\s+function\s+(\w+)/)?.[1])
235
+ .filter(n => n && n !== "__construct" && n !== "middleware");
236
+ if (basename.endsWith("Controller.php") || parent === "Controller") {
237
+ if (pubMethods.length > 0) {
238
+ const display = pubMethods.slice(0, 5).join(", ");
239
+ return cap(pubMethods.length > 5 ? `${display} + ${pubMethods.length - 5} more` : display);
240
+ }
241
+ }
242
+ if (parent === "Model" || parent === "Authenticatable") {
243
+ const parts = [];
244
+ const tbl = content.match(/\$table\s*=\s*['"]([^'"]+)['"]/);
245
+ if (tbl)
246
+ parts.push(`table: ${tbl[1]}`);
247
+ const fill = content.match(/\$fillable\s*=\s*\[([^\]]*)\]/s);
248
+ if (fill) {
249
+ const c = (fill[1].match(/['"]/g) || []).length / 2;
250
+ parts.push(`${Math.floor(c)} fields`);
251
+ }
252
+ const rels = (content.match(/\$this->(hasMany|hasOne|belongsTo|belongsToMany|morphMany|morphTo)\(/g) || []).length;
253
+ if (rels)
254
+ parts.push(`${rels} rels`);
255
+ return cap(parts.length ? `Model — ${parts.join(", ")}` : `Model: ${className}`);
256
+ }
257
+ if (basename.match(/^\d{4}_\d{2}_\d{2}/)) {
258
+ const create = content.match(/Schema::create\(\s*['"]([^'"]+)['"]/);
259
+ if (create)
260
+ return `Migration: create ${create[1]} table`;
261
+ const alter = content.match(/Schema::table\(\s*['"]([^'"]+)['"]/);
262
+ if (alter)
263
+ return `Migration: alter ${alter[1]} table`;
264
+ return "Database migration";
265
+ }
266
+ if (className && pubMethods.length > 0) {
267
+ const display = pubMethods.slice(0, 4).join(", ");
268
+ return cap(pubMethods.length > 4 ? `${className}: ${display} + ${pubMethods.length - 4} more` : `${className}: ${display}`);
269
+ }
270
+ }
271
+ // ─── TS/JS/React/Next.js ─────────────────────────────────
272
+ if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") {
273
+ // React component
274
+ if (ext === ".tsx" || ext === ".jsx") {
275
+ const comp = content.match(/(?:export\s+(?:default\s+)?)?(?:function|const)\s+(\w+)/);
276
+ const parts = [];
277
+ if (comp)
278
+ parts.push(comp[1]);
279
+ const renders = [];
280
+ if (/<(?:form|Form)/i.test(content))
281
+ renders.push("form");
282
+ if (/<(?:table|Table|DataTable)/i.test(content))
283
+ renders.push("table");
284
+ if (/<(?:dialog|Dialog|Modal|Drawer)/i.test(content))
285
+ renders.push("modal");
286
+ if (renders.length)
287
+ parts.push(`renders ${renders.join(", ")}`);
288
+ if (parts.length)
289
+ return cap(parts.join(" — "));
290
+ }
291
+ // Next.js conventions
292
+ if (basename === "page.tsx" || basename === "page.js")
293
+ return "Next.js page component";
294
+ if (basename === "layout.tsx" || basename === "layout.js")
295
+ return "Next.js layout";
296
+ if (basename === "route.ts" || basename === "route.js") {
297
+ const methods = [...new Set((content.match(/export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)/g) || [])
298
+ .map(m => m.match(/(GET|POST|PUT|PATCH|DELETE)/)?.[1]))].filter(Boolean);
299
+ return methods.length ? `Next.js API route: ${methods.join(", ")}` : "Next.js API route";
300
+ }
301
+ // Express/Fastify routes
302
+ const routeHits = content.match(/\.(get|post|put|patch|delete)\s*\(\s*['"`]/g);
303
+ if (routeHits && routeHits.length > 0) {
304
+ const methods = [...new Set(routeHits.map(r => r.match(/\.(get|post|put|patch|delete)/)?.[1]?.toUpperCase()))];
305
+ return cap(`API routes: ${methods.join(", ")} (${routeHits.length} endpoints)`);
306
+ }
307
+ // tRPC router
308
+ if (content.includes("createTRPCRouter") || content.includes("publicProcedure")) {
309
+ const procs = (content.match(/\.(query|mutation|subscription)\s*\(/g) || []).length;
310
+ return procs ? `tRPC router: ${procs} procedures` : "tRPC router";
311
+ }
312
+ // Zod schemas
313
+ if (content.includes("z.object") || content.includes("z.string")) {
314
+ const schemas = (content.match(/(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*z\./g) || [])
315
+ .map(s => s.match(/(?:const|let)\s+(\w+)/)?.[1]).filter(Boolean);
316
+ if (schemas.length)
317
+ return cap(`Zod schemas: ${schemas.slice(0, 4).join(", ")}${schemas.length > 4 ? ` + ${schemas.length - 4} more` : ""}`);
318
+ }
319
+ // Exports summary
320
+ const exports = (content.match(/export\s+(?:async\s+)?(?:function|class|const|interface|type|enum)\s+(\w+)/g) || [])
321
+ .map(e => e.match(/(\w+)$/)?.[1]).filter(Boolean);
322
+ if (exports.length > 0 && exports.length <= 5)
323
+ return `Exports ${exports.join(", ")}`;
324
+ if (exports.length > 5)
325
+ return cap(`Exports ${exports.slice(0, 4).join(", ")} + ${exports.length - 4} more`);
326
+ }
327
+ // ─── Python / Django / FastAPI / Flask ────────────────────
328
+ if (ext === ".py") {
329
+ // Django model
330
+ if (content.includes("models.Model")) {
331
+ const cls = content.match(/class\s+(\w+)\(.*models\.Model\)/);
332
+ const fields = (content.match(/^\s+\w+\s*=\s*models\.\w+/gm) || []).length;
333
+ return cap(`Model: ${cls?.[1] || "unknown"}, ${fields} fields`);
334
+ }
335
+ // FastAPI/Flask routes
336
+ if (content.includes("@router.") || content.includes("@app.")) {
337
+ const routes = (content.match(/@(?:router|app)\.(get|post|put|patch|delete)\s*\(/g) || []);
338
+ return cap(routes.length ? `API: ${routes.length} endpoints` : "API router");
339
+ }
340
+ // Pydantic
341
+ if (content.includes("BaseModel") && content.includes("Field(")) {
342
+ const cls = content.match(/class\s+(\w+)\(.*BaseModel\)/);
343
+ return cls ? `Pydantic: ${cls[1]}` : "Pydantic model";
344
+ }
345
+ // Celery
346
+ if (content.includes("@shared_task") || content.includes("@app.task")) {
347
+ const tasks = (content.match(/def\s+(\w+)/g) || []).map(m => m.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_"));
348
+ return cap(tasks.length ? `Celery tasks: ${tasks.join(", ")}` : "Celery task");
349
+ }
350
+ // Generic
351
+ const pyClass = content.match(/class\s+(\w+)/);
352
+ const funcs = (content.match(/def\s+(\w+)/g) || []).map(f => f.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_"));
353
+ if (pyClass && funcs.length > 0)
354
+ return cap(funcs.length > 4 ? `${pyClass[1]}: ${funcs.slice(0, 4).join(", ")} + ${funcs.length - 4} more` : `${pyClass[1]}: ${funcs.join(", ")}`);
355
+ if (funcs.length > 0)
356
+ return cap(funcs.slice(0, 4).join(", "));
357
+ }
358
+ // ─── Go ──────────────────────────────────────────────────
359
+ if (ext === ".go") {
360
+ const handlers = (content.match(/func\s+(\w+)\s*\(\s*\w+\s+http\.ResponseWriter/g) || [])
361
+ .map(m => m.match(/func\s+(\w+)/)?.[1]).filter(Boolean);
362
+ if (handlers.length)
363
+ return cap(`HTTP handlers: ${handlers.slice(0, 5).join(", ")}`);
364
+ const iface = content.match(/type\s+(\w+)\s+interface\s*\{/);
365
+ if (iface)
366
+ return `Interface: ${iface[1]}`;
367
+ const structM = content.match(/type\s+(\w+)\s+struct\s*\{/);
368
+ if (structM)
369
+ return `Struct: ${structM[1]}`;
370
+ const funcs = (content.match(/^func\s+(\w+)/gm) || []).map(m => m.match(/func\s+(\w+)/)?.[1]).filter(n => n && n[0] === n[0].toUpperCase());
371
+ if (funcs.length)
372
+ return cap(funcs.slice(0, 5).join(", "));
373
+ }
374
+ // ─── Rust ────────────────────────────────────────────────
375
+ if (ext === ".rs") {
376
+ const structM = content.match(/pub\s+struct\s+(\w+)/);
377
+ if (structM) {
378
+ const methods = (content.match(/pub\s+(?:async\s+)?fn\s+(\w+)/g) || []).map(m => m.match(/fn\s+(\w+)/)?.[1]).filter(Boolean);
379
+ return cap(methods.length ? `${structM[1]}: ${methods.slice(0, 4).join(", ")}` : `Struct: ${structM[1]}`);
380
+ }
381
+ const traitM = content.match(/pub\s+trait\s+(\w+)/);
382
+ if (traitM)
383
+ return `Trait: ${traitM[1]}`;
384
+ const enumM = content.match(/pub\s+enum\s+(\w+)/);
385
+ if (enumM)
386
+ return `Enum: ${enumM[1]}`;
387
+ const fns = (content.match(/pub\s+(?:async\s+)?fn\s+(\w+)/g) || []).map(m => m.match(/fn\s+(\w+)/)?.[1]).filter(Boolean);
388
+ if (fns.length)
389
+ return cap(fns.slice(0, 5).join(", "));
390
+ }
391
+ // ─── Java / Spring ───────────────────────────────────────
392
+ if (ext === ".java") {
393
+ const cls = content.match(/(?:public\s+)?class\s+(\w+)/);
394
+ const className = cls?.[1] || basename.replace(".java", "");
395
+ const annotations = (content.match(/@(RestController|Controller|Service|Repository|Component|Entity|Configuration)/g) || []).map(a => a.slice(1));
396
+ const mappings = (content.match(/@(?:Get|Post|Put|Patch|Delete|Request)Mapping/g) || []).length;
397
+ if (mappings)
398
+ return cap(`${annotations[0] || "Spring"}: ${className} (${mappings} endpoints)`);
399
+ if (annotations.length)
400
+ return `${annotations[0]}: ${className}`;
401
+ if (content.includes("@Entity"))
402
+ return `Entity: ${className}`;
403
+ const methods = (content.match(/public\s+(?:static\s+)?(?:\w+(?:<[\w,\s]+>)?)\s+(\w+)\s*\(/g) || [])
404
+ .map(m => m.match(/(\w+)\s*\(/)?.[1]).filter(n => n && n !== className);
405
+ if (methods.length)
406
+ return cap(`${className}: ${methods.slice(0, 4).join(", ")}`);
407
+ return className ? `Class: ${className}` : "";
408
+ }
409
+ // ─── Kotlin ──────────────────────────────────────────────
410
+ if (ext === ".kt" || ext === ".kts") {
411
+ const cls = content.match(/(?:data\s+)?class\s+(\w+)/);
412
+ if (content.match(/data\s+class/))
413
+ return `Data class: ${cls?.[1] || basename.replace(/\.kts?$/, "")}`;
414
+ if (content.includes("routing {"))
415
+ return "Ktor routing";
416
+ const fns = (content.match(/fun\s+(\w+)/g) || []).map(m => m.match(/fun\s+(\w+)/)?.[1]).filter(Boolean);
417
+ if (cls && fns.length)
418
+ return cap(`${cls[1]}: ${fns.slice(0, 4).join(", ")}`);
419
+ if (fns.length)
420
+ return cap(fns.slice(0, 5).join(", "));
421
+ }
422
+ // ─── C# / .NET ───────────────────────────────────────────
423
+ if (ext === ".cs") {
424
+ const cls = content.match(/(?:public\s+)?(?:partial\s+)?class\s+(\w+)(?:\s*:\s*(\w+))?/);
425
+ const className = cls?.[1] || basename.replace(".cs", "");
426
+ const parent = cls?.[2] || "";
427
+ if (parent === "Controller" || parent === "ControllerBase" || content.includes("[ApiController]")) {
428
+ const actions = (content.match(/\[Http(Get|Post|Put|Patch|Delete)\]/g) || []).map(a => a.match(/Http(\w+)/)?.[1]).filter(Boolean);
429
+ return cap(actions.length ? `API Controller: ${className} (${[...new Set(actions)].join(", ")})` : `Controller: ${className}`);
430
+ }
431
+ if (parent === "DbContext" || content.includes("DbSet<")) {
432
+ const sets = (content.match(/DbSet<(\w+)>/g) || []).map(s => s.match(/<(\w+)>/)?.[1]).filter(Boolean);
433
+ return cap(sets.length ? `DbContext: ${sets.join(", ")}` : `DbContext: ${className}`);
434
+ }
435
+ return className ? `Class: ${className}` : "";
436
+ }
437
+ // ─── Ruby / Rails ────────────────────────────────────────
438
+ if (ext === ".rb") {
439
+ const cls = content.match(/class\s+(\w+)(?:\s*<\s*(\w+(?:::\w+)?))?/);
440
+ const className = cls?.[1] || "";
441
+ const parent = cls?.[2] || "";
442
+ if (parent?.includes("Controller")) {
443
+ const actions = (content.match(/def\s+(index|show|new|create|edit|update|destroy|\w+)/g) || [])
444
+ .map(m => m.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_"));
445
+ return cap(actions.length ? `Controller: ${actions.join(", ")}` : `Controller: ${className}`);
446
+ }
447
+ if (parent === "ApplicationRecord" || parent === "ActiveRecord::Base")
448
+ return `Model: ${className}`;
449
+ if (basename.match(/^\d{14}_/)) {
450
+ const create = content.match(/create_table\s+:(\w+)/);
451
+ return create ? `Migration: create ${create[1]}` : "Database migration";
452
+ }
453
+ const methods = (content.match(/def\s+(\w+)/g) || []).map(m => m.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_"));
454
+ if (cls && methods.length)
455
+ return cap(`${className}: ${methods.slice(0, 4).join(", ")}`);
456
+ }
457
+ // ─── Swift ───────────────────────────────────────────────
458
+ if (ext === ".swift") {
459
+ if (content.includes(": View") || content.includes("some View")) {
460
+ const name = content.match(/struct\s+(\w+)\s*:\s*View/);
461
+ return name ? `SwiftUI view: ${name[1]}` : "SwiftUI view";
462
+ }
463
+ const proto = content.match(/protocol\s+(\w+)/);
464
+ if (proto)
465
+ return `Protocol: ${proto[1]}`;
466
+ const struct = content.match(/(?:public\s+)?struct\s+(\w+)/);
467
+ const cls = content.match(/(?:public\s+)?class\s+(\w+)/);
468
+ const name = struct?.[1] || cls?.[1] || "";
469
+ if (name)
470
+ return `${struct ? "Struct" : "Class"}: ${name}`;
471
+ }
472
+ // ─── Dart / Flutter ──────────────────────────────────────
473
+ if (ext === ".dart") {
474
+ if (content.includes("StatefulWidget") || content.includes("StatelessWidget")) {
475
+ const name = content.match(/class\s+(\w+)\s+extends\s+(?:Stateful|Stateless)Widget/);
476
+ return name ? `${content.includes("StatefulWidget") ? "Stateful" : "Stateless"} widget: ${name[1]}` : "Flutter widget";
477
+ }
478
+ const cls = content.match(/class\s+(\w+)/);
479
+ if (cls)
480
+ return `Class: ${cls[1]}`;
481
+ }
482
+ // ─── Vue / Svelte / Astro ────────────────────────────────
483
+ if (ext === ".vue") {
484
+ const name = content.match(/name:\s*['"]([^'"]+)['"]/);
485
+ const setup = content.includes("<script setup");
486
+ const parts = [];
487
+ if (name)
488
+ parts.push(name[1]);
489
+ if (setup)
490
+ parts.push("setup");
491
+ return cap(parts.length ? `Vue: ${parts.join(", ")}` : "Vue component");
492
+ }
493
+ if (ext === ".svelte")
494
+ return `Svelte: ${basename.replace(".svelte", "")}`;
495
+ if (ext === ".astro")
496
+ return `Astro: ${basename.replace(".astro", "")}`;
497
+ // ─── CSS / SCSS / Less ───────────────────────────────────
498
+ if (ext === ".css" || ext === ".scss" || ext === ".less") {
499
+ const rules = (content.match(/^[.#@][^\n{]+/gm) || []).length;
500
+ const vars = (content.match(/--[\w-]+\s*:/g) || []).length;
501
+ const parts = [];
502
+ if (rules)
503
+ parts.push(`${rules} rules`);
504
+ if (vars)
505
+ parts.push(`${vars} vars`);
506
+ return cap(parts.length ? `Styles: ${parts.join(", ")}` : "Stylesheet");
507
+ }
508
+ // ─── SQL ─────────────────────────────────────────────────
509
+ if (ext === ".sql") {
510
+ const creates = (content.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?(\w+)/gi) || [])
511
+ .map(m => m.match(/(?:TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?)([`"']?\w+)/i)?.[1]?.replace(/[`"']/g, "")).filter(Boolean);
512
+ if (creates.length)
513
+ return cap(`SQL: tables: ${creates.slice(0, 4).join(", ")}`);
514
+ }
515
+ // ─── Proto / GraphQL ─────────────────────────────────────
516
+ if (ext === ".proto") {
517
+ const msgs = (content.match(/message\s+(\w+)/g) || []).map(m => m.match(/message\s+(\w+)/)?.[1]).filter(Boolean);
518
+ const services = (content.match(/service\s+(\w+)/g) || []).map(m => m.match(/service\s+(\w+)/)?.[1]).filter(Boolean);
519
+ const parts = [];
520
+ if (msgs.length)
521
+ parts.push(`messages: ${msgs.slice(0, 3).join(", ")}`);
522
+ if (services.length)
523
+ parts.push(`services: ${services.join(", ")}`);
524
+ return cap(parts.length ? `Proto: ${parts.join(", ")}` : "");
525
+ }
526
+ if (ext === ".graphql" || ext === ".gql") {
527
+ const types = (content.match(/type\s+(\w+)/g) || []).map(m => m.match(/type\s+(\w+)/)?.[1]).filter(Boolean);
528
+ return cap(types.length ? `GraphQL: types: ${types.slice(0, 4).join(", ")}` : "GraphQL schema");
529
+ }
530
+ // ─── YAML ────────────────────────────────────────────────
531
+ if (ext === ".yaml" || ext === ".yml") {
532
+ if (content.includes("runs-on:")) {
533
+ const name = content.match(/^name:\s*(.+)$/m);
534
+ return cap(name ? `CI: ${name[1].trim()}` : "GitHub Actions workflow");
535
+ }
536
+ if (content.includes("apiVersion:") && content.includes("kind:")) {
537
+ const kind = content.match(/kind:\s*(\w+)/);
538
+ return cap(kind ? `K8s ${kind[1]}` : "Kubernetes manifest");
539
+ }
540
+ if (content.includes("services:") && (basename.includes("docker") || basename.includes("compose"))) {
541
+ const services = (content.match(/^\s{2}\w+:/gm) || []).length;
542
+ return `Docker Compose: ${services} services`;
543
+ }
544
+ }
545
+ // ─── TOML ────────────────────────────────────────────────
546
+ if (ext === ".toml") {
547
+ const desc = content.match(/^description\s*=\s*"([^"]+)"/m);
548
+ if (desc)
549
+ return cap(desc[1]);
550
+ }
551
+ // ─── Elixir ──────────────────────────────────────────────
552
+ if (ext === ".ex" || ext === ".exs") {
553
+ const mod = content.match(/defmodule\s+([\w.]+)/);
554
+ if (content.includes("Phoenix.LiveView"))
555
+ return cap(mod ? `LiveView: ${mod[1]}` : "Phoenix LiveView");
556
+ if (content.includes("Controller"))
557
+ return cap(mod ? `Phoenix controller: ${mod[1]}` : "Phoenix controller");
558
+ const fns = (content.match(/def\s+(\w+)/g) || []).map(m => m.match(/def\s+(\w+)/)?.[1]).filter(Boolean);
559
+ if (mod && fns.length)
560
+ return cap(`${mod[1]}: ${fns.slice(0, 4).join(", ")}`);
561
+ if (mod)
562
+ return mod[1];
563
+ }
564
+ // ─── Lua ─────────────────────────────────────────────────
565
+ if (ext === ".lua") {
566
+ const fns = (content.match(/function\s+(?:\w+[.:])?(\w+)/g) || []).map(m => m.match(/(\w+)\s*$/)?.[1]).filter(Boolean);
567
+ if (fns.length)
568
+ return cap(fns.slice(0, 5).join(", "));
569
+ }
570
+ // ─── Zig ─────────────────────────────────────────────────
571
+ if (ext === ".zig") {
572
+ const fns = (content.match(/pub\s+fn\s+(\w+)/g) || []).map(m => m.match(/fn\s+(\w+)/)?.[1]).filter(Boolean);
573
+ if (fns.length)
574
+ return cap(fns.slice(0, 5).join(", "));
575
+ }
576
+ // Last resort
577
+ const declM = content.match(/(?:function|class|const|interface|type|enum)\s+(\w+)/);
578
+ if (declM) {
579
+ const name = declM[1];
580
+ const methods = (content.match(/(?:public\s+)?(?:async\s+)?(?:function\s+|(?:get|set)\s+)(\w+)\s*\(/g) || [])
581
+ .map(m => m.match(/(\w+)\s*\(/)?.[1]).filter(n => n && n !== name && n !== "__construct" && n !== "constructor");
582
+ if (methods.length > 0 && methods.length <= 5)
583
+ return cap(`${name}: ${methods.join(", ")}`);
584
+ if (methods.length > 5)
585
+ return cap(`${name}: ${methods.slice(0, 3).join(", ")} + ${methods.length - 3} more`);
586
+ return `Declares ${name}`;
587
+ }
588
+ return "";
589
+ }
590
+ export function estimateTokens(text, type = "mixed") {
591
+ const ratio = type === "code" ? 3.5 : type === "prose" ? 4.0 : 3.75;
592
+ return Math.ceil(text.length / ratio);
593
+ }
594
+ export function timestamp() {
595
+ return new Date().toISOString();
596
+ }
597
+ export function timeShort() {
598
+ const d = new Date();
599
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
600
+ }
601
+ export function readStdin() {
602
+ return new Promise((resolve) => {
603
+ const chunks = [];
604
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
605
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
606
+ // If no stdin data after 4s, resolve with whatever we have so far.
607
+ // On Windows, stdin delivery from Claude Code hooks can be slow.
608
+ setTimeout(() => resolve(chunks.length ? Buffer.concat(chunks).toString("utf-8") : "{}"), 4000);
609
+ });
610
+ }
611
+ export function normalizePath(p) {
612
+ return p.replace(/\\/g, "/");
613
+ }
614
+ //# sourceMappingURL=shared.js.map