mop-agent 0.1.0

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.
Files changed (86) hide show
  1. package/README.md +177 -0
  2. package/apps/web/.env.example +18 -0
  3. package/apps/web/app/api/actions/[id]/approve/route.ts +15 -0
  4. package/apps/web/app/api/actions/[id]/deny/route.ts +15 -0
  5. package/apps/web/app/api/actions/route.ts +29 -0
  6. package/apps/web/app/api/auth/[...all]/route.ts +4 -0
  7. package/apps/web/app/api/chat/route.ts +50 -0
  8. package/apps/web/app/api/consolidate/route.ts +10 -0
  9. package/apps/web/app/api/graph/route.ts +34 -0
  10. package/apps/web/app/api/invites/route.ts +38 -0
  11. package/apps/web/app/api/link/code/route.ts +13 -0
  12. package/apps/web/app/api/link/pair/route.ts +41 -0
  13. package/apps/web/app/api/me/route.ts +11 -0
  14. package/apps/web/app/api/members/route.ts +16 -0
  15. package/apps/web/app/api/projects/[id]/memory/route.ts +12 -0
  16. package/apps/web/app/api/projects/[id]/state/route.ts +19 -0
  17. package/apps/web/app/api/projects/route.ts +21 -0
  18. package/apps/web/app/api/providers/route.ts +32 -0
  19. package/apps/web/app/api/semantic/route.ts +9 -0
  20. package/apps/web/app/api/setup/status/route.ts +6 -0
  21. package/apps/web/app/api/skills/route.ts +23 -0
  22. package/apps/web/app/brain/[projectId]/page.tsx +50 -0
  23. package/apps/web/app/brain/graph/page.tsx +54 -0
  24. package/apps/web/app/brain/page.tsx +167 -0
  25. package/apps/web/app/chat/[projectId]/page.tsx +113 -0
  26. package/apps/web/app/layout.tsx +24 -0
  27. package/apps/web/app/page.tsx +72 -0
  28. package/apps/web/app/settings/page.tsx +63 -0
  29. package/apps/web/app/setup/page.tsx +113 -0
  30. package/apps/web/app/team/page.tsx +86 -0
  31. package/apps/web/bin/mop-agent.mjs +85 -0
  32. package/apps/web/lib/auth-client.ts +5 -0
  33. package/apps/web/lib/auth.ts +86 -0
  34. package/apps/web/lib/authz.ts +23 -0
  35. package/apps/web/lib/brain/answer.ts +27 -0
  36. package/apps/web/lib/brain/approvals.ts +81 -0
  37. package/apps/web/lib/brain/broker.ts +98 -0
  38. package/apps/web/lib/brain/consolidate.ts +133 -0
  39. package/apps/web/lib/brain/mirror.ts +80 -0
  40. package/apps/web/lib/brain/scheduler.ts +30 -0
  41. package/apps/web/lib/brain/skills.ts +34 -0
  42. package/apps/web/lib/channels/binding.ts +26 -0
  43. package/apps/web/lib/channels/discord.ts +28 -0
  44. package/apps/web/lib/channels/handler.ts +44 -0
  45. package/apps/web/lib/channels/index.ts +18 -0
  46. package/apps/web/lib/channels/telegram.ts +18 -0
  47. package/apps/web/lib/crypto.ts +35 -0
  48. package/apps/web/lib/db/client.ts +34 -0
  49. package/apps/web/lib/db/migrate.ts +116 -0
  50. package/apps/web/lib/db/paths.ts +25 -0
  51. package/apps/web/lib/db/schema.ts +105 -0
  52. package/apps/web/lib/link/store.ts +89 -0
  53. package/apps/web/lib/memory/embed.ts +111 -0
  54. package/apps/web/lib/memory/local-embedder.ts +26 -0
  55. package/apps/web/lib/providers/anthropic.ts +23 -0
  56. package/apps/web/lib/providers/config.ts +55 -0
  57. package/apps/web/lib/providers/echo.ts +26 -0
  58. package/apps/web/lib/providers/index.ts +41 -0
  59. package/apps/web/lib/providers/openrouter.ts +24 -0
  60. package/apps/web/lib/providers/types.ts +14 -0
  61. package/apps/web/lib/ws/gateway.ts +113 -0
  62. package/apps/web/next-env.d.ts +6 -0
  63. package/apps/web/next.config.mjs +9 -0
  64. package/apps/web/package.json +44 -0
  65. package/apps/web/scripts/migrate.ts +12 -0
  66. package/apps/web/server.ts +27 -0
  67. package/apps/web/tsconfig.json +31 -0
  68. package/installer/bootstrap.mjs +161 -0
  69. package/installer/lib.mjs +196 -0
  70. package/installer/mop-agent.mjs +322 -0
  71. package/npm-shrinkwrap.json +5032 -0
  72. package/package.json +71 -0
  73. package/packages/flow-connector/bin/cli.mjs +67 -0
  74. package/packages/flow-connector/package.json +26 -0
  75. package/packages/flow-connector/src/exec.ts +81 -0
  76. package/packages/flow-connector/src/index.ts +17 -0
  77. package/packages/flow-connector/src/linkfile.ts +46 -0
  78. package/packages/flow-connector/src/pair.ts +66 -0
  79. package/packages/flow-connector/src/serve.ts +103 -0
  80. package/packages/flow-connector/src/snapshot.ts +94 -0
  81. package/packages/flow-connector/src/tools.ts +198 -0
  82. package/packages/flow-connector/tsconfig.json +10 -0
  83. package/packages/link-protocol/package.json +17 -0
  84. package/packages/link-protocol/src/index.ts +245 -0
  85. package/packages/link-protocol/tsconfig.json +10 -0
  86. package/tsconfig.base.json +18 -0
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ /** Project Brain — overview (state) + recent memory + artifacts, read from mirror. */
3
+ import { use, useEffect, useState } from "react";
4
+
5
+ type Memory = { id: string; kind: string; summary: string; body: string | null; at: number; actor: string | null };
6
+ type StateRes = { state: unknown; artifacts: Array<{ path: string }>; memoryCount: number; updatedAt: number };
7
+
8
+ export default function ProjectBrainPage({ params }: { params: Promise<{ projectId: string }> }) {
9
+ const { projectId } = use(params);
10
+ const [mem, setMem] = useState<Memory[]>([]);
11
+ const [info, setInfo] = useState<StateRes | null>(null);
12
+
13
+ useEffect(() => {
14
+ fetch(`/api/projects/${projectId}/memory`).then((r) => r.json()).then((d) => setMem(d.memory ?? [])).catch(() => {});
15
+ fetch(`/api/projects/${projectId}/state`).then((r) => r.json()).then((d) => setInfo(d)).catch(() => {});
16
+ }, [projectId]);
17
+
18
+ return (
19
+ <main style={{ maxWidth: 900, margin: "0 auto", padding: "40px 24px" }}>
20
+ <a href="/brain" style={{ color: "#7aa2ff" }}>← Brain</a>
21
+ <h1 style={{ fontSize: 24 }}>{projectId}</h1>
22
+ <a href={`/chat/${projectId}`} style={{ color: "#7aa2ff" }}>💬 Chat with this project →</a>
23
+
24
+ <h2 style={{ fontSize: 16, marginTop: 24, opacity: 0.8 }}>
25
+ Recent memory ({info?.memoryCount ?? mem.length})
26
+ </h2>
27
+ <ul style={{ listStyle: "none", padding: 0 }}>
28
+ {mem.map((m) => (
29
+ <li key={m.id} style={card}>
30
+ <span style={{ opacity: 0.6 }}>[{m.kind}]</span> {m.summary}
31
+ {m.body && <div style={{ opacity: 0.6, fontSize: 13, marginTop: 4 }}>{m.body}</div>}
32
+ </li>
33
+ ))}
34
+ {mem.length === 0 && <p style={{ opacity: 0.5 }}>No memory yet (project may be offline / not synced).</p>}
35
+ </ul>
36
+
37
+ <h2 style={{ fontSize: 16, marginTop: 24, opacity: 0.8 }}>Artifacts ({info?.artifacts?.length ?? 0})</h2>
38
+ <ul style={{ opacity: 0.7 }}>
39
+ {(info?.artifacts ?? []).map((a) => <li key={a.path}>{a.path}</li>)}
40
+ </ul>
41
+ </main>
42
+ );
43
+ }
44
+
45
+ const card: React.CSSProperties = {
46
+ border: "1px solid #1f2a3a",
47
+ borderRadius: 8,
48
+ padding: "10px 14px",
49
+ marginBottom: 8,
50
+ };
@@ -0,0 +1,54 @@
1
+ "use client";
2
+ /** Brain knowledge graph (Fasa 5) — projects ⟷ patterns ⟷ skills, via React Flow. */
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { ReactFlow, Background, Controls, type Edge, type Node } from "reactflow";
5
+ import "reactflow/dist/style.css";
6
+
7
+ type GNode = { id: string; label: string; type: "project" | "pattern" | "skill" };
8
+ type GEdge = { from: string; to: string };
9
+
10
+ const COLOR = { project: "#2b5cff", pattern: "#1a7f4b", skill: "#a15c00" };
11
+
12
+ export default function GraphPage() {
13
+ const [data, setData] = useState<{ nodes: GNode[]; edges: GEdge[] }>({ nodes: [], edges: [] });
14
+
15
+ useEffect(() => {
16
+ fetch("/api/graph").then((r) => r.json()).then((d) => setData({ nodes: d.nodes ?? [], edges: d.edges ?? [] })).catch(() => {});
17
+ }, []);
18
+
19
+ const nodes: Node[] = useMemo(() => {
20
+ const cols: Record<GNode["type"], number> = { project: 0, pattern: 1, skill: 2 };
21
+ const counts: Record<string, number> = {};
22
+ return data.nodes.map((n) => {
23
+ const col = cols[n.type];
24
+ const row = (counts[n.type] = (counts[n.type] ?? 0) + 1);
25
+ return {
26
+ id: n.id,
27
+ position: { x: col * 280, y: row * 90 },
28
+ data: { label: `${n.type === "project" ? "📦" : n.type === "pattern" ? "🌐" : "🛠"} ${n.label}` },
29
+ style: { border: `1px solid ${COLOR[n.type]}`, borderRadius: 8, background: "#111824", color: "#e6edf3", fontSize: 12, width: 240 },
30
+ };
31
+ });
32
+ }, [data.nodes]);
33
+
34
+ const edges: Edge[] = useMemo(
35
+ () => data.edges.map((e, i) => ({ id: `e${i}`, source: e.from, target: e.to, style: { stroke: "#2a3a4f" } })),
36
+ [data.edges],
37
+ );
38
+
39
+ return (
40
+ <main style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
41
+ <div style={{ padding: "12px 16px" }}>
42
+ <a href="/brain" style={{ color: "#7aa2ff" }}>← Brain</a>{" "}
43
+ <strong>Knowledge Graph</strong>{" "}
44
+ <span style={{ opacity: 0.6, fontSize: 13 }}>{data.nodes.length} nodes · {data.edges.length} edges</span>
45
+ </div>
46
+ <div style={{ flex: 1 }}>
47
+ <ReactFlow nodes={nodes} edges={edges} fitView>
48
+ <Background color="#1f2a3a" />
49
+ <Controls />
50
+ </ReactFlow>
51
+ </div>
52
+ </main>
53
+ );
54
+ }
@@ -0,0 +1,167 @@
1
+ "use client";
2
+ /** Brain dashboard — project list with status + links into each Project Brain. */
3
+ import { useEffect, useState } from "react";
4
+
5
+ type Project = {
6
+ id: string;
7
+ name: string;
8
+ status: string;
9
+ memoryCount: number;
10
+ artifactCount: number;
11
+ mopFlowVersion?: string;
12
+ };
13
+
14
+ type SemanticNote = { id: string; title: string; body: string; sourceProjects: string[]; confidence: number };
15
+ type Action = { id: string; projectId: string; tool: string; summary: string; status: string; error?: string };
16
+
17
+ export default function BrainPage() {
18
+ const [projects, setProjects] = useState<Project[]>([]);
19
+ const [code, setCode] = useState<string>("");
20
+ const [notes, setNotes] = useState<SemanticNote[]>([]);
21
+ const [actions, setActions] = useState<Action[]>([]);
22
+ const [consolidating, setConsolidating] = useState(false);
23
+ const [consolidateMsg, setConsolidateMsg] = useState("");
24
+
25
+ function loadNotes() {
26
+ fetch("/api/semantic").then((r) => r.json()).then((d) => setNotes(d.notes ?? [])).catch(() => {});
27
+ }
28
+ function loadActions() {
29
+ fetch("/api/actions").then((r) => r.json()).then((d) => setActions(d.actions ?? [])).catch(() => {});
30
+ }
31
+
32
+ useEffect(() => {
33
+ const load = () => {
34
+ fetch("/api/projects")
35
+ .then((r) => r.json())
36
+ .then((d: { projects: Project[] }) => setProjects(d.projects))
37
+ .catch(() => {});
38
+ loadActions();
39
+ };
40
+ load();
41
+ loadNotes();
42
+ const t = setInterval(load, 4000);
43
+ return () => clearInterval(t);
44
+ }, []);
45
+
46
+ async function decide(id: string, what: "approve" | "deny") {
47
+ await fetch(`/api/actions/${id}/${what}`, { method: "POST" });
48
+ loadActions();
49
+ }
50
+
51
+ async function genCode() {
52
+ const r = await fetch("/api/link/code", { method: "POST" });
53
+ const d = await r.json();
54
+ setCode(r.ok ? d.code : `error: ${d.error}`);
55
+ }
56
+
57
+ async function runConsolidate() {
58
+ setConsolidating(true);
59
+ setConsolidateMsg("…");
60
+ const r = await fetch("/api/consolidate", { method: "POST" });
61
+ const d = await r.json();
62
+ setConsolidateMsg(
63
+ r.ok ? `scanned ${d.scanned} memories → ${d.notesCreated} pattern(s) promoted` : `error: ${d.error}`,
64
+ );
65
+ setConsolidating(false);
66
+ loadNotes();
67
+ }
68
+
69
+ return (
70
+ <main style={{ maxWidth: 900, margin: "0 auto", padding: "40px 24px" }}>
71
+ <h1 style={{ fontSize: 24 }}>🧠 Brain</h1>
72
+ <p style={{ opacity: 0.65 }}>
73
+ Main Brain + linked project brains. <a href="/brain/graph" style={{ color: "#7aa2ff" }}>🕸 Knowledge graph →</a>{" "}
74
+ <a href="/settings" style={{ color: "#7aa2ff" }}>⚙️ Settings →</a>{" "}
75
+ <a href="/team" style={{ color: "#7aa2ff" }}>👥 Team →</a>
76
+ </p>
77
+
78
+ <div style={{ margin: "20px 0", display: "flex", gap: 12, alignItems: "center" }}>
79
+ <button onClick={genCode} style={btn}>+ Link project</button>
80
+ {code && (
81
+ <code style={{ background: "#111824", padding: "6px 10px", borderRadius: 6 }}>
82
+ mop-flow-dev link --url {typeof window !== "undefined" ? window.location.origin : ""} --code {code} --project &lt;id&gt;
83
+ </code>
84
+ )}
85
+ </div>
86
+
87
+ <section style={{ margin: "8px 0 24px", border: "1px solid #1f2a3a", borderRadius: 8, padding: 16 }}>
88
+ <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
89
+ <h2 style={{ fontSize: 16, margin: 0, opacity: 0.85 }}>🌐 Main Brain ({notes.length})</h2>
90
+ <button onClick={runConsolidate} disabled={consolidating} style={{ ...btn, background: "#1a7f4b", borderColor: "#1a7f4b" }}>
91
+ {consolidating ? "consolidating…" : "⟳ Consolidate"}
92
+ </button>
93
+ {consolidateMsg && <span style={{ opacity: 0.65, fontSize: 13 }}>{consolidateMsg}</span>}
94
+ </div>
95
+ <p style={{ opacity: 0.55, fontSize: 13, marginBottom: 8 }}>
96
+ Recurring patterns promoted from project memory (episodic → semantic).
97
+ </p>
98
+ {notes.length === 0 ? (
99
+ <p style={{ opacity: 0.5, fontSize: 13 }}>No patterns yet — link projects, then Consolidate.</p>
100
+ ) : (
101
+ <ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
102
+ {notes.map((n) => (
103
+ <li key={n.id} style={{ padding: "8px 0", borderTop: "1px solid #14202e" }}>
104
+ <strong>{n.title}</strong>{" "}
105
+ <span style={{ opacity: 0.5, fontSize: 12 }}>
106
+ · {n.confidence}% · {n.sourceProjects.length} project(s)
107
+ </span>
108
+ <div style={{ opacity: 0.65, fontSize: 13, marginTop: 2 }}>{n.body}</div>
109
+ </li>
110
+ ))}
111
+ </ul>
112
+ )}
113
+ </section>
114
+
115
+ {actions.length > 0 && (
116
+ <section style={{ margin: "8px 0 24px", border: "1px solid #3a2f1f", borderRadius: 8, padding: 16 }}>
117
+ <h2 style={{ fontSize: 16, margin: "0 0 8px", opacity: 0.85 }}>⚠️ Approvals</h2>
118
+ <ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
119
+ {actions.map((a) => (
120
+ <li key={a.id} style={{ padding: "8px 0", borderTop: "1px solid #2a2415" }}>
121
+ <code style={{ opacity: 0.7 }}>{a.tool}</code> · {a.projectId} — {a.summary}{" "}
122
+ <span style={{ opacity: 0.5, fontSize: 12 }}>[{a.status}{a.error ? `: ${a.error}` : ""}]</span>
123
+ {a.status === "pending" && (
124
+ <span style={{ float: "right" }}>
125
+ <button onClick={() => decide(a.id, "approve")} style={{ ...btn, padding: "4px 10px", background: "#1a7f4b", borderColor: "#1a7f4b" }}>Approve</button>{" "}
126
+ <button onClick={() => decide(a.id, "deny")} style={{ ...btn, padding: "4px 10px", background: "#7f1a1a", borderColor: "#7f1a1a" }}>Deny</button>
127
+ </span>
128
+ )}
129
+ </li>
130
+ ))}
131
+ </ul>
132
+ </section>
133
+ )}
134
+
135
+ <h2 style={{ fontSize: 16, opacity: 0.8 }}>Projects ({projects.length})</h2>
136
+ {projects.length === 0 && <p style={{ opacity: 0.5 }}>No projects linked yet.</p>}
137
+ <ul style={{ listStyle: "none", padding: 0 }}>
138
+ {projects.map((p) => (
139
+ <li key={p.id} style={card}>
140
+ <a href={`/brain/${p.id}`} style={{ color: "#7aa2ff", textDecoration: "none", fontWeight: 600 }}>
141
+ {p.name}
142
+ </a>
143
+ <span style={{ opacity: 0.6, marginLeft: 8 }}>
144
+ {p.status === "online" ? "🟢 online" : "⚪ offline"} · {p.memoryCount} memories · {p.artifactCount} artifacts
145
+ </span>
146
+ <a href={`/chat/${p.id}`} style={{ float: "right", color: "#7aa2ff" }}>chat →</a>
147
+ </li>
148
+ ))}
149
+ </ul>
150
+ </main>
151
+ );
152
+ }
153
+
154
+ const card: React.CSSProperties = {
155
+ border: "1px solid #1f2a3a",
156
+ borderRadius: 8,
157
+ padding: "12px 16px",
158
+ marginBottom: 8,
159
+ };
160
+ const btn: React.CSSProperties = {
161
+ padding: "8px 14px",
162
+ borderRadius: 8,
163
+ border: "1px solid #2b5cff",
164
+ background: "#2b5cff",
165
+ color: "white",
166
+ cursor: "pointer",
167
+ };
@@ -0,0 +1,113 @@
1
+ "use client";
2
+ /** Grounded chat with a project — streams the answer from /api/chat. */
3
+ import { use, useRef, useState } from "react";
4
+
5
+ type Turn = { role: "user" | "assistant"; content: string };
6
+
7
+ export default function ChatPage({ params }: { params: Promise<{ projectId: string }> }) {
8
+ const { projectId } = use(params);
9
+ const [turns, setTurns] = useState<Turn[]>([]);
10
+ const [input, setInput] = useState("");
11
+ const [crossProject, setCrossProject] = useState(false);
12
+ const [busy, setBusy] = useState(false);
13
+ const [saveMsg, setSaveMsg] = useState("");
14
+ const endRef = useRef<HTMLDivElement>(null);
15
+
16
+ async function saveLastToMemory() {
17
+ const last = [...turns].reverse().find((t) => t.role === "assistant")?.content;
18
+ if (!last) return;
19
+ const summary = last.replace(/\s+/g, " ").slice(0, 160);
20
+ const r = await fetch("/api/actions", {
21
+ method: "POST",
22
+ headers: { "content-type": "application/json" },
23
+ body: JSON.stringify({
24
+ projectId,
25
+ tool: "append_memory",
26
+ args: { kind: "conversation", actor: "agent", summary, body: last },
27
+ summary: `Save to ${projectId} memory: ${summary.slice(0, 60)}…`,
28
+ }),
29
+ });
30
+ setSaveMsg(r.ok ? "queued for approval → see /brain" : "error");
31
+ }
32
+
33
+ async function send() {
34
+ const message = input.trim();
35
+ if (!message || busy) return;
36
+ setInput("");
37
+ setBusy(true);
38
+ setTurns((t) => [...t, { 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({ projectId, message, allowCrossProject: crossProject }),
44
+ });
45
+
46
+ if (!res.ok || !res.body) {
47
+ setTurns((t) => {
48
+ const copy = [...t];
49
+ copy[copy.length - 1] = { role: "assistant", content: `[error ${res.status}]` };
50
+ return copy;
51
+ });
52
+ setBusy(false);
53
+ return;
54
+ }
55
+
56
+ const reader = res.body.getReader();
57
+ const dec = new TextDecoder();
58
+ let acc = "";
59
+ for (;;) {
60
+ const { done, value } = await reader.read();
61
+ if (done) break;
62
+ acc += dec.decode(value, { stream: true });
63
+ setTurns((t) => {
64
+ const copy = [...t];
65
+ copy[copy.length - 1] = { role: "assistant", content: acc };
66
+ return copy;
67
+ });
68
+ endRef.current?.scrollIntoView({ behavior: "smooth" });
69
+ }
70
+ setBusy(false);
71
+ }
72
+
73
+ return (
74
+ <main style={{ maxWidth: 760, margin: "0 auto", padding: "32px 24px", display: "flex", flexDirection: "column", height: "90vh" }}>
75
+ <a href={`/brain/${projectId}`} style={{ color: "#7aa2ff" }}>← {projectId}</a>
76
+ <h1 style={{ fontSize: 20 }}>💬 Chat · {projectId}</h1>
77
+
78
+ <div style={{ flex: 1, overflowY: "auto", border: "1px solid #1f2a3a", borderRadius: 8, padding: 16, margin: "12px 0" }}>
79
+ {turns.map((t, i) => (
80
+ <div key={i} style={{ marginBottom: 14 }}>
81
+ <strong style={{ color: t.role === "user" ? "#e6edf3" : "#7aa2ff" }}>{t.role === "user" ? "you" : "agent"}</strong>
82
+ <div style={{ whiteSpace: "pre-wrap", marginTop: 2 }}>{t.content || "…"}</div>
83
+ </div>
84
+ ))}
85
+ <div ref={endRef} />
86
+ </div>
87
+
88
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
89
+ <label style={{ fontSize: 13, opacity: 0.7 }}>
90
+ <input type="checkbox" checked={crossProject} onChange={(e) => setCrossProject(e.target.checked)} /> allow cross-project recall
91
+ </label>
92
+ <span style={{ fontSize: 13 }}>
93
+ {saveMsg && <span style={{ opacity: 0.6, marginRight: 8 }}>{saveMsg}</span>}
94
+ <button onClick={saveLastToMemory} disabled={turns.length === 0} style={{ padding: "4px 10px", borderRadius: 6, border: "1px solid #1f2a3a", background: "#111824", color: "#e6edf3", cursor: "pointer" }}>
95
+ 💾 Save to memory
96
+ </button>
97
+ </span>
98
+ </div>
99
+ <div style={{ display: "flex", gap: 8 }}>
100
+ <input
101
+ value={input}
102
+ onChange={(e) => setInput(e.target.value)}
103
+ onKeyDown={(e) => e.key === "Enter" && send()}
104
+ placeholder="Ask about this project…"
105
+ style={{ flex: 1, padding: "10px 12px", borderRadius: 8, border: "1px solid #1f2a3a", background: "#111824", color: "#e6edf3" }}
106
+ />
107
+ <button onClick={send} disabled={busy} style={{ padding: "10px 16px", borderRadius: 8, border: "1px solid #2b5cff", background: "#2b5cff", color: "white", cursor: "pointer" }}>
108
+ {busy ? "…" : "Send"}
109
+ </button>
110
+ </div>
111
+ </main>
112
+ );
113
+ }
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export const metadata = {
4
+ title: "MOP-AGENT",
5
+ description: "Self-hostable AI brain — remembers everything, federates every project.",
6
+ };
7
+
8
+ export default function RootLayout({ children }: { children: ReactNode }) {
9
+ return (
10
+ <html lang="en">
11
+ <body
12
+ style={{
13
+ margin: 0,
14
+ fontFamily:
15
+ "ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
16
+ background: "#0b0f17",
17
+ color: "#e6edf3",
18
+ }}
19
+ >
20
+ {children}
21
+ </body>
22
+ </html>
23
+ );
24
+ }
@@ -0,0 +1,72 @@
1
+ /**
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).
4
+ */
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
+ 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
+ );
72
+ }
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ /** Provider settings — plug an AI key (stored encrypted). */
3
+ import { useEffect, useState } from "react";
4
+
5
+ type Masked = { configured: boolean; provider?: string; model?: string | null; keyHint?: string };
6
+
7
+ export default function SettingsPage() {
8
+ const [config, setConfig] = useState<Masked>({ configured: false });
9
+ const [env, setEnv] = useState<{ anthropic: boolean; openrouter: boolean }>({ anthropic: false, openrouter: false });
10
+ const [provider, setProvider] = useState<"anthropic" | "openrouter">("openrouter");
11
+ const [apiKey, setApiKey] = useState("");
12
+ const [model, setModel] = useState("");
13
+ const [msg, setMsg] = useState("");
14
+
15
+ function load() {
16
+ fetch("/api/providers").then((r) => r.json()).then((d) => { setConfig(d.config); setEnv(d.env); }).catch(() => {});
17
+ }
18
+ useEffect(load, []);
19
+
20
+ async function save(e: React.FormEvent) {
21
+ e.preventDefault();
22
+ setMsg("…");
23
+ const r = await fetch("/api/providers", {
24
+ method: "POST",
25
+ headers: { "content-type": "application/json" },
26
+ body: JSON.stringify({ provider, apiKey, model: model || undefined }),
27
+ });
28
+ const d = await r.json();
29
+ setMsg(r.ok ? "✅ saved (key encrypted)" : `error: ${d.error}`);
30
+ setApiKey("");
31
+ if (r.ok) setConfig(d.config);
32
+ }
33
+
34
+ return (
35
+ <main style={{ maxWidth: 520, margin: "0 auto", padding: "48px 24px" }}>
36
+ <a href="/brain" style={{ color: "#7aa2ff" }}>← Brain</a>
37
+ <h1 style={{ fontSize: 24 }}>⚙️ Provider settings</h1>
38
+
39
+ <div style={{ border: "1px solid #1f2a3a", borderRadius: 8, padding: 14, margin: "12px 0", opacity: 0.9 }}>
40
+ {config.configured
41
+ ? <>Active: <strong>{config.provider}</strong>{config.model ? ` · ${config.model}` : ""} · key {config.keyHint}</>
42
+ : "No provider key saved yet — chat uses the offline echo provider."}
43
+ <div style={{ fontSize: 12, opacity: 0.6, marginTop: 6 }}>
44
+ env keys: anthropic {env.anthropic ? "✓" : "—"} · openrouter {env.openrouter ? "✓" : "—"} (DB config overrides env)
45
+ </div>
46
+ </div>
47
+
48
+ <form onSubmit={save} style={{ display: "grid", gap: 12 }}>
49
+ <select value={provider} onChange={(e) => setProvider(e.target.value as "anthropic" | "openrouter")} style={inp}>
50
+ <option value="openrouter">OpenRouter (any model)</option>
51
+ <option value="anthropic">Anthropic</option>
52
+ </select>
53
+ <input placeholder="API key" type="password" value={apiKey} onChange={(e) => setApiKey(e.target.value)} required style={inp} />
54
+ <input placeholder={provider === "anthropic" ? "model (e.g. claude-sonnet-4-6)" : "model (e.g. anthropic/claude-sonnet-4.6)"} value={model} onChange={(e) => setModel(e.target.value)} style={inp} />
55
+ <button type="submit" style={btn}>Save</button>
56
+ </form>
57
+ {msg && <p style={{ marginTop: 14, opacity: 0.85 }}>{msg}</p>}
58
+ </main>
59
+ );
60
+ }
61
+
62
+ const inp: React.CSSProperties = { padding: "10px 12px", borderRadius: 8, border: "1px solid #1f2a3a", background: "#111824", color: "#e6edf3" };
63
+ const btn: React.CSSProperties = { padding: "10px 12px", borderRadius: 8, border: "1px solid #2b5cff", background: "#2b5cff", color: "white", cursor: "pointer" };
@@ -0,0 +1,113 @@
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
+ */
6
+ import { useEffect, useState } from "react";
7
+ import { signUp, signIn } from "@/lib/auth-client";
8
+
9
+ export default function SetupPage() {
10
+ const [ownerExists, setOwnerExists] = useState<boolean | null>(null);
11
+ const [email, setEmail] = useState("");
12
+ const [password, setPassword] = useState("");
13
+ const [name, setName] = useState("");
14
+ const [msg, setMsg] = useState<string>("");
15
+ const [mode, setMode] = useState<"signup" | "signin">("signup");
16
+
17
+ 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");
23
+ })
24
+ .catch(() => setOwnerExists(null));
25
+ }, []);
26
+
27
+ async function submit(e: React.FormEvent) {
28
+ e.preventDefault();
29
+ setMsg("…");
30
+ 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 = "/";
38
+ }
39
+ }
40
+
41
+ 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>
51
+
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>
82
+
83
+ {msg && <p style={{ marginTop: 16, opacity: 0.85 }}>{msg}</p>}
84
+
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>
95
+ </main>
96
+ );
97
+ }
98
+
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
+ };