@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 +1 -1
- package/dist/agent.js +108 -0
- package/dist/cli.js +10 -0
- package/dist/scheduler.js +20 -15
- package/dist/storage.js +1 -1
- package/package.json +1 -1
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,
|
|
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.
|
|
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 =
|
|
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
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
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("
|
|
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-
|
|
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 (
|
|
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.
|
|
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");
|