@iamem/amem 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +534 -0
  3. package/dist/activity.d.ts +27 -0
  4. package/dist/activity.js +202 -0
  5. package/dist/api/routes.d.ts +31 -0
  6. package/dist/api/routes.js +1345 -0
  7. package/dist/attest.d.ts +52 -0
  8. package/dist/attest.js +192 -0
  9. package/dist/backup-schedule.d.ts +25 -0
  10. package/dist/backup-schedule.js +216 -0
  11. package/dist/capture.d.ts +34 -0
  12. package/dist/capture.js +257 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +1007 -0
  15. package/dist/context.d.ts +46 -0
  16. package/dist/context.js +334 -0
  17. package/dist/crypto.d.ts +39 -0
  18. package/dist/crypto.js +166 -0
  19. package/dist/db.d.ts +192 -0
  20. package/dist/db.js +666 -0
  21. package/dist/draft-quality.d.ts +19 -0
  22. package/dist/draft-quality.js +85 -0
  23. package/dist/embed.d.ts +76 -0
  24. package/dist/embed.js +331 -0
  25. package/dist/estimate.d.ts +32 -0
  26. package/dist/estimate.js +69 -0
  27. package/dist/freshness.d.ts +15 -0
  28. package/dist/freshness.js +93 -0
  29. package/dist/hook.d.ts +16 -0
  30. package/dist/hook.js +177 -0
  31. package/dist/hygiene-schedule.d.ts +28 -0
  32. package/dist/hygiene-schedule.js +221 -0
  33. package/dist/hygiene.d.ts +61 -0
  34. package/dist/hygiene.js +196 -0
  35. package/dist/install/claude.d.ts +6 -0
  36. package/dist/install/claude.js +69 -0
  37. package/dist/install/cursor.d.ts +7 -0
  38. package/dist/install/cursor.js +80 -0
  39. package/dist/install/hosts.d.ts +21 -0
  40. package/dist/install/hosts.js +186 -0
  41. package/dist/install/skills.d.ts +8 -0
  42. package/dist/install/skills.js +67 -0
  43. package/dist/it-pack.d.ts +20 -0
  44. package/dist/it-pack.js +84 -0
  45. package/dist/kinds.d.ts +18 -0
  46. package/dist/kinds.js +106 -0
  47. package/dist/license.d.ts +48 -0
  48. package/dist/license.js +172 -0
  49. package/dist/mcp.d.ts +40 -0
  50. package/dist/mcp.js +435 -0
  51. package/dist/paths.d.ts +11 -0
  52. package/dist/paths.js +55 -0
  53. package/dist/personal.d.ts +7 -0
  54. package/dist/personal.js +44 -0
  55. package/dist/platforms.d.ts +11 -0
  56. package/dist/platforms.js +32 -0
  57. package/dist/policy.d.ts +46 -0
  58. package/dist/policy.js +254 -0
  59. package/dist/prefs.d.ts +6 -0
  60. package/dist/prefs.js +11 -0
  61. package/dist/proposal.d.ts +90 -0
  62. package/dist/proposal.js +376 -0
  63. package/dist/publish.d.ts +28 -0
  64. package/dist/publish.js +57 -0
  65. package/dist/remember-contract.d.ts +23 -0
  66. package/dist/remember-contract.js +117 -0
  67. package/dist/repo-identity.d.ts +15 -0
  68. package/dist/repo-identity.js +82 -0
  69. package/dist/rules-sync.d.ts +7 -0
  70. package/dist/rules-sync.js +47 -0
  71. package/dist/savings-export.d.ts +49 -0
  72. package/dist/savings-export.js +141 -0
  73. package/dist/scan.d.ts +16 -0
  74. package/dist/scan.js +109 -0
  75. package/dist/search.d.ts +25 -0
  76. package/dist/search.js +150 -0
  77. package/dist/service.d.ts +20 -0
  78. package/dist/service.js +254 -0
  79. package/dist/shop.d.ts +9 -0
  80. package/dist/shop.js +15 -0
  81. package/dist/ui/server.d.ts +21 -0
  82. package/dist/ui/server.js +268 -0
  83. package/dist/vault.d.ts +25 -0
  84. package/dist/vault.js +42 -0
  85. package/dist/workspace-setup.d.ts +8 -0
  86. package/dist/workspace-setup.js +55 -0
  87. package/docs/agent-install-prompt.md +41 -0
  88. package/docs/backlog.md +59 -0
  89. package/docs/enterprise-endpoint.md +98 -0
  90. package/docs/license.md +54 -0
  91. package/docs/npm-release.md +38 -0
  92. package/docs/remember-contract.md +42 -0
  93. package/package.json +64 -0
  94. package/scripts/mdm-offboard.sh +14 -0
  95. package/skills/amem-bootstrap/SKILL.md +83 -0
  96. package/skills/amem-update-working-memory/SKILL.md +54 -0
  97. package/templates/cursor-rule.mdc +23 -0
  98. package/templates/mdm/co.amem.managed.plist +33 -0
  99. package/templates/policy.deny-default.toml +20 -0
  100. package/templates/policy.example.toml +22 -0
  101. package/ui-static/app.js +3966 -0
  102. package/ui-static/index.html +178 -0
  103. package/ui-static/orbit.js +389 -0
  104. package/ui-static/styles.css +2411 -0
@@ -0,0 +1,254 @@
1
+ import { existsSync, mkdirSync, unlinkSync, writeFileSync, chmodSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import { amemHome } from "./paths.js";
6
+ import { resolveAmemProgramArgs } from "./install/skills.js";
7
+ export const SERVICE_LABEL = "co.amem.ui";
8
+ export const SYSTEMD_UNIT = "amem-ui.service";
9
+ export function servicePlatform() {
10
+ if (process.platform === "darwin")
11
+ return "darwin";
12
+ if (process.platform === "linux")
13
+ return "linux";
14
+ if (process.platform === "win32")
15
+ return "win32";
16
+ return "unsupported";
17
+ }
18
+ export function isServiceSupported() {
19
+ return servicePlatform() !== "unsupported";
20
+ }
21
+ export function launchAgentPath() {
22
+ return join(homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
23
+ }
24
+ export function systemdUserUnitPath() {
25
+ return join(homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
26
+ }
27
+ export function windowsStartupCmdPath() {
28
+ const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
29
+ return join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "amem-ui.cmd");
30
+ }
31
+ export function serviceInstallPath() {
32
+ const p = servicePlatform();
33
+ if (p === "darwin")
34
+ return launchAgentPath();
35
+ if (p === "linux")
36
+ return systemdUserUnitPath();
37
+ if (p === "win32")
38
+ return windowsStartupCmdPath();
39
+ return "";
40
+ }
41
+ export function isServiceInstalled() {
42
+ const path = serviceInstallPath();
43
+ return Boolean(path && existsSync(path));
44
+ }
45
+ function escapeXml(value) {
46
+ return value
47
+ .replace(/&/g, "&")
48
+ .replace(/</g, "&lt;")
49
+ .replace(/>/g, "&gt;")
50
+ .replace(/"/g, "&quot;");
51
+ }
52
+ function programCommandLine() {
53
+ const args = resolveAmemProgramArgs("ui", "--no-open");
54
+ const shellLine = args.map((a) => (/\s/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a)).join(" ");
55
+ return { args, shellLine };
56
+ }
57
+ function plistBody() {
58
+ const { args } = programCommandLine();
59
+ const argXml = args.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
60
+ const logs = amemHome();
61
+ const pathEnv = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].join(":");
62
+ return `<?xml version="1.0" encoding="UTF-8"?>
63
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
64
+ <plist version="1.0">
65
+ <dict>
66
+ <key>Label</key>
67
+ <string>${SERVICE_LABEL}</string>
68
+ <key>ProgramArguments</key>
69
+ <array>
70
+ ${argXml}
71
+ </array>
72
+ <key>RunAtLoad</key>
73
+ <true/>
74
+ <key>KeepAlive</key>
75
+ <dict>
76
+ <key>SuccessfulExit</key>
77
+ <false/>
78
+ </dict>
79
+ <key>WorkingDirectory</key>
80
+ <string>${escapeXml(homedir())}</string>
81
+ <key>StandardOutPath</key>
82
+ <string>${escapeXml(join(logs, "ui.log"))}</string>
83
+ <key>StandardErrorPath</key>
84
+ <string>${escapeXml(join(logs, "ui.err.log"))}</string>
85
+ <key>EnvironmentVariables</key>
86
+ <dict>
87
+ <key>PATH</key>
88
+ <string>${escapeXml(pathEnv)}</string>
89
+ </dict>
90
+ </dict>
91
+ </plist>
92
+ `;
93
+ }
94
+ function systemdUnitBody() {
95
+ const { shellLine } = programCommandLine();
96
+ const logs = amemHome();
97
+ return `[Unit]
98
+ Description=amem local UI (loopback only)
99
+ After=default.target
100
+
101
+ [Service]
102
+ Type=simple
103
+ ExecStart=${shellLine}
104
+ WorkingDirectory=${homedir()}
105
+ Restart=on-failure
106
+ RestartSec=3
107
+ StandardOutput=append:${join(logs, "ui.log")}
108
+ StandardError=append:${join(logs, "ui.err.log")}
109
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homedir()}/.local/bin
110
+
111
+ [Install]
112
+ WantedBy=default.target
113
+ `;
114
+ }
115
+ function windowsCmdBody() {
116
+ const { shellLine } = programCommandLine();
117
+ return `@echo off\r\nrem amem local UI (Generated by amem)\r\nstart "" /B ${shellLine}\r\n`;
118
+ }
119
+ function uid() {
120
+ return String(process.getuid?.() ?? "");
121
+ }
122
+ function launchctl(args) {
123
+ execFileSync("launchctl", args, { stdio: ["ignore", "pipe", "pipe"] });
124
+ }
125
+ function systemctlUser(args) {
126
+ execFileSync("systemctl", ["--user", ...args], { stdio: ["ignore", "pipe", "pipe"] });
127
+ }
128
+ function installDarwin() {
129
+ const path = launchAgentPath();
130
+ mkdirSync(join(homedir(), "Library", "LaunchAgents"), { recursive: true });
131
+ writeFileSync(path, plistBody(), "utf8");
132
+ const domain = `gui/${uid()}`;
133
+ try {
134
+ launchctl(["bootout", domain, path]);
135
+ }
136
+ catch {
137
+ // not loaded yet
138
+ }
139
+ try {
140
+ launchctl(["bootstrap", domain, path]);
141
+ }
142
+ catch {
143
+ try {
144
+ launchctl(["load", "-w", path]);
145
+ }
146
+ catch {
147
+ // plist is on disk; next login still picks it up
148
+ }
149
+ }
150
+ return { path, installed: true };
151
+ }
152
+ function uninstallDarwin() {
153
+ const path = launchAgentPath();
154
+ if (existsSync(path)) {
155
+ try {
156
+ launchctl(["bootout", `gui/${uid()}`, path]);
157
+ }
158
+ catch {
159
+ try {
160
+ launchctl(["unload", "-w", path]);
161
+ }
162
+ catch {
163
+ // ignore
164
+ }
165
+ }
166
+ unlinkSync(path);
167
+ }
168
+ return { path, installed: false };
169
+ }
170
+ function installLinux() {
171
+ const path = systemdUserUnitPath();
172
+ mkdirSync(join(homedir(), ".config", "systemd", "user"), { recursive: true });
173
+ mkdirSync(amemHome(), { recursive: true });
174
+ writeFileSync(path, systemdUnitBody(), "utf8");
175
+ try {
176
+ systemctlUser(["daemon-reload"]);
177
+ }
178
+ catch {
179
+ // unit file still valid for next login / manual start
180
+ }
181
+ try {
182
+ systemctlUser(["enable", "--now", SYSTEMD_UNIT]);
183
+ }
184
+ catch {
185
+ try {
186
+ systemctlUser(["enable", SYSTEMD_UNIT]);
187
+ }
188
+ catch {
189
+ // enabled-at-login may require lingering; file is installed
190
+ }
191
+ }
192
+ return { path, installed: true };
193
+ }
194
+ function uninstallLinux() {
195
+ const path = systemdUserUnitPath();
196
+ try {
197
+ systemctlUser(["disable", "--now", SYSTEMD_UNIT]);
198
+ }
199
+ catch {
200
+ try {
201
+ systemctlUser(["disable", SYSTEMD_UNIT]);
202
+ }
203
+ catch {
204
+ // ignore
205
+ }
206
+ }
207
+ if (existsSync(path))
208
+ unlinkSync(path);
209
+ try {
210
+ systemctlUser(["daemon-reload"]);
211
+ }
212
+ catch {
213
+ // ignore
214
+ }
215
+ return { path, installed: false };
216
+ }
217
+ function installWindows() {
218
+ const path = windowsStartupCmdPath();
219
+ mkdirSync(join(path, ".."), { recursive: true });
220
+ writeFileSync(path, windowsCmdBody(), "utf8");
221
+ try {
222
+ chmodSync(path, 0o644);
223
+ }
224
+ catch {
225
+ // windows may ignore mode
226
+ }
227
+ return { path, installed: true };
228
+ }
229
+ function uninstallWindows() {
230
+ const path = windowsStartupCmdPath();
231
+ if (existsSync(path))
232
+ unlinkSync(path);
233
+ return { path, installed: false };
234
+ }
235
+ export function installLoginService() {
236
+ const platform = servicePlatform();
237
+ if (platform === "darwin")
238
+ return { ...installDarwin(), platform };
239
+ if (platform === "linux")
240
+ return { ...installLinux(), platform };
241
+ if (platform === "win32")
242
+ return { ...installWindows(), platform };
243
+ throw new Error("Login auto-start is not supported on this OS.");
244
+ }
245
+ export function uninstallLoginService() {
246
+ const platform = servicePlatform();
247
+ if (platform === "darwin")
248
+ return { ...uninstallDarwin(), platform };
249
+ if (platform === "linux")
250
+ return { ...uninstallLinux(), platform };
251
+ if (platform === "win32")
252
+ return { ...uninstallWindows(), platform };
253
+ return { path: serviceInstallPath(), installed: false, platform };
254
+ }
package/dist/shop.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /** Public shop URL for the local UI. The seller process is not part of the CLI. */
2
+ export declare function shopStatus(): {
3
+ url: string;
4
+ enabled: boolean;
5
+ proUrl: string;
6
+ itUrl: string;
7
+ proPrice: string;
8
+ itPrice: string;
9
+ };
package/dist/shop.js ADDED
@@ -0,0 +1,15 @@
1
+ /** Public shop URL for the local UI. The seller process is not part of the CLI. */
2
+ export function shopStatus() {
3
+ const url = (process.env.AMEM_SHOP_URL || "https://getamem.com").replace(/\/$/, "");
4
+ const disabled = String(process.env.AMEM_SHOP_LOCAL || "").trim() === "0";
5
+ const proCents = Number(process.env.STRIPE_AMOUNT_PRO_CENTS || 1200);
6
+ const itCents = Number(process.env.STRIPE_AMOUNT_IT_CENTS || 4900);
7
+ return {
8
+ url,
9
+ enabled: !disabled,
10
+ proUrl: `${url}/buy/pro`,
11
+ itUrl: `${url}/buy/it`,
12
+ proPrice: `$${(proCents / 100).toFixed(0)}`,
13
+ itPrice: `$${(itCents / 100).toFixed(0)}`,
14
+ };
15
+ }
@@ -0,0 +1,21 @@
1
+ export type UiServerOptions = {
2
+ port?: number;
3
+ cwd?: string;
4
+ openBrowser?: boolean;
5
+ /** Loopback only — non-loopback values are forced to 127.0.0.1 */
6
+ host?: string;
7
+ landingUrl?: string;
8
+ };
9
+ export declare function buildUiLandingUrl(port: number, cwd: string): string;
10
+ export declare function openUiInBrowser(url: string): void;
11
+ export declare function isAddrInUse(error: unknown): boolean;
12
+ export declare function probeUiHealth(port: number, host?: string): Promise<{
13
+ reachable: boolean;
14
+ hasVault: boolean;
15
+ features: string[];
16
+ }>;
17
+ export declare function startUiServer(options?: UiServerOptions): Promise<{
18
+ port: number;
19
+ url: string;
20
+ close: () => Promise<void>;
21
+ }>;
@@ -0,0 +1,268 @@
1
+ import { createServer } from "node:http";
2
+ import { readFileSync, existsSync, statSync } from "node:fs";
3
+ import { extname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { execFile } from "node:child_process";
6
+ import { handleApi } from "../api/routes.js";
7
+ import { detectRepoIdentity } from "../repo-identity.js";
8
+ import { handleMcpHttpBody, isJsonRpcMessage } from "../mcp.js";
9
+ const MIME = {
10
+ ".html": "text/html; charset=utf-8",
11
+ ".js": "text/javascript; charset=utf-8",
12
+ ".css": "text/css; charset=utf-8",
13
+ ".svg": "image/svg+xml",
14
+ ".json": "application/json",
15
+ ".map": "application/json",
16
+ };
17
+ function uiRoot() {
18
+ // dist/ui/server.js -> package root is ../..
19
+ const here = fileURLToPath(new URL(".", import.meta.url));
20
+ const packageRoot = join(here, "..", "..");
21
+ const viteDist = join(packageRoot, "dist", "ui-app");
22
+ const staticFallback = join(packageRoot, "ui-static");
23
+ if (existsSync(join(viteDist, "index.html")))
24
+ return viteDist;
25
+ if (existsSync(join(staticFallback, "index.html")))
26
+ return staticFallback;
27
+ return staticFallback;
28
+ }
29
+ async function readBody(req) {
30
+ const chunks = [];
31
+ for await (const chunk of req) {
32
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
33
+ }
34
+ if (chunks.length === 0)
35
+ return null;
36
+ const raw = Buffer.concat(chunks).toString("utf8");
37
+ if (!raw)
38
+ return null;
39
+ try {
40
+ return JSON.parse(raw);
41
+ }
42
+ catch {
43
+ return raw;
44
+ }
45
+ }
46
+ function sendJson(res, status, body) {
47
+ const payload = JSON.stringify(body);
48
+ res.writeHead(status, {
49
+ "Content-Type": "application/json; charset=utf-8",
50
+ "Cache-Control": "no-store",
51
+ "Access-Control-Allow-Origin": "*",
52
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
53
+ "Access-Control-Allow-Headers": "Content-Type, Accept, mcp-session-id, mcp-protocol-version, x-amem-workspace",
54
+ });
55
+ res.end(payload);
56
+ }
57
+ const MCP_CORS = {
58
+ "Access-Control-Allow-Origin": "*",
59
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
60
+ "Access-Control-Allow-Headers": "Content-Type, Accept, mcp-session-id, mcp-protocol-version, x-amem-workspace",
61
+ "Access-Control-Expose-Headers": "mcp-session-id, mcp-protocol-version",
62
+ };
63
+ function workspaceFromMcpReq(req, url) {
64
+ const q = url.searchParams.get("workspace")?.trim();
65
+ if (q)
66
+ return q;
67
+ const header = req.headers["x-amem-workspace"];
68
+ if (typeof header === "string" && header.trim())
69
+ return header.trim();
70
+ return process.env.AMEM_WORKSPACE || undefined;
71
+ }
72
+ function sendMcp(res, req, body, workspace) {
73
+ const handled = handleMcpHttpBody(body, workspace);
74
+ if (handled.status === 202) {
75
+ res.writeHead(202, MCP_CORS);
76
+ res.end();
77
+ return;
78
+ }
79
+ const payload = JSON.stringify(handled.body);
80
+ const accept = String(req.headers.accept ?? "");
81
+ const preferSse = accept.includes("text/event-stream") && !accept.includes("application/json");
82
+ if (preferSse) {
83
+ res.writeHead(handled.status, {
84
+ "Content-Type": "text/event-stream",
85
+ "Cache-Control": "no-cache",
86
+ Connection: "keep-alive",
87
+ ...MCP_CORS,
88
+ });
89
+ res.end(`event: message\ndata: ${payload}\n\n`);
90
+ return;
91
+ }
92
+ res.writeHead(handled.status, {
93
+ "Content-Type": "application/json; charset=utf-8",
94
+ "Cache-Control": "no-store",
95
+ ...MCP_CORS,
96
+ });
97
+ res.end(payload);
98
+ }
99
+ function serveStatic(res, urlPath) {
100
+ const root = uiRoot();
101
+ let rel = urlPath === "/" ? "/index.html" : urlPath;
102
+ rel = rel.split("?")[0] ?? rel;
103
+ const filePath = join(root, rel.replace(/^\//, ""));
104
+ if (!filePath.startsWith(root) || !existsSync(filePath) || !statSync(filePath).isFile()) {
105
+ // A missing asset must 404: falling back to index.html would hand the browser
106
+ // HTML with a JS content type and fail the module load for an unclear reason.
107
+ const ext = extname(filePath);
108
+ if (ext && ext !== ".html") {
109
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }).end("Not found");
110
+ return;
111
+ }
112
+ // SPA fallback
113
+ const index = join(root, "index.html");
114
+ if (existsSync(index)) {
115
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
116
+ res.end(readFileSync(index));
117
+ return;
118
+ }
119
+ res.writeHead(404).end("UI not built. Run npm run build.");
120
+ return;
121
+ }
122
+ const type = MIME[extname(filePath)] ?? "application/octet-stream";
123
+ const noStore = filePath.endsWith(".js") ||
124
+ filePath.endsWith(".css") ||
125
+ filePath.endsWith(".html") ||
126
+ filePath.endsWith("index.html");
127
+ res.writeHead(200, {
128
+ "Content-Type": type,
129
+ ...(noStore ? { "Cache-Control": "no-store" } : {}),
130
+ });
131
+ res.end(readFileSync(filePath));
132
+ }
133
+ function resolveLoopbackHost(host) {
134
+ if (!host || host === "localhost" || host === "127.0.0.1")
135
+ return "127.0.0.1";
136
+ return "127.0.0.1";
137
+ }
138
+ export function buildUiLandingUrl(port, cwd) {
139
+ const identity = detectRepoIdentity(cwd);
140
+ const q = new URLSearchParams();
141
+ q.set("tab", "setup");
142
+ q.set("scope", "all");
143
+ q.set("path", identity.rootPath);
144
+ return `http://127.0.0.1:${port}/?${q.toString()}`;
145
+ }
146
+ export function openUiInBrowser(url) {
147
+ try {
148
+ if (process.platform === "darwin")
149
+ execFile("open", [url]);
150
+ else if (process.platform === "win32")
151
+ execFile("cmd", ["/c", "start", url]);
152
+ else
153
+ execFile("xdg-open", [url]);
154
+ }
155
+ catch {
156
+ // ignore
157
+ }
158
+ }
159
+ export function isAddrInUse(error) {
160
+ return (typeof error === "object" &&
161
+ error !== null &&
162
+ "code" in error &&
163
+ error.code === "EADDRINUSE");
164
+ }
165
+ export async function probeUiHealth(port, host = "127.0.0.1") {
166
+ try {
167
+ const res = await fetch(`http://${host}:${port}/api/health`, {
168
+ signal: AbortSignal.timeout(1000),
169
+ });
170
+ if (res.ok) {
171
+ const body = (await res.json());
172
+ const features = Array.isArray(body.features)
173
+ ? body.features.filter((f) => typeof f === "string")
174
+ : [];
175
+ return { reachable: true, hasVault: features.includes("vault"), features };
176
+ }
177
+ }
178
+ catch {
179
+ // older builds have no /api/health
180
+ }
181
+ try {
182
+ const res = await fetch(`http://${host}:${port}/api/vault`, {
183
+ signal: AbortSignal.timeout(1000),
184
+ });
185
+ return { reachable: true, hasVault: res.ok, features: res.ok ? ["vault"] : [] };
186
+ }
187
+ catch {
188
+ return { reachable: false, hasVault: false, features: [] };
189
+ }
190
+ }
191
+ export async function startUiServer(options = {}) {
192
+ const requestedPort = options.port ?? 7843;
193
+ const cwd = options.cwd ?? process.cwd();
194
+ const listenHost = resolveLoopbackHost(options.host);
195
+ const server = createServer(async (req, res) => {
196
+ try {
197
+ const host = req.headers.host ?? `${listenHost}:${requestedPort}`;
198
+ const url = new URL(req.url ?? "/", `http://${host}`);
199
+ if (req.method === "OPTIONS") {
200
+ res.writeHead(204, MCP_CORS);
201
+ res.end();
202
+ return;
203
+ }
204
+ const mcpPath = url.pathname === "/mcp" || url.pathname === "/sse";
205
+ if (mcpPath && req.method === "GET") {
206
+ res.writeHead(200, {
207
+ "Content-Type": "text/event-stream",
208
+ "Cache-Control": "no-cache",
209
+ Connection: "keep-alive",
210
+ ...MCP_CORS,
211
+ });
212
+ res.write(": amem mcp\n\n");
213
+ req.on("close", () => res.end());
214
+ return;
215
+ }
216
+ if (mcpPath && req.method === "DELETE") {
217
+ res.writeHead(204, MCP_CORS);
218
+ res.end();
219
+ return;
220
+ }
221
+ if (url.pathname.startsWith("/api/")) {
222
+ const body = req.method === "GET" || req.method === "HEAD" ? null : await readBody(req);
223
+ const result = handleApi({
224
+ method: req.method ?? "GET",
225
+ pathname: url.pathname,
226
+ searchParams: url.searchParams,
227
+ body,
228
+ cwd,
229
+ });
230
+ sendJson(res, result.status, result.body);
231
+ return;
232
+ }
233
+ if (mcpPath && req.method === "POST") {
234
+ sendMcp(res, req, await readBody(req), workspaceFromMcpReq(req, url));
235
+ return;
236
+ }
237
+ if (req.method === "POST") {
238
+ const body = await readBody(req);
239
+ if (isJsonRpcMessage(body)) {
240
+ sendMcp(res, req, body, workspaceFromMcpReq(req, url));
241
+ return;
242
+ }
243
+ }
244
+ serveStatic(res, url.pathname);
245
+ }
246
+ catch (error) {
247
+ const message = error instanceof Error ? error.message : String(error);
248
+ sendJson(res, 500, { error: message });
249
+ }
250
+ });
251
+ await new Promise((resolve, reject) => {
252
+ server.once("error", reject);
253
+ server.listen(requestedPort, listenHost, () => resolve());
254
+ });
255
+ const addr = server.address();
256
+ const port = typeof addr === "object" && addr ? addr.port : requestedPort;
257
+ const url = options.landingUrl ?? `http://${listenHost}:${port}`;
258
+ if (options.openBrowser !== false) {
259
+ openUiInBrowser(url);
260
+ }
261
+ return {
262
+ port,
263
+ url,
264
+ close: () => new Promise((resolve, reject) => {
265
+ server.close((err) => (err ? reject(err) : resolve()));
266
+ }),
267
+ };
268
+ }
@@ -0,0 +1,25 @@
1
+ export type BackupEntry = {
2
+ name: string;
3
+ path: string;
4
+ bytes: number;
5
+ mtime: string;
6
+ encrypted: boolean;
7
+ };
8
+ export type VaultStatus = {
9
+ dbPath: string;
10
+ encryptedAtRest: boolean;
11
+ unlocked: boolean;
12
+ encCopyPresent: boolean;
13
+ encryptionConfigured: boolean;
14
+ backup: {
15
+ dir: string;
16
+ scheduled: boolean;
17
+ schedulePath: string;
18
+ last: BackupEntry | null;
19
+ recent: BackupEntry[];
20
+ count: number;
21
+ };
22
+ };
23
+ export declare function listBackups(dir?: string): BackupEntry[];
24
+ /** Lock/backup chrome — does not open the SQLite DB (works while locked). */
25
+ export declare function vaultStatus(): VaultStatus;
package/dist/vault.js ADDED
@@ -0,0 +1,42 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { defaultBackupDir, encryptedDbPath, encryptionConfigured, isDbEncryptedAtRest, } from "./crypto.js";
4
+ import { backupSchedulePath, isBackupScheduleInstalled } from "./backup-schedule.js";
5
+ import { dbPath } from "./paths.js";
6
+ export function listBackups(dir = defaultBackupDir()) {
7
+ if (!existsSync(dir))
8
+ return [];
9
+ return readdirSync(dir)
10
+ .filter((name) => name.endsWith(".db") || name.endsWith(".db.enc"))
11
+ .map((name) => {
12
+ const path = join(dir, name);
13
+ const st = statSync(path);
14
+ return {
15
+ name,
16
+ path,
17
+ bytes: st.size,
18
+ mtime: st.mtime.toISOString(),
19
+ encrypted: name.endsWith(".enc"),
20
+ };
21
+ })
22
+ .sort((a, b) => b.mtime.localeCompare(a.mtime));
23
+ }
24
+ /** Lock/backup chrome — does not open the SQLite DB (works while locked). */
25
+ export function vaultStatus() {
26
+ const backups = listBackups();
27
+ return {
28
+ dbPath: dbPath(),
29
+ encryptedAtRest: isDbEncryptedAtRest(),
30
+ unlocked: existsSync(dbPath()),
31
+ encCopyPresent: existsSync(encryptedDbPath()),
32
+ encryptionConfigured: encryptionConfigured(),
33
+ backup: {
34
+ dir: defaultBackupDir(),
35
+ scheduled: isBackupScheduleInstalled(),
36
+ schedulePath: isBackupScheduleInstalled() ? backupSchedulePath() : "",
37
+ last: backups[0] ?? null,
38
+ recent: backups.slice(0, 8),
39
+ count: backups.length,
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,8 @@
1
+ import { type RepoRow } from "./db.js";
2
+ export type WorkspaceReady = {
3
+ workspace: string;
4
+ seeded: number;
5
+ contextOk: boolean;
6
+ checks: string[];
7
+ };
8
+ export declare function provisionWorkspace(repo: RepoRow, platform?: string): WorkspaceReady;