skillo 0.2.5 → 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 (67) 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 +499 -1807
  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 +352 -65
  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-YOL4R2RH.js → tray-WKFGUUTO.js} +117 -179
  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-YOL4R2RH.js.map +0 -1
  64. /package/dist/{api-client-KUQW7FSC.js.map → api-client-BF6GDR7Q.js.map} +0 -0
  65. /package/dist/{config-P5EM5L7N.js.map → config-ZOKAP2LJ.js.map} +0 -0
  66. /package/dist/{database-F3BFFZKG.js.map → daemon-6DTCMOJB.js.map} +0 -0
  67. /package/dist/{paths-INOKEM66.js.map → database-KQY5OSCS.js.map} +0 -0
@@ -5,8 +5,8 @@ import {
5
5
  loadConfig,
6
6
  saveConfig,
7
7
  setConfigValue
8
- } from "./chunk-CPL3P2OF.js";
9
- import "./chunk-WJKZWKER.js";
8
+ } from "./chunk-QUXHHRRK.js";
9
+ import "./chunk-63FVALWX.js";
10
10
  export {
11
11
  getConfigValue,
12
12
  getDefaultConfig,
@@ -14,4 +14,4 @@ export {
14
14
  saveConfig,
15
15
  setConfigValue
16
16
  };
17
- //# sourceMappingURL=config-P5EM5L7N.js.map
17
+ //# sourceMappingURL=config-ZOKAP2LJ.js.map
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ensureLoggedIn,
4
+ isDaemonRunning,
5
+ isTrayRunning,
6
+ logsCommand,
7
+ serviceCommand,
8
+ startCommand,
9
+ startDaemonProcess,
10
+ startTrayProcess,
11
+ stopCommand
12
+ } from "./chunk-XLJGCOVT.js";
13
+ import "./chunk-VAQ73XPE.js";
14
+ import "./chunk-6UGTWBUW.js";
15
+ import "./chunk-QUXHHRRK.js";
16
+ import "./chunk-63FVALWX.js";
17
+ export {
18
+ ensureLoggedIn,
19
+ isDaemonRunning,
20
+ isTrayRunning,
21
+ logsCommand,
22
+ serviceCommand,
23
+ startCommand,
24
+ startDaemonProcess,
25
+ startTrayProcess,
26
+ stopCommand
27
+ };
28
+ //# sourceMappingURL=daemon-6DTCMOJB.js.map
@@ -1,5 +1,5 @@
1
1
  // src/daemon-runner.ts
2
- import { existsSync as existsSync6, writeFileSync as writeFileSync4, unlinkSync, readFileSync as readFileSync4, readdirSync as readdirSync2, appendFileSync } from "fs";
2
+ import { existsSync as existsSync8, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2, appendFileSync } from "fs";
3
3
 
4
4
  // src/core/config.ts
5
5
  import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
@@ -33,11 +33,11 @@ function getSkillsDir() {
33
33
  function getClaudeDir() {
34
34
  return join(getHomeDir(), ".claude");
35
35
  }
36
- function ensureDirectory(path3) {
37
- if (existsSync(path3)) {
36
+ function ensureDirectory(path4) {
37
+ if (existsSync(path4)) {
38
38
  return false;
39
39
  }
40
- mkdirSync(path3, { recursive: true });
40
+ mkdirSync(path4, { recursive: true });
41
41
  return true;
42
42
  }
43
43
  function getLogFile() {
@@ -138,8 +138,8 @@ function deepMerge(base, override) {
138
138
  }
139
139
  return result;
140
140
  }
141
- function loadConfig(path3) {
142
- const configPath = path3 || getConfigFile();
141
+ function loadConfig(path4) {
142
+ const configPath = path4 || getConfigFile();
143
143
  if (!existsSync2(configPath)) {
144
144
  return getDefaultConfig();
145
145
  }
@@ -153,8 +153,8 @@ function loadConfig(path3) {
153
153
  return getDefaultConfig();
154
154
  }
155
155
  }
156
- function saveConfig(config, path3) {
157
- const configPath = path3 || getConfigFile();
156
+ function saveConfig(config, path4) {
157
+ const configPath = path4 || getConfigFile();
158
158
  ensureDirectory(dirname(configPath));
159
159
  const converted = convertKeysToSnakeCase(config);
160
160
  const content = YAML.stringify(converted, {
@@ -257,6 +257,20 @@ var ApiClient = class {
257
257
  saveConfig(config);
258
258
  this.baseUrl = url;
259
259
  }
260
+ /**
261
+ * Safely parse JSON from a fetch response, returning an error ApiResponse on failure.
262
+ */
263
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
264
+ async parseJsonResponse(response) {
265
+ try {
266
+ return { data: await response.json() };
267
+ } catch {
268
+ return {
269
+ success: false,
270
+ error: `Server returned non-JSON response (status ${response.status})`
271
+ };
272
+ }
273
+ }
260
274
  /**
261
275
  * Make an authenticated API request
262
276
  */
@@ -275,7 +289,9 @@ var ApiClient = class {
275
289
  ...options,
276
290
  headers
277
291
  });
278
- const data = await response.json();
292
+ const parsed = await this.parseJsonResponse(response);
293
+ if ("success" in parsed) return parsed;
294
+ const { data } = parsed;
279
295
  if (process.env.SKILLO_DEBUG) {
280
296
  console.log("[DEBUG] Request:", url);
281
297
  console.log("[DEBUG] Status:", response.status);
@@ -396,7 +412,9 @@ var ApiClient = class {
396
412
  headers: { "Content-Type": "application/json" },
397
413
  body: JSON.stringify({ device_name: deviceName })
398
414
  });
399
- const data = await response.json();
415
+ const parsed = await this.parseJsonResponse(response);
416
+ if ("success" in parsed) return parsed;
417
+ const { data } = parsed;
400
418
  if (!response.ok) {
401
419
  return {
402
420
  success: false,
@@ -424,7 +442,9 @@ var ApiClient = class {
424
442
  const url = `${this.baseUrl}/token?code=${encodeURIComponent(code)}`;
425
443
  try {
426
444
  const response = await fetch(url, { method: "GET" });
427
- const data = await response.json();
445
+ const parsed = await this.parseJsonResponse(response);
446
+ if ("success" in parsed) return parsed;
447
+ const { data } = parsed;
428
448
  if (!response.ok) {
429
449
  return {
430
450
  success: false,
@@ -450,7 +470,9 @@ var ApiClient = class {
450
470
  headers: { "Content-Type": "application/json" },
451
471
  body: JSON.stringify({ code, device_name: deviceName })
452
472
  });
453
- const data = await response.json();
473
+ const parsed = await this.parseJsonResponse(response);
474
+ if ("success" in parsed) return parsed;
475
+ const { data } = parsed;
454
476
  if (!response.ok) {
455
477
  return {
456
478
  success: false,
@@ -494,6 +516,42 @@ var ApiClient = class {
494
516
  });
495
517
  }
496
518
  // ============================================================================
519
+ // SKILL INSTALLATION
520
+ // ============================================================================
521
+ /**
522
+ * Get pending skill installs/uninstalls (daemon polling)
523
+ */
524
+ async getPendingInstalls() {
525
+ return this.request("/skills/pending-installs", { method: "GET" });
526
+ }
527
+ /**
528
+ * Report install/uninstall completion (daemon)
529
+ */
530
+ async reportInstallComplete(subscriptionId, action) {
531
+ return this.request("/skills/install-complete", {
532
+ method: "POST",
533
+ body: JSON.stringify({ subscriptionId, action })
534
+ });
535
+ }
536
+ /**
537
+ * Install a skill by slug (CLI direct install)
538
+ */
539
+ async installSkill(slug) {
540
+ return this.request("/skills/install", {
541
+ method: "POST",
542
+ body: JSON.stringify({ slug })
543
+ });
544
+ }
545
+ /**
546
+ * Uninstall a skill by slug (CLI)
547
+ */
548
+ async uninstallSkill(slug) {
549
+ return this.request("/skills/uninstall", {
550
+ method: "POST",
551
+ body: JSON.stringify({ slug })
552
+ });
553
+ }
554
+ // ============================================================================
497
555
  // PROJECT TRACKING
498
556
  // ============================================================================
499
557
  /**
@@ -508,16 +566,20 @@ var ApiClient = class {
508
566
  /**
509
567
  * Disconnect a project from tracking
510
568
  */
511
- async disconnectProject(path3) {
512
- return this.request(`/projects/connect?path=${encodeURIComponent(path3)}`, {
569
+ async disconnectProject(path4, gitRemoteNormalized) {
570
+ const params = new URLSearchParams({ path: path4 });
571
+ if (gitRemoteNormalized) params.set("gitRemoteNormalized", gitRemoteNormalized);
572
+ return this.request(`/projects/connect?${params.toString()}`, {
513
573
  method: "DELETE"
514
574
  });
515
575
  }
516
576
  /**
517
577
  * Get tracking status for a project
518
578
  */
519
- async getProjectStatus(path3) {
520
- return this.request(`/projects/connect?path=${encodeURIComponent(path3)}`, {
579
+ async getProjectStatus(path4, gitRemoteNormalized) {
580
+ const params = new URLSearchParams({ path: path4 });
581
+ if (gitRemoteNormalized) params.set("gitRemoteNormalized", gitRemoteNormalized);
582
+ return this.request(`/projects/connect?${params.toString()}`, {
521
583
  method: "GET"
522
584
  });
523
585
  }
@@ -533,8 +595,8 @@ var ApiClient = class {
533
595
  * Check if a path is in a tracked project
534
596
  * Returns the project if tracked, null if not
535
597
  */
536
- async isProjectTracked(path3) {
537
- const result = await this.getProjectStatus(path3);
598
+ async isProjectTracked(path4) {
599
+ const result = await this.getProjectStatus(path4);
538
600
  if (result.success && result.data?.connected) {
539
601
  return {
540
602
  tracked: true,
@@ -574,6 +636,7 @@ function matchesProjectPath(claudeProjectPath, filterPath) {
574
636
  }
575
637
  var projectCache = {
576
638
  projects: /* @__PURE__ */ new Map(),
639
+ gitRemoteIndex: /* @__PURE__ */ new Map(),
577
640
  lastFetched: 0
578
641
  };
579
642
  var CACHE_TTL_MS = 6e4;
@@ -586,12 +649,17 @@ async function loadTrackedProjects(client) {
586
649
  const result = await client.listProjects(true);
587
650
  if (result.success && result.data?.projects) {
588
651
  projectCache.projects.clear();
652
+ projectCache.gitRemoteIndex.clear();
589
653
  for (const project of result.data.projects) {
590
654
  const normalizedPath = normalizePath(project.path);
591
- projectCache.projects.set(normalizedPath, {
655
+ const info = {
592
656
  tracked: project.trackingEnabled,
593
657
  name: project.name
594
- });
658
+ };
659
+ projectCache.projects.set(normalizedPath, info);
660
+ if (project.gitRemoteNormalized) {
661
+ projectCache.gitRemoteIndex.set(project.gitRemoteNormalized, info);
662
+ }
595
663
  }
596
664
  projectCache.lastFetched = now;
597
665
  }
@@ -612,6 +680,20 @@ async function isProjectTracked(projectPath) {
612
680
  return info;
613
681
  }
614
682
  }
683
+ if (projectCache.gitRemoteIndex.size > 0) {
684
+ try {
685
+ const { getGitInfo } = await import("./git-OUAHIOY2.js");
686
+ const gitInfo = getGitInfo(projectPath);
687
+ if (gitInfo.remoteNormalized) {
688
+ const remoteMatch = projectCache.gitRemoteIndex.get(gitInfo.remoteNormalized);
689
+ if (remoteMatch) return remoteMatch;
690
+ }
691
+ } catch (error) {
692
+ if (process.env.DEBUG) {
693
+ console.error("[skillo] git detection failed:", error);
694
+ }
695
+ }
696
+ }
615
697
  return { tracked: false };
616
698
  }
617
699
  var ClaudeWatcher = class {
@@ -697,6 +779,8 @@ var ClaudeWatcher = class {
697
779
  if (!trackingStatus.tracked) {
698
780
  continue;
699
781
  }
782
+ entry.toolsUsed = extractToolsUsed(entry.display);
783
+ entry.filesReferenced = extractFilesReferenced(entry.display);
700
784
  newPrompts.push(entry);
701
785
  } catch {
702
786
  }
@@ -724,6 +808,46 @@ var ClaudeWatcher = class {
724
808
  }
725
809
  }
726
810
  };
811
+ var TOOL_PATTERNS = [
812
+ /\b(grep|rg|ripgrep)\b/i,
813
+ /\b(git)\s+(commit|push|pull|merge|rebase|checkout|branch|stash|log|diff|add|reset)\b/i,
814
+ /\b(npm|yarn|pnpm|bun)\s+(install|run|build|test|dev|start|publish)\b/i,
815
+ /\b(docker|docker-compose|podman)\s+\w+/i,
816
+ /\b(curl|wget|fetch)\b/i,
817
+ /\b(vim|nvim|nano|code|emacs)\b/i,
818
+ /\b(make|cmake|cargo|go\s+build|rustc|gcc|clang)\b/i,
819
+ /\b(pytest|jest|vitest|mocha|cypress|playwright)\b/i,
820
+ /\b(prisma|drizzle|sequelize|typeorm)\s+\w+/i,
821
+ /\b(kubectl|helm|terraform|ansible)\s+\w+/i
822
+ ];
823
+ function extractToolsUsed(text) {
824
+ const tools = /* @__PURE__ */ new Set();
825
+ for (const pattern of TOOL_PATTERNS) {
826
+ const match = text.match(pattern);
827
+ if (match) {
828
+ tools.add(match[0].trim().toLowerCase());
829
+ }
830
+ }
831
+ return Array.from(tools);
832
+ }
833
+ var FILE_PATTERN = /(?:^|\s|['"`])((?:\.{0,2}\/)?[\w-]+\/[\w./-]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|yaml|yml|toml|md|css|scss|html|sql|py|rs|go|java|rb|sh|prisma|graphql))\b/gi;
834
+ var CONFIG_FILE_PATTERN = /(?:^|\s|['"`])((?:package\.json|tsconfig\.json|\.env|Dockerfile|Makefile|docker-compose\.ya?ml|\.gitignore|prisma\/schema\.prisma))\b/gi;
835
+ function extractFilesReferenced(text) {
836
+ const files = /* @__PURE__ */ new Set();
837
+ let match;
838
+ while ((match = FILE_PATTERN.exec(text)) !== null) {
839
+ const filePath = match[1];
840
+ if (filePath.length < 200 && !filePath.includes("..")) {
841
+ files.add(filePath);
842
+ }
843
+ }
844
+ FILE_PATTERN.lastIndex = 0;
845
+ while ((match = CONFIG_FILE_PATTERN.exec(text)) !== null) {
846
+ files.add(match[1]);
847
+ }
848
+ CONFIG_FILE_PATTERN.lastIndex = 0;
849
+ return Array.from(files);
850
+ }
727
851
 
728
852
  // src/core/skill-usage-detector.ts
729
853
  import * as fs2 from "fs";
@@ -931,9 +1055,122 @@ var SkillUsageDetector = class {
931
1055
  }
932
1056
  };
933
1057
 
1058
+ // src/core/skill-installer.ts
1059
+ import * as fs3 from "fs";
1060
+ import * as path3 from "path";
1061
+ var SkillInstaller = class {
1062
+ intervalId = null;
1063
+ intervalMs;
1064
+ callbacks;
1065
+ client;
1066
+ constructor(client, options = {}) {
1067
+ this.client = client;
1068
+ this.intervalMs = options.intervalMs || 3e4;
1069
+ this.callbacks = options.callbacks || {};
1070
+ }
1071
+ log(level, msg) {
1072
+ this.callbacks.log?.(level, msg);
1073
+ }
1074
+ async start() {
1075
+ this.log("INFO", "Skill installer started");
1076
+ await this.poll();
1077
+ this.intervalId = setInterval(() => this.poll(), this.intervalMs);
1078
+ }
1079
+ stop() {
1080
+ if (this.intervalId) {
1081
+ clearInterval(this.intervalId);
1082
+ this.intervalId = null;
1083
+ }
1084
+ }
1085
+ async poll() {
1086
+ try {
1087
+ const response = await this.client.getPendingInstalls();
1088
+ if (!response.success || !response.data) {
1089
+ if (response.error) {
1090
+ this.log("WARN", `Failed to fetch pending installs: ${response.error}`);
1091
+ }
1092
+ return;
1093
+ }
1094
+ const { pendingInstalls, pendingUninstalls, personalSkills } = response.data;
1095
+ if (personalSkills?.length) {
1096
+ for (const skill of personalSkills) {
1097
+ try {
1098
+ const existing = this.readSkill(skill.slug);
1099
+ if (existing === skill.content) continue;
1100
+ this.writeSkill(skill.slug, skill.content);
1101
+ this.log("INFO", `Installed personal skill: ${skill.slug}`);
1102
+ this.callbacks.onInstall?.(skill.slug);
1103
+ } catch (err) {
1104
+ const error = err instanceof Error ? err : new Error(String(err));
1105
+ this.log("ERROR", `Failed to install personal skill ${skill.slug}: ${error.message}`);
1106
+ this.callbacks.onError?.(error);
1107
+ if (error.message.includes("ENOSPC") || error.message.includes("EACCES")) {
1108
+ this.log("ERROR", "Filesystem error detected, stopping personal skill installation");
1109
+ break;
1110
+ }
1111
+ }
1112
+ }
1113
+ }
1114
+ for (const install of pendingInstalls) {
1115
+ try {
1116
+ this.writeSkill(install.slug, install.content);
1117
+ this.log("INFO", `Installed skill: ${install.slug}`);
1118
+ this.callbacks.onInstall?.(install.slug);
1119
+ await this.client.reportInstallComplete(install.subscriptionId, "installed");
1120
+ } catch (err) {
1121
+ const error = err instanceof Error ? err : new Error(String(err));
1122
+ this.log("ERROR", `Failed to install skill ${install.slug}: ${error.message}`);
1123
+ this.callbacks.onError?.(error);
1124
+ }
1125
+ }
1126
+ for (const uninstall of pendingUninstalls) {
1127
+ try {
1128
+ this.removeSkill(uninstall.slug);
1129
+ this.log("INFO", `Uninstalled skill: ${uninstall.slug}`);
1130
+ this.callbacks.onUninstall?.(uninstall.slug);
1131
+ await this.client.reportInstallComplete(uninstall.subscriptionId, "uninstalled");
1132
+ } catch (err) {
1133
+ const error = err instanceof Error ? err : new Error(String(err));
1134
+ this.log("ERROR", `Failed to uninstall skill ${uninstall.slug}: ${error.message}`);
1135
+ this.callbacks.onError?.(error);
1136
+ }
1137
+ }
1138
+ } catch (error) {
1139
+ const err = error instanceof Error ? error : new Error(String(error));
1140
+ this.log("ERROR", `Skill installer poll error: ${err.message}`);
1141
+ this.callbacks.onError?.(err);
1142
+ }
1143
+ }
1144
+ writeSkill(slug, content) {
1145
+ const skillsDir = getSkillsDir();
1146
+ const skillDir = path3.join(skillsDir, slug);
1147
+ if (!fs3.existsSync(skillsDir)) {
1148
+ fs3.mkdirSync(skillsDir, { recursive: true });
1149
+ }
1150
+ if (!fs3.existsSync(skillDir)) {
1151
+ fs3.mkdirSync(skillDir, { recursive: true });
1152
+ }
1153
+ fs3.writeFileSync(path3.join(skillDir, "SKILL.md"), content, "utf-8");
1154
+ }
1155
+ readSkill(slug) {
1156
+ const skillFile = path3.join(getSkillsDir(), slug, "SKILL.md");
1157
+ try {
1158
+ return fs3.existsSync(skillFile) ? fs3.readFileSync(skillFile, "utf-8") : null;
1159
+ } catch {
1160
+ return null;
1161
+ }
1162
+ }
1163
+ removeSkill(slug) {
1164
+ const skillDir = path3.join(getSkillsDir(), slug);
1165
+ if (fs3.existsSync(skillDir)) {
1166
+ fs3.rmSync(skillDir, { recursive: true, force: true });
1167
+ }
1168
+ }
1169
+ };
1170
+
934
1171
  // src/utils/status-writer.ts
935
- import { writeFileSync as writeFileSync3, readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
936
- import { join as join4 } from "path";
1172
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
1173
+ import { join as join5 } from "path";
937
1174
  var STATUS_FILE = "daemon-status.json";
938
1175
  var WRITE_INTERVAL_MS = 1e4;
939
1176
  var StatusWriter = class _StatusWriter {
@@ -941,7 +1178,7 @@ var StatusWriter = class _StatusWriter {
941
1178
  status;
942
1179
  filePath;
943
1180
  constructor() {
944
- this.filePath = join4(getDataDir(), STATUS_FILE);
1181
+ this.filePath = join5(getDataDir(), STATUS_FILE);
945
1182
  this.status = {
946
1183
  running: true,
947
1184
  pid: process.pid,
@@ -980,18 +1217,22 @@ var StatusWriter = class _StatusWriter {
980
1217
  this.status.skillDetector = { ...this.status.skillDetector, ...partial.skillDetector };
981
1218
  delete partial.skillDetector;
982
1219
  }
1220
+ if (partial.skillInstaller) {
1221
+ this.status.skillInstaller = { ...this.status.skillInstaller, ...partial.skillInstaller };
1222
+ delete partial.skillInstaller;
1223
+ }
983
1224
  Object.assign(this.status, partial);
984
1225
  }
985
1226
  /** Get the status file path */
986
1227
  static getStatusFilePath() {
987
- return join4(getDataDir(), STATUS_FILE);
1228
+ return join5(getDataDir(), STATUS_FILE);
988
1229
  }
989
1230
  /** Read current status from disk (static, for tray/status commands) */
990
1231
  static read() {
991
1232
  const filePath = _StatusWriter.getStatusFilePath();
992
1233
  try {
993
- if (existsSync5(filePath)) {
994
- return JSON.parse(readFileSync3(filePath, "utf-8"));
1234
+ if (existsSync6(filePath)) {
1235
+ return JSON.parse(readFileSync4(filePath, "utf-8"));
995
1236
  }
996
1237
  } catch {
997
1238
  }
@@ -1000,39 +1241,32 @@ var StatusWriter = class _StatusWriter {
1000
1241
  write() {
1001
1242
  this.status.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1002
1243
  try {
1003
- writeFileSync3(this.filePath, JSON.stringify(this.status, null, 2), "utf-8");
1244
+ writeFileSync4(this.filePath, JSON.stringify(this.status, null, 2), "utf-8");
1004
1245
  } catch {
1005
1246
  }
1006
1247
  }
1007
1248
  };
1008
1249
 
1009
- // src/daemon-runner.ts
1010
- function log(level, msg) {
1011
- const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] ${msg}
1012
- `;
1013
- try {
1014
- appendFileSync(getLogFile(), line);
1015
- } catch {
1016
- }
1017
- }
1018
- async function updateTrackedProjectsCache(client) {
1250
+ // src/utils/daemon-tasks.ts
1251
+ import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, unlinkSync, readdirSync as readdirSync2 } from "fs";
1252
+ async function updateTrackedProjectsCache(client, log2) {
1019
1253
  try {
1020
1254
  const result = await client.listProjects(true);
1021
1255
  if (result.success && result.data?.projects) {
1022
- const cacheData = {
1023
- updatedAt: Date.now(),
1024
- projects: result.data.projects.filter((p) => p.trackingEnabled).map((p) => ({ path: p.path, name: p.name }))
1025
- };
1026
- writeFileSync4(getTrackedProjectsCacheFile(), JSON.stringify(cacheData, null, 2));
1027
- log("DEBUG", `Updated tracked projects cache: ${cacheData.projects.length} project(s)`);
1256
+ const projects = result.data.projects.filter((p) => p.trackingEnabled).map((p) => ({ path: p.path, name: p.name, gitRemoteNormalized: p.gitRemoteNormalized }));
1257
+ const cacheData = { updatedAt: Date.now(), projects };
1258
+ writeFileSync5(getTrackedProjectsCacheFile(), JSON.stringify(cacheData, null, 2));
1259
+ log2("DEBUG", `Updated tracked projects cache: ${projects.length} project(s)`);
1260
+ return projects;
1028
1261
  }
1029
1262
  } catch (error) {
1030
- log("ERROR", `Failed to update tracked projects cache: ${error}`);
1263
+ log2("ERROR", `Failed to update tracked projects cache: ${error}`);
1031
1264
  }
1265
+ return null;
1032
1266
  }
1033
- async function cleanupStaleSessions(client) {
1267
+ async function cleanupStaleSessions(client, log2) {
1034
1268
  const sessionsDir = getActiveSessionsDir();
1035
- if (!existsSync6(sessionsDir)) return;
1269
+ if (!existsSync7(sessionsDir)) return;
1036
1270
  try {
1037
1271
  const files = readdirSync2(sessionsDir);
1038
1272
  for (const file of files) {
@@ -1047,10 +1281,10 @@ async function cleanupStaleSessions(client) {
1047
1281
  }
1048
1282
  if (!isRunning) {
1049
1283
  try {
1050
- const sessionData = JSON.parse(readFileSync4(`${sessionsDir}/${file}`, "utf-8"));
1284
+ const sessionData = JSON.parse(readFileSync5(`${sessionsDir}/${file}`, "utf-8"));
1051
1285
  if (sessionData.sessionId) {
1052
1286
  await client.endSession(sessionData.sessionId);
1053
- log("INFO", `Ended stale session ${sessionData.sessionId.slice(0, 8)} (PID ${pid} dead)`);
1287
+ log2("INFO", `Ended stale session ${sessionData.sessionId.slice(0, 8)} (PID ${pid} dead)`);
1054
1288
  }
1055
1289
  unlinkSync(`${sessionsDir}/${file}`);
1056
1290
  } catch {
@@ -1062,20 +1296,38 @@ async function cleanupStaleSessions(client) {
1062
1296
  }
1063
1297
  }
1064
1298
  } catch (error) {
1065
- log("ERROR", `Stale session cleanup error: ${error}`);
1299
+ log2("ERROR", `Stale session cleanup error: ${error}`);
1300
+ }
1301
+ }
1302
+ function readTrackedProjectsFromCache() {
1303
+ try {
1304
+ const cacheData = JSON.parse(readFileSync5(getTrackedProjectsCacheFile(), "utf-8"));
1305
+ return cacheData.projects || [];
1306
+ } catch {
1307
+ return null;
1308
+ }
1309
+ }
1310
+
1311
+ // src/daemon-runner.ts
1312
+ function log(level, msg) {
1313
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] ${msg}
1314
+ `;
1315
+ try {
1316
+ appendFileSync(getLogFile(), line);
1317
+ } catch {
1066
1318
  }
1067
1319
  }
1068
1320
  async function main() {
1069
1321
  const pidFile = getPidFile();
1070
- writeFileSync4(pidFile, String(process.pid));
1322
+ writeFileSync6(pidFile, String(process.pid));
1071
1323
  log("INFO", `Daemon starting (PID: ${process.pid})`);
1072
1324
  const statusWriter = new StatusWriter();
1073
1325
  const cleanup = () => {
1074
1326
  log("INFO", "Daemon shutting down");
1075
1327
  statusWriter.stop();
1076
- if (existsSync6(pidFile)) {
1328
+ if (existsSync8(pidFile)) {
1077
1329
  try {
1078
- unlinkSync(pidFile);
1330
+ unlinkSync2(pidFile);
1079
1331
  } catch {
1080
1332
  }
1081
1333
  }
@@ -1096,15 +1348,37 @@ async function main() {
1096
1348
  cleanup();
1097
1349
  return;
1098
1350
  }
1351
+ try {
1352
+ const authResult = await client.authenticate();
1353
+ const user = authResult.data?.user || authResult.user;
1354
+ if (authResult.success && user) {
1355
+ statusWriter.update({ user: user.name });
1356
+ log("INFO", `Authenticated as: ${user.name}`);
1357
+ }
1358
+ } catch (err) {
1359
+ log("WARN", `Could not fetch user info: ${err}`);
1360
+ }
1099
1361
  ensureDirectory(getActiveSessionsDir());
1100
- await updateTrackedProjectsCache(client);
1101
- await cleanupStaleSessions(client);
1362
+ const projects = await updateTrackedProjectsCache(client, log);
1363
+ if (projects) {
1364
+ statusWriter.update({ trackedProjects: projects });
1365
+ } else {
1366
+ const cached = readTrackedProjectsFromCache();
1367
+ if (cached) statusWriter.update({ trackedProjects: cached });
1368
+ }
1369
+ await cleanupStaleSessions(client, log);
1102
1370
  const watchInterval = (config.daemon?.conversationCheckInterval || 5) * 1e3;
1103
1371
  const watcher = new ClaudeWatcher(client, {
1104
1372
  intervalMs: watchInterval,
1105
1373
  callbacks: {
1106
- onSync: (count) => log("INFO", `Synced ${count} Claude prompt(s)`),
1107
- onError: (err) => log("ERROR", `Watcher error: ${err.message}`),
1374
+ onSync: (count) => {
1375
+ log("INFO", `Synced ${count} Claude prompt(s)`);
1376
+ statusWriter.update({ claudeWatcher: { promptsSynced: count, lastSync: (/* @__PURE__ */ new Date()).toISOString(), lastError: null } });
1377
+ },
1378
+ onError: (err) => {
1379
+ log("ERROR", `Watcher error: ${err.message}`);
1380
+ statusWriter.update({ claudeWatcher: { lastError: err.message } });
1381
+ },
1108
1382
  log: (level, msg) => log(level, msg)
1109
1383
  }
1110
1384
  });
@@ -1124,18 +1398,31 @@ async function main() {
1124
1398
  }
1125
1399
  });
1126
1400
  await skillDetector.start();
1401
+ const skillInstaller = new SkillInstaller(client, {
1402
+ intervalMs: 3e4,
1403
+ callbacks: {
1404
+ onInstall: (slug) => {
1405
+ log("INFO", `Installed skill: ${slug}`);
1406
+ statusWriter.update({ skillInstaller: { lastInstall: slug, lastAction: (/* @__PURE__ */ new Date()).toISOString(), lastError: null } });
1407
+ },
1408
+ onUninstall: (slug) => {
1409
+ log("INFO", `Uninstalled skill: ${slug}`);
1410
+ statusWriter.update({ skillInstaller: { lastUninstall: slug, lastAction: (/* @__PURE__ */ new Date()).toISOString(), lastError: null } });
1411
+ },
1412
+ onError: (err) => {
1413
+ log("ERROR", `Skill installer error: ${err.message}`);
1414
+ statusWriter.update({ skillInstaller: { lastError: err.message } });
1415
+ },
1416
+ log: (level, msg) => log(level, msg)
1417
+ }
1418
+ });
1419
+ await skillInstaller.start();
1127
1420
  statusWriter.start();
1128
1421
  setInterval(async () => {
1129
- await updateTrackedProjectsCache(client);
1130
- try {
1131
- const cacheData = JSON.parse(readFileSync4(getTrackedProjectsCacheFile(), "utf-8"));
1132
- statusWriter.update({
1133
- trackedProjects: cacheData.projects || []
1134
- });
1135
- } catch {
1136
- }
1422
+ const updated = await updateTrackedProjectsCache(client, log);
1423
+ if (updated) statusWriter.update({ trackedProjects: updated });
1137
1424
  }, 6e4);
1138
- setInterval(() => cleanupStaleSessions(client), 3e5);
1425
+ setInterval(() => cleanupStaleSessions(client, log), 3e5);
1139
1426
  log("INFO", "Daemon started successfully \u2014 watching Claude conversations & skill usage");
1140
1427
  await new Promise(() => {
1141
1428
  });