@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.
- package/LICENSE +21 -0
- package/README.md +534 -0
- package/dist/activity.d.ts +27 -0
- package/dist/activity.js +202 -0
- package/dist/api/routes.d.ts +31 -0
- package/dist/api/routes.js +1345 -0
- package/dist/attest.d.ts +52 -0
- package/dist/attest.js +192 -0
- package/dist/backup-schedule.d.ts +25 -0
- package/dist/backup-schedule.js +216 -0
- package/dist/capture.d.ts +34 -0
- package/dist/capture.js +257 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1007 -0
- package/dist/context.d.ts +46 -0
- package/dist/context.js +334 -0
- package/dist/crypto.d.ts +39 -0
- package/dist/crypto.js +166 -0
- package/dist/db.d.ts +192 -0
- package/dist/db.js +666 -0
- package/dist/draft-quality.d.ts +19 -0
- package/dist/draft-quality.js +85 -0
- package/dist/embed.d.ts +76 -0
- package/dist/embed.js +331 -0
- package/dist/estimate.d.ts +32 -0
- package/dist/estimate.js +69 -0
- package/dist/freshness.d.ts +15 -0
- package/dist/freshness.js +93 -0
- package/dist/hook.d.ts +16 -0
- package/dist/hook.js +177 -0
- package/dist/hygiene-schedule.d.ts +28 -0
- package/dist/hygiene-schedule.js +221 -0
- package/dist/hygiene.d.ts +61 -0
- package/dist/hygiene.js +196 -0
- package/dist/install/claude.d.ts +6 -0
- package/dist/install/claude.js +69 -0
- package/dist/install/cursor.d.ts +7 -0
- package/dist/install/cursor.js +80 -0
- package/dist/install/hosts.d.ts +21 -0
- package/dist/install/hosts.js +186 -0
- package/dist/install/skills.d.ts +8 -0
- package/dist/install/skills.js +67 -0
- package/dist/it-pack.d.ts +20 -0
- package/dist/it-pack.js +84 -0
- package/dist/kinds.d.ts +18 -0
- package/dist/kinds.js +106 -0
- package/dist/license.d.ts +48 -0
- package/dist/license.js +172 -0
- package/dist/mcp.d.ts +40 -0
- package/dist/mcp.js +435 -0
- package/dist/paths.d.ts +11 -0
- package/dist/paths.js +55 -0
- package/dist/personal.d.ts +7 -0
- package/dist/personal.js +44 -0
- package/dist/platforms.d.ts +11 -0
- package/dist/platforms.js +32 -0
- package/dist/policy.d.ts +46 -0
- package/dist/policy.js +254 -0
- package/dist/prefs.d.ts +6 -0
- package/dist/prefs.js +11 -0
- package/dist/proposal.d.ts +90 -0
- package/dist/proposal.js +376 -0
- package/dist/publish.d.ts +28 -0
- package/dist/publish.js +57 -0
- package/dist/remember-contract.d.ts +23 -0
- package/dist/remember-contract.js +117 -0
- package/dist/repo-identity.d.ts +15 -0
- package/dist/repo-identity.js +82 -0
- package/dist/rules-sync.d.ts +7 -0
- package/dist/rules-sync.js +47 -0
- package/dist/savings-export.d.ts +49 -0
- package/dist/savings-export.js +141 -0
- package/dist/scan.d.ts +16 -0
- package/dist/scan.js +109 -0
- package/dist/search.d.ts +25 -0
- package/dist/search.js +150 -0
- package/dist/service.d.ts +20 -0
- package/dist/service.js +254 -0
- package/dist/shop.d.ts +9 -0
- package/dist/shop.js +15 -0
- package/dist/ui/server.d.ts +21 -0
- package/dist/ui/server.js +268 -0
- package/dist/vault.d.ts +25 -0
- package/dist/vault.js +42 -0
- package/dist/workspace-setup.d.ts +8 -0
- package/dist/workspace-setup.js +55 -0
- package/docs/agent-install-prompt.md +41 -0
- package/docs/backlog.md +59 -0
- package/docs/enterprise-endpoint.md +98 -0
- package/docs/license.md +54 -0
- package/docs/npm-release.md +38 -0
- package/docs/remember-contract.md +42 -0
- package/package.json +64 -0
- package/scripts/mdm-offboard.sh +14 -0
- package/skills/amem-bootstrap/SKILL.md +83 -0
- package/skills/amem-update-working-memory/SKILL.md +54 -0
- package/templates/cursor-rule.mdc +23 -0
- package/templates/mdm/co.amem.managed.plist +33 -0
- package/templates/policy.deny-default.toml +20 -0
- package/templates/policy.example.toml +22 -0
- package/ui-static/app.js +3966 -0
- package/ui-static/index.html +178 -0
- package/ui-static/orbit.js +389 -0
- package/ui-static/styles.css +2411 -0
package/dist/service.js
ADDED
|
@@ -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, "<")
|
|
49
|
+
.replace(/>/g, ">")
|
|
50
|
+
.replace(/"/g, """);
|
|
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
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
|
+
}
|
package/dist/vault.d.ts
ADDED
|
@@ -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
|
+
}
|