owlmem 0.6.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 +38 -1
  2. package/owlmem.mjs +477 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,11 +19,45 @@ changes is backed up first (`<file>.bak-owlmem-<time>`). It also adds a short "T
19
19
  | `--yes` | don't ask (when stdin isn't a terminal — an AI running it — it never asks) |
20
20
  | `--only claude-code,cursor` | only these apps (`claude-code`, `codex`, `cursor`, `vscode`, `vscode-insiders`, `windsurf`, `gemini`, `claude-desktop`) |
21
21
  | `--no-rules` | leave the instruction files alone |
22
- | `--uninstall` | remove the OwlMem entries and rules block again |
22
+ | `--wake` / `--no-wake` | turn on auto-wake (below) without asking / don't ask about it |
23
+ | `--uninstall` | remove the OwlMem entries and rules block again, and auto-wake |
23
24
 
24
25
  Signing in is done by each app (standard MCP OAuth): the first time it uses OwlMem it opens the owlmem.com
25
26
  page, where you sign in and click **Allow**. AI agents can read the setup guide at https://mcp.owlmem.com.
26
27
 
28
+ ## Auto-wake
29
+
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
+ 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` (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
+
34
+ ```sh
35
+ npx -y owlmem install --wake # or answer "y" when install asks; --yes alone leaves it off
36
+ npx -y owlmem wake enable # on its own (options: --boot, --max N, --timeout MIN, --again)
37
+ npx -y owlmem wake status
38
+ npx -y owlmem wake disable # also done by npx -y owlmem uninstall
39
+ ```
40
+
41
+ - **Pairing.** `wake enable` prints a link: open it, check the code matches, and pick which AI each app on this
42
+ computer is. Until then nothing starts. The computer is identified by its device key (next to `config.json`).
43
+ - **Runs as you.** A systemd user service (Linux), a LaunchAgent (macOS) or a logon task (Windows) — this user
44
+ only, no admin rights. `--boot` (Linux) turns on lingering so it runs from boot, not just from login.
45
+ - **One program per computer** for every paired AI. One task at a time per AI, at most 2 at once (`--max`),
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. 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.
49
+ - **Working folder.** A line like `cwd: ~/Projects/app` in the task picks the folder. In a Git repository the AI
50
+ gets its own worktree and branch (`owlmem/task-<id>`), commits there and doesn't merge — the result names the
51
+ branch. Other folders take one task at a time; with no folder it works in an empty one.
52
+ - **Safety.** The AI uses its own OwlMem connection and the permissions it always has: no `--dangerously-*` flags.
53
+ Claude Code is only additionally allowed the two OwlMem tools it needs to report back (`kb_task_update`,
54
+ `kb_inbox`). Anything that needs a person — an approval, a sign-in, a code — it declines, saying what to do.
55
+ It is told never to put secrets in its result.
56
+ - **Turning it off.** Each AI has an auto-wake switch in the console (AI agents → Agents), where you can also remove
57
+ the computer. When the computer is removed, the AI is disconnected or the account is gone, the program gets a 401,
58
+ removes its own autostart entry and exits. The command for each app is in `~/.config/owlmem/wake/wake.json`;
59
+ logs and worktrees stay in that folder after uninstalling.
60
+
27
61
  ## Vault
28
62
 
29
63
  ```sh
@@ -43,6 +77,9 @@ Run `npx owlmem device` on that machine to register it and print its fingerprint
43
77
  device key kept next to its config, so the same token on another machine gets nothing. By default a standing grant covers
44
78
  `owlmem run` / `vault_run` only (the secret goes into a command); viewing in plain text must be allowed separately.
45
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.
46
83
 
47
84
  Let your AI use the vault directly through a local MCP server:
48
85
 
package/owlmem.mjs CHANGED
@@ -5,9 +5,10 @@
5
5
  // server only ever sees ciphertext. Your space key comes from the recovery code shown once when the vault
6
6
  // was set up in the console; `owlmem vault unlock` stores it on this machine (0600).
7
7
  //
8
- // owlmem install [--dry-run] [--yes] [--only a,b] [--no-rules] [--uninstall]
8
+ // owlmem install [--dry-run] [--yes] [--only a,b] [--no-rules] [--wake] [--uninstall]
9
9
  // connect Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI and
10
10
  // Claude Desktop on this computer to OwlMem (npx -y owlmem install)
11
+ // owlmem wake enable|disable|status auto-wake: start the AI on this computer when it gets a task (runs from login)
11
12
  // owlmem login save this agent's token (from the console)
12
13
  // owlmem vault unlock paste the recovery code once per machine
13
14
  // owlmem vault ls
@@ -32,12 +33,13 @@
32
33
  // device key there, so standing grants don't apply. Writing, deleting and exporting always need approval.
33
34
 
34
35
  import { webcrypto as crypto } from "node:crypto";
35
- import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync } from "node:fs";
36
- import { homedir, hostname } from "node:os";
37
- import { join, dirname } from "node:path";
36
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync, rmSync, statSync, realpathSync, openSync, closeSync, appendFileSync } from "node:fs";
37
+ import { homedir, hostname, userInfo } from "node:os";
38
+ import { join, dirname, basename, relative, isAbsolute } from "node:path";
39
+ import { fileURLToPath } from "node:url";
38
40
  import { spawn, spawnSync } from "node:child_process";
39
41
 
40
- export const VERSION = "0.6.0";
42
+ export const VERSION = "0.7.2";
41
43
  const BASE = (process.env.OWLMEM_URL || "https://owlmem.com").replace(/\/$/, "");
42
44
  const DIR = process.env.OWLMEM_HOME || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "owlmem");
43
45
  const enc = new TextEncoder(), dec = new TextDecoder();
@@ -200,10 +202,11 @@ export async function deviceFingerprint(spki) {
200
202
  return h.match(/.{4}/g).join("-");
201
203
  }
202
204
  // mode: "run" = the secret goes into a command; "view" = it is shown (stdout / the AI's conversation); "other" = anything else
203
- async function signHeaders(method, path, mode) {
205
+ // url: the full request URL (only its path is signed)
206
+ async function signHeaders(method, url, mode) {
204
207
  const d = await device();
205
208
  const ts = String(Math.floor(Date.now() / 1000)), nonce = b64url(crypto.getRandomValues(new Uint8Array(18)));
206
- const msg = ["owlmem-device-v1", method.toUpperCase(), new URL(BASE + "/v1/vault/" + path).pathname, mode, ts, nonce].join("\n");
209
+ const msg = ["owlmem-device-v1", method.toUpperCase(), new URL(url).pathname, mode, ts, nonce].join("\n");
207
210
  const sig = new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, d.key, enc.encode(msg)));
208
211
  return { "X-OwlMem-Device": d.spki, "X-OwlMem-Device-Name": (process.env.OWLMEM_DEVICE_NAME || hostname()).replace(/[^\w .@()-]/g, "").slice(0, 60),
209
212
  "X-OwlMem-Mode": mode, "X-OwlMem-Time": ts, "X-OwlMem-Nonce": nonce, "X-OwlMem-Sig": b64url(sig) };
@@ -213,7 +216,7 @@ async function signHeaders(method, path, mode) {
213
216
  async function call(method, path, body, mode = "other") {
214
217
  let r;
215
218
  try {
216
- const sh = await signHeaders(method, path, mode).catch(() => ({})); // can't write the device key: no standing grants, everything else still works
219
+ const sh = await signHeaders(method, BASE + "/v1/vault/" + path, mode).catch(() => ({})); // can't write the device key: no standing grants, everything else still works
217
220
  r = await fetch(BASE + "/v1/vault/" + path, { method, headers: { Authorization: "Bearer " + token(), "Content-Type": "application/json", "User-Agent": "owlmem-cli/" + VERSION, ...sh },
218
221
  body: body === undefined ? undefined : JSON.stringify(body) });
219
222
  } catch (e) { throw new Fail("Can't reach OwlMem (" + BASE + "): " + e.message); }
@@ -229,16 +232,21 @@ async function approved(method, path, body, kind = "access", mode = "other") {
229
232
  if (r.status < 400) return r.data;
230
233
  if (r.data.error !== "approval_required") throw new Fail(r.data.detail || r.data.error || "HTTP " + r.status);
231
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 || "";
232
237
  say(`Waiting for approval in the OwlMem console (request ${id}${kind === "access" ? "" : ", " + kind}) …`);
238
+ if (hint) say(hint);
233
239
  const until = Date.now() + WAIT_MS;
234
240
  let st = "pending";
235
241
  while (Date.now() < until) {
236
242
  await new Promise((res) => setTimeout(res, Number(process.env.OWLMEM_POLL_MS || 3000)));
237
- 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;
238
245
  if (st !== "pending") break;
239
246
  }
240
- 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).`);
241
- 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);
242
250
  say("Approved.");
243
251
  if (kind !== "access") extra = (path.includes("?") ? "&" : "?") + "request=" + encodeURIComponent(id);
244
252
  }
@@ -703,13 +711,15 @@ const LOGIN = {
703
711
  };
704
712
 
705
713
  async function install(args) {
706
- const opts = { dryRun: false, yes: false, noRules: false, uninstall: false, only: null };
714
+ const opts = { dryRun: false, yes: false, noRules: false, uninstall: false, only: null, wake: null };
707
715
  for (let i = 0; i < args.length; i++) {
708
716
  const a = args[i];
709
717
  if (a === "--dry-run") opts.dryRun = true;
710
718
  else if (a === "--yes" || a === "-y") opts.yes = true;
711
719
  else if (a === "--no-rules") opts.noRules = true;
712
720
  else if (a === "--uninstall") opts.uninstall = true;
721
+ else if (a === "--wake") opts.wake = true;
722
+ else if (a === "--no-wake") opts.wake = false;
713
723
  else if (a === "--only" || a.startsWith("--only=")) {
714
724
  const v = a.includes("=") ? a.slice(7) : args[++i];
715
725
  opts.only = String(v || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -729,6 +739,7 @@ async function install(args) {
729
739
  out(items.some((r) => r.state === "ok") || opts.uninstall ? "Nothing to change." : "No supported AI apps found. Manual setup for every app: https://owlmem.com/docs");
730
740
  if (!opts.uninstall && items.some((r) => r.state === "ok")) nextSteps(items, out);
731
741
  process.exitCode = items.some((r) => r.state === "fail") ? 1 : 0;
742
+ if (!opts.dryRun) await installWake(opts, items, out);
732
743
  return;
733
744
  }
734
745
  if (opts.dryRun) { out(`Dry run: ${changes.length} app(s) would change. Run again without --dry-run to apply.`); return; }
@@ -750,9 +761,24 @@ async function install(args) {
750
761
  if (ans.startsWith("y")) await new Promise((res) => spawn("codex", ["mcp", "login", "owlmem"], { stdio: "inherit", shell: process.platform === "win32" }).on("exit", res).on("error", res));
751
762
  }
752
763
  }
764
+ await installWake(opts, items, out);
753
765
  if (items.some((r) => r.state === "fail")) process.exitCode = 1;
754
766
  }
755
767
 
768
+ // `owlmem install` asks once whether to turn on auto-wake (--wake: yes without asking; --yes alone: no). `uninstall` removes it.
769
+ async function installWake(opts, items, out) {
770
+ try {
771
+ if (opts.uninstall) { await wakeDisable(true); return; }
772
+ if (opts.wake === false || loadWake()) return;
773
+ if (!items.some((r) => (r.id === "claude-code" || r.id === "codex") && ["ok", "add", "update"].includes(r.state))) { if (opts.wake) out("Auto-wake: needs Claude Code or Codex on this computer."); return; }
774
+ let on = opts.wake === true;
775
+ if (!on && !opts.yes && process.stdin.isTTY && process.stdout.isTTY)
776
+ on = (await ask("\nStart your AI automatically when it gets a task (auto-wake, runs in the background)? [y/N] ", false)).toLowerCase().startsWith("y");
777
+ if (!on) { if (!opts.yes) out("Auto-wake is off. Turn it on any time: npx -y owlmem wake enable"); return; }
778
+ out(); await wakeEnable([]);
779
+ } catch (e) { out("Auto-wake: " + (e instanceof Fail ? e.message : e?.message || e)); process.exitCode = 1; }
780
+ }
781
+
756
782
  function nextSteps(items, out) {
757
783
  const live = items.filter((r) => ["ok", "add", "update"].includes(r.state));
758
784
  if (!live.length) return;
@@ -765,6 +791,441 @@ function nextSteps(items, out) {
765
791
  out("Already-running sessions don't see new MCP servers: start a new session, then ask your AI to run kb_search.");
766
792
  }
767
793
 
794
+ // ── auto-wake: start the AI on this computer when it gets a task ──
795
+ // One background program per user per computer (`owlmem wake run`, started at login by systemd --user / a LaunchAgent / a
796
+ // logon task) long-polls OwlMem. When an agent paired with this computer gets a task, it runs that agent's command line
797
+ // unattended (`claude -p`, `codex exec`) with the task as the prompt. The woken AI uses its own OwlMem connection (the one
798
+ // `owlmem install` set up), so it acts as itself. It gets no extra permissions: no --dangerously-* flags; anything that
799
+ // needs a person, it declines with what the person has to do.
800
+ // This computer is identified by its device key (device.json); the pairing is approved in the console, where each AI has an
801
+ // auto-wake switch and the computer can be removed. If OwlMem answers 401 (computer removed, AI disconnected, account gone)
802
+ // the program removes its own autostart entry and exits.
803
+ export const WAKE_CLIENTS = {
804
+ "claude-code": { name: "Claude Code", bin: "claude",
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
806
+ command: ["claude", "-p", "--allowedTools", "mcp__owlmem__kb_task_update,mcp__owlmem__kb_inbox"], prompt: "stdin" },
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" },
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
+ }
831
+ export const WAKE_DEFAULTS = { max_parallel: 2, timeout_min: 30 };
832
+ const WAKE_UNIT = "owlmem-wake.service", WAKE_LABEL = "com.owlmem.wake", WAKE_TASK = "OwlMem Wake", WAKE_RUNKEY = "OwlMemWake";
833
+ const wakeDir = (dir = DIR) => join(dir, "wake");
834
+ const wakeFile = (dir = DIR) => join(wakeDir(dir), "wake.json");
835
+ export function loadWake(dir = DIR) { try { return JSON.parse(readFileSync(wakeFile(dir), "utf8")); } catch { return null; } }
836
+ function saveWake(dir, c) { saveFile(wakeFile(dir), JSON.stringify(c, null, 2) + "\n"); }
837
+
838
+ // Where the autostart entry goes and what it says. o: {platform, home, dir, node, script, path, uid, user, boot}
839
+ const xml = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
840
+ const unitq = (s) => '"' + String(s).replace(/%/g, "%%").replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
841
+ export function autostart(o) {
842
+ const p = o.platform, args = [o.node, o.script, "wake", "run"];
843
+ if (p === "linux") {
844
+ const file = join(o.xdg || join(o.home, ".config"), "systemd", "user", WAKE_UNIT);
845
+ const text = `[Unit]
846
+ Description=OwlMem auto-wake (starts your AI when it gets a task)
847
+ Documentation=https://owlmem.com/docs#wake
848
+ After=network-online.target
849
+
850
+ [Service]
851
+ Type=simple
852
+ ExecStart=${args.map(unitq).join(" ")}
853
+ Environment=${unitq("OWLMEM_HOME=" + o.dir)}
854
+ Environment=${unitq("PATH=" + o.path)}
855
+ Restart=on-failure
856
+ RestartSec=30
857
+
858
+ [Install]
859
+ WantedBy=default.target
860
+ `;
861
+ const sc = (...a) => ["systemctl", "--user", ...a];
862
+ return { kind: "systemd", file, files: [{ file, text }],
863
+ start: [{ argv: sc("daemon-reload") }, { argv: sc("enable", "--now", WAKE_UNIT) },
864
+ ...(o.boot ? [{ argv: ["loginctl", "enable-linger", o.user], ignoreFail: true, note: "boot" }] : [])],
865
+ stop: [{ argv: sc("disable", "--now", WAKE_UNIT), ignoreFail: true }],
866
+ selfStop: [{ argv: sc("disable", WAKE_UNIT), ignoreFail: true }], // from inside the service: don't stop ourselves mid-cleanup
867
+ after: [{ argv: sc("daemon-reload"), ignoreFail: true }] };
868
+ }
869
+ if (p === "darwin") {
870
+ const file = join(o.home, "Library", "LaunchAgents", WAKE_LABEL + ".plist");
871
+ const log = join(wakeDir(o.dir), "wake.out.log");
872
+ const text = `<?xml version="1.0" encoding="UTF-8"?>
873
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
874
+ <plist version="1.0">
875
+ <dict>
876
+ <key>Label</key><string>${WAKE_LABEL}</string>
877
+ <key>ProgramArguments</key>
878
+ <array>${args.map((a) => `<string>${xml(a)}</string>`).join("")}</array>
879
+ <key>EnvironmentVariables</key>
880
+ <dict><key>OWLMEM_HOME</key><string>${xml(o.dir)}</string><key>PATH</key><string>${xml(o.path)}</string></dict>
881
+ <key>RunAtLoad</key><true/>
882
+ <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
883
+ <key>ThrottleInterval</key><integer>30</integer>
884
+ <key>StandardOutPath</key><string>${xml(log)}</string>
885
+ <key>StandardErrorPath</key><string>${xml(log)}</string>
886
+ </dict>
887
+ </plist>
888
+ `;
889
+ return { kind: "launchd", file, files: [{ file, text }],
890
+ start: [{ argv: ["launchctl", "bootstrap", "gui/" + o.uid, file], fallback: { argv: ["launchctl", "load", "-w", file] } }],
891
+ stop: [{ argv: ["launchctl", "bootout", "gui/" + o.uid + "/" + WAKE_LABEL], ignoreFail: true }],
892
+ selfStop: [], after: [] };
893
+ }
894
+ if (p === "win32") {
895
+ const vbs = join(wakeDir(o.dir), "owlmem-wake.vbs");
896
+ const vq = (s) => '""' + String(s).replace(/"/g, '""""') + '""';
897
+ const text = `' OwlMem auto-wake: runs "owlmem wake run" without a console window (started at logon)\r
898
+ Set sh = CreateObject("WScript.Shell")\r
899
+ sh.Environment("PROCESS")("OWLMEM_HOME") = "${String(o.dir).replace(/"/g, '""')}"\r
900
+ sh.Run "${args.map(vq).join(" ")}", 0, False\r
901
+ `;
902
+ const tr = `wscript.exe "${vbs}"`;
903
+ const runKey = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
904
+ return { kind: "schtasks", file: vbs, files: [{ file: vbs, text }],
905
+ start: [{ argv: ["schtasks", "/Create", "/TN", WAKE_TASK, "/SC", "ONLOGON", "/TR", tr, "/RL", "LIMITED", "/F"],
906
+ fallback: { argv: ["reg", "add", runKey, "/v", WAKE_RUNKEY, "/t", "REG_SZ", "/d", tr, "/f"] } },
907
+ { argv: ["wscript.exe", vbs], ignoreFail: true, detach: true }],
908
+ stop: [{ argv: ["schtasks", "/End", "/TN", WAKE_TASK], ignoreFail: true }, { argv: ["schtasks", "/Delete", "/TN", WAKE_TASK, "/F"], ignoreFail: true },
909
+ { argv: ["reg", "delete", runKey, "/v", WAKE_RUNKEY, "/f"], ignoreFail: true }],
910
+ selfStop: [{ argv: ["schtasks", "/Delete", "/TN", WAKE_TASK, "/F"], ignoreFail: true }, { argv: ["reg", "delete", runKey, "/v", WAKE_RUNKEY, "/f"], ignoreFail: true }],
911
+ after: [] };
912
+ }
913
+ throw new Fail("Auto-wake isn't supported on " + p + ".");
914
+ }
915
+ // The autostart entry outlives this run: under Nix (and similar) process.execPath is a versioned store path that
916
+ // disappears after the next system upgrade, so prefer a stable path that resolves to the same node binary.
917
+ export function stableNode(execPath = process.execPath, candidates = ["/run/current-system/sw/bin/node", join(home(), ".nix-profile", "bin", "node"), "/usr/local/bin/node", "/usr/bin/node", onPath("node")]) {
918
+ let real; try { real = realpathSync(execPath); } catch { return execPath; }
919
+ for (const c of candidates) { try { if (c && c !== execPath && realpathSync(c) === real) return c; } catch {} }
920
+ return execPath;
921
+ }
922
+ function autostartOpts(dir = DIR, extra = {}) {
923
+ let uid = "", user = "";
924
+ try { const u = userInfo(); uid = String(u.uid); user = u.username; } catch {}
925
+ return { platform: platform(), home: home(), xdg: process.env.XDG_CONFIG_HOME, dir, node: stableNode(), script: join(wakeDir(dir), "owlmem.mjs"),
926
+ path: process.env.PATH || "", uid, user, ...extra };
927
+ }
928
+ // run(argv, op) → {ok, out}; op.detach: start it and don't wait (the Windows launcher)
929
+ function wakeRunCmd(argv, op = {}) {
930
+ if (!op.detach) return runCmd(argv);
931
+ try { spawn(argv[0], argv.slice(1), { stdio: "ignore", detached: true }).on("error", () => {}).unref(); return { ok: true, out: "" }; } catch (e) { return { ok: false, out: e.message }; }
932
+ }
933
+ function runOps(ops, run) {
934
+ const out = [];
935
+ for (const op of ops) {
936
+ let r = run(op.argv, op);
937
+ if (!r.ok && op.fallback) r = run(op.fallback.argv);
938
+ out.push({ argv: op.argv, ...r });
939
+ if (!r.ok && !op.ignoreFail) throw new Fail("`" + op.argv.join(" ") + "` failed: " + (r.out || "").split("\n").slice(-2).join(" "));
940
+ }
941
+ return out;
942
+ }
943
+ export function installAutostart(o, run = wakeRunCmd) {
944
+ const a = autostart(o);
945
+ for (const f of a.files) { mkdirSync(dirname(f.file), { recursive: true }); writeFileSync(f.file, f.text); }
946
+ return { ...a, ran: runOps(a.start, run) };
947
+ }
948
+ // Remove the autostart entry. self: called by the running program itself (after a 401), so it doesn't stop its own service first.
949
+ export function removeAutostart(o, run = wakeRunCmd, self = false) {
950
+ const a = autostart(o);
951
+ const ran = runOps(self ? a.selfStop : a.stop, run);
952
+ for (const f of a.files) rmSync(f.file, { force: true });
953
+ ran.push(...runOps(a.after, run));
954
+ return { ...a, ran };
955
+ }
956
+ // Everything local except the work itself: autostart entry, wake.json, the program copy. Worktrees and logs stay (they hold the AI's work).
957
+ export function wakeUninstall(o, run = wakeRunCmd, self = false) {
958
+ const r = removeAutostart(o, run, self);
959
+ for (const f of ["wake.json", "owlmem.mjs", "owlmem-wake.vbs"]) rmSync(join(wakeDir(o.dir), f), { force: true });
960
+ return r;
961
+ }
962
+
963
+ // ── a task's working directory ──
964
+ // A task can name one with a line like "cwd: ~/Projects/app" (also dir:, workdir:, 目录:). Git repository → a separate
965
+ // worktree on its own branch (owlmem/task-<id>), so two tasks in one project don't collide; the AI commits there and doesn't merge.
966
+ // Plain directory → one task at a time in it. No directory → an empty folder of its own (OwlMem-only work).
967
+ const DIR_RE = /^[ \t>*-]*(?:cwd|dir|directory|workdir|working directory|目录|工作目录)[ \t]*[::=][ \t]*`?([^`\r\n]+?)`?[ \t]*$/im;
968
+ export function taskDir(body, h = home()) {
969
+ const m = DIR_RE.exec(String(body || ""));
970
+ if (!m) return null;
971
+ let d = m[1].trim().replace(/^["']|["']$/g, "");
972
+ if (d === "~" || d.startsWith("~/") || d.startsWith("~\\")) d = join(h, d.slice(2));
973
+ return isAbsolute(d) ? d : null;
974
+ }
975
+ function gitRun(args) { const r = spawnSync("git", args, { encoding: "utf8", timeout: 60000 }); return { ok: r.status === 0, out: ((r.stdout || "") + (r.stderr || "")).trim() }; }
976
+ export function planWorkdir(task, h = home(), git = gitRun) {
977
+ const d = taskDir(task.body, h);
978
+ if (!d) return { kind: "scratch" };
979
+ let st = null; try { st = statSync(d); } catch {}
980
+ if (!st || !st.isDirectory()) return { kind: "missing", dir: d };
981
+ const real = realpathSync(d);
982
+ const top = git(["-C", real, "rev-parse", "--show-toplevel"]);
983
+ if (top.ok && top.out) return { kind: "worktree", dir: real, top: realpathSync(top.out.split("\n")[0].trim()) };
984
+ return { kind: "dir", dir: real, lock: real };
985
+ }
986
+ export function prepareWorkdir(task, plan, dir = DIR, git = gitRun) {
987
+ if (plan.kind === "missing") throw new Fail("the task's directory doesn't exist on this computer: " + plan.dir);
988
+ if (plan.kind === "scratch") { const cwd = join(wakeDir(dir), "scratch", "task-" + task.id); mkdirSync(cwd, { recursive: true }); return { cwd }; }
989
+ if (plan.kind === "dir") return { cwd: plan.dir };
990
+ const branch = "owlmem/task-" + task.id, wt = join(wakeDir(dir), "worktrees", basename(plan.top) + "-task-" + task.id);
991
+ if (!existsSync(wt)) {
992
+ mkdirSync(dirname(wt), { recursive: true });
993
+ let r = git(["-C", plan.top, "worktree", "add", "-b", branch, wt, "HEAD"]);
994
+ if (!r.ok && /already exists/i.test(r.out)) r = git(["-C", plan.top, "worktree", "add", wt, branch]);
995
+ if (!r.ok) throw new Fail("couldn't make a git worktree in " + plan.top + ": " + r.out.split("\n").slice(-1)[0]);
996
+ }
997
+ const rel = relative(plan.top, plan.dir);
998
+ return { cwd: rel ? join(wt, rel) : wt, worktree: wt, branch, repo: plan.top };
999
+ }
1000
+
1001
+ export function wakePrompt(task, place, o = {}) {
1002
+ const id = task.id, mins = o.timeout_min || WAKE_DEFAULTS.timeout_min;
1003
+ const where = place.worktree
1004
+ ? `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.`
1005
+ : place.scratch ? `There is no project directory for this task: you are in an empty folder (${place.cwd}). Use the OwlMem tools.`
1006
+ : `Work in ${place.cwd}. Other tasks wait until you finish here.`;
1007
+ return `OwlMem auto-wake started you on ${o.machine || "this computer"} to handle one task. Nobody is watching this session and nobody can answer questions.
1008
+
1009
+ Task #${id}, for ${task.agent}${task.broadcast ? " (sent to every agent; it is yours once you take it)" : ""}, from ${task.from}:
1010
+ Title: ${task.title}
1011
+
1012
+ ${task.body || "(no details)"}
1013
+
1014
+ Rules:
1015
+ 1. First call kb_task_update with id=${id} and status="doing". If the OwlMem tools aren't available, or OwlMem says the task isn't yours or is already taken, stop there and do nothing else.
1016
+ 2. ${where}
1017
+ 3. When you're finished, call kb_task_update with id=${id}, status="done" and note = the result (what you did, where it is).
1018
+ 4. If you can't do it — including anything that needs a person to approve, sign in, enter a code or confirm — call kb_task_update with id=${id}, status="declined" and note = what a person needs to do. Don't try to get around a permission you don't have.
1019
+ 5. Never put passwords, tokens, keys or other secrets in the note or in any message.
1020
+ 6. You have ${mins} minutes. After that this session is stopped and the task is handed to a person.
1021
+ `;
1022
+ }
1023
+
1024
+ // ── running a task ──
1025
+ export async function runTask(task, cfg, o = {}) {
1026
+ const dir = o.dir || DIR, cl = (cfg.clients || {})[task.client];
1027
+ if (!cl || !Array.isArray(cl.command) || !cl.command.length) return { outcome: "error", detail: "no command for " + task.client + " on this computer (wake.json)" };
1028
+ let place;
1029
+ try { const plan = o.plan || planWorkdir(task, o.home || home(), o.git); place = prepareWorkdir(task, plan, dir, o.git); if (plan.kind === "scratch") place.scratch = true; }
1030
+ catch (e) { return { outcome: "error", detail: e.message }; }
1031
+ const prompt = wakePrompt(task, place, { machine: cfg.machine, timeout_min: cfg.timeout_min });
1032
+ const argv = [...cl.command, ...(cl.prompt === "arg" ? [prompt] : [])];
1033
+ mkdirSync(join(wakeDir(dir), "logs"), { recursive: true });
1034
+ const logPath = join(wakeDir(dir), "logs", "task-" + task.id + ".log");
1035
+ const fd = openSync(logPath, "a");
1036
+ const win = (o.platform || platform()) === "win32";
1037
+ const ms = o.timeoutMs ?? (cfg.timeout_min || WAKE_DEFAULTS.timeout_min) * 60000;
1038
+ try {
1039
+ return await new Promise((res) => {
1040
+ let ch, timedOut = false, killer = null;
1041
+ try {
1042
+ ch = spawn(argv[0], argv.slice(1), { cwd: place.cwd, stdio: ["pipe", fd, fd], detached: !win, shell: win, windowsHide: true,
1043
+ env: { ...process.env, OWLMEM_NO_BROWSER: "1", OWLMEM_WAKE_TASK: String(task.id) } });
1044
+ } catch (e) { res({ outcome: "error", detail: "couldn't start " + argv[0] + ": " + e.message }); return; }
1045
+ 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 {} } };
1046
+ const timer = setTimeout(() => { timedOut = true; kill("SIGTERM"); killer = setTimeout(() => kill("SIGKILL"), 10000); }, ms);
1047
+ ch.on("error", (e) => { clearTimeout(timer); clearTimeout(killer); res({ outcome: "error", detail: (e.code === "ENOENT" ? "command not found: " : "couldn't start: ") + argv[0] }); });
1048
+ ch.on("close", (code, sig) => { clearTimeout(timer); clearTimeout(killer); res(timedOut ? { outcome: "timeout", code } : { outcome: "exit", code: code ?? (sig ? 128 : null) }); });
1049
+ ch.stdin.on("error", () => {});
1050
+ ch.stdin.end(cl.prompt === "arg" ? "" : prompt);
1051
+ });
1052
+ } finally { closeSync(fd); }
1053
+ }
1054
+
1055
+ // One at a time per AI, one at a time per plain directory, at most `max` at once on the computer. start(job) → Promise<result>.
1056
+ export class Scheduler {
1057
+ constructor({ max = WAKE_DEFAULTS.max_parallel, start, onDone } = {}) {
1058
+ Object.assign(this, { max, start, onDone, queue: [], running: new Set(), agents: new Set(), locks: new Set() });
1059
+ }
1060
+ add(job) { this.queue.push(job); this.pump(); }
1061
+ pump() {
1062
+ for (let i = 0; i < this.queue.length && this.running.size < this.max;) {
1063
+ const j = this.queue[i];
1064
+ if (this.agents.has(j.agent) || (j.lock && this.locks.has(j.lock))) { i++; continue; }
1065
+ this.queue.splice(i, 1); this.running.add(j); this.agents.add(j.agent); if (j.lock) this.locks.add(j.lock);
1066
+ Promise.resolve().then(() => this.start(j)).catch((e) => ({ outcome: "error", detail: e?.message || String(e) })).then((res) => {
1067
+ this.running.delete(j); this.agents.delete(j.agent); if (j.lock) this.locks.delete(j.lock);
1068
+ try { this.onDone?.(j, res); } catch {}
1069
+ this.pump();
1070
+ });
1071
+ }
1072
+ }
1073
+ busy() { return [...new Set([...this.agents, ...this.queue.map((j) => j.agent)])]; }
1074
+ slots() { return Math.max(0, this.max - this.running.size - this.queue.length); }
1075
+ get size() { return this.running.size + this.queue.length; }
1076
+ }
1077
+
1078
+ // The long-poll loop. api(sub, body) → {status, data}. Returns "revoked" after cleaning up on a 401.
1079
+ export async function wakeLoop(o) {
1080
+ const { cfg, api, sched, log = () => {}, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), once = false, cleanup } = o;
1081
+ let backoff = 0;
1082
+ for (;;) {
1083
+ const t0 = Date.now();
1084
+ let r;
1085
+ try { r = await api("poll", { busy: sched.busy(), slots: sched.slots(), timeout: (cfg.timeout_min || WAKE_DEFAULTS.timeout_min) * 60, wait: 50, version: VERSION }); }
1086
+ catch (e) { r = { status: 0, data: { detail: e?.message || String(e) } }; }
1087
+ if (r.status === 401) {
1088
+ log("OwlMem no longer accepts this computer for auto-wake (" + (r.data?.detail || r.data?.error || "401") + "). Removing auto-wake from this computer.");
1089
+ if (cleanup) cleanup();
1090
+ return "revoked";
1091
+ }
1092
+ if (r.status !== 200) { // network trouble or a server hiccup: try again, waiting longer each time (up to 5 minutes)
1093
+ backoff = Math.min(300000, backoff ? backoff * 2 : 2000);
1094
+ log("Can't reach OwlMem (" + (r.data?.detail || "HTTP " + r.status) + "); trying again in " + Math.round(backoff / 1000) + " s.");
1095
+ if (once) return "error";
1096
+ await sleep(backoff); continue;
1097
+ }
1098
+ backoff = 0;
1099
+ for (const t of r.data.tasks || []) {
1100
+ const plan = o.plan ? o.plan(t) : planWorkdir(t);
1101
+ log("Task #" + t.id + " for " + t.agent + ": starting " + t.client + (plan.dir ? " in " + plan.dir : "") + ".");
1102
+ sched.add({ id: t.id, agent: t.agent, lock: plan.lock || null, task: t, plan });
1103
+ }
1104
+ if (once) return r.data.status;
1105
+ if (Date.now() - t0 < 1000 && !(r.data.tasks || []).length) await sleep(1000); // never spin
1106
+ }
1107
+ }
1108
+
1109
+ function wakeApi() {
1110
+ return async (sub, body) => {
1111
+ const url = BASE + "/v1/wake/" + sub;
1112
+ let r;
1113
+ try {
1114
+ const sh = await signHeaders("POST", url, "other");
1115
+ r = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": "owlmem-cli/" + VERSION, ...sh },
1116
+ body: JSON.stringify(body || {}), signal: AbortSignal.timeout(90000) });
1117
+ } catch (e) { return { status: 0, data: { detail: e.message } }; }
1118
+ return { status: r.status, data: await r.json().catch(() => ({})) };
1119
+ };
1120
+ }
1121
+
1122
+ function wakeLogger(dir = DIR) {
1123
+ const f = join(wakeDir(dir), "wake.log");
1124
+ return (msg) => {
1125
+ const line = new Date().toISOString() + " " + msg + "\n";
1126
+ try { mkdirSync(wakeDir(dir), { recursive: true }); if (existsSync(f) && statSync(f).size > 1 << 20) copyFileSync(f, f + ".1"), writeFileSync(f, ""); appendFileSync(f, line); } catch {}
1127
+ process.stderr.write(line);
1128
+ };
1129
+ }
1130
+
1131
+ async function wakeRun() {
1132
+ const cfg = loadWake();
1133
+ if (!cfg) throw new Fail("Auto-wake isn't set up on this computer. Run: npx -y owlmem wake enable");
1134
+ const log = wakeLogger(), api = wakeApi();
1135
+ const report = async (j, res) => {
1136
+ log("Task #" + j.id + ": " + res.outcome + (res.code != null ? " (exit code " + res.code + ")" : "") + (res.detail ? " — " + res.detail : ""));
1137
+ for (let i = 0; i < 5; i++) {
1138
+ const r = await api("report", { id: j.id, outcome: res.outcome, code: res.code ?? null, detail: res.detail || "" });
1139
+ if (r.status === 200 || r.status === 404 || r.status === 401) return;
1140
+ await new Promise((ok) => setTimeout(ok, 5000 * (i + 1)));
1141
+ }
1142
+ };
1143
+ 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); } });
1144
+ log("OwlMem auto-wake " + VERSION + " running on " + (cfg.machine || hostname()) + ".");
1145
+ const out = await wakeLoop({ cfg, api, sched, log, cleanup: () => { try { wakeUninstall(autostartOpts(), wakeRunCmd, true); } catch (e) { log("cleanup: " + e.message); } } });
1146
+ if (out === "revoked") process.exit(0);
1147
+ }
1148
+
1149
+ async function wakeEnable(args = []) {
1150
+ const boot = args.includes("--boot"), again = args.includes("--again");
1151
+ 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; };
1152
+ const max = num("--max", 1, 16), timeout = num("--timeout", 1, 240);
1153
+ const old = loadWake() || {};
1154
+ const clients = { ...(old.clients || {}) };
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
+ }
1159
+ if (!Object.keys(clients).length)
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).");
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 };
1163
+ // OwlMem knows these app names; a command you add yourself to wake.json goes under "gemini" or "custom"
1164
+ const r = await wakeApi()("pair", { clients: Object.keys(clients).filter((c) => WAKE_APP_IDS.includes(c)), force: again });
1165
+ if (r.status !== 200) throw new Fail("OwlMem didn't accept the pairing: " + (r.data.detail || r.data.error || "HTTP " + r.status));
1166
+ mkdirSync(wakeDir(), { recursive: true });
1167
+ copyFileSync(fileURLToPath(import.meta.url), join(wakeDir(), "owlmem.mjs")); // the service runs this copy (npx's cache can be cleaned out)
1168
+ if (r.data.status === "pending") cfg.pair_url = r.data.url; else delete cfg.pair_url;
1169
+ saveWake(DIR, cfg);
1170
+ const a = installAutostart(autostartOpts(DIR, { boot: cfg.boot }));
1171
+ say("Auto-wake is installed (" + a.kind + ": " + tilde(a.file) + ").");
1172
+ 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.");
1173
+ if (r.data.status === "pending") {
1174
+ 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.");
1175
+ openBrowser(r.data.url);
1176
+ } 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).");
1177
+ }
1178
+
1179
+ function wakeDisable(quiet = false) {
1180
+ const had = !!loadWake();
1181
+ const o = autostartOpts();
1182
+ const present = existsSync(autostart(o).file);
1183
+ if (!had && !present) { if (!quiet) say("Auto-wake isn't set up on this computer."); return false; }
1184
+ const api = wakeApi();
1185
+ return api("bye", {}).catch(() => null).then(() => {
1186
+ wakeUninstall(o);
1187
+ say("Auto-wake removed from this computer (autostart entry and settings). Worktrees and logs it made stay in " + tilde(wakeDir()) + ".");
1188
+ return true;
1189
+ });
1190
+ }
1191
+
1192
+ async function wakeStatus() {
1193
+ const cfg = loadWake(), a = autostart(autostartOpts());
1194
+ if (!cfg) { say("Auto-wake isn't set up on this computer. Turn it on: npx -y owlmem wake enable"); return; }
1195
+ say("Autostart: " + (existsSync(a.file) ? a.kind + " (" + tilde(a.file) + ")" : "missing"));
1196
+ say("Apps: " + Object.entries(cfg.clients || {}).map(([k, v]) => k + " → " + v.command.join(" ")).join("; "));
1197
+ say("At most " + cfg.max_parallel + " at once, " + cfg.timeout_min + " minutes per task. Log: " + tilde(join(wakeDir(), "wake.log")));
1198
+ const r = await wakeApi()("poll", { wait: 0, slots: 0 });
1199
+ if (r.status === 401) say("OwlMem: this computer isn't paired any more. Pair it again: npx -y owlmem wake enable --again");
1200
+ else if (r.status !== 200) say("OwlMem: can't reach it (" + (r.data.detail || "HTTP " + r.status) + ")");
1201
+ else if (r.data.status === "pending") say("OwlMem: waiting for approval in the console" + (cfg.pair_url ? ": " + cfg.pair_url : "") + " (code " + r.data.code + ")");
1202
+ else say("OwlMem: paired as " + r.data.code + ". " + r.data.agents.map((x) => x.agent + " (" + x.client + (x.wake ? "" : ", switched off") + ")").join(", "));
1203
+ }
1204
+
1205
+ const WAKE_HELP = `Usage: npx -y owlmem wake <enable|disable|status>
1206
+
1207
+ Auto-wake starts the AI on this computer when it gets an OwlMem task (Claude Code with \`claude -p\`, Codex with
1208
+ \`codex exec\`), without you opening it. It runs in the background from login, for this user only.
1209
+
1210
+ enable [--boot] [--max N] [--timeout MIN] [--again]
1211
+ install it and pair this computer (approve in the console). --boot (Linux): run from boot,
1212
+ not just login. --max: tasks at once (default 2). --timeout: minutes per task (default 30).
1213
+ --again: pair again (choose the AIs again).
1214
+ disable remove it from this computer (also done by npx -y owlmem uninstall)
1215
+ status show what's set up and what OwlMem says
1216
+
1217
+ Turn it off for one AI with the switch in the console (AI agents → Agents). Settings, including the command for
1218
+ each app ("claude-code", "codex", or your own under "gemini" / "custom"): ~/.config/owlmem/wake/wake.json`;
1219
+
1220
+ async function wakeCmd(sub, args) {
1221
+ if (sub === "enable") return wakeEnable(args);
1222
+ if (sub === "disable") return wakeDisable();
1223
+ if (sub === "status") return wakeStatus();
1224
+ if (sub === "run") return wakeRun();
1225
+ console.log(WAKE_HELP);
1226
+ if (sub && sub !== "help" && sub !== "--help") process.exitCode = 2;
1227
+ }
1228
+
768
1229
  const INSTALL_HELP = `Usage: npx -y owlmem install [options]
769
1230
 
770
1231
  Connects Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI and Claude Desktop on this computer to
@@ -775,7 +1236,9 @@ as <file>.bak-owlmem-<time>. Also adds the OwlMem usage rules to CLAUDE.md / AGE
775
1236
  --yes, -y don't ask (non-interactive runs, e.g. an AI running this, never ask)
776
1237
  --only a,b only these apps: ${CLIENT_IDS.join(", ")}
777
1238
  --no-rules don't touch CLAUDE.md / AGENTS.md / GEMINI.md
778
- --uninstall remove OwlMem entries and the rules block (with backups)
1239
+ --uninstall remove OwlMem entries and the rules block (with backups), and auto-wake
1240
+ --wake also turn on auto-wake: start your AI when it gets a task (without --wake, --yes leaves it off)
1241
+ --no-wake don't ask about auto-wake
779
1242
 
780
1243
  Signing in happens in each app the first time it uses OwlMem (standard MCP OAuth): you click "Allow" once.`;
781
1244
 
@@ -785,6 +1248,7 @@ export async function main(argv) {
785
1248
  if (cmd === "install") { await install(argv.slice(1)); return; }
786
1249
  if (cmd === "uninstall") { await install(["--uninstall", ...argv.slice(1)]); return; }
787
1250
  if (cmd === "mcp") { await mcpServe(); return; }
1251
+ if (cmd === "wake") { await wakeCmd(sub, rest); return; }
788
1252
  if (cmd === "device") { // register this machine for standing grants and show its fingerprint
789
1253
  const r = await call("POST", "device", {});
790
1254
  if (r.status >= 400) throw new Fail(r.data.detail || r.data.error || "HTTP " + r.status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "owlmem",
3
- "version": "0.6.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" },