@tickrmeter/ai-usage 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@ Privacy-preserving personal subscription monitoring for Codex / ChatGPT and Clau
15
15
  4. The helper detects the Codex desktop app, Codex CLI, and Claude Code, uploads normalized limit metadata, and installs a user-scoped five-minute sync using Task Scheduler, launchd, or a systemd user timer (cron fallback).
16
16
 
17
17
  The initial `npx` command installs a fixed copy under `~/.tickrmeter-ai/app`. Scheduled runs invoke that fixed version, never `npx ...@latest`.
18
- On Windows, Task Scheduler invokes that installed Node CLI directly. The helper does not install or execute hidden VBS, WScript, or PowerShell wrappers.
18
+ On Windows, a standard per-user Startup entry starts the installed Node CLI once at sign-in. The CLI launches one background agent that performs the five-minute sync without recurring console windows. The helper does not install or execute hidden VBS, WScript, or PowerShell wrappers.
19
19
 
20
20
  ## Commands
21
21
 
package/dist/agent.js ADDED
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runBackgroundAgent = exports.runAgentLoop = exports.stopBackgroundAgent = exports.startBackgroundAgent = exports.AGENT_INTERVAL_MS = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const child_process_1 = require("child_process");
10
+ const scheduler_1 = require("./scheduler");
11
+ const storage_1 = require("./storage");
12
+ const sync_1 = require("./sync");
13
+ exports.AGENT_INTERVAL_MS = 5 * 60 * 1000;
14
+ const agentPidPath = () => path_1.default.join((0, storage_1.getDataDir)(), "agent.pid");
15
+ const readAgentPid = () => {
16
+ try {
17
+ const value = Number(fs_1.default.readFileSync(agentPidPath(), "utf8").trim());
18
+ return Number.isInteger(value) && value > 0 ? value : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ };
24
+ const isProcessRunning = (pid) => {
25
+ try {
26
+ process.kill(pid, 0);
27
+ return true;
28
+ }
29
+ catch (error) {
30
+ return error?.code === "EPERM";
31
+ }
32
+ };
33
+ const clearAgentPid = (expectedPid) => {
34
+ if (expectedPid !== undefined && readAgentPid() !== expectedPid)
35
+ return;
36
+ fs_1.default.rmSync(agentPidPath(), { force: true });
37
+ };
38
+ const startBackgroundAgent = (spawner = child_process_1.spawn, nodePath = (0, scheduler_1.resolveNodePath)(), cliPath = (0, storage_1.getInstalledCliPath)()) => {
39
+ const existingPid = readAgentPid();
40
+ if (existingPid && isProcessRunning(existingPid))
41
+ return { started: false, pid: existingPid };
42
+ clearAgentPid();
43
+ const child = spawner(nodePath, [cliPath, "agent"], {
44
+ detached: true,
45
+ windowsHide: true,
46
+ stdio: "ignore",
47
+ });
48
+ if (!child.pid)
49
+ throw new Error("AI_USAGE_AGENT_START_FAILED");
50
+ (0, storage_1.atomicWriteFile)(agentPidPath(), `${child.pid}\n`);
51
+ child.unref();
52
+ return { started: true, pid: child.pid };
53
+ };
54
+ exports.startBackgroundAgent = startBackgroundAgent;
55
+ const stopBackgroundAgent = () => {
56
+ const pid = readAgentPid();
57
+ if (!pid)
58
+ return { stopped: false };
59
+ if (pid !== process.pid && isProcessRunning(pid)) {
60
+ try {
61
+ process.kill(pid);
62
+ }
63
+ catch {
64
+ // The agent also exits when the bridge config is removed.
65
+ }
66
+ }
67
+ clearAgentPid(pid);
68
+ return { stopped: true };
69
+ };
70
+ exports.stopBackgroundAgent = stopBackgroundAgent;
71
+ const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
72
+ const runAgentLoop = async (syncer = sync_1.syncPersonalUsage, shouldContinue = () => Boolean((0, storage_1.readConfig)()), sleeper = wait, intervalMs = exports.AGENT_INTERVAL_MS) => {
73
+ while (shouldContinue()) {
74
+ await sleeper(intervalMs);
75
+ if (!shouldContinue())
76
+ break;
77
+ try {
78
+ await syncer();
79
+ }
80
+ catch {
81
+ // A temporary provider/network error must not stop future syncs.
82
+ }
83
+ }
84
+ };
85
+ exports.runAgentLoop = runAgentLoop;
86
+ const runBackgroundAgent = async () => {
87
+ const existingPid = readAgentPid();
88
+ if (existingPid && existingPid !== process.pid && isProcessRunning(existingPid))
89
+ return;
90
+ (0, storage_1.atomicWriteFile)(agentPidPath(), `${process.pid}\n`);
91
+ const cleanup = () => clearAgentPid(process.pid);
92
+ process.once("exit", cleanup);
93
+ process.once("SIGINT", () => {
94
+ cleanup();
95
+ process.exit(0);
96
+ });
97
+ process.once("SIGTERM", () => {
98
+ cleanup();
99
+ process.exit(0);
100
+ });
101
+ try {
102
+ await (0, exports.runAgentLoop)();
103
+ }
104
+ finally {
105
+ cleanup();
106
+ }
107
+ };
108
+ exports.runBackgroundAgent = runBackgroundAgent;
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const claude_1 = require("./claude");
5
+ const agent_1 = require("./agent");
5
6
  const codex_1 = require("./codex");
6
7
  const http_1 = require("./http");
7
8
  const scheduler_1 = require("./scheduler");
@@ -61,6 +62,8 @@ const connect = async (code, server) => {
61
62
  console.log(`Claude Code capture not installed: ${claudeInstall.code}`);
62
63
  const report = await (0, sync_1.syncPersonalUsage)();
63
64
  report.providers.forEach(printProvider);
65
+ if (process.platform === "win32")
66
+ (0, agent_1.startBackgroundAgent)();
64
67
  if (!report.uploaded.length)
65
68
  console.log("No provider snapshot was available yet. Run tickrmeter-ai doctor for setup help.");
66
69
  };
@@ -73,6 +76,8 @@ const sync = async () => {
73
76
  (0, scheduler_1.ensureScheduler)();
74
77
  }
75
78
  const report = await (0, sync_1.syncPersonalUsage)();
79
+ if (process.platform === "win32")
80
+ (0, agent_1.startBackgroundAgent)();
76
81
  console.log(report.uploaded.length ? `Uploaded: ${report.uploaded.join(", ")}` : "No snapshots available to upload.");
77
82
  report.providers.filter((provider) => !provider.ok).forEach(printProvider);
78
83
  };
@@ -120,6 +125,7 @@ const disconnect = async () => {
120
125
  console.log("Bridge could not be revoked online; local credentials will still be removed.");
121
126
  }
122
127
  }
128
+ (0, agent_1.stopBackgroundAgent)();
123
129
  (0, scheduler_1.uninstallScheduler)();
124
130
  (0, claude_1.uninstallClaudeStatusLine)();
125
131
  (0, storage_1.removeLocalBridgeCredential)();
@@ -149,6 +155,10 @@ const main = async () => {
149
155
  return doctor();
150
156
  if (command === "disconnect")
151
157
  return disconnect();
158
+ if (command === "start-agent")
159
+ return (0, agent_1.startBackgroundAgent)();
160
+ if (command === "agent")
161
+ return (0, agent_1.runBackgroundAgent)();
152
162
  return help();
153
163
  };
154
164
  main().catch((error) => {
package/dist/scheduler.js CHANGED
@@ -3,17 +3,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.uninstallScheduler = exports.ensureScheduler = exports.installScheduler = exports.buildCronLine = exports.buildSystemdTimer = exports.buildSystemdService = exports.buildLaunchAgentPlist = exports.buildWindowsTaskArgs = exports.getFixedSyncCommand = exports.resolveNodePath = exports.SCHEDULER_VERSION = exports.CRON_MARKER = exports.SYSTEMD_NAME = exports.LAUNCH_AGENT_LABEL = exports.WINDOWS_TASK_NAME = void 0;
6
+ exports.uninstallScheduler = exports.ensureScheduler = exports.installScheduler = exports.buildCronLine = exports.buildSystemdTimer = exports.buildSystemdService = exports.buildLaunchAgentPlist = exports.buildWindowsRunArgs = exports.getFixedAgentStartCommand = exports.getFixedSyncCommand = exports.resolveNodePath = exports.SCHEDULER_VERSION = exports.CRON_MARKER = exports.SYSTEMD_NAME = exports.LAUNCH_AGENT_LABEL = exports.WINDOWS_RUN_VALUE_NAME = exports.WINDOWS_RUN_KEY = exports.WINDOWS_TASK_NAME = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const os_1 = __importDefault(require("os"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const child_process_1 = require("child_process");
11
11
  const storage_1 = require("./storage");
12
12
  exports.WINDOWS_TASK_NAME = "TickrMeter AI Usage Sync";
13
+ exports.WINDOWS_RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
14
+ exports.WINDOWS_RUN_VALUE_NAME = "TickrMeter AI Usage";
13
15
  exports.LAUNCH_AGENT_LABEL = "com.tickrmeter.ai-usage";
14
16
  exports.SYSTEMD_NAME = "tickrmeter-ai-usage";
15
17
  exports.CRON_MARKER = "# TickrMeter AI Usage Sync";
16
- exports.SCHEDULER_VERSION = 3;
18
+ exports.SCHEDULER_VERSION = 5;
17
19
  const quoteShell = (value) => `"${value.replace(/"/g, '\\"')}"`;
18
20
  const resolveNodePath = (nodePath = process.execPath) => {
19
21
  try {
@@ -26,20 +28,21 @@ const resolveNodePath = (nodePath = process.execPath) => {
26
28
  exports.resolveNodePath = resolveNodePath;
27
29
  const getFixedSyncCommand = (nodePath = (0, exports.resolveNodePath)(), cliPath = (0, storage_1.getInstalledCliPath)()) => `${quoteShell(nodePath)} ${quoteShell(cliPath)} sync`;
28
30
  exports.getFixedSyncCommand = getFixedSyncCommand;
31
+ const getFixedAgentStartCommand = (nodePath = (0, exports.resolveNodePath)(), cliPath = (0, storage_1.getInstalledCliPath)()) => `${quoteShell(nodePath)} ${quoteShell(cliPath)} start-agent`;
32
+ exports.getFixedAgentStartCommand = getFixedAgentStartCommand;
29
33
  const legacyWindowsSyncScriptPath = () => path_1.default.join((0, storage_1.getDataDir)(), "sync-hidden.vbs");
30
- const buildWindowsTaskArgs = (command = (0, exports.getFixedSyncCommand)()) => [
31
- "/Create",
32
- "/TN",
33
- exports.WINDOWS_TASK_NAME,
34
- "/TR",
34
+ const buildWindowsRunArgs = (command = (0, exports.getFixedAgentStartCommand)()) => [
35
+ "ADD",
36
+ exports.WINDOWS_RUN_KEY,
37
+ "/V",
38
+ exports.WINDOWS_RUN_VALUE_NAME,
39
+ "/T",
40
+ "REG_SZ",
41
+ "/D",
35
42
  command,
36
- "/SC",
37
- "MINUTE",
38
- "/MO",
39
- "5",
40
43
  "/F",
41
44
  ];
42
- exports.buildWindowsTaskArgs = buildWindowsTaskArgs;
45
+ exports.buildWindowsRunArgs = buildWindowsRunArgs;
43
46
  const buildLaunchAgentPlist = (nodePath = (0, exports.resolveNodePath)(), cliPath = (0, storage_1.getInstalledCliPath)()) => `<?xml version="1.0" encoding="UTF-8"?>
44
47
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
45
48
  <plist version="1.0"><dict>
@@ -100,11 +103,12 @@ const installCron = (runner = run) => {
100
103
  const installScheduler = (platform = process.platform, runner = run) => {
101
104
  let kind;
102
105
  if (platform === "win32") {
103
- const result = runner("schtasks", (0, exports.buildWindowsTaskArgs)());
106
+ const result = runner("reg", (0, exports.buildWindowsRunArgs)());
104
107
  if (result.status !== 0)
105
108
  throw new Error("AI_USAGE_SCHEDULER_INSTALL_FAILED");
109
+ runner("schtasks", ["/Delete", "/TN", exports.WINDOWS_TASK_NAME, "/F"]);
106
110
  fs_1.default.rmSync(legacyWindowsSyncScriptPath(), { force: true });
107
- kind = "windows-task";
111
+ kind = "windows-startup";
108
112
  }
109
113
  else if (platform === "darwin") {
110
114
  const file = launchAgentPath();
@@ -145,7 +149,8 @@ exports.ensureScheduler = ensureScheduler;
145
149
  const uninstallScheduler = (runner = run) => {
146
150
  const state = (0, storage_1.readState)();
147
151
  const kind = state.scheduler?.kind;
148
- if (kind === "windows-task") {
152
+ if (state.scheduler?.platform === "win32") {
153
+ runner("reg", ["DELETE", exports.WINDOWS_RUN_KEY, "/V", exports.WINDOWS_RUN_VALUE_NAME, "/F"]);
149
154
  runner("schtasks", ["/Delete", "/TN", exports.WINDOWS_TASK_NAME, "/F"]);
150
155
  }
151
156
  fs_1.default.rmSync(legacyWindowsSyncScriptPath(), { force: true });
package/dist/storage.js CHANGED
@@ -7,7 +7,7 @@ exports.removeLocalBridgeCredential = exports.installFixedCliCopy = exports.writ
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const os_1 = __importDefault(require("os"));
9
9
  const path_1 = __importDefault(require("path"));
10
- exports.CLI_VERSION = "0.1.2";
10
+ exports.CLI_VERSION = "0.1.3";
11
11
  const getDataDir = () => process.env.TICKRMETER_AI_HOME || path_1.default.join(os_1.default.homedir(), ".tickrmeter-ai");
12
12
  exports.getDataDir = getDataDir;
13
13
  const getConfigPath = () => path_1.default.join((0, exports.getDataDir)(), "config.json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tickrmeter/ai-usage",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Privacy-preserving TickrMeter personal AI usage bridge",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {