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 +25 -12
- package/apps/web/app/api/chat/route.ts +17 -8
- package/apps/web/app/assistant/layout.tsx +7 -0
- package/apps/web/app/assistant/page.tsx +196 -0
- package/apps/web/app/brain/layout.tsx +7 -0
- package/apps/web/app/brain/page.tsx +1 -0
- package/apps/web/app/chat/layout.tsx +7 -0
- package/apps/web/app/layout.tsx +1 -1
- package/apps/web/app/page.tsx +7 -68
- package/apps/web/app/settings/layout.tsx +7 -0
- package/apps/web/app/settings/page.tsx +1 -1
- package/apps/web/app/setup/page.tsx +104 -85
- package/apps/web/app/team/layout.tsx +7 -0
- package/apps/web/lib/brain/broker.ts +3 -2
- package/apps/web/lib/page-auth.ts +12 -0
- package/installer/bootstrap.mjs +2 -3
- package/installer/mop-agent.mjs +10 -34
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
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.
|
|
9
|
-
> installer
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
15
|
+
projectId?: string;
|
|
15
16
|
message: string;
|
|
16
17
|
allowCrossProject?: boolean;
|
|
17
18
|
};
|
|
18
|
-
if (
|
|
19
|
-
return Response.json({ error: "
|
|
19
|
+
if (typeof message !== "string" || !message.trim()) {
|
|
20
|
+
return Response.json({ error: "missing_message" }, { status: 400 });
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
const
|
|
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
|
|
27
|
-
|
|
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,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
|
|
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" };
|
|
@@ -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>{" "}
|
package/apps/web/app/layout.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
|
|
3
3
|
export const metadata = {
|
|
4
4
|
title: "MOP-AGENT",
|
|
5
|
-
description: "Self-
|
|
5
|
+
description: "Self-hosted AI assistant with persistent, cross-project memory.",
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
package/apps/web/app/page.tsx
CHANGED
|
@@ -1,72 +1,11 @@
|
|
|
1
|
+
import { requirePageSession } from "@/lib/page-auth";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
-
|
|
18
|
-
|
|
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
|
}
|
|
@@ -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="/
|
|
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 {
|
|
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
|
|
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
|
-
|
|
19
|
-
.then((r) => r.json())
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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(() =>
|
|
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
|
-
|
|
33
|
+
setBusy(true);
|
|
34
|
+
setMsg("");
|
|
35
|
+
|
|
30
36
|
if (mode === "signup") {
|
|
31
|
-
const res = await signUp.email({ email, password, name: name || email });
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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={
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
106
|
+
{msg && <p role="alert" style={{ marginTop: 16, color: "#ff9b9b" }}>{msg}</p>}
|
|
84
107
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
</
|
|
94
|
-
</
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
};
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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" };
|
|
@@ -52,7 +52,8 @@ export class ContextPack {
|
|
|
52
52
|
|
|
53
53
|
export type RecallOptions = {
|
|
54
54
|
query: string;
|
|
55
|
-
|
|
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) =>
|
|
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
|
+
}
|
package/installer/bootstrap.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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})
|
package/installer/mop-agent.mjs
CHANGED
|
@@ -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 #
|
|
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
|
|
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
|
|
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
|
|
344
|
-
["2", "
|
|
320
|
+
["1", "install", "Install (dependencies + complete setup)"],
|
|
321
|
+
["2", "update", "Update MOP-AGENT + restart"],
|
|
345
322
|
["3", "status", "Show service health"],
|
|
346
|
-
["4", "
|
|
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}`);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mop-agent",
|
|
3
|
-
"version": "0.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.
|
|
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.
|
|
4
|
-
"description": "Self-hosted AI
|
|
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": [
|