jetic-cli 0.1.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -445
- package/dist/dashboard/assets/index-8Xh271uc.js +384 -0
- package/dist/dashboard/assets/index-ANQK1AeT.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/index.js +811 -136
- package/package.json +3 -2
- package/dist/dashboard/assets/index-DEc0dXNe.css +0 -1
- package/dist/dashboard/assets/index-DZwpY2T2.js +0 -384
package/dist/index.js
CHANGED
|
@@ -268088,6 +268088,13 @@ var require_workflow_simulator = __commonJS({
|
|
|
268088
268088
|
const colonIdx = expr.indexOf(":");
|
|
268089
268089
|
const scope = expr.slice(0, colonIdx);
|
|
268090
268090
|
const key = expr.slice(colonIdx + 1);
|
|
268091
|
+
if (scope === "human") {
|
|
268092
|
+
const envVal = process.env[`JETIC_HUMAN_${key.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`];
|
|
268093
|
+
if (envVal !== void 0 && envVal !== "") {
|
|
268094
|
+
replacements.push({ placeholder: match[0], resolved: envVal });
|
|
268095
|
+
continue;
|
|
268096
|
+
}
|
|
268097
|
+
}
|
|
268091
268098
|
const mem = new memory_1.JeticMemory({ scope });
|
|
268092
268099
|
const memVal = await mem.get(key);
|
|
268093
268100
|
replacements.push({ placeholder: match[0], resolved: memVal != null ? String(memVal) : "" });
|
|
@@ -269011,17 +269018,87 @@ var require_validation_tools = __commonJS({
|
|
|
269011
269018
|
}
|
|
269012
269019
|
}
|
|
269013
269020
|
}
|
|
269021
|
+
const fieldCompleteness = {};
|
|
269022
|
+
for (const ep of model.endpoints) {
|
|
269023
|
+
const routeKey = `${ep.method.toUpperCase()} ${ep.path}`;
|
|
269024
|
+
const missing = [];
|
|
269025
|
+
if (!ep.description || ep.description.trim() === "") {
|
|
269026
|
+
missing.push("description");
|
|
269027
|
+
issues.push({
|
|
269028
|
+
severity: "warning",
|
|
269029
|
+
endpointId: ep.id,
|
|
269030
|
+
route: routeKey,
|
|
269031
|
+
code: "MISSING_FIELD_DESCRIPTION",
|
|
269032
|
+
message: `Endpoint "${routeKey}" has no description.`,
|
|
269033
|
+
recommendation: "Add a concise description of what this endpoint does."
|
|
269034
|
+
});
|
|
269035
|
+
}
|
|
269036
|
+
if (!ep.tags || ep.tags.length === 0) {
|
|
269037
|
+
missing.push("tags");
|
|
269038
|
+
issues.push({
|
|
269039
|
+
severity: "warning",
|
|
269040
|
+
endpointId: ep.id,
|
|
269041
|
+
route: routeKey,
|
|
269042
|
+
code: "MISSING_FIELD_TAGS",
|
|
269043
|
+
message: `Endpoint "${routeKey}" has no tags.`,
|
|
269044
|
+
recommendation: 'Add at least one tag to group this endpoint (e.g. "auth", "users").'
|
|
269045
|
+
});
|
|
269046
|
+
}
|
|
269047
|
+
if (ep.parameters) {
|
|
269048
|
+
for (const p of ep.parameters) {
|
|
269049
|
+
if (!p.description || p.description.trim() === "") {
|
|
269050
|
+
const field = `parameters[${p.name}].description`;
|
|
269051
|
+
missing.push(field);
|
|
269052
|
+
issues.push({
|
|
269053
|
+
severity: "warning",
|
|
269054
|
+
endpointId: ep.id,
|
|
269055
|
+
route: routeKey,
|
|
269056
|
+
code: "MISSING_FIELD_PARAM_DESCRIPTION",
|
|
269057
|
+
message: `Parameter "${p.name}" on "${routeKey}" has no description.`,
|
|
269058
|
+
recommendation: `Add a description to parameter "${p.name}".`
|
|
269059
|
+
});
|
|
269060
|
+
}
|
|
269061
|
+
}
|
|
269062
|
+
}
|
|
269063
|
+
if (ep.responses) {
|
|
269064
|
+
for (const [statusCode, resp] of Object.entries(ep.responses)) {
|
|
269065
|
+
const r = resp;
|
|
269066
|
+
if (!r.description || String(r.description).trim() === "") {
|
|
269067
|
+
const field = `responses[${statusCode}].description`;
|
|
269068
|
+
missing.push(field);
|
|
269069
|
+
issues.push({
|
|
269070
|
+
severity: "warning",
|
|
269071
|
+
endpointId: ep.id,
|
|
269072
|
+
route: routeKey,
|
|
269073
|
+
code: "MISSING_FIELD_RESPONSE_DESCRIPTION",
|
|
269074
|
+
message: `Response "${statusCode}" on "${routeKey}" has no description.`,
|
|
269075
|
+
recommendation: `Add a description to the ${statusCode} response (e.g. "User created successfully").`
|
|
269076
|
+
});
|
|
269077
|
+
}
|
|
269078
|
+
}
|
|
269079
|
+
}
|
|
269080
|
+
if (missing.length > 0) {
|
|
269081
|
+
fieldCompleteness[ep.id] = { missingFields: missing };
|
|
269082
|
+
}
|
|
269083
|
+
}
|
|
269084
|
+
const allFieldsFilled = Object.keys(fieldCompleteness).length === 0;
|
|
269014
269085
|
const errors = issues.filter((i) => i.severity === "error");
|
|
269015
269086
|
const warnings = issues.filter((i) => i.severity === "warning");
|
|
269016
269087
|
return {
|
|
269017
269088
|
filePath,
|
|
269018
269089
|
isValid: errors.length === 0,
|
|
269090
|
+
allFieldsFilled,
|
|
269019
269091
|
totalEndpoints: model.endpoints.length,
|
|
269020
269092
|
summary: {
|
|
269021
269093
|
errorsCount: errors.length,
|
|
269022
269094
|
warningsCount: warnings.length
|
|
269023
269095
|
},
|
|
269024
|
-
issues
|
|
269096
|
+
issues,
|
|
269097
|
+
fieldCompleteness,
|
|
269098
|
+
phaseGate: {
|
|
269099
|
+
phase3Complete: errors.length === 0 && allFieldsFilled,
|
|
269100
|
+
message: errors.length === 0 && allFieldsFilled ? "Model is valid and all fields are complete. You may advance to Phase 4 \u2014 ask the user what simulation or workflow they want to build." : "Fix all errors and fill missing fields before proceeding to Phase 4."
|
|
269101
|
+
}
|
|
269025
269102
|
};
|
|
269026
269103
|
}
|
|
269027
269104
|
}
|
|
@@ -269477,8 +269554,8 @@ var require_workflow_authoring_tools = __commonJS({
|
|
|
269477
269554
|
method: zod_1.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]).describe("HTTP method"),
|
|
269478
269555
|
path: zod_1.z.string().min(1).describe("Route path; :params resolve from body/memory (NOT from {{templates}} \u2014 those only work in body/inject)"),
|
|
269479
269556
|
description: zod_1.z.string().optional().describe("What this step does"),
|
|
269480
|
-
body: zod_1.z.record(zod_1.z.any()).optional().describe("Request payload. Values support {{faker.*}}, {{scope:key}} and {{
|
|
269481
|
-
inject: zod_1.z.record(zod_1.z.string()).optional().describe('Memory/header injection: "header:Name" or "body:field" (or bare header name) -> "scope:key" or "{{template}}"'),
|
|
269557
|
+
body: zod_1.z.record(zod_1.z.any()).optional().describe("Request payload. Values support {{faker.*}}, {{scope:key}}, {{workflow:key}} and {{human:key}} templates (human pauses for interactive input at runtime)"),
|
|
269558
|
+
inject: zod_1.z.record(zod_1.z.string()).optional().describe('Memory/header injection: "header:Name" or "body:field" (or bare header name) -> "scope:key" or "{{template}}" ({{human:key}} supported)'),
|
|
269482
269559
|
capture: zod_1.z.record(zod_1.z.string()).optional().describe('Save RESPONSE fields to memory: "scope:key" -> "dot.path.in.response"'),
|
|
269483
269560
|
captureInput: zod_1.z.record(zod_1.z.string()).optional().describe('Save REQUEST body fields to memory BEFORE sending: "scope:key" -> "bodyField"'),
|
|
269484
269561
|
expectStatus: zod_1.z.number().int().positive().optional().describe("Expected HTTP status (default 200; any 2xx passes when expecting 2xx)")
|
|
@@ -269584,6 +269661,7 @@ var require_workflow_authoring_tools = __commonJS({
|
|
|
269584
269661
|
}
|
|
269585
269662
|
const seenNames = /* @__PURE__ */ new Set();
|
|
269586
269663
|
const defined = /* @__PURE__ */ new Set();
|
|
269664
|
+
const humanKeys = /* @__PURE__ */ new Set();
|
|
269587
269665
|
wf.steps.forEach((rawStep, i) => {
|
|
269588
269666
|
const stepNo = i + 1;
|
|
269589
269667
|
const sName = typeof rawStep?.name === "string" ? rawStep.name : void 0;
|
|
@@ -269685,6 +269763,11 @@ var require_workflow_authoring_tools = __commonJS({
|
|
|
269685
269763
|
err(errors, stepNo, sName, "BAD_TEMPLATE", `"{{${ref}}}" has an empty key.`, "Use {{scope:key}} with a real key.");
|
|
269686
269764
|
continue;
|
|
269687
269765
|
}
|
|
269766
|
+
if (scope === "human") {
|
|
269767
|
+
if (key)
|
|
269768
|
+
humanKeys.add(key);
|
|
269769
|
+
continue;
|
|
269770
|
+
}
|
|
269688
269771
|
const full = `${scope}:${key}`;
|
|
269689
269772
|
if (!defined.has(full) && !persistedMemory.has(full)) {
|
|
269690
269773
|
err(errors, stepNo, sName, "UNDEFINED_MEMORY_REF", `"{{${ref}}}" in step ${stepNo} is never captured by an earlier step and is not in memory.json \u2014 it resolves to "".`, `Capture "${full}" in an earlier step (capture/captureInput), or seed it first.`);
|
|
@@ -269732,15 +269815,16 @@ var require_workflow_authoring_tools = __commonJS({
|
|
|
269732
269815
|
}
|
|
269733
269816
|
}
|
|
269734
269817
|
});
|
|
269735
|
-
return toReport(errors, warnings);
|
|
269818
|
+
return toReport(errors, warnings, humanKeys);
|
|
269736
269819
|
}
|
|
269737
|
-
function toReport(errors, warnings) {
|
|
269820
|
+
function toReport(errors, warnings, humanKeys) {
|
|
269738
269821
|
return {
|
|
269739
269822
|
valid: errors.length === 0,
|
|
269740
269823
|
errorsCount: errors.length,
|
|
269741
269824
|
warningsCount: warnings.length,
|
|
269742
269825
|
errors,
|
|
269743
|
-
warnings
|
|
269826
|
+
warnings,
|
|
269827
|
+
humanKeys: [...humanKeys || []].sort()
|
|
269744
269828
|
};
|
|
269745
269829
|
}
|
|
269746
269830
|
function slugifyWorkflowName(name) {
|
|
@@ -269897,6 +269981,7 @@ var require_workflow_authoring_tools = __commonJS({
|
|
|
269897
269981
|
slug,
|
|
269898
269982
|
filePath,
|
|
269899
269983
|
stepsCount: def.steps.length,
|
|
269984
|
+
needsHuman: report.humanKeys,
|
|
269900
269985
|
validation: { ...report, warnings }
|
|
269901
269986
|
};
|
|
269902
269987
|
}
|
|
@@ -269911,6 +269996,10 @@ var require_workflow_authoring_tools = __commonJS({
|
|
|
269911
269996
|
slug,
|
|
269912
269997
|
filePath,
|
|
269913
269998
|
stepsCount: def.steps.length,
|
|
269999
|
+
needsHuman: report.humanKeys,
|
|
270000
|
+
...report.humanKeys.length > 0 ? {
|
|
270001
|
+
humanNote: `Steps need interactive human input for: ${report.humanKeys.map((k) => `{{human:${k}}}`).join(", ")}. Runners pause for it (CLI terminal prompt / dashboard dialog); pre-seed with ${report.humanKeys.map((k) => `JETIC_HUMAN_${k.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`).join(", ")} env or human: memory. Tell the user.`
|
|
270002
|
+
} : {},
|
|
269914
270003
|
validation: { ...report, warnings },
|
|
269915
270004
|
next: `Run it with jetic_simulate_workflow { "workflow": "${slug}" }.`
|
|
269916
270005
|
};
|
|
@@ -269948,7 +270037,7 @@ var require_workflow_authoring_tools = __commonJS({
|
|
|
269948
270037
|
` + report.errors.map((e) => `- [step ${e.stepIndex ?? "-"}] ${e.message} Fix: ${e.fix}`).join("\n"));
|
|
269949
270038
|
}
|
|
269950
270039
|
if (args.validateOnly === true) {
|
|
269951
|
-
return { success: true, action: "validated", saved: false, slug: resolved.slug, filePath: resolved.filePath, stepsCount: def.steps.length, validation: report };
|
|
270040
|
+
return { success: true, action: "validated", saved: false, slug: resolved.slug, filePath: resolved.filePath, stepsCount: def.steps.length, needsHuman: report.humanKeys, validation: report };
|
|
269952
270041
|
}
|
|
269953
270042
|
writeWorkflowFile(resolved.filePath, def);
|
|
269954
270043
|
return {
|
|
@@ -269957,6 +270046,7 @@ var require_workflow_authoring_tools = __commonJS({
|
|
|
269957
270046
|
slug: resolved.slug,
|
|
269958
270047
|
filePath: resolved.filePath,
|
|
269959
270048
|
stepsCount: def.steps.length,
|
|
270049
|
+
needsHuman: report.humanKeys,
|
|
269960
270050
|
validation: report,
|
|
269961
270051
|
next: `Run it with jetic_simulate_workflow { "workflow": "${resolved.slug}" }.`
|
|
269962
270052
|
};
|
|
@@ -269981,6 +270071,229 @@ var require_workflow_authoring_tools = __commonJS({
|
|
|
269981
270071
|
}
|
|
269982
270072
|
});
|
|
269983
270073
|
|
|
270074
|
+
// ../../packages/mcp-server/dist/tools/project-tools.js
|
|
270075
|
+
var require_project_tools = __commonJS({
|
|
270076
|
+
"../../packages/mcp-server/dist/tools/project-tools.js"(exports2) {
|
|
270077
|
+
"use strict";
|
|
270078
|
+
init_cjs_shims();
|
|
270079
|
+
var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
270080
|
+
if (k2 === void 0) k2 = k;
|
|
270081
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
270082
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
270083
|
+
desc = { enumerable: true, get: function() {
|
|
270084
|
+
return m[k];
|
|
270085
|
+
} };
|
|
270086
|
+
}
|
|
270087
|
+
Object.defineProperty(o, k2, desc);
|
|
270088
|
+
}) : (function(o, m, k, k2) {
|
|
270089
|
+
if (k2 === void 0) k2 = k;
|
|
270090
|
+
o[k2] = m[k];
|
|
270091
|
+
}));
|
|
270092
|
+
var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
|
|
270093
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
270094
|
+
}) : function(o, v) {
|
|
270095
|
+
o["default"] = v;
|
|
270096
|
+
});
|
|
270097
|
+
var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
|
|
270098
|
+
var ownKeys = function(o) {
|
|
270099
|
+
ownKeys = Object.getOwnPropertyNames || function(o2) {
|
|
270100
|
+
var ar = [];
|
|
270101
|
+
for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
|
|
270102
|
+
return ar;
|
|
270103
|
+
};
|
|
270104
|
+
return ownKeys(o);
|
|
270105
|
+
};
|
|
270106
|
+
return function(mod) {
|
|
270107
|
+
if (mod && mod.__esModule) return mod;
|
|
270108
|
+
var result = {};
|
|
270109
|
+
if (mod != null) {
|
|
270110
|
+
for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
270111
|
+
}
|
|
270112
|
+
__setModuleDefault(result, mod);
|
|
270113
|
+
return result;
|
|
270114
|
+
};
|
|
270115
|
+
})();
|
|
270116
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
270117
|
+
exports2.scanProjectSchema = exports2.initProjectSchema = void 0;
|
|
270118
|
+
exports2.handleInitProject = handleInitProject;
|
|
270119
|
+
exports2.handleScanProject = handleScanProject;
|
|
270120
|
+
var zod_1 = require("zod");
|
|
270121
|
+
var fs4 = __importStar(require("fs"));
|
|
270122
|
+
var path7 = __importStar(require("path"));
|
|
270123
|
+
var types_1 = require_types();
|
|
270124
|
+
var model_1 = require_dist2();
|
|
270125
|
+
exports2.initProjectSchema = zod_1.z.object({
|
|
270126
|
+
projectPath: zod_1.z.string().optional().describe("Path to the project root to initialize (defaults to JETIC_PROJECT_PATH / cwd)"),
|
|
270127
|
+
projectName: zod_1.z.string().optional().describe("Project name (default: directory basename)"),
|
|
270128
|
+
language: zod_1.z.string().optional().describe('Primary language (default: "typescript" when tsconfig.json exists, else omitted)'),
|
|
270129
|
+
framework: zod_1.z.string().optional().describe('Framework (default: "express" when express is a dependency, else omitted)'),
|
|
270130
|
+
environmentName: zod_1.z.string().optional().describe('First environment name (default "local")'),
|
|
270131
|
+
baseUrl: zod_1.z.string().optional().describe('First environment base URL (default "http://localhost:3000")'),
|
|
270132
|
+
overwrite: zod_1.z.boolean().optional().describe("Replace an existing .jetic/model.json (default false \u2014 never clobber silently)")
|
|
270133
|
+
});
|
|
270134
|
+
function detectStack(projectRoot) {
|
|
270135
|
+
let hasExpress = false;
|
|
270136
|
+
let hasPackageJson = false;
|
|
270137
|
+
try {
|
|
270138
|
+
const pkgPath = path7.join(projectRoot, "package.json");
|
|
270139
|
+
if (fs4.existsSync(pkgPath)) {
|
|
270140
|
+
hasPackageJson = true;
|
|
270141
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
270142
|
+
const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {}, ...pkg.peerDependencies || {} };
|
|
270143
|
+
hasExpress = "express" in deps;
|
|
270144
|
+
}
|
|
270145
|
+
} catch {
|
|
270146
|
+
}
|
|
270147
|
+
return { hasPackageJson, hasExpress, hasTsConfig: fs4.existsSync(path7.join(projectRoot, "tsconfig.json")) };
|
|
270148
|
+
}
|
|
270149
|
+
function handleInitProject(args) {
|
|
270150
|
+
const filePath = (0, types_1.resolveModelPath)(args.projectPath);
|
|
270151
|
+
const projectRoot = (0, types_1.projectRootOfModel)(filePath);
|
|
270152
|
+
const overwrite = args.overwrite === true;
|
|
270153
|
+
if (fs4.existsSync(filePath) && !overwrite) {
|
|
270154
|
+
const { model: model2 } = (0, types_1.loadModel)(args.projectPath);
|
|
270155
|
+
return {
|
|
270156
|
+
success: false,
|
|
270157
|
+
action: "already-initialized",
|
|
270158
|
+
filePath,
|
|
270159
|
+
modelExists: true,
|
|
270160
|
+
endpointsCount: model2.endpoints?.length || 0,
|
|
270161
|
+
message: `Model already exists at ${filePath} \u2014 nothing was overwritten. Continue with step 2 (jetic_scan for Express, or jetic_add_endpoint per endpoint).`,
|
|
270162
|
+
next: "If this is an Express+TypeScript project, run jetic_scan next. Otherwise read the code and add endpoints with jetic_add_endpoint, then jetic_verify_model."
|
|
270163
|
+
};
|
|
270164
|
+
}
|
|
270165
|
+
const stack = detectStack(projectRoot);
|
|
270166
|
+
const model = {
|
|
270167
|
+
version: model_1.CURRENT_MODEL_VERSION,
|
|
270168
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
270169
|
+
project: {
|
|
270170
|
+
name: args.projectName || path7.basename(projectRoot) || "jetic-project",
|
|
270171
|
+
...args.language || (stack.hasTsConfig ? "typescript" : void 0) ? { language: args.language || "typescript" } : {},
|
|
270172
|
+
...args.framework || (stack.hasExpress ? "express" : void 0) ? { framework: args.framework || "express" } : {}
|
|
270173
|
+
},
|
|
270174
|
+
environments: [
|
|
270175
|
+
{ name: args.environmentName || "local", baseUrl: args.baseUrl || "http://localhost:3000" }
|
|
270176
|
+
],
|
|
270177
|
+
securitySchemes: {},
|
|
270178
|
+
resources: [],
|
|
270179
|
+
endpoints: [],
|
|
270180
|
+
dependencies: [],
|
|
270181
|
+
workflows: [],
|
|
270182
|
+
stateMachines: []
|
|
270183
|
+
};
|
|
270184
|
+
(0, types_1.saveModel)(filePath, model);
|
|
270185
|
+
fs4.mkdirSync(path7.join(path7.dirname(filePath), "workflows"), { recursive: true });
|
|
270186
|
+
const configPath = path7.join(path7.dirname(filePath), "config.json");
|
|
270187
|
+
if (!fs4.existsSync(configPath)) {
|
|
270188
|
+
fs4.writeFileSync(configPath, JSON.stringify({}, null, 2), "utf-8");
|
|
270189
|
+
}
|
|
270190
|
+
const scanRecommended = stack.hasExpress && stack.hasTsConfig;
|
|
270191
|
+
return {
|
|
270192
|
+
success: true,
|
|
270193
|
+
action: overwrite ? "overwritten" : "created",
|
|
270194
|
+
filePath,
|
|
270195
|
+
project: model.project,
|
|
270196
|
+
detected: stack,
|
|
270197
|
+
scanRecommended,
|
|
270198
|
+
next: scanRecommended ? "Step 2: this looks like an Express+TypeScript project \u2014 run jetic_scan next to auto-fill model.json (no AI key needed), then refine with jetic_add_endpoint and jetic_verify_model." : "Step 2: no auto-scan applies here \u2014 read the backend code and add each endpoint with jetic_add_endpoint (middleware, security, parameters, responses), then jetic_verify_model until clean. Step 3 (only on request): author workflows with jetic_validate_workflow \u2192 jetic_create_workflow \u2192 jetic_simulate_workflow."
|
|
270199
|
+
};
|
|
270200
|
+
}
|
|
270201
|
+
exports2.scanProjectSchema = zod_1.z.object({
|
|
270202
|
+
projectPath: zod_1.z.string().optional().describe("Path to the Express project root (must contain tsconfig.json). Defaults to JETIC_PROJECT_PATH / cwd."),
|
|
270203
|
+
mode: zod_1.z.enum(["merge", "overwrite"]).optional().describe("merge (default): upsert scanned routes, keep hand-added endpoints. overwrite: replace the whole endpoints array.")
|
|
270204
|
+
});
|
|
270205
|
+
async function quietly(fn) {
|
|
270206
|
+
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
270207
|
+
const origLog = console.log;
|
|
270208
|
+
const origInfo = console.info;
|
|
270209
|
+
const origDebug = console.debug?.bind(console);
|
|
270210
|
+
const noop = () => {
|
|
270211
|
+
};
|
|
270212
|
+
process.stdout.write = noop;
|
|
270213
|
+
console.log = noop;
|
|
270214
|
+
console.info = noop;
|
|
270215
|
+
if (console.debug)
|
|
270216
|
+
console.debug = noop;
|
|
270217
|
+
try {
|
|
270218
|
+
return await fn();
|
|
270219
|
+
} finally {
|
|
270220
|
+
process.stdout.write = origStdoutWrite;
|
|
270221
|
+
console.log = origLog;
|
|
270222
|
+
console.info = origInfo;
|
|
270223
|
+
if (origDebug)
|
|
270224
|
+
console.debug = origDebug;
|
|
270225
|
+
}
|
|
270226
|
+
}
|
|
270227
|
+
async function handleScanProject(args) {
|
|
270228
|
+
const filePath = (0, types_1.resolveModelPath)(args.projectPath);
|
|
270229
|
+
const projectRoot = (0, types_1.projectRootOfModel)(filePath);
|
|
270230
|
+
const mode = args.mode || "merge";
|
|
270231
|
+
if (!fs4.existsSync(path7.join(projectRoot, "tsconfig.json"))) {
|
|
270232
|
+
throw new Error(`Cannot scan ${projectRoot}: no tsconfig.json found. jetic_scan only supports Express+TypeScript projects. For any other stack, read the code and model each endpoint with jetic_add_endpoint instead.`);
|
|
270233
|
+
}
|
|
270234
|
+
const { ExpressScanner: ExpressScanner2 } = await Promise.resolve().then(() => __importStar(require_dist3()));
|
|
270235
|
+
const config = {
|
|
270236
|
+
projectRoot,
|
|
270237
|
+
jeticDir: path7.dirname(filePath)
|
|
270238
|
+
// No `ai` key on purpose: static discovery only, no provider, no keys.
|
|
270239
|
+
};
|
|
270240
|
+
let scanned;
|
|
270241
|
+
try {
|
|
270242
|
+
scanned = await quietly(() => new ExpressScanner2(config).scan());
|
|
270243
|
+
} catch (err) {
|
|
270244
|
+
throw new Error(`Scan failed for ${projectRoot}: ${err?.message || err}. If this is not an Express+TypeScript codebase, model endpoints manually with jetic_add_endpoint.`);
|
|
270245
|
+
}
|
|
270246
|
+
const { model: existing, exists } = (0, types_1.loadModel)(args.projectPath);
|
|
270247
|
+
let added = 0;
|
|
270248
|
+
let updated = 0;
|
|
270249
|
+
let preserved = 0;
|
|
270250
|
+
if (!exists || mode === "overwrite") {
|
|
270251
|
+
if (mode === "overwrite" && exists)
|
|
270252
|
+
preserved = 0;
|
|
270253
|
+
existing.endpoints = scanned.endpoints;
|
|
270254
|
+
added = scanned.endpoints.length;
|
|
270255
|
+
if (!existing.environments || existing.environments.length === 0) {
|
|
270256
|
+
existing.environments = scanned.environments;
|
|
270257
|
+
}
|
|
270258
|
+
} else {
|
|
270259
|
+
const index = /* @__PURE__ */ new Map();
|
|
270260
|
+
existing.endpoints.forEach((e, i) => index.set(`${e.method.toUpperCase()} ${e.path}`, i));
|
|
270261
|
+
for (const s of scanned.endpoints) {
|
|
270262
|
+
const key = `${s.method.toUpperCase()} ${s.path}`;
|
|
270263
|
+
const at = index.get(key);
|
|
270264
|
+
if (at === void 0) {
|
|
270265
|
+
existing.endpoints.push(s);
|
|
270266
|
+
added++;
|
|
270267
|
+
} else {
|
|
270268
|
+
existing.endpoints[at] = s;
|
|
270269
|
+
updated++;
|
|
270270
|
+
}
|
|
270271
|
+
}
|
|
270272
|
+
preserved = existing.endpoints.length - added - updated;
|
|
270273
|
+
if (!existing.environments || existing.environments.length === 0) {
|
|
270274
|
+
existing.environments = scanned.environments;
|
|
270275
|
+
}
|
|
270276
|
+
}
|
|
270277
|
+
if (!existing.project?.framework && scanned.project?.framework) {
|
|
270278
|
+
existing.project = { ...scanned.project, ...existing.project };
|
|
270279
|
+
}
|
|
270280
|
+
(0, types_1.saveModel)(filePath, existing);
|
|
270281
|
+
const total = existing.endpoints.length;
|
|
270282
|
+
return {
|
|
270283
|
+
success: true,
|
|
270284
|
+
action: "scanned",
|
|
270285
|
+
filePath,
|
|
270286
|
+
mode,
|
|
270287
|
+
stats: { scanned: scanned.endpoints.length, added, updated, preserved, total },
|
|
270288
|
+
...scanned.endpoints.length === 0 ? {
|
|
270289
|
+
warning: "Static scan found 0 endpoints. Check that Express routes live under this project root (custom entry points or routers may need manual modeling with jetic_add_endpoint)."
|
|
270290
|
+
} : {},
|
|
270291
|
+
next: total === 0 ? "No endpoints yet \u2014 model them manually with jetic_add_endpoint, then jetic_verify_model." : `Next: refine with jetic_add_endpoint / jetic_update_endpoint (AI enrichment is skipped in scans \u2014 add constraints, examples, ownership by hand), then jetic_verify_model until clean. Step 3 (only on request): workflows via jetic_validate_workflow \u2192 jetic_create_workflow \u2192 jetic_simulate_workflow.`
|
|
270292
|
+
};
|
|
270293
|
+
}
|
|
270294
|
+
}
|
|
270295
|
+
});
|
|
270296
|
+
|
|
269984
270297
|
// ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/zod-compat.js
|
|
269985
270298
|
var require_zod_compat = __commonJS({
|
|
269986
270299
|
"../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/zod-compat.js"(exports2) {
|
|
@@ -283991,6 +284304,112 @@ var require_serial_transport = __commonJS({
|
|
|
283991
284304
|
}
|
|
283992
284305
|
});
|
|
283993
284306
|
|
|
284307
|
+
// ../../packages/mcp-server/dist/tools/session-tools.js
|
|
284308
|
+
var require_session_tools = __commonJS({
|
|
284309
|
+
"../../packages/mcp-server/dist/tools/session-tools.js"(exports2) {
|
|
284310
|
+
"use strict";
|
|
284311
|
+
init_cjs_shims();
|
|
284312
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
284313
|
+
exports2.getSessionPhaseSchema = void 0;
|
|
284314
|
+
exports2.handleGetSessionPhase = handleGetSessionPhase;
|
|
284315
|
+
var zod_1 = require("zod");
|
|
284316
|
+
var types_1 = require_types();
|
|
284317
|
+
var validation_tools_1 = require_validation_tools();
|
|
284318
|
+
exports2.getSessionPhaseSchema = zod_1.z.object({
|
|
284319
|
+
projectPath: zod_1.z.string().optional().describe("Path to the project root or .jetic folder (defaults to JETIC_PROJECT_PATH / cwd)")
|
|
284320
|
+
});
|
|
284321
|
+
function checkAllFieldsFilled(model) {
|
|
284322
|
+
let incompleteCount = 0;
|
|
284323
|
+
for (const ep of model.endpoints) {
|
|
284324
|
+
let incomplete = false;
|
|
284325
|
+
if (!ep.description || ep.description.trim() === "")
|
|
284326
|
+
incomplete = true;
|
|
284327
|
+
if (!ep.tags || ep.tags.length === 0)
|
|
284328
|
+
incomplete = true;
|
|
284329
|
+
if (ep.parameters) {
|
|
284330
|
+
for (const p of ep.parameters) {
|
|
284331
|
+
if (!p.description || p.description.trim() === "") {
|
|
284332
|
+
incomplete = true;
|
|
284333
|
+
break;
|
|
284334
|
+
}
|
|
284335
|
+
}
|
|
284336
|
+
}
|
|
284337
|
+
if (ep.responses) {
|
|
284338
|
+
for (const resp of Object.values(ep.responses)) {
|
|
284339
|
+
const r = resp;
|
|
284340
|
+
if (!r.description || String(r.description).trim() === "") {
|
|
284341
|
+
incomplete = true;
|
|
284342
|
+
break;
|
|
284343
|
+
}
|
|
284344
|
+
}
|
|
284345
|
+
}
|
|
284346
|
+
if (incomplete)
|
|
284347
|
+
incompleteCount++;
|
|
284348
|
+
}
|
|
284349
|
+
return { allFilled: incompleteCount === 0, incompleteCount };
|
|
284350
|
+
}
|
|
284351
|
+
function handleGetSessionPhase(args) {
|
|
284352
|
+
const { model, exists } = (0, types_1.loadModel)(args.projectPath);
|
|
284353
|
+
if (!exists) {
|
|
284354
|
+
return {
|
|
284355
|
+
phase: 1,
|
|
284356
|
+
phaseName: "init",
|
|
284357
|
+
modelExists: false,
|
|
284358
|
+
endpointsCount: 0,
|
|
284359
|
+
modelIsValid: false,
|
|
284360
|
+
allFieldsFilled: false,
|
|
284361
|
+
nextAction: "PHASE 1 \u2014 INITIALIZE: No .jetic/model.json found. Call jetic_init now to scaffold the project. Do not proceed until modelExists is true."
|
|
284362
|
+
};
|
|
284363
|
+
}
|
|
284364
|
+
const endpointsCount = model.endpoints?.length ?? 0;
|
|
284365
|
+
if (endpointsCount === 0) {
|
|
284366
|
+
return {
|
|
284367
|
+
phase: 2,
|
|
284368
|
+
phaseName: "scan",
|
|
284369
|
+
modelExists: true,
|
|
284370
|
+
endpointsCount: 0,
|
|
284371
|
+
modelIsValid: false,
|
|
284372
|
+
allFieldsFilled: false,
|
|
284373
|
+
nextAction: "PHASE 2 \u2014 SCAN: Model exists but has no endpoints. If the project has tsconfig.json (Express+TypeScript), call jetic_scan. Otherwise read the source code and call jetic_add_endpoint for every API route. Do not proceed to Phase 3 until endpointsCount > 0."
|
|
284374
|
+
};
|
|
284375
|
+
}
|
|
284376
|
+
let modelIsValid = false;
|
|
284377
|
+
try {
|
|
284378
|
+
const verification = (0, validation_tools_1.handleVerifyModel)({ projectPath: args.projectPath });
|
|
284379
|
+
modelIsValid = verification.isValid;
|
|
284380
|
+
} catch {
|
|
284381
|
+
modelIsValid = false;
|
|
284382
|
+
}
|
|
284383
|
+
const { allFilled, incompleteCount } = checkAllFieldsFilled(model);
|
|
284384
|
+
if (!modelIsValid || !allFilled) {
|
|
284385
|
+
const reasons = [];
|
|
284386
|
+
if (!modelIsValid)
|
|
284387
|
+
reasons.push("model has validation errors (call jetic_verify_model to see them)");
|
|
284388
|
+
if (!allFilled)
|
|
284389
|
+
reasons.push(`${incompleteCount} endpoint(s) have incomplete fields (missing description, tags, parameter descriptions, or response descriptions \u2014 call jetic_verify_model to see the full list)`);
|
|
284390
|
+
return {
|
|
284391
|
+
phase: 3,
|
|
284392
|
+
phaseName: "verify",
|
|
284393
|
+
modelExists: true,
|
|
284394
|
+
endpointsCount,
|
|
284395
|
+
modelIsValid,
|
|
284396
|
+
allFieldsFilled: allFilled,
|
|
284397
|
+
nextAction: `PHASE 3 \u2014 VERIFY: ${reasons.join("; ")}. Fix all issues with jetic_update_endpoint / jetic_add_endpoint, then call jetic_verify_model until both modelIsValid and allFieldsFilled are true. Do not touch any workflow tool until Phase 4 is reached.`
|
|
284398
|
+
};
|
|
284399
|
+
}
|
|
284400
|
+
return {
|
|
284401
|
+
phase: 4,
|
|
284402
|
+
phaseName: "simulate",
|
|
284403
|
+
modelExists: true,
|
|
284404
|
+
endpointsCount,
|
|
284405
|
+
modelIsValid: true,
|
|
284406
|
+
allFieldsFilled: true,
|
|
284407
|
+
nextAction: 'PHASE 4 \u2014 SIMULATE: The model is valid and complete. ASK THE USER: "What simulation or workflow do you want to build?" Wait for their answer before calling any workflow tool. Do not invent a workflow goal \u2014 the user must specify it.'
|
|
284408
|
+
};
|
|
284409
|
+
}
|
|
284410
|
+
}
|
|
284411
|
+
});
|
|
284412
|
+
|
|
283994
284413
|
// ../../packages/mcp-server/dist/server.js
|
|
283995
284414
|
var require_server3 = __commonJS({
|
|
283996
284415
|
"../../packages/mcp-server/dist/server.js"(exports2) {
|
|
@@ -284050,6 +284469,8 @@ var require_server3 = __commonJS({
|
|
|
284050
284469
|
var environment_tools_1 = require_environment_tools();
|
|
284051
284470
|
var workflow_tools_1 = require_workflow_tools();
|
|
284052
284471
|
var workflow_authoring_tools_1 = require_workflow_authoring_tools();
|
|
284472
|
+
var project_tools_1 = require_project_tools();
|
|
284473
|
+
var session_tools_1 = require_session_tools();
|
|
284053
284474
|
exports2.JETIC_MCP_SERVER_NAME = "jetic-mcp-server";
|
|
284054
284475
|
function getJeticMcpVersion() {
|
|
284055
284476
|
try {
|
|
@@ -284062,15 +284483,37 @@ var require_server3 = __commonJS({
|
|
|
284062
284483
|
return "0.1.0";
|
|
284063
284484
|
}
|
|
284064
284485
|
exports2.JETIC_MCP_INSTRUCTIONS = [
|
|
284065
|
-
"Jetic MCP manages API behavioral models
|
|
284066
|
-
"
|
|
284067
|
-
"
|
|
284068
|
-
"
|
|
284069
|
-
"
|
|
284070
|
-
"
|
|
284071
|
-
"
|
|
284072
|
-
"
|
|
284073
|
-
"
|
|
284486
|
+
"Jetic MCP manages API behavioral models in `.jetic/`. You MUST follow this strict 4-phase order. Never skip, reorder, or bypass any phase.",
|
|
284487
|
+
"",
|
|
284488
|
+
"\u2501\u2501 PHASE 1 \u2014 INITIALIZE \u2501\u2501",
|
|
284489
|
+
"Call `jetic_get_session_phase` at the very start of every session. If phase=1 (modelExists:false), call `jetic_init` to scaffold .jetic/model.json.",
|
|
284490
|
+
"Gate: DO NOT advance to Phase 2 until modelExists is true.",
|
|
284491
|
+
"",
|
|
284492
|
+
"\u2501\u2501 PHASE 2 \u2014 SCAN \u2501\u2501",
|
|
284493
|
+
"Populate the model with endpoints:",
|
|
284494
|
+
" \u2022 Express+TypeScript (tsconfig.json present): call `jetic_scan` (static, keyless, no AI provider needed).",
|
|
284495
|
+
" \u2022 Any other stack or scan gaps: read the source code and call `jetic_add_endpoint` for every route (include middleware chains, security, constraints, pagination, produces/consumes).",
|
|
284496
|
+
"Gate: DO NOT advance to Phase 3 until endpointsCount > 0.",
|
|
284497
|
+
"",
|
|
284498
|
+
"\u2501\u2501 PHASE 3 \u2014 VERIFY \u2501\u2501",
|
|
284499
|
+
"Call `jetic_verify_model`. Fix every error AND every MISSING_FIELD_* warning:",
|
|
284500
|
+
" \u2022 Fill `description` on every endpoint.",
|
|
284501
|
+
" \u2022 Add at least one `tag` per endpoint.",
|
|
284502
|
+
" \u2022 Add `description` to every parameter.",
|
|
284503
|
+
" \u2022 Add `description` to every response status code.",
|
|
284504
|
+
"Use `jetic_update_endpoint` to fill gaps. Call `jetic_verify_model` again after each batch of updates.",
|
|
284505
|
+
"Gate: DO NOT advance to Phase 4 until jetic_get_session_phase returns phase=4 (modelIsValid AND allFieldsFilled both true).",
|
|
284506
|
+
"",
|
|
284507
|
+
"\u2501\u2501 PHASE 4 \u2014 SIMULATE \u2501\u2501",
|
|
284508
|
+
'STOP. Ask the user: "What simulation or workflow do you want to build?"',
|
|
284509
|
+
"Wait for their explicit answer. Then and only then: author the workflow with `jetic_validate_workflow` (dry run) \u2192 `jetic_create_workflow` \u2192 `jetic_simulate_workflow`.",
|
|
284510
|
+
"Chain steps using capture/captureInput (producer) + inject or {{workflow:key}} (consumer). Use {{human:key}} ONLY for values unknowable beforehand (OTP codes, real secrets).",
|
|
284511
|
+
"Never start Phase 4 without an explicit user-specified goal.",
|
|
284512
|
+
"",
|
|
284513
|
+
"\u2501\u2501 GENERAL RULES \u2501\u2501",
|
|
284514
|
+
"No AI-provider setup is needed: never ask for OpenRouter/OpenAI keys and never use `jetic config ai`.",
|
|
284515
|
+
"Mutating tools write to disk \u2014 confirm intent with the user before destructive operations (delete/overwrite).",
|
|
284516
|
+
"Project resolution: every tool accepts an optional `projectPath`. If omitted, falls back to JETIC_PROJECT_PATH env var, then cwd."
|
|
284074
284517
|
].join("\n");
|
|
284075
284518
|
var READ_ONLY = {
|
|
284076
284519
|
readOnlyHint: true,
|
|
@@ -284118,7 +284561,10 @@ var require_server3 = __commonJS({
|
|
|
284118
284561
|
}
|
|
284119
284562
|
});
|
|
284120
284563
|
};
|
|
284121
|
-
register("
|
|
284564
|
+
register("jetic_get_session_phase", "Get session phase", "ALWAYS CALL THIS FIRST. Returns the current workflow phase (1=init, 2=scan, 3=verify, 4=simulate), gate conditions (modelExists, endpointsCount, modelIsValid, allFieldsFilled), and the exact nextAction the AI must take. Never skip phase gates.", session_tools_1.getSessionPhaseSchema.shape, READ_ONLY, session_tools_1.handleGetSessionPhase);
|
|
284565
|
+
register("jetic_init", "Initialize Jetic project", "PHASE 1: Scaffolds .jetic/model.json (+ workflows/, config.json) for repos that have none. Refuses to clobber an existing model (pass overwrite:true to replace). Reports whether auto-scan applies (Express+TypeScript) and what to do next.", project_tools_1.initProjectSchema.shape, WRITES_MODEL, project_tools_1.handleInitProject);
|
|
284566
|
+
register("jetic_scan", "Scan Express project", "PHASE 2 (Express+TypeScript only \u2014 requires tsconfig.json): Static AST scan that fills model.json with routes, middleware, and auth heuristics. Keyless \u2014 no AI provider involved. Default merge upserts scanned routes and keeps hand-added endpoints; overwrite replaces all endpoints. Fails with guidance for non-Express stacks (model those manually with jetic_add_endpoint).", project_tools_1.scanProjectSchema.shape, WRITES_MODEL, project_tools_1.handleScanProject);
|
|
284567
|
+
register("jetic_read_model", "Read Jetic model", "Reads .jetic/model.json from the workspace and returns the full behavioral model or a metadata summary.", model_tools_1.readModelSchema.shape, READ_ONLY, model_tools_1.handleReadModel);
|
|
284122
284568
|
register("jetic_list_endpoints", "List endpoints", "Lists API endpoints discovered in .jetic/model.json with optional filters by method, tag, resource, or path substring.", model_tools_1.listEndpointsSchema.shape, READ_ONLY, model_tools_1.handleListEndpoints);
|
|
284123
284569
|
register("jetic_get_endpoint", "Get endpoint details", "Returns full structural details (parameters, request body, schemas, security, responses) of one endpoint, looked up by ID or by method + path.", model_tools_1.getEndpointSchema.shape, READ_ONLY, model_tools_1.handleGetEndpoint);
|
|
284124
284570
|
register("jetic_add_endpoint", "Add endpoint", "Adds a new endpoint definition to .jetic/model.json (or updates the existing method+path route when overwriteIfExists is true, the default). Adheres to the Jetic Behavioral Model schema.", endpoint_tools_1.addEndpointSchema.shape, WRITES_MODEL, endpoint_tools_1.handleAddEndpoint);
|
|
@@ -284130,7 +284576,7 @@ var require_server3 = __commonJS({
|
|
|
284130
284576
|
register("jetic_list_workflows", "List workflows", "Lists multi-step integration test workflows from .jetic/workflows/*.json, .jetic/workflow.json, or embedded model.json workflows.", workflow_tools_1.listWorkflowsSchema.shape, READ_ONLY, workflow_tools_1.handleListWorkflows);
|
|
284131
284577
|
register("jetic_simulate_workflow", "Simulate workflow", "Executes a full end-to-end multi-step workflow simulation with dynamic memory capture, header injection, and schema validation against the live server.", workflow_tools_1.simulateWorkflowSchema.shape, READ_LIVE, workflow_tools_1.handleSimulateWorkflow);
|
|
284132
284578
|
register("jetic_simulate", "Simulate workflow (alias)", "Alias of jetic_simulate_workflow: runs a multi-step user-journey workflow against the live server. Prefer jetic_simulate_workflow in new prompts.", workflow_tools_1.simulateWorkflowSchema.shape, READ_LIVE, workflow_tools_1.handleSimulateWorkflow);
|
|
284133
|
-
register("jetic_validate_workflow", "Validate workflow", "Deep-validates a workflow WITHOUT saving or running it: step structure, endpoint cross-checks against model.json, and memory data-flow analysis (every {{scope:key}} must be captured by an earlier step). Accepts a saved workflow reference or an inline definition for pre-save dry runs.", workflow_authoring_tools_1.validateWorkflowSchema.shape, READ_ONLY, workflow_authoring_tools_1.handleValidateWorkflow);
|
|
284579
|
+
register("jetic_validate_workflow", "Validate workflow", "Deep-validates a workflow WITHOUT saving or running it: step structure, endpoint cross-checks against model.json, and memory data-flow analysis (every {{scope:key}} except {{human:key}} must be captured by an earlier step). Accepts a saved workflow reference or an inline definition for pre-save dry runs.", workflow_authoring_tools_1.validateWorkflowSchema.shape, READ_ONLY, workflow_authoring_tools_1.handleValidateWorkflow);
|
|
284134
284580
|
register("jetic_create_workflow", "Create workflow", "Creates a .jetic/workflows/<slug>.json workflow from ordered steps with capture/inject memory chaining. Refuses to save when validation errors exist (error messages say exactly how to fix); use validateOnly for a dry run. Can also migrate an embedded model.json workflow via fromModelWorkflow.", workflow_authoring_tools_1.createWorkflowSchema.shape, WRITES_MODEL, workflow_authoring_tools_1.handleCreateWorkflow);
|
|
284135
284581
|
register("jetic_update_workflow", "Update workflow", "Updates a saved file workflow: rename, description/environment, full steps replacement, or appendSteps. Validates before writing and refuses to save on errors.", workflow_authoring_tools_1.updateWorkflowSchema.shape, WRITES_MODEL, workflow_authoring_tools_1.handleUpdateWorkflow);
|
|
284136
284582
|
register("jetic_delete_workflow", "Delete workflow", "Deletes a saved .jetic/workflows/*.json workflow file by slug, name, or path. Destructive: confirm with the user before calling. Cannot delete model.json-embedded workflows.", workflow_authoring_tools_1.deleteWorkflowSchema.shape, DESTRUCTIVE, workflow_authoring_tools_1.handleDeleteWorkflow);
|
|
@@ -284200,6 +284646,7 @@ var require_dist7 = __commonJS({
|
|
|
284200
284646
|
__exportStar(require_environment_tools(), exports2);
|
|
284201
284647
|
__exportStar(require_workflow_tools(), exports2);
|
|
284202
284648
|
__exportStar(require_workflow_authoring_tools(), exports2);
|
|
284649
|
+
__exportStar(require_project_tools(), exports2);
|
|
284203
284650
|
__exportStar(require_server3(), exports2);
|
|
284204
284651
|
__exportStar(require_serial_transport(), exports2);
|
|
284205
284652
|
var fs4 = __importStar(require("fs"));
|
|
@@ -284663,9 +285110,15 @@ Use EXACTLY these placeholders in body/inject values:
|
|
|
284663
285110
|
{{faker.commerce.productName}} product name
|
|
284664
285111
|
{{faker.phone.number}} phone number
|
|
284665
285112
|
{{workflow:KEY}} value captured from a previous step
|
|
285113
|
+
{{human:KEY}} ask the human at runtime (interactive prompt, waits for input).
|
|
285114
|
+
Use ONLY for values unknowable beforehand (OTP/2FA codes,
|
|
285115
|
+
CAPTCHAs, real personal secrets). First entry is saved to
|
|
285116
|
+
human:KEY memory; JETIC_HUMAN_KEY env var skips the prompt.
|
|
284666
285117
|
|
|
284667
|
-
\u2501\u2501\u2501 FIELD RULES \u2501\u2501\u2501
|
|
285118
|
+
\u2501\u2501\u2501 FIELD RULES \u2501\u2501\u2501
|
|
284668
285119
|
"body" \u2014 request body. Use {{faker.*}} for generated fields, {{workflow:KEY}} for previously captured values.
|
|
285120
|
+
Use {{human:KEY}} SPARINGLY and only when no other source exists (OTP codes, real secrets) \u2014
|
|
285121
|
+
it pauses execution waiting for a person to type a value.
|
|
284669
285122
|
|
|
284670
285123
|
"captureInput" \u2014 save RESOLVED body field values to memory BEFORE the HTTP call.
|
|
284671
285124
|
Only for faker-generated fields you need to re-use in later steps.
|
|
@@ -284716,7 +285169,7 @@ all [requiresAuth] endpoints inject Authorization, captures wired correctly betw
|
|
|
284716
285169
|
method: import_zod.z.string().describe("HTTP method in uppercase: GET, POST, PUT, PATCH, DELETE"),
|
|
284717
285170
|
path: import_zod.z.string().describe("Exact endpoint path, e.g. /api/auth/login"),
|
|
284718
285171
|
description: import_zod.z.string().optional().describe("One sentence describing what this step does"),
|
|
284719
|
-
body: import_zod.z.record(import_zod.z.string(), import_zod.z.any()).optional().describe("Request body. Use {{faker.X}} for generated values, {{workflow:KEY}} for captured values"),
|
|
285172
|
+
body: import_zod.z.record(import_zod.z.string(), import_zod.z.any()).optional().describe("Request body. Use {{faker.X}} for generated values, {{workflow:KEY}} for captured values, {{human:KEY}} only for OTP/secret values a person must type at runtime"),
|
|
284720
285173
|
captureInput: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional().describe('Save resolved request body fields to memory BEFORE the HTTP call. Format: { "workflow:KEY": "bodyFieldName" }'),
|
|
284721
285174
|
capture: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional().describe('Save response body fields to memory AFTER success. Format: { "workflow:KEY": "dot.path" }'),
|
|
284722
285175
|
inject: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional().describe('Inject memory values into headers/body. Use "header:Authorization" for Bearer auth'),
|
|
@@ -284748,12 +285201,12 @@ function deepGet(obj, dotPath) {
|
|
|
284748
285201
|
}
|
|
284749
285202
|
return current;
|
|
284750
285203
|
}
|
|
284751
|
-
async function resolveConditionRef(ref) {
|
|
284752
|
-
return ref.replace(/\{\{([^}]+)\}\}/g, () => "").trim() === ref.trim() ? ref : await resolveTemplateString(ref);
|
|
285204
|
+
async function resolveConditionRef(ref, context = "condition") {
|
|
285205
|
+
return ref.replace(/\{\{([^}]+)\}\}/g, () => "").trim() === ref.trim() ? ref : await resolveTemplateString(ref, context);
|
|
284753
285206
|
}
|
|
284754
|
-
async function evalConditionRule(rule) {
|
|
284755
|
-
const left = await resolveConditionRef(rule.left);
|
|
284756
|
-
const right = rule.right !== void 0 ? await resolveConditionRef(rule.right) : void 0;
|
|
285207
|
+
async function evalConditionRule(rule, context = "condition") {
|
|
285208
|
+
const left = await resolveConditionRef(rule.left, context);
|
|
285209
|
+
const right = rule.right !== void 0 ? await resolveConditionRef(rule.right, context) : void 0;
|
|
284757
285210
|
const op = rule.operator;
|
|
284758
285211
|
switch (op) {
|
|
284759
285212
|
case "equals":
|
|
@@ -284788,14 +285241,14 @@ async function evalConditionRule(rule) {
|
|
|
284788
285241
|
return false;
|
|
284789
285242
|
}
|
|
284790
285243
|
}
|
|
284791
|
-
async function evaluateStepCondition(condition) {
|
|
285244
|
+
async function evaluateStepCondition(condition, context = "condition") {
|
|
284792
285245
|
const { rules } = condition;
|
|
284793
285246
|
const ruleList = rules.all ?? rules.any ?? [];
|
|
284794
285247
|
const isAnd = !!rules.all;
|
|
284795
285248
|
if (ruleList.length === 0) return { passed: true, reason: "No rules defined" };
|
|
284796
285249
|
const results = [];
|
|
284797
285250
|
for (const rule of ruleList) {
|
|
284798
|
-
results.push({ rule, ok: await evalConditionRule(rule) });
|
|
285251
|
+
results.push({ rule, ok: await evalConditionRule(rule, context) });
|
|
284799
285252
|
}
|
|
284800
285253
|
const passed = isAnd ? results.every((r) => r.ok) : results.some((r) => r.ok);
|
|
284801
285254
|
const failing = results.filter((r) => !r.ok);
|
|
@@ -284804,12 +285257,89 @@ async function evaluateStepCondition(condition) {
|
|
|
284804
285257
|
).join(", ")}`;
|
|
284805
285258
|
return { passed, reason };
|
|
284806
285259
|
}
|
|
284807
|
-
|
|
285260
|
+
function envKeyForHuman(key) {
|
|
285261
|
+
return `JETIC_HUMAN_${key.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
285262
|
+
}
|
|
285263
|
+
var HumanInputError = class extends Error {
|
|
285264
|
+
constructor(message) {
|
|
285265
|
+
super(message);
|
|
285266
|
+
this.name = "HumanInputError";
|
|
285267
|
+
}
|
|
285268
|
+
};
|
|
285269
|
+
function isSecretHumanKey(key) {
|
|
285270
|
+
return /pass|secret|token|pwd|private|credential/i.test(key);
|
|
285271
|
+
}
|
|
285272
|
+
async function promptLine(question) {
|
|
285273
|
+
const readline3 = await new Function("modulePath", "return import(modulePath)")("node:readline/promises");
|
|
285274
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
285275
|
+
try {
|
|
285276
|
+
return await rl.question(question);
|
|
285277
|
+
} finally {
|
|
285278
|
+
rl.close();
|
|
285279
|
+
}
|
|
285280
|
+
}
|
|
285281
|
+
async function promptSecret(question) {
|
|
285282
|
+
const readline3 = await new Function("modulePath", "return import(modulePath)")("node:readline");
|
|
285283
|
+
return new Promise((resolve2) => {
|
|
285284
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
285285
|
+
process.stdout.write(question);
|
|
285286
|
+
const w = rl;
|
|
285287
|
+
const orig = w._writeToOutput.bind(rl);
|
|
285288
|
+
w._writeToOutput = (s) => {
|
|
285289
|
+
if (s === "\n" || s === "\r\n" || s === "\r") orig(s);
|
|
285290
|
+
else w.output.write("*".repeat(s.length));
|
|
285291
|
+
};
|
|
285292
|
+
rl.question("", (answer) => {
|
|
285293
|
+
w._writeToOutput = orig;
|
|
285294
|
+
process.stdout.write("\n");
|
|
285295
|
+
rl.close();
|
|
285296
|
+
resolve2(answer);
|
|
285297
|
+
});
|
|
285298
|
+
});
|
|
285299
|
+
}
|
|
285300
|
+
async function askHuman(key, context) {
|
|
285301
|
+
const envName = envKeyForHuman(key);
|
|
285302
|
+
const fromEnv = process.env[envName];
|
|
285303
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
285304
|
+
const mem = new import_memory2.JeticMemory({ scope: "human" });
|
|
285305
|
+
const cached = await mem.get(key);
|
|
285306
|
+
if (cached !== null && cached !== void 0 && String(cached) !== "") {
|
|
285307
|
+
console.log(` ${c.dim}\u21B3 using saved human value for "${key}" (jetic memory set human:${key} \u2026 to change)${c.reset}`);
|
|
285308
|
+
return String(cached);
|
|
285309
|
+
}
|
|
285310
|
+
if (!process.stdin.isTTY) {
|
|
285311
|
+
throw new HumanInputError(
|
|
285312
|
+
`Workflow needs human input for "{{human:${key}}}"${context ? ` (${context})` : ""} but the terminal is not interactive. Pre-seed it with: jetic memory set human:${key} <value> \u2014 or export ${envName}=<value>`
|
|
285313
|
+
);
|
|
285314
|
+
}
|
|
285315
|
+
console.log(`
|
|
285316
|
+
${c.yellow}${c.bold}\u25C9 Human input required${c.reset}${context ? ` ${c.dim}${context}${c.reset}` : ""}`);
|
|
285317
|
+
const question = ` ${c.cyan}Enter value for "${key}":${c.reset} `;
|
|
285318
|
+
const value = isSecretHumanKey(key) ? await promptSecret(question) : await promptLine(question);
|
|
285319
|
+
if (value !== "") {
|
|
285320
|
+
await mem.set(key, value);
|
|
285321
|
+
console.log(` ${c.dim}\u21B3 saved to human:${key} memory (reused automatically next time)${c.reset}
|
|
285322
|
+
`);
|
|
285323
|
+
} else {
|
|
285324
|
+
console.log(` ${c.dim}\u21B3 empty value accepted for "${key}" (not saved)${c.reset}
|
|
285325
|
+
`);
|
|
285326
|
+
}
|
|
285327
|
+
return value;
|
|
285328
|
+
}
|
|
285329
|
+
async function resolveTemplateString(value, context) {
|
|
284808
285330
|
const templateRe = /\{\{([^}]+)\}\}/g;
|
|
284809
285331
|
let match;
|
|
284810
285332
|
const replacements = [];
|
|
284811
285333
|
while ((match = templateRe.exec(value)) !== null) {
|
|
284812
285334
|
const expr = match[1].trim();
|
|
285335
|
+
if (!expr.startsWith("faker.")) {
|
|
285336
|
+
const colonIdx = expr.indexOf(":");
|
|
285337
|
+
if (colonIdx > 0 && expr.slice(0, colonIdx) === "human") {
|
|
285338
|
+
const key = expr.slice(colonIdx + 1);
|
|
285339
|
+
replacements.push({ placeholder: match[0], resolved: await askHuman(key, context) });
|
|
285340
|
+
continue;
|
|
285341
|
+
}
|
|
285342
|
+
}
|
|
284813
285343
|
if (expr.includes(":") && !expr.startsWith("faker.")) {
|
|
284814
285344
|
const colonIdx = expr.indexOf(":");
|
|
284815
285345
|
const scope = expr.slice(0, colonIdx);
|
|
@@ -284839,21 +285369,21 @@ async function resolveTemplateString(value) {
|
|
|
284839
285369
|
}
|
|
284840
285370
|
return result;
|
|
284841
285371
|
}
|
|
284842
|
-
async function resolveBodyTemplates(body) {
|
|
285372
|
+
async function resolveBodyTemplates(body, context) {
|
|
284843
285373
|
const resolved = {};
|
|
284844
285374
|
for (const [key, value] of Object.entries(body)) {
|
|
284845
|
-
resolved[key] = typeof value === "string" ? await resolveTemplateString(value) : value;
|
|
285375
|
+
resolved[key] = typeof value === "string" ? await resolveTemplateString(value, context) : value;
|
|
284846
285376
|
}
|
|
284847
285377
|
return resolved;
|
|
284848
285378
|
}
|
|
284849
|
-
async function resolveInjections(inject, memory, allMemory) {
|
|
285379
|
+
async function resolveInjections(inject, memory, allMemory, context) {
|
|
284850
285380
|
const headers = {};
|
|
284851
285381
|
const body = {};
|
|
284852
285382
|
if (!inject) return { headers, body };
|
|
284853
285383
|
for (const [target, memKeyOrTemplate] of Object.entries(inject)) {
|
|
284854
285384
|
let strValue;
|
|
284855
285385
|
if (memKeyOrTemplate.includes("{{")) {
|
|
284856
|
-
strValue = await resolveTemplateString(memKeyOrTemplate);
|
|
285386
|
+
strValue = await resolveTemplateString(memKeyOrTemplate, context);
|
|
284857
285387
|
} else {
|
|
284858
285388
|
const [scope, key] = memKeyOrTemplate.includes(":") ? memKeyOrTemplate.split(":", 2) : ["workflow", memKeyOrTemplate];
|
|
284859
285389
|
const scopedMemory = new import_memory2.JeticMemory({ scope });
|
|
@@ -284906,8 +285436,9 @@ async function resolvePathParams(urlPath, body) {
|
|
|
284906
285436
|
async function executeStep(step, baseUrl, defaultBody) {
|
|
284907
285437
|
const startTime = Date.now();
|
|
284908
285438
|
const memory = new import_memory2.JeticMemory({ scope: "workflow" });
|
|
284909
|
-
const
|
|
284910
|
-
const
|
|
285439
|
+
const stepLabel = step.name || `${step.method} ${step.path}`;
|
|
285440
|
+
const { headers, body: injectedBody } = await resolveInjections(step.inject, memory, memory, stepLabel);
|
|
285441
|
+
const resolvedStepBody = await resolveBodyTemplates(step.body || {}, stepLabel);
|
|
284911
285442
|
const requestBody = { ...defaultBody, ...injectedBody, ...resolvedStepBody };
|
|
284912
285443
|
const inputCaptured = await captureInputToMemory(step.captureInput, requestBody);
|
|
284913
285444
|
const resolvedPath = await resolvePathParams(step.path, requestBody);
|
|
@@ -285251,71 +285782,86 @@ var simulateWorkflowCommand = new import_commander6.Command("workflow").descript
|
|
|
285251
285782
|
const stepLabel = `${c.dim}${i + 1}/${workflow.steps.length}${c.reset} ${getMethodColor(step.method)}${step.method}${c.reset} ${step.path}`;
|
|
285252
285783
|
const spinner = new Spinner();
|
|
285253
285784
|
spinner.start(`${stepLabel} ${c.dim}${step.name}${c.reset}...`);
|
|
285254
|
-
|
|
285255
|
-
|
|
285256
|
-
|
|
285257
|
-
|
|
285258
|
-
|
|
285259
|
-
|
|
285260
|
-
|
|
285261
|
-
|
|
285262
|
-
|
|
285263
|
-
|
|
285264
|
-
|
|
285265
|
-
|
|
285785
|
+
let result;
|
|
285786
|
+
try {
|
|
285787
|
+
result = await executeStep(step, baseUrl, {});
|
|
285788
|
+
if (step.condition) {
|
|
285789
|
+
const condResult = await evaluateStepCondition(step.condition, `condition of step "${step.name || step.path}"`);
|
|
285790
|
+
result.conditionResult = condResult;
|
|
285791
|
+
if (!condResult.passed) {
|
|
285792
|
+
const { onFail, switchToWorkflow } = step.condition;
|
|
285793
|
+
results.push(result);
|
|
285794
|
+
const icon2 = result.passed ? TICK : CROSS;
|
|
285795
|
+
spinner.stop(` ${icon2} ${stepLabel} ${formatStatus(result.status)} ${c.dim}${result.durationMs}ms${c.reset}`);
|
|
285796
|
+
renderStepResult(result, i, workflow.steps.length);
|
|
285797
|
+
if (onFail === "abort") {
|
|
285798
|
+
console.log(` ${c.red}${c.bold}\u26A0 Condition failed \u2014 aborting workflow.${c.reset}
|
|
285266
285799
|
`);
|
|
285267
|
-
|
|
285268
|
-
|
|
285269
|
-
|
|
285270
|
-
|
|
285800
|
+
stopOnFailure = true;
|
|
285801
|
+
break;
|
|
285802
|
+
} else if (onFail === "continue") {
|
|
285803
|
+
console.log(` ${c.yellow}\u2298 Condition failed \u2014 skipping remaining steps.${c.reset}
|
|
285271
285804
|
`);
|
|
285272
|
-
|
|
285273
|
-
|
|
285274
|
-
|
|
285275
|
-
|
|
285805
|
+
stopOnFailure = true;
|
|
285806
|
+
break;
|
|
285807
|
+
} else if (onFail === "switch" && switchToWorkflow) {
|
|
285808
|
+
console.log(` ${ARROW} Condition failed \u2014 switching to workflow: ${c.bold}${switchToWorkflow}${c.reset}
|
|
285276
285809
|
`);
|
|
285277
|
-
|
|
285278
|
-
|
|
285279
|
-
|
|
285280
|
-
|
|
285281
|
-
|
|
285810
|
+
const switchPath = path3.join(workflowsDir, `${switchToWorkflow}.json`);
|
|
285811
|
+
if (fs.existsSync(switchPath)) {
|
|
285812
|
+
try {
|
|
285813
|
+
const switchedWorkflow = JSON.parse(fs.readFileSync(switchPath, "utf-8"));
|
|
285814
|
+
console.log(` ${TICK} Loaded switch workflow: ${c.bold}${switchedWorkflow.name}${c.reset}
|
|
285282
285815
|
`);
|
|
285283
|
-
|
|
285284
|
-
|
|
285816
|
+
renderWorkflowHeader(switchedWorkflow);
|
|
285817
|
+
console.log(` ${c.magenta}\u{1F680}${c.reset} Executing sub-workflow...
|
|
285818
|
+
`);
|
|
285819
|
+
for (let si = 0; si < switchedWorkflow.steps.length; si++) {
|
|
285820
|
+
const switchStep = switchedWorkflow.steps[si];
|
|
285821
|
+
const switchLabel = `${c.dim}${si + 1}/${switchedWorkflow.steps.length}${c.reset} ${getMethodColor(switchStep.method)}${switchStep.method}${c.reset} ${switchStep.path}`;
|
|
285822
|
+
const switchSpinner = new Spinner();
|
|
285823
|
+
switchSpinner.start(`${switchLabel} ${c.dim}${switchStep.name}${c.reset}...`);
|
|
285824
|
+
const switchResult = await executeStep(switchStep, baseUrl, {});
|
|
285825
|
+
if (switchStep.condition) {
|
|
285826
|
+
switchResult.conditionResult = await evaluateStepCondition(
|
|
285827
|
+
switchStep.condition,
|
|
285828
|
+
`condition of step "${switchStep.name || switchStep.path}"`
|
|
285829
|
+
);
|
|
285830
|
+
}
|
|
285831
|
+
results.push(switchResult);
|
|
285832
|
+
const switchIcon = switchResult.passed ? TICK : CROSS;
|
|
285833
|
+
switchSpinner.stop(` ${switchIcon} ${switchLabel} ${formatStatus(switchResult.status)} ${c.dim}${switchResult.durationMs}ms${c.reset}`);
|
|
285834
|
+
renderStepResult(switchResult, si, switchedWorkflow.steps.length);
|
|
285835
|
+
}
|
|
285836
|
+
} catch (e) {
|
|
285837
|
+
console.log(` ${c.red}\u2717 Failed to load switch workflow: ${e.message}${c.reset}
|
|
285285
285838
|
`);
|
|
285286
|
-
for (let si = 0; si < switchedWorkflow.steps.length; si++) {
|
|
285287
|
-
const switchStep = switchedWorkflow.steps[si];
|
|
285288
|
-
const switchLabel = `${c.dim}${si + 1}/${switchedWorkflow.steps.length}${c.reset} ${getMethodColor(switchStep.method)}${switchStep.method}${c.reset} ${switchStep.path}`;
|
|
285289
|
-
const switchSpinner = new Spinner();
|
|
285290
|
-
switchSpinner.start(`${switchLabel} ${c.dim}${switchStep.name}${c.reset}...`);
|
|
285291
|
-
const switchResult = await executeStep(switchStep, baseUrl, {});
|
|
285292
|
-
if (switchStep.condition) {
|
|
285293
|
-
switchResult.conditionResult = await evaluateStepCondition(switchStep.condition);
|
|
285294
|
-
}
|
|
285295
|
-
results.push(switchResult);
|
|
285296
|
-
const switchIcon = switchResult.passed ? TICK : CROSS;
|
|
285297
|
-
switchSpinner.stop(` ${switchIcon} ${switchLabel} ${formatStatus(switchResult.status)} ${c.dim}${switchResult.durationMs}ms${c.reset}`);
|
|
285298
|
-
renderStepResult(switchResult, si, switchedWorkflow.steps.length);
|
|
285299
285839
|
}
|
|
285300
|
-
}
|
|
285301
|
-
console.log(` ${c.
|
|
285840
|
+
} else {
|
|
285841
|
+
console.log(` ${c.yellow}\u26A0 Switch workflow not found: ${switchPath}${c.reset}
|
|
285302
285842
|
`);
|
|
285303
285843
|
}
|
|
285304
|
-
|
|
285305
|
-
|
|
285306
|
-
`);
|
|
285307
|
-
}
|
|
285308
|
-
if (step.condition.returnOnComplete) {
|
|
285309
|
-
console.log(` ${c.green}\u21A9 Sub-workflow complete. Returning to main workflow...${c.reset}
|
|
285844
|
+
if (step.condition.returnOnComplete) {
|
|
285845
|
+
console.log(` ${c.green}\u21A9 Sub-workflow complete. Returning to main workflow...${c.reset}
|
|
285310
285846
|
`);
|
|
285311
|
-
|
|
285312
|
-
|
|
285313
|
-
|
|
285314
|
-
|
|
285847
|
+
continue;
|
|
285848
|
+
} else {
|
|
285849
|
+
stopOnFailure = true;
|
|
285850
|
+
break;
|
|
285851
|
+
}
|
|
285315
285852
|
}
|
|
285853
|
+
continue;
|
|
285316
285854
|
}
|
|
285317
|
-
continue;
|
|
285318
285855
|
}
|
|
285856
|
+
} catch (err) {
|
|
285857
|
+
if (err instanceof HumanInputError || err?.name === "HumanInputError") {
|
|
285858
|
+
spinner.stop(` ${c.red}\u2717${c.reset} ${stepLabel} ${c.dim}needs human input${c.reset}`);
|
|
285859
|
+
console.error(`
|
|
285860
|
+
${c.red}${err.message}${c.reset}
|
|
285861
|
+
`);
|
|
285862
|
+
process.exit(1);
|
|
285863
|
+
}
|
|
285864
|
+
throw err;
|
|
285319
285865
|
}
|
|
285320
285866
|
results.push(result);
|
|
285321
285867
|
const icon = result.passed ? TICK : CROSS;
|
|
@@ -286228,8 +286774,29 @@ Output valid JSON only.
|
|
|
286228
286774
|
res.status(500).json({ error: err.message });
|
|
286229
286775
|
}
|
|
286230
286776
|
});
|
|
286777
|
+
const humanInputWaiters = /* @__PURE__ */ new Map();
|
|
286778
|
+
const HUMAN_INPUT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
286779
|
+
class HumanInputAbort extends Error {
|
|
286780
|
+
constructor(key, reason) {
|
|
286781
|
+
super(`Human input for "{{human:${key}}}" ${reason}`);
|
|
286782
|
+
this.name = "HumanInputAbort";
|
|
286783
|
+
}
|
|
286784
|
+
}
|
|
286785
|
+
app.post("/api/workflows/human-input", (req, res) => {
|
|
286786
|
+
const { runId, key, value } = req.body;
|
|
286787
|
+
if (!runId || !key) {
|
|
286788
|
+
return res.status(400).json({ error: "runId and key are required" });
|
|
286789
|
+
}
|
|
286790
|
+
const waiter = humanInputWaiters.get(runId);
|
|
286791
|
+
if (!waiter || waiter.key !== key) {
|
|
286792
|
+
return res.status(404).json({ error: "no pending human input for this run/key (answered, timed out, or run ended)" });
|
|
286793
|
+
}
|
|
286794
|
+
humanInputWaiters.delete(runId);
|
|
286795
|
+
waiter.resolve(typeof value === "string" ? value : null);
|
|
286796
|
+
res.json({ ok: true });
|
|
286797
|
+
});
|
|
286231
286798
|
app.post("/api/workflows/run", async (req, res) => {
|
|
286232
|
-
const { file } = req.body;
|
|
286799
|
+
const { file, humanInputs: preseededHuman } = req.body;
|
|
286233
286800
|
if (!file) {
|
|
286234
286801
|
return res.status(400).json({ error: "file is required" });
|
|
286235
286802
|
}
|
|
@@ -286270,10 +286837,46 @@ Output valid JSON only.
|
|
|
286270
286837
|
}
|
|
286271
286838
|
}
|
|
286272
286839
|
if (!baseUrl) baseUrl = "http://localhost:4000";
|
|
286273
|
-
|
|
286840
|
+
const runId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
286841
|
+
const runHumanCache = new Map(Object.entries(preseededHuman ?? {}));
|
|
286842
|
+
const HUMAN_KEY_RE = /\{\{\s*human:([^}]+?)\s*\}\}/g;
|
|
286843
|
+
const collectHumanRefs = () => {
|
|
286844
|
+
const keys = /* @__PURE__ */ new Set();
|
|
286845
|
+
for (const step of workflow.steps || []) {
|
|
286846
|
+
for (const hay of [step?.path, JSON.stringify(step?.body ?? {}), JSON.stringify(step?.inject ?? {}), JSON.stringify(step?.condition ?? {})]) {
|
|
286847
|
+
if (!hay) continue;
|
|
286848
|
+
HUMAN_KEY_RE.lastIndex = 0;
|
|
286849
|
+
let m;
|
|
286850
|
+
while ((m = HUMAN_KEY_RE.exec(hay)) !== null) {
|
|
286851
|
+
const k = (m[1] || "").trim();
|
|
286852
|
+
if (k) keys.add(k);
|
|
286853
|
+
}
|
|
286854
|
+
}
|
|
286855
|
+
}
|
|
286856
|
+
return [...keys];
|
|
286857
|
+
};
|
|
286858
|
+
const humanEnvName = (key) => `JETIC_HUMAN_${key.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
286859
|
+
const humanSource = async (key) => {
|
|
286860
|
+
if (runHumanCache.has(key)) return "run";
|
|
286861
|
+
if (process.env[humanEnvName(key)]) return "env";
|
|
286862
|
+
try {
|
|
286863
|
+
const v = await new Mem({ scope: "human" }).get(key);
|
|
286864
|
+
if (v != null && String(v) !== "") return "memory";
|
|
286865
|
+
} catch {
|
|
286866
|
+
}
|
|
286867
|
+
return "prompt";
|
|
286868
|
+
};
|
|
286869
|
+
const neededHuman = [];
|
|
286870
|
+
for (const k of collectHumanRefs()) neededHuman.push({ key: k, source: await humanSource(k) });
|
|
286871
|
+
send("start", { name: workflow.name, totalSteps: workflow.steps.length, baseUrl, runId, humanKeys: neededHuman });
|
|
286274
286872
|
let aborted = false;
|
|
286275
286873
|
req.on("close", () => {
|
|
286276
286874
|
aborted = true;
|
|
286875
|
+
const waiter = humanInputWaiters.get(runId);
|
|
286876
|
+
if (waiter) {
|
|
286877
|
+
humanInputWaiters.delete(runId);
|
|
286878
|
+
waiter.resolve(null);
|
|
286879
|
+
}
|
|
286277
286880
|
});
|
|
286278
286881
|
function deepGet2(obj, dotPath) {
|
|
286279
286882
|
return dotPath.replace(/\[(\d+)\]/g, ".$1").split(".").reduce((o, k) => o?.[k], obj);
|
|
@@ -286293,42 +286896,101 @@ Output valid JSON only.
|
|
|
286293
286896
|
return _;
|
|
286294
286897
|
});
|
|
286295
286898
|
}
|
|
286296
|
-
async function resolveMemoryTemplates(value) {
|
|
286899
|
+
async function resolveMemoryTemplates(value, stepIndex = -1, stepName = "") {
|
|
286297
286900
|
const matches = [...value.matchAll(/\{\{([^}]+)\}\}/g)];
|
|
286298
286901
|
let result = value;
|
|
286299
286902
|
for (const [placeholder, expr] of matches.map((m) => [m[0], m[1].trim()])) {
|
|
286300
286903
|
if (expr.includes(":") && !expr.startsWith("faker.")) {
|
|
286301
286904
|
const [scope, key] = expr.split(":", 2);
|
|
286302
|
-
const
|
|
286303
|
-
|
|
286304
|
-
result = result.replace(placeholder, val != null ? String(val) : "");
|
|
286905
|
+
const val = await resolveMemValue(scope, key, stepIndex, stepName);
|
|
286906
|
+
result = result.replace(placeholder, val);
|
|
286305
286907
|
}
|
|
286306
286908
|
}
|
|
286307
286909
|
return result;
|
|
286308
286910
|
}
|
|
286309
|
-
async function resolveStr(v) {
|
|
286911
|
+
async function resolveStr(v, stepIndex = -1, stepName = "") {
|
|
286310
286912
|
let s = await resolveTemplate(v);
|
|
286311
|
-
s = await resolveMemoryTemplates(s);
|
|
286913
|
+
s = await resolveMemoryTemplates(s, stepIndex, stepName);
|
|
286312
286914
|
return s;
|
|
286313
286915
|
}
|
|
286314
|
-
async function
|
|
286916
|
+
async function resolveValueRecursive(val, stepIndex = -1, stepName = "") {
|
|
286917
|
+
if (typeof val === "string") {
|
|
286918
|
+
return await resolveStr(val, stepIndex, stepName);
|
|
286919
|
+
}
|
|
286920
|
+
if (Array.isArray(val)) {
|
|
286921
|
+
const res2 = [];
|
|
286922
|
+
for (const item of val) {
|
|
286923
|
+
res2.push(await resolveValueRecursive(item, stepIndex, stepName));
|
|
286924
|
+
}
|
|
286925
|
+
return res2;
|
|
286926
|
+
}
|
|
286927
|
+
if (val && typeof val === "object" && val.constructor === Object) {
|
|
286928
|
+
const res2 = {};
|
|
286929
|
+
for (const [k, v] of Object.entries(val)) {
|
|
286930
|
+
res2[k] = await resolveValueRecursive(v, stepIndex, stepName);
|
|
286931
|
+
}
|
|
286932
|
+
return res2;
|
|
286933
|
+
}
|
|
286934
|
+
return val;
|
|
286935
|
+
}
|
|
286936
|
+
async function resolveMemValue(scope, key, stepIndex, stepName) {
|
|
286937
|
+
if (scope !== "human") {
|
|
286938
|
+
const mem2 = new Mem({ scope });
|
|
286939
|
+
const val = await mem2.get(key);
|
|
286940
|
+
return val != null ? String(val) : "";
|
|
286941
|
+
}
|
|
286942
|
+
if (runHumanCache.has(key)) {
|
|
286943
|
+
send("human_resolved", { index: stepIndex, key, source: "run" });
|
|
286944
|
+
return runHumanCache.get(key);
|
|
286945
|
+
}
|
|
286946
|
+
const envVal = process.env[humanEnvName(key)];
|
|
286947
|
+
if (envVal !== void 0 && envVal !== "") {
|
|
286948
|
+
send("human_resolved", { index: stepIndex, key, source: "env" });
|
|
286949
|
+
return envVal;
|
|
286950
|
+
}
|
|
286951
|
+
const mem = new Mem({ scope: "human" });
|
|
286952
|
+
const answer = await waitForHumanInput(runId, key, stepIndex, stepName, send);
|
|
286953
|
+
if (answer === null) {
|
|
286954
|
+
throw new HumanInputAbort(key, aborted ? "was cancelled" : "timed out after 10 minutes");
|
|
286955
|
+
}
|
|
286956
|
+
await mem.set(key, answer);
|
|
286957
|
+
send("human_resolved", { index: stepIndex, key, source: "prompt" });
|
|
286958
|
+
return answer;
|
|
286959
|
+
}
|
|
286960
|
+
function waitForHumanInput(forRunId, key, stepIndex, stepName, sendFn) {
|
|
286961
|
+
return new Promise((resolve2) => {
|
|
286962
|
+
const timer = setTimeout(() => {
|
|
286963
|
+
if (humanInputWaiters.get(forRunId)?.key === key) {
|
|
286964
|
+
humanInputWaiters.delete(forRunId);
|
|
286965
|
+
resolve2(null);
|
|
286966
|
+
}
|
|
286967
|
+
}, HUMAN_INPUT_TIMEOUT_MS);
|
|
286968
|
+
humanInputWaiters.set(forRunId, {
|
|
286969
|
+
key,
|
|
286970
|
+
stepIndex,
|
|
286971
|
+
resolve: (value) => {
|
|
286972
|
+
clearTimeout(timer);
|
|
286973
|
+
resolve2(value);
|
|
286974
|
+
}
|
|
286975
|
+
});
|
|
286976
|
+
sendFn("human_input_required", { runId: forRunId, key, stepIndex, stepName });
|
|
286977
|
+
});
|
|
286978
|
+
}
|
|
286979
|
+
async function executeStep2(step, stepIndex) {
|
|
286315
286980
|
const startTime = Date.now();
|
|
286981
|
+
const stepName = step.name || `${step.method} ${step.path}`;
|
|
286316
286982
|
const headers = { "Content-Type": "application/json" };
|
|
286317
286983
|
const bodyExtra = {};
|
|
286318
286984
|
for (const [target, memKeyOrTpl] of Object.entries(step.inject ?? {})) {
|
|
286319
|
-
const strVal = memKeyOrTpl.includes("{{") ? await resolveStr(memKeyOrTpl) : await (async () => {
|
|
286985
|
+
const strVal = memKeyOrTpl.includes("{{") ? await resolveStr(memKeyOrTpl, stepIndex, stepName) : await (async () => {
|
|
286320
286986
|
const [scope, key] = memKeyOrTpl.includes(":") ? memKeyOrTpl.split(":", 2) : ["workflow", memKeyOrTpl];
|
|
286321
|
-
|
|
286322
|
-
return val != null ? String(val) : "";
|
|
286987
|
+
return resolveMemValue(scope, key, stepIndex, stepName);
|
|
286323
286988
|
})();
|
|
286324
286989
|
if (!strVal) continue;
|
|
286325
286990
|
if (target.startsWith("body:")) bodyExtra[target.slice(5)] = strVal;
|
|
286326
286991
|
else headers[target.startsWith("header:") ? target.slice(7) : target] = strVal;
|
|
286327
286992
|
}
|
|
286328
|
-
const resolvedBody = {};
|
|
286329
|
-
for (const [k, v] of Object.entries(step.body ?? {})) {
|
|
286330
|
-
resolvedBody[k] = typeof v === "string" ? await resolveStr(v) : v;
|
|
286331
|
-
}
|
|
286993
|
+
const resolvedBody = await resolveValueRecursive(step.body ?? {}, stepIndex, stepName);
|
|
286332
286994
|
const requestBody = { ...bodyExtra, ...resolvedBody };
|
|
286333
286995
|
for (const [memKey, bodyField] of Object.entries(step.captureInput ?? {})) {
|
|
286334
286996
|
const val = requestBody[bodyField];
|
|
@@ -286341,7 +287003,7 @@ Output valid JSON only.
|
|
|
286341
287003
|
/:([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
286342
287004
|
(_, p) => requestBody[p] !== void 0 ? String(requestBody[p]) : `:${p}`
|
|
286343
287005
|
);
|
|
286344
|
-
resolvedPath = await resolveStr(resolvedPath);
|
|
287006
|
+
resolvedPath = await resolveStr(resolvedPath, stepIndex, stepName);
|
|
286345
287007
|
const finalUrl = `${baseUrl.replace(/\/$/, "")}${resolvedPath}`;
|
|
286346
287008
|
const expectedStatus = step.expectStatus ?? 200;
|
|
286347
287009
|
try {
|
|
@@ -286384,22 +287046,20 @@ Output valid JSON only.
|
|
|
286384
287046
|
}
|
|
286385
287047
|
let passed = 0;
|
|
286386
287048
|
let failed = 0;
|
|
286387
|
-
async function resolveCondRef(ref) {
|
|
287049
|
+
async function resolveCondRef(ref, stepIndex, stepName) {
|
|
286388
287050
|
const matches = [...ref.matchAll(/\{\{([^}]+)\}\}/g)];
|
|
286389
287051
|
let result = ref;
|
|
286390
287052
|
for (const [placeholder, expr] of matches.map((m) => [m[0], m[1].trim()])) {
|
|
286391
287053
|
if (expr.includes(":") && !expr.startsWith("faker.")) {
|
|
286392
287054
|
const [scope, key] = expr.split(":", 2);
|
|
286393
|
-
|
|
286394
|
-
const val = await mem.get(key);
|
|
286395
|
-
result = result.replace(placeholder, val != null ? String(val) : "");
|
|
287055
|
+
result = result.replace(placeholder, await resolveMemValue(scope, key, stepIndex, stepName));
|
|
286396
287056
|
}
|
|
286397
287057
|
}
|
|
286398
287058
|
return result;
|
|
286399
287059
|
}
|
|
286400
|
-
async function evalCondRule(rule) {
|
|
286401
|
-
const left = await resolveCondRef(rule.left);
|
|
286402
|
-
const right = rule.right !== void 0 ? await resolveCondRef(rule.right) : void 0;
|
|
287060
|
+
async function evalCondRule(rule, stepIndex, stepName) {
|
|
287061
|
+
const left = await resolveCondRef(rule.left, stepIndex, stepName);
|
|
287062
|
+
const right = rule.right !== void 0 ? await resolveCondRef(rule.right, stepIndex, stepName) : void 0;
|
|
286403
287063
|
switch (rule.operator) {
|
|
286404
287064
|
case "equals":
|
|
286405
287065
|
return left === right;
|
|
@@ -286433,12 +287093,12 @@ Output valid JSON only.
|
|
|
286433
287093
|
return false;
|
|
286434
287094
|
}
|
|
286435
287095
|
}
|
|
286436
|
-
async function evalCondition(condition) {
|
|
287096
|
+
async function evalCondition(condition, stepIndex, stepName) {
|
|
286437
287097
|
const rules = condition.rules?.all ?? condition.rules?.any ?? [];
|
|
286438
287098
|
const isAnd = !!condition.rules?.all;
|
|
286439
287099
|
if (rules.length === 0) return { passed: true, reason: "No rules" };
|
|
286440
287100
|
const results = [];
|
|
286441
|
-
for (const rule of rules) results.push({ rule, ok: await evalCondRule(rule) });
|
|
287101
|
+
for (const rule of rules) results.push({ rule, ok: await evalCondRule(rule, stepIndex, stepName) });
|
|
286442
287102
|
const passed2 = isAnd ? results.every((r) => r.ok) : results.some((r) => r.ok);
|
|
286443
287103
|
const failing = results.filter((r) => !r.ok);
|
|
286444
287104
|
const reason = passed2 ? `All conditions met (${isAnd ? "AND" : "OR"})` : `Failed: ${failing.map((r) => `${r.rule.left} ${r.rule.operator}${r.rule.right !== void 0 ? " " + r.rule.right : ""}`).join(", ")}`;
|
|
@@ -286448,7 +287108,17 @@ Output valid JSON only.
|
|
|
286448
287108
|
if (aborted) break;
|
|
286449
287109
|
const step = workflow.steps[i];
|
|
286450
287110
|
send("step_start", { index: i, step: { name: step.name, method: step.method, path: step.path } });
|
|
286451
|
-
|
|
287111
|
+
let result;
|
|
287112
|
+
try {
|
|
287113
|
+
result = await executeStep2(step, i);
|
|
287114
|
+
} catch (err) {
|
|
287115
|
+
if (err instanceof HumanInputAbort || err?.name === "HumanInputAbort") {
|
|
287116
|
+
send("aborted", { index: i, reason: err.message });
|
|
287117
|
+
res.end();
|
|
287118
|
+
return;
|
|
287119
|
+
}
|
|
287120
|
+
throw err;
|
|
287121
|
+
}
|
|
286452
287122
|
if (result.passed) passed++;
|
|
286453
287123
|
else failed++;
|
|
286454
287124
|
send("step_result", {
|
|
@@ -286463,7 +287133,7 @@ Output valid JSON only.
|
|
|
286463
287133
|
error: result.error
|
|
286464
287134
|
});
|
|
286465
287135
|
if (step.condition && !aborted) {
|
|
286466
|
-
const condResult = await evalCondition(step.condition);
|
|
287136
|
+
const condResult = await evalCondition(step.condition, i, step.name || `${step.method} ${step.path}`);
|
|
286467
287137
|
send("condition_result", {
|
|
286468
287138
|
index: i,
|
|
286469
287139
|
passed: condResult.passed,
|
|
@@ -286608,29 +287278,34 @@ Output valid JSON only.
|
|
|
286608
287278
|
}
|
|
286609
287279
|
});
|
|
286610
287280
|
try {
|
|
286611
|
-
let dashboardDistPath =
|
|
286612
|
-
|
|
286613
|
-
|
|
287281
|
+
let dashboardDistPath = null;
|
|
287282
|
+
try {
|
|
287283
|
+
const dashboardPackagePath = require.resolve("@jetic/dashboard/package.json");
|
|
287284
|
+
const workspaceDist = import_path.default.join(import_path.default.dirname(dashboardPackagePath), "dist");
|
|
287285
|
+
if (import_fs.default.existsSync(workspaceDist)) dashboardDistPath = workspaceDist;
|
|
287286
|
+
} catch {
|
|
286614
287287
|
}
|
|
286615
|
-
if (!
|
|
286616
|
-
|
|
286617
|
-
|
|
286618
|
-
dashboardDistPath = import_path.default.join(import_path.default.dirname(dashboardPackagePath), "dist");
|
|
286619
|
-
} catch {
|
|
286620
|
-
}
|
|
287288
|
+
if (!dashboardDistPath) {
|
|
287289
|
+
const bundled = import_path.default.join(__dirname, "dashboard");
|
|
287290
|
+
if (import_fs.default.existsSync(bundled)) dashboardDistPath = bundled;
|
|
286621
287291
|
}
|
|
286622
|
-
if (!
|
|
286623
|
-
|
|
287292
|
+
if (!dashboardDistPath) {
|
|
287293
|
+
const sibling = import_path.default.join(__dirname, "..", "dashboard");
|
|
287294
|
+
if (import_fs.default.existsSync(sibling)) dashboardDistPath = sibling;
|
|
287295
|
+
}
|
|
287296
|
+
if (!dashboardDistPath) {
|
|
287297
|
+
console.warn("Dashboard build not found. Build it with: pnpm --filter @jetic/dashboard run build");
|
|
287298
|
+
} else {
|
|
287299
|
+
app.use(import_express.default.static(dashboardDistPath));
|
|
287300
|
+
app.get("*", (_req, res) => {
|
|
287301
|
+
const indexPath = import_path.default.join(dashboardDistPath, "index.html");
|
|
287302
|
+
if (import_fs.default.existsSync(indexPath)) {
|
|
287303
|
+
res.sendFile(indexPath);
|
|
287304
|
+
} else {
|
|
287305
|
+
res.status(404).send("Dashboard not built yet.");
|
|
287306
|
+
}
|
|
287307
|
+
});
|
|
286624
287308
|
}
|
|
286625
|
-
app.use(import_express.default.static(dashboardDistPath));
|
|
286626
|
-
app.get("*", (_req, res) => {
|
|
286627
|
-
const indexPath = import_path.default.join(dashboardDistPath, "index.html");
|
|
286628
|
-
if (import_fs.default.existsSync(indexPath)) {
|
|
286629
|
-
res.sendFile(indexPath);
|
|
286630
|
-
} else {
|
|
286631
|
-
res.status(404).send("Dashboard not built yet.");
|
|
286632
|
-
}
|
|
286633
|
-
});
|
|
286634
287309
|
} catch (e) {
|
|
286635
287310
|
console.error("Could not find dashboard static files. Ensure it is built.", e);
|
|
286636
287311
|
}
|
|
@@ -286801,7 +287476,7 @@ var mcpCommand = new import_commander10.Command("mcp").description("Launch Model
|
|
|
286801
287476
|
|
|
286802
287477
|
// src/index.ts
|
|
286803
287478
|
var program = new import_commander11.Command();
|
|
286804
|
-
program.name("jetic").description("AI-Native API Behavior Testing").version("0.1
|
|
287479
|
+
program.name("jetic").description("AI-Native API Behavior Testing").version("0.2.1");
|
|
286805
287480
|
program.addCommand(initCommand);
|
|
286806
287481
|
program.addCommand(scanCommand);
|
|
286807
287482
|
program.addCommand(inspectCommand);
|