@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,155 @@
1
+ /**
2
+ * Test helpers for service modules.
3
+ *
4
+ * Usage:
5
+ * import { createTestHarness } from "../src/core/testing.js";
6
+ *
7
+ * const t = await createTestHarness({
8
+ * services: [import("../services/board/index.js")],
9
+ * });
10
+ *
11
+ * const res = await t.fetch("/board/tasks", { auth: true });
12
+ * expect(res.status).toBe(200);
13
+ *
14
+ * t.cleanup();
15
+ */
16
+
17
+ import { mkdirSync, rmSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { createServer } from "./server.js";
20
+ import type { ServiceModule } from "./types.js";
21
+
22
+ export interface TestHarnessOptions {
23
+ /** Service modules to load. Can be module objects or dynamic imports. */
24
+ services: Array<ServiceModule | Promise<{ default: ServiceModule }>>;
25
+ /** Auth token (default: "test-token") */
26
+ authToken?: string;
27
+ /** Temp data dir for stores (default: auto-created, auto-cleaned) */
28
+ dataDir?: string;
29
+ }
30
+
31
+ export interface TestHarness {
32
+ /** The Hono app — call app.fetch() to make requests */
33
+ app: { fetch: (req: Request) => Promise<Response> };
34
+ /** Auth token for this test */
35
+ authToken: string;
36
+ /** Temp directory for data files */
37
+ dataDir: string;
38
+
39
+ /** Make a request with optional auth and body */
40
+ fetch(
41
+ path: string,
42
+ opts?: {
43
+ method?: string;
44
+ body?: unknown;
45
+ auth?: boolean | string;
46
+ headers?: Record<string, string>;
47
+ },
48
+ ): Promise<Response>;
49
+
50
+ /** Make a request and parse JSON response */
51
+ json<T = unknown>(
52
+ path: string,
53
+ opts?: {
54
+ method?: string;
55
+ body?: unknown;
56
+ auth?: boolean | string;
57
+ },
58
+ ): Promise<{ status: number; data: T }>;
59
+
60
+ /** Clean up temp directories */
61
+ cleanup(): void;
62
+ }
63
+
64
+ export async function createTestHarness(
65
+ options: TestHarnessOptions,
66
+ ): Promise<TestHarness> {
67
+ const authToken = options.authToken ?? "test-token";
68
+ const dataDir =
69
+ options.dataDir ??
70
+ join(import.meta.dir, "..", "..", "tests", `.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
71
+
72
+ // Set up env
73
+ const prevToken = process.env.VERS_AUTH_TOKEN;
74
+ process.env.VERS_AUTH_TOKEN = authToken;
75
+
76
+ // Create temp dirs
77
+ mkdirSync(dataDir, { recursive: true });
78
+
79
+ // Resolve modules
80
+ const modules: ServiceModule[] = [];
81
+ for (const mod of options.services) {
82
+ if ("name" in mod && "routes" in mod) {
83
+ // Already a ServiceModule
84
+ modules.push(mod as ServiceModule);
85
+ } else {
86
+ // Dynamic import — resolve the promise and get default export
87
+ const resolved = await (mod as Promise<{ default: ServiceModule }>);
88
+ modules.push(resolved.default);
89
+ }
90
+ }
91
+
92
+ // Create server with the modules directly (no services dir needed)
93
+ const emptyDir = join(dataDir, "_empty_services");
94
+ mkdirSync(emptyDir, { recursive: true });
95
+ const { app } = await createServer({
96
+ modules,
97
+ servicesDir: emptyDir,
98
+ });
99
+
100
+ function makeFetch(
101
+ path: string,
102
+ opts: {
103
+ method?: string;
104
+ body?: unknown;
105
+ auth?: boolean | string;
106
+ headers?: Record<string, string>;
107
+ } = {},
108
+ ): Promise<Response> {
109
+ const headers: Record<string, string> = { ...(opts.headers || {}) };
110
+ if (opts.body) headers["Content-Type"] = "application/json";
111
+ if (opts.auth === true) {
112
+ headers["Authorization"] = `Bearer ${authToken}`;
113
+ } else if (typeof opts.auth === "string") {
114
+ headers["Authorization"] = `Bearer ${opts.auth}`;
115
+ }
116
+
117
+ return app.fetch(
118
+ new Request(`http://localhost${path}`, {
119
+ method: opts.method ?? "GET",
120
+ headers,
121
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
122
+ }),
123
+ );
124
+ }
125
+
126
+ async function makeJson<T = unknown>(
127
+ path: string,
128
+ opts: {
129
+ method?: string;
130
+ body?: unknown;
131
+ auth?: boolean | string;
132
+ } = {},
133
+ ): Promise<{ status: number; data: T }> {
134
+ const res = await makeFetch(path, opts);
135
+ return { status: res.status, data: (await res.json()) as T };
136
+ }
137
+
138
+ function cleanup() {
139
+ rmSync(dataDir, { recursive: true, force: true });
140
+ if (prevToken !== undefined) {
141
+ process.env.VERS_AUTH_TOKEN = prevToken;
142
+ } else {
143
+ delete process.env.VERS_AUTH_TOKEN;
144
+ }
145
+ }
146
+
147
+ return {
148
+ app,
149
+ authToken,
150
+ dataDir,
151
+ fetch: makeFetch,
152
+ json: makeJson,
153
+ cleanup,
154
+ };
155
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Core types for the reef plugin system.
3
+ *
4
+ * A ServiceModule is the fundamental unit — it declares both server-side
5
+ * routes and client-side pi extension behavior in one place.
6
+ */
7
+
8
+ import type { Hono } from "hono";
9
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
10
+ import type { ServiceEventBus } from "./events.js";
11
+
12
+ // =============================================================================
13
+ // Tool result types (matching pi's expected shape)
14
+ // =============================================================================
15
+
16
+ export interface ToolContent {
17
+ type: "text";
18
+ text: string;
19
+ }
20
+
21
+ export interface ToolResult {
22
+ content: ToolContent[];
23
+ details?: Record<string, unknown>;
24
+ isError?: boolean;
25
+ }
26
+
27
+ // =============================================================================
28
+ // FleetClient — injected into every service's client-side code
29
+ // =============================================================================
30
+
31
+ export interface FleetClient {
32
+ /** Make an authenticated API call to the fleet server */
33
+ api<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
34
+
35
+ /** Get the base URL, or null if not configured */
36
+ getBaseUrl(): string | null;
37
+
38
+ /** This agent's name (from VERS_AGENT_NAME or fallback) */
39
+ readonly agentName: string;
40
+
41
+ /** This agent's VM ID, if set */
42
+ readonly vmId: string | undefined;
43
+
44
+ /** This agent's role (from VERS_AGENT_ROLE or "worker") */
45
+ readonly agentRole: string;
46
+
47
+ /** Build a successful tool result */
48
+ ok(text: string, details?: Record<string, unknown>): ToolResult;
49
+
50
+ /** Build an error tool result */
51
+ err(text: string): ToolResult;
52
+
53
+ /** Build a "no URL configured" error result */
54
+ noUrl(): ToolResult;
55
+ }
56
+
57
+ // =============================================================================
58
+ // Widget contribution — services add lines to the composite status widget
59
+ // =============================================================================
60
+
61
+ export interface WidgetContribution {
62
+ /** Lines to contribute to the status widget */
63
+ getLines(client: FleetClient): Promise<string[]>;
64
+ }
65
+
66
+ // =============================================================================
67
+ // ServiceContext — passed to modules during server-side initialization
68
+ // =============================================================================
69
+
70
+ export interface ServiceContext {
71
+ /** Server-side event bus for inter-module communication */
72
+ events: ServiceEventBus;
73
+
74
+ /** Get another module's store by service name. Returns undefined if not found. */
75
+ getStore<T = unknown>(serviceName: string): T | undefined;
76
+
77
+ /** All currently loaded modules (read-only view). */
78
+ getModules(): ServiceModule[];
79
+
80
+ /** Get a loaded module by name. */
81
+ getModule(name: string): ServiceModule | undefined;
82
+
83
+ /**
84
+ * Load or reload a module from a directory under the services dir.
85
+ * Returns the module name and whether it was added or updated.
86
+ */
87
+ loadModule(dirName: string): Promise<{ name: string; action: "added" | "updated" }>;
88
+
89
+ /** Unload a module by name. Flushes and closes its store. */
90
+ unloadModule(name: string): Promise<void>;
91
+
92
+ /** The resolved services directory path. */
93
+ servicesDir: string;
94
+ }
95
+
96
+ // =============================================================================
97
+ // ServiceModule — the plugin interface
98
+ // =============================================================================
99
+
100
+ export interface ServiceModule {
101
+ /** Unique name, used as route prefix: /board, /feed, etc. */
102
+ name: string;
103
+
104
+ /** Human-readable description */
105
+ description?: string;
106
+
107
+ // --- Server side ---
108
+
109
+ /** Hono routes, mounted at /{name}/* (or root if mountAtRoot is true) */
110
+ routes?: Hono;
111
+
112
+ /** Mount routes at / instead of /{name}/. Used for UI, webhooks, etc. */
113
+ mountAtRoot?: boolean;
114
+
115
+ /** Whether routes need bearer auth. Default: true */
116
+ requiresAuth?: boolean;
117
+
118
+ /** Store handle for graceful shutdown (flush pending writes, close connections) */
119
+ store?: {
120
+ flush?(): void;
121
+ close?(): Promise<void>;
122
+ };
123
+
124
+ /**
125
+ * Server-side initialization hook. Called after all modules are loaded,
126
+ * so you can subscribe to events or look up other modules' stores.
127
+ */
128
+ init?(ctx: ServiceContext): void;
129
+
130
+ // --- Client side (pi extension) ---
131
+
132
+ /** Register tools the LLM can call */
133
+ registerTools?(pi: ExtensionAPI, client: FleetClient): void;
134
+
135
+ /** Register automatic behaviors (event handlers, timers) */
136
+ registerBehaviors?(pi: ExtensionAPI, client: FleetClient): void;
137
+
138
+ /** Contribute lines to the composite status widget */
139
+ widget?: WidgetContribution;
140
+
141
+ // --- Metadata ---
142
+
143
+ /** Services this module depends on (for load ordering) */
144
+ dependencies?: string[];
145
+
146
+ /**
147
+ * Route documentation. Keyed by "METHOD /path" (path relative to module root).
148
+ * Used by the docs service to generate API documentation.
149
+ *
150
+ * @example
151
+ * routeDocs: {
152
+ * "POST /tasks": {
153
+ * summary: "Create a task",
154
+ * body: {
155
+ * title: { type: "string", required: true, description: "Task title" },
156
+ * assignee: { type: "string", description: "Agent or user to assign to" },
157
+ * },
158
+ * response: "The created task object with generated ID and timestamps",
159
+ * },
160
+ * "GET /tasks": {
161
+ * summary: "List tasks with optional filters",
162
+ * query: {
163
+ * status: { type: "string", description: "open | in_progress | in_review | blocked | done" },
164
+ * },
165
+ * },
166
+ * }
167
+ */
168
+ routeDocs?: Record<string, RouteDocs>;
169
+ }
170
+
171
+ // =============================================================================
172
+ // Route documentation types
173
+ // =============================================================================
174
+
175
+ export interface ParamDoc {
176
+ type: string;
177
+ required?: boolean;
178
+ description?: string;
179
+ }
180
+
181
+ export interface RouteDocs {
182
+ /** Short description of what this endpoint does */
183
+ summary: string;
184
+ /** Longer explanation, usage notes, or examples */
185
+ detail?: string;
186
+ /** URL path parameters (e.g. :id) */
187
+ params?: Record<string, ParamDoc>;
188
+ /** Query string parameters */
189
+ query?: Record<string, ParamDoc>;
190
+ /** Request body fields (for POST/PATCH/PUT) */
191
+ body?: Record<string, ParamDoc>;
192
+ /** Description of the response */
193
+ response?: string;
194
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Pi extension entrypoint — discovers service modules and composes their
3
+ * client-side code into a single extension that agents install.
4
+ *
5
+ * This is the client half. The server half is src/main.ts.
6
+ */
7
+
8
+ import { createExtension } from "./core/extension.js";
9
+ import { discoverServiceModules, filterClientModules } from "./core/discover.js";
10
+ import { DEFAULT_SERVICES_DIR } from "./core/server.js";
11
+
12
+ const servicesDir = process.env.SERVICES_DIR ?? DEFAULT_SERVICES_DIR;
13
+ const allModules = await discoverServiceModules(servicesDir);
14
+ const clientModules = filterClientModules(allModules);
15
+
16
+ export default createExtension(clientModules);
package/src/main.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Server entrypoint — discovers service modules and starts the server.
3
+ *
4
+ * Configure via env vars:
5
+ * SERVICES_DIR — path to services directory (default: ./services)
6
+ * PORT — server port (default: 3000)
7
+ */
8
+
9
+ import { startServer } from "./core/server.js";
10
+
11
+ await startServer();