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.
@@ -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, i) => ({
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 startTray() {
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
- const clickedTitle = action.item.title;
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-RZQV6GDP.js.map
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillo",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Autonomous workflow learning & skill generation system - Learn workflows by observation, not explanation",
5
5
  "keywords": [
6
6
  "cli",
@@ -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"]}