owlmem 0.9.2 → 0.9.4

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 +1 -1
  2. package/owlmem.mjs +60 -3
  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`, Antigravity with `agy -p`, Hermes with `hermes -z` (for Codex and Antigravity, which refuse tool calls when nobody is watching, auto-wake approves just the two OwlMem tools used to report back: in `~/.codex/config.toml` and `~/.gemini/antigravity-cli/settings.json`; for Hermes, only if OwlMem is marked `trust: untrusted`, under `command_allowlist` in `~/.hermes/config.yaml`) — with the task as the prompt. The AI marks the task done or declined as usual. When a task it gave out is done or declined, it is woken too, to read the result. The program updates itself (from npm, when nothing is running).
32
+ `claude -p`, Codex with `codex exec`, Antigravity with `agy -p`, Hermes with `hermes -z`, DeepSeek Harness with `dsh --profile headless` (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`; for DeepSeek Harness, its headless profile gets the model and the OwlMem server copied from the web profile, and its credential store turned back on, in `~/.dsh/profiles/headless/cordis.patch.yml`) — with the task as the prompt. The AI marks the task done or declined as usual. When a task it gave out is done or declined, it is woken too, to read the result. The program updates itself (from npm, when nothing is running).
33
33
 
34
34
  When someone turns auto-wake on for an AI in the console and no computer runs it yet, that AI is told so the next time it connects:
35
35
  with your OK it runs `npx -y owlmem wake enable` itself and sends you the pairing link to approve.
package/owlmem.mjs CHANGED
@@ -41,7 +41,7 @@ import { join, dirname, basename, relative, isAbsolute } from "node:path";
41
41
  import { fileURLToPath } from "node:url";
42
42
  import { spawn, spawnSync } from "node:child_process";
43
43
 
44
- export const VERSION = "0.9.2";
44
+ export const VERSION = "0.9.4";
45
45
  const BASE = (process.env.OWLMEM_URL || "https://owlmem.com").replace(/\/$/, "");
46
46
  const DIR = process.env.OWLMEM_HOME || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "owlmem");
47
47
  const enc = new TextEncoder(), dec = new TextDecoder();
@@ -817,8 +817,11 @@ export const WAKE_CLIENTS = {
817
817
  // Its installer puts hermes under ~/.hermes/hermes-agent or ~/.local/bin; a system-wide service install under /var/lib/hermes
818
818
  hermes: { name: "Hermes", bin: "hermes", command: ["hermes", "-z"], prompt: "arg",
819
819
  home: [[".hermes", "hermes-agent", ".hermes", "bin", "hermes"], [".local", "bin", "hermes"], "/var/lib/hermes/hermes-agent/.hermes/bin/hermes"] },
820
+ // DeepSeek Harness: the headless profile answers one task (from stdin) and exits. Its own profile needs OwlMem and a model,
821
+ // copied from the web profile (see allowDsh). On NixOS dsh must run on the official Node build: use the dsh wrapper found here.
822
+ dsh: { name: "DeepSeek Harness", bin: "dsh", command: ["dsh", "--profile", "headless"], prompt: "stdin", home: [[".local", "bin", "dsh"]] },
820
823
  };
821
- export const WAKE_APP_IDS = ["claude-code", "codex", "antigravity", "hermes", "gemini", "custom"];
824
+ export const WAKE_APP_IDS = ["claude-code", "codex", "antigravity", "hermes", "dsh", "gemini", "custom"];
822
825
  // Where an app's command is: on PATH, or the first of its default install places that exists (path parts under your home
823
826
  // folder, or an absolute path), used by full path
824
827
  export function wakeBin(c, home_ = home(), exists = existsSync, path = onPath) {
@@ -854,6 +857,40 @@ export function allowAntigravity(home_ = home()) {
854
857
  writeFileSync(f, JSON.stringify(j, null, 2) + "\n");
855
858
  return true;
856
859
  }
860
+ // DeepSeek Harness keeps each profile under ~/.dsh/profiles/<name>/, configured by a patch list (cordis.patch.yml). The headless
861
+ // profile that auto-wake runs starts empty: copy over from the web profile what it needs to work for OwlMem — the default model
862
+ // and its provider (agent-default-model, llm-pi-ai) and the OwlMem MCP server — and turn its credential store back on (headless
863
+ // turns it off, but the web profile keeps the provider's key there). Adds entries only; the file is backed up first.
864
+ // Returns the names of what was added; [] when nothing was missing; "init" when the headless profile isn't there yet (run
865
+ // `dsh --profile headless --help` once); "noweb" when OwlMem isn't in the web profile either, so there's nothing to copy.
866
+ export function allowDsh(home_ = home(), env = process.env, now = new Date()) {
867
+ const dir = join(env.DSH_HOME || join(home_, ".dsh"), "profiles"), hf = join(dir, "headless", "cordis.patch.yml");
868
+ if (!existsSync(join(dir, "headless"))) return "init";
869
+ let h = ""; try { h = readFileSync(hf, "utf8"); } catch { /* an empty profile has no patch file yet */ }
870
+ let w = ""; try { w = readFileSync(join(dir, "web", "cordis.patch.yml"), "utf8"); } catch { /* no web profile */ }
871
+ // the top-level entries of a patch list: each starts with "- " at column 0 and runs to the next one
872
+ const items = (t) => t.split(/\n(?=- )/).map((x) => x.replace(/^[\s\S]*?(?=^- )/m, "")).filter((x) => /^- /.test(x));
873
+ const has = (t, rx) => items(t).some((x) => rx.test(x));
874
+ const want = [
875
+ ["model", /^- id: agent-default-model\b/m], ["provider", /^- id: llm-pi-ai\b/m], ["owlmem", /serverName:\s*["']?owlmem\b/],
876
+ ];
877
+ const add = [], names = [];
878
+ for (const [name, rx] of want) {
879
+ if (has(h, rx)) continue;
880
+ const from = items(w).find((x) => rx.test(x));
881
+ if (from) { add.push(from.replace(/\s+$/, "")); names.push(name); }
882
+ }
883
+ if (!has(h, /serverName:\s*["']?owlmem\b/) && !names.includes("owlmem")) return "noweb";
884
+ if (!/^- id: credentials\s*\n\s+disabled:\s*false\b/m.test(h)) {
885
+ add.push("# headless turns the credential store off; the web profile keeps the model's key there\n- id: credentials\n disabled: false");
886
+ names.push("credentials");
887
+ }
888
+ if (!add.length) return [];
889
+ if (existsSync(hf)) copyFileSync(hf, hf + ".bak-" + now.toISOString().slice(0, 10).replace(/-/g, ""));
890
+ const base = h.replace(/^\[\]\s*$/m, "").replace(/\s*$/, "");
891
+ writeFileSync(hf, (base ? base + "\n" : "") + "# Added by OwlMem auto-wake (owlmem wake enable): what the headless profile needs to answer OwlMem tasks\n" + add.join("\n") + "\n");
892
+ return names;
893
+ }
857
894
  // Hermes calls MCP tools without asking unless the server is marked `trust: untrusted` (or an unknown value) under mcp_servers in
858
895
  // ~/.hermes/config.yaml; then unattended runs refuse its write tools. Only in that case, add WAKE_TOOLS
859
896
  // to the top-level command_allowlist as mcp:<server>/<tool> (what Hermes itself writes for "Always"). Adds lines only,
@@ -1216,6 +1253,19 @@ export class Scheduler {
1216
1253
  get size() { return this.running.size + this.queue.length; }
1217
1254
  }
1218
1255
 
1256
+ // Check-ins for the AI (0.9.4, 站长 11-01「npm 方式、不消耗 token 上报」): while an AI this program started is still working
1257
+ // on a task, tell OwlMem every ALIVE_EVERY that its process is running. That counts as the task's heartbeat, so the AI
1258
+ // doesn't spend tokens reporting in itself. Only tasks whose process is running (the scheduler's running set); replies
1259
+ // (type note) have no heartbeat. When the process ends this stops by itself and report() says how it ended.
1260
+ export const ALIVE_EVERY = 60 * 1000;
1261
+ export async function aliveTick(sched, api, log = () => {}) {
1262
+ const ids = [...sched.running].filter((j) => j.task?.type !== "note" && j.id != null).map((j) => j.id);
1263
+ if (!ids.length) return [];
1264
+ const r = await api("alive", { ids });
1265
+ if (r.status !== 200 && r.status !== 404) log("check-in: OwlMem answered " + (r.data?.detail || "HTTP " + r.status));
1266
+ return ids;
1267
+ }
1268
+
1219
1269
  // The long-poll loop. api(sub, body) → {status, data}. Returns "revoked" after cleaning up on a 401.
1220
1270
  export async function wakeLoop(o) {
1221
1271
  const { cfg, api, sched, log = () => {}, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), once = false, cleanup } = o;
@@ -1549,6 +1599,7 @@ async function wakeRun() {
1549
1599
  log("OwlMem auto-wake " + VERSION + " running on " + (cfg.machine || hostname()) + ".");
1550
1600
  const usage = () => usageTick(cfg, api, log).catch((e) => log("usage: " + e.message));
1551
1601
  setTimeout(usage, 30000).unref(); setInterval(usage, USAGE_EVERY).unref();
1602
+ setInterval(() => aliveTick(sched, api, log).catch((e) => log("check-in: " + e.message)), ALIVE_EVERY).unref();
1552
1603
  const out = await wakeLoop({ cfg, api, sched, log, update: (v) => wakeUpdate(v, { log }),
1553
1604
  cleanup: () => { try { wakeUninstall(autostartOpts(), wakeRunCmd, true); } catch (e) { log("cleanup: " + e.message); } } });
1554
1605
  if (out === "revoked") process.exit(0);
@@ -1576,11 +1627,17 @@ async function wakeEnable(args = []) {
1576
1627
  }
1577
1628
  if (!Object.keys(clients).length) {
1578
1629
  const told = await wakeNoClients(wakeApi()).catch(() => false);
1579
- 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\"." +
1630
+ 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, DeepSeek Harness: dsh). Install one, or add its command to " + wakeFile() + " under \"clients\"." +
1580
1631
  (told ? "\nOwlMem support has been told; you'll hear from us once your AI app is supported." : ""));
1581
1632
  }
1582
1633
  if (clients.codex && allowCodex()) say("Let Codex report back to OwlMem when nobody is watching (tool approvals in ~/.codex/config.toml).");
1583
1634
  if (clients.antigravity && allowAntigravity()) say("Let Antigravity's command line report back to OwlMem when nobody is watching (rules in ~/.gemini/antigravity-cli/settings.json).");
1635
+ if (clients.dsh) {
1636
+ const d = allowDsh();
1637
+ if (d === "init") say("DeepSeek Harness: run `dsh --profile headless --help` once to create its headless profile, then run this again.");
1638
+ else if (d === "noweb") say("DeepSeek Harness: OwlMem isn't set up in its web profile, so the headless profile can't be set up from it. Connect OwlMem in DeepSeek Harness first, then run this again.");
1639
+ else if (d.length) say("Set up DeepSeek Harness's headless profile for auto-wake (" + d.join(", ") + " in ~/.dsh/profiles/headless/cordis.patch.yml, copied from the web profile; the old file is backed up next to it).");
1640
+ }
1584
1641
  if (clients.hermes) {
1585
1642
  const h = allowHermes();
1586
1643
  if (h === true) say("Let Hermes report back to OwlMem when nobody is watching (entries under command_allowlist in ~/.hermes/config.yaml).");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "owlmem",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
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" },