@femtomc/mu-server 26.2.19
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 +148 -0
- package/dist/api/forum.d.ts +2 -0
- package/dist/api/forum.js +41 -0
- package/dist/api/issues.d.ts +2 -0
- package/dist/api/issues.js +87 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.js +66 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# @femtomc/mu-server
|
|
2
|
+
|
|
3
|
+
HTTP JSON API server for mu issue and forum stores. Provides the backend for the mu web UI and can also be used standalone for programmatic access.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @femtomc/mu-server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createServer } from "@femtomc/mu-server";
|
|
15
|
+
|
|
16
|
+
// Create server with default options (uses current directory as repo root)
|
|
17
|
+
const server = createServer();
|
|
18
|
+
|
|
19
|
+
// Or specify custom repo root and port
|
|
20
|
+
const server = createServer({
|
|
21
|
+
repoRoot: "/path/to/repo",
|
|
22
|
+
port: 8080
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Start the server
|
|
26
|
+
Bun.serve(server);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API Endpoints
|
|
30
|
+
|
|
31
|
+
### Health Check
|
|
32
|
+
|
|
33
|
+
- `GET /healthz` or `GET /health` - Returns 200 OK
|
|
34
|
+
|
|
35
|
+
### Status
|
|
36
|
+
|
|
37
|
+
- `GET /api/status` - Returns repository status
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"repo_root": "/path/to/repo",
|
|
41
|
+
"open_count": 10,
|
|
42
|
+
"ready_count": 3
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Issues
|
|
47
|
+
|
|
48
|
+
- `GET /api/issues` - List issues
|
|
49
|
+
- Query params: `?status=open&tag=bug`
|
|
50
|
+
- `GET /api/issues/:id` - Get issue by ID
|
|
51
|
+
- `POST /api/issues` - Create new issue
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"title": "Issue title",
|
|
55
|
+
"body": "Issue description",
|
|
56
|
+
"tags": ["bug", "priority"],
|
|
57
|
+
"priority": 2,
|
|
58
|
+
"execution_spec": { ... }
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
- `PATCH /api/issues/:id` - Update issue
|
|
62
|
+
- `POST /api/issues/:id/close` - Close issue
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"outcome": "success"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
- `POST /api/issues/:id/claim` - Claim issue (changes status to in_progress)
|
|
69
|
+
- `GET /api/issues/ready` - Get ready issues
|
|
70
|
+
- Query param: `?root=issue-id`
|
|
71
|
+
|
|
72
|
+
### Forum
|
|
73
|
+
|
|
74
|
+
- `GET /api/forum/topics` - List forum topics
|
|
75
|
+
- Query param: `?prefix=issue:`
|
|
76
|
+
- `GET /api/forum/read` - Read messages from topic
|
|
77
|
+
- Query params: `?topic=issue:123&limit=50`
|
|
78
|
+
- `POST /api/forum/post` - Post message to topic
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"topic": "issue:123",
|
|
82
|
+
"body": "Message content",
|
|
83
|
+
"author": "username"
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Running the Server
|
|
88
|
+
|
|
89
|
+
### With Web UI (Recommended)
|
|
90
|
+
|
|
91
|
+
The easiest way to run the server with the web interface:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# From any mu repository
|
|
95
|
+
mu serve
|
|
96
|
+
|
|
97
|
+
# Or with custom ports
|
|
98
|
+
mu serve --port 8080 --api-port 3001
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Standalone Server
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Install globally
|
|
105
|
+
bun install -g @femtomc/mu-server
|
|
106
|
+
|
|
107
|
+
# Run server (looks for .mu/ in current directory or ancestors)
|
|
108
|
+
mu-server
|
|
109
|
+
|
|
110
|
+
# Or set custom port
|
|
111
|
+
PORT=8080 mu-server
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Programmatic
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Run the example
|
|
118
|
+
bun run example.js
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Install dependencies
|
|
125
|
+
bun install
|
|
126
|
+
|
|
127
|
+
# Run tests
|
|
128
|
+
bun test
|
|
129
|
+
|
|
130
|
+
# Build
|
|
131
|
+
bun run build
|
|
132
|
+
|
|
133
|
+
# Start server (after building)
|
|
134
|
+
bun run start
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Architecture
|
|
138
|
+
|
|
139
|
+
The server uses:
|
|
140
|
+
- Filesystem-backed JSONL stores (FsJsonlStore)
|
|
141
|
+
- IssueStore and ForumStore from mu packages
|
|
142
|
+
- Bun's built-in HTTP server
|
|
143
|
+
- Simple REST-style JSON API
|
|
144
|
+
|
|
145
|
+
All data is persisted to `.mu/` directory:
|
|
146
|
+
- `.mu/issues.jsonl` - Issue data
|
|
147
|
+
- `.mu/forum.jsonl` - Forum messages
|
|
148
|
+
- `.mu/events.jsonl` - Event log
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export async function forumRoutes(request, context) {
|
|
2
|
+
const url = new URL(request.url);
|
|
3
|
+
const path = url.pathname.replace("/api/forum", "") || "/";
|
|
4
|
+
const method = request.method;
|
|
5
|
+
try {
|
|
6
|
+
// List topics - GET /api/forum/topics
|
|
7
|
+
if (path === "/topics" && method === "GET") {
|
|
8
|
+
const prefix = url.searchParams.get("prefix");
|
|
9
|
+
const topics = await context.forumStore.topics(prefix);
|
|
10
|
+
return Response.json(topics);
|
|
11
|
+
}
|
|
12
|
+
// Read messages - GET /api/forum/read
|
|
13
|
+
if (path === "/read" && method === "GET") {
|
|
14
|
+
const topic = url.searchParams.get("topic");
|
|
15
|
+
const limit = url.searchParams.get("limit");
|
|
16
|
+
if (!topic) {
|
|
17
|
+
return new Response("Topic is required", { status: 400 });
|
|
18
|
+
}
|
|
19
|
+
const messages = await context.forumStore.read(topic, limit ? parseInt(limit, 10) : 50);
|
|
20
|
+
return Response.json(messages);
|
|
21
|
+
}
|
|
22
|
+
// Post message - POST /api/forum/post
|
|
23
|
+
if (path === "/post" && method === "POST") {
|
|
24
|
+
const body = await request.json();
|
|
25
|
+
const { topic, body: messageBody, author } = body;
|
|
26
|
+
if (!topic || !messageBody) {
|
|
27
|
+
return new Response("Topic and body are required", { status: 400 });
|
|
28
|
+
}
|
|
29
|
+
const message = await context.forumStore.post(topic, messageBody, author || "system");
|
|
30
|
+
return Response.json(message, { status: 201 });
|
|
31
|
+
}
|
|
32
|
+
return new Response("Not Found", { status: 404 });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error("Forum API error:", error);
|
|
36
|
+
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : "Internal server error" }), {
|
|
37
|
+
status: 500,
|
|
38
|
+
headers: { "Content-Type": "application/json" }
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export async function issueRoutes(request, context) {
|
|
2
|
+
const url = new URL(request.url);
|
|
3
|
+
const path = url.pathname.replace("/api/issues", "") || "/";
|
|
4
|
+
const method = request.method;
|
|
5
|
+
try {
|
|
6
|
+
// List issues - GET /api/issues
|
|
7
|
+
if (path === "/" && method === "GET") {
|
|
8
|
+
const status = url.searchParams.get("status");
|
|
9
|
+
const tag = url.searchParams.get("tag");
|
|
10
|
+
const issues = await context.issueStore.list({
|
|
11
|
+
status: status,
|
|
12
|
+
tag: tag || undefined
|
|
13
|
+
});
|
|
14
|
+
return Response.json(issues);
|
|
15
|
+
}
|
|
16
|
+
// Get ready issues - GET /api/issues/ready
|
|
17
|
+
if (path === "/ready" && method === "GET") {
|
|
18
|
+
const root = url.searchParams.get("root");
|
|
19
|
+
const issues = await context.issueStore.ready(root || undefined);
|
|
20
|
+
return Response.json(issues);
|
|
21
|
+
}
|
|
22
|
+
// Get single issue - GET /api/issues/:id
|
|
23
|
+
if (path.startsWith("/") && method === "GET") {
|
|
24
|
+
const id = path.slice(1);
|
|
25
|
+
if (id) {
|
|
26
|
+
const issue = await context.issueStore.get(id);
|
|
27
|
+
if (!issue) {
|
|
28
|
+
return new Response("Issue not found", { status: 404 });
|
|
29
|
+
}
|
|
30
|
+
return Response.json(issue);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Create issue - POST /api/issues
|
|
34
|
+
if (path === "/" && method === "POST") {
|
|
35
|
+
const body = await request.json();
|
|
36
|
+
const { title, body: issueBody, tags, priority, execution_spec } = body;
|
|
37
|
+
if (!title) {
|
|
38
|
+
return new Response("Title is required", { status: 400 });
|
|
39
|
+
}
|
|
40
|
+
const issue = await context.issueStore.create(title, {
|
|
41
|
+
body: issueBody,
|
|
42
|
+
tags,
|
|
43
|
+
priority,
|
|
44
|
+
execution_spec
|
|
45
|
+
});
|
|
46
|
+
return Response.json(issue, { status: 201 });
|
|
47
|
+
}
|
|
48
|
+
// Update issue - PATCH /api/issues/:id
|
|
49
|
+
if (path.startsWith("/") && method === "PATCH") {
|
|
50
|
+
const id = path.slice(1);
|
|
51
|
+
if (id) {
|
|
52
|
+
const body = await request.json();
|
|
53
|
+
const issue = await context.issueStore.update(id, body);
|
|
54
|
+
return Response.json(issue);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Close issue - POST /api/issues/:id/close
|
|
58
|
+
if (path.endsWith("/close") && method === "POST") {
|
|
59
|
+
const id = path.slice(1, -6); // Remove leading / and trailing /close
|
|
60
|
+
const body = await request.json();
|
|
61
|
+
const { outcome } = body;
|
|
62
|
+
if (!outcome) {
|
|
63
|
+
return new Response("Outcome is required", { status: 400 });
|
|
64
|
+
}
|
|
65
|
+
const issue = await context.issueStore.close(id, outcome);
|
|
66
|
+
return Response.json(issue);
|
|
67
|
+
}
|
|
68
|
+
// Claim issue - POST /api/issues/:id/claim
|
|
69
|
+
if (path.endsWith("/claim") && method === "POST") {
|
|
70
|
+
const id = path.slice(1, -6); // Remove leading / and trailing /claim
|
|
71
|
+
const success = await context.issueStore.claim(id);
|
|
72
|
+
if (!success) {
|
|
73
|
+
return new Response("Failed to claim issue", { status: 409 });
|
|
74
|
+
}
|
|
75
|
+
const issue = await context.issueStore.get(id);
|
|
76
|
+
return Response.json(issue);
|
|
77
|
+
}
|
|
78
|
+
return new Response("Not Found", { status: 404 });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error("Issue API error:", error);
|
|
82
|
+
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : "Internal server error" }), {
|
|
83
|
+
status: 500,
|
|
84
|
+
headers: { "Content-Type": "application/json" }
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createServer } from "./server.js";
|
|
3
|
+
import { findRepoRoot } from "@femtomc/mu-core/node";
|
|
4
|
+
const port = parseInt(process.env.PORT || "3000", 10);
|
|
5
|
+
let repoRoot;
|
|
6
|
+
try {
|
|
7
|
+
repoRoot = findRepoRoot();
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
console.error("Error: Could not find .mu directory. Run 'mu init' first.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
console.log(`Starting mu-server on port ${port}...`);
|
|
14
|
+
console.log(`Repository root: ${repoRoot}`);
|
|
15
|
+
const server = createServer({ repoRoot, port });
|
|
16
|
+
Bun.serve(server);
|
|
17
|
+
console.log(`Server running at http://localhost:${port}`);
|
|
18
|
+
console.log(`Health check: http://localhost:${port}/healthz`);
|
|
19
|
+
console.log(`API Status: http://localhost:${port}/api/status`);
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createServer, createContext } from "./server.js";
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EventLog } from "@femtomc/mu-core/node";
|
|
2
|
+
import { IssueStore } from "@femtomc/mu-issue";
|
|
3
|
+
import { ForumStore } from "@femtomc/mu-forum";
|
|
4
|
+
export type ServerOptions = {
|
|
5
|
+
repoRoot?: string;
|
|
6
|
+
port?: number;
|
|
7
|
+
};
|
|
8
|
+
export type ServerContext = {
|
|
9
|
+
repoRoot: string;
|
|
10
|
+
issueStore: IssueStore;
|
|
11
|
+
forumStore: ForumStore;
|
|
12
|
+
eventLog: EventLog;
|
|
13
|
+
};
|
|
14
|
+
export declare function createContext(repoRoot: string): ServerContext;
|
|
15
|
+
export declare function createServer(options?: ServerOptions): {
|
|
16
|
+
port: number;
|
|
17
|
+
fetch: (request: Request) => Promise<Response>;
|
|
18
|
+
hostname: string;
|
|
19
|
+
};
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { fsEventLogFromRepoRoot, FsJsonlStore, getStorePaths } from "@femtomc/mu-core/node";
|
|
2
|
+
import { IssueStore } from "@femtomc/mu-issue";
|
|
3
|
+
import { ForumStore } from "@femtomc/mu-forum";
|
|
4
|
+
import { issueRoutes } from "./api/issues.js";
|
|
5
|
+
import { forumRoutes } from "./api/forum.js";
|
|
6
|
+
export function createContext(repoRoot) {
|
|
7
|
+
const paths = getStorePaths(repoRoot);
|
|
8
|
+
const eventLog = fsEventLogFromRepoRoot(repoRoot);
|
|
9
|
+
const issueStore = new IssueStore(new FsJsonlStore(paths.issuesPath), { events: eventLog });
|
|
10
|
+
const forumStore = new ForumStore(new FsJsonlStore(paths.forumPath), { events: eventLog });
|
|
11
|
+
return { repoRoot, issueStore, forumStore, eventLog };
|
|
12
|
+
}
|
|
13
|
+
export function createServer(options = {}) {
|
|
14
|
+
const repoRoot = options.repoRoot || process.cwd();
|
|
15
|
+
const context = createContext(repoRoot);
|
|
16
|
+
const handleRequest = async (request) => {
|
|
17
|
+
const url = new URL(request.url);
|
|
18
|
+
const path = url.pathname;
|
|
19
|
+
// CORS headers for development
|
|
20
|
+
const headers = new Headers({
|
|
21
|
+
"Access-Control-Allow-Origin": "*",
|
|
22
|
+
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
23
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
24
|
+
});
|
|
25
|
+
// Handle preflight requests
|
|
26
|
+
if (request.method === "OPTIONS") {
|
|
27
|
+
return new Response(null, { status: 204, headers });
|
|
28
|
+
}
|
|
29
|
+
// Health check
|
|
30
|
+
if (path === "/healthz" || path === "/health") {
|
|
31
|
+
return new Response("ok", { status: 200, headers });
|
|
32
|
+
}
|
|
33
|
+
// Status endpoint
|
|
34
|
+
if (path === "/api/status") {
|
|
35
|
+
const issues = await context.issueStore.list();
|
|
36
|
+
const openIssues = issues.filter(i => i.status === "open");
|
|
37
|
+
const readyIssues = await context.issueStore.ready();
|
|
38
|
+
return Response.json({
|
|
39
|
+
repo_root: context.repoRoot,
|
|
40
|
+
open_count: openIssues.length,
|
|
41
|
+
ready_count: readyIssues.length
|
|
42
|
+
}, { headers });
|
|
43
|
+
}
|
|
44
|
+
// Issue routes
|
|
45
|
+
if (path.startsWith("/api/issues")) {
|
|
46
|
+
const response = await issueRoutes(request, context);
|
|
47
|
+
// Add CORS headers to the response
|
|
48
|
+
headers.forEach((value, key) => response.headers.set(key, value));
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
// Forum routes
|
|
52
|
+
if (path.startsWith("/api/forum")) {
|
|
53
|
+
const response = await forumRoutes(request, context);
|
|
54
|
+
// Add CORS headers to the response
|
|
55
|
+
headers.forEach((value, key) => response.headers.set(key, value));
|
|
56
|
+
return response;
|
|
57
|
+
}
|
|
58
|
+
return new Response("Not Found", { status: 404, headers });
|
|
59
|
+
};
|
|
60
|
+
const server = {
|
|
61
|
+
port: options.port || 3000,
|
|
62
|
+
fetch: handleRequest,
|
|
63
|
+
hostname: "0.0.0.0",
|
|
64
|
+
};
|
|
65
|
+
return server;
|
|
66
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@femtomc/mu-server",
|
|
3
|
+
"version": "26.2.19",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mu-server": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/**"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.build.json",
|
|
21
|
+
"test": "bun test",
|
|
22
|
+
"start": "bun run dist/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@femtomc/mu-core": "26.2.19",
|
|
26
|
+
"@femtomc/mu-issue": "26.2.19",
|
|
27
|
+
"@femtomc/mu-forum": "26.2.19"
|
|
28
|
+
}
|
|
29
|
+
}
|