@flink-app/flink 0.14.3 → 2.0.0-alpha.100

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 (280) hide show
  1. package/CHANGELOG.md +1051 -0
  2. package/SCHEMA_EXTRACTION_ANALYSIS.md +494 -0
  3. package/SIMPLE_AST_FEASIBILITY.md +570 -0
  4. package/bin/flink.ts +13 -2
  5. package/cli/build.ts +24 -44
  6. package/cli/clean.ts +13 -25
  7. package/cli/cli-utils.ts +190 -17
  8. package/cli/dev.ts +252 -0
  9. package/cli/loadEnvFiles.ts +116 -0
  10. package/cli/run.ts +45 -62
  11. package/dist/bin/flink.js +61 -2
  12. package/dist/cli/build.js +20 -25
  13. package/dist/cli/clean.js +12 -10
  14. package/dist/cli/cli-utils.d.ts +34 -3
  15. package/dist/cli/cli-utils.js +193 -12
  16. package/dist/cli/dev.d.ts +2 -0
  17. package/dist/cli/dev.js +279 -0
  18. package/dist/cli/loadEnvFiles.d.ts +30 -0
  19. package/dist/cli/loadEnvFiles.js +113 -0
  20. package/dist/cli/run.js +47 -46
  21. package/dist/src/DependencyTracker.d.ts +44 -0
  22. package/dist/src/DependencyTracker.js +239 -0
  23. package/dist/src/FlinkApp.d.ts +163 -10
  24. package/dist/src/FlinkApp.js +847 -184
  25. package/dist/src/FlinkContext.d.ts +41 -0
  26. package/dist/src/FlinkErrors.d.ts +19 -6
  27. package/dist/src/FlinkErrors.js +36 -42
  28. package/dist/src/FlinkHttpHandler.d.ts +219 -26
  29. package/dist/src/FlinkHttpHandler.js +37 -1
  30. package/dist/src/FlinkJob.d.ts +10 -0
  31. package/dist/src/FlinkLog.d.ts +82 -18
  32. package/dist/src/FlinkLog.js +165 -13
  33. package/dist/src/FlinkLogFactory.d.ts +288 -0
  34. package/dist/src/FlinkLogFactory.js +619 -0
  35. package/dist/src/FlinkRepo.d.ts +10 -2
  36. package/dist/src/FlinkRepo.js +11 -1
  37. package/dist/src/FlinkRequestContext.d.ts +63 -0
  38. package/dist/src/FlinkRequestContext.js +74 -0
  39. package/dist/src/FlinkResponse.d.ts +6 -0
  40. package/dist/src/FlinkService.d.ts +38 -0
  41. package/dist/src/FlinkService.js +46 -0
  42. package/dist/src/LeaderElection.d.ts +45 -0
  43. package/dist/src/LeaderElection.js +269 -0
  44. package/dist/src/SchemaCache.d.ts +84 -0
  45. package/dist/src/SchemaCache.js +289 -0
  46. package/dist/src/TypeScriptCompiler.d.ts +161 -51
  47. package/dist/src/TypeScriptCompiler.js +1253 -617
  48. package/dist/src/TypeScriptUtils.js +4 -0
  49. package/dist/src/ai/AgentRunner.d.ts +39 -0
  50. package/dist/src/ai/AgentRunner.js +760 -0
  51. package/dist/src/ai/ConversationAgent.d.ts +279 -0
  52. package/dist/src/ai/ConversationAgent.js +404 -0
  53. package/dist/src/ai/ConversationFlinkAgent.d.ts +278 -0
  54. package/dist/src/ai/ConversationFlinkAgent.js +404 -0
  55. package/dist/src/ai/FlinkAgent.d.ts +690 -0
  56. package/dist/src/ai/FlinkAgent.js +729 -0
  57. package/dist/src/ai/FlinkTool.d.ts +135 -0
  58. package/dist/src/ai/FlinkTool.js +2 -0
  59. package/dist/src/ai/InMemoryConversationAgent.d.ts +121 -0
  60. package/dist/src/ai/InMemoryConversationAgent.js +209 -0
  61. package/dist/src/ai/LLMAdapter.d.ts +148 -0
  62. package/dist/src/ai/LLMAdapter.js +2 -0
  63. package/dist/src/ai/PersistentFlinkAgent.d.ts +278 -0
  64. package/dist/src/ai/PersistentFlinkAgent.js +403 -0
  65. package/dist/src/ai/SubAgentExecutor.d.ts +38 -0
  66. package/dist/src/ai/SubAgentExecutor.js +223 -0
  67. package/dist/src/ai/ToolExecutor.d.ts +64 -0
  68. package/dist/src/ai/ToolExecutor.js +497 -0
  69. package/dist/src/ai/agentInstructions.d.ts +68 -0
  70. package/dist/src/ai/agentInstructions.js +286 -0
  71. package/dist/src/ai/index.d.ts +8 -0
  72. package/dist/src/ai/index.js +26 -0
  73. package/dist/src/ai/instructionFileLoader.d.ts +44 -0
  74. package/dist/src/ai/instructionFileLoader.js +179 -0
  75. package/dist/src/auth/FlinkAuthPlugin.d.ts +1 -1
  76. package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
  77. package/dist/src/handlers/StreamWriterFactory.js +83 -0
  78. package/dist/src/index.d.ts +14 -0
  79. package/dist/src/index.js +17 -0
  80. package/dist/src/loadPluginSchemas.d.ts +45 -0
  81. package/dist/src/loadPluginSchemas.js +143 -0
  82. package/dist/src/schema-extraction/ComplexTypeDetection.d.ts +40 -0
  83. package/dist/src/schema-extraction/ComplexTypeDetection.js +75 -0
  84. package/dist/src/schema-extraction/TypeScriptSourceParser.d.ts +321 -0
  85. package/dist/src/schema-extraction/TypeScriptSourceParser.js +925 -0
  86. package/dist/src/schema-extraction/TypeScriptSourceParser.spec.d.ts +1 -0
  87. package/dist/src/schema-extraction/TypeScriptSourceParser.spec.js +233 -0
  88. package/dist/src/schema-extraction/TypeScriptTokenizer.d.ts +57 -0
  89. package/dist/src/schema-extraction/TypeScriptTokenizer.js +177 -0
  90. package/dist/src/schema-extraction/index.d.ts +2 -0
  91. package/dist/src/schema-extraction/index.js +20 -0
  92. package/dist/src/schema-extraction/types.d.ts +31 -0
  93. package/dist/src/schema-extraction/types.js +2 -0
  94. package/dist/src/utils/loadFlinkConfig.d.ts +53 -0
  95. package/dist/src/utils/loadFlinkConfig.js +77 -0
  96. package/dist/src/utils.d.ts +30 -0
  97. package/dist/src/utils.js +52 -0
  98. package/dist/src/workers/SchemaGeneratorWorker.d.ts +1 -0
  99. package/dist/src/workers/SchemaGeneratorWorker.js +49 -0
  100. package/dist/src/workers/WorkerPool.d.ts +60 -0
  101. package/dist/src/workers/WorkerPool.js +306 -0
  102. package/examples/logging-hierarchical-example.ts +125 -0
  103. package/package.json +29 -4
  104. package/readme.md +499 -0
  105. package/spec/AgentDescendantDetection.spec.ts +335 -0
  106. package/spec/AgentDuplicateDetection.spec.ts +112 -0
  107. package/spec/AgentObserver.spec.ts +266 -0
  108. package/spec/AgentRunner.spec.ts +1062 -0
  109. package/spec/AsyncLocalStorageContext.spec.ts +223 -0
  110. package/spec/ConversationHooks.spec.ts +257 -0
  111. package/spec/FlinkAgent.spec.ts +681 -0
  112. package/spec/FlinkApp.htmlResponse.spec.ts +260 -0
  113. package/spec/FlinkApp.onError.invocation.spec.ts +151 -0
  114. package/spec/FlinkApp.onError.spec.ts +1 -2
  115. package/spec/FlinkApp.query.spec.ts +107 -0
  116. package/spec/FlinkApp.routeOrdering.spec.ts +61 -0
  117. package/spec/FlinkApp.undefinedResponse.spec.ts +123 -0
  118. package/spec/FlinkApp.validationMode.spec.ts +155 -0
  119. package/spec/FlinkJob.spec.ts +171 -0
  120. package/spec/FlinkLogFactory.spec.ts +337 -0
  121. package/spec/FlinkRepo.spec.ts +1 -1
  122. package/spec/LeaderElection.spec.ts +174 -0
  123. package/spec/StreamingIntegration.spec.ts +139 -0
  124. package/spec/ToolExecutor.spec.ts +465 -0
  125. package/spec/TypeScriptCompiler.spec.ts +1 -1
  126. package/spec/TypeScriptSourceParser.spec.ts +1215 -0
  127. package/spec/TypeScriptTokenizer.spec.ts +366 -0
  128. package/spec/ai/ContextCompaction.spec.ts +405 -0
  129. package/spec/ai/ConversationAgent.spec.ts +520 -0
  130. package/spec/ai/InMemoryConversationAgent.spec.ts +144 -0
  131. package/spec/ai/agentInstructions.spec.ts +358 -0
  132. package/spec/fixtures/agent-instructions/TestAgent.ts +24 -0
  133. package/spec/fixtures/agent-instructions/simple.md +3 -0
  134. package/spec/fixtures/agent-instructions/template.md +18 -0
  135. package/spec/fixtures/agent-instructions/yaml-format.yaml +9 -0
  136. package/spec/mock-project/dist/.tsbuildinfo +1 -0
  137. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +56 -0
  138. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +58 -0
  139. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +52 -0
  140. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +52 -0
  141. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +52 -0
  142. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +54 -0
  143. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +54 -0
  144. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +57 -0
  145. package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +57 -0
  146. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
  147. package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
  148. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +57 -0
  149. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +75 -0
  150. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +57 -0
  151. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +58 -0
  152. package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +58 -0
  153. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +54 -0
  154. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +55 -0
  155. package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +54 -0
  156. package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +54 -0
  157. package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
  158. package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
  159. package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
  160. package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
  161. package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
  162. package/spec/mock-project/dist/src/FlinkApp.js +1000 -0
  163. package/spec/mock-project/dist/src/FlinkContext.js +2 -0
  164. package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
  165. package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
  166. package/spec/mock-project/dist/src/FlinkJob.js +2 -0
  167. package/spec/mock-project/dist/src/FlinkLog.js +119 -0
  168. package/spec/mock-project/dist/src/FlinkLogFactory.js +617 -0
  169. package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
  170. package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
  171. package/spec/mock-project/dist/src/FlinkRequestContext.js +74 -0
  172. package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
  173. package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
  174. package/spec/mock-project/dist/src/ai/AgentRunner.js +632 -0
  175. package/spec/mock-project/dist/src/ai/ConversationAgent.js +402 -0
  176. package/spec/mock-project/dist/src/ai/ConversationFlinkAgent.js +422 -0
  177. package/spec/mock-project/dist/src/ai/FlinkAgent.js +699 -0
  178. package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
  179. package/spec/mock-project/dist/src/ai/InMemoryConversationAgent.js +209 -0
  180. package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
  181. package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +223 -0
  182. package/spec/mock-project/dist/src/ai/ToolExecutor.js +412 -0
  183. package/spec/mock-project/dist/src/ai/agentInstructions.js +246 -0
  184. package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
  185. package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
  186. package/spec/mock-project/dist/src/handlers/GetCar.js +26 -52
  187. package/spec/mock-project/dist/src/handlers/GetCar.js.map +1 -0
  188. package/spec/mock-project/dist/src/handlers/GetCar2.js +32 -54
  189. package/spec/mock-project/dist/src/handlers/GetCar2.js.map +1 -0
  190. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js +26 -48
  191. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js.map +1 -0
  192. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js +28 -48
  193. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js.map +1 -0
  194. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js +29 -48
  195. package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js.map +1 -0
  196. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js +26 -50
  197. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js.map +1 -0
  198. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js +28 -50
  199. package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js.map +1 -0
  200. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js +27 -53
  201. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js.map +1 -0
  202. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js +29 -53
  203. package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js.map +1 -0
  204. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js +16 -49
  205. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js.map +1 -0
  206. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js +25 -50
  207. package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js.map +1 -0
  208. package/spec/mock-project/dist/src/handlers/PatchCar.js +27 -53
  209. package/spec/mock-project/dist/src/handlers/PatchCar.js.map +1 -0
  210. package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js +44 -70
  211. package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js.map +1 -0
  212. package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js +27 -53
  213. package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js.map +1 -0
  214. package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js +28 -54
  215. package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js.map +1 -0
  216. package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js +28 -54
  217. package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js.map +1 -0
  218. package/spec/mock-project/dist/src/handlers/PostCar.js +24 -50
  219. package/spec/mock-project/dist/src/handlers/PostCar.js.map +1 -0
  220. package/spec/mock-project/dist/src/handlers/PostLogin.js +25 -51
  221. package/spec/mock-project/dist/src/handlers/PostLogin.js.map +1 -0
  222. package/spec/mock-project/dist/src/handlers/PostLogout.js +24 -50
  223. package/spec/mock-project/dist/src/handlers/PostLogout.js.map +1 -0
  224. package/spec/mock-project/dist/src/handlers/PutCar.js +24 -50
  225. package/spec/mock-project/dist/src/handlers/PutCar.js.map +1 -0
  226. package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
  227. package/spec/mock-project/dist/src/index.js +52 -76
  228. package/spec/mock-project/dist/src/index.js.map +1 -0
  229. package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
  230. package/spec/mock-project/dist/src/repos/CarRepo.js +12 -24
  231. package/spec/mock-project/dist/src/repos/CarRepo.js.map +1 -0
  232. package/spec/mock-project/dist/src/schemas/Car.js +3 -1
  233. package/spec/mock-project/dist/src/schemas/Car.js.map +1 -0
  234. package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js +3 -1
  235. package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js.map +1 -0
  236. package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js +3 -1
  237. package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js.map +1 -0
  238. package/spec/mock-project/dist/src/utils.js +290 -0
  239. package/spec/mock-project/tsconfig.json +6 -1
  240. package/spec/schema-generation-nested-objects.spec.ts +97 -0
  241. package/spec/testHelpers.ts +49 -0
  242. package/spec/utils.caseConversion.spec.ts +78 -0
  243. package/spec/utils.spec.ts +13 -13
  244. package/src/DependencyTracker.ts +166 -0
  245. package/src/FlinkApp.ts +919 -155
  246. package/src/FlinkContext.ts +43 -0
  247. package/src/FlinkErrors.ts +32 -12
  248. package/src/FlinkHttpHandler.ts +246 -28
  249. package/src/FlinkJob.ts +11 -0
  250. package/src/FlinkLog.ts +119 -12
  251. package/src/FlinkLogFactory.ts +699 -0
  252. package/src/FlinkRepo.ts +10 -3
  253. package/src/FlinkRequestContext.ts +95 -0
  254. package/src/FlinkResponse.ts +6 -0
  255. package/src/FlinkService.ts +49 -0
  256. package/src/LeaderElection.ts +203 -0
  257. package/src/SchemaCache.ts +232 -0
  258. package/src/TypeScriptCompiler.ts +1347 -610
  259. package/src/TypeScriptUtils.ts +5 -0
  260. package/src/ai/AgentRunner.ts +646 -0
  261. package/src/ai/ConversationAgent.ts +413 -0
  262. package/src/ai/FlinkAgent.ts +1069 -0
  263. package/src/ai/FlinkTool.ts +165 -0
  264. package/src/ai/InMemoryConversationAgent.ts +149 -0
  265. package/src/ai/LLMAdapter.ts +126 -0
  266. package/src/ai/ToolExecutor.ts +485 -0
  267. package/src/ai/agentInstructions.ts +245 -0
  268. package/src/ai/index.ts +8 -0
  269. package/src/ai/instructionFileLoader.ts +156 -0
  270. package/src/auth/FlinkAuthPlugin.ts +2 -1
  271. package/src/handlers/StreamWriterFactory.ts +84 -0
  272. package/src/index.ts +14 -0
  273. package/src/loadPluginSchemas.ts +141 -0
  274. package/src/schema-extraction/TypeScriptSourceParser.ts +1058 -0
  275. package/src/schema-extraction/TypeScriptTokenizer.ts +205 -0
  276. package/src/schema-extraction/index.ts +2 -0
  277. package/src/schema-extraction/types.ts +34 -0
  278. package/src/utils/loadFlinkConfig.ts +89 -0
  279. package/src/utils.ts +52 -0
  280. package/tsconfig.json +6 -1
@@ -0,0 +1,245 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import Handlebars from "handlebars";
4
+ import { FlinkContext } from "../FlinkContext";
5
+ import { InstructionsCallback, AgentExecuteContext } from "./FlinkAgent";
6
+
7
+ /**
8
+ * File cache structure: stores file content and modification time
9
+ */
10
+ interface FileCacheEntry {
11
+ content: string;
12
+ mtime: number;
13
+ compiledTemplate?: Handlebars.TemplateDelegate;
14
+ hasTemplateExpressions?: boolean;
15
+ }
16
+
17
+ /**
18
+ * In-memory file cache (per-process, not persistent)
19
+ */
20
+ const fileCache = new Map<string, FileCacheEntry>();
21
+
22
+ /**
23
+ * Get caller's file location using stack trace API
24
+ * This enables agent-relative path resolution for ./ and ../ prefixes
25
+ *
26
+ * Walks the stack to find the first frame outside this file,
27
+ * which is the agent class that called agentInstructions().
28
+ */
29
+ function getCallerFilePath(): string {
30
+ const originalPrepareStackTrace = Error.prepareStackTrace;
31
+ Error.prepareStackTrace = (_, stack) => stack;
32
+ const stack = new Error().stack as unknown as NodeJS.CallSite[];
33
+ Error.prepareStackTrace = originalPrepareStackTrace;
34
+
35
+ // Find first frame outside this file (skips getCallerFilePath, resolveFilePath, agentInstructions)
36
+ const thisFile = stack[0]?.getFileName();
37
+ for (let i = 1; i < stack.length; i++) {
38
+ const fileName = stack[i]?.getFileName();
39
+ if (fileName && fileName !== thisFile) {
40
+ return fileName;
41
+ }
42
+ }
43
+
44
+ throw new Error("Could not determine caller file location for agent-relative path");
45
+ }
46
+
47
+ /**
48
+ * Resolve file path based on prefix:
49
+ * - ./ or ../ = agent-relative (using caller's location)
50
+ * - otherwise = project root-relative
51
+ */
52
+ function resolveFilePath(filePath: string): string {
53
+ // Always try project root-relative first (most intuitive for users)
54
+ const cwdResolved = path.join(process.cwd(), filePath);
55
+ if (fs.existsSync(cwdResolved)) {
56
+ return cwdResolved;
57
+ }
58
+
59
+ if (filePath.startsWith("./") || filePath.startsWith("../")) {
60
+ // Agent-relative: get caller's file location via stack trace
61
+ const callerFile = getCallerFilePath();
62
+ const resolved = path.resolve(path.dirname(callerFile), filePath);
63
+
64
+ if (fs.existsSync(resolved)) {
65
+ return resolved;
66
+ }
67
+
68
+ // At runtime, callerFile points to compiled JS in dist/.
69
+ // Non-JS assets (e.g. .md instruction files) live in the source tree,
70
+ // so try the equivalent source path with dist/ removed.
71
+ const distIndex = resolved.lastIndexOf(path.sep + "dist" + path.sep);
72
+ if (distIndex !== -1) {
73
+ const sourcePath = resolved.substring(0, distIndex) + resolved.substring(distIndex + 5); // remove "/dist"
74
+ if (fs.existsSync(sourcePath)) {
75
+ return sourcePath;
76
+ }
77
+ }
78
+
79
+ return resolved;
80
+ } else {
81
+ // Project root-relative
82
+ return cwdResolved;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Load file contents with smart caching
88
+ * Cache is invalidated when file modification time changes
89
+ */
90
+ function loadFile(resolvedPath: string, originalPath: string): FileCacheEntry {
91
+ try {
92
+ const stats = fs.statSync(resolvedPath);
93
+ const mtime = stats.mtimeMs;
94
+
95
+ // Check cache
96
+ const cached = fileCache.get(resolvedPath);
97
+ if (cached && cached.mtime === mtime) {
98
+ return cached;
99
+ }
100
+
101
+ // Read file and update cache (clear compiled template on file change)
102
+ const content = fs.readFileSync(resolvedPath, "utf-8");
103
+ const entry: FileCacheEntry = { content, mtime };
104
+ fileCache.set(resolvedPath, entry);
105
+
106
+ return entry;
107
+ } catch (err: any) {
108
+ if (err.code === "ENOENT") {
109
+ throw new Error(`Agent instructions file not found: ${resolvedPath} (from: ${originalPath})`);
110
+ }
111
+ throw new Error(`Failed to load agent instructions file: ${resolvedPath} - ${err.message}`);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Render template using Handlebars with compiled template caching.
117
+ * Skips Handlebars entirely when content has no template expressions.
118
+ */
119
+ function renderTemplate(cached: FileCacheEntry, data: any, filePath: string): string {
120
+ if (cached.hasTemplateExpressions === false) {
121
+ return cached.content;
122
+ }
123
+
124
+ try {
125
+ if (!cached.compiledTemplate) {
126
+ cached.hasTemplateExpressions = /\{\{/.test(cached.content);
127
+ if (!cached.hasTemplateExpressions) {
128
+ return cached.content;
129
+ }
130
+ cached.compiledTemplate = Handlebars.compile(cached.content);
131
+ }
132
+ return cached.compiledTemplate(data);
133
+ } catch (err: any) {
134
+ throw new Error(`Failed to render template in ${filePath}: ${err.message}`);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Load agent instructions from external files with template variable support
140
+ *
141
+ * Supports multiple file types: .md, .yaml, .txt, etc. (loaded as plain text)
142
+ *
143
+ * ## Path Resolution
144
+ *
145
+ * - `./` or `../` prefix: Agent-relative path (based on caller's file location)
146
+ * - Otherwise: Project root-relative path
147
+ *
148
+ * ## Template Variables
149
+ *
150
+ * Uses Handlebars templating with automatic context helpers:
151
+ * - `ctx`: Full FlinkContext
152
+ * - `agentContext`: AgentExecuteContext
153
+ * - `user`: Shortcut to agentContext.user
154
+ * - Custom variables from static object or callback
155
+ *
156
+ * ## Caching
157
+ *
158
+ * Files are cached in-memory and reloaded only when modification time changes
159
+ *
160
+ * @example Agent-relative path with static variables
161
+ * ```typescript
162
+ * export default class CarAgent extends FlinkAgent<AppCtx> {
163
+ * id = "car-agent";
164
+ * instructions = agentInstructions("./instructions/car-agent.md");
165
+ * tools = [SearchCarsTool];
166
+ * }
167
+ * ```
168
+ *
169
+ * @example Dynamic variables with callback
170
+ * ```typescript
171
+ * export default class SupportAgent extends FlinkAgent<AppCtx> {
172
+ * id = "support-agent";
173
+ * instructions = agentInstructions(
174
+ * "./instructions/support-agent.md",
175
+ * (ctx, agentContext) => ({
176
+ * isBusinessHours: new Date().getHours() >= 9 && new Date().getHours() < 17,
177
+ * customerTier: agentContext.user?.tier || "standard",
178
+ * })
179
+ * );
180
+ * tools = [CreateTicketTool];
181
+ * }
182
+ * ```
183
+ *
184
+ * @example Template file (instructions/support-agent.md)
185
+ * ```markdown
186
+ * You are a customer support agent.
187
+ *
188
+ * Customer: {{user.name}} ({{customerTier}})
189
+ * {{#if user.isPremium}}
190
+ * ⭐ VIP CUSTOMER - Provide white-glove service!
191
+ * {{/if}}
192
+ *
193
+ * {{#unless isBusinessHours}}
194
+ * NOTE: Outside business hours. Suggest emergency contact.
195
+ * {{/unless}}
196
+ * ```
197
+ *
198
+ * @param filePath - Path to instructions file (./ for agent-relative, otherwise project root)
199
+ * @param variables - Static object or callback returning template variables
200
+ * @returns InstructionsCallback compatible with FlinkAgent.instructions property
201
+ */
202
+ export function agentInstructions<Ctx extends FlinkContext = FlinkContext>(
203
+ filePath: string,
204
+ variables?:
205
+ | Record<string, any>
206
+ | ((ctx: Ctx, agentContext: AgentExecuteContext) => Record<string, any> | Promise<Record<string, any>>)
207
+ ): InstructionsCallback<Ctx> {
208
+ // Resolve path once at definition time for early validation
209
+ const resolvedPath = resolveFilePath(filePath);
210
+
211
+ // Return callback that will be invoked by FlinkAgent
212
+ return async (ctx: Ctx, agentContext: AgentExecuteContext): Promise<string> => {
213
+ // Load file (uses cache if available and mtime unchanged)
214
+ const cached = loadFile(resolvedPath, filePath);
215
+
216
+ // Skip template processing entirely when no expressions exist
217
+ if (cached.hasTemplateExpressions === false) {
218
+ return cached.content;
219
+ }
220
+
221
+ // Resolve variables (static or callback)
222
+ let vars: Record<string, any> = {};
223
+ if (variables) {
224
+ if (typeof variables === "function") {
225
+ vars = await Promise.resolve(variables(ctx, agentContext));
226
+ } else {
227
+ vars = variables;
228
+ }
229
+ }
230
+
231
+ // Merge with automatic context helpers
232
+ const templateData = {
233
+ ...vars,
234
+ ctx,
235
+ agentContext,
236
+ user: agentContext.user,
237
+ };
238
+
239
+ // Render template (compiles and caches Handlebars template on first call)
240
+ return renderTemplate(cached, templateData, resolvedPath);
241
+ };
242
+ }
243
+
244
+ // Re-export types for convenience
245
+ export type { InstructionsCallback, AgentExecuteContext, InstructionsReturn } from "./FlinkAgent";
@@ -0,0 +1,8 @@
1
+ export * from "./FlinkTool";
2
+ export * from "./FlinkAgent";
3
+ export * from "./ConversationAgent";
4
+ export * from "./InMemoryConversationAgent";
5
+ export * from "./ToolExecutor";
6
+ export * from "./AgentRunner";
7
+ export * from "./LLMAdapter";
8
+ export { agentInstructions } from "./agentInstructions";
@@ -0,0 +1,156 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import Handlebars from "handlebars";
4
+
5
+ // Enable {{{{raw}}}}...{{{{/raw}}}} blocks to pass content through unprocessed.
6
+ // Without this, content inside raw blocks is silently dropped.
7
+ Handlebars.registerHelper("raw", function (this: any, options: any) {
8
+ return options.fn(this);
9
+ });
10
+
11
+ /**
12
+ * Return type for the FlinkAgent.instructions() method.
13
+ *
14
+ * Supported forms:
15
+ * - `string` — Plain text used as-is, OR a file path (see below) which is auto-loaded.
16
+ * - `{ file, params? }` — Explicitly load a file with optional Handlebars template params.
17
+ *
18
+ * **Path resolution** — all paths resolve relative to the **project root** (`process.cwd()`):
19
+ * - `"instructions/foo.md"` → `<project-root>/instructions/foo.md`
20
+ * - `"./instructions/foo.md"` → `<project-root>/instructions/foo.md`
21
+ * - `"/instructions/foo.md"` → `<project-root>/instructions/foo.md` (leading slash stripped)
22
+ *
23
+ * Auto-loaded string extensions: `.md`, `.txt`, `.yaml`, `.yml`, `.xml`, `.toml`, `.ini`, `.json`, `.html`
24
+ *
25
+ * @example Plain text
26
+ * instructions() {
27
+ * return "You are a helpful car assistant.";
28
+ * }
29
+ *
30
+ * @example Auto-load file (project-root-relative)
31
+ * instructions() {
32
+ * return "instructions/car-agent.md";
33
+ * }
34
+ *
35
+ * @example File with template params
36
+ * async instructions(_ctx, agentCtx) {
37
+ * return {
38
+ * file: "instructions/support.md",
39
+ * params: {
40
+ * customerTier: agentCtx.user?.tier || "standard",
41
+ * isBusinessHours: new Date().getHours() >= 9,
42
+ * },
43
+ * };
44
+ * }
45
+ */
46
+ export type InstructionsReturn = string | { file: string; params?: Record<string, any> };
47
+
48
+ interface FileCacheEntry {
49
+ content: string;
50
+ resolvedContent: string;
51
+ mtime: number;
52
+ compiledTemplate?: Handlebars.TemplateDelegate;
53
+ hasTemplateExpressions?: boolean;
54
+ }
55
+
56
+ const fileCache = new Map<string, FileCacheEntry>();
57
+
58
+ const IMPORT_REGEX = /\{\{\s*import\s+["']([^"']+)['"]\s*\}\}/g;
59
+
60
+ function resolveImports(content: string, currentDir: string, visited: Set<string> = new Set()): string {
61
+ return content.replace(IMPORT_REGEX, (match, importPath) => {
62
+ const resolved = path.resolve(currentDir, importPath);
63
+ if (visited.has(resolved)) {
64
+ throw new Error(`Circular import detected: ${resolved}`);
65
+ }
66
+ const nextVisited = new Set(visited);
67
+ nextVisited.add(resolved);
68
+ try {
69
+ const importedContent = fs.readFileSync(resolved, "utf-8");
70
+ return resolveImports(importedContent, path.dirname(resolved), nextVisited);
71
+ } catch (err: any) {
72
+ if (err.code === "ENOENT") {
73
+ throw new Error(`Imported markdown file not found: ${resolved} (imported from: ${currentDir})`);
74
+ }
75
+ throw err;
76
+ }
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Resolve a file path relative to project root (process.cwd()).
82
+ * Leading `./` and `/` are normalised away — all paths are treated as project-root-relative.
83
+ */
84
+ function resolveFilePath(filePath: string): string {
85
+ // Strip leading slash so "/foo.md" behaves the same as "foo.md"
86
+ const normalised = filePath.replace(/^\/+/, "");
87
+ return path.resolve(process.cwd(), normalised);
88
+ }
89
+
90
+ function loadFile(filePath: string): FileCacheEntry {
91
+ const resolvedPath = resolveFilePath(filePath);
92
+ try {
93
+ const stats = fs.statSync(resolvedPath);
94
+ const mtime = stats.mtimeMs;
95
+
96
+ const cached = fileCache.get(resolvedPath);
97
+ if (cached && cached.mtime === mtime) {
98
+ return cached;
99
+ }
100
+
101
+ const content = fs.readFileSync(resolvedPath, "utf-8");
102
+ const resolvedContent = resolveImports(content, path.dirname(resolvedPath));
103
+ const entry: FileCacheEntry = { content, resolvedContent, mtime };
104
+ fileCache.set(resolvedPath, entry);
105
+ return entry;
106
+ } catch (err: any) {
107
+ if (err.code === "ENOENT") {
108
+ throw new Error(`Agent instructions file not found: ${resolvedPath} (from: ${filePath})`);
109
+ }
110
+ throw new Error(`Failed to load agent instructions file: ${resolvedPath} - ${err.message}`);
111
+ }
112
+ }
113
+
114
+ function renderTemplate(entry: FileCacheEntry, data: any): string {
115
+ if (entry.hasTemplateExpressions === false) {
116
+ return entry.resolvedContent;
117
+ }
118
+
119
+ if (!entry.compiledTemplate) {
120
+ entry.hasTemplateExpressions = /\{\{/.test(entry.resolvedContent);
121
+ if (!entry.hasTemplateExpressions) {
122
+ return entry.resolvedContent;
123
+ }
124
+ entry.compiledTemplate = Handlebars.compile(entry.resolvedContent);
125
+ }
126
+
127
+ return entry.compiledTemplate(data);
128
+ }
129
+
130
+ const TEXT_FILE_EXTENSIONS = [".md", ".txt", ".yaml", ".yml", ".xml", ".toml", ".ini", ".json", ".html", ".htm"];
131
+
132
+ function isTextFilePath(value: string): boolean {
133
+ const trimmed = value.trimEnd().toLowerCase();
134
+ return TEXT_FILE_EXTENSIONS.some((ext) => trimmed.endsWith(ext));
135
+ }
136
+
137
+ /**
138
+ * Resolve an InstructionsReturn value to a plain string.
139
+ * @internal Used by FlinkAgent.toAgentProps()
140
+ */
141
+ export async function resolveInstructionsReturn(result: InstructionsReturn, ctx: any, agentContext: any): Promise<string> {
142
+ if (typeof result === "string") {
143
+ if (isTextFilePath(result)) {
144
+ const entry = loadFile(result.trimEnd());
145
+ const templateData = { ctx, agentContext, user: agentContext?.user };
146
+ return renderTemplate(entry, templateData);
147
+ }
148
+ return result;
149
+ }
150
+
151
+ // { file, params? }
152
+ const { file, params } = result;
153
+ const entry = loadFile(file);
154
+ const templateData = { ...params, ctx, agentContext, user: agentContext?.user };
155
+ return renderTemplate(entry, templateData);
156
+ }
@@ -3,7 +3,8 @@ import { FlinkRequest } from "../FlinkHttpHandler";
3
3
  export interface FlinkAuthPlugin {
4
4
  authenticateRequest: (
5
5
  req: FlinkRequest,
6
- permissions: string | string[]
6
+ permissions: string | string[],
7
+ ctx?: any
7
8
  ) => Promise<boolean>;
8
9
  createToken: (payload: any, roles: string[]) => Promise<string>;
9
10
  }
@@ -0,0 +1,84 @@
1
+ import { Response } from "express";
2
+ import { StreamWriter, StreamFormat } from "../FlinkHttpHandler";
3
+ import { log } from "../FlinkLog";
4
+
5
+ /**
6
+ * Factory for creating StreamWriter instances for SSE and NDJSON streaming.
7
+ *
8
+ * Handles HTTP headers, connection lifecycle, and format-specific serialization.
9
+ */
10
+ export class StreamWriterFactory {
11
+ /**
12
+ * Create a StreamWriter for the given format.
13
+ *
14
+ * Sets appropriate HTTP headers and manages the stream lifecycle including
15
+ * client disconnect detection.
16
+ *
17
+ * @param res - Express response object
18
+ * @param format - Stream format (sse or ndjson)
19
+ * @returns StreamWriter instance for writing data to the stream
20
+ */
21
+ static create<T = any>(res: Response, format: StreamFormat): StreamWriter<T> {
22
+ // Set appropriate headers based on format
23
+ if (format === "sse") {
24
+ res.setHeader("Content-Type", "text/event-stream");
25
+ res.setHeader("Cache-Control", "no-cache");
26
+ res.setHeader("Connection", "keep-alive");
27
+ res.flushHeaders();
28
+ } else if (format === "ndjson") {
29
+ res.setHeader("Content-Type", "application/x-ndjson");
30
+ res.setHeader("Cache-Control", "no-cache");
31
+ res.flushHeaders();
32
+ }
33
+
34
+ let isOpen = true;
35
+
36
+ // Detect client disconnect
37
+ res.on("close", () => {
38
+ isOpen = false;
39
+ });
40
+
41
+ return {
42
+ write: (data: T) => {
43
+ if (!isOpen) return;
44
+
45
+ try {
46
+ const json = JSON.stringify(data);
47
+
48
+ if (format === "sse") {
49
+ res.write(`data: ${json}\n\n`);
50
+ } else if (format === "ndjson") {
51
+ res.write(`${json}\n`);
52
+ }
53
+ } catch (err) {
54
+ log.error("StreamWriter serialization error:", { error: err });
55
+ isOpen = false;
56
+ }
57
+ },
58
+
59
+ error: (error: Error | string) => {
60
+ if (!isOpen) return;
61
+
62
+ const errorMessage = typeof error === "string" ? error : error.message;
63
+
64
+ if (format === "sse") {
65
+ res.write(`event: error\ndata: ${JSON.stringify({ error: errorMessage })}\n\n`);
66
+ } else if (format === "ndjson") {
67
+ res.write(`${JSON.stringify({ error: errorMessage })}\n`);
68
+ }
69
+
70
+ res.end();
71
+ isOpen = false;
72
+ },
73
+
74
+ end: () => {
75
+ if (isOpen) {
76
+ res.end();
77
+ isOpen = false;
78
+ }
79
+ },
80
+
81
+ isOpen: () => isOpen,
82
+ };
83
+ }
84
+ }
package/src/index.ts CHANGED
@@ -1,14 +1,28 @@
1
1
  export * from "./FlinkLog";
2
+ export * from "./FlinkLogFactory";
2
3
  export * from "./FlinkApp";
4
+ export * from "./utils/loadFlinkConfig";
3
5
  export * from "./FlinkHttpHandler";
4
6
  export * from "./FlinkContext";
5
7
  export * from "./FlinkRepo";
6
8
  export * from "./FlinkResponse";
9
+ export * from "./FlinkRequestContext";
7
10
  export * from "./FlinkErrors";
8
11
  export * from "./FlinkPlugin";
9
12
  export * from "./FlinkJob";
13
+ export * from "./FlinkService";
14
+ export { LeaderElection } from "./LeaderElection";
15
+ export type { LeaderElectionOptions } from "./LeaderElection";
10
16
  export * from "./auth/FlinkAuthUser";
11
17
  export * from "./auth/FlinkAuthPlugin";
18
+ export * from "./ai/FlinkTool";
19
+ export * from "./ai/FlinkAgent";
20
+ export * from "./ai/ConversationAgent";
21
+ export * from "./ai/InMemoryConversationAgent";
22
+ export * from "./ai/ToolExecutor";
23
+ export * from "./ai/LLMAdapter";
24
+ export { agentInstructions } from "./ai/agentInstructions";
25
+ export { loadPluginSchemas } from "./loadPluginSchemas";
12
26
 
13
27
  // Re-export Express types for plugins and consumer apps
14
28
  // This ensures type consistency across the framework and plugins
@@ -0,0 +1,141 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+
4
+ interface SchemaManifest {
5
+ version?: string;
6
+ schemas?: Record<string, any>;
7
+ definitions?: Record<string, any>;
8
+ handlers: Record<
9
+ string,
10
+ {
11
+ reqSchemaName?: string;
12
+ resSchemaName?: string;
13
+ queryMetadata?: any[];
14
+ paramsMetadata?: any[];
15
+ assumedMethod?: string;
16
+ }
17
+ >;
18
+ tools: Record<string, any>;
19
+ }
20
+
21
+ /**
22
+ * Prefix a schema $id or $ref value with the plugin namespace.
23
+ */
24
+ function prefixId(pluginId: string, id: string): string {
25
+ return `${pluginId}::${id}`;
26
+ }
27
+
28
+ /**
29
+ * Deep-clone a schema and prefix all $id and $ref values with the plugin namespace.
30
+ * Only prefixes $ref values that refer to schemas within this plugin's universe.
31
+ */
32
+ function prefixSchema(pluginId: string, schema: any, knownIds: Set<string>): any {
33
+ if (!schema || typeof schema !== "object") return schema;
34
+
35
+ if (Array.isArray(schema)) {
36
+ return schema.map((item) => prefixSchema(pluginId, item, knownIds));
37
+ }
38
+
39
+ const result: any = {};
40
+ for (const [key, value] of Object.entries(schema)) {
41
+ if (key === "$id" && typeof value === "string") {
42
+ result[key] = prefixId(pluginId, value);
43
+ } else if (key === "$ref" && typeof value === "string") {
44
+ // Only prefix refs that point to known plugin schemas
45
+ if (knownIds.has(value)) {
46
+ result[key] = prefixId(pluginId, value);
47
+ } else {
48
+ result[key] = value;
49
+ }
50
+ } else if (typeof value === "object") {
51
+ result[key] = prefixSchema(pluginId, value, knownIds);
52
+ } else {
53
+ result[key] = value;
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Load a plugin's schema manifest and provide helpers for getting
61
+ * namespaced schemas suitable for use with `app.registerSchemas()` and `addHandler()`.
62
+ *
63
+ * @param packageName The npm package name (e.g., "@flink-app/generic-auth-plugin").
64
+ * The package's `dist/.flink/schema-manifest.json` will be loaded.
65
+ * @returns Object with helper methods for retrieving prefixed schemas.
66
+ */
67
+ export function loadPluginSchemas(packageName: string) {
68
+ let manifest: SchemaManifest;
69
+
70
+ // Resolve the package directory
71
+ const packageDir = path.dirname(require.resolve(path.join(packageName, "package.json")));
72
+ const manifestPath = path.join(packageDir, "dist/.flink/schema-manifest.json");
73
+
74
+ if (!fs.existsSync(manifestPath)) {
75
+ // No manifest — return empty helpers
76
+ manifest = { handlers: {}, tools: {} };
77
+ } else {
78
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
79
+ }
80
+
81
+ // Collect all known schema $ids for ref resolution
82
+ const schemas = manifest.schemas || manifest.definitions || {};
83
+ const knownIds = new Set<string>();
84
+ for (const schema of Object.values(schemas)) {
85
+ if (schema && typeof schema === "object" && schema.$id) {
86
+ knownIds.add(schema.$id);
87
+ }
88
+ }
89
+
90
+ return {
91
+ /**
92
+ * Get prefixed req/res schemas for a specific handler file path.
93
+ *
94
+ * @param pluginId Plugin instance ID used as namespace prefix
95
+ * @param filePath Handler file path as it appears in the manifest (e.g., "src/handlers/UserLogin.ts")
96
+ */
97
+ getHandlerSchemas(
98
+ pluginId: string,
99
+ filePath: string
100
+ ): { reqSchema?: object; resSchema?: object } {
101
+ const metadata = manifest.handlers[filePath];
102
+ if (!metadata) return {};
103
+
104
+ const result: { reqSchema?: object; resSchema?: object } = {};
105
+
106
+ if (metadata.reqSchemaName && schemas[metadata.reqSchemaName]) {
107
+ result.reqSchema = prefixSchema(pluginId, schemas[metadata.reqSchemaName], knownIds);
108
+ }
109
+ if (metadata.resSchemaName && schemas[metadata.resSchemaName]) {
110
+ result.resSchema = prefixSchema(pluginId, schemas[metadata.resSchemaName], knownIds);
111
+ }
112
+
113
+ return result;
114
+ },
115
+
116
+ /**
117
+ * Get all schemas from the plugin's manifest, prefixed with the plugin namespace.
118
+ * Pass the result to `app.registerSchemas()` for $ref resolution.
119
+ *
120
+ * @param pluginId Plugin instance ID used as namespace prefix
121
+ */
122
+ getAllSchemas(pluginId: string): Record<string, any> {
123
+ const prefixed: Record<string, any> = {};
124
+ for (const [name, schema] of Object.entries(schemas)) {
125
+ if (schema && typeof schema === "object") {
126
+ const prefixedSchema = prefixSchema(pluginId, schema, knownIds);
127
+ const prefixedName = prefixId(pluginId, name);
128
+ prefixed[prefixedName] = prefixedSchema;
129
+ }
130
+ }
131
+ return prefixed;
132
+ },
133
+
134
+ /**
135
+ * Get the raw manifest (for inspection/debugging).
136
+ */
137
+ getManifest(): SchemaManifest {
138
+ return manifest;
139
+ },
140
+ };
141
+ }