owlmem 0.9.1 → 0.9.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/owlmem.mjs +41 -4
- 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.2";
|
|
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();
|
|
@@ -1237,6 +1237,7 @@ export async function wakeLoop(o) {
|
|
|
1237
1237
|
await sleep(backoff); continue;
|
|
1238
1238
|
}
|
|
1239
1239
|
backoff = 0;
|
|
1240
|
+
if (Array.isArray(r.data.agents)) cfg.live = r.data.agents; // which AIs the owner lets us read Claude quota for (read_quota)
|
|
1240
1241
|
for (const t of r.data.tasks || []) {
|
|
1241
1242
|
const plan = isReply(t) ? { kind: "scratch" } : o.plan ? o.plan(t) : planWorkdir(t); // a reply is read in an empty folder
|
|
1242
1243
|
log((isReply(t) ? "Reply #" + t.id + " (task #" + t.task_id + ")" : jobLabel(t)) + " for " + t.agent + ": starting " + t.client + (plan.dir ? " in " + plan.dir : "") + ".");
|
|
@@ -1420,7 +1421,9 @@ export function usageLine(client, line, f) {
|
|
|
1420
1421
|
const rl = j?.payload?.type === "token_count" ? j.payload.rate_limits : null;
|
|
1421
1422
|
if (rl && at >= (f.st?.limit_at || 0)) { // Codex says how much of its plan's window is used and when it resets
|
|
1422
1423
|
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
|
-
|
|
1424
|
+
const windows = [rl.primary, rl.secondary].filter((x) => x && typeof x.used_percent === "number")
|
|
1425
|
+
.map((x) => ({ minutes: x.window_minutes, used_percent: x.used_percent, resets_at: x.resets_at }));
|
|
1426
|
+
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, windows,
|
|
1424
1427
|
...(rl.rate_limit_reached_type ? { block: "limit", block_at: at } : {}) };
|
|
1425
1428
|
}
|
|
1426
1429
|
const info = j?.payload?.type === "token_count" ? j.payload.info : null, last = info?.last_token_usage, d = usageDay(j?.timestamp);
|
|
@@ -1479,10 +1482,39 @@ export function usageStatus(state, clients) {
|
|
|
1479
1482
|
const o = out[f.client] || (out[f.client] = {});
|
|
1480
1483
|
if ((f.st.ok_at || 0) > (o.ok_at || 0)) o.ok_at = f.st.ok_at;
|
|
1481
1484
|
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 });
|
|
1485
|
+
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, windows: f.st.windows });
|
|
1483
1486
|
}
|
|
1484
1487
|
return out;
|
|
1485
1488
|
}
|
|
1489
|
+
// Claude Code's own quota (0.9.2, only when the AI's owner turned "Read Claude quota" on in the console): ask Anthropic's
|
|
1490
|
+
// usage endpoint with the sign-in Claude Code keeps on this computer, the same one its /usage screen uses. The sign-in is
|
|
1491
|
+
// only sent to api.anthropic.com and never leaves this computer otherwise; OwlMem gets the percentages and reset times.
|
|
1492
|
+
// An expired sign-in isn't refreshed here (Claude Code does that itself): no quota this round.
|
|
1493
|
+
export function claudeToken(h = home(), env = process.env, plat = process.platform, now = Date.now()) {
|
|
1494
|
+
let raw = null;
|
|
1495
|
+
try { raw = readFileSync(join(env.CLAUDE_CONFIG_DIR || join(h, ".claude"), ".credentials.json"), "utf8"); } catch {}
|
|
1496
|
+
if (!raw && plat === "darwin") {
|
|
1497
|
+
const r = spawnSync("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { encoding: "utf8", timeout: 10000 });
|
|
1498
|
+
if (r.status === 0) raw = r.stdout;
|
|
1499
|
+
}
|
|
1500
|
+
let o; try { o = JSON.parse(raw || "null")?.claudeAiOauth; } catch { return null; }
|
|
1501
|
+
if (!o?.accessToken || (o.expiresAt && o.expiresAt < now + 60000)) return null;
|
|
1502
|
+
return o.accessToken;
|
|
1503
|
+
}
|
|
1504
|
+
export function claudeWindows(j) {
|
|
1505
|
+
const sec = (v) => { const t = Date.parse(v); return isNaN(t) ? undefined : Math.floor(t / 1000); };
|
|
1506
|
+
return [["five_hour", 300], ["seven_day", 10080]].filter(([k]) => typeof j?.[k]?.utilization === "number")
|
|
1507
|
+
.map(([k, minutes]) => ({ minutes, used_percent: j[k].utilization, resets_at: sec(j[k].resets_at) }));
|
|
1508
|
+
}
|
|
1509
|
+
async function claudeQuota(fetchImpl = fetch) {
|
|
1510
|
+
const tok = claudeToken();
|
|
1511
|
+
if (!tok) return null;
|
|
1512
|
+
const r = await fetchImpl("https://api.anthropic.com/api/oauth/usage", { headers: { Authorization: "Bearer " + tok, "anthropic-beta": "oauth-2025-04-20", "User-Agent": "owlmem-cli/" + VERSION },
|
|
1513
|
+
signal: AbortSignal.timeout(15000) });
|
|
1514
|
+
if (!r.ok) return null;
|
|
1515
|
+
const w = claudeWindows(await r.json());
|
|
1516
|
+
return w.length ? w : null;
|
|
1517
|
+
}
|
|
1486
1518
|
async function usageTick(cfg, api, log, dir = DIR) {
|
|
1487
1519
|
const file = join(wakeDir(dir), "usage.json");
|
|
1488
1520
|
let state = {}; try { state = JSON.parse(readFileSync(file, "utf8")); } catch {}
|
|
@@ -1491,7 +1523,12 @@ async function usageTick(cfg, api, log, dir = DIR) {
|
|
|
1491
1523
|
if (!clients.length) return;
|
|
1492
1524
|
scanUsage(state, Object.fromEntries(Object.entries(usageRoots()).filter(([c]) => clients.includes(c))));
|
|
1493
1525
|
try { writeFileSync(file + ".tmp", JSON.stringify(state)); renameSync(file + ".tmp", file); } catch (e) { log("usage: " + e.message); }
|
|
1494
|
-
const
|
|
1526
|
+
const status = usageStatus(state, clients);
|
|
1527
|
+
if (clients.includes("claude-code") && (cfg.live || []).some((a) => a.client === "claude-code" && a.read_quota)) {
|
|
1528
|
+
const w = await claudeQuota().catch(() => null);
|
|
1529
|
+
if (w) status["claude-code"] = { ...status["claude-code"], windows: w, limit_at: Math.floor(Date.now() / 1000) };
|
|
1530
|
+
}
|
|
1531
|
+
const r = await api("usage", { days: usageReport(state, clients), status });
|
|
1495
1532
|
if (r.status !== 200 && r.status !== 404) log("usage: OwlMem answered " + (r.data?.detail || "HTTP " + r.status));
|
|
1496
1533
|
}
|
|
1497
1534
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "owlmem",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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" },
|