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.
- package/README.md +177 -0
- package/apps/web/.env.example +18 -0
- package/apps/web/app/api/actions/[id]/approve/route.ts +15 -0
- package/apps/web/app/api/actions/[id]/deny/route.ts +15 -0
- package/apps/web/app/api/actions/route.ts +29 -0
- package/apps/web/app/api/auth/[...all]/route.ts +4 -0
- package/apps/web/app/api/chat/route.ts +50 -0
- package/apps/web/app/api/consolidate/route.ts +10 -0
- package/apps/web/app/api/graph/route.ts +34 -0
- package/apps/web/app/api/invites/route.ts +38 -0
- package/apps/web/app/api/link/code/route.ts +13 -0
- package/apps/web/app/api/link/pair/route.ts +41 -0
- package/apps/web/app/api/me/route.ts +11 -0
- package/apps/web/app/api/members/route.ts +16 -0
- package/apps/web/app/api/projects/[id]/memory/route.ts +12 -0
- package/apps/web/app/api/projects/[id]/state/route.ts +19 -0
- package/apps/web/app/api/projects/route.ts +21 -0
- package/apps/web/app/api/providers/route.ts +32 -0
- package/apps/web/app/api/semantic/route.ts +9 -0
- package/apps/web/app/api/setup/status/route.ts +6 -0
- package/apps/web/app/api/skills/route.ts +23 -0
- package/apps/web/app/brain/[projectId]/page.tsx +50 -0
- package/apps/web/app/brain/graph/page.tsx +54 -0
- package/apps/web/app/brain/page.tsx +167 -0
- package/apps/web/app/chat/[projectId]/page.tsx +113 -0
- package/apps/web/app/layout.tsx +24 -0
- package/apps/web/app/page.tsx +72 -0
- package/apps/web/app/settings/page.tsx +63 -0
- package/apps/web/app/setup/page.tsx +113 -0
- package/apps/web/app/team/page.tsx +86 -0
- package/apps/web/bin/mop-agent.mjs +85 -0
- package/apps/web/lib/auth-client.ts +5 -0
- package/apps/web/lib/auth.ts +86 -0
- package/apps/web/lib/authz.ts +23 -0
- package/apps/web/lib/brain/answer.ts +27 -0
- package/apps/web/lib/brain/approvals.ts +81 -0
- package/apps/web/lib/brain/broker.ts +98 -0
- package/apps/web/lib/brain/consolidate.ts +133 -0
- package/apps/web/lib/brain/mirror.ts +80 -0
- package/apps/web/lib/brain/scheduler.ts +30 -0
- package/apps/web/lib/brain/skills.ts +34 -0
- package/apps/web/lib/channels/binding.ts +26 -0
- package/apps/web/lib/channels/discord.ts +28 -0
- package/apps/web/lib/channels/handler.ts +44 -0
- package/apps/web/lib/channels/index.ts +18 -0
- package/apps/web/lib/channels/telegram.ts +18 -0
- package/apps/web/lib/crypto.ts +35 -0
- package/apps/web/lib/db/client.ts +34 -0
- package/apps/web/lib/db/migrate.ts +116 -0
- package/apps/web/lib/db/paths.ts +25 -0
- package/apps/web/lib/db/schema.ts +105 -0
- package/apps/web/lib/link/store.ts +89 -0
- package/apps/web/lib/memory/embed.ts +111 -0
- package/apps/web/lib/memory/local-embedder.ts +26 -0
- package/apps/web/lib/providers/anthropic.ts +23 -0
- package/apps/web/lib/providers/config.ts +55 -0
- package/apps/web/lib/providers/echo.ts +26 -0
- package/apps/web/lib/providers/index.ts +41 -0
- package/apps/web/lib/providers/openrouter.ts +24 -0
- package/apps/web/lib/providers/types.ts +14 -0
- package/apps/web/lib/ws/gateway.ts +113 -0
- package/apps/web/next-env.d.ts +6 -0
- package/apps/web/next.config.mjs +9 -0
- package/apps/web/package.json +44 -0
- package/apps/web/scripts/migrate.ts +12 -0
- package/apps/web/server.ts +27 -0
- package/apps/web/tsconfig.json +31 -0
- package/installer/bootstrap.mjs +161 -0
- package/installer/lib.mjs +196 -0
- package/installer/mop-agent.mjs +322 -0
- package/npm-shrinkwrap.json +5032 -0
- package/package.json +71 -0
- package/packages/flow-connector/bin/cli.mjs +67 -0
- package/packages/flow-connector/package.json +26 -0
- package/packages/flow-connector/src/exec.ts +81 -0
- package/packages/flow-connector/src/index.ts +17 -0
- package/packages/flow-connector/src/linkfile.ts +46 -0
- package/packages/flow-connector/src/pair.ts +66 -0
- package/packages/flow-connector/src/serve.ts +103 -0
- package/packages/flow-connector/src/snapshot.ts +94 -0
- package/packages/flow-connector/src/tools.ts +198 -0
- package/packages/flow-connector/tsconfig.json +10 -0
- package/packages/link-protocol/package.json +17 -0
- package/packages/link-protocol/src/index.ts +245 -0
- package/packages/link-protocol/tsconfig.json +10 -0
- 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,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,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
|
+
}
|