mop-agent 0.1.7 → 0.1.9

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.
@@ -0,0 +1,110 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { usePathname } from "next/navigation";
5
+ import { useState } from "react";
6
+ import { signOut } from "@/lib/auth-client";
7
+
8
+ export type AppViewer = {
9
+ name: string;
10
+ email: string;
11
+ role: "owner" | "member";
12
+ };
13
+
14
+ function pageTitle(pathname: string): string {
15
+ if (pathname === "/assistant") return "Assistant";
16
+ if (pathname === "/brain/graph") return "Knowledge Graph";
17
+ if (pathname.startsWith("/brain/")) return "Project Brain";
18
+ if (pathname.startsWith("/brain")) return "Brain";
19
+ if (pathname.startsWith("/chat/")) return "Project Chat";
20
+ if (pathname.startsWith("/settings")) return "Settings";
21
+ return "MOP-AGENT";
22
+ }
23
+
24
+ export function AppShell({ viewer, children }: { viewer: AppViewer; children: ReactNode }) {
25
+ const pathname = usePathname();
26
+ const [menuOpen, setMenuOpen] = useState(false);
27
+ const isAdmin = viewer.role === "owner";
28
+ const title = pageTitle(pathname);
29
+
30
+ async function logout() {
31
+ await signOut();
32
+ window.location.replace("/login");
33
+ }
34
+
35
+ const nav = [
36
+ { href: "/assistant", label: "Assistant", icon: "✦", active: pathname.startsWith("/assistant") || pathname.startsWith("/chat/") },
37
+ { href: "/brain", label: "Brain", icon: "◉", active: pathname.startsWith("/brain") },
38
+ ];
39
+
40
+ return (
41
+ <div className="mop-app-frame">
42
+ <header className="mop-app-topbar">
43
+ <a className="mop-app-brand" href="/assistant" aria-label="MOP-AGENT home">
44
+ <img src="/icon.svg" alt="" />
45
+ <span>MOP-AGENT</span>
46
+ </a>
47
+ <div className="mop-app-topbar-main">
48
+ <button
49
+ className="mop-menu-toggle"
50
+ type="button"
51
+ aria-label="Toggle navigation"
52
+ aria-expanded={menuOpen}
53
+ onClick={() => setMenuOpen((open) => !open)}
54
+ >
55
+
56
+ </button>
57
+ <div className="mop-topbar-title">
58
+ <span className="mop-live-dot" />
59
+ <strong>{title}</strong>
60
+ </div>
61
+ <div className="mop-topbar-center">MOP MEMORYCORE</div>
62
+ <div className="mop-topbar-meta">
63
+ <span>{isAdmin ? "ADMIN" : "MEMBER"}</span>
64
+ <span className="mop-version">v0.1.9</span>
65
+ </div>
66
+ </div>
67
+ </header>
68
+
69
+ {menuOpen && <button className="mop-sidebar-scrim" aria-label="Close navigation" onClick={() => setMenuOpen(false)} />}
70
+
71
+ <aside className={`mop-app-sidebar${menuOpen ? " is-open" : ""}`}>
72
+ <div className="mop-nav-section">
73
+ <p>WORKSPACE</p>
74
+ <nav>
75
+ {nav.map((item) => (
76
+ <a key={item.href} href={item.href} className={item.active ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
77
+ <span className="mop-nav-icon">{item.icon}</span>
78
+ <span>{item.label}</span>
79
+ </a>
80
+ ))}
81
+ </nav>
82
+ </div>
83
+
84
+ {isAdmin && (
85
+ <div className="mop-nav-section">
86
+ <p>ADMIN</p>
87
+ <nav>
88
+ <a href="/settings" className={pathname.startsWith("/settings") ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
89
+ <span className="mop-nav-icon">⚙</span>
90
+ <span>Settings</span>
91
+ </a>
92
+ </nav>
93
+ </div>
94
+ )}
95
+
96
+ <div className="mop-sidebar-spacer" />
97
+ <button className="mop-account-card" type="button" onClick={logout} title="Sign out">
98
+ <span className="mop-account-avatar">{viewer.name.slice(0, 1).toUpperCase()}</span>
99
+ <span className="mop-account-copy">
100
+ <strong>{viewer.name}</strong>
101
+ <small>{isAdmin ? "Administrator" : "Member"}</small>
102
+ </span>
103
+ <span aria-hidden="true">↪</span>
104
+ </button>
105
+ </aside>
106
+
107
+ <main className="mop-app-main">{children}</main>
108
+ </div>
109
+ );
110
+ }
@@ -1,4 +1,4 @@
1
- import { auth, ownerExists } from "@/lib/auth";
1
+ import { auth, getRole, ownerExists } from "@/lib/auth";
2
2
  import { headers } from "next/headers";
3
3
  import { redirect } from "next/navigation";
4
4
  import { unstable_noStore as noStore } from "next/cache";
@@ -10,6 +10,13 @@ export async function requirePageSession() {
10
10
  if (!ownerExists()) redirect("/setup");
11
11
 
12
12
  const session = await auth.api.getSession({ headers: await headers() });
13
- if (!session) redirect("/setup");
13
+ if (!session) redirect("/login");
14
+ return session;
15
+ }
16
+
17
+ /** Server-side guard for pages that expose installation-wide administration. */
18
+ export async function requireOwnerPage() {
19
+ const session = await requirePageSession();
20
+ if (getRole(session.user.id) !== "owner") redirect("/assistant");
14
21
  return session;
15
22
  }
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { spawnSync } from "node:child_process";
14
14
  import { randomBytes } from "node:crypto";
15
- import { chmodSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
15
+ import { chmodSync, writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
16
16
  import { createInterface } from "node:readline/promises";
17
17
  import { stdin as input, stdout as output } from "node:process";
18
18
  import { dirname, resolve } from "node:path";
@@ -102,6 +102,57 @@ function q(value) {
102
102
  return `'${String(value).replaceAll("'", `'"'"'`)}'`;
103
103
  }
104
104
 
105
+ function readRuntimeConfig() {
106
+ const envPath = `${APP_DIR}/apps/web/.env`;
107
+ if (!existsSync(envPath)) throw new Error(`Runtime environment is missing: ${envPath}`);
108
+ const env = {};
109
+ for (const raw of readFileSync(envPath, "utf8").split(/\r?\n/)) {
110
+ const line = raw.trim();
111
+ if (!line || line.startsWith("#")) continue;
112
+ const at = line.indexOf("=");
113
+ if (at < 1) continue;
114
+ env[line.slice(0, at)] = line.slice(at + 1).replace(/^(['"])(.*)\1$/, "$2");
115
+ }
116
+ const port = env.PORT || "3000";
117
+ if (!isValidPort(port)) throw new Error(`Invalid PORT in ${envPath}: ${port}`);
118
+ let publicUrl;
119
+ try {
120
+ publicUrl = new URL(env.BETTER_AUTH_URL || `http://localhost:${port}`);
121
+ } catch {
122
+ throw new Error(`Invalid BETTER_AUTH_URL in ${envPath}`);
123
+ }
124
+ return { env, port, publicUrl };
125
+ }
126
+
127
+ /** Restore the installer-owned vhost after every update, including TLS. */
128
+ function reconcileNginx() {
129
+ const os = detectOS();
130
+ const { port, publicUrl } = readRuntimeConfig();
131
+ const domain = publicUrl.hostname;
132
+ if (!isValidDomain(domain) && domain !== "localhost") {
133
+ throw new Error(`Invalid domain in BETTER_AUTH_URL: ${domain}`);
134
+ }
135
+ const nginx = nginxPaths(os.family);
136
+ const certDir = `/etc/letsencrypt/live/${domain}`;
137
+ const hasTls = publicUrl.protocol === "https:" && existsSync(`${certDir}/fullchain.pem`) && existsSync(`${certDir}/privkey.pem`);
138
+ const vhost = hasTls ? renderNginxTlsVhost({ domain, port }) : renderNginxVhost({ domain, port });
139
+
140
+ console.log(c("cyan", "▸ Restore nginx reverse proxy"));
141
+ writeConf(nginx.conf, vhost, { privileged: true });
142
+ runSteps([
143
+ ...(nginx.enabled ? [{ label: "Enable nginx vhost", cmd: `ln -sf ${nginx.conf} ${nginx.enabled}` }] : []),
144
+ { label: "Verify nginx configuration", cmd: "nginx -t" },
145
+ { label: "Enable + start nginx", cmd: "systemctl enable --now nginx" },
146
+ { label: "Reload nginx", cmd: "systemctl reload nginx" },
147
+ { label: "Verify local application", cmd: `curl --fail --silent --show-error --max-time 15 ${q(`http://127.0.0.1:${port}/api/setup/status`)} >/dev/null` },
148
+ {
149
+ label: "Verify domain reverse proxy",
150
+ cmd: `curl --fail --silent --show-error --max-time 15 --resolve ${q(`${domain}:${hasTls ? "443" : "80"}:127.0.0.1`)} ${q(`${hasTls ? "https" : "http"}://${domain}/api/setup/status`)} >/dev/null`,
151
+ },
152
+ ], { privileged: true });
153
+ return { domain, port, protocol: hasTls ? "https" : "http" };
154
+ }
155
+
105
156
  // ---- commands ----------------------------------------------------------
106
157
 
107
158
  async function cmdInstall() {
@@ -242,21 +293,29 @@ function cmdUpdate() {
242
293
  { label: "Start new service", cmd: "systemctl start mop-agent", privileged: true },
243
294
  { label: "Verify service", cmd: "sleep 2 && systemctl is-active --quiet mop-agent", privileged: true },
244
295
  ]);
245
- console.log(c("green", "\n✓ updated, rebuilt, and service verified\n"));
296
+ const proxy = reconcileNginx();
297
+ console.log(c("green", `\n✓ updated and verified through ${proxy.protocol}://${proxy.domain}\n`));
246
298
  }
247
299
 
248
300
  function cmdStatus() {
249
301
  banner();
250
302
  if (!printInstallLocations()) return;
303
+ let runtime;
304
+ try { runtime = readRuntimeConfig(); } catch { runtime = null; }
305
+ const os = detectOS();
306
+ const nginx = nginxPaths(os.family);
251
307
  const checks = [
252
308
  ["service", "systemctl is-active mop-agent 2>/dev/null || echo inactive"],
253
309
  ["nginx", "systemctl is-active nginx 2>/dev/null || echo inactive"],
310
+ ["nginx conf", `test -f ${q(nginx.conf)} && echo present || echo missing`],
311
+ ...(nginx.enabled ? [["nginx link", `test -L ${q(nginx.enabled)} && echo present || echo missing`]] : []),
254
312
  [".env", existsSync(`${APP_DIR}/apps/web/.env`) ? "echo present" : "echo missing"],
313
+ ...(runtime ? [["local app", `curl --silent --output /dev/null --write-out '%{http_code}' --max-time 5 ${q(`http://127.0.0.1:${runtime.port}/api/setup/status`)} || echo failed`]] : []),
255
314
  ];
256
315
  for (const [label, cmd] of checks) {
257
316
  const r = run(cmd, { capture: true });
258
317
  const val = DRY ? "(dry-run)" : r.stdout.trim();
259
- console.log(` ${label.padEnd(10)} ${val === "active" || val === "present" ? c("green", val) : c("yellow", val)}`);
318
+ console.log(` ${label.padEnd(10)} ${val === "active" || val === "present" || val === "200" ? c("green", val) : c("yellow", val)}`);
260
319
  }
261
320
  console.log("");
262
321
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "mop-agent",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "mop-agent",
9
- "version": "0.1.7",
9
+ "version": "0.1.9",
10
10
  "license": "UNLICENSED",
11
11
  "workspaces": [
12
12
  "packages/*",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mop-agent",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Self-hosted AI assistant with persistent cross-project memory, installed with npx mop-agent.",
5
5
  "author": "BURHANDEV ENTERPRISE",
6
6
  "license": "UNLICENSED",
@@ -33,6 +33,7 @@
33
33
  "files": [
34
34
  "apps/web/app",
35
35
  "apps/web/bin",
36
+ "apps/web/components",
36
37
  "apps/web/lib",
37
38
  "apps/web/scripts",
38
39
  "apps/web/.env.example",