@sourcepress/server 0.1.0

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 (184) hide show
  1. package/.omc/state/agent-replay-31d84b63-606a-4368-b9e6-93fe4f5ae0f7.jsonl +2 -0
  2. package/.omc/state/last-tool-error.json +7 -0
  3. package/.omc/state/mission-state.json +53 -0
  4. package/.omc/state/subagent-tracking.json +17 -0
  5. package/.turbo/turbo-build.log +4 -0
  6. package/.turbo/turbo-test.log +26 -0
  7. package/dist/__tests__/app-integration.test.d.ts +2 -0
  8. package/dist/__tests__/app-integration.test.d.ts.map +1 -0
  9. package/dist/__tests__/app-integration.test.js +71 -0
  10. package/dist/__tests__/app-integration.test.js.map +1 -0
  11. package/dist/__tests__/approval.test.d.ts +2 -0
  12. package/dist/__tests__/approval.test.d.ts.map +1 -0
  13. package/dist/__tests__/approval.test.js +170 -0
  14. package/dist/__tests__/approval.test.js.map +1 -0
  15. package/dist/__tests__/content.test.d.ts +2 -0
  16. package/dist/__tests__/content.test.d.ts.map +1 -0
  17. package/dist/__tests__/content.test.js +187 -0
  18. package/dist/__tests__/content.test.js.map +1 -0
  19. package/dist/__tests__/engine.test.d.ts +2 -0
  20. package/dist/__tests__/engine.test.d.ts.map +1 -0
  21. package/dist/__tests__/engine.test.js +77 -0
  22. package/dist/__tests__/engine.test.js.map +1 -0
  23. package/dist/__tests__/eval.test.d.ts +2 -0
  24. package/dist/__tests__/eval.test.d.ts.map +1 -0
  25. package/dist/__tests__/eval.test.js +320 -0
  26. package/dist/__tests__/eval.test.js.map +1 -0
  27. package/dist/__tests__/graph.test.d.ts +2 -0
  28. package/dist/__tests__/graph.test.d.ts.map +1 -0
  29. package/dist/__tests__/graph.test.js +169 -0
  30. package/dist/__tests__/graph.test.js.map +1 -0
  31. package/dist/__tests__/health.test.d.ts +2 -0
  32. package/dist/__tests__/health.test.d.ts.map +1 -0
  33. package/dist/__tests__/health.test.js +56 -0
  34. package/dist/__tests__/health.test.js.map +1 -0
  35. package/dist/__tests__/import.test.d.ts +2 -0
  36. package/dist/__tests__/import.test.d.ts.map +1 -0
  37. package/dist/__tests__/import.test.js +138 -0
  38. package/dist/__tests__/import.test.js.map +1 -0
  39. package/dist/__tests__/intent.test.d.ts +2 -0
  40. package/dist/__tests__/intent.test.d.ts.map +1 -0
  41. package/dist/__tests__/intent.test.js +122 -0
  42. package/dist/__tests__/intent.test.js.map +1 -0
  43. package/dist/__tests__/jobs.test.d.ts +2 -0
  44. package/dist/__tests__/jobs.test.d.ts.map +1 -0
  45. package/dist/__tests__/jobs.test.js +96 -0
  46. package/dist/__tests__/jobs.test.js.map +1 -0
  47. package/dist/__tests__/knowledge.test.d.ts +2 -0
  48. package/dist/__tests__/knowledge.test.d.ts.map +1 -0
  49. package/dist/__tests__/knowledge.test.js +110 -0
  50. package/dist/__tests__/knowledge.test.js.map +1 -0
  51. package/dist/__tests__/media-routes.test.d.ts +2 -0
  52. package/dist/__tests__/media-routes.test.d.ts.map +1 -0
  53. package/dist/__tests__/media-routes.test.js +88 -0
  54. package/dist/__tests__/media-routes.test.js.map +1 -0
  55. package/dist/__tests__/schema.test.d.ts +2 -0
  56. package/dist/__tests__/schema.test.d.ts.map +1 -0
  57. package/dist/__tests__/schema.test.js +92 -0
  58. package/dist/__tests__/schema.test.js.map +1 -0
  59. package/dist/app.d.ts +7 -0
  60. package/dist/app.d.ts.map +1 -0
  61. package/dist/app.js +85 -0
  62. package/dist/app.js.map +1 -0
  63. package/dist/engine.d.ts +38 -0
  64. package/dist/engine.d.ts.map +1 -0
  65. package/dist/engine.js +106 -0
  66. package/dist/engine.js.map +1 -0
  67. package/dist/index.d.ts +8 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +9 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/middleware/auth.d.ts +17 -0
  72. package/dist/middleware/auth.d.ts.map +1 -0
  73. package/dist/middleware/auth.js +54 -0
  74. package/dist/middleware/auth.js.map +1 -0
  75. package/dist/middleware/cors.d.ts +2 -0
  76. package/dist/middleware/cors.d.ts.map +1 -0
  77. package/dist/middleware/cors.js +13 -0
  78. package/dist/middleware/cors.js.map +1 -0
  79. package/dist/middleware/error-handler.d.ts +24 -0
  80. package/dist/middleware/error-handler.d.ts.map +1 -0
  81. package/dist/middleware/error-handler.js +30 -0
  82. package/dist/middleware/error-handler.js.map +1 -0
  83. package/dist/middleware/index.d.ts +7 -0
  84. package/dist/middleware/index.d.ts.map +1 -0
  85. package/dist/middleware/index.js +5 -0
  86. package/dist/middleware/index.js.map +1 -0
  87. package/dist/middleware/rate-limit.d.ts +11 -0
  88. package/dist/middleware/rate-limit.d.ts.map +1 -0
  89. package/dist/middleware/rate-limit.js +26 -0
  90. package/dist/middleware/rate-limit.js.map +1 -0
  91. package/dist/middleware/route-error-handler.d.ts +12 -0
  92. package/dist/middleware/route-error-handler.d.ts.map +1 -0
  93. package/dist/middleware/route-error-handler.js +9 -0
  94. package/dist/middleware/route-error-handler.js.map +1 -0
  95. package/dist/routes/approval.d.ts +4 -0
  96. package/dist/routes/approval.d.ts.map +1 -0
  97. package/dist/routes/approval.js +70 -0
  98. package/dist/routes/approval.js.map +1 -0
  99. package/dist/routes/content.d.ts +4 -0
  100. package/dist/routes/content.d.ts.map +1 -0
  101. package/dist/routes/content.js +145 -0
  102. package/dist/routes/content.js.map +1 -0
  103. package/dist/routes/eval.d.ts +4 -0
  104. package/dist/routes/eval.d.ts.map +1 -0
  105. package/dist/routes/eval.js +178 -0
  106. package/dist/routes/eval.js.map +1 -0
  107. package/dist/routes/graph.d.ts +4 -0
  108. package/dist/routes/graph.d.ts.map +1 -0
  109. package/dist/routes/graph.js +90 -0
  110. package/dist/routes/graph.js.map +1 -0
  111. package/dist/routes/health.d.ts +13 -0
  112. package/dist/routes/health.d.ts.map +1 -0
  113. package/dist/routes/health.js +19 -0
  114. package/dist/routes/health.js.map +1 -0
  115. package/dist/routes/import.d.ts +4 -0
  116. package/dist/routes/import.d.ts.map +1 -0
  117. package/dist/routes/import.js +85 -0
  118. package/dist/routes/import.js.map +1 -0
  119. package/dist/routes/index.d.ts +12 -0
  120. package/dist/routes/index.d.ts.map +1 -0
  121. package/dist/routes/index.js +12 -0
  122. package/dist/routes/index.js.map +1 -0
  123. package/dist/routes/intent.d.ts +4 -0
  124. package/dist/routes/intent.d.ts.map +1 -0
  125. package/dist/routes/intent.js +80 -0
  126. package/dist/routes/intent.js.map +1 -0
  127. package/dist/routes/jobs.d.ts +4 -0
  128. package/dist/routes/jobs.d.ts.map +1 -0
  129. package/dist/routes/jobs.js +67 -0
  130. package/dist/routes/jobs.js.map +1 -0
  131. package/dist/routes/knowledge.d.ts +4 -0
  132. package/dist/routes/knowledge.d.ts.map +1 -0
  133. package/dist/routes/knowledge.js +48 -0
  134. package/dist/routes/knowledge.js.map +1 -0
  135. package/dist/routes/media.d.ts +4 -0
  136. package/dist/routes/media.d.ts.map +1 -0
  137. package/dist/routes/media.js +87 -0
  138. package/dist/routes/media.js.map +1 -0
  139. package/dist/routes/schema.d.ts +4 -0
  140. package/dist/routes/schema.d.ts.map +1 -0
  141. package/dist/routes/schema.js +54 -0
  142. package/dist/routes/schema.js.map +1 -0
  143. package/dist/standalone.d.ts +2 -0
  144. package/dist/standalone.d.ts.map +1 -0
  145. package/dist/standalone.js +115 -0
  146. package/dist/standalone.js.map +1 -0
  147. package/package.json +36 -0
  148. package/src/__tests__/app-integration.test.ts +80 -0
  149. package/src/__tests__/approval.test.ts +195 -0
  150. package/src/__tests__/content.test.ts +202 -0
  151. package/src/__tests__/engine.test.ts +86 -0
  152. package/src/__tests__/eval.test.ts +343 -0
  153. package/src/__tests__/graph.test.ts +182 -0
  154. package/src/__tests__/health.test.ts +68 -0
  155. package/src/__tests__/import.test.ts +148 -0
  156. package/src/__tests__/intent.test.ts +133 -0
  157. package/src/__tests__/jobs.test.ts +107 -0
  158. package/src/__tests__/knowledge.test.ts +121 -0
  159. package/src/__tests__/media-routes.test.ts +109 -0
  160. package/src/__tests__/schema.test.ts +100 -0
  161. package/src/app.ts +92 -0
  162. package/src/engine.ts +168 -0
  163. package/src/index.ts +31 -0
  164. package/src/middleware/auth.ts +66 -0
  165. package/src/middleware/cors.ts +15 -0
  166. package/src/middleware/error-handler.ts +42 -0
  167. package/src/middleware/index.ts +6 -0
  168. package/src/middleware/rate-limit.ts +27 -0
  169. package/src/middleware/route-error-handler.ts +13 -0
  170. package/src/routes/approval.ts +90 -0
  171. package/src/routes/content.ts +256 -0
  172. package/src/routes/eval.ts +262 -0
  173. package/src/routes/graph.ts +111 -0
  174. package/src/routes/health.ts +33 -0
  175. package/src/routes/import.ts +122 -0
  176. package/src/routes/index.ts +11 -0
  177. package/src/routes/intent.ts +105 -0
  178. package/src/routes/jobs.ts +84 -0
  179. package/src/routes/knowledge.ts +73 -0
  180. package/src/routes/media.ts +117 -0
  181. package/src/routes/schema.ts +75 -0
  182. package/src/standalone.ts +130 -0
  183. package/tsconfig.json +8 -0
  184. package/vitest.config.ts +7 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.js","sourceRoot":"","sources":["../../src/routes/media.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { Hono } from "hono";
2
+ import type { EngineContext } from "../engine.js";
3
+ export declare function schemaRoutes(engine: EngineContext): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
4
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/routes/schema.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAIlD,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,8EAoEjD"}
@@ -0,0 +1,54 @@
1
+ import { collectionToZod } from "@sourcepress/core";
2
+ import { Hono } from "hono";
3
+ import { SourcePressError } from "../middleware/error-handler.js";
4
+ import { handleRouteError } from "../middleware/route-error-handler.js";
5
+ export function schemaRoutes(engine) {
6
+ const app = new Hono();
7
+ // GET /schema — full schema (all collections)
8
+ app.get("/schema", (c) => {
9
+ const collections = engine.listCollections();
10
+ const schema = {};
11
+ for (const name of collections) {
12
+ const def = engine.getCollectionDef(name);
13
+ if (def) {
14
+ schema[name] = {
15
+ name: def.name,
16
+ path: def.path,
17
+ format: def.format,
18
+ fields: def.fields,
19
+ };
20
+ }
21
+ }
22
+ return c.json({
23
+ collections: schema,
24
+ total: collections.length,
25
+ });
26
+ });
27
+ // GET /schema/:collection — schema for a single collection
28
+ app.get("/schema/:collection", (c) => {
29
+ const collection = c.req.param("collection");
30
+ const def = engine.getCollectionDef(collection);
31
+ if (!def) {
32
+ throw new SourcePressError(404, "COLLECTION_NOT_FOUND", `Collection "${collection}" not found`);
33
+ }
34
+ // Generate Zod schema description for reference
35
+ let zodDescription = null;
36
+ try {
37
+ const zodSchema = collectionToZod(def);
38
+ zodDescription = JSON.stringify(zodSchema.shape, null, 2);
39
+ }
40
+ catch {
41
+ zodDescription = null;
42
+ }
43
+ return c.json({
44
+ name: def.name,
45
+ path: def.path,
46
+ format: def.format,
47
+ fields: def.fields,
48
+ zod_shape: zodDescription,
49
+ });
50
+ });
51
+ app.onError(handleRouteError);
52
+ return app;
53
+ }
54
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/routes/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAExE,MAAM,UAAU,YAAY,CAAC,MAAqB;IACjD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,8CAA8C;IAC9C,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;QACxB,MAAM,WAAW,GAAG,MAAM,CAAC,eAAe,EAAE,CAAC;QAC7C,MAAM,MAAM,GAQR,EAAE,CAAC;QAEP,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,MAAM,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;YAC1C,IAAI,GAAG,EAAE,CAAC;gBACT,MAAM,CAAC,IAAI,CAAC,GAAG;oBACd,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,MAAM,EAAE,GAAG,CAAC,MAAM;iBAClB,CAAC;YACH,CAAC;QACF,CAAC;QAED,OAAO,CAAC,CAAC,IAAI,CAAC;YACb,WAAW,EAAE,MAAM;YACnB,KAAK,EAAE,WAAW,CAAC,MAAM;SACzB,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,2DAA2D;IAC3D,GAAG,CAAC,GAAG,CAAC,qBAAqB,EAAE,CAAC,CAAC,EAAE,EAAE;QACpC,MAAM,UAAU,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QAC7C,MAAM,GAAG,GAAG,MAAM,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;QAEhD,IAAI,CAAC,GAAG,EAAE,CAAC;YACV,MAAM,IAAI,gBAAgB,CACzB,GAAG,EACH,sBAAsB,EACtB,eAAe,UAAU,aAAa,CACtC,CAAC;QACH,CAAC;QAED,gDAAgD;QAChD,IAAI,cAAc,GAAkB,IAAI,CAAC;QACzC,IAAI,CAAC;YACJ,MAAM,SAAS,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;YACvC,cAAc,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC3D,CAAC;QAAC,MAAM,CAAC;YACR,cAAc,GAAG,IAAI,CAAC;QACvB,CAAC;QAED,OAAO,CAAC,CAAC,IAAI,CAAC;YACb,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,SAAS,EAAE,cAAc;SACzB,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAE9B,OAAO,GAAG,CAAC;AACZ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=standalone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standalone.d.ts","sourceRoot":"","sources":["../src/standalone.ts"],"names":[],"mappings":""}
@@ -0,0 +1,115 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { serve } from "@hono/node-server";
4
+ import { createApp } from "./app.js";
5
+ import { createEngine } from "./engine.js";
6
+ function loadEnv(dir) {
7
+ const envPath = resolve(dir, ".env");
8
+ if (!existsSync(envPath))
9
+ return;
10
+ const content = readFileSync(envPath, "utf-8");
11
+ for (const line of content.split("\n")) {
12
+ const trimmed = line.trim();
13
+ if (!trimmed || trimmed.startsWith("#"))
14
+ continue;
15
+ const eqIndex = trimmed.indexOf("=");
16
+ if (eqIndex === -1)
17
+ continue;
18
+ const key = trimmed.slice(0, eqIndex).trim();
19
+ const value = trimmed.slice(eqIndex + 1).trim();
20
+ if (!process.env[key]) {
21
+ process.env[key] = value;
22
+ }
23
+ }
24
+ }
25
+ // TODO: loadConfig + loadEnv here duplicate the logic in packages/cli/src/config.ts — consolidate into a shared utility
26
+ async function loadConfig(dir) {
27
+ const tsPath = resolve(dir, "sourcepress.config.ts");
28
+ const jsPath = resolve(dir, "sourcepress.config.js");
29
+ // Prefer .js (works without compilation) over .ts
30
+ const configPath = existsSync(jsPath) ? jsPath : existsSync(tsPath) ? tsPath : null;
31
+ if (configPath) {
32
+ try {
33
+ // Try direct import first (works if running via tsx or if .js)
34
+ const mod = await import(configPath);
35
+ return mod.default ?? mod;
36
+ }
37
+ catch {
38
+ // If .ts import fails, try compiling with esbuild
39
+ if (configPath.endsWith(".ts")) {
40
+ try {
41
+ const { execFileSync } = await import("node:child_process");
42
+ const tmpJs = resolve(dir, ".sourcepress.config.tmp.mjs");
43
+ execFileSync("npx", [
44
+ "esbuild",
45
+ configPath,
46
+ "--bundle",
47
+ "--format=esm",
48
+ "--platform=node",
49
+ `--outfile=${tmpJs}`,
50
+ "--external:@sourcepress/*",
51
+ ], { stdio: "pipe" });
52
+ const mod = await import(tmpJs);
53
+ const { unlinkSync } = await import("node:fs");
54
+ unlinkSync(tmpJs);
55
+ return mod.default ?? mod;
56
+ }
57
+ catch (e) {
58
+ console.warn(`Could not compile ${configPath}:`, e instanceof Error ? e.message : e);
59
+ }
60
+ }
61
+ console.warn(`Could not load ${configPath}, using env-based config`);
62
+ }
63
+ }
64
+ // Fallback: build config from env vars
65
+ return {
66
+ repository: {
67
+ owner: process.env.GITHUB_OWNER ?? "sourcepress",
68
+ repo: process.env.GITHUB_REPO ?? "demo",
69
+ branch: process.env.GITHUB_BRANCH ?? "main",
70
+ },
71
+ ai: {
72
+ provider: process.env.AI_PROVIDER ?? "anthropic",
73
+ model: process.env.AI_MODEL ?? "claude-sonnet-4-5-20250514",
74
+ },
75
+ collections: {},
76
+ knowledge: { path: "knowledge/", graph: { backend: "local" } },
77
+ intent: { path: "intent/" },
78
+ };
79
+ }
80
+ async function main() {
81
+ const projectDir = process.argv[2] || process.cwd();
82
+ const port = Number(process.env.PORT ?? 4321);
83
+ // Load .env from project directory
84
+ loadEnv(projectDir);
85
+ const githubToken = process.env.GITHUB_TOKEN;
86
+ if (!githubToken) {
87
+ console.error("GITHUB_TOKEN is required. Set it in .env or as environment variable.");
88
+ process.exit(1);
89
+ }
90
+ const config = await loadConfig(projectDir);
91
+ console.log(`Project: ${projectDir}`);
92
+ console.log(`Repository: ${config.repository.owner}/${config.repository.repo}`);
93
+ console.log(`Collections: ${Object.keys(config.collections).join(", ") || "(none)"}`);
94
+ const apiKeysRaw = process.env.SOURCEPRESS_API_KEYS ?? process.env.API_KEYS;
95
+ const apiKeys = apiKeysRaw
96
+ ? apiKeysRaw
97
+ .split(",")
98
+ .map((k) => k.trim())
99
+ .filter(Boolean)
100
+ : undefined;
101
+ const engine = await createEngine({
102
+ config,
103
+ githubToken,
104
+ aiApiKey: process.env.ANTHROPIC_API_KEY ?? process.env.AI_API_KEY,
105
+ apiKeys,
106
+ });
107
+ const app = createApp(engine, apiKeys);
108
+ console.log(`\nSourcePress API running at http://localhost:${port}`);
109
+ console.log(`Health: http://localhost:${port}/api/health`);
110
+ console.log(`Schema: http://localhost:${port}/api/schema`);
111
+ console.log(`Content: http://localhost:${port}/api/content`);
112
+ serve({ fetch: app.fetch, port });
113
+ }
114
+ main();
115
+ //# sourceMappingURL=standalone.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standalone.js","sourceRoot":"","sources":["../src/standalone.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@sourcepress/server",
3
+ "version": "0.1.0",
4
+ "publishConfig": { "access": "public" },
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ }
11
+ },
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "test": "vitest run",
15
+ "typecheck": "tsc --noEmit",
16
+ "clean": "rm -rf dist",
17
+ "dev": "node --import tsx src/standalone.ts"
18
+ },
19
+ "dependencies": {
20
+ "@hono/node-server": "^1.13.0",
21
+ "@sourcepress/ai": "workspace:*",
22
+ "@sourcepress/cache": "workspace:*",
23
+ "@sourcepress/core": "workspace:*",
24
+ "@sourcepress/github": "workspace:*",
25
+ "@sourcepress/jobs": "workspace:*",
26
+ "@sourcepress/knowledge": "workspace:*",
27
+ "@sourcepress/media": "workspace:*",
28
+ "hono": "^4.7.0",
29
+ "hono-rate-limiter": "^0.5.3"
30
+ },
31
+ "devDependencies": {
32
+ "tsx": "^4.21.0",
33
+ "typescript": "^5.7.0",
34
+ "vitest": "^3.0.0"
35
+ }
36
+ }
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createApp } from "../app.js";
3
+ import type { EngineContext } from "../engine.js";
4
+
5
+ function createMinimalMockEngine(): EngineContext {
6
+ return {
7
+ config: {
8
+ intent: { path: "intent/" },
9
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
10
+ } as any,
11
+ github: {
12
+ getTree: vi.fn().mockResolvedValue([]),
13
+ getFile: vi.fn().mockResolvedValue(null),
14
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
15
+ } as any,
16
+ knowledge: {
17
+ getGraph: vi.fn().mockReturnValue(null),
18
+ query: vi.fn().mockReturnValue(null),
19
+ findStale: vi.fn().mockReturnValue([]),
20
+ findGaps: vi.fn().mockReturnValue([]),
21
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
22
+ } as any,
23
+ knowledgeStore: {
24
+ list: vi.fn().mockResolvedValue([]),
25
+ retrieve: vi.fn().mockResolvedValue(null),
26
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
27
+ } as any,
28
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
29
+ cache: {} as any,
30
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
31
+ budget: {} as any,
32
+ // biome-ignore lint/suspicious/noExplicitAny: partial mock of EngineContext
33
+ provider: {} as any,
34
+ listContent: vi.fn().mockResolvedValue([]),
35
+ getContent: vi.fn().mockResolvedValue(null),
36
+ getCollectionDef: vi.fn().mockReturnValue(null),
37
+ listCollections: vi.fn().mockReturnValue([]),
38
+ };
39
+ }
40
+
41
+ describe("Full app integration", () => {
42
+ it("creates app without engine (health-only mode)", async () => {
43
+ const app = createApp();
44
+ const res = await app.request("/api/health");
45
+ expect(res.status).toBe(200);
46
+ });
47
+
48
+ it("creates app with engine (all routes)", async () => {
49
+ const engine = createMinimalMockEngine();
50
+ const app = createApp(engine);
51
+
52
+ // Health works
53
+ const health = await app.request("/api/health");
54
+ expect(health.status).toBe(200);
55
+
56
+ // Schema works (empty)
57
+ const schema = await app.request("/api/schema");
58
+ expect(schema.status).toBe(200);
59
+ const schemaBody = (await schema.json()) as { total: number };
60
+ expect(schemaBody.total).toBe(0);
61
+
62
+ // Knowledge works (empty)
63
+ const knowledge = await app.request("/api/knowledge");
64
+ expect(knowledge.status).toBe(200);
65
+
66
+ // Graph works (empty)
67
+ const graph = await app.request("/api/graph");
68
+ expect(graph.status).toBe(200);
69
+ const graphBody = (await graph.json()) as { status: string };
70
+ expect(graphBody.status).toBe("empty");
71
+
72
+ // Content works (empty)
73
+ const content = await app.request("/api/content");
74
+ expect(content.status).toBe(200);
75
+
76
+ // Intent works (empty)
77
+ const intent = await app.request("/api/intent");
78
+ expect(intent.status).toBe(200);
79
+ });
80
+ });
@@ -0,0 +1,195 @@
1
+ import type { ApprovalProvider, ApprovalRequest } from "@sourcepress/core";
2
+ import { Hono } from "hono";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import type { EngineContext } from "../engine.js";
5
+ import { errorHandler } from "../middleware/error-handler.js";
6
+ import { approvalRoutes } from "../routes/approval.js";
7
+
8
+ const mockRequest: ApprovalRequest = {
9
+ id: "42",
10
+ change: {
11
+ collection: "posts",
12
+ slug: "my-post",
13
+ path: "content/posts/my-post.md",
14
+ action: "create",
15
+ content: "# Hello",
16
+ frontmatter: { title: "My Post" },
17
+ provenance: {
18
+ generated_by: "claude-3-5-sonnet",
19
+ generated_at: "2026-04-04T10:00:00Z",
20
+ source_files: [],
21
+ prompt_version: "v1",
22
+ },
23
+ },
24
+ status: "pending",
25
+ submitted_at: "2026-04-04T10:00:00Z",
26
+ submitted_by: "claude-3-5-sonnet",
27
+ pr_url: "https://github.com/org/repo/pull/42",
28
+ pr_number: 42,
29
+ };
30
+
31
+ function createMockApproval(): ApprovalProvider {
32
+ return {
33
+ submit: vi.fn().mockResolvedValue(mockRequest),
34
+ status: vi.fn().mockResolvedValue("pending"),
35
+ approve: vi.fn().mockResolvedValue(undefined),
36
+ reject: vi.fn().mockResolvedValue(undefined),
37
+ pending: vi.fn().mockResolvedValue([mockRequest]),
38
+ onStatusChange: vi.fn(),
39
+ };
40
+ }
41
+
42
+ function createMockEngine(withApproval = true): EngineContext {
43
+ const approval = withApproval ? createMockApproval() : undefined;
44
+
45
+ const github = {
46
+ createOrUpdateFile: vi.fn().mockResolvedValue(undefined),
47
+ };
48
+
49
+ const config = {
50
+ approval: {
51
+ provider: "github-pr",
52
+ rules: {
53
+ "content/posts/*": "pr",
54
+ "content/pages/*": "direct",
55
+ },
56
+ },
57
+ collections: {},
58
+ repository: { owner: "org", repo: "repo", branch: "main" },
59
+ ai: { provider: "anthropic", model: "claude-3-5-sonnet", daily_limit_usd: 5 },
60
+ };
61
+
62
+ return { approval, github, config } as unknown as EngineContext;
63
+ }
64
+
65
+ function createTestApp(engine: EngineContext) {
66
+ const app = new Hono().basePath("/api");
67
+ app.use("*", errorHandler());
68
+ app.route("/", approvalRoutes(engine));
69
+ return app;
70
+ }
71
+
72
+ describe("Approval routes", () => {
73
+ let engine: EngineContext;
74
+ let app: Hono;
75
+
76
+ beforeEach(() => {
77
+ engine = createMockEngine();
78
+ app = createTestApp(engine);
79
+ });
80
+
81
+ it("GET /api/approval/pending returns pending items", async () => {
82
+ const res = await app.request("/api/approval/pending");
83
+ expect(res.status).toBe(200);
84
+ const body = (await res.json()) as Record<string, unknown>;
85
+ expect(body.total).toBe(1);
86
+ const items = body.items as ApprovalRequest[];
87
+ expect(items[0].id).toBe("42");
88
+ });
89
+
90
+ it("POST /api/approval with 'pr' rule creates approval request", async () => {
91
+ const change = {
92
+ collection: "posts",
93
+ slug: "my-post",
94
+ path: "content/posts/my-post.md",
95
+ action: "create",
96
+ content: "# Hello",
97
+ frontmatter: { title: "My Post" },
98
+ provenance: {
99
+ generated_by: "claude-3-5-sonnet",
100
+ generated_at: "2026-04-04T10:00:00Z",
101
+ source_files: [],
102
+ prompt_version: "v1",
103
+ },
104
+ };
105
+ const res = await app.request("/api/approval", {
106
+ method: "POST",
107
+ headers: { "Content-Type": "application/json" },
108
+ body: JSON.stringify(change),
109
+ });
110
+ expect(res.status).toBe(201);
111
+ const body = (await res.json()) as ApprovalRequest;
112
+ expect(body.status).toBe("pending");
113
+ expect(body.pr_number).toBe(42);
114
+ expect(engine.approval?.submit).toHaveBeenCalledWith(change);
115
+ });
116
+
117
+ it("POST /api/approval with 'direct' rule commits directly", async () => {
118
+ const change = {
119
+ collection: "pages",
120
+ slug: "about",
121
+ path: "content/pages/about.md",
122
+ action: "create",
123
+ content: "# About",
124
+ frontmatter: { title: "About" },
125
+ provenance: {
126
+ generated_by: "claude-3-5-sonnet",
127
+ generated_at: "2026-04-04T10:00:00Z",
128
+ source_files: [],
129
+ prompt_version: "v1",
130
+ },
131
+ };
132
+ const res = await app.request("/api/approval", {
133
+ method: "POST",
134
+ headers: { "Content-Type": "application/json" },
135
+ body: JSON.stringify(change),
136
+ });
137
+ expect(res.status).toBe(201);
138
+ const body = (await res.json()) as Record<string, unknown>;
139
+ expect(body.status).toBe("direct");
140
+ expect(body.path).toBe("content/pages/about.md");
141
+ expect(engine.approval?.submit).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it("POST /api/approval/:id/approve calls provider.approve", async () => {
145
+ const res = await app.request("/api/approval/42/approve", {
146
+ method: "POST",
147
+ headers: { "Content-Type": "application/json" },
148
+ body: JSON.stringify({ by: "editor@example.com", comment: "Looks good!" }),
149
+ });
150
+ expect(res.status).toBe(200);
151
+ const body = (await res.json()) as Record<string, unknown>;
152
+ expect(body.status).toBe("approved");
153
+ expect(engine.approval?.approve).toHaveBeenCalledWith(
154
+ "42",
155
+ "editor@example.com",
156
+ "Looks good!",
157
+ );
158
+ });
159
+
160
+ it("POST /api/approval/:id/reject calls provider.reject", async () => {
161
+ const res = await app.request("/api/approval/42/reject", {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/json" },
164
+ body: JSON.stringify({ by: "editor@example.com", reason: "Needs revision" }),
165
+ });
166
+ expect(res.status).toBe(200);
167
+ const body = (await res.json()) as Record<string, unknown>;
168
+ expect(body.status).toBe("rejected");
169
+ expect(engine.approval?.reject).toHaveBeenCalledWith(
170
+ "42",
171
+ "editor@example.com",
172
+ "Needs revision",
173
+ );
174
+ });
175
+
176
+ it("returns 501 when approval not configured", async () => {
177
+ const noApprovalEngine = createMockEngine(false);
178
+ const noApprovalApp = createTestApp(noApprovalEngine);
179
+ const res = await noApprovalApp.request("/api/approval/pending");
180
+ expect(res.status).toBe(501);
181
+ const body = (await res.json()) as Record<string, unknown>;
182
+ expect(body.code).toBe("APPROVAL_NOT_CONFIGURED");
183
+ });
184
+
185
+ it("POST /api/approval validates required fields", async () => {
186
+ const res = await app.request("/api/approval", {
187
+ method: "POST",
188
+ headers: { "Content-Type": "application/json" },
189
+ body: JSON.stringify({ collection: "posts" }),
190
+ });
191
+ expect(res.status).toBe(400);
192
+ const body = (await res.json()) as Record<string, unknown>;
193
+ expect(body.code).toBe("INVALID_INPUT");
194
+ });
195
+ });