@aaa-sentinel/ai-usage 0.2.0 → 0.2.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 +5 -3
- package/dist/cli.js +23 -9
- package/dist/hook.js +51 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -226,9 +226,11 @@ systemd=true
|
|
|
226
226
|
usage. Remove or change it and the agent does not put it back; `logout` restores yours.
|
|
227
227
|
- **GitHub Copilot.** Needs the [GitHub CLI](https://cli.github.com) signed in to the account that
|
|
228
228
|
holds the Copilot plan: `gh auth login`, then `gh auth status` must show that account
|
|
229
|
-
(`winget install GitHub.cli` on Windows, `brew install gh` on macOS).
|
|
229
|
+
(`winget install GitHub.cli` on Windows, `brew install gh` on macOS). The default `gh` login
|
|
230
|
+
cannot read billing usage: add the `user` scope once with `gh auth refresh -h github.com -s user`.
|
|
231
|
+
Premium requests by model
|
|
230
232
|
are read through `gh`; GitHub does not report tokens. This is account usage, not device usage, so
|
|
231
|
-
two laptops do not double it. If `gh` answers 401 or 403, an empty row is not written. The
|
|
233
|
+
two laptops do not double it. If `gh` answers 401 or 403, or asks for the `user` scope, an empty row is not written. The
|
|
232
234
|
month's AI credits are read the same way and only their total is sent, to compare with the
|
|
233
235
|
credits the plan includes.
|
|
234
236
|
- **Codex.** Session counters, only when Codex is signed in with the ChatGPT subscription
|
|
@@ -276,7 +278,7 @@ measures Codex, not the ChatGPT chat.
|
|
|
276
278
|
| Exit code `3` — *No se pudo registrar el arranque* | Windows: a company policy may block Task Scheduler, ask IT. macOS: run `login` in the Terminal of your session, not over SSH. Linux/WSL: no systemd user instance, see [Automatic start](#automatic-start). Nothing is left installed |
|
|
277
279
|
| Exit code `4` — *El servidor no reconoce el token* | The token was rotated or the connection was revoked or removed. Get a new token and repeat step 4 |
|
|
278
280
|
| *El uso de esta herramienta no corresponde a esta conexión…* | The token was enrolled for a different tool. Use the token of the connection that matches this machine's tool |
|
|
279
|
-
| Exit code `5` | Copilot: install `gh
|
|
281
|
+
| Exit code `5` | Copilot: install `gh`, `gh auth login` to the account that holds Copilot, then `gh auth refresh -h github.com -s user` |
|
|
280
282
|
| `sentinel-usage: fetch failed` | No route to `https://api.aaa-sentinel.com` on port 443: proxy or firewall. Ask IT. For npm behind a proxy: `npm config set proxy …` and `npm config set https-proxy …` |
|
|
281
283
|
| The card stays on *Silent agent* | No usage this period yet. Use the tool, run `sentinel-usage report` and read its message |
|
|
282
284
|
|
package/dist/cli.js
CHANGED
|
@@ -63,7 +63,7 @@ ${args.map(cmdArg).join(" ")}\r
|
|
|
63
63
|
<Actions Context="Author"><Exec><Command>${xmlEscape(windowsCmd)}</Command></Exec></Actions>\r
|
|
64
64
|
</Task>\r
|
|
65
65
|
`;
|
|
66
|
-
writeFileSync(logonXml, Buffer.from(
|
|
66
|
+
writeFileSync(logonXml, Buffer.from(`${String.fromCharCode(65279)}${xml}`, "utf16le"));
|
|
67
67
|
try {
|
|
68
68
|
await run(exec, "schtasks", ["/Create", "/F", "/TN", TASK_LOGON, "/XML", logonXml]);
|
|
69
69
|
} finally {
|
|
@@ -190,6 +190,13 @@ WantedBy=timers.target
|
|
|
190
190
|
|
|
191
191
|
// src/quota.ts
|
|
192
192
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
193
|
+
|
|
194
|
+
// src/bom.ts
|
|
195
|
+
function stripBom(text) {
|
|
196
|
+
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/quota.ts
|
|
193
200
|
import { dirname } from "node:path";
|
|
194
201
|
var MAX_RESETS_PER_DAY = 48;
|
|
195
202
|
function num(value) {
|
|
@@ -279,7 +286,7 @@ function statusLineCommand(args, platform) {
|
|
|
279
286
|
function parseSettings(text) {
|
|
280
287
|
if (!text.trim()) return {};
|
|
281
288
|
try {
|
|
282
|
-
return asRecord(JSON.parse(text));
|
|
289
|
+
return asRecord(JSON.parse(stripBom(text)));
|
|
283
290
|
} catch {
|
|
284
291
|
return null;
|
|
285
292
|
}
|
|
@@ -387,7 +394,7 @@ function asRecord2(value) {
|
|
|
387
394
|
}
|
|
388
395
|
function mergeCursorHook(existing, command) {
|
|
389
396
|
let parsed = {};
|
|
390
|
-
if (existing.trim()) parsed = JSON.parse(existing);
|
|
397
|
+
if (existing.trim()) parsed = JSON.parse(stripBom(existing));
|
|
391
398
|
const root = asRecord2(parsed) ?? {};
|
|
392
399
|
const hooks = asRecord2(root["hooks"]) ?? {};
|
|
393
400
|
const stopRaw = hooks["stop"];
|
|
@@ -402,7 +409,7 @@ function mergeCursorHook(existing, command) {
|
|
|
402
409
|
}
|
|
403
410
|
function removeCursorHook(existing, command) {
|
|
404
411
|
if (!existing.trim()) return existing;
|
|
405
|
-
const root = asRecord2(JSON.parse(existing));
|
|
412
|
+
const root = asRecord2(JSON.parse(stripBom(existing)));
|
|
406
413
|
if (!root) return existing;
|
|
407
414
|
const hooks = asRecord2(root["hooks"]);
|
|
408
415
|
if (!hooks || !Array.isArray(hooks["stop"])) return existing;
|
|
@@ -952,6 +959,10 @@ function readCodexIncrement(dir, previous) {
|
|
|
952
959
|
// src/copilot.ts
|
|
953
960
|
var MODEL_PATTERN4 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
954
961
|
var API_VERSION = "2026-03-10";
|
|
962
|
+
function missingUserScope(result) {
|
|
963
|
+
return result.code !== 0 && /needs the "user" scope/i.test(`${result.stdout}
|
|
964
|
+
${result.stderr}`);
|
|
965
|
+
}
|
|
955
966
|
function authFailure(result) {
|
|
956
967
|
if (result.code === 0) return false;
|
|
957
968
|
const text = `${result.stdout}
|
|
@@ -1025,6 +1036,7 @@ async function ghJson(gh, path) {
|
|
|
1025
1036
|
`X-GitHub-Api-Version: ${API_VERSION}`,
|
|
1026
1037
|
path
|
|
1027
1038
|
]);
|
|
1039
|
+
if (missingUserScope(result)) return { auth: true, scope: true, payload: null };
|
|
1028
1040
|
if (authFailure(result)) return { auth: true, payload: null };
|
|
1029
1041
|
if (result.code !== 0) return { auth: false, payload: null };
|
|
1030
1042
|
try {
|
|
@@ -1070,6 +1082,7 @@ async function readCopilotDay(gh, day) {
|
|
|
1070
1082
|
const query = `year=${year}&month=${Number(month)}&day=${Number(date)}`;
|
|
1071
1083
|
const base = `/users/${login2}/settings/billing`;
|
|
1072
1084
|
const premium = await ghJson(gh, `${base}/premium_request/usage?${query}`);
|
|
1085
|
+
if (premium.scope) return { status: "scope" };
|
|
1073
1086
|
if (premium.auth) return { status: "auth" };
|
|
1074
1087
|
const rows = rowsFromCopilotUsage(premium.payload, day);
|
|
1075
1088
|
return rows.length > 0 ? { status: "rows", rows } : { status: "empty" };
|
|
@@ -1242,7 +1255,7 @@ function forgetLocalState(paths, source) {
|
|
|
1242
1255
|
if (source === "claude_code" && paths.quotaPath) rmSync2(paths.quotaPath, { force: true });
|
|
1243
1256
|
}
|
|
1244
1257
|
async function login(options) {
|
|
1245
|
-
const token = options.token
|
|
1258
|
+
const token = stripBom(options.token).trim();
|
|
1246
1259
|
if (!TOKEN_PATTERN.test(token)) {
|
|
1247
1260
|
return { code: EXIT_USAGE, message: "El token no tiene el formato de un alta de uso." };
|
|
1248
1261
|
}
|
|
@@ -1448,8 +1461,8 @@ async function reportConnection(options, connection, source, context) {
|
|
|
1448
1461
|
const credits = await readCopilotMonthCredits(options.gh, today);
|
|
1449
1462
|
for (const day of [previousUtcDay(today), today]) {
|
|
1450
1463
|
const read = await readCopilotDay(options.gh, day);
|
|
1451
|
-
if (read.status === "auth") {
|
|
1452
|
-
copilotAuth =
|
|
1464
|
+
if (read.status === "auth" || read.status === "scope") {
|
|
1465
|
+
copilotAuth = read.status;
|
|
1453
1466
|
break;
|
|
1454
1467
|
}
|
|
1455
1468
|
if (read.status === "unavailable") {
|
|
@@ -1497,7 +1510,8 @@ async function reportConnection(options, connection, source, context) {
|
|
|
1497
1510
|
if (outcome.error) return run2({ code: EXIT_REPORT, message: outcome.error });
|
|
1498
1511
|
if (outcome.mismatch) return run2({ code: 0, message: "El uso de esta herramienta no corresponde a esta conexi\xF3n y no se ha subido." });
|
|
1499
1512
|
if (copilotAuth && !outcome.acceptedAny) {
|
|
1500
|
-
|
|
1513
|
+
const fix = copilotAuth === "scope" ? "ejecuta gh auth refresh -h github.com -s user" : "inicia sesi\xF3n con gh auth login en la cuenta de Copilot";
|
|
1514
|
+
return run2({ code: EXIT_COPILOT, message: `gh no tiene permiso para leer el uso de Copilot: ${fix}. No se ha escrito una fila vac\xEDa.` });
|
|
1501
1515
|
}
|
|
1502
1516
|
if (copilotUnavailable) {
|
|
1503
1517
|
return run2({ code: EXIT_COPILOT, message: "No se pudo usar gh. Hace falta GitHub CLI con gh auth login en la cuenta de Copilot." });
|
|
@@ -1603,7 +1617,7 @@ var runPreviousWithShell = (command, input) => {
|
|
|
1603
1617
|
function statusLine(options) {
|
|
1604
1618
|
let parsed = null;
|
|
1605
1619
|
try {
|
|
1606
|
-
parsed = JSON.parse(options.input);
|
|
1620
|
+
parsed = JSON.parse(stripBom(options.input));
|
|
1607
1621
|
} catch {
|
|
1608
1622
|
parsed = null;
|
|
1609
1623
|
}
|
package/dist/hook.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
// src/hook.ts
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
// src/cursor.ts
|
|
2
5
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { mkdirSync,
|
|
6
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
4
7
|
import { join } from "node:path";
|
|
5
8
|
|
|
9
|
+
// src/bom.ts
|
|
10
|
+
function stripBom(text) {
|
|
11
|
+
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
// src/cursor.ts
|
|
7
15
|
var MODEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
8
16
|
var TOKEN_MAX = 1e12;
|
|
@@ -12,9 +20,9 @@ function finiteInt(value) {
|
|
|
12
20
|
if (rounded > TOKEN_MAX) return void 0;
|
|
13
21
|
return rounded;
|
|
14
22
|
}
|
|
15
|
-
function parseStopPayload(
|
|
16
|
-
if (!
|
|
17
|
-
const record =
|
|
23
|
+
function parseStopPayload(payload, at) {
|
|
24
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
|
25
|
+
const record = payload;
|
|
18
26
|
if (record["hook_event_name"] !== "stop") return null;
|
|
19
27
|
const modelRaw = record["model"];
|
|
20
28
|
const model = typeof modelRaw === "string" && MODEL_PATTERN.test(modelRaw) ? modelRaw : void 0;
|
|
@@ -22,25 +30,45 @@ function parseStopPayload(payload2, at) {
|
|
|
22
30
|
const output = finiteInt(record["output_tokens"]);
|
|
23
31
|
const cacheRead = finiteInt(record["cache_read_tokens"]);
|
|
24
32
|
const cacheWrite = finiteInt(record["cache_write_tokens"]);
|
|
25
|
-
const
|
|
26
|
-
if (model)
|
|
33
|
+
const event = { at, tokensKnown: false };
|
|
34
|
+
if (model) event.model = model;
|
|
27
35
|
if (input !== void 0 && output !== void 0 && cacheRead !== void 0 && cacheWrite !== void 0) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
event.tokensKnown = true;
|
|
37
|
+
event.inputTokens = Math.max(0, input - cacheRead - cacheWrite);
|
|
38
|
+
event.outputTokens = output;
|
|
39
|
+
event.cacheReadTokens = cacheRead;
|
|
40
|
+
event.cacheWriteTokens = cacheWrite;
|
|
33
41
|
}
|
|
34
|
-
return
|
|
42
|
+
return event;
|
|
35
43
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
function recordStop(raw2, spool2, at) {
|
|
45
|
+
let payload;
|
|
46
|
+
try {
|
|
47
|
+
payload = JSON.parse(stripBom(raw2));
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const event = parseStopPayload(payload, at);
|
|
52
|
+
if (!event) return false;
|
|
53
|
+
try {
|
|
54
|
+
mkdirSync(spool2, { recursive: true, mode: 448 });
|
|
55
|
+
writeFileSync(join(spool2, `${Date.now()}-${randomBytes(4).toString("hex")}.json`), serializeSpoolEvent(event), {
|
|
56
|
+
flag: "wx",
|
|
57
|
+
mode: 384
|
|
58
|
+
});
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function serializeSpoolEvent(event) {
|
|
65
|
+
const line = { at: event.at, tokensKnown: event.tokensKnown };
|
|
66
|
+
if (event.model) line["model"] = event.model;
|
|
67
|
+
if (event.tokensKnown) {
|
|
68
|
+
line["inputTokens"] = event.inputTokens ?? 0;
|
|
69
|
+
line["outputTokens"] = event.outputTokens ?? 0;
|
|
70
|
+
line["cacheReadTokens"] = event.cacheReadTokens ?? 0;
|
|
71
|
+
line["cacheWriteTokens"] = event.cacheWriteTokens ?? 0;
|
|
44
72
|
}
|
|
45
73
|
return JSON.stringify(line);
|
|
46
74
|
}
|
|
@@ -48,20 +76,10 @@ function serializeSpoolEvent(event2) {
|
|
|
48
76
|
// src/hook.ts
|
|
49
77
|
var spool = process.argv[2];
|
|
50
78
|
if (!spool) process.exit(0);
|
|
51
|
-
var
|
|
52
|
-
try {
|
|
53
|
-
payload = JSON.parse(readFileSync(0, "utf8"));
|
|
54
|
-
} catch {
|
|
55
|
-
process.exit(0);
|
|
56
|
-
}
|
|
57
|
-
var event = parseStopPayload(payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
58
|
-
if (!event) process.exit(0);
|
|
79
|
+
var raw;
|
|
59
80
|
try {
|
|
60
|
-
|
|
61
|
-
writeFileSync(join(spool, `${Date.now()}-${randomBytes(4).toString("hex")}.json`), serializeSpoolEvent(event), {
|
|
62
|
-
flag: "wx",
|
|
63
|
-
mode: 384
|
|
64
|
-
});
|
|
81
|
+
raw = readFileSync(0, "utf8");
|
|
65
82
|
} catch {
|
|
66
83
|
process.exit(0);
|
|
67
84
|
}
|
|
85
|
+
recordStop(raw, spool, (/* @__PURE__ */ new Date()).toISOString());
|
package/package.json
CHANGED