@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 +1 -0
- package/dist/agent.js +108 -0
- package/dist/cli.js +17 -2
- package/dist/scheduler.js +45 -43
- package/dist/storage.js +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
51
|
-
const buildLaunchAgentPlist = (nodePath =
|
|
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 =
|
|
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
|
|
112
|
-
(
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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");
|