owlmem 0.7.9 → 0.8.0

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 (2) hide show
  1. package/owlmem.mjs +58 -12
  2. package/package.json +1 -1
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.7.9";
44
+ export const VERSION = "0.8.0";
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();
@@ -1074,10 +1074,13 @@ export function prepareWorkdir(task, plan, dir = DIR, git = gitRun) {
1074
1074
  return { cwd: rel ? join(wt, rel) : wt, worktree: wt, branch, repo: plan.top };
1075
1075
  }
1076
1076
 
1077
- // A job from OwlMem is a task given to this AI (type "task"), or the result of a task this AI gave someone else (type "reply":
1078
- // the automatic note sent when that task was done or declined). Older OwlMem servers send no type: a task.
1077
+ // A job from OwlMem is a task given to this AI (type "task"), the result of a task this AI gave someone else (type "reply":
1078
+ // the automatic note sent when that task was done or declined), or a note a person sent this AI (type "note", 0.8.0).
1079
+ // Older OwlMem servers send no type: a task.
1079
1080
  const isReply = (t) => t?.type === "reply";
1080
- const jobName = (t) => (isReply(t) ? "reply-" : "task-") + t.id;
1081
+ const isNote = (t) => t?.type === "note";
1082
+ const jobLabel = (t) => (isReply(t) ? "Reply #" : isNote(t) ? "Note #" : "Task #") + t.id;
1083
+ const jobName = (t) => (isReply(t) ? "reply-" : isNote(t) ? "note-" : "task-") + t.id;
1081
1084
  const shortName = (who) => { who = String(who || ""); return who.startsWith("user:") ? who.slice(5) : who.includes("/") ? who.split("/").slice(1).join("/") : who; };
1082
1085
 
1083
1086
  // Woken for a reply: read it (kb_inbox marks it read; that's how OwlMem knows it was handled), act on it only if needed.
@@ -1099,8 +1102,44 @@ Never put passwords, tokens, keys or other secrets in any message. You have ${mi
1099
1102
  `;
1100
1103
  }
1101
1104
 
1105
+ // Woken for a note a person sent this AI: a person writing to an AI usually wants something done, whichever kind they picked.
1106
+ // Read it (kb_inbox marks it read; that's how OwlMem knows it was handled), do it if it asks for something, and tell them the result.
1107
+ export function notePrompt(n, place, o = {}) {
1108
+ const who = shortName(n.from), mins = o.timeout_min || WAKE_DEFAULTS.timeout_min;
1109
+ const where = place.worktree
1110
+ ? (n.zh ? `在 ${place.cwd} 里做(${place.repo} 的独立 git 工作树,分支 ${place.branch};只在这个分支提交,不合并、不推送别的分支,结果里写上分支名)。`
1111
+ : `Work in ${place.cwd}. It is a separate git worktree of ${place.repo} on the branch ${place.branch}. Commit on this branch only, don't merge or push other branches, and put the branch name in your result.`)
1112
+ : place.scratch ? (n.zh ? `留言没指定目录:你在一个空文件夹(${place.cwd})里。` : `The note names no project directory: you are in an empty folder (${place.cwd}).`)
1113
+ : (n.zh ? `在 ${place.cwd} 里做。` : `Work in ${place.cwd}.`);
1114
+ if (n.zh) return `奥麦自动唤醒在 ${o.machine || "这台电脑"} 上启动了你(${n.agent}),因为 ${who}(人)给你发了一条留言。没有人在看这个会话,也没人能回答问题。
1115
+
1116
+ 留言 #${n.id}:${n.title}
1117
+
1118
+ ${n.body || "(没有正文)"}
1119
+
1120
+ 先用 kb_inbox 看这条留言(会标成已读)。${where}
1121
+ - 它要你做事:不需要人参与的就直接做完,然后用 kb_send 发给 ${n.from}(kind=note)报结果,标题一句话说结果。
1122
+ - 做不了(要人输验证码、被安全检查拦下、要动有风险的线上服务):不要硬做,用 kb_send 告诉 ${n.from} 卡在哪、需要他做什么。
1123
+ - 只是告诉你一件事、不用做什么:看完就结束,不要发消息。
1124
+ 不要把密码、令牌、密钥写进任何消息。你有 ${mins} 分钟。
1125
+ `;
1126
+ return `OwlMem auto-wake started you (${n.agent}) on ${o.machine || "this computer"} because ${who}, a person, sent you a note. Nobody is watching this session and nobody can answer questions.
1127
+
1128
+ Note #${n.id}: ${n.title}
1129
+
1130
+ ${n.body || "(no details)"}
1131
+
1132
+ First read it with kb_inbox (that marks it read). ${where}
1133
+ - If it asks you to do something you can do without a person, do it, then tell ${n.from} the result with kb_send (kind=note), with the result in one line as the title.
1134
+ - If you can't (it needs a code typed in, a safety check stops you, or it means a risky change to a live service), don't force it: tell ${n.from} with kb_send what is blocking and what they need to do.
1135
+ - If it only tells you something, end here and don't send any message.
1136
+ Never put passwords, tokens, keys or other secrets in any message. You have ${mins} minutes.
1137
+ `;
1138
+ }
1139
+
1102
1140
  export function wakePrompt(task, place, o = {}) {
1103
1141
  if (isReply(task)) return replyPrompt(task, o);
1142
+ if (isNote(task)) return notePrompt(task, place, o);
1104
1143
  const id = task.id, mins = o.timeout_min || WAKE_DEFAULTS.timeout_min;
1105
1144
  const where = place.worktree
1106
1145
  ? `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.`
@@ -1142,7 +1181,7 @@ export async function runTask(task, cfg, o = {}) {
1142
1181
  let ch, timedOut = false, killer = null;
1143
1182
  try {
1144
1183
  ch = spawn(argv[0], argv.slice(1), { cwd: place.cwd, stdio: ["pipe", fd, fd], detached: !win, shell: win, windowsHide: true,
1145
- env: { ...process.env, OWLMEM_NO_BROWSER: "1", [isReply(task) ? "OWLMEM_WAKE_REPLY" : "OWLMEM_WAKE_TASK"]: String(task.id) } });
1184
+ env: { ...process.env, OWLMEM_NO_BROWSER: "1", [isReply(task) ? "OWLMEM_WAKE_REPLY" : isNote(task) ? "OWLMEM_WAKE_NOTE" : "OWLMEM_WAKE_TASK"]: String(task.id) } });
1146
1185
  } catch (e) { res({ outcome: "error", detail: "couldn't start " + argv[0] + ": " + e.message }); return; }
1147
1186
  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 {} } };
1148
1187
  const timer = setTimeout(() => { timedOut = true; kill("SIGTERM"); killer = setTimeout(() => kill("SIGKILL"), 10000); }, ms);
@@ -1200,7 +1239,7 @@ export async function wakeLoop(o) {
1200
1239
  backoff = 0;
1201
1240
  for (const t of r.data.tasks || []) {
1202
1241
  const plan = isReply(t) ? { kind: "scratch" } : o.plan ? o.plan(t) : planWorkdir(t); // a reply is read in an empty folder
1203
- log((isReply(t) ? "Reply #" + t.id + " (task #" + t.task_id + ")" : "Task #" + t.id) + " for " + t.agent + ": starting " + t.client + (plan.dir ? " in " + plan.dir : "") + ".");
1242
+ log((isReply(t) ? "Reply #" + t.id + " (task #" + t.task_id + ")" : jobLabel(t)) + " for " + t.agent + ": starting " + t.client + (plan.dir ? " in " + plan.dir : "") + ".");
1204
1243
  sched.add({ id: t.id, agent: t.agent, lock: plan.lock || null, task: t, plan });
1205
1244
  }
1206
1245
  // OwlMem says a newer version is out: update only while nothing runs or waits (no task is cut off), then restart
@@ -1216,12 +1255,13 @@ export async function wakeLoop(o) {
1216
1255
  // The program runs its own copy (wake/owlmem.mjs). When OwlMem's poll answer names a newer version (latest), and nothing is
1217
1256
  // running: download that version's package from the npm registry, check it against the registry's sha512, take owlmem.mjs
1218
1257
  // out of it, check that it runs and says it is that version, then swap it in (rename in the same folder, so it's all or
1219
- // nothing) and restart. If anything fails the old copy keeps running, and that version isn't tried again for 24 hours.
1258
+ // nothing) and restart. If anything fails the old copy keeps running, and that version isn't tried again for 24 hours;
1259
+ // except a 404 for the version: npm takes a few minutes to serve a new one everywhere, so that's tried again in 10 minutes.
1220
1260
  // Restart: on Linux and macOS the program exits with WAKE_RESTART_CODE; the systemd unit (Restart=on-failure) and the
1221
1261
  // LaunchAgent (KeepAlive unless the exit was successful) start it again 30 s later. On Windows nothing watches it, so it
1222
1262
  // starts the new copy itself (detached, like the logon launcher) and then exits.
1223
1263
  export const WAKE_RESTART_CODE = 75;
1224
- const UPDATE_RETRY = 24 * 3600 * 1000;
1264
+ const UPDATE_RETRY = 24 * 3600 * 1000, UPDATE_NOT_YET = 10 * 60 * 1000;
1225
1265
  const REGISTRY = () => String(process.env.npm_config_registry || "https://registry.npmjs.org").replace(/\/+$/, "");
1226
1266
  export function newer(a, b) {
1227
1267
  const p = (v) => (/^\d+\.\d+\.\d+$/.test(String(v || "")) ? String(v).split(".").map(Number) : null);
@@ -1243,16 +1283,22 @@ export function tarFile(tgz, want = "package/owlmem.mjs") {
1243
1283
  }
1244
1284
  return null;
1245
1285
  }
1246
- // → "restart" (new copy in place) | "skip" (tried that version less than 24 h ago) | "failed" (old copy stays)
1286
+ // → "restart" (new copy in place) | "skip" (tried that version less than 24 h ago, or 10 min for a 404) | "failed" (old copy stays)
1247
1287
  export async function wakeUpdate(latest, o = {}) {
1248
1288
  const dir = o.dir || DIR, log = o.log || (() => {}), now = o.now || Date.now, get = o.fetch || fetch;
1249
1289
  const wd = wakeDir(dir), state = join(wd, "update.json"), target = join(wd, "owlmem.mjs"), tmpFile = join(wd, "owlmem.update.mjs");
1250
1290
  let st = {}; try { st = JSON.parse(readFileSync(state, "utf8")); } catch {}
1251
- if (st.version === latest && now() - (st.at || 0) < UPDATE_RETRY) return "skip";
1252
- try { mkdirSync(wd, { recursive: true }); writeFileSync(state, JSON.stringify({ version: latest, at: now() }) + "\n"); } catch {}
1291
+ if (st.version === latest && now() - (st.at || 0) < (st.wait || UPDATE_RETRY)) return "skip";
1292
+ const mark = (wait) => { try { mkdirSync(wd, { recursive: true }); writeFileSync(state, JSON.stringify({ version: latest, at: now(), ...(wait ? { wait } : {}) }) + "\n"); } catch {} };
1293
+ mark();
1253
1294
  log("Updating auto-wake from " + VERSION + " to " + latest + ".");
1254
1295
  try {
1255
1296
  const meta = await get(REGISTRY() + "/owlmem/" + encodeURIComponent(latest), { signal: AbortSignal.timeout(60000) });
1297
+ if (meta.status === 404) {
1298
+ mark(UPDATE_NOT_YET);
1299
+ log(latest + " isn't on npm yet; trying again in 10 minutes.");
1300
+ return "failed";
1301
+ }
1256
1302
  if (!meta.ok) throw new Error("registry answered HTTP " + meta.status);
1257
1303
  const dist = (await meta.json())?.dist || {};
1258
1304
  const m = /^sha512-(.+)$/.exec(String(dist.integrity || ""));
@@ -1328,7 +1374,7 @@ async function wakeRun() {
1328
1374
  const log = wakeLogger(), api = wakeApi();
1329
1375
  try { if (wakeTools(cfg)) { saveWake(DIR, cfg); log("Unattended runs may now also use: " + WAKE_TOOLS.join(", ") + "."); } } catch (e) { log("tools: " + e.message); }
1330
1376
  const report = async (j, res) => {
1331
- log((isReply(j.task) ? "Reply #" : "Task #") + j.id + ": " + res.outcome + (res.code != null ? " (exit code " + res.code + ")" : "") + (res.detail ? " — " + res.detail : ""));
1377
+ log(jobLabel(j.task) + ": " + res.outcome + (res.code != null ? " (exit code " + res.code + ")" : "") + (res.detail ? " — " + res.detail : ""));
1332
1378
  for (let i = 0; i < 5; i++) {
1333
1379
  const r = await api("report", { id: j.id, outcome: res.outcome, code: res.code ?? null, detail: res.detail || "" });
1334
1380
  if (r.status === 200 || r.status === 404 || r.status === 401) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "owlmem",
3
- "version": "0.7.9",
3
+ "version": "0.8.0",
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" },