mop-agent 0.1.2 → 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.2` contains the corrected VPS
9
- > installer flow. After publishing 0.1.2, 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
@@ -60,6 +60,17 @@ The first run copies the npm-packaged runtime from the temporary npx cache into
60
60
  - `Delete` — removes the service and nginx configuration while preserving data
61
61
  unless purge is explicitly requested.
62
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.
73
+
63
74
  During `setup`, choose one deployment mode:
64
75
 
65
76
  - `public` — enter a public domain and optionally obtain a Let's Encrypt HTTPS
@@ -139,7 +150,7 @@ npm run typecheck
139
150
  npm run dev:web
140
151
  ```
141
152
 
142
- 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
143
154
  automation remain TODO; do not use `sudo` in PowerShell or Command Prompt.
144
155
 
145
156
  ## macOS
@@ -155,7 +166,7 @@ npm run typecheck
155
166
  npm run dev:web
156
167
  ```
157
168
 
158
- 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
159
170
  installer is not implemented; use a supported Linux VPS for production.
160
171
 
161
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
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "mop-agent",
3
- "version": "0.1.2",
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.2",
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.2",
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": [