@femtomc/mu-server 26.2.25 → 26.2.26

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/dist/server.js CHANGED
@@ -1,8 +1,25 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { extname, join, resolve } from "node:path";
1
4
  import { fsEventLogFromRepoRoot, FsJsonlStore, getStorePaths } from "@femtomc/mu-core/node";
2
5
  import { IssueStore } from "@femtomc/mu-issue";
3
6
  import { ForumStore } from "@femtomc/mu-forum";
4
7
  import { issueRoutes } from "./api/issues.js";
5
8
  import { forumRoutes } from "./api/forum.js";
9
+ const MIME_TYPES = {
10
+ ".html": "text/html; charset=utf-8",
11
+ ".js": "text/javascript; charset=utf-8",
12
+ ".css": "text/css; charset=utf-8",
13
+ ".json": "application/json",
14
+ ".png": "image/png",
15
+ ".jpg": "image/jpeg",
16
+ ".svg": "image/svg+xml",
17
+ ".ico": "image/x-icon",
18
+ ".woff": "font/woff",
19
+ ".woff2": "font/woff2",
20
+ };
21
+ // Resolve public/ dir relative to this file (works in npm global installs)
22
+ const PUBLIC_DIR = join(new URL(".", import.meta.url).pathname, "..", "public");
6
23
  export function createContext(repoRoot) {
7
24
  const paths = getStorePaths(repoRoot);
8
25
  const eventLog = fsEventLogFromRepoRoot(repoRoot);
@@ -55,6 +72,28 @@ export function createServer(options = {}) {
55
72
  headers.forEach((value, key) => response.headers.set(key, value));
56
73
  return response;
57
74
  }
75
+ // Static file serving (bundled web UI)
76
+ if (existsSync(PUBLIC_DIR)) {
77
+ // Try to serve the exact file (with path traversal protection)
78
+ const filePath = resolve(PUBLIC_DIR, `.${path === "/" ? "/index.html" : path}`);
79
+ if (!filePath.startsWith(PUBLIC_DIR)) {
80
+ return new Response("Forbidden", { status: 403, headers });
81
+ }
82
+ if (existsSync(filePath)) {
83
+ const ext = extname(filePath);
84
+ const mime = MIME_TYPES[ext] ?? "application/octet-stream";
85
+ const body = await readFile(filePath);
86
+ headers.set("Content-Type", mime);
87
+ return new Response(body, { status: 200, headers });
88
+ }
89
+ // SPA fallback: serve index.html for non-API, non-file paths
90
+ const indexPath = join(PUBLIC_DIR, "index.html");
91
+ if (existsSync(indexPath)) {
92
+ const body = await readFile(indexPath);
93
+ headers.set("Content-Type", "text/html; charset=utf-8");
94
+ return new Response(body, { status: 200, headers });
95
+ }
96
+ }
58
97
  return new Response("Not Found", { status: 404, headers });
59
98
  };
60
99
  const server = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.25",
3
+ "version": "26.2.26",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -14,7 +14,8 @@
14
14
  }
15
15
  },
16
16
  "files": [
17
- "dist/**"
17
+ "dist/**",
18
+ "public/**"
18
19
  ],
19
20
  "scripts": {
20
21
  "build": "tsc -p tsconfig.build.json",
@@ -22,8 +23,8 @@
22
23
  "start": "bun run dist/cli.js"
23
24
  },
24
25
  "dependencies": {
25
- "@femtomc/mu-core": "26.2.25",
26
- "@femtomc/mu-issue": "26.2.25",
27
- "@femtomc/mu-forum": "26.2.25"
26
+ "@femtomc/mu-core": "26.2.26",
27
+ "@femtomc/mu-issue": "26.2.26",
28
+ "@femtomc/mu-forum": "26.2.26"
28
29
  }
29
30
  }
@@ -0,0 +1,85 @@
1
+ (function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))a(r);new MutationObserver(r=>{for(const o of r)if(o.type==="childList")for(const c of o.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&a(c)}).observe(document,{childList:!0,subtree:!0});function s(r){const o={};return r.integrity&&(o.integrity=r.integrity),r.referrerPolicy&&(o.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?o.credentials="include":r.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function a(r){if(r.ep)return;r.ep=!0;const o=s(r);fetch(r.href,o)}})();const T="";class g extends Error{constructor(e,s){super(s),this.status=e,this.name="ApiError"}}async function n(t,e){const s=await fetch(`${T}${t}`,{...e,headers:{"Content-Type":"application/json",...e==null?void 0:e.headers}});if(!s.ok){const a=await s.text();throw new g(s.status,a||`HTTP ${s.status}`)}return s.json()}const d={async getStatus(){return n("/api/status")},async listIssues(t){const e=new URLSearchParams;t!=null&&t.status&&e.set("status",t.status),t!=null&&t.tag&&e.set("tag",t.tag);const s=e.toString();return n(`/api/issues${s?`?${s}`:""}`)},async getIssue(t){return n(`/api/issues/${t}`)},async createIssue(t){return n("/api/issues",{method:"POST",body:JSON.stringify(t)})},async updateIssue(t,e){return n(`/api/issues/${t}`,{method:"PATCH",body:JSON.stringify(e)})},async claimIssue(t){return n(`/api/issues/${t}/claim`,{method:"POST",body:JSON.stringify({})})},async closeIssue(t,e){return n(`/api/issues/${t}/close`,{method:"POST",body:JSON.stringify({outcome:e})})},async getReadyIssues(t){const e=t?`?root=${encodeURIComponent(t)}`:"";return n(`/api/issues/ready${e}`)},async postMessage(t,e,s){return n("/api/forum/post",{method:"POST",body:JSON.stringify({topic:t,body:e,author:s})})},async readMessages(t,e){const s=new URLSearchParams({topic:t});return e&&s.set("limit",String(e)),n(`/api/forum/read?${s}`)},async listTopics(t,e){const s=new URLSearchParams;t&&s.set("prefix",t),e&&s.set("limit",String(e));const a=s.toString();return n(`/api/forum/topics${a?`?${a}`:""}`)}},h="mu-web:last_topic";function S(){return typeof localStorage<"u"?localStorage:null}function w(){try{const t=S();if(!t)return null;const e=String(t.getItem(h)??"").trim();return e.length>0?e:null}catch{return null}}function x(t){try{const e=S();if(!e)return;const s=t.trim();if(!s)return;e.setItem(h,s)}catch{}}const L=document.querySelector("#app");if(!L)throw new Error("missing #app");const C=L;C.innerHTML=`
2
+ <div class="container">
3
+ <h1>mu</h1>
4
+ <div class="row muted">
5
+ <span class="pill" data-testid="status-pill">Connecting...</span>
6
+ <button data-testid="refresh">Refresh</button>
7
+ <span class="muted" data-testid="repo-root"></span>
8
+ </div>
9
+
10
+ <div class="grid">
11
+ <div class="card">
12
+ <h2>Issues</h2>
13
+ <div class="row">
14
+ <input data-testid="issue-title" placeholder="Issue title" />
15
+ <button data-testid="create-issue">Create</button>
16
+ </div>
17
+ <p class="muted">
18
+ All issues: <span data-testid="issues-count">0</span>
19
+ Ready leaves: <span data-testid="ready-count">0</span>
20
+ </p>
21
+ <div class="grid" style="grid-template-columns: 1fr; gap: 10px;">
22
+ <div>
23
+ <div class="muted">Issues</div>
24
+ <pre data-testid="issues-json">[]</pre>
25
+ </div>
26
+ <div>
27
+ <div class="muted">Ready Leaves</div>
28
+ <pre data-testid="ready-json">[]</pre>
29
+ </div>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="card">
34
+ <h2>Forum</h2>
35
+
36
+ <div class="muted">Post</div>
37
+ <div class="row">
38
+ <input data-testid="forum-topic" placeholder="Topic (e.g. issue:mu-123)" />
39
+ <input data-testid="forum-author" placeholder="Author" value="worker" />
40
+ </div>
41
+ <textarea data-testid="forum-body" placeholder="Message body"></textarea>
42
+ <div class="row">
43
+ <button data-testid="forum-post">Post</button>
44
+ </div>
45
+
46
+ <div style="height: 10px;"></div>
47
+
48
+ <div class="muted">Read</div>
49
+ <div class="row">
50
+ <input data-testid="read-topic" placeholder="Topic to read" />
51
+ <button data-testid="forum-read">Read</button>
52
+ </div>
53
+
54
+ <div style="height: 10px;"></div>
55
+
56
+ <div class="muted">Topics</div>
57
+ <div class="row">
58
+ <input data-testid="topics-prefix" placeholder="Prefix (optional)" />
59
+ <button data-testid="topics-refresh">List</button>
60
+ </div>
61
+
62
+ <p class="muted">
63
+ Topics: <span data-testid="topics-count">0</span>
64
+ </p>
65
+
66
+ <div class="grid" style="grid-template-columns: 1fr; gap: 10px;">
67
+ <div>
68
+ <div class="muted">Topics</div>
69
+ <pre data-testid="topics-json">[]</pre>
70
+ </div>
71
+ <div>
72
+ <div class="muted">Messages</div>
73
+ <pre data-testid="messages-json">[]</pre>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+
79
+ <div class="card" style="margin-top: 16px;">
80
+ <div class="muted">Errors</div>
81
+ <pre data-testid="errors"></pre>
82
+ </div>
83
+ </div>
84
+ `;function i(t){const e=C.querySelector(`[data-testid="${t}"]`);if(!e)throw new Error(`missing [data-testid=${JSON.stringify(t)}]`);return e}const u=i("status-pill"),y=i("repo-root"),v=i("errors"),m=w();m&&(i("read-topic").value=m);function f(t){if(!t){v.textContent="";return}if(t instanceof g){v.textContent=`API Error (${t.status}): ${t.message}`;return}if(t instanceof Error){v.textContent=`${t.name}: ${t.message}
85
+ ${t.stack??""}`.trim();return}v.textContent=String(t)}async function b(){try{const t=await d.getStatus();return u.textContent=`Connected to ${window.location.host}`,u.classList.add("success"),u.classList.remove("error"),y.textContent=t.repo_root||"",!0}catch(t){return u.textContent="Connection failed",u.classList.add("error"),u.classList.remove("success"),y.textContent="",f(t),!1}}async function l(t={}){try{f(null);const[e,s,a]=await Promise.all([d.listIssues(),d.getReadyIssues(),d.getStatus()]),r=i("topics-prefix").value.trim()||void 0,o=await d.listTopics(r);i("issues-count").textContent=String(e.length),i("ready-count").textContent=String(s.length),i("topics-count").textContent=String(o.length),i("issues-json").textContent=JSON.stringify(e,null,2),i("ready-json").textContent=JSON.stringify(s,null,2),i("topics-json").textContent=JSON.stringify(o,null,2);let c=t.readTopic??i("read-topic").value.trim();if(!c){const p=w();p&&(c=p,i("read-topic").value=p)}if(c){const p=await d.readMessages(c,50);i("messages-json").textContent=JSON.stringify(p,null,2)}else i("messages-json").textContent="[]"}catch(e){f(e)}}i("refresh").addEventListener("click",()=>{l()});i("topics-refresh").addEventListener("click",()=>{l()});i("forum-read").addEventListener("click",()=>{const t=i("read-topic").value.trim();x(t),l({readTopic:t})});i("create-issue").addEventListener("click",()=>{const t=i("issue-title"),e=t.value.trim();if(!e){f(new Error("issue title required"));return}(async()=>(await d.createIssue({title:e,tags:["node:agent"]}),t.value="",await l()))()});i("forum-post").addEventListener("click",()=>{const t=i("forum-topic").value.trim(),e=i("forum-author").value.trim()||"system",s=i("forum-body").value;if(!t){f(new Error("topic required"));return}(async()=>(await d.postMessage(t,s,e),x(t),i("forum-body").value="",i("read-topic").value=t,await l({readTopic:t})))()});(async()=>await b()&&await l())();
@@ -0,0 +1 @@
1
+ :root{color-scheme:light;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;line-height:1.4}body{margin:0;padding:24px;background:#0b0f14;color:#e8edf2}a{color:inherit}.container{max-width:980px;margin:0 auto}h1{font-size:20px;font-weight:700;margin:0 0 12px}.muted{color:#a6b4c2}.grid{display:grid;grid-template-columns:1fr;gap:16px;margin-top:16px}@media (min-width: 900px){.grid{grid-template-columns:1fr 1fr}}.card{border:1px solid #1f2a35;background:#0e141b;border-radius:10px;padding:14px}.row{display:flex;flex-wrap:wrap;gap:8px;align-items:center}input,textarea{width:100%;box-sizing:border-box;background:#0b0f14;color:#e8edf2;border:1px solid #233041;border-radius:8px;padding:10px}textarea{min-height:90px;resize:vertical}button{cursor:pointer;border:1px solid #2b3a4c;background:#111a24;color:#e8edf2;border-radius:8px;padding:10px 12px}button:hover{background:#132030}pre{margin:0;padding:10px;background:#0b0f14;border:1px solid #233041;border-radius:8px;overflow:auto}.pill{display:inline-block;font-size:12px;padding:3px 8px;border-radius:999px;border:1px solid #2b3a4c;color:#a6b4c2}.pill.success{background:#0d3b16;border-color:#1e5c2f;color:#5cb85c}.pill.error{background:#3b0d16;border-color:#5c1e2f;color:#d9534f}
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>mu</title>
7
+ <script type="module" crossorigin src="/assets/index-0FGFtKeu.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-D_8anM-D.css">
9
+ </head>
10
+ <body>
11
+ <div id="app"></div>
12
+ </body>
13
+ </html>
14
+