tokencanary 0.1.2 → 0.1.4
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 +12 -8
- package/dist/{chunk-7J7NAB7G.js → chunk-66ONBXHK.js} +2 -2
- package/dist/chunk-DCCFWI4O.js +230 -0
- package/dist/chunk-DCCFWI4O.js.map +1 -0
- package/dist/{chunk-XZJP3JGL.js → chunk-IUASFEXE.js} +2 -2
- package/dist/cli.js +15 -9
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +76 -4
- package/dist/daemon.js.map +1 -1
- package/dist/{install-WJNHGXM4.js → install-NA7R44O4.js} +79 -13
- package/dist/install-NA7R44O4.js.map +1 -0
- package/dist/{license-Y2YM32GO.js → license-VKOP67LZ.js} +28 -4
- package/dist/license-VKOP67LZ.js.map +1 -0
- package/dist/{logs-MTV2LXO2.js → logs-U4OOGTGV.js} +2 -2
- package/dist/stats-7NLMKM67.js +32 -0
- package/dist/stats-7NLMKM67.js.map +1 -0
- package/dist/status-2EAJQ5TH.js +57 -0
- package/dist/status-2EAJQ5TH.js.map +1 -0
- package/dist/stop-QJSJTHHO.js +8 -0
- package/dist/{uninstall-7IO2C4AA.js → uninstall-WUG7BEBH.js} +3 -3
- package/dist/{upgrade-DTXXOQPO.js → upgrade-IR4MHUD2.js} +7 -4
- package/dist/upgrade-IR4MHUD2.js.map +1 -0
- package/package.json +4 -1
- package/dist/chunk-ULNEBCOY.js +0 -137
- package/dist/chunk-ULNEBCOY.js.map +0 -1
- package/dist/install-WJNHGXM4.js.map +0 -1
- package/dist/license-Y2YM32GO.js.map +0 -1
- package/dist/status-X2ZAE5UB.js +0 -24
- package/dist/status-X2ZAE5UB.js.map +0 -1
- package/dist/stop-NM5OTQVV.js +0 -8
- package/dist/upgrade-DTXXOQPO.js.map +0 -1
- /package/dist/{chunk-7J7NAB7G.js.map → chunk-66ONBXHK.js.map} +0 -0
- /package/dist/{chunk-XZJP3JGL.js.map → chunk-IUASFEXE.js.map} +0 -0
- /package/dist/{logs-MTV2LXO2.js.map → logs-U4OOGTGV.js.map} +0 -0
- /package/dist/{stop-NM5OTQVV.js.map → stop-QJSJTHHO.js.map} +0 -0
- /package/dist/{uninstall-7IO2C4AA.js.map → uninstall-WUG7BEBH.js.map} +0 -0
package/README.md
CHANGED
|
@@ -37,11 +37,11 @@ tokencanary setup
|
|
|
37
37
|
claude
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
`tokencanary setup` checks your environment, starts your trial, writes config to `~/.tokencanary/`, and configures Claude Code hooks automatically when possible. If auto-config isn’t available, it prints clear instructions.
|
|
40
|
+
`tokencanary setup` checks your environment, starts your trial (via our servers), writes config to `~/.tokencanary/`, and configures Claude Code hooks automatically when possible. If auto-config isn’t available, it prints clear instructions. An internet connection is required for setup and for refreshing license status.
|
|
41
41
|
|
|
42
42
|
## How it works
|
|
43
43
|
|
|
44
|
-
The CLI installs a small daemon and writes a hook config file. Claude Code sends prompts through the configured URLs; Token Canary compresses them locally and forwards the result. All processing stays on your machine
|
|
44
|
+
The CLI installs a small daemon and writes a hook config file. Claude Code sends prompts through the configured URLs; Token Canary compresses them locally and forwards the result. All prompt processing stays on your machine. Trial and license are validated by our servers; the CLI and daemon cache license status and can use it offline for a short grace period.
|
|
45
45
|
|
|
46
46
|
## Commands
|
|
47
47
|
|
|
@@ -53,27 +53,31 @@ The CLI installs a small daemon and writes a hook config file. Claude Code sends
|
|
|
53
53
|
| `tokencanary stop` | Stop the daemon |
|
|
54
54
|
| `tokencanary uninstall` | Stop daemon and remove `~/.tokencanary` |
|
|
55
55
|
| `tokencanary status` | Daemon and license status |
|
|
56
|
+
| `tokencanary stats` | Session savings (requests optimized, chars reduced) |
|
|
56
57
|
| `tokencanary logs` | Recent logs (`~/.tokencanary/logs/`) |
|
|
57
58
|
| `tokencanary upgrade` | Open browser to subscribe ($19/mo or $99/yr) |
|
|
58
59
|
| `tokencanary license` | License and trial info |
|
|
59
60
|
|
|
60
61
|
## Trial and pricing
|
|
61
62
|
|
|
62
|
-
- **Trial:** 30 days from first
|
|
63
|
-
- **After trial:** Optimization is disabled; Claude Code works normally. You are prompted to upgrade.
|
|
64
|
-
- **Pricing:** $19/month or $99/year. Run `tokencanary upgrade` to open the checkout.
|
|
63
|
+
- **Trial:** 30 days from first `tokencanary setup`, full optimization. Trial is created and validated by our servers; one trial per machine.
|
|
64
|
+
- **After trial:** Optimization is disabled; Claude Code works normally. You are prompted to upgrade.
|
|
65
|
+
- **Pricing:** $19/month or $99/year. Run `tokencanary upgrade` to open the checkout in your browser.
|
|
65
66
|
|
|
66
67
|
## Privacy and security
|
|
67
68
|
|
|
68
69
|
- Prompt processing runs locally. Only compressed payloads are sent to Claude as part of normal Claude Code usage.
|
|
69
|
-
-
|
|
70
|
-
- Config and logs are stored under `~/.tokencanary/` on your machine.
|
|
70
|
+
- Setup and license checks communicate with Token Canary servers (install ID, machine ID, plan, expiry). No prompt content is sent.
|
|
71
|
+
- Config and logs are stored under `~/.tokencanary/` on your machine. Session stats (for `tokencanary stats`) are in `~/.tokencanary/stats.json` and represent the current daemon session; they reset when the daemon restarts.
|
|
71
72
|
|
|
72
73
|
## Troubleshooting
|
|
73
74
|
|
|
74
75
|
- **`tokencanary doctor` fails:** Ensure Node.js 18+, Claude Code on PATH, and that `~/.tokencanary` (or the reported config directory) is writable.
|
|
76
|
+
- **"Could not start trial" on setup:** An internet connection is required; check your network and try again.
|
|
75
77
|
- **Hooks not applied:** Run `tokencanary setup` again to try auto-config, or add the hook URLs from `~/.tokencanary/hooks.json` to Claude Code (e.g. via `/hooks` or `~/.claude/settings.json`).
|
|
76
|
-
- **Daemon not running:** Run `tokencanary status`; if needed run `tokencanary setup` again.
|
|
78
|
+
- **Daemon not running:** Run `tokencanary status`; if needed run `tokencanary setup` again.
|
|
79
|
+
- **No stats yet:** Run `tokencanary stats` after using Claude Code with optimization enabled; stats reset when the daemon restarts. Use `tokencanary stop` then `tokencanary setup` to restart. Check `tokencanary logs` for errors.
|
|
80
|
+
- **Hooks and Cursor/Claude Code:** Token Canary hooks are designed to **fail open**. If the daemon is down, the hook config is missing, or the request times out, prompts are passed through unchanged and the host is never blocked. You only get optimization when the daemon is running and responds successfully.
|
|
77
81
|
|
|
78
82
|
## Support
|
|
79
83
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getConfigDir
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-DCCFWI4O.js";
|
|
4
4
|
|
|
5
5
|
// src/checks/claude.ts
|
|
6
6
|
import { execSync } from "child_process";
|
|
@@ -87,4 +87,4 @@ export {
|
|
|
87
87
|
runDoctor,
|
|
88
88
|
runAllChecks
|
|
89
89
|
};
|
|
90
|
-
//# sourceMappingURL=chunk-
|
|
90
|
+
//# sourceMappingURL=chunk-66ONBXHK.js.map
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// ../shared/dist/constants.js
|
|
2
|
+
var APP_NAME = "tokencanary";
|
|
3
|
+
function getLogDir() {
|
|
4
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? process.env.TMP ?? "/tmp";
|
|
5
|
+
return `${home}/.${APP_NAME}/logs`;
|
|
6
|
+
}
|
|
7
|
+
function getConfigDir() {
|
|
8
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? process.env.TMP ?? "/tmp";
|
|
9
|
+
return `${home}/.${APP_NAME}`;
|
|
10
|
+
}
|
|
11
|
+
var TRIAL_DAYS = 30;
|
|
12
|
+
var OFFLINE_GRACE_DAYS = 3;
|
|
13
|
+
var DEFAULT_DAEMON_PORT = 3847;
|
|
14
|
+
var LICENSE_FILENAME = "license.json";
|
|
15
|
+
var STATS_FILENAME = "stats.json";
|
|
16
|
+
var CONFIG_FILENAME = "config.json";
|
|
17
|
+
function getLicensePath() {
|
|
18
|
+
return `${getConfigDir()}/${LICENSE_FILENAME}`;
|
|
19
|
+
}
|
|
20
|
+
function getStatsPath() {
|
|
21
|
+
return `${getConfigDir()}/${STATS_FILENAME}`;
|
|
22
|
+
}
|
|
23
|
+
function getConfigPath() {
|
|
24
|
+
return `${getConfigDir()}/${CONFIG_FILENAME}`;
|
|
25
|
+
}
|
|
26
|
+
var DEFAULT_API_URL = "https://www.tokencanary.com";
|
|
27
|
+
function getApiBaseUrl() {
|
|
28
|
+
return process.env.TOKENCANARY_API_URL ?? DEFAULT_API_URL;
|
|
29
|
+
}
|
|
30
|
+
var DEFAULT_UPGRADE_URL = `${DEFAULT_API_URL}/upgrade`;
|
|
31
|
+
function getUpgradeUrl(installId) {
|
|
32
|
+
const base = process.env.TOKENCANARY_UPGRADE_URL ?? DEFAULT_UPGRADE_URL;
|
|
33
|
+
if (!installId)
|
|
34
|
+
return base;
|
|
35
|
+
const u = new URL(base);
|
|
36
|
+
u.searchParams.set("install_id", installId);
|
|
37
|
+
return u.toString();
|
|
38
|
+
}
|
|
39
|
+
var HOOK_TIMEOUT_MS = 5e3;
|
|
40
|
+
|
|
41
|
+
// ../shared/dist/fingerprint.js
|
|
42
|
+
import { createHash } from "crypto";
|
|
43
|
+
import { hostname, platform, arch, cpus, networkInterfaces } from "os";
|
|
44
|
+
function getMachineId() {
|
|
45
|
+
const parts = [];
|
|
46
|
+
try {
|
|
47
|
+
parts.push(hostname());
|
|
48
|
+
} catch {
|
|
49
|
+
parts.push("unknown-host");
|
|
50
|
+
}
|
|
51
|
+
parts.push(platform());
|
|
52
|
+
parts.push(arch());
|
|
53
|
+
try {
|
|
54
|
+
const cpu = cpus()[0];
|
|
55
|
+
parts.push(cpu?.model ?? "unknown-cpu");
|
|
56
|
+
} catch {
|
|
57
|
+
parts.push("unknown-cpu");
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const nets = networkInterfaces();
|
|
61
|
+
const addrs = [];
|
|
62
|
+
for (const name of Object.keys(nets)) {
|
|
63
|
+
const list = nets[name];
|
|
64
|
+
if (list) {
|
|
65
|
+
for (const iface of list) {
|
|
66
|
+
if (iface.address && !iface.internal)
|
|
67
|
+
addrs.push(name + ":" + iface.address);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
addrs.sort();
|
|
72
|
+
if (addrs.length > 0) {
|
|
73
|
+
parts.push(createHash("sha256").update(addrs.join(",")).digest("hex").slice(0, 16));
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
return createHash("sha256").update(parts.join("|")).digest("hex");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ../shared/dist/license.js
|
|
81
|
+
var MS_PER_DAY = 86400 * 1e3;
|
|
82
|
+
function isLicenseValid(license, now = /* @__PURE__ */ new Date(), options) {
|
|
83
|
+
if (!license)
|
|
84
|
+
return false;
|
|
85
|
+
const expiry = new Date(license.expiry);
|
|
86
|
+
if (expiry > now)
|
|
87
|
+
return true;
|
|
88
|
+
if (options?.lastKnownValid) {
|
|
89
|
+
const graceEnd = new Date(options.lastKnownValid.getTime() + OFFLINE_GRACE_DAYS * MS_PER_DAY);
|
|
90
|
+
if (now < graceEnd)
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
function getPlanDisplay(plan) {
|
|
96
|
+
switch (plan) {
|
|
97
|
+
case "trial":
|
|
98
|
+
return "Trial";
|
|
99
|
+
case "monthly":
|
|
100
|
+
return "Monthly";
|
|
101
|
+
case "yearly":
|
|
102
|
+
return "Yearly";
|
|
103
|
+
default:
|
|
104
|
+
return String(plan);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ../shared/dist/license-store.js
|
|
109
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
110
|
+
function readLicenseSync() {
|
|
111
|
+
const path = getLicensePath();
|
|
112
|
+
if (!existsSync(path))
|
|
113
|
+
return null;
|
|
114
|
+
try {
|
|
115
|
+
const raw = readFileSync(path, "utf8");
|
|
116
|
+
const data = JSON.parse(raw);
|
|
117
|
+
if (typeof data.installId === "string" && typeof data.machineId === "string" && typeof data.plan === "string" && typeof data.expiry === "string" && typeof data.createdAt === "string") {
|
|
118
|
+
return data;
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function writeLicenseSync(license) {
|
|
125
|
+
const path = getLicensePath();
|
|
126
|
+
writeFileSync(path, JSON.stringify(license, null, 2), "utf8");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ../shared/dist/license-api.js
|
|
130
|
+
function apiResponseToLicenseInfo(r) {
|
|
131
|
+
return {
|
|
132
|
+
installId: r.install_id,
|
|
133
|
+
machineId: r.machine_id,
|
|
134
|
+
plan: r.plan,
|
|
135
|
+
expiry: r.expiry,
|
|
136
|
+
createdAt: r.created_at
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ../shared/dist/session-stats.js
|
|
141
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync } from "fs";
|
|
142
|
+
function defaultStats() {
|
|
143
|
+
return {
|
|
144
|
+
requests_optimized: 0,
|
|
145
|
+
input_chars: 0,
|
|
146
|
+
output_chars: 0,
|
|
147
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function readSessionStatsSync() {
|
|
151
|
+
const path = getStatsPath();
|
|
152
|
+
if (!existsSync2(path))
|
|
153
|
+
return null;
|
|
154
|
+
try {
|
|
155
|
+
const raw = readFileSync2(path, "utf8");
|
|
156
|
+
const data = JSON.parse(raw);
|
|
157
|
+
if (typeof data.requests_optimized === "number" && typeof data.input_chars === "number" && typeof data.output_chars === "number" && typeof data.updated_at === "string") {
|
|
158
|
+
return data;
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
function writeSessionStatsSync(stats) {
|
|
165
|
+
const dir = getConfigDir();
|
|
166
|
+
mkdirSync(dir, { recursive: true });
|
|
167
|
+
const path = getStatsPath();
|
|
168
|
+
writeFileSync2(path, JSON.stringify(stats, null, 2), "utf8");
|
|
169
|
+
}
|
|
170
|
+
function resetSessionStatsSync() {
|
|
171
|
+
writeSessionStatsSync(defaultStats());
|
|
172
|
+
}
|
|
173
|
+
function updateSessionStatsSync(inputChars, outputChars) {
|
|
174
|
+
const current = readSessionStatsSync() ?? defaultStats();
|
|
175
|
+
const next = {
|
|
176
|
+
requests_optimized: current.requests_optimized + 1,
|
|
177
|
+
input_chars: current.input_chars + inputChars,
|
|
178
|
+
output_chars: current.output_chars + outputChars,
|
|
179
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
180
|
+
};
|
|
181
|
+
writeSessionStatsSync(next);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ../shared/dist/config.js
|
|
185
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
186
|
+
var DEFAULT_PRICE_PER_1K = 3e-3;
|
|
187
|
+
function readConfigSync() {
|
|
188
|
+
const path = getConfigPath();
|
|
189
|
+
if (!existsSync3(path))
|
|
190
|
+
return {};
|
|
191
|
+
try {
|
|
192
|
+
const raw = readFileSync3(path, "utf8");
|
|
193
|
+
const data = JSON.parse(raw);
|
|
194
|
+
const config = {};
|
|
195
|
+
if (typeof data.quiet === "boolean")
|
|
196
|
+
config.quiet = data.quiet;
|
|
197
|
+
if (typeof data.pricePer1kTokens === "number")
|
|
198
|
+
config.pricePer1kTokens = data.pricePer1kTokens;
|
|
199
|
+
return config;
|
|
200
|
+
} catch {
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function getPricePer1kTokens() {
|
|
205
|
+
const config = readConfigSync();
|
|
206
|
+
return config.pricePer1kTokens ?? DEFAULT_PRICE_PER_1K;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export {
|
|
210
|
+
getLogDir,
|
|
211
|
+
getConfigDir,
|
|
212
|
+
TRIAL_DAYS,
|
|
213
|
+
DEFAULT_DAEMON_PORT,
|
|
214
|
+
getStatsPath,
|
|
215
|
+
getApiBaseUrl,
|
|
216
|
+
getUpgradeUrl,
|
|
217
|
+
HOOK_TIMEOUT_MS,
|
|
218
|
+
getMachineId,
|
|
219
|
+
isLicenseValid,
|
|
220
|
+
getPlanDisplay,
|
|
221
|
+
readLicenseSync,
|
|
222
|
+
writeLicenseSync,
|
|
223
|
+
apiResponseToLicenseInfo,
|
|
224
|
+
readSessionStatsSync,
|
|
225
|
+
resetSessionStatsSync,
|
|
226
|
+
updateSessionStatsSync,
|
|
227
|
+
readConfigSync,
|
|
228
|
+
getPricePer1kTokens
|
|
229
|
+
};
|
|
230
|
+
//# sourceMappingURL=chunk-DCCFWI4O.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../shared/src/constants.ts","../../shared/src/fingerprint.ts","../../shared/src/license.ts","../../shared/src/license-store.ts","../../shared/src/license-api.ts","../../shared/src/session-stats.ts","../../shared/src/config.ts"],"sourcesContent":["/** App name for config/log paths */\nexport const APP_NAME = \"tokencanary\";\n\n/** Default log directory under user home */\nexport function getLogDir(): string {\n const home =\n process.env.HOME ?? process.env.USERPROFILE ?? process.env.TMP ?? \"/tmp\";\n return `${home}/.${APP_NAME}/logs`;\n}\n\n/** Default config directory under user home */\nexport function getConfigDir(): string {\n const home =\n process.env.HOME ?? process.env.USERPROFILE ?? process.env.TMP ?? \"/tmp\";\n return `${home}/.${APP_NAME}`;\n}\n\n/** Trial duration in days */\nexport const TRIAL_DAYS = 30;\n\n/** Offline grace period in days */\nexport const OFFLINE_GRACE_DAYS = 3;\n\n/** Default daemon port for hook HTTP server */\nexport const DEFAULT_DAEMON_PORT = 3847;\n\n/** License file name under config dir */\nexport const LICENSE_FILENAME = \"license.json\";\n\n/** Stats file name under config dir (session stats since daemon start) */\nexport const STATS_FILENAME = \"stats.json\";\n\n/** User config file under config dir (quiet, pricePer1kTokens, etc.) */\nexport const CONFIG_FILENAME = \"config.json\";\n\nexport function getLicensePath(): string {\n return `${getConfigDir()}/${LICENSE_FILENAME}`;\n}\n\nexport function getStatsPath(): string {\n return `${getConfigDir()}/${STATS_FILENAME}`;\n}\n\nexport function getConfigPath(): string {\n return `${getConfigDir()}/${CONFIG_FILENAME}`;\n}\n\n/** Default API/app base URL (override with TOKENCANARY_API_URL). */\nexport const DEFAULT_API_URL = \"https://www.tokencanary.com\";\n\nexport function getApiBaseUrl(): string {\n return process.env.TOKENCANARY_API_URL ?? DEFAULT_API_URL;\n}\n\n/** Upgrade page URL; pass installId to prefill for checkout. */\nexport const DEFAULT_UPGRADE_URL = `${DEFAULT_API_URL}/upgrade`;\n\nexport function getUpgradeUrl(installId?: string): string {\n const base = process.env.TOKENCANARY_UPGRADE_URL ?? DEFAULT_UPGRADE_URL;\n if (!installId) return base;\n const u = new URL(base);\n u.searchParams.set(\"install_id\", installId);\n return u.toString();\n}\n\n/** Hook request timeout in ms (fail open) */\nexport const HOOK_TIMEOUT_MS = 5000;\n","import { createHash } from \"node:crypto\";\nimport { hostname, platform, arch, cpus, networkInterfaces } from \"node:os\";\n\n/**\n * Deterministic machine fingerprint for license binding.\n * Uses hostname, OS, CPU model, and hashed network info (no raw MAC).\n */\nexport function getMachineId(): string {\n const parts: string[] = [];\n try {\n parts.push(hostname());\n } catch {\n parts.push(\"unknown-host\");\n }\n parts.push(platform());\n parts.push(arch());\n try {\n const cpu = cpus()[0];\n parts.push(cpu?.model ?? \"unknown-cpu\");\n } catch {\n parts.push(\"unknown-cpu\");\n }\n try {\n const nets = networkInterfaces();\n const addrs: string[] = [];\n for (const name of Object.keys(nets)) {\n const list = nets[name];\n if (list) {\n for (const iface of list) {\n if (iface.address && !iface.internal) addrs.push(name + \":\" + iface.address);\n }\n }\n }\n addrs.sort();\n if (addrs.length > 0) {\n parts.push(createHash(\"sha256\").update(addrs.join(\",\")).digest(\"hex\").slice(0, 16));\n }\n } catch {\n // skip network\n }\n return createHash(\"sha256\").update(parts.join(\"|\")).digest(\"hex\");\n}\n","import type { LicenseInfo, Plan } from \"./types.js\";\nimport { TRIAL_DAYS, OFFLINE_GRACE_DAYS } from \"./constants.js\";\n\nconst MS_PER_DAY = 86400 * 1000;\n\nexport function createTrialLicense(installId: string, machineId: string): LicenseInfo {\n const now = new Date();\n const expiry = new Date(now.getTime() + TRIAL_DAYS * MS_PER_DAY);\n return {\n installId,\n machineId,\n plan: \"trial\",\n expiry: expiry.toISOString(),\n createdAt: now.toISOString(),\n };\n}\n\n/**\n * Check if license is valid at the given date.\n * - Trial/monthly/yearly: valid if expiry > now.\n * - Offline grace: if lastKnownValid and within OFFLINE_GRACE_DAYS, still valid.\n */\nexport function isLicenseValid(\n license: LicenseInfo | null,\n now: Date = new Date(),\n options?: { lastKnownValid?: Date }\n): boolean {\n if (!license) return false;\n const expiry = new Date(license.expiry);\n if (expiry > now) return true;\n if (options?.lastKnownValid) {\n const graceEnd = new Date(\n options.lastKnownValid.getTime() + OFFLINE_GRACE_DAYS * MS_PER_DAY\n );\n if (now < graceEnd) return true;\n }\n return false;\n}\n\nexport function getPlanDisplay(plan: Plan): string {\n switch (plan) {\n case \"trial\":\n return \"Trial\";\n case \"monthly\":\n return \"Monthly\";\n case \"yearly\":\n return \"Yearly\";\n default:\n return String(plan);\n }\n}\n","import { readFileSync, writeFileSync, existsSync } from \"node:fs\";\nimport { getLicensePath } from \"./constants.js\";\nimport type { LicenseInfo } from \"./types.js\";\n\nexport function readLicenseSync(): LicenseInfo | null {\n const path = getLicensePath();\n if (!existsSync(path)) return null;\n try {\n const raw = readFileSync(path, \"utf8\");\n const data = JSON.parse(raw) as LicenseInfo;\n if (\n typeof data.installId === \"string\" &&\n typeof data.machineId === \"string\" &&\n typeof data.plan === \"string\" &&\n typeof data.expiry === \"string\" &&\n typeof data.createdAt === \"string\"\n ) {\n return data;\n }\n } catch {\n // invalid or missing\n }\n return null;\n}\n\nexport function writeLicenseSync(license: LicenseInfo): void {\n const path = getLicensePath();\n writeFileSync(path, JSON.stringify(license, null, 2), \"utf8\");\n}\n","import type { LicenseInfo } from \"./types.js\";\n\n/** Response shape from POST /api/install and POST /api/license/check */\nexport interface LicenseApiResponse {\n install_id: string;\n machine_id: string;\n plan: \"trial\" | \"monthly\" | \"yearly\";\n expiry: string;\n created_at: string;\n}\n\nexport function apiResponseToLicenseInfo(r: LicenseApiResponse): LicenseInfo {\n return {\n installId: r.install_id,\n machineId: r.machine_id,\n plan: r.plan,\n expiry: r.expiry,\n createdAt: r.created_at,\n };\n}\n","import { readFileSync, writeFileSync, existsSync, mkdirSync } from \"node:fs\";\nimport { getStatsPath, getConfigDir } from \"./constants.js\";\nimport type { SessionStats } from \"./types.js\";\n\nfunction defaultStats(): SessionStats {\n return {\n requests_optimized: 0,\n input_chars: 0,\n output_chars: 0,\n updated_at: new Date().toISOString(),\n };\n}\n\nexport function readSessionStatsSync(): SessionStats | null {\n const path = getStatsPath();\n if (!existsSync(path)) return null;\n try {\n const raw = readFileSync(path, \"utf8\");\n const data = JSON.parse(raw) as SessionStats;\n if (\n typeof data.requests_optimized === \"number\" &&\n typeof data.input_chars === \"number\" &&\n typeof data.output_chars === \"number\" &&\n typeof data.updated_at === \"string\"\n ) {\n return data;\n }\n } catch {\n // invalid or missing\n }\n return null;\n}\n\nexport function writeSessionStatsSync(stats: SessionStats): void {\n const dir = getConfigDir();\n mkdirSync(dir, { recursive: true });\n const path = getStatsPath();\n writeFileSync(path, JSON.stringify(stats, null, 2), \"utf8\");\n}\n\n/** Reset stats to zeros (call on daemon start). */\nexport function resetSessionStatsSync(): void {\n writeSessionStatsSync(defaultStats());\n}\n\n/** Add one request's chars to current stats and persist. */\nexport function updateSessionStatsSync(inputChars: number, outputChars: number): void {\n const current = readSessionStatsSync() ?? defaultStats();\n const next: SessionStats = {\n requests_optimized: current.requests_optimized + 1,\n input_chars: current.input_chars + inputChars,\n output_chars: current.output_chars + outputChars,\n updated_at: new Date().toISOString(),\n };\n writeSessionStatsSync(next);\n}\n","import { readFileSync, existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { getConfigPath, getConfigDir } from \"./constants.js\";\n\n/** User-configurable options (same file used by CLI and daemon). */\nexport interface UserConfig {\n /** When true, do not show in-Claude systemMessage (savings). */\n quiet?: boolean;\n /** Price per 1k tokens (for $ estimate in stats/systemMessage). Default ~$3/1M = 0.003. */\n pricePer1kTokens?: number;\n}\n\nconst DEFAULT_PRICE_PER_1K = 0.003;\n\nexport function readConfigSync(): UserConfig {\n const path = getConfigPath();\n if (!existsSync(path)) return {};\n try {\n const raw = readFileSync(path, \"utf8\");\n const data = JSON.parse(raw) as Record<string, unknown>;\n const config: UserConfig = {};\n if (typeof data.quiet === \"boolean\") config.quiet = data.quiet;\n if (typeof data.pricePer1kTokens === \"number\")\n config.pricePer1kTokens = data.pricePer1kTokens;\n return config;\n } catch {\n return {};\n }\n}\n\nexport function writeConfigSync(config: UserConfig): void {\n const dir = getConfigDir();\n mkdirSync(dir, { recursive: true });\n const path = getConfigPath();\n const existing = readConfigSync();\n const merged = { ...existing, ...config };\n writeFileSync(path, JSON.stringify(merged, null, 2), \"utf8\");\n}\n\n/** Price per 1k tokens for $ estimate (from config or default). */\nexport function getPricePer1kTokens(): number {\n const config = readConfigSync();\n return config.pricePer1kTokens ?? DEFAULT_PRICE_PER_1K;\n}\n"],"mappings":"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","names":["readFileSync","writeFileSync","existsSync","existsSync","readFileSync","writeFileSync","readFileSync","existsSync","mkdirSync","writeFileSync","existsSync","readFileSync"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getConfigDir
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-DCCFWI4O.js";
|
|
4
4
|
|
|
5
5
|
// src/commands/stop.ts
|
|
6
6
|
import { readFile, unlink } from "fs/promises";
|
|
@@ -43,4 +43,4 @@ async function runStopCmd(_args) {
|
|
|
43
43
|
export {
|
|
44
44
|
runStopCmd
|
|
45
45
|
};
|
|
46
|
-
//# sourceMappingURL=chunk-
|
|
46
|
+
//# sourceMappingURL=chunk-IUASFEXE.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runDoctor
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-66ONBXHK.js";
|
|
5
|
+
import "./chunk-DCCFWI4O.js";
|
|
6
6
|
|
|
7
7
|
// src/commands/index.ts
|
|
8
8
|
var COMMANDS = {
|
|
@@ -12,6 +12,7 @@ var COMMANDS = {
|
|
|
12
12
|
stop: runStop,
|
|
13
13
|
uninstall: runUninstall,
|
|
14
14
|
status: runStatus,
|
|
15
|
+
stats: runStats,
|
|
15
16
|
logs: runLogs,
|
|
16
17
|
upgrade: runUpgrade,
|
|
17
18
|
license: runLicense
|
|
@@ -61,6 +62,7 @@ Commands:
|
|
|
61
62
|
stop Stop the daemon
|
|
62
63
|
uninstall Remove hooks and stop daemon
|
|
63
64
|
status Daemon and license status
|
|
65
|
+
stats Session savings (requests optimized, chars reduced)
|
|
64
66
|
logs Recent logs
|
|
65
67
|
upgrade Open browser to subscribe
|
|
66
68
|
license License and trial info
|
|
@@ -71,31 +73,35 @@ Quick start:
|
|
|
71
73
|
`);
|
|
72
74
|
}
|
|
73
75
|
async function runInstall(_args) {
|
|
74
|
-
const { runInstallCmd } = await import("./install-
|
|
76
|
+
const { runInstallCmd } = await import("./install-NA7R44O4.js");
|
|
75
77
|
return runInstallCmd(_args);
|
|
76
78
|
}
|
|
77
79
|
async function runStop(_args) {
|
|
78
|
-
const { runStopCmd } = await import("./stop-
|
|
80
|
+
const { runStopCmd } = await import("./stop-QJSJTHHO.js");
|
|
79
81
|
return runStopCmd(_args);
|
|
80
82
|
}
|
|
81
83
|
async function runUninstall(_args) {
|
|
82
|
-
const { runUninstallCmd } = await import("./uninstall-
|
|
84
|
+
const { runUninstallCmd } = await import("./uninstall-WUG7BEBH.js");
|
|
83
85
|
return runUninstallCmd(_args);
|
|
84
86
|
}
|
|
85
87
|
async function runStatus(_args) {
|
|
86
|
-
const { runStatusCmd } = await import("./status-
|
|
88
|
+
const { runStatusCmd } = await import("./status-2EAJQ5TH.js");
|
|
87
89
|
return runStatusCmd(_args);
|
|
88
90
|
}
|
|
91
|
+
async function runStats(_args) {
|
|
92
|
+
const { runStatsCmd } = await import("./stats-7NLMKM67.js");
|
|
93
|
+
return runStatsCmd(_args);
|
|
94
|
+
}
|
|
89
95
|
async function runLogs(_args) {
|
|
90
|
-
const { runLogsCmd } = await import("./logs-
|
|
96
|
+
const { runLogsCmd } = await import("./logs-U4OOGTGV.js");
|
|
91
97
|
return runLogsCmd(_args);
|
|
92
98
|
}
|
|
93
99
|
async function runUpgrade(_args) {
|
|
94
|
-
const { runUpgradeCmd } = await import("./upgrade-
|
|
100
|
+
const { runUpgradeCmd } = await import("./upgrade-IR4MHUD2.js");
|
|
95
101
|
return runUpgradeCmd(_args);
|
|
96
102
|
}
|
|
97
103
|
async function runLicense(_args) {
|
|
98
|
-
const { runLicenseCmd } = await import("./license-
|
|
104
|
+
const { runLicenseCmd } = await import("./license-VKOP67LZ.js");
|
|
99
105
|
return runLicenseCmd(_args);
|
|
100
106
|
}
|
|
101
107
|
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/index.ts","../src/cli.ts"],"sourcesContent":["import { runDoctor } from \"./doctor.js\";\n\nconst COMMANDS: Record<string, (args: string[]) => Promise<number>> = {\n doctor: runDoctor,\n setup: runInstall,\n install: runInstall,\n stop: runStop,\n uninstall: runUninstall,\n status: runStatus,\n logs: runLogs,\n upgrade: runUpgrade,\n license: runLicense,\n};\n\nasync function getVersion(): Promise<string> {\n try {\n const { createRequire } = await import(\"node:module\");\n const { dirname, join } = await import(\"node:path\");\n const { fileURLToPath } = await import(\"node:url\");\n const req = createRequire(import.meta.url);\n const cliDir = dirname(fileURLToPath(import.meta.url));\n const pkg = req(join(cliDir, \"..\", \"package.json\")) as { version?: string };\n return pkg?.version ?? \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\nexport async function runCli(args: string[]): Promise<void> {\n const cmd = args[0];\n if (!cmd || cmd === \"--help\" || cmd === \"-h\") {\n printHelp();\n return;\n }\n if (cmd === \"--version\" || cmd === \"-v\") {\n console.log(await getVersion());\n process.exit(0);\n }\n const handler = COMMANDS[cmd];\n if (!handler) {\n console.error(`Unknown command: ${cmd}`);\n printHelp();\n process.exit(1);\n }\n const code = await handler(args.slice(1));\n process.exit(code);\n}\n\nfunction printHelp(): void {\n console.log(`\nToken Canary — Reduce Claude Code token usage\n\nUsage: tokencanary <command>\n\nCommands:\n doctor Check environment (Claude Code, OS, config)\n setup Set up hooks and start daemon (run after doctor)\n install Alias for setup\n stop Stop the daemon\n uninstall Remove hooks and stop daemon\n status Daemon and license status\n logs Recent logs\n upgrade Open browser to subscribe\n license License and trial info\n\nQuick start:\n tokencanary doctor\n tokencanary setup\n`);\n}\n\nasync function runInstall(_args: string[]): Promise<number> {\n const { runInstallCmd } = await import(\"./install.js\");\n return runInstallCmd(_args);\n}\n\nasync function runStop(_args: string[]): Promise<number> {\n const { runStopCmd } = await import(\"./stop.js\");\n return runStopCmd(_args);\n}\n\nasync function runUninstall(_args: string[]): Promise<number> {\n const { runUninstallCmd } = await import(\"./uninstall.js\");\n return runUninstallCmd(_args);\n}\n\nasync function runStatus(_args: string[]): Promise<number> {\n const { runStatusCmd } = await import(\"./status.js\");\n return runStatusCmd(_args);\n}\n\nasync function runLogs(_args: string[]): Promise<number> {\n const { runLogsCmd } = await import(\"./logs.js\");\n return runLogsCmd(_args);\n}\n\nasync function runUpgrade(_args: string[]): Promise<number> {\n const { runUpgradeCmd } = await import(\"./upgrade.js\");\n return runUpgradeCmd(_args);\n}\n\nasync function runLicense(_args: string[]): Promise<number> {\n const { runLicenseCmd } = await import(\"./license.js\");\n return runLicenseCmd(_args);\n}\n","#!/usr/bin/env node\nimport { runCli } from \"./commands/index.js\";\n\nrunCli(process.argv.slice(2)).catch((err: unknown) => {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n});\n"],"mappings":";;;;;;;AAEA,IAAM,WAAgE;AAAA,EACpE,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AACX;AAEA,eAAe,aAA8B;AAC3C,MAAI;AACF,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,QAAa;AACpD,UAAM,EAAE,SAAS,KAAK,IAAI,MAAM,OAAO,MAAW;AAClD,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,KAAU;AACjD,UAAM,MAAM,cAAc,YAAY,GAAG;AACzC,UAAM,SAAS,QAAQ,cAAc,YAAY,GAAG,CAAC;AACrD,UAAM,MAAM,IAAI,KAAK,QAAQ,MAAM,cAAc,CAAC;AAClD,WAAO,KAAK,WAAW;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,OAAO,MAA+B;AAC1D,QAAM,MAAM,KAAK,CAAC;AAClB,MAAI,CAAC,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAC5C,cAAU;AACV;AAAA,EACF;AACA,MAAI,QAAQ,eAAe,QAAQ,MAAM;AACvC,YAAQ,IAAI,MAAM,WAAW,CAAC;AAC9B,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,UAAU,SAAS,GAAG;AAC5B,MAAI,CAAC,SAAS;AACZ,YAAQ,MAAM,oBAAoB,GAAG,EAAE;AACvC,cAAU;AACV,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,OAAO,MAAM,QAAQ,KAAK,MAAM,CAAC,CAAC;AACxC,UAAQ,KAAK,IAAI;AACnB;AAEA,SAAS,YAAkB;AACzB,UAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,
|
|
1
|
+
{"version":3,"sources":["../src/commands/index.ts","../src/cli.ts"],"sourcesContent":["import { runDoctor } from \"./doctor.js\";\n\nconst COMMANDS: Record<string, (args: string[]) => Promise<number>> = {\n doctor: runDoctor,\n setup: runInstall,\n install: runInstall,\n stop: runStop,\n uninstall: runUninstall,\n status: runStatus,\n stats: runStats,\n logs: runLogs,\n upgrade: runUpgrade,\n license: runLicense,\n};\n\nasync function getVersion(): Promise<string> {\n try {\n const { createRequire } = await import(\"node:module\");\n const { dirname, join } = await import(\"node:path\");\n const { fileURLToPath } = await import(\"node:url\");\n const req = createRequire(import.meta.url);\n const cliDir = dirname(fileURLToPath(import.meta.url));\n const pkg = req(join(cliDir, \"..\", \"package.json\")) as { version?: string };\n return pkg?.version ?? \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\nexport async function runCli(args: string[]): Promise<void> {\n const cmd = args[0];\n if (!cmd || cmd === \"--help\" || cmd === \"-h\") {\n printHelp();\n return;\n }\n if (cmd === \"--version\" || cmd === \"-v\") {\n console.log(await getVersion());\n process.exit(0);\n }\n const handler = COMMANDS[cmd];\n if (!handler) {\n console.error(`Unknown command: ${cmd}`);\n printHelp();\n process.exit(1);\n }\n const code = await handler(args.slice(1));\n process.exit(code);\n}\n\nfunction printHelp(): void {\n console.log(`\nToken Canary — Reduce Claude Code token usage\n\nUsage: tokencanary <command>\n\nCommands:\n doctor Check environment (Claude Code, OS, config)\n setup Set up hooks and start daemon (run after doctor)\n install Alias for setup\n stop Stop the daemon\n uninstall Remove hooks and stop daemon\n status Daemon and license status\n stats Session savings (requests optimized, chars reduced)\n logs Recent logs\n upgrade Open browser to subscribe\n license License and trial info\n\nQuick start:\n tokencanary doctor\n tokencanary setup\n`);\n}\n\nasync function runInstall(_args: string[]): Promise<number> {\n const { runInstallCmd } = await import(\"./install.js\");\n return runInstallCmd(_args);\n}\n\nasync function runStop(_args: string[]): Promise<number> {\n const { runStopCmd } = await import(\"./stop.js\");\n return runStopCmd(_args);\n}\n\nasync function runUninstall(_args: string[]): Promise<number> {\n const { runUninstallCmd } = await import(\"./uninstall.js\");\n return runUninstallCmd(_args);\n}\n\nasync function runStatus(_args: string[]): Promise<number> {\n const { runStatusCmd } = await import(\"./status.js\");\n return runStatusCmd(_args);\n}\n\nasync function runStats(_args: string[]): Promise<number> {\n const { runStatsCmd } = await import(\"./stats.js\");\n return runStatsCmd(_args);\n}\n\nasync function runLogs(_args: string[]): Promise<number> {\n const { runLogsCmd } = await import(\"./logs.js\");\n return runLogsCmd(_args);\n}\n\nasync function runUpgrade(_args: string[]): Promise<number> {\n const { runUpgradeCmd } = await import(\"./upgrade.js\");\n return runUpgradeCmd(_args);\n}\n\nasync function runLicense(_args: string[]): Promise<number> {\n const { runLicenseCmd } = await import(\"./license.js\");\n return runLicenseCmd(_args);\n}\n","#!/usr/bin/env node\nimport { runCli } from \"./commands/index.js\";\n\nrunCli(process.argv.slice(2)).catch((err: unknown) => {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n});\n"],"mappings":";;;;;;;AAEA,IAAM,WAAgE;AAAA,EACpE,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AACX;AAEA,eAAe,aAA8B;AAC3C,MAAI;AACF,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,QAAa;AACpD,UAAM,EAAE,SAAS,KAAK,IAAI,MAAM,OAAO,MAAW;AAClD,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,KAAU;AACjD,UAAM,MAAM,cAAc,YAAY,GAAG;AACzC,UAAM,SAAS,QAAQ,cAAc,YAAY,GAAG,CAAC;AACrD,UAAM,MAAM,IAAI,KAAK,QAAQ,MAAM,cAAc,CAAC;AAClD,WAAO,KAAK,WAAW;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,OAAO,MAA+B;AAC1D,QAAM,MAAM,KAAK,CAAC;AAClB,MAAI,CAAC,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAC5C,cAAU;AACV;AAAA,EACF;AACA,MAAI,QAAQ,eAAe,QAAQ,MAAM;AACvC,YAAQ,IAAI,MAAM,WAAW,CAAC;AAC9B,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,UAAU,SAAS,GAAG;AAC5B,MAAI,CAAC,SAAS;AACZ,YAAQ,MAAM,oBAAoB,GAAG,EAAE;AACvC,cAAU;AACV,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,OAAO,MAAM,QAAQ,KAAK,MAAM,CAAC,CAAC;AACxC,UAAQ,KAAK,IAAI;AACnB;AAEA,SAAS,YAAkB;AACzB,UAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAoBb;AACD;AAEA,eAAe,WAAW,OAAkC;AAC1D,QAAM,EAAE,cAAc,IAAI,MAAM,OAAO,uBAAc;AACrD,SAAO,cAAc,KAAK;AAC5B;AAEA,eAAe,QAAQ,OAAkC;AACvD,QAAM,EAAE,WAAW,IAAI,MAAM,OAAO,oBAAW;AAC/C,SAAO,WAAW,KAAK;AACzB;AAEA,eAAe,aAAa,OAAkC;AAC5D,QAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,yBAAgB;AACzD,SAAO,gBAAgB,KAAK;AAC9B;AAEA,eAAe,UAAU,OAAkC;AACzD,QAAM,EAAE,aAAa,IAAI,MAAM,OAAO,sBAAa;AACnD,SAAO,aAAa,KAAK;AAC3B;AAEA,eAAe,SAAS,OAAkC;AACxD,QAAM,EAAE,YAAY,IAAI,MAAM,OAAO,qBAAY;AACjD,SAAO,YAAY,KAAK;AAC1B;AAEA,eAAe,QAAQ,OAAkC;AACvD,QAAM,EAAE,WAAW,IAAI,MAAM,OAAO,oBAAW;AAC/C,SAAO,WAAW,KAAK;AACzB;AAEA,eAAe,WAAW,OAAkC;AAC1D,QAAM,EAAE,cAAc,IAAI,MAAM,OAAO,uBAAc;AACrD,SAAO,cAAc,KAAK;AAC5B;AAEA,eAAe,WAAW,OAAkC;AAC1D,QAAM,EAAE,cAAc,IAAI,MAAM,OAAO,uBAAc;AACrD,SAAO,cAAc,KAAK;AAC5B;;;AC5GA,OAAO,QAAQ,KAAK,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,QAAiB;AACpD,UAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAC9D,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
|
package/dist/daemon.js
CHANGED
|
@@ -2,15 +2,21 @@ import {
|
|
|
2
2
|
DEFAULT_DAEMON_PORT,
|
|
3
3
|
HOOK_TIMEOUT_MS,
|
|
4
4
|
getLogDir,
|
|
5
|
+
getPricePer1kTokens,
|
|
5
6
|
isLicenseValid,
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
readConfigSync,
|
|
8
|
+
readLicenseSync,
|
|
9
|
+
readSessionStatsSync,
|
|
10
|
+
resetSessionStatsSync,
|
|
11
|
+
updateSessionStatsSync
|
|
12
|
+
} from "./chunk-DCCFWI4O.js";
|
|
8
13
|
|
|
9
14
|
// ../daemon/src/index.ts
|
|
10
15
|
import { fileURLToPath } from "url";
|
|
11
16
|
|
|
12
17
|
// ../daemon/src/server.ts
|
|
13
18
|
import { createServer } from "http";
|
|
19
|
+
import { randomUUID } from "crypto";
|
|
14
20
|
|
|
15
21
|
// ../hooks/dist/compress.js
|
|
16
22
|
var STACK_LINE = /^\s*at\s+/;
|
|
@@ -65,6 +71,7 @@ function compressPrompt(text) {
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
// ../daemon/src/handlers.ts
|
|
74
|
+
var SYSTEM_MESSAGE_EVERY_N_REQUESTS = 5;
|
|
68
75
|
async function handleHook(name, payload) {
|
|
69
76
|
switch (name) {
|
|
70
77
|
case "UserPromptSubmit":
|
|
@@ -75,6 +82,19 @@ async function handleHook(name, payload) {
|
|
|
75
82
|
return payload;
|
|
76
83
|
}
|
|
77
84
|
}
|
|
85
|
+
function buildSystemMessage() {
|
|
86
|
+
const stats = readSessionStatsSync();
|
|
87
|
+
if (!stats || stats.requests_optimized === 0) {
|
|
88
|
+
return "Token Canary is optimizing. Run `tokencanary stats` for details.";
|
|
89
|
+
}
|
|
90
|
+
const reduced = stats.input_chars - stats.output_chars;
|
|
91
|
+
const pct = stats.input_chars > 0 ? Math.round((1 - stats.output_chars / stats.input_chars) * 100) : 0;
|
|
92
|
+
const pricePer1k = getPricePer1kTokens();
|
|
93
|
+
const estimatedTokens = reduced / 4;
|
|
94
|
+
const estimatedDollars = estimatedTokens / 1e3 * pricePer1k;
|
|
95
|
+
const dollarStr = estimatedDollars >= 0.01 ? ` (~$${estimatedDollars.toFixed(2)} est.)` : "";
|
|
96
|
+
return `Token Canary: ~${pct}% saved this session${dollarStr}. Run \`tokencanary stats\` for details.`;
|
|
97
|
+
}
|
|
78
98
|
async function handleUserPromptSubmit(payload) {
|
|
79
99
|
const license = readLicenseSync();
|
|
80
100
|
if (!isLicenseValid(license)) {
|
|
@@ -86,7 +106,20 @@ async function handleUserPromptSubmit(payload) {
|
|
|
86
106
|
if (typeof prompt !== "string") return payload;
|
|
87
107
|
try {
|
|
88
108
|
const compressed = compressPrompt(prompt);
|
|
89
|
-
|
|
109
|
+
const inputChars = prompt.length;
|
|
110
|
+
const outputChars = compressed.length;
|
|
111
|
+
updateSessionStatsSync(inputChars, outputChars);
|
|
112
|
+
const result = { ...obj, prompt: compressed };
|
|
113
|
+
const config = readConfigSync();
|
|
114
|
+
if (config.quiet === true) {
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
const stats = readSessionStatsSync();
|
|
118
|
+
if (stats && stats.requests_optimized > 0 && stats.requests_optimized % SYSTEM_MESSAGE_EVERY_N_REQUESTS === 0) {
|
|
119
|
+
const msg = buildSystemMessage();
|
|
120
|
+
if (msg) result.systemMessage = msg;
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
90
123
|
} catch {
|
|
91
124
|
return payload;
|
|
92
125
|
}
|
|
@@ -118,6 +151,16 @@ function logToFile(level, message) {
|
|
|
118
151
|
} catch {
|
|
119
152
|
}
|
|
120
153
|
}
|
|
154
|
+
function logStructuredRequest(entry) {
|
|
155
|
+
try {
|
|
156
|
+
ensureLogDir();
|
|
157
|
+
const dir = getLogDir();
|
|
158
|
+
const path = join(dir, LOG_FILENAME);
|
|
159
|
+
const line = JSON.stringify(entry) + "\n";
|
|
160
|
+
appendFileSync(path, line, "utf8");
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
121
164
|
|
|
122
165
|
// ../daemon/src/server.ts
|
|
123
166
|
var PORT = Number(process.env.TOKENCANARY_PORT) || DEFAULT_DAEMON_PORT;
|
|
@@ -135,7 +178,8 @@ function createDaemonServer() {
|
|
|
135
178
|
return;
|
|
136
179
|
}
|
|
137
180
|
const name = req.url.slice("/hook/".length).split("?")[0];
|
|
138
|
-
|
|
181
|
+
const requestId = randomUUID();
|
|
182
|
+
const startTime = Date.now();
|
|
139
183
|
let body = "";
|
|
140
184
|
for await (const chunk of req) {
|
|
141
185
|
body += chunk;
|
|
@@ -155,17 +199,45 @@ function createDaemonServer() {
|
|
|
155
199
|
const work = handleHook(name, payload);
|
|
156
200
|
try {
|
|
157
201
|
const result = await Promise.race([work, timeout]);
|
|
202
|
+
const latencyMs = Date.now() - startTime;
|
|
203
|
+
const entry = {
|
|
204
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
205
|
+
request_id: requestId,
|
|
206
|
+
hook: name,
|
|
207
|
+
latency_ms: latencyMs
|
|
208
|
+
};
|
|
209
|
+
if (name === "UserPromptSubmit" && payload !== null && typeof payload === "object" && result !== null && typeof result === "object") {
|
|
210
|
+
const inPrompt = payload.prompt;
|
|
211
|
+
const outPrompt = result.prompt;
|
|
212
|
+
if (typeof inPrompt === "string" && typeof outPrompt === "string") {
|
|
213
|
+
entry.input_chars = inPrompt.length;
|
|
214
|
+
entry.output_chars = outPrompt.length;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
logStructuredRequest(entry);
|
|
158
218
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
159
219
|
res.end(JSON.stringify(result));
|
|
160
220
|
} catch (err) {
|
|
161
221
|
const msg = err instanceof Error ? err.message : String(err);
|
|
162
222
|
logToFile("error", `hook ${name} failed: ${msg}`);
|
|
223
|
+
const latencyMs = Date.now() - startTime;
|
|
224
|
+
logStructuredRequest({
|
|
225
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
226
|
+
request_id: requestId,
|
|
227
|
+
hook: name,
|
|
228
|
+
latency_ms: latencyMs,
|
|
229
|
+
error: msg
|
|
230
|
+
});
|
|
163
231
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
164
232
|
res.end(JSON.stringify(payload));
|
|
165
233
|
}
|
|
166
234
|
});
|
|
167
235
|
}
|
|
168
236
|
async function startDaemon() {
|
|
237
|
+
try {
|
|
238
|
+
resetSessionStatsSync();
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
169
241
|
const server = createDaemonServer();
|
|
170
242
|
server.listen(PORT, () => {
|
|
171
243
|
process.stdout.write(`Token Canary daemon listening on port ${PORT}
|
package/dist/daemon.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../daemon/src/index.ts","../../daemon/src/server.ts","../../hooks/src/compress.ts","../../daemon/src/handlers.ts","../../daemon/src/log.ts"],"sourcesContent":["import { fileURLToPath } from \"node:url\";\nimport { startDaemon as start } from \"./server.js\";\n\nexport { startDaemon, createDaemonServer } from \"./server.js\";\nexport { handleHook } from \"./handlers.js\";\n\nif (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {\n start().catch((err: unknown) => {\n console.error(err);\n process.exit(1);\n });\n}\n","import { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { DEFAULT_DAEMON_PORT, HOOK_TIMEOUT_MS } from \"@token-canary/shared\";\nimport { handleHook } from \"./handlers.js\";\nimport { logToFile } from \"./log.js\";\n\nconst PORT = Number(process.env.TOKENCANARY_PORT) || DEFAULT_DAEMON_PORT;\n\nexport function createDaemonServer() {\n return createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const pathname = req.url?.split(\"?\")[0];\n if (req.method === \"GET\" && (pathname === \"/\" || pathname === \"/health\")) {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ok\", daemon: \"running\" }));\n return;\n }\n if (req.method !== \"POST\" || !req.url?.startsWith(\"/hook/\")) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Not found\" }));\n return;\n }\n const name = req.url.slice(\"/hook/\".length).split(\"?\")[0];\n logToFile(\"info\", `request /hook/${name}`);\n let body = \"\";\n for await (const chunk of req) {\n body += chunk;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(body || \"{}\");\n } catch {\n logToFile(\"error\", \"Invalid JSON body\");\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid JSON\" }));\n return;\n }\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(\"timeout\")), HOOK_TIMEOUT_MS)\n );\n const work = handleHook(name, payload);\n try {\n const result = await Promise.race([work, timeout]);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(result));\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n logToFile(\"error\", `hook ${name} failed: ${msg}`);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n }\n });\n}\n\nexport async function startDaemon(): Promise<void> {\n const server = createDaemonServer();\n server.listen(PORT, () => {\n process.stdout.write(`Token Canary daemon listening on port ${PORT}\\n`);\n });\n}\n","/**\n * Compress prompt text: remove duplicate lines, keep error lines, keep first stack trace.\n * Used for UserPromptSubmit hook to reduce token usage.\n */\nconst STACK_LINE = /^\\s*at\\s+/;\nconst MAX_DUPE_LINES = 5;\n\nfunction isStackLine(line: string): boolean {\n return STACK_LINE.test(line.trim());\n}\n\n/** Remove duplicate consecutive lines, cap repeats at MAX_DUPE_LINES */\nfunction dedupeLines(lines: string[]): string[] {\n const out: string[] = [];\n let prev = \"\";\n let count = 0;\n for (const line of lines) {\n if (line === prev) {\n count++;\n if (count <= MAX_DUPE_LINES) out.push(line);\n } else {\n prev = line;\n count = 1;\n out.push(line);\n }\n }\n return out;\n}\n\n/** Keep first stack trace block (consecutive \"at ...\" lines), drop later ones */\nfunction keepFirstStack(lines: string[]): string[] {\n const out: string[] = [];\n let inStack = false;\n let keptOne = false;\n for (const line of lines) {\n if (isStackLine(line)) {\n if (!keptOne) {\n inStack = true;\n out.push(line);\n }\n } else {\n if (inStack) keptOne = true;\n inStack = false;\n out.push(line);\n }\n }\n return out;\n}\n\n/**\n * Compress prompt: dedupe lines, keep first stack trace only.\n * If the text is small (< MAX_LINES_BEFORE_DEDUPE lines), still dedupes.\n */\nexport function compressPrompt(text: string): string {\n if (!text || text.length === 0) return text;\n const lines = text.split(/\\r?\\n/);\n let result = lines;\n result = keepFirstStack(result);\n result = dedupeLines(result);\n return result.join(\"\\n\");\n}\n","import { readLicenseSync, isLicenseValid } from \"@token-canary/shared\";\nimport { compressPrompt } from \"@token-canary/hooks\";\n\n/** Route hook name to handler; fail open: return payload on error */\nexport async function handleHook(name: string, payload: unknown): Promise<unknown> {\n switch (name) {\n case \"UserPromptSubmit\":\n return handleUserPromptSubmit(payload);\n case \"PreToolUse\":\n return handlePreToolUse(payload);\n default:\n return payload;\n }\n}\n\nasync function handleUserPromptSubmit(payload: unknown): Promise<unknown> {\n const license = readLicenseSync();\n if (!isLicenseValid(license)) {\n return payload;\n }\n if (payload === null || typeof payload !== \"object\") return payload;\n const obj = payload as Record<string, unknown>;\n const prompt = obj.prompt ?? obj.text ?? obj.content;\n if (typeof prompt !== \"string\") return payload;\n try {\n const compressed = compressPrompt(prompt);\n return { ...obj, prompt: compressed };\n } catch {\n return payload;\n }\n}\n\nasync function handlePreToolUse(payload: unknown): Promise<unknown> {\n return payload;\n}\n","import { mkdirSync, appendFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getLogDir } from \"@token-canary/shared\";\n\nconst LOG_FILENAME = \"daemon.log\";\n\nlet logDirEnsured = false;\n\nfunction ensureLogDir(): void {\n if (logDirEnsured) return;\n const dir = getLogDir();\n mkdirSync(dir, { recursive: true });\n logDirEnsured = true;\n}\n\n/**\n * Append one line to ~/.tokencanary/logs/daemon.log. Used for errors and request tracing.\n * Safe to call from request handlers; failures are swallowed.\n */\nexport function logToFile(level: string, message: string): void {\n try {\n ensureLogDir();\n const dir = getLogDir();\n const path = join(dir, LOG_FILENAME);\n const ts = new Date().toISOString();\n const line = `${ts} ${level} ${message}\\n`;\n appendFileSync(path, line, \"utf8\");\n } catch {\n // do not break daemon if logging fails\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../daemon/src/index.ts","../../daemon/src/server.ts","../../hooks/src/compress.ts","../../daemon/src/handlers.ts","../../daemon/src/log.ts"],"sourcesContent":["import { fileURLToPath } from \"node:url\";\nimport { startDaemon as start } from \"./server.js\";\n\nexport { startDaemon, createDaemonServer } from \"./server.js\";\nexport { handleHook } from \"./handlers.js\";\n\nif (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {\n start().catch((err: unknown) => {\n console.error(err);\n process.exit(1);\n });\n}\n","import { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { randomUUID } from \"node:crypto\";\nimport {\n DEFAULT_DAEMON_PORT,\n HOOK_TIMEOUT_MS,\n resetSessionStatsSync,\n} from \"@token-canary/shared\";\nimport { handleHook } from \"./handlers.js\";\nimport { logToFile, logStructuredRequest } from \"./log.js\";\n\nconst PORT = Number(process.env.TOKENCANARY_PORT) || DEFAULT_DAEMON_PORT;\n\nexport function createDaemonServer() {\n return createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const pathname = req.url?.split(\"?\")[0];\n if (req.method === \"GET\" && (pathname === \"/\" || pathname === \"/health\")) {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ok\", daemon: \"running\" }));\n return;\n }\n if (req.method !== \"POST\" || !req.url?.startsWith(\"/hook/\")) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Not found\" }));\n return;\n }\n const name = req.url.slice(\"/hook/\".length).split(\"?\")[0];\n const requestId = randomUUID();\n const startTime = Date.now();\n let body = \"\";\n for await (const chunk of req) {\n body += chunk;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(body || \"{}\");\n } catch {\n logToFile(\"error\", \"Invalid JSON body\");\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid JSON\" }));\n return;\n }\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(\"timeout\")), HOOK_TIMEOUT_MS)\n );\n const work = handleHook(name, payload);\n try {\n const result = await Promise.race([work, timeout]);\n const latencyMs = Date.now() - startTime;\n const entry: Record<string, unknown> = {\n ts: new Date().toISOString(),\n request_id: requestId,\n hook: name,\n latency_ms: latencyMs,\n };\n if (\n name === \"UserPromptSubmit\" &&\n payload !== null &&\n typeof payload === \"object\" &&\n result !== null &&\n typeof result === \"object\"\n ) {\n const inPrompt = (payload as Record<string, unknown>).prompt;\n const outPrompt = (result as Record<string, unknown>).prompt;\n if (typeof inPrompt === \"string\" && typeof outPrompt === \"string\") {\n entry.input_chars = inPrompt.length;\n entry.output_chars = outPrompt.length;\n }\n }\n logStructuredRequest(entry);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(result));\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n logToFile(\"error\", `hook ${name} failed: ${msg}`);\n const latencyMs = Date.now() - startTime;\n logStructuredRequest({\n ts: new Date().toISOString(),\n request_id: requestId,\n hook: name,\n latency_ms: latencyMs,\n error: msg,\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n }\n });\n}\n\nexport async function startDaemon(): Promise<void> {\n try {\n resetSessionStatsSync();\n } catch {\n // do not block daemon start if stats reset fails\n }\n const server = createDaemonServer();\n server.listen(PORT, () => {\n process.stdout.write(`Token Canary daemon listening on port ${PORT}\\n`);\n });\n}\n","/**\n * Compress prompt text: remove duplicate lines, keep error lines, keep first stack trace.\n * Used for UserPromptSubmit hook to reduce token usage.\n */\nconst STACK_LINE = /^\\s*at\\s+/;\nconst MAX_DUPE_LINES = 5;\n\nfunction isStackLine(line: string): boolean {\n return STACK_LINE.test(line.trim());\n}\n\n/** Remove duplicate consecutive lines, cap repeats at MAX_DUPE_LINES */\nfunction dedupeLines(lines: string[]): string[] {\n const out: string[] = [];\n let prev = \"\";\n let count = 0;\n for (const line of lines) {\n if (line === prev) {\n count++;\n if (count <= MAX_DUPE_LINES) out.push(line);\n } else {\n prev = line;\n count = 1;\n out.push(line);\n }\n }\n return out;\n}\n\n/** Keep first stack trace block (consecutive \"at ...\" lines), drop later ones */\nfunction keepFirstStack(lines: string[]): string[] {\n const out: string[] = [];\n let inStack = false;\n let keptOne = false;\n for (const line of lines) {\n if (isStackLine(line)) {\n if (!keptOne) {\n inStack = true;\n out.push(line);\n }\n } else {\n if (inStack) keptOne = true;\n inStack = false;\n out.push(line);\n }\n }\n return out;\n}\n\n/**\n * Compress prompt: dedupe lines, keep first stack trace only.\n * If the text is small (< MAX_LINES_BEFORE_DEDUPE lines), still dedupes.\n */\nexport function compressPrompt(text: string): string {\n if (!text || text.length === 0) return text;\n const lines = text.split(/\\r?\\n/);\n let result = lines;\n result = keepFirstStack(result);\n result = dedupeLines(result);\n return result.join(\"\\n\");\n}\n","import {\n readLicenseSync,\n isLicenseValid,\n updateSessionStatsSync,\n readSessionStatsSync,\n readConfigSync,\n getPricePer1kTokens,\n} from \"@token-canary/shared\";\nimport { compressPrompt } from \"@token-canary/hooks\";\n\n/**\n * Throttle: show systemMessage only every N requests to avoid spam.\n * Tune this constant to change how often the in-Claude savings message appears.\n */\nconst SYSTEM_MESSAGE_EVERY_N_REQUESTS = 5;\n\n/** Route hook name to handler; fail open: return payload on error */\nexport async function handleHook(name: string, payload: unknown): Promise<unknown> {\n switch (name) {\n case \"UserPromptSubmit\":\n return handleUserPromptSubmit(payload);\n case \"PreToolUse\":\n return handlePreToolUse(payload);\n default:\n return payload;\n }\n}\n\nfunction buildSystemMessage(): string | undefined {\n const stats = readSessionStatsSync();\n if (!stats || stats.requests_optimized === 0) {\n return \"Token Canary is optimizing. Run `tokencanary stats` for details.\";\n }\n const reduced = stats.input_chars - stats.output_chars;\n const pct =\n stats.input_chars > 0\n ? Math.round((1 - stats.output_chars / stats.input_chars) * 100)\n : 0;\n const pricePer1k = getPricePer1kTokens();\n const estimatedTokens = reduced / 4;\n const estimatedDollars = (estimatedTokens / 1000) * pricePer1k;\n const dollarStr =\n estimatedDollars >= 0.01\n ? ` (~$${estimatedDollars.toFixed(2)} est.)`\n : \"\";\n return `Token Canary: ~${pct}% saved this session${dollarStr}. Run \\`tokencanary stats\\` for details.`;\n}\n\nasync function handleUserPromptSubmit(payload: unknown): Promise<unknown> {\n const license = readLicenseSync();\n if (!isLicenseValid(license)) {\n return payload;\n }\n if (payload === null || typeof payload !== \"object\") return payload;\n const obj = payload as Record<string, unknown>;\n const prompt = obj.prompt ?? obj.text ?? obj.content;\n if (typeof prompt !== \"string\") return payload;\n try {\n const compressed = compressPrompt(prompt);\n const inputChars = prompt.length;\n const outputChars = compressed.length;\n updateSessionStatsSync(inputChars, outputChars);\n\n const result: Record<string, unknown> = { ...obj, prompt: compressed };\n\n const config = readConfigSync();\n if (config.quiet === true) {\n return result;\n }\n\n const stats = readSessionStatsSync();\n if (\n stats &&\n stats.requests_optimized > 0 &&\n stats.requests_optimized % SYSTEM_MESSAGE_EVERY_N_REQUESTS === 0\n ) {\n const msg = buildSystemMessage();\n if (msg) result.systemMessage = msg;\n }\n\n return result;\n } catch {\n return payload;\n }\n}\n\nasync function handlePreToolUse(payload: unknown): Promise<unknown> {\n return payload;\n}\n","import { mkdirSync, appendFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getLogDir } from \"@token-canary/shared\";\n\nconst LOG_FILENAME = \"daemon.log\";\n\nlet logDirEnsured = false;\n\nfunction ensureLogDir(): void {\n if (logDirEnsured) return;\n const dir = getLogDir();\n mkdirSync(dir, { recursive: true });\n logDirEnsured = true;\n}\n\n/**\n * Append one line to ~/.tokencanary/logs/daemon.log. Used for errors and request tracing.\n * Safe to call from request handlers; failures are swallowed.\n */\nexport function logToFile(level: string, message: string): void {\n try {\n ensureLogDir();\n const dir = getLogDir();\n const path = join(dir, LOG_FILENAME);\n const ts = new Date().toISOString();\n const line = `${ts} ${level} ${message}\\n`;\n appendFileSync(path, line, \"utf8\");\n } catch {\n // do not break daemon if logging fails\n }\n}\n\n/** Structured request log: one JSON line per hook request (request_id, latency_ms, optional sizes). Do not log prompt content. */\nexport function logStructuredRequest(entry: Record<string, unknown>): void {\n try {\n ensureLogDir();\n const dir = getLogDir();\n const path = join(dir, LOG_FILENAME);\n const line = JSON.stringify(entry) + \"\\n\";\n appendFileSync(path, line, \"utf8\");\n } catch {\n // do not break daemon if logging fails\n }\n}\n"],"mappings":"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","names":[]}
|