owlmem 0.7.0 → 0.7.3
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 +6 -2
- package/owlmem.mjs +51 -7
- 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` — 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` (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`) — with the task as the prompt. The AI marks the task done or declined as usual.
|
|
33
33
|
|
|
34
34
|
```sh
|
|
35
35
|
npx -y owlmem install --wake # or answer "y" when install asks; --yes alone leaves it off
|
|
@@ -44,7 +44,8 @@ npx -y owlmem wake disable # also done by npx -y owlmem uninstall
|
|
|
44
44
|
only, no admin rights. `--boot` (Linux) turns on lingering so it runs from boot, not just from login.
|
|
45
45
|
- **One program per computer** for every paired AI. One task at a time per AI, at most 2 at once (`--max`),
|
|
46
46
|
30 minutes per task (`--timeout`); a task that runs out of time is stopped and marked declined for a person.
|
|
47
|
-
A task sent to every agent wakes just one.
|
|
47
|
+
A task sent to every agent wakes just one. Replies in a thread (`kb_send` with `thread`) don't wake anyone;
|
|
48
|
+
asking someone in a thread to do something is a new task (`kind` `task` with `thread`), and that wakes them.
|
|
48
49
|
- **Working folder.** A line like `cwd: ~/Projects/app` in the task picks the folder. In a Git repository the AI
|
|
49
50
|
gets its own worktree and branch (`owlmem/task-<id>`), commits there and doesn't merge — the result names the
|
|
50
51
|
branch. Other folders take one task at a time; with no folder it works in an empty one.
|
|
@@ -76,6 +77,9 @@ Run `npx owlmem device` on that machine to register it and print its fingerprint
|
|
|
76
77
|
device key kept next to its config, so the same token on another machine gets nothing. By default a standing grant covers
|
|
77
78
|
`owlmem run` / `vault_run` only (the secret goes into a command); viewing in plain text must be allowed separately.
|
|
78
79
|
Saving, deleting and exporting always need approval.
|
|
80
|
+
If an agent without access asks for an item, the answer names up to three agents in the space with a standing grant for it,
|
|
81
|
+
so it can hand the job to one of them (`kb_send` with `kind` `task`): that agent runs it with `vault_run` and sends back only
|
|
82
|
+
the result. Secrets themselves never go into messages, documents or memory — OwlMem refuses text that looks like a key.
|
|
79
83
|
|
|
80
84
|
Let your AI use the vault directly through a local MCP server:
|
|
81
85
|
|
package/owlmem.mjs
CHANGED
|
@@ -39,7 +39,7 @@ import { join, dirname, basename, relative, isAbsolute } from "node:path";
|
|
|
39
39
|
import { fileURLToPath } from "node:url";
|
|
40
40
|
import { spawn, spawnSync } from "node:child_process";
|
|
41
41
|
|
|
42
|
-
export const VERSION = "0.7.
|
|
42
|
+
export const VERSION = "0.7.3";
|
|
43
43
|
const BASE = (process.env.OWLMEM_URL || "https://owlmem.com").replace(/\/$/, "");
|
|
44
44
|
const DIR = process.env.OWLMEM_HOME || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "owlmem");
|
|
45
45
|
const enc = new TextEncoder(), dec = new TextDecoder();
|
|
@@ -232,16 +232,21 @@ async function approved(method, path, body, kind = "access", mode = "other") {
|
|
|
232
232
|
if (r.status < 400) return r.data;
|
|
233
233
|
if (r.data.error !== "approval_required") throw new Fail(r.data.detail || r.data.error || "HTTP " + r.status);
|
|
234
234
|
const id = r.data.request;
|
|
235
|
+
// the server may name other agents that could do this for us (a standing grant for this item) — names only
|
|
236
|
+
let hint = r.data.hint || "";
|
|
235
237
|
say(`Waiting for approval in the OwlMem console (request ${id}${kind === "access" ? "" : ", " + kind}) …`);
|
|
238
|
+
if (hint) say(hint);
|
|
236
239
|
const until = Date.now() + WAIT_MS;
|
|
237
240
|
let st = "pending";
|
|
238
241
|
while (Date.now() < until) {
|
|
239
242
|
await new Promise((res) => setTimeout(res, Number(process.env.OWLMEM_POLL_MS || 3000)));
|
|
240
|
-
|
|
243
|
+
const s = (await call("GET", "requests/" + id)).data;
|
|
244
|
+
st = s.status; if (s.hint) hint = s.hint;
|
|
241
245
|
if (st !== "pending") break;
|
|
242
246
|
}
|
|
243
|
-
|
|
244
|
-
if (st
|
|
247
|
+
const tail = hint ? " " + hint : "";
|
|
248
|
+
if (st === "pending") throw new Fail(`Waiting for approval (request ${id}). Ask the space owner to approve it in the OwlMem console (Vault), then try again. For unattended jobs, ask them for a standing grant for this agent and this machine (run: owlmem device).` + tail);
|
|
249
|
+
if (st !== "approved") throw new Fail((st === "denied" ? "The request was denied." : "The request expired before it was approved.") + tail);
|
|
245
250
|
say("Approved.");
|
|
246
251
|
if (kind !== "access") extra = (path.includes("?") ? "&" : "?") + "request=" + encodeURIComponent(id);
|
|
247
252
|
}
|
|
@@ -800,7 +805,41 @@ export const WAKE_CLIENTS = {
|
|
|
800
805
|
// -p runs one prompt and exits; it may only use the two OwlMem tools it needs to report back, beyond what your own settings allow
|
|
801
806
|
command: ["claude", "-p", "--allowedTools", "mcp__owlmem__kb_task_update,mcp__owlmem__kb_inbox"], prompt: "stdin" },
|
|
802
807
|
codex: { name: "Codex", bin: "codex", command: ["codex", "exec", "--skip-git-repo-check", "-"], prompt: "stdin" },
|
|
808
|
+
// agy -p takes the prompt as an argument; headless it denies MCP tools unless settings.json allows them (see allowAntigravity)
|
|
809
|
+
antigravity: { name: "Antigravity", bin: "agy", home: [".gemini", "bin", "agy"], command: ["agy", "-p"], prompt: "arg" },
|
|
803
810
|
};
|
|
811
|
+
export const WAKE_APP_IDS = ["claude-code", "codex", "antigravity", "gemini", "custom"];
|
|
812
|
+
// Where an app's command is: on PATH, or (Antigravity) its default install place, used by full path
|
|
813
|
+
export function wakeBin(c, home_ = home(), exists = existsSync) {
|
|
814
|
+
if (onPath(c.bin)) return c.bin;
|
|
815
|
+
const p = c.home && join(home_, ...c.home);
|
|
816
|
+
return p && exists(p) ? p : null;
|
|
817
|
+
}
|
|
818
|
+
// codex exec runs with approval "never", so an MCP call that needs approval is refused: approve just the two OwlMem
|
|
819
|
+
// tools it needs to report back, as tool tables under the [mcp_servers.owlmem] that install wrote (append only)
|
|
820
|
+
export function allowCodex(home_ = home()) {
|
|
821
|
+
const f = join(process.env.CODEX_HOME || join(home_, ".codex"), "config.toml");
|
|
822
|
+
let t; try { t = readFileSync(f, "utf8"); } catch { return false; }
|
|
823
|
+
if (!/^\[mcp_servers\.owlmem\]\s*$/m.test(t)) return false;
|
|
824
|
+
const add = ["kb_task_update", "kb_inbox"].filter((n) => !new RegExp("^\\[mcp_servers\\.owlmem\\.tools\\." + n + "\\]\\s*$", "m").test(t));
|
|
825
|
+
if (!add.length) return false;
|
|
826
|
+
writeFileSync(f, t.replace(/\n*$/, "\n") + "\n# OwlMem auto-wake: unattended codex exec may call these two OwlMem tools to report back\n" +
|
|
827
|
+
add.map((n) => `[mcp_servers.owlmem.tools.${n}]\napproval_mode = "approve"\n`).join("\n"));
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
// Antigravity's CLI reads ~/.gemini/antigravity-cli/settings.json: add only the two rules it needs to report back
|
|
831
|
+
export function allowAntigravity(home_ = home()) {
|
|
832
|
+
const f = join(home_, ".gemini", "antigravity-cli", "settings.json"), need = ["mcp(owlmem/kb_task_update)", "mcp(owlmem/kb_inbox)"];
|
|
833
|
+
let j = {};
|
|
834
|
+
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."); } }
|
|
835
|
+
const allow = Array.isArray(j.permissions?.allow) ? j.permissions.allow : [];
|
|
836
|
+
const add = need.filter((r) => !allow.includes(r));
|
|
837
|
+
if (!add.length) return false;
|
|
838
|
+
j.permissions = { ...(j.permissions || {}), allow: [...allow, ...add] };
|
|
839
|
+
mkdirSync(dirname(f), { recursive: true });
|
|
840
|
+
writeFileSync(f, JSON.stringify(j, null, 2) + "\n");
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
804
843
|
export const WAKE_DEFAULTS = { max_parallel: 2, timeout_min: 30 };
|
|
805
844
|
const WAKE_UNIT = "owlmem-wake.service", WAKE_LABEL = "com.owlmem.wake", WAKE_TASK = "OwlMem Wake", WAKE_RUNKEY = "OwlMemWake";
|
|
806
845
|
const wakeDir = (dir = DIR) => join(dir, "wake");
|
|
@@ -1125,12 +1164,17 @@ async function wakeEnable(args = []) {
|
|
|
1125
1164
|
const max = num("--max", 1, 16), timeout = num("--timeout", 1, 240);
|
|
1126
1165
|
const old = loadWake() || {};
|
|
1127
1166
|
const clients = { ...(old.clients || {}) };
|
|
1128
|
-
for (const [id, c] of Object.entries(WAKE_CLIENTS))
|
|
1167
|
+
for (const [id, c] of Object.entries(WAKE_CLIENTS)) {
|
|
1168
|
+
const bin = !clients[id] && wakeBin(c);
|
|
1169
|
+
if (bin) clients[id] = { command: [bin, ...c.command.slice(1)], prompt: c.prompt };
|
|
1170
|
+
}
|
|
1129
1171
|
if (!Object.keys(clients).length)
|
|
1130
|
-
throw new Fail("No AI command line found on this computer that auto-wake can start (Claude Code: claude, Codex: codex). Install one, or add its command to " + wakeFile() + " under \"clients\".");
|
|
1172
|
+
throw new Fail("No AI command line found on this computer that auto-wake can start (Claude Code: claude, Codex: codex, Antigravity: agy). Install one, or add its command to " + wakeFile() + " under \"clients\".");
|
|
1173
|
+
if (clients.codex && allowCodex()) say("Let Codex report back to OwlMem when nobody is watching (2 tool approvals in ~/.codex/config.toml).");
|
|
1174
|
+
if (clients.antigravity && allowAntigravity()) say("Let Antigravity's command line report back to OwlMem when nobody is watching (2 rules in ~/.gemini/antigravity-cli/settings.json).");
|
|
1131
1175
|
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 };
|
|
1132
1176
|
// OwlMem knows these app names; a command you add yourself to wake.json goes under "gemini" or "custom"
|
|
1133
|
-
const r = await wakeApi()("pair", { clients: Object.keys(clients).filter((c) =>
|
|
1177
|
+
const r = await wakeApi()("pair", { clients: Object.keys(clients).filter((c) => WAKE_APP_IDS.includes(c)), force: again });
|
|
1134
1178
|
if (r.status !== 200) throw new Fail("OwlMem didn't accept the pairing: " + (r.data.detail || r.data.error || "HTTP " + r.status));
|
|
1135
1179
|
mkdirSync(wakeDir(), { recursive: true });
|
|
1136
1180
|
copyFileSync(fileURLToPath(import.meta.url), join(wakeDir(), "owlmem.mjs")); // the service runs this copy (npx's cache can be cleaned out)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "owlmem",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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" },
|