@versdotsh/reef 0.1.2

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 (83) hide show
  1. package/.github/workflows/test.yml +47 -0
  2. package/README.md +257 -0
  3. package/bun.lock +587 -0
  4. package/examples/services/board/board.test.ts +215 -0
  5. package/examples/services/board/index.ts +155 -0
  6. package/examples/services/board/routes.ts +335 -0
  7. package/examples/services/board/store.ts +329 -0
  8. package/examples/services/board/tools.ts +214 -0
  9. package/examples/services/commits/commits.test.ts +74 -0
  10. package/examples/services/commits/index.ts +14 -0
  11. package/examples/services/commits/routes.ts +43 -0
  12. package/examples/services/commits/store.ts +114 -0
  13. package/examples/services/feed/behaviors.ts +23 -0
  14. package/examples/services/feed/feed.test.ts +101 -0
  15. package/examples/services/feed/index.ts +117 -0
  16. package/examples/services/feed/routes.ts +224 -0
  17. package/examples/services/feed/store.ts +194 -0
  18. package/examples/services/feed/tools.ts +83 -0
  19. package/examples/services/journal/index.ts +15 -0
  20. package/examples/services/journal/journal.test.ts +57 -0
  21. package/examples/services/journal/routes.ts +45 -0
  22. package/examples/services/journal/store.ts +119 -0
  23. package/examples/services/journal/tools.ts +32 -0
  24. package/examples/services/log/index.ts +15 -0
  25. package/examples/services/log/log.test.ts +70 -0
  26. package/examples/services/log/routes.ts +44 -0
  27. package/examples/services/log/store.ts +105 -0
  28. package/examples/services/log/tools.ts +57 -0
  29. package/examples/services/registry/behaviors.ts +128 -0
  30. package/examples/services/registry/index.ts +37 -0
  31. package/examples/services/registry/registry.test.ts +135 -0
  32. package/examples/services/registry/routes.ts +76 -0
  33. package/examples/services/registry/store.ts +224 -0
  34. package/examples/services/registry/tools.ts +116 -0
  35. package/examples/services/reports/index.ts +14 -0
  36. package/examples/services/reports/reports.test.ts +75 -0
  37. package/examples/services/reports/routes.ts +42 -0
  38. package/examples/services/reports/store.ts +110 -0
  39. package/examples/services/ui/auth.ts +61 -0
  40. package/examples/services/ui/index.ts +16 -0
  41. package/examples/services/ui/routes.ts +160 -0
  42. package/examples/services/ui/static/app.js +369 -0
  43. package/examples/services/ui/static/index.html +42 -0
  44. package/examples/services/ui/static/style.css +157 -0
  45. package/examples/services/usage/behaviors.ts +166 -0
  46. package/examples/services/usage/index.ts +19 -0
  47. package/examples/services/usage/routes.ts +53 -0
  48. package/examples/services/usage/store.ts +341 -0
  49. package/examples/services/usage/tools.ts +75 -0
  50. package/examples/services/usage/usage.test.ts +91 -0
  51. package/package.json +29 -0
  52. package/services/agent/index.ts +465 -0
  53. package/services/board/index.ts +155 -0
  54. package/services/board/routes.ts +335 -0
  55. package/services/board/store.ts +329 -0
  56. package/services/board/tools.ts +214 -0
  57. package/services/docs/index.ts +391 -0
  58. package/services/feed/behaviors.ts +23 -0
  59. package/services/feed/index.ts +117 -0
  60. package/services/feed/routes.ts +224 -0
  61. package/services/feed/store.ts +194 -0
  62. package/services/feed/tools.ts +83 -0
  63. package/services/installer/index.ts +574 -0
  64. package/services/services/index.ts +165 -0
  65. package/services/ui/auth.ts +61 -0
  66. package/services/ui/index.ts +16 -0
  67. package/services/ui/routes.ts +160 -0
  68. package/services/ui/static/app.js +369 -0
  69. package/services/ui/static/index.html +42 -0
  70. package/services/ui/static/style.css +157 -0
  71. package/skills/create-service/SKILL.md +698 -0
  72. package/src/core/auth.ts +28 -0
  73. package/src/core/client.ts +99 -0
  74. package/src/core/discover.ts +152 -0
  75. package/src/core/events.ts +44 -0
  76. package/src/core/extension.ts +66 -0
  77. package/src/core/server.ts +262 -0
  78. package/src/core/testing.ts +155 -0
  79. package/src/core/types.ts +194 -0
  80. package/src/extension.ts +16 -0
  81. package/src/main.ts +11 -0
  82. package/tests/server.test.ts +1338 -0
  83. package/tsconfig.json +29 -0
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Reports store — markdown reports with tagging.
3
+ */
4
+
5
+ import { ulid } from "ulid";
6
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
7
+ import { dirname } from "node:path";
8
+
9
+ export interface Report {
10
+ id: string;
11
+ title: string;
12
+ author: string;
13
+ content: string;
14
+ tags: string[];
15
+ createdAt: string;
16
+ updatedAt: string;
17
+ }
18
+
19
+ export interface CreateReportInput {
20
+ title: string;
21
+ author: string;
22
+ content: string;
23
+ tags?: string[];
24
+ }
25
+
26
+ export interface ReportFilters {
27
+ author?: string;
28
+ tag?: string;
29
+ }
30
+
31
+ export class ValidationError extends Error {
32
+ constructor(message: string) { super(message); this.name = "ValidationError"; }
33
+ }
34
+
35
+ export class ReportsStore {
36
+ private reports = new Map<string, Report>();
37
+ private filePath: string;
38
+ private writeTimer: ReturnType<typeof setTimeout> | null = null;
39
+
40
+ constructor(filePath = "data/reports.json") {
41
+ this.filePath = filePath;
42
+ this.load();
43
+ }
44
+
45
+ private load(): void {
46
+ try {
47
+ if (existsSync(this.filePath)) {
48
+ const raw = readFileSync(this.filePath, "utf-8");
49
+ const data = JSON.parse(raw);
50
+ if (Array.isArray(data.reports)) {
51
+ for (const r of data.reports) this.reports.set(r.id, r);
52
+ }
53
+ }
54
+ } catch {
55
+ this.reports = new Map();
56
+ }
57
+ }
58
+
59
+ private scheduleSave(): void {
60
+ if (this.writeTimer) return;
61
+ this.writeTimer = setTimeout(() => {
62
+ this.writeTimer = null;
63
+ this.flush();
64
+ }, 100);
65
+ }
66
+
67
+ flush(): void {
68
+ if (this.writeTimer) { clearTimeout(this.writeTimer); this.writeTimer = null; }
69
+ const dir = dirname(this.filePath);
70
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
71
+ writeFileSync(this.filePath, JSON.stringify({ reports: Array.from(this.reports.values()) }, null, 2));
72
+ }
73
+
74
+ create(input: CreateReportInput): Report {
75
+ if (!input.title?.trim()) throw new ValidationError("title is required");
76
+ if (!input.author?.trim()) throw new ValidationError("author is required");
77
+ if (!input.content?.trim()) throw new ValidationError("content is required");
78
+
79
+ const now = new Date().toISOString();
80
+ const report: Report = {
81
+ id: ulid(),
82
+ title: input.title.trim(),
83
+ author: input.author.trim(),
84
+ content: input.content,
85
+ tags: input.tags || [],
86
+ createdAt: now,
87
+ updatedAt: now,
88
+ };
89
+
90
+ this.reports.set(report.id, report);
91
+ this.scheduleSave();
92
+ return report;
93
+ }
94
+
95
+ get(id: string): Report | undefined { return this.reports.get(id); }
96
+
97
+ list(filters?: ReportFilters): Report[] {
98
+ let results = Array.from(this.reports.values());
99
+ if (filters?.author) results = results.filter((r) => r.author === filters.author);
100
+ if (filters?.tag) results = results.filter((r) => r.tags.includes(filters.tag!));
101
+ results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
102
+ return results;
103
+ }
104
+
105
+ delete(id: string): boolean {
106
+ const existed = this.reports.delete(id);
107
+ if (existed) this.scheduleSave();
108
+ return existed;
109
+ }
110
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Magic link auth for the web UI.
3
+ *
4
+ * Flow: agent generates a magic link via POST /auth/magic-link (bearer auth),
5
+ * user opens it in browser, gets a session cookie, UI proxies API calls.
6
+ */
7
+
8
+ import { ulid } from "ulid";
9
+
10
+ interface MagicLink {
11
+ token: string;
12
+ expiresAt: string;
13
+ }
14
+
15
+ interface Session {
16
+ id: string;
17
+ createdAt: string;
18
+ expiresAt: string;
19
+ }
20
+
21
+ const magicLinks = new Map<string, MagicLink>();
22
+ const sessions = new Map<string, Session>();
23
+
24
+ const LINK_TTL_MS = 5 * 60 * 1000; // 5 minutes
25
+ const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
26
+
27
+ export function createMagicLink(): MagicLink {
28
+ const token = ulid();
29
+ const expiresAt = new Date(Date.now() + LINK_TTL_MS).toISOString();
30
+ const link: MagicLink = { token, expiresAt };
31
+ magicLinks.set(token, link);
32
+ return link;
33
+ }
34
+
35
+ export function consumeMagicLink(token: string): boolean {
36
+ const link = magicLinks.get(token);
37
+ if (!link) return false;
38
+ magicLinks.delete(token);
39
+ return new Date(link.expiresAt).getTime() > Date.now();
40
+ }
41
+
42
+ export function createSession(): Session {
43
+ const session: Session = {
44
+ id: ulid(),
45
+ createdAt: new Date().toISOString(),
46
+ expiresAt: new Date(Date.now() + SESSION_TTL_MS).toISOString(),
47
+ };
48
+ sessions.set(session.id, session);
49
+ return session;
50
+ }
51
+
52
+ export function validateSession(sessionId: string | undefined): boolean {
53
+ if (!sessionId) return false;
54
+ const session = sessions.get(sessionId);
55
+ if (!session) return false;
56
+ if (new Date(session.expiresAt).getTime() < Date.now()) {
57
+ sessions.delete(sessionId);
58
+ return false;
59
+ }
60
+ return true;
61
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * UI service module — web dashboard with magic link auth and API proxy.
3
+ */
4
+
5
+ import type { ServiceModule } from "../src/core/types.js";
6
+ import { createRoutes } from "./routes.js";
7
+
8
+ const ui: ServiceModule = {
9
+ name: "ui",
10
+ description: "Web dashboard",
11
+ routes: createRoutes(),
12
+ mountAtRoot: true, // Serves at /ui/*, /auth/* — handles its own session auth
13
+ requiresAuth: false,
14
+ };
15
+
16
+ export default ui;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * UI routes — serves the dashboard, handles magic link auth, proxies API calls.
3
+ */
4
+
5
+ import { Hono } from "hono";
6
+ import { createMagicLink, consumeMagicLink, createSession, validateSession } from "./auth.js";
7
+ import { readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ const AUTH_TOKEN = process.env.VERS_AUTH_TOKEN || "test-token";
11
+
12
+ function getStaticDir(): string {
13
+ return join(import.meta.dir, "static");
14
+ }
15
+
16
+ function getSessionId(c: any): string | undefined {
17
+ const cookie = c.req.header("cookie") || "";
18
+ const match = cookie.match(/(?:^|;\s*)session=([^;]+)/);
19
+ return match?.[1];
20
+ }
21
+
22
+ function hasBearerAuth(c: any): boolean {
23
+ const auth = c.req.header("authorization") || "";
24
+ return auth === `Bearer ${AUTH_TOKEN}`;
25
+ }
26
+
27
+ export function createRoutes(): Hono {
28
+ const routes = new Hono();
29
+
30
+ // --- Auth ---
31
+
32
+ // Generate magic link (requires bearer auth)
33
+ routes.post("/auth/magic-link", (c) => {
34
+ if (!hasBearerAuth(c)) return c.json({ error: "Unauthorized" }, 401);
35
+
36
+ const link = createMagicLink();
37
+ const host = c.req.header("host") || "localhost:3000";
38
+ const proto = c.req.header("x-forwarded-proto") || "https";
39
+ const url = `${proto}://${host}/ui/login?token=${link.token}`;
40
+ return c.json({ url, expiresAt: link.expiresAt });
41
+ });
42
+
43
+ // Login page / magic link consumer
44
+ routes.get("/ui/login", (c) => {
45
+ const token = c.req.query("token");
46
+
47
+ if (token) {
48
+ const valid = consumeMagicLink(token);
49
+ if (valid) {
50
+ const session = createSession();
51
+ return c.html(
52
+ `<html><head><meta http-equiv="refresh" content="0;url=/ui/"></head></html>`,
53
+ 200,
54
+ { "Set-Cookie": `session=${session.id}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400` },
55
+ );
56
+ }
57
+ return c.html(`
58
+ <html><body style="background:#0a0a0a;color:#f55;font-family:monospace;padding:2em">
59
+ <h2>Invalid or expired link</h2>
60
+ <p>Request a new magic link from the API.</p>
61
+ </body></html>
62
+ `, 401);
63
+ }
64
+
65
+ return c.html(`
66
+ <html><body style="background:#0a0a0a;color:#888;font-family:monospace;padding:2em">
67
+ <h2>Fleet Services Dashboard</h2>
68
+ <p>Access requires a magic link. Generate one via:</p>
69
+ <pre style="color:#4f9">POST /auth/magic-link</pre>
70
+ </body></html>
71
+ `);
72
+ });
73
+
74
+ // --- Session-protected UI routes ---
75
+
76
+ routes.use("/ui/*", async (c, next) => {
77
+ const path = new URL(c.req.url).pathname;
78
+ if (path === "/ui/login" || path.startsWith("/ui/static/")) return next();
79
+
80
+ // In dev mode (no auth token set), skip session check
81
+ if (!process.env.VERS_AUTH_TOKEN) return next();
82
+
83
+ const sessionId = getSessionId(c);
84
+ if (!validateSession(sessionId)) return c.redirect("/ui/login");
85
+ return next();
86
+ });
87
+
88
+ // Dashboard
89
+ routes.get("/ui/", (c) => {
90
+ try {
91
+ const html = readFileSync(join(getStaticDir(), "index.html"), "utf-8");
92
+ return c.html(html);
93
+ } catch {
94
+ return c.text("Dashboard files not found", 500);
95
+ }
96
+ });
97
+
98
+ // Static files
99
+ routes.get("/ui/static/:file", (c) => {
100
+ const file = c.req.param("file");
101
+ if (file.includes("..") || file.includes("/")) return c.text("Not found", 404);
102
+
103
+ try {
104
+ const content = readFileSync(join(getStaticDir(), file), "utf-8");
105
+ const ext = file.split(".").pop();
106
+ const contentType =
107
+ ext === "css" ? "text/css" :
108
+ ext === "js" ? "application/javascript" :
109
+ "text/plain";
110
+ return c.body(content, 200, { "Content-Type": contentType });
111
+ } catch {
112
+ return c.text("Not found", 404);
113
+ }
114
+ });
115
+
116
+ // --- API proxy (injects bearer token so browser never needs it) ---
117
+
118
+ routes.all("/ui/api/*", async (c) => {
119
+ const url = new URL(c.req.url);
120
+ const apiPath = url.pathname.replace(/^\/ui\/api/, "");
121
+ const queryString = url.search;
122
+
123
+ const port = process.env.PORT || "3000";
124
+ const internalUrl = `http://127.0.0.1:${port}${apiPath}${queryString}`;
125
+
126
+ const headers: Record<string, string> = {
127
+ Authorization: `Bearer ${AUTH_TOKEN}`,
128
+ };
129
+ const contentType = c.req.header("content-type");
130
+ if (contentType) headers["Content-Type"] = contentType;
131
+
132
+ const method = c.req.method;
133
+ const body = method !== "GET" && method !== "HEAD" ? await c.req.text() : undefined;
134
+
135
+ try {
136
+ const resp = await fetch(internalUrl, { method, headers, body });
137
+
138
+ // SSE passthrough
139
+ if (resp.headers.get("content-type")?.includes("text/event-stream")) {
140
+ return new Response(resp.body, {
141
+ status: resp.status,
142
+ headers: {
143
+ "Content-Type": "text/event-stream",
144
+ "Cache-Control": "no-cache",
145
+ Connection: "keep-alive",
146
+ },
147
+ });
148
+ }
149
+
150
+ const text = await resp.text();
151
+ return c.body(text, resp.status as any, {
152
+ "Content-Type": resp.headers.get("content-type") || "application/json",
153
+ });
154
+ } catch (e) {
155
+ return c.json({ error: "Proxy error", details: String(e) }, 502);
156
+ }
157
+ });
158
+
159
+ return routes;
160
+ }