@aaa-sentinel/ai-usage 0.1.2 → 0.2.0

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.
Files changed (3) hide show
  1. package/README.md +339 -324
  2. package/dist/cli.js +541 -146
  3. package/package.json +42 -42
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli.ts
4
4
  import { execFile } from "node:child_process";
5
5
  import { homedir, userInfo } from "node:os";
6
- import { dirname as dirname2, join as join5 } from "node:path";
6
+ import { dirname as dirname3, join as join5 } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { promisify } from "node:util";
9
9
 
@@ -188,10 +188,137 @@ WantedBy=timers.target
188
188
  };
189
189
  }
190
190
 
191
+ // src/quota.ts
192
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
193
+ import { dirname } from "node:path";
194
+ var MAX_RESETS_PER_DAY = 48;
195
+ function num(value) {
196
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
197
+ }
198
+ function asRecord(value) {
199
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
200
+ }
201
+ function readQuotaState(path) {
202
+ if (!existsSync2(path)) return { days: {} };
203
+ try {
204
+ const root = asRecord(JSON.parse(readFileSync2(path, "utf8")));
205
+ const days = {};
206
+ for (const [day, raw] of Object.entries(asRecord(root?.["days"]) ?? {})) {
207
+ const entry = asRecord(raw);
208
+ if (!entry || !/^\d{4}-\d{2}-\d{2}$/.test(day)) continue;
209
+ const parsed = {};
210
+ const five = num(entry["fiveHourPeak"]);
211
+ const seven = num(entry["sevenDayPeak"]);
212
+ if (five !== void 0) parsed.fiveHourPeak = five;
213
+ if (seven !== void 0) parsed.sevenDayPeak = seven;
214
+ const resets = entry["fiveHourLimitResets"];
215
+ if (Array.isArray(resets)) {
216
+ const valid = resets.filter((end) => typeof end === "number" && Number.isInteger(end) && end > 0);
217
+ if (valid.length > 0) parsed.fiveHourLimitResets = valid.slice(-MAX_RESETS_PER_DAY);
218
+ }
219
+ days[day] = parsed;
220
+ }
221
+ const lastRaw = asRecord(root?.["last"]);
222
+ const state = { days };
223
+ if (lastRaw) {
224
+ const last = {};
225
+ const five = num(lastRaw["fiveHour"]);
226
+ const seven = num(lastRaw["sevenDay"]);
227
+ if (five !== void 0) last.fiveHour = five;
228
+ if (seven !== void 0) last.sevenDay = seven;
229
+ state.last = last;
230
+ }
231
+ return state;
232
+ } catch {
233
+ return { days: {} };
234
+ }
235
+ }
236
+ function writeQuotaState(path, state) {
237
+ mkdirSync2(dirname(path), { recursive: true, mode: 448 });
238
+ const temp = `${path}.${process.pid}.tmp`;
239
+ writeFileSync2(temp, `${JSON.stringify(state)}
240
+ `, { mode: 384 });
241
+ renameSync(temp, path);
242
+ }
243
+ function recordStatusSample(state, input, now) {
244
+ const limits = asRecord(asRecord(input)?.["rate_limits"]);
245
+ if (!limits) return false;
246
+ const fiveHour = asRecord(limits["five_hour"]);
247
+ const sevenDay = asRecord(limits["seven_day"]);
248
+ const five = num(fiveHour?.["used_percentage"]);
249
+ const seven = num(sevenDay?.["used_percentage"]);
250
+ if (five === void 0 && seven === void 0) return false;
251
+ const day = now.toISOString().slice(0, 10);
252
+ const entry = state.days[day] ?? {};
253
+ const before = JSON.stringify(entry);
254
+ if (five !== void 0) entry.fiveHourPeak = Math.max(entry.fiveHourPeak ?? 0, five);
255
+ if (seven !== void 0) entry.sevenDayPeak = Math.max(entry.sevenDayPeak ?? 0, seven);
256
+ const resetsAt = num(fiveHour?.["resets_at"]);
257
+ if (five !== void 0 && five >= 100 && resetsAt !== void 0 && Number.isInteger(resetsAt)) {
258
+ const resets = new Set(entry.fiveHourLimitResets ?? []);
259
+ resets.add(resetsAt);
260
+ entry.fiveHourLimitResets = [...resets].slice(-MAX_RESETS_PER_DAY);
261
+ }
262
+ state.days[day] = entry;
263
+ const lastBefore = JSON.stringify(state.last ?? {});
264
+ state.last = {
265
+ ...five !== void 0 ? { fiveHour: five } : state.last?.fiveHour !== void 0 ? { fiveHour: state.last.fiveHour } : {},
266
+ ...seven !== void 0 ? { sevenDay: seven } : state.last?.sevenDay !== void 0 ? { sevenDay: state.last.sevenDay } : {}
267
+ };
268
+ return JSON.stringify(entry) !== before || JSON.stringify(state.last) !== lastBefore;
269
+ }
270
+ function defaultStatusText(state) {
271
+ const parts = [];
272
+ if (state.last?.fiveHour !== void 0) parts.push(`5 h ${Math.round(state.last.fiveHour)} %`);
273
+ if (state.last?.sevenDay !== void 0) parts.push(`semana ${Math.round(state.last.sevenDay)} %`);
274
+ return parts.join(" \xB7 ");
275
+ }
276
+ function statusLineCommand(args, platform) {
277
+ return args.map((arg) => platform === "win32" ? arg.replaceAll("\\", "/") : arg).map((arg) => `"${arg.replaceAll('"', '\\"')}"`).join(" ");
278
+ }
279
+ function parseSettings(text) {
280
+ if (!text.trim()) return {};
281
+ try {
282
+ return asRecord(JSON.parse(text));
283
+ } catch {
284
+ return null;
285
+ }
286
+ }
287
+ function statusLineOf(root) {
288
+ const raw = asRecord(root["statusLine"]);
289
+ if (!raw || typeof raw["command"] !== "string") return void 0;
290
+ const setting = { type: typeof raw["type"] === "string" ? raw["type"] : "command", command: raw["command"] };
291
+ if (typeof raw["padding"] === "number") setting.padding = raw["padding"];
292
+ return setting;
293
+ }
294
+ function mergeClaudeStatusLine(existing, command) {
295
+ const root = parseSettings(existing);
296
+ if (!root) return { status: "unreadable" };
297
+ const previous = statusLineOf(root);
298
+ if (previous?.command === command) return { status: "already" };
299
+ const next = { ...root, statusLine: { type: "command", command, ...previous?.padding !== void 0 ? { padding: previous.padding } : {} } };
300
+ return { status: "installed", text: `${JSON.stringify(next, null, 2)}
301
+ `, ...previous ? { previous } : {} };
302
+ }
303
+ function hasClaudeStatusLine(existing, command) {
304
+ const root = parseSettings(existing);
305
+ return root ? statusLineOf(root)?.command === command : false;
306
+ }
307
+ function removeClaudeStatusLine(existing, command, previous) {
308
+ const root = parseSettings(existing);
309
+ if (!root || statusLineOf(root)?.command !== command) return existing;
310
+ const next = { ...root };
311
+ if (previous) next["statusLine"] = { ...previous };
312
+ else delete next["statusLine"];
313
+ return `${JSON.stringify(next, null, 2)}
314
+ `;
315
+ }
316
+
191
317
  // src/session.ts
318
+ import { spawnSync } from "node:child_process";
192
319
  import { randomBytes } from "node:crypto";
193
- import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
194
- import { dirname, join as join4 } from "node:path";
320
+ import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
321
+ import { dirname as dirname2, join as join4 } from "node:path";
195
322
 
196
323
  // src/cursor.ts
197
324
  var MODEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
@@ -254,19 +381,19 @@ ${event.model ?? ""}`;
254
381
  }
255
382
  return byDay;
256
383
  }
257
- function asRecord(value) {
384
+ function asRecord2(value) {
258
385
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
259
386
  return value;
260
387
  }
261
388
  function mergeCursorHook(existing, command) {
262
389
  let parsed = {};
263
390
  if (existing.trim()) parsed = JSON.parse(existing);
264
- const root = asRecord(parsed) ?? {};
265
- const hooks = asRecord(root["hooks"]) ?? {};
391
+ const root = asRecord2(parsed) ?? {};
392
+ const hooks = asRecord2(root["hooks"]) ?? {};
266
393
  const stopRaw = hooks["stop"];
267
- const stop = Array.isArray(stopRaw) ? stopRaw.filter((entry) => asRecord(entry)?.["command"] !== command) : [];
394
+ const stop = Array.isArray(stopRaw) ? stopRaw.filter((entry) => asRecord2(entry)?.["command"] !== command) : [];
268
395
  const kept = stop.flatMap((entry) => {
269
- const record = asRecord(entry);
396
+ const record = asRecord2(entry);
270
397
  if (!record || typeof record["command"] !== "string") return [];
271
398
  return [record];
272
399
  });
@@ -275,11 +402,11 @@ function mergeCursorHook(existing, command) {
275
402
  }
276
403
  function removeCursorHook(existing, command) {
277
404
  if (!existing.trim()) return existing;
278
- const root = asRecord(JSON.parse(existing));
405
+ const root = asRecord2(JSON.parse(existing));
279
406
  if (!root) return existing;
280
- const hooks = asRecord(root["hooks"]);
407
+ const hooks = asRecord2(root["hooks"]);
281
408
  if (!hooks || !Array.isArray(hooks["stop"])) return existing;
282
- const stop = hooks["stop"].filter((entry) => asRecord(entry)?.["command"] !== command);
409
+ const stop = hooks["stop"].filter((entry) => asRecord2(entry)?.["command"] !== command);
283
410
  if (stop.length === 0) {
284
411
  const rest = {};
285
412
  for (const [key, value] of Object.entries(hooks)) {
@@ -574,7 +701,7 @@ function readClaudeIncrement(dir, previous) {
574
701
  }
575
702
 
576
703
  // src/codex.ts
577
- import { closeSync as closeSync2, openSync as openSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, readSync as readSync2, statSync as statSync2 } from "node:fs";
704
+ import { closeSync as closeSync2, openSync as openSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, readSync as readSync2, statSync as statSync2 } from "node:fs";
578
705
  import { join as join3, relative as relative2 } from "node:path";
579
706
  var MODEL_PATTERN3 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
580
707
  var TOKEN_MAX3 = 1e12;
@@ -772,7 +899,7 @@ function readChunk2(path, offset) {
772
899
  }
773
900
  function readCodexAuth(home) {
774
901
  try {
775
- return classifyCodexAuth(readFileSync2(join3(home, "auth.json"), "utf8"));
902
+ return classifyCodexAuth(readFileSync3(join3(home, "auth.json"), "utf8"));
776
903
  } catch {
777
904
  return "unknown";
778
905
  }
@@ -906,12 +1033,39 @@ async function ghJson(gh, path) {
906
1033
  return { auth: false, payload: null };
907
1034
  }
908
1035
  }
909
- async function readCopilotDay(gh, day) {
1036
+ function creditsFromCopilotUsage(payload) {
1037
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return void 0;
1038
+ const items = payload["usageItems"];
1039
+ if (!Array.isArray(items)) return void 0;
1040
+ let total = 0;
1041
+ for (const item of items) {
1042
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
1043
+ const record = item;
1044
+ const unit = typeof record["unitType"] === "string" ? record["unitType"].toLowerCase() : "";
1045
+ const quantity = typeof record["grossQuantity"] === "number" && Number.isFinite(record["grossQuantity"]) && record["grossQuantity"] >= 0 ? record["grossQuantity"] : void 0;
1046
+ if (unit.includes("credit") && quantity !== void 0) total += quantity;
1047
+ }
1048
+ return total;
1049
+ }
1050
+ async function copilotLogin(gh) {
910
1051
  const user = await ghJson(gh, "user");
911
1052
  if (user.auth) return { status: "auth" };
912
1053
  if (!user.payload) return { status: "unavailable" };
913
1054
  const login2 = user.payload && typeof user.payload === "object" && !Array.isArray(user.payload) ? user.payload["login"] : void 0;
914
1055
  if (typeof login2 !== "string" || !/^[A-Za-z0-9-]{1,39}$/.test(login2)) return { status: "empty" };
1056
+ return { status: "ok", login: login2 };
1057
+ }
1058
+ async function readCopilotMonthCredits(gh, day) {
1059
+ const user = await copilotLogin(gh);
1060
+ if (user.status !== "ok") return void 0;
1061
+ const [year, month] = day.split("-");
1062
+ const usage = await ghJson(gh, `/users/${user.login}/settings/billing/ai_credit/usage?year=${year}&month=${Number(month)}`);
1063
+ return usage.payload ? creditsFromCopilotUsage(usage.payload) : void 0;
1064
+ }
1065
+ async function readCopilotDay(gh, day) {
1066
+ const user = await copilotLogin(gh);
1067
+ if (user.status !== "ok") return { status: user.status };
1068
+ const login2 = user.login;
915
1069
  const [year, month, date] = day.split("-");
916
1070
  const query = `year=${year}&month=${Number(month)}&day=${Number(date)}`;
917
1071
  const base = `/users/${login2}/settings/billing`;
@@ -975,7 +1129,8 @@ async function postTelemetry(options) {
975
1129
  turns: row.turns,
976
1130
  tokensKnown: row.tokensKnown,
977
1131
  ...row.model ? { model: row.model } : {}
978
- }))
1132
+ })),
1133
+ ...options.quota ? { quota: options.quota } : {}
979
1134
  })
980
1135
  });
981
1136
  let code;
@@ -995,47 +1150,82 @@ var EXIT_REPORT = 4;
995
1150
  var EXIT_COPILOT = 5;
996
1151
  var MAX_ROWS = 40;
997
1152
  var TOKEN_PATTERN = /^sau_[A-Za-z0-9_-]{20,200}$/;
1153
+ function parseStatusLineConfig(value) {
1154
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1155
+ const record = value;
1156
+ if (record["state"] !== "installed" && record["state"] !== "declined") return void 0;
1157
+ const config = { state: record["state"] };
1158
+ const previous = record["previous"];
1159
+ if (previous && typeof previous === "object" && !Array.isArray(previous)) {
1160
+ const raw = previous;
1161
+ if (typeof raw["command"] === "string") {
1162
+ config.previous = {
1163
+ type: typeof raw["type"] === "string" ? raw["type"] : "command",
1164
+ command: raw["command"],
1165
+ ...typeof raw["padding"] === "number" ? { padding: raw["padding"] } : {}
1166
+ };
1167
+ }
1168
+ }
1169
+ return config;
1170
+ }
998
1171
  var DEVICE_PATTERN = /^[a-f0-9]{16}$/;
999
1172
  function newDevice() {
1000
1173
  return randomBytes(8).toString("hex");
1001
1174
  }
1002
- function readJson(path) {
1003
- if (!existsSync2(path)) return {};
1175
+ function parseConnection(record) {
1176
+ if (typeof record["token"] !== "string") return null;
1177
+ const connection = { token: record["token"] };
1178
+ if (isUsageSource(record["source"])) connection.source = record["source"];
1179
+ if (typeof record["addedAt"] === "string") connection.addedAt = record["addedAt"];
1180
+ if (typeof record["lastAcceptedDay"] === "string") connection.lastAcceptedDay = record["lastAcceptedDay"];
1181
+ if (record["claude"]) connection.claude = parseClaudeState(record["claude"]);
1182
+ if (record["codex"]) connection.codex = parseCodexState(record["codex"]);
1183
+ return connection;
1184
+ }
1185
+ function readConfig(path) {
1186
+ if (!existsSync3(path)) return { connections: [] };
1004
1187
  try {
1005
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
1006
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
1188
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
1189
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { connections: [] };
1007
1190
  const record = parsed;
1008
- const config = {};
1009
- if (typeof record["token"] === "string") config.token = record["token"];
1191
+ const config = { connections: [] };
1010
1192
  if (typeof record["device"] === "string" && DEVICE_PATTERN.test(record["device"])) config.device = record["device"];
1011
- if (typeof record["lastAcceptedDay"] === "string") config.lastAcceptedDay = record["lastAcceptedDay"];
1012
- if (record["claude"]) config.claude = parseClaudeState(record["claude"]);
1013
- if (record["codex"]) config.codex = parseCodexState(record["codex"]);
1014
- if (isUsageSource(record["source"])) config.source = record["source"];
1193
+ if (Array.isArray(record["connections"])) {
1194
+ for (const entry of record["connections"]) {
1195
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
1196
+ const connection = parseConnection(entry);
1197
+ if (connection) config.connections.push(connection);
1198
+ }
1199
+ } else {
1200
+ const legacy = parseConnection(record);
1201
+ if (legacy) config.connections.push(legacy);
1202
+ }
1203
+ const statusLine2 = parseStatusLineConfig(record["claudeStatusLine"]);
1204
+ if (statusLine2) config.claudeStatusLine = statusLine2;
1015
1205
  return config;
1016
1206
  } catch {
1017
- return {};
1207
+ return { connections: [] };
1018
1208
  }
1019
1209
  }
1020
- function writeJson(path, value) {
1021
- mkdirSync2(dirname(path), { recursive: true, mode: 448 });
1022
- writeFileSync2(path, `${JSON.stringify(value)}
1210
+ function writeConfig(path, value) {
1211
+ mkdirSync3(dirname2(path), { recursive: true, mode: 448 });
1212
+ writeFileSync3(path, `${JSON.stringify({ version: 2, ...value })}
1023
1213
  `, { mode: 384 });
1024
1214
  chmodSync(path, 384);
1025
1215
  }
1026
1216
  function restoreFile(path, previous) {
1027
1217
  if (previous === null) {
1028
- if (existsSync2(path)) unlinkSync(path);
1218
+ if (existsSync3(path)) unlinkSync(path);
1029
1219
  return;
1030
1220
  }
1031
- writeFileSync2(path, previous);
1221
+ writeFileSync3(path, previous);
1032
1222
  }
1033
1223
  function readSpool(dir) {
1034
- if (!existsSync2(dir)) return [];
1224
+ if (!existsSync3(dir)) return [];
1035
1225
  return readdirSync3(dir).filter((name) => name.endsWith(".json")).map((name) => {
1036
1226
  const path = join4(dir, name);
1037
1227
  try {
1038
- return { path, event: readSpoolEvent(JSON.parse(readFileSync3(path, "utf8"))) };
1228
+ return { path, event: readSpoolEvent(JSON.parse(readFileSync4(path, "utf8"))) };
1039
1229
  } catch {
1040
1230
  return { path, event: null };
1041
1231
  }
@@ -1047,27 +1237,46 @@ function utcDay(date) {
1047
1237
  function clearSpool(dir) {
1048
1238
  rmSync2(dir, { recursive: true, force: true });
1049
1239
  }
1240
+ function forgetLocalState(paths, source) {
1241
+ if (source === "cursor") clearSpool(paths.spoolPath);
1242
+ if (source === "claude_code" && paths.quotaPath) rmSync2(paths.quotaPath, { force: true });
1243
+ }
1050
1244
  async function login(options) {
1051
- const token = options.token.replace(/^\uFEFF/, "").trim();
1245
+ const token = options.token.replace(/^/, "").trim();
1052
1246
  if (!TOKEN_PATTERN.test(token)) {
1053
1247
  return { code: EXIT_USAGE, message: "El token no tiene el formato de un alta de uso." };
1054
1248
  }
1055
- const previousConfig = existsSync2(options.paths.configPath) ? readFileSync3(options.paths.configPath, "utf8") : null;
1056
- const previousHooks = existsSync2(options.paths.hooksPath) ? readFileSync3(options.paths.hooksPath, "utf8") : null;
1057
- const config = readJson(options.paths.configPath);
1058
- const tokenChanged = config.token !== void 0 && config.token !== token;
1059
- const next = { token, device: config.device ?? newDevice() };
1060
- if (config.lastAcceptedDay) next.lastAcceptedDay = config.lastAcceptedDay;
1061
- const since = utcDay(options.now ?? /* @__PURE__ */ new Date());
1062
- next.claude = config.claude && !tokenChanged ? config.claude : emptyClaudeState(since);
1063
- next.codex = config.codex && !tokenChanged ? config.codex : emptyCodexState(since);
1064
- if (config.source && !tokenChanged) next.source = config.source;
1065
- writeJson(options.paths.configPath, next);
1249
+ const previousConfig = existsSync3(options.paths.configPath) ? readFileSync4(options.paths.configPath, "utf8") : null;
1250
+ const previousHooks = existsSync3(options.paths.hooksPath) ? readFileSync4(options.paths.hooksPath, "utf8") : null;
1251
+ const config = readConfig(options.paths.configPath);
1252
+ const now = options.now ?? /* @__PURE__ */ new Date();
1253
+ const existing = config.connections.find((connection) => connection.token === token);
1254
+ let source = existing?.source;
1255
+ let replaced = [];
1256
+ if (!existing) {
1257
+ try {
1258
+ source = await options.probe?.(token);
1259
+ } catch {
1260
+ source = void 0;
1261
+ }
1262
+ const since = utcDay(now);
1263
+ const added = {
1264
+ token,
1265
+ addedAt: now.toISOString(),
1266
+ claude: emptyClaudeState(since),
1267
+ codex: emptyCodexState(since),
1268
+ ...source ? { source } : {}
1269
+ };
1270
+ replaced = source ? config.connections.filter((connection) => connection.source === source) : [];
1271
+ config.connections = [...config.connections.filter((connection) => !replaced.includes(connection)), added];
1272
+ }
1273
+ config.device = config.device ?? newDevice();
1274
+ writeConfig(options.paths.configPath, config);
1066
1275
  try {
1067
- mkdirSync2(dirname(options.paths.hooksPath), { recursive: true });
1068
- writeFileSync2(options.paths.hooksPath, mergeCursorHook(previousHooks ?? "", options.hookCommand));
1276
+ mkdirSync3(dirname2(options.paths.hooksPath), { recursive: true });
1277
+ writeFileSync3(options.paths.hooksPath, mergeCursorHook(previousHooks ?? "", options.hookCommand));
1069
1278
  await options.launcher.install();
1070
- if (tokenChanged) clearSpool(options.paths.spoolPath);
1279
+ if (replaced.length > 0) forgetLocalState(options.paths, source);
1071
1280
  } catch (err) {
1072
1281
  restoreFile(options.paths.configPath, previousConfig);
1073
1282
  restoreFile(options.paths.hooksPath, previousHooks);
@@ -1077,72 +1286,108 @@ async function login(options) {
1077
1286
  message: launcherFailed ? `No se pudo registrar el arranque. No se ha dejado el token instalado. ${err.message}` : `No se pudo instalar el hook de Cursor. No se ha dejado el token instalado. ${err.message}`
1078
1287
  };
1079
1288
  }
1080
- return { code: 0, message: "Agente instalado. El arranque subir\xE1 el uso; no hace falta lanzarlo en cada sesi\xF3n." };
1289
+ const tools = config.connections.map((connection) => connection.source ?? "sin comprobar").join(", ");
1290
+ const replacedNote = replaced.length > 0 ? ` Sustituye a la conexi\xF3n anterior de ${source}.` : "";
1291
+ return {
1292
+ code: 0,
1293
+ message: `Agente instalado. El arranque subir\xE1 el uso; no hace falta lanzarlo en cada sesi\xF3n.${replacedNote} Este ordenador informa de: ${tools}.`
1294
+ };
1081
1295
  }
1082
1296
  async function logout(options) {
1297
+ const config = readConfig(options.paths.configPath);
1298
+ const leaving = options.source ? config.connections.filter((connection) => connection.source === options.source) : config.connections;
1299
+ if (options.source && leaving.length === 0) {
1300
+ return { code: EXIT_USAGE, message: `Este ordenador no informa de ${options.source}.` };
1301
+ }
1302
+ const staying = config.connections.filter((connection) => !leaving.includes(connection));
1083
1303
  let launcherError;
1084
- try {
1085
- await options.launcher.uninstall();
1086
- } catch (err) {
1087
- launcherError = err;
1304
+ if (staying.length === 0) {
1305
+ try {
1306
+ await options.launcher.uninstall();
1307
+ } catch (err) {
1308
+ launcherError = err;
1309
+ }
1088
1310
  }
1089
1311
  let hookError = false;
1090
- if (existsSync2(options.paths.hooksPath)) {
1091
- try {
1092
- const current = readFileSync3(options.paths.hooksPath, "utf8");
1093
- writeFileSync2(options.paths.hooksPath, removeCursorHook(current, options.hookCommand));
1094
- } catch {
1095
- hookError = true;
1312
+ const keepsCursor = staying.some((connection) => connection.source === "cursor" || !connection.source);
1313
+ if (!keepsCursor) {
1314
+ if (existsSync3(options.paths.hooksPath)) {
1315
+ try {
1316
+ const current = readFileSync4(options.paths.hooksPath, "utf8");
1317
+ writeFileSync3(options.paths.hooksPath, removeCursorHook(current, options.hookCommand));
1318
+ } catch {
1319
+ hookError = true;
1320
+ }
1096
1321
  }
1322
+ clearSpool(options.paths.spoolPath);
1097
1323
  }
1098
- clearSpool(options.paths.spoolPath);
1099
- const config = readJson(options.paths.configPath);
1100
- if (config.token || config.lastAcceptedDay) {
1101
- const next = {};
1102
- if (config.lastAcceptedDay) next.lastAcceptedDay = config.lastAcceptedDay;
1103
- writeJson(options.paths.configPath, next);
1324
+ const next = { ...config.device ? { device: config.device } : {}, connections: staying };
1325
+ const keepsClaude = staying.some((connection) => connection.source === "claude_code" || !connection.source);
1326
+ if (keepsClaude) {
1327
+ if (config.claudeStatusLine) next.claudeStatusLine = config.claudeStatusLine;
1328
+ } else {
1329
+ if (config.claudeStatusLine?.state === "installed") {
1330
+ restoreClaudeStatusLine(options.paths, options.statusLineCommand, config.claudeStatusLine.previous);
1331
+ }
1332
+ if (options.paths.quotaPath) rmSync2(options.paths.quotaPath, { force: true });
1104
1333
  }
1334
+ if (existsSync3(options.paths.configPath) || staying.length > 0) writeConfig(options.paths.configPath, next);
1105
1335
  if (launcherError) {
1106
1336
  return { code: EXIT_LAUNCHER, message: `No se pudo quitar el arranque. ${launcherError.message}` };
1107
1337
  }
1108
1338
  if (hookError) {
1109
1339
  return { code: EXIT_USAGE, message: "El arranque se quit\xF3, pero el hook de Cursor no: su fichero no es JSON." };
1110
1340
  }
1341
+ if (staying.length > 0) {
1342
+ return { code: 0, message: `Conexi\xF3n retirada. Este ordenador sigue informando de: ${staying.map((connection) => connection.source ?? "sin comprobar").join(", ")}.` };
1343
+ }
1111
1344
  return { code: 0, message: "Agente retirado." };
1112
1345
  }
1113
1346
  function dropCursorHook(paths, hookCommand) {
1114
- if (!hookCommand || !existsSync2(paths.hooksPath)) return;
1347
+ if (!hookCommand || !existsSync3(paths.hooksPath)) return;
1115
1348
  try {
1116
- const current = readFileSync3(paths.hooksPath, "utf8");
1349
+ const current = readFileSync4(paths.hooksPath, "utf8");
1117
1350
  const next = removeCursorHook(current, hookCommand);
1118
- if (next !== current) writeFileSync2(paths.hooksPath, next);
1351
+ if (next !== current) writeFileSync3(paths.hooksPath, next);
1119
1352
  } catch {
1120
1353
  }
1121
1354
  }
1122
- async function report(options) {
1123
- const config = readJson(options.paths.configPath);
1124
- const token = config.token;
1125
- if (!token) return { code: EXIT_USAGE, message: "No hay token. Hay que hacer login en esta conexi\xF3n." };
1126
- const device = config.device ?? newDevice();
1127
- const baseUrl = resolveUsageBaseUrl(options.baseUrl);
1128
- if (!baseUrl) return { code: EXIT_USAGE, message: "La URL del API tiene que ser https." };
1129
- const today = utcDay(options.now ?? /* @__PURE__ */ new Date());
1130
- const probe = await fetchUsageSource({ baseUrl, token, fetchImpl: options.fetchImpl });
1131
- if (probe.status === 401) {
1132
- return { code: EXIT_REPORT, message: "El servidor no reconoce el token. Hay que repetir el login con un token nuevo." };
1355
+ function restoreClaudeStatusLine(paths, command, previous) {
1356
+ if (!command || !paths.claudeSettingsPath || !existsSync3(paths.claudeSettingsPath)) return;
1357
+ try {
1358
+ const current = readFileSync4(paths.claudeSettingsPath, "utf8");
1359
+ const next = removeClaudeStatusLine(current, command, previous);
1360
+ if (next !== current) writeFileSync3(paths.claudeSettingsPath, next);
1361
+ } catch {
1133
1362
  }
1134
- const source = probe.source ?? config.source;
1135
- if (!source) {
1136
- return { code: EXIT_REPORT, message: `No se pudo saber qu\xE9 herramienta informa esta conexi\xF3n (${probe.status}). Se reintentar\xE1.` };
1363
+ }
1364
+ function ensureClaudeStatusLine(paths, command, current) {
1365
+ const settingsPath = paths.claudeSettingsPath;
1366
+ if (!command || !settingsPath || current?.state === "declined") return current;
1367
+ try {
1368
+ const text = existsSync3(settingsPath) ? readFileSync4(settingsPath, "utf8") : "";
1369
+ if (current?.state === "installed") {
1370
+ return hasClaudeStatusLine(text, command) ? current : { state: "declined", ...current.previous ? { previous: current.previous } : {} };
1371
+ }
1372
+ const merged = mergeClaudeStatusLine(text, command);
1373
+ if (merged.status === "unreadable") return current;
1374
+ if (merged.status === "already") return { state: "installed" };
1375
+ mkdirSync3(dirname2(settingsPath), { recursive: true });
1376
+ writeFileSync3(settingsPath, merged.text);
1377
+ return { state: "installed", ...merged.previous ? { previous: merged.previous } : {} };
1378
+ } catch {
1379
+ return current;
1137
1380
  }
1138
- const files = readSpool(options.paths.spoolPath);
1381
+ }
1382
+ async function reportConnection(options, connection, source, context) {
1383
+ const { baseUrl, device, today } = context;
1384
+ const token = connection.token;
1139
1385
  const accepted = /* @__PURE__ */ new Set();
1140
1386
  const outcome = { error: void 0, mismatch: false, acceptedAny: false };
1141
- let lastAccepted = config.lastAcceptedDay;
1142
- let claudeState = config.claude;
1143
- let codexState = config.codex;
1144
- const send = async (day, rows, kept) => {
1145
- if (rows.length === 0) return "skip";
1387
+ let lastAccepted = connection.lastAcceptedDay;
1388
+ let statusLine2 = context.statusLine;
1389
+ const send = async (day, rows, kept, quota) => {
1390
+ if (rows.length === 0 && !quota) return "skip";
1146
1391
  if (rows.length > MAX_ROWS) {
1147
1392
  outcome.error = `El d\xEDa ${day} tiene m\xE1s de ${MAX_ROWS} modelos y no se ha subido.`;
1148
1393
  return "stop";
@@ -1154,7 +1399,8 @@ async function report(options) {
1154
1399
  rows,
1155
1400
  fetchImpl: options.fetchImpl,
1156
1401
  source,
1157
- ...source === "github_copilot" ? {} : { device }
1402
+ ...source === "github_copilot" ? {} : { device },
1403
+ ...quota ? { quota } : {}
1158
1404
  });
1159
1405
  if (result.ok) {
1160
1406
  outcome.acceptedAny = true;
@@ -1174,24 +1420,32 @@ async function report(options) {
1174
1420
  let copilotUnavailable = false;
1175
1421
  let codexAuth;
1176
1422
  if (source === "cursor") {
1177
- const byDay = aggregateByDay(files.flatMap((file) => file.event ? [file.event] : []));
1423
+ const byDay = aggregateByDay(context.spool.flatMap((file) => file.event ? [file.event] : []));
1178
1424
  for (const day of [...byDay.keys()].sort()) {
1179
1425
  if (await send(day, byDay.get(day) ?? [], " El spool de ese d\xEDa sigue en el port\xE1til.") === "stop") break;
1180
1426
  }
1181
1427
  } else if (source === "claude_code") {
1428
+ statusLine2 = ensureClaudeStatusLine(options.paths, options.statusLineCommand, statusLine2);
1182
1429
  const claudeDir = options.claudeProjectsDir;
1183
- const ready = claudeDir && existsSync2(claudeDir) ? readClaudeIncrement(claudeDir, config.claude ?? emptyClaudeState(today)) : null;
1184
- if (ready) {
1185
- for (const day of ready.dirtyDays) {
1186
- if (await send(day, rowsForDay(ready.state.days, day), " La transcripci\xF3n sigue en el port\xE1til.") === "stop") break;
1187
- }
1188
- if (!outcome.error) {
1189
- claudeState = ready.state;
1190
- if (ready.caughtUp) pruneClaudeDays(claudeState, previousUtcDay(today));
1191
- }
1430
+ const ready = claudeDir && existsSync3(claudeDir) ? readClaudeIncrement(claudeDir, connection.claude ?? emptyClaudeState(today)) : null;
1431
+ const quotaDays = options.paths.quotaPath ? readQuotaState(options.paths.quotaPath).days : {};
1432
+ const days = [.../* @__PURE__ */ new Set([...ready?.dirtyDays ?? [], ...Object.keys(quotaDays)])].sort();
1433
+ for (const day of days) {
1434
+ const rows = ready ? rowsForDay(ready.state.days, day) : [];
1435
+ if (await send(day, rows, " La transcripci\xF3n sigue en el port\xE1til.", quotaDays[day]) === "stop") break;
1436
+ }
1437
+ if (ready && !outcome.error) {
1438
+ connection.claude = ready.state;
1439
+ if (ready.caughtUp) pruneClaudeDays(connection.claude, previousUtcDay(today));
1440
+ }
1441
+ if (options.paths.quotaPath && accepted.size > 0) {
1442
+ const fresh = readQuotaState(options.paths.quotaPath);
1443
+ for (const day of Object.keys(fresh.days)) if (day < today && accepted.has(day)) delete fresh.days[day];
1444
+ writeQuotaState(options.paths.quotaPath, fresh);
1192
1445
  }
1193
1446
  } else if (source === "github_copilot") {
1194
1447
  if (options.gh) {
1448
+ const credits = await readCopilotMonthCredits(options.gh, today);
1195
1449
  for (const day of [previousUtcDay(today), today]) {
1196
1450
  const read = await readCopilotDay(options.gh, day);
1197
1451
  if (read.status === "auth") {
@@ -1202,78 +1456,183 @@ async function report(options) {
1202
1456
  copilotUnavailable = true;
1203
1457
  break;
1204
1458
  }
1205
- if (read.status !== "rows") continue;
1206
- if (await send(day, read.rows, "") === "stop") break;
1459
+ const quota = day === today && credits !== void 0 ? { monthAiCredits: credits } : void 0;
1460
+ if (read.status !== "rows" && !quota) continue;
1461
+ if (await send(day, read.status === "rows" ? read.rows : [], "", quota) === "stop") break;
1207
1462
  }
1208
1463
  }
1209
1464
  } else {
1210
1465
  const codexHome2 = options.codexHome;
1211
- if (codexHome2 && existsSync2(codexHome2)) {
1466
+ if (codexHome2 && existsSync3(codexHome2)) {
1212
1467
  const auth = readCodexAuth(codexHome2);
1213
1468
  const sessions = join4(codexHome2, "sessions");
1214
1469
  if (auth !== "chatgpt") codexAuth = auth;
1215
- else if (existsSync2(sessions)) {
1216
- const ready = readCodexIncrement(sessions, codexState ?? emptyCodexState(today));
1470
+ else if (existsSync3(sessions)) {
1471
+ const ready = readCodexIncrement(sessions, connection.codex ?? emptyCodexState(today));
1217
1472
  for (const day of ready.dirtyDays) {
1218
1473
  if (await send(day, rowsForCodexDay(ready.state.days, day), " La sesi\xF3n sigue en el port\xE1til.") === "stop") break;
1219
1474
  }
1220
1475
  if (!outcome.error) {
1221
- codexState = ready.state;
1476
+ connection.codex = ready.state;
1222
1477
  if (ready.caughtUp) {
1223
1478
  const before = previousUtcDay(today);
1224
- for (const day of Object.keys(codexState.days)) {
1225
- if (day < before) delete codexState.days[day];
1479
+ for (const day of Object.keys(connection.codex.days)) {
1480
+ if (day < before) delete connection.codex.days[day];
1226
1481
  }
1227
1482
  }
1228
1483
  }
1229
1484
  }
1230
1485
  }
1231
1486
  }
1232
- const dropSpool = source !== "cursor" || outcome.mismatch;
1233
- if (dropSpool) dropCursorHook(options.paths, options.hookCommand);
1234
- for (const file of files) {
1235
- const day = file.event?.at.slice(0, 10);
1236
- if (dropSpool || day && day < today && accepted.has(day)) unlinkSync(file.path);
1237
- }
1238
- const next = { token, device };
1239
- if (lastAccepted) next.lastAcceptedDay = lastAccepted;
1240
- if (claudeState) next.claude = claudeState;
1241
- if (codexState) next.codex = codexState;
1242
- if (!outcome.mismatch) next.source = source;
1243
- writeJson(options.paths.configPath, next);
1244
- if (outcome.error) return { code: EXIT_REPORT, message: outcome.error };
1245
- if (outcome.mismatch) {
1246
- return { code: 0, message: "El uso de esta herramienta no corresponde a esta conexi\xF3n y no se ha subido." };
1247
- }
1487
+ if (lastAccepted) connection.lastAcceptedDay = lastAccepted;
1488
+ if (!outcome.mismatch) connection.source = source;
1489
+ const run2 = (result) => ({
1490
+ connection,
1491
+ source,
1492
+ result,
1493
+ mismatch: outcome.mismatch,
1494
+ accepted,
1495
+ ...statusLine2 ? { statusLine: statusLine2 } : {}
1496
+ });
1497
+ if (outcome.error) return run2({ code: EXIT_REPORT, message: outcome.error });
1498
+ if (outcome.mismatch) return run2({ code: 0, message: "El uso de esta herramienta no corresponde a esta conexi\xF3n y no se ha subido." });
1248
1499
  if (copilotAuth && !outcome.acceptedAny) {
1249
- return { code: EXIT_COPILOT, message: "gh no tiene permiso para leer el uso de Copilot. No se ha escrito una fila vac\xEDa." };
1500
+ return run2({ code: EXIT_COPILOT, message: "gh no tiene permiso para leer el uso de Copilot. No se ha escrito una fila vac\xEDa." });
1250
1501
  }
1251
1502
  if (copilotUnavailable) {
1252
- return { code: EXIT_COPILOT, message: "No se pudo usar gh. Hace falta GitHub CLI con gh auth login en la cuenta de Copilot." };
1503
+ return run2({ code: EXIT_COPILOT, message: "No se pudo usar gh. Hace falta GitHub CLI con gh auth login en la cuenta de Copilot." });
1253
1504
  }
1254
1505
  if (!outcome.acceptedAny) {
1255
- if (codexAuth === "api_key") {
1256
- return { code: 0, message: "Codex usa una API key. Ese gasto no se atribuye a la cuenta individual." };
1506
+ if (codexAuth === "api_key") return run2({ code: 0, message: "Codex usa una API key. Ese gasto no se atribuye a la cuenta individual." });
1507
+ if (codexAuth === "unknown") return run2({ code: 0, message: "No se pudo confirmar que Codex use la suscripci\xF3n de ChatGPT. No se ha subido nada." });
1508
+ return run2({ code: 0, message: "No hay uso nuevo que subir." });
1509
+ }
1510
+ return run2({ code: 0, message: `Uso aceptado hasta ${lastAccepted ?? "hoy"}.` });
1511
+ }
1512
+ async function report(options) {
1513
+ const config = readConfig(options.paths.configPath);
1514
+ if (config.connections.length === 0) return { code: EXIT_USAGE, message: "No hay token. Hay que hacer login en esta conexi\xF3n." };
1515
+ const device = config.device ?? newDevice();
1516
+ const baseUrl = resolveUsageBaseUrl(options.baseUrl);
1517
+ if (!baseUrl) return { code: EXIT_USAGE, message: "La URL del API tiene que ser https." };
1518
+ const today = utcDay(options.now ?? /* @__PURE__ */ new Date());
1519
+ const resolved = [];
1520
+ for (const connection of config.connections) {
1521
+ const probe = await fetchUsageSource({ baseUrl, token: connection.token, fetchImpl: options.fetchImpl });
1522
+ if (probe.status === 401) {
1523
+ resolved.push({
1524
+ connection,
1525
+ ...connection.source ? { source: connection.source } : {},
1526
+ failure: { code: EXIT_REPORT, message: "El servidor no reconoce el token. Hay que repetir el login con un token nuevo." }
1527
+ });
1528
+ continue;
1257
1529
  }
1258
- if (codexAuth === "unknown") {
1259
- return { code: 0, message: "No se pudo confirmar que Codex use la suscripci\xF3n de ChatGPT. No se ha subido nada." };
1530
+ const source = probe.source ?? connection.source;
1531
+ resolved.push(
1532
+ source ? { connection, source } : { connection, failure: { code: EXIT_REPORT, message: `No se pudo saber qu\xE9 herramienta informa esta conexi\xF3n (${probe.status}). Se reintentar\xE1.` } }
1533
+ );
1534
+ }
1535
+ const kept = [];
1536
+ const dropped = [];
1537
+ for (let index = resolved.length - 1; index >= 0; index--) {
1538
+ const entry = resolved[index];
1539
+ if (entry.source && kept.some((other) => !other.failure && other.source === entry.source)) dropped.push(entry);
1540
+ else kept.unshift(entry);
1541
+ }
1542
+ let spool = readSpool(options.paths.spoolPath);
1543
+ for (const entry of dropped) {
1544
+ if (entry.source !== "cursor") continue;
1545
+ const winner = kept.find((other) => other.source === "cursor")?.connection.addedAt;
1546
+ for (const file of spool) {
1547
+ if (!winner || !file.event || file.event.at < winner) unlinkSync(file.path);
1548
+ }
1549
+ spool = readSpool(options.paths.spoolPath);
1550
+ }
1551
+ const runs = [];
1552
+ const failures = [];
1553
+ let statusLine2 = config.claudeStatusLine;
1554
+ for (const entry of kept) {
1555
+ if (entry.failure || !entry.source) {
1556
+ failures.push({ ...entry.source ? { source: entry.source } : {}, result: entry.failure });
1557
+ continue;
1260
1558
  }
1261
- return { code: 0, message: "No hay uso nuevo que subir." };
1559
+ const run2 = await reportConnection(options, entry.connection, entry.source, {
1560
+ baseUrl,
1561
+ device,
1562
+ today,
1563
+ spool,
1564
+ ...statusLine2 ? { statusLine: statusLine2 } : {}
1565
+ });
1566
+ if (entry.source === "claude_code") statusLine2 = run2.statusLine;
1567
+ runs.push(run2);
1568
+ }
1569
+ const pending = kept.some((entry) => !entry.source);
1570
+ const cursorRun = runs.find((run2) => run2.source === "cursor" && !run2.mismatch);
1571
+ const keepsCursor = pending || kept.some((entry) => entry.source === "cursor" && (entry.failure || !runs.find((run2) => run2.connection === entry.connection)?.mismatch));
1572
+ if (!keepsCursor) dropCursorHook(options.paths, options.hookCommand);
1573
+ for (const file of spool) {
1574
+ const day = file.event?.at.slice(0, 10);
1575
+ if (!keepsCursor || cursorRun && day && day < today && cursorRun.accepted.has(day)) unlinkSync(file.path);
1576
+ }
1577
+ const keepsClaude = pending || kept.some((entry) => entry.source === "claude_code" && (entry.failure || !runs.find((run2) => run2.connection === entry.connection)?.mismatch));
1578
+ if (!keepsClaude && statusLine2?.state === "installed") {
1579
+ restoreClaudeStatusLine(options.paths, options.statusLineCommand, statusLine2.previous);
1580
+ statusLine2 = void 0;
1262
1581
  }
1263
- return { code: 0, message: `Uso aceptado hasta ${lastAccepted ?? "hoy"}.` };
1582
+ const next = { device, connections: kept.map((entry) => entry.connection) };
1583
+ if (statusLine2) next.claudeStatusLine = statusLine2;
1584
+ writeConfig(options.paths.configPath, next);
1585
+ const all = [
1586
+ ...dropped.map((entry) => ({ source: entry.source, result: { code: 0, message: "Sustituida por un login m\xE1s reciente de la misma herramienta." } })),
1587
+ ...failures,
1588
+ ...runs.map((run2) => ({ source: run2.source, result: run2.result }))
1589
+ ];
1590
+ if (all.length === 1) return all[0].result;
1591
+ return {
1592
+ code: all.find((entry) => entry.result.code !== 0)?.result.code ?? 0,
1593
+ message: all.map((entry) => `${entry.source ?? "sin comprobar"}: ${entry.result.message}`).join("\n")
1594
+ };
1264
1595
  }
1265
1596
  function previousUtcDay(day) {
1266
1597
  return new Date(Date.parse(`${day}T00:00:00Z`) - 864e5).toISOString().slice(0, 10);
1267
1598
  }
1599
+ var runPreviousWithShell = (command, input) => {
1600
+ const result = spawnSync(command, { shell: process.env["SHELL"] || true, input, encoding: "utf8", timeout: 5e3, windowsHide: true });
1601
+ return result.status === 0 && typeof result.stdout === "string" ? result.stdout : null;
1602
+ };
1603
+ function statusLine(options) {
1604
+ let parsed = null;
1605
+ try {
1606
+ parsed = JSON.parse(options.input);
1607
+ } catch {
1608
+ parsed = null;
1609
+ }
1610
+ let state = { days: {} };
1611
+ if (options.paths.quotaPath) {
1612
+ try {
1613
+ state = readQuotaState(options.paths.quotaPath);
1614
+ if (parsed && recordStatusSample(state, parsed, options.now)) writeQuotaState(options.paths.quotaPath, state);
1615
+ } catch {
1616
+ }
1617
+ }
1618
+ const previous = readConfig(options.paths.configPath).claudeStatusLine?.previous;
1619
+ if (previous) {
1620
+ const output = (options.runPrevious ?? runPreviousWithShell)(previous.command, options.input);
1621
+ if (output !== null) return output;
1622
+ }
1623
+ return defaultStatusText(state);
1624
+ }
1268
1625
  async function status(options) {
1269
- const config = readJson(options.paths.configPath);
1626
+ const config = readConfig(options.paths.configPath);
1270
1627
  const installed = await options.launcher.installed();
1271
- const lines = [
1272
- `agente: ${config.token ? "instalado" : "ausente"}`,
1273
- `arranque: ${installed ? "s\xED" : "no"}`,
1274
- `herramienta: ${config.source ?? "sin comprobar"}`,
1275
- `\xFAltimo d\xEDa aceptado: ${config.lastAcceptedDay ?? "ninguno"}`
1276
- ];
1628
+ const lines = [`agente: ${config.connections.length > 0 ? "instalado" : "ausente"}`, `arranque: ${installed ? "s\xED" : "no"}`];
1629
+ for (const connection of config.connections) {
1630
+ lines.push(`herramienta: ${connection.source ?? "sin comprobar"}`, `\xFAltimo d\xEDa aceptado: ${connection.lastAcceptedDay ?? "ninguno"}`);
1631
+ if (connection.source === "claude_code") {
1632
+ const state = config.claudeStatusLine?.state;
1633
+ lines.push(`l\xEDnea de estado de Claude Code: ${state === "installed" ? "puesta" : state === "declined" ? "retirada por ti, no se vuelve a poner" : "no"}`);
1634
+ }
1635
+ }
1277
1636
  return { code: 0, message: lines.join("\n") };
1278
1637
  }
1279
1638
 
@@ -1289,14 +1648,24 @@ function nodeExec(file, args) {
1289
1648
  })
1290
1649
  );
1291
1650
  }
1651
+ function claudeConfigDir() {
1652
+ return process.env["CLAUDE_CONFIG_DIR"] ?? join5(homedir(), ".claude");
1653
+ }
1292
1654
  function pathsFromEnv() {
1293
1655
  const home = process.env["SENTINEL_USAGE_HOME"] ?? join5(homedir(), ".config", "aaa-sentinel");
1294
1656
  return {
1295
1657
  configPath: join5(home, "usage.json"),
1296
1658
  spoolPath: join5(home, "spool"),
1297
- hooksPath: process.env["SENTINEL_USAGE_CURSOR_HOOKS"] ?? join5(homedir(), ".cursor", "hooks.json")
1659
+ hooksPath: process.env["SENTINEL_USAGE_CURSOR_HOOKS"] ?? join5(homedir(), ".cursor", "hooks.json"),
1660
+ quotaPath: join5(home, "claude-quota.json"),
1661
+ claudeSettingsPath: process.env["SENTINEL_USAGE_CLAUDE_SETTINGS"] ?? join5(claudeConfigDir(), "settings.json")
1298
1662
  };
1299
1663
  }
1664
+ function statusLineArgs() {
1665
+ if (process.env["SENTINEL_USAGE_NO_STATUSLINE"] === "1") return null;
1666
+ const cliJs = fileURLToPath(new URL("./cli.js", import.meta.url));
1667
+ return [process.execPath, cliJs, "statusline"];
1668
+ }
1300
1669
  function codexHome() {
1301
1670
  return process.env["SENTINEL_USAGE_CODEX_HOME"] || process.env["CODEX_HOME"] || join5(homedir(), ".codex");
1302
1671
  }
@@ -1312,7 +1681,7 @@ function ghRun(args) {
1312
1681
  }
1313
1682
  function claudeProjectsDir() {
1314
1683
  if (process.env["SENTINEL_USAGE_CLAUDE_DIR"]) return process.env["SENTINEL_USAGE_CLAUDE_DIR"];
1315
- return join5(process.env["CLAUDE_CONFIG_DIR"] ?? join5(homedir(), ".claude"), "projects");
1684
+ return join5(claudeConfigDir(), "projects");
1316
1685
  }
1317
1686
  function hookArgs(spoolPath) {
1318
1687
  const hookJs = fileURLToPath(new URL("./hook.js", import.meta.url));
@@ -1325,7 +1694,13 @@ function reportArgs() {
1325
1694
  async function main() {
1326
1695
  const [command, tokenArg] = [process.argv[2], process.argv[3]];
1327
1696
  const paths = pathsFromEnv();
1697
+ if (command === "statusline") {
1698
+ process.stdout.write(statusLine({ input: await readStdin(), paths, now: /* @__PURE__ */ new Date() }));
1699
+ return 0;
1700
+ }
1328
1701
  const hookCommand = formatCommand(hookArgs(paths.spoolPath));
1702
+ const statusArgs = statusLineArgs();
1703
+ const statusLineCommand2 = statusArgs ? statusLineCommand(statusArgs, process.platform) : void 0;
1329
1704
  const uid = process.getuid?.();
1330
1705
  const launcher = createLauncher({
1331
1706
  platform: process.platform,
@@ -1333,7 +1708,7 @@ async function main() {
1333
1708
  // El hook solo escribe el spool. Si la tarea lo relanza, el informe no sale del portátil.
1334
1709
  args: reportArgs(),
1335
1710
  homeDir: homedir(),
1336
- scriptDir: dirname2(paths.configPath),
1711
+ scriptDir: dirname3(paths.configPath),
1337
1712
  ...uid !== void 0 ? { uid } : {},
1338
1713
  windowsUser: process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${userInfo().username}` : userInfo().username
1339
1714
  });
@@ -1347,17 +1722,28 @@ async function main() {
1347
1722
  return 2;
1348
1723
  }
1349
1724
  const token = (await readStdin()).trim();
1350
- const result = await login({ token, paths, launcher, hookCommand });
1725
+ const probe = sourceProbe();
1726
+ const result = await login({ token, paths, launcher, hookCommand, ...probe ? { probe } : {} });
1351
1727
  console.log(result.message);
1352
1728
  return result.code;
1353
1729
  }
1354
1730
  if (command === "logout") {
1355
- const result = await logout({ paths, launcher, hookCommand });
1731
+ if (tokenArg && !isUsageSource(tokenArg)) {
1732
+ console.log("Herramienta desconocida. Usa cursor, claude_code, github_copilot o codex, o nada para retirarlas todas.");
1733
+ return 2;
1734
+ }
1735
+ const result = await logout({
1736
+ paths,
1737
+ launcher,
1738
+ hookCommand,
1739
+ ...statusLineCommand2 ? { statusLineCommand: statusLineCommand2 } : {},
1740
+ ...tokenArg && isUsageSource(tokenArg) ? { source: tokenArg } : {}
1741
+ });
1356
1742
  console.log(result.message);
1357
1743
  return result.code;
1358
1744
  }
1359
1745
  if (command === "report") {
1360
- const baseUrl = process.env["SENTINEL_USAGE_BASE_URL"] ?? DEFAULT_BASE_URL;
1746
+ const baseUrl = usageBaseUrl();
1361
1747
  const result = await report({
1362
1748
  paths,
1363
1749
  baseUrl,
@@ -1365,7 +1751,8 @@ async function main() {
1365
1751
  claudeProjectsDir: claudeProjectsDir(),
1366
1752
  codexHome: codexHome(),
1367
1753
  gh: ghRun,
1368
- hookCommand
1754
+ hookCommand,
1755
+ ...statusLineCommand2 ? { statusLineCommand: statusLineCommand2 } : {}
1369
1756
  });
1370
1757
  console.log(result.message);
1371
1758
  return result.code;
@@ -1375,9 +1762,17 @@ async function main() {
1375
1762
  console.log(result.message);
1376
1763
  return result.code;
1377
1764
  }
1378
- console.log("Uso: sentinel-usage login|logout|report|status");
1765
+ console.log("Uso: sentinel-usage login|logout [herramienta]|report|status");
1379
1766
  return 2;
1380
1767
  }
1768
+ function usageBaseUrl() {
1769
+ return process.env["SENTINEL_USAGE_BASE_URL"] ?? DEFAULT_BASE_URL;
1770
+ }
1771
+ function sourceProbe() {
1772
+ const baseUrl = resolveUsageBaseUrl(usageBaseUrl());
1773
+ if (!baseUrl) return void 0;
1774
+ return async (token) => (await fetchUsageSource({ baseUrl, token, fetchImpl: fetch })).source;
1775
+ }
1381
1776
  function readStdin() {
1382
1777
  return new Promise((resolve, reject) => {
1383
1778
  const chunks = [];