skillo 0.1.5 → 0.2.2
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/dist/{api-client-WO6NUCIJ.js → api-client-KUQW7FSC.js} +3 -2
- package/dist/chunk-2CVEPT6U.js +463 -0
- package/dist/chunk-2CVEPT6U.js.map +1 -0
- package/dist/chunk-CPL3P2OF.js +183 -0
- package/dist/chunk-CPL3P2OF.js.map +1 -0
- package/dist/{chunk-SYULMYPN.js → chunk-ODOZM4QV.js} +4 -178
- package/dist/chunk-ODOZM4QV.js.map +1 -0
- package/dist/cli.js +692 -522
- package/dist/cli.js.map +1 -1
- package/dist/config-P5EM5L7N.js +17 -0
- package/dist/config-P5EM5L7N.js.map +1 -0
- package/dist/daemon-runner.js +338 -22
- package/dist/daemon-runner.js.map +1 -1
- package/dist/database-F3BFFZKG.js +9 -0
- package/dist/database-F3BFFZKG.js.map +1 -0
- package/dist/tray-62I5KIFE.js +278 -0
- package/dist/tray-62I5KIFE.js.map +1 -0
- package/package.json +3 -1
- package/scripts/postinstall.mjs +65 -11
- package/dist/chunk-SYULMYPN.js.map +0 -1
- /package/dist/{api-client-WO6NUCIJ.js.map → api-client-KUQW7FSC.js.map} +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getConfigValue,
|
|
4
|
+
getDefaultConfig,
|
|
5
|
+
loadConfig,
|
|
6
|
+
saveConfig,
|
|
7
|
+
setConfigValue
|
|
8
|
+
} from "./chunk-CPL3P2OF.js";
|
|
9
|
+
import "./chunk-WJKZWKER.js";
|
|
10
|
+
export {
|
|
11
|
+
getConfigValue,
|
|
12
|
+
getDefaultConfig,
|
|
13
|
+
loadConfig,
|
|
14
|
+
saveConfig,
|
|
15
|
+
setConfigValue
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=config-P5EM5L7N.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/daemon-runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/daemon-runner.ts
|
|
2
|
-
import { existsSync as
|
|
2
|
+
import { existsSync as existsSync6, writeFileSync as writeFileSync4, unlinkSync, readFileSync as readFileSync4, readdirSync as readdirSync2, appendFileSync } from "fs";
|
|
3
3
|
|
|
4
4
|
// src/core/config.ts
|
|
5
5
|
import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
@@ -25,11 +25,19 @@ function getConfigDir() {
|
|
|
25
25
|
if (xdgConfig) return join(xdgConfig, "skillo");
|
|
26
26
|
return join(getHomeDir(), ".config", "skillo");
|
|
27
27
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
28
|
+
function getSkillsDir() {
|
|
29
|
+
const envPath = process.env.SKILLO_SKILLS_DIR;
|
|
30
|
+
if (envPath) return envPath;
|
|
31
|
+
return join(getClaudeDir(), "skills");
|
|
32
|
+
}
|
|
33
|
+
function getClaudeDir() {
|
|
34
|
+
return join(getHomeDir(), ".claude");
|
|
35
|
+
}
|
|
36
|
+
function ensureDirectory(path3) {
|
|
37
|
+
if (existsSync(path3)) {
|
|
30
38
|
return false;
|
|
31
39
|
}
|
|
32
|
-
mkdirSync(
|
|
40
|
+
mkdirSync(path3, { recursive: true });
|
|
33
41
|
return true;
|
|
34
42
|
}
|
|
35
43
|
function getLogFile() {
|
|
@@ -130,8 +138,8 @@ function deepMerge(base, override) {
|
|
|
130
138
|
}
|
|
131
139
|
return result;
|
|
132
140
|
}
|
|
133
|
-
function loadConfig(
|
|
134
|
-
const configPath =
|
|
141
|
+
function loadConfig(path3) {
|
|
142
|
+
const configPath = path3 || getConfigFile();
|
|
135
143
|
if (!existsSync2(configPath)) {
|
|
136
144
|
return getDefaultConfig();
|
|
137
145
|
}
|
|
@@ -145,8 +153,8 @@ function loadConfig(path2) {
|
|
|
145
153
|
return getDefaultConfig();
|
|
146
154
|
}
|
|
147
155
|
}
|
|
148
|
-
function saveConfig(config,
|
|
149
|
-
const configPath =
|
|
156
|
+
function saveConfig(config, path3) {
|
|
157
|
+
const configPath = path3 || getConfigFile();
|
|
150
158
|
ensureDirectory(dirname(configPath));
|
|
151
159
|
const converted = convertKeysToSnakeCase(config);
|
|
152
160
|
const content = YAML.stringify(converted, {
|
|
@@ -500,16 +508,16 @@ var ApiClient = class {
|
|
|
500
508
|
/**
|
|
501
509
|
* Disconnect a project from tracking
|
|
502
510
|
*/
|
|
503
|
-
async disconnectProject(
|
|
504
|
-
return this.request(`/projects/connect?path=${encodeURIComponent(
|
|
511
|
+
async disconnectProject(path3) {
|
|
512
|
+
return this.request(`/projects/connect?path=${encodeURIComponent(path3)}`, {
|
|
505
513
|
method: "DELETE"
|
|
506
514
|
});
|
|
507
515
|
}
|
|
508
516
|
/**
|
|
509
517
|
* Get tracking status for a project
|
|
510
518
|
*/
|
|
511
|
-
async getProjectStatus(
|
|
512
|
-
return this.request(`/projects/connect?path=${encodeURIComponent(
|
|
519
|
+
async getProjectStatus(path3) {
|
|
520
|
+
return this.request(`/projects/connect?path=${encodeURIComponent(path3)}`, {
|
|
513
521
|
method: "GET"
|
|
514
522
|
});
|
|
515
523
|
}
|
|
@@ -525,8 +533,8 @@ var ApiClient = class {
|
|
|
525
533
|
* Check if a path is in a tracked project
|
|
526
534
|
* Returns the project if tracked, null if not
|
|
527
535
|
*/
|
|
528
|
-
async isProjectTracked(
|
|
529
|
-
const result = await this.getProjectStatus(
|
|
536
|
+
async isProjectTracked(path3) {
|
|
537
|
+
const result = await this.getProjectStatus(path3);
|
|
530
538
|
if (result.success && result.data?.connected) {
|
|
531
539
|
return {
|
|
532
540
|
tracked: true,
|
|
@@ -717,6 +725,287 @@ var ClaudeWatcher = class {
|
|
|
717
725
|
}
|
|
718
726
|
};
|
|
719
727
|
|
|
728
|
+
// src/core/skill-usage-detector.ts
|
|
729
|
+
import * as fs2 from "fs";
|
|
730
|
+
import * as path2 from "path";
|
|
731
|
+
import * as readline2 from "readline";
|
|
732
|
+
var MAX_NEW_BYTES_PER_FILE = 2 * 1024 * 1024;
|
|
733
|
+
var OFFSETS_FILE = "skill-usage-offsets.json";
|
|
734
|
+
var SkillUsageDetector = class {
|
|
735
|
+
intervalId = null;
|
|
736
|
+
intervalMs;
|
|
737
|
+
callbacks;
|
|
738
|
+
client;
|
|
739
|
+
constructor(client, options = {}) {
|
|
740
|
+
this.client = client;
|
|
741
|
+
this.intervalMs = options.intervalMs || 3e4;
|
|
742
|
+
this.callbacks = options.callbacks || {};
|
|
743
|
+
}
|
|
744
|
+
log(level, msg) {
|
|
745
|
+
this.callbacks.log?.(level, msg);
|
|
746
|
+
}
|
|
747
|
+
async start() {
|
|
748
|
+
this.initOffsets();
|
|
749
|
+
this.log("INFO", "Skill usage detector started");
|
|
750
|
+
this.intervalId = setInterval(() => this.detect(), this.intervalMs);
|
|
751
|
+
}
|
|
752
|
+
stop() {
|
|
753
|
+
if (this.intervalId) {
|
|
754
|
+
clearInterval(this.intervalId);
|
|
755
|
+
this.intervalId = null;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
/** Build slug inventory from ~/.claude/skills/ */
|
|
759
|
+
getDeployedSkillSlugs() {
|
|
760
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
761
|
+
const skillsDir = getSkillsDir();
|
|
762
|
+
if (!fs2.existsSync(skillsDir)) return slugs;
|
|
763
|
+
try {
|
|
764
|
+
const entries = fs2.readdirSync(skillsDir, { withFileTypes: true });
|
|
765
|
+
for (const entry of entries) {
|
|
766
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
767
|
+
const skillFile = path2.join(skillsDir, entry.name, "SKILL.md");
|
|
768
|
+
if (fs2.existsSync(skillFile)) {
|
|
769
|
+
slugs.add(entry.name);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
} catch {
|
|
773
|
+
}
|
|
774
|
+
return slugs;
|
|
775
|
+
}
|
|
776
|
+
/** Encode project path to Claude's directory name format */
|
|
777
|
+
encodeProjectPath(projectPath) {
|
|
778
|
+
return projectPath.replace(/\//g, "-");
|
|
779
|
+
}
|
|
780
|
+
/** Find session JSONL files for tracked projects */
|
|
781
|
+
getSessionFiles() {
|
|
782
|
+
const claudeDir = getClaudeDir();
|
|
783
|
+
const projectsDir = path2.join(claudeDir, "projects");
|
|
784
|
+
if (!fs2.existsSync(projectsDir)) return [];
|
|
785
|
+
const files = [];
|
|
786
|
+
try {
|
|
787
|
+
const projectDirs = fs2.readdirSync(projectsDir, { withFileTypes: true });
|
|
788
|
+
for (const dir of projectDirs) {
|
|
789
|
+
if (!dir.isDirectory()) continue;
|
|
790
|
+
const dirPath = path2.join(projectsDir, dir.name);
|
|
791
|
+
const jsonlFiles = fs2.readdirSync(dirPath).filter(
|
|
792
|
+
(f) => f.endsWith(".jsonl") && !f.startsWith("agent-")
|
|
793
|
+
);
|
|
794
|
+
for (const f of jsonlFiles) {
|
|
795
|
+
files.push(path2.join(dirPath, f));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
} catch {
|
|
799
|
+
}
|
|
800
|
+
return files;
|
|
801
|
+
}
|
|
802
|
+
/** Load persisted offsets */
|
|
803
|
+
loadOffsets() {
|
|
804
|
+
const offsetsPath = path2.join(getDataDir(), OFFSETS_FILE);
|
|
805
|
+
try {
|
|
806
|
+
if (fs2.existsSync(offsetsPath)) {
|
|
807
|
+
return JSON.parse(fs2.readFileSync(offsetsPath, "utf-8"));
|
|
808
|
+
}
|
|
809
|
+
} catch {
|
|
810
|
+
}
|
|
811
|
+
return {};
|
|
812
|
+
}
|
|
813
|
+
/** Save offsets */
|
|
814
|
+
saveOffsets(state) {
|
|
815
|
+
ensureDirectory(getDataDir());
|
|
816
|
+
const offsetsPath = path2.join(getDataDir(), OFFSETS_FILE);
|
|
817
|
+
try {
|
|
818
|
+
fs2.writeFileSync(offsetsPath, JSON.stringify(state), "utf-8");
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/** Initialize offsets to current file sizes (skip existing data) */
|
|
823
|
+
initOffsets() {
|
|
824
|
+
const files = this.getSessionFiles();
|
|
825
|
+
const state = this.loadOffsets();
|
|
826
|
+
for (const file of files) {
|
|
827
|
+
if (!state[file]) {
|
|
828
|
+
try {
|
|
829
|
+
const stats = fs2.statSync(file);
|
|
830
|
+
state[file] = { byteOffset: stats.size, lastModified: stats.mtimeMs };
|
|
831
|
+
} catch {
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
this.saveOffsets(state);
|
|
836
|
+
}
|
|
837
|
+
/** Main detection cycle */
|
|
838
|
+
async detect() {
|
|
839
|
+
try {
|
|
840
|
+
const skillSlugs = this.getDeployedSkillSlugs();
|
|
841
|
+
if (skillSlugs.size === 0) return;
|
|
842
|
+
const skillsDir = getSkillsDir();
|
|
843
|
+
const files = this.getSessionFiles();
|
|
844
|
+
const offsets = this.loadOffsets();
|
|
845
|
+
const events = [];
|
|
846
|
+
for (const file of files) {
|
|
847
|
+
try {
|
|
848
|
+
const stats = fs2.statSync(file);
|
|
849
|
+
const currentOffset = offsets[file]?.byteOffset ?? 0;
|
|
850
|
+
const currentMtime = offsets[file]?.lastModified ?? 0;
|
|
851
|
+
if (stats.size <= currentOffset && stats.mtimeMs === currentMtime) continue;
|
|
852
|
+
const startOffset = Math.max(currentOffset, stats.size - MAX_NEW_BYTES_PER_FILE);
|
|
853
|
+
if (stats.size <= startOffset) {
|
|
854
|
+
offsets[file] = { byteOffset: stats.size, lastModified: stats.mtimeMs };
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
const stream = fs2.createReadStream(file, {
|
|
858
|
+
start: startOffset,
|
|
859
|
+
encoding: "utf-8"
|
|
860
|
+
});
|
|
861
|
+
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
862
|
+
for await (const line of rl) {
|
|
863
|
+
if (!line.trim()) continue;
|
|
864
|
+
try {
|
|
865
|
+
const entry = JSON.parse(line);
|
|
866
|
+
const msg = entry?.message;
|
|
867
|
+
if (!msg || msg.role !== "assistant") continue;
|
|
868
|
+
const content = msg.content;
|
|
869
|
+
if (!Array.isArray(content)) continue;
|
|
870
|
+
const sessionId = entry.sessionId || "";
|
|
871
|
+
const timestamp = entry.timestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
872
|
+
const cwd = entry.cwd || "";
|
|
873
|
+
for (const block of content) {
|
|
874
|
+
if (block?.type !== "tool_use") continue;
|
|
875
|
+
const toolName = block.name;
|
|
876
|
+
const toolInput = block.input || {};
|
|
877
|
+
if (toolName === "Read") {
|
|
878
|
+
const filePath = toolInput.file_path || "";
|
|
879
|
+
if (filePath.includes("/skills/") && filePath.endsWith("/SKILL.md")) {
|
|
880
|
+
const parts = filePath.split("/skills/");
|
|
881
|
+
if (parts.length >= 2) {
|
|
882
|
+
const afterSkills = parts[parts.length - 1];
|
|
883
|
+
const slug = afterSkills.split("/")[0];
|
|
884
|
+
if (slug && skillSlugs.has(slug)) {
|
|
885
|
+
events.push({
|
|
886
|
+
skillSlug: slug,
|
|
887
|
+
claudeSessionId: sessionId,
|
|
888
|
+
projectPath: cwd,
|
|
889
|
+
timestamp
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
offsets[file] = { byteOffset: stats.size, lastModified: stats.mtimeMs };
|
|
900
|
+
} catch {
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
for (const key of Object.keys(offsets)) {
|
|
904
|
+
if (!fs2.existsSync(key)) {
|
|
905
|
+
delete offsets[key];
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
this.saveOffsets(offsets);
|
|
909
|
+
const seen = /* @__PURE__ */ new Set();
|
|
910
|
+
const uniqueEvents = events.filter((e) => {
|
|
911
|
+
const key = `${e.skillSlug}:${e.claudeSessionId}:${e.timestamp}`;
|
|
912
|
+
if (seen.has(key)) return false;
|
|
913
|
+
seen.add(key);
|
|
914
|
+
return true;
|
|
915
|
+
});
|
|
916
|
+
if (uniqueEvents.length > 0) {
|
|
917
|
+
this.log("INFO", `Detected ${uniqueEvents.length} skill usage(s), reporting...`);
|
|
918
|
+
const response = await this.client.reportSkillUsage(uniqueEvents);
|
|
919
|
+
if (response.success) {
|
|
920
|
+
this.log("INFO", `Reported ${response.data?.logged ?? 0} skill usage(s)`);
|
|
921
|
+
this.callbacks.onDetection?.(response.data?.logged ?? 0);
|
|
922
|
+
} else {
|
|
923
|
+
this.log("ERROR", `Report failed: ${response.error}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
} catch (error) {
|
|
927
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
928
|
+
this.log("ERROR", `Detection error: ${err.message}`);
|
|
929
|
+
this.callbacks.onError?.(err);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
// 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";
|
|
937
|
+
var STATUS_FILE = "daemon-status.json";
|
|
938
|
+
var WRITE_INTERVAL_MS = 1e4;
|
|
939
|
+
var StatusWriter = class _StatusWriter {
|
|
940
|
+
intervalId = null;
|
|
941
|
+
status;
|
|
942
|
+
filePath;
|
|
943
|
+
constructor() {
|
|
944
|
+
this.filePath = join4(getDataDir(), STATUS_FILE);
|
|
945
|
+
this.status = {
|
|
946
|
+
running: true,
|
|
947
|
+
pid: process.pid,
|
|
948
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
949
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
950
|
+
claudeWatcher: { lastSync: null, promptsSynced: 0, lastError: null },
|
|
951
|
+
skillDetector: { deployedSkills: 0, usagesDetected: 0, lastDetection: null, lastError: null },
|
|
952
|
+
trackedProjects: [],
|
|
953
|
+
activeSessions: 0
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
/** Start periodic writes */
|
|
957
|
+
start() {
|
|
958
|
+
ensureDirectory(getDataDir());
|
|
959
|
+
this.write();
|
|
960
|
+
this.intervalId = setInterval(() => this.write(), WRITE_INTERVAL_MS);
|
|
961
|
+
}
|
|
962
|
+
/** Stop writing and mark as not running */
|
|
963
|
+
stop() {
|
|
964
|
+
if (this.intervalId) {
|
|
965
|
+
clearInterval(this.intervalId);
|
|
966
|
+
this.intervalId = null;
|
|
967
|
+
}
|
|
968
|
+
this.status.running = false;
|
|
969
|
+
this.status.pid = null;
|
|
970
|
+
this.status.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
971
|
+
this.write();
|
|
972
|
+
}
|
|
973
|
+
/** Merge partial status updates */
|
|
974
|
+
update(partial) {
|
|
975
|
+
if (partial.claudeWatcher) {
|
|
976
|
+
this.status.claudeWatcher = { ...this.status.claudeWatcher, ...partial.claudeWatcher };
|
|
977
|
+
delete partial.claudeWatcher;
|
|
978
|
+
}
|
|
979
|
+
if (partial.skillDetector) {
|
|
980
|
+
this.status.skillDetector = { ...this.status.skillDetector, ...partial.skillDetector };
|
|
981
|
+
delete partial.skillDetector;
|
|
982
|
+
}
|
|
983
|
+
Object.assign(this.status, partial);
|
|
984
|
+
}
|
|
985
|
+
/** Get the status file path */
|
|
986
|
+
static getStatusFilePath() {
|
|
987
|
+
return join4(getDataDir(), STATUS_FILE);
|
|
988
|
+
}
|
|
989
|
+
/** Read current status from disk (static, for tray/status commands) */
|
|
990
|
+
static read() {
|
|
991
|
+
const filePath = _StatusWriter.getStatusFilePath();
|
|
992
|
+
try {
|
|
993
|
+
if (existsSync5(filePath)) {
|
|
994
|
+
return JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
995
|
+
}
|
|
996
|
+
} catch {
|
|
997
|
+
}
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
write() {
|
|
1001
|
+
this.status.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1002
|
+
try {
|
|
1003
|
+
writeFileSync3(this.filePath, JSON.stringify(this.status, null, 2), "utf-8");
|
|
1004
|
+
} catch {
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
|
|
720
1009
|
// src/daemon-runner.ts
|
|
721
1010
|
function log(level, msg) {
|
|
722
1011
|
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] ${msg}
|
|
@@ -734,7 +1023,7 @@ async function updateTrackedProjectsCache(client) {
|
|
|
734
1023
|
updatedAt: Date.now(),
|
|
735
1024
|
projects: result.data.projects.filter((p) => p.trackingEnabled).map((p) => ({ path: p.path, name: p.name }))
|
|
736
1025
|
};
|
|
737
|
-
|
|
1026
|
+
writeFileSync4(getTrackedProjectsCacheFile(), JSON.stringify(cacheData, null, 2));
|
|
738
1027
|
log("DEBUG", `Updated tracked projects cache: ${cacheData.projects.length} project(s)`);
|
|
739
1028
|
}
|
|
740
1029
|
} catch (error) {
|
|
@@ -743,9 +1032,9 @@ async function updateTrackedProjectsCache(client) {
|
|
|
743
1032
|
}
|
|
744
1033
|
async function cleanupStaleSessions(client) {
|
|
745
1034
|
const sessionsDir = getActiveSessionsDir();
|
|
746
|
-
if (!
|
|
1035
|
+
if (!existsSync6(sessionsDir)) return;
|
|
747
1036
|
try {
|
|
748
|
-
const files =
|
|
1037
|
+
const files = readdirSync2(sessionsDir);
|
|
749
1038
|
for (const file of files) {
|
|
750
1039
|
if (!file.endsWith(".json")) continue;
|
|
751
1040
|
const pid = parseInt(file.replace(".json", ""), 10);
|
|
@@ -758,7 +1047,7 @@ async function cleanupStaleSessions(client) {
|
|
|
758
1047
|
}
|
|
759
1048
|
if (!isRunning) {
|
|
760
1049
|
try {
|
|
761
|
-
const sessionData = JSON.parse(
|
|
1050
|
+
const sessionData = JSON.parse(readFileSync4(`${sessionsDir}/${file}`, "utf-8"));
|
|
762
1051
|
if (sessionData.sessionId) {
|
|
763
1052
|
await client.endSession(sessionData.sessionId);
|
|
764
1053
|
log("INFO", `Ended stale session ${sessionData.sessionId.slice(0, 8)} (PID ${pid} dead)`);
|
|
@@ -778,11 +1067,13 @@ async function cleanupStaleSessions(client) {
|
|
|
778
1067
|
}
|
|
779
1068
|
async function main() {
|
|
780
1069
|
const pidFile = getPidFile();
|
|
781
|
-
|
|
1070
|
+
writeFileSync4(pidFile, String(process.pid));
|
|
782
1071
|
log("INFO", `Daemon starting (PID: ${process.pid})`);
|
|
1072
|
+
const statusWriter = new StatusWriter();
|
|
783
1073
|
const cleanup = () => {
|
|
784
1074
|
log("INFO", "Daemon shutting down");
|
|
785
|
-
|
|
1075
|
+
statusWriter.stop();
|
|
1076
|
+
if (existsSync6(pidFile)) {
|
|
786
1077
|
try {
|
|
787
1078
|
unlinkSync(pidFile);
|
|
788
1079
|
} catch {
|
|
@@ -818,9 +1109,34 @@ async function main() {
|
|
|
818
1109
|
}
|
|
819
1110
|
});
|
|
820
1111
|
await watcher.start();
|
|
821
|
-
|
|
1112
|
+
const skillDetector = new SkillUsageDetector(client, {
|
|
1113
|
+
intervalMs: 3e4,
|
|
1114
|
+
callbacks: {
|
|
1115
|
+
onDetection: (count) => {
|
|
1116
|
+
log("INFO", `Detected ${count} skill usage(s)`);
|
|
1117
|
+
statusWriter.update({ skillDetector: { usagesDetected: count, lastDetection: (/* @__PURE__ */ new Date()).toISOString(), lastError: null } });
|
|
1118
|
+
},
|
|
1119
|
+
onError: (err) => {
|
|
1120
|
+
log("ERROR", `Skill detection error: ${err.message}`);
|
|
1121
|
+
statusWriter.update({ skillDetector: { lastError: err.message } });
|
|
1122
|
+
},
|
|
1123
|
+
log: (level, msg) => log(level, msg)
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
await skillDetector.start();
|
|
1127
|
+
statusWriter.start();
|
|
1128
|
+
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
|
+
}
|
|
1137
|
+
}, 6e4);
|
|
822
1138
|
setInterval(() => cleanupStaleSessions(client), 3e5);
|
|
823
|
-
log("INFO", "Daemon started successfully \u2014 watching Claude conversations");
|
|
1139
|
+
log("INFO", "Daemon started successfully \u2014 watching Claude conversations & skill usage");
|
|
824
1140
|
await new Promise(() => {
|
|
825
1141
|
});
|
|
826
1142
|
}
|