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
package/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # MOP-AGENT
2
+
3
+ MOP-AGENT is a self-hosted AI brain and control plane for projects connected
4
+ through MOP-FLOW. It stores project memory, performs semantic recall and
5
+ consolidation, serves grounded chat, and can request approved actions from a
6
+ linked FLOW node.
7
+
8
+ > **Release status:** npm package `mop-agent@0.1.0` is prepared but may not have
9
+ > been published yet. After publication, the canonical installation command is
10
+ > exactly `npx mop-agent`.
11
+
12
+ ## Current status
13
+
14
+ The application core through Fasa 7 foundation is implemented: reverse-WSS
15
+ project links, SQLite + sqlite-vec storage, Better Auth, semantic recall,
16
+ provider settings, consolidation, approval-based write-back, Telegram and
17
+ Discord adapters, skills, graph UI, execution backends, and team invites.
18
+
19
+ The npm bootstrap stages the packaged application durably at `/opt/mop-agent`,
20
+ uses the proven SQLite + sqlite-vec backend, and asks for sudo only for specific
21
+ OS operations. Package, bootstrap, installer, and smoke verification pass
22
+ locally. A clean VPS installation remains the final production verification.
23
+
24
+ ## Platform support
25
+
26
+ | Platform | Current support | Recommended use |
27
+ | --- | --- | --- |
28
+ | Debian, Ubuntu, Kali, Mint | Installer candidate | Linux VPS production target |
29
+ | Fedora, RHEL, Rocky, Alma | Installer candidate; paths need live verification | Linux VPS production target |
30
+ | Arch, Manjaro, Alpine | Installer candidate; paths need live verification | Advanced/test use |
31
+ | Windows | Native installer not available | Use WSL2 Ubuntu, or run development mode natively |
32
+ | macOS | Production installer not available | Run development mode; deploy production on Linux |
33
+
34
+ The automated installer depends on Linux facilities such as `systemd`, nginx,
35
+ Certbot, and standard Linux filesystem paths. Native Windows services/IIS and
36
+ macOS launchd/Homebrew automation have not been implemented.
37
+
38
+ ## Linux installation
39
+
40
+ Prerequisites:
41
+
42
+ - A Linux VPS with root/sudo access
43
+ - Node.js 20 or newer and npm
44
+ - A domain with an `A`/`AAAA` record pointing to the server
45
+ - Inbound ports 80 and 443 allowed by the firewall/security group
46
+
47
+ Run as your normal user:
48
+
49
+ ```bash
50
+ npx mop-agent
51
+ ```
52
+
53
+ The first run copies the npm-packaged runtime from the temporary npx cache into
54
+ `/opt/mop-agent`, installs its dependencies, and opens the TUI. Choose
55
+ `install` to install nginx/Certbot, then `setup` to configure the domain,
56
+ SQLite database, HTTPS, and systemd service. The menu remains open between
57
+ steps.
58
+
59
+ The installer requests `sudo` only when it needs to write under `/opt` or
60
+ `/etc`, install OS packages, or control nginx/systemd. Do not run the entire
61
+ npm/npx process with `sudo`.
62
+
63
+ Subsequent operations use the same command:
64
+
65
+ ```bash
66
+ npx mop-agent status
67
+ npx mop-agent update
68
+ npx mop-agent uninstall
69
+ ```
70
+
71
+ ### Linux filesystem map
72
+
73
+ MOP-AGENT is a long-running Node.js service behind nginx, not a static website,
74
+ so it uses `/opt/mop-agent` rather than `/var/www` for application code.
75
+
76
+ | Purpose | Debian/Ubuntu | RHEL/Arch/Alpine |
77
+ | --- | --- | --- |
78
+ | Application source | `/opt/mop-agent` | `/opt/mop-agent` |
79
+ | Environment file | `/opt/mop-agent/apps/web/.env` | same |
80
+ | Brain database/data (current) | `/opt/mop-agent/data` | same |
81
+ | nginx vhost | `/etc/nginx/sites-available/mop-agent.conf` | `/etc/nginx/conf.d/mop-agent.conf` |
82
+ | nginx enable link | `/etc/nginx/sites-enabled/mop-agent.conf` | not needed (`conf.d` is included directly) |
83
+ | systemd unit | `/etc/systemd/system/mop-agent.service` | same |
84
+ | TLS certificates | `/etc/letsencrypt/live/<domain>/` | same |
85
+ | Service logs | `journalctl -u mop-agent -f` | same |
86
+
87
+ `MOP_AGENT_DIR` can override `/opt/mop-agent`. Updates preserve
88
+ `apps/web/.env` and `data/`; uninstall preserves SQLite brain data unless the
89
+ user explicitly passes `--purge`.
90
+
91
+ Useful operations after setup:
92
+
93
+ ```bash
94
+ sudo systemctl status mop-agent
95
+ sudo journalctl -u mop-agent -f
96
+ sudo nginx -t
97
+ sudo systemctl reload nginx
98
+ ```
99
+
100
+ ## Windows
101
+
102
+ ### Recommended: WSL2
103
+
104
+ Install Ubuntu under WSL2, enable systemd in WSL, then run `npx mop-agent`
105
+ inside the WSL terminal. All paths such as `/opt/mop-agent` and `/etc/nginx/...`
106
+ exist inside the WSL filesystem, not under `C:\Program Files`.
107
+
108
+ ### Native Windows development
109
+
110
+ PowerShell can run the application for development, but the Linux installer,
111
+ nginx, Certbot, and systemd steps do not apply:
112
+
113
+ ```powershell
114
+ git clone https://github.com/BURHANDEV-ENTERPRISE/mop-agent.git
115
+ cd mop-agent
116
+ npm ci
117
+ Copy-Item apps/web/.env.example apps/web/.env
118
+ npm run typecheck
119
+ npm run dev:web
120
+ ```
121
+
122
+ Open `http://localhost:3000/setup`. Native Windows production service and HTTPS
123
+ automation remain TODO; do not use `sudo` in PowerShell or Command Prompt.
124
+
125
+ ## macOS
126
+
127
+ macOS currently supports development mode only:
128
+
129
+ ```bash
130
+ git clone https://github.com/BURHANDEV-ENTERPRISE/mop-agent.git
131
+ cd mop-agent
132
+ npm ci
133
+ cp apps/web/.env.example apps/web/.env
134
+ npm run typecheck
135
+ npm run dev:web
136
+ ```
137
+
138
+ Open `http://localhost:3000/setup`. A launchd/Homebrew/nginx production
139
+ installer is not implemented; use a supported Linux VPS for production.
140
+
141
+ ## Development
142
+
143
+ ```bash
144
+ git clone https://github.com/BURHANDEV-ENTERPRISE/mop-agent.git
145
+ cd mop-agent
146
+ npm ci
147
+ cp apps/web/.env.example apps/web/.env
148
+ npm run typecheck
149
+ npm run dev:web
150
+ ```
151
+
152
+ Set at least `BETTER_AUTH_SECRET` and `MOP_AGENT_SECRET` in
153
+ `apps/web/.env`. With no Anthropic/OpenRouter key, chat falls back to the local
154
+ offline echo provider.
155
+
156
+ Repository layout:
157
+
158
+ ```text
159
+ mop-agent/
160
+ ├── apps/web/ # Next.js UI, API, auth, brain, WS gateway
161
+ ├── packages/link-protocol/ # shared AGENT <-> FLOW schemas
162
+ ├── packages/flow-connector/ # reverse-WSS MOP-FLOW connector
163
+ ├── installer/ # installer TUI and platform plans
164
+ ├── scripts/ # smoke tests
165
+ └── data/ # runtime SQLite/brain data (gitignored)
166
+ ```
167
+
168
+ ## Verification
169
+
170
+ ```bash
171
+ npm run typecheck
172
+ npx tsx scripts/smoke-installer.mts
173
+ # Run the remaining smoke-*.mts scripts before a release.
174
+ ```
175
+
176
+ The complete npm publication checklist and installer acceptance criteria are in
177
+ [`TODO.md`](TODO.md).
@@ -0,0 +1,18 @@
1
+ # MOP-AGENT web — copy to .env and fill in.
2
+ PORT=3000
3
+
4
+ # --- Fasa 2 (auth + db + secrets) ---
5
+ # BETTER_AUTH_SECRET=
6
+ # BETTER_AUTH_URL=http://localhost:3000
7
+ # MOP_AGENT_SECRET= # AES-GCM key for provider keys at rest (32 bytes hex)
8
+ # MOP_AGENT_DATA_DIR= # defaults to OS data dir if unset
9
+
10
+ # --- providers (Fasa 2/3) ---
11
+ # ANTHROPIC_API_KEY=
12
+ # OPENROUTER_API_KEY=
13
+ # MOP_AGENT_PROVIDER=anthropic # anthropic | openrouter | echo (default: auto/echo)
14
+ # MOP_AGENT_MODEL=
15
+
16
+ # --- channels (Fasa 4.5) — set a token to auto-start that bot ---
17
+ # TELEGRAM_BOT_TOKEN=
18
+ # DISCORD_BOT_TOKEN=
@@ -0,0 +1,15 @@
1
+ /** POST /api/actions/[id]/approve — approve + execute over the live link (owner). */
2
+ import { requireRole } from "@/lib/authz";
3
+ import { approveAction } from "@/lib/brain/approvals";
4
+
5
+ export async function POST(
6
+ req: Request,
7
+ { params }: { params: Promise<{ id: string }> },
8
+ ): Promise<Response> {
9
+ const a = await requireRole(req, ["owner"]);
10
+ if (!a.ok) return a.response;
11
+ const { id } = await params;
12
+ const action = await approveAction(id);
13
+ if (!action) return Response.json({ error: "not_found" }, { status: 404 });
14
+ return Response.json({ action });
15
+ }
@@ -0,0 +1,15 @@
1
+ /** POST /api/actions/[id]/deny — deny a pending write action (owner). */
2
+ import { requireRole } from "@/lib/authz";
3
+ import { denyAction } from "@/lib/brain/approvals";
4
+
5
+ export async function POST(
6
+ req: Request,
7
+ { params }: { params: Promise<{ id: string }> },
8
+ ): Promise<Response> {
9
+ const a = await requireRole(req, ["owner"]);
10
+ if (!a.ok) return a.response;
11
+ const { id } = await params;
12
+ const action = denyAction(id);
13
+ if (!action) return Response.json({ error: "not_found" }, { status: 404 });
14
+ return Response.json({ action });
15
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * GET /api/actions — list pending/recent write actions (owner).
3
+ * POST /api/actions — request a write action { projectId, tool, args, summary } (owner).
4
+ */
5
+ import type { McpToolName } from "@mop/link-protocol";
6
+ import { auth } from "@/lib/auth";
7
+ import { listActions, requestAction } from "@/lib/brain/approvals";
8
+
9
+ export async function GET(req: Request): Promise<Response> {
10
+ const session = await auth.api.getSession({ headers: req.headers });
11
+ if (!session) return Response.json({ error: "unauthorized" }, { status: 401 });
12
+ return Response.json({ actions: listActions() });
13
+ }
14
+
15
+ export async function POST(req: Request): Promise<Response> {
16
+ const session = await auth.api.getSession({ headers: req.headers });
17
+ if (!session) return Response.json({ error: "unauthorized" }, { status: 401 });
18
+ const body = (await req.json()) as {
19
+ projectId: string;
20
+ tool: McpToolName;
21
+ args: Record<string, unknown>;
22
+ summary?: string;
23
+ };
24
+ if (!body?.projectId || !body?.tool) {
25
+ return Response.json({ error: "missing_projectId_or_tool" }, { status: 400 });
26
+ }
27
+ const action = requestAction(body);
28
+ return Response.json({ action });
29
+ }
@@ -0,0 +1,4 @@
1
+ import { auth } from "@/lib/auth";
2
+ import { toNextJsHandler } from "better-auth/next-js";
3
+
4
+ export const { GET, POST } = toNextJsHandler(auth);
@@ -0,0 +1,50 @@
1
+ /**
2
+ * POST /api/chat — grounded chat. Owner session -> recall project context ->
3
+ * stream the provider's answer. Body: { projectId, message, allowCrossProject? }
4
+ */
5
+ import { auth } from "@/lib/auth";
6
+ import { recall } from "@/lib/brain/broker";
7
+ import { resolveProvider } from "@/lib/providers";
8
+
9
+ export async function POST(req: Request): Promise<Response> {
10
+ const session = await auth.api.getSession({ headers: req.headers });
11
+ if (!session) return Response.json({ error: "unauthorized" }, { status: 401 });
12
+
13
+ const { projectId, message, allowCrossProject } = (await req.json()) as {
14
+ projectId: string;
15
+ message: string;
16
+ allowCrossProject?: boolean;
17
+ };
18
+ if (!projectId || !message) {
19
+ return Response.json({ error: "missing_projectId_or_message" }, { status: 400 });
20
+ }
21
+
22
+ const pack = await recall({ query: message, projectId, allowCrossProject: !!allowCrossProject });
23
+ const provider = resolveProvider(session.user.id);
24
+
25
+ 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.",
28
+ "",
29
+ pack.toPromptString(),
30
+ ].join("\n");
31
+
32
+ const encoder = new TextEncoder();
33
+ const stream = new ReadableStream<Uint8Array>({
34
+ async start(controller) {
35
+ try {
36
+ for await (const delta of provider.chat({ system, messages: [{ role: "user", content: message }] })) {
37
+ controller.enqueue(encoder.encode(delta));
38
+ }
39
+ } catch (e) {
40
+ controller.enqueue(encoder.encode(`\n[provider error: ${e instanceof Error ? e.message : String(e)}]`));
41
+ } finally {
42
+ controller.close();
43
+ }
44
+ },
45
+ });
46
+
47
+ return new Response(stream, {
48
+ headers: { "Content-Type": "text/plain; charset=utf-8", "X-Provider": provider.id },
49
+ });
50
+ }
@@ -0,0 +1,10 @@
1
+ /** POST /api/consolidate — owner-triggered episodic→semantic consolidation. */
2
+ import { requireRole } from "@/lib/authz";
3
+ import { consolidate } from "@/lib/brain/consolidate";
4
+
5
+ export async function POST(req: Request): Promise<Response> {
6
+ const a = await requireRole(req, ["owner"]);
7
+ if (!a.ok) return a.response;
8
+ const result = await consolidate();
9
+ return Response.json(result);
10
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * GET /api/graph — Brain knowledge graph (owner).
3
+ * Nodes: projects, semantic notes (Main Brain), skills.
4
+ * Edges: semantic note / skill → each of its source projects.
5
+ */
6
+ import { auth } from "@/lib/auth";
7
+ import { listProjects } from "@/lib/link/store";
8
+ import { listSemanticNotes } from "@/lib/brain/consolidate";
9
+ import { listSkills } from "@/lib/brain/skills";
10
+
11
+ export type GraphNode = { id: string; label: string; type: "project" | "pattern" | "skill" };
12
+ export type GraphEdge = { from: string; to: string };
13
+
14
+ export async function GET(req: Request): Promise<Response> {
15
+ const session = await auth.api.getSession({ headers: req.headers });
16
+ if (!session) return Response.json({ error: "unauthorized" }, { status: 401 });
17
+
18
+ const nodes: GraphNode[] = [];
19
+ const edges: GraphEdge[] = [];
20
+
21
+ for (const p of listProjects()) nodes.push({ id: `project:${p.id}`, label: p.name, type: "project" });
22
+
23
+ for (const n of listSemanticNotes()) {
24
+ nodes.push({ id: `pattern:${n.id}`, label: n.title, type: "pattern" });
25
+ for (const pid of n.sourceProjects ?? []) edges.push({ from: `pattern:${n.id}`, to: `project:${pid}` });
26
+ }
27
+
28
+ for (const s of listSkills()) {
29
+ nodes.push({ id: `skill:${s.id}`, label: s.name, type: "skill" });
30
+ for (const pid of s.sourceProjects ?? []) edges.push({ from: `skill:${s.id}`, to: `project:${pid}` });
31
+ }
32
+
33
+ return Response.json({ nodes, edges });
34
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * GET /api/invites — list invites (owner).
3
+ * POST /api/invites — invite an email { email, role? } (owner). Email-scoped.
4
+ */
5
+ import { eq } from "drizzle-orm";
6
+ import { getDb } from "@/lib/db/client";
7
+ import { invite } from "@/lib/db/schema";
8
+ import { requireRole } from "@/lib/authz";
9
+
10
+ export async function GET(req: Request): Promise<Response> {
11
+ const a = await requireRole(req, ["owner"]);
12
+ if (!a.ok) return a.response;
13
+ return Response.json({ invites: getDb().select().from(invite).all() });
14
+ }
15
+
16
+ export async function POST(req: Request): Promise<Response> {
17
+ const a = await requireRole(req, ["owner"]);
18
+ if (!a.ok) return a.response;
19
+ const body = (await req.json()) as { email?: string; role?: "member" | "owner"; ttlDays?: number };
20
+ if (!body?.email) return Response.json({ error: "missing_email" }, { status: 400 });
21
+ const role = body.role === "owner" ? "owner" : "member";
22
+ const expiresAt = Date.now() + (body.ttlDays ?? 7) * 86_400_000;
23
+ getDb()
24
+ .insert(invite)
25
+ .values({ email: body.email, role, expiresAt, usedAt: null, invitedBy: a.userId, createdAt: Date.now() })
26
+ .onConflictDoUpdate({ target: invite.email, set: { role, expiresAt, usedAt: null, invitedBy: a.userId } })
27
+ .run();
28
+ return Response.json({ ok: true, email: body.email, role, expiresAt });
29
+ }
30
+
31
+ export async function DELETE(req: Request): Promise<Response> {
32
+ const a = await requireRole(req, ["owner"]);
33
+ if (!a.ok) return a.response;
34
+ const email = new URL(req.url).searchParams.get("email");
35
+ if (!email) return Response.json({ error: "missing_email" }, { status: 400 });
36
+ getDb().delete(invite).where(eq(invite.email, email)).run();
37
+ return Response.json({ ok: true });
38
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * POST /api/link/code — generate a one-time pairing code for "Link Project".
3
+ * Owner-only: requires a valid Better Auth session.
4
+ */
5
+ import { requireRole } from "@/lib/authz";
6
+ import { createPairingCode } from "@/lib/link/store";
7
+
8
+ export async function POST(req: Request): Promise<Response> {
9
+ const a = await requireRole(req, ["owner"]);
10
+ if (!a.ok) return a.response;
11
+ const { code, expiresAt } = createPairingCode();
12
+ return Response.json({ code, expiresAt });
13
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * POST /api/link/pair — FLOW exchanges a one-time pairing code for a link token.
3
+ * Body: { code, manifest } (see @mop/link-protocol PairRequest)
4
+ */
5
+ import {
6
+ LINK_WS_PATH,
7
+ type PairRequest,
8
+ type PairResponse,
9
+ } from "@mop/link-protocol";
10
+ import { consumePairingCode, registerProject } from "@/lib/link/store";
11
+
12
+ export async function POST(req: Request): Promise<Response> {
13
+ let body: PairRequest;
14
+ try {
15
+ body = (await req.json()) as PairRequest;
16
+ } catch {
17
+ return Response.json({ error: "bad_json" }, { status: 400 });
18
+ }
19
+
20
+ if (!body?.code || !body?.manifest?.projectId) {
21
+ return Response.json({ error: "missing_code_or_manifest" }, { status: 400 });
22
+ }
23
+
24
+ if (!consumePairingCode(body.code)) {
25
+ return Response.json({ error: "invalid_or_expired_code" }, { status: 401 });
26
+ }
27
+
28
+ const { linkToken } = registerProject(body.manifest);
29
+
30
+ const wsUrl = new URL(req.url);
31
+ wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
32
+ wsUrl.pathname = LINK_WS_PATH;
33
+ wsUrl.search = "";
34
+
35
+ const out: PairResponse = {
36
+ projectId: body.manifest.projectId,
37
+ linkToken,
38
+ wsUrl: wsUrl.toString(),
39
+ };
40
+ return Response.json(out);
41
+ }
@@ -0,0 +1,11 @@
1
+ /** GET /api/me — current user + role. */
2
+ import { auth, getRole } from "@/lib/auth";
3
+
4
+ export async function GET(req: Request): Promise<Response> {
5
+ const session = await auth.api.getSession({ headers: req.headers });
6
+ if (!session) return Response.json({ error: "unauthorized" }, { status: 401 });
7
+ return Response.json({
8
+ user: { id: session.user.id, email: session.user.email, name: session.user.name },
9
+ role: getRole(session.user.id) ?? "member",
10
+ });
11
+ }
@@ -0,0 +1,16 @@
1
+ /** GET /api/members — list users + roles (owner). */
2
+ import { getSqlite } from "@/lib/db/client";
3
+ import { requireRole } from "@/lib/authz";
4
+
5
+ export async function GET(req: Request): Promise<Response> {
6
+ const a = await requireRole(req, ["owner"]);
7
+ if (!a.ok) return a.response;
8
+ const members = getSqlite()
9
+ .prepare(
10
+ `SELECT u.id, u.email, u.name, COALESCE(r.role, 'member') AS role
11
+ FROM user u LEFT JOIN app_role r ON r.user_id = u.id
12
+ ORDER BY r.role = 'owner' DESC, u.email`,
13
+ )
14
+ .all();
15
+ return Response.json({ members });
16
+ }
@@ -0,0 +1,12 @@
1
+ import { auth } from "@/lib/auth";
2
+ import { listProjectMemory } from "@/lib/brain/mirror";
3
+
4
+ export async function GET(
5
+ req: Request,
6
+ { params }: { params: Promise<{ id: string }> },
7
+ ): Promise<Response> {
8
+ const session = await auth.api.getSession({ headers: req.headers });
9
+ if (!session) return Response.json({ error: "unauthorized" }, { status: 401 });
10
+ const { id } = await params;
11
+ return Response.json({ memory: listProjectMemory(id) });
12
+ }
@@ -0,0 +1,19 @@
1
+ import { auth } from "@/lib/auth";
2
+ import { getMirror } from "@/lib/brain/mirror";
3
+
4
+ export async function GET(
5
+ req: Request,
6
+ { params }: { params: Promise<{ id: string }> },
7
+ ): Promise<Response> {
8
+ const session = await auth.api.getSession({ headers: req.headers });
9
+ if (!session) return Response.json({ error: "unauthorized" }, { status: 401 });
10
+ const { id } = await params;
11
+ const mirror = getMirror(id);
12
+ if (!mirror) return Response.json({ error: "not_found" }, { status: 404 });
13
+ return Response.json({
14
+ state: mirror.state,
15
+ artifacts: mirror.artifacts,
16
+ memoryCount: mirror.memoryCount,
17
+ updatedAt: mirror.updatedAt,
18
+ });
19
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * GET /api/projects — linked projects + their live status and mirror summary.
3
+ */
4
+ import { listProjects } from "@/lib/link/store";
5
+ import { getMirror } from "@/lib/brain/mirror";
6
+
7
+ export async function GET(): Promise<Response> {
8
+ const projects = listProjects().map((p) => {
9
+ const mirror = getMirror(p.id);
10
+ return {
11
+ id: p.id,
12
+ name: p.name,
13
+ status: p.status,
14
+ mopFlowVersion: p.mopFlowVersion,
15
+ lastSeenAt: p.lastSeenAt,
16
+ memoryCount: mirror?.memoryCount ?? 0,
17
+ artifactCount: mirror?.artifacts.length ?? 0,
18
+ };
19
+ });
20
+ return Response.json({ projects });
21
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * GET /api/providers — current provider config (masked) + env-key availability.
3
+ * POST /api/providers — set { provider, apiKey, model } (owner; key encrypted at rest).
4
+ */
5
+ import { requireAuth, requireRole } from "@/lib/authz";
6
+ import { getProviderConfigMasked, setProviderConfig, type ProviderId } from "@/lib/providers/config";
7
+
8
+ export async function GET(req: Request): Promise<Response> {
9
+ const a = await requireAuth(req);
10
+ if (!a.ok) return a.response;
11
+ return Response.json({
12
+ config: getProviderConfigMasked(a.userId),
13
+ env: {
14
+ anthropic: !!process.env.ANTHROPIC_API_KEY,
15
+ openrouter: !!process.env.OPENROUTER_API_KEY,
16
+ },
17
+ });
18
+ }
19
+
20
+ export async function POST(req: Request): Promise<Response> {
21
+ const a = await requireRole(req, ["owner"]);
22
+ if (!a.ok) return a.response;
23
+ const body = (await req.json()) as { provider: ProviderId; apiKey: string; model?: string };
24
+ if (!body?.provider || !body?.apiKey) {
25
+ return Response.json({ error: "missing_provider_or_apiKey" }, { status: 400 });
26
+ }
27
+ if (body.provider !== "anthropic" && body.provider !== "openrouter") {
28
+ return Response.json({ error: "unknown_provider" }, { status: 400 });
29
+ }
30
+ setProviderConfig(a.userId, { provider: body.provider, apiKey: body.apiKey, model: body.model });
31
+ return Response.json({ config: getProviderConfigMasked(a.userId) });
32
+ }
@@ -0,0 +1,9 @@
1
+ /** GET /api/semantic — list Main Brain semantic notes (owner-only). */
2
+ import { auth } from "@/lib/auth";
3
+ import { listSemanticNotes } from "@/lib/brain/consolidate";
4
+
5
+ export async function GET(req: Request): Promise<Response> {
6
+ const session = await auth.api.getSession({ headers: req.headers });
7
+ if (!session) return Response.json({ error: "unauthorized" }, { status: 401 });
8
+ return Response.json({ notes: listSemanticNotes() });
9
+ }
@@ -0,0 +1,6 @@
1
+ /** GET /api/setup/status — whether the owner account has been created yet. */
2
+ import { ownerExists } from "@/lib/auth";
3
+
4
+ export async function GET(): Promise<Response> {
5
+ return Response.json({ ownerExists: ownerExists() });
6
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * GET /api/skills — list procedural skills (owner).
3
+ * POST /api/skills — add a skill { name, description, body, sourceProjects? } (owner).
4
+ */
5
+ import { requireAuth, requireRole } from "@/lib/authz";
6
+ import { addSkill, listSkills } from "@/lib/brain/skills";
7
+
8
+ export async function GET(req: Request): Promise<Response> {
9
+ const a = await requireAuth(req);
10
+ if (!a.ok) return a.response;
11
+ return Response.json({ skills: listSkills() });
12
+ }
13
+
14
+ export async function POST(req: Request): Promise<Response> {
15
+ const a = await requireRole(req, ["owner"]);
16
+ if (!a.ok) return a.response;
17
+ const body = (await req.json()) as { name: string; description: string; body: string; sourceProjects?: string[] };
18
+ if (!body?.name || !body?.body) {
19
+ return Response.json({ error: "missing_name_or_body" }, { status: 400 });
20
+ }
21
+ const id = await addSkill({ name: body.name, description: body.description ?? "", body: body.body, sourceProjects: body.sourceProjects });
22
+ return Response.json({ id });
23
+ }