owlmem 0.7.9 → 0.9.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.
- package/owlmem.mjs +155 -13
- package/package.json +1 -1
package/owlmem.mjs
CHANGED
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
// device key there, so standing grants don't apply. Viewing, writing, deleting and exporting always need approval.
|
|
35
35
|
|
|
36
36
|
import { webcrypto as crypto, createHash } from "node:crypto";
|
|
37
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync, rmSync, statSync, realpathSync, openSync, closeSync, appendFileSync, renameSync } from "node:fs";
|
|
37
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync, rmSync, statSync, realpathSync, openSync, closeSync, appendFileSync, renameSync, readdirSync, readSync } from "node:fs";
|
|
38
38
|
import { gunzipSync } from "node:zlib";
|
|
39
39
|
import { homedir, hostname, userInfo } from "node:os";
|
|
40
40
|
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.
|
|
44
|
+
export const VERSION = "0.9.0";
|
|
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();
|
|
@@ -1074,10 +1074,13 @@ export function prepareWorkdir(task, plan, dir = DIR, git = gitRun) {
|
|
|
1074
1074
|
return { cwd: rel ? join(wt, rel) : wt, worktree: wt, branch, repo: plan.top };
|
|
1075
1075
|
}
|
|
1076
1076
|
|
|
1077
|
-
// A job from OwlMem is a task given to this AI (type "task"),
|
|
1078
|
-
// the automatic note sent when that task was done or declined)
|
|
1077
|
+
// A job from OwlMem is a task given to this AI (type "task"), the result of a task this AI gave someone else (type "reply":
|
|
1078
|
+
// the automatic note sent when that task was done or declined), or a note a person sent this AI (type "note", 0.8.0).
|
|
1079
|
+
// Older OwlMem servers send no type: a task.
|
|
1079
1080
|
const isReply = (t) => t?.type === "reply";
|
|
1080
|
-
const
|
|
1081
|
+
const isNote = (t) => t?.type === "note";
|
|
1082
|
+
const jobLabel = (t) => (isReply(t) ? "Reply #" : isNote(t) ? "Note #" : "Task #") + t.id;
|
|
1083
|
+
const jobName = (t) => (isReply(t) ? "reply-" : isNote(t) ? "note-" : "task-") + t.id;
|
|
1081
1084
|
const shortName = (who) => { who = String(who || ""); return who.startsWith("user:") ? who.slice(5) : who.includes("/") ? who.split("/").slice(1).join("/") : who; };
|
|
1082
1085
|
|
|
1083
1086
|
// Woken for a reply: read it (kb_inbox marks it read; that's how OwlMem knows it was handled), act on it only if needed.
|
|
@@ -1099,8 +1102,44 @@ Never put passwords, tokens, keys or other secrets in any message. You have ${mi
|
|
|
1099
1102
|
`;
|
|
1100
1103
|
}
|
|
1101
1104
|
|
|
1105
|
+
// Woken for a note a person sent this AI: a person writing to an AI usually wants something done, whichever kind they picked.
|
|
1106
|
+
// Read it (kb_inbox marks it read; that's how OwlMem knows it was handled), do it if it asks for something, and tell them the result.
|
|
1107
|
+
export function notePrompt(n, place, o = {}) {
|
|
1108
|
+
const who = shortName(n.from), mins = o.timeout_min || WAKE_DEFAULTS.timeout_min;
|
|
1109
|
+
const where = place.worktree
|
|
1110
|
+
? (n.zh ? `在 ${place.cwd} 里做(${place.repo} 的独立 git 工作树,分支 ${place.branch};只在这个分支提交,不合并、不推送别的分支,结果里写上分支名)。`
|
|
1111
|
+
: `Work in ${place.cwd}. It is a separate git worktree of ${place.repo} on the branch ${place.branch}. Commit on this branch only, don't merge or push other branches, and put the branch name in your result.`)
|
|
1112
|
+
: place.scratch ? (n.zh ? `留言没指定目录:你在一个空文件夹(${place.cwd})里。` : `The note names no project directory: you are in an empty folder (${place.cwd}).`)
|
|
1113
|
+
: (n.zh ? `在 ${place.cwd} 里做。` : `Work in ${place.cwd}.`);
|
|
1114
|
+
if (n.zh) return `奥麦自动唤醒在 ${o.machine || "这台电脑"} 上启动了你(${n.agent}),因为 ${who}(人)给你发了一条留言。没有人在看这个会话,也没人能回答问题。
|
|
1115
|
+
|
|
1116
|
+
留言 #${n.id}:${n.title}
|
|
1117
|
+
|
|
1118
|
+
${n.body || "(没有正文)"}
|
|
1119
|
+
|
|
1120
|
+
先用 kb_inbox 看这条留言(会标成已读)。${where}
|
|
1121
|
+
- 它要你做事:不需要人参与的就直接做完,然后用 kb_send 发给 ${n.from}(kind=note)报结果,标题一句话说结果。
|
|
1122
|
+
- 做不了(要人输验证码、被安全检查拦下、要动有风险的线上服务):不要硬做,用 kb_send 告诉 ${n.from} 卡在哪、需要他做什么。
|
|
1123
|
+
- 只是告诉你一件事、不用做什么:看完就结束,不要发消息。
|
|
1124
|
+
不要把密码、令牌、密钥写进任何消息。你有 ${mins} 分钟。
|
|
1125
|
+
`;
|
|
1126
|
+
return `OwlMem auto-wake started you (${n.agent}) on ${o.machine || "this computer"} because ${who}, a person, sent you a note. Nobody is watching this session and nobody can answer questions.
|
|
1127
|
+
|
|
1128
|
+
Note #${n.id}: ${n.title}
|
|
1129
|
+
|
|
1130
|
+
${n.body || "(no details)"}
|
|
1131
|
+
|
|
1132
|
+
First read it with kb_inbox (that marks it read). ${where}
|
|
1133
|
+
- If it asks you to do something you can do without a person, do it, then tell ${n.from} the result with kb_send (kind=note), with the result in one line as the title.
|
|
1134
|
+
- If you can't (it needs a code typed in, a safety check stops you, or it means a risky change to a live service), don't force it: tell ${n.from} with kb_send what is blocking and what they need to do.
|
|
1135
|
+
- If it only tells you something, end here and don't send any message.
|
|
1136
|
+
Never put passwords, tokens, keys or other secrets in any message. You have ${mins} minutes.
|
|
1137
|
+
`;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1102
1140
|
export function wakePrompt(task, place, o = {}) {
|
|
1103
1141
|
if (isReply(task)) return replyPrompt(task, o);
|
|
1142
|
+
if (isNote(task)) return notePrompt(task, place, o);
|
|
1104
1143
|
const id = task.id, mins = o.timeout_min || WAKE_DEFAULTS.timeout_min;
|
|
1105
1144
|
const where = place.worktree
|
|
1106
1145
|
? `Work in ${place.cwd}. It is a separate git worktree of ${place.repo} on the branch ${place.branch}, made for this task so it doesn't collide with other work. Commit your changes on this branch only. Don't merge, rebase or push other branches, and don't touch ${place.repo} itself. Put the branch name in your result so a person can review and merge it.`
|
|
@@ -1142,7 +1181,7 @@ export async function runTask(task, cfg, o = {}) {
|
|
|
1142
1181
|
let ch, timedOut = false, killer = null;
|
|
1143
1182
|
try {
|
|
1144
1183
|
ch = spawn(argv[0], argv.slice(1), { cwd: place.cwd, stdio: ["pipe", fd, fd], detached: !win, shell: win, windowsHide: true,
|
|
1145
|
-
env: { ...process.env, OWLMEM_NO_BROWSER: "1", [isReply(task) ? "OWLMEM_WAKE_REPLY" : "OWLMEM_WAKE_TASK"]: String(task.id) } });
|
|
1184
|
+
env: { ...process.env, OWLMEM_NO_BROWSER: "1", [isReply(task) ? "OWLMEM_WAKE_REPLY" : isNote(task) ? "OWLMEM_WAKE_NOTE" : "OWLMEM_WAKE_TASK"]: String(task.id) } });
|
|
1146
1185
|
} catch (e) { res({ outcome: "error", detail: "couldn't start " + argv[0] + ": " + e.message }); return; }
|
|
1147
1186
|
const kill = (sig) => { try { if (win) spawnSync("taskkill", ["/T", "/F", "/PID", String(ch.pid)]); else process.kill(-ch.pid, sig); } catch { try { ch.kill(sig); } catch {} } };
|
|
1148
1187
|
const timer = setTimeout(() => { timedOut = true; kill("SIGTERM"); killer = setTimeout(() => kill("SIGKILL"), 10000); }, ms);
|
|
@@ -1200,7 +1239,7 @@ export async function wakeLoop(o) {
|
|
|
1200
1239
|
backoff = 0;
|
|
1201
1240
|
for (const t of r.data.tasks || []) {
|
|
1202
1241
|
const plan = isReply(t) ? { kind: "scratch" } : o.plan ? o.plan(t) : planWorkdir(t); // a reply is read in an empty folder
|
|
1203
|
-
log((isReply(t) ? "Reply #" + t.id + " (task #" + t.task_id + ")" :
|
|
1242
|
+
log((isReply(t) ? "Reply #" + t.id + " (task #" + t.task_id + ")" : jobLabel(t)) + " for " + t.agent + ": starting " + t.client + (plan.dir ? " in " + plan.dir : "") + ".");
|
|
1204
1243
|
sched.add({ id: t.id, agent: t.agent, lock: plan.lock || null, task: t, plan });
|
|
1205
1244
|
}
|
|
1206
1245
|
// OwlMem says a newer version is out: update only while nothing runs or waits (no task is cut off), then restart
|
|
@@ -1216,12 +1255,13 @@ export async function wakeLoop(o) {
|
|
|
1216
1255
|
// The program runs its own copy (wake/owlmem.mjs). When OwlMem's poll answer names a newer version (latest), and nothing is
|
|
1217
1256
|
// running: download that version's package from the npm registry, check it against the registry's sha512, take owlmem.mjs
|
|
1218
1257
|
// out of it, check that it runs and says it is that version, then swap it in (rename in the same folder, so it's all or
|
|
1219
|
-
// nothing) and restart. If anything fails the old copy keeps running, and that version isn't tried again for 24 hours
|
|
1258
|
+
// nothing) and restart. If anything fails the old copy keeps running, and that version isn't tried again for 24 hours;
|
|
1259
|
+
// except a 404 for the version: npm takes a few minutes to serve a new one everywhere, so that's tried again in 10 minutes.
|
|
1220
1260
|
// Restart: on Linux and macOS the program exits with WAKE_RESTART_CODE; the systemd unit (Restart=on-failure) and the
|
|
1221
1261
|
// LaunchAgent (KeepAlive unless the exit was successful) start it again 30 s later. On Windows nothing watches it, so it
|
|
1222
1262
|
// starts the new copy itself (detached, like the logon launcher) and then exits.
|
|
1223
1263
|
export const WAKE_RESTART_CODE = 75;
|
|
1224
|
-
const UPDATE_RETRY = 24 * 3600 * 1000;
|
|
1264
|
+
const UPDATE_RETRY = 24 * 3600 * 1000, UPDATE_NOT_YET = 10 * 60 * 1000;
|
|
1225
1265
|
const REGISTRY = () => String(process.env.npm_config_registry || "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
1226
1266
|
export function newer(a, b) {
|
|
1227
1267
|
const p = (v) => (/^\d+\.\d+\.\d+$/.test(String(v || "")) ? String(v).split(".").map(Number) : null);
|
|
@@ -1243,16 +1283,22 @@ export function tarFile(tgz, want = "package/owlmem.mjs") {
|
|
|
1243
1283
|
}
|
|
1244
1284
|
return null;
|
|
1245
1285
|
}
|
|
1246
|
-
// → "restart" (new copy in place) | "skip" (tried that version less than 24 h ago) | "failed" (old copy stays)
|
|
1286
|
+
// → "restart" (new copy in place) | "skip" (tried that version less than 24 h ago, or 10 min for a 404) | "failed" (old copy stays)
|
|
1247
1287
|
export async function wakeUpdate(latest, o = {}) {
|
|
1248
1288
|
const dir = o.dir || DIR, log = o.log || (() => {}), now = o.now || Date.now, get = o.fetch || fetch;
|
|
1249
1289
|
const wd = wakeDir(dir), state = join(wd, "update.json"), target = join(wd, "owlmem.mjs"), tmpFile = join(wd, "owlmem.update.mjs");
|
|
1250
1290
|
let st = {}; try { st = JSON.parse(readFileSync(state, "utf8")); } catch {}
|
|
1251
|
-
if (st.version === latest && now() - (st.at || 0) < UPDATE_RETRY) return "skip";
|
|
1252
|
-
try { mkdirSync(wd, { recursive: true }); writeFileSync(state, JSON.stringify({ version: latest, at: now() }) + "\n"); } catch {}
|
|
1291
|
+
if (st.version === latest && now() - (st.at || 0) < (st.wait || UPDATE_RETRY)) return "skip";
|
|
1292
|
+
const mark = (wait) => { try { mkdirSync(wd, { recursive: true }); writeFileSync(state, JSON.stringify({ version: latest, at: now(), ...(wait ? { wait } : {}) }) + "\n"); } catch {} };
|
|
1293
|
+
mark();
|
|
1253
1294
|
log("Updating auto-wake from " + VERSION + " to " + latest + ".");
|
|
1254
1295
|
try {
|
|
1255
1296
|
const meta = await get(REGISTRY() + "/owlmem/" + encodeURIComponent(latest), { signal: AbortSignal.timeout(60000) });
|
|
1297
|
+
if (meta.status === 404) {
|
|
1298
|
+
mark(UPDATE_NOT_YET);
|
|
1299
|
+
log(latest + " isn't on npm yet; trying again in 10 minutes.");
|
|
1300
|
+
return "failed";
|
|
1301
|
+
}
|
|
1256
1302
|
if (!meta.ok) throw new Error("registry answered HTTP " + meta.status);
|
|
1257
1303
|
const dist = (await meta.json())?.dist || {};
|
|
1258
1304
|
const m = /^sha512-(.+)$/.exec(String(dist.integrity || ""));
|
|
@@ -1322,13 +1368,107 @@ export function wakeTools(cfg, home_ = home(), env = process.env) {
|
|
|
1322
1368
|
return changed;
|
|
1323
1369
|
}
|
|
1324
1370
|
|
|
1371
|
+
// ── token usage (0.9.0) ──
|
|
1372
|
+
// An AI can't see how many tokens it used, but its app records them on this computer. Auto-wake adds them up per app and
|
|
1373
|
+
// UTC day and tells OwlMem every USAGE_EVERY, so every agent in the space can see who is busy (kb_team). Read: Claude Code's
|
|
1374
|
+
// transcripts (~/.claude/projects/**/*.jsonl, message.usage, each message once) and Codex's sessions (~/.codex/sessions/**/
|
|
1375
|
+
// *.jsonl, token_count events). Only the numbers leave the computer — no text, no paths. Files are read from where the last
|
|
1376
|
+
// scan stopped (usage.json in the wake folder), so a scan only reads what's new.
|
|
1377
|
+
export const USAGE_EVERY = 10 * 60 * 1000, USAGE_DAYS = 8, USAGE_KEEP = 35;
|
|
1378
|
+
const usageDay = (ts) => { const d = new Date(ts); return isNaN(d) ? null : d.toISOString().slice(0, 10); };
|
|
1379
|
+
export function usageRoots(h = home(), env = process.env) {
|
|
1380
|
+
return { "claude-code": join(env.CLAUDE_CONFIG_DIR || join(h, ".claude"), "projects"), codex: join(env.CODEX_HOME || join(h, ".codex"), "sessions") };
|
|
1381
|
+
}
|
|
1382
|
+
function jsonlFiles(dir, since, out = []) {
|
|
1383
|
+
let ents = []; try { ents = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
|
|
1384
|
+
for (const e of ents) {
|
|
1385
|
+
const p = join(dir, e.name);
|
|
1386
|
+
if (e.isDirectory()) jsonlFiles(p, since, out);
|
|
1387
|
+
else if (e.name.endsWith(".jsonl")) { try { const st = statSync(p); if (st.mtimeMs >= since) out.push([p, st.size]); } catch {} }
|
|
1388
|
+
}
|
|
1389
|
+
return out;
|
|
1390
|
+
}
|
|
1391
|
+
const usageAdd = (days, d, u) => { const x = days[d] || (days[d] = { input: 0, output: 0, cache_read: 0, cache_write: 0 }); for (const k in x) x[k] += Math.max(0, Number(u[k]) || 0); };
|
|
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
|
+
export function usageLine(client, line, f) {
|
|
1394
|
+
if (client === "claude-code") {
|
|
1395
|
+
if (!line.includes('"usage"')) return null;
|
|
1396
|
+
let j; try { j = JSON.parse(line); } catch { return null; }
|
|
1397
|
+
const m = j?.message, u = m?.usage, d = usageDay(j?.timestamp);
|
|
1398
|
+
if (j?.type !== "assistant" || !u || !d) return null;
|
|
1399
|
+
const id = (m.id || "") + ":" + (j.requestId || ""); // Claude Code writes one line per content block, each with the message's usage
|
|
1400
|
+
f.ids = f.ids || [];
|
|
1401
|
+
if (m.id && f.ids.includes(id)) return null;
|
|
1402
|
+
if (m.id) { f.ids.push(id); if (f.ids.length > 200) f.ids.shift(); }
|
|
1403
|
+
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
|
+
}
|
|
1405
|
+
if (!line.includes('"token_count"')) return null;
|
|
1406
|
+
let j; try { j = JSON.parse(line); } catch { return null; }
|
|
1407
|
+
const info = j?.payload?.type === "token_count" ? j.payload.info : null, last = info?.last_token_usage, d = usageDay(j?.timestamp);
|
|
1408
|
+
if (!last || !d) return null;
|
|
1409
|
+
const total = info.total_token_usage?.total_tokens;
|
|
1410
|
+
if (total != null && total === f.total) return null; // Codex repeats the event when nothing new was used
|
|
1411
|
+
f.total = total;
|
|
1412
|
+
const cached = Number(last.cached_input_tokens) || 0;
|
|
1413
|
+
return [d, { input: (Number(last.input_tokens) || 0) - cached, output: last.output_tokens, cache_read: cached, cache_write: last.cache_write_input_tokens }];
|
|
1414
|
+
}
|
|
1415
|
+
export function scanUsage(state, roots = usageRoots(), now = Date.now()) {
|
|
1416
|
+
state.files = state.files || {};
|
|
1417
|
+
const since = now - USAGE_KEEP * 86400000;
|
|
1418
|
+
for (const [client, dir] of Object.entries(roots)) {
|
|
1419
|
+
for (const [p, size] of jsonlFiles(dir, since)) {
|
|
1420
|
+
let f = state.files[p];
|
|
1421
|
+
if (!f || f.client !== client || size < f.off) f = state.files[p] = { client, off: 0, days: {} };
|
|
1422
|
+
if (size === f.off) continue;
|
|
1423
|
+
let fd; try { fd = openSync(p, "r"); } catch { continue; }
|
|
1424
|
+
try {
|
|
1425
|
+
const buf = Buffer.alloc(1 << 20); let rest = "", pos = f.off;
|
|
1426
|
+
for (;;) {
|
|
1427
|
+
const n = readSync(fd, buf, 0, buf.length, pos); if (n <= 0) break;
|
|
1428
|
+
pos += n;
|
|
1429
|
+
const text = rest + buf.toString("utf8", 0, n), cut = text.lastIndexOf("\n");
|
|
1430
|
+
if (cut < 0) { rest = text; continue; }
|
|
1431
|
+
for (const l of text.slice(0, cut).split("\n")) { const r = usageLine(client, l, f); if (r) usageAdd(f.days, r[0], r[1]); }
|
|
1432
|
+
rest = text.slice(cut + 1);
|
|
1433
|
+
}
|
|
1434
|
+
f.off = pos - Buffer.byteLength(rest); // an unfinished last line is read again next time
|
|
1435
|
+
} finally { closeSync(fd); }
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
const lo = usageDay(since);
|
|
1439
|
+
for (const [p, f] of Object.entries(state.files)) {
|
|
1440
|
+
for (const d of Object.keys(f.days)) if (d < lo) delete f.days[d];
|
|
1441
|
+
if (!Object.keys(f.days).length && !existsSync(p)) delete state.files[p];
|
|
1442
|
+
}
|
|
1443
|
+
return state;
|
|
1444
|
+
}
|
|
1445
|
+
// The last USAGE_DAYS days, per day and app, for the apps auto-wake runs here: {"2026-10-30": {"claude-code": {input, …}}}.
|
|
1446
|
+
export function usageReport(state, clients, now = Date.now()) {
|
|
1447
|
+
const lo = usageDay(now - (USAGE_DAYS - 1) * 86400000), out = {};
|
|
1448
|
+
for (const f of Object.values(state.files || {})) {
|
|
1449
|
+
if (!clients.includes(f.client)) continue;
|
|
1450
|
+
for (const [d, u] of Object.entries(f.days)) if (d >= lo) usageAdd(out[d] || (out[d] = {}), f.client, u);
|
|
1451
|
+
}
|
|
1452
|
+
return out;
|
|
1453
|
+
}
|
|
1454
|
+
async function usageTick(cfg, api, log, dir = DIR) {
|
|
1455
|
+
const file = join(wakeDir(dir), "usage.json");
|
|
1456
|
+
let state = {}; try { state = JSON.parse(readFileSync(file, "utf8")); } catch {}
|
|
1457
|
+
const clients = Object.keys(cfg.clients || {}).filter((c) => c in usageRoots());
|
|
1458
|
+
if (!clients.length) return;
|
|
1459
|
+
scanUsage(state, Object.fromEntries(Object.entries(usageRoots()).filter(([c]) => clients.includes(c))));
|
|
1460
|
+
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) });
|
|
1462
|
+
if (r.status !== 200 && r.status !== 404) log("usage: OwlMem answered " + (r.data?.detail || "HTTP " + r.status));
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1325
1465
|
async function wakeRun() {
|
|
1326
1466
|
const cfg = loadWake();
|
|
1327
1467
|
if (!cfg) throw new Fail("Auto-wake isn't set up on this computer. Run: npx -y owlmem wake enable");
|
|
1328
1468
|
const log = wakeLogger(), api = wakeApi();
|
|
1329
1469
|
try { if (wakeTools(cfg)) { saveWake(DIR, cfg); log("Unattended runs may now also use: " + WAKE_TOOLS.join(", ") + "."); } } catch (e) { log("tools: " + e.message); }
|
|
1330
1470
|
const report = async (j, res) => {
|
|
1331
|
-
log((
|
|
1471
|
+
log(jobLabel(j.task) + ": " + res.outcome + (res.code != null ? " (exit code " + res.code + ")" : "") + (res.detail ? " — " + res.detail : ""));
|
|
1332
1472
|
for (let i = 0; i < 5; i++) {
|
|
1333
1473
|
const r = await api("report", { id: j.id, outcome: res.outcome, code: res.code ?? null, detail: res.detail || "" });
|
|
1334
1474
|
if (r.status === 200 || r.status === 404 || r.status === 401) return;
|
|
@@ -1337,6 +1477,8 @@ async function wakeRun() {
|
|
|
1337
1477
|
};
|
|
1338
1478
|
const sched = new Scheduler({ max: cfg.max_parallel || WAKE_DEFAULTS.max_parallel, start: (j) => runTask(j.task, cfg, { plan: j.plan }), onDone: (j, res) => { report(j, res); } });
|
|
1339
1479
|
log("OwlMem auto-wake " + VERSION + " running on " + (cfg.machine || hostname()) + ".");
|
|
1480
|
+
const usage = () => usageTick(cfg, api, log).catch((e) => log("usage: " + e.message));
|
|
1481
|
+
setTimeout(usage, 30000).unref(); setInterval(usage, USAGE_EVERY).unref();
|
|
1340
1482
|
const out = await wakeLoop({ cfg, api, sched, log, update: (v) => wakeUpdate(v, { log }),
|
|
1341
1483
|
cleanup: () => { try { wakeUninstall(autostartOpts(), wakeRunCmd, true); } catch (e) { log("cleanup: " + e.message); } } });
|
|
1342
1484
|
if (out === "revoked") process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "owlmem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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" },
|