owlmem 0.7.6 → 0.7.8
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/README.md +1 -1
- package/owlmem.mjs +153 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ page, where you sign in and click **Allow**. AI agents can read the setup guide
|
|
|
29
29
|
|
|
30
30
|
An AI on your computer normally sees a task only when someone opens it. Auto-wake starts it as soon as it gets one:
|
|
31
31
|
a small background program long-polls OwlMem and runs that AI's command line unattended — Claude Code with
|
|
32
|
-
`claude -p`, Codex with `codex exec`, Antigravity with `agy -p`, Hermes with `hermes -z` (for Codex and Antigravity, which refuse tool calls when nobody is watching, auto-wake approves just the two OwlMem tools used to report back: in `~/.codex/config.toml` and `~/.gemini/antigravity-cli/settings.json`; for Hermes, only if OwlMem is marked `trust: untrusted`, under `command_allowlist` in `~/.hermes/config.yaml`) — with the task as the prompt. The AI marks the task done or declined as usual.
|
|
32
|
+
`claude -p`, Codex with `codex exec`, Antigravity with `agy -p`, Hermes with `hermes -z` (for Codex and Antigravity, which refuse tool calls when nobody is watching, auto-wake approves just the two OwlMem tools used to report back: in `~/.codex/config.toml` and `~/.gemini/antigravity-cli/settings.json`; for Hermes, only if OwlMem is marked `trust: untrusted`, under `command_allowlist` in `~/.hermes/config.yaml`) — with the task as the prompt. The AI marks the task done or declined as usual. When a task it gave out is done or declined, it is woken too, to read the result. The program updates itself (from npm, when nothing is running).
|
|
33
33
|
|
|
34
34
|
When someone turns auto-wake on for an AI in the console and no computer runs it yet, that AI is told so the next time it connects:
|
|
35
35
|
with your OK it runs `npx -y owlmem wake enable` itself and sends you the pairing link to approve.
|
package/owlmem.mjs
CHANGED
|
@@ -33,14 +33,15 @@
|
|
|
33
33
|
// a command (`owlmem run`, vault_run) or is shown (`vault get`, vault_get). A token copied to another machine has no
|
|
34
34
|
// device key there, so standing grants don't apply. Viewing, writing, deleting and exporting always need approval.
|
|
35
35
|
|
|
36
|
-
import { webcrypto as crypto } from "node:crypto";
|
|
37
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync, rmSync, statSync, realpathSync, openSync, closeSync, appendFileSync } from "node:fs";
|
|
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";
|
|
38
|
+
import { gunzipSync } from "node:zlib";
|
|
38
39
|
import { homedir, hostname, userInfo } from "node:os";
|
|
39
40
|
import { join, dirname, basename, relative, isAbsolute } from "node:path";
|
|
40
41
|
import { fileURLToPath } from "node:url";
|
|
41
42
|
import { spawn, spawnSync } from "node:child_process";
|
|
42
43
|
|
|
43
|
-
export const VERSION = "0.7.
|
|
44
|
+
export const VERSION = "0.7.7";
|
|
44
45
|
const BASE = (process.env.OWLMEM_URL || "https://owlmem.com").replace(/\/$/, "");
|
|
45
46
|
const DIR = process.env.OWLMEM_HOME || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "owlmem");
|
|
46
47
|
const enc = new TextEncoder(), dec = new TextDecoder();
|
|
@@ -797,14 +798,18 @@ function nextSteps(items, out) {
|
|
|
797
798
|
// logon task) long-polls OwlMem. When an agent paired with this computer gets a task, it runs that agent's command line
|
|
798
799
|
// unattended (`claude -p`, `codex exec`) with the task as the prompt. The woken AI uses its own OwlMem connection (the one
|
|
799
800
|
// `owlmem install` set up), so it acts as itself. It gets no extra permissions: no --dangerously-* flags; anything that
|
|
800
|
-
// needs a person, it declines with what the person has to do.
|
|
801
|
+
// needs a person, it declines with what the person has to do. When a task the agent gave out is done or declined, it is
|
|
802
|
+
// woken too, to read the result (OwlMem sends those only to 0.7.7 and later).
|
|
801
803
|
// This computer is identified by its device key (device.json); the pairing is approved in the console, where each AI has an
|
|
802
804
|
// auto-wake switch and the computer can be removed. If OwlMem answers 401 (computer removed, AI disconnected, account gone)
|
|
803
805
|
// the program removes its own autostart entry and exits.
|
|
806
|
+
// The OwlMem tools an unattended wake run may call without asking: answer the task, read its inbox, and send (report the
|
|
807
|
+
// result to its person, hand work on). 0.7.8 added kb_send (站长 10-28).
|
|
808
|
+
export const WAKE_TOOLS = ["kb_task_update", "kb_inbox", "kb_send"];
|
|
804
809
|
export const WAKE_CLIENTS = {
|
|
805
810
|
"claude-code": { name: "Claude Code", bin: "claude",
|
|
806
|
-
// -p runs one prompt and exits; it may only use the
|
|
807
|
-
command: ["claude", "-p", "--allowedTools", "
|
|
811
|
+
// -p runs one prompt and exits; it may only use the OwlMem tools in WAKE_TOOLS, beyond what your own settings allow
|
|
812
|
+
command: ["claude", "-p", "--allowedTools", WAKE_TOOLS.map((n) => "mcp__owlmem__" + n).join(",")], prompt: "stdin" },
|
|
808
813
|
codex: { name: "Codex", bin: "codex", command: ["codex", "exec", "--skip-git-repo-check", "-"], prompt: "stdin" },
|
|
809
814
|
// agy -p takes the prompt as an argument; headless it denies MCP tools unless settings.json allows them (see allowAntigravity)
|
|
810
815
|
antigravity: { name: "Antigravity", bin: "agy", home: [[".gemini", "bin", "agy"]], command: ["agy", "-p"], prompt: "arg" },
|
|
@@ -824,21 +829,21 @@ export function wakeBin(c, home_ = home(), exists = existsSync, path = onPath) {
|
|
|
824
829
|
}
|
|
825
830
|
return null;
|
|
826
831
|
}
|
|
827
|
-
// codex exec runs with approval "never", so an MCP call that needs approval is refused: approve just the
|
|
828
|
-
// tools
|
|
832
|
+
// codex exec runs with approval "never", so an MCP call that needs approval is refused: approve just the OwlMem
|
|
833
|
+
// tools in WAKE_TOOLS, as tool tables under the [mcp_servers.owlmem] that install wrote (append only)
|
|
829
834
|
export function allowCodex(home_ = home()) {
|
|
830
835
|
const f = join(process.env.CODEX_HOME || join(home_, ".codex"), "config.toml");
|
|
831
836
|
let t; try { t = readFileSync(f, "utf8"); } catch { return false; }
|
|
832
837
|
if (!/^\[mcp_servers\.owlmem\]\s*$/m.test(t)) return false;
|
|
833
|
-
const add =
|
|
838
|
+
const add = WAKE_TOOLS.filter((n) => !new RegExp("^\\[mcp_servers\\.owlmem\\.tools\\." + n + "\\]\\s*$", "m").test(t));
|
|
834
839
|
if (!add.length) return false;
|
|
835
|
-
writeFileSync(f, t.replace(/\n*$/, "\n") + "\n# OwlMem auto-wake: unattended codex exec may call these
|
|
840
|
+
writeFileSync(f, t.replace(/\n*$/, "\n") + "\n# OwlMem auto-wake: unattended codex exec may call these OwlMem tools to report back\n" +
|
|
836
841
|
add.map((n) => `[mcp_servers.owlmem.tools.${n}]\napproval_mode = "approve"\n`).join("\n"));
|
|
837
842
|
return true;
|
|
838
843
|
}
|
|
839
|
-
// Antigravity's CLI reads ~/.gemini/antigravity-cli/settings.json: add only the
|
|
844
|
+
// Antigravity's CLI reads ~/.gemini/antigravity-cli/settings.json: add only the rules for WAKE_TOOLS
|
|
840
845
|
export function allowAntigravity(home_ = home()) {
|
|
841
|
-
const f = join(home_, ".gemini", "antigravity-cli", "settings.json"), need =
|
|
846
|
+
const f = join(home_, ".gemini", "antigravity-cli", "settings.json"), need = WAKE_TOOLS.map((n) => "mcp(owlmem/" + n + ")");
|
|
842
847
|
let j = {};
|
|
843
848
|
if (existsSync(f)) { try { j = JSON.parse(readFileSync(f, "utf8")); } catch { throw new Fail("Can't read " + tilde(f) + " (not JSON); fix it or add " + need.join(", ") + " under permissions.allow yourself."); } }
|
|
844
849
|
const allow = Array.isArray(j.permissions?.allow) ? j.permissions.allow : [];
|
|
@@ -850,8 +855,8 @@ export function allowAntigravity(home_ = home()) {
|
|
|
850
855
|
return true;
|
|
851
856
|
}
|
|
852
857
|
// Hermes calls MCP tools without asking unless the server is marked `trust: untrusted` (or an unknown value) under mcp_servers in
|
|
853
|
-
// ~/.hermes/config.yaml; then unattended runs refuse its write tools. Only in that case, add
|
|
854
|
-
//
|
|
858
|
+
// ~/.hermes/config.yaml; then unattended runs refuse its write tools. Only in that case, add WAKE_TOOLS
|
|
859
|
+
// to the top-level command_allowlist as mcp:<server>/<tool> (what Hermes itself writes for "Always"). Adds lines only,
|
|
855
860
|
// never rewrites what's there; a command_allowlist it can't extend safely (not a plain block list) returns "manual".
|
|
856
861
|
// false: nothing to do (no config, no OwlMem server in it, a trusted one, or the entries are already there).
|
|
857
862
|
export function allowHermes(home_ = home(), env = process.env) {
|
|
@@ -877,10 +882,10 @@ export function allowHermes(home_ = home(), env = process.env) {
|
|
|
877
882
|
break;
|
|
878
883
|
}
|
|
879
884
|
if (!server || trust === "full") return false;
|
|
880
|
-
const need =
|
|
885
|
+
const need = WAKE_TOOLS.map((n) => "mcp:" + server + "/" + n);
|
|
881
886
|
const at = lines.findIndex((l) => /^command_allowlist:/.test(l));
|
|
882
887
|
if (at < 0) {
|
|
883
|
-
writeFileSync(f, t.replace(/\n*$/, "\n") + "# OwlMem auto-wake: unattended hermes -z may call these
|
|
888
|
+
writeFileSync(f, t.replace(/\n*$/, "\n") + "# OwlMem auto-wake: unattended hermes -z may call these OwlMem tools to report back\ncommand_allowlist:\n" +
|
|
884
889
|
need.map((r) => " - " + r + "\n").join(""));
|
|
885
890
|
return true;
|
|
886
891
|
}
|
|
@@ -1056,7 +1061,7 @@ export function planWorkdir(task, h = home(), git = gitRun) {
|
|
|
1056
1061
|
}
|
|
1057
1062
|
export function prepareWorkdir(task, plan, dir = DIR, git = gitRun) {
|
|
1058
1063
|
if (plan.kind === "missing") throw new Fail("the task's directory doesn't exist on this computer: " + plan.dir);
|
|
1059
|
-
if (plan.kind === "scratch") { const cwd = join(wakeDir(dir), "scratch",
|
|
1064
|
+
if (plan.kind === "scratch") { const cwd = join(wakeDir(dir), "scratch", jobName(task)); mkdirSync(cwd, { recursive: true }); return { cwd }; }
|
|
1060
1065
|
if (plan.kind === "dir") return { cwd: plan.dir };
|
|
1061
1066
|
const branch = "owlmem/task-" + task.id, wt = join(wakeDir(dir), "worktrees", basename(plan.top) + "-task-" + task.id);
|
|
1062
1067
|
if (!existsSync(wt)) {
|
|
@@ -1069,7 +1074,33 @@ export function prepareWorkdir(task, plan, dir = DIR, git = gitRun) {
|
|
|
1069
1074
|
return { cwd: rel ? join(wt, rel) : wt, worktree: wt, branch, repo: plan.top };
|
|
1070
1075
|
}
|
|
1071
1076
|
|
|
1077
|
+
// A job from OwlMem is a task given to this AI (type "task"), or the result of a task this AI gave someone else (type "reply":
|
|
1078
|
+
// the automatic note sent when that task was done or declined). Older OwlMem servers send no type: a task.
|
|
1079
|
+
const isReply = (t) => t?.type === "reply";
|
|
1080
|
+
const jobName = (t) => (isReply(t) ? "reply-" : "task-") + t.id;
|
|
1081
|
+
const shortName = (who) => { who = String(who || ""); return who.startsWith("user:") ? who.slice(5) : who.includes("/") ? who.split("/").slice(1).join("/") : who; };
|
|
1082
|
+
|
|
1083
|
+
// 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.
|
|
1084
|
+
export function replyPrompt(r, o = {}) {
|
|
1085
|
+
const by = shortName(r.by), done = r.result === "done", note = String(r.note || "").trim(), mins = o.timeout_min || WAKE_DEFAULTS.timeout_min;
|
|
1086
|
+
if (r.zh) return `奥麦自动唤醒在 ${o.machine || "这台电脑"} 上启动了你(${r.agent})。没有人在看这个会话,也没人能回答问题。
|
|
1087
|
+
|
|
1088
|
+
你派的任务 #${r.task_id}「${r.task_title || ""}」有了结果:${by} ${done ? "完成" : "拒绝"}了,${done ? "结果" : "原因"}:${note || "(没写)"}
|
|
1089
|
+
|
|
1090
|
+
先用 kb_inbox 看这封回信(#${r.id},会标成已读);需要接着做(换人派、把结果告诉站长、自己补做)就做;没有要做的就直接结束,不要发任何消息。
|
|
1091
|
+
不要把密码、令牌、密钥写进任何消息。你有 ${mins} 分钟。
|
|
1092
|
+
`;
|
|
1093
|
+
return `OwlMem auto-wake started you (${r.agent}) on ${o.machine || "this computer"}. Nobody is watching this session and nobody can answer questions.
|
|
1094
|
+
|
|
1095
|
+
The task you gave out, #${r.task_id} "${r.task_title || ""}", has a result: ${by} ${done ? "finished it" : "declined it"}. ${done ? "Result" : "Reason"}: ${note || "(none given)"}
|
|
1096
|
+
|
|
1097
|
+
First read this reply with kb_inbox (#${r.id}; that marks it read). If something needs doing next (hand it to someone else, tell your user the result, do it yourself), do it. If nothing does, just end here and don't send any message.
|
|
1098
|
+
Never put passwords, tokens, keys or other secrets in any message. You have ${mins} minutes.
|
|
1099
|
+
`;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1072
1102
|
export function wakePrompt(task, place, o = {}) {
|
|
1103
|
+
if (isReply(task)) return replyPrompt(task, o);
|
|
1073
1104
|
const id = task.id, mins = o.timeout_min || WAKE_DEFAULTS.timeout_min;
|
|
1074
1105
|
const where = place.worktree
|
|
1075
1106
|
? `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.`
|
|
@@ -1102,7 +1133,7 @@ export async function runTask(task, cfg, o = {}) {
|
|
|
1102
1133
|
const prompt = wakePrompt(task, place, { machine: cfg.machine, timeout_min: cfg.timeout_min });
|
|
1103
1134
|
const argv = [...cl.command, ...(cl.prompt === "arg" ? [prompt] : [])];
|
|
1104
1135
|
mkdirSync(join(wakeDir(dir), "logs"), { recursive: true });
|
|
1105
|
-
const logPath = join(wakeDir(dir), "logs",
|
|
1136
|
+
const logPath = join(wakeDir(dir), "logs", jobName(task) + ".log");
|
|
1106
1137
|
const fd = openSync(logPath, "a");
|
|
1107
1138
|
const win = (o.platform || platform()) === "win32";
|
|
1108
1139
|
const ms = o.timeoutMs ?? (cfg.timeout_min || WAKE_DEFAULTS.timeout_min) * 60000;
|
|
@@ -1111,7 +1142,7 @@ export async function runTask(task, cfg, o = {}) {
|
|
|
1111
1142
|
let ch, timedOut = false, killer = null;
|
|
1112
1143
|
try {
|
|
1113
1144
|
ch = spawn(argv[0], argv.slice(1), { cwd: place.cwd, stdio: ["pipe", fd, fd], detached: !win, shell: win, windowsHide: true,
|
|
1114
|
-
env: { ...process.env, OWLMEM_NO_BROWSER: "1", OWLMEM_WAKE_TASK: String(task.id) } });
|
|
1145
|
+
env: { ...process.env, OWLMEM_NO_BROWSER: "1", [isReply(task) ? "OWLMEM_WAKE_REPLY" : "OWLMEM_WAKE_TASK"]: String(task.id) } });
|
|
1115
1146
|
} catch (e) { res({ outcome: "error", detail: "couldn't start " + argv[0] + ": " + e.message }); return; }
|
|
1116
1147
|
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 {} } };
|
|
1117
1148
|
const timer = setTimeout(() => { timedOut = true; kill("SIGTERM"); killer = setTimeout(() => kill("SIGKILL"), 10000); }, ms);
|
|
@@ -1168,15 +1199,93 @@ export async function wakeLoop(o) {
|
|
|
1168
1199
|
}
|
|
1169
1200
|
backoff = 0;
|
|
1170
1201
|
for (const t of r.data.tasks || []) {
|
|
1171
|
-
const plan = o.plan ? o.plan(t) : planWorkdir(t);
|
|
1172
|
-
log("Task #" + t.id + " for " + t.agent + ": starting " + t.client + (plan.dir ? " in " + plan.dir : "") + ".");
|
|
1202
|
+
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 + ")" : "Task #" + t.id) + " for " + t.agent + ": starting " + t.client + (plan.dir ? " in " + plan.dir : "") + ".");
|
|
1173
1204
|
sched.add({ id: t.id, agent: t.agent, lock: plan.lock || null, task: t, plan });
|
|
1174
1205
|
}
|
|
1206
|
+
// OwlMem says a newer version is out: update only while nothing runs or waits (no task is cut off), then restart
|
|
1207
|
+
if (o.update && newer(r.data.latest, VERSION) && !(r.data.tasks || []).length && sched.size === 0) {
|
|
1208
|
+
if ((await o.update(r.data.latest)) === "restart") return "updated";
|
|
1209
|
+
}
|
|
1175
1210
|
if (once) return r.data.status;
|
|
1176
1211
|
if (Date.now() - t0 < 1000 && !(r.data.tasks || []).length) await sleep(1000); // never spin
|
|
1177
1212
|
}
|
|
1178
1213
|
}
|
|
1179
1214
|
|
|
1215
|
+
// ── auto-wake updates itself ──
|
|
1216
|
+
// The program runs its own copy (wake/owlmem.mjs). When OwlMem's poll answer names a newer version (latest), and nothing is
|
|
1217
|
+
// running: download that version's package from the npm registry, check it against the registry's sha512, take owlmem.mjs
|
|
1218
|
+
// 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.
|
|
1220
|
+
// Restart: on Linux and macOS the program exits with WAKE_RESTART_CODE; the systemd unit (Restart=on-failure) and the
|
|
1221
|
+
// LaunchAgent (KeepAlive unless the exit was successful) start it again 30 s later. On Windows nothing watches it, so it
|
|
1222
|
+
// starts the new copy itself (detached, like the logon launcher) and then exits.
|
|
1223
|
+
export const WAKE_RESTART_CODE = 75;
|
|
1224
|
+
const UPDATE_RETRY = 24 * 3600 * 1000;
|
|
1225
|
+
const REGISTRY = () => String(process.env.npm_config_registry || "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
1226
|
+
export function newer(a, b) {
|
|
1227
|
+
const p = (v) => (/^\d+\.\d+\.\d+$/.test(String(v || "")) ? String(v).split(".").map(Number) : null);
|
|
1228
|
+
const x = p(a), y = p(b);
|
|
1229
|
+
if (!x || !y) return false;
|
|
1230
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i];
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
// The one file we need from an npm package (.tgz): plain ustar, read in memory.
|
|
1234
|
+
export function tarFile(tgz, want = "package/owlmem.mjs") {
|
|
1235
|
+
const t = gunzipSync(tgz);
|
|
1236
|
+
for (let off = 0; off + 512 <= t.length;) {
|
|
1237
|
+
const h = t.subarray(off, off + 512);
|
|
1238
|
+
if (h.every((b) => b === 0)) break;
|
|
1239
|
+
const str = (a, b) => h.subarray(a, b).toString("utf8").replace(/\0.*$/s, "");
|
|
1240
|
+
const name = (str(345, 500) ? str(345, 500) + "/" : "") + str(0, 100), size = parseInt(str(124, 136).trim() || "0", 8), type = str(156, 157);
|
|
1241
|
+
if ((type === "0" || type === "") && name === want) return t.subarray(off + 512, off + 512 + size);
|
|
1242
|
+
off += 512 + Math.ceil(size / 512) * 512;
|
|
1243
|
+
}
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
// → "restart" (new copy in place) | "skip" (tried that version less than 24 h ago) | "failed" (old copy stays)
|
|
1247
|
+
export async function wakeUpdate(latest, o = {}) {
|
|
1248
|
+
const dir = o.dir || DIR, log = o.log || (() => {}), now = o.now || Date.now, get = o.fetch || fetch;
|
|
1249
|
+
const wd = wakeDir(dir), state = join(wd, "update.json"), target = join(wd, "owlmem.mjs"), tmpFile = join(wd, "owlmem.update.mjs");
|
|
1250
|
+
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 {}
|
|
1253
|
+
log("Updating auto-wake from " + VERSION + " to " + latest + ".");
|
|
1254
|
+
try {
|
|
1255
|
+
const meta = await get(REGISTRY() + "/owlmem/" + encodeURIComponent(latest), { signal: AbortSignal.timeout(60000) });
|
|
1256
|
+
if (!meta.ok) throw new Error("registry answered HTTP " + meta.status);
|
|
1257
|
+
const dist = (await meta.json())?.dist || {};
|
|
1258
|
+
const m = /^sha512-(.+)$/.exec(String(dist.integrity || ""));
|
|
1259
|
+
if (!dist.tarball || !m) throw new Error("the registry gave no package or checksum");
|
|
1260
|
+
const res = await get(dist.tarball, { signal: AbortSignal.timeout(120000) });
|
|
1261
|
+
if (!res.ok) throw new Error("download answered HTTP " + res.status);
|
|
1262
|
+
const tgz = Buffer.from(await res.arrayBuffer());
|
|
1263
|
+
if (createHash("sha512").update(tgz).digest("base64") !== m[1]) throw new Error("the download doesn't match the registry's checksum");
|
|
1264
|
+
const file = tarFile(tgz);
|
|
1265
|
+
if (!file) throw new Error("owlmem.mjs isn't in the package");
|
|
1266
|
+
writeFileSync(tmpFile, file);
|
|
1267
|
+
const check = (o.check || ((f) => { const r = spawnSync(process.execPath, [f, "--version"], { encoding: "utf8", timeout: 30000, env: { ...process.env, OWLMEM_NO_BROWSER: "1" } }); return (r.stdout || "").trim(); }))(tmpFile);
|
|
1268
|
+
if (check !== latest) throw new Error("the new copy says it is " + JSON.stringify(check) + ", not " + latest);
|
|
1269
|
+
renameSync(tmpFile, target);
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
rmSync(tmpFile, { force: true });
|
|
1272
|
+
log("Update to " + latest + " failed (" + (e?.message || e) + "); keeping " + VERSION + " and not trying " + latest + " again for 24 hours.");
|
|
1273
|
+
return "failed";
|
|
1274
|
+
}
|
|
1275
|
+
log("Updated auto-wake to " + latest + "; restarting.");
|
|
1276
|
+
return "restart";
|
|
1277
|
+
}
|
|
1278
|
+
// After an update: come back as the new copy (see above).
|
|
1279
|
+
export function wakeRestart(o = {}) {
|
|
1280
|
+
const p = o.platform || platform(), exit = o.exit || ((c) => process.exit(c));
|
|
1281
|
+
if (p === "win32") {
|
|
1282
|
+
const run = o.spawn || ((argv) => spawn(argv[0], argv.slice(1), { stdio: "ignore", detached: true, windowsHide: true, env: process.env }).on("error", () => {}).unref());
|
|
1283
|
+
run([process.execPath, join(wakeDir(o.dir || DIR), "owlmem.mjs"), "wake", "run"]);
|
|
1284
|
+
return exit(0);
|
|
1285
|
+
}
|
|
1286
|
+
return exit(WAKE_RESTART_CODE);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1180
1289
|
function wakeApi() {
|
|
1181
1290
|
return async (sub, body) => {
|
|
1182
1291
|
const url = BASE + "/v1/wake/" + sub;
|
|
@@ -1199,12 +1308,27 @@ function wakeLogger(dir = DIR) {
|
|
|
1199
1308
|
};
|
|
1200
1309
|
}
|
|
1201
1310
|
|
|
1311
|
+
// Computers set up before a tool joined WAKE_TOOLS (0.7.8: kb_send) catch up when the program starts after its self-update:
|
|
1312
|
+
// Claude Code's --allowedTools in wake.json if it is still one we wrote, and the other apps' own settings (append only).
|
|
1313
|
+
// Returns true when wake.json changed.
|
|
1314
|
+
export function wakeTools(cfg, home_ = home(), env = process.env) {
|
|
1315
|
+
const ours = ["mcp__owlmem__kb_task_update,mcp__owlmem__kb_inbox"], now = WAKE_TOOLS.map((n) => "mcp__owlmem__" + n).join(",");
|
|
1316
|
+
const cmd = cfg.clients?.["claude-code"]?.command, i = Array.isArray(cmd) ? cmd.indexOf("--allowedTools") : -1;
|
|
1317
|
+
let changed = false;
|
|
1318
|
+
if (i >= 0 && ours.includes(cmd[i + 1])) { cmd[i + 1] = now; changed = true; }
|
|
1319
|
+
if (cfg.clients?.codex) allowCodex(home_);
|
|
1320
|
+
if (cfg.clients?.antigravity) allowAntigravity(home_);
|
|
1321
|
+
if (cfg.clients?.hermes) allowHermes(home_, env);
|
|
1322
|
+
return changed;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1202
1325
|
async function wakeRun() {
|
|
1203
1326
|
const cfg = loadWake();
|
|
1204
1327
|
if (!cfg) throw new Fail("Auto-wake isn't set up on this computer. Run: npx -y owlmem wake enable");
|
|
1205
1328
|
const log = wakeLogger(), api = wakeApi();
|
|
1329
|
+
try { if (wakeTools(cfg)) { saveWake(DIR, cfg); log("Unattended runs may now also use: " + WAKE_TOOLS.join(", ") + "."); } } catch (e) { log("tools: " + e.message); }
|
|
1206
1330
|
const report = async (j, res) => {
|
|
1207
|
-
log("Task #" + j.id + ": " + res.outcome + (res.code != null ? " (exit code " + res.code + ")" : "") + (res.detail ? " — " + res.detail : ""));
|
|
1331
|
+
log((isReply(j.task) ? "Reply #" : "Task #") + j.id + ": " + res.outcome + (res.code != null ? " (exit code " + res.code + ")" : "") + (res.detail ? " — " + res.detail : ""));
|
|
1208
1332
|
for (let i = 0; i < 5; i++) {
|
|
1209
1333
|
const r = await api("report", { id: j.id, outcome: res.outcome, code: res.code ?? null, detail: res.detail || "" });
|
|
1210
1334
|
if (r.status === 200 || r.status === 404 || r.status === 401) return;
|
|
@@ -1213,8 +1337,10 @@ async function wakeRun() {
|
|
|
1213
1337
|
};
|
|
1214
1338
|
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); } });
|
|
1215
1339
|
log("OwlMem auto-wake " + VERSION + " running on " + (cfg.machine || hostname()) + ".");
|
|
1216
|
-
const out = await wakeLoop({ cfg, api, sched, log,
|
|
1340
|
+
const out = await wakeLoop({ cfg, api, sched, log, update: (v) => wakeUpdate(v, { log }),
|
|
1341
|
+
cleanup: () => { try { wakeUninstall(autostartOpts(), wakeRunCmd, true); } catch (e) { log("cleanup: " + e.message); } } });
|
|
1217
1342
|
if (out === "revoked") process.exit(0);
|
|
1343
|
+
if (out === "updated") wakeRestart();
|
|
1218
1344
|
}
|
|
1219
1345
|
|
|
1220
1346
|
// No command line auto-wake can start: tell OwlMem support which AI apps this computer has, so it can add them. Only the
|
|
@@ -1241,12 +1367,12 @@ async function wakeEnable(args = []) {
|
|
|
1241
1367
|
throw new Fail("No AI command line found on this computer that auto-wake can start (Claude Code: claude, Codex: codex, Antigravity: agy, Hermes: hermes). Install one, or add its command to " + wakeFile() + " under \"clients\"." +
|
|
1242
1368
|
(told ? "\nOwlMem support has been told; you'll hear from us once your AI app is supported." : ""));
|
|
1243
1369
|
}
|
|
1244
|
-
if (clients.codex && allowCodex()) say("Let Codex report back to OwlMem when nobody is watching (
|
|
1245
|
-
if (clients.antigravity && allowAntigravity()) say("Let Antigravity's command line report back to OwlMem when nobody is watching (
|
|
1370
|
+
if (clients.codex && allowCodex()) say("Let Codex report back to OwlMem when nobody is watching (tool approvals in ~/.codex/config.toml).");
|
|
1371
|
+
if (clients.antigravity && allowAntigravity()) say("Let Antigravity's command line report back to OwlMem when nobody is watching (rules in ~/.gemini/antigravity-cli/settings.json).");
|
|
1246
1372
|
if (clients.hermes) {
|
|
1247
1373
|
const h = allowHermes();
|
|
1248
|
-
if (h === true) say("Let Hermes report back to OwlMem when nobody is watching (
|
|
1249
|
-
else if (h === "manual") say("Hermes treats OwlMem as untrusted: add mcp:<server>/kb_task_update and mcp:<server>/
|
|
1374
|
+
if (h === true) say("Let Hermes report back to OwlMem when nobody is watching (entries under command_allowlist in ~/.hermes/config.yaml).");
|
|
1375
|
+
else if (h === "manual") say("Hermes treats OwlMem as untrusted: add mcp:<server>/kb_task_update, mcp:<server>/kb_inbox and mcp:<server>/kb_send to command_allowlist in ~/.hermes/config.yaml, or it can't report back when nobody is watching.");
|
|
1250
1376
|
}
|
|
1251
1377
|
const cfg = { v: 1, ...WAKE_DEFAULTS, ...old, machine: process.env.OWLMEM_DEVICE_NAME || hostname(), clients, ...(max ? { max_parallel: max } : {}), ...(timeout ? { timeout_min: timeout } : {}), boot: boot || !!old.boot };
|
|
1252
1378
|
// OwlMem knows these app names; a command you add yourself to wake.json goes under "gemini" or "custom"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "owlmem",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
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" },
|