mop-agent 0.1.9 → 0.1.10

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,7 +5,7 @@ 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` contains the corrected VPS
8
+ > **Release status:** npm package `mop-agent@0.1.10` contains the corrected VPS
9
9
  > installer, one-time Admin setup/login flow, and shared retro application shell.
10
10
  > The canonical installation command is exactly `npx mop-agent`.
11
11
 
@@ -2,16 +2,13 @@
2
2
 
3
3
  import type { CSSProperties } from "react";
4
4
  import { useEffect, useRef, useState } from "react";
5
+ import { useMemoryCore } from "@/components/AppShell";
5
6
 
6
7
  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
8
 
10
9
  export default function AssistantPage() {
10
+ const { selectedProject, setSelectedProject, projects, provider } = useMemoryCore();
11
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
12
  const [name, setName] = useState("Admin");
16
13
  const [input, setInput] = useState("");
17
14
  const [busy, setBusy] = useState(false);
@@ -19,17 +16,12 @@ export default function AssistantPage() {
19
16
  const endRef = useRef<HTMLDivElement>(null);
20
17
 
21
18
  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 });
19
+ fetch("/api/me").then((r) => r.json()).then((me) => {
29
20
  setName(me.user?.name || me.user?.email || "Admin");
30
21
  }).catch(() => {});
31
22
  }, []);
32
23
 
24
+
33
25
  async function send(prefill?: string) {
34
26
  const message = (prefill ?? input).trim();
35
27
  if (!message || busy) return;
@@ -77,22 +69,6 @@ export default function AssistantPage() {
77
69
 
78
70
  return (
79
71
  <section className="mop-assistant-page">
80
- <div className="mop-assistant-toolbar">
81
- <div>
82
- <strong style={{ fontFamily: '"SFMono-Regular", Consolas, monospace', color: "#742220" }}>LIVE ASSISTANT</strong>
83
- <span style={{ color: "rgba(45,74,62,.62)", marginLeft: 10, fontSize: 12 }}>
84
- {provider.configured ? `${provider.provider}${provider.model ? ` · ${provider.model}` : ""}` : "offline demo"}
85
- </span>
86
- </div>
87
- <label style={{ color: "#2d4a3e", fontSize: 12 }}>
88
- MEMORY SCOPE&nbsp;
89
- <select value={selectedProject} onChange={(e) => setSelectedProject(e.target.value)} style={selectStyle}>
90
- <option value="">All memory</option>
91
- {projects.map((project) => <option key={project.id} value={project.id}>{project.name}</option>)}
92
- </select>
93
- </label>
94
- </div>
95
-
96
72
  <div className="mop-assistant-conversation">
97
73
  {turns.length === 0 ? (
98
74
  <div className="mop-assistant-welcome">
@@ -152,7 +128,7 @@ export default function AssistantPage() {
152
128
  }
153
129
 
154
130
  const selectStyle: CSSProperties = { color: "#2d4a3e", border: "1px solid rgba(45,74,62,.42)", padding: "6px 8px", background: "#fffdf2" };
155
- const assistantLogo: CSSProperties = { width: 86, height: 86, display: "grid", placeItems: "center", overflow: "hidden", background: "#2d4a3e", border: "2px solid #742220", boxShadow: "5px 5px 0 rgba(45,74,62,.18)" };
131
+ const assistantLogo: CSSProperties = { width: 86, height: 86, display: "grid", placeItems: "center" };
156
132
  const promptGrid: CSSProperties = { width: "min(100%, 650px)", display: "grid", gridTemplateColumns: "repeat(2,minmax(0,1fr))", gap: 10, marginTop: 28 };
157
133
  const promptCard: CSSProperties = { display: "flex", justifyContent: "space-between", padding: "14px 15px", border: "1px solid rgba(45,74,62,.38)", borderBottomWidth: 3, background: "#fffdf2", color: "#2d4a3e", cursor: "pointer", textAlign: "left" };
158
134
  const botAvatar: CSSProperties = { width: 32, height: 32, display: "grid", placeItems: "center", background: "#742220", color: "#fef9e1" };
@@ -411,11 +411,12 @@ button {
411
411
 
412
412
  .mop-settings-grid {
413
413
  display: grid;
414
- grid-template-columns: 210px minmax(0, 1fr);
414
+ grid-template-columns: 1fr;
415
415
  gap: 18px;
416
416
  align-items: start;
417
417
  }
418
418
 
419
+
419
420
  .mop-settings-nav { padding: 9px; }
420
421
  .mop-settings-nav button {
421
422
  width: 100%;
@@ -504,4 +505,65 @@ button {
504
505
  .mop-settings-nav { display: flex; gap: 7px; }
505
506
  .mop-settings-nav button { justify-content: center; }
506
507
  .mop-user-invite-form { grid-template-columns: 1fr !important; }
508
+ .mop-settings-sidebar {
509
+ position: fixed;
510
+ top: 62px;
511
+ left: 0;
512
+ bottom: 0;
513
+ width: min(82vw, 280px);
514
+ height: auto;
515
+ transform: translateX(-105%);
516
+ transition: transform 160ms steps(4, end);
517
+ display: flex !important;
518
+ flex-direction: column !important;
519
+ }
520
+ .mop-settings-sidebar.is-open {
521
+ transform: translateX(0);
522
+ }
523
+ }
524
+
525
+ .mop-settings-sidebar {
526
+ grid-area: sidebar;
527
+ position: sticky;
528
+ top: 70px;
529
+ height: calc(100vh - 70px);
530
+ z-index: 40;
531
+ display: flex;
532
+ flex-direction: column;
533
+ overflow-y: auto;
534
+ padding: 15px 9px 12px;
535
+ border-right: 2px solid rgba(45, 74, 62, .46);
536
+ }
537
+
538
+ .mop-back-workspace-btn {
539
+ display: flex;
540
+ align-items: center;
541
+ justify-content: center;
542
+ gap: 8px;
543
+ width: 100%;
544
+ min-height: 40px;
545
+ margin-bottom: 9px;
546
+ padding: 9px 12px;
547
+ border: 1px solid var(--mop-red);
548
+ background: var(--mop-red);
549
+ color: var(--mop-cream);
550
+ font-family: "SFMono-Regular", Consolas, monospace;
551
+ font-size: 11px;
552
+ font-weight: 900;
553
+ text-decoration: none;
554
+ cursor: pointer;
555
+ transition: transform 80ms steps(2, end), box-shadow 80ms steps(2, end);
556
+ box-shadow: 2px 2px 0 rgba(45, 74, 62, .17);
557
+ }
558
+
559
+ .mop-back-workspace-btn:hover {
560
+ transform: translate(-1px, -1px);
561
+ box-shadow: 3px 3px 0 rgba(45, 74, 62, .24);
562
+ color: var(--mop-cream);
563
+ }
564
+
565
+ .mop-back-workspace-btn:active {
566
+ transform: translate(1px, 1px);
567
+ box-shadow: 0 0 0 rgba(45, 74, 62, 0);
507
568
  }
569
+
@@ -2,13 +2,13 @@
2
2
 
3
3
  import type { CSSProperties } from "react";
4
4
  import { useEffect, useState } from "react";
5
+ import { useMemoryCore } from "@/components/AppShell";
5
6
 
6
- type Section = "providers" | "users";
7
7
  type Masked = { configured: boolean; provider?: string; model?: string | null; keyHint?: string };
8
8
  type Member = { id: string; email: string; name: string; role: string };
9
9
 
10
10
  export default function SettingsPage() {
11
- const [section, setSection] = useState<Section>("providers");
11
+ const { settingsSection: section } = useMemoryCore();
12
12
  const [config, setConfig] = useState<Masked>({ configured: false });
13
13
  const [env, setEnv] = useState<{ anthropic: boolean; openrouter: boolean }>({ anthropic: false, openrouter: false });
14
14
  const [provider, setProvider] = useState<"anthropic" | "openrouter">("openrouter");
@@ -34,17 +34,10 @@ export default function SettingsPage() {
34
34
  }
35
35
 
36
36
  useEffect(() => {
37
- const requested = new URLSearchParams(window.location.search).get("section");
38
- if (requested === "users") setSection("users");
39
37
  loadProvider();
40
38
  loadUsers();
41
39
  }, []);
42
40
 
43
- function chooseSection(next: Section) {
44
- setSection(next);
45
- const url = next === "providers" ? "/settings" : "/settings?section=users";
46
- window.history.replaceState(null, "", url);
47
- }
48
41
 
49
42
  async function saveProvider(e: React.FormEvent) {
50
43
  e.preventDefault();
@@ -90,15 +83,6 @@ export default function SettingsPage() {
90
83
  </header>
91
84
 
92
85
  <div className="mop-settings-grid">
93
- <aside className="mop-settings-nav mop-panel" aria-label="Settings sections">
94
- <button className={section === "providers" ? "is-active" : ""} onClick={() => chooseSection("providers")}>
95
- <span>◇</span><strong>Providers</strong>
96
- </button>
97
- <button className={section === "users" ? "is-active" : ""} onClick={() => chooseSection("users")}>
98
- <span>♙</span><strong>Users</strong>
99
- </button>
100
- </aside>
101
-
102
86
  <section className="mop-settings-content mop-panel">
103
87
  {section === "providers" ? (
104
88
  <>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { ReactNode } from "react";
4
4
  import { usePathname } from "next/navigation";
5
- import { useState } from "react";
5
+ import { useState, useEffect, createContext, useContext } from "react";
6
6
  import { signOut } from "@/lib/auth-client";
7
7
 
8
8
  export type AppViewer = {
@@ -11,6 +11,31 @@ export type AppViewer = {
11
11
  role: "owner" | "member";
12
12
  };
13
13
 
14
+ export type Project = { id: string; name: string; status: string };
15
+ export type ProviderState = { configured: boolean; provider?: string; model?: string | null };
16
+
17
+ interface MemoryCoreContextType {
18
+ selectedProject: string;
19
+ setSelectedProject: (id: string) => void;
20
+ projects: Project[];
21
+ provider: ProviderState;
22
+ settingsSection: "providers" | "users";
23
+ setSettingsSection: (section: "providers" | "users") => void;
24
+ }
25
+
26
+ const MemoryCoreContext = createContext<MemoryCoreContextType | undefined>(undefined);
27
+
28
+ export function useMemoryCore() {
29
+ const context = useContext(MemoryCoreContext);
30
+ if (!context) {
31
+ throw new Error("useMemoryCore must be used within a MemoryCoreProvider");
32
+ }
33
+ return context;
34
+ }
35
+
36
+ const selectStyle = { color: "#2d4a3e", border: "1px solid rgba(45,74,62,.42)", padding: "6px 8px", background: "#fffdf2" };
37
+
38
+
14
39
  function pageTitle(pathname: string): string {
15
40
  if (pathname === "/assistant") return "Assistant";
16
41
  if (pathname === "/brain/graph") return "Knowledge Graph";
@@ -27,84 +52,156 @@ export function AppShell({ viewer, children }: { viewer: AppViewer; children: Re
27
52
  const isAdmin = viewer.role === "owner";
28
53
  const title = pageTitle(pathname);
29
54
 
55
+ const [projects, setProjects] = useState<Project[]>([]);
56
+ const [provider, setProvider] = useState<ProviderState>({ configured: false });
57
+ const [selectedProject, setSelectedProject] = useState("");
58
+ const [settingsSection, setSettingsSection] = useState<"providers" | "users">("providers");
59
+
60
+ useEffect(() => {
61
+ Promise.all([
62
+ fetch("/api/projects").then((r) => r.json()),
63
+ fetch("/api/providers").then((r) => r.json()),
64
+ ]).then(([projectData, providerData]) => {
65
+ setProjects(projectData.projects ?? []);
66
+ setProvider(providerData.config ?? { configured: false });
67
+ }).catch(() => {});
68
+
69
+ const requested = new URLSearchParams(window.location.search).get("section");
70
+ if (requested === "users") setSettingsSection("users");
71
+ }, []);
72
+
30
73
  async function logout() {
31
74
  await signOut();
32
75
  window.location.replace("/login");
33
76
  }
34
77
 
78
+ const selectSection = (sec: "providers" | "users") => {
79
+ setSettingsSection(sec);
80
+ const url = sec === "providers" ? "/settings" : "/settings?section=users";
81
+ window.history.replaceState(null, "", url);
82
+ };
83
+
84
+ const isSettings = pathname.startsWith("/settings");
85
+
35
86
  const nav = [
36
87
  { href: "/assistant", label: "Assistant", icon: "✦", active: pathname.startsWith("/assistant") || pathname.startsWith("/chat/") },
37
88
  { href: "/brain", label: "Brain", icon: "◉", active: pathname.startsWith("/brain") },
38
89
  ];
39
90
 
40
91
  return (
41
- <div className="mop-app-frame">
42
- <header className="mop-app-topbar">
43
- <a className="mop-app-brand" href="/assistant" aria-label="MOP-AGENT home">
44
- <img src="/icon.svg" alt="" />
45
- <span>MOP-AGENT</span>
46
- </a>
47
- <div className="mop-app-topbar-main">
48
- <button
49
- className="mop-menu-toggle"
50
- type="button"
51
- aria-label="Toggle navigation"
52
- aria-expanded={menuOpen}
53
- onClick={() => setMenuOpen((open) => !open)}
54
- >
55
-
56
- </button>
57
- <div className="mop-topbar-title">
58
- <span className="mop-live-dot" />
59
- <strong>{title}</strong>
60
- </div>
61
- <div className="mop-topbar-center">MOP MEMORYCORE</div>
62
- <div className="mop-topbar-meta">
63
- <span>{isAdmin ? "ADMIN" : "MEMBER"}</span>
64
- <span className="mop-version">v0.1.9</span>
65
- </div>
66
- </div>
67
- </header>
68
-
69
- {menuOpen && <button className="mop-sidebar-scrim" aria-label="Close navigation" onClick={() => setMenuOpen(false)} />}
70
-
71
- <aside className={`mop-app-sidebar${menuOpen ? " is-open" : ""}`}>
72
- <div className="mop-nav-section">
73
- <p>WORKSPACE</p>
74
- <nav>
75
- {nav.map((item) => (
76
- <a key={item.href} href={item.href} className={item.active ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
77
- <span className="mop-nav-icon">{item.icon}</span>
78
- <span>{item.label}</span>
79
- </a>
80
- ))}
81
- </nav>
82
- </div>
83
-
84
- {isAdmin && (
85
- <div className="mop-nav-section">
86
- <p>ADMIN</p>
87
- <nav>
88
- <a href="/settings" className={pathname.startsWith("/settings") ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
89
- <span className="mop-nav-icon">⚙</span>
90
- <span>Settings</span>
91
- </a>
92
- </nav>
92
+ <MemoryCoreContext.Provider value={{ selectedProject, setSelectedProject, projects, provider, settingsSection, setSettingsSection }}>
93
+ <div className="mop-app-frame">
94
+ <header className="mop-app-topbar">
95
+ <a className="mop-app-brand" href="/assistant" aria-label="MOP-AGENT home">
96
+ <img src="/icon.svg" alt="" />
97
+ <span>MOP-AGENT</span>
98
+ </a>
99
+ <div className="mop-app-topbar-main">
100
+ <button
101
+ className="mop-menu-toggle"
102
+ type="button"
103
+ aria-label="Toggle navigation"
104
+ aria-expanded={menuOpen}
105
+ onClick={() => setMenuOpen((open) => !open)}
106
+ >
107
+
108
+ </button>
109
+ {pathname === "/assistant" ? (
110
+ <div className="mop-assistant-toolbar" style={{ border: 0, padding: 0, margin: 0, background: "transparent", width: "100%", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
111
+ <div>
112
+ <strong style={{ fontFamily: '"SFMono-Regular", Consolas, monospace', color: "#742220" }}>LIVE ASSISTANT</strong>
113
+ <span style={{ color: "rgba(45,74,62,.62)", marginLeft: 10, fontSize: 12 }}>
114
+ {provider.configured ? `${provider.provider}${provider.model ? ` · ${provider.model}` : ""}` : "offline demo"}
115
+ </span>
116
+ </div>
117
+ <label style={{ color: "#2d4a3e", fontSize: 12 }}>
118
+ MEMORY SCOPE&nbsp;
119
+ <select value={selectedProject} onChange={(e) => setSelectedProject(e.target.value)} style={selectStyle}>
120
+ <option value="">All memory</option>
121
+ {projects.map((project) => <option key={project.id} value={project.id}>{project.name}</option>)}
122
+ </select>
123
+ </label>
124
+ </div>
125
+ ) : (
126
+ <>
127
+ <div className="mop-topbar-title">
128
+ <span className="mop-live-dot" />
129
+ <strong>{title}</strong>
130
+ </div>
131
+ <div className="mop-topbar-center">MOP MEMORYCORE</div>
132
+ <div className="mop-topbar-meta">
133
+ <span>{isAdmin ? "ADMIN" : "MEMBER"}</span>
134
+ <span className="mop-version">v0.1.10</span>
135
+ </div>
136
+ </>
137
+ )}
93
138
  </div>
94
- )}
95
-
96
- <div className="mop-sidebar-spacer" />
97
- <button className="mop-account-card" type="button" onClick={logout} title="Sign out">
98
- <span className="mop-account-avatar">{viewer.name.slice(0, 1).toUpperCase()}</span>
99
- <span className="mop-account-copy">
100
- <strong>{viewer.name}</strong>
101
- <small>{isAdmin ? "Administrator" : "Member"}</small>
102
- </span>
103
- <span aria-hidden="true">↪</span>
104
- </button>
105
- </aside>
106
-
107
- <main className="mop-app-main">{children}</main>
108
- </div>
139
+ </header>
140
+
141
+ {menuOpen && <button className="mop-sidebar-scrim" aria-label="Close navigation" onClick={() => setMenuOpen(false)} />}
142
+
143
+ <aside className={isSettings
144
+ ? `mop-settings-nav mop-panel mop-settings-sidebar${menuOpen ? " is-open" : ""}`
145
+ : `mop-app-sidebar${menuOpen ? " is-open" : ""}`}
146
+ >
147
+ {isSettings ? (
148
+ <>
149
+ <button className={settingsSection === "providers" ? "is-active" : ""} onClick={() => { selectSection("providers"); setMenuOpen(false); }}>
150
+ <span>◇</span><strong>Providers</strong>
151
+ </button>
152
+ <button className={settingsSection === "users" ? "is-active" : ""} onClick={() => { selectSection("users"); setMenuOpen(false); }}>
153
+ <span>♙</span><strong>Users</strong>
154
+ </button>
155
+ </>
156
+ ) : (
157
+ <>
158
+ <div className="mop-nav-section">
159
+ <p>WORKSPACE</p>
160
+ <nav>
161
+ {nav.map((item) => (
162
+ <a key={item.href} href={item.href} className={item.active ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
163
+ <span className="mop-nav-icon">{item.icon}</span>
164
+ <span>{item.label}</span>
165
+ </a>
166
+ ))}
167
+ </nav>
168
+ </div>
169
+
170
+ {isAdmin && (
171
+ <div className="mop-nav-section">
172
+ <p>ADMIN</p>
173
+ <nav>
174
+ <a href="/settings" className={pathname.startsWith("/settings") ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
175
+ <span className="mop-nav-icon">⚙</span>
176
+ <span>Settings</span>
177
+ </a>
178
+ </nav>
179
+ </div>
180
+ )}
181
+ </>
182
+ )}
183
+
184
+ <div className="mop-sidebar-spacer" />
185
+
186
+ {isSettings && (
187
+ <a href="/assistant" className="mop-back-workspace-btn">
188
+ <span>← BACK TO WORKSPACE</span>
189
+ </a>
190
+ )}
191
+
192
+ <button className="mop-account-card" type="button" onClick={logout} title="Sign out">
193
+ <span className="mop-account-avatar">{viewer.name.slice(0, 1).toUpperCase()}</span>
194
+ <span className="mop-account-copy">
195
+ <strong>{viewer.name}</strong>
196
+ <small>{isAdmin ? "Administrator" : "Member"}</small>
197
+ </span>
198
+ <span aria-hidden="true">↪</span>
199
+ </button>
200
+ </aside>
201
+
202
+ <main className="mop-app-main">{children}</main>
203
+ </div>
204
+ </MemoryCoreContext.Provider>
109
205
  );
110
206
  }
207
+
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "mop-agent",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "mop-agent",
9
- "version": "0.1.9",
9
+ "version": "0.1.10",
10
10
  "license": "UNLICENSED",
11
11
  "workspaces": [
12
12
  "packages/*",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mop-agent",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Self-hosted AI assistant with persistent cross-project memory, installed with npx mop-agent.",
5
5
  "author": "BURHANDEV ENTERPRISE",
6
6
  "license": "UNLICENSED",