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.
- package/owlmem.mjs +58 -12
- 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.
|
|
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"),
|
|
1078
|
-
// the automatic note sent when that task was done or declined)
|
|
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
|
|
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 + ")" :
|
|
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((
|
|
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.
|
|
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" },
|