mop-agent 0.1.1 → 0.1.3

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 CHANGED
@@ -5,8 +5,8 @@ through MOP-FLOW. It stores project memory, performs semantic recall and
5
5
  consolidation, serves grounded chat, and can request approved actions from a
6
6
  linked FLOW node.
7
7
 
8
- > **Release status:** npm package `mop-agent@0.1.1` contains the root/VPS
9
- > installer fix. After publishing 0.1.1, the canonical installation command is
8
+ > **Release status:** npm package `mop-agent@0.1.3` contains the corrected VPS
9
+ > installer and first-run Admin/Assistant flow. The canonical installation command is
10
10
  > exactly `npx mop-agent`.
11
11
 
12
12
  ## Current status
@@ -51,10 +51,25 @@ npx mop-agent
51
51
  ```
52
52
 
53
53
  The first run copies the npm-packaged runtime from the temporary npx cache into
54
- `/opt/mop-agent`, installs its dependencies, and opens the TUI. Choose
55
- `install` to install nginx/Certbot, then `setup` to configure the domain,
56
- SQLite database, HTTPS, and systemd service. The menu remains open between
57
- steps.
54
+ `/opt/mop-agent`, installs its dependencies, and opens a four-action TUI:
55
+
56
+ - `Install` installs nginx/Certbot and immediately continues through the
57
+ complete domain, SQLite, HTTPS, and systemd setup.
58
+ - `Update` — updates only MOP-AGENT, migrates/builds, and restarts it.
59
+ - `Status` — reports service health and filesystem locations.
60
+ - `Delete` — removes the service and nginx configuration while preserving data
61
+ unless purge is explicitly requested.
62
+
63
+ After installation, open the configured URL in a browser. The application flow
64
+ is intentionally separate from the server installer:
65
+
66
+ 1. On a fresh database, `/` redirects to **Create Admin account**.
67
+ 2. Creating the first Admin also signs that account in.
68
+ 3. Returning users are shown the login form.
69
+ 4. Successful setup/login opens the main **Assistant**. It can be used before
70
+ any project is linked; Brain is the optional memory/project control surface.
71
+ 5. Add OpenRouter or Anthropic under **Providers** for full model responses.
72
+ Until then, the built-in offline echo provider confirms the chat pipeline.
58
73
 
59
74
  During `setup`, choose one deployment mode:
60
75
 
@@ -72,10 +87,8 @@ needs to write under `/opt` or `/etc`, install OS packages, or control
72
87
  nginx/systemd. When launched as root, it creates a locked-down `mop-agent`
73
88
  system account and runs the web service under that account—not as root.
74
89
 
75
- During the `install` step, MOP-AGENT checks the installed npm version. If a
76
- newer npm is available it displays the version and Node.js requirement, then
77
- asks before running the global npm update. Set `MOP_AGENT_SKIP_NPM_UPDATE=1` to
78
- skip this check.
90
+ MOP-AGENT never upgrades or modifies the system npm installation. npm and
91
+ Node.js upgrades remain an explicit server-administration task.
79
92
 
80
93
  Subsequent operations use the same command:
81
94
 
@@ -137,7 +150,7 @@ npm run typecheck
137
150
  npm run dev:web
138
151
  ```
139
152
 
140
- Open `http://localhost:3000/setup`. Native Windows production service and HTTPS
153
+ Open `http://localhost:3000`; it redirects to Admin setup on first run. Native Windows production service and HTTPS
141
154
  automation remain TODO; do not use `sudo` in PowerShell or Command Prompt.
142
155
 
143
156
  ## macOS
@@ -153,7 +166,7 @@ npm run typecheck
153
166
  npm run dev:web
154
167
  ```
155
168
 
156
- Open `http://localhost:3000/setup`. A launchd/Homebrew/nginx production
169
+ Open `http://localhost:3000`; it redirects to Admin setup on first run. A launchd/Homebrew/nginx production
157
170
  installer is not implemented; use a supported Linux VPS for production.
158
171
 
159
172
  ## Development
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * POST /api/chat — grounded chat. Owner session -> recall project context ->
3
- * stream the provider's answer. Body: { projectId, message, allowCrossProject? }
3
+ * stream the provider's answer. projectId is optional for the main assistant.
4
+ * Body: { message, projectId?, allowCrossProject? }
4
5
  */
5
6
  import { auth } from "@/lib/auth";
6
7
  import { recall } from "@/lib/brain/broker";
@@ -11,20 +12,28 @@ export async function POST(req: Request): Promise<Response> {
11
12
  if (!session) return Response.json({ error: "unauthorized" }, { status: 401 });
12
13
 
13
14
  const { projectId, message, allowCrossProject } = (await req.json()) as {
14
- projectId: string;
15
+ projectId?: string;
15
16
  message: string;
16
17
  allowCrossProject?: boolean;
17
18
  };
18
- if (!projectId || !message) {
19
- return Response.json({ error: "missing_projectId_or_message" }, { status: 400 });
19
+ if (typeof message !== "string" || !message.trim()) {
20
+ return Response.json({ error: "missing_message" }, { status: 400 });
20
21
  }
21
22
 
22
- const pack = await recall({ query: message, projectId, allowCrossProject: !!allowCrossProject });
23
+ const centralAssistant = !projectId;
24
+ const pack = await recall({
25
+ query: message.trim(),
26
+ projectId,
27
+ // The main assistant is the authenticated, cross-project surface.
28
+ allowCrossProject: centralAssistant || !!allowCrossProject,
29
+ });
23
30
  const provider = resolveProvider(session.user.id);
24
31
 
25
32
  const system = [
26
- "You are the MOP-AGENT brain. Answer using the project context below when relevant.",
27
- "If the context is empty, say so plainly.",
33
+ "You are MOP-AGENT, a self-hosted AI assistant with persistent memory.",
34
+ centralAssistant
35
+ ? "Help the user directly. Use the available cross-project memory when relevant; a linked project is not required."
36
+ : "Help the user with the selected project and use its memory when relevant.",
28
37
  "",
29
38
  pack.toPromptString(),
30
39
  ].join("\n");
@@ -33,7 +42,7 @@ export async function POST(req: Request): Promise<Response> {
33
42
  const stream = new ReadableStream<Uint8Array>({
34
43
  async start(controller) {
35
44
  try {
36
- for await (const delta of provider.chat({ system, messages: [{ role: "user", content: message }] })) {
45
+ for await (const delta of provider.chat({ system, messages: [{ role: "user", content: message.trim() }] })) {
37
46
  controller.enqueue(encoder.encode(delta));
38
47
  }
39
48
  } catch (e) {
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ import { requirePageSession } from "@/lib/page-auth";
3
+
4
+ export default async function AssistantLayout({ children }: { children: ReactNode }) {
5
+ await requirePageSession();
6
+ return children;
7
+ }
@@ -0,0 +1,196 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { signOut } from "@/lib/auth-client";
5
+
6
+ type Turn = { role: "user" | "assistant"; content: string };
7
+ type Project = { id: string; name: string; status: string };
8
+ type ProviderState = { configured: boolean; provider?: string; model?: string | null };
9
+
10
+ export default function AssistantPage() {
11
+ const [turns, setTurns] = useState<Turn[]>([]);
12
+ const [projects, setProjects] = useState<Project[]>([]);
13
+ const [selectedProject, setSelectedProject] = useState("");
14
+ const [provider, setProvider] = useState<ProviderState>({ configured: false });
15
+ const [name, setName] = useState("Admin");
16
+ const [input, setInput] = useState("");
17
+ const [busy, setBusy] = useState(false);
18
+ const [providerUsed, setProviderUsed] = useState("");
19
+ const endRef = useRef<HTMLDivElement>(null);
20
+
21
+ useEffect(() => {
22
+ Promise.all([
23
+ fetch("/api/projects").then((r) => r.json()),
24
+ fetch("/api/providers").then((r) => r.json()),
25
+ fetch("/api/me").then((r) => r.json()),
26
+ ]).then(([projectData, providerData, me]) => {
27
+ setProjects(projectData.projects ?? []);
28
+ setProvider(providerData.config ?? { configured: false });
29
+ setName(me.user?.name || me.user?.email || "Admin");
30
+ }).catch(() => {});
31
+ }, []);
32
+
33
+ async function send(prefill?: string) {
34
+ const message = (prefill ?? input).trim();
35
+ if (!message || busy) return;
36
+ setInput("");
37
+ setBusy(true);
38
+ setTurns((current) => [...current, { role: "user", content: message }, { role: "assistant", content: "" }]);
39
+
40
+ const res = await fetch("/api/chat", {
41
+ method: "POST",
42
+ headers: { "content-type": "application/json" },
43
+ body: JSON.stringify({
44
+ message,
45
+ projectId: selectedProject || undefined,
46
+ allowCrossProject: !selectedProject,
47
+ }),
48
+ });
49
+
50
+ setProviderUsed(res.headers.get("X-Provider") ?? "");
51
+ if (!res.ok || !res.body) {
52
+ setTurns((current) => {
53
+ const next = [...current];
54
+ next[next.length - 1] = { role: "assistant", content: `Unable to answer (error ${res.status}).` };
55
+ return next;
56
+ });
57
+ setBusy(false);
58
+ return;
59
+ }
60
+
61
+ const reader = res.body.getReader();
62
+ const decoder = new TextDecoder();
63
+ let answer = "";
64
+ for (;;) {
65
+ const { done, value } = await reader.read();
66
+ if (done) break;
67
+ answer += decoder.decode(value, { stream: true });
68
+ setTurns((current) => {
69
+ const next = [...current];
70
+ next[next.length - 1] = { role: "assistant", content: answer };
71
+ return next;
72
+ });
73
+ endRef.current?.scrollIntoView({ behavior: "smooth" });
74
+ }
75
+ setBusy(false);
76
+ }
77
+
78
+ async function logout() {
79
+ await signOut();
80
+ window.location.replace("/setup");
81
+ }
82
+
83
+ return (
84
+ <main style={appShell}>
85
+ <aside style={sidebar}>
86
+ <a href="/assistant" style={brand}><span style={brandMark}>M</span><strong>MOP-AGENT</strong></a>
87
+ <nav style={{ display: "grid", gap: 6, marginTop: 34 }}>
88
+ <a href="/assistant" style={{ ...navItem, ...navActive }}>✦ Assistant</a>
89
+ <a href="/brain" style={navItem}>◉ Brain</a>
90
+ <a href="/settings" style={navItem}>⚙ Providers</a>
91
+ <a href="/team" style={navItem}>♙ Team</a>
92
+ </nav>
93
+
94
+ <div style={{ marginTop: "auto", display: "grid", gap: 10 }}>
95
+ {!provider.configured && (
96
+ <a href="/settings" style={setupCard}>
97
+ <strong style={{ color: "#e9efff" }}>Connect an AI model</strong>
98
+ <span style={{ fontSize: 12, lineHeight: 1.45 }}>Offline demo is active. Add OpenRouter or Anthropic for full answers.</span>
99
+ </a>
100
+ )}
101
+ <button onClick={logout} style={accountButton} title="Sign out">
102
+ <span style={avatar}>{name.slice(0, 1).toUpperCase()}</span>
103
+ <span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{name}</span>
104
+ <span style={{ marginLeft: "auto", opacity: .55 }}>↪</span>
105
+ </button>
106
+ </div>
107
+ </aside>
108
+
109
+ <section style={workspace}>
110
+ <header style={topbar}>
111
+ <div>
112
+ <strong>Assistant</strong>
113
+ <span style={{ color: "#637188", marginLeft: 9, fontSize: 12 }}>{provider.configured ? `${provider.provider}${provider.model ? ` · ${provider.model}` : ""}` : "offline demo"}</span>
114
+ </div>
115
+ <label style={{ color: "#8090a6", fontSize: 12 }}>
116
+ Memory scope&nbsp;
117
+ <select value={selectedProject} onChange={(e) => setSelectedProject(e.target.value)} style={selectStyle}>
118
+ <option value="">All memory</option>
119
+ {projects.map((project) => <option key={project.id} value={project.id}>{project.name}</option>)}
120
+ </select>
121
+ </label>
122
+ </header>
123
+
124
+ <div style={conversation}>
125
+ {turns.length === 0 ? (
126
+ <div style={welcome}>
127
+ <div style={assistantOrb}>✦</div>
128
+ <p style={{ color: "#7d9dff", fontSize: 12, fontWeight: 800, letterSpacing: ".13em" }}>MOP-AGENT IS READY</p>
129
+ <h1 style={{ fontSize: "clamp(28px, 4vw, 42px)", margin: "8px 0 12px" }}>What are we working on, {name.split(" ")[0]}?</h1>
130
+ <p style={{ color: "#8290a4", maxWidth: 610, lineHeight: 1.65 }}>
131
+ Start talking immediately. Link projects when you want MOP-AGENT to remember their state and work across them.
132
+ </p>
133
+ <div style={promptGrid}>
134
+ {["Help me plan today’s work", "What can MOP-AGENT do?", "Summarize what you remember", "Plan a new software project"].map((prompt) => (
135
+ <button key={prompt} onClick={() => send(prompt)} style={promptCard}>{prompt}<span>→</span></button>
136
+ ))}
137
+ </div>
138
+ {projects.length === 0 && (
139
+ <p style={{ fontSize: 13, color: "#6f7d91", marginTop: 24 }}>
140
+ No project linked yet—this does not block chat. <a href="/brain" style={{ color: "#88a3ff" }}>Link one from Brain →</a>
141
+ </p>
142
+ )}
143
+ </div>
144
+ ) : (
145
+ <div style={{ width: "min(100%, 820px)", margin: "0 auto", padding: "28px 0 160px" }}>
146
+ {turns.map((turn, index) => (
147
+ <article key={index} style={{ display: "grid", gridTemplateColumns: "34px 1fr", gap: 13, marginBottom: 26 }}>
148
+ <span style={turn.role === "assistant" ? botAvatar : userAvatar}>{turn.role === "assistant" ? "✦" : name.slice(0, 1).toUpperCase()}</span>
149
+ <div>
150
+ <strong style={{ fontSize: 13, color: turn.role === "assistant" ? "#91a9ff" : "#dce5f4" }}>{turn.role === "assistant" ? "MOP-AGENT" : "You"}</strong>
151
+ <div style={{ whiteSpace: "pre-wrap", lineHeight: 1.7, marginTop: 6, color: "#c4cfdd" }}>{turn.content || "Thinking…"}</div>
152
+ </div>
153
+ </article>
154
+ ))}
155
+ <div ref={endRef} />
156
+ </div>
157
+ )}
158
+ </div>
159
+
160
+ <div style={composerWrap}>
161
+ <div style={composer}>
162
+ <textarea value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} placeholder="Message MOP-AGENT…" rows={1} style={textarea} />
163
+ <button onClick={() => send()} disabled={busy || !input.trim()} style={{ ...sendButton, opacity: busy || !input.trim() ? .45 : 1 }}>↑</button>
164
+ </div>
165
+ <div style={{ textAlign: "center", color: "#536074", fontSize: 11, marginTop: 8 }}>
166
+ {providerUsed ? `Answered by ${providerUsed} · ` : ""}{selectedProject ? "Selected project memory" : "Cross-project memory"}
167
+ </div>
168
+ </div>
169
+ </section>
170
+ </main>
171
+ );
172
+ }
173
+
174
+ const appShell: React.CSSProperties = { minHeight: "100vh", display: "grid", gridTemplateColumns: "235px 1fr", background: "#080c13" };
175
+ const sidebar: React.CSSProperties = { padding: "22px 15px", borderRight: "1px solid #182131", background: "#0b1019", display: "flex", flexDirection: "column", minHeight: "calc(100vh - 44px)" };
176
+ const brand: React.CSSProperties = { display: "flex", alignItems: "center", gap: 10, color: "#eaf0fa", textDecoration: "none", padding: "4px 8px" };
177
+ const brandMark: React.CSSProperties = { width: 28, height: 28, borderRadius: 8, display: "grid", placeItems: "center", background: "linear-gradient(135deg,#4b73ff,#8159e8)", fontSize: 13 };
178
+ const navItem: React.CSSProperties = { color: "#8795a9", textDecoration: "none", padding: "10px 12px", borderRadius: 8, fontSize: 14 };
179
+ const navActive: React.CSSProperties = { color: "#e7edff", background: "#172137", boxShadow: "inset 2px 0 #6687ff" };
180
+ const setupCard: React.CSSProperties = { display: "grid", gap: 5, padding: 12, border: "1px solid #293752", borderRadius: 10, background: "#111a2a", color: "#8090a8", textDecoration: "none" };
181
+ const accountButton: React.CSSProperties = { display: "flex", alignItems: "center", gap: 9, padding: 8, border: 0, borderRadius: 9, background: "transparent", color: "#98a6b9", cursor: "pointer", textAlign: "left" };
182
+ const avatar: React.CSSProperties = { width: 28, height: 28, borderRadius: 8, display: "grid", placeItems: "center", background: "#232f45", color: "#b9c8df", fontSize: 12 };
183
+ const workspace: React.CSSProperties = { minWidth: 0, minHeight: "100vh", position: "relative", display: "flex", flexDirection: "column", background: "radial-gradient(circle at 50% 4%, #10182a 0, #080c13 38%)" };
184
+ const topbar: React.CSSProperties = { height: 62, padding: "0 24px", display: "flex", alignItems: "center", justifyContent: "space-between", borderBottom: "1px solid #182131", background: "rgba(8,12,19,.72)", backdropFilter: "blur(16px)" };
185
+ const selectStyle: React.CSSProperties = { color: "#b9c5d6", border: "1px solid #273348", borderRadius: 7, padding: "6px 8px", background: "#0d131e" };
186
+ const conversation: React.CSSProperties = { flex: 1, overflowY: "auto", padding: "0 28px" };
187
+ const welcome: React.CSSProperties = { minHeight: "calc(100vh - 220px)", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", textAlign: "center", paddingBottom: 60 };
188
+ const assistantOrb: React.CSSProperties = { width: 58, height: 58, borderRadius: 18, display: "grid", placeItems: "center", fontSize: 24, color: "white", background: "linear-gradient(135deg,#456fff,#8b56df)", boxShadow: "0 15px 55px rgba(85,105,255,.28)" };
189
+ const promptGrid: React.CSSProperties = { width: "min(100%, 650px)", display: "grid", gridTemplateColumns: "repeat(2,minmax(0,1fr))", gap: 10, marginTop: 28 };
190
+ const promptCard: React.CSSProperties = { display: "flex", justifyContent: "space-between", padding: "14px 15px", borderRadius: 10, border: "1px solid #222e42", background: "rgba(16,23,36,.72)", color: "#aebacc", cursor: "pointer", textAlign: "left" };
191
+ const botAvatar: React.CSSProperties = { width: 32, height: 32, borderRadius: 9, display: "grid", placeItems: "center", background: "linear-gradient(135deg,#456fff,#8058df)", color: "white" };
192
+ const userAvatar: React.CSSProperties = { ...botAvatar, background: "#263248", color: "#c5d0df", fontSize: 12 };
193
+ const composerWrap: React.CSSProperties = { position: "absolute", left: 0, right: 0, bottom: 0, padding: "28px 30px 18px", background: "linear-gradient(transparent,#080c13 28%)" };
194
+ const composer: React.CSSProperties = { width: "min(calc(100% - 32px), 800px)", margin: "0 auto", display: "flex", alignItems: "flex-end", gap: 10, padding: "10px 10px 10px 15px", border: "1px solid #2a3951", borderRadius: 14, background: "#101722", boxShadow: "0 15px 55px rgba(0,0,0,.28)" };
195
+ const textarea: React.CSSProperties = { flex: 1, resize: "none", border: 0, outline: 0, background: "transparent", color: "#e7edf5", font: "inherit", lineHeight: 1.55, padding: "5px 0" };
196
+ const sendButton: React.CSSProperties = { width: 34, height: 34, border: 0, borderRadius: 9, background: "#5577f7", color: "white", fontSize: 18, cursor: "pointer" };
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ import { requirePageSession } from "@/lib/page-auth";
3
+
4
+ export default async function BrainLayout({ children }: { children: ReactNode }) {
5
+ await requirePageSession();
6
+ return children;
7
+ }
@@ -68,6 +68,7 @@ export default function BrainPage() {
68
68
 
69
69
  return (
70
70
  <main style={{ maxWidth: 900, margin: "0 auto", padding: "40px 24px" }}>
71
+ <p style={{ margin: 0 }}><a href="/assistant" style={{ color: "#7aa2ff" }}>← Assistant</a></p>
71
72
  <h1 style={{ fontSize: 24 }}>🧠 Brain</h1>
72
73
  <p style={{ opacity: 0.65 }}>
73
74
  Main Brain + linked project brains. <a href="/brain/graph" style={{ color: "#7aa2ff" }}>🕸 Knowledge graph →</a>{" "}
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ import { requirePageSession } from "@/lib/page-auth";
3
+
4
+ export default async function ChatLayout({ children }: { children: ReactNode }) {
5
+ await requirePageSession();
6
+ return children;
7
+ }
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
2
2
 
3
3
  export const metadata = {
4
4
  title: "MOP-AGENT",
5
- description: "Self-hostable AI brain remembers everything, federates every project.",
5
+ description: "Self-hosted AI assistant with persistent, cross-project memory.",
6
6
  };
7
7
 
8
8
  export default function RootLayout({ children }: { children: ReactNode }) {
@@ -1,72 +1,11 @@
1
+ import { requirePageSession } from "@/lib/page-auth";
2
+ import { redirect } from "next/navigation";
3
+
1
4
  /**
2
- * Status landing (Fasa 1). Shows linked projects + a "Link Project" helper.
3
- * TODO Fasa 2+: replace with setup wizard + Brain dashboard (see PRD §13 wireframes).
5
+ * Product entry point. First-run and signed-out users belong in the account
6
+ * setup flow; authenticated users land in the assistant, not the Brain tools.
4
7
  */
5
- async function getProjects(): Promise<{ projects: Array<Record<string, unknown>> }> {
6
- try {
7
- const res = await fetch(`http://localhost:${process.env.PORT ?? 3000}/api/projects`, {
8
- cache: "no-store",
9
- });
10
- return (await res.json()) as { projects: Array<Record<string, unknown>> };
11
- } catch {
12
- return { projects: [] };
13
- }
14
- }
15
-
16
8
  export default async function Home() {
17
- const { projects } = await getProjects();
18
- return (
19
- <main style={{ maxWidth: 820, margin: "0 auto", padding: "48px 24px" }}>
20
- <h1 style={{ fontSize: 28, marginBottom: 4 }}>🧠 MOP-AGENT</h1>
21
- <p style={{ opacity: 0.7, marginTop: 0 }}>
22
- The Brain — remembers everything, federates every project. Status: scaffolding (Fasa 1).
23
- </p>
24
-
25
- <section style={{ marginTop: 32 }}>
26
- <h2 style={{ fontSize: 18 }}>Linked projects ({projects.length})</h2>
27
- {projects.length === 0 ? (
28
- <p style={{ opacity: 0.6 }}>No projects linked yet.</p>
29
- ) : (
30
- <ul style={{ listStyle: "none", padding: 0 }}>
31
- {projects.map((p) => (
32
- <li
33
- key={String(p.id)}
34
- style={{
35
- border: "1px solid #1f2a3a",
36
- borderRadius: 8,
37
- padding: "12px 16px",
38
- marginBottom: 8,
39
- }}
40
- >
41
- <strong>{String(p.name)}</strong>{" "}
42
- <span style={{ opacity: 0.6 }}>
43
- · {String(p.status)} · {String(p.memoryCount)} memories
44
- </span>
45
- </li>
46
- ))}
47
- </ul>
48
- )}
49
- </section>
50
-
51
- <section style={{ marginTop: 32 }}>
52
- <h2 style={{ fontSize: 18 }}>Link a project (dev)</h2>
53
- <pre
54
- style={{
55
- background: "#111824",
56
- border: "1px solid #1f2a3a",
57
- borderRadius: 8,
58
- padding: 16,
59
- overflowX: "auto",
60
- }}
61
- >
62
- {`# 1) get a pairing code
63
- curl -X POST http://localhost:3000/api/link/code
64
-
65
- # 2) in a project dir that has .MOP/, run the dev connector
66
- mop-flow-dev link --url http://localhost:3000 --code <CODE> --project <id>
67
- mop-flow-dev serve`}
68
- </pre>
69
- </section>
70
- </main>
71
- );
9
+ await requirePageSession();
10
+ redirect("/assistant");
72
11
  }
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ import { requirePageSession } from "@/lib/page-auth";
3
+
4
+ export default async function SettingsLayout({ children }: { children: ReactNode }) {
5
+ await requirePageSession();
6
+ return children;
7
+ }
@@ -33,7 +33,7 @@ export default function SettingsPage() {
33
33
 
34
34
  return (
35
35
  <main style={{ maxWidth: 520, margin: "0 auto", padding: "48px 24px" }}>
36
- <a href="/brain" style={{ color: "#7aa2ff" }}>← Brain</a>
36
+ <a href="/assistant" style={{ color: "#7aa2ff" }}>← Assistant</a>
37
37
  <h1 style={{ fontSize: 24 }}>⚙️ Provider settings</h1>
38
38
 
39
39
  <div style={{ border: "1px solid #1f2a3a", borderRadius: 8, padding: 14, margin: "12px 0", opacity: 0.9 }}>
@@ -1,113 +1,132 @@
1
1
  "use client";
2
- /**
3
- * Owner setup (Fasa 2). First run only: create the owner account.
4
- * Once an owner exists, the form locks (signups closed — single-owner self-host).
5
- */
2
+
6
3
  import { useEffect, useState } from "react";
7
- import { signUp, signIn } from "@/lib/auth-client";
4
+ import { signIn, signUp } from "@/lib/auth-client";
8
5
 
9
6
  export default function SetupPage() {
10
7
  const [ownerExists, setOwnerExists] = useState<boolean | null>(null);
11
8
  const [email, setEmail] = useState("");
12
9
  const [password, setPassword] = useState("");
13
10
  const [name, setName] = useState("");
14
- const [msg, setMsg] = useState<string>("");
11
+ const [msg, setMsg] = useState("");
12
+ const [busy, setBusy] = useState(false);
15
13
  const [mode, setMode] = useState<"signup" | "signin">("signup");
16
14
 
17
15
  useEffect(() => {
18
- fetch("/api/setup/status")
19
- .then((r) => r.json())
20
- .then((d: { ownerExists: boolean }) => {
21
- setOwnerExists(d.ownerExists);
22
- setMode(d.ownerExists ? "signin" : "signup");
16
+ Promise.all([
17
+ fetch("/api/setup/status").then((r) => r.json()) as Promise<{ ownerExists: boolean }>,
18
+ fetch("/api/me"),
19
+ ])
20
+ .then(([status, me]) => {
21
+ if (me.ok) {
22
+ window.location.replace("/assistant");
23
+ return;
24
+ }
25
+ setOwnerExists(status.ownerExists);
26
+ setMode(status.ownerExists ? "signin" : "signup");
23
27
  })
24
- .catch(() => setOwnerExists(null));
28
+ .catch(() => setMsg("Unable to reach MOP-AGENT. Please refresh."));
25
29
  }, []);
26
30
 
27
31
  async function submit(e: React.FormEvent) {
28
32
  e.preventDefault();
29
- setMsg("…");
33
+ setBusy(true);
34
+ setMsg("");
35
+
30
36
  if (mode === "signup") {
31
- const res = await signUp.email({ email, password, name: name || email });
32
- setMsg(res.error ? `Error: ${res.error.message}` : "Owner created. You can sign in.");
33
- if (!res.error) setMode("signin");
34
- } else {
35
- const res = await signIn.email({ email, password });
36
- setMsg(res.error ? `Error: ${res.error.message}` : "Signed in ✓ → go to /");
37
- if (!res.error) window.location.href = "/";
37
+ const res = await signUp.email({ email, password, name: name.trim() || email });
38
+ if (res.error) {
39
+ setMsg(res.error.message ?? "Account setup failed.");
40
+ setBusy(false);
41
+ return;
42
+ }
43
+ // Better Auth creates the first session together with the owner account.
44
+ window.location.replace("/assistant");
45
+ return;
46
+ }
47
+
48
+ const res = await signIn.email({ email, password });
49
+ if (res.error) {
50
+ setMsg(res.error.message ?? "Sign in failed.");
51
+ setBusy(false);
52
+ return;
38
53
  }
54
+ window.location.replace("/assistant");
39
55
  }
40
56
 
57
+ const loading = ownerExists === null && !msg;
58
+
41
59
  return (
42
- <main style={{ maxWidth: 420, margin: "0 auto", padding: "64px 24px" }}>
43
- <h1 style={{ fontSize: 24 }}>🧠 MOP-AGENT · Setup</h1>
44
- <p style={{ opacity: 0.7 }}>
45
- {ownerExists === null
46
- ? ""
47
- : ownerExists
48
- ? "Owner exists — sign in."
49
- : "Create the owner account (first run)."}
50
- </p>
60
+ <main style={shell}>
61
+ <section style={brandPanel}>
62
+ <div style={logo}>M</div>
63
+ <p style={eyebrow}>SELF-HOSTED AI ASSISTANT</p>
64
+ <h1 style={{ fontSize: "clamp(32px, 5vw, 52px)", lineHeight: 1.05, margin: "12px 0 18px" }}>
65
+ Your assistant.<br />Your server. Your memory.
66
+ </h1>
67
+ <p style={{ color: "#9aa8bd", fontSize: 17, lineHeight: 1.7, maxWidth: 520 }}>
68
+ MOP-AGENT gives you one private assistant across your projects. Brain is the memory layer behind it—not the first hurdle.
69
+ </p>
70
+ <div style={featureRow}>
71
+ <span>◆ Private</span><span>◆ Persistent memory</span><span>◆ Cross-project</span>
72
+ </div>
73
+ </section>
74
+
75
+ <section style={formWrap}>
76
+ <div style={formCard}>
77
+ <p style={eyebrow}>{mode === "signup" ? "FIRST-RUN SETUP" : "WELCOME BACK"}</p>
78
+ <h2 style={{ fontSize: 27, margin: "8px 0" }}>
79
+ {loading ? "Checking server…" : mode === "signup" ? "Create Admin account" : "Sign in to MOP-AGENT"}
80
+ </h2>
81
+ <p style={{ color: "#8c9bb0", lineHeight: 1.55, marginTop: 0 }}>
82
+ {mode === "signup"
83
+ ? "This first account controls providers, team access, projects, and system memory."
84
+ : "Use your Admin or invited team account."}
85
+ </p>
51
86
 
52
- <form onSubmit={submit} style={{ display: "grid", gap: 12, marginTop: 24 }}>
53
- {mode === "signup" && (
54
- <input
55
- placeholder="Name"
56
- value={name}
57
- onChange={(e) => setName(e.target.value)}
58
- style={inputStyle}
59
- />
60
- )}
61
- <input
62
- placeholder="Email"
63
- type="email"
64
- required
65
- value={email}
66
- onChange={(e) => setEmail(e.target.value)}
67
- style={inputStyle}
68
- />
69
- <input
70
- placeholder="Password (min 8)"
71
- type="password"
72
- required
73
- minLength={8}
74
- value={password}
75
- onChange={(e) => setPassword(e.target.value)}
76
- style={inputStyle}
77
- />
78
- <button type="submit" style={buttonStyle}>
79
- {mode === "signup" ? "Create owner" : "Sign in"}
80
- </button>
81
- </form>
87
+ {!loading && (
88
+ <form onSubmit={submit} style={{ display: "grid", gap: 14, marginTop: 28 }}>
89
+ {mode === "signup" && (
90
+ <label style={label}>Display name
91
+ <input placeholder="Admin" autoComplete="name" value={name} onChange={(e) => setName(e.target.value)} style={inputStyle} />
92
+ </label>
93
+ )}
94
+ <label style={label}>Email
95
+ <input placeholder="admin@example.com" type="email" autoComplete="email" required value={email} onChange={(e) => setEmail(e.target.value)} style={inputStyle} />
96
+ </label>
97
+ <label style={label}>Password
98
+ <input placeholder="Minimum 8 characters" type="password" autoComplete={mode === "signup" ? "new-password" : "current-password"} required minLength={8} value={password} onChange={(e) => setPassword(e.target.value)} style={inputStyle} />
99
+ </label>
100
+ <button type="submit" disabled={busy} style={{ ...buttonStyle, opacity: busy ? 0.65 : 1 }}>
101
+ {busy ? "Please wait…" : mode === "signup" ? "Create Admin & continue" : "Sign in"}
102
+ </button>
103
+ </form>
104
+ )}
82
105
 
83
- {msg && <p style={{ marginTop: 16, opacity: 0.85 }}>{msg}</p>}
106
+ {msg && <p role="alert" style={{ marginTop: 16, color: "#ff9b9b" }}>{msg}</p>}
84
107
 
85
- <p style={{ marginTop: 16, fontSize: 13, opacity: 0.7 }}>
86
- {mode === "signin" ? "Invited to join the team? " : "Already have an account? "}
87
- <button
88
- type="button"
89
- onClick={() => setMode(mode === "signin" ? "signup" : "signin")}
90
- style={{ background: "none", border: "none", color: "#7aa2ff", cursor: "pointer", padding: 0 }}
91
- >
92
- {mode === "signin" ? "Create account with your invited email" : "Sign in"}
93
- </button>
94
- </p>
108
+ {!loading && (
109
+ <p style={{ marginTop: 22, fontSize: 13, color: "#7f8da2" }}>
110
+ {mode === "signin" ? "Invited team member? " : "Already created an account? "}
111
+ <button type="button" onClick={() => { setMode(mode === "signin" ? "signup" : "signin"); setMsg(""); }} style={textButton}>
112
+ {mode === "signin" ? "Create invited account" : "Sign in"}
113
+ </button>
114
+ </p>
115
+ )}
116
+ </div>
117
+ </section>
95
118
  </main>
96
119
  );
97
120
  }
98
121
 
99
- const inputStyle: React.CSSProperties = {
100
- padding: "10px 12px",
101
- borderRadius: 8,
102
- border: "1px solid #1f2a3a",
103
- background: "#111824",
104
- color: "#e6edf3",
105
- };
106
- const buttonStyle: React.CSSProperties = {
107
- padding: "10px 12px",
108
- borderRadius: 8,
109
- border: "1px solid #2b5cff",
110
- background: "#2b5cff",
111
- color: "white",
112
- cursor: "pointer",
113
- };
122
+ const shell: React.CSSProperties = { minHeight: "100vh", display: "grid", gridTemplateColumns: "minmax(0, 1.25fr) minmax(380px, .75fr)", background: "radial-gradient(circle at 18% 12%, #17233c 0, #0b0f17 36%, #080b11 100%)" };
123
+ const brandPanel: React.CSSProperties = { padding: "clamp(48px, 8vw, 110px)", display: "flex", flexDirection: "column", justifyContent: "center" };
124
+ const formWrap: React.CSSProperties = { display: "flex", alignItems: "center", justifyContent: "center", padding: 28, background: "rgba(5, 8, 13, .58)", borderLeft: "1px solid #1b2637" };
125
+ const formCard: React.CSSProperties = { width: "min(100%, 430px)", padding: "38px 34px", border: "1px solid #243149", borderRadius: 18, background: "rgba(13, 19, 30, .94)", boxShadow: "0 24px 80px rgba(0,0,0,.35)" };
126
+ const logo: React.CSSProperties = { width: 48, height: 48, borderRadius: 14, display: "grid", placeItems: "center", fontSize: 22, fontWeight: 800, background: "linear-gradient(135deg, #4f7cff, #8f5cff)", boxShadow: "0 10px 32px rgba(79,124,255,.32)" };
127
+ const eyebrow: React.CSSProperties = { margin: "20px 0 0", color: "#7d9dff", fontSize: 12, fontWeight: 800, letterSpacing: ".16em" };
128
+ const featureRow: React.CSSProperties = { display: "flex", flexWrap: "wrap", gap: 20, color: "#8695aa", fontSize: 13, marginTop: 34 };
129
+ const label: React.CSSProperties = { display: "grid", gap: 7, color: "#bac6d8", fontSize: 13, fontWeight: 650 };
130
+ const inputStyle: React.CSSProperties = { padding: "12px 13px", borderRadius: 9, border: "1px solid #2a3951", outline: "none", background: "#0c121d", color: "#eef3fa", fontSize: 15 };
131
+ const buttonStyle: React.CSSProperties = { marginTop: 4, padding: "12px 14px", borderRadius: 9, border: "1px solid #5278ff", background: "linear-gradient(135deg, #416cff, #6d54e8)", color: "white", fontWeight: 750, fontSize: 15, cursor: "pointer" };
132
+ const textButton: React.CSSProperties = { padding: 0, border: 0, background: "none", color: "#85a1ff", cursor: "pointer" };
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ import { requirePageSession } from "@/lib/page-auth";
3
+
4
+ export default async function TeamLayout({ children }: { children: ReactNode }) {
5
+ await requirePageSession();
6
+ return children;
7
+ }
@@ -52,7 +52,8 @@ export class ContextPack {
52
52
 
53
53
  export type RecallOptions = {
54
54
  query: string;
55
- projectId: string;
55
+ /** Omit for the main assistant, which recalls shared/all-project memory. */
56
+ projectId?: string;
56
57
  allowCrossProject?: boolean;
57
58
  k?: number;
58
59
  };
@@ -71,7 +72,7 @@ export async function recall(opts: RecallOptions): Promise<ContextPack> {
71
72
  : [];
72
73
  const order = new Map(hits.map((h, i) => [h.refId, i]));
73
74
  const episodic: RecalledMemory[] = episodicRows
74
- .filter((m) => m.projectId === opts.projectId || opts.allowCrossProject === true)
75
+ .filter((m) => opts.allowCrossProject === true || (!!opts.projectId && m.projectId === opts.projectId))
75
76
  .sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0))
76
77
  .map((m) => ({
77
78
  id: m.id,
@@ -0,0 +1,12 @@
1
+ import { auth, ownerExists } from "@/lib/auth";
2
+ import { headers } from "next/headers";
3
+ import { redirect } from "next/navigation";
4
+
5
+ /** Server-side guard for authenticated application pages. */
6
+ export async function requirePageSession() {
7
+ if (!ownerExists()) redirect("/setup");
8
+
9
+ const session = await auth.api.getSession({ headers: await headers() });
10
+ if (!session) redirect("/setup");
11
+ return session;
12
+ }
@@ -45,11 +45,10 @@ MOP-AGENT ${VERSION}
45
45
 
46
46
  Usage:
47
47
  npx mop-agent Open the installer menu
48
- npx mop-agent install Install nginx and Certbot
49
- npx mop-agent setup Configure domain, HTTPS, app and systemd
48
+ npx mop-agent install Install dependencies and complete setup
50
49
  npx mop-agent status Show service health and file locations
51
50
  npx mop-agent update Apply the npm version selected by npx
52
- npx mop-agent uninstall Remove service and nginx config
51
+ npx mop-agent delete Remove service and nginx config
53
52
 
54
53
  Environment:
55
54
  MOP_AGENT_DIR Durable app directory (default: ${DEFAULT_DIR})
@@ -3,11 +3,10 @@
3
3
  * MOP-AGENT installer / operator (TUI). Self-host with one command.
4
4
  *
5
5
  * npx mop-agent # interactive TUI
6
- * npx mop-agent install # install system deps (nginx/certbot)
7
- * npx mop-agent setup # domain / SQLite / ssl / systemd
6
+ * npx mop-agent install # dependencies + complete setup
8
7
  * npx mop-agent update # migrate + build + restart staged npm version
9
8
  * npx mop-agent status # health
10
- * npx mop-agent uninstall # remove service + nginx vhost (keeps data unless --purge)
9
+ * npx mop-agent delete # remove service + nginx vhost (keeps data unless --purge)
11
10
  *
12
11
  * Run as a normal user. Privileged OS operations request sudo individually.
13
12
  */
@@ -109,16 +108,16 @@ async function cmdInstall() {
109
108
  banner();
110
109
  const os = detectOS();
111
110
  if (!printInstallLocations(os)) return;
112
- await maybeUpdateNpm();
113
111
  console.log(c("bold", "Installing system dependencies (nginx and Certbot)…\n"));
114
112
  runSteps(planInstallDeps(os), { privileged: true });
115
- console.log(c("green", "\n✓ dependencies step complete. Next: mop-agent setup\n"));
113
+ console.log(c("green", "\n✓ dependencies installed. Continuing to application setup…\n"));
114
+ await cmdSetup({ continuation: true });
116
115
  }
117
116
 
118
- async function cmdSetup() {
119
- banner();
117
+ async function cmdSetup({ continuation = false } = {}) {
118
+ if (!continuation) banner();
120
119
  const os = detectOS();
121
- if (!printInstallLocations(os)) return;
120
+ if (!continuation && !printInstallLocations(os)) return;
122
121
  const rl = createInterface({ input, output });
123
122
  const ask = async (q, def) => (await rl.question(c("cyan", ` ${q}${def ? c("gray", ` [${def}]`) : ""}: `))).trim() || def || "";
124
123
 
@@ -303,28 +302,6 @@ function randomToken(n) {
303
302
  return randomBytes(Math.ceil(n / 2)).toString("hex").slice(0, n);
304
303
  }
305
304
 
306
- async function maybeUpdateNpm() {
307
- if (args["skip-npm-update"] || process.env.MOP_AGENT_SKIP_NPM_UPDATE === "1") return;
308
- const current = run("npm --version", { capture: true }).stdout.trim();
309
- const latestResult = run("npm view npm version", { capture: true, allowFailure: true });
310
- const latest = latestResult.stdout.trim();
311
- if (!latest || latest === current) {
312
- console.log(c("green", `✓ npm ${current || "unknown"} is current\n`));
313
- return;
314
- }
315
- const engineResult = run("npm view npm@latest engines.node", { capture: true, allowFailure: true });
316
- const engine = engineResult.stdout.trim();
317
- const rl = createInterface({ input, output });
318
- const answer = (await rl.question(c("cyan", ` Update npm ${current} → ${latest}${engine ? ` (Node ${engine})` : ""}? [Y/n]: `))).trim().toLowerCase();
319
- rl.close();
320
- if (answer && !answer.startsWith("y")) {
321
- console.log(c("gray", " npm update skipped.\n"));
322
- return;
323
- }
324
- run("npm install -g npm@latest", { privileged: true });
325
- console.log(c("green", `✓ npm updated to ${latest}\n`));
326
- }
327
-
328
305
  function ensureRootServiceUser(os) {
329
306
  const user = "mop-agent";
330
307
  const create = os.family === "alpine"
@@ -340,11 +317,10 @@ async function tui() {
340
317
  if (DRY) console.log(c("yellow", " Running in DRY-RUN (no changes).\n"));
341
318
  const rl = createInterface({ input, output });
342
319
  const menu = [
343
- ["1", "install", "Install system deps (nginx and Certbot)"],
344
- ["2", "setup", "Configure domain, SQLite, SSL, systemd service"],
320
+ ["1", "install", "Install (dependencies + complete setup)"],
321
+ ["2", "update", "Update MOP-AGENT + restart"],
345
322
  ["3", "status", "Show service health"],
346
- ["4", "update", "Update to latest + restart"],
347
- ["5", "uninstall", "Remove service + nginx vhost"],
323
+ ["4", "delete", "Delete service + nginx config"],
348
324
  ["q", "quit", "Exit"],
349
325
  ];
350
326
  for (const [k, , desc] of menu) console.log(` ${c("cyan", k)} ${desc}`);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "mop-agent",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "mop-agent",
9
- "version": "0.1.1",
9
+ "version": "0.1.3",
10
10
  "license": "UNLICENSED",
11
11
  "workspaces": [
12
12
  "packages/*",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mop-agent",
3
- "version": "0.1.1",
4
- "description": "Self-hosted AI brain and control plane for MOP-FLOW projects, installed with npx mop-agent.",
3
+ "version": "0.1.3",
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",
7
7
  "keywords": [