@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 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,2 @@
1
+ import type { ServerContext } from "../server.js";
2
+ export declare function forumRoutes(request: Request, context: ServerContext): Promise<Response>;
@@ -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,2 @@
1
+ import type { ServerContext } from "../server.js";
2
+ export declare function issueRoutes(request: Request, context: ServerContext): Promise<Response>;
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
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`);
@@ -0,0 +1,2 @@
1
+ export type { ServerOptions, ServerContext } from "./server.js";
2
+ export { createServer, createContext } from "./server.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createServer, createContext } from "./server.js";
@@ -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
+ }