@iamem/amem 0.1.1 → 0.1.3

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 (47) hide show
  1. package/README.md +98 -4
  2. package/desktop/icons/icon-1024.png +0 -0
  3. package/desktop/icons/icon.icns +0 -0
  4. package/desktop/icons/icon.png +0 -0
  5. package/desktop/main.js +100 -0
  6. package/desktop/package.json +16 -0
  7. package/desktop/preload.js +4 -0
  8. package/desktop/scripts/ensure-electron.mjs +122 -0
  9. package/dist/api/routes.js +331 -1
  10. package/dist/app-shell.d.ts +13 -0
  11. package/dist/app-shell.js +126 -0
  12. package/dist/attest.d.ts +13 -0
  13. package/dist/attest.js +44 -0
  14. package/dist/cli.js +258 -2
  15. package/dist/context.d.ts +10 -1
  16. package/dist/context.js +105 -3
  17. package/dist/db.d.ts +141 -0
  18. package/dist/db.js +398 -0
  19. package/dist/embed.js +5 -14
  20. package/dist/hook.js +8 -1
  21. package/dist/hygiene.d.ts +1 -2
  22. package/dist/hygiene.js +1 -2
  23. package/dist/install/hosts.js +8 -0
  24. package/dist/install/skills.js +10 -5
  25. package/dist/license.d.ts +1 -0
  26. package/dist/license.js +21 -19
  27. package/dist/mcp.js +221 -0
  28. package/dist/policy.d.ts +6 -0
  29. package/dist/policy.js +16 -1
  30. package/dist/publish.d.ts +1 -1
  31. package/dist/publish.js +1 -0
  32. package/dist/skill-capture.d.ts +43 -0
  33. package/dist/skill-capture.js +146 -0
  34. package/dist/skills.d.ts +106 -0
  35. package/dist/skills.js +422 -0
  36. package/docs/backlog.md +9 -0
  37. package/docs/npm-release.md +1 -1
  38. package/package.json +7 -2
  39. package/scripts/postinstall.js +62 -0
  40. package/skills/amem-tasks/SKILL.md +100 -0
  41. package/skills/amem-write-skill/SKILL.md +99 -0
  42. package/templates/cursor-rule.mdc +16 -6
  43. package/templates/policy.deny-default.toml +5 -0
  44. package/templates/policy.example.toml +8 -0
  45. package/ui-static/app.js +435 -227
  46. package/ui-static/index.html +11 -34
  47. package/ui-static/styles.css +299 -0
package/README.md CHANGED
@@ -52,7 +52,8 @@ Guarantees:
52
52
 
53
53
  - `~/.amem` is created with mode `0700`
54
54
  - Local UI binds to `127.0.0.1` only
55
- - No telemetry, no managed sync, no “share with org” mode
55
+ - Memory never leaves the machine — no managed sync, no “share with org” mode
56
+ - Optional anonymous install ping only (see [Telemetry](#telemetry)); opt out anytime
56
57
  - Agents are instructed to store **repo facts**, not proprietary prompting strategy
57
58
  - Optional AES-256-GCM lock and encrypted local backups — still no cloud
58
59
 
@@ -87,6 +88,22 @@ See [docs/npm-release.md](docs/npm-release.md). CI runs `npm test` and `npm run
87
88
 
88
89
  If `npm install` fails compiling native code, install Xcode CLT (macOS) or `build-essential` (Linux) and retry, or use a Node 20/22 official binary that matches the prebuild matrix.
89
90
 
91
+ ### Telemetry
92
+
93
+ On `npm install` / `npx` (outside CI and tests), amem may send a single anonymous POST to `https://getamem.com/api/beacon/npm-install` with:
94
+
95
+ - package name and version
96
+ - Node.js version
97
+ - OS platform and CPU architecture
98
+
99
+ No code, paths, usernames, emails, IPs, or memory contents are included. Opt out:
100
+
101
+ ```bash
102
+ AMEM_TELEMETRY_DISABLED=1 npm i -g @iamem/amem
103
+ ```
104
+
105
+ Memory under `~/.amem` still never leaves your machine.
106
+
90
107
  ### Quick paths
91
108
 
92
109
  ```bash
@@ -142,6 +159,17 @@ amem ui
142
159
 
143
160
  That opens `http://127.0.0.1:7843` on the **Setup** tab. It scans your home folder for git repos (skips `Library`, `node_modules`, `Downloads`, and similar noise). Check the ones you want, pick clients (Cursor, Claude Code, Windsurf, Continue, Aider, Zed, …), then **Start tracking selected**. Each pick is bound in `~/.amem` and gets the matching installer when available.
144
161
 
162
+ Prefer a desktop window instead of a browser tab (same localhost server, same privacy):
163
+
164
+ ```bash
165
+ # once per machine/checkout (downloads Electron — not included in npm i -g)
166
+ npm run app:setup
167
+
168
+ amem app
169
+ ```
170
+
171
+ `amem ui` keeps opening the browser; `amem app` opens Electron. Both talk to `127.0.0.1` only. If the UI server is already running, `amem app` attaches to it. Global installs: run `npm run app:setup` from the package directory (or clone), then `amem app`.
172
+
145
173
  The header has a **Personal** switcher (cross-repo prefs) and **Lock / backup** chrome — lock status, last backup, and a daily local schedule. Memory shows the same lock/backup chips. The Setup tab includes a copyable **remember contract** for any MCP host (`amem recipe`).
146
174
 
147
175
  Optional: check **Start amem ui when this computer logs in** so the localhost server comes back after a reboot:
@@ -156,7 +184,8 @@ Tabs after setup:
156
184
 
157
185
  1. **Setup** — scan/select repos, platforms, login auto-start, bootstrap proposal
158
186
  2. **Memory** — facts by file, scored drafts (approve / replace older / dismiss / reject noisy), edit/pin/delete, search, recent hits/misses
159
- 3. **Stats** — estimated tokens saved per LLM, plus JSON / markdown / PDF export (proxies, not a bill)
187
+ 3. **Tasks** — per-project Kanban for deferred agent work (Backlog → Next → Doing → Blocked → Done). MCP: `amem_task_add` / `amem_task_update` / `amem_task_complete`. Open tasks appear in `amem_context`. Use Memory for durable facts; Tasks for “do later.”
188
+ 4. **Stats** — estimated tokens saved per LLM, plus JSON / markdown / PDF export (proxies, not a bill)
160
189
 
161
190
  Server-only (no browser open):
162
191
 
@@ -225,6 +254,8 @@ Memory is a small local graph in SQLite:
225
254
  | **Claim** | A durable fact with file anchors (may be `active` or `superseded`; optional pin) |
226
255
  | **Edge** | Links (claim → flow → component); `kind: "supersedes"` archives the target claim |
227
256
  | **Draft** | Pending session / miss→learn proposals waiting for Memory approve |
257
+ | **Task** | Deferred work on the project Kanban (Backlog / Next / Doing / Blocked / Done) — not a durable fact |
258
+ | **Skill** | A reusable multi-step procedure, stored as a `SKILL.md` file (see below) |
228
259
  | **Usage event** | Each `amem context` hit + token estimate |
229
260
 
230
261
  Claims are the retrieval unit. Ranking combines:
@@ -252,6 +283,60 @@ Example claim:
252
283
 
253
284
  ---
254
285
 
286
+ ## Skills (procedural memory)
287
+
288
+ Claims answer *what is true*. Skills answer *how we do this here* — a deploy sequence, a
289
+ migration dance, a debugging path someone already walked. They are too long to sit in every
290
+ prompt, so they load on demand.
291
+
292
+ Skills live as `SKILL.md` files under `~/.amem/skills/`, indexed in SQLite for ranking:
293
+
294
+ ```
295
+ ~/.amem/skills/deploy-staging/SKILL.md
296
+ ~/.amem/skills/deploy-staging/references/runbook.md
297
+ ```
298
+
299
+ **Progressive disclosure.** A context packet carries only names and descriptions. The agent
300
+ calls `amem_skill_view` to pull a body once it decides the procedure applies, so an unused
301
+ library of skills costs almost no tokens.
302
+
303
+ **The learning loop.** amem ships no model, so it never writes a skill itself. At session end
304
+ it looks for the shape of a hard-won procedure — enumerated steps or real commands, plus an
305
+ error it recovered from or a correction you gave. When several signals line up it queues one
306
+ suggestion, which reaches your agent as a nudge in the next context packet. The agent writes
307
+ the skill; you approve it. If a skill was loaded during a session that still went sideways,
308
+ amem queues a revision instead of a duplicate.
309
+
310
+ The bar is deliberately high, and a session can queue at most one suggestion.
311
+
312
+ ```bash
313
+ amem skills list # index of what is stored
314
+ amem skills show deploy-staging # full body
315
+ amem skills new deploy-staging --desc "Deploy staging and verify health"
316
+ amem skills import ./some-skill # bring in an agentskills.io skill
317
+ amem skills drafts # pending suggestions and staged writes
318
+ amem skills approve <draft-id>
319
+ ```
320
+
321
+ Skills are also a Skills tab in `amem ui`.
322
+
323
+ **Safety.** Skills are instructions an agent will follow, so content is scanned for
324
+ credentials and prompt-injection patterns before any write. Three policy keys control them:
325
+
326
+ | Key | Default | Effect |
327
+ | --- | --- | --- |
328
+ | `skills_enabled` | `true` | Master switch for storage, ranking, and injection |
329
+ | `skill_write_approval` | `false` | Stage agent writes for review instead of writing to disk |
330
+ | `skill_capture` | `true` | Allow session-end skill suggestions |
331
+
332
+ An unreadable `policy.toml` forces `skill_write_approval` on. `amem doctor --attest` reports
333
+ every installed skill with a content hash, so you can diff what agents are being told to do.
334
+
335
+ > Backups currently copy the database only — `~/.amem/skills/` is not included yet. Keep
336
+ > skills you care about in version control until that lands.
337
+
338
+ ---
339
+
255
340
  ## Token savings (estimates)
256
341
 
257
342
  Every `amem context` logs a usage event. The UI **Stats** tab breaks this down by platform (`cursor`, `claude`, …).
@@ -352,6 +437,9 @@ MCP tools (stdio or HTTP):
352
437
  | `amem_context` | Ranked memory packet for the current question |
353
438
  | `amem_remember` | Store a durable fact after an outcome |
354
439
  | `amem_recipe` | Generic read-then-write contract (any MCP host) |
440
+ | `amem_skill_list` | Cheap index of stored procedures (names + descriptions only) |
441
+ | `amem_skill_view` | Load one skill body, after the index says it applies |
442
+ | `amem_skill_save` | Store a multi-step procedure you just worked out |
355
443
  | `amem_repos` | What is monitored (git repos + named workspaces) |
356
444
  | `amem_stats` | Lookup time, estimated tokens/ms saved, hit rate |
357
445
  | `amem_graph` | Claims / components / flows stored for a workspace or repo |
@@ -372,6 +460,8 @@ amem doctor [--attest] [--json]
372
460
  amem context "<query>" [--workspace <name>] [--platform …]
373
461
  amem remember "<text>" [--workspace <name>] [--kind …] [--anchor <path>]
374
462
  amem recipe [--json]
463
+ amem skills list|show <name>|new <name> [--desc <text>]|rm <name>|sync|import <path>
464
+ amem skills drafts|approve <draft-id>|dismiss <draft-id>
375
465
  amem mcp [--print-config] [--workspace <name>]
376
466
  amem propose validate|diff|apply <file.json>
377
467
  amem export [--out <file.json>]
@@ -388,6 +478,7 @@ amem usage export [--format json|md|pdf] [--days 30] [--scope current|all] [--ou
388
478
  amem license status|apply|activate|clear|issue|keys
389
479
  amem embed status|use hash|use ngram|reindex
390
480
  amem ui [--port 7843] [--no-open]
481
+ amem app [--port 7843]
391
482
  amem service install|uninstall|status
392
483
  ```
393
484
 
@@ -399,11 +490,13 @@ amem service install|uninstall|status
399
490
  | `context` | Retrieve a Markdown packet; log usage |
400
491
  | `remember` | Store one local fact |
401
492
  | `mcp` | Stdio MCP tools; HTTP MCP at `http://127.0.0.1:7843/mcp` while UI runs |
493
+ | `skills` | Manage procedural memory (`list`, `show`, `new`, `import`, `drafts`, `approve`) |
402
494
  | `propose diff` | Preview claim/component/flow changes before apply |
403
495
  | `propose apply` | Upsert structured memory locally |
404
496
  | `lock` / `unlock` | Optional AES-256-GCM encrypt-at-rest for `graph.db` |
405
497
  | `backup` | Local snapshot (optionally encrypted); `schedule` for daily timer |
406
- | `ui` | Setup wizard + Memory + Stats on localhost |
498
+ | `ui` | Setup wizard + Memory + Stats in the browser (localhost) |
499
+ | `app` | Same UI in an Electron window (`npm run app:setup` once) |
407
500
  | `service` | Login item so `amem ui` starts after reboot |
408
501
  | `doctor --attest` | Privacy/policy attestation for IT tickets |
409
502
  | `export` / `wipe` | Personal backup or delete (still local) |
@@ -490,7 +583,8 @@ IT / DevEx can govern the **fleet**: approved install, policy, attestation, offb
490
583
 
491
584
  Hard guarantees (not configurable away):
492
585
 
493
- - No telemetry
586
+ - No memory telemetry or claim upload — `~/.amem` stays local
587
+ - Optional anonymous npm install ping only (opt out: `AMEM_TELEMETRY_DISABLED=1`)
494
588
  - UI binds to loopback only (`127.0.0.1`)
495
589
  - Memory stays under `~/.amem` (mode `0700`)
496
590
 
Binary file
Binary file
Binary file
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Thin Electron shell: loads the existing amem localhost UI.
3
+ * No Node integration in the renderer — API stays on 127.0.0.1.
4
+ *
5
+ * macOS note: BrowserWindow `icon` does not change the Dock for an unpackaged
6
+ * Electron.app. We must call app.dock.setIcon() with a PNG (icns often loads empty).
7
+ */
8
+ const { app, BrowserWindow, nativeImage } = require("electron");
9
+ const fs = require("node:fs");
10
+ const path = require("node:path");
11
+
12
+ function uiUrl() {
13
+ const fromEnv = String(process.env.AMEM_UI_URL || "").trim();
14
+ if (fromEnv) return fromEnv;
15
+ const arg = process.argv.find((a) => /^https?:\/\//.test(a));
16
+ return arg || "http://127.0.0.1:7843/";
17
+ }
18
+
19
+ function iconsDir() {
20
+ return path.join(__dirname, "icons");
21
+ }
22
+
23
+ /** Prefer PNG for Dock — nativeImage + .icns is unreliable for unpackaged Electron. */
24
+ function loadAppIcon() {
25
+ const dir = iconsDir();
26
+ const candidates =
27
+ process.platform === "darwin"
28
+ ? ["icon-1024.png", "icon.png", "icon.icns"]
29
+ : process.platform === "win32"
30
+ ? ["icon.ico", "icon.png", "icon-1024.png"]
31
+ : ["icon.png", "icon-1024.png"];
32
+
33
+ for (const name of candidates) {
34
+ const iconPath = path.join(dir, name);
35
+ if (!fs.existsSync(iconPath)) continue;
36
+ let image = nativeImage.createFromPath(iconPath);
37
+ if (image.isEmpty()) continue;
38
+ // Dock looks sharper with a mid-size bitmap.
39
+ const { width } = image.getSize();
40
+ if (width > 512) {
41
+ image = image.resize({ width: 512, height: 512, quality: "best" });
42
+ }
43
+ return image;
44
+ }
45
+ console.warn("[amem-desktop] no usable icon in", dir);
46
+ return null;
47
+ }
48
+
49
+ function applyDockIcon(icon) {
50
+ if (!icon || process.platform !== "darwin" || !app.dock) return;
51
+ try {
52
+ app.dock.setIcon(icon);
53
+ app.dock.show();
54
+ } catch (err) {
55
+ console.warn("[amem-desktop] dock.setIcon failed:", err instanceof Error ? err.message : err);
56
+ }
57
+ }
58
+
59
+ function createWindow(icon) {
60
+ const win = new BrowserWindow({
61
+ width: 1280,
62
+ height: 860,
63
+ minWidth: 880,
64
+ minHeight: 560,
65
+ title: "amem",
66
+ show: false,
67
+ backgroundColor: "#0b0f12",
68
+ ...(icon ? { icon } : {}),
69
+ webPreferences: {
70
+ preload: path.join(__dirname, "preload.js"),
71
+ contextIsolation: true,
72
+ nodeIntegration: false,
73
+ sandbox: true,
74
+ },
75
+ });
76
+
77
+ win.once("ready-to-show", () => {
78
+ applyDockIcon(icon);
79
+ win.show();
80
+ });
81
+ win.loadURL(uiUrl()).catch((err) => {
82
+ console.error("[amem-desktop] failed to load UI:", err instanceof Error ? err.message : err);
83
+ });
84
+ }
85
+
86
+ app.setName("amem");
87
+
88
+ app.whenReady().then(() => {
89
+ const icon = loadAppIcon();
90
+ applyDockIcon(icon);
91
+ createWindow(icon);
92
+ app.on("activate", () => {
93
+ applyDockIcon(icon);
94
+ if (BrowserWindow.getAllWindows().length === 0) createWindow(icon);
95
+ });
96
+ });
97
+
98
+ app.on("window-all-closed", () => {
99
+ app.quit();
100
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "amem-desktop",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "description": "Electron shell for local amem Brain UI (localhost only).",
6
+ "main": "main.js",
7
+ "scripts": {
8
+ "start": "electron ."
9
+ },
10
+ "engines": {
11
+ "node": ">=20"
12
+ },
13
+ "dependencies": {
14
+ "electron": "^33.2.1"
15
+ }
16
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Preload is intentionally empty for v1 — the UI talks to localhost over fetch.
3
+ * Kept for secure defaults (contextIsolation + sandbox).
4
+ */
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Ensure the Electron binary finished downloading after npm install
4
+ * (some environments skip lifecycle scripts / botch macOS framework unzip).
5
+ */
6
+ import { existsSync, rmSync, writeFileSync, statSync, mkdirSync } from "node:fs";
7
+ import { createRequire } from "node:module";
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import { spawnSync } from "node:child_process";
11
+
12
+ const desktopRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
13
+ const electronPkg = join(desktopRoot, "node_modules", "electron");
14
+ const pathTxt = join(electronPkg, "path.txt");
15
+ const distDir = join(electronPkg, "dist");
16
+
17
+ if (!existsSync(join(electronPkg, "package.json"))) {
18
+ console.error("electron is not installed under desktop/. Run: npm install --prefix desktop");
19
+ process.exit(1);
20
+ }
21
+
22
+ function platformRelative() {
23
+ if (process.platform === "darwin") return "Electron.app/Contents/MacOS/Electron";
24
+ if (process.platform === "win32") return "electron.exe";
25
+ return "electron";
26
+ }
27
+
28
+ function binaryPath() {
29
+ return join(distDir, platformRelative());
30
+ }
31
+
32
+ function frameworkOk() {
33
+ if (process.platform !== "darwin") return true;
34
+ return existsSync(
35
+ join(distDir, "Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework"),
36
+ );
37
+ }
38
+
39
+ function looksComplete() {
40
+ const bin = binaryPath();
41
+ if (!existsSync(bin)) return false;
42
+ if (!frameworkOk()) return false;
43
+ return true;
44
+ }
45
+
46
+ function requireBin() {
47
+ try {
48
+ const require = createRequire(pathToFileURL(join(desktopRoot, "package.json")).href);
49
+ const bin = require("electron");
50
+ return typeof bin === "string" && existsSync(bin) ? bin : null;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ function downloadZip() {
57
+ const require = createRequire(join(electronPkg, "package.json"));
58
+ const { downloadArtifact } = require("@electron/get");
59
+ const { version } = require(join(electronPkg, "package.json"));
60
+ const checksums = require(join(electronPkg, "checksums.json"));
61
+ return downloadArtifact({
62
+ version,
63
+ artifactName: "electron",
64
+ force: true,
65
+ platform: process.platform,
66
+ arch: process.arch,
67
+ checksums,
68
+ });
69
+ }
70
+
71
+ async function ensure() {
72
+ if (looksComplete()) {
73
+ if (!existsSync(pathTxt)) writeFileSync(pathTxt, platformRelative());
74
+ console.log(`Electron ready: ${requireBin() || binaryPath()}`);
75
+ return;
76
+ }
77
+
78
+ console.log("Downloading Electron binary…");
79
+ rmSync(distDir, { recursive: true, force: true });
80
+ try {
81
+ rmSync(pathTxt, { force: true });
82
+ } catch {
83
+ /* ignore */
84
+ }
85
+
86
+ const zipPath = await downloadZip();
87
+ mkdirSync(distDir, { recursive: true });
88
+
89
+ // System unzip preserves macOS framework symlinks; extract-zip often does not.
90
+ const unzip = spawnSync("unzip", ["-o", "-q", zipPath, "-d", distDir], { stdio: "inherit" });
91
+ if (unzip.status !== 0) {
92
+ // Fallback for Windows / machines without unzip
93
+ const extract = requireFromElectron("extract-zip");
94
+ await extract(zipPath, { dir: distDir });
95
+ }
96
+
97
+ writeFileSync(pathTxt, platformRelative());
98
+ // Match electron/install.js version marker when present
99
+ try {
100
+ const { version } = createRequire(join(electronPkg, "package.json"))("./package.json");
101
+ writeFileSync(join(distDir, "version"), version);
102
+ } catch {
103
+ /* ignore */
104
+ }
105
+
106
+ if (!looksComplete()) {
107
+ console.error("Electron binary still incomplete after install.");
108
+ console.error("Try: rm -rf desktop/node_modules && npm run app:setup");
109
+ process.exit(1);
110
+ }
111
+
112
+ console.log(`Electron ready: ${requireBin() || binaryPath()}`);
113
+ }
114
+
115
+ function requireFromElectron(id) {
116
+ return createRequire(join(electronPkg, "package.json"))(id);
117
+ }
118
+
119
+ ensure().catch((err) => {
120
+ console.error(err instanceof Error ? err.stack : err);
121
+ process.exit(1);
122
+ });