@tickrmeter/ai-usage 0.1.0 → 0.1.2

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, Task Scheduler invokes that installed Node CLI directly. The helper does not install or execute hidden VBS, WScript, or PowerShell wrappers.
18
19
 
19
20
  ## Commands
20
21
 
package/dist/claude.js CHANGED
@@ -41,7 +41,19 @@ const finitePercent = (value) => typeof value === "number" && Number.isFinite(va
41
41
  const resetIso = (value) => {
42
42
  if (typeof value !== "string" && typeof value !== "number")
43
43
  return undefined;
44
- const date = new Date(value);
44
+ const normalized = typeof value === "string" ? value.trim() : value;
45
+ if (normalized === "")
46
+ return undefined;
47
+ const numeric = typeof normalized === "number" ? normalized : Number(normalized);
48
+ const isNumericTimestamp = Number.isFinite(numeric) && /^\d+(\.\d+)?$/.test(String(normalized));
49
+ let date = isNumericTimestamp
50
+ ? new Date(numeric < 10_000_000_000 ? numeric * 1000 : numeric)
51
+ : new Date(normalized);
52
+ // Versions before 0.1.1 interpreted Unix seconds as milliseconds and
53
+ // persisted a 1970 ISO date. Recover that timestamp during the next sync.
54
+ if (!isNumericTimestamp && date.getTime() >= 1_000_000_000 && date.getTime() < 10_000_000_000) {
55
+ date = new Date(date.getTime() * 1000);
56
+ }
45
57
  return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
46
58
  };
47
59
  const extractClaudeSnapshot = (input, capturedAt = new Date()) => {
@@ -80,7 +92,8 @@ const readClaudeCapture = () => {
80
92
  const snapshot = (0, storage_1.readJson)((0, storage_1.getClaudeCapturePath)());
81
93
  if (!snapshot || snapshot.source !== "claude" || Number.isNaN(Date.parse(snapshot.capturedAt)))
82
94
  return null;
83
- return { ...snapshot, stale: Date.now() - Date.parse(snapshot.capturedAt) > 60 * 60 * 1000 };
95
+ const windows = snapshot.windows?.map((window) => ({ ...window, resetsAt: resetIso(window.resetsAt) }));
96
+ return { ...snapshot, windows, stale: Date.now() - Date.parse(snapshot.capturedAt) > 60 * 60 * 1000 };
84
97
  };
85
98
  exports.readClaudeCapture = readClaudeCapture;
86
99
  const quote = (value) => `"${value.replace(/"/g, '\\"')}"`;
package/dist/cli.js CHANGED
@@ -65,8 +65,13 @@ const connect = async (code, server) => {
65
65
  console.log("No provider snapshot was available yet. Run tickrmeter-ai doctor for setup help.");
66
66
  };
67
67
  const sync = async () => {
68
- if ((0, storage_1.readConfig)())
69
- (0, storage_1.installFixedCliCopy)();
68
+ if ((0, storage_1.readConfig)()) {
69
+ const installedCliPath = (0, storage_1.installFixedCliCopy)();
70
+ if (installedCliPath)
71
+ (0, scheduler_1.installScheduler)();
72
+ else
73
+ (0, scheduler_1.ensureScheduler)();
74
+ }
70
75
  const report = await (0, sync_1.syncPersonalUsage)();
71
76
  console.log(report.uploaded.length ? `Uploaded: ${report.uploaded.join(", ")}` : "No snapshots available to upload.");
72
77
  report.providers.filter((provider) => !provider.ok).forEach(printProvider);
package/dist/scheduler.js CHANGED
@@ -3,7 +3,7 @@ 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.buildWindowsTaskArgs = exports.getFixedSyncCommand = exports.resolveNodePath = exports.SCHEDULER_VERSION = exports.CRON_MARKER = exports.SYSTEMD_NAME = exports.LAUNCH_AGENT_LABEL = 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"));
@@ -13,29 +13,21 @@ exports.WINDOWS_TASK_NAME = "TickrMeter AI Usage Sync";
13
13
  exports.LAUNCH_AGENT_LABEL = "com.tickrmeter.ai-usage";
14
14
  exports.SYSTEMD_NAME = "tickrmeter-ai-usage";
15
15
  exports.CRON_MARKER = "# TickrMeter AI Usage Sync";
16
+ exports.SCHEDULER_VERSION = 3;
16
17
  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)}`;
18
+ const resolveNodePath = (nodePath = process.execPath) => {
19
+ try {
20
+ return fs_1.default.realpathSync(nodePath);
21
+ }
22
+ catch {
23
+ return nodePath;
24
+ }
36
25
  };
37
- exports.getWindowsTaskCommand = getWindowsTaskCommand;
38
- const buildWindowsTaskArgs = (command = (0, exports.getWindowsTaskCommand)()) => [
26
+ exports.resolveNodePath = resolveNodePath;
27
+ const getFixedSyncCommand = (nodePath = (0, exports.resolveNodePath)(), cliPath = (0, storage_1.getInstalledCliPath)()) => `${quoteShell(nodePath)} ${quoteShell(cliPath)} sync`;
28
+ exports.getFixedSyncCommand = getFixedSyncCommand;
29
+ const legacyWindowsSyncScriptPath = () => path_1.default.join((0, storage_1.getDataDir)(), "sync-hidden.vbs");
30
+ const buildWindowsTaskArgs = (command = (0, exports.getFixedSyncCommand)()) => [
39
31
  "/Create",
40
32
  "/TN",
41
33
  exports.WINDOWS_TASK_NAME,
@@ -48,7 +40,7 @@ const buildWindowsTaskArgs = (command = (0, exports.getWindowsTaskCommand)()) =>
48
40
  "/F",
49
41
  ];
50
42
  exports.buildWindowsTaskArgs = buildWindowsTaskArgs;
51
- const buildLaunchAgentPlist = (nodePath = process.execPath, cliPath = (0, storage_1.getInstalledCliPath)()) => `<?xml version="1.0" encoding="UTF-8"?>
43
+ const buildLaunchAgentPlist = (nodePath = (0, exports.resolveNodePath)(), cliPath = (0, storage_1.getInstalledCliPath)()) => `<?xml version="1.0" encoding="UTF-8"?>
52
44
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
53
45
  <plist version="1.0"><dict>
54
46
  <key>Label</key><string>${exports.LAUNCH_AGENT_LABEL}</string>
@@ -58,7 +50,7 @@ const buildLaunchAgentPlist = (nodePath = process.execPath, cliPath = (0, storag
58
50
  </dict></plist>
59
51
  `;
60
52
  exports.buildLaunchAgentPlist = buildLaunchAgentPlist;
61
- const buildSystemdService = (nodePath = process.execPath, cliPath = (0, storage_1.getInstalledCliPath)()) => `[Unit]
53
+ const buildSystemdService = (nodePath = (0, exports.resolveNodePath)(), cliPath = (0, storage_1.getInstalledCliPath)()) => `[Unit]
62
54
  Description=TickrMeter AI Usage sync
63
55
 
64
56
  [Service]
@@ -108,13 +100,10 @@ const installCron = (runner = run) => {
108
100
  const installScheduler = (platform = process.platform, runner = run) => {
109
101
  let kind;
110
102
  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 });
103
+ const result = runner("schtasks", (0, exports.buildWindowsTaskArgs)());
104
+ if (result.status !== 0)
116
105
  throw new Error("AI_USAGE_SCHEDULER_INSTALL_FAILED");
117
- }
106
+ fs_1.default.rmSync(legacyWindowsSyncScriptPath(), { force: true });
118
107
  kind = "windows-task";
119
108
  }
120
109
  else if (platform === "darwin") {
@@ -141,17 +130,25 @@ const installScheduler = (platform = process.platform, runner = run) => {
141
130
  }
142
131
  }
143
132
  const state = (0, storage_1.readState)();
144
- (0, storage_1.writeState)({ ...state, scheduler: { platform, kind, installedAt: new Date().toISOString() } });
133
+ (0, storage_1.writeState)({ ...state, scheduler: { platform, kind, installedAt: new Date().toISOString(), version: exports.SCHEDULER_VERSION } });
145
134
  return { kind };
146
135
  };
147
136
  exports.installScheduler = installScheduler;
137
+ const ensureScheduler = (platform = process.platform, runner = run) => {
138
+ const current = (0, storage_1.readState)().scheduler;
139
+ if (current?.version === exports.SCHEDULER_VERSION)
140
+ return { kind: current.kind, migrated: false };
141
+ const result = (0, exports.installScheduler)(platform, runner);
142
+ return { ...result, migrated: Boolean(current) };
143
+ };
144
+ exports.ensureScheduler = ensureScheduler;
148
145
  const uninstallScheduler = (runner = run) => {
149
146
  const state = (0, storage_1.readState)();
150
147
  const kind = state.scheduler?.kind;
151
148
  if (kind === "windows-task") {
152
149
  runner("schtasks", ["/Delete", "/TN", exports.WINDOWS_TASK_NAME, "/F"]);
153
- fs_1.default.rmSync(windowsSyncScriptPath(), { force: true });
154
150
  }
151
+ fs_1.default.rmSync(legacyWindowsSyncScriptPath(), { force: true });
155
152
  if (kind === "launch-agent") {
156
153
  runner("launchctl", ["unload", launchAgentPath()]);
157
154
  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.0";
10
+ exports.CLI_VERSION = "0.1.2";
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.0",
3
+ "version": "0.1.2",
4
4
  "description": "Privacy-preserving TickrMeter personal AI usage bridge",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {