@corgelabs/corge 0.0.1 → 1.0.1

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 (341) hide show
  1. package/dist/ai/index.d.ts +3 -0
  2. package/dist/ai/index.d.ts.map +1 -0
  3. package/dist/ai/index.js +3 -0
  4. package/dist/ai/index.js.map +1 -0
  5. package/dist/ai/schema.d.ts +83 -0
  6. package/dist/ai/schema.d.ts.map +1 -0
  7. package/dist/ai/schema.js +19 -0
  8. package/dist/ai/schema.js.map +1 -0
  9. package/dist/ai/service.d.ts +14 -0
  10. package/dist/ai/service.d.ts.map +1 -0
  11. package/dist/ai/service.js +266 -0
  12. package/dist/ai/service.js.map +1 -0
  13. package/dist/ai/utils.d.ts +10 -0
  14. package/dist/ai/utils.d.ts.map +1 -0
  15. package/dist/ai/utils.js +27 -0
  16. package/dist/ai/utils.js.map +1 -0
  17. package/dist/attachments/index.d.ts +4 -0
  18. package/dist/attachments/index.d.ts.map +1 -0
  19. package/dist/attachments/index.js +4 -0
  20. package/dist/attachments/index.js.map +1 -0
  21. package/dist/attachments/routes.d.ts +75 -0
  22. package/dist/attachments/routes.d.ts.map +1 -0
  23. package/dist/attachments/routes.js +18 -0
  24. package/dist/attachments/routes.js.map +1 -0
  25. package/dist/attachments/schema.d.ts +61 -0
  26. package/dist/attachments/schema.d.ts.map +1 -0
  27. package/dist/attachments/schema.js +30 -0
  28. package/dist/attachments/schema.js.map +1 -0
  29. package/dist/attachments/service.d.ts +27 -0
  30. package/dist/attachments/service.d.ts.map +1 -0
  31. package/dist/attachments/service.js +224 -0
  32. package/dist/attachments/service.js.map +1 -0
  33. package/dist/attachments/utils.d.ts +9 -0
  34. package/dist/attachments/utils.d.ts.map +1 -0
  35. package/dist/attachments/utils.js +80 -0
  36. package/dist/attachments/utils.js.map +1 -0
  37. package/dist/auth/index.d.ts +4 -0
  38. package/dist/auth/index.d.ts.map +1 -0
  39. package/dist/auth/index.js +4 -0
  40. package/dist/auth/index.js.map +1 -0
  41. package/dist/auth/oidc.d.ts +25 -0
  42. package/dist/auth/oidc.d.ts.map +1 -0
  43. package/dist/auth/oidc.js +298 -0
  44. package/dist/auth/oidc.js.map +1 -0
  45. package/dist/auth/recovery.d.ts +41 -0
  46. package/dist/auth/recovery.d.ts.map +1 -0
  47. package/dist/auth/recovery.js +250 -0
  48. package/dist/auth/recovery.js.map +1 -0
  49. package/dist/auth/routes.d.ts +226 -0
  50. package/dist/auth/routes.d.ts.map +1 -0
  51. package/dist/auth/routes.js +96 -0
  52. package/dist/auth/routes.js.map +1 -0
  53. package/dist/auth/schema.d.ts +158 -0
  54. package/dist/auth/schema.d.ts.map +1 -0
  55. package/dist/auth/schema.js +82 -0
  56. package/dist/auth/schema.js.map +1 -0
  57. package/dist/auth/service.d.ts +96 -0
  58. package/dist/auth/service.d.ts.map +1 -0
  59. package/dist/auth/service.js +129 -0
  60. package/dist/auth/service.js.map +1 -0
  61. package/dist/auth/session.d.ts +51 -0
  62. package/dist/auth/session.d.ts.map +1 -0
  63. package/dist/auth/session.js +344 -0
  64. package/dist/auth/session.js.map +1 -0
  65. package/dist/auth/utils.d.ts +17 -0
  66. package/dist/auth/utils.d.ts.map +1 -0
  67. package/dist/auth/utils.js +96 -0
  68. package/dist/auth/utils.js.map +1 -0
  69. package/dist/bin/corge.js +542 -0
  70. package/dist/chat/context.d.ts +10 -0
  71. package/dist/chat/context.d.ts.map +1 -0
  72. package/dist/chat/context.js +221 -0
  73. package/dist/chat/context.js.map +1 -0
  74. package/dist/chat/index.d.ts +4 -0
  75. package/dist/chat/index.d.ts.map +1 -0
  76. package/dist/chat/index.js +4 -0
  77. package/dist/chat/index.js.map +1 -0
  78. package/dist/chat/routes.d.ts +160 -0
  79. package/dist/chat/routes.d.ts.map +1 -0
  80. package/dist/chat/routes.js +210 -0
  81. package/dist/chat/routes.js.map +1 -0
  82. package/dist/chat/schema.d.ts +285 -0
  83. package/dist/chat/schema.d.ts.map +1 -0
  84. package/dist/chat/schema.js +74 -0
  85. package/dist/chat/schema.js.map +1 -0
  86. package/dist/chat/service.d.ts +64 -0
  87. package/dist/chat/service.d.ts.map +1 -0
  88. package/dist/chat/service.js +279 -0
  89. package/dist/chat/service.js.map +1 -0
  90. package/dist/chat/stream.d.ts +53 -0
  91. package/dist/chat/stream.d.ts.map +1 -0
  92. package/dist/chat/stream.js +339 -0
  93. package/dist/chat/stream.js.map +1 -0
  94. package/dist/chat/utils.d.ts +61 -0
  95. package/dist/chat/utils.d.ts.map +1 -0
  96. package/dist/chat/utils.js +285 -0
  97. package/dist/chat/utils.js.map +1 -0
  98. package/dist/cli/builder.d.ts +6 -0
  99. package/dist/cli/builder.d.ts.map +1 -0
  100. package/dist/cli/builder.js +91 -0
  101. package/dist/cli/builder.js.map +1 -0
  102. package/dist/cli/deployer.d.ts +2 -0
  103. package/dist/cli/deployer.d.ts.map +1 -0
  104. package/dist/cli/deployer.js +152 -0
  105. package/dist/cli/deployer.js.map +1 -0
  106. package/dist/cli/dev-server.d.ts +2 -0
  107. package/dist/cli/dev-server.d.ts.map +1 -0
  108. package/dist/cli/dev-server.js +70 -0
  109. package/dist/cli/dev-server.js.map +1 -0
  110. package/dist/cli/env.d.ts +4 -0
  111. package/dist/cli/env.d.ts.map +1 -0
  112. package/dist/cli/env.js +68 -0
  113. package/dist/cli/env.js.map +1 -0
  114. package/dist/cli/init.d.ts +6 -0
  115. package/dist/cli/init.d.ts.map +1 -0
  116. package/dist/cli/init.js +97 -0
  117. package/dist/cli/init.js.map +1 -0
  118. package/dist/config.d.ts +262 -0
  119. package/dist/config.d.ts.map +1 -0
  120. package/dist/config.js +212 -0
  121. package/dist/config.js.map +1 -0
  122. package/dist/db/index.d.ts +3 -0
  123. package/dist/db/index.d.ts.map +1 -0
  124. package/dist/db/index.js +3 -0
  125. package/dist/db/index.js.map +1 -0
  126. package/dist/db/schema.d.ts +410 -0
  127. package/dist/db/schema.d.ts.map +1 -0
  128. package/dist/db/schema.js +294 -0
  129. package/dist/db/schema.js.map +1 -0
  130. package/dist/db/service.d.ts +42 -0
  131. package/dist/db/service.d.ts.map +1 -0
  132. package/dist/db/service.js +372 -0
  133. package/dist/db/service.js.map +1 -0
  134. package/dist/db/utils.d.ts +25 -0
  135. package/dist/db/utils.d.ts.map +1 -0
  136. package/dist/db/utils.js +188 -0
  137. package/dist/db/utils.js.map +1 -0
  138. package/dist/emails/index.d.ts +4 -0
  139. package/dist/emails/index.d.ts.map +1 -0
  140. package/dist/emails/index.js +4 -0
  141. package/dist/emails/index.js.map +1 -0
  142. package/dist/emails/routes.d.ts +191 -0
  143. package/dist/emails/routes.d.ts.map +1 -0
  144. package/dist/emails/routes.js +39 -0
  145. package/dist/emails/routes.js.map +1 -0
  146. package/dist/emails/schema.d.ts +237 -0
  147. package/dist/emails/schema.d.ts.map +1 -0
  148. package/dist/emails/schema.js +98 -0
  149. package/dist/emails/schema.js.map +1 -0
  150. package/dist/emails/service.d.ts +22 -0
  151. package/dist/emails/service.d.ts.map +1 -0
  152. package/dist/emails/service.js +337 -0
  153. package/dist/emails/service.js.map +1 -0
  154. package/dist/emails/utils.d.ts +49 -0
  155. package/dist/emails/utils.d.ts.map +1 -0
  156. package/dist/emails/utils.js +341 -0
  157. package/dist/emails/utils.js.map +1 -0
  158. package/dist/files/index.d.ts +4 -0
  159. package/dist/files/index.d.ts.map +1 -0
  160. package/dist/files/index.js +4 -0
  161. package/dist/files/index.js.map +1 -0
  162. package/dist/files/routes.d.ts +72 -0
  163. package/dist/files/routes.d.ts.map +1 -0
  164. package/dist/files/routes.js +38 -0
  165. package/dist/files/routes.js.map +1 -0
  166. package/dist/files/schema.d.ts +57 -0
  167. package/dist/files/schema.d.ts.map +1 -0
  168. package/dist/files/schema.js +39 -0
  169. package/dist/files/schema.js.map +1 -0
  170. package/dist/files/service.d.ts +27 -0
  171. package/dist/files/service.d.ts.map +1 -0
  172. package/dist/files/service.js +175 -0
  173. package/dist/files/service.js.map +1 -0
  174. package/dist/files/utils.d.ts +3 -0
  175. package/dist/files/utils.d.ts.map +1 -0
  176. package/dist/files/utils.js +63 -0
  177. package/dist/files/utils.js.map +1 -0
  178. package/dist/index.d.ts +875 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +9919 -0
  181. package/dist/index.js.map +7 -0
  182. package/dist/payments/billing.d.ts +29 -0
  183. package/dist/payments/billing.d.ts.map +1 -0
  184. package/dist/payments/billing.js +378 -0
  185. package/dist/payments/billing.js.map +1 -0
  186. package/dist/payments/index.d.ts +4 -0
  187. package/dist/payments/index.d.ts.map +1 -0
  188. package/dist/payments/index.js +4 -0
  189. package/dist/payments/index.js.map +1 -0
  190. package/dist/payments/routes.d.ts +340 -0
  191. package/dist/payments/routes.d.ts.map +1 -0
  192. package/dist/payments/routes.js +56 -0
  193. package/dist/payments/routes.js.map +1 -0
  194. package/dist/payments/schema.d.ts +412 -0
  195. package/dist/payments/schema.d.ts.map +1 -0
  196. package/dist/payments/schema.js +260 -0
  197. package/dist/payments/schema.js.map +1 -0
  198. package/dist/payments/service.d.ts +31 -0
  199. package/dist/payments/service.d.ts.map +1 -0
  200. package/dist/payments/service.js +322 -0
  201. package/dist/payments/service.js.map +1 -0
  202. package/dist/payments/utils.d.ts +61 -0
  203. package/dist/payments/utils.d.ts.map +1 -0
  204. package/dist/payments/utils.js +369 -0
  205. package/dist/payments/utils.js.map +1 -0
  206. package/dist/router/guard.d.ts +19 -0
  207. package/dist/router/guard.d.ts.map +1 -0
  208. package/dist/router/guard.js +243 -0
  209. package/dist/router/guard.js.map +1 -0
  210. package/dist/router/index.d.ts +3 -0
  211. package/dist/router/index.d.ts.map +1 -0
  212. package/dist/router/index.js +3 -0
  213. package/dist/router/index.js.map +1 -0
  214. package/dist/router/schema.d.ts +103 -0
  215. package/dist/router/schema.d.ts.map +1 -0
  216. package/dist/router/schema.js +282 -0
  217. package/dist/router/schema.js.map +1 -0
  218. package/dist/router/service.d.ts +25 -0
  219. package/dist/router/service.d.ts.map +1 -0
  220. package/dist/router/service.js +339 -0
  221. package/dist/router/service.js.map +1 -0
  222. package/dist/tasks/index.d.ts +4 -0
  223. package/dist/tasks/index.d.ts.map +1 -0
  224. package/dist/tasks/index.js +4 -0
  225. package/dist/tasks/index.js.map +1 -0
  226. package/dist/tasks/routes.d.ts +54 -0
  227. package/dist/tasks/routes.d.ts.map +1 -0
  228. package/dist/tasks/routes.js +195 -0
  229. package/dist/tasks/routes.js.map +1 -0
  230. package/dist/tasks/schema.d.ts +261 -0
  231. package/dist/tasks/schema.d.ts.map +1 -0
  232. package/dist/tasks/schema.js +35 -0
  233. package/dist/tasks/schema.js.map +1 -0
  234. package/dist/tasks/service.d.ts +32 -0
  235. package/dist/tasks/service.d.ts.map +1 -0
  236. package/dist/tasks/service.js +201 -0
  237. package/dist/tasks/service.js.map +1 -0
  238. package/dist/tasks/steps.d.ts +18 -0
  239. package/dist/tasks/steps.d.ts.map +1 -0
  240. package/dist/tasks/steps.js +365 -0
  241. package/dist/tasks/steps.js.map +1 -0
  242. package/dist/tasks/utils.d.ts +75 -0
  243. package/dist/tasks/utils.d.ts.map +1 -0
  244. package/dist/tasks/utils.js +311 -0
  245. package/dist/tasks/utils.js.map +1 -0
  246. package/dist/tasks/watcher.d.ts +13 -0
  247. package/dist/tasks/watcher.d.ts.map +1 -0
  248. package/dist/tasks/watcher.js +255 -0
  249. package/dist/tasks/watcher.js.map +1 -0
  250. package/dist/tasks/worker.d.ts +28 -0
  251. package/dist/tasks/worker.d.ts.map +1 -0
  252. package/dist/tasks/worker.js +326 -0
  253. package/dist/tasks/worker.js.map +1 -0
  254. package/dist/utils/index.d.ts +3 -0
  255. package/dist/utils/index.d.ts.map +1 -0
  256. package/dist/utils/index.js +3 -0
  257. package/dist/utils/index.js.map +1 -0
  258. package/dist/utils/schema.d.ts +79 -0
  259. package/dist/utils/schema.d.ts.map +1 -0
  260. package/dist/utils/schema.js +41 -0
  261. package/dist/utils/schema.js.map +1 -0
  262. package/dist/utils/service.d.ts +32 -0
  263. package/dist/utils/service.d.ts.map +1 -0
  264. package/dist/utils/service.js +384 -0
  265. package/dist/utils/service.js.map +1 -0
  266. package/docs/SDK_SPEC.md +1645 -0
  267. package/infra/firestore.base.indexes.json +138 -0
  268. package/infra/firestore.base.rules +152 -0
  269. package/infra/storage.base.rules +8 -0
  270. package/package.json +58 -13
  271. package/src/ai/index.ts +2 -0
  272. package/src/ai/schema.ts +64 -0
  273. package/src/ai/service.ts +317 -0
  274. package/src/ai/utils.ts +38 -0
  275. package/src/attachments/index.ts +3 -0
  276. package/src/attachments/routes.ts +28 -0
  277. package/src/attachments/schema.ts +45 -0
  278. package/src/attachments/service.ts +315 -0
  279. package/src/attachments/utils.ts +115 -0
  280. package/src/auth/index.ts +3 -0
  281. package/src/auth/oidc.ts +419 -0
  282. package/src/auth/recovery.ts +365 -0
  283. package/src/auth/routes.ts +142 -0
  284. package/src/auth/schema.ts +181 -0
  285. package/src/auth/service.ts +183 -0
  286. package/src/auth/session.ts +414 -0
  287. package/src/auth/utils.ts +106 -0
  288. package/src/chat/context.ts +310 -0
  289. package/src/chat/index.ts +3 -0
  290. package/src/chat/routes.ts +264 -0
  291. package/src/chat/schema.ts +295 -0
  292. package/src/chat/service.ts +360 -0
  293. package/src/chat/stream.ts +481 -0
  294. package/src/chat/utils.ts +370 -0
  295. package/src/cli/builder.ts +93 -0
  296. package/src/cli/deployer.ts +184 -0
  297. package/src/cli/dev-server.ts +77 -0
  298. package/src/cli/env.ts +89 -0
  299. package/src/cli/init.ts +121 -0
  300. package/src/config.ts +447 -0
  301. package/src/db/index.ts +2 -0
  302. package/src/db/schema.ts +409 -0
  303. package/src/db/service.ts +477 -0
  304. package/src/db/utils.ts +250 -0
  305. package/src/emails/index.ts +3 -0
  306. package/src/emails/routes.ts +64 -0
  307. package/src/emails/schema.ts +239 -0
  308. package/src/emails/service.ts +488 -0
  309. package/src/emails/utils.ts +449 -0
  310. package/src/files/index.ts +3 -0
  311. package/src/files/routes.ts +44 -0
  312. package/src/files/schema.ts +76 -0
  313. package/src/files/service.ts +238 -0
  314. package/src/files/utils.ts +77 -0
  315. package/src/index.ts +425 -0
  316. package/src/payments/billing.ts +488 -0
  317. package/src/payments/index.ts +3 -0
  318. package/src/payments/routes.ts +92 -0
  319. package/src/payments/schema.ts +401 -0
  320. package/src/payments/service.ts +445 -0
  321. package/src/payments/utils.ts +472 -0
  322. package/src/router/guard.ts +319 -0
  323. package/src/router/index.ts +2 -0
  324. package/src/router/schema.ts +458 -0
  325. package/src/router/service.ts +453 -0
  326. package/src/tasks/index.ts +11 -0
  327. package/src/tasks/routes.ts +230 -0
  328. package/src/tasks/schema.ts +310 -0
  329. package/src/tasks/service.ts +254 -0
  330. package/src/tasks/steps.ts +472 -0
  331. package/src/tasks/utils.ts +445 -0
  332. package/src/tasks/watcher.ts +307 -0
  333. package/src/tasks/worker.ts +461 -0
  334. package/src/utils/index.ts +2 -0
  335. package/src/utils/schema.ts +69 -0
  336. package/src/utils/service.ts +461 -0
  337. package/tests/e2e.test.ts +2531 -0
  338. package/tsconfig.base.json +12 -0
  339. package/README.md +0 -2
  340. package/index.d.ts +0 -1
  341. package/index.js +0 -1
@@ -0,0 +1,370 @@
1
+ import { FieldValue, Timestamp, type DocumentReference, type Firestore } from 'firebase-admin/firestore'
2
+ import { createUIMessageStreamResponse, toUIMessageStream, type LanguageModelUsage, type ModelMessage, type streamText } from 'ai'
3
+ import { z } from 'zod'
4
+ import type { ProcessedAttachment } from '../attachments'
5
+ import type { EntryId } from '../db'
6
+ import type { FileRef } from '../files'
7
+ import type {
8
+ ChatMessagePublic,
9
+ ChatMessageRole,
10
+ ContextEntry,
11
+ ContextItemSpec,
12
+ DynamicContextBlockInput,
13
+ RawAttachmentPart,
14
+ UserContext,
15
+ streamChatBody
16
+ } from './schema'
17
+
18
+ export const FORBIDDEN_TOOL_SCHEMA_KEYS = new Set([
19
+ 'userId',
20
+ 'chatId',
21
+ 'role',
22
+ 'userRole',
23
+ 'token',
24
+ 'idToken',
25
+ 'apiKey',
26
+ 'session'
27
+ ])
28
+
29
+ export const sanitizeToolInputSchema = (schema: z.ZodType<any>): z.ZodType<any> => {
30
+ if (schema instanceof z.ZodObject) {
31
+ const shape = schema.shape as Record<string, z.ZodType<any>>
32
+ const keysToOmit: Record<string, true> = {}
33
+ let hasForbidden = false
34
+ for (const key of Object.keys(shape)) {
35
+ if (FORBIDDEN_TOOL_SCHEMA_KEYS.has(key)) {
36
+ keysToOmit[key] = true
37
+ hasForbidden = true
38
+ }
39
+ }
40
+ if (hasForbidden) {
41
+ return schema.omit(keysToOmit)
42
+ }
43
+ }
44
+ return schema
45
+ }
46
+
47
+ export const isUserContextShape = (value: unknown): value is UserContext =>
48
+ Boolean(value) &&
49
+ typeof value === 'object' &&
50
+ typeof (value as UserContext).state === 'object' &&
51
+ (value as UserContext).state !== null
52
+
53
+ export const formatAttachmentLine = (filename: string, item: ProcessedAttachment): string =>
54
+ 'error' in item
55
+ ? `[attached ${filename} (ref: ${item.ref}) (error: ${item.error})]\n`
56
+ : `[attached ${filename} (ref: ${item.ref}) (type: ${item.scan.type}) (fields: ${JSON.stringify(item.scan.fields ?? {})}) (markdown: ${item.scan.markdown ?? ''})]\n`
57
+
58
+ export const isSafePublicUrl = (raw?: string): boolean => {
59
+ if (!raw) return false
60
+ try {
61
+ const u = new URL(raw)
62
+ const host = u.hostname.toLowerCase()
63
+ return (
64
+ (u.protocol === 'https:' || u.protocol === 'http:') &&
65
+ !/^(\d+|0x[0-9a-f]+)$/i.test(host) &&
66
+ !/^(localhost|127\.|10\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|0\.|metadata\.|\[(::1?|fc|fd|fe80:|::ffff:))/i.test(
67
+ host
68
+ ) &&
69
+ !/(\.internal|\.local|\.nip\.io|\.sslip\.io)\.?$/i.test(host)
70
+ )
71
+ } catch {
72
+ return false
73
+ }
74
+ }
75
+
76
+ export const extractPart = (p: Record<string, unknown>): RawAttachmentPart | undefined => {
77
+ if (!p || typeof p !== 'object' || (p.type !== 'file' && !('url' in p))) return undefined
78
+ const url = String(p.url ?? '')
79
+ const dataUrl = url.startsWith('data:') ? url : undefined
80
+ const ref = url.match(/^\/api\/files\/([A-Za-z0-9_-]+_[a-f0-9]{24})$/)?.[1]
81
+ return dataUrl || ref ? { dataUrl, ref, filename: typeof p.filename === 'string' ? p.filename : 'file' } : undefined
82
+ }
83
+
84
+ export const extractTurnAttachmentParts = (msg: z.infer<typeof streamChatBody>['message']): RawAttachmentPart[] => {
85
+ const fromParts = (msg.parts ?? [])
86
+ .map((p) => extractPart(p))
87
+ .filter((p): p is RawAttachmentPart => Boolean(p))
88
+ const seenRefs = new Set(fromParts.map((p) => p.ref).filter(Boolean))
89
+ const fromRefs: RawAttachmentPart[] = (msg.attachments ?? [])
90
+ .filter((ref) => !seenRefs.has(ref))
91
+ .map((ref) => ({ ref, filename: ref }))
92
+ return [...fromParts, ...fromRefs]
93
+ }
94
+
95
+ export const extractMessageText = (msg: { content?: unknown; parts?: readonly unknown[] }): string => {
96
+ if (typeof msg.content === 'string' && msg.content.trim()) return msg.content.trim()
97
+ const items = (Array.isArray(msg.content) ? msg.content : Array.isArray(msg.parts) ? msg.parts : []) as Record<
98
+ string,
99
+ unknown
100
+ >[]
101
+ return items
102
+ .filter((p) => p && typeof p === 'object' && p.type === 'text' && typeof p.text === 'string')
103
+ .map((p) => (p.text as string).trim())
104
+ .filter(Boolean)
105
+ .join('\n')
106
+ }
107
+
108
+ export const deriveTitle = (
109
+ msgOrList: { content?: unknown; parts?: readonly unknown[]; role?: string } | readonly Record<string, unknown>[]
110
+ ): string => {
111
+ const list = Array.isArray(msgOrList) ? msgOrList : [msgOrList]
112
+ for (const m of list) {
113
+ if (m.role && m.role !== 'user') continue
114
+ const text = extractMessageText(m as { content?: unknown; parts?: readonly unknown[] })
115
+ if (text) return text.slice(0, 80)
116
+ }
117
+ return 'New Chat'
118
+ }
119
+
120
+ export const extractLatestUserPrompt = (messages: readonly (Record<string, unknown> | ModelMessage)[]): string => {
121
+ for (let i = messages.length - 1; i >= 0; i--) {
122
+ const m = messages[i] as Record<string, unknown>
123
+ if (m?.role !== 'user') continue
124
+ const text = extractMessageText(m as { content?: unknown; parts?: readonly unknown[] })
125
+ if (text) return text
126
+ }
127
+ return ''
128
+ }
129
+
130
+ export const formatUsage = (usage: LanguageModelUsage | null) => ({
131
+ prompt_tokens: usage?.inputTokens ?? 0,
132
+ completion_tokens: usage?.outputTokens ?? 0,
133
+ total_tokens: usage?.totalTokens ?? (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0)
134
+ })
135
+
136
+ export const isContextItemVisible = (item: ContextItemSpec, audiences?: readonly string[]): boolean =>
137
+ !audiences || !item.audiences || item.audiences.some((a) => audiences.includes(a))
138
+
139
+ export const formatContextBlock = (entries: readonly ContextEntry[]): string => {
140
+ if (entries.length === 0) return ''
141
+ const sources = entries
142
+ .map(
143
+ (entry) =>
144
+ `<source path=${JSON.stringify(entry.path)} description=${JSON.stringify(entry.description)}>\n${entry.markdown.trim()}\n</source>`
145
+ )
146
+ .join('\n')
147
+ return `<infobase_context>\n${sources}\n</infobase_context>`
148
+ }
149
+
150
+ export const buildDynamicContextBlock = (input: DynamicContextBlockInput): string => {
151
+ const sections: string[] = [`Current UTC Date: ${input.currentTimeIso ?? new Date().toISOString()}`]
152
+
153
+ if (input.userContext) {
154
+ sections.push(`<user_state>\n${JSON.stringify(input.userContext.state)}\n</user_state>`)
155
+ if (input.userContext.details && Object.keys(input.userContext.details).length > 0) {
156
+ sections.push(`<user_records>\n${JSON.stringify(input.userContext.details)}\n</user_records>`)
157
+ }
158
+ }
159
+
160
+ if (input.recalledMemories && input.recalledMemories.length > 0) {
161
+ const items = input.recalledMemories
162
+ .map(
163
+ (m) =>
164
+ `<memory scope=${JSON.stringify(m.scope)} chatId=${JSON.stringify(m.chatId)} role=${JSON.stringify(m.role)}>\n${m.content.trim()}\n</memory>`
165
+ )
166
+ .join('\n')
167
+ sections.push(`<recalled_memory>\n${items}\n</recalled_memory>`)
168
+ }
169
+
170
+ if (input.infobaseContextEntries && input.infobaseContextEntries.length > 0) {
171
+ const block = formatContextBlock(input.infobaseContextEntries)
172
+ if (block) sections.push(block)
173
+ }
174
+
175
+ if (input.stagedAttachmentSummaries && input.stagedAttachmentSummaries.length > 0) {
176
+ sections.push(`<staged_attachments>\n${input.stagedAttachmentSummaries.join('\n').trim()}\n</staged_attachments>`)
177
+ }
178
+
179
+ return sections.join('\n\n')
180
+ }
181
+
182
+ export const PUBLIC_MESSAGE_FIELDS = [
183
+ 'id',
184
+ 'chatId',
185
+ 'userId',
186
+ 'role',
187
+ 'content',
188
+ 'parts',
189
+ 'toolInvocations',
190
+ 'attachments',
191
+ 'tokenCount',
192
+ 'cancelled',
193
+ 'createdAt'
194
+ ] as const
195
+
196
+ export const RECALL_MESSAGE_FIELDS = ['id', 'chatId', 'role', 'content'] as const
197
+
198
+ export const mapFirestoreMessageDoc = (
199
+ id: string,
200
+ raw: Record<string, unknown>
201
+ ): ChatMessagePublic => {
202
+ const createdRaw = raw.createdAt
203
+ const createdAt =
204
+ createdRaw instanceof Timestamp
205
+ ? createdRaw.toDate()
206
+ : createdRaw instanceof Date
207
+ ? createdRaw
208
+ : typeof createdRaw === 'string' || typeof createdRaw === 'number'
209
+ ? new Date(createdRaw)
210
+ : new Date()
211
+
212
+ const role: ChatMessageRole =
213
+ raw.role === 'assistant' || raw.role === 'system' || raw.role === 'tool' ? raw.role : 'user'
214
+ const content = typeof raw.content === 'string' ? raw.content : ''
215
+ const parts = Array.isArray(raw.parts) ? raw.parts : content ? [{ type: 'text', text: content }] : []
216
+
217
+ return {
218
+ id,
219
+ chatId: typeof raw.chatId === 'string' ? raw.chatId : '',
220
+ userId: typeof raw.userId === 'string' ? raw.userId : '',
221
+ role,
222
+ content,
223
+ parts,
224
+ ...(Array.isArray(raw.toolInvocations) ? { toolInvocations: raw.toolInvocations } : {}),
225
+ ...(Array.isArray(raw.attachments) ? { attachments: raw.attachments as string[] } : {}),
226
+ ...(typeof raw.tokenCount === 'number' ? { tokenCount: raw.tokenCount } : {}),
227
+ ...(typeof raw.cancelled === 'boolean' ? { cancelled: raw.cancelled } : {}),
228
+ createdAt
229
+ }
230
+ }
231
+
232
+ export const toModelMessageFromDoc = (doc: ChatMessagePublic): ModelMessage | undefined => {
233
+ const text = doc.content.trim()
234
+ if (!text) return undefined
235
+ if (doc.role === 'assistant') {
236
+ return { role: 'assistant', content: [{ type: 'text', text }] }
237
+ }
238
+ if (doc.role === 'user') {
239
+ return { role: 'user', content: [{ type: 'text', text }] }
240
+ }
241
+ return undefined
242
+ }
243
+
244
+ export const extractToolInvocations = (messages: readonly { content: unknown }[]): unknown[] => {
245
+ const toolInvocations: unknown[] = []
246
+ for (const m of messages) {
247
+ if (!Array.isArray(m.content)) continue
248
+ for (const part of m.content) {
249
+ if (part && typeof part === 'object' && 'type' in part && (part.type === 'tool-call' || part.type === 'tool-result')) {
250
+ toolInvocations.push(part)
251
+ }
252
+ }
253
+ }
254
+ return toolInvocations
255
+ }
256
+
257
+ export const buildUIMessageResponse = (
258
+ turn: ReturnType<typeof streamText>,
259
+ chatId: string,
260
+ generationId: string,
261
+ completionPromise: Promise<void>,
262
+ onDelta: (delta: string) => void
263
+ ): Response => {
264
+ const baseRes = createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: turn.stream }) })
265
+ const headers = new Headers(baseRes.headers)
266
+ headers.set('x-chat-id', chatId)
267
+ headers.set('x-generation-id', generationId)
268
+
269
+ const decoder = new TextDecoder()
270
+ const [clientBranch, monitorBranch] = baseRes.body!.tee()
271
+
272
+ void (async () => {
273
+ const reader = monitorBranch.getReader()
274
+ try {
275
+ while (true) {
276
+ const { done, value } = await reader.read()
277
+ if (done) break
278
+ for (const line of decoder.decode(value, { stream: true }).split('\n')) {
279
+ if (!line.startsWith('data: ')) continue
280
+ try {
281
+ const payload = JSON.parse(line.slice(6)) as { type?: string; delta?: string; textDelta?: string }
282
+ if (payload.type === 'text-delta') onDelta(payload.delta ?? payload.textDelta ?? '')
283
+ } catch {}
284
+ }
285
+ }
286
+ } catch {
287
+ } finally {
288
+ reader.releaseLock()
289
+ }
290
+ })()
291
+
292
+ const syncedClientStream = clientBranch.pipeThrough(
293
+ new TransformStream<Uint8Array, Uint8Array>({
294
+ async flush() {
295
+ await completionPromise.catch(() => {})
296
+ }
297
+ })
298
+ )
299
+
300
+ return new Response(syncedClientStream, { status: baseRes.status, headers })
301
+ }
302
+
303
+ export const buildCompletedTurnWriteBatch = (
304
+ db: Firestore,
305
+ opts: {
306
+ chatDocRef: DocumentReference
307
+ chatId: string
308
+ userId: EntryId
309
+ resolvedAgentId: string
310
+ title: string
311
+ selectedModel: string
312
+ userMsgId: string
313
+ assistantMsgId: string
314
+ userCreatedAtMs: number
315
+ finalUserText: string
316
+ userPartsForStorage: unknown[]
317
+ stagedRefs: FileRef[]
318
+ finalAssistantText: string
319
+ toolInvocations: unknown[]
320
+ totalTokens?: number
321
+ cancelled: boolean
322
+ userVector: number[] | null
323
+ assistantVector: number[] | null
324
+ }
325
+ ) => {
326
+ const messagesCol = opts.chatDocRef.collection('messages')
327
+ const batch = db.batch()
328
+ batch.set(messagesCol.doc(opts.userMsgId), {
329
+ id: opts.userMsgId,
330
+ chatId: opts.chatId,
331
+ userId: opts.userId,
332
+ role: 'user',
333
+ content: opts.finalUserText,
334
+ parts: opts.userPartsForStorage,
335
+ ...(opts.stagedRefs.length > 0 ? { attachments: opts.stagedRefs } : {}),
336
+ createdAt: Timestamp.fromMillis(opts.userCreatedAtMs),
337
+ ...(opts.userVector ? { embedding: FieldValue.vector(opts.userVector) } : {})
338
+ })
339
+
340
+ batch.set(messagesCol.doc(opts.assistantMsgId), {
341
+ id: opts.assistantMsgId,
342
+ chatId: opts.chatId,
343
+ userId: opts.userId,
344
+ role: 'assistant',
345
+ content: opts.finalAssistantText,
346
+ parts: opts.finalAssistantText ? [{ type: 'text', text: opts.finalAssistantText }] : [],
347
+ ...(opts.toolInvocations.length > 0 ? { toolInvocations: opts.toolInvocations } : {}),
348
+ ...(opts.totalTokens !== undefined ? { tokenCount: opts.totalTokens } : {}),
349
+ cancelled: opts.cancelled,
350
+ createdAt: Timestamp.fromMillis(Math.max(opts.userCreatedAtMs + 1, Date.now())),
351
+ ...(opts.assistantVector ? { embedding: FieldValue.vector(opts.assistantVector) } : {})
352
+ })
353
+
354
+ batch.set(
355
+ opts.chatDocRef,
356
+ {
357
+ id: opts.chatId,
358
+ userId: opts.userId,
359
+ agentId: opts.resolvedAgentId,
360
+ title: opts.title,
361
+ model: opts.selectedModel,
362
+ deleted: false,
363
+ activeGenerationId: null,
364
+ messageCount: FieldValue.increment(2),
365
+ updatedAt: FieldValue.serverTimestamp()
366
+ },
367
+ { merge: true }
368
+ )
369
+ return batch
370
+ }
@@ -0,0 +1,93 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import { dirname, join, resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { build, type Plugin } from 'esbuild'
6
+
7
+ export const getCorgeRootDir = (): string => {
8
+ const currentFile = fileURLToPath(import.meta.url)
9
+ const currentDir = dirname(currentFile)
10
+ for (const candidate of [
11
+ resolve(currentDir, '../..'),
12
+ resolve(currentDir, '..')
13
+ ]) {
14
+ if (existsSync(join(candidate, 'package.json')) && existsSync(join(candidate, 'tsconfig.base.json'))) {
15
+ return candidate
16
+ }
17
+ }
18
+ return resolve(currentDir, '../..')
19
+ }
20
+
21
+ export const resolveCorgeEntrypoint = (cwd = process.cwd()): string => {
22
+ try {
23
+ const req = createRequire(join(cwd, 'package.json'))
24
+ return req.resolve('@corgelabs/corge')
25
+ } catch {
26
+ const root = getCorgeRootDir()
27
+ const distEntry = join(root, 'dist/index.js')
28
+ return existsSync(distEntry) ? distEntry : join(root, 'src/index.ts')
29
+ }
30
+ }
31
+
32
+ const resolveWithExtensions = (basePath: string): string | undefined => {
33
+ for (const candidate of [
34
+ basePath,
35
+ `${basePath}.ts`,
36
+ `${basePath}.js`,
37
+ join(basePath, 'index.ts'),
38
+ join(basePath, 'index.js')
39
+ ]) {
40
+ if (existsSync(candidate)) return candidate
41
+ }
42
+ return undefined
43
+ }
44
+
45
+ export const corgeAliasPlugin = (cwd = process.cwd()): Plugin => ({
46
+ name: 'corge-alias',
47
+ setup(b) {
48
+ b.onResolve({ filter: /^(@core|@corgelabs\/corge)$/ }, () => ({
49
+ path: resolveCorgeEntrypoint(cwd),
50
+ external: false
51
+ }))
52
+
53
+ b.onResolve({ filter: /^@(config|routes|services|utils)\/(.+)$/ }, (args) => {
54
+ const match = /^@(config|routes|services|utils)\/(.+)$/.exec(args.path)
55
+ if (!match) return undefined
56
+ const [, folder, rest] = match
57
+ const target = resolveWithExtensions(join(cwd, 'src', folder!, rest!))
58
+ if (target) return { path: target, external: false }
59
+ return undefined
60
+ })
61
+
62
+ b.onResolve({ filter: /^@app-entry$/ }, () => {
63
+ const target = resolveWithExtensions(join(cwd, 'src/index'))
64
+ if (target) return { path: target, external: false }
65
+ return { path: 'corge-fallback-app', namespace: 'corge-virtual' }
66
+ })
67
+
68
+ b.onLoad({ filter: /.*/, namespace: 'corge-virtual' }, () => ({
69
+ contents: [
70
+ "import { core } from '@core'",
71
+ 'export const { devApp, app, functions } = core.createApp({})'
72
+ ].join('\n'),
73
+ loader: 'ts',
74
+ resolveDir: cwd
75
+ }))
76
+ }
77
+ })
78
+
79
+ export const buildConsumerApp = async (cwd = process.cwd()): Promise<void> => {
80
+ await build({
81
+ absWorkingDir: cwd,
82
+ entryPoints: ['src/index.ts'],
83
+ bundle: true,
84
+ platform: 'node',
85
+ target: 'node24',
86
+ format: 'cjs',
87
+ packages: 'external',
88
+ plugins: [corgeAliasPlugin(cwd)],
89
+ footer: { js: 'module.exports = functions;' },
90
+ outfile: 'dist/index.cjs',
91
+ sourcemap: true
92
+ })
93
+ }
@@ -0,0 +1,184 @@
1
+ import { execSync, spawnSync } from 'node:child_process'
2
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { buildConsumerApp, getCorgeRootDir } from './builder'
5
+
6
+ interface FirestoreIndexesFile {
7
+ indexes?: unknown[]
8
+ fieldOverrides?: unknown[]
9
+ }
10
+
11
+ const mergeFirestoreIndexes = (base: FirestoreIndexesFile, custom: FirestoreIndexesFile): FirestoreIndexesFile => {
12
+ const seenIndexes = new Set<string>()
13
+ const mergedIndexes: unknown[] = []
14
+ for (const item of [...(base.indexes ?? []), ...(custom.indexes ?? [])]) {
15
+ const key = JSON.stringify(item)
16
+ if (!seenIndexes.has(key)) {
17
+ seenIndexes.add(key)
18
+ mergedIndexes.push(item)
19
+ }
20
+ }
21
+
22
+ const seenOverrides = new Set<string>()
23
+ const mergedOverrides: unknown[] = []
24
+ for (const item of [...(base.fieldOverrides ?? []), ...(custom.fieldOverrides ?? [])]) {
25
+ const key = JSON.stringify(item)
26
+ if (!seenOverrides.has(key)) {
27
+ seenOverrides.add(key)
28
+ mergedOverrides.push(item)
29
+ }
30
+ }
31
+
32
+ return {
33
+ indexes: mergedIndexes,
34
+ fieldOverrides: mergedOverrides
35
+ }
36
+ }
37
+
38
+ export const deployConsumerApp = async (
39
+ target: 'prod' | 'dev' | undefined,
40
+ cwd = process.cwd()
41
+ ): Promise<void> => {
42
+ try {
43
+ process.loadEnvFile(join(cwd, '.env'))
44
+ } catch {}
45
+
46
+ if (target !== 'prod' && target !== 'dev') {
47
+ console.error('Usage: corge deploy <prod|dev>')
48
+ process.exit(1)
49
+ }
50
+
51
+ const requiredBranch = target === 'prod' ? 'main' : 'dev'
52
+ const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf8' }).trim()
53
+
54
+ if (currentBranch !== requiredBranch) {
55
+ console.error(
56
+ `Deploy blocked: "deploy ${target}" can only run on branch "${requiredBranch}" (current branch: "${currentBranch}").`
57
+ )
58
+ process.exit(1)
59
+ }
60
+
61
+ const project = process.env.GCP_PROJECT
62
+ if (!project) {
63
+ console.error('Missing GCP_PROJECT in .env')
64
+ process.exit(1)
65
+ }
66
+
67
+ await buildConsumerApp(cwd)
68
+
69
+ const corgeRoot = getCorgeRootDir()
70
+ const firebaseJsonPath = join(cwd, 'firebase.json')
71
+ const pkgJsonPath = join(cwd, 'package.json')
72
+ const firestoreRulesPath = join(cwd, 'firestore.rules')
73
+ const firestoreIndexesPath = join(cwd, 'firestore.indexes.json')
74
+ const storageRulesPath = join(cwd, 'storage.rules')
75
+
76
+ const rawFirebaseJson = readFileSync(firebaseJsonPath, 'utf8')
77
+ const rawPkgJson = readFileSync(pkgJsonPath, 'utf8')
78
+ const hadFirestoreRules = existsSync(firestoreRulesPath)
79
+ const rawFirestoreRules = hadFirestoreRules ? readFileSync(firestoreRulesPath, 'utf8') : null
80
+ const hadFirestoreIndexes = existsSync(firestoreIndexesPath)
81
+ const rawFirestoreIndexes = hadFirestoreIndexes ? readFileSync(firestoreIndexesPath, 'utf8') : null
82
+ const hadStorageRules = existsSync(storageRulesPath)
83
+ const rawStorageRules = hadStorageRules ? readFileSync(storageRulesPath, 'utf8') : null
84
+
85
+ const restoreFiles = () => {
86
+ try {
87
+ writeFileSync(firebaseJsonPath, rawFirebaseJson, 'utf8')
88
+ } catch {}
89
+ try {
90
+ writeFileSync(pkgJsonPath, rawPkgJson, 'utf8')
91
+ } catch {}
92
+ try {
93
+ if (hadFirestoreRules && rawFirestoreRules !== null) {
94
+ writeFileSync(firestoreRulesPath, rawFirestoreRules, 'utf8')
95
+ } else if (!hadFirestoreRules && existsSync(firestoreRulesPath)) {
96
+ unlinkSync(firestoreRulesPath)
97
+ }
98
+ } catch {}
99
+ try {
100
+ if (hadFirestoreIndexes && rawFirestoreIndexes !== null) {
101
+ writeFileSync(firestoreIndexesPath, rawFirestoreIndexes, 'utf8')
102
+ } else if (!hadFirestoreIndexes && existsSync(firestoreIndexesPath)) {
103
+ unlinkSync(firestoreIndexesPath)
104
+ }
105
+ } catch {}
106
+ try {
107
+ if (hadStorageRules && rawStorageRules !== null) {
108
+ writeFileSync(storageRulesPath, rawStorageRules, 'utf8')
109
+ } else if (!hadStorageRules && existsSync(storageRulesPath)) {
110
+ unlinkSync(storageRulesPath)
111
+ }
112
+ } catch {}
113
+ }
114
+
115
+ process.once('SIGINT', () => {
116
+ restoreFiles()
117
+ process.exit(130)
118
+ })
119
+ process.once('SIGTERM', () => {
120
+ restoreFiles()
121
+ process.exit(143)
122
+ })
123
+
124
+ try {
125
+ // 1. Provision or merge base Firestore/Storage rules & composite indexes from @corgelabs/corge/infra
126
+ const baseFirestoreRules = readFileSync(join(corgeRoot, 'infra/firestore.base.rules'), 'utf8')
127
+ const baseStorageRules = readFileSync(join(corgeRoot, 'infra/storage.base.rules'), 'utf8')
128
+ const baseIndexes = JSON.parse(
129
+ readFileSync(join(corgeRoot, 'infra/firestore.base.indexes.json'), 'utf8')
130
+ ) as FirestoreIndexesFile
131
+
132
+ if (!hadFirestoreRules) {
133
+ writeFileSync(firestoreRulesPath, baseFirestoreRules, 'utf8')
134
+ }
135
+ if (!hadStorageRules) {
136
+ writeFileSync(storageRulesPath, baseStorageRules, 'utf8')
137
+ }
138
+
139
+ const customIndexes = hadFirestoreIndexes && rawFirestoreIndexes
140
+ ? (JSON.parse(rawFirestoreIndexes) as FirestoreIndexesFile)
141
+ : {}
142
+ const mergedIndexes = mergeFirestoreIndexes(baseIndexes, customIndexes)
143
+ writeFileSync(firestoreIndexesPath, JSON.stringify(mergedIndexes, null, 2) + '\n', 'utf8')
144
+
145
+ // 2. Rewrite firebase.json for target environment
146
+ const cfg = JSON.parse(rawFirebaseJson) as Record<string, unknown>
147
+ const prodDb = { database: project, rules: 'firestore.rules', indexes: 'firestore.indexes.json' }
148
+ const devDb = { database: `${project}-dev`, rules: 'firestore.rules', indexes: 'firestore.indexes.json' }
149
+ const prodBucket = { bucket: `${project}.firebasestorage.app`, rules: 'storage.rules' }
150
+ const devBucket = { bucket: `${project}-dev`, rules: 'storage.rules' }
151
+
152
+ cfg.firestore = target === 'prod' ? [prodDb] : [devDb]
153
+ cfg.storage = target === 'prod' ? [prodBucket] : [devBucket]
154
+ writeFileSync(firebaseJsonPath, JSON.stringify(cfg, null, 2) + '\n', 'utf8')
155
+
156
+ // 3. Expand @corgelabs/corge runtime dependencies into consumer package.json so Cloud Build needs zero npm auth
157
+ const corgePkg = JSON.parse(readFileSync(join(corgeRoot, 'package.json'), 'utf8')) as {
158
+ dependencies?: Record<string, string>
159
+ }
160
+ const consumerPkg = JSON.parse(rawPkgJson) as {
161
+ dependencies?: Record<string, string>
162
+ [k: string]: unknown
163
+ }
164
+ const expandedDeps: Record<string, string> = { ...(corgePkg.dependencies ?? {}), ...(consumerPkg.dependencies ?? {}) }
165
+ delete expandedDeps['@corgelabs/corge']
166
+ delete expandedDeps.esbuild
167
+ consumerPkg.dependencies = expandedDeps
168
+ writeFileSync(pkgJsonPath, JSON.stringify(consumerPkg, null, 2) + '\n', 'utf8')
169
+
170
+ const onlyTargets =
171
+ target === 'prod'
172
+ ? `functions:${project},functions:${project}-tasks,firestore:${project},storage`
173
+ : `functions:${project}-dev,functions:${project}-tasks-dev,firestore:${project}-dev,storage`
174
+
175
+ const deployRes = spawnSync(
176
+ 'npx',
177
+ ['-y', 'firebase-tools@latest', 'deploy', '-P', project, '--only', onlyTargets],
178
+ { cwd, stdio: 'inherit' }
179
+ )
180
+ process.exitCode = deployRes.status ?? 1
181
+ } finally {
182
+ restoreFiles()
183
+ }
184
+ }