skillo 0.2.3 → 0.2.5
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/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/{tray-RZQV6GDP.js → tray-YOL4R2RH.js} +150 -39
- package/dist/tray-YOL4R2RH.js.map +1 -0
- package/package.json +1 -1
- package/scripts/postinstall.mjs +20 -1
- package/scripts/tray-helper-darwin +0 -0
- package/scripts/tray-helper-darwin.swift +180 -0
- package/dist/tray-RZQV6GDP.js.map +0 -1
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
// src/tray/tray.ts
|
|
8
8
|
import SysTrayModule from "systray2";
|
|
9
9
|
import { readFileSync as readFileSync2, existsSync as existsSync2, writeFileSync as writeFileSync2, unlinkSync, chmodSync, readdirSync } from "fs";
|
|
10
|
-
import { exec } from "child_process";
|
|
11
|
-
import { join as join2 } from "path";
|
|
10
|
+
import { exec, execSync, spawn } from "child_process";
|
|
11
|
+
import { join as join2, dirname } from "path";
|
|
12
12
|
import { platform, homedir } from "os";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
13
14
|
|
|
14
15
|
// src/utils/status-writer.ts
|
|
15
16
|
import { writeFileSync, readFileSync, existsSync } from "fs";
|
|
@@ -112,7 +113,7 @@ function buildMenu(status) {
|
|
|
112
113
|
const statusIcon = running ? "\u25CF" : "\u25CB";
|
|
113
114
|
const statusText = running ? "Running" : "Stopped";
|
|
114
115
|
const projects = status?.trackedProjects ?? [];
|
|
115
|
-
const projectItems = projects.length > 0 ? projects.map((p
|
|
116
|
+
const projectItems = projects.length > 0 ? projects.map((p) => ({
|
|
116
117
|
title: ` ${p.name}`,
|
|
117
118
|
tooltip: p.path,
|
|
118
119
|
enabled: false,
|
|
@@ -124,7 +125,6 @@ function buildMenu(status) {
|
|
|
124
125
|
checked: false
|
|
125
126
|
}];
|
|
126
127
|
const items = [
|
|
127
|
-
// Status header
|
|
128
128
|
{
|
|
129
129
|
title: `${statusIcon} Skillo \u2014 ${statusText}`,
|
|
130
130
|
tooltip: "Daemon status",
|
|
@@ -132,7 +132,6 @@ function buildMenu(status) {
|
|
|
132
132
|
checked: false
|
|
133
133
|
},
|
|
134
134
|
SEPARATOR,
|
|
135
|
-
// User
|
|
136
135
|
{
|
|
137
136
|
title: status?.user ? `Logged in as: ${status.user}` : "Not logged in",
|
|
138
137
|
tooltip: "User info",
|
|
@@ -140,7 +139,6 @@ function buildMenu(status) {
|
|
|
140
139
|
checked: false
|
|
141
140
|
},
|
|
142
141
|
SEPARATOR,
|
|
143
|
-
// Projects
|
|
144
142
|
{
|
|
145
143
|
title: `Tracked Projects (${projects.length})`,
|
|
146
144
|
tooltip: "Projects being tracked",
|
|
@@ -149,7 +147,6 @@ function buildMenu(status) {
|
|
|
149
147
|
},
|
|
150
148
|
...projectItems,
|
|
151
149
|
SEPARATOR,
|
|
152
|
-
// Sync info
|
|
153
150
|
{
|
|
154
151
|
title: `Last sync: ${formatTimeAgo(status?.claudeWatcher?.lastSync)}`,
|
|
155
152
|
tooltip: "Last Claude conversation sync",
|
|
@@ -169,7 +166,6 @@ function buildMenu(status) {
|
|
|
169
166
|
checked: false
|
|
170
167
|
},
|
|
171
168
|
SEPARATOR,
|
|
172
|
-
// Actions
|
|
173
169
|
{
|
|
174
170
|
title: running ? "Stop Daemon" : "Start Daemon",
|
|
175
171
|
tooltip: running ? "Stop the Skillo daemon" : "Start the Skillo daemon",
|
|
@@ -183,7 +179,6 @@ function buildMenu(status) {
|
|
|
183
179
|
checked: false
|
|
184
180
|
},
|
|
185
181
|
SEPARATOR,
|
|
186
|
-
// Quit
|
|
187
182
|
{
|
|
188
183
|
title: "Quit Tray",
|
|
189
184
|
tooltip: "Hide tray icon (daemon keeps running)",
|
|
@@ -215,6 +210,129 @@ function execSkillo(args) {
|
|
|
215
210
|
exec(`skillo ${args}`, () => {
|
|
216
211
|
});
|
|
217
212
|
}
|
|
213
|
+
function handleClick(title, cleanup) {
|
|
214
|
+
if (title === "Stop Daemon" || title === "Start Daemon") {
|
|
215
|
+
execSkillo(title === "Stop Daemon" ? "stop" : "start");
|
|
216
|
+
} else if (title === "Open Dashboard") {
|
|
217
|
+
openBrowser("https://www.skillo.one/dashboard");
|
|
218
|
+
} else if (title === "Quit Tray") {
|
|
219
|
+
cleanup();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function findDarwinHelper() {
|
|
223
|
+
const candidates = [
|
|
224
|
+
join2(getDataDir(), "tray-helper-darwin"),
|
|
225
|
+
join2(dirname(fileURLToPath(import.meta.url)), "..", "scripts", "tray-helper-darwin"),
|
|
226
|
+
join2(dirname(fileURLToPath(import.meta.url)), "scripts", "tray-helper-darwin")
|
|
227
|
+
];
|
|
228
|
+
for (const p of candidates) {
|
|
229
|
+
if (existsSync2(p)) return p;
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
function compileDarwinHelper() {
|
|
234
|
+
const srcCandidates = [
|
|
235
|
+
join2(dirname(fileURLToPath(import.meta.url)), "..", "scripts", "tray-helper-darwin.swift"),
|
|
236
|
+
join2(dirname(fileURLToPath(import.meta.url)), "scripts", "tray-helper-darwin.swift")
|
|
237
|
+
];
|
|
238
|
+
let src = null;
|
|
239
|
+
for (const p of srcCandidates) {
|
|
240
|
+
if (existsSync2(p)) {
|
|
241
|
+
src = p;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!src) return null;
|
|
246
|
+
const out = join2(getDataDir(), "tray-helper-darwin");
|
|
247
|
+
try {
|
|
248
|
+
execSync(`swiftc "${src}" -o "${out}" -O 2>/dev/null`, { timeout: 6e4 });
|
|
249
|
+
chmodSync(out, 493);
|
|
250
|
+
return out;
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function startDarwinTray(pidFile) {
|
|
256
|
+
let helperPath = findDarwinHelper();
|
|
257
|
+
if (!helperPath) {
|
|
258
|
+
helperPath = compileDarwinHelper();
|
|
259
|
+
}
|
|
260
|
+
if (!helperPath) {
|
|
261
|
+
await startSystray2Tray(pidFile);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const initialStatus = readStatus();
|
|
265
|
+
const menu = buildMenu(initialStatus);
|
|
266
|
+
const helper = spawn(helperPath, [], {
|
|
267
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
268
|
+
});
|
|
269
|
+
const cleanup = () => {
|
|
270
|
+
try {
|
|
271
|
+
unlinkSync(pidFile);
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
helper.kill();
|
|
275
|
+
process.exit(0);
|
|
276
|
+
};
|
|
277
|
+
process.on("SIGINT", cleanup);
|
|
278
|
+
process.on("SIGTERM", cleanup);
|
|
279
|
+
helper.on("exit", () => {
|
|
280
|
+
try {
|
|
281
|
+
unlinkSync(pidFile);
|
|
282
|
+
} catch {
|
|
283
|
+
}
|
|
284
|
+
process.exit(0);
|
|
285
|
+
});
|
|
286
|
+
await new Promise((resolve, reject) => {
|
|
287
|
+
let buf = "";
|
|
288
|
+
const onData = (data) => {
|
|
289
|
+
buf += data.toString();
|
|
290
|
+
const lines = buf.split("\n");
|
|
291
|
+
buf = lines.pop() || "";
|
|
292
|
+
for (const line of lines) {
|
|
293
|
+
if (!line.trim()) continue;
|
|
294
|
+
try {
|
|
295
|
+
const msg = JSON.parse(line);
|
|
296
|
+
if (msg.type === "ready") {
|
|
297
|
+
helper.stdout.removeListener("data", onData);
|
|
298
|
+
resolve();
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
helper.stdout.on("data", onData);
|
|
305
|
+
setTimeout(() => reject(new Error("Tray helper did not become ready")), 1e4);
|
|
306
|
+
});
|
|
307
|
+
helper.stdin.write(JSON.stringify(menu) + "\n");
|
|
308
|
+
let clickBuf = "";
|
|
309
|
+
helper.stdout.on("data", (data) => {
|
|
310
|
+
clickBuf += data.toString();
|
|
311
|
+
const lines = clickBuf.split("\n");
|
|
312
|
+
clickBuf = lines.pop() || "";
|
|
313
|
+
for (const line of lines) {
|
|
314
|
+
if (!line.trim()) continue;
|
|
315
|
+
try {
|
|
316
|
+
const msg = JSON.parse(line);
|
|
317
|
+
if (msg.type === "clicked" && msg.item) {
|
|
318
|
+
handleClick(msg.item.title, cleanup);
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
setInterval(() => {
|
|
325
|
+
try {
|
|
326
|
+
const status = readStatus();
|
|
327
|
+
const updatedMenu = buildMenu(status);
|
|
328
|
+
const action = { type: "update-menu", menu: updatedMenu };
|
|
329
|
+
helper.stdin.write(JSON.stringify(action) + "\n");
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
}, STATUS_POLL_MS);
|
|
333
|
+
await new Promise(() => {
|
|
334
|
+
});
|
|
335
|
+
}
|
|
218
336
|
function fixSystrayBinaryPermissions() {
|
|
219
337
|
if (platform() === "win32") return;
|
|
220
338
|
try {
|
|
@@ -231,20 +349,8 @@ function fixSystrayBinaryPermissions() {
|
|
|
231
349
|
} catch {
|
|
232
350
|
}
|
|
233
351
|
}
|
|
234
|
-
async function
|
|
352
|
+
async function startSystray2Tray(pidFile) {
|
|
235
353
|
fixSystrayBinaryPermissions();
|
|
236
|
-
const pidFile = join2(getDataDir(), TRAY_PID_FILE);
|
|
237
|
-
ensureDirectory(getDataDir());
|
|
238
|
-
if (existsSync2(pidFile)) {
|
|
239
|
-
try {
|
|
240
|
-
const pid = parseInt(readFileSync2(pidFile, "utf-8").trim(), 10);
|
|
241
|
-
process.kill(pid, 0);
|
|
242
|
-
console.error("Tray icon is already running (PID: " + pid + ")");
|
|
243
|
-
process.exit(1);
|
|
244
|
-
} catch {
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
writeFileSync2(pidFile, String(process.pid));
|
|
248
354
|
const initialStatus = readStatus();
|
|
249
355
|
const menu = buildMenu(initialStatus);
|
|
250
356
|
const systray = new SysTray({
|
|
@@ -264,34 +370,39 @@ async function startTray() {
|
|
|
264
370
|
process.on("SIGTERM", cleanup);
|
|
265
371
|
await systray.ready();
|
|
266
372
|
systray.onClick(async (action) => {
|
|
267
|
-
|
|
268
|
-
if (clickedTitle === "Stop Daemon" || clickedTitle === "Start Daemon") {
|
|
269
|
-
if (clickedTitle === "Stop Daemon") {
|
|
270
|
-
execSkillo("stop");
|
|
271
|
-
} else {
|
|
272
|
-
execSkillo("start");
|
|
273
|
-
}
|
|
274
|
-
} else if (clickedTitle === "Open Dashboard") {
|
|
275
|
-
openBrowser("https://www.skillo.one/dashboard");
|
|
276
|
-
} else if (clickedTitle === "Quit Tray") {
|
|
277
|
-
cleanup();
|
|
278
|
-
}
|
|
373
|
+
handleClick(action.item.title, cleanup);
|
|
279
374
|
});
|
|
280
375
|
setInterval(async () => {
|
|
281
376
|
try {
|
|
282
377
|
const status = readStatus();
|
|
283
378
|
const updatedMenu = buildMenu(status);
|
|
284
|
-
await systray.sendAction({
|
|
285
|
-
type: "update-menu",
|
|
286
|
-
menu: updatedMenu
|
|
287
|
-
});
|
|
379
|
+
await systray.sendAction({ type: "update-menu", menu: updatedMenu });
|
|
288
380
|
} catch {
|
|
289
381
|
}
|
|
290
382
|
}, STATUS_POLL_MS);
|
|
291
383
|
await new Promise(() => {
|
|
292
384
|
});
|
|
293
385
|
}
|
|
386
|
+
async function startTray() {
|
|
387
|
+
const pidFile = join2(getDataDir(), TRAY_PID_FILE);
|
|
388
|
+
ensureDirectory(getDataDir());
|
|
389
|
+
if (existsSync2(pidFile)) {
|
|
390
|
+
try {
|
|
391
|
+
const pid = parseInt(readFileSync2(pidFile, "utf-8").trim(), 10);
|
|
392
|
+
process.kill(pid, 0);
|
|
393
|
+
console.error("Tray icon is already running (PID: " + pid + ")");
|
|
394
|
+
process.exit(1);
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
writeFileSync2(pidFile, String(process.pid));
|
|
399
|
+
if (platform() === "darwin") {
|
|
400
|
+
await startDarwinTray(pidFile);
|
|
401
|
+
} else {
|
|
402
|
+
await startSystray2Tray(pidFile);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
294
405
|
export {
|
|
295
406
|
startTray
|
|
296
407
|
};
|
|
297
|
-
//# sourceMappingURL=tray-
|
|
408
|
+
//# sourceMappingURL=tray-YOL4R2RH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tray/tray.ts","../src/utils/status-writer.ts"],"sourcesContent":["/**\n * Skillo System Tray Icon\n *\n * Shows daemon status, tracked projects, sync info in the system tray.\n * Reads ~/.skillo/daemon-status.json (written by StatusWriter in daemon).\n *\n * macOS: native Swift helper (scripts/tray-helper-darwin) — arm64+x86_64.\n * Linux/Windows: systray2 (Go-based binary).\n */\n\nimport SysTrayModule from \"systray2\";\nimport { readFileSync, existsSync, writeFileSync, unlinkSync, chmodSync, readdirSync } from \"fs\";\nimport { exec, execSync, spawn } from \"child_process\";\nimport { join, dirname } from \"path\";\nimport { platform, homedir } from \"os\";\nimport { fileURLToPath } from \"url\";\nimport { StatusWriter, type DaemonStatus } from \"../utils/status-writer.js\";\nimport { getDataDir, ensureDirectory } from \"../utils/paths.js\";\n\n// Handle CJS/ESM interop — systray2 is CJS, so default import may be wrapped\nconst SysTray = (SysTrayModule as any).default || SysTrayModule;\n\ntype MenuItem = {\n title: string;\n tooltip: string;\n checked?: boolean;\n enabled?: boolean;\n hidden?: boolean;\n items?: MenuItem[];\n};\n\ntype Menu = {\n icon: string;\n title: string;\n tooltip: string;\n items: MenuItem[];\n};\n\nconst SEPARATOR: MenuItem = { title: \"<SEPARATOR>\", tooltip: \"\", enabled: true };\n\n// Minimal 16x16 template icon (black circle on transparent, suitable for macOS menu bar)\nconst ICON_BASE64 =\n \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAhklEQVQ4T2NkoBAwUqifgWoG\" +\n \"MDIyNjAyMP5nYGD4T8gFjHADGBkZAxgZGQMIGQK3gYmJqYCRkfE/IyPj/4KCgv9whzAyMDIw\" +\n \"MDL+Z2Bk/M/AwMjwn4GBsaCgoOA/uhehuwBuACMjI9wQBgYGBkZGxv8FBQUBcBfgC0e4C4gJ\" +\n \"R7gBxIQj1QwAAFbxMBHleBQjAAAAAElFTkSuQmCC\";\n\nconst TRAY_PID_FILE = \"tray.pid\";\nconst STATUS_POLL_MS = 3000;\n\nfunction formatTimeAgo(isoString: string | null | undefined): string {\n if (!isoString) return \"Never\";\n const diff = Date.now() - new Date(isoString).getTime();\n const seconds = Math.floor(diff / 1000);\n if (seconds < 60) return \"Just now\";\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes} min ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n return `${days}d ago`;\n}\n\nfunction readStatus(): DaemonStatus | null {\n return StatusWriter.read();\n}\n\nfunction buildMenu(status: DaemonStatus | null): Menu {\n const running = status?.running ?? false;\n const statusIcon = running ? \"\\u25CF\" : \"\\u25CB\"; // ● or ○\n const statusText = running ? \"Running\" : \"Stopped\";\n\n const projects = status?.trackedProjects ?? [];\n const projectItems: MenuItem[] = projects.length > 0\n ? projects.map((p) => ({\n title: ` ${p.name}`,\n tooltip: p.path,\n enabled: false,\n checked: false,\n }))\n : [{\n title: \" (none)\",\n tooltip: \"No tracked projects\",\n enabled: false,\n checked: false,\n }];\n\n const items: MenuItem[] = [\n {\n title: `${statusIcon} Skillo \\u2014 ${statusText}`,\n tooltip: \"Daemon status\",\n enabled: false,\n checked: false,\n },\n SEPARATOR,\n {\n title: status?.user ? `Logged in as: ${status.user}` : \"Not logged in\",\n tooltip: \"User info\",\n enabled: false,\n checked: false,\n },\n SEPARATOR,\n {\n title: `Tracked Projects (${projects.length})`,\n tooltip: \"Projects being tracked\",\n enabled: false,\n checked: false,\n },\n ...projectItems,\n SEPARATOR,\n {\n title: `Last sync: ${formatTimeAgo(status?.claudeWatcher?.lastSync)}`,\n tooltip: \"Last Claude conversation sync\",\n enabled: false,\n checked: false,\n },\n {\n title: `Prompts synced: ${status?.claudeWatcher?.promptsSynced ?? 0}`,\n tooltip: \"Total prompts synced\",\n enabled: false,\n checked: false,\n },\n {\n title: `Skills detected: ${status?.skillDetector?.usagesDetected ?? 0}`,\n tooltip: \"Skill usages detected\",\n enabled: false,\n checked: false,\n },\n SEPARATOR,\n {\n title: running ? \"Stop Daemon\" : \"Start Daemon\",\n tooltip: running ? \"Stop the Skillo daemon\" : \"Start the Skillo daemon\",\n enabled: true,\n checked: false,\n },\n {\n title: \"Open Dashboard\",\n tooltip: \"Open Skillo dashboard in browser\",\n enabled: true,\n checked: false,\n },\n SEPARATOR,\n {\n title: \"Quit Tray\",\n tooltip: \"Hide tray icon (daemon keeps running)\",\n enabled: true,\n checked: false,\n },\n ];\n\n return {\n icon: ICON_BASE64,\n title: \"\",\n tooltip: `Skillo \\u2014 ${statusText}`,\n items,\n };\n}\n\nfunction openBrowser(url: string) {\n const os = platform();\n let cmd: string;\n if (os === \"darwin\") {\n cmd = `open \"${url}\"`;\n } else if (os === \"win32\") {\n cmd = `start \"\" \"${url}\"`;\n } else {\n cmd = `xdg-open \"${url}\"`;\n }\n exec(cmd, () => {});\n}\n\nfunction execSkillo(args: string) {\n exec(`skillo ${args}`, () => {});\n}\n\nfunction handleClick(title: string, cleanup: () => void) {\n if (title === \"Stop Daemon\" || title === \"Start Daemon\") {\n execSkillo(title === \"Stop Daemon\" ? \"stop\" : \"start\");\n } else if (title === \"Open Dashboard\") {\n openBrowser(\"https://www.skillo.one/dashboard\");\n } else if (title === \"Quit Tray\") {\n cleanup();\n }\n}\n\n// ── macOS native tray (Swift helper) ─────────────────────────────────────────\n\nfunction findDarwinHelper(): string | null {\n // Look for the compiled helper next to the running script, or in scripts/\n const candidates = [\n join(getDataDir(), \"tray-helper-darwin\"),\n join(dirname(fileURLToPath(import.meta.url)), \"..\", \"scripts\", \"tray-helper-darwin\"),\n join(dirname(fileURLToPath(import.meta.url)), \"scripts\", \"tray-helper-darwin\"),\n ];\n for (const p of candidates) {\n if (existsSync(p)) return p;\n }\n return null;\n}\n\nfunction compileDarwinHelper(): string | null {\n // Compile from source if Swift is available\n const srcCandidates = [\n join(dirname(fileURLToPath(import.meta.url)), \"..\", \"scripts\", \"tray-helper-darwin.swift\"),\n join(dirname(fileURLToPath(import.meta.url)), \"scripts\", \"tray-helper-darwin.swift\"),\n ];\n let src: string | null = null;\n for (const p of srcCandidates) {\n if (existsSync(p)) { src = p; break; }\n }\n if (!src) return null;\n\n const out = join(getDataDir(), \"tray-helper-darwin\");\n try {\n execSync(`swiftc \"${src}\" -o \"${out}\" -O 2>/dev/null`, { timeout: 60000 });\n chmodSync(out, 0o755);\n return out;\n } catch {\n return null;\n }\n}\n\nasync function startDarwinTray(pidFile: string): Promise<void> {\n let helperPath = findDarwinHelper();\n if (!helperPath) {\n helperPath = compileDarwinHelper();\n }\n if (!helperPath) {\n // Fall back to systray2\n await startSystray2Tray(pidFile);\n return;\n }\n\n const initialStatus = readStatus();\n const menu = buildMenu(initialStatus);\n\n const helper = spawn(helperPath, [], {\n stdio: [\"pipe\", \"pipe\", \"ignore\"],\n });\n\n const cleanup = () => {\n try { unlinkSync(pidFile); } catch { /* ignore */ }\n helper.kill();\n process.exit(0);\n };\n process.on(\"SIGINT\", cleanup);\n process.on(\"SIGTERM\", cleanup);\n helper.on(\"exit\", () => {\n try { unlinkSync(pidFile); } catch { /* ignore */ }\n process.exit(0);\n });\n\n // Wait for ready, then send menu\n await new Promise<void>((resolve, reject) => {\n let buf = \"\";\n const onData = (data: Buffer) => {\n buf += data.toString();\n const lines = buf.split(\"\\n\");\n buf = lines.pop() || \"\";\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const msg = JSON.parse(line);\n if (msg.type === \"ready\") {\n helper.stdout!.removeListener(\"data\", onData);\n resolve();\n }\n } catch { /* ignore parse errors */ }\n }\n };\n helper.stdout!.on(\"data\", onData);\n setTimeout(() => reject(new Error(\"Tray helper did not become ready\")), 10000);\n });\n\n // Send initial menu\n helper.stdin!.write(JSON.stringify(menu) + \"\\n\");\n\n // Listen for clicks\n let clickBuf = \"\";\n helper.stdout!.on(\"data\", (data: Buffer) => {\n clickBuf += data.toString();\n const lines = clickBuf.split(\"\\n\");\n clickBuf = lines.pop() || \"\";\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const msg = JSON.parse(line);\n if (msg.type === \"clicked\" && msg.item) {\n handleClick(msg.item.title, cleanup);\n }\n } catch { /* ignore */ }\n }\n });\n\n // Poll and update\n setInterval(() => {\n try {\n const status = readStatus();\n const updatedMenu = buildMenu(status);\n const action = { type: \"update-menu\", menu: updatedMenu };\n helper.stdin!.write(JSON.stringify(action) + \"\\n\");\n } catch { /* ignore */ }\n }, STATUS_POLL_MS);\n\n await new Promise(() => {});\n}\n\n// ── systray2 tray (Linux/Windows) ────────────────────────────────────────────\n\nfunction fixSystrayBinaryPermissions() {\n if (platform() === \"win32\") return;\n try {\n const cacheDir = join(homedir(), \".cache\", \"node-systray\");\n if (!existsSync(cacheDir)) return;\n for (const ver of readdirSync(cacheDir)) {\n const dir = join(cacheDir, ver);\n for (const file of readdirSync(dir)) {\n if (file.startsWith(\"tray_\")) {\n chmodSync(join(dir, file), 0o755);\n }\n }\n }\n } catch { /* best-effort */ }\n}\n\nasync function startSystray2Tray(pidFile: string): Promise<void> {\n fixSystrayBinaryPermissions();\n\n const initialStatus = readStatus();\n const menu = buildMenu(initialStatus);\n\n const systray = new SysTray({\n menu,\n debug: false,\n copyDir: true,\n });\n\n const cleanup = () => {\n try { unlinkSync(pidFile); } catch { /* ignore */ }\n systray.kill(false);\n process.exit(0);\n };\n process.on(\"SIGINT\", cleanup);\n process.on(\"SIGTERM\", cleanup);\n\n await systray.ready();\n\n systray.onClick(async (action: { item: MenuItem }) => {\n handleClick(action.item.title, cleanup);\n });\n\n setInterval(async () => {\n try {\n const status = readStatus();\n const updatedMenu = buildMenu(status);\n await systray.sendAction({ type: \"update-menu\", menu: updatedMenu });\n } catch { /* ignore */ }\n }, STATUS_POLL_MS);\n\n await new Promise(() => {});\n}\n\n// ── Entry point ──────────────────────────────────────────────────────────────\n\nexport async function startTray(): Promise<void> {\n const pidFile = join(getDataDir(), TRAY_PID_FILE);\n ensureDirectory(getDataDir());\n\n // Check if another tray is already running\n if (existsSync(pidFile)) {\n try {\n const pid = parseInt(readFileSync(pidFile, \"utf-8\").trim(), 10);\n process.kill(pid, 0);\n console.error(\"Tray icon is already running (PID: \" + pid + \")\");\n process.exit(1);\n } catch {\n // Stale PID file, continue\n }\n }\n\n writeFileSync(pidFile, String(process.pid));\n\n if (platform() === \"darwin\") {\n await startDarwinTray(pidFile);\n } else {\n await startSystray2Tray(pidFile);\n }\n}\n","/**\n * StatusWriter — writes daemon status JSON for tray icon and diagnostics.\n *\n * Writes ~/.skillo/daemon-status.json every 10s and on key events.\n * Tray icon and `skillo status` read this file.\n */\n\nimport { writeFileSync, readFileSync, existsSync } from \"fs\";\nimport { join } from \"path\";\nimport { getDataDir, ensureDirectory } from \"./paths.js\";\n\nexport interface DaemonStatus {\n running: boolean;\n pid: number | null;\n startedAt: string | null;\n updatedAt: string;\n user?: string;\n claudeWatcher?: {\n lastSync?: string | null;\n promptsSynced?: number;\n lastError?: string | null;\n };\n skillDetector?: {\n deployedSkills?: number;\n usagesDetected?: number;\n lastDetection?: string | null;\n lastError?: string | null;\n };\n trackedProjects?: Array<{ name: string; path: string }>;\n activeSessions?: number;\n}\n\nconst STATUS_FILE = \"daemon-status.json\";\nconst WRITE_INTERVAL_MS = 10000;\n\nexport class StatusWriter {\n private intervalId: ReturnType<typeof setInterval> | null = null;\n private status: DaemonStatus;\n private filePath: string;\n\n constructor() {\n this.filePath = join(getDataDir(), STATUS_FILE);\n this.status = {\n running: true,\n pid: process.pid,\n startedAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n claudeWatcher: { lastSync: null, promptsSynced: 0, lastError: null },\n skillDetector: { deployedSkills: 0, usagesDetected: 0, lastDetection: null, lastError: null },\n trackedProjects: [],\n activeSessions: 0,\n };\n }\n\n /** Start periodic writes */\n start() {\n ensureDirectory(getDataDir());\n this.write();\n this.intervalId = setInterval(() => this.write(), WRITE_INTERVAL_MS);\n }\n\n /** Stop writing and mark as not running */\n stop() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = null;\n }\n this.status.running = false;\n this.status.pid = null;\n this.status.updatedAt = new Date().toISOString();\n this.write();\n }\n\n /** Merge partial status updates */\n update(partial: Partial<DaemonStatus>) {\n // Deep merge for nested objects\n if (partial.claudeWatcher) {\n this.status.claudeWatcher = { ...this.status.claudeWatcher, ...partial.claudeWatcher };\n delete partial.claudeWatcher;\n }\n if (partial.skillDetector) {\n this.status.skillDetector = { ...this.status.skillDetector, ...partial.skillDetector };\n delete partial.skillDetector;\n }\n Object.assign(this.status, partial);\n }\n\n /** Get the status file path */\n static getStatusFilePath(): string {\n return join(getDataDir(), STATUS_FILE);\n }\n\n /** Read current status from disk (static, for tray/status commands) */\n static read(): DaemonStatus | null {\n const filePath = StatusWriter.getStatusFilePath();\n try {\n if (existsSync(filePath)) {\n return JSON.parse(readFileSync(filePath, \"utf-8\"));\n }\n } catch {\n // ignore\n }\n return null;\n }\n\n private write() {\n this.status.updatedAt = new Date().toISOString();\n try {\n writeFileSync(this.filePath, JSON.stringify(this.status, null, 2), \"utf-8\");\n } catch {\n // Can't write status, ignore\n }\n }\n}\n"],"mappings":"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","names":["readFileSync","existsSync","writeFileSync","join","join","existsSync","readFileSync","writeFileSync"]}
|
package/package.json
CHANGED
package/scripts/postinstall.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Cross-platform: macOS, Linux, Windows (PowerShell).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from "fs";
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, chmodSync, readdirSync } from "fs";
|
|
12
12
|
import { join } from "path";
|
|
13
13
|
import { homedir, platform } from "os";
|
|
14
14
|
|
|
@@ -319,9 +319,28 @@ function install(shell) {
|
|
|
319
319
|
return true;
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
// ── Fix systray2 binary permissions (npm strips +x) ─────────────────────────
|
|
323
|
+
|
|
324
|
+
function fixSystrayBinaries() {
|
|
325
|
+
if (isWin) return;
|
|
326
|
+
try {
|
|
327
|
+
// Fix binaries relative to this postinstall script (../node_modules/systray2/traybin/)
|
|
328
|
+
const trayBinDir = join(new URL(".", import.meta.url).pathname, "..", "node_modules", "systray2", "traybin");
|
|
329
|
+
if (!existsSync(trayBinDir)) return;
|
|
330
|
+
for (const f of readdirSync(trayBinDir)) {
|
|
331
|
+
if (f.startsWith("tray_")) {
|
|
332
|
+
chmodSync(join(trayBinDir, f), 0o755);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
// best-effort
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
322
340
|
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
323
341
|
|
|
324
342
|
try {
|
|
343
|
+
fixSystrayBinaries();
|
|
325
344
|
const shell = detectShell();
|
|
326
345
|
const installed = install(shell);
|
|
327
346
|
|
|
Binary file
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skillo Tray Helper for macOS
|
|
3
|
+
*
|
|
4
|
+
* Native Swift menu bar icon. Communicates with Node via stdin/stdout JSON lines.
|
|
5
|
+
*
|
|
6
|
+
* Protocol (compatible with systray2):
|
|
7
|
+
* stdin <- JSON menu config (first line) + {"type":"update-menu","menu":{...}} for updates
|
|
8
|
+
* stdout -> {"type":"ready"} on launch, {"type":"clicked","item":{...},"seq_id":N} on click
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import Cocoa
|
|
12
|
+
|
|
13
|
+
// MARK: - JSON models
|
|
14
|
+
|
|
15
|
+
struct TrayMenuItem: Codable {
|
|
16
|
+
let title: String
|
|
17
|
+
let tooltip: String
|
|
18
|
+
var enabled: Bool?
|
|
19
|
+
var checked: Bool?
|
|
20
|
+
var hidden: Bool?
|
|
21
|
+
var items: [TrayMenuItem]?
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
struct MenuConfig: Codable {
|
|
25
|
+
let icon: String
|
|
26
|
+
let title: String
|
|
27
|
+
let tooltip: String
|
|
28
|
+
let items: [TrayMenuItem]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
struct UpdateAction: Codable {
|
|
32
|
+
let type: String
|
|
33
|
+
let menu: MenuConfig?
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
struct ClickEvent: Codable {
|
|
37
|
+
let type: String
|
|
38
|
+
let item: TrayMenuItem
|
|
39
|
+
let seq_id: Int
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// MARK: - Tray controller
|
|
43
|
+
|
|
44
|
+
class TrayController: NSObject, NSApplicationDelegate {
|
|
45
|
+
var statusItem: NSStatusItem!
|
|
46
|
+
var flatItems: [(TrayMenuItem, Int)] = []
|
|
47
|
+
|
|
48
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
49
|
+
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
50
|
+
statusItem.button?.title = "S"
|
|
51
|
+
|
|
52
|
+
// Emit ready
|
|
53
|
+
writeLine("{\"type\": \"ready\"}")
|
|
54
|
+
|
|
55
|
+
// Start reading stdin on background thread
|
|
56
|
+
startStdinReader()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// MARK: stdin reader
|
|
60
|
+
|
|
61
|
+
func startStdinReader() {
|
|
62
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
63
|
+
let handle = FileHandle.standardInput
|
|
64
|
+
var buf = ""
|
|
65
|
+
|
|
66
|
+
while true {
|
|
67
|
+
guard let data = try? handle.availableData, !data.isEmpty else {
|
|
68
|
+
// stdin closed — parent exited
|
|
69
|
+
DispatchQueue.main.async { NSApp.terminate(nil) }
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
guard let chunk = String(data: data, encoding: .utf8) else { continue }
|
|
73
|
+
buf += chunk
|
|
74
|
+
|
|
75
|
+
while let newline = buf.firstIndex(of: "\n") {
|
|
76
|
+
let line = String(buf[buf.startIndex..<newline]).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
77
|
+
buf = String(buf[buf.index(after: newline)...])
|
|
78
|
+
if !line.isEmpty {
|
|
79
|
+
DispatchQueue.main.async { self?.handleLine(line) }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func handleLine(_ line: String) {
|
|
87
|
+
guard let data = line.data(using: .utf8) else { return }
|
|
88
|
+
|
|
89
|
+
// Try update action
|
|
90
|
+
if let action = try? JSONDecoder().decode(UpdateAction.self, from: data),
|
|
91
|
+
action.type == "update-menu",
|
|
92
|
+
let menu = action.menu {
|
|
93
|
+
applyMenu(menu)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Try initial menu config
|
|
98
|
+
if let menu = try? JSONDecoder().decode(MenuConfig.self, from: data) {
|
|
99
|
+
applyMenu(menu)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// MARK: Menu rendering
|
|
104
|
+
|
|
105
|
+
func applyMenu(_ config: MenuConfig) {
|
|
106
|
+
flatItems = []
|
|
107
|
+
|
|
108
|
+
// Icon
|
|
109
|
+
if let btn = statusItem.button {
|
|
110
|
+
if !config.icon.isEmpty, let imgData = Data(base64Encoded: config.icon) {
|
|
111
|
+
let img = NSImage(data: imgData)
|
|
112
|
+
img?.size = NSSize(width: 18, height: 18)
|
|
113
|
+
img?.isTemplate = true
|
|
114
|
+
btn.image = img
|
|
115
|
+
btn.title = ""
|
|
116
|
+
} else if !config.title.isEmpty {
|
|
117
|
+
btn.image = nil
|
|
118
|
+
btn.title = config.title
|
|
119
|
+
}
|
|
120
|
+
btn.toolTip = config.tooltip
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Menu items
|
|
124
|
+
let menu = NSMenu()
|
|
125
|
+
var seq = 0
|
|
126
|
+
|
|
127
|
+
for item in config.items {
|
|
128
|
+
if item.title == "<SEPARATOR>" {
|
|
129
|
+
menu.addItem(NSMenuItem.separator())
|
|
130
|
+
} else {
|
|
131
|
+
let mi = NSMenuItem(title: item.title, action: nil, keyEquivalent: "")
|
|
132
|
+
mi.toolTip = item.tooltip
|
|
133
|
+
mi.tag = seq
|
|
134
|
+
|
|
135
|
+
if item.enabled == true {
|
|
136
|
+
mi.target = self
|
|
137
|
+
mi.action = #selector(onItemClick(_:))
|
|
138
|
+
} else {
|
|
139
|
+
mi.isEnabled = false
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if item.checked == true { mi.state = .on }
|
|
143
|
+
if item.hidden == true { mi.isHidden = true }
|
|
144
|
+
|
|
145
|
+
menu.addItem(mi)
|
|
146
|
+
}
|
|
147
|
+
flatItems.append((item, seq))
|
|
148
|
+
seq += 1
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
statusItem.menu = menu
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// MARK: Click handler
|
|
155
|
+
|
|
156
|
+
@objc func onItemClick(_ sender: NSMenuItem) {
|
|
157
|
+
let seq = sender.tag
|
|
158
|
+
guard seq < flatItems.count else { return }
|
|
159
|
+
let (item, _) = flatItems[seq]
|
|
160
|
+
if let jsonData = try? JSONEncoder().encode(ClickEvent(type: "clicked", item: item, seq_id: seq)),
|
|
161
|
+
let jsonStr = String(data: jsonData, encoding: .utf8) {
|
|
162
|
+
writeLine(jsonStr)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// MARK: stdout
|
|
167
|
+
|
|
168
|
+
func writeLine(_ s: String) {
|
|
169
|
+
let out = s + "\n"
|
|
170
|
+
FileHandle.standardOutput.write(out.data(using: .utf8)!)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// MARK: - Main
|
|
175
|
+
|
|
176
|
+
let app = NSApplication.shared
|
|
177
|
+
app.setActivationPolicy(.accessory)
|
|
178
|
+
let controller = TrayController()
|
|
179
|
+
app.delegate = controller
|
|
180
|
+
app.run()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tray/tray.ts","../src/utils/status-writer.ts"],"sourcesContent":["/**\n * Skillo System Tray Icon\n *\n * Shows daemon status, tracked projects, sync info in the system tray.\n * Reads ~/.skillo/daemon-status.json (written by StatusWriter in daemon).\n * Uses systray2 for cross-platform tray support.\n */\n\nimport SysTrayModule from \"systray2\";\nimport { readFileSync, existsSync, writeFileSync, unlinkSync, chmodSync, readdirSync } from \"fs\";\nimport { exec } from \"child_process\";\nimport { join } from \"path\";\nimport { platform, homedir } from \"os\";\nimport { StatusWriter, type DaemonStatus } from \"../utils/status-writer.js\";\nimport { getDataDir, ensureDirectory } from \"../utils/paths.js\";\n\n// Handle CJS/ESM interop — systray2 is CJS, so default import may be wrapped\nconst SysTray = (SysTrayModule as any).default || SysTrayModule;\n\ntype MenuItem = {\n title: string;\n tooltip: string;\n checked?: boolean;\n enabled?: boolean;\n hidden?: boolean;\n items?: MenuItem[];\n};\n\ntype Menu = {\n icon: string;\n title: string;\n tooltip: string;\n items: MenuItem[];\n};\n\nconst SEPARATOR: MenuItem = { title: \"<SEPARATOR>\", tooltip: \"\", enabled: true };\n\n// Minimal 16x16 template icon (black circle on transparent, suitable for macOS menu bar)\n// This is a tiny PNG encoded in base64\nconst ICON_BASE64 =\n \"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAhklEQVQ4T2NkoBAwUqifgWoG\" +\n \"MDIyNjAyMP5nYGD4T8gFjHADGBkZAxgZGQMIGQK3gYmJqYCRkfE/IyPj/4KCgv9whzAyMDIw\" +\n \"MDL+Z2Bk/M/AwMjwn4GBsaCgoOA/uhehuwBuACMjI9wQBgYGBkZGxv8FBQUBcBfgC0e4C4gJ\" +\n \"R7gBxIQj1QwAAFbxMBHleBQjAAAAAElFTkSuQmCC\";\n\nconst TRAY_PID_FILE = \"tray.pid\";\nconst STATUS_POLL_MS = 3000;\n\n// Menu item sequence IDs\nconst SEQ = {\n STATUS_HEADER: 0,\n SEP1: 1,\n USER: 2,\n SEP2: 3,\n PROJECTS_HEADER: 4,\n // Projects: 100-199\n SEP3: 200,\n LAST_SYNC: 201,\n PROMPTS_SYNCED: 202,\n SKILLS_DETECTED: 203,\n SEP4: 204,\n TOGGLE_DAEMON: 205,\n OPEN_DASHBOARD: 206,\n SEP5: 207,\n QUIT: 208,\n};\n\nfunction formatTimeAgo(isoString: string | null | undefined): string {\n if (!isoString) return \"Never\";\n const diff = Date.now() - new Date(isoString).getTime();\n const seconds = Math.floor(diff / 1000);\n if (seconds < 60) return \"Just now\";\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes} min ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n return `${days}d ago`;\n}\n\nfunction readStatus(): DaemonStatus | null {\n return StatusWriter.read();\n}\n\nfunction buildMenu(status: DaemonStatus | null): Menu {\n const running = status?.running ?? false;\n const statusIcon = running ? \"\\u25CF\" : \"\\u25CB\"; // ● or ○\n const statusText = running ? \"Running\" : \"Stopped\";\n\n const projects = status?.trackedProjects ?? [];\n const projectItems: MenuItem[] = projects.length > 0\n ? projects.map((p, i) => ({\n title: ` ${p.name}`,\n tooltip: p.path,\n enabled: false,\n checked: false,\n }))\n : [{\n title: \" (none)\",\n tooltip: \"No tracked projects\",\n enabled: false,\n checked: false,\n }];\n\n const items: MenuItem[] = [\n // Status header\n {\n title: `${statusIcon} Skillo \\u2014 ${statusText}`,\n tooltip: \"Daemon status\",\n enabled: false,\n checked: false,\n },\n SEPARATOR,\n // User\n {\n title: status?.user ? `Logged in as: ${status.user}` : \"Not logged in\",\n tooltip: \"User info\",\n enabled: false,\n checked: false,\n },\n SEPARATOR,\n // Projects\n {\n title: `Tracked Projects (${projects.length})`,\n tooltip: \"Projects being tracked\",\n enabled: false,\n checked: false,\n },\n ...projectItems,\n SEPARATOR,\n // Sync info\n {\n title: `Last sync: ${formatTimeAgo(status?.claudeWatcher?.lastSync)}`,\n tooltip: \"Last Claude conversation sync\",\n enabled: false,\n checked: false,\n },\n {\n title: `Prompts synced: ${status?.claudeWatcher?.promptsSynced ?? 0}`,\n tooltip: \"Total prompts synced\",\n enabled: false,\n checked: false,\n },\n {\n title: `Skills detected: ${status?.skillDetector?.usagesDetected ?? 0}`,\n tooltip: \"Skill usages detected\",\n enabled: false,\n checked: false,\n },\n SEPARATOR,\n // Actions\n {\n title: running ? \"Stop Daemon\" : \"Start Daemon\",\n tooltip: running ? \"Stop the Skillo daemon\" : \"Start the Skillo daemon\",\n enabled: true,\n checked: false,\n },\n {\n title: \"Open Dashboard\",\n tooltip: \"Open Skillo dashboard in browser\",\n enabled: true,\n checked: false,\n },\n SEPARATOR,\n // Quit\n {\n title: \"Quit Tray\",\n tooltip: \"Hide tray icon (daemon keeps running)\",\n enabled: true,\n checked: false,\n },\n ];\n\n return {\n icon: ICON_BASE64,\n title: \"\",\n tooltip: `Skillo \\u2014 ${statusText}`,\n items,\n };\n}\n\nfunction openBrowser(url: string) {\n const os = platform();\n let cmd: string;\n if (os === \"darwin\") {\n cmd = `open \"${url}\"`;\n } else if (os === \"win32\") {\n cmd = `start \"\" \"${url}\"`;\n } else {\n cmd = `xdg-open \"${url}\"`;\n }\n exec(cmd, () => {});\n}\n\nfunction execSkillo(args: string) {\n exec(`skillo ${args}`, () => {});\n}\n\n/** Ensure the systray2 binary has execute permission (npm sometimes strips it) */\nfunction fixSystrayBinaryPermissions() {\n if (platform() === \"win32\") return;\n try {\n const cacheDir = join(homedir(), \".cache\", \"node-systray\");\n if (!existsSync(cacheDir)) return;\n for (const ver of readdirSync(cacheDir)) {\n const dir = join(cacheDir, ver);\n for (const file of readdirSync(dir)) {\n if (file.startsWith(\"tray_\")) {\n chmodSync(join(dir, file), 0o755);\n }\n }\n }\n } catch {\n // best-effort\n }\n}\n\nexport async function startTray(): Promise<void> {\n fixSystrayBinaryPermissions();\n\n const pidFile = join(getDataDir(), TRAY_PID_FILE);\n ensureDirectory(getDataDir());\n\n // Check if another tray is already running\n if (existsSync(pidFile)) {\n try {\n const pid = parseInt(readFileSync(pidFile, \"utf-8\").trim(), 10);\n process.kill(pid, 0); // Check if alive\n console.error(\"Tray icon is already running (PID: \" + pid + \")\");\n process.exit(1);\n } catch {\n // Stale PID file, continue\n }\n }\n\n // Write our PID\n writeFileSync(pidFile, String(process.pid));\n\n // Build initial menu\n const initialStatus = readStatus();\n const menu = buildMenu(initialStatus);\n\n const systray = new SysTray({\n menu,\n debug: false,\n copyDir: true,\n });\n\n // Cleanup on exit\n const cleanup = () => {\n try { unlinkSync(pidFile); } catch { /* ignore */ }\n systray.kill(false);\n process.exit(0);\n };\n process.on(\"SIGINT\", cleanup);\n process.on(\"SIGTERM\", cleanup);\n\n // Handle clicks\n await systray.ready();\n\n systray.onClick(async (action) => {\n const clickedTitle = action.item.title;\n\n if (clickedTitle === \"Stop Daemon\" || clickedTitle === \"Start Daemon\") {\n if (clickedTitle === \"Stop Daemon\") {\n execSkillo(\"stop\");\n } else {\n execSkillo(\"start\");\n }\n } else if (clickedTitle === \"Open Dashboard\") {\n openBrowser(\"https://www.skillo.one/dashboard\");\n } else if (clickedTitle === \"Quit Tray\") {\n cleanup();\n }\n });\n\n // Poll status and update menu\n setInterval(async () => {\n try {\n const status = readStatus();\n const updatedMenu = buildMenu(status);\n await systray.sendAction({\n type: \"update-menu\",\n menu: updatedMenu,\n });\n } catch {\n // ignore update errors\n }\n }, STATUS_POLL_MS);\n\n // Keep alive\n await new Promise(() => {});\n}\n","/**\n * StatusWriter — writes daemon status JSON for tray icon and diagnostics.\n *\n * Writes ~/.skillo/daemon-status.json every 10s and on key events.\n * Tray icon and `skillo status` read this file.\n */\n\nimport { writeFileSync, readFileSync, existsSync } from \"fs\";\nimport { join } from \"path\";\nimport { getDataDir, ensureDirectory } from \"./paths.js\";\n\nexport interface DaemonStatus {\n running: boolean;\n pid: number | null;\n startedAt: string | null;\n updatedAt: string;\n user?: string;\n claudeWatcher?: {\n lastSync?: string | null;\n promptsSynced?: number;\n lastError?: string | null;\n };\n skillDetector?: {\n deployedSkills?: number;\n usagesDetected?: number;\n lastDetection?: string | null;\n lastError?: string | null;\n };\n trackedProjects?: Array<{ name: string; path: string }>;\n activeSessions?: number;\n}\n\nconst STATUS_FILE = \"daemon-status.json\";\nconst WRITE_INTERVAL_MS = 10000;\n\nexport class StatusWriter {\n private intervalId: ReturnType<typeof setInterval> | null = null;\n private status: DaemonStatus;\n private filePath: string;\n\n constructor() {\n this.filePath = join(getDataDir(), STATUS_FILE);\n this.status = {\n running: true,\n pid: process.pid,\n startedAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n claudeWatcher: { lastSync: null, promptsSynced: 0, lastError: null },\n skillDetector: { deployedSkills: 0, usagesDetected: 0, lastDetection: null, lastError: null },\n trackedProjects: [],\n activeSessions: 0,\n };\n }\n\n /** Start periodic writes */\n start() {\n ensureDirectory(getDataDir());\n this.write();\n this.intervalId = setInterval(() => this.write(), WRITE_INTERVAL_MS);\n }\n\n /** Stop writing and mark as not running */\n stop() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = null;\n }\n this.status.running = false;\n this.status.pid = null;\n this.status.updatedAt = new Date().toISOString();\n this.write();\n }\n\n /** Merge partial status updates */\n update(partial: Partial<DaemonStatus>) {\n // Deep merge for nested objects\n if (partial.claudeWatcher) {\n this.status.claudeWatcher = { ...this.status.claudeWatcher, ...partial.claudeWatcher };\n delete partial.claudeWatcher;\n }\n if (partial.skillDetector) {\n this.status.skillDetector = { ...this.status.skillDetector, ...partial.skillDetector };\n delete partial.skillDetector;\n }\n Object.assign(this.status, partial);\n }\n\n /** Get the status file path */\n static getStatusFilePath(): string {\n return join(getDataDir(), STATUS_FILE);\n }\n\n /** Read current status from disk (static, for tray/status commands) */\n static read(): DaemonStatus | null {\n const filePath = StatusWriter.getStatusFilePath();\n try {\n if (existsSync(filePath)) {\n return JSON.parse(readFileSync(filePath, \"utf-8\"));\n }\n } catch {\n // ignore\n }\n return null;\n }\n\n private write() {\n this.status.updatedAt = new Date().toISOString();\n try {\n writeFileSync(this.filePath, JSON.stringify(this.status, null, 2), \"utf-8\");\n } catch {\n // Can't write status, ignore\n }\n }\n}\n"],"mappings":"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","names":["readFileSync","existsSync","writeFileSync","join","join","existsSync","readFileSync","writeFileSync"]}
|