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.
- package/README.md +198 -198
- package/dist/api-client-BF6GDR7Q.js +12 -0
- package/dist/{chunk-WJKZWKER.js → chunk-63FVALWX.js} +1 -1
- package/dist/chunk-63FVALWX.js.map +1 -0
- package/dist/chunk-6GOJPFZ7.js +113 -0
- package/dist/chunk-6GOJPFZ7.js.map +1 -0
- package/dist/chunk-6UGTWBUW.js +89 -0
- package/dist/chunk-6UGTWBUW.js.map +1 -0
- package/dist/{chunk-2CVEPT6U.js → chunk-73NUWYUO.js} +2 -2
- package/dist/chunk-73NUWYUO.js.map +1 -0
- package/dist/chunk-QIV4VIXA.js +221 -0
- package/dist/chunk-QIV4VIXA.js.map +1 -0
- package/dist/{chunk-CPL3P2OF.js → chunk-QUXHHRRK.js} +2 -2
- package/dist/chunk-QUXHHRRK.js.map +1 -0
- package/dist/{chunk-ODOZM4QV.js → chunk-RQ2SC5HW.js} +72 -10
- package/dist/chunk-RQ2SC5HW.js.map +1 -0
- package/dist/chunk-U53QWUOR.js +727 -0
- package/dist/chunk-U53QWUOR.js.map +1 -0
- package/dist/chunk-VAQ73XPE.js +68 -0
- package/dist/chunk-VAQ73XPE.js.map +1 -0
- package/dist/chunk-XLJGCOVT.js +975 -0
- package/dist/chunk-XLJGCOVT.js.map +1 -0
- package/dist/{claude-watcher-N6GN6WHJ.js → claude-watcher-WKGBJYKN.js} +65 -3
- package/dist/claude-watcher-WKGBJYKN.js.map +1 -0
- package/dist/cli.js +499 -1807
- package/dist/cli.js.map +1 -1
- package/dist/{config-P5EM5L7N.js → config-ZOKAP2LJ.js} +3 -3
- package/dist/daemon-6DTCMOJB.js +28 -0
- package/dist/daemon-runner.js +352 -65
- package/dist/daemon-runner.js.map +1 -1
- package/dist/database-KQY5OSCS.js +9 -0
- package/dist/git-OGUSYBJS.js +16 -0
- package/dist/git-OGUSYBJS.js.map +1 -0
- package/dist/git-OUAHIOY2.js +110 -0
- package/dist/git-OUAHIOY2.js.map +1 -0
- package/dist/index.js.map +1 -1
- package/dist/{paths-INOKEM66.js → paths-MPOZBOKE.js} +2 -2
- package/dist/paths-MPOZBOKE.js.map +1 -0
- package/dist/project-OFU2W6MH.js +19 -0
- package/dist/project-OFU2W6MH.js.map +1 -0
- package/dist/shell-NZABRJLA.js +16 -0
- package/dist/shell-NZABRJLA.js.map +1 -0
- package/dist/skill-installer-F67OAOQN.js +121 -0
- package/dist/skill-installer-F67OAOQN.js.map +1 -0
- package/dist/{skill-usage-detector-EO26MRYV.js → skill-usage-detector-MSW5VWQZ.js} +2 -2
- package/dist/skill-usage-detector-MSW5VWQZ.js.map +1 -0
- package/dist/{tray-YOL4R2RH.js → tray-WKFGUUTO.js} +117 -179
- package/dist/tray-WKFGUUTO.js.map +1 -0
- package/package.json +62 -63
- package/scripts/postinstall.mjs +415 -364
- package/scripts/tray-helper-darwin +0 -0
- package/scripts/tray-helper-darwin.swift +180 -180
- package/scripts/tray-helper-linux.py +322 -0
- package/scripts/tray-helper-windows.cs +322 -0
- package/dist/api-client-KUQW7FSC.js +0 -12
- package/dist/chunk-2CVEPT6U.js.map +0 -1
- package/dist/chunk-CPL3P2OF.js.map +0 -1
- package/dist/chunk-ODOZM4QV.js.map +0 -1
- package/dist/chunk-WJKZWKER.js.map +0 -1
- package/dist/claude-watcher-N6GN6WHJ.js.map +0 -1
- package/dist/database-F3BFFZKG.js +0 -9
- package/dist/skill-usage-detector-EO26MRYV.js.map +0 -1
- package/dist/tray-YOL4R2RH.js.map +0 -1
- /package/dist/{api-client-KUQW7FSC.js.map → api-client-BF6GDR7Q.js.map} +0 -0
- /package/dist/{config-P5EM5L7N.js.map → config-ZOKAP2LJ.js.map} +0 -0
- /package/dist/{database-F3BFFZKG.js.map → daemon-6DTCMOJB.js.map} +0 -0
- /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-
|
|
9
|
-
import "./chunk-
|
|
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-
|
|
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
|
package/dist/daemon-runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/daemon-runner.ts
|
|
2
|
-
import { existsSync as
|
|
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(
|
|
37
|
-
if (existsSync(
|
|
36
|
+
function ensureDirectory(path4) {
|
|
37
|
+
if (existsSync(path4)) {
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
|
-
mkdirSync(
|
|
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(
|
|
142
|
-
const configPath =
|
|
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,
|
|
157
|
-
const configPath =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
512
|
-
|
|
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(
|
|
520
|
-
|
|
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(
|
|
537
|
-
const result = await this.getProjectStatus(
|
|
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
|
-
|
|
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
|
|
936
|
-
import { join as
|
|
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 =
|
|
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
|
|
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 (
|
|
994
|
-
return JSON.parse(
|
|
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
|
-
|
|
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-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
};
|
|
1026
|
-
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
1284
|
+
const sessionData = JSON.parse(readFileSync5(`${sessionsDir}/${file}`, "utf-8"));
|
|
1051
1285
|
if (sessionData.sessionId) {
|
|
1052
1286
|
await client.endSession(sessionData.sessionId);
|
|
1053
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1328
|
+
if (existsSync8(pidFile)) {
|
|
1077
1329
|
try {
|
|
1078
|
-
|
|
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
|
-
|
|
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) =>
|
|
1107
|
-
|
|
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
|
-
|
|
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
|
});
|