owlmem 0.7.3 → 0.7.6

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 +11 -8
  2. package/owlmem.mjs +100 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -29,7 +29,10 @@ 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` (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.
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.
33
+
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
+ with your OK it runs `npx -y owlmem wake enable` itself and sends you the pairing link to approve.
33
36
 
34
37
  ```sh
35
38
  npx -y owlmem install --wake # or answer "y" when install asks; --yes alone leaves it off
@@ -71,13 +74,13 @@ npx owlmem vault import bitwarden_export.json # also 1Password / KeePass CSV,
71
74
  Vault items are encrypted on your machine with a key only you hold; OwlMem stores ciphertext it cannot read.
72
75
  Every read or write needs the space owner's approval in the console (passkey, authenticator app or GitHub).
73
76
 
74
- For unattended jobs (scheduled tasks, overnight deploys) the owner can set a **standing grant**: one agent, on one machine,
75
- may read chosen items or a folder without asking each time, until a set date (90 days at most), optionally only during set hours.
76
- Run `npx owlmem device` on that machine to register it and print its fingerprint. The program signs every vault request with a
77
- device key kept next to its config, so the same token on another machine gets nothing. By default a standing grant covers
78
- `owlmem run` / `vault_run` only (the secret goes into a command); viewing in plain text must be allowed separately.
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,
77
+ For unattended jobs (scheduled tasks, overnight deploys) the owner can turn on an agent's **standing grant**: in the console
78
+ (Agents, one switch per agent), or while approving one of its requests. The agent can then use any vault item without asking,
79
+ on computers the owner approved (a new computer asks once; approving remembers it), and only through `owlmem run` / `vault_run`,
80
+ so the secret goes into a command. The program signs every vault request with a device key kept next to its config, so the same
81
+ token on another machine gets nothing (`npx owlmem device` prints this machine's fingerprint). Viewing a secret, saving, deleting
82
+ and exporting always need approval.
83
+ If an agent without access asks for an item, the answer names up to three agents in the space with a standing grant,
81
84
  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
85
  the result. Secrets themselves never go into messages, documents or memory — OwlMem refuses text that looks like a key.
83
86
 
package/owlmem.mjs CHANGED
@@ -19,18 +19,19 @@
19
19
  // owlmem vault export [--format bitwarden-json|keepass-csv|env] > file
20
20
  // owlmem run --env VAR=<name>[:field] [...] -- <command> [args] run a command with secrets in its environment
21
21
  // owlmem vault keygen (setup without a browser) print a new recovery code and its key check
22
- // owlmem device register this machine and show its fingerprint (for standing grants)
22
+ // owlmem device show this machine's fingerprint (standing grants are bound to machines)
23
23
  // owlmem mcp local MCP server for your AI (vault_list, vault_run, vault_get, vault_put)
24
24
  // e.g. claude mcp add owlmem-vault -- node ~/owlmem.mjs mcp
25
25
  //
26
26
  // Reading, writing, deleting and exporting need the space owner's approval in the console; this program waits
27
27
  // for it (up to 5 minutes). Secrets go to stdout only; messages go to stderr.
28
28
  //
29
- // Standing grants (for unattended jobs): the owner can let one agent, on one machine, read chosen items without
30
- // asking each time — until a set date, optionally only during set hours. This program signs every vault request
31
- // with a device key kept on this machine (device.json next to config.json), and says whether the secret goes into a
32
- // command (`owlmem run`, vault_run) or is shown (`vault get`, vault_get). A token copied to another machine has no
33
- // device key there, so standing grants don't apply. Writing, deleting and exporting always need approval.
29
+ // Standing grants (for unattended jobs): the owner turns one on for an agent in the console (Agents), or while approving
30
+ // one of its requests. The agent can then use any vault item without asking, on computers the owner approved (a new
31
+ // computer asks once; approving remembers it), and only through `owlmem run` / vault_run. This program signs every vault
32
+ // request with a device key kept on this machine (device.json next to config.json), and says whether the secret goes into
33
+ // a command (`owlmem run`, vault_run) or is shown (`vault get`, vault_get). A token copied to another machine has no
34
+ // device key there, so standing grants don't apply. Viewing, writing, deleting and exporting always need approval.
34
35
 
35
36
  import { webcrypto as crypto } from "node:crypto";
36
37
  import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync, rmSync, statSync, realpathSync, openSync, closeSync, appendFileSync } from "node:fs";
@@ -39,7 +40,7 @@ import { join, dirname, basename, relative, isAbsolute } from "node:path";
39
40
  import { fileURLToPath } from "node:url";
40
41
  import { spawn, spawnSync } from "node:child_process";
41
42
 
42
- export const VERSION = "0.7.3";
43
+ export const VERSION = "0.7.6";
43
44
  const BASE = (process.env.OWLMEM_URL || "https://owlmem.com").replace(/\/$/, "");
44
45
  const DIR = process.env.OWLMEM_HOME || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "owlmem");
45
46
  const enc = new TextEncoder(), dec = new TextDecoder();
@@ -232,7 +233,7 @@ async function approved(method, path, body, kind = "access", mode = "other") {
232
233
  if (r.status < 400) return r.data;
233
234
  if (r.data.error !== "approval_required") throw new Fail(r.data.detail || r.data.error || "HTTP " + r.status);
234
235
  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
+ // the server may name other agents that could do this for us (they have a standing grant) — names only
236
237
  let hint = r.data.hint || "";
237
238
  say(`Waiting for approval in the OwlMem console (request ${id}${kind === "access" ? "" : ", " + kind}) …`);
238
239
  if (hint) say(hint);
@@ -245,7 +246,7 @@ async function approved(method, path, body, kind = "access", mode = "other") {
245
246
  if (st !== "pending") break;
246
247
  }
247
248
  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 === "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 to turn on your standing grant (console → Agents, or when they approve).` + tail);
249
250
  if (st !== "approved") throw new Fail((st === "denied" ? "The request was denied." : "The request expired before it was approved.") + tail);
250
251
  say("Approved.");
251
252
  if (kind !== "access") extra = (path.includes("?") ? "&" : "?") + "request=" + encodeURIComponent(id);
@@ -430,7 +431,7 @@ function pick(body, field) {
430
431
  const MCP_TOOLS = [
431
432
  { name: "vault_list", description: "List the credentials in the team vault (names and descriptions only; no secrets).", inputSchema: { type: "object", properties: {} } },
432
433
  { name: "vault_run",
433
- description: "PREFERRED way to use a secret: run a shell command with vault items injected as environment variables. The secret values are masked as **** in the output returned to you, so they never enter the conversation. Needs the space owner's approval in the OwlMem console the first time (then 15 min – 2 h), unless they set a standing grant for you on this machine — standing grants cover vault_run only (not vault_get) unless the owner also allowed viewing.",
434
+ description: "PREFERRED way to use a secret: run a shell command with vault items injected as environment variables. The secret values are masked as **** in the output returned to you, so they never enter the conversation. Needs the space owner's approval in the OwlMem console the first time (then 15 min – 2 h), unless they turned on your standing grant and approved this computer — standing grants cover vault_run only, never vault_get.",
434
435
  inputSchema: { type: "object", properties: {
435
436
  command: { type: "string", description: "Shell command to run (sh -c)" },
436
437
  env: { type: "object", description: 'Map of ENV_VAR to "item" or "item:field", e.g. {"DB_PASSWORD": "prod/db:password"}', additionalProperties: { type: "string" } },
@@ -806,14 +807,22 @@ export const WAKE_CLIENTS = {
806
807
  command: ["claude", "-p", "--allowedTools", "mcp__owlmem__kb_task_update,mcp__owlmem__kb_inbox"], prompt: "stdin" },
807
808
  codex: { name: "Codex", bin: "codex", command: ["codex", "exec", "--skip-git-repo-check", "-"], prompt: "stdin" },
808
809
  // 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" },
810
+ antigravity: { name: "Antigravity", bin: "agy", home: [[".gemini", "bin", "agy"]], command: ["agy", "-p"], prompt: "arg" },
811
+ // hermes -z runs one prompt (an argument) and exits; it calls MCP tools without asking unless OwlMem is marked untrusted (see allowHermes).
812
+ // Its installer puts hermes under ~/.hermes/hermes-agent or ~/.local/bin; a system-wide service install under /var/lib/hermes
813
+ hermes: { name: "Hermes", bin: "hermes", command: ["hermes", "-z"], prompt: "arg",
814
+ home: [[".hermes", "hermes-agent", ".hermes", "bin", "hermes"], [".local", "bin", "hermes"], "/var/lib/hermes/hermes-agent/.hermes/bin/hermes"] },
810
815
  };
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;
816
+ export const WAKE_APP_IDS = ["claude-code", "codex", "antigravity", "hermes", "gemini", "custom"];
817
+ // Where an app's command is: on PATH, or the first of its default install places that exists (path parts under your home
818
+ // folder, or an absolute path), used by full path
819
+ export function wakeBin(c, home_ = home(), exists = existsSync, path = onPath) {
820
+ if (path(c.bin)) return c.bin;
821
+ for (const w of c.home || []) {
822
+ const p = typeof w === "string" ? w : join(home_, ...w);
823
+ if (exists(p)) return p;
824
+ }
825
+ return null;
817
826
  }
818
827
  // codex exec runs with approval "never", so an MCP call that needs approval is refused: approve just the two OwlMem
819
828
  // tools it needs to report back, as tool tables under the [mcp_servers.owlmem] that install wrote (append only)
@@ -840,6 +849,56 @@ export function allowAntigravity(home_ = home()) {
840
849
  writeFileSync(f, JSON.stringify(j, null, 2) + "\n");
841
850
  return true;
842
851
  }
852
+ // 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 the two OwlMem tools it reports back
854
+ // with to the top-level command_allowlist as mcp:<server>/<tool> (what Hermes itself writes for "Always"). Adds lines only,
855
+ // never rewrites what's there; a command_allowlist it can't extend safely (not a plain block list) returns "manual".
856
+ // false: nothing to do (no config, no OwlMem server in it, a trusted one, or the entries are already there).
857
+ export function allowHermes(home_ = home(), env = process.env) {
858
+ const f = join(env.HERMES_HOME || join(home_, ".hermes"), "config.yaml");
859
+ let t; try { t = readFileSync(f, "utf8"); } catch { return false; }
860
+ const lines = t.split(/\r?\n/), ind = (l) => l.match(/^ */)[0].length, live = (l) => !!l.trim() && !l.trim().startsWith("#");
861
+ const top = lines.findIndex((l) => /^mcp_servers:\s*(#.*)?$/.test(l));
862
+ if (top < 0) return false;
863
+ let end = top + 1;
864
+ while (end < lines.length && (!live(lines[end]) || ind(lines[end]) > 0)) end++;
865
+ const kids = [];
866
+ let step = 0;
867
+ for (let i = top + 1; i < end; i++) if (live(lines[i])) { step ||= ind(lines[i]); if (ind(lines[i]) === step) kids.push(i); }
868
+ let server = null, trust = "full";
869
+ for (const [k, i] of kids.entries()) { // the OwlMem entry: named owlmem, or its url is ours
870
+ const m = lines[i].trim().match(/^["']?([A-Za-z0-9_.-]+)["']?:\s*(#.*)?$/);
871
+ if (!m) continue;
872
+ const body = lines.slice(i + 1, k + 1 < kids.length ? kids[k + 1] : end), first = body.find(live);
873
+ if (m[1] !== "owlmem" && !body.some((l) => l.includes("owlmem.com/mcp"))) continue;
874
+ server = m[1];
875
+ const tr = first && body.find((l) => ind(l) === ind(first) && /^\s*trust:/.test(l));
876
+ if (tr) trust = tr.replace(/#.*/, "").slice(tr.indexOf(":") + 1).trim().replace(/^["']|["']$/g, "");
877
+ break;
878
+ }
879
+ if (!server || trust === "full") return false;
880
+ const need = ["kb_task_update", "kb_inbox"].map((n) => "mcp:" + server + "/" + n);
881
+ const at = lines.findIndex((l) => /^command_allowlist:/.test(l));
882
+ if (at < 0) {
883
+ writeFileSync(f, t.replace(/\n*$/, "\n") + "# OwlMem auto-wake: unattended hermes -z may call these two OwlMem tools to report back\ncommand_allowlist:\n" +
884
+ need.map((r) => " - " + r + "\n").join(""));
885
+ return true;
886
+ }
887
+ if (!/^command_allowlist:\s*(#.*)?$/.test(lines[at])) return "manual"; // [a, b] on one line, or a string: leave it to the person
888
+ let last = at, pad = null;
889
+ for (let i = at + 1; i < lines.length && (!live(lines[i]) || ind(lines[i]) > 0 || lines[i].startsWith("- ")); i++) {
890
+ if (!live(lines[i])) continue;
891
+ if (!/^\s*- /.test(lines[i])) return "manual";
892
+ pad ??= lines[i].match(/^\s*/)[0];
893
+ last = i;
894
+ }
895
+ const have = lines.slice(at + 1, last + 1).map((l) => l.replace(/\s#.*/, "").trim().replace(/^-\s*/, "").replace(/^["']|["']$/g, ""));
896
+ const add = need.filter((r) => !have.includes(r));
897
+ if (!add.length) return false;
898
+ lines.splice(last + 1, 0, ...add.map((r) => (pad ?? " ") + "- " + r));
899
+ writeFileSync(f, lines.join("\n"));
900
+ return true;
901
+ }
843
902
  export const WAKE_DEFAULTS = { max_parallel: 2, timeout_min: 30 };
844
903
  const WAKE_UNIT = "owlmem-wake.service", WAKE_LABEL = "com.owlmem.wake", WAKE_TASK = "OwlMem Wake", WAKE_RUNKEY = "OwlMemWake";
845
904
  const wakeDir = (dir = DIR) => join(dir, "wake");
@@ -1158,6 +1217,15 @@ async function wakeRun() {
1158
1217
  if (out === "revoked") process.exit(0);
1159
1218
  }
1160
1219
 
1220
+ // No command line auto-wake can start: tell OwlMem support which AI apps this computer has, so it can add them. Only the
1221
+ // system, CPU architecture, node version and app names (what install detects) — no paths, user name or computer name.
1222
+ export async function wakeNoClients(api, items = null) {
1223
+ let apps = [];
1224
+ try { apps = (items || plan()).filter((r) => r.state !== "missing").map((r) => r.name); } catch {}
1225
+ const r = await api("feedback", { os: platform(), arch: process.arch, node: process.versions.node, apps: [...new Set(apps)] });
1226
+ return r.status === 200;
1227
+ }
1228
+
1161
1229
  async function wakeEnable(args = []) {
1162
1230
  const boot = args.includes("--boot"), again = args.includes("--again");
1163
1231
  const num = (name, lo, hi) => { const i = args.indexOf(name); if (i < 0) return undefined; const n = Number(args[i + 1]); if (!Number.isInteger(n) || n < lo || n > hi) throw new Fail(`${name} takes a whole number from ${lo} to ${hi}.`); return n; };
@@ -1168,10 +1236,18 @@ async function wakeEnable(args = []) {
1168
1236
  const bin = !clients[id] && wakeBin(c);
1169
1237
  if (bin) clients[id] = { command: [bin, ...c.command.slice(1)], prompt: c.prompt };
1170
1238
  }
1171
- if (!Object.keys(clients).length)
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\".");
1239
+ if (!Object.keys(clients).length) {
1240
+ const told = await wakeNoClients(wakeApi()).catch(() => false);
1241
+ 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
+ (told ? "\nOwlMem support has been told; you'll hear from us once your AI app is supported." : ""));
1243
+ }
1173
1244
  if (clients.codex && allowCodex()) say("Let Codex report back to OwlMem when nobody is watching (2 tool approvals in ~/.codex/config.toml).");
1174
1245
  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).");
1246
+ if (clients.hermes) {
1247
+ const h = allowHermes();
1248
+ if (h === true) say("Let Hermes report back to OwlMem when nobody is watching (2 entries under command_allowlist in ~/.hermes/config.yaml).");
1249
+ else if (h === "manual") say("Hermes treats OwlMem as untrusted: add mcp:<server>/kb_task_update and mcp:<server>/kb_inbox to command_allowlist in ~/.hermes/config.yaml, or it can't report back when nobody is watching.");
1250
+ }
1175
1251
  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 };
1176
1252
  // OwlMem knows these app names; a command you add yourself to wake.json goes under "gemini" or "custom"
1177
1253
  const r = await wakeApi()("pair", { clients: Object.keys(clients).filter((c) => WAKE_APP_IDS.includes(c)), force: again });
@@ -1184,7 +1260,8 @@ async function wakeEnable(args = []) {
1184
1260
  say("Auto-wake is installed (" + a.kind + ": " + tilde(a.file) + ").");
1185
1261
  say("Apps it can start here: " + Object.keys(clients).map((c) => WAKE_CLIENTS[c]?.name || c).join(", ") + ". At most " + cfg.max_parallel + " at once, " + cfg.timeout_min + " minutes per task.");
1186
1262
  if (r.data.status === "pending") {
1187
- say("\nApprove this computer in the OwlMem console and pick which AI each app is:\n\n " + r.data.url + "\n\nCheck that the console shows this code: " + r.data.code + "\nUntil then auto-wake waits and starts nothing.");
1263
+ say("\nApprove this computer in the OwlMem console and pick which AI each app is:\n\n " + r.data.url + "\n\nCheck that the console shows this code: " + r.data.code + "\nUntil then auto-wake waits and starts nothing." +
1264
+ "\n(If you're an AI agent running this: send the link and the code to the person who turned auto-wake on, with kb_send.)");
1188
1265
  openBrowser(r.data.url);
1189
1266
  } else say("This computer is already paired (" + r.data.code + "). Turn auto-wake on or off for each AI in the console (AI agents → Agents).");
1190
1267
  }
@@ -1218,7 +1295,7 @@ async function wakeStatus() {
1218
1295
  const WAKE_HELP = `Usage: npx -y owlmem wake <enable|disable|status>
1219
1296
 
1220
1297
  Auto-wake starts the AI on this computer when it gets an OwlMem task (Claude Code with \`claude -p\`, Codex with
1221
- \`codex exec\`), without you opening it. It runs in the background from login, for this user only.
1298
+ \`codex exec\`, Antigravity with \`agy -p\`, Hermes with \`hermes -z\`), without you opening it. It runs in the background from login, for this user only.
1222
1299
 
1223
1300
  enable [--boot] [--max N] [--timeout MIN] [--again]
1224
1301
  install it and pair this computer (approve in the console). --boot (Linux): run from boot,
@@ -1262,12 +1339,12 @@ export async function main(argv) {
1262
1339
  if (cmd === "uninstall") { await install(["--uninstall", ...argv.slice(1)]); return; }
1263
1340
  if (cmd === "mcp") { await mcpServe(); return; }
1264
1341
  if (cmd === "wake") { await wakeCmd(sub, rest); return; }
1265
- if (cmd === "device") { // register this machine for standing grants and show its fingerprint
1342
+ if (cmd === "device") { // show this machine's fingerprint (the console lists approved computers with it)
1266
1343
  const r = await call("POST", "device", {});
1267
1344
  if (r.status >= 400) throw new Fail(r.data.detail || r.data.error || "HTTP " + r.status);
1268
1345
  say("This machine: " + (process.env.OWLMEM_DEVICE_NAME || hostname()));
1269
1346
  console.log(r.data.device);
1270
- say("Registered for " + r.data.agent + ". In the OwlMem console (Vault → Standing grants) pick the machine with this fingerprint.");
1347
+ say("This computer's fingerprint for " + r.data.agent + ". With a standing grant on, the first use here asks once; approving remembers this computer.");
1271
1348
  return;
1272
1349
  }
1273
1350
  if (cmd === "login") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "owlmem",
3
- "version": "0.7.3",
3
+ "version": "0.7.6",
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" },