skillo 0.2.6 → 0.2.8

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 (68) hide show
  1. package/README.md +198 -198
  2. package/dist/api-client-BF6GDR7Q.js +12 -0
  3. package/dist/{chunk-WJKZWKER.js → chunk-63FVALWX.js} +1 -1
  4. package/dist/chunk-63FVALWX.js.map +1 -0
  5. package/dist/chunk-6GOJPFZ7.js +113 -0
  6. package/dist/chunk-6GOJPFZ7.js.map +1 -0
  7. package/dist/chunk-6UGTWBUW.js +89 -0
  8. package/dist/chunk-6UGTWBUW.js.map +1 -0
  9. package/dist/{chunk-2CVEPT6U.js → chunk-73NUWYUO.js} +2 -2
  10. package/dist/chunk-73NUWYUO.js.map +1 -0
  11. package/dist/chunk-QIV4VIXA.js +221 -0
  12. package/dist/chunk-QIV4VIXA.js.map +1 -0
  13. package/dist/{chunk-CPL3P2OF.js → chunk-QUXHHRRK.js} +2 -2
  14. package/dist/chunk-QUXHHRRK.js.map +1 -0
  15. package/dist/{chunk-ODOZM4QV.js → chunk-RQ2SC5HW.js} +72 -10
  16. package/dist/chunk-RQ2SC5HW.js.map +1 -0
  17. package/dist/chunk-U53QWUOR.js +727 -0
  18. package/dist/chunk-U53QWUOR.js.map +1 -0
  19. package/dist/chunk-VAQ73XPE.js +68 -0
  20. package/dist/chunk-VAQ73XPE.js.map +1 -0
  21. package/dist/chunk-XLJGCOVT.js +975 -0
  22. package/dist/chunk-XLJGCOVT.js.map +1 -0
  23. package/dist/{claude-watcher-N6GN6WHJ.js → claude-watcher-WKGBJYKN.js} +65 -3
  24. package/dist/claude-watcher-WKGBJYKN.js.map +1 -0
  25. package/dist/cli.js +495 -1846
  26. package/dist/cli.js.map +1 -1
  27. package/dist/{config-P5EM5L7N.js → config-ZOKAP2LJ.js} +3 -3
  28. package/dist/daemon-6DTCMOJB.js +28 -0
  29. package/dist/daemon-runner.js +338 -71
  30. package/dist/daemon-runner.js.map +1 -1
  31. package/dist/database-KQY5OSCS.js +9 -0
  32. package/dist/git-OGUSYBJS.js +16 -0
  33. package/dist/git-OGUSYBJS.js.map +1 -0
  34. package/dist/git-OUAHIOY2.js +110 -0
  35. package/dist/git-OUAHIOY2.js.map +1 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/{paths-INOKEM66.js → paths-MPOZBOKE.js} +2 -2
  38. package/dist/paths-MPOZBOKE.js.map +1 -0
  39. package/dist/project-OFU2W6MH.js +19 -0
  40. package/dist/project-OFU2W6MH.js.map +1 -0
  41. package/dist/shell-NZABRJLA.js +16 -0
  42. package/dist/shell-NZABRJLA.js.map +1 -0
  43. package/dist/skill-installer-F67OAOQN.js +121 -0
  44. package/dist/skill-installer-F67OAOQN.js.map +1 -0
  45. package/dist/{skill-usage-detector-EO26MRYV.js → skill-usage-detector-MSW5VWQZ.js} +2 -2
  46. package/dist/skill-usage-detector-MSW5VWQZ.js.map +1 -0
  47. package/dist/tray-WKFGUUTO.js +346 -0
  48. package/dist/tray-WKFGUUTO.js.map +1 -0
  49. package/package.json +62 -63
  50. package/scripts/postinstall.mjs +415 -364
  51. package/scripts/tray-helper-darwin +0 -0
  52. package/scripts/tray-helper-darwin.swift +180 -180
  53. package/scripts/tray-helper-linux.py +322 -0
  54. package/scripts/tray-helper-windows.cs +322 -0
  55. package/dist/api-client-KUQW7FSC.js +0 -12
  56. package/dist/chunk-2CVEPT6U.js.map +0 -1
  57. package/dist/chunk-CPL3P2OF.js.map +0 -1
  58. package/dist/chunk-ODOZM4QV.js.map +0 -1
  59. package/dist/chunk-WJKZWKER.js.map +0 -1
  60. package/dist/claude-watcher-N6GN6WHJ.js.map +0 -1
  61. package/dist/database-F3BFFZKG.js +0 -9
  62. package/dist/skill-usage-detector-EO26MRYV.js.map +0 -1
  63. package/dist/tray-UCAI2U2C.js +0 -408
  64. package/dist/tray-UCAI2U2C.js.map +0 -1
  65. /package/dist/{api-client-KUQW7FSC.js.map → api-client-BF6GDR7Q.js.map} +0 -0
  66. /package/dist/{config-P5EM5L7N.js.map → config-ZOKAP2LJ.js.map} +0 -0
  67. /package/dist/{database-F3BFFZKG.js.map → daemon-6DTCMOJB.js.map} +0 -0
  68. /package/dist/{paths-INOKEM66.js.map → database-KQY5OSCS.js.map} +0 -0
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Skillo Tray Helper for Windows
3
+ *
4
+ * Native C# system tray icon using NotifyIcon (System.Windows.Forms).
5
+ * Communicates with Node.js via stdin/stdout JSON lines.
6
+ *
7
+ * Compile with .NET Framework 4.x csc.exe (pre-installed on Windows 10/11):
8
+ * csc.exe /nologo /target:winexe /r:System.Windows.Forms.dll /r:System.Drawing.dll /r:System.Web.Extensions.dll /out:tray-helper-windows.exe tray-helper-windows.cs
9
+ *
10
+ * Protocol:
11
+ * stdout -> {"type":"ready"} on launch
12
+ * stdin <- JSON menu config (first line) + {"type":"update-menu","menu":{...}} for updates
13
+ * stdout -> {"type":"clicked","item":{...},"seq_id":N} on click
14
+ * stdin EOF -> clean exit
15
+ */
16
+
17
+ using System;
18
+ using System.Collections;
19
+ using System.Collections.Generic;
20
+ using System.Drawing;
21
+ using System.IO;
22
+ using System.Runtime.InteropServices;
23
+ using System.Text;
24
+ using System.Threading;
25
+ using System.Web.Script.Serialization;
26
+ using System.Windows.Forms;
27
+
28
+ class TrayApplicationContext : ApplicationContext
29
+ {
30
+ [DllImport("user32.dll", CharSet = CharSet.Auto)]
31
+ extern static bool DestroyIcon(IntPtr handle);
32
+
33
+ private NotifyIcon trayIcon;
34
+ private Form hiddenForm;
35
+ private JavaScriptSerializer json;
36
+ private Icon currentIcon = null;
37
+ private IntPtr currentIconHandle = IntPtr.Zero;
38
+ private StreamWriter stdoutWriter;
39
+ private StreamReader stdinReader;
40
+
41
+ public TrayApplicationContext()
42
+ {
43
+ // Use raw streams for piped stdio (/target:winexe has no console handle)
44
+ stdoutWriter = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false));
45
+ stdoutWriter.AutoFlush = true;
46
+ stdinReader = new StreamReader(Console.OpenStandardInput(), new UTF8Encoding(false));
47
+
48
+ json = new JavaScriptSerializer();
49
+
50
+ // Hidden form for cross-thread Invoke()
51
+ hiddenForm = new Form();
52
+ hiddenForm.ShowInTaskbar = false;
53
+ hiddenForm.WindowState = FormWindowState.Minimized;
54
+ hiddenForm.FormBorderStyle = FormBorderStyle.None;
55
+ hiddenForm.Opacity = 0;
56
+ hiddenForm.Show();
57
+ hiddenForm.Visible = false;
58
+ // Force handle creation so Invoke() works from background thread
59
+ var dummy = hiddenForm.Handle;
60
+
61
+ // Create tray icon
62
+ trayIcon = new NotifyIcon();
63
+ trayIcon.Text = "Skillo";
64
+ trayIcon.Visible = true;
65
+
66
+ // Set a default icon (application icon or generic)
67
+ try
68
+ {
69
+ trayIcon.Icon = SystemIcons.Application;
70
+ }
71
+ catch { }
72
+
73
+ // Start stdin reader after form handle is created
74
+ Thread stdinThread = new Thread(StdinReader);
75
+ stdinThread.IsBackground = true;
76
+ stdinThread.Start();
77
+
78
+ // Emit ready
79
+ WriteLine("{\"type\":\"ready\"}");
80
+ }
81
+
82
+ private void StdinReader()
83
+ {
84
+ try
85
+ {
86
+ string line;
87
+ while ((line = stdinReader.ReadLine()) != null)
88
+ {
89
+ string trimmed = line.Trim();
90
+ if (string.IsNullOrEmpty(trimmed)) continue;
91
+
92
+ string captured = trimmed;
93
+ try
94
+ {
95
+ hiddenForm.Invoke(new Action(() => ProcessLine(captured)));
96
+ }
97
+ catch (ObjectDisposedException)
98
+ {
99
+ break;
100
+ }
101
+ catch (InvalidOperationException)
102
+ {
103
+ break;
104
+ }
105
+ }
106
+ }
107
+ catch { }
108
+
109
+ // stdin closed (parent died) - clean exit
110
+ try
111
+ {
112
+ hiddenForm.Invoke(new Action(() => Cleanup()));
113
+ }
114
+ catch
115
+ {
116
+ Environment.Exit(0);
117
+ }
118
+ }
119
+
120
+ private void ProcessLine(string line)
121
+ {
122
+ try
123
+ {
124
+ var dict = json.Deserialize<Dictionary<string, object>>(line);
125
+ if (dict == null) return;
126
+
127
+ // Check if it's an update-menu action
128
+ if (dict.ContainsKey("type") && (string)dict["type"] == "update-menu")
129
+ {
130
+ if (dict.ContainsKey("menu"))
131
+ {
132
+ ApplyMenu(dict["menu"] as Dictionary<string, object>);
133
+ }
134
+ return;
135
+ }
136
+
137
+ // Otherwise treat as initial menu config
138
+ if (dict.ContainsKey("items"))
139
+ {
140
+ ApplyMenu(dict);
141
+ }
142
+ }
143
+ catch { /* ignore parse errors */ }
144
+ }
145
+
146
+ private Icon CreateIconFromBase64(string base64)
147
+ {
148
+ byte[] bytes = Convert.FromBase64String(base64);
149
+ // Bitmap must stay alive through GetHicon() — dispose manually after
150
+ var ms = new MemoryStream(bytes);
151
+ var bmp = new Bitmap(ms);
152
+ IntPtr hIcon = bmp.GetHicon();
153
+ bmp.Dispose();
154
+ ms.Dispose();
155
+ return Icon.FromHandle(hIcon);
156
+ // Caller must DestroyIcon(icon.Handle) after icon.Dispose()
157
+ }
158
+
159
+ private void ApplyMenu(Dictionary<string, object> config)
160
+ {
161
+ if (config == null) return;
162
+
163
+ // Icon
164
+ try
165
+ {
166
+ if (config.ContainsKey("icon"))
167
+ {
168
+ string iconBase64 = config["icon"] as string;
169
+ if (!string.IsNullOrEmpty(iconBase64))
170
+ {
171
+ Icon newIcon = CreateIconFromBase64(iconBase64);
172
+ IntPtr newHandle = newIcon.Handle;
173
+
174
+ var oldIcon = currentIcon;
175
+ var oldHandle = currentIconHandle;
176
+
177
+ trayIcon.Icon = newIcon;
178
+ currentIcon = newIcon;
179
+ currentIconHandle = newHandle;
180
+
181
+ // Dispose old icon and destroy its GDI handle
182
+ // Icon.FromHandle() does NOT own the handle, so we must DestroyIcon explicitly
183
+ if (oldIcon != null)
184
+ {
185
+ oldIcon.Dispose();
186
+ }
187
+ if (oldHandle != IntPtr.Zero)
188
+ {
189
+ DestroyIcon(oldHandle);
190
+ }
191
+ }
192
+ }
193
+ }
194
+ catch { /* use existing icon */ }
195
+
196
+ // Tooltip
197
+ try
198
+ {
199
+ if (config.ContainsKey("tooltip"))
200
+ {
201
+ string tooltip = config["tooltip"] as string;
202
+ if (tooltip != null)
203
+ {
204
+ // NotifyIcon.Text limit is 63 chars in .NET Framework 4.x
205
+ if (tooltip.Length > 63)
206
+ tooltip = tooltip.Substring(0, 62) + "\u2026";
207
+ trayIcon.Text = tooltip;
208
+ }
209
+ }
210
+ }
211
+ catch { }
212
+
213
+ // Menu items
214
+ try
215
+ {
216
+ var menuStrip = new ContextMenuStrip();
217
+ var items = config.ContainsKey("items") ? config["items"] as ArrayList : null;
218
+ if (items == null) return;
219
+
220
+ int seq = 0;
221
+ foreach (object rawItem in items)
222
+ {
223
+ var item = rawItem as Dictionary<string, object>;
224
+ if (item == null) { seq++; continue; }
225
+
226
+ string title = item.ContainsKey("title") ? item["title"] as string : "";
227
+
228
+ if (title == "<SEPARATOR>")
229
+ {
230
+ menuStrip.Items.Add(new ToolStripSeparator());
231
+ }
232
+ else
233
+ {
234
+ bool hidden = item.ContainsKey("hidden") && item["hidden"] is bool && (bool)item["hidden"];
235
+ if (hidden) { seq++; continue; }
236
+
237
+ var menuItem = new ToolStripMenuItem(title);
238
+
239
+ bool enabled = !item.ContainsKey("enabled") || (item["enabled"] is bool && (bool)item["enabled"]);
240
+ menuItem.Enabled = enabled;
241
+
242
+ bool isChecked = item.ContainsKey("checked") && item["checked"] is bool && (bool)item["checked"];
243
+ menuItem.Checked = isChecked;
244
+
245
+ if (item.ContainsKey("tooltip"))
246
+ {
247
+ menuItem.ToolTipText = item["tooltip"] as string;
248
+ }
249
+
250
+ if (enabled)
251
+ {
252
+ int capturedSeq = seq;
253
+ Dictionary<string, object> capturedItem = item;
254
+ menuItem.Click += (s, e) => OnItemClick(capturedSeq, capturedItem);
255
+ }
256
+
257
+ menuStrip.Items.Add(menuItem);
258
+ }
259
+ seq++;
260
+ }
261
+
262
+ trayIcon.ContextMenuStrip = menuStrip;
263
+ }
264
+ catch { }
265
+ }
266
+
267
+ private void OnItemClick(int seqId, Dictionary<string, object> item)
268
+ {
269
+ try
270
+ {
271
+ var clickEvent = new Dictionary<string, object>();
272
+ clickEvent["type"] = "clicked";
273
+ clickEvent["item"] = item;
274
+ clickEvent["seq_id"] = seqId;
275
+ WriteLine(json.Serialize(clickEvent));
276
+ }
277
+ catch { }
278
+ }
279
+
280
+ private void WriteLine(string s)
281
+ {
282
+ try
283
+ {
284
+ stdoutWriter.WriteLine(s);
285
+ }
286
+ catch { }
287
+ }
288
+
289
+ private void Cleanup()
290
+ {
291
+ try
292
+ {
293
+ trayIcon.Visible = false;
294
+ trayIcon.Dispose();
295
+
296
+ if (currentIcon != null)
297
+ {
298
+ currentIcon.Dispose();
299
+ currentIcon = null;
300
+ }
301
+ if (currentIconHandle != IntPtr.Zero)
302
+ {
303
+ DestroyIcon(currentIconHandle);
304
+ currentIconHandle = IntPtr.Zero;
305
+ }
306
+ }
307
+ catch { }
308
+
309
+ Application.Exit();
310
+ }
311
+ }
312
+
313
+ static class Program
314
+ {
315
+ [STAThread]
316
+ static void Main()
317
+ {
318
+ Application.EnableVisualStyles();
319
+ Application.SetCompatibleTextRenderingDefault(false);
320
+ Application.Run(new TrayApplicationContext());
321
+ }
322
+ }
@@ -1,12 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- ApiClient,
4
- getApiClient
5
- } from "./chunk-ODOZM4QV.js";
6
- import "./chunk-CPL3P2OF.js";
7
- import "./chunk-WJKZWKER.js";
8
- export {
9
- ApiClient,
10
- getApiClient
11
- };
12
- //# sourceMappingURL=api-client-KUQW7FSC.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/database.ts"],"sourcesContent":["/**\n * Database operations for Skillo.\n * Uses sql.js for pure JavaScript SQLite (no native dependencies).\n */\n\nimport initSqlJs, { Database as SqlJsDatabase, SqlValue } from \"sql.js\";\nimport { readFileSync, writeFileSync, existsSync } from \"fs\";\nimport { randomUUID } from \"crypto\";\nimport { dirname } from \"path\";\nimport { ensureDirectory, getDbPath } from \"../utils/paths.js\";\n\nexport interface Command {\n id: string;\n timestamp: string;\n command: string;\n normalized: string;\n variables: string | null;\n cwd: string;\n exitCode: number | null;\n durationMs: number | null;\n sessionId: string;\n projectId: string | null;\n createdAt: string;\n}\n\nexport interface Session {\n id: string;\n startedAt: string;\n endedAt: string | null;\n shell: string;\n projectPath: string | null;\n commandCount: number;\n createdAt: string;\n}\n\nexport interface Pattern {\n id: string;\n sourceType: string;\n description: string;\n count: number;\n firstSeen: string;\n lastSeen: string;\n data: Record<string, unknown>;\n status: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface Skill {\n id: string;\n name: string;\n path: string;\n sourcePatterns: string | null;\n sourceType: string;\n triggerPhrase: string | null;\n createdAt: string;\n usageCount: number;\n lastUsedAt: string | null;\n}\n\nexport interface Stats {\n commandsToday: number;\n commandsTotal: number;\n patternsActive: number;\n skillsTotal: number;\n conversationsTotal: number;\n}\n\nlet SQL: Awaited<ReturnType<typeof initSqlJs>> | null = null;\n\nasync function getSqlJs() {\n if (!SQL) {\n SQL = await initSqlJs();\n }\n return SQL;\n}\n\nexport class SkilloDatabase {\n private db: SqlJsDatabase | null = null;\n private dbPath: string;\n private initialized = false;\n\n constructor(dbPath?: string) {\n this.dbPath = dbPath || getDbPath();\n ensureDirectory(dirname(this.dbPath));\n }\n\n private async getDb(): Promise<SqlJsDatabase> {\n if (!this.db) {\n const SQL = await getSqlJs();\n\n if (existsSync(this.dbPath)) {\n const buffer = readFileSync(this.dbPath);\n this.db = new SQL.Database(buffer);\n } else {\n this.db = new SQL.Database();\n }\n }\n return this.db;\n }\n\n private save(): void {\n if (this.db) {\n const data = this.db.export();\n const buffer = Buffer.from(data);\n writeFileSync(this.dbPath, buffer);\n }\n }\n\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n const db = await this.getDb();\n\n const schema = `\n -- Command history\n CREATE TABLE IF NOT EXISTS commands (\n id TEXT PRIMARY KEY,\n timestamp DATETIME NOT NULL,\n command TEXT NOT NULL,\n normalized TEXT NOT NULL,\n variables TEXT,\n cwd TEXT NOT NULL,\n exit_code INTEGER,\n duration_ms INTEGER,\n session_id TEXT NOT NULL,\n project_id TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n );\n\n CREATE INDEX IF NOT EXISTS idx_commands_session ON commands(session_id);\n CREATE INDEX IF NOT EXISTS idx_commands_timestamp ON commands(timestamp);\n CREATE INDEX IF NOT EXISTS idx_commands_normalized ON commands(normalized);\n\n -- Terminal sessions\n CREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n started_at DATETIME NOT NULL,\n ended_at DATETIME,\n shell TEXT NOT NULL,\n project_path TEXT,\n command_count INTEGER DEFAULT 0,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n );\n\n -- Conversations (Claude Code)\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n project_path TEXT,\n started_at DATETIME,\n updated_at DATETIME,\n message_count INTEGER DEFAULT 0,\n tool_calls TEXT,\n files_touched TEXT,\n intent_summary TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_project ON conversations(project_path);\n\n -- Detected patterns\n CREATE TABLE IF NOT EXISTS patterns (\n id TEXT PRIMARY KEY,\n source_type TEXT NOT NULL,\n description TEXT NOT NULL,\n count INTEGER DEFAULT 1,\n first_seen DATETIME NOT NULL,\n last_seen DATETIME NOT NULL,\n data TEXT NOT NULL,\n status TEXT DEFAULT 'active',\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n );\n\n CREATE INDEX IF NOT EXISTS idx_patterns_status ON patterns(status);\n CREATE INDEX IF NOT EXISTS idx_patterns_count ON patterns(count);\n\n -- Generated skills\n CREATE TABLE IF NOT EXISTS skills (\n id TEXT PRIMARY KEY,\n name TEXT UNIQUE NOT NULL,\n path TEXT NOT NULL,\n source_patterns TEXT,\n source_type TEXT NOT NULL,\n trigger_phrase TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n usage_count INTEGER DEFAULT 0,\n last_used_at DATETIME\n );\n\n CREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name);\n\n -- Configuration\n CREATE TABLE IF NOT EXISTS config (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n );\n\n -- Ignored patterns\n CREATE TABLE IF NOT EXISTS ignored_patterns (\n pattern_hash TEXT PRIMARY KEY,\n reason TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n );\n\n -- Notifications\n CREATE TABLE IF NOT EXISTS notifications (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n title TEXT NOT NULL,\n message TEXT,\n data TEXT,\n is_read INTEGER DEFAULT 0,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n );\n `;\n\n db.run(schema);\n this.save();\n this.initialized = true;\n }\n\n close(): void {\n if (this.db) {\n this.save();\n this.db.close();\n this.db = null;\n }\n }\n\n // Command operations\n async addCommand(params: {\n command: string;\n normalized: string;\n cwd: string;\n sessionId: string;\n exitCode?: number | null;\n durationMs?: number | null;\n variables?: Record<string, unknown> | null;\n projectId?: string | null;\n }): Promise<string> {\n const db = await this.getDb();\n const id = randomUUID();\n const timestamp = new Date().toISOString();\n\n db.run(\n `INSERT INTO commands (id, timestamp, command, normalized, variables, cwd, exit_code, duration_ms, session_id, project_id)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n id,\n timestamp,\n params.command,\n params.normalized,\n params.variables ? JSON.stringify(params.variables) : null,\n params.cwd,\n params.exitCode ?? null,\n params.durationMs ?? null,\n params.sessionId,\n params.projectId ?? null,\n ]\n );\n\n this.save();\n return id;\n }\n\n async getRecentCommands(options?: {\n limit?: number;\n sessionId?: string;\n days?: number;\n }): Promise<Command[]> {\n const db = await this.getDb();\n const limit = options?.limit ?? 100;\n let query = \"SELECT * FROM commands\";\n const params: SqlValue[] = [];\n const conditions: string[] = [];\n\n if (options?.sessionId) {\n conditions.push(\"session_id = ?\");\n params.push(options.sessionId);\n }\n\n if (options?.days) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - options.days);\n conditions.push(\"timestamp >= ?\");\n params.push(cutoff.toISOString());\n }\n\n if (conditions.length > 0) {\n query += \" WHERE \" + conditions.join(\" AND \");\n }\n\n query += \" ORDER BY timestamp DESC LIMIT ?\";\n params.push(limit);\n\n const results = db.exec(query, params);\n if (results.length === 0) return [];\n\n return results[0].values.map((row) => this.mapCommand(results[0].columns, row as SqlValue[]));\n }\n\n // Session operations\n async createSession(shell: string, projectPath?: string | null): Promise<string> {\n const db = await this.getDb();\n const id = randomUUID();\n const startedAt = new Date().toISOString();\n\n db.run(\n `INSERT INTO sessions (id, started_at, shell, project_path) VALUES (?, ?, ?, ?)`,\n [id, startedAt, shell, projectPath ?? null]\n );\n\n this.save();\n return id;\n }\n\n async endSession(sessionId: string): Promise<void> {\n const db = await this.getDb();\n const endedAt = new Date().toISOString();\n\n db.run(\n `UPDATE sessions\n SET ended_at = ?, command_count = (SELECT COUNT(*) FROM commands WHERE session_id = ?)\n WHERE id = ?`,\n [endedAt, sessionId, sessionId]\n );\n\n this.save();\n }\n\n async getSession(sessionId: string): Promise<Session | null> {\n const db = await this.getDb();\n const results = db.exec(\"SELECT * FROM sessions WHERE id = ?\", [sessionId]);\n if (results.length === 0 || results[0].values.length === 0) return null;\n return this.mapSession(results[0].columns, results[0].values[0]);\n }\n\n // Pattern operations\n async addPattern(params: {\n sourceType: string;\n description: string;\n data: Record<string, unknown>;\n patternHash?: string;\n }): Promise<string> {\n const db = await this.getDb();\n const patternId = params.patternHash || randomUUID();\n const now = new Date().toISOString();\n\n // Check if pattern exists\n const existing = db.exec(\"SELECT id, count FROM patterns WHERE id = ?\", [patternId]);\n\n if (existing.length > 0 && existing[0].values.length > 0) {\n // Update count\n db.run(`UPDATE patterns SET count = count + 1, last_seen = ?, updated_at = ? WHERE id = ?`, [\n now,\n now,\n patternId,\n ]);\n } else {\n // Insert new\n db.run(\n `INSERT INTO patterns (id, source_type, description, data, first_seen, last_seen)\n VALUES (?, ?, ?, ?, ?, ?)`,\n [patternId, params.sourceType, params.description, JSON.stringify(params.data), now, now]\n );\n }\n\n this.save();\n return patternId;\n }\n\n async getPatterns(options?: {\n type?: string;\n status?: string;\n minCount?: number;\n limit?: number;\n }): Promise<Pattern[]> {\n const db = await this.getDb();\n const minCount = options?.minCount ?? 1;\n const limit = options?.limit ?? 50;\n\n let query = \"SELECT * FROM patterns WHERE count >= ?\";\n const params: SqlValue[] = [minCount];\n\n if (options?.type) {\n query += \" AND source_type = ?\";\n params.push(options.type);\n }\n\n if (options?.status) {\n query += \" AND status = ?\";\n params.push(options.status);\n }\n\n query += \" ORDER BY count DESC, last_seen DESC LIMIT ?\";\n params.push(limit);\n\n const results = db.exec(query, params);\n if (results.length === 0) return [];\n\n return results[0].values.map((row) => this.mapPattern(results[0].columns, row as SqlValue[]));\n }\n\n async getPattern(patternId: string): Promise<Pattern | null> {\n const db = await this.getDb();\n\n // Try exact match first\n let results = db.exec(\"SELECT * FROM patterns WHERE id = ?\", [patternId]);\n\n if (results.length === 0 || results[0].values.length === 0) {\n // Try partial match\n results = db.exec(\"SELECT * FROM patterns WHERE id LIKE ?\", [`${patternId}%`]);\n }\n\n if (results.length === 0 || results[0].values.length === 0) return null;\n return this.mapPattern(results[0].columns, results[0].values[0]);\n }\n\n async updatePatternStatus(patternId: string, status: string): Promise<void> {\n const db = await this.getDb();\n const now = new Date().toISOString();\n\n db.run(`UPDATE patterns SET status = ?, updated_at = ? WHERE id = ? OR id LIKE ?`, [\n status,\n now,\n patternId,\n `${patternId}%`,\n ]);\n\n this.save();\n }\n\n async addIgnoredPattern(patternHash: string, reason?: string | null): Promise<void> {\n const db = await this.getDb();\n\n db.run(`INSERT OR REPLACE INTO ignored_patterns (pattern_hash, reason) VALUES (?, ?)`, [\n patternHash,\n reason ?? null,\n ]);\n\n this.save();\n }\n\n async isPatternIgnored(patternHash: string): Promise<boolean> {\n const db = await this.getDb();\n const results = db.exec(\"SELECT 1 FROM ignored_patterns WHERE pattern_hash = ?\", [patternHash]);\n return results.length > 0 && results[0].values.length > 0;\n }\n\n // Skill operations\n async registerSkill(params: {\n name: string;\n path: string;\n sourceType: string;\n triggerPhrase?: string | null;\n sourcePatterns?: string[] | null;\n }): Promise<string> {\n const db = await this.getDb();\n const id = randomUUID();\n\n db.run(\n `INSERT INTO skills (id, name, path, source_patterns, source_type, trigger_phrase)\n VALUES (?, ?, ?, ?, ?, ?)`,\n [\n id,\n params.name,\n params.path,\n params.sourcePatterns ? JSON.stringify(params.sourcePatterns) : null,\n params.sourceType,\n params.triggerPhrase ?? null,\n ]\n );\n\n this.save();\n return id;\n }\n\n async getSkills(options?: {\n source?: string;\n sortBy?: \"name\" | \"created\" | \"usage\";\n }): Promise<Skill[]> {\n const db = await this.getDb();\n const sortMap = {\n name: \"name\",\n created: \"created_at\",\n usage: \"usage_count DESC\",\n };\n const order = sortMap[options?.sortBy ?? \"name\"];\n\n let query = \"SELECT * FROM skills\";\n const params: SqlValue[] = [];\n\n if (options?.source) {\n query += \" WHERE source_type = ?\";\n params.push(options.source);\n }\n\n query += ` ORDER BY ${order}`;\n\n const results = db.exec(query, params);\n if (results.length === 0) return [];\n\n return results[0].values.map((row) => this.mapSkill(results[0].columns, row as SqlValue[]));\n }\n\n async getSkill(name: string): Promise<Skill | null> {\n const db = await this.getDb();\n const results = db.exec(\"SELECT * FROM skills WHERE name = ?\", [name]);\n if (results.length === 0 || results[0].values.length === 0) return null;\n return this.mapSkill(results[0].columns, results[0].values[0]);\n }\n\n async deleteSkill(name: string): Promise<void> {\n const db = await this.getDb();\n db.run(\"DELETE FROM skills WHERE name = ?\", [name]);\n this.save();\n }\n\n async recordSkillUsage(name: string): Promise<void> {\n const db = await this.getDb();\n const now = new Date().toISOString();\n\n db.run(`UPDATE skills SET usage_count = usage_count + 1, last_used_at = ? WHERE name = ?`, [\n now,\n name,\n ]);\n\n this.save();\n }\n\n // Statistics\n async getStats(): Promise<Stats> {\n const db = await this.getDb();\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n const todayIso = today.toISOString();\n\n const getCount = (query: string, params: SqlValue[] = []): number => {\n const results = db.exec(query, params);\n if (results.length === 0 || results[0].values.length === 0) return 0;\n return results[0].values[0][0] as number;\n };\n\n return {\n commandsToday: getCount(\"SELECT COUNT(*) FROM commands WHERE timestamp >= ?\", [todayIso]),\n commandsTotal: getCount(\"SELECT COUNT(*) FROM commands\"),\n patternsActive: getCount(\"SELECT COUNT(*) FROM patterns WHERE status = 'active'\"),\n skillsTotal: getCount(\"SELECT COUNT(*) FROM skills\"),\n conversationsTotal: getCount(\"SELECT COUNT(*) FROM conversations\"),\n };\n }\n\n // Mapping helpers\n private mapCommand(columns: string[], values: SqlValue[]): Command {\n const row = this.toObject(columns, values);\n return {\n id: row.id as string,\n timestamp: row.timestamp as string,\n command: row.command as string,\n normalized: row.normalized as string,\n variables: row.variables as string | null,\n cwd: row.cwd as string,\n exitCode: row.exit_code as number | null,\n durationMs: row.duration_ms as number | null,\n sessionId: row.session_id as string,\n projectId: row.project_id as string | null,\n createdAt: row.created_at as string,\n };\n }\n\n private mapSession(columns: string[], values: SqlValue[]): Session {\n const row = this.toObject(columns, values);\n return {\n id: row.id as string,\n startedAt: row.started_at as string,\n endedAt: row.ended_at as string | null,\n shell: row.shell as string,\n projectPath: row.project_path as string | null,\n commandCount: row.command_count as number,\n createdAt: row.created_at as string,\n };\n }\n\n private mapPattern(columns: string[], values: SqlValue[]): Pattern {\n const row = this.toObject(columns, values);\n return {\n id: row.id as string,\n sourceType: row.source_type as string,\n description: row.description as string,\n count: row.count as number,\n firstSeen: row.first_seen as string,\n lastSeen: row.last_seen as string,\n data: JSON.parse(row.data as string),\n status: row.status as string,\n createdAt: row.created_at as string,\n updatedAt: row.updated_at as string,\n };\n }\n\n private mapSkill(columns: string[], values: SqlValue[]): Skill {\n const row = this.toObject(columns, values);\n return {\n id: row.id as string,\n name: row.name as string,\n path: row.path as string,\n sourcePatterns: row.source_patterns as string | null,\n sourceType: row.source_type as string,\n triggerPhrase: row.trigger_phrase as string | null,\n createdAt: row.created_at as string,\n usageCount: row.usage_count as number,\n lastUsedAt: row.last_used_at as string | null,\n };\n }\n\n private toObject(columns: string[], values: SqlValue[]): Record<string, SqlValue> {\n const obj: Record<string, SqlValue> = {};\n columns.forEach((col, i) => {\n obj[col] = values[i];\n });\n return obj;\n }\n}\n"],"mappings":";;;;;;;AAKA,OAAO,eAAwD;AAC/D,SAAS,cAAc,eAAe,kBAAkB;AACxD,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AA4DxB,IAAI,MAAoD;AAExD,eAAe,WAAW;AACxB,MAAI,CAAC,KAAK;AACR,UAAM,MAAM,UAAU;AAAA,EACxB;AACA,SAAO;AACT;AAEO,IAAM,iBAAN,MAAqB;AAAA,EAClB,KAA2B;AAAA,EAC3B;AAAA,EACA,cAAc;AAAA,EAEtB,YAAY,QAAiB;AAC3B,SAAK,SAAS,UAAU,UAAU;AAClC,oBAAgB,QAAQ,KAAK,MAAM,CAAC;AAAA,EACtC;AAAA,EAEA,MAAc,QAAgC;AAC5C,QAAI,CAAC,KAAK,IAAI;AACZ,YAAMA,OAAM,MAAM,SAAS;AAE3B,UAAI,WAAW,KAAK,MAAM,GAAG;AAC3B,cAAM,SAAS,aAAa,KAAK,MAAM;AACvC,aAAK,KAAK,IAAIA,KAAI,SAAS,MAAM;AAAA,MACnC,OAAO;AACL,aAAK,KAAK,IAAIA,KAAI,SAAS;AAAA,MAC7B;AAAA,IACF;AACA,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,OAAa;AACnB,QAAI,KAAK,IAAI;AACX,YAAM,OAAO,KAAK,GAAG,OAAO;AAC5B,YAAM,SAAS,OAAO,KAAK,IAAI;AAC/B,oBAAc,KAAK,QAAQ,MAAM;AAAA,IACnC;AAAA,EACF;AAAA,EAEA,MAAM,aAA4B;AAChC,QAAI,KAAK,YAAa;AAEtB,UAAM,KAAK,MAAM,KAAK,MAAM;AAE5B,UAAM,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwGf,OAAG,IAAI,MAAM;AACb,SAAK,KAAK;AACV,SAAK,cAAc;AAAA,EACrB;AAAA,EAEA,QAAc;AACZ,QAAI,KAAK,IAAI;AACX,WAAK,KAAK;AACV,WAAK,GAAG,MAAM;AACd,WAAK,KAAK;AAAA,IACZ;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,WAAW,QASG;AAClB,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,KAAK,WAAW;AACtB,UAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AAEzC,OAAG;AAAA,MACD;AAAA;AAAA,MAEA;AAAA,QACE;AAAA,QACA;AAAA,QACA,OAAO;AAAA,QACP,OAAO;AAAA,QACP,OAAO,YAAY,KAAK,UAAU,OAAO,SAAS,IAAI;AAAA,QACtD,OAAO;AAAA,QACP,OAAO,YAAY;AAAA,QACnB,OAAO,cAAc;AAAA,QACrB,OAAO;AAAA,QACP,OAAO,aAAa;AAAA,MACtB;AAAA,IACF;AAEA,SAAK,KAAK;AACV,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,kBAAkB,SAID;AACrB,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,QAAQ,SAAS,SAAS;AAChC,QAAI,QAAQ;AACZ,UAAM,SAAqB,CAAC;AAC5B,UAAM,aAAuB,CAAC;AAE9B,QAAI,SAAS,WAAW;AACtB,iBAAW,KAAK,gBAAgB;AAChC,aAAO,KAAK,QAAQ,SAAS;AAAA,IAC/B;AAEA,QAAI,SAAS,MAAM;AACjB,YAAM,SAAS,oBAAI,KAAK;AACxB,aAAO,QAAQ,OAAO,QAAQ,IAAI,QAAQ,IAAI;AAC9C,iBAAW,KAAK,gBAAgB;AAChC,aAAO,KAAK,OAAO,YAAY,CAAC;AAAA,IAClC;AAEA,QAAI,WAAW,SAAS,GAAG;AACzB,eAAS,YAAY,WAAW,KAAK,OAAO;AAAA,IAC9C;AAEA,aAAS;AACT,WAAO,KAAK,KAAK;AAEjB,UAAM,UAAU,GAAG,KAAK,OAAO,MAAM;AACrC,QAAI,QAAQ,WAAW,EAAG,QAAO,CAAC;AAElC,WAAO,QAAQ,CAAC,EAAE,OAAO,IAAI,CAAC,QAAQ,KAAK,WAAW,QAAQ,CAAC,EAAE,SAAS,GAAiB,CAAC;AAAA,EAC9F;AAAA;AAAA,EAGA,MAAM,cAAc,OAAe,aAA8C;AAC/E,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,KAAK,WAAW;AACtB,UAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AAEzC,OAAG;AAAA,MACD;AAAA,MACA,CAAC,IAAI,WAAW,OAAO,eAAe,IAAI;AAAA,IAC5C;AAEA,SAAK,KAAK;AACV,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,WAAW,WAAkC;AACjD,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,WAAU,oBAAI,KAAK,GAAE,YAAY;AAEvC,OAAG;AAAA,MACD;AAAA;AAAA;AAAA,MAGA,CAAC,SAAS,WAAW,SAAS;AAAA,IAChC;AAEA,SAAK,KAAK;AAAA,EACZ;AAAA,EAEA,MAAM,WAAW,WAA4C;AAC3D,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,UAAU,GAAG,KAAK,uCAAuC,CAAC,SAAS,CAAC;AAC1E,QAAI,QAAQ,WAAW,KAAK,QAAQ,CAAC,EAAE,OAAO,WAAW,EAAG,QAAO;AACnE,WAAO,KAAK,WAAW,QAAQ,CAAC,EAAE,SAAS,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;AAAA,EACjE;AAAA;AAAA,EAGA,MAAM,WAAW,QAKG;AAClB,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,YAAY,OAAO,eAAe,WAAW;AACnD,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AAGnC,UAAM,WAAW,GAAG,KAAK,+CAA+C,CAAC,SAAS,CAAC;AAEnF,QAAI,SAAS,SAAS,KAAK,SAAS,CAAC,EAAE,OAAO,SAAS,GAAG;AAExD,SAAG,IAAI,qFAAqF;AAAA,QAC1F;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,OAAO;AAEL,SAAG;AAAA,QACD;AAAA;AAAA,QAEA,CAAC,WAAW,OAAO,YAAY,OAAO,aAAa,KAAK,UAAU,OAAO,IAAI,GAAG,KAAK,GAAG;AAAA,MAC1F;AAAA,IACF;AAEA,SAAK,KAAK;AACV,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,YAAY,SAKK;AACrB,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,WAAW,SAAS,YAAY;AACtC,UAAM,QAAQ,SAAS,SAAS;AAEhC,QAAI,QAAQ;AACZ,UAAM,SAAqB,CAAC,QAAQ;AAEpC,QAAI,SAAS,MAAM;AACjB,eAAS;AACT,aAAO,KAAK,QAAQ,IAAI;AAAA,IAC1B;AAEA,QAAI,SAAS,QAAQ;AACnB,eAAS;AACT,aAAO,KAAK,QAAQ,MAAM;AAAA,IAC5B;AAEA,aAAS;AACT,WAAO,KAAK,KAAK;AAEjB,UAAM,UAAU,GAAG,KAAK,OAAO,MAAM;AACrC,QAAI,QAAQ,WAAW,EAAG,QAAO,CAAC;AAElC,WAAO,QAAQ,CAAC,EAAE,OAAO,IAAI,CAAC,QAAQ,KAAK,WAAW,QAAQ,CAAC,EAAE,SAAS,GAAiB,CAAC;AAAA,EAC9F;AAAA,EAEA,MAAM,WAAW,WAA4C;AAC3D,UAAM,KAAK,MAAM,KAAK,MAAM;AAG5B,QAAI,UAAU,GAAG,KAAK,uCAAuC,CAAC,SAAS,CAAC;AAExE,QAAI,QAAQ,WAAW,KAAK,QAAQ,CAAC,EAAE,OAAO,WAAW,GAAG;AAE1D,gBAAU,GAAG,KAAK,0CAA0C,CAAC,GAAG,SAAS,GAAG,CAAC;AAAA,IAC/E;AAEA,QAAI,QAAQ,WAAW,KAAK,QAAQ,CAAC,EAAE,OAAO,WAAW,EAAG,QAAO;AACnE,WAAO,KAAK,WAAW,QAAQ,CAAC,EAAE,SAAS,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;AAAA,EACjE;AAAA,EAEA,MAAM,oBAAoB,WAAmB,QAA+B;AAC1E,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AAEnC,OAAG,IAAI,4EAA4E;AAAA,MACjF;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAG,SAAS;AAAA,IACd,CAAC;AAED,SAAK,KAAK;AAAA,EACZ;AAAA,EAEA,MAAM,kBAAkB,aAAqB,QAAuC;AAClF,UAAM,KAAK,MAAM,KAAK,MAAM;AAE5B,OAAG,IAAI,gFAAgF;AAAA,MACrF;AAAA,MACA,UAAU;AAAA,IACZ,CAAC;AAED,SAAK,KAAK;AAAA,EACZ;AAAA,EAEA,MAAM,iBAAiB,aAAuC;AAC5D,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,UAAU,GAAG,KAAK,yDAAyD,CAAC,WAAW,CAAC;AAC9F,WAAO,QAAQ,SAAS,KAAK,QAAQ,CAAC,EAAE,OAAO,SAAS;AAAA,EAC1D;AAAA;AAAA,EAGA,MAAM,cAAc,QAMA;AAClB,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,KAAK,WAAW;AAEtB,OAAG;AAAA,MACD;AAAA;AAAA,MAEA;AAAA,QACE;AAAA,QACA,OAAO;AAAA,QACP,OAAO;AAAA,QACP,OAAO,iBAAiB,KAAK,UAAU,OAAO,cAAc,IAAI;AAAA,QAChE,OAAO;AAAA,QACP,OAAO,iBAAiB;AAAA,MAC1B;AAAA,IACF;AAEA,SAAK,KAAK;AACV,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,UAAU,SAGK;AACnB,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,UAAU;AAAA,MACd,MAAM;AAAA,MACN,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AACA,UAAM,QAAQ,QAAQ,SAAS,UAAU,MAAM;AAE/C,QAAI,QAAQ;AACZ,UAAM,SAAqB,CAAC;AAE5B,QAAI,SAAS,QAAQ;AACnB,eAAS;AACT,aAAO,KAAK,QAAQ,MAAM;AAAA,IAC5B;AAEA,aAAS,aAAa,KAAK;AAE3B,UAAM,UAAU,GAAG,KAAK,OAAO,MAAM;AACrC,QAAI,QAAQ,WAAW,EAAG,QAAO,CAAC;AAElC,WAAO,QAAQ,CAAC,EAAE,OAAO,IAAI,CAAC,QAAQ,KAAK,SAAS,QAAQ,CAAC,EAAE,SAAS,GAAiB,CAAC;AAAA,EAC5F;AAAA,EAEA,MAAM,SAAS,MAAqC;AAClD,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,UAAU,GAAG,KAAK,uCAAuC,CAAC,IAAI,CAAC;AACrE,QAAI,QAAQ,WAAW,KAAK,QAAQ,CAAC,EAAE,OAAO,WAAW,EAAG,QAAO;AACnE,WAAO,KAAK,SAAS,QAAQ,CAAC,EAAE,SAAS,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;AAAA,EAC/D;AAAA,EAEA,MAAM,YAAY,MAA6B;AAC7C,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,OAAG,IAAI,qCAAqC,CAAC,IAAI,CAAC;AAClD,SAAK,KAAK;AAAA,EACZ;AAAA,EAEA,MAAM,iBAAiB,MAA6B;AAClD,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AAEnC,OAAG,IAAI,oFAAoF;AAAA,MACzF;AAAA,MACA;AAAA,IACF,CAAC;AAED,SAAK,KAAK;AAAA,EACZ;AAAA;AAAA,EAGA,MAAM,WAA2B;AAC/B,UAAM,KAAK,MAAM,KAAK,MAAM;AAC5B,UAAM,QAAQ,oBAAI,KAAK;AACvB,UAAM,SAAS,GAAG,GAAG,GAAG,CAAC;AACzB,UAAM,WAAW,MAAM,YAAY;AAEnC,UAAM,WAAW,CAAC,OAAe,SAAqB,CAAC,MAAc;AACnE,YAAM,UAAU,GAAG,KAAK,OAAO,MAAM;AACrC,UAAI,QAAQ,WAAW,KAAK,QAAQ,CAAC,EAAE,OAAO,WAAW,EAAG,QAAO;AACnE,aAAO,QAAQ,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC;AAAA,IAC/B;AAEA,WAAO;AAAA,MACL,eAAe,SAAS,sDAAsD,CAAC,QAAQ,CAAC;AAAA,MACxF,eAAe,SAAS,+BAA+B;AAAA,MACvD,gBAAgB,SAAS,uDAAuD;AAAA,MAChF,aAAa,SAAS,6BAA6B;AAAA,MACnD,oBAAoB,SAAS,oCAAoC;AAAA,IACnE;AAAA,EACF;AAAA;AAAA,EAGQ,WAAW,SAAmB,QAA6B;AACjE,UAAM,MAAM,KAAK,SAAS,SAAS,MAAM;AACzC,WAAO;AAAA,MACL,IAAI,IAAI;AAAA,MACR,WAAW,IAAI;AAAA,MACf,SAAS,IAAI;AAAA,MACb,YAAY,IAAI;AAAA,MAChB,WAAW,IAAI;AAAA,MACf,KAAK,IAAI;AAAA,MACT,UAAU,IAAI;AAAA,MACd,YAAY,IAAI;AAAA,MAChB,WAAW,IAAI;AAAA,MACf,WAAW,IAAI;AAAA,MACf,WAAW,IAAI;AAAA,IACjB;AAAA,EACF;AAAA,EAEQ,WAAW,SAAmB,QAA6B;AACjE,UAAM,MAAM,KAAK,SAAS,SAAS,MAAM;AACzC,WAAO;AAAA,MACL,IAAI,IAAI;AAAA,MACR,WAAW,IAAI;AAAA,MACf,SAAS,IAAI;AAAA,MACb,OAAO,IAAI;AAAA,MACX,aAAa,IAAI;AAAA,MACjB,cAAc,IAAI;AAAA,MAClB,WAAW,IAAI;AAAA,IACjB;AAAA,EACF;AAAA,EAEQ,WAAW,SAAmB,QAA6B;AACjE,UAAM,MAAM,KAAK,SAAS,SAAS,MAAM;AACzC,WAAO;AAAA,MACL,IAAI,IAAI;AAAA,MACR,YAAY,IAAI;AAAA,MAChB,aAAa,IAAI;AAAA,MACjB,OAAO,IAAI;AAAA,MACX,WAAW,IAAI;AAAA,MACf,UAAU,IAAI;AAAA,MACd,MAAM,KAAK,MAAM,IAAI,IAAc;AAAA,MACnC,QAAQ,IAAI;AAAA,MACZ,WAAW,IAAI;AAAA,MACf,WAAW,IAAI;AAAA,IACjB;AAAA,EACF;AAAA,EAEQ,SAAS,SAAmB,QAA2B;AAC7D,UAAM,MAAM,KAAK,SAAS,SAAS,MAAM;AACzC,WAAO;AAAA,MACL,IAAI,IAAI;AAAA,MACR,MAAM,IAAI;AAAA,MACV,MAAM,IAAI;AAAA,MACV,gBAAgB,IAAI;AAAA,MACpB,YAAY,IAAI;AAAA,MAChB,eAAe,IAAI;AAAA,MACnB,WAAW,IAAI;AAAA,MACf,YAAY,IAAI;AAAA,MAChB,YAAY,IAAI;AAAA,IAClB;AAAA,EACF;AAAA,EAEQ,SAAS,SAAmB,QAA8C;AAChF,UAAM,MAAgC,CAAC;AACvC,YAAQ,QAAQ,CAAC,KAAK,MAAM;AAC1B,UAAI,GAAG,IAAI,OAAO,CAAC;AAAA,IACrB,CAAC;AACD,WAAO;AAAA,EACT;AACF;","names":["SQL"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/config.ts"],"sourcesContent":["/**\n * Configuration management for Skillo.\n */\n\nimport { readFileSync, writeFileSync, existsSync } from \"fs\";\nimport { dirname } from \"path\";\nimport YAML from \"yaml\";\nimport { ensureDirectory, getConfigFile } from \"../utils/paths.js\";\n\nexport interface SkilloConfig {\n // Shell settings\n defaultShell: string | null;\n\n // Pattern detection\n patternDetection: {\n minCount: number;\n sessionTimeout: number;\n similarityThreshold: number;\n maxSequenceLength: number;\n };\n\n // Privacy settings\n privacy: {\n autoRedact: boolean;\n trackOutput: boolean;\n neverTrack: string[];\n redactionPatterns: string[];\n };\n\n // Notification settings\n notifications: {\n enabled: boolean;\n style: \"inline\" | \"desktop\" | \"both\";\n sound: boolean;\n };\n\n // Skill generation\n skillGeneration: {\n outputDir: string | null;\n includeScripts: boolean;\n includeExamples: boolean;\n autoGenerate: boolean;\n };\n\n // Claude Code integration\n claudeCode: {\n watchConversations: boolean;\n conversationDir: string | null;\n };\n\n // Daemon settings\n daemon: {\n logLevel: \"DEBUG\" | \"INFO\" | \"WARN\" | \"ERROR\";\n patternCheckInterval: number;\n conversationCheckInterval: number;\n };\n\n // Team settings\n team: {\n enabled: boolean;\n slug: string | null;\n autoSync: boolean;\n syncInterval: number;\n };\n\n // API settings\n api: {\n baseUrl: string | null;\n timeout: number;\n };\n\n // API key (stored separately, not in YAML for security)\n apiKey?: string;\n}\n\nexport function getDefaultConfig(): SkilloConfig {\n return {\n defaultShell: null,\n\n patternDetection: {\n minCount: 3,\n sessionTimeout: 30,\n similarityThreshold: 0.8,\n maxSequenceLength: 10,\n },\n\n privacy: {\n autoRedact: true,\n trackOutput: false,\n neverTrack: [\n \"*password*\",\n \"*secret*\",\n \"vault *\",\n \"1password *\",\n \"op *\",\n \"export *_KEY=*\",\n \"export *_SECRET=*\",\n \"export *_TOKEN=*\",\n ],\n redactionPatterns: [\n \"password[=:]\\\\s*\\\\S+\",\n \"token[=:]\\\\s*\\\\S+\",\n \"secret[=:]\\\\s*\\\\S+\",\n \"api[_-]?key[=:]\\\\s*\\\\S+\",\n \"auth[=:]\\\\s*\\\\S+\",\n \"bearer\\\\s+\\\\S+\",\n \"-----BEGIN.*PRIVATE KEY-----\",\n \"AKIA[0-9A-Z]{16}\",\n '[\"\\'][A-Za-z0-9+/]{40,}[\"\\']',\n ],\n },\n\n notifications: {\n enabled: true,\n style: \"inline\",\n sound: false,\n },\n\n skillGeneration: {\n outputDir: null,\n includeScripts: true,\n includeExamples: true,\n autoGenerate: false,\n },\n\n claudeCode: {\n watchConversations: true,\n conversationDir: null,\n },\n\n daemon: {\n logLevel: \"INFO\",\n patternCheckInterval: 60,\n conversationCheckInterval: 5,\n },\n\n team: {\n enabled: false,\n slug: null,\n autoSync: true,\n syncInterval: 300,\n },\n\n api: {\n baseUrl: null,\n timeout: 30000,\n },\n };\n}\n\nfunction deepMerge<T extends object>(base: T, override: Partial<T>): T {\n const result = { ...base };\n\n for (const key of Object.keys(override) as Array<keyof T>) {\n const value = override[key];\n if (\n value !== undefined &&\n typeof result[key] === \"object\" &&\n result[key] !== null &&\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value)\n ) {\n result[key] = deepMerge(\n result[key] as Record<string, unknown>,\n value as Record<string, unknown>\n ) as T[keyof T];\n } else if (value !== undefined) {\n result[key] = value as T[keyof T];\n }\n }\n\n return result;\n}\n\nexport function loadConfig(path?: string): SkilloConfig {\n const configPath = path || getConfigFile();\n\n if (!existsSync(configPath)) {\n return getDefaultConfig();\n }\n\n try {\n const content = readFileSync(configPath, \"utf-8\");\n const parsed = YAML.parse(content) || {};\n\n // Convert snake_case to camelCase for compatibility\n const converted = convertKeysToCamelCase(parsed);\n const defaultConfig = getDefaultConfig();\n\n // Deep merge with default config\n return deepMerge(defaultConfig, converted as Partial<SkilloConfig>);\n } catch {\n return getDefaultConfig();\n }\n}\n\nexport function saveConfig(config: SkilloConfig, path?: string): void {\n const configPath = path || getConfigFile();\n\n ensureDirectory(dirname(configPath));\n\n // Convert camelCase to snake_case for YAML file\n const converted = convertKeysToSnakeCase(config);\n\n const content = YAML.stringify(converted, {\n indent: 2,\n lineWidth: 0,\n });\n\n writeFileSync(configPath, content, \"utf-8\");\n}\n\nexport function getConfigValue(config: SkilloConfig, key: string): unknown {\n const keys = key.split(\".\");\n let current: unknown = config;\n\n for (const k of keys) {\n if (current && typeof current === \"object\" && k in current) {\n current = (current as Record<string, unknown>)[k];\n } else {\n return undefined;\n }\n }\n\n return current;\n}\n\nexport function setConfigValue(config: SkilloConfig, key: string, value: unknown): SkilloConfig {\n const keys = key.split(\".\");\n const result = JSON.parse(JSON.stringify(config)) as SkilloConfig;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let current: any = result;\n\n for (let i = 0; i < keys.length - 1; i++) {\n const k = keys[i];\n if (!(k in current)) {\n current[k] = {};\n }\n current = current[k] as Record<string, unknown>;\n }\n\n current[keys[keys.length - 1]] = value;\n return result;\n}\n\n// Helper functions for key conversion\nfunction toCamelCase(str: string): string {\n return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());\n}\n\nfunction toSnakeCase(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);\n}\n\nfunction convertKeysToCamelCase(obj: unknown): unknown {\n if (Array.isArray(obj)) {\n return obj.map(convertKeysToCamelCase);\n }\n\n if (obj !== null && typeof obj === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[toCamelCase(key)] = convertKeysToCamelCase(value);\n }\n return result;\n }\n\n return obj;\n}\n\nfunction convertKeysToSnakeCase(obj: unknown): unknown {\n if (Array.isArray(obj)) {\n return obj.map(convertKeysToSnakeCase);\n }\n\n if (obj !== null && typeof obj === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[toSnakeCase(key)] = convertKeysToSnakeCase(value);\n }\n return result;\n }\n\n return obj;\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/api-client.ts"],"sourcesContent":["/**\n * Skillo Platform API Client\n *\n * Handles communication with the Skillo platform API.\n */\n\nimport { loadConfig, saveConfig } from \"./config.js\";\n\nconst DEFAULT_API_URL = \"https://www.skillo.one/api/cli\";\n\ninterface ApiResponse<T = unknown> {\n success: boolean;\n data?: T;\n error?: string;\n message?: string;\n}\n\ninterface AuthResponse {\n success: boolean;\n user: {\n id: string;\n email: string;\n name: string;\n };\n}\n\ninterface SyncResponse {\n success: boolean;\n skills?: Array<{\n id: string;\n name: string;\n slug: string;\n description: string;\n content: string;\n commands: string[];\n }>;\n patterns?: Array<{\n id: string;\n name: string;\n description: string;\n commands: string[];\n status: string;\n }>;\n}\n\ninterface GenerateResponse {\n success: boolean;\n skill: {\n id: string;\n name: string;\n slug: string;\n description: string;\n content: string;\n commands: string[];\n };\n}\n\ninterface DeviceAuthResponse {\n code: string;\n verification_url: string;\n expires_in: number;\n interval: number;\n}\n\ninterface TokenStatusResponse {\n status: \"pending\" | \"ready\" | \"expired\" | \"used\" | \"not_found\";\n}\n\ninterface TokenExchangeResponse {\n success: boolean;\n api_key: string;\n user: {\n id: string;\n };\n}\n\nexport class ApiClient {\n private baseUrl: string;\n private apiKey: string | null;\n\n constructor() {\n const config = loadConfig();\n this.baseUrl = config.api?.baseUrl || DEFAULT_API_URL;\n this.apiKey = this.loadApiKey();\n }\n\n /**\n * Load API key from config\n */\n private loadApiKey(): string | null {\n const config = loadConfig();\n return (config as { apiKey?: string }).apiKey || null;\n }\n\n /**\n * Save API key to config\n */\n saveApiKey(key: string): void {\n const config = loadConfig();\n (config as { apiKey?: string }).apiKey = key;\n saveConfig(config);\n this.apiKey = key;\n }\n\n /**\n * Clear API key from config\n */\n clearApiKey(): void {\n const config = loadConfig();\n delete (config as { apiKey?: string }).apiKey;\n saveConfig(config);\n this.apiKey = null;\n }\n\n /**\n * Check if API key is configured\n */\n hasApiKey(): boolean {\n return !!this.apiKey;\n }\n\n /**\n * Get the configured API key (masked)\n */\n getMaskedApiKey(): string | null {\n if (!this.apiKey) return null;\n return `${this.apiKey.substring(0, 12)}...${this.apiKey.substring(this.apiKey.length - 4)}`;\n }\n\n /**\n * Set the API base URL\n */\n setBaseUrl(url: string): void {\n const config = loadConfig();\n if (!config.api) {\n (config as { api?: { baseUrl: string } }).api = { baseUrl: url };\n } else {\n config.api.baseUrl = url;\n }\n saveConfig(config);\n this.baseUrl = url;\n }\n\n /**\n * Make an authenticated API request\n */\n private async request<T>(\n endpoint: string,\n options: RequestInit = {}\n ): Promise<ApiResponse<T>> {\n if (!this.apiKey) {\n return { success: false, error: \"No API key configured. Run 'skillo login' first.\" };\n }\n\n const url = `${this.baseUrl}${endpoint}`;\n const headers: HeadersInit = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n ...options.headers,\n };\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n });\n\n const data = await response.json();\n\n if (process.env.SKILLO_DEBUG) {\n console.log('[DEBUG] Request:', url);\n console.log('[DEBUG] Status:', response.status);\n console.log('[DEBUG] Response:', JSON.stringify(data));\n }\n\n if (!response.ok) {\n return {\n success: false,\n error: data.error || `Request failed with status ${response.status}`,\n };\n }\n\n // If the response already has success/data structure, return as-is\n // Otherwise wrap the data\n if (data.success !== undefined && data.data !== undefined) {\n return { success: data.success, data: data.data as T, message: data.message };\n }\n\n // If response just has success field (no data wrapper), return as-is\n if (data.success !== undefined) {\n return data;\n }\n\n // Legacy: wrap raw data\n return { success: true, data: data as T };\n } catch (error) {\n if (process.env.SKILLO_DEBUG) {\n console.log('[DEBUG] Error:', error);\n }\n if (error instanceof Error) {\n if (error.message.includes(\"ECONNREFUSED\")) {\n return {\n success: false,\n error: \"Cannot connect to Skillo platform. Is the server running?\",\n };\n }\n return { success: false, error: error.message };\n }\n return { success: false, error: \"Unknown error occurred\" };\n }\n }\n\n /**\n * Authenticate with the platform\n */\n async authenticate(): Promise<ApiResponse<AuthResponse>> {\n return this.request<AuthResponse>(\"/auth\", { method: \"POST\" });\n }\n\n /**\n * Sync commands to platform\n */\n async syncCommands(\n commands: Array<{\n timestamp: string;\n command: string;\n normalized: string;\n cwd: string;\n exitCode?: number | null;\n durationMs?: number | null;\n sessionId?: string;\n variables?: Record<string, string> | null;\n }>\n ): Promise<ApiResponse> {\n return this.request(\"/sync\", {\n method: \"POST\",\n body: JSON.stringify({\n type: \"commands\",\n data: commands,\n }),\n });\n }\n\n /**\n * Sync a pattern to platform\n */\n async syncPattern(pattern: {\n sourceType: string;\n name?: string;\n description?: string;\n commands: string[];\n category?: string;\n frequency?: number;\n score?: number;\n firstSeen?: string;\n lastSeen?: string;\n context?: Record<string, unknown>;\n }): Promise<ApiResponse<{ pattern: { id: string; name: string; status: string } }>> {\n return this.request(\"/sync\", {\n method: \"POST\",\n body: JSON.stringify({\n type: \"pattern\",\n data: pattern,\n }),\n });\n }\n\n /**\n * Create a session on the platform\n */\n async createSession(session: {\n startedAt: string;\n shell: string;\n endedAt?: string;\n commandCount?: number;\n }): Promise<ApiResponse<{ session: { id: string } }>> {\n return this.request(\"/sync\", {\n method: \"POST\",\n body: JSON.stringify({\n type: \"session\",\n data: session,\n }),\n });\n }\n\n /**\n * End a session on the platform\n */\n async endSession(sessionId: string): Promise<ApiResponse<{ success: boolean }>> {\n return this.request(\"/sessions\", {\n method: \"PATCH\",\n body: JSON.stringify({ sessionId }),\n });\n }\n\n /**\n * Start a new session on the platform (for standalone terminals)\n */\n async startSession(shell: string, projectPath?: string): Promise<ApiResponse<{ sessionId: string }>> {\n return this.request(\"/sessions\", {\n method: \"POST\",\n body: JSON.stringify({ shell, projectPath }),\n });\n }\n\n /**\n * Download skills and patterns from platform\n */\n async downloadData(type: \"all\" | \"skills\" | \"patterns\" = \"all\"): Promise<ApiResponse<SyncResponse>> {\n return this.request<SyncResponse>(`/sync?type=${type}`, { method: \"GET\" });\n }\n\n /**\n * Generate a skill from a pattern\n */\n async generateSkill(options: {\n patternId?: string;\n commands?: string[];\n name?: string;\n description?: string;\n category?: string;\n context?: Record<string, unknown>;\n }): Promise<ApiResponse<GenerateResponse>> {\n return this.request<GenerateResponse>(\"/generate\", {\n method: \"POST\",\n body: JSON.stringify(options),\n });\n }\n\n /**\n * Start device authorization flow (no auth required)\n */\n async startDeviceAuth(deviceName?: string): Promise<ApiResponse<DeviceAuthResponse>> {\n const url = `${this.baseUrl}/auth/device`;\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ device_name: deviceName }),\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n return {\n success: false,\n error: data.error || `Request failed with status ${response.status}`,\n };\n }\n\n return { success: true, data };\n } catch (error) {\n if (error instanceof Error) {\n if (error.message.includes(\"ECONNREFUSED\")) {\n return {\n success: false,\n error: \"Cannot connect to Skillo platform. Is the server running?\",\n };\n }\n return { success: false, error: error.message };\n }\n return { success: false, error: \"Unknown error occurred\" };\n }\n }\n\n /**\n * Check token status (polling)\n */\n async checkTokenStatus(code: string): Promise<ApiResponse<TokenStatusResponse>> {\n const url = `${this.baseUrl}/token?code=${encodeURIComponent(code)}`;\n try {\n const response = await fetch(url, { method: \"GET\" });\n const data = await response.json();\n\n if (!response.ok) {\n return {\n success: false,\n error: data.error || `Request failed with status ${response.status}`,\n };\n }\n\n return { success: true, data };\n } catch (error) {\n if (error instanceof Error) {\n return { success: false, error: error.message };\n }\n return { success: false, error: \"Unknown error occurred\" };\n }\n }\n\n /**\n * Exchange code for API key\n */\n async exchangeToken(code: string, deviceName?: string): Promise<ApiResponse<TokenExchangeResponse>> {\n const url = `${this.baseUrl}/token`;\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ code, device_name: deviceName }),\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n return {\n success: false,\n error: data.error || `Request failed with status ${response.status}`,\n };\n }\n\n return { success: true, data };\n } catch (error) {\n if (error instanceof Error) {\n return { success: false, error: error.message };\n }\n return { success: false, error: \"Unknown error occurred\" };\n }\n }\n\n /**\n * Sync Claude Code prompts to platform\n */\n async syncClaudePrompts(\n prompts: Array<{\n display: string;\n timestamp: number;\n project: string;\n sessionId: string;\n pastedContents?: Record<string, unknown>;\n }>,\n options?: {\n terminalSessionId?: string;\n projectPath?: string;\n }\n ): Promise<ApiResponse<{\n sessionsCreated: number;\n promptsCreated: number;\n promptsSkipped: number;\n }>> {\n return this.request(\"/claude\", {\n method: \"POST\",\n body: JSON.stringify({\n prompts,\n terminalSessionId: options?.terminalSessionId,\n projectPath: options?.projectPath,\n }),\n });\n }\n\n /**\n * Get Claude Code sessions from platform\n */\n async getClaudeSessions(limit = 20): Promise<ApiResponse<{\n sessions: Array<{\n id: string;\n claudeSessionId: string;\n projectPath: string;\n projectName: string;\n startedAt: string;\n endedAt?: string;\n promptCount: number;\n prompts?: Array<{\n id: string;\n prompt: string;\n timestamp: string;\n category?: string;\n }>;\n }>;\n }>> {\n return this.request(`/claude?limit=${limit}`, { method: \"GET\" });\n }\n\n /**\n * Report skill usage detections to platform\n */\n async reportSkillUsage(\n usages: Array<{\n skillSlug: string;\n claudeSessionId: string;\n projectPath: string;\n timestamp: string;\n }>\n ): Promise<ApiResponse<{ logged: number }>> {\n return this.request(\"/skill-usage\", {\n method: \"POST\",\n body: JSON.stringify({ usages }),\n });\n }\n\n // ============================================================================\n // PROJECT TRACKING\n // ============================================================================\n\n /**\n * Connect a project for tracking\n */\n async connectProject(params: {\n path: string;\n name?: string;\n gitRemote?: string;\n gitRemoteNormalized?: string;\n language?: string;\n framework?: string;\n }): Promise<ApiResponse<{\n id: string;\n name: string;\n path: string;\n trackingEnabled: boolean;\n connectedAt: string;\n }>> {\n return this.request(\"/projects/connect\", {\n method: \"POST\",\n body: JSON.stringify(params),\n });\n }\n\n /**\n * Disconnect a project from tracking\n */\n async disconnectProject(path: string): Promise<ApiResponse<{\n id: string;\n name: string;\n trackingEnabled: boolean;\n }>> {\n return this.request(`/projects/connect?path=${encodeURIComponent(path)}`, {\n method: \"DELETE\",\n });\n }\n\n /**\n * Get tracking status for a project\n */\n async getProjectStatus(path: string): Promise<ApiResponse<{\n connected: boolean;\n tracked: boolean;\n connectedAt?: string;\n project?: {\n id: string;\n name: string;\n path: string;\n gitRemote?: string;\n gitRemoteNormalized?: string;\n trackingEnabled: boolean;\n };\n }>> {\n return this.request(`/projects/connect?path=${encodeURIComponent(path)}`, {\n method: \"GET\",\n });\n }\n\n /**\n * List all tracked projects\n */\n async listProjects(includeDisabled = false): Promise<ApiResponse<{\n projects: Array<{\n id: string;\n name: string;\n path: string;\n gitRemote?: string;\n gitRemoteNormalized?: string;\n trackingEnabled: boolean;\n connectedAt?: string;\n }>;\n totalTracked: number;\n totalProjects: number;\n }>> {\n return this.request(`/projects/tracked?includeDisabled=${includeDisabled}`, {\n method: \"GET\",\n });\n }\n\n /**\n * Check if a path is in a tracked project\n * Returns the project if tracked, null if not\n */\n async isProjectTracked(path: string): Promise<{\n tracked: boolean;\n project?: {\n id: string;\n name: string;\n path: string;\n };\n }> {\n const result = await this.getProjectStatus(path);\n if (result.success && result.data?.connected) {\n return {\n tracked: true,\n project: result.data.project ? {\n id: result.data.project.id,\n name: result.data.project.name,\n path: result.data.project.path,\n } : undefined,\n };\n }\n return { tracked: false };\n }\n}\n\n// Singleton instance\nlet clientInstance: ApiClient | null = null;\n\nexport function getApiClient(): ApiClient {\n if (!clientInstance) {\n clientInstance = new ApiClient();\n }\n return clientInstance;\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/utils/paths.ts"],"sourcesContent":["/**\n * Path utilities for Skillo.\n *\n * Handles platform-specific path resolution for data directories,\n * configuration files, and skill storage locations.\n */\n\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { mkdirSync, existsSync } from \"fs\";\n\nexport function getHomeDir(): string {\n return homedir();\n}\n\n/**\n * Get Skillo data directory.\n * This is where the database and other data files are stored.\n *\n * - Linux/macOS: ~/.skillo/\n * - Windows: %USERPROFILE%/.skillo/\n */\nexport function getDataDir(): string {\n const envPath = process.env.SKILLO_DATA_DIR;\n if (envPath) return envPath;\n\n return join(getHomeDir(), \".skillo\");\n}\n\n/**\n * Get Skillo configuration directory.\n * This is where config.yaml and other configuration files are stored.\n *\n * - Linux: ~/.config/skillo/\n * - macOS: ~/.config/skillo/\n * - Windows: %USERPROFILE%/.config/skillo/\n */\nexport function getConfigDir(): string {\n const envPath = process.env.SKILLO_CONFIG_DIR;\n if (envPath) return envPath;\n\n const xdgConfig = process.env.XDG_CONFIG_HOME;\n if (xdgConfig) return join(xdgConfig, \"skillo\");\n\n return join(getHomeDir(), \".config\", \"skillo\");\n}\n\n/**\n * Get personal skills directory.\n * This is where generated skills are stored and where Claude Code\n * reads personal skills from.\n *\n * - All platforms: ~/.claude/skills/\n */\nexport function getSkillsDir(): string {\n const envPath = process.env.SKILLO_SKILLS_DIR;\n if (envPath) return envPath;\n\n return join(getClaudeDir(), \"skills\");\n}\n\n/**\n * Get Claude Code directory.\n * This is where Claude Code stores its data, including conversations.\n *\n * - All platforms: ~/.claude/\n */\nexport function getClaudeDir(): string {\n return join(getHomeDir(), \".claude\");\n}\n\n/**\n * Get project-specific skills directory.\n * This is where project-specific skills are stored.\n *\n * - Path: <project>/.claude/skills/\n */\nexport function getProjectSkillsDir(projectPath?: string): string {\n const project = projectPath || process.cwd();\n return join(project, \".claude\", \"skills\");\n}\n\n/**\n * Get team skills directory.\n * This is where team skills are synced to.\n *\n * - Path: ~/.claude/skills/team/<team-slug>/\n */\nexport function getTeamSkillsDir(teamSlug?: string): string {\n const base = join(getSkillsDir(), \"team\");\n if (teamSlug) return join(base, teamSlug);\n return base;\n}\n\n/**\n * Ensure a directory exists, creating it if necessary.\n *\n * @returns True if directory was created, False if it already existed.\n */\nexport function ensureDirectory(path: string): boolean {\n if (existsSync(path)) {\n return false;\n }\n\n mkdirSync(path, { recursive: true });\n return true;\n}\n\n/**\n * Get path to daemon log file.\n */\nexport function getLogFile(): string {\n return join(getDataDir(), \"daemon.log\");\n}\n\n/**\n * Get path to daemon PID file.\n */\nexport function getPidFile(): string {\n return join(getDataDir(), \"daemon.pid\");\n}\n\n/**\n * Get path to SQLite database file.\n */\nexport function getDbPath(): string {\n return join(getDataDir(), \"skillo.db\");\n}\n\n/**\n * Get path to config file.\n */\nexport function getConfigFile(): string {\n return join(getConfigDir(), \"config.yaml\");\n}\n\n/**\n * Get path to tracked projects cache file.\n * Written by daemon, read by shell hooks for fast project detection.\n */\nexport function getTrackedProjectsCacheFile(): string {\n return join(getDataDir(), \"tracked-projects.json\");\n}\n\n/**\n * Get path to active sessions directory.\n * Each terminal gets a file named by its PID.\n */\nexport function getActiveSessionsDir(): string {\n return join(getDataDir(), \"active-sessions\");\n}\n\n/**\n * Get path to shell integration directory.\n */\nexport function getShellIntegrationDir(): string {\n return join(getDataDir(), \"shell-integration\");\n}\n"],"mappings":";;;AAOA,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB,SAAS,WAAW,kBAAkB;AAE/B,SAAS,aAAqB;AACnC,SAAO,QAAQ;AACjB;AASO,SAAS,aAAqB;AACnC,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,QAAS,QAAO;AAEpB,SAAO,KAAK,WAAW,GAAG,SAAS;AACrC;AAUO,SAAS,eAAuB;AACrC,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,QAAS,QAAO;AAEpB,QAAM,YAAY,QAAQ,IAAI;AAC9B,MAAI,UAAW,QAAO,KAAK,WAAW,QAAQ;AAE9C,SAAO,KAAK,WAAW,GAAG,WAAW,QAAQ;AAC/C;AASO,SAAS,eAAuB;AACrC,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,QAAS,QAAO;AAEpB,SAAO,KAAK,aAAa,GAAG,QAAQ;AACtC;AAQO,SAAS,eAAuB;AACrC,SAAO,KAAK,WAAW,GAAG,SAAS;AACrC;AAQO,SAAS,oBAAoB,aAA8B;AAChE,QAAM,UAAU,eAAe,QAAQ,IAAI;AAC3C,SAAO,KAAK,SAAS,WAAW,QAAQ;AAC1C;AAQO,SAAS,iBAAiB,UAA2B;AAC1D,QAAM,OAAO,KAAK,aAAa,GAAG,MAAM;AACxC,MAAI,SAAU,QAAO,KAAK,MAAM,QAAQ;AACxC,SAAO;AACT;AAOO,SAAS,gBAAgB,MAAuB;AACrD,MAAI,WAAW,IAAI,GAAG;AACpB,WAAO;AAAA,EACT;AAEA,YAAU,MAAM,EAAE,WAAW,KAAK,CAAC;AACnC,SAAO;AACT;AAKO,SAAS,aAAqB;AACnC,SAAO,KAAK,WAAW,GAAG,YAAY;AACxC;AAKO,SAAS,aAAqB;AACnC,SAAO,KAAK,WAAW,GAAG,YAAY;AACxC;AAKO,SAAS,YAAoB;AAClC,SAAO,KAAK,WAAW,GAAG,WAAW;AACvC;AAKO,SAAS,gBAAwB;AACtC,SAAO,KAAK,aAAa,GAAG,aAAa;AAC3C;AAMO,SAAS,8BAAsC;AACpD,SAAO,KAAK,WAAW,GAAG,uBAAuB;AACnD;AAMO,SAAS,uBAA+B;AAC7C,SAAO,KAAK,WAAW,GAAG,iBAAiB;AAC7C;AAKO,SAAS,yBAAiC;AAC/C,SAAO,KAAK,WAAW,GAAG,mBAAmB;AAC/C;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/claude-watcher.ts"],"sourcesContent":["/**\n * Claude Watcher - Reusable module for watching Claude Code history.\n *\n * Polls ~/.claude/history.jsonl for new entries and syncs them to the platform.\n * Used by both the daemon (background) and the `claude watch` command (foreground).\n */\n\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport * as os from \"os\";\nimport * as readline from \"readline\";\n\n// Types\nexport interface ClaudeHistoryEntry {\n display: string;\n timestamp: number;\n project: string;\n sessionId: string;\n pastedContents?: Record<string, unknown>;\n}\n\ninterface TrackedProjectInfo {\n tracked: boolean;\n name?: string;\n}\n\nexport interface ClaudeWatcherCallbacks {\n onSync?: (promptsCreated: number) => void;\n onError?: (error: Error) => void;\n log?: (level: string, msg: string) => void;\n}\n\n// ── Path helpers ──\n\nfunction getClaudeHistoryPath(): string {\n return path.join(os.homedir(), \".claude\", \"history.jsonl\");\n}\n\nfunction normalizePath(p: string): string {\n return p.toLowerCase().replace(/\\\\/g, \"/\").replace(/\\/+$/, \"\");\n}\n\nfunction matchesProjectPath(claudeProjectPath: string, filterPath: string): boolean {\n const normalizedClaude = normalizePath(claudeProjectPath);\n const normalizedFilter = normalizePath(filterPath);\n return normalizedClaude === normalizedFilter ||\n normalizedClaude.startsWith(normalizedFilter + \"/\");\n}\n\n// ── Project tracking cache ──\n\nconst projectCache = {\n projects: new Map<string, TrackedProjectInfo>(),\n lastFetched: 0,\n};\n\nconst CACHE_TTL_MS = 60000;\n\nexport async function loadTrackedProjects(\n client: { listProjects: (includeDisabled: boolean) => Promise<{ success: boolean; data?: { projects: Array<{ path: string; name: string; trackingEnabled: boolean }> } }> }\n): Promise<Map<string, TrackedProjectInfo>> {\n const now = Date.now();\n\n if (projectCache.lastFetched > 0 && now - projectCache.lastFetched < CACHE_TTL_MS) {\n return projectCache.projects;\n }\n\n try {\n const result = await client.listProjects(true);\n if (result.success && result.data?.projects) {\n projectCache.projects.clear();\n for (const project of result.data.projects) {\n const normalizedPath = normalizePath(project.path);\n projectCache.projects.set(normalizedPath, {\n tracked: project.trackingEnabled,\n name: project.name,\n });\n }\n projectCache.lastFetched = now;\n }\n } catch {\n // Use stale cache on failure\n }\n\n return projectCache.projects;\n}\n\nexport function getTrackedProjectCount(): number {\n return Array.from(projectCache.projects.values()).filter(p => p.tracked).length;\n}\n\nasync function isProjectTracked(projectPath: string): Promise<TrackedProjectInfo> {\n const normalizedPath = normalizePath(projectPath);\n\n if (projectCache.projects.has(normalizedPath)) {\n return projectCache.projects.get(normalizedPath)!;\n }\n\n for (const [trackedPath, info] of projectCache.projects.entries()) {\n if (normalizedPath.startsWith(trackedPath + \"/\")) {\n return info;\n }\n }\n\n return { tracked: false };\n}\n\n// ── Watcher ──\n\nexport class ClaudeWatcher {\n private lastSize = 0;\n private lastMtime = 0;\n private intervalId: ReturnType<typeof setInterval> | null = null;\n private historyPath: string;\n private intervalMs: number;\n private projectFilter?: string;\n private sessionFilter?: string;\n private callbacks: ClaudeWatcherCallbacks;\n private client: {\n syncClaudePrompts: (prompts: ClaudeHistoryEntry[], options?: { terminalSessionId?: string; projectPath?: string }) => Promise<{ success: boolean; data?: { promptsCreated: number; sessionsCreated: number; promptsSkipped: number }; error?: string }>;\n listProjects: (includeDisabled: boolean) => Promise<{ success: boolean; data?: { projects: Array<{ path: string; name: string; trackingEnabled: boolean }> } }>;\n };\n\n constructor(\n client: ClaudeWatcher[\"client\"],\n options: {\n intervalMs?: number;\n projectFilter?: string;\n sessionFilter?: string;\n callbacks?: ClaudeWatcherCallbacks;\n } = {}\n ) {\n this.client = client;\n this.historyPath = getClaudeHistoryPath();\n this.intervalMs = options.intervalMs || 5000;\n this.projectFilter = options.projectFilter;\n this.sessionFilter = options.sessionFilter;\n this.callbacks = options.callbacks || {};\n }\n\n private log(level: string, msg: string) {\n if (this.callbacks.log) {\n this.callbacks.log(level, msg);\n }\n }\n\n /** Initialize file position to current end (don't sync old entries). */\n initPosition() {\n try {\n if (fs.existsSync(this.historyPath)) {\n const stats = fs.statSync(this.historyPath);\n this.lastSize = stats.size;\n this.lastMtime = stats.mtimeMs;\n }\n } catch {\n // File doesn't exist yet\n }\n }\n\n /** Start watching. Runs an initial check then polls on interval. */\n async start() {\n this.initPosition();\n\n // Load tracked projects\n await loadTrackedProjects(this.client);\n const trackedCount = getTrackedProjectCount();\n this.log(\"INFO\", `Watching ${trackedCount} tracked project(s)`);\n\n // Initial check\n await this.checkAndSync();\n\n // Poll\n this.intervalId = setInterval(() => this.checkAndSync(), this.intervalMs);\n }\n\n /** Stop watching. */\n stop() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = null;\n }\n }\n\n /** Check for new entries and sync them. */\n async checkAndSync() {\n try {\n if (!fs.existsSync(this.historyPath)) return;\n\n const stats = fs.statSync(this.historyPath);\n\n // Skip if no changes\n if (stats.size === this.lastSize && stats.mtimeMs === this.lastMtime) {\n return;\n }\n\n // Refresh tracked projects cache\n await loadTrackedProjects(this.client);\n\n // Read new content\n const newPrompts: ClaudeHistoryEntry[] = [];\n\n if (stats.size > this.lastSize) {\n const stream = fs.createReadStream(this.historyPath, {\n start: this.lastSize,\n encoding: \"utf-8\",\n });\n\n const rl = readline.createInterface({\n input: stream,\n crlfDelay: Infinity,\n });\n\n for await (const line of rl) {\n if (!line.trim()) continue;\n\n try {\n const entry = JSON.parse(line) as ClaudeHistoryEntry;\n if (!entry.display || !entry.timestamp || !entry.project || !entry.sessionId) {\n continue;\n }\n\n // Project path filter\n if (this.projectFilter && !matchesProjectPath(entry.project, this.projectFilter)) {\n continue;\n }\n\n // Privacy: only tracked projects\n const trackingStatus = await isProjectTracked(entry.project);\n if (!trackingStatus.tracked) {\n continue;\n }\n\n newPrompts.push(entry);\n } catch {\n // Skip invalid JSON\n }\n }\n }\n\n this.lastSize = stats.size;\n this.lastMtime = stats.mtimeMs;\n\n if (newPrompts.length > 0) {\n this.log(\"INFO\", `Detected ${newPrompts.length} new prompt(s), syncing...`);\n\n const response = await this.client.syncClaudePrompts(newPrompts, {\n terminalSessionId: this.sessionFilter,\n projectPath: this.projectFilter,\n });\n\n if (response.success && response.data) {\n this.log(\"INFO\", `Synced ${response.data.promptsCreated} prompt(s)`);\n this.callbacks.onSync?.(response.data.promptsCreated);\n } else {\n this.log(\"ERROR\", `Sync failed: ${response.error}`);\n }\n }\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n this.log(\"ERROR\", `Watch error: ${err.message}`);\n this.callbacks.onError?.(err);\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- SkilloDatabase
4
- } from "./chunk-2CVEPT6U.js";
5
- import "./chunk-WJKZWKER.js";
6
- export {
7
- SkilloDatabase
8
- };
9
- //# sourceMappingURL=database-F3BFFZKG.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/skill-usage-detector.ts"],"sourcesContent":["/**\n * Skill Usage Detector — detects when Claude Code uses skills.\n *\n * Scans Claude Code session JSONL files for:\n * - Read tool calls to ~/.claude/skills/{slug}/SKILL.md\n * - Bash tool calls matching skill command arrays\n *\n * Tracks byte offsets per file to avoid re-processing.\n * Reports detections to the Skillo platform via batch API.\n */\n\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport * as readline from \"readline\";\nimport { getSkillsDir, getClaudeDir, getDataDir, ensureDirectory } from \"../utils/paths.js\";\n\nexport interface SkillUsageEvent {\n skillSlug: string;\n claudeSessionId: string;\n projectPath: string;\n timestamp: string;\n}\n\ninterface OffsetState {\n [filePath: string]: {\n byteOffset: number;\n lastModified: number;\n };\n}\n\nexport interface SkillUsageDetectorCallbacks {\n onDetection?: (count: number) => void;\n onError?: (error: Error) => void;\n log?: (level: string, msg: string) => void;\n}\n\nconst MAX_NEW_BYTES_PER_FILE = 2 * 1024 * 1024; // 2MB cap per cycle\nconst OFFSETS_FILE = \"skill-usage-offsets.json\";\n\nexport class SkillUsageDetector {\n private intervalId: ReturnType<typeof setInterval> | null = null;\n private intervalMs: number;\n private callbacks: SkillUsageDetectorCallbacks;\n private client: {\n reportSkillUsage: (usages: SkillUsageEvent[]) => Promise<{\n success: boolean;\n data?: { logged: number };\n error?: string;\n }>;\n };\n\n constructor(\n client: SkillUsageDetector[\"client\"],\n options: {\n intervalMs?: number;\n callbacks?: SkillUsageDetectorCallbacks;\n } = {}\n ) {\n this.client = client;\n this.intervalMs = options.intervalMs || 30000;\n this.callbacks = options.callbacks || {};\n }\n\n private log(level: string, msg: string) {\n this.callbacks.log?.(level, msg);\n }\n\n async start() {\n // Initialize offsets to current file sizes\n this.initOffsets();\n this.log(\"INFO\", \"Skill usage detector started\");\n\n // Poll\n this.intervalId = setInterval(() => this.detect(), this.intervalMs);\n }\n\n stop() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = null;\n }\n }\n\n /** Build slug inventory from ~/.claude/skills/ */\n private getDeployedSkillSlugs(): Set<string> {\n const slugs = new Set<string>();\n const skillsDir = getSkillsDir();\n\n if (!fs.existsSync(skillsDir)) return slugs;\n\n try {\n const entries = fs.readdirSync(skillsDir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name.startsWith(\".\")) continue;\n const skillFile = path.join(skillsDir, entry.name, \"SKILL.md\");\n if (fs.existsSync(skillFile)) {\n slugs.add(entry.name);\n }\n }\n } catch {\n // ignore\n }\n\n return slugs;\n }\n\n /** Encode project path to Claude's directory name format */\n private encodeProjectPath(projectPath: string): string {\n // /Users/foo/bar → -Users-foo-bar\n return projectPath.replace(/\\//g, \"-\");\n }\n\n /** Find session JSONL files for tracked projects */\n private getSessionFiles(): string[] {\n const claudeDir = getClaudeDir();\n const projectsDir = path.join(claudeDir, \"projects\");\n\n if (!fs.existsSync(projectsDir)) return [];\n\n const files: string[] = [];\n try {\n const projectDirs = fs.readdirSync(projectsDir, { withFileTypes: true });\n for (const dir of projectDirs) {\n if (!dir.isDirectory()) continue;\n const dirPath = path.join(projectsDir, dir.name);\n const jsonlFiles = fs.readdirSync(dirPath).filter(\n (f) => f.endsWith(\".jsonl\") && !f.startsWith(\"agent-\")\n );\n for (const f of jsonlFiles) {\n files.push(path.join(dirPath, f));\n }\n }\n } catch {\n // ignore\n }\n\n return files;\n }\n\n /** Load persisted offsets */\n private loadOffsets(): OffsetState {\n const offsetsPath = path.join(getDataDir(), OFFSETS_FILE);\n try {\n if (fs.existsSync(offsetsPath)) {\n return JSON.parse(fs.readFileSync(offsetsPath, \"utf-8\"));\n }\n } catch {\n // ignore\n }\n return {};\n }\n\n /** Save offsets */\n private saveOffsets(state: OffsetState) {\n ensureDirectory(getDataDir());\n const offsetsPath = path.join(getDataDir(), OFFSETS_FILE);\n try {\n fs.writeFileSync(offsetsPath, JSON.stringify(state), \"utf-8\");\n } catch {\n // ignore\n }\n }\n\n /** Initialize offsets to current file sizes (skip existing data) */\n private initOffsets() {\n const files = this.getSessionFiles();\n const state = this.loadOffsets();\n\n for (const file of files) {\n if (!state[file]) {\n try {\n const stats = fs.statSync(file);\n state[file] = { byteOffset: stats.size, lastModified: stats.mtimeMs };\n } catch {\n // ignore\n }\n }\n }\n\n this.saveOffsets(state);\n }\n\n /** Main detection cycle */\n async detect() {\n try {\n const skillSlugs = this.getDeployedSkillSlugs();\n if (skillSlugs.size === 0) return; // No skills deployed\n\n const skillsDir = getSkillsDir();\n const files = this.getSessionFiles();\n const offsets = this.loadOffsets();\n const events: SkillUsageEvent[] = [];\n\n for (const file of files) {\n try {\n const stats = fs.statSync(file);\n const currentOffset = offsets[file]?.byteOffset ?? 0;\n const currentMtime = offsets[file]?.lastModified ?? 0;\n\n // Skip unchanged files\n if (stats.size <= currentOffset && stats.mtimeMs === currentMtime) continue;\n\n // Cap how much we read per cycle\n const startOffset = Math.max(currentOffset, stats.size - MAX_NEW_BYTES_PER_FILE);\n\n if (stats.size <= startOffset) {\n offsets[file] = { byteOffset: stats.size, lastModified: stats.mtimeMs };\n continue;\n }\n\n // Read new content\n const stream = fs.createReadStream(file, {\n start: startOffset,\n encoding: \"utf-8\",\n });\n\n const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });\n\n for await (const line of rl) {\n if (!line.trim()) continue;\n\n try {\n const entry = JSON.parse(line);\n const msg = entry?.message;\n if (!msg || msg.role !== \"assistant\") continue;\n\n const content = msg.content;\n if (!Array.isArray(content)) continue;\n\n const sessionId = entry.sessionId || \"\";\n const timestamp = entry.timestamp || new Date().toISOString();\n const cwd = entry.cwd || \"\";\n\n for (const block of content) {\n if (block?.type !== \"tool_use\") continue;\n\n const toolName = block.name;\n const toolInput = block.input || {};\n\n // Signal 1: Read calls to skill files\n if (toolName === \"Read\") {\n const filePath = toolInput.file_path || \"\";\n // Match paths like ~/.claude/skills/{slug}/SKILL.md or full path\n if (filePath.includes(\"/skills/\") && filePath.endsWith(\"/SKILL.md\")) {\n // Extract slug from path\n const parts = filePath.split(\"/skills/\");\n if (parts.length >= 2) {\n const afterSkills = parts[parts.length - 1]; // \"my-skill/SKILL.md\"\n const slug = afterSkills.split(\"/\")[0];\n if (slug && skillSlugs.has(slug)) {\n events.push({\n skillSlug: slug,\n claudeSessionId: sessionId,\n projectPath: cwd,\n timestamp,\n });\n }\n }\n }\n }\n\n // Signal 2: Bash commands matching skill commands\n // (skipped for v1 — Read detection is sufficient)\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n offsets[file] = { byteOffset: stats.size, lastModified: stats.mtimeMs };\n } catch {\n // Skip inaccessible files\n }\n }\n\n // Clean up offsets for deleted files\n for (const key of Object.keys(offsets)) {\n if (!fs.existsSync(key)) {\n delete offsets[key];\n }\n }\n\n this.saveOffsets(offsets);\n\n // Deduplicate: same slug+session within batch\n const seen = new Set<string>();\n const uniqueEvents = events.filter((e) => {\n const key = `${e.skillSlug}:${e.claudeSessionId}:${e.timestamp}`;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n\n // Report to server\n if (uniqueEvents.length > 0) {\n this.log(\"INFO\", `Detected ${uniqueEvents.length} skill usage(s), reporting...`);\n\n const response = await this.client.reportSkillUsage(uniqueEvents);\n if (response.success) {\n this.log(\"INFO\", `Reported ${response.data?.logged ?? 0} skill usage(s)`);\n this.callbacks.onDetection?.(response.data?.logged ?? 0);\n } else {\n this.log(\"ERROR\", `Report failed: ${response.error}`);\n }\n }\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n this.log(\"ERROR\", `Detection error: ${err.message}`);\n this.callbacks.onError?.(err);\n }\n }\n}\n"],"mappings":"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","names":[]}