owlmem 0.7.0 → 0.7.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.
Files changed (3) hide show
  1. package/README.md +6 -2
  2. package/owlmem.mjs +38 -7
  3. 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` (auto-wake adds two allow rules for its OwlMem reporting tools to `~/.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.0";
42
+ export const VERSION = "0.7.2";
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
- st = (await call("GET", "requests/" + id)).data.status;
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
- 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).`);
244
- if (st !== "approved") throw new Fail(st === "denied" ? "The request was denied." : "The request expired before it was approved.");
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,29 @@ 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
+ // Antigravity's CLI reads ~/.gemini/antigravity-cli/settings.json: add only the two rules it needs to report back
819
+ export function allowAntigravity(home_ = home()) {
820
+ const f = join(home_, ".gemini", "antigravity-cli", "settings.json"), need = ["mcp(owlmem/kb_task_update)", "mcp(owlmem/kb_inbox)"];
821
+ let j = {};
822
+ 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."); } }
823
+ const allow = Array.isArray(j.permissions?.allow) ? j.permissions.allow : [];
824
+ const add = need.filter((r) => !allow.includes(r));
825
+ if (!add.length) return false;
826
+ j.permissions = { ...(j.permissions || {}), allow: [...allow, ...add] };
827
+ mkdirSync(dirname(f), { recursive: true });
828
+ writeFileSync(f, JSON.stringify(j, null, 2) + "\n");
829
+ return true;
830
+ }
804
831
  export const WAKE_DEFAULTS = { max_parallel: 2, timeout_min: 30 };
805
832
  const WAKE_UNIT = "owlmem-wake.service", WAKE_LABEL = "com.owlmem.wake", WAKE_TASK = "OwlMem Wake", WAKE_RUNKEY = "OwlMemWake";
806
833
  const wakeDir = (dir = DIR) => join(dir, "wake");
@@ -1125,12 +1152,16 @@ async function wakeEnable(args = []) {
1125
1152
  const max = num("--max", 1, 16), timeout = num("--timeout", 1, 240);
1126
1153
  const old = loadWake() || {};
1127
1154
  const clients = { ...(old.clients || {}) };
1128
- for (const [id, c] of Object.entries(WAKE_CLIENTS)) if (!clients[id] && onPath(c.bin)) clients[id] = { command: c.command, prompt: c.prompt };
1155
+ for (const [id, c] of Object.entries(WAKE_CLIENTS)) {
1156
+ const bin = !clients[id] && wakeBin(c);
1157
+ if (bin) clients[id] = { command: [bin, ...c.command.slice(1)], prompt: c.prompt };
1158
+ }
1129
1159
  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\".");
1160
+ 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\".");
1161
+ 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
1162
  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
1163
  // 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) => ["claude-code", "codex", "gemini", "custom"].includes(c)), force: again });
1164
+ const r = await wakeApi()("pair", { clients: Object.keys(clients).filter((c) => WAKE_APP_IDS.includes(c)), force: again });
1134
1165
  if (r.status !== 200) throw new Fail("OwlMem didn't accept the pairing: " + (r.data.detail || r.data.error || "HTTP " + r.status));
1135
1166
  mkdirSync(wakeDir(), { recursive: true });
1136
1167
  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.0",
3
+ "version": "0.7.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" },