owlmem 0.9.0 → 0.9.1

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 (2) hide show
  1. package/owlmem.mjs +38 -5
  2. package/package.json +1 -1
package/owlmem.mjs CHANGED
@@ -41,7 +41,7 @@ import { join, dirname, basename, relative, isAbsolute } from "node:path";
41
41
  import { fileURLToPath } from "node:url";
42
42
  import { spawn, spawnSync } from "node:child_process";
43
43
 
44
- export const VERSION = "0.9.0";
44
+ export const VERSION = "0.9.1";
45
45
  const BASE = (process.env.OWLMEM_URL || "https://owlmem.com").replace(/\/$/, "");
46
46
  const DIR = process.env.OWLMEM_HOME || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "owlmem");
47
47
  const enc = new TextEncoder(), dec = new TextDecoder();
@@ -1392,18 +1392,37 @@ const usageAdd = (days, d, u) => { const x = days[d] || (days[d] = { input: 0, o
1392
1392
  // One line of a transcript → [day, usage] or null. f keeps what a file needs between lines (Claude: message ids seen; Codex: last total).
1393
1393
  export function usageLine(client, line, f) {
1394
1394
  if (client === "claude-code") {
1395
- if (!line.includes('"usage"')) return null;
1395
+ const err = line.includes('"isApiErrorMessage":true');
1396
+ if (!err && !line.includes('"usage"')) return null;
1396
1397
  let j; try { j = JSON.parse(line); } catch { return null; }
1397
- const m = j?.message, u = m?.usage, d = usageDay(j?.timestamp);
1398
+ const m = j?.message, u = m?.usage, d = usageDay(j?.timestamp), at = Math.floor(Date.parse(j?.timestamp) / 1000) || 0;
1399
+ if (err) { // the app's own error message in the transcript: out of quota, or signed out
1400
+ const text = (Array.isArray(m?.content) ? m.content : []).map((b) => b?.text || "").join(" ");
1401
+ const kind = j.error === "rate_limit" || /usage limit|hit your limit|limit reached|rate.?limit/i.test(text) ? "limit" : j.error === "authentication_failed" || /authenticat|log ?in|oauth/i.test(text) ? "auth" : null;
1402
+ if (kind && at >= (f.st?.block_at || 0)) { const r = /\|(\d{10})\b/.exec(text); f.st = { ...f.st, block: kind, block_at: at, resets_at: r ? Number(r[1]) : undefined }; }
1403
+ return null;
1404
+ }
1398
1405
  if (j?.type !== "assistant" || !u || !d) return null;
1406
+ if (at > (f.st?.ok_at || 0)) f.st = { ...f.st, ok_at: at };
1399
1407
  const id = (m.id || "") + ":" + (j.requestId || ""); // Claude Code writes one line per content block, each with the message's usage
1400
1408
  f.ids = f.ids || [];
1401
1409
  if (m.id && f.ids.includes(id)) return null;
1402
1410
  if (m.id) { f.ids.push(id); if (f.ids.length > 200) f.ids.shift(); }
1403
1411
  return [d, { input: u.input_tokens, output: u.output_tokens, cache_read: u.cache_read_input_tokens, cache_write: u.cache_creation_input_tokens }];
1404
1412
  }
1405
- if (!line.includes('"token_count"')) return null;
1413
+ if (!line.includes('"token_count"') && !line.includes('"error"')) return null;
1406
1414
  let j; try { j = JSON.parse(line); } catch { return null; }
1415
+ const at = Math.floor(Date.parse(j?.timestamp) / 1000) || 0;
1416
+ if (j?.payload?.type === "error") {
1417
+ if (/usage limit|rate.?limit|quota/i.test(String(j.payload.message || "")) && at >= (f.st?.block_at || 0)) f.st = { ...f.st, block: "limit", block_at: at };
1418
+ return null;
1419
+ }
1420
+ const rl = j?.payload?.type === "token_count" ? j.payload.rate_limits : null;
1421
+ if (rl && at >= (f.st?.limit_at || 0)) { // Codex says how much of its plan's window is used and when it resets
1422
+ const w = [rl.primary, rl.secondary].filter((x) => x && typeof x.used_percent === "number").sort((a, b) => b.used_percent - a.used_percent)[0];
1423
+ f.st = { ...f.st, limit_at: at, ok_at: at, used_percent: w?.used_percent, resets_at: w?.resets_at, window_minutes: w?.window_minutes,
1424
+ ...(rl.rate_limit_reached_type ? { block: "limit", block_at: at } : {}) };
1425
+ }
1407
1426
  const info = j?.payload?.type === "token_count" ? j.payload.info : null, last = info?.last_token_usage, d = usageDay(j?.timestamp);
1408
1427
  if (!last || !d) return null;
1409
1428
  const total = info.total_token_usage?.total_tokens;
@@ -1451,14 +1470,28 @@ export function usageReport(state, clients, now = Date.now()) {
1451
1470
  }
1452
1471
  return out;
1453
1472
  }
1473
+ // The newest quota state per app: what its latest transcript says (Claude Code: an out-of-quota or signed-out error after
1474
+ // the last answer; Codex: percent of its plan's window used, when it resets).
1475
+ export function usageStatus(state, clients) {
1476
+ const out = {};
1477
+ for (const f of Object.values(state.files || {})) {
1478
+ if (!clients.includes(f.client) || !f.st) continue;
1479
+ const o = out[f.client] || (out[f.client] = {});
1480
+ if ((f.st.ok_at || 0) > (o.ok_at || 0)) o.ok_at = f.st.ok_at;
1481
+ if ((f.st.block_at || 0) > (o.block_at || 0)) Object.assign(o, { block: f.st.block, block_at: f.st.block_at, resets_at: f.st.resets_at ?? o.resets_at });
1482
+ if ((f.st.limit_at || 0) > (o.limit_at || 0)) Object.assign(o, { limit_at: f.st.limit_at, used_percent: f.st.used_percent, window_minutes: f.st.window_minutes, resets_at: f.st.resets_at });
1483
+ }
1484
+ return out;
1485
+ }
1454
1486
  async function usageTick(cfg, api, log, dir = DIR) {
1455
1487
  const file = join(wakeDir(dir), "usage.json");
1456
1488
  let state = {}; try { state = JSON.parse(readFileSync(file, "utf8")); } catch {}
1489
+ if (state.v !== 2) state = { v: 2 }; // 0.9.1 also reads quota state: read every file once more
1457
1490
  const clients = Object.keys(cfg.clients || {}).filter((c) => c in usageRoots());
1458
1491
  if (!clients.length) return;
1459
1492
  scanUsage(state, Object.fromEntries(Object.entries(usageRoots()).filter(([c]) => clients.includes(c))));
1460
1493
  try { writeFileSync(file + ".tmp", JSON.stringify(state)); renameSync(file + ".tmp", file); } catch (e) { log("usage: " + e.message); }
1461
- const r = await api("usage", { days: usageReport(state, clients) });
1494
+ const r = await api("usage", { days: usageReport(state, clients), status: usageStatus(state, clients) });
1462
1495
  if (r.status !== 200 && r.status !== 404) log("usage: OwlMem answered " + (r.data?.detail || "HTTP " + r.status));
1463
1496
  }
1464
1497
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "owlmem",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "OwlMem command line: connect your AI apps to OwlMem in one command (npx -y owlmem install), an end-to-end encrypted vault for your AI agents, and a local MCP server.",
5
5
  "type": "module",
6
6
  "bin": { "owlmem": "owlmem.mjs" },