@flink-app/flink 2.0.0-alpha.59 → 2.0.0-alpha.60
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 +2 -0
- package/cli/dev.ts +14 -0
- package/cli/loadEnvFiles.ts +116 -0
- package/cli/run.ts +14 -0
- package/dist/cli/dev.js +15 -4
- package/dist/cli/loadEnvFiles.d.ts +30 -0
- package/dist/cli/loadEnvFiles.js +113 -0
- package/dist/cli/run.js +15 -4
- package/dist/src/ai/AgentRunner.js +1 -1
- package/dist/src/ai/FlinkAgent.d.ts +3 -2
- package/dist/src/ai/agentInstructions.js +60 -20
- package/package.json +2 -2
- package/spec/AgentRunner.spec.ts +1 -1
- package/spec/FlinkAgent.spec.ts +1 -1
- package/src/ai/AgentRunner.ts +1 -1
- package/src/ai/FlinkAgent.ts +3 -2
- package/src/ai/agentInstructions.ts +67 -21
package/CHANGELOG.md
CHANGED
package/cli/dev.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { existsSync } from "fs";
|
|
|
6
6
|
import { parseArgs, hasFlag, getOption, normalizeEntry, printHelp, compile, forkApp } from "./cli-utils";
|
|
7
7
|
import { FlinkLogFactory } from "../src/FlinkLogFactory";
|
|
8
8
|
import { loadFlinkConfig } from "../src/utils/loadFlinkConfig";
|
|
9
|
+
import { loadEnvFiles } from "./loadEnvFiles";
|
|
9
10
|
|
|
10
11
|
// Load config BEFORE creating logger
|
|
11
12
|
const flinkConfig = loadFlinkConfig();
|
|
@@ -16,6 +17,19 @@ const logger = FlinkLogFactory.createLogger("flink.dev");
|
|
|
16
17
|
module.exports = async function dev(args: string[]) {
|
|
17
18
|
const parsed = parseArgs(args);
|
|
18
19
|
|
|
20
|
+
// Load .env files before anything else so all env vars are available at startup.
|
|
21
|
+
// NODE_ENV defaults to "development" when not explicitly set.
|
|
22
|
+
if (!process.env.NODE_ENV) {
|
|
23
|
+
process.env.NODE_ENV = "development";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const initLogger = FlinkLogFactory.createLogger("flink.init");
|
|
27
|
+
const { loaded, injected } = loadEnvFiles(parsed.dir, "development");
|
|
28
|
+
|
|
29
|
+
if (loaded.length > 0) {
|
|
30
|
+
initLogger.info(`Loaded env files: ${loaded.join(", ")} (${injected} vars injected)`);
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
if (hasFlag(parsed, "help")) {
|
|
20
34
|
printHelp({
|
|
21
35
|
description:
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { resolve, join } from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse the contents of a .env file into a key/value map.
|
|
6
|
+
*
|
|
7
|
+
* Rules:
|
|
8
|
+
* - Empty lines and lines starting with # are skipped
|
|
9
|
+
* - "export FOO=bar" is treated the same as "FOO=bar"
|
|
10
|
+
* - Quoted values (single or double) are unquoted, and their content is used as-is
|
|
11
|
+
* - Unquoted values have inline comments stripped: FOO=bar # comment → "bar"
|
|
12
|
+
*/
|
|
13
|
+
export function parseEnvContent(content: string): Record<string, string> {
|
|
14
|
+
const result: Record<string, string> = {};
|
|
15
|
+
|
|
16
|
+
for (const rawLine of content.split("\n")) {
|
|
17
|
+
let line = rawLine.trim();
|
|
18
|
+
|
|
19
|
+
// Skip empty lines and full-line comments
|
|
20
|
+
if (!line || line.startsWith("#")) continue;
|
|
21
|
+
|
|
22
|
+
// Strip "export " prefix
|
|
23
|
+
if (line.startsWith("export ")) {
|
|
24
|
+
line = line.slice(7).trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const eqIdx = line.indexOf("=");
|
|
28
|
+
if (eqIdx === -1) continue;
|
|
29
|
+
|
|
30
|
+
const key = line.slice(0, eqIdx).trim();
|
|
31
|
+
if (!key) continue;
|
|
32
|
+
|
|
33
|
+
let value = line.slice(eqIdx + 1);
|
|
34
|
+
|
|
35
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
36
|
+
// Quoted value — strip quotes, preserve content as-is
|
|
37
|
+
value = value.slice(1, -1);
|
|
38
|
+
} else {
|
|
39
|
+
// Unquoted — strip inline comments (" # ...") and trim
|
|
40
|
+
const commentIdx = value.search(/ #/);
|
|
41
|
+
if (commentIdx !== -1) {
|
|
42
|
+
value = value.slice(0, commentIdx);
|
|
43
|
+
}
|
|
44
|
+
value = value.trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
result[key] = value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readEnvFile(filepath: string): Record<string, string> | null {
|
|
54
|
+
if (!existsSync(filepath)) return null;
|
|
55
|
+
try {
|
|
56
|
+
return parseEnvContent(readFileSync(filepath, "utf-8"));
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface LoadEnvFilesResult {
|
|
63
|
+
/** Relative filenames that were found and loaded, in load order */
|
|
64
|
+
loaded: string[];
|
|
65
|
+
/** Number of variables injected into process.env (excludes already-set vars) */
|
|
66
|
+
injected: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Load .env files in Next.js-compatible order.
|
|
71
|
+
*
|
|
72
|
+
* Priority (highest wins):
|
|
73
|
+
* 1. process.env (already-set variables are never overridden)
|
|
74
|
+
* 2. .env.[environment].local
|
|
75
|
+
* 3. .env.local (skipped when environment === "test")
|
|
76
|
+
* 4. .env.[environment]
|
|
77
|
+
* 5. .env
|
|
78
|
+
*
|
|
79
|
+
* @param dir Root directory of the Flink app (where .env files live)
|
|
80
|
+
* @param environment "development" | "production" | "test"
|
|
81
|
+
*/
|
|
82
|
+
export function loadEnvFiles(dir: string, environment: "development" | "production" | "test"): LoadEnvFilesResult {
|
|
83
|
+
const absoluteDir = resolve(dir);
|
|
84
|
+
|
|
85
|
+
// Files listed from lowest to highest priority so that Object.assign
|
|
86
|
+
// naturally lets later files override earlier ones.
|
|
87
|
+
const candidates: string[] = [
|
|
88
|
+
".env",
|
|
89
|
+
`.env.${environment}`,
|
|
90
|
+
// .env.local is intentionally skipped in test (matches Next.js behaviour)
|
|
91
|
+
...(environment !== "test" ? [".env.local"] : []),
|
|
92
|
+
`.env.${environment}.local`,
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const loaded: string[] = [];
|
|
96
|
+
const merged: Record<string, string> = {};
|
|
97
|
+
|
|
98
|
+
for (const filename of candidates) {
|
|
99
|
+
const parsed = readEnvFile(join(absoluteDir, filename));
|
|
100
|
+
if (parsed !== null) {
|
|
101
|
+
loaded.push(filename);
|
|
102
|
+
Object.assign(merged, parsed);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Inject into process.env — existing values always win
|
|
107
|
+
let injected = 0;
|
|
108
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
109
|
+
if (!(key in process.env)) {
|
|
110
|
+
process.env[key] = value;
|
|
111
|
+
injected++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { loaded, injected };
|
|
116
|
+
}
|
package/cli/run.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { parseArgs, hasFlag, getOption, normalizeEntry, printHelp, compile, forkApp } from "./cli-utils";
|
|
3
3
|
import { FlinkLogFactory } from "../src/FlinkLogFactory";
|
|
4
4
|
import { loadFlinkConfig } from "../src/utils/loadFlinkConfig";
|
|
5
|
+
import { loadEnvFiles } from "./loadEnvFiles";
|
|
5
6
|
|
|
6
7
|
// Load config BEFORE creating logger
|
|
7
8
|
const flinkConfig = loadFlinkConfig();
|
|
@@ -10,6 +11,19 @@ FlinkLogFactory.configure(flinkConfig?.logging);
|
|
|
10
11
|
module.exports = async function run(args: string[]) {
|
|
11
12
|
const parsed = parseArgs(args);
|
|
12
13
|
|
|
14
|
+
// Load .env files before anything else so all env vars are available at startup.
|
|
15
|
+
// NODE_ENV defaults to "production" when not explicitly set.
|
|
16
|
+
if (!process.env.NODE_ENV) {
|
|
17
|
+
process.env.NODE_ENV = "production";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const initLogger = FlinkLogFactory.createLogger("flink.init");
|
|
21
|
+
const { loaded, injected } = loadEnvFiles(parsed.dir, "production");
|
|
22
|
+
|
|
23
|
+
if (loaded.length > 0) {
|
|
24
|
+
initLogger.info(`Loaded env files: ${loaded.join(", ")} (${injected} vars injected)`);
|
|
25
|
+
}
|
|
26
|
+
|
|
13
27
|
if (hasFlag(parsed, "help")) {
|
|
14
28
|
printHelp({
|
|
15
29
|
description: "Compiles and starts the application.",
|
package/dist/cli/dev.js
CHANGED
|
@@ -47,18 +47,29 @@ var fs_1 = require("fs");
|
|
|
47
47
|
var cli_utils_1 = require("./cli-utils");
|
|
48
48
|
var FlinkLogFactory_1 = require("../src/FlinkLogFactory");
|
|
49
49
|
var loadFlinkConfig_1 = require("../src/utils/loadFlinkConfig");
|
|
50
|
+
var loadEnvFiles_1 = require("./loadEnvFiles");
|
|
50
51
|
// Load config BEFORE creating logger
|
|
51
52
|
var flinkConfig = (0, loadFlinkConfig_1.loadFlinkConfig)();
|
|
52
53
|
FlinkLogFactory_1.FlinkLogFactory.configure(flinkConfig === null || flinkConfig === void 0 ? void 0 : flinkConfig.logging);
|
|
53
54
|
var logger = FlinkLogFactory_1.FlinkLogFactory.createLogger("flink.dev");
|
|
54
55
|
module.exports = function dev(args) {
|
|
55
56
|
return __awaiter(this, void 0, void 0, function () {
|
|
56
|
-
var parsed, dir, entry, typecheck, noTypecheck, serverProcess, tscProcess, isRebuilding, pendingRebuild, watcher;
|
|
57
|
+
var parsed, initLogger, _a, loaded, injected, dir, entry, typecheck, noTypecheck, serverProcess, tscProcess, isRebuilding, pendingRebuild, watcher;
|
|
57
58
|
var _this = this;
|
|
58
|
-
return __generator(this, function (
|
|
59
|
-
switch (
|
|
59
|
+
return __generator(this, function (_b) {
|
|
60
|
+
switch (_b.label) {
|
|
60
61
|
case 0:
|
|
61
62
|
parsed = (0, cli_utils_1.parseArgs)(args);
|
|
63
|
+
// Load .env files before anything else so all env vars are available at startup.
|
|
64
|
+
// NODE_ENV defaults to "development" when not explicitly set.
|
|
65
|
+
if (!process.env.NODE_ENV) {
|
|
66
|
+
process.env.NODE_ENV = "development";
|
|
67
|
+
}
|
|
68
|
+
initLogger = FlinkLogFactory_1.FlinkLogFactory.createLogger("flink.init");
|
|
69
|
+
_a = (0, loadEnvFiles_1.loadEnvFiles)(parsed.dir, "development"), loaded = _a.loaded, injected = _a.injected;
|
|
70
|
+
if (loaded.length > 0) {
|
|
71
|
+
initLogger.info("Loaded env files: ".concat(loaded.join(", "), " (").concat(injected, " vars injected)"));
|
|
72
|
+
}
|
|
62
73
|
if ((0, cli_utils_1.hasFlag)(parsed, "help")) {
|
|
63
74
|
(0, cli_utils_1.printHelp)({
|
|
64
75
|
description: "Starts development mode with watch and auto-reload.\n Fast rebuilds with swc (skips type checking by default).\n Runs background tsc --watch for async type error reporting.",
|
|
@@ -81,7 +92,7 @@ module.exports = function dev(args) {
|
|
|
81
92
|
console.log("\uD83D\uDCE6 Initial build ".concat(typecheck ? "(with type checking)" : "(fast mode, skipping type check)", "..."));
|
|
82
93
|
return [4 /*yield*/, (0, cli_utils_1.compile)({ dir: dir, entry: entry, typeCheck: typecheck })];
|
|
83
94
|
case 1:
|
|
84
|
-
|
|
95
|
+
_b.sent();
|
|
85
96
|
serverProcess = null;
|
|
86
97
|
tscProcess = null;
|
|
87
98
|
isRebuilding = false;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse the contents of a .env file into a key/value map.
|
|
3
|
+
*
|
|
4
|
+
* Rules:
|
|
5
|
+
* - Empty lines and lines starting with # are skipped
|
|
6
|
+
* - "export FOO=bar" is treated the same as "FOO=bar"
|
|
7
|
+
* - Quoted values (single or double) are unquoted, and their content is used as-is
|
|
8
|
+
* - Unquoted values have inline comments stripped: FOO=bar # comment → "bar"
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseEnvContent(content: string): Record<string, string>;
|
|
11
|
+
export interface LoadEnvFilesResult {
|
|
12
|
+
/** Relative filenames that were found and loaded, in load order */
|
|
13
|
+
loaded: string[];
|
|
14
|
+
/** Number of variables injected into process.env (excludes already-set vars) */
|
|
15
|
+
injected: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Load .env files in Next.js-compatible order.
|
|
19
|
+
*
|
|
20
|
+
* Priority (highest wins):
|
|
21
|
+
* 1. process.env (already-set variables are never overridden)
|
|
22
|
+
* 2. .env.[environment].local
|
|
23
|
+
* 3. .env.local (skipped when environment === "test")
|
|
24
|
+
* 4. .env.[environment]
|
|
25
|
+
* 5. .env
|
|
26
|
+
*
|
|
27
|
+
* @param dir Root directory of the Flink app (where .env files live)
|
|
28
|
+
* @param environment "development" | "production" | "test"
|
|
29
|
+
*/
|
|
30
|
+
export declare function loadEnvFiles(dir: string, environment: "development" | "production" | "test"): LoadEnvFilesResult;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
3
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
4
|
+
if (ar || !(i in from)) {
|
|
5
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
6
|
+
ar[i] = from[i];
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.parseEnvContent = parseEnvContent;
|
|
13
|
+
exports.loadEnvFiles = loadEnvFiles;
|
|
14
|
+
var fs_1 = require("fs");
|
|
15
|
+
var path_1 = require("path");
|
|
16
|
+
/**
|
|
17
|
+
* Parse the contents of a .env file into a key/value map.
|
|
18
|
+
*
|
|
19
|
+
* Rules:
|
|
20
|
+
* - Empty lines and lines starting with # are skipped
|
|
21
|
+
* - "export FOO=bar" is treated the same as "FOO=bar"
|
|
22
|
+
* - Quoted values (single or double) are unquoted, and their content is used as-is
|
|
23
|
+
* - Unquoted values have inline comments stripped: FOO=bar # comment → "bar"
|
|
24
|
+
*/
|
|
25
|
+
function parseEnvContent(content) {
|
|
26
|
+
var result = {};
|
|
27
|
+
for (var _i = 0, _a = content.split("\n"); _i < _a.length; _i++) {
|
|
28
|
+
var rawLine = _a[_i];
|
|
29
|
+
var line = rawLine.trim();
|
|
30
|
+
// Skip empty lines and full-line comments
|
|
31
|
+
if (!line || line.startsWith("#"))
|
|
32
|
+
continue;
|
|
33
|
+
// Strip "export " prefix
|
|
34
|
+
if (line.startsWith("export ")) {
|
|
35
|
+
line = line.slice(7).trim();
|
|
36
|
+
}
|
|
37
|
+
var eqIdx = line.indexOf("=");
|
|
38
|
+
if (eqIdx === -1)
|
|
39
|
+
continue;
|
|
40
|
+
var key = line.slice(0, eqIdx).trim();
|
|
41
|
+
if (!key)
|
|
42
|
+
continue;
|
|
43
|
+
var value = line.slice(eqIdx + 1);
|
|
44
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
45
|
+
// Quoted value — strip quotes, preserve content as-is
|
|
46
|
+
value = value.slice(1, -1);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Unquoted — strip inline comments (" # ...") and trim
|
|
50
|
+
var commentIdx = value.search(/ #/);
|
|
51
|
+
if (commentIdx !== -1) {
|
|
52
|
+
value = value.slice(0, commentIdx);
|
|
53
|
+
}
|
|
54
|
+
value = value.trim();
|
|
55
|
+
}
|
|
56
|
+
result[key] = value;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function readEnvFile(filepath) {
|
|
61
|
+
if (!(0, fs_1.existsSync)(filepath))
|
|
62
|
+
return null;
|
|
63
|
+
try {
|
|
64
|
+
return parseEnvContent((0, fs_1.readFileSync)(filepath, "utf-8"));
|
|
65
|
+
}
|
|
66
|
+
catch (_a) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Load .env files in Next.js-compatible order.
|
|
72
|
+
*
|
|
73
|
+
* Priority (highest wins):
|
|
74
|
+
* 1. process.env (already-set variables are never overridden)
|
|
75
|
+
* 2. .env.[environment].local
|
|
76
|
+
* 3. .env.local (skipped when environment === "test")
|
|
77
|
+
* 4. .env.[environment]
|
|
78
|
+
* 5. .env
|
|
79
|
+
*
|
|
80
|
+
* @param dir Root directory of the Flink app (where .env files live)
|
|
81
|
+
* @param environment "development" | "production" | "test"
|
|
82
|
+
*/
|
|
83
|
+
function loadEnvFiles(dir, environment) {
|
|
84
|
+
var absoluteDir = (0, path_1.resolve)(dir);
|
|
85
|
+
// Files listed from lowest to highest priority so that Object.assign
|
|
86
|
+
// naturally lets later files override earlier ones.
|
|
87
|
+
var candidates = __spreadArray(__spreadArray([
|
|
88
|
+
".env",
|
|
89
|
+
".env.".concat(environment)
|
|
90
|
+
], (environment !== "test" ? [".env.local"] : []), true), [
|
|
91
|
+
".env.".concat(environment, ".local"),
|
|
92
|
+
], false);
|
|
93
|
+
var loaded = [];
|
|
94
|
+
var merged = {};
|
|
95
|
+
for (var _i = 0, candidates_1 = candidates; _i < candidates_1.length; _i++) {
|
|
96
|
+
var filename = candidates_1[_i];
|
|
97
|
+
var parsed = readEnvFile((0, path_1.join)(absoluteDir, filename));
|
|
98
|
+
if (parsed !== null) {
|
|
99
|
+
loaded.push(filename);
|
|
100
|
+
Object.assign(merged, parsed);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Inject into process.env — existing values always win
|
|
104
|
+
var injected = 0;
|
|
105
|
+
for (var _a = 0, _b = Object.entries(merged); _a < _b.length; _a++) {
|
|
106
|
+
var _c = _b[_a], key = _c[0], value = _c[1];
|
|
107
|
+
if (!(key in process.env)) {
|
|
108
|
+
process.env[key] = value;
|
|
109
|
+
injected++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { loaded: loaded, injected: injected };
|
|
113
|
+
}
|
package/dist/cli/run.js
CHANGED
|
@@ -40,16 +40,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
40
40
|
var cli_utils_1 = require("./cli-utils");
|
|
41
41
|
var FlinkLogFactory_1 = require("../src/FlinkLogFactory");
|
|
42
42
|
var loadFlinkConfig_1 = require("../src/utils/loadFlinkConfig");
|
|
43
|
+
var loadEnvFiles_1 = require("./loadEnvFiles");
|
|
43
44
|
// Load config BEFORE creating logger
|
|
44
45
|
var flinkConfig = (0, loadFlinkConfig_1.loadFlinkConfig)();
|
|
45
46
|
FlinkLogFactory_1.FlinkLogFactory.configure(flinkConfig === null || flinkConfig === void 0 ? void 0 : flinkConfig.logging);
|
|
46
47
|
module.exports = function run(args) {
|
|
47
48
|
return __awaiter(this, void 0, void 0, function () {
|
|
48
|
-
var parsed, entry;
|
|
49
|
-
return __generator(this, function (
|
|
50
|
-
switch (
|
|
49
|
+
var parsed, initLogger, _a, loaded, injected, entry;
|
|
50
|
+
return __generator(this, function (_b) {
|
|
51
|
+
switch (_b.label) {
|
|
51
52
|
case 0:
|
|
52
53
|
parsed = (0, cli_utils_1.parseArgs)(args);
|
|
54
|
+
// Load .env files before anything else so all env vars are available at startup.
|
|
55
|
+
// NODE_ENV defaults to "production" when not explicitly set.
|
|
56
|
+
if (!process.env.NODE_ENV) {
|
|
57
|
+
process.env.NODE_ENV = "production";
|
|
58
|
+
}
|
|
59
|
+
initLogger = FlinkLogFactory_1.FlinkLogFactory.createLogger("flink.init");
|
|
60
|
+
_a = (0, loadEnvFiles_1.loadEnvFiles)(parsed.dir, "production"), loaded = _a.loaded, injected = _a.injected;
|
|
61
|
+
if (loaded.length > 0) {
|
|
62
|
+
initLogger.info("Loaded env files: ".concat(loaded.join(", "), " (").concat(injected, " vars injected)"));
|
|
63
|
+
}
|
|
53
64
|
if ((0, cli_utils_1.hasFlag)(parsed, "help")) {
|
|
54
65
|
(0, cli_utils_1.printHelp)({
|
|
55
66
|
description: "Compiles and starts the application.",
|
|
@@ -80,7 +91,7 @@ module.exports = function run(args) {
|
|
|
80
91
|
typeCheck: true,
|
|
81
92
|
})];
|
|
82
93
|
case 1:
|
|
83
|
-
|
|
94
|
+
_b.sent();
|
|
84
95
|
(0, cli_utils_1.forkApp)({
|
|
85
96
|
dir: parsed.dir,
|
|
86
97
|
args: args,
|
|
@@ -132,7 +132,7 @@ var AgentRunner = /** @class */ (function () {
|
|
|
132
132
|
return [3 /*break*/, 6];
|
|
133
133
|
case 5:
|
|
134
134
|
err_1 = _o.sent();
|
|
135
|
-
throw new Error("Failed to resolve
|
|
135
|
+
throw new Error("Failed to resolve instructions for agent ".concat(this.agentName, ": ").concat(err_1.message));
|
|
136
136
|
case 6:
|
|
137
137
|
if (input.history && input.history.length > 0) {
|
|
138
138
|
// Start with history
|
|
@@ -481,8 +481,9 @@ export interface AgentExecuteInput<ConversationCtx = any> {
|
|
|
481
481
|
*/
|
|
482
482
|
conversationContext?: ConversationCtx;
|
|
483
483
|
/**
|
|
484
|
-
* Previous conversation messages for context
|
|
485
|
-
*
|
|
484
|
+
* Previous conversation messages for context, in chronological order (oldest first).
|
|
485
|
+
* The new `message` will be appended after these history messages.
|
|
486
|
+
* Agent can load this in beforeRun hook based on conversationId.
|
|
486
487
|
*/
|
|
487
488
|
history?: Message[];
|
|
488
489
|
/**
|
|
@@ -84,19 +84,25 @@ var fileCache = new Map();
|
|
|
84
84
|
/**
|
|
85
85
|
* Get caller's file location using stack trace API
|
|
86
86
|
* This enables agent-relative path resolution for ./ and ../ prefixes
|
|
87
|
+
*
|
|
88
|
+
* Walks the stack to find the first frame outside this file,
|
|
89
|
+
* which is the agent class that called agentInstructions().
|
|
87
90
|
*/
|
|
88
91
|
function getCallerFilePath() {
|
|
92
|
+
var _a, _b;
|
|
89
93
|
var originalPrepareStackTrace = Error.prepareStackTrace;
|
|
90
94
|
Error.prepareStackTrace = function (_, stack) { return stack; };
|
|
91
95
|
var stack = new Error().stack;
|
|
92
96
|
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
93
|
-
//
|
|
94
|
-
var
|
|
95
|
-
var
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
// Find first frame outside this file (skips getCallerFilePath, resolveFilePath, agentInstructions)
|
|
98
|
+
var thisFile = (_a = stack[0]) === null || _a === void 0 ? void 0 : _a.getFileName();
|
|
99
|
+
for (var i = 1; i < stack.length; i++) {
|
|
100
|
+
var fileName = (_b = stack[i]) === null || _b === void 0 ? void 0 : _b.getFileName();
|
|
101
|
+
if (fileName && fileName !== thisFile) {
|
|
102
|
+
return fileName;
|
|
103
|
+
}
|
|
98
104
|
}
|
|
99
|
-
|
|
105
|
+
throw new Error("Could not determine caller file location for agent-relative path");
|
|
100
106
|
}
|
|
101
107
|
/**
|
|
102
108
|
* Resolve file path based on prefix:
|
|
@@ -104,14 +110,33 @@ function getCallerFilePath() {
|
|
|
104
110
|
* - otherwise = project root-relative
|
|
105
111
|
*/
|
|
106
112
|
function resolveFilePath(filePath) {
|
|
113
|
+
// Always try project root-relative first (most intuitive for users)
|
|
114
|
+
var cwdResolved = path.join(process.cwd(), filePath);
|
|
115
|
+
if (fs.existsSync(cwdResolved)) {
|
|
116
|
+
return cwdResolved;
|
|
117
|
+
}
|
|
107
118
|
if (filePath.startsWith("./") || filePath.startsWith("../")) {
|
|
108
119
|
// Agent-relative: get caller's file location via stack trace
|
|
109
120
|
var callerFile = getCallerFilePath();
|
|
110
|
-
|
|
121
|
+
var resolved = path.resolve(path.dirname(callerFile), filePath);
|
|
122
|
+
if (fs.existsSync(resolved)) {
|
|
123
|
+
return resolved;
|
|
124
|
+
}
|
|
125
|
+
// At runtime, callerFile points to compiled JS in dist/.
|
|
126
|
+
// Non-JS assets (e.g. .md instruction files) live in the source tree,
|
|
127
|
+
// so try the equivalent source path with dist/ removed.
|
|
128
|
+
var distIndex = resolved.lastIndexOf(path.sep + "dist" + path.sep);
|
|
129
|
+
if (distIndex !== -1) {
|
|
130
|
+
var sourcePath = resolved.substring(0, distIndex) + resolved.substring(distIndex + 5); // remove "/dist"
|
|
131
|
+
if (fs.existsSync(sourcePath)) {
|
|
132
|
+
return sourcePath;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return resolved;
|
|
111
136
|
}
|
|
112
137
|
else {
|
|
113
138
|
// Project root-relative
|
|
114
|
-
return
|
|
139
|
+
return cwdResolved;
|
|
115
140
|
}
|
|
116
141
|
}
|
|
117
142
|
/**
|
|
@@ -125,12 +150,13 @@ function loadFile(resolvedPath, originalPath) {
|
|
|
125
150
|
// Check cache
|
|
126
151
|
var cached = fileCache.get(resolvedPath);
|
|
127
152
|
if (cached && cached.mtime === mtime) {
|
|
128
|
-
return cached
|
|
153
|
+
return cached;
|
|
129
154
|
}
|
|
130
|
-
// Read file and update cache
|
|
155
|
+
// Read file and update cache (clear compiled template on file change)
|
|
131
156
|
var content = fs.readFileSync(resolvedPath, "utf-8");
|
|
132
|
-
|
|
133
|
-
|
|
157
|
+
var entry = { content: content, mtime: mtime };
|
|
158
|
+
fileCache.set(resolvedPath, entry);
|
|
159
|
+
return entry;
|
|
134
160
|
}
|
|
135
161
|
catch (err) {
|
|
136
162
|
if (err.code === "ENOENT") {
|
|
@@ -140,12 +166,22 @@ function loadFile(resolvedPath, originalPath) {
|
|
|
140
166
|
}
|
|
141
167
|
}
|
|
142
168
|
/**
|
|
143
|
-
* Render template using Handlebars
|
|
169
|
+
* Render template using Handlebars with compiled template caching.
|
|
170
|
+
* Skips Handlebars entirely when content has no template expressions.
|
|
144
171
|
*/
|
|
145
|
-
function renderTemplate(
|
|
172
|
+
function renderTemplate(cached, data, filePath) {
|
|
173
|
+
if (cached.hasTemplateExpressions === false) {
|
|
174
|
+
return cached.content;
|
|
175
|
+
}
|
|
146
176
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
177
|
+
if (!cached.compiledTemplate) {
|
|
178
|
+
cached.hasTemplateExpressions = /\{\{/.test(cached.content);
|
|
179
|
+
if (!cached.hasTemplateExpressions) {
|
|
180
|
+
return cached.content;
|
|
181
|
+
}
|
|
182
|
+
cached.compiledTemplate = handlebars_1.default.compile(cached.content);
|
|
183
|
+
}
|
|
184
|
+
return cached.compiledTemplate(data);
|
|
149
185
|
}
|
|
150
186
|
catch (err) {
|
|
151
187
|
throw new Error("Failed to render template in ".concat(filePath, ": ").concat(err.message));
|
|
@@ -221,11 +257,15 @@ function agentInstructions(filePath, variables) {
|
|
|
221
257
|
var resolvedPath = resolveFilePath(filePath);
|
|
222
258
|
// Return callback that will be invoked by FlinkAgent
|
|
223
259
|
return function (ctx, agentContext) { return __awaiter(_this, void 0, void 0, function () {
|
|
224
|
-
var
|
|
260
|
+
var cached, vars, templateData;
|
|
225
261
|
return __generator(this, function (_a) {
|
|
226
262
|
switch (_a.label) {
|
|
227
263
|
case 0:
|
|
228
|
-
|
|
264
|
+
cached = loadFile(resolvedPath, filePath);
|
|
265
|
+
// Skip template processing entirely when no expressions exist
|
|
266
|
+
if (cached.hasTemplateExpressions === false) {
|
|
267
|
+
return [2 /*return*/, cached.content];
|
|
268
|
+
}
|
|
229
269
|
vars = {};
|
|
230
270
|
if (!variables) return [3 /*break*/, 3];
|
|
231
271
|
if (!(typeof variables === "function")) return [3 /*break*/, 2];
|
|
@@ -238,8 +278,8 @@ function agentInstructions(filePath, variables) {
|
|
|
238
278
|
_a.label = 3;
|
|
239
279
|
case 3:
|
|
240
280
|
templateData = __assign(__assign({}, vars), { ctx: ctx, agentContext: agentContext, user: agentContext.user });
|
|
241
|
-
// Render template
|
|
242
|
-
return [2 /*return*/, renderTemplate(
|
|
281
|
+
// Render template (compiles and caches Handlebars template on first call)
|
|
282
|
+
return [2 /*return*/, renderTemplate(cached, templateData, resolvedPath)];
|
|
243
283
|
}
|
|
244
284
|
});
|
|
245
285
|
}); };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/flink",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.60",
|
|
4
4
|
"description": "Typescript only framework for creating REST-like APIs on top of Express and mongodb",
|
|
5
5
|
"types": "dist/src/index.d.ts",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"ts-morph": "24.0.0",
|
|
57
57
|
"uuid": "^8.3.2",
|
|
58
58
|
"zod": "^4.3.6",
|
|
59
|
-
"@flink-app/ts-source-to-json-schema": "^0.1.
|
|
59
|
+
"@flink-app/ts-source-to-json-schema": "^0.1.6"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@swc/core": "^1.15.17",
|
package/spec/AgentRunner.spec.ts
CHANGED
|
@@ -993,7 +993,7 @@ describe("AgentRunner", () => {
|
|
|
993
993
|
}
|
|
994
994
|
fail("Expected error to be thrown");
|
|
995
995
|
} catch (err: any) {
|
|
996
|
-
expect(err.message).toContain("Failed to resolve
|
|
996
|
+
expect(err.message).toContain("Failed to resolve instructions for agent test_agent");
|
|
997
997
|
expect(err.message).toContain("Database connection failed");
|
|
998
998
|
}
|
|
999
999
|
});
|
package/spec/FlinkAgent.spec.ts
CHANGED
|
@@ -504,7 +504,7 @@ describe("FlinkAgent", () => {
|
|
|
504
504
|
(agent as any).ctx = mockCtx;
|
|
505
505
|
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
506
506
|
|
|
507
|
-
await expectAsync(agent.query("Hello")).toBeRejectedWithError(/Failed to resolve
|
|
507
|
+
await expectAsync(agent.query("Hello")).toBeRejectedWithError(/Failed to resolve instructions/);
|
|
508
508
|
});
|
|
509
509
|
});
|
|
510
510
|
|
package/src/ai/AgentRunner.ts
CHANGED
|
@@ -60,7 +60,7 @@ export class AgentRunner {
|
|
|
60
60
|
resolvedInstructions =
|
|
61
61
|
typeof this.agentProps.instructions === "function" ? await this.agentProps.instructions(this.ctx, execContext) : this.agentProps.instructions;
|
|
62
62
|
} catch (err: any) {
|
|
63
|
-
throw new Error(`Failed to resolve
|
|
63
|
+
throw new Error(`Failed to resolve instructions for agent ${this.agentName}: ${err.message}`);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Initialize messages from history + new input
|
package/src/ai/FlinkAgent.ts
CHANGED
|
@@ -841,8 +841,9 @@ export interface AgentExecuteInput<ConversationCtx = any> {
|
|
|
841
841
|
conversationContext?: ConversationCtx;
|
|
842
842
|
|
|
843
843
|
/**
|
|
844
|
-
* Previous conversation messages for context
|
|
845
|
-
*
|
|
844
|
+
* Previous conversation messages for context, in chronological order (oldest first).
|
|
845
|
+
* The new `message` will be appended after these history messages.
|
|
846
|
+
* Agent can load this in beforeRun hook based on conversationId.
|
|
846
847
|
*/
|
|
847
848
|
history?: Message[];
|
|
848
849
|
|
|
@@ -10,6 +10,8 @@ import { InstructionsCallback, AgentExecuteContext } from "./FlinkAgent";
|
|
|
10
10
|
interface FileCacheEntry {
|
|
11
11
|
content: string;
|
|
12
12
|
mtime: number;
|
|
13
|
+
compiledTemplate?: Handlebars.TemplateDelegate;
|
|
14
|
+
hasTemplateExpressions?: boolean;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -20,6 +22,9 @@ const fileCache = new Map<string, FileCacheEntry>();
|
|
|
20
22
|
/**
|
|
21
23
|
* Get caller's file location using stack trace API
|
|
22
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().
|
|
23
28
|
*/
|
|
24
29
|
function getCallerFilePath(): string {
|
|
25
30
|
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
@@ -27,15 +32,16 @@ function getCallerFilePath(): string {
|
|
|
27
32
|
const stack = new Error().stack as unknown as NodeJS.CallSite[];
|
|
28
33
|
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
29
34
|
|
|
30
|
-
//
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
}
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
throw new Error("Could not determine caller file location for agent-relative path");
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
/**
|
|
@@ -44,13 +50,36 @@ function getCallerFilePath(): string {
|
|
|
44
50
|
* - otherwise = project root-relative
|
|
45
51
|
*/
|
|
46
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
|
+
|
|
47
59
|
if (filePath.startsWith("./") || filePath.startsWith("../")) {
|
|
48
60
|
// Agent-relative: get caller's file location via stack trace
|
|
49
61
|
const callerFile = getCallerFilePath();
|
|
50
|
-
|
|
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;
|
|
51
80
|
} else {
|
|
52
81
|
// Project root-relative
|
|
53
|
-
return
|
|
82
|
+
return cwdResolved;
|
|
54
83
|
}
|
|
55
84
|
}
|
|
56
85
|
|
|
@@ -58,7 +87,7 @@ function resolveFilePath(filePath: string): string {
|
|
|
58
87
|
* Load file contents with smart caching
|
|
59
88
|
* Cache is invalidated when file modification time changes
|
|
60
89
|
*/
|
|
61
|
-
function loadFile(resolvedPath: string, originalPath: string):
|
|
90
|
+
function loadFile(resolvedPath: string, originalPath: string): FileCacheEntry {
|
|
62
91
|
try {
|
|
63
92
|
const stats = fs.statSync(resolvedPath);
|
|
64
93
|
const mtime = stats.mtimeMs;
|
|
@@ -66,14 +95,15 @@ function loadFile(resolvedPath: string, originalPath: string): string {
|
|
|
66
95
|
// Check cache
|
|
67
96
|
const cached = fileCache.get(resolvedPath);
|
|
68
97
|
if (cached && cached.mtime === mtime) {
|
|
69
|
-
return cached
|
|
98
|
+
return cached;
|
|
70
99
|
}
|
|
71
100
|
|
|
72
|
-
// Read file and update cache
|
|
101
|
+
// Read file and update cache (clear compiled template on file change)
|
|
73
102
|
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
74
|
-
|
|
103
|
+
const entry: FileCacheEntry = { content, mtime };
|
|
104
|
+
fileCache.set(resolvedPath, entry);
|
|
75
105
|
|
|
76
|
-
return
|
|
106
|
+
return entry;
|
|
77
107
|
} catch (err: any) {
|
|
78
108
|
if (err.code === "ENOENT") {
|
|
79
109
|
throw new Error(`Agent instructions file not found: ${resolvedPath} (from: ${originalPath})`);
|
|
@@ -83,12 +113,23 @@ function loadFile(resolvedPath: string, originalPath: string): string {
|
|
|
83
113
|
}
|
|
84
114
|
|
|
85
115
|
/**
|
|
86
|
-
* Render template using Handlebars
|
|
116
|
+
* Render template using Handlebars with compiled template caching.
|
|
117
|
+
* Skips Handlebars entirely when content has no template expressions.
|
|
87
118
|
*/
|
|
88
|
-
function renderTemplate(
|
|
119
|
+
function renderTemplate(cached: FileCacheEntry, data: any, filePath: string): string {
|
|
120
|
+
if (cached.hasTemplateExpressions === false) {
|
|
121
|
+
return cached.content;
|
|
122
|
+
}
|
|
123
|
+
|
|
89
124
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
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);
|
|
92
133
|
} catch (err: any) {
|
|
93
134
|
throw new Error(`Failed to render template in ${filePath}: ${err.message}`);
|
|
94
135
|
}
|
|
@@ -170,7 +211,12 @@ export function agentInstructions<Ctx extends FlinkContext = FlinkContext>(
|
|
|
170
211
|
// Return callback that will be invoked by FlinkAgent
|
|
171
212
|
return async (ctx: Ctx, agentContext: AgentExecuteContext): Promise<string> => {
|
|
172
213
|
// Load file (uses cache if available and mtime unchanged)
|
|
173
|
-
const
|
|
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
|
+
}
|
|
174
220
|
|
|
175
221
|
// Resolve variables (static or callback)
|
|
176
222
|
let vars: Record<string, any> = {};
|
|
@@ -190,8 +236,8 @@ export function agentInstructions<Ctx extends FlinkContext = FlinkContext>(
|
|
|
190
236
|
user: agentContext.user,
|
|
191
237
|
};
|
|
192
238
|
|
|
193
|
-
// Render template
|
|
194
|
-
return renderTemplate(
|
|
239
|
+
// Render template (compiles and caches Handlebars template on first call)
|
|
240
|
+
return renderTemplate(cached, templateData, resolvedPath);
|
|
195
241
|
};
|
|
196
242
|
}
|
|
197
243
|
|