@iamem/amem 0.1.0 → 0.1.2
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 -3
- package/desktop/icons/icon-1024.png +0 -0
- package/desktop/icons/icon.icns +0 -0
- package/desktop/icons/icon.png +0 -0
- package/desktop/main.js +100 -0
- package/desktop/package.json +16 -0
- package/desktop/preload.js +4 -0
- package/desktop/scripts/ensure-electron.mjs +122 -0
- package/dist/api/routes.js +4 -0
- package/dist/app-shell.d.ts +13 -0
- package/dist/app-shell.js +126 -0
- package/dist/capture.d.ts +3 -0
- package/dist/capture.js +15 -1
- package/dist/cli.js +17 -0
- package/dist/hygiene.d.ts +9 -0
- package/dist/hygiene.js +66 -8
- package/dist/publish.d.ts +1 -1
- package/dist/publish.js +1 -0
- package/docs/backlog.md +2 -2
- package/docs/enterprise-endpoint.md +2 -1
- package/docs/npm-release.md +1 -1
- package/package.json +7 -2
- package/scripts/postinstall.js +62 -0
- package/ui-static/app.js +20 -3
- package/ui-static/styles.css +4 -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
|
-
-
|
|
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:
|
|
@@ -388,6 +416,7 @@ amem usage export [--format json|md|pdf] [--days 30] [--scope current|all] [--ou
|
|
|
388
416
|
amem license status|apply|activate|clear|issue|keys
|
|
389
417
|
amem embed status|use hash|use ngram|reindex
|
|
390
418
|
amem ui [--port 7843] [--no-open]
|
|
419
|
+
amem app [--port 7843]
|
|
391
420
|
amem service install|uninstall|status
|
|
392
421
|
```
|
|
393
422
|
|
|
@@ -403,7 +432,8 @@ amem service install|uninstall|status
|
|
|
403
432
|
| `propose apply` | Upsert structured memory locally |
|
|
404
433
|
| `lock` / `unlock` | Optional AES-256-GCM encrypt-at-rest for `graph.db` |
|
|
405
434
|
| `backup` | Local snapshot (optionally encrypted); `schedule` for daily timer |
|
|
406
|
-
| `ui` | Setup wizard + Memory + Stats
|
|
435
|
+
| `ui` | Setup wizard + Memory + Stats in the browser (localhost) |
|
|
436
|
+
| `app` | Same UI in an Electron window (`npm run app:setup` once) |
|
|
407
437
|
| `service` | Login item so `amem ui` starts after reboot |
|
|
408
438
|
| `doctor --attest` | Privacy/policy attestation for IT tickets |
|
|
409
439
|
| `export` / `wipe` | Personal backup or delete (still local) |
|
|
@@ -490,7 +520,8 @@ IT / DevEx can govern the **fleet**: approved install, policy, attestation, offb
|
|
|
490
520
|
|
|
491
521
|
Hard guarantees (not configurable away):
|
|
492
522
|
|
|
493
|
-
- No telemetry
|
|
523
|
+
- No memory telemetry or claim upload — `~/.amem` stays local
|
|
524
|
+
- Optional anonymous npm install ping only (opt out: `AMEM_TELEMETRY_DISABLED=1`)
|
|
494
525
|
- UI binds to loopback only (`127.0.0.1`)
|
|
495
526
|
- Memory stays under `~/.amem` (mode `0700`)
|
|
496
527
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/desktop/main.js
ADDED
|
@@ -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,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
|
+
});
|
package/dist/api/routes.js
CHANGED
|
@@ -9,6 +9,7 @@ import { installClaude, claudeInstallHealth } from "../install/claude.js";
|
|
|
9
9
|
import { installCursor, cursorInstallHealth } from "../install/cursor.js";
|
|
10
10
|
import { hostInstallHealth, installHost } from "../install/hosts.js";
|
|
11
11
|
import { decorateDraft, decorateDrafts } from "../draft-quality.js";
|
|
12
|
+
import { isUsefulRememberText } from "../capture.js";
|
|
12
13
|
import { buildSavingsExport, formatSavingsMarkdown, savingsPdf, } from "../savings-export.js";
|
|
13
14
|
import { assertPlatformAllowed, assertRemoteAllowed, loadPolicy, } from "../policy.js";
|
|
14
15
|
import { applyProposal, applySupersedes, parseProposalJson, validateProposal } from "../proposal.js";
|
|
@@ -412,6 +413,9 @@ function runRemember(repo, body) {
|
|
|
412
413
|
const text = bodyField(body, "text");
|
|
413
414
|
if (!text)
|
|
414
415
|
return err(400, "text required");
|
|
416
|
+
if (!isUsefulRememberText(text)) {
|
|
417
|
+
return err(400, "text too trivial or secret-like — remember a durable fact with context");
|
|
418
|
+
}
|
|
415
419
|
const kind = bodyField(body, "kind") ?? "session";
|
|
416
420
|
const id = bodyField(body, "id") ??
|
|
417
421
|
`claim.remember_${createHash("sha256").update(text).digest("hex").slice(0, 12)}`;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function desktopDir(pkgRoot?: string): string;
|
|
2
|
+
export declare function electronInstallHint(dir?: string): string;
|
|
3
|
+
/** Absolute path to the Electron binary, or null if desktop deps are missing. */
|
|
4
|
+
export declare function resolveElectronBinary(dir?: string): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Start or attach to amem ui, then open the Electron window.
|
|
7
|
+
* Owns server shutdown only when this process started it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runDesktopApp(options?: {
|
|
10
|
+
port?: number;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
pkgRoot?: string;
|
|
13
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desktop (Electron) shell launcher for the localhost Brain UI.
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { loadPolicy } from "./policy.js";
|
|
10
|
+
import { buildUiLandingUrl, isAddrInUse, probeUiHealth, startUiServer, } from "./ui/server.js";
|
|
11
|
+
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
export function desktopDir(pkgRoot = PKG_ROOT) {
|
|
13
|
+
return join(pkgRoot, "desktop");
|
|
14
|
+
}
|
|
15
|
+
export function electronInstallHint(dir = desktopDir()) {
|
|
16
|
+
return `npm run app:setup # or: npm install --prefix "${dir}" && node desktop/scripts/ensure-electron.mjs`;
|
|
17
|
+
}
|
|
18
|
+
/** Absolute path to the Electron binary, or null if desktop deps are missing. */
|
|
19
|
+
export function resolveElectronBinary(dir = desktopDir()) {
|
|
20
|
+
const pkgJson = join(dir, "package.json");
|
|
21
|
+
if (!existsSync(pkgJson))
|
|
22
|
+
return null;
|
|
23
|
+
const candidates = [
|
|
24
|
+
join(dir, "node_modules/electron/dist/Electron.app/Contents/MacOS/Electron"),
|
|
25
|
+
join(dir, "node_modules/electron/dist/electron"),
|
|
26
|
+
join(dir, "node_modules/electron/dist/electron.exe"),
|
|
27
|
+
];
|
|
28
|
+
try {
|
|
29
|
+
const require = createRequire(pkgJson);
|
|
30
|
+
const bin = require("electron");
|
|
31
|
+
if (typeof bin === "string" && bin && existsSync(bin))
|
|
32
|
+
return bin;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* path.txt missing or postinstall incomplete — try dist paths */
|
|
36
|
+
}
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
if (existsSync(candidate))
|
|
39
|
+
return candidate;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function spawnElectron(electronBin, dir, url) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const env = { ...process.env, AMEM_UI_URL: url };
|
|
46
|
+
// Parent IDEs sometimes set this; it makes Electron run as plain Node.
|
|
47
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
48
|
+
const child = spawn(electronBin, [".", url], {
|
|
49
|
+
cwd: dir,
|
|
50
|
+
env,
|
|
51
|
+
stdio: "inherit",
|
|
52
|
+
});
|
|
53
|
+
child.on("error", reject);
|
|
54
|
+
child.on("exit", (code, signal) => {
|
|
55
|
+
if (signal) {
|
|
56
|
+
resolve(1);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
resolve(code ?? 0);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Start or attach to amem ui, then open the Electron window.
|
|
65
|
+
* Owns server shutdown only when this process started it.
|
|
66
|
+
*/
|
|
67
|
+
export async function runDesktopApp(options = {}) {
|
|
68
|
+
const port = options.port ?? 7843;
|
|
69
|
+
const cwd = options.cwd ?? process.cwd();
|
|
70
|
+
const dir = desktopDir(options.pkgRoot ?? PKG_ROOT);
|
|
71
|
+
const electronBin = resolveElectronBinary(dir);
|
|
72
|
+
if (!electronBin) {
|
|
73
|
+
const err = new Error([
|
|
74
|
+
"Electron desktop shell is not installed.",
|
|
75
|
+
`Run once: ${electronInstallHint(dir)}`,
|
|
76
|
+
"Then: amem app",
|
|
77
|
+
"(Browser UI still works with: amem ui)",
|
|
78
|
+
].join("\n"));
|
|
79
|
+
err.code = "AMEM_ELECTRON_MISSING";
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
const policy = loadPolicy().policy;
|
|
83
|
+
const landing = buildUiLandingUrl(port, cwd);
|
|
84
|
+
let owned = null;
|
|
85
|
+
try {
|
|
86
|
+
owned = await startUiServer({
|
|
87
|
+
port,
|
|
88
|
+
cwd,
|
|
89
|
+
openBrowser: false,
|
|
90
|
+
host: policy.ui_bind,
|
|
91
|
+
landingUrl: landing,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (!isAddrInUse(error))
|
|
96
|
+
throw error;
|
|
97
|
+
const probe = await probeUiHealth(port);
|
|
98
|
+
if (!probe.hasVault) {
|
|
99
|
+
throw new Error([
|
|
100
|
+
`Port ${port} is already serving an older amem without lock/backup APIs.`,
|
|
101
|
+
"Stop that process, then run amem app again:",
|
|
102
|
+
` lsof -nP -iTCP:${port} -sTCP:LISTEN`,
|
|
103
|
+
].join("\n"));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log(`amem app → ${landing}`);
|
|
107
|
+
if (owned)
|
|
108
|
+
console.log("UI server started for this window (localhost only).");
|
|
109
|
+
else
|
|
110
|
+
console.log("Attached to UI server already running on this port.");
|
|
111
|
+
try {
|
|
112
|
+
const code = await spawnElectron(electronBin, dir, landing);
|
|
113
|
+
if (code !== 0)
|
|
114
|
+
process.exitCode = code;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
if (owned) {
|
|
118
|
+
try {
|
|
119
|
+
await owned.close();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
/* ignore */
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
package/dist/capture.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type ProposalDraftRow, type RepoRow, type UsageEventRow } from "./db.js";
|
|
2
2
|
import { type Proposal } from "./proposal.js";
|
|
3
|
+
/** Reject empty / chat-noise / secret-like text before it becomes a claim. */
|
|
3
4
|
export declare function isUsefulCaptureText(text: string): boolean;
|
|
5
|
+
/** Same guard for explicit amem_remember / API writes (including short “test”). */
|
|
6
|
+
export declare function isUsefulRememberText(text: string): boolean;
|
|
4
7
|
export declare function extractCaptureAnchors(text: string, repoRoot: string): string[];
|
|
5
8
|
/** High-quality durable facts apply without waiting for amem_remember. */
|
|
6
9
|
export declare function shouldAutoApplyProposal(proposal: Proposal): boolean;
|
package/dist/capture.js
CHANGED
|
@@ -8,9 +8,10 @@ import { applyProposal } from "./proposal.js";
|
|
|
8
8
|
import { scoreProposal } from "./draft-quality.js";
|
|
9
9
|
import { tokenJaccard } from "./search.js";
|
|
10
10
|
import { isAutoApplyAll } from "./prefs.js";
|
|
11
|
-
const TRIVIAL = /^(ok|okay|yes|yep|no|nah|thanks|thank you|continue|go ahead|sure|please)\.?$/i;
|
|
11
|
+
const TRIVIAL = /^(ok|okay|yes|yep|no|nah|thanks|thank you|continue|go ahead|sure|please|test|testing|hello|hi|hey|ping|asdf|foo|bar)\.?$/i;
|
|
12
12
|
const SECRET = /password|api[_-]?key|secret|token\s*[:=]|begin (rsa |openssh )?private/i;
|
|
13
13
|
const PATH_RE = /\b(?:[\w.-]+\/)*[\w.-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|md|json|yml|yaml|sql)\b/g;
|
|
14
|
+
/** Reject empty / chat-noise / secret-like text before it becomes a claim. */
|
|
14
15
|
export function isUsefulCaptureText(text) {
|
|
15
16
|
const t = text.trim();
|
|
16
17
|
if (t.length < 16)
|
|
@@ -21,6 +22,19 @@ export function isUsefulCaptureText(text) {
|
|
|
21
22
|
return false;
|
|
22
23
|
return true;
|
|
23
24
|
}
|
|
25
|
+
/** Same guard for explicit amem_remember / API writes (including short “test”). */
|
|
26
|
+
export function isUsefulRememberText(text) {
|
|
27
|
+
const t = text.trim();
|
|
28
|
+
if (!t)
|
|
29
|
+
return false;
|
|
30
|
+
if (TRIVIAL.test(t))
|
|
31
|
+
return false;
|
|
32
|
+
if (t.length < 8)
|
|
33
|
+
return false;
|
|
34
|
+
if (SECRET.test(t))
|
|
35
|
+
return false;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
24
38
|
export function extractCaptureAnchors(text, repoRoot) {
|
|
25
39
|
const found = [...text.matchAll(PATH_RE)].map((m) => m[0]);
|
|
26
40
|
const unique = [...new Set(found)].slice(0, 6);
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { amemHome, dbPath, tryEnsureDir } from "./paths.js";
|
|
|
12
12
|
import { assertExportAllowed, assertPlatformAllowed, assertRemoteAllowed, assertUiAllowed, loadPolicy, } from "./policy.js";
|
|
13
13
|
import { applyProposal, diffProposal, exportRepoMemory, formatProposalDiff, loadProposalFile, validateProposal, } from "./proposal.js";
|
|
14
14
|
import { detectRepoIdentity, parseWorkspaceSlug, workspaceIdentity } from "./repo-identity.js";
|
|
15
|
+
import { runDesktopApp } from "./app-shell.js";
|
|
15
16
|
import { startUiServer, buildUiLandingUrl, openUiInBrowser, isAddrInUse, probeUiHealth, } from "./ui/server.js";
|
|
16
17
|
import { installLoginService, isServiceInstalled, isServiceSupported, uninstallLoginService } from "./service.js";
|
|
17
18
|
import { mcpClientConfig, runMcpServer } from "./mcp.js";
|
|
@@ -66,6 +67,7 @@ Usage:
|
|
|
66
67
|
amem usage report --saved <n> [--platform cursor|claude] [--event-id <id>]
|
|
67
68
|
amem usage export [--format json|md|pdf] [--days 30] [--scope current|all] [--out <file>]
|
|
68
69
|
amem ui [--port 7843] [--no-open]
|
|
70
|
+
amem app [--port 7843]
|
|
69
71
|
amem service install|uninstall|status
|
|
70
72
|
amem mcp [--print-config] [--workspace <name>]
|
|
71
73
|
amem license status|apply|clear|issue|keys
|
|
@@ -846,6 +848,21 @@ async function main() {
|
|
|
846
848
|
}
|
|
847
849
|
break;
|
|
848
850
|
}
|
|
851
|
+
case "app": {
|
|
852
|
+
assertUiAllowed();
|
|
853
|
+
const port = Number(flagString(flags, "port") ?? "7843");
|
|
854
|
+
try {
|
|
855
|
+
await runDesktopApp({ port, cwd: process.cwd() });
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
859
|
+
closeDb();
|
|
860
|
+
process.exitCode = 1;
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
closeDb();
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
849
866
|
case "service": {
|
|
850
867
|
const sub = positional[1];
|
|
851
868
|
if (sub === "status") {
|
package/dist/hygiene.d.ts
CHANGED
|
@@ -23,9 +23,18 @@ export type HygienePreview = {
|
|
|
23
23
|
/** Estimated active count after decay + merge */
|
|
24
24
|
afterCleanup: number;
|
|
25
25
|
softPaywall: boolean;
|
|
26
|
+
/** Active claims with kind=session (chat noise indicator). */
|
|
27
|
+
sessionCount: number;
|
|
28
|
+
/** sessionCount / active, 0 when empty. */
|
|
29
|
+
sessionRatio: number;
|
|
26
30
|
};
|
|
27
31
|
export declare const SOFT_PAYWALL_FACTS = 200;
|
|
28
32
|
export declare const SOFT_PAYWALL_NOISE = 15;
|
|
33
|
+
/** Soft-paywall when session chat takeaways dominate the graph. */
|
|
34
|
+
export declare const SOFT_PAYWALL_SESSION_RATIO = 0.55;
|
|
35
|
+
export declare const SOFT_PAYWALL_SESSION_MIN = 25;
|
|
36
|
+
/** Unused unpinned session claims become decay candidates sooner than durable kinds. */
|
|
37
|
+
export declare const SESSION_UNUSED_DAYS = 14;
|
|
29
38
|
/** Free: counts only for soft paywall / banners. Never applies changes. */
|
|
30
39
|
export declare function hygienePreview(repoId: string, unusedDays?: number): HygienePreview;
|
|
31
40
|
/** Aggregate preview across all tracked repos (for All memory scope). */
|
package/dist/hygiene.js
CHANGED
|
@@ -10,6 +10,11 @@ import { tokenJaccard } from "./search.js";
|
|
|
10
10
|
import { parseAnchors } from "./freshness.js";
|
|
11
11
|
export const SOFT_PAYWALL_FACTS = 200;
|
|
12
12
|
export const SOFT_PAYWALL_NOISE = 15;
|
|
13
|
+
/** Soft-paywall when session chat takeaways dominate the graph. */
|
|
14
|
+
export const SOFT_PAYWALL_SESSION_RATIO = 0.55;
|
|
15
|
+
export const SOFT_PAYWALL_SESSION_MIN = 25;
|
|
16
|
+
/** Unused unpinned session claims become decay candidates sooner than durable kinds. */
|
|
17
|
+
export const SESSION_UNUSED_DAYS = 14;
|
|
13
18
|
function usedClaimIds(repoId, days) {
|
|
14
19
|
const ids = new Set();
|
|
15
20
|
for (const event of listUsageEvents({ repoId, days })) {
|
|
@@ -29,14 +34,24 @@ function usedClaimIds(repoId, days) {
|
|
|
29
34
|
function computeHygiene(repoId, unusedDays = 90) {
|
|
30
35
|
const claims = listClaims(repoId);
|
|
31
36
|
const used = usedClaimIds(repoId, unusedDays);
|
|
37
|
+
const usedSessions = usedClaimIds(repoId, SESSION_UNUSED_DAYS);
|
|
32
38
|
const cutoff = Date.now() - unusedDays * 86_400_000;
|
|
39
|
+
const sessionCutoff = Date.now() - SESSION_UNUSED_DAYS * 86_400_000;
|
|
33
40
|
const stale = claims.filter((c) => {
|
|
34
41
|
if (Number(c.pinned || 0) > 0)
|
|
35
42
|
return false;
|
|
43
|
+
const updated = Date.parse(c.updated_at);
|
|
44
|
+
if (!Number.isFinite(updated))
|
|
45
|
+
return false;
|
|
46
|
+
const isSession = (c.kind || "").toLowerCase() === "session";
|
|
47
|
+
if (isSession) {
|
|
48
|
+
if (usedSessions.has(c.id))
|
|
49
|
+
return false;
|
|
50
|
+
return updated < sessionCutoff;
|
|
51
|
+
}
|
|
36
52
|
if (used.has(c.id))
|
|
37
53
|
return false;
|
|
38
|
-
|
|
39
|
-
return Number.isFinite(updated) && updated < cutoff;
|
|
54
|
+
return updated < cutoff;
|
|
40
55
|
});
|
|
41
56
|
const duplicates = [];
|
|
42
57
|
for (let i = 0; i < claims.length; i++) {
|
|
@@ -63,16 +78,40 @@ function computeHygiene(repoId, unusedDays = 90) {
|
|
|
63
78
|
active: claims.length,
|
|
64
79
|
};
|
|
65
80
|
}
|
|
81
|
+
function sessionStats(repoId) {
|
|
82
|
+
const claims = listClaims(repoId);
|
|
83
|
+
const active = claims.length;
|
|
84
|
+
const sessionCount = claims.filter((c) => (c.kind || "").toLowerCase() === "session").length;
|
|
85
|
+
const sessionRatio = active > 0 ? sessionCount / active : 0;
|
|
86
|
+
return { sessionCount, sessionRatio, active };
|
|
87
|
+
}
|
|
88
|
+
function softPaywallFrom(preview) {
|
|
89
|
+
if (hasFeature(FEATURE_HYGIENE))
|
|
90
|
+
return false;
|
|
91
|
+
const noise = preview.staleCount + preview.duplicateCount;
|
|
92
|
+
if (preview.active >= SOFT_PAYWALL_FACTS || noise >= SOFT_PAYWALL_NOISE)
|
|
93
|
+
return true;
|
|
94
|
+
if (preview.sessionCount >= SOFT_PAYWALL_SESSION_MIN &&
|
|
95
|
+
preview.sessionRatio >= SOFT_PAYWALL_SESSION_RATIO) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
66
100
|
/** Free: counts only for soft paywall / banners. Never applies changes. */
|
|
67
101
|
export function hygienePreview(repoId, unusedDays = 90) {
|
|
68
102
|
const report = computeHygiene(repoId, unusedDays);
|
|
103
|
+
const { sessionCount, sessionRatio } = sessionStats(repoId);
|
|
69
104
|
const staleCount = report.stale.length;
|
|
70
105
|
const duplicateCount = report.duplicates.length;
|
|
71
106
|
const removable = Math.min(report.active, staleCount + duplicateCount);
|
|
72
107
|
const afterCleanup = Math.max(0, report.active - removable);
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
108
|
+
const softPaywall = softPaywallFrom({
|
|
109
|
+
active: report.active,
|
|
110
|
+
staleCount,
|
|
111
|
+
duplicateCount,
|
|
112
|
+
sessionCount,
|
|
113
|
+
sessionRatio,
|
|
114
|
+
});
|
|
76
115
|
return {
|
|
77
116
|
active: report.active,
|
|
78
117
|
staleCount,
|
|
@@ -80,6 +119,8 @@ export function hygienePreview(repoId, unusedDays = 90) {
|
|
|
80
119
|
pendingDrafts: report.pendingDrafts,
|
|
81
120
|
afterCleanup,
|
|
82
121
|
softPaywall,
|
|
122
|
+
sessionCount,
|
|
123
|
+
sessionRatio,
|
|
83
124
|
};
|
|
84
125
|
}
|
|
85
126
|
/** Aggregate preview across all tracked repos (for All memory scope). */
|
|
@@ -88,18 +129,35 @@ export function hygienePreviewAll(unusedDays = 90) {
|
|
|
88
129
|
let staleCount = 0;
|
|
89
130
|
let duplicateCount = 0;
|
|
90
131
|
let pendingDrafts = 0;
|
|
132
|
+
let sessionCount = 0;
|
|
91
133
|
for (const repo of listRepos()) {
|
|
92
134
|
const p = hygienePreview(repo.id, unusedDays);
|
|
93
135
|
active += p.active;
|
|
94
136
|
staleCount += p.staleCount;
|
|
95
137
|
duplicateCount += p.duplicateCount;
|
|
96
138
|
pendingDrafts += p.pendingDrafts;
|
|
139
|
+
sessionCount += p.sessionCount;
|
|
97
140
|
}
|
|
98
141
|
const removable = Math.min(active, staleCount + duplicateCount);
|
|
99
142
|
const afterCleanup = Math.max(0, active - removable);
|
|
100
|
-
const
|
|
101
|
-
const softPaywall =
|
|
102
|
-
|
|
143
|
+
const sessionRatio = active > 0 ? sessionCount / active : 0;
|
|
144
|
+
const softPaywall = softPaywallFrom({
|
|
145
|
+
active,
|
|
146
|
+
staleCount,
|
|
147
|
+
duplicateCount,
|
|
148
|
+
sessionCount,
|
|
149
|
+
sessionRatio,
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
active,
|
|
153
|
+
staleCount,
|
|
154
|
+
duplicateCount,
|
|
155
|
+
pendingDrafts,
|
|
156
|
+
afterCleanup,
|
|
157
|
+
softPaywall,
|
|
158
|
+
sessionCount,
|
|
159
|
+
sessionRatio,
|
|
160
|
+
};
|
|
103
161
|
}
|
|
104
162
|
export function hygieneReport(repoId, unusedDays = 90) {
|
|
105
163
|
requireFeature(FEATURE_HYGIENE, "Memory hygiene");
|
package/dist/publish.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Paths that must be inside the published tarball for `npx @iamem/amem setup`. */
|
|
2
|
-
export declare const REQUIRED_PACK_PATHS: readonly ["dist/cli.js", "dist/mcp.js", "dist/remember-contract.js", "ui-static/index.html", "ui-static/app.js", "ui-static/styles.css", "docs/remember-contract.md", "docs/agent-install-prompt.md", "docs/npm-release.md", "skills/amem-update-working-memory/SKILL.md", "scripts/mdm-offboard.sh"];
|
|
2
|
+
export declare const REQUIRED_PACK_PATHS: readonly ["dist/cli.js", "dist/mcp.js", "dist/remember-contract.js", "ui-static/index.html", "ui-static/app.js", "ui-static/styles.css", "docs/remember-contract.md", "docs/agent-install-prompt.md", "docs/npm-release.md", "skills/amem-update-working-memory/SKILL.md", "scripts/mdm-offboard.sh", "scripts/postinstall.js"];
|
|
3
3
|
export declare const PUBLISH_PACKAGE_NAME = "@iamem/amem";
|
|
4
4
|
export type PublishReady = {
|
|
5
5
|
root: string;
|
package/dist/publish.js
CHANGED
|
@@ -14,6 +14,7 @@ export const REQUIRED_PACK_PATHS = [
|
|
|
14
14
|
"docs/npm-release.md",
|
|
15
15
|
"skills/amem-update-working-memory/SKILL.md",
|
|
16
16
|
"scripts/mdm-offboard.sh",
|
|
17
|
+
"scripts/postinstall.js",
|
|
17
18
|
];
|
|
18
19
|
export const PUBLISH_PACKAGE_NAME = "@iamem/amem";
|
|
19
20
|
export function packageRoot() {
|
package/docs/backlog.md
CHANGED
|
@@ -37,16 +37,16 @@ Updated after completing the Feature Map **Later** phase (local embedding model
|
|
|
37
37
|
|
|
38
38
|
## Open
|
|
39
39
|
|
|
40
|
-
- First npm publish of `@iamem/amem` (org `@iamem` already exists; need valid `NPM_TOKEN` / `npm login`, then tag `v0.1.0` or `npm publish --access public`).
|
|
41
40
|
- Prompt-pack before/after Stats benchmark; restore wizard polish; IT seat pack.
|
|
42
41
|
- Decide one-time vs subscription (offline files cannot revoke on cancel unless you add `expires_at` and re-issue).
|
|
43
42
|
- Optional vendored ONNX/MiniLM weights in a paid pack (external command is the local hook today).
|
|
43
|
+
- Move CI publish to npm Trusted Publishing (OIDC) before Jan 2027 GAT bypass-2FA publish sunset.
|
|
44
44
|
|
|
45
45
|
## Shipped (go-to-market / upsell)
|
|
46
46
|
|
|
47
47
|
- Public Checkout at **getamem.com** (tryamem redirects); Stripe live webhook.
|
|
48
48
|
- Mailtrap **live send** (`MAILTRAP_USE_TESTING=false`); thank-you download still works if mail fails.
|
|
49
|
-
-
|
|
49
|
+
- **`@iamem/amem@0.1.0` on npm** — `npx @iamem/amem setup` / `npm i -g @iamem/amem` (CLI binary `amem`).
|
|
50
50
|
- UI **Apply license** (paste/drop) + **Turn on Pro retrieval** checklist.
|
|
51
51
|
- Memory **retrieval showdown** (free hash vs Pro n-gram) + top-bar **Try retrieval**.
|
|
52
52
|
- Remember-contract guidance: prefer durable kinds; avoid `session` spam.
|
|
@@ -30,7 +30,7 @@ The pack includes deny-by-default `policy.toml`, an MDM plist stub, `mdm-offboar
|
|
|
30
30
|
|
|
31
31
|
## Install (DevEx / IT)
|
|
32
32
|
|
|
33
|
-
1. Ship a pinned amem build
|
|
33
|
+
1. Ship a pinned amem build: `npm i -g @iamem/amem` (or internal mirror / pkg). Node 20+ required (`better-sqlite3` prebuilds on common macOS/Linux).
|
|
34
34
|
2. Deploy policy:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
@@ -78,6 +78,7 @@ That is not a remote public API:
|
|
|
78
78
|
- Nothing in `~/.amem` is uploaded or synced.
|
|
79
79
|
- Binding is loopback-only — LAN/WAN clients cannot reach the UI by default.
|
|
80
80
|
- Treat any process on the same machine as potentially able to call `127.0.0.1:7843` while the UI is running (same as any local MCP server).
|
|
81
|
+
- Do not expose the UI on a non-loopback bind; policy forces loopback. Tightening `Access-Control-Allow-Origin` to specific Origins is optional and can break some MCP hosts — leave `*` unless you control every client Origin.
|
|
81
82
|
|
|
82
83
|
## Offboarding
|
|
83
84
|
|
package/docs/npm-release.md
CHANGED
|
@@ -35,4 +35,4 @@ This repo does **not** auto-publish on every push.
|
|
|
35
35
|
|
|
36
36
|
## Pack contents
|
|
37
37
|
|
|
38
|
-
`package.json#files` includes `dist`, `ui-static`, `docs`, `skills`,
|
|
38
|
+
`package.json#files` includes `dist`, `ui-static`, `docs`, `skills`, `scripts/mdm-offboard.sh`, and `scripts/postinstall.js` (anonymous install ping; opt out with `AMEM_TELEMETRY_DISABLED=1`). Native code is pulled in as the `better-sqlite3` dependency — do not vendor it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iamem/amem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Local personal agent memory for Cursor and Claude Code. Private to your machine — never shared.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,17 +16,22 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
+
"desktop",
|
|
19
20
|
"skills",
|
|
20
21
|
"templates",
|
|
21
22
|
"docs",
|
|
22
23
|
"ui-static",
|
|
23
|
-
"scripts/mdm-offboard.sh"
|
|
24
|
+
"scripts/mdm-offboard.sh",
|
|
25
|
+
"scripts/postinstall.js"
|
|
24
26
|
],
|
|
25
27
|
"scripts": {
|
|
26
28
|
"build": "tsc",
|
|
27
29
|
"dev": "tsc --watch",
|
|
28
30
|
"prepare": "npm run build",
|
|
31
|
+
"postinstall": "node ./scripts/postinstall.js",
|
|
29
32
|
"amem": "node dist/cli.js",
|
|
33
|
+
"app": "node dist/cli.js app",
|
|
34
|
+
"app:setup": "npm install --prefix desktop && node desktop/scripts/ensure-electron.mjs",
|
|
30
35
|
"smoke": "node scripts/smoke.mjs",
|
|
31
36
|
"pack:check": "npm run build && node scripts/check-pack.mjs",
|
|
32
37
|
"prepublishOnly": "npm test && node scripts/check-pack.mjs",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Anonymous install ping for @iamem/amem (package version, Node, OS/arch only).
|
|
4
|
+
* Opt out: AMEM_TELEMETRY_DISABLED=1 — skipped automatically in CI / NODE_ENV=test.
|
|
5
|
+
* Never throws; always exits 0 so install is never blocked.
|
|
6
|
+
*/
|
|
7
|
+
import https from "node:https";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
function done() {
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
if (
|
|
19
|
+
process.env.AMEM_TELEMETRY_DISABLED ||
|
|
20
|
+
process.env.CI ||
|
|
21
|
+
process.env.NODE_ENV === "test"
|
|
22
|
+
) {
|
|
23
|
+
done();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
28
|
+
const body = JSON.stringify({
|
|
29
|
+
event: "npm_install",
|
|
30
|
+
package: pkg.name || "@iamem/amem",
|
|
31
|
+
version: pkg.version || "",
|
|
32
|
+
node: process.version,
|
|
33
|
+
platform: os.platform(),
|
|
34
|
+
arch: os.arch(),
|
|
35
|
+
ts: Date.now(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const req = https.request(
|
|
39
|
+
{
|
|
40
|
+
hostname: "getamem.com",
|
|
41
|
+
path: "/api/beacon/npm-install",
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: {
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
"Content-Length": Buffer.byteLength(body),
|
|
46
|
+
"User-Agent": `amem-postinstall/${pkg.name || "amem"}@${pkg.version || "0"}`,
|
|
47
|
+
},
|
|
48
|
+
timeout: 2000,
|
|
49
|
+
},
|
|
50
|
+
() => done(),
|
|
51
|
+
);
|
|
52
|
+
req.on("error", () => done());
|
|
53
|
+
req.on("timeout", () => {
|
|
54
|
+
req.destroy();
|
|
55
|
+
done();
|
|
56
|
+
});
|
|
57
|
+
setTimeout(done, 2000);
|
|
58
|
+
req.write(body);
|
|
59
|
+
req.end();
|
|
60
|
+
} catch {
|
|
61
|
+
done();
|
|
62
|
+
}
|
package/ui-static/app.js
CHANGED
|
@@ -320,6 +320,8 @@ function writeUrlState() {
|
|
|
320
320
|
|
|
321
321
|
function setTab(tab) {
|
|
322
322
|
if (tab === "stats") tab = "analytics";
|
|
323
|
+
// Paid licenses: Plans is a sell page — don't open it; Setup still has Apply license.
|
|
324
|
+
if (tab === "welcome" && isPaidLicense()) tab = "dashboard";
|
|
323
325
|
if (tab === "brain") {
|
|
324
326
|
state.brainAll = true;
|
|
325
327
|
persistBrainAll(true);
|
|
@@ -405,6 +407,7 @@ async function refreshVault() {
|
|
|
405
407
|
} catch {
|
|
406
408
|
state.license = state.status?.license || null;
|
|
407
409
|
}
|
|
410
|
+
paintPlansNavVisibility(isPaidLicense());
|
|
408
411
|
try {
|
|
409
412
|
state.embed = await apiUnscoped("/api/embed");
|
|
410
413
|
} catch {
|
|
@@ -1051,9 +1054,17 @@ function isPaidLicense() {
|
|
|
1051
1054
|
const tier = String(state.license?.tier || state.status?.license?.tier || "free").toLowerCase();
|
|
1052
1055
|
const paid = state.license?.valid !== false && (tier === "pro" || tier === "it");
|
|
1053
1056
|
if (paid) persistLicenseTier(tier);
|
|
1057
|
+
paintPlansNavVisibility(paid);
|
|
1054
1058
|
return paid;
|
|
1055
1059
|
}
|
|
1056
1060
|
|
|
1061
|
+
/** Paid users don't need the Plans sell tab in the sidebar (Setup still has Apply license). */
|
|
1062
|
+
function paintPlansNavVisibility(paid) {
|
|
1063
|
+
const btn = document.querySelector('#tabs button[data-tab="welcome"]');
|
|
1064
|
+
if (!btn) return;
|
|
1065
|
+
btn.classList.toggle("hidden", Boolean(paid));
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1057
1068
|
function licenseApplyHtml(idPrefix = "lic") {
|
|
1058
1069
|
return `
|
|
1059
1070
|
<div class="license-apply" id="${idPrefix}ApplyBox">
|
|
@@ -2098,14 +2109,20 @@ async function loadSoftPaywall() {
|
|
|
2098
2109
|
return;
|
|
2099
2110
|
}
|
|
2100
2111
|
const noise = (preview.staleCount || 0) + (preview.duplicateCount || 0);
|
|
2112
|
+
const sessions = Number(preview.sessionCount || 0);
|
|
2113
|
+
const ratio = Number(preview.sessionRatio || 0);
|
|
2114
|
+
const sessionHeavy = sessions >= 25 && ratio >= 0.55;
|
|
2101
2115
|
const shop = state.shop || {};
|
|
2102
2116
|
const proUrl = shop.proUrl || "https://getamem.com/buy/pro";
|
|
2103
2117
|
el.classList.remove("hidden");
|
|
2118
|
+
const why = sessionHeavy
|
|
2119
|
+
? `${preview.active} facts · <b>${Math.round(ratio * 100)}%</b> are short-lived <code>session</code> kinds (~${sessions}). Free preview → Pro applies Cleanup to archive unused sessions.`
|
|
2120
|
+
: `${preview.active} facts · ~${noise} unused/duplicates → about <b>${preview.afterCleanup}</b> after cleanup. Free still works — this is optional.`;
|
|
2104
2121
|
el.innerHTML = `
|
|
2105
2122
|
<div class="soft-paywall-inner">
|
|
2106
2123
|
<div>
|
|
2107
|
-
<strong
|
|
2108
|
-
<p class="note" style="margin:0.25rem 0 0">${
|
|
2124
|
+
<strong>${sessionHeavy ? "Session noise is crowding retrieval" : "Pro can clean this"}</strong>
|
|
2125
|
+
<p class="note" style="margin:0.25rem 0 0">${why}</p>
|
|
2109
2126
|
</div>
|
|
2110
2127
|
<div class="soft-paywall-actions">
|
|
2111
2128
|
<a class="btn small" href="${esc(proUrl)}" target="_blank" rel="noopener">Buy Pro · Apply cleanup</a>
|
|
@@ -3810,7 +3827,7 @@ async function render() {
|
|
|
3810
3827
|
document.querySelectorAll("#tabs button[data-tab]").forEach((b) => {
|
|
3811
3828
|
b.addEventListener("click", () => setTab(b.dataset.tab));
|
|
3812
3829
|
});
|
|
3813
|
-
$("#brandBtn")?.addEventListener("click", () => setTab("welcome"));
|
|
3830
|
+
$("#brandBtn")?.addEventListener("click", () => setTab(isPaidLicense() ? "dashboard" : "welcome"));
|
|
3814
3831
|
|
|
3815
3832
|
$("#brainSearch")?.addEventListener("input", (e) => {
|
|
3816
3833
|
if (state.tab !== "brain") return;
|