@tickrmeter/ai-usage 0.1.1 → 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,6 +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, 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.
18
19
 
19
20
  ## Commands
20
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,13 +62,22 @@ 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
  };
67
70
  const sync = async () => {
68
- if ((0, storage_1.readConfig)())
69
- (0, storage_1.installFixedCliCopy)();
71
+ if ((0, storage_1.readConfig)()) {
72
+ const installedCliPath = (0, storage_1.installFixedCliCopy)();
73
+ if (installedCliPath)
74
+ (0, scheduler_1.installScheduler)();
75
+ else
76
+ (0, scheduler_1.ensureScheduler)();
77
+ }
70
78
  const report = await (0, sync_1.syncPersonalUsage)();
79
+ if (process.platform === "win32")
80
+ (0, agent_1.startBackgroundAgent)();
71
81
  console.log(report.uploaded.length ? `Uploaded: ${report.uploaded.join(", ")}` : "No snapshots available to upload.");
72
82
  report.providers.filter((provider) => !provider.ok).forEach(printProvider);
73
83
  };
@@ -115,6 +125,7 @@ const disconnect = async () => {
115
125
  console.log("Bridge could not be revoked online; local credentials will still be removed.");
116
126
  }
117
127
  }
128
+ (0, agent_1.stopBackgroundAgent)();
118
129
  (0, scheduler_1.uninstallScheduler)();
119
130
  (0, claude_1.uninstallClaudeStatusLine)();
120
131
  (0, storage_1.removeLocalBridgeCredential)();
@@ -144,6 +155,10 @@ const main = async () => {
144
155
  return doctor();
145
156
  if (command === "disconnect")
146
157
  return disconnect();
158
+ if (command === "start-agent")
159
+ return (0, agent_1.startBackgroundAgent)();
160
+ if (command === "agent")
161
+ return (0, agent_1.runBackgroundAgent)();
147
162
  return help();
148
163
  };
149
164
  main().catch((error) => {
package/dist/scheduler.js CHANGED
@@ -3,52 +3,47 @@ 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.installScheduler = exports.buildCronLine = exports.buildSystemdTimer = exports.buildSystemdService = exports.buildLaunchAgentPlist = exports.buildWindowsTaskArgs = exports.getWindowsTaskCommand = exports.buildWindowsSyncScript = exports.getFixedSyncCommand = 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";
18
+ exports.SCHEDULER_VERSION = 5;
16
19
  const quoteShell = (value) => `"${value.replace(/"/g, '\\"')}"`;
17
- const getFixedSyncCommand = (nodePath = process.execPath, cliPath = (0, storage_1.getInstalledCliPath)()) => `${quoteShell(nodePath)} ${quoteShell(cliPath)} sync`;
18
- exports.getFixedSyncCommand = getFixedSyncCommand;
19
- const quoteVbsString = (value) => `"${value.replace(/"/g, '""')}"`;
20
- const windowsSyncScriptPath = () => path_1.default.join((0, storage_1.getDataDir)(), "sync-hidden.vbs");
21
- const buildWindowsSyncScript = (nodePath = process.execPath, cliPath = (0, storage_1.getInstalledCliPath)()) => {
22
- const command = `${quoteShell(nodePath)} ${quoteShell(cliPath)} sync`;
23
- return [
24
- 'Set shell = CreateObject("WScript.Shell")',
25
- `command = ${quoteVbsString(command)}`,
26
- "exitCode = shell.Run(command, 0, True)",
27
- "WScript.Quit exitCode",
28
- "",
29
- ].join("\r\n");
30
- };
31
- exports.buildWindowsSyncScript = buildWindowsSyncScript;
32
- const getWindowsTaskCommand = (scriptPath = windowsSyncScriptPath()) => {
33
- const windowsDirectory = process.env.SystemRoot || "C:\\Windows";
34
- const wscriptPath = path_1.default.join(windowsDirectory, "System32", "wscript.exe");
35
- return `${quoteShell(wscriptPath)} //B //NoLogo ${quoteShell(scriptPath)}`;
20
+ const resolveNodePath = (nodePath = process.execPath) => {
21
+ try {
22
+ return fs_1.default.realpathSync(nodePath);
23
+ }
24
+ catch {
25
+ return nodePath;
26
+ }
36
27
  };
37
- exports.getWindowsTaskCommand = getWindowsTaskCommand;
38
- const buildWindowsTaskArgs = (command = (0, exports.getWindowsTaskCommand)()) => [
39
- "/Create",
40
- "/TN",
41
- exports.WINDOWS_TASK_NAME,
42
- "/TR",
28
+ exports.resolveNodePath = resolveNodePath;
29
+ const getFixedSyncCommand = (nodePath = (0, exports.resolveNodePath)(), cliPath = (0, storage_1.getInstalledCliPath)()) => `${quoteShell(nodePath)} ${quoteShell(cliPath)} sync`;
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;
33
+ const legacyWindowsSyncScriptPath = () => path_1.default.join((0, storage_1.getDataDir)(), "sync-hidden.vbs");
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",
43
42
  command,
44
- "/SC",
45
- "MINUTE",
46
- "/MO",
47
- "5",
48
43
  "/F",
49
44
  ];
50
- exports.buildWindowsTaskArgs = buildWindowsTaskArgs;
51
- const buildLaunchAgentPlist = (nodePath = process.execPath, cliPath = (0, storage_1.getInstalledCliPath)()) => `<?xml version="1.0" encoding="UTF-8"?>
45
+ exports.buildWindowsRunArgs = buildWindowsRunArgs;
46
+ const buildLaunchAgentPlist = (nodePath = (0, exports.resolveNodePath)(), cliPath = (0, storage_1.getInstalledCliPath)()) => `<?xml version="1.0" encoding="UTF-8"?>
52
47
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
53
48
  <plist version="1.0"><dict>
54
49
  <key>Label</key><string>${exports.LAUNCH_AGENT_LABEL}</string>
@@ -58,7 +53,7 @@ const buildLaunchAgentPlist = (nodePath = process.execPath, cliPath = (0, storag
58
53
  </dict></plist>
59
54
  `;
60
55
  exports.buildLaunchAgentPlist = buildLaunchAgentPlist;
61
- const buildSystemdService = (nodePath = process.execPath, cliPath = (0, storage_1.getInstalledCliPath)()) => `[Unit]
56
+ const buildSystemdService = (nodePath = (0, exports.resolveNodePath)(), cliPath = (0, storage_1.getInstalledCliPath)()) => `[Unit]
62
57
  Description=TickrMeter AI Usage sync
63
58
 
64
59
  [Service]
@@ -108,14 +103,12 @@ const installCron = (runner = run) => {
108
103
  const installScheduler = (platform = process.platform, runner = run) => {
109
104
  let kind;
110
105
  if (platform === "win32") {
111
- const scriptPath = windowsSyncScriptPath();
112
- (0, storage_1.atomicWriteFile)(scriptPath, (0, exports.buildWindowsSyncScript)());
113
- const result = runner("schtasks", (0, exports.buildWindowsTaskArgs)((0, exports.getWindowsTaskCommand)(scriptPath)));
114
- if (result.status !== 0) {
115
- fs_1.default.rmSync(scriptPath, { force: true });
106
+ const result = runner("reg", (0, exports.buildWindowsRunArgs)());
107
+ if (result.status !== 0)
116
108
  throw new Error("AI_USAGE_SCHEDULER_INSTALL_FAILED");
117
- }
118
- kind = "windows-task";
109
+ runner("schtasks", ["/Delete", "/TN", exports.WINDOWS_TASK_NAME, "/F"]);
110
+ fs_1.default.rmSync(legacyWindowsSyncScriptPath(), { force: true });
111
+ kind = "windows-startup";
119
112
  }
120
113
  else if (platform === "darwin") {
121
114
  const file = launchAgentPath();
@@ -141,17 +134,26 @@ const installScheduler = (platform = process.platform, runner = run) => {
141
134
  }
142
135
  }
143
136
  const state = (0, storage_1.readState)();
144
- (0, storage_1.writeState)({ ...state, scheduler: { platform, kind, installedAt: new Date().toISOString() } });
137
+ (0, storage_1.writeState)({ ...state, scheduler: { platform, kind, installedAt: new Date().toISOString(), version: exports.SCHEDULER_VERSION } });
145
138
  return { kind };
146
139
  };
147
140
  exports.installScheduler = installScheduler;
141
+ const ensureScheduler = (platform = process.platform, runner = run) => {
142
+ const current = (0, storage_1.readState)().scheduler;
143
+ if (current?.version === exports.SCHEDULER_VERSION)
144
+ return { kind: current.kind, migrated: false };
145
+ const result = (0, exports.installScheduler)(platform, runner);
146
+ return { ...result, migrated: Boolean(current) };
147
+ };
148
+ exports.ensureScheduler = ensureScheduler;
148
149
  const uninstallScheduler = (runner = run) => {
149
150
  const state = (0, storage_1.readState)();
150
151
  const kind = state.scheduler?.kind;
151
- 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"]);
152
154
  runner("schtasks", ["/Delete", "/TN", exports.WINDOWS_TASK_NAME, "/F"]);
153
- fs_1.default.rmSync(windowsSyncScriptPath(), { force: true });
154
155
  }
156
+ fs_1.default.rmSync(legacyWindowsSyncScriptPath(), { force: true });
155
157
  if (kind === "launch-agent") {
156
158
  runner("launchctl", ["unload", launchAgentPath()]);
157
159
  fs_1.default.rmSync(launchAgentPath(), { 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.1";
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.1",
3
+ "version": "0.1.3",
4
4
  "description": "Privacy-preserving TickrMeter personal AI usage bridge",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {