@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,262 @@
1
+ import { EvalRunner, judge } from "@sourcepress/ai";
2
+ import type { EvalRunConfig } from "@sourcepress/ai";
3
+ import { Hono } from "hono";
4
+ import type { EngineContext } from "../engine.js";
5
+ import { SourcePressError } from "../middleware/error-handler.js";
6
+ import { handleRouteError } from "../middleware/route-error-handler.js";
7
+
8
+ export function evalRoutes(engine: EngineContext) {
9
+ const app = new Hono();
10
+
11
+ app.onError(handleRouteError);
12
+
13
+ // POST /eval/judge — single judge pass on content
14
+ app.post("/eval/judge", async (c) => {
15
+ const body = await c.req.json<{
16
+ draft: string;
17
+ gold_standard: string;
18
+ judge_prompt: string;
19
+ intent?: string;
20
+ }>();
21
+
22
+ if (!body.draft || !body.gold_standard || !body.judge_prompt) {
23
+ throw new SourcePressError(
24
+ 400,
25
+ "INVALID_INPUT",
26
+ "draft, gold_standard, and judge_prompt are required",
27
+ );
28
+ }
29
+
30
+ const result = await judge(
31
+ {
32
+ draft: body.draft,
33
+ gold_standard: body.gold_standard,
34
+ judge_prompt: body.judge_prompt,
35
+ intent: body.intent,
36
+ },
37
+ engine.provider,
38
+ engine.budget,
39
+ );
40
+
41
+ return c.json({
42
+ score: result.score,
43
+ reasoning: result.reasoning,
44
+ usage: result.usage,
45
+ });
46
+ });
47
+
48
+ // POST /eval/run — full generate-judge-improve loop
49
+ app.post("/eval/run", async (c) => {
50
+ const body = await c.req.json<{
51
+ content_type: string;
52
+ knowledge_context: string;
53
+ gold_standard: string;
54
+ judge_prompt: string;
55
+ generation_prompt: string;
56
+ intent?: string;
57
+ threshold?: number;
58
+ max_iterations?: number;
59
+ collection_schema?: Record<string, unknown>;
60
+ }>();
61
+
62
+ if (
63
+ !body.content_type ||
64
+ !body.knowledge_context ||
65
+ !body.gold_standard ||
66
+ !body.judge_prompt ||
67
+ !body.generation_prompt
68
+ ) {
69
+ throw new SourcePressError(
70
+ 400,
71
+ "INVALID_INPUT",
72
+ "content_type, knowledge_context, gold_standard, judge_prompt, and generation_prompt are required",
73
+ );
74
+ }
75
+
76
+ const threshold =
77
+ body.threshold ??
78
+ (engine.config.evals as { threshold?: number } | undefined)?.threshold ??
79
+ 70;
80
+ const maxIterations = body.max_iterations ?? 3;
81
+
82
+ const config: EvalRunConfig = {
83
+ content_type: body.content_type,
84
+ knowledge_context: body.knowledge_context,
85
+ gold_standard: body.gold_standard,
86
+ judge_prompt: body.judge_prompt,
87
+ generation_prompt: body.generation_prompt,
88
+ intent: body.intent,
89
+ threshold,
90
+ max_iterations: maxIterations,
91
+ collection_schema: body.collection_schema,
92
+ };
93
+
94
+ const runner = new EvalRunner(engine.provider, engine.budget);
95
+ const result = await runner.run(config);
96
+
97
+ // Log result to results.tsv
98
+ try {
99
+ const resultsPath = "evals/results.tsv";
100
+ const existing = await engine.github.getFile(resultsPath);
101
+ const header = "date\tcontent_type\tprompt_version\tscore\tstatus\treasoning";
102
+ const row = `${new Date().toISOString()}\t${body.content_type}\tdefault\t${result.final_score}\t${result.final_status}\t${(result.iterations[result.iterations.length - 1]?.reasoning ?? "").replace(/\t/g, " ")}`;
103
+ const content = existing ? `${existing.content.trimEnd()}\n${row}\n` : `${header}\n${row}\n`;
104
+ await engine.github.createOrUpdateFile(
105
+ resultsPath,
106
+ content,
107
+ "eval: log result",
108
+ undefined,
109
+ existing?.sha,
110
+ );
111
+ } catch {
112
+ // Non-critical — don't fail the eval if logging fails
113
+ }
114
+
115
+ return c.json(result);
116
+ });
117
+
118
+ // GET /eval/results — list eval results from evals/results.tsv in the repo
119
+ app.get("/eval/results", async (c) => {
120
+ const resultsPath = "evals/results.tsv";
121
+ const file = await engine.github.getFile(resultsPath);
122
+
123
+ if (!file) {
124
+ return c.json({ items: [], total: 0 });
125
+ }
126
+
127
+ const lines = file.content.trim().split("\n");
128
+ if (lines.length <= 1) {
129
+ return c.json({ items: [], total: 0 });
130
+ }
131
+
132
+ // Parse TSV: date, content_type, prompt_version, score, status, reasoning
133
+ const headers = lines[0].split("\t");
134
+ const items = lines.slice(1).map((line) => {
135
+ const values = line.split("\t");
136
+ const obj: Record<string, string> = {};
137
+ for (let i = 0; i < headers.length; i++) {
138
+ obj[headers[i].trim()] = values[i]?.trim() ?? "";
139
+ }
140
+ return obj;
141
+ });
142
+
143
+ // Filter by content_type if provided
144
+ const contentType = c.req.query("content_type");
145
+ const filtered = contentType
146
+ ? items.filter((item) => item.content_type === contentType)
147
+ : items;
148
+
149
+ return c.json({ items: filtered, total: filtered.length });
150
+ });
151
+
152
+ // GET /eval/prompts — list generation prompts from evals/prompts/ in the repo
153
+ app.get("/eval/prompts", async (c) => {
154
+ const tree = await engine.github.getTree();
155
+ const promptFiles = tree
156
+ .filter((entry) => entry.path.startsWith("evals/prompts/") && entry.type === "blob")
157
+ .map((entry) => ({
158
+ path: entry.path,
159
+ name: entry.path.replace("evals/prompts/", "").replace(/\.(md|txt)$/, ""),
160
+ }));
161
+
162
+ return c.json({ items: promptFiles, total: promptFiles.length });
163
+ });
164
+
165
+ // GET /eval/prompts/:name — get a single generation prompt
166
+ app.get("/eval/prompts/:name", async (c) => {
167
+ const name = c.req.param("name");
168
+ const filePath = `evals/prompts/${name}.md`;
169
+ const file = await engine.github.getFile(filePath);
170
+
171
+ if (!file) {
172
+ throw new SourcePressError(404, "PROMPT_NOT_FOUND", `Generation prompt "${name}" not found`);
173
+ }
174
+
175
+ return c.json({
176
+ name,
177
+ path: file.path,
178
+ content: file.content,
179
+ });
180
+ });
181
+
182
+ // PUT /eval/prompts/:name — update a generation prompt (direct commit, self-improving)
183
+ app.put("/eval/prompts/:name", async (c) => {
184
+ const name = c.req.param("name");
185
+ const filePath = `evals/prompts/${name}.md`;
186
+ const body = await c.req.json<{ content: string }>();
187
+
188
+ if (!body.content) {
189
+ throw new SourcePressError(400, "INVALID_INPUT", "content is required");
190
+ }
191
+
192
+ const existing = await engine.github.getFile(filePath);
193
+ const existingSha = existing?.sha;
194
+
195
+ const result = await engine.github.createOrUpdateFile(
196
+ filePath,
197
+ body.content,
198
+ `update(eval): improve ${name} generation prompt`,
199
+ undefined,
200
+ existingSha,
201
+ );
202
+
203
+ return c.json({
204
+ updated: true,
205
+ path: filePath,
206
+ commit_sha: result.commit_sha,
207
+ });
208
+ });
209
+
210
+ // GET /eval/standards — list gold standard files
211
+ app.get("/eval/standards", async (c) => {
212
+ const tree = await engine.github.getTree();
213
+ const standardFiles = tree
214
+ .filter((entry) => entry.path.startsWith("evals/standards/") && entry.type === "blob")
215
+ .map((entry) => ({
216
+ path: entry.path,
217
+ name: entry.path.replace("evals/standards/", "").replace(/\.(mdx|md)$/, ""),
218
+ }));
219
+
220
+ return c.json({ items: standardFiles, total: standardFiles.length });
221
+ });
222
+
223
+ // GET /eval/standards/:name — get a single gold standard
224
+ app.get("/eval/standards/:name", async (c) => {
225
+ const name = c.req.param("name");
226
+ // Try .mdx first, then .md
227
+ let file = await engine.github.getFile(`evals/standards/${name}.mdx`);
228
+ if (!file) {
229
+ file = await engine.github.getFile(`evals/standards/${name}.md`);
230
+ }
231
+
232
+ if (!file) {
233
+ throw new SourcePressError(404, "STANDARD_NOT_FOUND", `Gold standard "${name}" not found`);
234
+ }
235
+
236
+ return c.json({
237
+ name,
238
+ path: file.path,
239
+ content: file.content,
240
+ });
241
+ });
242
+
243
+ // GET /eval/judge-prompt — get the judge.md prompt
244
+ app.get("/eval/judge-prompt", async (c) => {
245
+ const file = await engine.github.getFile("evals/judge.md");
246
+
247
+ if (!file) {
248
+ throw new SourcePressError(
249
+ 404,
250
+ "JUDGE_PROMPT_NOT_FOUND",
251
+ "evals/judge.md not found in repository",
252
+ );
253
+ }
254
+
255
+ return c.json({
256
+ path: file.path,
257
+ content: file.content,
258
+ });
259
+ });
260
+
261
+ return app;
262
+ }
@@ -0,0 +1,111 @@
1
+ import { Hono } from "hono";
2
+ import type { EngineContext } from "../engine.js";
3
+ import { SourcePressError } from "../middleware/error-handler.js";
4
+ import { handleRouteError } from "../middleware/route-error-handler.js";
5
+
6
+ export function graphRoutes(engine: EngineContext) {
7
+ const app = new Hono();
8
+
9
+ app.onError(handleRouteError);
10
+
11
+ // GET /graph — get full graph summary
12
+ app.get("/graph", async (c) => {
13
+ const graph = engine.knowledge.getGraph();
14
+
15
+ if (!graph) {
16
+ return c.json({
17
+ status: "empty",
18
+ message: "No graph built yet. POST /api/graph/rebuild to build.",
19
+ entities: 0,
20
+ relations: 0,
21
+ clusters: 0,
22
+ });
23
+ }
24
+
25
+ // Serialize Map to array for JSON
26
+ const entities = Array.from(graph.entities.values()).map((e) => ({
27
+ name: e.name,
28
+ type: e.type,
29
+ aliases: e.aliases,
30
+ confidence: e.confidence,
31
+ source_file: e.source_file,
32
+ }));
33
+
34
+ return c.json({
35
+ status: "built",
36
+ built_at: graph.built_at,
37
+ file_count: graph.file_count,
38
+ entity_count: entities.length,
39
+ relation_count: graph.relations.length,
40
+ cluster_count: graph.clusters.length,
41
+ entities,
42
+ relations: graph.relations,
43
+ clusters: graph.clusters,
44
+ });
45
+ });
46
+
47
+ // GET /graph/entity/:name — query a specific entity
48
+ app.get("/graph/entity/:name", async (c) => {
49
+ const name = decodeURIComponent(c.req.param("name"));
50
+ const result = engine.knowledge.query(name);
51
+
52
+ if (!result) {
53
+ throw new SourcePressError(
54
+ 404,
55
+ "ENTITY_NOT_FOUND",
56
+ `Entity "${name}" not found in graph. Graph may need rebuilding.`,
57
+ );
58
+ }
59
+
60
+ return c.json(result);
61
+ });
62
+
63
+ // GET /graph/stale — find stale content
64
+ app.get("/graph/stale", async (c) => {
65
+ const collections = engine.listCollections();
66
+ const allContent = [];
67
+ for (const coll of collections) {
68
+ const files = await engine.listContent(coll);
69
+ allContent.push(...files);
70
+ }
71
+
72
+ // Build timestamps from knowledge store
73
+ const knowledgeFiles = await engine.knowledgeStore.list();
74
+ const timestamps: Record<string, string> = {};
75
+ for (const f of knowledgeFiles) {
76
+ timestamps[f.path] = f.ingested_at;
77
+ }
78
+
79
+ const stale = engine.knowledge.findStale(allContent, timestamps);
80
+ return c.json({ items: stale, total: stale.length });
81
+ });
82
+
83
+ // GET /graph/gaps — find knowledge gaps
84
+ app.get("/graph/gaps", async (c) => {
85
+ const collections = engine.listCollections();
86
+ const allContent = [];
87
+ for (const coll of collections) {
88
+ const files = await engine.listContent(coll);
89
+ allContent.push(...files);
90
+ }
91
+
92
+ const gaps = engine.knowledge.findGaps(allContent);
93
+ return c.json({ items: gaps, total: gaps.length });
94
+ });
95
+
96
+ // POST /graph/rebuild — rebuild the knowledge graph
97
+ app.post("/graph/rebuild", async (c) => {
98
+ const graph = await engine.knowledge.buildGraph();
99
+
100
+ return c.json({
101
+ rebuilt: true,
102
+ built_at: graph.built_at,
103
+ entity_count: graph.entities.size,
104
+ relation_count: graph.relations.length,
105
+ cluster_count: graph.clusters.length,
106
+ file_count: graph.file_count,
107
+ });
108
+ });
109
+
110
+ return app;
111
+ }
@@ -0,0 +1,33 @@
1
+ import { Hono } from "hono";
2
+
3
+ export interface HealthStatus {
4
+ status: "ok" | "degraded" | "error";
5
+ version: string;
6
+ timestamp: string;
7
+ services: {
8
+ github: "connected" | "disconnected" | "unknown";
9
+ knowledge: "ready" | "empty" | "unknown";
10
+ cache: "ready" | "unknown";
11
+ };
12
+ }
13
+
14
+ export function healthRoutes() {
15
+ const app = new Hono();
16
+
17
+ app.get("/health", (c) => {
18
+ const health: HealthStatus = {
19
+ status: "ok",
20
+ version: "0.1.0",
21
+ timestamp: new Date().toISOString(),
22
+ services: {
23
+ github: "unknown",
24
+ knowledge: "unknown",
25
+ cache: "unknown",
26
+ },
27
+ };
28
+
29
+ return c.json(health);
30
+ });
31
+
32
+ return app;
33
+ }
@@ -0,0 +1,122 @@
1
+ import { Hono } from "hono";
2
+ import type { EngineContext } from "../engine.js";
3
+ import { SourcePressError } from "../middleware/error-handler.js";
4
+ import { handleRouteError } from "../middleware/route-error-handler.js";
5
+
6
+ const MAX_BATCH_SIZE = 50;
7
+
8
+ export function importRoutes(engine: EngineContext) {
9
+ const app = new Hono();
10
+
11
+ app.onError(handleRouteError);
12
+
13
+ // POST /knowledge/import — scrape a single URL (synchronous)
14
+ app.post("/knowledge/import", async (c) => {
15
+ const body = await c.req.json<{ url: string }>();
16
+ if (!body.url) {
17
+ throw new SourcePressError(400, "INVALID_INPUT", "url is required");
18
+ }
19
+
20
+ try {
21
+ const result = await engine.knowledge.importUrl(body.url);
22
+ return c.json(
23
+ {
24
+ imported: true,
25
+ path: result.path,
26
+ type: result.type,
27
+ quality: result.quality,
28
+ quality_score: result.quality_score,
29
+ entities: result.entities,
30
+ source_url: result.source_url,
31
+ },
32
+ 201,
33
+ );
34
+ } catch (error) {
35
+ throw new SourcePressError(
36
+ 422,
37
+ "IMPORT_FAILED",
38
+ error instanceof Error ? error.message : "Failed to import URL",
39
+ );
40
+ }
41
+ });
42
+
43
+ // POST /knowledge/import/batch — import multiple URLs (async job)
44
+ app.post("/knowledge/import/batch", async (c) => {
45
+ if (!engine.jobs) {
46
+ throw new SourcePressError(501, "JOBS_NOT_CONFIGURED", "Job system not configured");
47
+ }
48
+
49
+ const body = await c.req.json<{ urls: string[] }>();
50
+ if (!body.urls || !Array.isArray(body.urls) || body.urls.length === 0) {
51
+ throw new SourcePressError(
52
+ 400,
53
+ "INVALID_INPUT",
54
+ "urls array is required and must not be empty",
55
+ );
56
+ }
57
+ if (body.urls.length > MAX_BATCH_SIZE) {
58
+ throw new SourcePressError(
59
+ 400,
60
+ "BATCH_TOO_LARGE",
61
+ `Batch size ${body.urls.length} exceeds maximum of ${MAX_BATCH_SIZE}`,
62
+ );
63
+ }
64
+
65
+ const jobId = await engine.jobs.enqueue({
66
+ type: "import-batch",
67
+ params: { urls: body.urls },
68
+ });
69
+ const jobStatus = await engine.jobs.status(jobId);
70
+
71
+ return c.json(jobStatus, 202);
72
+ });
73
+
74
+ // POST /knowledge/import/sitemap — parse sitemap for interactive selection
75
+ app.post("/knowledge/import/sitemap", async (c) => {
76
+ const body = await c.req.json<{ url: string }>();
77
+ if (!body.url) {
78
+ throw new SourcePressError(400, "INVALID_INPUT", "url is required");
79
+ }
80
+
81
+ try {
82
+ const result = await engine.knowledge.parseSitemap(body.url);
83
+ return c.json(result);
84
+ } catch (error) {
85
+ throw new SourcePressError(
86
+ 422,
87
+ "SITEMAP_PARSE_FAILED",
88
+ error instanceof Error ? error.message : "Failed to parse sitemap",
89
+ );
90
+ }
91
+ });
92
+
93
+ // POST /knowledge/import/sitemap/run — run sitemap import with filters (async job)
94
+ app.post("/knowledge/import/sitemap/run", async (c) => {
95
+ if (!engine.jobs) {
96
+ throw new SourcePressError(501, "JOBS_NOT_CONFIGURED", "Job system not configured");
97
+ }
98
+
99
+ const body = await c.req.json<{
100
+ sitemap_url: string;
101
+ include?: string[];
102
+ exclude?: string[];
103
+ }>();
104
+ if (!body.sitemap_url) {
105
+ throw new SourcePressError(400, "INVALID_INPUT", "sitemap_url is required");
106
+ }
107
+
108
+ const jobId = await engine.jobs.enqueue({
109
+ type: "import-sitemap",
110
+ params: {
111
+ sitemap_url: body.sitemap_url,
112
+ include: body.include,
113
+ exclude: body.exclude,
114
+ },
115
+ });
116
+ const jobStatus = await engine.jobs.status(jobId);
117
+
118
+ return c.json(jobStatus, 202);
119
+ });
120
+
121
+ return app;
122
+ }
@@ -0,0 +1,11 @@
1
+ export { approvalRoutes } from "./approval.js";
2
+ export { healthRoutes } from "./health.js";
3
+ export { contentRoutes } from "./content.js";
4
+ export { knowledgeRoutes } from "./knowledge.js";
5
+ export { graphRoutes } from "./graph.js";
6
+ export { schemaRoutes } from "./schema.js";
7
+ export { intentRoutes } from "./intent.js";
8
+ export { evalRoutes } from "./eval.js";
9
+ export { mediaRoutes } from "./media.js";
10
+ export { jobRoutes } from "./jobs.js";
11
+ export { importRoutes } from "./import.js";
@@ -0,0 +1,105 @@
1
+ import { Hono } from "hono";
2
+ import type { EngineContext } from "../engine.js";
3
+ import { SourcePressError } from "../middleware/error-handler.js";
4
+ import { handleRouteError } from "../middleware/route-error-handler.js";
5
+
6
+ export function intentRoutes(engine: EngineContext) {
7
+ const app = new Hono();
8
+
9
+ // GET /intent — list intent files
10
+ app.get("/intent", async (c) => {
11
+ const intentPath = engine.config.intent.path;
12
+ const tree = await engine.github.getTree();
13
+ const intentFiles = tree
14
+ .filter((entry) => entry.path.startsWith(intentPath) && entry.type === "blob")
15
+ .map((entry) => ({
16
+ path: entry.path,
17
+ name: entry.path.replace(intentPath, "").replace(/\.(md|yaml|json)$/, ""),
18
+ }));
19
+
20
+ return c.json({ items: intentFiles, total: intentFiles.length });
21
+ });
22
+
23
+ // GET /intent/:name — get a single intent file
24
+ app.get("/intent/:name", async (c) => {
25
+ const name = c.req.param("name");
26
+ const intentPath = engine.config.intent.path;
27
+ const filePath = `${intentPath}${name}.md`;
28
+ const file = await engine.github.getFile(filePath);
29
+
30
+ if (!file) {
31
+ throw new SourcePressError(404, "INTENT_NOT_FOUND", `Intent "${name}" not found`);
32
+ }
33
+
34
+ return c.json({
35
+ name,
36
+ path: file.path,
37
+ content: file.content,
38
+ });
39
+ });
40
+
41
+ // PUT /intent/:name — update an intent file (direct commit, intent is human-owned)
42
+ app.put("/intent/:name", async (c) => {
43
+ const name = c.req.param("name");
44
+ const intentPath = engine.config.intent.path;
45
+ const filePath = `${intentPath}${name}.md`;
46
+
47
+ const body = await c.req.json<{ content: string }>();
48
+
49
+ if (!body.content) {
50
+ throw new SourcePressError(400, "INVALID_INPUT", "content is required");
51
+ }
52
+
53
+ // Check if file exists to get SHA
54
+ const existing = await engine.github.getFile(filePath);
55
+ const existingSha = existing?.sha;
56
+
57
+ const result = await engine.github.createOrUpdateFile(
58
+ filePath,
59
+ body.content,
60
+ `update(intent): update ${name}`,
61
+ undefined,
62
+ existingSha,
63
+ );
64
+
65
+ return c.json({
66
+ updated: true,
67
+ path: filePath,
68
+ commit_sha: result.commit_sha,
69
+ });
70
+ });
71
+
72
+ // GET /intent/:name/impact — analyze which content is affected by this intent
73
+ app.get("/intent/:name/impact", async (c) => {
74
+ const name = c.req.param("name");
75
+ const intentPath = engine.config.intent.path;
76
+ const filePath = `${intentPath}${name}.md`;
77
+ const file = await engine.github.getFile(filePath);
78
+
79
+ if (!file) {
80
+ throw new SourcePressError(404, "INTENT_NOT_FOUND", `Intent "${name}" not found`);
81
+ }
82
+
83
+ // Collect all content
84
+ const collections = engine.listCollections();
85
+ const allContent = [];
86
+ for (const coll of collections) {
87
+ const files = await engine.listContent(coll);
88
+ allContent.push(...files);
89
+ }
90
+
91
+ // Return content count — full AI-based impact analysis requires
92
+ // the previous version of intent, which we defer to a future enhancement
93
+ return c.json({
94
+ intent: name,
95
+ content_count: allContent.length,
96
+ message:
97
+ "Full AI-based intent impact analysis requires previous intent version. Use POST /api/intent/:name/impact with previous_content field for detailed analysis.",
98
+ content_paths: allContent.map((f) => f.path),
99
+ });
100
+ });
101
+
102
+ app.onError(handleRouteError);
103
+
104
+ return app;
105
+ }