@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,84 @@
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 jobRoutes(engine: EngineContext) {
7
+ const app = new Hono();
8
+
9
+ // GET /jobs — list all jobs
10
+ app.get("/jobs", async (c) => {
11
+ if (!engine.jobs) {
12
+ throw new SourcePressError(501, "JOBS_NOT_CONFIGURED", "Job system not configured");
13
+ }
14
+ const status = c.req.query("status") as
15
+ | "queued"
16
+ | "running"
17
+ | "completed"
18
+ | "failed"
19
+ | "cancelled"
20
+ | undefined;
21
+ const type = c.req.query("type");
22
+ const limitStr = c.req.query("limit");
23
+ const limit = limitStr ? Number.parseInt(limitStr, 10) : undefined;
24
+
25
+ const jobs = await engine.jobs.list({ status, type, limit });
26
+ return c.json({ items: jobs, total: jobs.length });
27
+ });
28
+
29
+ // GET /jobs/:id — get job status
30
+ app.get("/jobs/:id", async (c) => {
31
+ if (!engine.jobs) {
32
+ throw new SourcePressError(501, "JOBS_NOT_CONFIGURED", "Job system not configured");
33
+ }
34
+ const id = c.req.param("id");
35
+ const job = await engine.jobs.status(id);
36
+ if (!job) {
37
+ throw new SourcePressError(404, "JOB_NOT_FOUND", `Job "${id}" not found`);
38
+ }
39
+ return c.json(job);
40
+ });
41
+
42
+ // POST /jobs — start a new job
43
+ app.post("/jobs", async (c) => {
44
+ if (!engine.jobs) {
45
+ throw new SourcePressError(501, "JOBS_NOT_CONFIGURED", "Job system not configured");
46
+ }
47
+ const body = await c.req.json<{ type: string; params?: Record<string, unknown> }>();
48
+ if (!body.type) {
49
+ throw new SourcePressError(400, "INVALID_INPUT", "type is required");
50
+ }
51
+ try {
52
+ const jobId = await engine.jobs.enqueue({ type: body.type, params: body.params ?? {} });
53
+ const jobStatus = await engine.jobs.status(jobId);
54
+ return c.json(jobStatus, 201);
55
+ } catch (error) {
56
+ if (error instanceof Error && error.message.includes("No handler registered")) {
57
+ throw new SourcePressError(400, "UNKNOWN_JOB_TYPE", error.message);
58
+ }
59
+ throw error;
60
+ }
61
+ });
62
+
63
+ // DELETE /jobs/:id — cancel a job
64
+ app.delete("/jobs/:id", async (c) => {
65
+ if (!engine.jobs) {
66
+ throw new SourcePressError(501, "JOBS_NOT_CONFIGURED", "Job system not configured");
67
+ }
68
+ const id = c.req.param("id");
69
+ const existing = await engine.jobs.status(id);
70
+ if (!existing) {
71
+ throw new SourcePressError(
72
+ 404,
73
+ "JOB_NOT_FOUND",
74
+ `Job "${id}" not found or already completed`,
75
+ );
76
+ }
77
+ await engine.jobs.cancel(id);
78
+ return c.json({ cancelled: true, id });
79
+ });
80
+
81
+ app.onError(handleRouteError);
82
+
83
+ return app;
84
+ }
@@ -0,0 +1,73 @@
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 knowledgeRoutes(engine: EngineContext) {
7
+ const app = new Hono();
8
+
9
+ app.onError(handleRouteError);
10
+
11
+ // GET /knowledge — list knowledge files
12
+ app.get("/knowledge", async (c) => {
13
+ const files = await engine.knowledgeStore.list();
14
+ const items = files.map((f) => ({
15
+ path: f.path,
16
+ type: f.type,
17
+ quality: f.quality,
18
+ quality_score: f.quality_score,
19
+ entities: f.entities,
20
+ ingested_at: f.ingested_at,
21
+ source: f.source,
22
+ }));
23
+
24
+ return c.json({ items, total: items.length });
25
+ });
26
+
27
+ // GET /knowledge/:path — get a single knowledge file (path is URL-encoded)
28
+ app.get("/knowledge/:path{.+}", async (c) => {
29
+ const path = c.req.param("path");
30
+ const file = await engine.knowledgeStore.retrieve(path);
31
+
32
+ if (!file) {
33
+ throw new SourcePressError(404, "KNOWLEDGE_NOT_FOUND", `Knowledge file "${path}" not found`);
34
+ }
35
+
36
+ return c.json(file);
37
+ });
38
+
39
+ // POST /knowledge — ingest new knowledge (classify + store)
40
+ app.post("/knowledge", async (c) => {
41
+ const body = await c.req.json<{
42
+ path: string;
43
+ body: string;
44
+ source?: "manual" | "url" | "document" | "transcript" | "scrape";
45
+ source_url?: string;
46
+ }>();
47
+
48
+ if (!body.path || !body.body) {
49
+ throw new SourcePressError(400, "INVALID_INPUT", "path and body are required");
50
+ }
51
+
52
+ const knowledgeFile = await engine.knowledge.ingest(
53
+ body.path,
54
+ body.body,
55
+ body.source ?? "manual",
56
+ body.source_url,
57
+ );
58
+
59
+ return c.json(
60
+ {
61
+ ingested: true,
62
+ path: knowledgeFile.path,
63
+ type: knowledgeFile.type,
64
+ quality: knowledgeFile.quality,
65
+ quality_score: knowledgeFile.quality_score,
66
+ entities: knowledgeFile.entities,
67
+ },
68
+ 201,
69
+ );
70
+ });
71
+
72
+ return app;
73
+ }
@@ -0,0 +1,117 @@
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 mediaRoutes(engine: EngineContext) {
7
+ const app = new Hono();
8
+
9
+ app.onError(handleRouteError);
10
+
11
+ // GET /media — list all media with metadata
12
+ app.get("/media", async (c) => {
13
+ if (!engine.media) {
14
+ throw new SourcePressError(501, "MEDIA_NOT_CONFIGURED", "Media storage not configured");
15
+ }
16
+
17
+ const prefix = c.req.query("prefix");
18
+ const items = await engine.media.list(prefix ?? undefined);
19
+ return c.json({ items, total: items.length });
20
+ });
21
+
22
+ // POST /media/upload — upload a media file (multipart form)
23
+ app.post("/media/upload", async (c) => {
24
+ if (!engine.media) {
25
+ throw new SourcePressError(501, "MEDIA_NOT_CONFIGURED", "Media storage not configured");
26
+ }
27
+
28
+ const formData = await c.req.formData();
29
+ const file = formData.get("file");
30
+ const path = formData.get("path");
31
+ const source = formData.get("source");
32
+
33
+ if (!file || !(file instanceof File)) {
34
+ throw new SourcePressError(400, "INVALID_INPUT", "Missing file in form data");
35
+ }
36
+ if (!path || typeof path !== "string") {
37
+ throw new SourcePressError(400, "INVALID_INPUT", "Missing path in form data");
38
+ }
39
+ if (!source || typeof source !== "string") {
40
+ throw new SourcePressError(400, "INVALID_INPUT", "Missing source in form data");
41
+ }
42
+
43
+ const validSources = ["uploaded", "ai-generated", "scraped", "stock"] as const;
44
+ if (!validSources.includes(source as (typeof validSources)[number])) {
45
+ throw new SourcePressError(
46
+ 400,
47
+ "INVALID_INPUT",
48
+ `Invalid source. Must be one of: ${validSources.join(", ")}`,
49
+ );
50
+ }
51
+
52
+ const buffer = Buffer.from(await file.arrayBuffer());
53
+ const alt = formData.get("alt");
54
+ const generatedBy = formData.get("generated_by");
55
+ const prompt = formData.get("prompt");
56
+
57
+ const ref = await engine.media.upload({
58
+ file: buffer,
59
+ path,
60
+ content_type: file.type,
61
+ source: source as "uploaded" | "ai-generated" | "scraped" | "stock",
62
+ alt: typeof alt === "string" ? alt : undefined,
63
+ generated_by: typeof generatedBy === "string" ? generatedBy : undefined,
64
+ prompt: typeof prompt === "string" ? prompt : undefined,
65
+ uploaded_by: "api", // TODO: use auth context when available
66
+ });
67
+
68
+ return c.json(ref, 201);
69
+ });
70
+
71
+ // GET /media/:path — get metadata for a specific file
72
+ app.get("/media/:path{.+}", async (c) => {
73
+ if (!engine.media) {
74
+ throw new SourcePressError(501, "MEDIA_NOT_CONFIGURED", "Media storage not configured");
75
+ }
76
+
77
+ const path = c.req.param("path");
78
+ const meta = await engine.media.getMeta(path);
79
+
80
+ if (!meta) {
81
+ throw new SourcePressError(404, "MEDIA_NOT_FOUND", `Media not found: ${path}`);
82
+ }
83
+
84
+ return c.json(meta);
85
+ });
86
+
87
+ // PUT /media/:path — update metadata for a media file
88
+ app.put("/media/:path{.+}", async (c) => {
89
+ if (!engine.media) {
90
+ throw new SourcePressError(501, "MEDIA_NOT_CONFIGURED", "Media storage not configured");
91
+ }
92
+
93
+ const path = c.req.param("path");
94
+ const body = await c.req.json<{
95
+ alt?: string;
96
+ source?: "uploaded" | "ai-generated" | "scraped" | "stock";
97
+ generated_by?: string;
98
+ prompt?: string;
99
+ }>();
100
+
101
+ const updated = await engine.media.updateMeta(path, body);
102
+ return c.json(updated);
103
+ });
104
+
105
+ // DELETE /media/:path — delete a media file
106
+ app.delete("/media/:path{.+}", async (c) => {
107
+ if (!engine.media) {
108
+ throw new SourcePressError(501, "MEDIA_NOT_CONFIGURED", "Media storage not configured");
109
+ }
110
+
111
+ const path = c.req.param("path");
112
+ await engine.media.delete(path);
113
+ return c.json({ deleted: true, path });
114
+ });
115
+
116
+ return app;
117
+ }
@@ -0,0 +1,75 @@
1
+ import { collectionToZod } from "@sourcepress/core";
2
+ import { Hono } from "hono";
3
+ import type { EngineContext } from "../engine.js";
4
+ import { SourcePressError } from "../middleware/error-handler.js";
5
+ import { handleRouteError } from "../middleware/route-error-handler.js";
6
+
7
+ export function schemaRoutes(engine: EngineContext) {
8
+ const app = new Hono();
9
+
10
+ // GET /schema — full schema (all collections)
11
+ app.get("/schema", (c) => {
12
+ const collections = engine.listCollections();
13
+ const schema: Record<
14
+ string,
15
+ {
16
+ name: string;
17
+ path: string;
18
+ format: string;
19
+ fields: Record<string, unknown>;
20
+ }
21
+ > = {};
22
+
23
+ for (const name of collections) {
24
+ const def = engine.getCollectionDef(name);
25
+ if (def) {
26
+ schema[name] = {
27
+ name: def.name,
28
+ path: def.path,
29
+ format: def.format,
30
+ fields: def.fields,
31
+ };
32
+ }
33
+ }
34
+
35
+ return c.json({
36
+ collections: schema,
37
+ total: collections.length,
38
+ });
39
+ });
40
+
41
+ // GET /schema/:collection — schema for a single collection
42
+ app.get("/schema/:collection", (c) => {
43
+ const collection = c.req.param("collection");
44
+ const def = engine.getCollectionDef(collection);
45
+
46
+ if (!def) {
47
+ throw new SourcePressError(
48
+ 404,
49
+ "COLLECTION_NOT_FOUND",
50
+ `Collection "${collection}" not found`,
51
+ );
52
+ }
53
+
54
+ // Generate Zod schema description for reference
55
+ let zodDescription: string | null = null;
56
+ try {
57
+ const zodSchema = collectionToZod(def);
58
+ zodDescription = JSON.stringify(zodSchema.shape, null, 2);
59
+ } catch {
60
+ zodDescription = null;
61
+ }
62
+
63
+ return c.json({
64
+ name: def.name,
65
+ path: def.path,
66
+ format: def.format,
67
+ fields: def.fields,
68
+ zod_shape: zodDescription,
69
+ });
70
+ });
71
+
72
+ app.onError(handleRouteError);
73
+
74
+ return app;
75
+ }
@@ -0,0 +1,130 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { serve } from "@hono/node-server";
4
+ import type { SourcePressConfig } from "@sourcepress/core";
5
+ import { createApp } from "./app.js";
6
+ import { createEngine } from "./engine.js";
7
+
8
+ function loadEnv(dir: string): void {
9
+ const envPath = resolve(dir, ".env");
10
+ if (!existsSync(envPath)) return;
11
+ const content = readFileSync(envPath, "utf-8");
12
+ for (const line of content.split("\n")) {
13
+ const trimmed = line.trim();
14
+ if (!trimmed || trimmed.startsWith("#")) continue;
15
+ const eqIndex = trimmed.indexOf("=");
16
+ if (eqIndex === -1) continue;
17
+ const key = trimmed.slice(0, eqIndex).trim();
18
+ const value = trimmed.slice(eqIndex + 1).trim();
19
+ if (!process.env[key]) {
20
+ process.env[key] = value;
21
+ }
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: string): Promise<SourcePressConfig> {
27
+ const tsPath = resolve(dir, "sourcepress.config.ts");
28
+ const jsPath = resolve(dir, "sourcepress.config.js");
29
+
30
+ // Prefer .js (works without compilation) over .ts
31
+ const configPath = existsSync(jsPath) ? jsPath : existsSync(tsPath) ? tsPath : null;
32
+
33
+ if (configPath) {
34
+ try {
35
+ // Try direct import first (works if running via tsx or if .js)
36
+ const mod = await import(configPath);
37
+ return mod.default ?? mod;
38
+ } catch {
39
+ // If .ts import fails, try compiling with esbuild
40
+ if (configPath.endsWith(".ts")) {
41
+ try {
42
+ const { execFileSync } = await import("node:child_process");
43
+ const tmpJs = resolve(dir, ".sourcepress.config.tmp.mjs");
44
+ execFileSync(
45
+ "npx",
46
+ [
47
+ "esbuild",
48
+ configPath,
49
+ "--bundle",
50
+ "--format=esm",
51
+ "--platform=node",
52
+ `--outfile=${tmpJs}`,
53
+ "--external:@sourcepress/*",
54
+ ],
55
+ { stdio: "pipe" },
56
+ );
57
+ const mod = await import(tmpJs);
58
+ const { unlinkSync } = await import("node:fs");
59
+ unlinkSync(tmpJs);
60
+ return mod.default ?? mod;
61
+ } catch (e) {
62
+ console.warn(`Could not compile ${configPath}:`, e instanceof Error ? e.message : e);
63
+ }
64
+ }
65
+ console.warn(`Could not load ${configPath}, using env-based config`);
66
+ }
67
+ }
68
+
69
+ // Fallback: build config from env vars
70
+ return {
71
+ repository: {
72
+ owner: process.env.GITHUB_OWNER ?? "sourcepress",
73
+ repo: process.env.GITHUB_REPO ?? "demo",
74
+ branch: process.env.GITHUB_BRANCH ?? "main",
75
+ },
76
+ ai: {
77
+ provider: (process.env.AI_PROVIDER as "anthropic" | "openai" | "local") ?? "anthropic",
78
+ model: process.env.AI_MODEL ?? "claude-sonnet-4-5-20250514",
79
+ },
80
+ collections: {},
81
+ knowledge: { path: "knowledge/", graph: { backend: "local" } },
82
+ intent: { path: "intent/" },
83
+ };
84
+ }
85
+
86
+ async function main() {
87
+ const projectDir = process.argv[2] || process.cwd();
88
+ const port = Number(process.env.PORT ?? 4321);
89
+
90
+ // Load .env from project directory
91
+ loadEnv(projectDir);
92
+
93
+ const githubToken = process.env.GITHUB_TOKEN;
94
+ if (!githubToken) {
95
+ console.error("GITHUB_TOKEN is required. Set it in .env or as environment variable.");
96
+ process.exit(1);
97
+ }
98
+
99
+ const config = await loadConfig(projectDir);
100
+
101
+ console.log(`Project: ${projectDir}`);
102
+ console.log(`Repository: ${config.repository.owner}/${config.repository.repo}`);
103
+ console.log(`Collections: ${Object.keys(config.collections).join(", ") || "(none)"}`);
104
+
105
+ const apiKeysRaw = process.env.SOURCEPRESS_API_KEYS ?? process.env.API_KEYS;
106
+ const apiKeys = apiKeysRaw
107
+ ? apiKeysRaw
108
+ .split(",")
109
+ .map((k) => k.trim())
110
+ .filter(Boolean)
111
+ : undefined;
112
+
113
+ const engine = await createEngine({
114
+ config,
115
+ githubToken,
116
+ aiApiKey: process.env.ANTHROPIC_API_KEY ?? process.env.AI_API_KEY,
117
+ apiKeys,
118
+ });
119
+
120
+ const app = createApp(engine, apiKeys);
121
+
122
+ console.log(`\nSourcePress API running at http://localhost:${port}`);
123
+ console.log(`Health: http://localhost:${port}/api/health`);
124
+ console.log(`Schema: http://localhost:${port}/api/schema`);
125
+ console.log(`Content: http://localhost:${port}/api/content`);
126
+
127
+ serve({ fetch: app.fetch, port });
128
+ }
129
+
130
+ main();
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["src/__tests__/**/*.test.ts"],
6
+ },
7
+ });