@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.
- package/CHANGELOG.md +1051 -0
- package/SCHEMA_EXTRACTION_ANALYSIS.md +494 -0
- package/SIMPLE_AST_FEASIBILITY.md +570 -0
- package/bin/flink.ts +13 -2
- package/cli/build.ts +24 -44
- package/cli/clean.ts +13 -25
- package/cli/cli-utils.ts +190 -17
- package/cli/dev.ts +252 -0
- package/cli/loadEnvFiles.ts +116 -0
- package/cli/run.ts +45 -62
- package/dist/bin/flink.js +61 -2
- package/dist/cli/build.js +20 -25
- package/dist/cli/clean.js +12 -10
- package/dist/cli/cli-utils.d.ts +34 -3
- package/dist/cli/cli-utils.js +193 -12
- package/dist/cli/dev.d.ts +2 -0
- package/dist/cli/dev.js +279 -0
- package/dist/cli/loadEnvFiles.d.ts +30 -0
- package/dist/cli/loadEnvFiles.js +113 -0
- package/dist/cli/run.js +47 -46
- package/dist/src/DependencyTracker.d.ts +44 -0
- package/dist/src/DependencyTracker.js +239 -0
- package/dist/src/FlinkApp.d.ts +163 -10
- package/dist/src/FlinkApp.js +847 -184
- package/dist/src/FlinkContext.d.ts +41 -0
- package/dist/src/FlinkErrors.d.ts +19 -6
- package/dist/src/FlinkErrors.js +36 -42
- package/dist/src/FlinkHttpHandler.d.ts +219 -26
- package/dist/src/FlinkHttpHandler.js +37 -1
- package/dist/src/FlinkJob.d.ts +10 -0
- package/dist/src/FlinkLog.d.ts +82 -18
- package/dist/src/FlinkLog.js +165 -13
- package/dist/src/FlinkLogFactory.d.ts +288 -0
- package/dist/src/FlinkLogFactory.js +619 -0
- package/dist/src/FlinkRepo.d.ts +10 -2
- package/dist/src/FlinkRepo.js +11 -1
- package/dist/src/FlinkRequestContext.d.ts +63 -0
- package/dist/src/FlinkRequestContext.js +74 -0
- package/dist/src/FlinkResponse.d.ts +6 -0
- package/dist/src/FlinkService.d.ts +38 -0
- package/dist/src/FlinkService.js +46 -0
- package/dist/src/LeaderElection.d.ts +45 -0
- package/dist/src/LeaderElection.js +269 -0
- package/dist/src/SchemaCache.d.ts +84 -0
- package/dist/src/SchemaCache.js +289 -0
- package/dist/src/TypeScriptCompiler.d.ts +161 -51
- package/dist/src/TypeScriptCompiler.js +1253 -617
- package/dist/src/TypeScriptUtils.js +4 -0
- package/dist/src/ai/AgentRunner.d.ts +39 -0
- package/dist/src/ai/AgentRunner.js +760 -0
- package/dist/src/ai/ConversationAgent.d.ts +279 -0
- package/dist/src/ai/ConversationAgent.js +404 -0
- package/dist/src/ai/ConversationFlinkAgent.d.ts +278 -0
- package/dist/src/ai/ConversationFlinkAgent.js +404 -0
- package/dist/src/ai/FlinkAgent.d.ts +690 -0
- package/dist/src/ai/FlinkAgent.js +729 -0
- package/dist/src/ai/FlinkTool.d.ts +135 -0
- package/dist/src/ai/FlinkTool.js +2 -0
- package/dist/src/ai/InMemoryConversationAgent.d.ts +121 -0
- package/dist/src/ai/InMemoryConversationAgent.js +209 -0
- package/dist/src/ai/LLMAdapter.d.ts +148 -0
- package/dist/src/ai/LLMAdapter.js +2 -0
- package/dist/src/ai/PersistentFlinkAgent.d.ts +278 -0
- package/dist/src/ai/PersistentFlinkAgent.js +403 -0
- package/dist/src/ai/SubAgentExecutor.d.ts +38 -0
- package/dist/src/ai/SubAgentExecutor.js +223 -0
- package/dist/src/ai/ToolExecutor.d.ts +64 -0
- package/dist/src/ai/ToolExecutor.js +497 -0
- package/dist/src/ai/agentInstructions.d.ts +68 -0
- package/dist/src/ai/agentInstructions.js +286 -0
- package/dist/src/ai/index.d.ts +8 -0
- package/dist/src/ai/index.js +26 -0
- package/dist/src/ai/instructionFileLoader.d.ts +44 -0
- package/dist/src/ai/instructionFileLoader.js +179 -0
- package/dist/src/auth/FlinkAuthPlugin.d.ts +1 -1
- package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
- package/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +17 -0
- package/dist/src/loadPluginSchemas.d.ts +45 -0
- package/dist/src/loadPluginSchemas.js +143 -0
- package/dist/src/schema-extraction/ComplexTypeDetection.d.ts +40 -0
- package/dist/src/schema-extraction/ComplexTypeDetection.js +75 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.d.ts +321 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.js +925 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.spec.d.ts +1 -0
- package/dist/src/schema-extraction/TypeScriptSourceParser.spec.js +233 -0
- package/dist/src/schema-extraction/TypeScriptTokenizer.d.ts +57 -0
- package/dist/src/schema-extraction/TypeScriptTokenizer.js +177 -0
- package/dist/src/schema-extraction/index.d.ts +2 -0
- package/dist/src/schema-extraction/index.js +20 -0
- package/dist/src/schema-extraction/types.d.ts +31 -0
- package/dist/src/schema-extraction/types.js +2 -0
- package/dist/src/utils/loadFlinkConfig.d.ts +53 -0
- package/dist/src/utils/loadFlinkConfig.js +77 -0
- package/dist/src/utils.d.ts +30 -0
- package/dist/src/utils.js +52 -0
- package/dist/src/workers/SchemaGeneratorWorker.d.ts +1 -0
- package/dist/src/workers/SchemaGeneratorWorker.js +49 -0
- package/dist/src/workers/WorkerPool.d.ts +60 -0
- package/dist/src/workers/WorkerPool.js +306 -0
- package/examples/logging-hierarchical-example.ts +125 -0
- package/package.json +29 -4
- package/readme.md +499 -0
- package/spec/AgentDescendantDetection.spec.ts +335 -0
- package/spec/AgentDuplicateDetection.spec.ts +112 -0
- package/spec/AgentObserver.spec.ts +266 -0
- package/spec/AgentRunner.spec.ts +1062 -0
- package/spec/AsyncLocalStorageContext.spec.ts +223 -0
- package/spec/ConversationHooks.spec.ts +257 -0
- package/spec/FlinkAgent.spec.ts +681 -0
- package/spec/FlinkApp.htmlResponse.spec.ts +260 -0
- package/spec/FlinkApp.onError.invocation.spec.ts +151 -0
- package/spec/FlinkApp.onError.spec.ts +1 -2
- package/spec/FlinkApp.query.spec.ts +107 -0
- package/spec/FlinkApp.routeOrdering.spec.ts +61 -0
- package/spec/FlinkApp.undefinedResponse.spec.ts +123 -0
- package/spec/FlinkApp.validationMode.spec.ts +155 -0
- package/spec/FlinkJob.spec.ts +171 -0
- package/spec/FlinkLogFactory.spec.ts +337 -0
- package/spec/FlinkRepo.spec.ts +1 -1
- package/spec/LeaderElection.spec.ts +174 -0
- package/spec/StreamingIntegration.spec.ts +139 -0
- package/spec/ToolExecutor.spec.ts +465 -0
- package/spec/TypeScriptCompiler.spec.ts +1 -1
- package/spec/TypeScriptSourceParser.spec.ts +1215 -0
- package/spec/TypeScriptTokenizer.spec.ts +366 -0
- package/spec/ai/ContextCompaction.spec.ts +405 -0
- package/spec/ai/ConversationAgent.spec.ts +520 -0
- package/spec/ai/InMemoryConversationAgent.spec.ts +144 -0
- package/spec/ai/agentInstructions.spec.ts +358 -0
- package/spec/fixtures/agent-instructions/TestAgent.ts +24 -0
- package/spec/fixtures/agent-instructions/simple.md +3 -0
- package/spec/fixtures/agent-instructions/template.md +18 -0
- package/spec/fixtures/agent-instructions/yaml-format.yaml +9 -0
- package/spec/mock-project/dist/.tsbuildinfo +1 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +56 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +52 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +75 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +54 -0
- package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
- package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
- package/spec/mock-project/dist/src/FlinkApp.js +1000 -0
- package/spec/mock-project/dist/src/FlinkContext.js +2 -0
- package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
- package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
- package/spec/mock-project/dist/src/FlinkJob.js +2 -0
- package/spec/mock-project/dist/src/FlinkLog.js +119 -0
- package/spec/mock-project/dist/src/FlinkLogFactory.js +617 -0
- package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
- package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
- package/spec/mock-project/dist/src/FlinkRequestContext.js +74 -0
- package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
- package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
- package/spec/mock-project/dist/src/ai/AgentRunner.js +632 -0
- package/spec/mock-project/dist/src/ai/ConversationAgent.js +402 -0
- package/spec/mock-project/dist/src/ai/ConversationFlinkAgent.js +422 -0
- package/spec/mock-project/dist/src/ai/FlinkAgent.js +699 -0
- package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
- package/spec/mock-project/dist/src/ai/InMemoryConversationAgent.js +209 -0
- package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
- package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +223 -0
- package/spec/mock-project/dist/src/ai/ToolExecutor.js +412 -0
- package/spec/mock-project/dist/src/ai/agentInstructions.js +246 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
- package/spec/mock-project/dist/src/handlers/GetCar.js +26 -52
- package/spec/mock-project/dist/src/handlers/GetCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCar2.js +32 -54
- package/spec/mock-project/dist/src/handlers/GetCar2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js +26 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js +28 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js +29 -48
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js +26 -50
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js +28 -50
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js +27 -53
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js +29 -53
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js +16 -49
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js +25 -50
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchCar.js +27 -53
- package/spec/mock-project/dist/src/handlers/PatchCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js +44 -70
- package/spec/mock-project/dist/src/handlers/PatchOnboardingSession.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js +27 -53
- package/spec/mock-project/dist/src/handlers/PatchOrderWithComplexTypes.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js +28 -54
- package/spec/mock-project/dist/src/handlers/PatchProductWithIntersection.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js +28 -54
- package/spec/mock-project/dist/src/handlers/PatchUserWithUnion.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostCar.js +24 -50
- package/spec/mock-project/dist/src/handlers/PostCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostLogin.js +25 -51
- package/spec/mock-project/dist/src/handlers/PostLogin.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PostLogout.js +24 -50
- package/spec/mock-project/dist/src/handlers/PostLogout.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/PutCar.js +24 -50
- package/spec/mock-project/dist/src/handlers/PutCar.js.map +1 -0
- package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/spec/mock-project/dist/src/index.js +52 -76
- package/spec/mock-project/dist/src/index.js.map +1 -0
- package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
- package/spec/mock-project/dist/src/repos/CarRepo.js +12 -24
- package/spec/mock-project/dist/src/repos/CarRepo.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/Car.js +3 -1
- package/spec/mock-project/dist/src/schemas/Car.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js +3 -1
- package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js.map +1 -0
- package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js +3 -1
- package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js.map +1 -0
- package/spec/mock-project/dist/src/utils.js +290 -0
- package/spec/mock-project/tsconfig.json +6 -1
- package/spec/schema-generation-nested-objects.spec.ts +97 -0
- package/spec/testHelpers.ts +49 -0
- package/spec/utils.caseConversion.spec.ts +78 -0
- package/spec/utils.spec.ts +13 -13
- package/src/DependencyTracker.ts +166 -0
- package/src/FlinkApp.ts +919 -155
- package/src/FlinkContext.ts +43 -0
- package/src/FlinkErrors.ts +32 -12
- package/src/FlinkHttpHandler.ts +246 -28
- package/src/FlinkJob.ts +11 -0
- package/src/FlinkLog.ts +119 -12
- package/src/FlinkLogFactory.ts +699 -0
- package/src/FlinkRepo.ts +10 -3
- package/src/FlinkRequestContext.ts +95 -0
- package/src/FlinkResponse.ts +6 -0
- package/src/FlinkService.ts +49 -0
- package/src/LeaderElection.ts +203 -0
- package/src/SchemaCache.ts +232 -0
- package/src/TypeScriptCompiler.ts +1347 -610
- package/src/TypeScriptUtils.ts +5 -0
- package/src/ai/AgentRunner.ts +646 -0
- package/src/ai/ConversationAgent.ts +413 -0
- package/src/ai/FlinkAgent.ts +1069 -0
- package/src/ai/FlinkTool.ts +165 -0
- package/src/ai/InMemoryConversationAgent.ts +149 -0
- package/src/ai/LLMAdapter.ts +126 -0
- package/src/ai/ToolExecutor.ts +485 -0
- package/src/ai/agentInstructions.ts +245 -0
- package/src/ai/index.ts +8 -0
- package/src/ai/instructionFileLoader.ts +156 -0
- package/src/auth/FlinkAuthPlugin.ts +2 -1
- package/src/handlers/StreamWriterFactory.ts +84 -0
- package/src/index.ts +14 -0
- package/src/loadPluginSchemas.ts +141 -0
- package/src/schema-extraction/TypeScriptSourceParser.ts +1058 -0
- package/src/schema-extraction/TypeScriptTokenizer.ts +205 -0
- package/src/schema-extraction/index.ts +2 -0
- package/src/schema-extraction/types.ts +34 -0
- package/src/utils/loadFlinkConfig.ts +89 -0
- package/src/utils.ts +52 -0
- 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";
|
package/src/ai/index.ts
ADDED
|
@@ -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
|
+
}
|