monty-cli 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/monty.js +106 -1
  2. package/package.json +1 -1
package/monty.js CHANGED
@@ -4,7 +4,7 @@
4
4
  const fs = require("node:fs");
5
5
  const os = require("node:os");
6
6
  const path = require("node:path");
7
- const { spawnSync } = require("node:child_process");
7
+ const { spawnSync, execSync } = require("node:child_process");
8
8
 
9
9
  const home = os.homedir();
10
10
  const montyDir = path.join(home, ".monty");
@@ -22,6 +22,7 @@ async function main() {
22
22
  if (command === "hook") return hookCommandHandler(args);
23
23
  if (command === "run") return runWrapped(args);
24
24
  if (command === "sync") return syncHistory(args);
25
+ if (command === "daemon") return daemon(args);
25
26
  if (command === "doctor") return doctor();
26
27
  if (command === "help" || command === "--help" || command === "-h") return help();
27
28
 
@@ -60,12 +61,15 @@ async function install(args) {
60
61
  upsertHookJson(codexPath, "codex");
61
62
  upsertCodexOtelConfig(codexConfigPath, config);
62
63
 
64
+ installLaunchdDaemon();
65
+
63
66
  console.log(`Monty installed for ${config.teamId}`);
64
67
  console.log(`Site: ${config.siteUrl}`);
65
68
  if (config.githubLogin) console.log(`GitHub avatar: ${config.githubLogin}`);
66
69
  console.log(`Claude hook: ${claudePath}`);
67
70
  console.log(`Codex hook: ${codexPath}`);
68
71
  console.log(`Codex telemetry: ${codexConfigPath}`);
72
+ console.log(`Daemon: com.monty.daemon (launchd)`);
69
73
  console.log("");
70
74
 
71
75
  if (!options["no-sync"]) {
@@ -286,6 +290,62 @@ async function sendHeartbeat() {
286
290
  }
287
291
  }
288
292
 
293
+ function detectLidClosed() {
294
+ try {
295
+ const out = execSync("ioreg -r -k AppleClamshellState -d 4 | grep AppleClamshellState | head -1", { encoding: "utf8", timeout: 3000 });
296
+ if (out.includes("Yes")) return true;
297
+ if (out.includes("No")) return false;
298
+ return false;
299
+ } catch {
300
+ return false;
301
+ }
302
+ }
303
+
304
+ function detectSessions() {
305
+ let claude = 0;
306
+ let codex = 0;
307
+ try { claude = parseInt(execSync("pgrep -x claude 2>/dev/null | wc -l", { encoding: "utf8", timeout: 2000 }).trim(), 10) || 0; } catch {}
308
+ try { codex = parseInt(execSync("pgrep -x codex 2>/dev/null | wc -l", { encoding: "utf8", timeout: 2000 }).trim(), 10) || 0; } catch {}
309
+ return { claude, codex };
310
+ }
311
+
312
+ async function sendPresence() {
313
+ const config = readConfig();
314
+ const siteUrl = cleanUrl(config.siteUrl || process.env.MONTY_SITE_URL || "https://www.trymonty.ai");
315
+ const headers = { "content-type": "application/json" };
316
+ const token = config.ingestToken || process.env.MONTY_INGEST_TOKEN;
317
+ if (token) headers.authorization = `Bearer ${token}`;
318
+
319
+ const controller = new AbortController();
320
+ const timeout = setTimeout(() => controller.abort(), 5000);
321
+ try {
322
+ await fetch(`${siteUrl}/api/lid-state`, {
323
+ method: "POST",
324
+ headers,
325
+ body: JSON.stringify({
326
+ user_name: config.userName || process.env.MONTY_USER || process.env.USER || "unknown",
327
+ lid_closed: detectLidClosed(),
328
+ sessions: detectSessions(),
329
+ }),
330
+ signal: controller.signal,
331
+ });
332
+ } catch {}
333
+ finally { clearTimeout(timeout); }
334
+ }
335
+
336
+ async function daemon(args) {
337
+ const options = parseArgs(args);
338
+ const intervalSec = Number(options.interval) || 5;
339
+ console.log(`Monty daemon running (every ${intervalSec}s). Press Ctrl+C to stop.`);
340
+
341
+ const tick = async () => {
342
+ await sendPresence().catch(() => {});
343
+ };
344
+
345
+ await tick();
346
+ setInterval(tick, intervalSec * 1000);
347
+ }
348
+
289
349
  async function runWrapped(args) {
290
350
  const [tool, ...toolArgs] = args;
291
351
  if (!tool || !["claude", "codex"].includes(tool)) {
@@ -668,6 +728,50 @@ function doctor() {
668
728
  }
669
729
  }
670
730
 
731
+ function installLaunchdDaemon() {
732
+ const label = "com.monty.daemon";
733
+ const plistPath = path.join(home, "Library", "LaunchAgents", `${label}.plist`);
734
+ const logPath = path.join(montyDir, "daemon.log");
735
+
736
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
737
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
738
+ <plist version="1.0">
739
+ <dict>
740
+ <key>Label</key>
741
+ <string>${label}</string>
742
+ <key>ProgramArguments</key>
743
+ <array>
744
+ <string>${process.execPath}</string>
745
+ <string>${installedCliPath}</string>
746
+ <string>daemon</string>
747
+ </array>
748
+ <key>RunAtLoad</key>
749
+ <true/>
750
+ <key>KeepAlive</key>
751
+ <true/>
752
+ <key>StandardOutPath</key>
753
+ <string>${logPath}</string>
754
+ <key>StandardErrorPath</key>
755
+ <string>${logPath}</string>
756
+ </dict>
757
+ </plist>
758
+ `;
759
+
760
+ try {
761
+ spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
762
+ } catch {}
763
+
764
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
765
+ fs.writeFileSync(plistPath, plist);
766
+
767
+ const result = spawnSync("launchctl", ["load", plistPath], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
768
+ if (result.status === 0) {
769
+ console.log(`Daemon started (${label})`);
770
+ } else {
771
+ console.log(`Daemon plist written to ${plistPath} (launchctl load may need manual run)`);
772
+ }
773
+ }
774
+
671
775
  function upsertHookJson(filePath, source) {
672
776
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
673
777
  const data = readJson(filePath, {});
@@ -964,6 +1068,7 @@ function help() {
964
1068
  Usage:
965
1069
  monty install --site http://localhost:3000 --team default
966
1070
  monty sync Sync all Claude Code & Codex history with real token counts
1071
+ monty daemon Run background presence reporter (lid state + session count)
967
1072
  monty capture --source test --prompt "hello"
968
1073
  monty run codex exec "prompt"
969
1074
  monty run claude -p "prompt"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monty-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Live AI prompt feed and token leaderboard for your engineering team. Works with Claude Code and Codex CLI.",
5
5
  "license": "MIT",
6
6
  "bin": {