@usequark/quark-create-app 1.23.11

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 (237) hide show
  1. package/README.md +210 -0
  2. package/package.json +56 -0
  3. package/src/dependabot-config.test.js +128 -0
  4. package/src/deploy/adapters/iac.js +203 -0
  5. package/src/deploy/adapters/index.js +27 -0
  6. package/src/deploy/adapters/railway.js +508 -0
  7. package/src/deploy/adapters/railway.test.js +318 -0
  8. package/src/deploy/contract.js +59 -0
  9. package/src/deploy/deploy.integration.test.js +183 -0
  10. package/src/deploy/deploy.js +430 -0
  11. package/src/deploy/deploy.test.js +105 -0
  12. package/src/deploy/discovery.js +115 -0
  13. package/src/deploy/discovery.test.js +141 -0
  14. package/src/deploy/index.js +38 -0
  15. package/src/deploy/inspect.js +130 -0
  16. package/src/index.js +2920 -0
  17. package/src/mobile-template.test.js +232 -0
  18. package/src/prepare-hooks.test.js +535 -0
  19. package/src/scaffold-guards.test.js +140 -0
  20. package/src/scaffold-output.test.js +228 -0
  21. package/src/template-scripts.test.js +160 -0
  22. package/src/utils.js +77 -0
  23. package/src/utils.test.js +95 -0
  24. package/src/worker-dockerfile.test.js +85 -0
  25. package/templates/base-project/.cursor/rules/quark.mdc +172 -0
  26. package/templates/base-project/.dockerignore +14 -0
  27. package/templates/base-project/.env.railway.example +87 -0
  28. package/templates/base-project/.github/copilot-instructions.md +65 -0
  29. package/templates/base-project/.github/dependabot.yml +31 -0
  30. package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
  31. package/templates/base-project/.github/workflows/ci.yml +116 -0
  32. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  33. package/templates/base-project/.github/workflows/install-windows.yml +27 -0
  34. package/templates/base-project/.github/workflows/release.yml +67 -0
  35. package/templates/base-project/.railway/railway.ts +40 -0
  36. package/templates/base-project/CLAUDE.md +163 -0
  37. package/templates/base-project/MAIN.md +21 -0
  38. package/templates/base-project/README.md +164 -0
  39. package/templates/base-project/apps/web/.env.example +99 -0
  40. package/templates/base-project/apps/web/Dockerfile +38 -0
  41. package/templates/base-project/apps/web/biome.json +8 -0
  42. package/templates/base-project/apps/web/jsconfig.json +9 -0
  43. package/templates/base-project/apps/web/next.config.js +154 -0
  44. package/templates/base-project/apps/web/package.json +42 -0
  45. package/templates/base-project/apps/web/postcss.config.mjs +7 -0
  46. package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
  47. package/templates/base-project/apps/web/public/quark.svg +47 -0
  48. package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
  49. package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
  50. package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
  51. package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
  52. package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
  53. package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
  54. package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
  55. package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
  56. package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
  57. package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
  58. package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
  59. package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
  60. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
  61. package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
  62. package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
  63. package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
  64. package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
  65. package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
  66. package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
  67. package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
  68. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
  69. package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
  70. package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
  71. package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
  72. package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
  73. package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
  74. package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
  75. package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
  76. package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
  77. package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
  78. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  79. package/templates/base-project/apps/web/src/app/global-error.js +58 -0
  80. package/templates/base-project/apps/web/src/app/globals.css +1166 -0
  81. package/templates/base-project/apps/web/src/app/icon.svg +47 -0
  82. package/templates/base-project/apps/web/src/app/layout.js +57 -0
  83. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  84. package/templates/base-project/apps/web/src/app/not-found.js +39 -0
  85. package/templates/base-project/apps/web/src/app/page.js +262 -0
  86. package/templates/base-project/apps/web/src/app/robots.js +23 -0
  87. package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
  88. package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
  89. package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
  90. package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
  91. package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
  92. package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
  93. package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
  94. package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
  95. package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
  96. package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
  97. package/templates/base-project/apps/web/src/lib/auth.js +191 -0
  98. package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
  99. package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
  100. package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
  101. package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
  102. package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
  103. package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
  104. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
  105. package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
  106. package/templates/base-project/apps/web/src/preflight.js +55 -0
  107. package/templates/base-project/apps/web/src/proxy.js +328 -0
  108. package/templates/base-project/biome.json +52 -0
  109. package/templates/base-project/docker-compose.override.yml +17 -0
  110. package/templates/base-project/docker-compose.yml +37 -0
  111. package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
  112. package/templates/base-project/docs/START_HERE.md +70 -0
  113. package/templates/base-project/docs/adr/README.md +42 -0
  114. package/templates/base-project/package.json +58 -0
  115. package/templates/base-project/packages/db/package.json +38 -0
  116. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
  117. package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
  118. package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
  119. package/templates/base-project/packages/db/prisma/seed.js +152 -0
  120. package/templates/base-project/packages/db/prisma.config.js +27 -0
  121. package/templates/base-project/packages/db/src/client.js +75 -0
  122. package/templates/base-project/packages/db/src/connection.js +44 -0
  123. package/templates/base-project/packages/db/src/context.js +275 -0
  124. package/templates/base-project/packages/db/src/index.js +5 -0
  125. package/templates/base-project/packages/db/src/ping.js +66 -0
  126. package/templates/base-project/packages/db/src/queries.js +292 -0
  127. package/templates/base-project/packages/db/src/schemas.js +30 -0
  128. package/templates/base-project/packages/db/src/umami.js +299 -0
  129. package/templates/base-project/pnpm-workspace.yaml +20 -0
  130. package/templates/base-project/scripts/check-loading.mjs +49 -0
  131. package/templates/base-project/scripts/check-standards.mjs +186 -0
  132. package/templates/base-project/scripts/clean-workspace.mjs +479 -0
  133. package/templates/base-project/scripts/dev-preflight.mjs +48 -0
  134. package/templates/base-project/scripts/doctor.js +408 -0
  135. package/templates/base-project/scripts/prepare.js +490 -0
  136. package/templates/base-project/scripts/run-tests.mjs +71 -0
  137. package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
  138. package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
  139. package/templates/base-project/skills/add-model/SKILL.md +28 -0
  140. package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
  141. package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
  142. package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
  143. package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
  144. package/templates/base-project/skills/i18n/SKILL.md +211 -0
  145. package/templates/base-project/skills/payment/SKILL.md +280 -0
  146. package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
  147. package/templates/base-project/turbo.json +143 -0
  148. package/templates/config/package.json +20 -0
  149. package/templates/config/src/app-url.js +134 -0
  150. package/templates/config/src/environment.js +284 -0
  151. package/templates/config/src/index.js +16 -0
  152. package/templates/config/src/load-config.js +157 -0
  153. package/templates/config/src/rate-limit.js +136 -0
  154. package/templates/config/src/redis.js +43 -0
  155. package/templates/config/src/validate-env.js +532 -0
  156. package/templates/jobs/README.md +52 -0
  157. package/templates/jobs/package.json +9 -0
  158. package/templates/jobs/src/definitions.js +13 -0
  159. package/templates/jobs/src/index.js +1 -0
  160. package/templates/mobile/.env.example +16 -0
  161. package/templates/mobile/CHECKLIST.md +40 -0
  162. package/templates/mobile/README.md +130 -0
  163. package/templates/mobile/app/(app)/_layout.tsx +83 -0
  164. package/templates/mobile/app/(app)/index.tsx +147 -0
  165. package/templates/mobile/app/(app)/profile.tsx +204 -0
  166. package/templates/mobile/app/(app)/settings.tsx +80 -0
  167. package/templates/mobile/app/(auth)/_layout.tsx +10 -0
  168. package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
  169. package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
  170. package/templates/mobile/app/+not-found.tsx +32 -0
  171. package/templates/mobile/app/_layout.tsx +18 -0
  172. package/templates/mobile/app.json +47 -0
  173. package/templates/mobile/assets/adaptive-icon.png +0 -0
  174. package/templates/mobile/assets/icon.png +0 -0
  175. package/templates/mobile/assets/notification-icon.png +0 -0
  176. package/templates/mobile/assets/splash.png +0 -0
  177. package/templates/mobile/biome.json +27 -0
  178. package/templates/mobile/components/AuthGate.tsx +19 -0
  179. package/templates/mobile/components/ThemeProvider.tsx +23 -0
  180. package/templates/mobile/eas.json +29 -0
  181. package/templates/mobile/hooks/use-auth.ts +127 -0
  182. package/templates/mobile/lib/api-client.ts +138 -0
  183. package/templates/mobile/lib/api.ts +83 -0
  184. package/templates/mobile/lib/auth-providers-google.ts +74 -0
  185. package/templates/mobile/lib/auth-providers.ts +74 -0
  186. package/templates/mobile/lib/auth.ts +100 -0
  187. package/templates/mobile/lib/config.ts +14 -0
  188. package/templates/mobile/lib/errors.ts +46 -0
  189. package/templates/mobile/lib/notifications.ts +77 -0
  190. package/templates/mobile/lib/storage.ts +25 -0
  191. package/templates/mobile/lib/theme-tokens.ts +16 -0
  192. package/templates/mobile/package.json +37 -0
  193. package/templates/mobile/tsconfig.json +10 -0
  194. package/templates/pwa/app/_components/PWARegister.js +15 -0
  195. package/templates/pwa/app/manifest.js +32 -0
  196. package/templates/pwa/public/sw.js +72 -0
  197. package/templates/ui/README.md +44 -0
  198. package/templates/ui/package.json +21 -0
  199. package/templates/ui/src/badge.js +23 -0
  200. package/templates/ui/src/button.js +111 -0
  201. package/templates/ui/src/card.js +128 -0
  202. package/templates/ui/src/checkbox.js +20 -0
  203. package/templates/ui/src/container.js +13 -0
  204. package/templates/ui/src/dialog.js +137 -0
  205. package/templates/ui/src/error-banner.js +15 -0
  206. package/templates/ui/src/footer.js +239 -0
  207. package/templates/ui/src/form-field.integration-test.js +142 -0
  208. package/templates/ui/src/form-field.js +50 -0
  209. package/templates/ui/src/index.js +24 -0
  210. package/templates/ui/src/input.js +11 -0
  211. package/templates/ui/src/label.js +14 -0
  212. package/templates/ui/src/lightbox.integration-test.js +167 -0
  213. package/templates/ui/src/lightbox.js +243 -0
  214. package/templates/ui/src/logo.js +52 -0
  215. package/templates/ui/src/navbar.js +602 -0
  216. package/templates/ui/src/password-input.js +31 -0
  217. package/templates/ui/src/rich-text.js +514 -0
  218. package/templates/ui/src/select.js +317 -0
  219. package/templates/ui/src/skeleton.js +10 -0
  220. package/templates/ui/src/spinner.js +31 -0
  221. package/templates/ui/src/table.js +169 -0
  222. package/templates/ui/src/textarea.js +11 -0
  223. package/templates/ui/src/theme-constants.js +24 -0
  224. package/templates/ui/src/theme.js +196 -0
  225. package/templates/ui/src/toast.js +206 -0
  226. package/templates/worker/Dockerfile +64 -0
  227. package/templates/worker/README.md +56 -0
  228. package/templates/worker/package.json +36 -0
  229. package/templates/worker/src/config/pricing.js +46 -0
  230. package/templates/worker/src/handlers/email-job-validation.js +36 -0
  231. package/templates/worker/src/handlers/email.js +98 -0
  232. package/templates/worker/src/handlers/files.js +59 -0
  233. package/templates/worker/src/handlers/index.js +20 -0
  234. package/templates/worker/src/handlers/push-notification.js +234 -0
  235. package/templates/worker/src/index.js +505 -0
  236. package/templates/worker/src/lib/railway.js +438 -0
  237. package/templates/worker/src/lib/tokens.js +81 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared connection string builder for PostgreSQL.
3
+ * Used by both the Prisma client (client.js) and Prisma CLI (prisma.config.js).
4
+ *
5
+ * Priority: DATABASE_URL env var → assembled from POSTGRES_* env vars.
6
+ *
7
+ * @param {{ throwOnMissing?: boolean }} [options]
8
+ * - throwOnMissing=true (default) - runtime: throws if credentials are missing.
9
+ * - throwOnMissing=false - CLI/CI: returns a placeholder URL so `prisma generate` works.
10
+ * @returns {string} PostgreSQL connection string
11
+ */
12
+ export function getConnectionString({ throwOnMissing = true } = {}) {
13
+ // 1. Prefer an explicit DATABASE_URL if set
14
+ if (process.env.DATABASE_URL) {
15
+ return process.env.DATABASE_URL;
16
+ }
17
+
18
+ // 2. Assemble from individual POSTGRES_* vars
19
+ const user = process.env.POSTGRES_USER;
20
+ const password = process.env.POSTGRES_PASSWORD;
21
+ const host = process.env.POSTGRES_HOST || "localhost";
22
+ const port = process.env.POSTGRES_PORT || "5432";
23
+ const db = process.env.POSTGRES_DB;
24
+
25
+ const hasCredentials = user && password && db;
26
+
27
+ if (!hasCredentials) {
28
+ if (throwOnMissing) {
29
+ const missing = [];
30
+ if (!user) missing.push("POSTGRES_USER");
31
+ if (!password) missing.push("POSTGRES_PASSWORD");
32
+ if (!db) missing.push("POSTGRES_DB");
33
+ throw new Error(
34
+ `Missing required database environment variables: ${missing.join(", ")}. ` +
35
+ "Set DATABASE_URL or the individual POSTGRES_* variables.",
36
+ );
37
+ }
38
+
39
+ // Placeholder for prisma generate / CI - will fail at connect time, not at config time.
40
+ return "postgresql://placeholder:placeholder@localhost:5432/placeholder?schema=public";
41
+ }
42
+
43
+ return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${encodeURIComponent(db)}?schema=public`;
44
+ }
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Context Retrieval Utility
3
+ *
4
+ * Generic, project-agnostic utility for retrieving relevant business context.
5
+ * Designed to be portable - can be copied to any Quark project with minimal changes.
6
+ *
7
+ * Features:
8
+ * - getRelevantContext(question, records) - filters context by keyword relevance
9
+ * - summarizeContext(records) - brief category overview
10
+ * - shouldProcess(input, previousHash) - dedup check for extraction
11
+ */
12
+
13
+ // ── Default Keyword Map ──────────────────────────────────────────────────────
14
+ // Maps question keywords to BusinessContext categories.
15
+ // Generic enough for any project - customize by extending or replacing.
16
+ const DEFAULT_KEYWORD_MAP = {
17
+ billing: [
18
+ "billing",
19
+ "invoice",
20
+ "pay",
21
+ "payment",
22
+ "money",
23
+ "cost",
24
+ "revenue",
25
+ "price",
26
+ "$",
27
+ "rate",
28
+ "fee",
29
+ "subscription",
30
+ "plan",
31
+ "charge",
32
+ ],
33
+ client: [
34
+ "client",
35
+ "customer",
36
+ "company",
37
+ "business",
38
+ "organization",
39
+ "project",
40
+ "account",
41
+ "partner",
42
+ "vendor",
43
+ ],
44
+ task: [
45
+ "task",
46
+ "todo",
47
+ "overdue",
48
+ "blocked",
49
+ "progress",
50
+ "status",
51
+ "assign",
52
+ "priority",
53
+ "deadline",
54
+ "due",
55
+ "complete",
56
+ "pipeline",
57
+ ],
58
+ tech_note: [
59
+ "tech",
60
+ "config",
61
+ "deploy",
62
+ "server",
63
+ "integration",
64
+ "api",
65
+ "code",
66
+ "bug",
67
+ "fix",
68
+ "setup",
69
+ "migration",
70
+ "database",
71
+ ],
72
+ process: [
73
+ "process",
74
+ "policy",
75
+ "rule",
76
+ "workflow",
77
+ "how",
78
+ "procedure",
79
+ "guideline",
80
+ "step",
81
+ "method",
82
+ "standard",
83
+ ],
84
+ preference: [
85
+ "prefer",
86
+ "like",
87
+ "want",
88
+ "style",
89
+ "format",
90
+ "communication",
91
+ "contact",
92
+ "schedule",
93
+ "frequency",
94
+ ],
95
+ };
96
+
97
+ // ── Relevance Scoring ────────────────────────────────────────────────────────
98
+
99
+ /**
100
+ * Scores a context record's relevance to a question.
101
+ * @param {Object} record - { key, value, category }
102
+ * @param {string} question - The user's question (lowercased)
103
+ * @param {Object} keywordMap - Custom keyword map (optional)
104
+ * @returns {number} Relevance score (0 = irrelevant, higher = more relevant)
105
+ */
106
+ function scoreRelevance(record, question, keywordMap = DEFAULT_KEYWORD_MAP) {
107
+ let score = 0;
108
+ const lowerQuestion = question;
109
+
110
+ // Score 1: Category matches question keywords
111
+ const categoryKeywords = keywordMap[record.category];
112
+ if (categoryKeywords) {
113
+ for (const kw of categoryKeywords) {
114
+ if (lowerQuestion.includes(kw)) {
115
+ score += 2;
116
+ }
117
+ }
118
+ }
119
+
120
+ // Score 2: Key words match question
121
+ const keyWords = record.key.toLowerCase().split(/[.\s_-]+/);
122
+ for (const word of keyWords) {
123
+ if (word.length > 2 && lowerQuestion.includes(word)) {
124
+ score += 3;
125
+ }
126
+ }
127
+
128
+ // Score 3: Value contains question keywords (partial match)
129
+ const valueLower = (record.value || "").toLowerCase();
130
+ const questionWords = lowerQuestion.split(/\s+/).filter((w) => w.length > 3);
131
+ for (const word of questionWords) {
132
+ if (valueLower.includes(word)) {
133
+ score += 1;
134
+ }
135
+ }
136
+
137
+ return score;
138
+ }
139
+
140
+ // ── Public API ───────────────────────────────────────────────────────────────
141
+
142
+ /**
143
+ * Filters context records to only those relevant to a question.
144
+ * Returns records with score > 0, sorted by relevance (highest first).
145
+ * If no records score > 0, returns a summary instead.
146
+ *
147
+ * @param {string} question - The user's question
148
+ * @param {Array<Object>} records - BusinessContext records [{ key, value, category, source }]
149
+ * @param {Object} [options]
150
+ * @param {number} [options.maxRecords=15] - Max records to return
151
+ * @param {number} [options.minScore=1] - Minimum relevance score
152
+ * @param {Object} [options.keywordMap] - Custom keyword map
153
+ * @returns {Object} { records: Array, summary: string|null }
154
+ */
155
+ export function getRelevantContext(question, records, options = {}) {
156
+ const {
157
+ maxRecords = 15,
158
+ minScore = 1,
159
+ keywordMap = DEFAULT_KEYWORD_MAP,
160
+ } = options;
161
+
162
+ if (!question || !records || records.length === 0) {
163
+ return { records: [], summary: "No business context available." };
164
+ }
165
+
166
+ const lowerQuestion = question.toLowerCase();
167
+
168
+ // Score all records
169
+ const scored = records.map((r) => ({
170
+ ...r,
171
+ _score: scoreRelevance(r, lowerQuestion, keywordMap),
172
+ }));
173
+
174
+ // Filter by minimum score
175
+ const relevant = scored
176
+ .filter((r) => r._score >= minScore)
177
+ .sort((a, b) => b._score - a._score)
178
+ .slice(0, maxRecords);
179
+
180
+ // Clean up internal score field
181
+ const cleanRecords = relevant.map(({ _score, ...r }) => r);
182
+
183
+ if (cleanRecords.length > 0) {
184
+ return { records: cleanRecords, summary: null };
185
+ }
186
+
187
+ // No relevant records found - return a summary instead
188
+ return {
189
+ records: [],
190
+ summary: summarizeContext(records),
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Creates a brief category summary of all context records.
196
+ * Tells the AI what's available without overwhelming it with details.
197
+ *
198
+ * @param {Array<Object>} records
199
+ * @returns {string}
200
+ */
201
+ export function summarizeContext(records) {
202
+ if (!records || records.length === 0) {
203
+ return "No business context available.";
204
+ }
205
+
206
+ const byCategory = {};
207
+ for (const r of records) {
208
+ if (!byCategory[r.category]) byCategory[r.category] = [];
209
+ byCategory[r.category].push(r);
210
+ }
211
+
212
+ const totalManual = records.filter((r) => r.source === "manual").length;
213
+ const totalSeed = records.filter((r) => r.source === "seed").length;
214
+
215
+ let summary = `Available business context (${records.length} records, ${totalSeed} curated + ${totalManual} manual):\n`;
216
+
217
+ for (const [category, entries] of Object.entries(byCategory)) {
218
+ const label = category.charAt(0).toUpperCase() + category.slice(1);
219
+ const manual = entries.filter((e) => e.source === "manual").length;
220
+ const seed = entries.filter((e) => e.source === "seed").length;
221
+ summary += `- **${label}**: ${entries.length} records`;
222
+ if (seed > 0) summary += ` (${seed} curated, ${manual} manual)`;
223
+ summary += "\n";
224
+ }
225
+
226
+ summary +=
227
+ "\nUse `getRelevantContext` with a specific question to retrieve detailed context.";
228
+
229
+ return summary;
230
+ }
231
+
232
+ /**
233
+ * Creates a deterministic hash of input data for dedup checking.
234
+ * Returns null if input is empty.
235
+ *
236
+ * @param {string} input - The data to hash (e.g., conversation text, task data)
237
+ * @returns {string|null} Hex hash string, or null if empty
238
+ */
239
+ export function hashInput(input) {
240
+ if (!input || typeof input !== "string" || input.trim().length === 0) {
241
+ return null;
242
+ }
243
+
244
+ // Simple hash function (no crypto dependency needed)
245
+ let hash = 0;
246
+ const str = input.trim();
247
+ for (let i = 0; i < str.length; i++) {
248
+ const char = str.charCodeAt(i);
249
+ hash = (hash << 5) - hash + char;
250
+ hash |= 0; // Convert to 32bit integer
251
+ }
252
+ return Math.abs(hash).toString(16);
253
+ }
254
+
255
+ /**
256
+ * Checks whether an input should be processed, based on a previous hash.
257
+ * Useful for skipping extraction when nothing has changed.
258
+ *
259
+ * @param {string} input - The current input data
260
+ * @param {string|null} previousHash - The hash from the last successful processing
261
+ * @returns {Object} { shouldProcess: boolean, hash: string|null }
262
+ */
263
+ export function shouldProcess(input, previousHash) {
264
+ const currentHash = hashInput(input);
265
+
266
+ if (!currentHash) {
267
+ return { shouldProcess: false, hash: null };
268
+ }
269
+
270
+ if (currentHash === previousHash) {
271
+ return { shouldProcess: false, hash: currentHash };
272
+ }
273
+
274
+ return { shouldProcess: true, hash: currentHash };
275
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./client.js";
2
+ export * from "./context.js";
3
+ export * from "./ping.js";
4
+ export * from "./queries.js";
5
+ export * from "./schemas.js";
@@ -0,0 +1,66 @@
1
+ import { getConnectionString } from "./connection.js";
2
+
3
+ /**
4
+ * Pings PostgreSQL to verify connectivity.
5
+ * Uses a raw `pg.Client` directly so the check is immune to Prisma
6
+ * adapter quirks - in particular, Prisma 7 + @prisma/adapter-pg surfaces
7
+ * connection failures as a misleading "Invalid invocation" error rather
8
+ * than a proper ECONNREFUSED.
9
+ *
10
+ * @param {object} [options]
11
+ * @param {number} [options.timeout=3000] - Connection/query timeout in milliseconds.
12
+ * @returns {Promise<{ status: "ok", latencyMs: number } | { status: "error", message: string }>}
13
+ */
14
+ export async function pingDatabase({ timeout = 3000 } = {}) {
15
+ /** @type {import("pg").Client | null} */
16
+ let client = null;
17
+ let connectionString;
18
+
19
+ try {
20
+ const { default: pg } = await import("pg");
21
+ connectionString = getConnectionString();
22
+
23
+ client = new pg.Client({
24
+ connectionString,
25
+ connectionTimeoutMillis: timeout,
26
+ query_timeout: timeout,
27
+ });
28
+
29
+ await client.connect();
30
+
31
+ const start = performance.now();
32
+ await client.query("SELECT 1");
33
+ const latencyMs = Math.round(performance.now() - start);
34
+
35
+ return { status: "ok", latencyMs };
36
+ } catch (/** @type {any} */ error) {
37
+ let message;
38
+ if (
39
+ error?.code === "MODULE_NOT_FOUND" ||
40
+ error?.code === "ERR_MODULE_NOT_FOUND"
41
+ ) {
42
+ message = "pg is not installed";
43
+ } else if (
44
+ error?.code === "ECONNREFUSED" ||
45
+ /ECONNREFUSED/i.test(error?.message ?? "")
46
+ ) {
47
+ try {
48
+ const { hostname, port } = new URL(connectionString);
49
+ message = `PostgreSQL unreachable at ${hostname}:${port || "5432"}`;
50
+ } catch {
51
+ message = "PostgreSQL unreachable";
52
+ }
53
+ } else if (error?.code === "ENOTFOUND") {
54
+ message = `PostgreSQL host not found: ${error.hostname ?? "unknown"}`;
55
+ } else {
56
+ message = error?.message ?? String(error);
57
+ }
58
+ return { status: "error", message };
59
+ } finally {
60
+ try {
61
+ await client?.end();
62
+ } catch {
63
+ // ignore disconnect errors
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,292 @@
1
+ import { prisma } from "./client.js";
2
+
3
+ /**
4
+ * Safe select for user queries - excludes sensitive fields (password).
5
+ * Use this on any query whose result is returned to the client.
6
+ */
7
+ const USER_SAFE_SELECT = {
8
+ id: true,
9
+ email: true,
10
+ emailVerified: true,
11
+ name: true,
12
+ image: true,
13
+ role: true,
14
+ createdAt: true,
15
+ updatedAt: true,
16
+ };
17
+
18
+ export { USER_SAFE_SELECT };
19
+
20
+ // User queries
21
+ export const user = {
22
+ findById: (id) => {
23
+ return prisma.user.findUnique({
24
+ where: { id },
25
+ select: USER_SAFE_SELECT,
26
+ });
27
+ },
28
+
29
+ /**
30
+ * findByEmail returns ALL fields including password.
31
+ * Only use for internal auth - never expose the result directly to clients.
32
+ */
33
+ findByEmail: (email) => {
34
+ return prisma.user.findUnique({
35
+ where: { email },
36
+ });
37
+ },
38
+ findAll: (options = {}) => {
39
+ const { skip = 0, take = 10 } = options;
40
+ return prisma.user.findMany({
41
+ skip,
42
+ take,
43
+ select: USER_SAFE_SELECT,
44
+ orderBy: { createdAt: "desc" },
45
+ });
46
+ },
47
+ create: (data) => {
48
+ return prisma.user.create({
49
+ data,
50
+ select: USER_SAFE_SELECT,
51
+ });
52
+ },
53
+ update: (id, data) => {
54
+ return prisma.user.update({
55
+ where: { id },
56
+ data,
57
+ select: USER_SAFE_SELECT,
58
+ });
59
+ },
60
+ delete: (id) => {
61
+ return prisma.user.delete({
62
+ where: { id },
63
+ });
64
+ },
65
+ count: () => {
66
+ return prisma.user.count();
67
+ },
68
+ };
69
+
70
+ // Job queries
71
+ export const job = {
72
+ findAll: (options = {}) => {
73
+ const { skip = 0, take = 50, where = {} } = options;
74
+ return prisma.job.findMany({
75
+ skip,
76
+ take,
77
+ where,
78
+ orderBy: { createdAt: "desc" },
79
+ });
80
+ },
81
+
82
+ findById: (id) => {
83
+ return prisma.job.findUnique({ where: { id } });
84
+ },
85
+
86
+ upsert: (id, data) => {
87
+ return prisma.job.upsert({
88
+ where: { id },
89
+ create: { id, ...data },
90
+ update: data,
91
+ });
92
+ },
93
+ };
94
+
95
+ // Account queries (NextAuth)
96
+ export const account = {
97
+ findById: (id) => {
98
+ return prisma.account.findUnique({
99
+ where: { id },
100
+ });
101
+ },
102
+ findByUserIdAndProvider: (userId, provider) => {
103
+ return prisma.account.findFirst({
104
+ where: { userId, provider },
105
+ });
106
+ },
107
+ findByUser: (userId) => {
108
+ return prisma.account.findMany({
109
+ where: { userId },
110
+ });
111
+ },
112
+ create: (data) => {
113
+ return prisma.account.create({
114
+ data,
115
+ });
116
+ },
117
+ delete: (id) => {
118
+ return prisma.account.delete({
119
+ where: { id },
120
+ });
121
+ },
122
+ };
123
+
124
+ // Session queries (NextAuth)
125
+ export const session = {
126
+ findByToken: (sessionToken) => {
127
+ return prisma.session.findUnique({
128
+ where: { sessionToken },
129
+ include: { user: { select: USER_SAFE_SELECT } },
130
+ });
131
+ },
132
+ findByUserId: (userId) => {
133
+ return prisma.session.findMany({
134
+ where: { userId },
135
+ });
136
+ },
137
+ create: (data) => {
138
+ return prisma.session.create({
139
+ data,
140
+ include: { user: { select: USER_SAFE_SELECT } },
141
+ });
142
+ },
143
+ update: (sessionToken, data) => {
144
+ return prisma.session.update({
145
+ where: { sessionToken },
146
+ data,
147
+ });
148
+ },
149
+ delete: (sessionToken) => {
150
+ return prisma.session.delete({
151
+ where: { sessionToken },
152
+ });
153
+ },
154
+ deleteByUserId: (userId) => {
155
+ return prisma.session.deleteMany({
156
+ where: { userId },
157
+ });
158
+ },
159
+ };
160
+
161
+ // Verification Token queries (NextAuth)
162
+ export const verificationToken = {
163
+ findByToken: (token) => {
164
+ return prisma.verificationToken.findUnique({
165
+ where: { token },
166
+ });
167
+ },
168
+ findByIdentifierAndToken: (identifier, token) => {
169
+ return prisma.verificationToken.findUnique({
170
+ where: { identifier_token: { identifier, token } },
171
+ });
172
+ },
173
+ create: (data) => {
174
+ return prisma.verificationToken.create({
175
+ data,
176
+ });
177
+ },
178
+ delete: (identifier, token) => {
179
+ return prisma.verificationToken.deleteMany({
180
+ where: { identifier, token },
181
+ });
182
+ },
183
+ deleteExpired: () => {
184
+ return prisma.verificationToken.deleteMany({
185
+ where: { expires: { lt: new Date() } },
186
+ });
187
+ },
188
+ };
189
+
190
+ // AuditLog queries
191
+ export const auditLog = {
192
+ findAll: (options = {}) => {
193
+ const { skip = 0, take = 50 } = options;
194
+ return prisma.auditLog.findMany({
195
+ skip,
196
+ take,
197
+ include: { user: { select: { id: true, email: true, name: true } } },
198
+ orderBy: { createdAt: "desc" },
199
+ });
200
+ },
201
+ findByUserId: (userId, options = {}) => {
202
+ const { skip = 0, take = 50 } = options;
203
+ return prisma.auditLog.findMany({
204
+ where: { userId },
205
+ skip,
206
+ take,
207
+ include: { user: { select: { id: true, email: true, name: true } } },
208
+ orderBy: { createdAt: "desc" },
209
+ });
210
+ },
211
+ findByEntity: (entity, options = {}) => {
212
+ const { skip = 0, take = 50 } = options;
213
+ return prisma.auditLog.findMany({
214
+ where: { entity },
215
+ skip,
216
+ take,
217
+ include: { user: { select: { id: true, email: true, name: true } } },
218
+ orderBy: { createdAt: "desc" },
219
+ });
220
+ },
221
+ findByAction: (action, options = {}) => {
222
+ const { skip = 0, take = 50 } = options;
223
+ return prisma.auditLog.findMany({
224
+ where: { action },
225
+ skip,
226
+ take,
227
+ include: { user: { select: { id: true, email: true, name: true } } },
228
+ orderBy: { createdAt: "desc" },
229
+ });
230
+ },
231
+ create: (data) => {
232
+ return prisma.auditLog.create({
233
+ data,
234
+ include: { user: { select: { id: true, email: true, name: true } } },
235
+ });
236
+ },
237
+ };
238
+
239
+ // File queries
240
+ export const file = {
241
+ create: (data) => {
242
+ return prisma.file.create({ data });
243
+ },
244
+ findById: (id) => {
245
+ return prisma.file.findUnique({
246
+ where: { id },
247
+ include: {
248
+ uploadedBy: { select: { id: true, email: true, name: true } },
249
+ },
250
+ });
251
+ },
252
+ findByStorageKey: (storageKey) => {
253
+ return prisma.file.findUnique({ where: { storageKey } });
254
+ },
255
+ findByUploader: (uploadedById, options = {}) => {
256
+ const { skip = 0, take = 50 } = options;
257
+ return prisma.file.findMany({
258
+ where: { uploadedById },
259
+ skip,
260
+ take,
261
+ orderBy: { createdAt: "desc" },
262
+ });
263
+ },
264
+ findOrphaned: (options = {}) => {
265
+ const { take = 100 } = options;
266
+ return prisma.file.findMany({
267
+ where: { uploadedById: null },
268
+ take,
269
+ orderBy: { createdAt: "asc" },
270
+ });
271
+ },
272
+ findOlderThan: (date, options = {}) => {
273
+ const { take = 100 } = options;
274
+ return prisma.file.findMany({
275
+ where: {
276
+ uploadedById: null,
277
+ createdAt: { lt: date },
278
+ },
279
+ take,
280
+ orderBy: { createdAt: "asc" },
281
+ });
282
+ },
283
+ delete: (id) => {
284
+ return prisma.file.delete({ where: { id } });
285
+ },
286
+ deleteMany: (ids) => {
287
+ return prisma.file.deleteMany({ where: { id: { in: ids } } });
288
+ },
289
+ count: (where = {}) => {
290
+ return prisma.file.count({ where });
291
+ },
292
+ };
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+
3
+ export const userCreateSchema = z.object({
4
+ email: z.string().email(),
5
+ name: z.string().optional(),
6
+ image: z.string().url().optional(),
7
+ });
8
+
9
+ export const userRegisterSchema = z.object({
10
+ email: z.string().email("Invalid email address"),
11
+ password: z
12
+ .string()
13
+ .min(8, "Password must be at least 8 characters")
14
+ .regex(/[A-Z]/, "Password must contain at least one uppercase letter")
15
+ .regex(/[a-z]/, "Password must contain at least one lowercase letter")
16
+ .regex(/[0-9]/, "Password must contain at least one number"),
17
+ name: z.string().min(2, "Name must be at least 2 characters").optional(),
18
+ });
19
+
20
+ export const userUpdateSchema = z.object({
21
+ email: z.string().email().optional(),
22
+ name: z.string().optional(),
23
+ image: z.string().url().optional(),
24
+ });
25
+
26
+ export const fileUploadSchema = z.object({
27
+ filename: z.string().min(1, "Filename is required"),
28
+ mimeType: z.string().min(1, "MIME type is required"),
29
+ size: z.number().int().positive("File size must be positive"),
30
+ });