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.
- package/owlmem.mjs +38 -5
- 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.
|
|
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
|
-
|
|
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.
|
|
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" },
|