owlmem 0.4.1 → 0.6.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/README.md +34 -0
- package/owlmem.mjs +371 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
Command line for [OwlMem](https://owlmem.com) — shared memory and an end-to-end encrypted vault for your AI agents.
|
|
4
4
|
|
|
5
|
+
## Connect your AI apps
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx -y owlmem install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Finds Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI and Claude Desktop on this computer and adds
|
|
12
|
+
OwlMem (`https://mcp.owlmem.com/mcp`) to each one. Config files are merged, not overwritten, and every file it
|
|
13
|
+
changes is backed up first (`<file>.bak-owlmem-<time>`). It also adds a short "Team memory" rules block to
|
|
14
|
+
`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md` and `~/.gemini/GEMINI.md` (between `<!-- owlmem:start -->` markers).
|
|
15
|
+
|
|
16
|
+
| Option | |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `--dry-run` | show the plan, change nothing |
|
|
19
|
+
| `--yes` | don't ask (when stdin isn't a terminal — an AI running it — it never asks) |
|
|
20
|
+
| `--only claude-code,cursor` | only these apps (`claude-code`, `codex`, `cursor`, `vscode`, `vscode-insiders`, `windsurf`, `gemini`, `claude-desktop`) |
|
|
21
|
+
| `--no-rules` | leave the instruction files alone |
|
|
22
|
+
| `--uninstall` | remove the OwlMem entries and rules block again |
|
|
23
|
+
|
|
24
|
+
Signing in is done by each app (standard MCP OAuth): the first time it uses OwlMem it opens the owlmem.com
|
|
25
|
+
page, where you sign in and click **Allow**. AI agents can read the setup guide at https://mcp.owlmem.com.
|
|
26
|
+
|
|
27
|
+
## Vault
|
|
28
|
+
|
|
5
29
|
```sh
|
|
6
30
|
npx owlmem login # paste the agent's token from the OwlMem console
|
|
7
31
|
npx owlmem vault unlock # paste your recovery code, once per machine
|
|
@@ -13,6 +37,13 @@ npx owlmem vault import bitwarden_export.json # also 1Password / KeePass CSV,
|
|
|
13
37
|
Vault items are encrypted on your machine with a key only you hold; OwlMem stores ciphertext it cannot read.
|
|
14
38
|
Every read or write needs the space owner's approval in the console (passkey, authenticator app or GitHub).
|
|
15
39
|
|
|
40
|
+
For unattended jobs (scheduled tasks, overnight deploys) the owner can set a **standing grant**: one agent, on one machine,
|
|
41
|
+
may read chosen items or a folder without asking each time, until a set date (90 days at most), optionally only during set hours.
|
|
42
|
+
Run `npx owlmem device` on that machine to register it and print its fingerprint. The program signs every vault request with a
|
|
43
|
+
device key kept next to its config, so the same token on another machine gets nothing. By default a standing grant covers
|
|
44
|
+
`owlmem run` / `vault_run` only (the secret goes into a command); viewing in plain text must be allowed separately.
|
|
45
|
+
Saving, deleting and exporting always need approval.
|
|
46
|
+
|
|
16
47
|
Let your AI use the vault directly through a local MCP server:
|
|
17
48
|
|
|
18
49
|
```sh
|
|
@@ -22,3 +53,6 @@ claude mcp add owlmem-vault -- npx -y owlmem mcp
|
|
|
22
53
|
Tools: `vault_list`, `vault_run` (secrets are injected into the command and masked in its output), `vault_get`, `vault_put`.
|
|
23
54
|
|
|
24
55
|
Requires Node 18+. No dependencies.
|
|
56
|
+
|
|
57
|
+
`owlmem vault unlock` opens your browser only when talking to owlmem.com; set `OWLMEM_NO_BROWSER=1` (or `BROWSER=none`)
|
|
58
|
+
to just print the link instead.
|
package/owlmem.mjs
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
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]
|
|
9
|
+
// connect Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI and
|
|
10
|
+
// Claude Desktop on this computer to OwlMem (npx -y owlmem install)
|
|
8
11
|
// owlmem login save this agent's token (from the console)
|
|
9
12
|
// owlmem vault unlock paste the recovery code once per machine
|
|
10
13
|
// owlmem vault ls
|
|
@@ -15,19 +18,26 @@
|
|
|
15
18
|
// owlmem vault export [--format bitwarden-json|keepass-csv|env] > file
|
|
16
19
|
// owlmem run --env VAR=<name>[:field] [...] -- <command> [args] run a command with secrets in its environment
|
|
17
20
|
// owlmem vault keygen (setup without a browser) print a new recovery code and its key check
|
|
21
|
+
// owlmem device register this machine and show its fingerprint (for standing grants)
|
|
18
22
|
// owlmem mcp local MCP server for your AI (vault_list, vault_run, vault_get, vault_put)
|
|
19
23
|
// e.g. claude mcp add owlmem-vault -- node ~/owlmem.mjs mcp
|
|
20
24
|
//
|
|
21
25
|
// Reading, writing, deleting and exporting need the space owner's approval in the console; this program waits
|
|
22
26
|
// for it (up to 5 minutes). Secrets go to stdout only; messages go to stderr.
|
|
27
|
+
//
|
|
28
|
+
// Standing grants (for unattended jobs): the owner can let one agent, on one machine, read chosen items without
|
|
29
|
+
// asking each time — until a set date, optionally only during set hours. This program signs every vault request
|
|
30
|
+
// with a device key kept on this machine (device.json next to config.json), and says whether the secret goes into a
|
|
31
|
+
// command (`owlmem run`, vault_run) or is shown (`vault get`, vault_get). A token copied to another machine has no
|
|
32
|
+
// device key there, so standing grants don't apply. Writing, deleting and exporting always need approval.
|
|
23
33
|
|
|
24
34
|
import { webcrypto as crypto } from "node:crypto";
|
|
25
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
|
|
26
|
-
import { homedir } from "node:os";
|
|
35
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync } from "node:fs";
|
|
36
|
+
import { homedir, hostname } from "node:os";
|
|
27
37
|
import { join, dirname } from "node:path";
|
|
28
|
-
import { spawn } from "node:child_process";
|
|
38
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
29
39
|
|
|
30
|
-
export const VERSION = "0.
|
|
40
|
+
export const VERSION = "0.6.0";
|
|
31
41
|
const BASE = (process.env.OWLMEM_URL || "https://owlmem.com").replace(/\/$/, "");
|
|
32
42
|
const DIR = process.env.OWLMEM_HOME || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "owlmem");
|
|
33
43
|
const enc = new TextEncoder(), dec = new TextDecoder();
|
|
@@ -169,11 +179,42 @@ function spaceKey(space) {
|
|
|
169
179
|
return unb64(readFileSync(p, "utf8").trim());
|
|
170
180
|
}
|
|
171
181
|
|
|
182
|
+
// ── this machine's device key (standing grants are bound to it) ──
|
|
183
|
+
// ECDSA P-256, made on first use, kept next to config.json (0600). The server only ever sees the public key and signatures.
|
|
184
|
+
let DEVICE = null;
|
|
185
|
+
async function device() {
|
|
186
|
+
if (DEVICE) return DEVICE;
|
|
187
|
+
const p = join(DIR, "device.json");
|
|
188
|
+
let d = null;
|
|
189
|
+
try { d = JSON.parse(readFileSync(p, "utf8")); } catch {}
|
|
190
|
+
if (!d || !d.pkcs8 || !d.spki) {
|
|
191
|
+
const kp = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
|
|
192
|
+
d = { v: 1, spki: b64url(new Uint8Array(await crypto.subtle.exportKey("spki", kp.publicKey))), pkcs8: b64(new Uint8Array(await crypto.subtle.exportKey("pkcs8", kp.privateKey))), created: new Date().toISOString() };
|
|
193
|
+
saveFile(p, JSON.stringify(d, null, 2));
|
|
194
|
+
}
|
|
195
|
+
const key = await crypto.subtle.importKey("pkcs8", unb64(d.pkcs8), { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
|
|
196
|
+
return (DEVICE = { spki: d.spki, key });
|
|
197
|
+
}
|
|
198
|
+
export async function deviceFingerprint(spki) {
|
|
199
|
+
const h = [...new Uint8Array(await crypto.subtle.digest("SHA-256", unb64(spki.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - spki.length % 4) % 4))))].map((x) => x.toString(16).padStart(2, "0")).join("").slice(0, 16).toUpperCase();
|
|
200
|
+
return h.match(/.{4}/g).join("-");
|
|
201
|
+
}
|
|
202
|
+
// 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) {
|
|
204
|
+
const d = await device();
|
|
205
|
+
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");
|
|
207
|
+
const sig = new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, d.key, enc.encode(msg)));
|
|
208
|
+
return { "X-OwlMem-Device": d.spki, "X-OwlMem-Device-Name": (process.env.OWLMEM_DEVICE_NAME || hostname()).replace(/[^\w .@()-]/g, "").slice(0, 60),
|
|
209
|
+
"X-OwlMem-Mode": mode, "X-OwlMem-Time": ts, "X-OwlMem-Nonce": nonce, "X-OwlMem-Sig": b64url(sig) };
|
|
210
|
+
}
|
|
211
|
+
|
|
172
212
|
// ── http ──
|
|
173
|
-
async function call(method, path, body) {
|
|
213
|
+
async function call(method, path, body, mode = "other") {
|
|
174
214
|
let r;
|
|
175
215
|
try {
|
|
176
|
-
|
|
216
|
+
const sh = await signHeaders(method, path, mode).catch(() => ({})); // can't write the device key: no standing grants, everything else still works
|
|
217
|
+
r = await fetch(BASE + "/v1/vault/" + path, { method, headers: { Authorization: "Bearer " + token(), "Content-Type": "application/json", "User-Agent": "owlmem-cli/" + VERSION, ...sh },
|
|
177
218
|
body: body === undefined ? undefined : JSON.stringify(body) });
|
|
178
219
|
} catch (e) { throw new Fail("Can't reach OwlMem (" + BASE + "): " + e.message); }
|
|
179
220
|
const data = await r.json().catch(() => ({}));
|
|
@@ -181,10 +222,10 @@ async function call(method, path, body) {
|
|
|
181
222
|
}
|
|
182
223
|
// Ask, and if the owner has to approve, wait for it (polling every 3 s, up to 5 minutes), then ask again.
|
|
183
224
|
let WAIT_MS = 5 * 60 * 1000; // 命令行等 5 分钟;MCP 里只等 30 秒,免得 AI 客户端的工具调用超时
|
|
184
|
-
async function approved(method, path, body, kind = "access") {
|
|
225
|
+
async function approved(method, path, body, kind = "access", mode = "other") {
|
|
185
226
|
let extra = "";
|
|
186
227
|
for (let round = 0; round < 3; round++) {
|
|
187
|
-
const r = await call(method, path + extra, body);
|
|
228
|
+
const r = await call(method, path + extra, body, mode);
|
|
188
229
|
if (r.status < 400) return r.data;
|
|
189
230
|
if (r.data.error !== "approval_required") throw new Fail(r.data.detail || r.data.error || "HTTP " + r.status);
|
|
190
231
|
const id = r.data.request;
|
|
@@ -196,7 +237,7 @@ async function approved(method, path, body, kind = "access") {
|
|
|
196
237
|
st = (await call("GET", "requests/" + id)).data.status;
|
|
197
238
|
if (st !== "pending") break;
|
|
198
239
|
}
|
|
199
|
-
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.`);
|
|
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).`);
|
|
200
241
|
if (st !== "approved") throw new Fail(st === "denied" ? "The request was denied." : "The request expired before it was approved.");
|
|
201
242
|
say("Approved.");
|
|
202
243
|
if (kind !== "access") extra = (path.includes("?") ? "&" : "?") + "request=" + encodeURIComponent(id);
|
|
@@ -311,6 +352,21 @@ export function guessCSV(text) {
|
|
|
311
352
|
}
|
|
312
353
|
// <<< vault core
|
|
313
354
|
|
|
355
|
+
// ── opening a browser ──
|
|
356
|
+
// Only for the real service: OWLMEM_NO_BROWSER=1 or BROWSER=none turns it off, and so does any OWLMEM_URL that isn't owlmem.com
|
|
357
|
+
// (tests and local previews run on 127.0.0.1 — 10-24 a test run kept opening tabs in the owner's browser). The link is always printed.
|
|
358
|
+
export function browserAllowed(env = process.env) {
|
|
359
|
+
if (env.OWLMEM_NO_BROWSER && env.OWLMEM_NO_BROWSER !== "0") return false;
|
|
360
|
+
if (String(env.BROWSER || "").toLowerCase() === "none") return false;
|
|
361
|
+
let host = "";
|
|
362
|
+
try { const u = new URL(env.OWLMEM_URL || "https://owlmem.com"); if (u.protocol !== "https:") return false; host = u.hostname; } catch { return false; }
|
|
363
|
+
return host === "owlmem.com" || host.endsWith(".owlmem.com");
|
|
364
|
+
}
|
|
365
|
+
function openBrowser(url) {
|
|
366
|
+
if (!browserAllowed()) return false;
|
|
367
|
+
try { spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open", [url], { stdio: "ignore", detached: true }).on("error", () => {}).unref(); return true; } catch { return false; }
|
|
368
|
+
}
|
|
369
|
+
|
|
314
370
|
// ── unlock via the console ──
|
|
315
371
|
async function unlockInBrowser(space) {
|
|
316
372
|
const kp = await unlockKeypair();
|
|
@@ -318,7 +374,7 @@ async function unlockInBrowser(space) {
|
|
|
318
374
|
if (r.status >= 400) throw new Fail(r.data.detail || r.data.error);
|
|
319
375
|
const fp = await unlockFingerprint(kp.pubkey);
|
|
320
376
|
say("Open this link, sign in, and approve with your passkey:\n\n " + r.data.url + "\n\nThe console will show a code — check it matches: " + fp + "\nWaiting (up to 10 minutes)…");
|
|
321
|
-
|
|
377
|
+
openBrowser(r.data.url);
|
|
322
378
|
const wait = Number(process.env.OWLMEM_POLL_MS || 2000), until = Date.now() + 10 * 60 * 1000;
|
|
323
379
|
while (Date.now() < until) {
|
|
324
380
|
await new Promise((res) => setTimeout(res, wait));
|
|
@@ -350,9 +406,9 @@ function ask(question, hidden = true) {
|
|
|
350
406
|
|
|
351
407
|
// ── commands ──
|
|
352
408
|
function flag(args, name, dflt) { const i = args.indexOf(name); if (i < 0) return dflt; const v = args[i + 1]; args.splice(i, 2); return v; }
|
|
353
|
-
async function fetchItem(name) {
|
|
409
|
+
async function fetchItem(name, mode = "view") {
|
|
354
410
|
const sp = await space(); const raw = spaceKey(sp.space);
|
|
355
|
-
const it = await approved("GET", itemPath(name));
|
|
411
|
+
const it = await approved("GET", itemPath(name), undefined, "access", mode);
|
|
356
412
|
return open(raw, sp.space, name, it.nonce, it.ciphertext);
|
|
357
413
|
}
|
|
358
414
|
function pick(body, field) {
|
|
@@ -366,7 +422,7 @@ function pick(body, field) {
|
|
|
366
422
|
const MCP_TOOLS = [
|
|
367
423
|
{ name: "vault_list", description: "List the credentials in the team vault (names and descriptions only; no secrets).", inputSchema: { type: "object", properties: {} } },
|
|
368
424
|
{ name: "vault_run",
|
|
369
|
-
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).",
|
|
425
|
+
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.",
|
|
370
426
|
inputSchema: { type: "object", properties: {
|
|
371
427
|
command: { type: "string", description: "Shell command to run (sh -c)" },
|
|
372
428
|
env: { type: "object", description: 'Map of ENV_VAR to "item" or "item:field", e.g. {"DB_PASSWORD": "prod/db:password"}', additionalProperties: { type: "string" } },
|
|
@@ -396,7 +452,7 @@ async function mcpCall(name, a) {
|
|
|
396
452
|
for (const [k, ref] of Object.entries(a.env || {})) {
|
|
397
453
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) throw new Fail("Bad variable name " + k);
|
|
398
454
|
const [item, field] = String(ref).split(":");
|
|
399
|
-
const v = pick(await fetchItem(item), field); env[k] = v; if (v && v.length >= 4) secrets.push(v);
|
|
455
|
+
const v = pick(await fetchItem(item, "run"), field); env[k] = v; if (v && v.length >= 4) secrets.push(v);
|
|
400
456
|
}
|
|
401
457
|
const mask = (t) => secrets.sort((x, y) => y.length - x.length).reduce((acc, v) => acc.split(v).join("****"), t);
|
|
402
458
|
const out = await new Promise((res) => {
|
|
@@ -434,10 +490,309 @@ async function mcpServe() {
|
|
|
434
490
|
await new Promise((res) => process.stdin.on("end", res));
|
|
435
491
|
}
|
|
436
492
|
|
|
493
|
+
// ── install: connect the AI apps on this machine to OwlMem's MCP server ──
|
|
494
|
+
// The snippets here must say the same as the website (design/owlmem/site.py CODE); tests/test_owlmem_install.py compares them.
|
|
495
|
+
export const MCP_URL = "https://mcp.owlmem.com/mcp";
|
|
496
|
+
export const RULES = `## Team memory (OwlMem)
|
|
497
|
+
- Before starting a task, use kb_search (documents) and kb_recall (facts) for the topic.
|
|
498
|
+
- When you learn something reusable — a fix, a gotcha, a decision — save it with kb_write
|
|
499
|
+
under lessons/ or projects/<name>/. Search first and update the existing page instead of
|
|
500
|
+
creating a duplicate.
|
|
501
|
+
- Store facts that change (prices, versions, status) with kb_retain and snapshot=true or an
|
|
502
|
+
expiry, so they retire on their own.
|
|
503
|
+
- Never put passwords or API keys in documents or memory. Use the OwlMem vault.`;
|
|
504
|
+
const MARK_START = "<!-- owlmem:start -->", MARK_END = "<!-- owlmem:end -->";
|
|
505
|
+
const DOCS = "https://owlmem.com/docs#";
|
|
506
|
+
// What each app's config file gets under its servers key (same shapes as on owlmem.com/docs)
|
|
507
|
+
export const ENTRIES = {
|
|
508
|
+
cursor: { url: MCP_URL },
|
|
509
|
+
vscode: { type: "http", url: MCP_URL },
|
|
510
|
+
windsurf: { serverUrl: MCP_URL },
|
|
511
|
+
gemini: { httpUrl: MCP_URL },
|
|
512
|
+
"claude-desktop": { command: "npx", args: ["-y", "mcp-remote", MCP_URL] },
|
|
513
|
+
};
|
|
514
|
+
export const CLAUDE_ADD = ["mcp", "add", "--transport", "http", "--scope", "user", "owlmem", MCP_URL];
|
|
515
|
+
export const CODEX_TOML = `[mcp_servers.owlmem]\nurl = "${MCP_URL}"`;
|
|
516
|
+
|
|
517
|
+
function onPath(name) {
|
|
518
|
+
const exts = process.platform === "win32" ? ["", ".cmd", ".exe", ".bat"] : [""];
|
|
519
|
+
for (const d of (process.env.PATH || "").split(process.platform === "win32" ? ";" : ":")) {
|
|
520
|
+
if (!d) continue;
|
|
521
|
+
for (const e of exts) { const p = join(d, name + e); if (existsSync(p)) return p; }
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
function home() { return process.env.HOME || process.env.USERPROFILE || homedir(); }
|
|
526
|
+
function platform() { return process.env.OWLMEM_PLATFORM || process.platform; } // OWLMEM_PLATFORM: tests only
|
|
527
|
+
function appData() { // per-user application data folder (VS Code, Claude Desktop)
|
|
528
|
+
const p = platform(), h = home();
|
|
529
|
+
if (p === "darwin") return join(h, "Library", "Application Support");
|
|
530
|
+
if (p === "win32") return process.env.APPDATA || join(h, "AppData", "Roaming");
|
|
531
|
+
return process.env.XDG_CONFIG_HOME || join(h, ".config");
|
|
532
|
+
}
|
|
533
|
+
const readText = (f) => { try { return readFileSync(f, "utf8"); } catch { return null; } };
|
|
534
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
535
|
+
|
|
536
|
+
// Client table. detect(): is this app on this machine? Each plan returns {state, ops, note}.
|
|
537
|
+
// state: "ok" (already set up) | "add" | "update" | "remove" | "absent" (nothing to remove) | "fail"
|
|
538
|
+
// ops: [{type:"write", file, text} | {type:"run", argv}]
|
|
539
|
+
function jsonClient(id, name, file, key, entry) {
|
|
540
|
+
return { id, name, file, key, entry, docs: DOCS + ({ windsurf: "others", gemini: "others", "claude-desktop": "claude" }[id] || id) };
|
|
541
|
+
}
|
|
542
|
+
export function clients() {
|
|
543
|
+
const h = home(), ad = appData();
|
|
544
|
+
const codexHome = process.env.CODEX_HOME || join(h, ".codex");
|
|
545
|
+
const list = [
|
|
546
|
+
{ id: "claude-code", name: "Claude Code", docs: DOCS + "claude-code", rules: join(process.env.CLAUDE_CONFIG_DIR || join(h, ".claude"), "CLAUDE.md"),
|
|
547
|
+
detect: () => !!onPath("claude") },
|
|
548
|
+
{ id: "codex", name: "Codex", docs: DOCS + "codex", file: join(codexHome, "config.toml"), rules: join(codexHome, "AGENTS.md"),
|
|
549
|
+
detect: () => !!onPath("codex") || existsSync(codexHome) },
|
|
550
|
+
Object.assign(jsonClient("cursor", "Cursor", join(h, ".cursor", "mcp.json"), "mcpServers", ENTRIES.cursor),
|
|
551
|
+
{ detect: () => existsSync(join(h, ".cursor")) || !!onPath("cursor") }),
|
|
552
|
+
Object.assign(jsonClient("vscode", "VS Code", join(ad, "Code", "User", "mcp.json"), "servers", ENTRIES.vscode),
|
|
553
|
+
{ detect: () => existsSync(join(ad, "Code", "User")) }),
|
|
554
|
+
Object.assign(jsonClient("vscode-insiders", "VS Code Insiders", join(ad, "Code - Insiders", "User", "mcp.json"), "servers", ENTRIES.vscode),
|
|
555
|
+
{ detect: () => existsSync(join(ad, "Code - Insiders", "User")), docs: DOCS + "vscode", optional: true }),
|
|
556
|
+
Object.assign(jsonClient("windsurf", "Windsurf", join(h, ".codeium", "windsurf", "mcp_config.json"), "mcpServers", ENTRIES.windsurf),
|
|
557
|
+
{ detect: () => existsSync(join(h, ".codeium", "windsurf")) }),
|
|
558
|
+
Object.assign(jsonClient("gemini", "Gemini CLI", join(h, ".gemini", "settings.json"), "mcpServers", ENTRIES.gemini),
|
|
559
|
+
{ detect: () => existsSync(join(h, ".gemini")) || !!onPath("gemini"), rules: join(h, ".gemini", "GEMINI.md") }),
|
|
560
|
+
Object.assign(jsonClient("claude-desktop", "Claude Desktop", join(ad, "Claude", "claude_desktop_config.json"), "mcpServers", ENTRIES["claude-desktop"]),
|
|
561
|
+
{ detect: () => existsSync(join(ad, "Claude")) }),
|
|
562
|
+
];
|
|
563
|
+
return list;
|
|
564
|
+
}
|
|
565
|
+
export const CLIENT_IDS = ["claude-code", "codex", "cursor", "vscode", "vscode-insiders", "windsurf", "gemini", "claude-desktop"];
|
|
566
|
+
|
|
567
|
+
function planJSON(c, uninstall) {
|
|
568
|
+
const text = readText(c.file);
|
|
569
|
+
let obj = {};
|
|
570
|
+
if (text !== null && text.trim()) {
|
|
571
|
+
try { obj = JSON.parse(text); } catch {
|
|
572
|
+
const hint = /\/\/|\/\*/.test(text) ? "it has comments or isn't plain JSON" : "it isn't valid JSON";
|
|
573
|
+
return { state: "fail", ops: [], note: `${c.file}: ${hint}, so it was left alone. Add OwlMem by hand: ${c.docs}` };
|
|
574
|
+
}
|
|
575
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return { state: "fail", ops: [], note: `${c.file}: unexpected content, left alone. Add OwlMem by hand: ${c.docs}` };
|
|
576
|
+
}
|
|
577
|
+
const servers = obj[c.key] && typeof obj[c.key] === "object" && !Array.isArray(obj[c.key]) ? obj[c.key] : null;
|
|
578
|
+
const cur = servers ? servers.owlmem : undefined;
|
|
579
|
+
if (uninstall) {
|
|
580
|
+
if (cur === undefined) return { state: "absent", ops: [] };
|
|
581
|
+
const next = { ...obj, [c.key]: { ...servers } }; delete next[c.key].owlmem;
|
|
582
|
+
return { state: "remove", ops: [{ type: "write", file: c.file, text: JSON.stringify(next, null, 2) + "\n" }] };
|
|
583
|
+
}
|
|
584
|
+
if (same(cur, c.entry)) return { state: "ok", ops: [] };
|
|
585
|
+
const next = { ...obj, [c.key]: { ...(servers || {}), owlmem: c.entry } };
|
|
586
|
+
return { state: cur === undefined ? "add" : "update", ops: [{ type: "write", file: c.file, text: JSON.stringify(next, null, 2) + "\n" }],
|
|
587
|
+
note: cur === undefined ? "" : "replaced the existing owlmem entry " + JSON.stringify(cur) };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Codex: ~/.codex/config.toml. Only the [mcp_servers.owlmem] table (and its sub-tables) is touched; everything else is kept as is.
|
|
591
|
+
export function tomlSplit(text) {
|
|
592
|
+
const rest = [], section = []; let mine = false;
|
|
593
|
+
for (const l of text.split("\n")) {
|
|
594
|
+
if (/^\s*\[\[/.test(l)) mine = false;
|
|
595
|
+
else { const m = /^\s*\[\s*([^\]]+?)\s*\]\s*(#.*)?$/.exec(l); if (m) { const n = m[1].replace(/["'\s]/g, ""); mine = n === "mcp_servers.owlmem" || n.startsWith("mcp_servers.owlmem."); } }
|
|
596
|
+
(mine ? section : rest).push(l);
|
|
597
|
+
}
|
|
598
|
+
return { rest: rest.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, ""), section: section.join("\n").trim(), found: section.length > 0 };
|
|
599
|
+
}
|
|
600
|
+
function planCodex(c, uninstall) {
|
|
601
|
+
const text = readText(c.file) ?? "";
|
|
602
|
+
if (/^\s*mcp_servers\s*[.=]|^\s*owlmem\s*=\s*\{/m.test(text))
|
|
603
|
+
return { state: "fail", ops: [], note: `${c.file} defines MCP servers in a form this installer doesn't edit; left alone. Add OwlMem by hand: ${c.docs}` };
|
|
604
|
+
const { rest, section, found } = tomlSplit(text);
|
|
605
|
+
if (uninstall) return found ? { state: "remove", ops: [{ type: "write", file: c.file, text: rest ? rest + "\n" : "" }] } : { state: "absent", ops: [] };
|
|
606
|
+
const norm = (s) => s.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).join("\n");
|
|
607
|
+
if (found && norm(section) === norm(CODEX_TOML)) return { state: "ok", ops: [] };
|
|
608
|
+
return { state: found ? "update" : "add", ops: [{ type: "write", file: c.file, text: (rest ? rest + "\n\n" : "") + CODEX_TOML + "\n" }],
|
|
609
|
+
note: found ? "replaced the existing [mcp_servers.owlmem] table" : "" };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Claude Code: through its own command (`claude mcp add … --scope user`); ~/.claude.json tells us what is there already.
|
|
613
|
+
function planClaude(c, uninstall) {
|
|
614
|
+
let cur;
|
|
615
|
+
try { cur = (JSON.parse(readText(join(home(), ".claude.json")) || "{}").mcpServers || {}).owlmem; } catch { cur = undefined; }
|
|
616
|
+
const rm = { type: "run", argv: ["claude", "mcp", "remove", "owlmem", "--scope", "user"], ignoreFail: !uninstall };
|
|
617
|
+
if (uninstall) return cur === undefined ? { state: "absent", ops: [] } : { state: "remove", ops: [rm] };
|
|
618
|
+
if (cur && cur.url === MCP_URL && (cur.type === "http" || cur.type === "streamable-http") && !cur.headers) return { state: "ok", ops: [] };
|
|
619
|
+
return { state: cur === undefined ? "add" : "update", ops: [...(cur === undefined ? [] : [rm]), { type: "run", argv: ["claude", ...CLAUDE_ADD], retryWith: rm }],
|
|
620
|
+
note: cur === undefined ? "" : "replaced the existing owlmem server " + JSON.stringify(cur) };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function withRules(text, uninstall) {
|
|
624
|
+
const t = text ?? "";
|
|
625
|
+
const re = new RegExp("\\n*" + MARK_START + "[\\s\\S]*?" + MARK_END + "\\n?");
|
|
626
|
+
if (uninstall) return re.test(t) ? t.replace(re, "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n") : t;
|
|
627
|
+
const block = MARK_START + "\n" + RULES + "\n" + MARK_END + "\n";
|
|
628
|
+
if (re.test(t)) return t.replace(re, (m) => (m.startsWith("\n") ? "\n\n" : "") + block);
|
|
629
|
+
return t.trim() ? t.replace(/\s*$/, "\n\n") + block : block;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function plan(opts = {}) {
|
|
633
|
+
const only = opts.only ? new Set(opts.only) : null;
|
|
634
|
+
const out = [];
|
|
635
|
+
for (const c of clients()) {
|
|
636
|
+
if (only && !only.has(c.id) && !(c.id === "vscode-insiders" && only.has("vscode"))) continue;
|
|
637
|
+
const r = { id: c.id, name: c.name, docs: c.docs, file: c.file, ops: [], note: "" };
|
|
638
|
+
if (!c.detect()) { if (!c.optional || only) Object.assign(r, { state: "missing" }); else continue; out.push(r); continue; }
|
|
639
|
+
Object.assign(r, c.id === "claude-code" ? planClaude(c, opts.uninstall) : c.id === "codex" ? planCodex(c, opts.uninstall) : planJSON(c, opts.uninstall));
|
|
640
|
+
if (c.rules && !opts.noRules && r.state !== "fail") {
|
|
641
|
+
const before = readText(c.rules), after = withRules(before, opts.uninstall);
|
|
642
|
+
if (after !== (before ?? "") && !(opts.uninstall && before === null)) { r.ops.push({ type: "write", file: c.rules, text: after, rules: true }); r.rulesChanged = true; }
|
|
643
|
+
r.rulesFile = c.rules;
|
|
644
|
+
}
|
|
645
|
+
out.push(r);
|
|
646
|
+
}
|
|
647
|
+
return out;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function stamp() { const d = new Date(), p = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`; }
|
|
651
|
+
function runCmd(argv) {
|
|
652
|
+
const r = spawnSync(argv[0], argv.slice(1), { encoding: "utf8", shell: process.platform === "win32", timeout: 60000 });
|
|
653
|
+
return { ok: r.status === 0, out: ((r.stderr || "") + (r.stdout || "")).trim() || (r.error ? r.error.message : "") };
|
|
654
|
+
}
|
|
655
|
+
export function apply(items) {
|
|
656
|
+
const ts = stamp();
|
|
657
|
+
for (const r of items) {
|
|
658
|
+
r.backups = [];
|
|
659
|
+
try {
|
|
660
|
+
for (const op of r.ops) {
|
|
661
|
+
if (op.type === "write") {
|
|
662
|
+
mkdirSync(dirname(op.file), { recursive: true });
|
|
663
|
+
if (existsSync(op.file)) { const b = op.file + ".bak-owlmem-" + ts; copyFileSync(op.file, b); r.backups.push(b); }
|
|
664
|
+
writeFileSync(op.file, op.text);
|
|
665
|
+
} else {
|
|
666
|
+
let res = runCmd(op.argv);
|
|
667
|
+
if (!res.ok && op.retryWith && /already exists/i.test(res.out)) { runCmd(op.retryWith.argv); res = runCmd(op.argv); }
|
|
668
|
+
if (!res.ok && !op.ignoreFail) throw new Error("`" + op.argv.join(" ") + "` failed: " + (res.out.split("\n").slice(-3).join(" ") || "no output"));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
} catch (e) { r.state = "fail"; r.note = (e.message || String(e)) + " — set it up by hand: " + r.docs; }
|
|
672
|
+
}
|
|
673
|
+
return items;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const tilde = (f) => { const h = home(); return f && h && f.startsWith(h + (platform() === "win32" ? "\\" : "/")) ? "~" + f.slice(h.length) : f; };
|
|
677
|
+
const SYM = { ok: "✓", add: "✓", update: "↺", remove: "✓", absent: "–", missing: "–", fail: "✗" };
|
|
678
|
+
function describe(r, done, uninstall) {
|
|
679
|
+
const verb = {
|
|
680
|
+
ok: "already set up", add: done ? "added" : "will add OwlMem", update: done ? "updated" : "will update the owlmem entry",
|
|
681
|
+
remove: done ? "removed" : "will remove OwlMem", absent: "not configured", missing: "not installed", fail: "failed",
|
|
682
|
+
}[r.state];
|
|
683
|
+
const sym = !done && r.state === "add" ? "+" : !done && r.state === "remove" ? "-" : SYM[r.state];
|
|
684
|
+
const lines = [`${sym} ${r.name.padEnd(17)} ${verb}${r.state !== "missing" && r.file && r.state !== "fail" ? " (" + tilde(r.file) + ")" : ""}`];
|
|
685
|
+
if (r.note) lines.push(" " + r.note);
|
|
686
|
+
for (const op of r.ops) {
|
|
687
|
+
if (op.type === "run" && !done) lines.push(" run: " + op.argv.join(" "));
|
|
688
|
+
if (op.rules) lines.push(` ${done ? "" : "will "}${uninstall ? (done ? "removed" : "remove") : (done ? "wrote" : "write")} the OwlMem rules block in ${tilde(op.file)}`);
|
|
689
|
+
}
|
|
690
|
+
if (done && r.backups?.length) lines.push(" backup: " + r.backups.join(", "));
|
|
691
|
+
return lines.join("\n");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const LOGIN = {
|
|
695
|
+
"claude-code": "Claude Code: run /mcp, pick owlmem, choose Authenticate.",
|
|
696
|
+
codex: "Codex: run `codex mcp login owlmem`.",
|
|
697
|
+
cursor: "Cursor: Settings → MCP, click \"Needs login\" next to owlmem.",
|
|
698
|
+
vscode: "VS Code: it asks you to sign in the first time Copilot uses owlmem (or run \"MCP: List Servers\" → owlmem → Start).",
|
|
699
|
+
"vscode-insiders": "VS Code Insiders: same as VS Code.",
|
|
700
|
+
windsurf: "Windsurf: open Settings → MCP servers and refresh; sign in when it asks.",
|
|
701
|
+
gemini: "Gemini CLI: run `/mcp auth owlmem`.",
|
|
702
|
+
"claude-desktop": "Claude Desktop: restart it; the sign-in page opens on the first start (via mcp-remote).",
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
async function install(args) {
|
|
706
|
+
const opts = { dryRun: false, yes: false, noRules: false, uninstall: false, only: null };
|
|
707
|
+
for (let i = 0; i < args.length; i++) {
|
|
708
|
+
const a = args[i];
|
|
709
|
+
if (a === "--dry-run") opts.dryRun = true;
|
|
710
|
+
else if (a === "--yes" || a === "-y") opts.yes = true;
|
|
711
|
+
else if (a === "--no-rules") opts.noRules = true;
|
|
712
|
+
else if (a === "--uninstall") opts.uninstall = true;
|
|
713
|
+
else if (a === "--only" || a.startsWith("--only=")) {
|
|
714
|
+
const v = a.includes("=") ? a.slice(7) : args[++i];
|
|
715
|
+
opts.only = String(v || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
716
|
+
const bad = opts.only.filter((x) => !CLIENT_IDS.includes(x));
|
|
717
|
+
if (bad.length || !opts.only.length) throw new Fail(`--only takes a comma-separated list of: ${CLIENT_IDS.join(", ")}`);
|
|
718
|
+
} else if (a === "--help" || a === "-h") { console.log(INSTALL_HELP); return; }
|
|
719
|
+
else throw new Fail("Unknown option " + a + "\n\n" + INSTALL_HELP);
|
|
720
|
+
}
|
|
721
|
+
const items = plan(opts);
|
|
722
|
+
const changes = items.filter((r) => r.ops.length);
|
|
723
|
+
const out = (s = "") => console.log(s);
|
|
724
|
+
out(opts.uninstall ? "OwlMem uninstall — " + (opts.dryRun ? "dry run, nothing is changed" : "plan") : "OwlMem install — connects the AI apps on this computer to " + MCP_URL + (opts.dryRun ? " (dry run, nothing is changed)" : ""));
|
|
725
|
+
out();
|
|
726
|
+
for (const r of items) out(describe(r, false, opts.uninstall));
|
|
727
|
+
out();
|
|
728
|
+
if (!changes.length) {
|
|
729
|
+
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
|
+
if (!opts.uninstall && items.some((r) => r.state === "ok")) nextSteps(items, out);
|
|
731
|
+
process.exitCode = items.some((r) => r.state === "fail") ? 1 : 0;
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (opts.dryRun) { out(`Dry run: ${changes.length} app(s) would change. Run again without --dry-run to apply.`); return; }
|
|
735
|
+
const tty = process.stdin.isTTY && process.stdout.isTTY;
|
|
736
|
+
if (tty && !opts.yes) {
|
|
737
|
+
const ans = (await ask("Apply these changes? [Y/n] ", false)).toLowerCase();
|
|
738
|
+
if (ans && !ans.startsWith("y")) { out("Nothing changed."); return; }
|
|
739
|
+
}
|
|
740
|
+
apply(changes);
|
|
741
|
+
out();
|
|
742
|
+
for (const r of changes) out(describe(r, true, opts.uninstall));
|
|
743
|
+
out();
|
|
744
|
+
if (opts.uninstall) out("OwlMem was removed from those apps. Backups of every changed file sit next to it (*.bak-owlmem-*).");
|
|
745
|
+
else {
|
|
746
|
+
nextSteps(items, out);
|
|
747
|
+
const codex = items.find((r) => r.id === "codex" && r.state !== "fail" && r.state !== "missing");
|
|
748
|
+
if (codex && tty && !opts.yes && onPath("codex") && browserAllowed()) { // codex mcp login opens a browser: same rule as openBrowser
|
|
749
|
+
const ans = (await ask("Sign Codex in now? This runs `codex mcp login owlmem` and opens your browser. [y/N] ", false)).toLowerCase();
|
|
750
|
+
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
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (items.some((r) => r.state === "fail")) process.exitCode = 1;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function nextSteps(items, out) {
|
|
757
|
+
const live = items.filter((r) => ["ok", "add", "update"].includes(r.state));
|
|
758
|
+
if (!live.length) return;
|
|
759
|
+
out("Next: sign in once in each app. The first time an app uses OwlMem it opens the owlmem.com sign-in page —");
|
|
760
|
+
out("sign in with your email (new accounts get a free space) and click \"Allow\". An AI can't click it for you.");
|
|
761
|
+
for (const r of live) out(" • " + LOGIN[r.id]);
|
|
762
|
+
if (live.some((r) => ["cursor", "vscode", "vscode-insiders", "windsurf"].includes(r.id)))
|
|
763
|
+
out("Cursor, VS Code and Windsurf have no global rules file: add the rules from https://owlmem.com/docs#instructions to your project rules.");
|
|
764
|
+
if (live.some((r) => r.id === "claude-desktop")) out("Claude Desktop: restart it to load OwlMem. (Or add https://mcp.owlmem.com/mcp under Settings → Connectors instead.)");
|
|
765
|
+
out("Already-running sessions don't see new MCP servers: start a new session, then ask your AI to run kb_search.");
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const INSTALL_HELP = `Usage: npx -y owlmem install [options]
|
|
769
|
+
|
|
770
|
+
Connects Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI and Claude Desktop on this computer to
|
|
771
|
+
OwlMem (${MCP_URL}). Config files are merged, never overwritten; each changed file is backed up first
|
|
772
|
+
as <file>.bak-owlmem-<time>. Also adds the OwlMem usage rules to CLAUDE.md / AGENTS.md / GEMINI.md.
|
|
773
|
+
|
|
774
|
+
--dry-run show what would change, change nothing
|
|
775
|
+
--yes, -y don't ask (non-interactive runs, e.g. an AI running this, never ask)
|
|
776
|
+
--only a,b only these apps: ${CLIENT_IDS.join(", ")}
|
|
777
|
+
--no-rules don't touch CLAUDE.md / AGENTS.md / GEMINI.md
|
|
778
|
+
--uninstall remove OwlMem entries and the rules block (with backups)
|
|
779
|
+
|
|
780
|
+
Signing in happens in each app the first time it uses OwlMem (standard MCP OAuth): you click "Allow" once.`;
|
|
781
|
+
|
|
437
782
|
export async function main(argv) {
|
|
438
783
|
const [cmd, sub, ...rest] = argv;
|
|
439
784
|
if (cmd === "--version" || cmd === "version") { console.log(VERSION); return; }
|
|
785
|
+
if (cmd === "install") { await install(argv.slice(1)); return; }
|
|
786
|
+
if (cmd === "uninstall") { await install(["--uninstall", ...argv.slice(1)]); return; }
|
|
440
787
|
if (cmd === "mcp") { await mcpServe(); return; }
|
|
788
|
+
if (cmd === "device") { // register this machine for standing grants and show its fingerprint
|
|
789
|
+
const r = await call("POST", "device", {});
|
|
790
|
+
if (r.status >= 400) throw new Fail(r.data.detail || r.data.error || "HTTP " + r.status);
|
|
791
|
+
say("This machine: " + (process.env.OWLMEM_DEVICE_NAME || hostname()));
|
|
792
|
+
console.log(r.data.device);
|
|
793
|
+
say("Registered for " + r.data.agent + ". In the OwlMem console (Vault → Standing grants) pick the machine with this fingerprint.");
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
441
796
|
if (cmd === "login") {
|
|
442
797
|
const t = (process.env.OWLMEM_TOKEN || await ask("Paste this agent's token (om_…, shown as dots), then Enter: ")).replace(/\x1b\[[0-9;]*[~A-Za-z]/g, "").replace(/[^A-Za-z0-9_-]/g, "");
|
|
443
798
|
if (!/^om_[A-Za-z0-9_-]{20,80}$/.test(t)) throw new Fail("That doesn't look like an OwlMem agent token (om_…).");
|
|
@@ -455,13 +810,13 @@ export async function main(argv) {
|
|
|
455
810
|
if (opts[i] !== "--env") throw new Fail("Unknown option " + opts[i]);
|
|
456
811
|
const m = /^([A-Za-z_][A-Za-z0-9_]*)=([^:]+)(?::(.+))?$/.exec(opts[++i] || "");
|
|
457
812
|
if (!m) throw new Fail("Use --env VAR=item or --env VAR=item:field");
|
|
458
|
-
env[m[1]] = pick(await fetchItem(m[2]), m[3] || (undefined));
|
|
813
|
+
env[m[1]] = pick(await fetchItem(m[2], "run"), m[3] || (undefined));
|
|
459
814
|
}
|
|
460
815
|
const child = spawn(command[0], command.slice(1), { stdio: "inherit", env });
|
|
461
816
|
await new Promise((res) => child.on("exit", (code, sig) => { process.exitCode = sig ? 1 : code; res(); }));
|
|
462
817
|
return;
|
|
463
818
|
}
|
|
464
|
-
if (cmd !== "vault") {
|
|
819
|
+
if (cmd !== "vault") { { const ls = readFileSync(new URL(import.meta.url), "utf8").split("\n").slice(1); say(ls.slice(0, ls.findIndex((l) => !l.startsWith("//"))).map((l) => l.slice(3)).join("\n")); } process.exitCode = cmd ? 2 : 0; return; }
|
|
465
820
|
const args = [...rest];
|
|
466
821
|
if (sub === "keygen") {
|
|
467
822
|
const sp = process.env.OWLMEM_SPACE || (await space().catch(() => ({}))).space;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "owlmem",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "OwlMem command line: end-to-end encrypted vault for your AI agents, and a local MCP server.",
|
|
3
|
+
"version": "0.6.0",
|
|
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" },
|
|
7
7
|
"files": ["owlmem.mjs", "README.md", "LICENSE"],
|