@iamem/amem 0.1.1 → 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/app-shell.d.ts +13 -0
- package/dist/app-shell.js +126 -0
- package/dist/cli.js +17 -0
- package/dist/publish.d.ts +1 -1
- package/dist/publish.js +1 -0
- package/docs/npm-release.md +1 -1
- package/package.json +7 -2
- package/scripts/postinstall.js +62 -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
|
+
});
|
|
@@ -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/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/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/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
|
+
}
|