jetic-cli 0.1.8 → 0.2.0
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 +101 -1
- 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 +1315 -148
- 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) : "" });
|
|
@@ -268509,7 +268516,7 @@ var require_endpoint_tools = __commonJS({
|
|
|
268509
268516
|
"use strict";
|
|
268510
268517
|
init_cjs_shims();
|
|
268511
268518
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
268512
|
-
exports2.deleteEndpointSchema = exports2.updateEndpointSchema = exports2.addEndpointSchema = void 0;
|
|
268519
|
+
exports2.deleteEndpointSchema = exports2.updateEndpointSchema = exports2.addEndpointSchema = exports2.constraintSchema = void 0;
|
|
268513
268520
|
exports2.handleAddEndpoint = handleAddEndpoint;
|
|
268514
268521
|
exports2.handleUpdateEndpoint = handleUpdateEndpoint;
|
|
268515
268522
|
exports2.handleDeleteEndpoint = handleDeleteEndpoint;
|
|
@@ -268517,11 +268524,13 @@ var require_endpoint_tools = __commonJS({
|
|
|
268517
268524
|
var crypto_1 = require("crypto");
|
|
268518
268525
|
var types_1 = require_types();
|
|
268519
268526
|
var parameterSchema = zod_1.z.object({
|
|
268520
|
-
name: zod_1.z.string().describe("Parameter name"),
|
|
268527
|
+
name: zod_1.z.string().min(1).describe("Parameter name"),
|
|
268521
268528
|
in: zod_1.z.enum(["query", "path", "header", "cookie", "body"]).optional().describe("Parameter location in HTTP request"),
|
|
268522
268529
|
type: zod_1.z.string().default("string").describe("Data type (string, number, boolean, array, object)"),
|
|
268523
268530
|
format: zod_1.z.string().optional().describe("Format specifier (e.g. uuid, email, date-time, int64)"),
|
|
268524
268531
|
required: zod_1.z.boolean().optional().describe("Whether parameter is required"),
|
|
268532
|
+
min: zod_1.z.number().optional().describe("Minimum numeric value"),
|
|
268533
|
+
max: zod_1.z.number().optional().describe("Maximum numeric value"),
|
|
268525
268534
|
default: zod_1.z.any().optional().describe("Default value"),
|
|
268526
268535
|
example: zod_1.z.any().optional().describe("Example value"),
|
|
268527
268536
|
description: zod_1.z.string().optional().describe("Description")
|
|
@@ -268530,46 +268539,115 @@ var require_endpoint_tools = __commonJS({
|
|
|
268530
268539
|
type: zod_1.z.string().describe("Data type (string, number, boolean, array, object)"),
|
|
268531
268540
|
format: zod_1.z.string().optional().describe("Format specifier"),
|
|
268532
268541
|
required: zod_1.z.boolean().optional().describe("Required flag"),
|
|
268542
|
+
min: zod_1.z.number().optional().describe("Minimum numeric value"),
|
|
268543
|
+
max: zod_1.z.number().optional().describe("Maximum numeric value"),
|
|
268544
|
+
minLength: zod_1.z.number().optional().describe("Minimum string length"),
|
|
268545
|
+
maxLength: zod_1.z.number().optional().describe("Maximum string length"),
|
|
268533
268546
|
description: zod_1.z.string().optional().describe("Description"),
|
|
268534
268547
|
example: zod_1.z.any().optional().describe("Example value"),
|
|
268535
268548
|
default: zod_1.z.any().optional().describe("Default value"),
|
|
268536
268549
|
enum: zod_1.z.array(zod_1.z.string()).optional().describe("Allowed enum values")
|
|
268537
268550
|
});
|
|
268551
|
+
exports2.constraintSchema = zod_1.z.object({
|
|
268552
|
+
field: zod_1.z.string().optional().describe("Field this constraint applies to"),
|
|
268553
|
+
rule: zod_1.z.string().optional().describe("Rule name (e.g. minLength, matches, requiredIf)"),
|
|
268554
|
+
value: zod_1.z.any().optional().describe("Rule value / threshold"),
|
|
268555
|
+
conflictStatus: zod_1.z.number().int().optional().describe("HTTP status on conflict"),
|
|
268556
|
+
failureStatus: zod_1.z.number().int().optional().describe("HTTP status on validation failure"),
|
|
268557
|
+
when: zod_1.z.object({
|
|
268558
|
+
field: zod_1.z.string().describe("Condition field"),
|
|
268559
|
+
present: zod_1.z.boolean().optional(),
|
|
268560
|
+
equals: zod_1.z.any().optional()
|
|
268561
|
+
}).optional().describe("Conditional constraint trigger"),
|
|
268562
|
+
then: zod_1.z.object({
|
|
268563
|
+
field: zod_1.z.string().describe("Field to constrain"),
|
|
268564
|
+
type: zod_1.z.string().describe("Expected type when condition holds")
|
|
268565
|
+
}).optional().describe("Conditional constraint effect")
|
|
268566
|
+
});
|
|
268538
268567
|
var requestBodySchema = zod_1.z.object({
|
|
268539
268568
|
contentType: zod_1.z.string().nullable().default("application/json").describe("Content-Type header (e.g. application/json, multipart/form-data)"),
|
|
268540
268569
|
required: zod_1.z.boolean().optional().describe("Whether request body is required"),
|
|
268541
|
-
fields: zod_1.z.record(fieldDefinitionSchema).default({}).describe("Key-value record of payload fields")
|
|
268570
|
+
fields: zod_1.z.record(fieldDefinitionSchema).default({}).describe("Key-value record of payload fields"),
|
|
268571
|
+
constraints: zod_1.z.array(exports2.constraintSchema).optional().describe("Business-rule constraints extracted from handler logic (e.g. password minLength)")
|
|
268542
268572
|
});
|
|
268543
268573
|
var responseDefinitionSchema = zod_1.z.object({
|
|
268544
268574
|
contentType: zod_1.z.string().optional().default("application/json"),
|
|
268545
268575
|
description: zod_1.z.string().optional().describe("Human readable response description"),
|
|
268546
|
-
schema: zod_1.z.record(zod_1.z.string()).optional().describe("Response body schema description (field -> type)"),
|
|
268547
|
-
example: zod_1.z.any().optional().describe("Response payload example")
|
|
268576
|
+
schema: zod_1.z.record(zod_1.z.string()).optional().describe("Response body schema description (field -> type, dot paths allowed)"),
|
|
268577
|
+
example: zod_1.z.any().optional().describe("Response payload example"),
|
|
268578
|
+
condition: zod_1.z.string().optional().describe("Condition under which this response occurs"),
|
|
268579
|
+
ownershipCheck: zod_1.z.boolean().optional().describe("Whether this response enforces an ownership check")
|
|
268548
268580
|
});
|
|
268549
268581
|
var endpointSecuritySchema = zod_1.z.object({
|
|
268550
|
-
scheme: zod_1.z.string().describe("Name of security scheme (e.g. bearerAuth, apiKey)"),
|
|
268582
|
+
scheme: zod_1.z.string().describe("Name of security scheme declared in top-level securitySchemes (e.g. bearerAuth, apiKey)"),
|
|
268551
268583
|
required: zod_1.z.boolean().optional().default(true),
|
|
268552
|
-
scopes: zod_1.z.array(zod_1.z.string()).optional()
|
|
268584
|
+
scopes: zod_1.z.array(zod_1.z.string()).optional().describe("OAuth2 scopes (only for oauth2 schemes)")
|
|
268553
268585
|
});
|
|
268554
268586
|
var sourceReferenceSchema = zod_1.z.object({
|
|
268555
268587
|
file: zod_1.z.string().describe("Relative or absolute path to source file containing handler/route"),
|
|
268556
268588
|
line: zod_1.z.number().describe("Line number in source file"),
|
|
268557
268589
|
column: zod_1.z.number().optional()
|
|
268558
268590
|
});
|
|
268591
|
+
var middlewareSchema = zod_1.z.object({
|
|
268592
|
+
name: zod_1.z.string().min(1).describe('Middleware name as in code (e.g. "auth", "cors", "express.json", "rateLimit")'),
|
|
268593
|
+
type: zod_1.z.string().optional().describe('Middleware category (e.g. "auth", "parser", "cors", "rate-limit", "validation", "logging")'),
|
|
268594
|
+
scheme: zod_1.z.string().optional().describe("Security scheme this middleware enforces (e.g. bearerAuth)"),
|
|
268595
|
+
config: zod_1.z.record(zod_1.z.any()).optional().describe('Middleware options (e.g. { "max": 100, "windowMs": 60000 })'),
|
|
268596
|
+
source: sourceReferenceSchema.optional().describe("Where the middleware is applied in source")
|
|
268597
|
+
});
|
|
268598
|
+
var paginationSchema = zod_1.z.object({
|
|
268599
|
+
type: zod_1.z.enum(["offset", "cursor", "page"]).describe("Pagination strategy"),
|
|
268600
|
+
pageParam: zod_1.z.string().optional().describe('Query param for page number/offset (e.g. "page")'),
|
|
268601
|
+
limitParam: zod_1.z.string().optional().describe('Query param for page size (e.g. "limit")'),
|
|
268602
|
+
cursorParam: zod_1.z.string().optional().describe("Query param for cursor (cursor strategy)"),
|
|
268603
|
+
totalPath: zod_1.z.string().optional().describe('Response path to total count (e.g. "meta.total")')
|
|
268604
|
+
});
|
|
268605
|
+
var rateLimitSchema = zod_1.z.object({
|
|
268606
|
+
windowMs: zod_1.z.number().int().positive().describe("Rate-limit window in milliseconds"),
|
|
268607
|
+
max: zod_1.z.number().int().positive().describe("Max requests per window"),
|
|
268608
|
+
scope: zod_1.z.string().optional().describe('Limit scope (e.g. "ip", "user", "global")')
|
|
268609
|
+
});
|
|
268610
|
+
var ownershipSchema = zod_1.z.object({
|
|
268611
|
+
ownerField: zod_1.z.string().describe('Resource field holding the owner id (e.g. "userId")'),
|
|
268612
|
+
mustMatchAuthClaim: zod_1.z.string().describe('Auth claim it must equal (e.g. "sub")')
|
|
268613
|
+
});
|
|
268614
|
+
var producesSchema = zod_1.z.object({
|
|
268615
|
+
variable: zod_1.z.string().optional().describe('Memory variable name produced (e.g. "accessToken")'),
|
|
268616
|
+
responseField: zod_1.z.string().optional().describe('Response path the value comes from (e.g. "data.accessToken")'),
|
|
268617
|
+
resourceStateChange: zod_1.z.object({
|
|
268618
|
+
resource: zod_1.z.string(),
|
|
268619
|
+
field: zod_1.z.string(),
|
|
268620
|
+
setFrom: zod_1.z.string()
|
|
268621
|
+
}).optional().describe("Resource state mutation caused by this endpoint")
|
|
268622
|
+
});
|
|
268623
|
+
var consumesSchema = zod_1.z.object({
|
|
268624
|
+
variable: zod_1.z.string().describe('Memory variable consumed (e.g. "accessToken")'),
|
|
268625
|
+
usedAs: zod_1.z.string().describe('How it is used: "header:<Name>" (Bearer prefix auto-added for Authorization) or "body:<field>" / "query:<param>"'),
|
|
268626
|
+
producedBy: zod_1.z.string().optional().describe('"METHOD /path" of the endpoint that produces it')
|
|
268627
|
+
});
|
|
268559
268628
|
exports2.addEndpointSchema = zod_1.z.object({
|
|
268560
268629
|
projectPath: zod_1.z.string().optional().describe("Path to project root or .jetic folder"),
|
|
268561
268630
|
method: zod_1.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]).describe("HTTP method"),
|
|
268562
268631
|
path: zod_1.z.string().describe("Route path (e.g., /api/v1/users or /api/users/:id)"),
|
|
268563
268632
|
name: zod_1.z.string().optional().describe('Human-readable endpoint name (e.g., "Create User")'),
|
|
268633
|
+
summary: zod_1.z.string().optional().describe("Short one-line summary"),
|
|
268564
268634
|
description: zod_1.z.string().optional().describe("Detailed description of endpoint functionality"),
|
|
268565
268635
|
tags: zod_1.z.array(zod_1.z.string()).optional().describe('Category tags (e.g., ["users", "auth"])'),
|
|
268636
|
+
deprecated: zod_1.z.boolean().optional().describe("Mark endpoint as deprecated"),
|
|
268637
|
+
timeout: zod_1.z.number().int().positive().optional().describe("Request timeout in milliseconds"),
|
|
268566
268638
|
handlerName: zod_1.z.string().optional().describe("Handler / controller function name (e.g. UserController.create)"),
|
|
268567
268639
|
resource: zod_1.z.string().optional().describe("Associated domain resource (e.g. User, Order)"),
|
|
268568
268640
|
source: sourceReferenceSchema.optional().describe("Source code provenance location"),
|
|
268569
|
-
security: zod_1.z.array(endpointSecuritySchema).optional().describe("Security requirements"),
|
|
268641
|
+
security: zod_1.z.array(endpointSecuritySchema).optional().describe("Security requirements (schemes must exist in top-level securitySchemes)"),
|
|
268642
|
+
middleware: zod_1.z.array(middlewareSchema).optional().describe("Middleware chain IN EXECUTION ORDER (e.g. cors \u2192 json parser \u2192 auth \u2192 rateLimit \u2192 validator)"),
|
|
268570
268643
|
parameters: zod_1.z.array(parameterSchema).optional().describe("Query, path, header, or cookie parameters"),
|
|
268571
|
-
requestBody: requestBodySchema.optional().describe("Request body specification"),
|
|
268644
|
+
requestBody: requestBodySchema.optional().describe("Request body specification incl. business-rule constraints"),
|
|
268572
268645
|
responses: zod_1.z.record(responseDefinitionSchema).optional().describe('Response definitions keyed by status code (e.g. "200", "400")'),
|
|
268646
|
+
pagination: paginationSchema.optional().describe("Pagination config for list endpoints"),
|
|
268647
|
+
rateLimit: rateLimitSchema.optional().describe("Rate-limit config"),
|
|
268648
|
+
ownership: ownershipSchema.optional().describe("Ownership / authorization rule"),
|
|
268649
|
+
produces: zod_1.z.array(producesSchema).optional().describe("Memory variables produced from responses (feeds workflows)"),
|
|
268650
|
+
consumes: zod_1.z.array(consumesSchema).optional().describe("Memory variables consumed as headers/body (chained auth)"),
|
|
268573
268651
|
overwriteIfExists: zod_1.z.boolean().optional().default(true).describe("If endpoint with same method and path exists, update it instead of erroring")
|
|
268574
268652
|
});
|
|
268575
268653
|
function handleAddEndpoint(args) {
|
|
@@ -268581,6 +268659,11 @@ var require_endpoint_tools = __commonJS({
|
|
|
268581
268659
|
if (existingIndex >= 0 && !overwriteIfExists) {
|
|
268582
268660
|
throw new Error(`Endpoint ${methodUpper} ${normalizedPath} already exists in ${filePath}. Set overwriteIfExists: true to update it.`);
|
|
268583
268661
|
}
|
|
268662
|
+
for (const mw of args.middleware || []) {
|
|
268663
|
+
if (!mw.name || !mw.name.trim()) {
|
|
268664
|
+
throw new Error('Middleware entries must have a non-empty "name".');
|
|
268665
|
+
}
|
|
268666
|
+
}
|
|
268584
268667
|
const source = args.source || {
|
|
268585
268668
|
file: "manual-mcp",
|
|
268586
268669
|
line: 1
|
|
@@ -268599,21 +268682,31 @@ var require_endpoint_tools = __commonJS({
|
|
|
268599
268682
|
description: "Internal server error"
|
|
268600
268683
|
}
|
|
268601
268684
|
};
|
|
268685
|
+
const previous = existingIndex >= 0 ? model.endpoints[existingIndex] : void 0;
|
|
268602
268686
|
const newEndpoint = {
|
|
268603
|
-
id:
|
|
268687
|
+
id: previous?.id || (0, crypto_1.randomUUID)(),
|
|
268604
268688
|
method: methodUpper,
|
|
268605
268689
|
path: normalizedPath,
|
|
268606
268690
|
name: args.name || `${methodUpper} ${normalizedPath}`,
|
|
268691
|
+
summary: args.summary,
|
|
268607
268692
|
description: args.description,
|
|
268608
268693
|
tags: args.tags || [],
|
|
268694
|
+
deprecated: args.deprecated,
|
|
268695
|
+
timeout: args.timeout,
|
|
268609
268696
|
handlerName: args.handlerName,
|
|
268610
268697
|
resource: args.resource,
|
|
268611
268698
|
source,
|
|
268612
268699
|
security: args.security,
|
|
268700
|
+
// Explicit middleware wins; otherwise preserve the previous chain (never silently drop it).
|
|
268701
|
+
middleware: args.middleware || previous?.middleware || [],
|
|
268613
268702
|
parameters: args.parameters,
|
|
268614
268703
|
requestBody: args.requestBody,
|
|
268615
268704
|
responses,
|
|
268616
|
-
|
|
268705
|
+
pagination: args.pagination,
|
|
268706
|
+
rateLimit: args.rateLimit || previous?.rateLimit,
|
|
268707
|
+
ownership: args.ownership,
|
|
268708
|
+
produces: args.produces,
|
|
268709
|
+
consumes: args.consumes
|
|
268617
268710
|
};
|
|
268618
268711
|
if (existingIndex >= 0) {
|
|
268619
268712
|
model.endpoints[existingIndex] = newEndpoint;
|
|
@@ -268621,31 +268714,45 @@ var require_endpoint_tools = __commonJS({
|
|
|
268621
268714
|
model.endpoints.push(newEndpoint);
|
|
268622
268715
|
}
|
|
268623
268716
|
(0, types_1.saveModel)(filePath, model);
|
|
268717
|
+
const unknownSchemes = (args.security || []).map((s) => s.scheme).filter((scheme) => model.securitySchemes && !(scheme in (model.securitySchemes || {})));
|
|
268624
268718
|
return {
|
|
268625
268719
|
success: true,
|
|
268626
268720
|
action: existingIndex >= 0 ? "updated" : "added",
|
|
268627
268721
|
filePath,
|
|
268628
268722
|
endpointId: newEndpoint.id,
|
|
268723
|
+
...unknownSchemes.length > 0 ? {
|
|
268724
|
+
warning: `Security scheme(s) not declared in top-level securitySchemes: ${unknownSchemes.join(", ")}. Declare them or run jetic_verify_model for details.`
|
|
268725
|
+
} : {},
|
|
268629
268726
|
endpoint: newEndpoint
|
|
268630
268727
|
};
|
|
268631
268728
|
}
|
|
268729
|
+
var endpointUpdatesSchema = zod_1.z.object({
|
|
268730
|
+
name: zod_1.z.string().optional(),
|
|
268731
|
+
summary: zod_1.z.string().optional(),
|
|
268732
|
+
description: zod_1.z.string().optional(),
|
|
268733
|
+
tags: zod_1.z.array(zod_1.z.string()).optional(),
|
|
268734
|
+
deprecated: zod_1.z.boolean().optional(),
|
|
268735
|
+
timeout: zod_1.z.number().int().positive().optional(),
|
|
268736
|
+
handlerName: zod_1.z.string().optional(),
|
|
268737
|
+
resource: zod_1.z.string().optional(),
|
|
268738
|
+
source: sourceReferenceSchema.optional(),
|
|
268739
|
+
security: zod_1.z.array(endpointSecuritySchema).optional(),
|
|
268740
|
+
middleware: zod_1.z.array(middlewareSchema).optional().describe("Replaces the whole middleware chain (order = execution order)"),
|
|
268741
|
+
parameters: zod_1.z.array(parameterSchema).optional(),
|
|
268742
|
+
requestBody: requestBodySchema.optional(),
|
|
268743
|
+
responses: zod_1.z.record(responseDefinitionSchema).optional(),
|
|
268744
|
+
pagination: paginationSchema.optional(),
|
|
268745
|
+
rateLimit: rateLimitSchema.nullable().optional().describe("Set to null to remove rate limiting"),
|
|
268746
|
+
ownership: ownershipSchema.optional(),
|
|
268747
|
+
produces: zod_1.z.array(producesSchema).optional(),
|
|
268748
|
+
consumes: zod_1.z.array(consumesSchema).optional()
|
|
268749
|
+
});
|
|
268632
268750
|
exports2.updateEndpointSchema = zod_1.z.object({
|
|
268633
268751
|
projectPath: zod_1.z.string().optional().describe("Path to project root or .jetic folder"),
|
|
268634
268752
|
id: zod_1.z.string().optional().describe("Target endpoint UUID"),
|
|
268635
268753
|
method: zod_1.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]).optional().describe("HTTP method"),
|
|
268636
268754
|
path: zod_1.z.string().optional().describe("Route path"),
|
|
268637
|
-
updates:
|
|
268638
|
-
name: zod_1.z.string().optional(),
|
|
268639
|
-
description: zod_1.z.string().optional(),
|
|
268640
|
-
tags: zod_1.z.array(zod_1.z.string()).optional(),
|
|
268641
|
-
handlerName: zod_1.z.string().optional(),
|
|
268642
|
-
resource: zod_1.z.string().optional(),
|
|
268643
|
-
source: sourceReferenceSchema.optional(),
|
|
268644
|
-
security: zod_1.z.array(endpointSecuritySchema).optional(),
|
|
268645
|
-
parameters: zod_1.z.array(parameterSchema).optional(),
|
|
268646
|
-
requestBody: requestBodySchema.optional(),
|
|
268647
|
-
responses: zod_1.z.record(responseDefinitionSchema).optional()
|
|
268648
|
-
}).describe("Partial endpoint properties to update")
|
|
268755
|
+
updates: endpointUpdatesSchema.describe("Partial endpoint properties to update (middleware replaces the whole chain)")
|
|
268649
268756
|
});
|
|
268650
268757
|
function handleUpdateEndpoint(args) {
|
|
268651
268758
|
const { model, filePath } = (0, types_1.loadModel)(args.projectPath);
|
|
@@ -268666,10 +268773,16 @@ var require_endpoint_tools = __commonJS({
|
|
|
268666
268773
|
const { updates } = args;
|
|
268667
268774
|
if (updates.name !== void 0)
|
|
268668
268775
|
endpoint.name = updates.name;
|
|
268776
|
+
if (updates.summary !== void 0)
|
|
268777
|
+
endpoint.summary = updates.summary;
|
|
268669
268778
|
if (updates.description !== void 0)
|
|
268670
268779
|
endpoint.description = updates.description;
|
|
268671
268780
|
if (updates.tags !== void 0)
|
|
268672
268781
|
endpoint.tags = updates.tags;
|
|
268782
|
+
if (updates.deprecated !== void 0)
|
|
268783
|
+
endpoint.deprecated = updates.deprecated;
|
|
268784
|
+
if (updates.timeout !== void 0)
|
|
268785
|
+
endpoint.timeout = updates.timeout;
|
|
268673
268786
|
if (updates.handlerName !== void 0)
|
|
268674
268787
|
endpoint.handlerName = updates.handlerName;
|
|
268675
268788
|
if (updates.resource !== void 0)
|
|
@@ -268678,12 +268791,29 @@ var require_endpoint_tools = __commonJS({
|
|
|
268678
268791
|
endpoint.source = updates.source;
|
|
268679
268792
|
if (updates.security !== void 0)
|
|
268680
268793
|
endpoint.security = updates.security;
|
|
268794
|
+
if (updates.middleware !== void 0) {
|
|
268795
|
+
for (const mw of updates.middleware) {
|
|
268796
|
+
if (!mw.name || !mw.name.trim())
|
|
268797
|
+
throw new Error('Middleware entries must have a non-empty "name".');
|
|
268798
|
+
}
|
|
268799
|
+
endpoint.middleware = updates.middleware;
|
|
268800
|
+
}
|
|
268681
268801
|
if (updates.parameters !== void 0)
|
|
268682
268802
|
endpoint.parameters = updates.parameters;
|
|
268683
268803
|
if (updates.requestBody !== void 0)
|
|
268684
268804
|
endpoint.requestBody = updates.requestBody;
|
|
268685
268805
|
if (updates.responses !== void 0)
|
|
268686
268806
|
endpoint.responses = updates.responses;
|
|
268807
|
+
if (updates.pagination !== void 0)
|
|
268808
|
+
endpoint.pagination = updates.pagination;
|
|
268809
|
+
if (updates.rateLimit !== void 0)
|
|
268810
|
+
endpoint.rateLimit = updates.rateLimit;
|
|
268811
|
+
if (updates.ownership !== void 0)
|
|
268812
|
+
endpoint.ownership = updates.ownership;
|
|
268813
|
+
if (updates.produces !== void 0)
|
|
268814
|
+
endpoint.produces = updates.produces;
|
|
268815
|
+
if (updates.consumes !== void 0)
|
|
268816
|
+
endpoint.consumes = updates.consumes;
|
|
268687
268817
|
(0, types_1.saveModel)(filePath, model);
|
|
268688
268818
|
return {
|
|
268689
268819
|
success: true,
|
|
@@ -269294,6 +269424,806 @@ var require_workflow_tools = __commonJS({
|
|
|
269294
269424
|
}
|
|
269295
269425
|
});
|
|
269296
269426
|
|
|
269427
|
+
// ../../packages/mcp-server/dist/tools/workflow-authoring-tools.js
|
|
269428
|
+
var require_workflow_authoring_tools = __commonJS({
|
|
269429
|
+
"../../packages/mcp-server/dist/tools/workflow-authoring-tools.js"(exports2) {
|
|
269430
|
+
"use strict";
|
|
269431
|
+
init_cjs_shims();
|
|
269432
|
+
var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
269433
|
+
if (k2 === void 0) k2 = k;
|
|
269434
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
269435
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
269436
|
+
desc = { enumerable: true, get: function() {
|
|
269437
|
+
return m[k];
|
|
269438
|
+
} };
|
|
269439
|
+
}
|
|
269440
|
+
Object.defineProperty(o, k2, desc);
|
|
269441
|
+
}) : (function(o, m, k, k2) {
|
|
269442
|
+
if (k2 === void 0) k2 = k;
|
|
269443
|
+
o[k2] = m[k];
|
|
269444
|
+
}));
|
|
269445
|
+
var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
|
|
269446
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
269447
|
+
}) : function(o, v) {
|
|
269448
|
+
o["default"] = v;
|
|
269449
|
+
});
|
|
269450
|
+
var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
|
|
269451
|
+
var ownKeys = function(o) {
|
|
269452
|
+
ownKeys = Object.getOwnPropertyNames || function(o2) {
|
|
269453
|
+
var ar = [];
|
|
269454
|
+
for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
|
|
269455
|
+
return ar;
|
|
269456
|
+
};
|
|
269457
|
+
return ownKeys(o);
|
|
269458
|
+
};
|
|
269459
|
+
return function(mod) {
|
|
269460
|
+
if (mod && mod.__esModule) return mod;
|
|
269461
|
+
var result = {};
|
|
269462
|
+
if (mod != null) {
|
|
269463
|
+
for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
269464
|
+
}
|
|
269465
|
+
__setModuleDefault(result, mod);
|
|
269466
|
+
return result;
|
|
269467
|
+
};
|
|
269468
|
+
})();
|
|
269469
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
269470
|
+
exports2.deleteWorkflowSchema = exports2.updateWorkflowSchema = exports2.createWorkflowSchema = exports2.validateWorkflowSchema = exports2.workflowStepSchema = void 0;
|
|
269471
|
+
exports2.validateWorkflowDefinition = validateWorkflowDefinition;
|
|
269472
|
+
exports2.slugifyWorkflowName = slugifyWorkflowName;
|
|
269473
|
+
exports2.handleValidateWorkflow = handleValidateWorkflow;
|
|
269474
|
+
exports2.handleCreateWorkflow = handleCreateWorkflow;
|
|
269475
|
+
exports2.handleUpdateWorkflow = handleUpdateWorkflow;
|
|
269476
|
+
exports2.handleDeleteWorkflow = handleDeleteWorkflow;
|
|
269477
|
+
var zod_1 = require("zod");
|
|
269478
|
+
var fs4 = __importStar(require("fs"));
|
|
269479
|
+
var path7 = __importStar(require("path"));
|
|
269480
|
+
var types_1 = require_types();
|
|
269481
|
+
var workflow_tools_1 = require_workflow_tools();
|
|
269482
|
+
exports2.workflowStepSchema = zod_1.z.object({
|
|
269483
|
+
name: zod_1.z.string().min(1).describe("Step name (must be unique within the workflow)"),
|
|
269484
|
+
method: zod_1.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]).describe("HTTP method"),
|
|
269485
|
+
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)"),
|
|
269486
|
+
description: zod_1.z.string().optional().describe("What this step does"),
|
|
269487
|
+
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)"),
|
|
269488
|
+
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)'),
|
|
269489
|
+
capture: zod_1.z.record(zod_1.z.string()).optional().describe('Save RESPONSE fields to memory: "scope:key" -> "dot.path.in.response"'),
|
|
269490
|
+
captureInput: zod_1.z.record(zod_1.z.string()).optional().describe('Save REQUEST body fields to memory BEFORE sending: "scope:key" -> "bodyField"'),
|
|
269491
|
+
expectStatus: zod_1.z.number().int().positive().optional().describe("Expected HTTP status (default 200; any 2xx passes when expecting 2xx)")
|
|
269492
|
+
// NOTE: .passthrough() is intentional — unknown keys must REACH the validator
|
|
269493
|
+
// (MODEL_DIALECT_KEY / UNKNOWN_STEP_KEY diagnostics). Zod's default strip
|
|
269494
|
+
// behavior would silently swallow the exact mistakes we need to report.
|
|
269495
|
+
}).passthrough();
|
|
269496
|
+
var inlineWorkflowSchema = zod_1.z.object({
|
|
269497
|
+
name: zod_1.z.string().min(1).describe("Workflow name (also used to derive the file slug)"),
|
|
269498
|
+
description: zod_1.z.string().optional(),
|
|
269499
|
+
environment: zod_1.z.string().optional().describe("Target environment name from the model (e.g. local)"),
|
|
269500
|
+
steps: zod_1.z.array(exports2.workflowStepSchema).min(1).describe("Ordered steps; later steps can use memory captured by earlier ones")
|
|
269501
|
+
}).passthrough();
|
|
269502
|
+
var TEMPLATE_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
269503
|
+
var KNOWN_WORKFLOW_KEYS = /* @__PURE__ */ new Set(["name", "description", "environment", "steps", "generatedAt"]);
|
|
269504
|
+
var KNOWN_STEP_KEYS = /* @__PURE__ */ new Set([
|
|
269505
|
+
"name",
|
|
269506
|
+
"method",
|
|
269507
|
+
"path",
|
|
269508
|
+
"description",
|
|
269509
|
+
"inject",
|
|
269510
|
+
"capture",
|
|
269511
|
+
"captureInput",
|
|
269512
|
+
"expectStatus",
|
|
269513
|
+
"body",
|
|
269514
|
+
"condition"
|
|
269515
|
+
]);
|
|
269516
|
+
var MODEL_DIALECT_KEYS = /* @__PURE__ */ new Set(["call", "auth", "bind"]);
|
|
269517
|
+
function err(list, stepIndex, stepName, code, message, fix) {
|
|
269518
|
+
list.push({ severity: "error", stepIndex, stepName, code, message, fix });
|
|
269519
|
+
}
|
|
269520
|
+
function warn(list, stepIndex, stepName, code, message, fix) {
|
|
269521
|
+
list.push({ severity: "warning", stepIndex, stepName, code, message, fix });
|
|
269522
|
+
}
|
|
269523
|
+
function splitMemKey(raw) {
|
|
269524
|
+
const idx = raw.indexOf(":");
|
|
269525
|
+
if (idx <= 0)
|
|
269526
|
+
return { scope: "workflow", key: raw };
|
|
269527
|
+
return { scope: raw.slice(0, idx), key: raw.slice(idx + 1) };
|
|
269528
|
+
}
|
|
269529
|
+
function readMemoryKeys(projectRoot) {
|
|
269530
|
+
const keys = /* @__PURE__ */ new Set();
|
|
269531
|
+
try {
|
|
269532
|
+
const memPath = path7.join(projectRoot, ".jetic", "memory.json");
|
|
269533
|
+
if (!fs4.existsSync(memPath))
|
|
269534
|
+
return keys;
|
|
269535
|
+
const data = JSON.parse(fs4.readFileSync(memPath, "utf-8"));
|
|
269536
|
+
for (const [scope, entries] of Object.entries(data || {})) {
|
|
269537
|
+
if (!entries || typeof entries !== "object")
|
|
269538
|
+
continue;
|
|
269539
|
+
for (const key of Object.keys(entries))
|
|
269540
|
+
keys.add(`${scope}:${key}`);
|
|
269541
|
+
}
|
|
269542
|
+
} catch {
|
|
269543
|
+
}
|
|
269544
|
+
return keys;
|
|
269545
|
+
}
|
|
269546
|
+
function collectTemplateRefs(value, out) {
|
|
269547
|
+
if (typeof value === "string") {
|
|
269548
|
+
TEMPLATE_RE.lastIndex = 0;
|
|
269549
|
+
let m;
|
|
269550
|
+
while ((m = TEMPLATE_RE.exec(value)) !== null)
|
|
269551
|
+
out.push(m[1].trim());
|
|
269552
|
+
} else if (Array.isArray(value)) {
|
|
269553
|
+
for (const v of value)
|
|
269554
|
+
collectTemplateRefs(v, out);
|
|
269555
|
+
} else if (value && typeof value === "object") {
|
|
269556
|
+
for (const v of Object.values(value))
|
|
269557
|
+
collectTemplateRefs(v, out);
|
|
269558
|
+
}
|
|
269559
|
+
}
|
|
269560
|
+
function suggestEndpoints(model, method, wfPath) {
|
|
269561
|
+
const samePath = model.filter((e) => e.path === wfPath).map((e) => `${e.method} ${e.path}`);
|
|
269562
|
+
if (samePath.length > 0)
|
|
269563
|
+
return samePath.slice(0, 3);
|
|
269564
|
+
const prefix = wfPath.split("/").slice(0, 3).join("/");
|
|
269565
|
+
return model.filter((e) => e.path.startsWith(prefix)).map((e) => `${e.method} ${e.path}`).slice(0, 3);
|
|
269566
|
+
}
|
|
269567
|
+
function validateWorkflowDefinition(wf, opts) {
|
|
269568
|
+
const errors = [];
|
|
269569
|
+
const warnings = [];
|
|
269570
|
+
const { model } = (0, types_1.loadModel)(opts?.projectPath);
|
|
269571
|
+
const projectRoot = (0, types_1.projectRootOfModel)((0, types_1.resolveModelPath)(opts?.projectPath));
|
|
269572
|
+
const persistedMemory = readMemoryKeys(projectRoot);
|
|
269573
|
+
if (!wf || typeof wf !== "object") {
|
|
269574
|
+
err(errors, void 0, void 0, "NOT_AN_OBJECT", "Workflow must be an object with name + steps.", "Pass { name, steps: [...] }.");
|
|
269575
|
+
return toReport(errors, warnings);
|
|
269576
|
+
}
|
|
269577
|
+
if (!wf.name || typeof wf.name !== "string" || !wf.name.trim()) {
|
|
269578
|
+
err(errors, void 0, void 0, "MISSING_NAME", 'Workflow needs a non-empty "name".', 'Set a descriptive name like "User onboarding".');
|
|
269579
|
+
}
|
|
269580
|
+
if (!Array.isArray(wf.steps) || wf.steps.length === 0) {
|
|
269581
|
+
err(errors, void 0, void 0, "NO_STEPS", "Workflow needs at least one step.", "Add steps with { name, method, path }.");
|
|
269582
|
+
return toReport(errors, warnings);
|
|
269583
|
+
}
|
|
269584
|
+
if (wf.environment && !(model.environments || []).some((e) => e.name === wf.environment)) {
|
|
269585
|
+
warn(warnings, void 0, void 0, "UNKNOWN_ENVIRONMENT", `Environment "${wf.environment}" is not declared in model.json.`, `Use one of: ${(model.environments || []).map((e) => e.name).join(", ") || "(none declared)"}, or add it with jetic_manage_environment.`);
|
|
269586
|
+
}
|
|
269587
|
+
for (const key of Object.keys(wf)) {
|
|
269588
|
+
if (!KNOWN_WORKFLOW_KEYS.has(key)) {
|
|
269589
|
+
warn(warnings, void 0, void 0, "UNKNOWN_WORKFLOW_KEY", `Top-level key "${key}" is not part of the file-workflow format and will be ignored.`, "Valid keys: name, description, environment, steps.");
|
|
269590
|
+
}
|
|
269591
|
+
}
|
|
269592
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
269593
|
+
const defined = /* @__PURE__ */ new Set();
|
|
269594
|
+
const humanKeys = /* @__PURE__ */ new Set();
|
|
269595
|
+
wf.steps.forEach((rawStep, i) => {
|
|
269596
|
+
const stepNo = i + 1;
|
|
269597
|
+
const sName = typeof rawStep?.name === "string" ? rawStep.name : void 0;
|
|
269598
|
+
if (!rawStep || typeof rawStep !== "object" || Array.isArray(rawStep)) {
|
|
269599
|
+
err(errors, stepNo, void 0, "STEP_NOT_AN_OBJECT", `Step ${stepNo} must be an object.`, "Use { name, method, path, ... }.");
|
|
269600
|
+
return;
|
|
269601
|
+
}
|
|
269602
|
+
const step = rawStep;
|
|
269603
|
+
for (const key of Object.keys(step)) {
|
|
269604
|
+
if (MODEL_DIALECT_KEYS.has(key)) {
|
|
269605
|
+
err(errors, stepNo, sName, "MODEL_DIALECT_KEY", `Step ${stepNo} uses "${key}" from the model-embedded workflow dialect, which file workflows ignore.`, 'File-workflow equivalent: split "call" into method+path; replace auth/bind with inject/capture.');
|
|
269606
|
+
} else if (!KNOWN_STEP_KEYS.has(key)) {
|
|
269607
|
+
warn(warnings, stepNo, sName, "UNKNOWN_STEP_KEY", `Step ${stepNo} has unknown key "${key}" which the simulator ignores.`, "Remove it or check the field name (valid: name, method, path, description, body, inject, capture, captureInput, expectStatus).");
|
|
269608
|
+
}
|
|
269609
|
+
}
|
|
269610
|
+
if ("condition" in step) {
|
|
269611
|
+
warn(warnings, stepNo, sName, "CONDITION_IGNORED", `"condition" is accepted but currently ignored by the simulator \u2014 the step always runs.`, "Encode branching as separate workflows, or gate with expectStatus per step.");
|
|
269612
|
+
}
|
|
269613
|
+
if (!sName || !sName.trim()) {
|
|
269614
|
+
err(errors, stepNo, void 0, "STEP_MISSING_NAME", `Step ${stepNo} needs a non-empty "name".`, "Give every step a unique descriptive name.");
|
|
269615
|
+
} else if (seenNames.has(sName)) {
|
|
269616
|
+
warn(warnings, stepNo, sName, "DUPLICATE_STEP_NAME", `Duplicate step name "${sName}".`, "Rename steps so logs stay readable.");
|
|
269617
|
+
} else {
|
|
269618
|
+
seenNames.add(sName);
|
|
269619
|
+
}
|
|
269620
|
+
const method = typeof step.method === "string" ? step.method.toUpperCase() : void 0;
|
|
269621
|
+
const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
|
|
269622
|
+
if (!method || !validMethods.includes(method)) {
|
|
269623
|
+
err(errors, stepNo, sName, "BAD_METHOD", `Step ${stepNo} has invalid method ${JSON.stringify(step.method)}.`, `Use one of: ${validMethods.join(", ")}.`);
|
|
269624
|
+
return;
|
|
269625
|
+
}
|
|
269626
|
+
if (typeof step.path !== "string" || !step.path.trim()) {
|
|
269627
|
+
err(errors, stepNo, sName, "BAD_PATH", `Step ${stepNo} needs a "path" string.`, 'Use the route path, e.g. "/api/users/:id".');
|
|
269628
|
+
return;
|
|
269629
|
+
}
|
|
269630
|
+
const wfPath = step.path.startsWith("/") ? step.path : `/${step.path}`;
|
|
269631
|
+
const endpoint = (model.endpoints || []).find((e) => e.method.toUpperCase() === method && e.path === wfPath);
|
|
269632
|
+
if (!endpoint) {
|
|
269633
|
+
const suggestions = suggestEndpoints(model.endpoints || [], method, wfPath);
|
|
269634
|
+
const msg = `Step ${stepNo} (${method} ${wfPath}) matches no endpoint in model.json.` + (suggestions.length > 0 ? ` Did you mean: ${suggestions.join(" | ")}?` : "");
|
|
269635
|
+
if (opts?.allowUnknownEndpoints) {
|
|
269636
|
+
warn(warnings, stepNo, sName, "UNKNOWN_ENDPOINT", msg, "Add the endpoint with jetic_add_endpoint so future runs stay verifiable.");
|
|
269637
|
+
} else {
|
|
269638
|
+
err(errors, stepNo, sName, "UNKNOWN_ENDPOINT", msg, "Add it with jetic_add_endpoint, fix the method/path, or re-run with allowUnknownEndpoints.");
|
|
269639
|
+
}
|
|
269640
|
+
} else if (step.expectStatus !== void 0) {
|
|
269641
|
+
const codes = Object.keys(endpoint.responses || {});
|
|
269642
|
+
const exp = Number(step.expectStatus);
|
|
269643
|
+
const in2xx = exp >= 200 && exp < 300;
|
|
269644
|
+
const modelHas2xx = codes.some((c3) => Number(c3) >= 200 && Number(c3) < 300);
|
|
269645
|
+
if (!codes.includes(String(exp)) && !(in2xx && modelHas2xx)) {
|
|
269646
|
+
warn(warnings, stepNo, sName, "UNEXPECTED_STATUS", `Step ${stepNo} expects ${exp} but the model declares [${codes.join(", ") || "none"}] for ${method} ${wfPath}.`, "Align expectStatus with the model, or update the endpoint responses.");
|
|
269647
|
+
}
|
|
269648
|
+
}
|
|
269649
|
+
if (step.body !== void 0 && (typeof step.body !== "object" || step.body === null || Array.isArray(step.body))) {
|
|
269650
|
+
err(errors, stepNo, sName, "BAD_BODY", `Step ${stepNo} "body" must be an object of field -> value.`, 'Use { "email": "{{faker.internet.email}}" }.');
|
|
269651
|
+
}
|
|
269652
|
+
if ((method === "GET" || method === "HEAD") && step.body && Object.keys(step.body).length > 0) {
|
|
269653
|
+
warn(warnings, stepNo, sName, "GET_WITH_BODY", `Step ${stepNo} is ${method} with a body \u2014 it will be sent as URL query params, not a JSON body.`, "This is usually what you want for filters; otherwise switch to POST.");
|
|
269654
|
+
}
|
|
269655
|
+
if (endpoint?.requestBody?.fields && step.body && typeof step.body === "object") {
|
|
269656
|
+
for (const field of Object.keys(step.body)) {
|
|
269657
|
+
if (!(field in endpoint.requestBody.fields)) {
|
|
269658
|
+
warn(warnings, stepNo, sName, "UNKNOWN_BODY_FIELD", `Step ${stepNo} sends body field "${field}" not declared on ${method} ${wfPath}.`, "Check the spelling, or extend the endpoint with jetic_update_endpoint.");
|
|
269659
|
+
}
|
|
269660
|
+
}
|
|
269661
|
+
}
|
|
269662
|
+
const pathParams = wfPath.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g) || [];
|
|
269663
|
+
for (const p of pathParams) {
|
|
269664
|
+
const pname = p.substring(1);
|
|
269665
|
+
const inBody = step.body && typeof step.body === "object" && pname in step.body;
|
|
269666
|
+
const inMemory = [...defined].some((d) => d.split(":")[1] === pname) || [...persistedMemory].some((d) => d.split(":")[1] === pname);
|
|
269667
|
+
if (!inBody && !inMemory) {
|
|
269668
|
+
err(errors, stepNo, sName, "UNRESOLVED_PATH_PARAM", `Path param ":${pname}" in step ${stepNo} has no body field or memory source \u2014 the URL will literally contain ":${pname}" and fail.`, `Add "${pname}" to this step's body, or capture it as memory in an earlier step (capture: { "workflow:${pname}": "data.${pname}" }).`);
|
|
269669
|
+
}
|
|
269670
|
+
}
|
|
269671
|
+
if (/\{\{[^}]+\}\}/.test(wfPath)) {
|
|
269672
|
+
warn(warnings, stepNo, sName, "PATH_TEMPLATE", `Step ${stepNo} path contains a {{template}} \u2014 paths only resolve :params, templates work in body/inject.`, 'Use "/api/users/:id" with an "id" body field or captured memory key.');
|
|
269673
|
+
}
|
|
269674
|
+
const refs = [];
|
|
269675
|
+
collectTemplateRefs(step.body, refs);
|
|
269676
|
+
if (step.inject && typeof step.inject === "object") {
|
|
269677
|
+
for (const v of Object.values(step.inject))
|
|
269678
|
+
collectTemplateRefs(v, refs);
|
|
269679
|
+
}
|
|
269680
|
+
for (const ref of refs) {
|
|
269681
|
+
if (ref.startsWith("faker.")) {
|
|
269682
|
+
if (ref.split(".").length < 3) {
|
|
269683
|
+
warn(warnings, stepNo, sName, "SUSPICIOUS_FAKER", `"{{${ref}}}" looks like an incomplete faker path.`, "Use full paths like {{faker.internet.email}} or {{faker.string.uuid}}.");
|
|
269684
|
+
}
|
|
269685
|
+
continue;
|
|
269686
|
+
}
|
|
269687
|
+
if (!ref.includes(":")) {
|
|
269688
|
+
err(errors, stepNo, sName, "BAD_TEMPLATE", `"{{${ref}}}" in step ${stepNo} needs a scope ("{{workflow:key}}", "{{scope:key}}") or faker ("{{faker.x.y}}").`, "Bare {{key}} is left literal \u2014 add the scope prefix.");
|
|
269689
|
+
continue;
|
|
269690
|
+
}
|
|
269691
|
+
const { scope, key } = splitMemKey(ref);
|
|
269692
|
+
if (!key) {
|
|
269693
|
+
err(errors, stepNo, sName, "BAD_TEMPLATE", `"{{${ref}}}" has an empty key.`, "Use {{scope:key}} with a real key.");
|
|
269694
|
+
continue;
|
|
269695
|
+
}
|
|
269696
|
+
if (scope === "human") {
|
|
269697
|
+
if (key)
|
|
269698
|
+
humanKeys.add(key);
|
|
269699
|
+
continue;
|
|
269700
|
+
}
|
|
269701
|
+
const full = `${scope}:${key}`;
|
|
269702
|
+
if (!defined.has(full) && !persistedMemory.has(full)) {
|
|
269703
|
+
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.`);
|
|
269704
|
+
} else if (!defined.has(full)) {
|
|
269705
|
+
warn(warnings, stepNo, sName, "MEMORY_FILE_DEPENDENT", `"{{${ref}}}" in step ${stepNo} only exists in the current memory.json, not from an earlier step \u2014 reruns after memory clear will break.`, `Capture "${full}" inside this workflow instead.`);
|
|
269706
|
+
}
|
|
269707
|
+
}
|
|
269708
|
+
if (step.inject !== void 0) {
|
|
269709
|
+
if (typeof step.inject !== "object" || step.inject === null || Array.isArray(step.inject)) {
|
|
269710
|
+
err(errors, stepNo, sName, "BAD_INJECT", `Step ${stepNo} "inject" must be an object.`, 'Use { "header:Authorization": "Bearer {{workflow:accessToken}}" }.');
|
|
269711
|
+
} else {
|
|
269712
|
+
for (const [target, memRef] of Object.entries(step.inject)) {
|
|
269713
|
+
if (!target.trim() || typeof memRef !== "string" || !memRef.trim()) {
|
|
269714
|
+
err(errors, stepNo, sName, "BAD_INJECT", `Step ${stepNo} has an empty inject target or value.`, 'Use "header:Name"/"body:field" -> "scope:key".');
|
|
269715
|
+
}
|
|
269716
|
+
}
|
|
269717
|
+
}
|
|
269718
|
+
}
|
|
269719
|
+
for (const field of ["capture", "captureInput"]) {
|
|
269720
|
+
const cap = step[field];
|
|
269721
|
+
if (cap === void 0)
|
|
269722
|
+
continue;
|
|
269723
|
+
if (typeof cap !== "object" || cap === null || Array.isArray(cap)) {
|
|
269724
|
+
err(errors, stepNo, sName, "BAD_CAPTURE", `Step ${stepNo} "${field}" must be an object.`, `Use { "workflow:myVar": "${field === "capture" ? "data.id" : "bodyField"}" }.`);
|
|
269725
|
+
continue;
|
|
269726
|
+
}
|
|
269727
|
+
for (const [memKey, srcPath] of Object.entries(cap)) {
|
|
269728
|
+
if (!memKey.trim() || typeof srcPath !== "string" || !srcPath.trim()) {
|
|
269729
|
+
err(errors, stepNo, sName, "BAD_CAPTURE", `Step ${stepNo} "${field}" has an empty key or path.`, 'Use { "workflow:myVar": "data.id" }.');
|
|
269730
|
+
continue;
|
|
269731
|
+
}
|
|
269732
|
+
if (field === "captureInput" && (!step.body || typeof step.body !== "object" || !(srcPath in step.body))) {
|
|
269733
|
+
warn(warnings, stepNo, sName, "CAPTURE_INPUT_MISSING_FIELD", `captureInput "${memKey}" reads body field "${srcPath}" which step ${stepNo} does not send.`, 'Add the field to body, or capture from the response with "capture" instead.');
|
|
269734
|
+
}
|
|
269735
|
+
}
|
|
269736
|
+
}
|
|
269737
|
+
for (const field of ["capture", "captureInput"]) {
|
|
269738
|
+
const cap = step[field];
|
|
269739
|
+
if (cap && typeof cap === "object" && !Array.isArray(cap)) {
|
|
269740
|
+
for (const memKey of Object.keys(cap)) {
|
|
269741
|
+
const { scope, key } = splitMemKey(memKey);
|
|
269742
|
+
if (key)
|
|
269743
|
+
defined.add(`${scope}:${key}`);
|
|
269744
|
+
}
|
|
269745
|
+
}
|
|
269746
|
+
}
|
|
269747
|
+
});
|
|
269748
|
+
return toReport(errors, warnings, humanKeys);
|
|
269749
|
+
}
|
|
269750
|
+
function toReport(errors, warnings, humanKeys) {
|
|
269751
|
+
return {
|
|
269752
|
+
valid: errors.length === 0,
|
|
269753
|
+
errorsCount: errors.length,
|
|
269754
|
+
warningsCount: warnings.length,
|
|
269755
|
+
errors,
|
|
269756
|
+
warnings,
|
|
269757
|
+
humanKeys: [...humanKeys || []].sort()
|
|
269758
|
+
};
|
|
269759
|
+
}
|
|
269760
|
+
function slugifyWorkflowName(name) {
|
|
269761
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
269762
|
+
return slug || "workflow";
|
|
269763
|
+
}
|
|
269764
|
+
function workflowsDir(projectPath) {
|
|
269765
|
+
return path7.join(path7.dirname((0, types_1.resolveModelPath)(projectPath)), "workflows");
|
|
269766
|
+
}
|
|
269767
|
+
function workflowFilePath(projectPath, slug) {
|
|
269768
|
+
return path7.join(workflowsDir(projectPath), `${slug}.json`);
|
|
269769
|
+
}
|
|
269770
|
+
function resolveFileWorkflow(projectPath, ref) {
|
|
269771
|
+
const listing = (0, workflow_tools_1.handleListWorkflows)({ projectPath });
|
|
269772
|
+
const search = ref.toLowerCase();
|
|
269773
|
+
const item = listing.workflows.find((w) => w.id.toLowerCase() === search || w.name.toLowerCase() === search || w.filePath && path7.resolve(w.filePath).toLowerCase() === path7.resolve(ref).toLowerCase());
|
|
269774
|
+
if (!item) {
|
|
269775
|
+
throw new Error(`Workflow "${ref}" not found. Available: ${listing.workflows.map((w) => w.id).join(", ") || "(none)"}.`);
|
|
269776
|
+
}
|
|
269777
|
+
if (item.source === "model_json" || !item.filePath) {
|
|
269778
|
+
throw new Error(`Workflow "${ref}" lives inside model.json, which this tool cannot edit. Migrate it with jetic_create_workflow { fromModelWorkflow: "${item.name}" } first.`);
|
|
269779
|
+
}
|
|
269780
|
+
let def;
|
|
269781
|
+
try {
|
|
269782
|
+
def = JSON.parse(fs4.readFileSync(item.filePath, "utf-8"));
|
|
269783
|
+
} catch (e) {
|
|
269784
|
+
throw new Error(`Cannot read workflow file at ${item.filePath}: ${e?.message || e}`);
|
|
269785
|
+
}
|
|
269786
|
+
return { slug: item.id, filePath: item.filePath, def };
|
|
269787
|
+
}
|
|
269788
|
+
function migrateModelWorkflow(modelWorkflow) {
|
|
269789
|
+
const notes = [];
|
|
269790
|
+
const steps = (modelWorkflow.steps || []).map((s, i) => {
|
|
269791
|
+
let method = "GET";
|
|
269792
|
+
let reqPath = "/";
|
|
269793
|
+
if (typeof s.call === "string" && s.call.includes(" ")) {
|
|
269794
|
+
const parts = s.call.split(" ");
|
|
269795
|
+
method = parts[0].toUpperCase();
|
|
269796
|
+
reqPath = parts.slice(1).join(" ");
|
|
269797
|
+
} else if (typeof s.call === "string") {
|
|
269798
|
+
reqPath = s.call;
|
|
269799
|
+
}
|
|
269800
|
+
if (s.auth)
|
|
269801
|
+
notes.push(`Step ${i + 1}: model "auth: ${s.auth}" has no file equivalent \u2014 express it with inject (header:Authorization).`);
|
|
269802
|
+
if (s.bind)
|
|
269803
|
+
notes.push(`Step ${i + 1}: model "bind" has no file equivalent \u2014 express it with capture/captureInput.`);
|
|
269804
|
+
return {
|
|
269805
|
+
name: s.description || `${method} ${reqPath}`,
|
|
269806
|
+
method,
|
|
269807
|
+
path: reqPath,
|
|
269808
|
+
description: s.description,
|
|
269809
|
+
body: s.body,
|
|
269810
|
+
expectStatus: s.expectStatus
|
|
269811
|
+
};
|
|
269812
|
+
});
|
|
269813
|
+
return { def: { name: modelWorkflow.name, description: modelWorkflow.description, steps }, notes };
|
|
269814
|
+
}
|
|
269815
|
+
function writeWorkflowFile(filePath, def) {
|
|
269816
|
+
fs4.mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
269817
|
+
const payload = { ...def, generatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
269818
|
+
const tmp = `${filePath}.tmp`;
|
|
269819
|
+
fs4.writeFileSync(tmp, JSON.stringify(payload, null, 2), "utf-8");
|
|
269820
|
+
fs4.renameSync(tmp, filePath);
|
|
269821
|
+
}
|
|
269822
|
+
exports2.validateWorkflowSchema = zod_1.z.object({
|
|
269823
|
+
projectPath: zod_1.z.string().optional().describe("Path to project root or .jetic folder"),
|
|
269824
|
+
workflow: zod_1.z.string().optional().describe('Workflow slug, name, or file path to validate (omit when passing inline "definition")'),
|
|
269825
|
+
definition: inlineWorkflowSchema.optional().describe("Inline workflow to dry-run validate WITHOUT saving (for pre-save checks)"),
|
|
269826
|
+
allowUnknownEndpoints: zod_1.z.boolean().optional().default(false).describe("Downgrade unknown-endpoint errors to warnings (endpoint exists on server but not in model yet)")
|
|
269827
|
+
});
|
|
269828
|
+
function handleValidateWorkflow(args) {
|
|
269829
|
+
if (!args.workflow && !args.definition) {
|
|
269830
|
+
throw new Error('Provide either "workflow" (slug/name/file) or an inline "definition" to validate.');
|
|
269831
|
+
}
|
|
269832
|
+
let def;
|
|
269833
|
+
let filePath;
|
|
269834
|
+
if (args.definition) {
|
|
269835
|
+
def = args.definition;
|
|
269836
|
+
} else {
|
|
269837
|
+
const resolved = resolveFileWorkflow(args.projectPath, args.workflow);
|
|
269838
|
+
def = resolved.def;
|
|
269839
|
+
filePath = resolved.filePath;
|
|
269840
|
+
}
|
|
269841
|
+
const report = validateWorkflowDefinition(def, {
|
|
269842
|
+
projectPath: args.projectPath,
|
|
269843
|
+
allowUnknownEndpoints: args.allowUnknownEndpoints === true
|
|
269844
|
+
});
|
|
269845
|
+
return { ...filePath ? { filePath } : { saved: false }, ...report };
|
|
269846
|
+
}
|
|
269847
|
+
exports2.createWorkflowSchema = zod_1.z.object({
|
|
269848
|
+
projectPath: zod_1.z.string().optional().describe("Path to project root or .jetic folder"),
|
|
269849
|
+
name: zod_1.z.string().min(1).optional().describe("Workflow name (required unless fromModelWorkflow is used)"),
|
|
269850
|
+
slug: zod_1.z.string().optional().describe('File slug override (default: derived from name, e.g. "User Onboarding" -> "user-onboarding")'),
|
|
269851
|
+
description: zod_1.z.string().optional(),
|
|
269852
|
+
environment: zod_1.z.string().optional().describe("Target environment name from the model (e.g. local)"),
|
|
269853
|
+
steps: zod_1.z.array(exports2.workflowStepSchema).min(1).optional().describe("Ordered steps (required unless fromModelWorkflow is used)"),
|
|
269854
|
+
fromModelWorkflow: zod_1.z.string().optional().describe("Migrate an embedded model.json workflow by name instead of authoring steps manually"),
|
|
269855
|
+
overwrite: zod_1.z.boolean().optional().default(false).describe("Overwrite the file if the slug already exists (default false \u2014 safe creation)"),
|
|
269856
|
+
validateOnly: zod_1.z.boolean().optional().default(false).describe("Only validate, do not write any file (dry run)"),
|
|
269857
|
+
allowUnknownEndpoints: zod_1.z.boolean().optional().default(false).describe("Save even when steps reference endpoints missing from model.json (recorded as warnings)")
|
|
269858
|
+
});
|
|
269859
|
+
function handleCreateWorkflow(args) {
|
|
269860
|
+
let def;
|
|
269861
|
+
const migrationNotes = [];
|
|
269862
|
+
if (args.fromModelWorkflow) {
|
|
269863
|
+
const { model } = (0, types_1.loadModel)(args.projectPath);
|
|
269864
|
+
const raw = (model.workflows || []).find((w) => w.name === args.fromModelWorkflow);
|
|
269865
|
+
if (!raw) {
|
|
269866
|
+
throw new Error(`Embedded workflow "${args.fromModelWorkflow}" not found in model.json. Available: ${(model.workflows || []).map((w) => w.name).join(", ") || "(none)"}.`);
|
|
269867
|
+
}
|
|
269868
|
+
const migrated = migrateModelWorkflow(raw);
|
|
269869
|
+
def = {
|
|
269870
|
+
...args.name ? { name: args.name } : {},
|
|
269871
|
+
...migrated.def,
|
|
269872
|
+
...args.description ? { description: args.description } : {},
|
|
269873
|
+
...args.environment ? { environment: args.environment } : {},
|
|
269874
|
+
...args.steps ? { steps: args.steps } : {}
|
|
269875
|
+
};
|
|
269876
|
+
migrationNotes.push(...migrated.notes);
|
|
269877
|
+
} else {
|
|
269878
|
+
if (!args.name)
|
|
269879
|
+
throw new Error('"name" is required to create a workflow (or use fromModelWorkflow to migrate one).');
|
|
269880
|
+
if (!args.steps || args.steps.length === 0) {
|
|
269881
|
+
throw new Error('"steps" with at least one step is required (or use fromModelWorkflow to migrate one).');
|
|
269882
|
+
}
|
|
269883
|
+
def = {
|
|
269884
|
+
name: args.name,
|
|
269885
|
+
...args.description ? { description: args.description } : {},
|
|
269886
|
+
...args.environment ? { environment: args.environment } : {},
|
|
269887
|
+
steps: args.steps
|
|
269888
|
+
};
|
|
269889
|
+
}
|
|
269890
|
+
const report = validateWorkflowDefinition(def, {
|
|
269891
|
+
projectPath: args.projectPath,
|
|
269892
|
+
allowUnknownEndpoints: args.allowUnknownEndpoints === true
|
|
269893
|
+
});
|
|
269894
|
+
const warnings = [...report.warnings];
|
|
269895
|
+
for (const note of migrationNotes) {
|
|
269896
|
+
warnings.push({ severity: "warning", code: "MIGRATION_NOTE", message: note, fix: "Adjust the saved file with jetic_update_workflow." });
|
|
269897
|
+
}
|
|
269898
|
+
if (!report.valid) {
|
|
269899
|
+
throw new Error(`Workflow "${def.name}" has ${report.errorsCount} error(s) and was NOT saved:
|
|
269900
|
+
` + report.errors.map((e) => `- [step ${e.stepIndex ?? "-"}] ${e.message} Fix: ${e.fix}`).join("\n"));
|
|
269901
|
+
}
|
|
269902
|
+
const slug = args.slug?.trim() ? slugifyWorkflowName(args.slug) : slugifyWorkflowName(def.name);
|
|
269903
|
+
const filePath = workflowFilePath(args.projectPath, slug);
|
|
269904
|
+
const overwrite = args.overwrite === true;
|
|
269905
|
+
const validateOnly = args.validateOnly === true;
|
|
269906
|
+
if (validateOnly) {
|
|
269907
|
+
return {
|
|
269908
|
+
success: true,
|
|
269909
|
+
action: "validated",
|
|
269910
|
+
saved: false,
|
|
269911
|
+
slug,
|
|
269912
|
+
filePath,
|
|
269913
|
+
stepsCount: def.steps.length,
|
|
269914
|
+
needsHuman: report.humanKeys,
|
|
269915
|
+
validation: { ...report, warnings }
|
|
269916
|
+
};
|
|
269917
|
+
}
|
|
269918
|
+
if (fs4.existsSync(filePath) && !overwrite) {
|
|
269919
|
+
throw new Error(`Workflow file already exists at ${filePath}. Pass overwrite: true to replace it, use jetic_update_workflow to edit it, or pick another slug.`);
|
|
269920
|
+
}
|
|
269921
|
+
const existedBefore = fs4.existsSync(filePath);
|
|
269922
|
+
writeWorkflowFile(filePath, def);
|
|
269923
|
+
return {
|
|
269924
|
+
success: true,
|
|
269925
|
+
action: existedBefore ? "overwritten" : "created",
|
|
269926
|
+
slug,
|
|
269927
|
+
filePath,
|
|
269928
|
+
stepsCount: def.steps.length,
|
|
269929
|
+
needsHuman: report.humanKeys,
|
|
269930
|
+
...report.humanKeys.length > 0 ? {
|
|
269931
|
+
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.`
|
|
269932
|
+
} : {},
|
|
269933
|
+
validation: { ...report, warnings },
|
|
269934
|
+
next: `Run it with jetic_simulate_workflow { "workflow": "${slug}" }.`
|
|
269935
|
+
};
|
|
269936
|
+
}
|
|
269937
|
+
exports2.updateWorkflowSchema = zod_1.z.object({
|
|
269938
|
+
projectPath: zod_1.z.string().optional().describe("Path to project root or .jetic folder"),
|
|
269939
|
+
workflow: zod_1.z.string().describe("Workflow slug, name, or file path to update"),
|
|
269940
|
+
name: zod_1.z.string().min(1).optional().describe("Rename the workflow (file slug stays the same)"),
|
|
269941
|
+
description: zod_1.z.string().optional().describe("Replace the description"),
|
|
269942
|
+
environment: zod_1.z.string().optional().describe("Replace the target environment"),
|
|
269943
|
+
steps: zod_1.z.array(exports2.workflowStepSchema).min(1).optional().describe("REPLACE all steps (use appendSteps to only add)"),
|
|
269944
|
+
appendSteps: zod_1.z.array(exports2.workflowStepSchema).optional().describe("Append steps to the end (memory from existing steps stays usable)"),
|
|
269945
|
+
validateOnly: zod_1.z.boolean().optional().default(false).describe("Only validate the result, do not write"),
|
|
269946
|
+
allowUnknownEndpoints: zod_1.z.boolean().optional().default(false)
|
|
269947
|
+
});
|
|
269948
|
+
function handleUpdateWorkflow(args) {
|
|
269949
|
+
const resolved = resolveFileWorkflow(args.projectPath, args.workflow);
|
|
269950
|
+
const def = {
|
|
269951
|
+
...resolved.def,
|
|
269952
|
+
...args.name ? { name: args.name } : {},
|
|
269953
|
+
...args.description !== void 0 ? { description: args.description } : {},
|
|
269954
|
+
...args.environment !== void 0 ? { environment: args.environment } : {},
|
|
269955
|
+
steps: [
|
|
269956
|
+
...args.steps ? [] : resolved.def.steps || [],
|
|
269957
|
+
...args.steps || [],
|
|
269958
|
+
...args.appendSteps || []
|
|
269959
|
+
]
|
|
269960
|
+
};
|
|
269961
|
+
const report = validateWorkflowDefinition(def, {
|
|
269962
|
+
projectPath: args.projectPath,
|
|
269963
|
+
allowUnknownEndpoints: args.allowUnknownEndpoints === true
|
|
269964
|
+
});
|
|
269965
|
+
if (!report.valid) {
|
|
269966
|
+
throw new Error(`Updated workflow has ${report.errorsCount} error(s) and was NOT saved:
|
|
269967
|
+
` + report.errors.map((e) => `- [step ${e.stepIndex ?? "-"}] ${e.message} Fix: ${e.fix}`).join("\n"));
|
|
269968
|
+
}
|
|
269969
|
+
if (args.validateOnly === true) {
|
|
269970
|
+
return { success: true, action: "validated", saved: false, slug: resolved.slug, filePath: resolved.filePath, stepsCount: def.steps.length, needsHuman: report.humanKeys, validation: report };
|
|
269971
|
+
}
|
|
269972
|
+
writeWorkflowFile(resolved.filePath, def);
|
|
269973
|
+
return {
|
|
269974
|
+
success: true,
|
|
269975
|
+
action: "updated",
|
|
269976
|
+
slug: resolved.slug,
|
|
269977
|
+
filePath: resolved.filePath,
|
|
269978
|
+
stepsCount: def.steps.length,
|
|
269979
|
+
needsHuman: report.humanKeys,
|
|
269980
|
+
validation: report,
|
|
269981
|
+
next: `Run it with jetic_simulate_workflow { "workflow": "${resolved.slug}" }.`
|
|
269982
|
+
};
|
|
269983
|
+
}
|
|
269984
|
+
exports2.deleteWorkflowSchema = zod_1.z.object({
|
|
269985
|
+
projectPath: zod_1.z.string().optional().describe("Path to project root or .jetic folder"),
|
|
269986
|
+
workflow: zod_1.z.string().describe("Workflow slug, name, or file path to delete")
|
|
269987
|
+
});
|
|
269988
|
+
function handleDeleteWorkflow(args) {
|
|
269989
|
+
const listing = (0, workflow_tools_1.handleListWorkflows)({ projectPath: args.projectPath });
|
|
269990
|
+
const search = args.workflow.toLowerCase();
|
|
269991
|
+
const item = listing.workflows.find((w) => w.id.toLowerCase() === search || w.name.toLowerCase() === search || w.filePath && path7.resolve(w.filePath).toLowerCase() === path7.resolve(args.workflow).toLowerCase());
|
|
269992
|
+
if (!item) {
|
|
269993
|
+
return { success: false, message: `Workflow "${args.workflow}" not found.` };
|
|
269994
|
+
}
|
|
269995
|
+
if (item.source === "model_json" || !item.filePath) {
|
|
269996
|
+
throw new Error(`Workflow "${args.workflow}" is embedded in model.json and cannot be deleted by this tool. Remove it by editing .jetic/model.json directly.`);
|
|
269997
|
+
}
|
|
269998
|
+
fs4.rmSync(item.filePath, { force: true });
|
|
269999
|
+
return { success: true, action: "deleted", slug: item.id, filePath: item.filePath };
|
|
270000
|
+
}
|
|
270001
|
+
}
|
|
270002
|
+
});
|
|
270003
|
+
|
|
270004
|
+
// ../../packages/mcp-server/dist/tools/project-tools.js
|
|
270005
|
+
var require_project_tools = __commonJS({
|
|
270006
|
+
"../../packages/mcp-server/dist/tools/project-tools.js"(exports2) {
|
|
270007
|
+
"use strict";
|
|
270008
|
+
init_cjs_shims();
|
|
270009
|
+
var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
270010
|
+
if (k2 === void 0) k2 = k;
|
|
270011
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
270012
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
270013
|
+
desc = { enumerable: true, get: function() {
|
|
270014
|
+
return m[k];
|
|
270015
|
+
} };
|
|
270016
|
+
}
|
|
270017
|
+
Object.defineProperty(o, k2, desc);
|
|
270018
|
+
}) : (function(o, m, k, k2) {
|
|
270019
|
+
if (k2 === void 0) k2 = k;
|
|
270020
|
+
o[k2] = m[k];
|
|
270021
|
+
}));
|
|
270022
|
+
var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
|
|
270023
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
270024
|
+
}) : function(o, v) {
|
|
270025
|
+
o["default"] = v;
|
|
270026
|
+
});
|
|
270027
|
+
var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
|
|
270028
|
+
var ownKeys = function(o) {
|
|
270029
|
+
ownKeys = Object.getOwnPropertyNames || function(o2) {
|
|
270030
|
+
var ar = [];
|
|
270031
|
+
for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
|
|
270032
|
+
return ar;
|
|
270033
|
+
};
|
|
270034
|
+
return ownKeys(o);
|
|
270035
|
+
};
|
|
270036
|
+
return function(mod) {
|
|
270037
|
+
if (mod && mod.__esModule) return mod;
|
|
270038
|
+
var result = {};
|
|
270039
|
+
if (mod != null) {
|
|
270040
|
+
for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
270041
|
+
}
|
|
270042
|
+
__setModuleDefault(result, mod);
|
|
270043
|
+
return result;
|
|
270044
|
+
};
|
|
270045
|
+
})();
|
|
270046
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
270047
|
+
exports2.scanProjectSchema = exports2.initProjectSchema = void 0;
|
|
270048
|
+
exports2.handleInitProject = handleInitProject;
|
|
270049
|
+
exports2.handleScanProject = handleScanProject;
|
|
270050
|
+
var zod_1 = require("zod");
|
|
270051
|
+
var fs4 = __importStar(require("fs"));
|
|
270052
|
+
var path7 = __importStar(require("path"));
|
|
270053
|
+
var types_1 = require_types();
|
|
270054
|
+
var model_1 = require_dist2();
|
|
270055
|
+
exports2.initProjectSchema = zod_1.z.object({
|
|
270056
|
+
projectPath: zod_1.z.string().optional().describe("Path to the project root to initialize (defaults to JETIC_PROJECT_PATH / cwd)"),
|
|
270057
|
+
projectName: zod_1.z.string().optional().describe("Project name (default: directory basename)"),
|
|
270058
|
+
language: zod_1.z.string().optional().describe('Primary language (default: "typescript" when tsconfig.json exists, else omitted)'),
|
|
270059
|
+
framework: zod_1.z.string().optional().describe('Framework (default: "express" when express is a dependency, else omitted)'),
|
|
270060
|
+
environmentName: zod_1.z.string().optional().describe('First environment name (default "local")'),
|
|
270061
|
+
baseUrl: zod_1.z.string().optional().describe('First environment base URL (default "http://localhost:3000")'),
|
|
270062
|
+
overwrite: zod_1.z.boolean().optional().describe("Replace an existing .jetic/model.json (default false \u2014 never clobber silently)")
|
|
270063
|
+
});
|
|
270064
|
+
function detectStack(projectRoot) {
|
|
270065
|
+
let hasExpress = false;
|
|
270066
|
+
let hasPackageJson = false;
|
|
270067
|
+
try {
|
|
270068
|
+
const pkgPath = path7.join(projectRoot, "package.json");
|
|
270069
|
+
if (fs4.existsSync(pkgPath)) {
|
|
270070
|
+
hasPackageJson = true;
|
|
270071
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
270072
|
+
const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {}, ...pkg.peerDependencies || {} };
|
|
270073
|
+
hasExpress = "express" in deps;
|
|
270074
|
+
}
|
|
270075
|
+
} catch {
|
|
270076
|
+
}
|
|
270077
|
+
return { hasPackageJson, hasExpress, hasTsConfig: fs4.existsSync(path7.join(projectRoot, "tsconfig.json")) };
|
|
270078
|
+
}
|
|
270079
|
+
function handleInitProject(args) {
|
|
270080
|
+
const filePath = (0, types_1.resolveModelPath)(args.projectPath);
|
|
270081
|
+
const projectRoot = (0, types_1.projectRootOfModel)(filePath);
|
|
270082
|
+
const overwrite = args.overwrite === true;
|
|
270083
|
+
if (fs4.existsSync(filePath) && !overwrite) {
|
|
270084
|
+
const { model: model2 } = (0, types_1.loadModel)(args.projectPath);
|
|
270085
|
+
return {
|
|
270086
|
+
success: false,
|
|
270087
|
+
action: "already-initialized",
|
|
270088
|
+
filePath,
|
|
270089
|
+
modelExists: true,
|
|
270090
|
+
endpointsCount: model2.endpoints?.length || 0,
|
|
270091
|
+
message: `Model already exists at ${filePath} \u2014 nothing was overwritten. Continue with step 2 (jetic_scan for Express, or jetic_add_endpoint per endpoint).`,
|
|
270092
|
+
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."
|
|
270093
|
+
};
|
|
270094
|
+
}
|
|
270095
|
+
const stack = detectStack(projectRoot);
|
|
270096
|
+
const model = {
|
|
270097
|
+
version: model_1.CURRENT_MODEL_VERSION,
|
|
270098
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
270099
|
+
project: {
|
|
270100
|
+
name: args.projectName || path7.basename(projectRoot) || "jetic-project",
|
|
270101
|
+
...args.language || (stack.hasTsConfig ? "typescript" : void 0) ? { language: args.language || "typescript" } : {},
|
|
270102
|
+
...args.framework || (stack.hasExpress ? "express" : void 0) ? { framework: args.framework || "express" } : {}
|
|
270103
|
+
},
|
|
270104
|
+
environments: [
|
|
270105
|
+
{ name: args.environmentName || "local", baseUrl: args.baseUrl || "http://localhost:3000" }
|
|
270106
|
+
],
|
|
270107
|
+
securitySchemes: {},
|
|
270108
|
+
resources: [],
|
|
270109
|
+
endpoints: [],
|
|
270110
|
+
dependencies: [],
|
|
270111
|
+
workflows: [],
|
|
270112
|
+
stateMachines: []
|
|
270113
|
+
};
|
|
270114
|
+
(0, types_1.saveModel)(filePath, model);
|
|
270115
|
+
fs4.mkdirSync(path7.join(path7.dirname(filePath), "workflows"), { recursive: true });
|
|
270116
|
+
const configPath = path7.join(path7.dirname(filePath), "config.json");
|
|
270117
|
+
if (!fs4.existsSync(configPath)) {
|
|
270118
|
+
fs4.writeFileSync(configPath, JSON.stringify({}, null, 2), "utf-8");
|
|
270119
|
+
}
|
|
270120
|
+
const scanRecommended = stack.hasExpress && stack.hasTsConfig;
|
|
270121
|
+
return {
|
|
270122
|
+
success: true,
|
|
270123
|
+
action: overwrite ? "overwritten" : "created",
|
|
270124
|
+
filePath,
|
|
270125
|
+
project: model.project,
|
|
270126
|
+
detected: stack,
|
|
270127
|
+
scanRecommended,
|
|
270128
|
+
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."
|
|
270129
|
+
};
|
|
270130
|
+
}
|
|
270131
|
+
exports2.scanProjectSchema = zod_1.z.object({
|
|
270132
|
+
projectPath: zod_1.z.string().optional().describe("Path to the Express project root (must contain tsconfig.json). Defaults to JETIC_PROJECT_PATH / cwd."),
|
|
270133
|
+
mode: zod_1.z.enum(["merge", "overwrite"]).optional().describe("merge (default): upsert scanned routes, keep hand-added endpoints. overwrite: replace the whole endpoints array.")
|
|
270134
|
+
});
|
|
270135
|
+
async function quietly(fn) {
|
|
270136
|
+
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
270137
|
+
const origLog = console.log;
|
|
270138
|
+
const origInfo = console.info;
|
|
270139
|
+
const origDebug = console.debug?.bind(console);
|
|
270140
|
+
const noop = () => {
|
|
270141
|
+
};
|
|
270142
|
+
process.stdout.write = noop;
|
|
270143
|
+
console.log = noop;
|
|
270144
|
+
console.info = noop;
|
|
270145
|
+
if (console.debug)
|
|
270146
|
+
console.debug = noop;
|
|
270147
|
+
try {
|
|
270148
|
+
return await fn();
|
|
270149
|
+
} finally {
|
|
270150
|
+
process.stdout.write = origStdoutWrite;
|
|
270151
|
+
console.log = origLog;
|
|
270152
|
+
console.info = origInfo;
|
|
270153
|
+
if (origDebug)
|
|
270154
|
+
console.debug = origDebug;
|
|
270155
|
+
}
|
|
270156
|
+
}
|
|
270157
|
+
async function handleScanProject(args) {
|
|
270158
|
+
const filePath = (0, types_1.resolveModelPath)(args.projectPath);
|
|
270159
|
+
const projectRoot = (0, types_1.projectRootOfModel)(filePath);
|
|
270160
|
+
const mode = args.mode || "merge";
|
|
270161
|
+
if (!fs4.existsSync(path7.join(projectRoot, "tsconfig.json"))) {
|
|
270162
|
+
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.`);
|
|
270163
|
+
}
|
|
270164
|
+
const { ExpressScanner: ExpressScanner2 } = await Promise.resolve().then(() => __importStar(require_dist3()));
|
|
270165
|
+
const config = {
|
|
270166
|
+
projectRoot,
|
|
270167
|
+
jeticDir: path7.dirname(filePath)
|
|
270168
|
+
// No `ai` key on purpose: static discovery only, no provider, no keys.
|
|
270169
|
+
};
|
|
270170
|
+
let scanned;
|
|
270171
|
+
try {
|
|
270172
|
+
scanned = await quietly(() => new ExpressScanner2(config).scan());
|
|
270173
|
+
} catch (err) {
|
|
270174
|
+
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.`);
|
|
270175
|
+
}
|
|
270176
|
+
const { model: existing, exists } = (0, types_1.loadModel)(args.projectPath);
|
|
270177
|
+
let added = 0;
|
|
270178
|
+
let updated = 0;
|
|
270179
|
+
let preserved = 0;
|
|
270180
|
+
if (!exists || mode === "overwrite") {
|
|
270181
|
+
if (mode === "overwrite" && exists)
|
|
270182
|
+
preserved = 0;
|
|
270183
|
+
existing.endpoints = scanned.endpoints;
|
|
270184
|
+
added = scanned.endpoints.length;
|
|
270185
|
+
if (!existing.environments || existing.environments.length === 0) {
|
|
270186
|
+
existing.environments = scanned.environments;
|
|
270187
|
+
}
|
|
270188
|
+
} else {
|
|
270189
|
+
const index = /* @__PURE__ */ new Map();
|
|
270190
|
+
existing.endpoints.forEach((e, i) => index.set(`${e.method.toUpperCase()} ${e.path}`, i));
|
|
270191
|
+
for (const s of scanned.endpoints) {
|
|
270192
|
+
const key = `${s.method.toUpperCase()} ${s.path}`;
|
|
270193
|
+
const at = index.get(key);
|
|
270194
|
+
if (at === void 0) {
|
|
270195
|
+
existing.endpoints.push(s);
|
|
270196
|
+
added++;
|
|
270197
|
+
} else {
|
|
270198
|
+
existing.endpoints[at] = s;
|
|
270199
|
+
updated++;
|
|
270200
|
+
}
|
|
270201
|
+
}
|
|
270202
|
+
preserved = existing.endpoints.length - added - updated;
|
|
270203
|
+
if (!existing.environments || existing.environments.length === 0) {
|
|
270204
|
+
existing.environments = scanned.environments;
|
|
270205
|
+
}
|
|
270206
|
+
}
|
|
270207
|
+
if (!existing.project?.framework && scanned.project?.framework) {
|
|
270208
|
+
existing.project = { ...scanned.project, ...existing.project };
|
|
270209
|
+
}
|
|
270210
|
+
(0, types_1.saveModel)(filePath, existing);
|
|
270211
|
+
const total = existing.endpoints.length;
|
|
270212
|
+
return {
|
|
270213
|
+
success: true,
|
|
270214
|
+
action: "scanned",
|
|
270215
|
+
filePath,
|
|
270216
|
+
mode,
|
|
270217
|
+
stats: { scanned: scanned.endpoints.length, added, updated, preserved, total },
|
|
270218
|
+
...scanned.endpoints.length === 0 ? {
|
|
270219
|
+
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)."
|
|
270220
|
+
} : {},
|
|
270221
|
+
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.`
|
|
270222
|
+
};
|
|
270223
|
+
}
|
|
270224
|
+
}
|
|
270225
|
+
});
|
|
270226
|
+
|
|
269297
270227
|
// ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/zod-compat.js
|
|
269298
270228
|
var require_zod_compat = __commonJS({
|
|
269299
270229
|
"../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/zod-compat.js"(exports2) {
|
|
@@ -283362,6 +284292,8 @@ var require_server3 = __commonJS({
|
|
|
283362
284292
|
var testing_tools_1 = require_testing_tools();
|
|
283363
284293
|
var environment_tools_1 = require_environment_tools();
|
|
283364
284294
|
var workflow_tools_1 = require_workflow_tools();
|
|
284295
|
+
var workflow_authoring_tools_1 = require_workflow_authoring_tools();
|
|
284296
|
+
var project_tools_1 = require_project_tools();
|
|
283365
284297
|
exports2.JETIC_MCP_SERVER_NAME = "jetic-mcp-server";
|
|
283366
284298
|
function getJeticMcpVersion() {
|
|
283367
284299
|
try {
|
|
@@ -283374,13 +284306,12 @@ var require_server3 = __commonJS({
|
|
|
283374
284306
|
return "0.1.0";
|
|
283375
284307
|
}
|
|
283376
284308
|
exports2.JETIC_MCP_INSTRUCTIONS = [
|
|
283377
|
-
"Jetic MCP manages API behavioral models
|
|
283378
|
-
"
|
|
283379
|
-
"
|
|
283380
|
-
"
|
|
283381
|
-
"
|
|
283382
|
-
"
|
|
283383
|
-
"5. Use `jetic_test_endpoint` / `jetic_simulate_workflow` only against a running server (local/staging); pass `baseUrl` to override the stored environment URL.",
|
|
284309
|
+
"Jetic MCP manages API behavioral models in `.jetic/`. Work in this structured order:",
|
|
284310
|
+
"1. INITIALIZE: if there is no `.jetic/model.json` (read tools report modelExists:false), call `jetic_init` first. It scaffolds the folder + model and tells you whether auto-scan applies. Never invent a model file by hand.",
|
|
284311
|
+
"2. MODEL: Express+TypeScript project with tsconfig.json \u2192 run `jetic_scan` (static, keyless) to fill model.json, then refine with `jetic_add_endpoint` / `jetic_update_endpoint`. Any other stack \u2014 or scan gaps \u2014 read the code and model each endpoint yourself with `jetic_add_endpoint` (full fidelity: middleware chains, security, constraints, pagination, produces/consumes). Run `jetic_verify_model` until clean.",
|
|
284312
|
+
"3. SIMULATE (only when the user asks): author workflows with `jetic_validate_workflow` (dry run) \u2192 `jetic_create_workflow` \u2192 `jetic_simulate_workflow`. Chain steps with capture/captureInput (producer) + inject or {{workflow:key}} templates (consumer). Use {{human:key}} ONLY for values unknowable beforehand (OTP codes, real secrets) \u2014 runners pause for interactive input and the create response lists them under needsHuman so you can warn the user.",
|
|
284313
|
+
"No AI-provider setup is needed for any of this: never ask for OpenRouter/OpenAI keys and never use `jetic config ai`.",
|
|
284314
|
+
"Mutating tools (`jetic_add_endpoint`, `jetic_update_endpoint`, `jetic_delete_endpoint`, `jetic_manage_environment`, workflow create/update) write to disk \u2014 confirm intent with the user first when the change is destructive (delete/overwrite).",
|
|
283384
284315
|
"Project resolution: every tool accepts an optional `projectPath`. If omitted, the server uses the `JETIC_PROJECT_PATH` environment variable, falling back to its own working directory. Set one of them to the project root so the server edits the right `.jetic/model.json`."
|
|
283385
284316
|
].join("\n");
|
|
283386
284317
|
var READ_ONLY = {
|
|
@@ -283441,6 +284372,12 @@ var require_server3 = __commonJS({
|
|
|
283441
284372
|
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);
|
|
283442
284373
|
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);
|
|
283443
284374
|
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);
|
|
284375
|
+
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);
|
|
284376
|
+
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);
|
|
284377
|
+
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);
|
|
284378
|
+
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);
|
|
284379
|
+
register("jetic_init", "Initialize Jetic project", "Step 1 of the structured order: 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);
|
|
284380
|
+
register("jetic_scan", "Scan Express project", "Step 2a for Express+TypeScript projects (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).", project_tools_1.scanProjectSchema.shape, WRITES_MODEL, project_tools_1.handleScanProject);
|
|
283444
284381
|
return server;
|
|
283445
284382
|
}
|
|
283446
284383
|
async function runMcpServer2() {
|
|
@@ -283506,6 +284443,8 @@ var require_dist7 = __commonJS({
|
|
|
283506
284443
|
__exportStar(require_testing_tools(), exports2);
|
|
283507
284444
|
__exportStar(require_environment_tools(), exports2);
|
|
283508
284445
|
__exportStar(require_workflow_tools(), exports2);
|
|
284446
|
+
__exportStar(require_workflow_authoring_tools(), exports2);
|
|
284447
|
+
__exportStar(require_project_tools(), exports2);
|
|
283509
284448
|
__exportStar(require_server3(), exports2);
|
|
283510
284449
|
__exportStar(require_serial_transport(), exports2);
|
|
283511
284450
|
var fs4 = __importStar(require("fs"));
|
|
@@ -283969,9 +284908,15 @@ Use EXACTLY these placeholders in body/inject values:
|
|
|
283969
284908
|
{{faker.commerce.productName}} product name
|
|
283970
284909
|
{{faker.phone.number}} phone number
|
|
283971
284910
|
{{workflow:KEY}} value captured from a previous step
|
|
284911
|
+
{{human:KEY}} ask the human at runtime (interactive prompt, waits for input).
|
|
284912
|
+
Use ONLY for values unknowable beforehand (OTP/2FA codes,
|
|
284913
|
+
CAPTCHAs, real personal secrets). First entry is saved to
|
|
284914
|
+
human:KEY memory; JETIC_HUMAN_KEY env var skips the prompt.
|
|
283972
284915
|
|
|
283973
|
-
\u2501\u2501\u2501 FIELD RULES \u2501\u2501\u2501
|
|
284916
|
+
\u2501\u2501\u2501 FIELD RULES \u2501\u2501\u2501
|
|
283974
284917
|
"body" \u2014 request body. Use {{faker.*}} for generated fields, {{workflow:KEY}} for previously captured values.
|
|
284918
|
+
Use {{human:KEY}} SPARINGLY and only when no other source exists (OTP codes, real secrets) \u2014
|
|
284919
|
+
it pauses execution waiting for a person to type a value.
|
|
283975
284920
|
|
|
283976
284921
|
"captureInput" \u2014 save RESOLVED body field values to memory BEFORE the HTTP call.
|
|
283977
284922
|
Only for faker-generated fields you need to re-use in later steps.
|
|
@@ -284022,7 +284967,7 @@ all [requiresAuth] endpoints inject Authorization, captures wired correctly betw
|
|
|
284022
284967
|
method: import_zod.z.string().describe("HTTP method in uppercase: GET, POST, PUT, PATCH, DELETE"),
|
|
284023
284968
|
path: import_zod.z.string().describe("Exact endpoint path, e.g. /api/auth/login"),
|
|
284024
284969
|
description: import_zod.z.string().optional().describe("One sentence describing what this step does"),
|
|
284025
|
-
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"),
|
|
284970
|
+
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"),
|
|
284026
284971
|
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" }'),
|
|
284027
284972
|
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" }'),
|
|
284028
284973
|
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'),
|
|
@@ -284054,12 +284999,12 @@ function deepGet(obj, dotPath) {
|
|
|
284054
284999
|
}
|
|
284055
285000
|
return current;
|
|
284056
285001
|
}
|
|
284057
|
-
async function resolveConditionRef(ref) {
|
|
284058
|
-
return ref.replace(/\{\{([^}]+)\}\}/g, () => "").trim() === ref.trim() ? ref : await resolveTemplateString(ref);
|
|
285002
|
+
async function resolveConditionRef(ref, context = "condition") {
|
|
285003
|
+
return ref.replace(/\{\{([^}]+)\}\}/g, () => "").trim() === ref.trim() ? ref : await resolveTemplateString(ref, context);
|
|
284059
285004
|
}
|
|
284060
|
-
async function evalConditionRule(rule) {
|
|
284061
|
-
const left = await resolveConditionRef(rule.left);
|
|
284062
|
-
const right = rule.right !== void 0 ? await resolveConditionRef(rule.right) : void 0;
|
|
285005
|
+
async function evalConditionRule(rule, context = "condition") {
|
|
285006
|
+
const left = await resolveConditionRef(rule.left, context);
|
|
285007
|
+
const right = rule.right !== void 0 ? await resolveConditionRef(rule.right, context) : void 0;
|
|
284063
285008
|
const op = rule.operator;
|
|
284064
285009
|
switch (op) {
|
|
284065
285010
|
case "equals":
|
|
@@ -284094,14 +285039,14 @@ async function evalConditionRule(rule) {
|
|
|
284094
285039
|
return false;
|
|
284095
285040
|
}
|
|
284096
285041
|
}
|
|
284097
|
-
async function evaluateStepCondition(condition) {
|
|
285042
|
+
async function evaluateStepCondition(condition, context = "condition") {
|
|
284098
285043
|
const { rules } = condition;
|
|
284099
285044
|
const ruleList = rules.all ?? rules.any ?? [];
|
|
284100
285045
|
const isAnd = !!rules.all;
|
|
284101
285046
|
if (ruleList.length === 0) return { passed: true, reason: "No rules defined" };
|
|
284102
285047
|
const results = [];
|
|
284103
285048
|
for (const rule of ruleList) {
|
|
284104
|
-
results.push({ rule, ok: await evalConditionRule(rule) });
|
|
285049
|
+
results.push({ rule, ok: await evalConditionRule(rule, context) });
|
|
284105
285050
|
}
|
|
284106
285051
|
const passed = isAnd ? results.every((r) => r.ok) : results.some((r) => r.ok);
|
|
284107
285052
|
const failing = results.filter((r) => !r.ok);
|
|
@@ -284110,12 +285055,89 @@ async function evaluateStepCondition(condition) {
|
|
|
284110
285055
|
).join(", ")}`;
|
|
284111
285056
|
return { passed, reason };
|
|
284112
285057
|
}
|
|
284113
|
-
|
|
285058
|
+
function envKeyForHuman(key) {
|
|
285059
|
+
return `JETIC_HUMAN_${key.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
285060
|
+
}
|
|
285061
|
+
var HumanInputError = class extends Error {
|
|
285062
|
+
constructor(message) {
|
|
285063
|
+
super(message);
|
|
285064
|
+
this.name = "HumanInputError";
|
|
285065
|
+
}
|
|
285066
|
+
};
|
|
285067
|
+
function isSecretHumanKey(key) {
|
|
285068
|
+
return /pass|secret|token|pwd|private|credential/i.test(key);
|
|
285069
|
+
}
|
|
285070
|
+
async function promptLine(question) {
|
|
285071
|
+
const readline3 = await new Function("modulePath", "return import(modulePath)")("node:readline/promises");
|
|
285072
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
285073
|
+
try {
|
|
285074
|
+
return await rl.question(question);
|
|
285075
|
+
} finally {
|
|
285076
|
+
rl.close();
|
|
285077
|
+
}
|
|
285078
|
+
}
|
|
285079
|
+
async function promptSecret(question) {
|
|
285080
|
+
const readline3 = await new Function("modulePath", "return import(modulePath)")("node:readline");
|
|
285081
|
+
return new Promise((resolve2) => {
|
|
285082
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
285083
|
+
process.stdout.write(question);
|
|
285084
|
+
const w = rl;
|
|
285085
|
+
const orig = w._writeToOutput.bind(rl);
|
|
285086
|
+
w._writeToOutput = (s) => {
|
|
285087
|
+
if (s === "\n" || s === "\r\n" || s === "\r") orig(s);
|
|
285088
|
+
else w.output.write("*".repeat(s.length));
|
|
285089
|
+
};
|
|
285090
|
+
rl.question("", (answer) => {
|
|
285091
|
+
w._writeToOutput = orig;
|
|
285092
|
+
process.stdout.write("\n");
|
|
285093
|
+
rl.close();
|
|
285094
|
+
resolve2(answer);
|
|
285095
|
+
});
|
|
285096
|
+
});
|
|
285097
|
+
}
|
|
285098
|
+
async function askHuman(key, context) {
|
|
285099
|
+
const envName = envKeyForHuman(key);
|
|
285100
|
+
const fromEnv = process.env[envName];
|
|
285101
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
285102
|
+
const mem = new import_memory2.JeticMemory({ scope: "human" });
|
|
285103
|
+
const cached = await mem.get(key);
|
|
285104
|
+
if (cached !== null && cached !== void 0 && String(cached) !== "") {
|
|
285105
|
+
console.log(` ${c.dim}\u21B3 using saved human value for "${key}" (jetic memory set human:${key} \u2026 to change)${c.reset}`);
|
|
285106
|
+
return String(cached);
|
|
285107
|
+
}
|
|
285108
|
+
if (!process.stdin.isTTY) {
|
|
285109
|
+
throw new HumanInputError(
|
|
285110
|
+
`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>`
|
|
285111
|
+
);
|
|
285112
|
+
}
|
|
285113
|
+
console.log(`
|
|
285114
|
+
${c.yellow}${c.bold}\u25C9 Human input required${c.reset}${context ? ` ${c.dim}${context}${c.reset}` : ""}`);
|
|
285115
|
+
const question = ` ${c.cyan}Enter value for "${key}":${c.reset} `;
|
|
285116
|
+
const value = isSecretHumanKey(key) ? await promptSecret(question) : await promptLine(question);
|
|
285117
|
+
if (value !== "") {
|
|
285118
|
+
await mem.set(key, value);
|
|
285119
|
+
console.log(` ${c.dim}\u21B3 saved to human:${key} memory (reused automatically next time)${c.reset}
|
|
285120
|
+
`);
|
|
285121
|
+
} else {
|
|
285122
|
+
console.log(` ${c.dim}\u21B3 empty value accepted for "${key}" (not saved)${c.reset}
|
|
285123
|
+
`);
|
|
285124
|
+
}
|
|
285125
|
+
return value;
|
|
285126
|
+
}
|
|
285127
|
+
async function resolveTemplateString(value, context) {
|
|
284114
285128
|
const templateRe = /\{\{([^}]+)\}\}/g;
|
|
284115
285129
|
let match;
|
|
284116
285130
|
const replacements = [];
|
|
284117
285131
|
while ((match = templateRe.exec(value)) !== null) {
|
|
284118
285132
|
const expr = match[1].trim();
|
|
285133
|
+
if (!expr.startsWith("faker.")) {
|
|
285134
|
+
const colonIdx = expr.indexOf(":");
|
|
285135
|
+
if (colonIdx > 0 && expr.slice(0, colonIdx) === "human") {
|
|
285136
|
+
const key = expr.slice(colonIdx + 1);
|
|
285137
|
+
replacements.push({ placeholder: match[0], resolved: await askHuman(key, context) });
|
|
285138
|
+
continue;
|
|
285139
|
+
}
|
|
285140
|
+
}
|
|
284119
285141
|
if (expr.includes(":") && !expr.startsWith("faker.")) {
|
|
284120
285142
|
const colonIdx = expr.indexOf(":");
|
|
284121
285143
|
const scope = expr.slice(0, colonIdx);
|
|
@@ -284145,21 +285167,21 @@ async function resolveTemplateString(value) {
|
|
|
284145
285167
|
}
|
|
284146
285168
|
return result;
|
|
284147
285169
|
}
|
|
284148
|
-
async function resolveBodyTemplates(body) {
|
|
285170
|
+
async function resolveBodyTemplates(body, context) {
|
|
284149
285171
|
const resolved = {};
|
|
284150
285172
|
for (const [key, value] of Object.entries(body)) {
|
|
284151
|
-
resolved[key] = typeof value === "string" ? await resolveTemplateString(value) : value;
|
|
285173
|
+
resolved[key] = typeof value === "string" ? await resolveTemplateString(value, context) : value;
|
|
284152
285174
|
}
|
|
284153
285175
|
return resolved;
|
|
284154
285176
|
}
|
|
284155
|
-
async function resolveInjections(inject, memory, allMemory) {
|
|
285177
|
+
async function resolveInjections(inject, memory, allMemory, context) {
|
|
284156
285178
|
const headers = {};
|
|
284157
285179
|
const body = {};
|
|
284158
285180
|
if (!inject) return { headers, body };
|
|
284159
285181
|
for (const [target, memKeyOrTemplate] of Object.entries(inject)) {
|
|
284160
285182
|
let strValue;
|
|
284161
285183
|
if (memKeyOrTemplate.includes("{{")) {
|
|
284162
|
-
strValue = await resolveTemplateString(memKeyOrTemplate);
|
|
285184
|
+
strValue = await resolveTemplateString(memKeyOrTemplate, context);
|
|
284163
285185
|
} else {
|
|
284164
285186
|
const [scope, key] = memKeyOrTemplate.includes(":") ? memKeyOrTemplate.split(":", 2) : ["workflow", memKeyOrTemplate];
|
|
284165
285187
|
const scopedMemory = new import_memory2.JeticMemory({ scope });
|
|
@@ -284212,8 +285234,9 @@ async function resolvePathParams(urlPath, body) {
|
|
|
284212
285234
|
async function executeStep(step, baseUrl, defaultBody) {
|
|
284213
285235
|
const startTime = Date.now();
|
|
284214
285236
|
const memory = new import_memory2.JeticMemory({ scope: "workflow" });
|
|
284215
|
-
const
|
|
284216
|
-
const
|
|
285237
|
+
const stepLabel = step.name || `${step.method} ${step.path}`;
|
|
285238
|
+
const { headers, body: injectedBody } = await resolveInjections(step.inject, memory, memory, stepLabel);
|
|
285239
|
+
const resolvedStepBody = await resolveBodyTemplates(step.body || {}, stepLabel);
|
|
284217
285240
|
const requestBody = { ...defaultBody, ...injectedBody, ...resolvedStepBody };
|
|
284218
285241
|
const inputCaptured = await captureInputToMemory(step.captureInput, requestBody);
|
|
284219
285242
|
const resolvedPath = await resolvePathParams(step.path, requestBody);
|
|
@@ -284557,71 +285580,86 @@ var simulateWorkflowCommand = new import_commander6.Command("workflow").descript
|
|
|
284557
285580
|
const stepLabel = `${c.dim}${i + 1}/${workflow.steps.length}${c.reset} ${getMethodColor(step.method)}${step.method}${c.reset} ${step.path}`;
|
|
284558
285581
|
const spinner = new Spinner();
|
|
284559
285582
|
spinner.start(`${stepLabel} ${c.dim}${step.name}${c.reset}...`);
|
|
284560
|
-
|
|
284561
|
-
|
|
284562
|
-
|
|
284563
|
-
|
|
284564
|
-
|
|
284565
|
-
|
|
284566
|
-
|
|
284567
|
-
|
|
284568
|
-
|
|
284569
|
-
|
|
284570
|
-
|
|
284571
|
-
|
|
285583
|
+
let result;
|
|
285584
|
+
try {
|
|
285585
|
+
result = await executeStep(step, baseUrl, {});
|
|
285586
|
+
if (step.condition) {
|
|
285587
|
+
const condResult = await evaluateStepCondition(step.condition, `condition of step "${step.name || step.path}"`);
|
|
285588
|
+
result.conditionResult = condResult;
|
|
285589
|
+
if (!condResult.passed) {
|
|
285590
|
+
const { onFail, switchToWorkflow } = step.condition;
|
|
285591
|
+
results.push(result);
|
|
285592
|
+
const icon2 = result.passed ? TICK : CROSS;
|
|
285593
|
+
spinner.stop(` ${icon2} ${stepLabel} ${formatStatus(result.status)} ${c.dim}${result.durationMs}ms${c.reset}`);
|
|
285594
|
+
renderStepResult(result, i, workflow.steps.length);
|
|
285595
|
+
if (onFail === "abort") {
|
|
285596
|
+
console.log(` ${c.red}${c.bold}\u26A0 Condition failed \u2014 aborting workflow.${c.reset}
|
|
284572
285597
|
`);
|
|
284573
|
-
|
|
284574
|
-
|
|
284575
|
-
|
|
284576
|
-
|
|
285598
|
+
stopOnFailure = true;
|
|
285599
|
+
break;
|
|
285600
|
+
} else if (onFail === "continue") {
|
|
285601
|
+
console.log(` ${c.yellow}\u2298 Condition failed \u2014 skipping remaining steps.${c.reset}
|
|
284577
285602
|
`);
|
|
284578
|
-
|
|
284579
|
-
|
|
284580
|
-
|
|
284581
|
-
|
|
285603
|
+
stopOnFailure = true;
|
|
285604
|
+
break;
|
|
285605
|
+
} else if (onFail === "switch" && switchToWorkflow) {
|
|
285606
|
+
console.log(` ${ARROW} Condition failed \u2014 switching to workflow: ${c.bold}${switchToWorkflow}${c.reset}
|
|
284582
285607
|
`);
|
|
284583
|
-
|
|
284584
|
-
|
|
284585
|
-
|
|
284586
|
-
|
|
284587
|
-
|
|
285608
|
+
const switchPath = path3.join(workflowsDir, `${switchToWorkflow}.json`);
|
|
285609
|
+
if (fs.existsSync(switchPath)) {
|
|
285610
|
+
try {
|
|
285611
|
+
const switchedWorkflow = JSON.parse(fs.readFileSync(switchPath, "utf-8"));
|
|
285612
|
+
console.log(` ${TICK} Loaded switch workflow: ${c.bold}${switchedWorkflow.name}${c.reset}
|
|
284588
285613
|
`);
|
|
284589
|
-
|
|
284590
|
-
|
|
285614
|
+
renderWorkflowHeader(switchedWorkflow);
|
|
285615
|
+
console.log(` ${c.magenta}\u{1F680}${c.reset} Executing sub-workflow...
|
|
285616
|
+
`);
|
|
285617
|
+
for (let si = 0; si < switchedWorkflow.steps.length; si++) {
|
|
285618
|
+
const switchStep = switchedWorkflow.steps[si];
|
|
285619
|
+
const switchLabel = `${c.dim}${si + 1}/${switchedWorkflow.steps.length}${c.reset} ${getMethodColor(switchStep.method)}${switchStep.method}${c.reset} ${switchStep.path}`;
|
|
285620
|
+
const switchSpinner = new Spinner();
|
|
285621
|
+
switchSpinner.start(`${switchLabel} ${c.dim}${switchStep.name}${c.reset}...`);
|
|
285622
|
+
const switchResult = await executeStep(switchStep, baseUrl, {});
|
|
285623
|
+
if (switchStep.condition) {
|
|
285624
|
+
switchResult.conditionResult = await evaluateStepCondition(
|
|
285625
|
+
switchStep.condition,
|
|
285626
|
+
`condition of step "${switchStep.name || switchStep.path}"`
|
|
285627
|
+
);
|
|
285628
|
+
}
|
|
285629
|
+
results.push(switchResult);
|
|
285630
|
+
const switchIcon = switchResult.passed ? TICK : CROSS;
|
|
285631
|
+
switchSpinner.stop(` ${switchIcon} ${switchLabel} ${formatStatus(switchResult.status)} ${c.dim}${switchResult.durationMs}ms${c.reset}`);
|
|
285632
|
+
renderStepResult(switchResult, si, switchedWorkflow.steps.length);
|
|
285633
|
+
}
|
|
285634
|
+
} catch (e) {
|
|
285635
|
+
console.log(` ${c.red}\u2717 Failed to load switch workflow: ${e.message}${c.reset}
|
|
284591
285636
|
`);
|
|
284592
|
-
for (let si = 0; si < switchedWorkflow.steps.length; si++) {
|
|
284593
|
-
const switchStep = switchedWorkflow.steps[si];
|
|
284594
|
-
const switchLabel = `${c.dim}${si + 1}/${switchedWorkflow.steps.length}${c.reset} ${getMethodColor(switchStep.method)}${switchStep.method}${c.reset} ${switchStep.path}`;
|
|
284595
|
-
const switchSpinner = new Spinner();
|
|
284596
|
-
switchSpinner.start(`${switchLabel} ${c.dim}${switchStep.name}${c.reset}...`);
|
|
284597
|
-
const switchResult = await executeStep(switchStep, baseUrl, {});
|
|
284598
|
-
if (switchStep.condition) {
|
|
284599
|
-
switchResult.conditionResult = await evaluateStepCondition(switchStep.condition);
|
|
284600
|
-
}
|
|
284601
|
-
results.push(switchResult);
|
|
284602
|
-
const switchIcon = switchResult.passed ? TICK : CROSS;
|
|
284603
|
-
switchSpinner.stop(` ${switchIcon} ${switchLabel} ${formatStatus(switchResult.status)} ${c.dim}${switchResult.durationMs}ms${c.reset}`);
|
|
284604
|
-
renderStepResult(switchResult, si, switchedWorkflow.steps.length);
|
|
284605
285637
|
}
|
|
284606
|
-
}
|
|
284607
|
-
console.log(` ${c.
|
|
285638
|
+
} else {
|
|
285639
|
+
console.log(` ${c.yellow}\u26A0 Switch workflow not found: ${switchPath}${c.reset}
|
|
284608
285640
|
`);
|
|
284609
285641
|
}
|
|
284610
|
-
|
|
284611
|
-
|
|
284612
|
-
`);
|
|
284613
|
-
}
|
|
284614
|
-
if (step.condition.returnOnComplete) {
|
|
284615
|
-
console.log(` ${c.green}\u21A9 Sub-workflow complete. Returning to main workflow...${c.reset}
|
|
285642
|
+
if (step.condition.returnOnComplete) {
|
|
285643
|
+
console.log(` ${c.green}\u21A9 Sub-workflow complete. Returning to main workflow...${c.reset}
|
|
284616
285644
|
`);
|
|
284617
|
-
|
|
284618
|
-
|
|
284619
|
-
|
|
284620
|
-
|
|
285645
|
+
continue;
|
|
285646
|
+
} else {
|
|
285647
|
+
stopOnFailure = true;
|
|
285648
|
+
break;
|
|
285649
|
+
}
|
|
284621
285650
|
}
|
|
285651
|
+
continue;
|
|
284622
285652
|
}
|
|
284623
|
-
continue;
|
|
284624
285653
|
}
|
|
285654
|
+
} catch (err) {
|
|
285655
|
+
if (err instanceof HumanInputError || err?.name === "HumanInputError") {
|
|
285656
|
+
spinner.stop(` ${c.red}\u2717${c.reset} ${stepLabel} ${c.dim}needs human input${c.reset}`);
|
|
285657
|
+
console.error(`
|
|
285658
|
+
${c.red}${err.message}${c.reset}
|
|
285659
|
+
`);
|
|
285660
|
+
process.exit(1);
|
|
285661
|
+
}
|
|
285662
|
+
throw err;
|
|
284625
285663
|
}
|
|
284626
285664
|
results.push(result);
|
|
284627
285665
|
const icon = result.passed ? TICK : CROSS;
|
|
@@ -285534,8 +286572,29 @@ Output valid JSON only.
|
|
|
285534
286572
|
res.status(500).json({ error: err.message });
|
|
285535
286573
|
}
|
|
285536
286574
|
});
|
|
286575
|
+
const humanInputWaiters = /* @__PURE__ */ new Map();
|
|
286576
|
+
const HUMAN_INPUT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
286577
|
+
class HumanInputAbort extends Error {
|
|
286578
|
+
constructor(key, reason) {
|
|
286579
|
+
super(`Human input for "{{human:${key}}}" ${reason}`);
|
|
286580
|
+
this.name = "HumanInputAbort";
|
|
286581
|
+
}
|
|
286582
|
+
}
|
|
286583
|
+
app.post("/api/workflows/human-input", (req, res) => {
|
|
286584
|
+
const { runId, key, value } = req.body;
|
|
286585
|
+
if (!runId || !key) {
|
|
286586
|
+
return res.status(400).json({ error: "runId and key are required" });
|
|
286587
|
+
}
|
|
286588
|
+
const waiter = humanInputWaiters.get(runId);
|
|
286589
|
+
if (!waiter || waiter.key !== key) {
|
|
286590
|
+
return res.status(404).json({ error: "no pending human input for this run/key (answered, timed out, or run ended)" });
|
|
286591
|
+
}
|
|
286592
|
+
humanInputWaiters.delete(runId);
|
|
286593
|
+
waiter.resolve(typeof value === "string" ? value : null);
|
|
286594
|
+
res.json({ ok: true });
|
|
286595
|
+
});
|
|
285537
286596
|
app.post("/api/workflows/run", async (req, res) => {
|
|
285538
|
-
const { file } = req.body;
|
|
286597
|
+
const { file, humanInputs: preseededHuman } = req.body;
|
|
285539
286598
|
if (!file) {
|
|
285540
286599
|
return res.status(400).json({ error: "file is required" });
|
|
285541
286600
|
}
|
|
@@ -285576,10 +286635,46 @@ Output valid JSON only.
|
|
|
285576
286635
|
}
|
|
285577
286636
|
}
|
|
285578
286637
|
if (!baseUrl) baseUrl = "http://localhost:4000";
|
|
285579
|
-
|
|
286638
|
+
const runId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
286639
|
+
const runHumanCache = new Map(Object.entries(preseededHuman ?? {}));
|
|
286640
|
+
const HUMAN_KEY_RE = /\{\{\s*human:([^}]+?)\s*\}\}/g;
|
|
286641
|
+
const collectHumanRefs = () => {
|
|
286642
|
+
const keys = /* @__PURE__ */ new Set();
|
|
286643
|
+
for (const step of workflow.steps || []) {
|
|
286644
|
+
for (const hay of [step?.path, JSON.stringify(step?.body ?? {}), JSON.stringify(step?.inject ?? {}), JSON.stringify(step?.condition ?? {})]) {
|
|
286645
|
+
if (!hay) continue;
|
|
286646
|
+
HUMAN_KEY_RE.lastIndex = 0;
|
|
286647
|
+
let m;
|
|
286648
|
+
while ((m = HUMAN_KEY_RE.exec(hay)) !== null) {
|
|
286649
|
+
const k = (m[1] || "").trim();
|
|
286650
|
+
if (k) keys.add(k);
|
|
286651
|
+
}
|
|
286652
|
+
}
|
|
286653
|
+
}
|
|
286654
|
+
return [...keys];
|
|
286655
|
+
};
|
|
286656
|
+
const humanEnvName = (key) => `JETIC_HUMAN_${key.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
286657
|
+
const humanSource = async (key) => {
|
|
286658
|
+
if (runHumanCache.has(key)) return "run";
|
|
286659
|
+
if (process.env[humanEnvName(key)]) return "env";
|
|
286660
|
+
try {
|
|
286661
|
+
const v = await new Mem({ scope: "human" }).get(key);
|
|
286662
|
+
if (v != null && String(v) !== "") return "memory";
|
|
286663
|
+
} catch {
|
|
286664
|
+
}
|
|
286665
|
+
return "prompt";
|
|
286666
|
+
};
|
|
286667
|
+
const neededHuman = [];
|
|
286668
|
+
for (const k of collectHumanRefs()) neededHuman.push({ key: k, source: await humanSource(k) });
|
|
286669
|
+
send("start", { name: workflow.name, totalSteps: workflow.steps.length, baseUrl, runId, humanKeys: neededHuman });
|
|
285580
286670
|
let aborted = false;
|
|
285581
286671
|
req.on("close", () => {
|
|
285582
286672
|
aborted = true;
|
|
286673
|
+
const waiter = humanInputWaiters.get(runId);
|
|
286674
|
+
if (waiter) {
|
|
286675
|
+
humanInputWaiters.delete(runId);
|
|
286676
|
+
waiter.resolve(null);
|
|
286677
|
+
}
|
|
285583
286678
|
});
|
|
285584
286679
|
function deepGet2(obj, dotPath) {
|
|
285585
286680
|
return dotPath.replace(/\[(\d+)\]/g, ".$1").split(".").reduce((o, k) => o?.[k], obj);
|
|
@@ -285599,42 +286694,101 @@ Output valid JSON only.
|
|
|
285599
286694
|
return _;
|
|
285600
286695
|
});
|
|
285601
286696
|
}
|
|
285602
|
-
async function resolveMemoryTemplates(value) {
|
|
286697
|
+
async function resolveMemoryTemplates(value, stepIndex = -1, stepName = "") {
|
|
285603
286698
|
const matches = [...value.matchAll(/\{\{([^}]+)\}\}/g)];
|
|
285604
286699
|
let result = value;
|
|
285605
286700
|
for (const [placeholder, expr] of matches.map((m) => [m[0], m[1].trim()])) {
|
|
285606
286701
|
if (expr.includes(":") && !expr.startsWith("faker.")) {
|
|
285607
286702
|
const [scope, key] = expr.split(":", 2);
|
|
285608
|
-
const
|
|
285609
|
-
|
|
285610
|
-
result = result.replace(placeholder, val != null ? String(val) : "");
|
|
286703
|
+
const val = await resolveMemValue(scope, key, stepIndex, stepName);
|
|
286704
|
+
result = result.replace(placeholder, val);
|
|
285611
286705
|
}
|
|
285612
286706
|
}
|
|
285613
286707
|
return result;
|
|
285614
286708
|
}
|
|
285615
|
-
async function resolveStr(v) {
|
|
286709
|
+
async function resolveStr(v, stepIndex = -1, stepName = "") {
|
|
285616
286710
|
let s = await resolveTemplate(v);
|
|
285617
|
-
s = await resolveMemoryTemplates(s);
|
|
286711
|
+
s = await resolveMemoryTemplates(s, stepIndex, stepName);
|
|
285618
286712
|
return s;
|
|
285619
286713
|
}
|
|
285620
|
-
async function
|
|
286714
|
+
async function resolveValueRecursive(val, stepIndex = -1, stepName = "") {
|
|
286715
|
+
if (typeof val === "string") {
|
|
286716
|
+
return await resolveStr(val, stepIndex, stepName);
|
|
286717
|
+
}
|
|
286718
|
+
if (Array.isArray(val)) {
|
|
286719
|
+
const res2 = [];
|
|
286720
|
+
for (const item of val) {
|
|
286721
|
+
res2.push(await resolveValueRecursive(item, stepIndex, stepName));
|
|
286722
|
+
}
|
|
286723
|
+
return res2;
|
|
286724
|
+
}
|
|
286725
|
+
if (val && typeof val === "object" && val.constructor === Object) {
|
|
286726
|
+
const res2 = {};
|
|
286727
|
+
for (const [k, v] of Object.entries(val)) {
|
|
286728
|
+
res2[k] = await resolveValueRecursive(v, stepIndex, stepName);
|
|
286729
|
+
}
|
|
286730
|
+
return res2;
|
|
286731
|
+
}
|
|
286732
|
+
return val;
|
|
286733
|
+
}
|
|
286734
|
+
async function resolveMemValue(scope, key, stepIndex, stepName) {
|
|
286735
|
+
if (scope !== "human") {
|
|
286736
|
+
const mem2 = new Mem({ scope });
|
|
286737
|
+
const val = await mem2.get(key);
|
|
286738
|
+
return val != null ? String(val) : "";
|
|
286739
|
+
}
|
|
286740
|
+
if (runHumanCache.has(key)) {
|
|
286741
|
+
send("human_resolved", { index: stepIndex, key, source: "run" });
|
|
286742
|
+
return runHumanCache.get(key);
|
|
286743
|
+
}
|
|
286744
|
+
const envVal = process.env[humanEnvName(key)];
|
|
286745
|
+
if (envVal !== void 0 && envVal !== "") {
|
|
286746
|
+
send("human_resolved", { index: stepIndex, key, source: "env" });
|
|
286747
|
+
return envVal;
|
|
286748
|
+
}
|
|
286749
|
+
const mem = new Mem({ scope: "human" });
|
|
286750
|
+
const answer = await waitForHumanInput(runId, key, stepIndex, stepName, send);
|
|
286751
|
+
if (answer === null) {
|
|
286752
|
+
throw new HumanInputAbort(key, aborted ? "was cancelled" : "timed out after 10 minutes");
|
|
286753
|
+
}
|
|
286754
|
+
await mem.set(key, answer);
|
|
286755
|
+
send("human_resolved", { index: stepIndex, key, source: "prompt" });
|
|
286756
|
+
return answer;
|
|
286757
|
+
}
|
|
286758
|
+
function waitForHumanInput(forRunId, key, stepIndex, stepName, sendFn) {
|
|
286759
|
+
return new Promise((resolve2) => {
|
|
286760
|
+
const timer = setTimeout(() => {
|
|
286761
|
+
if (humanInputWaiters.get(forRunId)?.key === key) {
|
|
286762
|
+
humanInputWaiters.delete(forRunId);
|
|
286763
|
+
resolve2(null);
|
|
286764
|
+
}
|
|
286765
|
+
}, HUMAN_INPUT_TIMEOUT_MS);
|
|
286766
|
+
humanInputWaiters.set(forRunId, {
|
|
286767
|
+
key,
|
|
286768
|
+
stepIndex,
|
|
286769
|
+
resolve: (value) => {
|
|
286770
|
+
clearTimeout(timer);
|
|
286771
|
+
resolve2(value);
|
|
286772
|
+
}
|
|
286773
|
+
});
|
|
286774
|
+
sendFn("human_input_required", { runId: forRunId, key, stepIndex, stepName });
|
|
286775
|
+
});
|
|
286776
|
+
}
|
|
286777
|
+
async function executeStep2(step, stepIndex) {
|
|
285621
286778
|
const startTime = Date.now();
|
|
286779
|
+
const stepName = step.name || `${step.method} ${step.path}`;
|
|
285622
286780
|
const headers = { "Content-Type": "application/json" };
|
|
285623
286781
|
const bodyExtra = {};
|
|
285624
286782
|
for (const [target, memKeyOrTpl] of Object.entries(step.inject ?? {})) {
|
|
285625
|
-
const strVal = memKeyOrTpl.includes("{{") ? await resolveStr(memKeyOrTpl) : await (async () => {
|
|
286783
|
+
const strVal = memKeyOrTpl.includes("{{") ? await resolveStr(memKeyOrTpl, stepIndex, stepName) : await (async () => {
|
|
285626
286784
|
const [scope, key] = memKeyOrTpl.includes(":") ? memKeyOrTpl.split(":", 2) : ["workflow", memKeyOrTpl];
|
|
285627
|
-
|
|
285628
|
-
return val != null ? String(val) : "";
|
|
286785
|
+
return resolveMemValue(scope, key, stepIndex, stepName);
|
|
285629
286786
|
})();
|
|
285630
286787
|
if (!strVal) continue;
|
|
285631
286788
|
if (target.startsWith("body:")) bodyExtra[target.slice(5)] = strVal;
|
|
285632
286789
|
else headers[target.startsWith("header:") ? target.slice(7) : target] = strVal;
|
|
285633
286790
|
}
|
|
285634
|
-
const resolvedBody = {};
|
|
285635
|
-
for (const [k, v] of Object.entries(step.body ?? {})) {
|
|
285636
|
-
resolvedBody[k] = typeof v === "string" ? await resolveStr(v) : v;
|
|
285637
|
-
}
|
|
286791
|
+
const resolvedBody = await resolveValueRecursive(step.body ?? {}, stepIndex, stepName);
|
|
285638
286792
|
const requestBody = { ...bodyExtra, ...resolvedBody };
|
|
285639
286793
|
for (const [memKey, bodyField] of Object.entries(step.captureInput ?? {})) {
|
|
285640
286794
|
const val = requestBody[bodyField];
|
|
@@ -285647,7 +286801,7 @@ Output valid JSON only.
|
|
|
285647
286801
|
/:([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
285648
286802
|
(_, p) => requestBody[p] !== void 0 ? String(requestBody[p]) : `:${p}`
|
|
285649
286803
|
);
|
|
285650
|
-
resolvedPath = await resolveStr(resolvedPath);
|
|
286804
|
+
resolvedPath = await resolveStr(resolvedPath, stepIndex, stepName);
|
|
285651
286805
|
const finalUrl = `${baseUrl.replace(/\/$/, "")}${resolvedPath}`;
|
|
285652
286806
|
const expectedStatus = step.expectStatus ?? 200;
|
|
285653
286807
|
try {
|
|
@@ -285690,22 +286844,20 @@ Output valid JSON only.
|
|
|
285690
286844
|
}
|
|
285691
286845
|
let passed = 0;
|
|
285692
286846
|
let failed = 0;
|
|
285693
|
-
async function resolveCondRef(ref) {
|
|
286847
|
+
async function resolveCondRef(ref, stepIndex, stepName) {
|
|
285694
286848
|
const matches = [...ref.matchAll(/\{\{([^}]+)\}\}/g)];
|
|
285695
286849
|
let result = ref;
|
|
285696
286850
|
for (const [placeholder, expr] of matches.map((m) => [m[0], m[1].trim()])) {
|
|
285697
286851
|
if (expr.includes(":") && !expr.startsWith("faker.")) {
|
|
285698
286852
|
const [scope, key] = expr.split(":", 2);
|
|
285699
|
-
|
|
285700
|
-
const val = await mem.get(key);
|
|
285701
|
-
result = result.replace(placeholder, val != null ? String(val) : "");
|
|
286853
|
+
result = result.replace(placeholder, await resolveMemValue(scope, key, stepIndex, stepName));
|
|
285702
286854
|
}
|
|
285703
286855
|
}
|
|
285704
286856
|
return result;
|
|
285705
286857
|
}
|
|
285706
|
-
async function evalCondRule(rule) {
|
|
285707
|
-
const left = await resolveCondRef(rule.left);
|
|
285708
|
-
const right = rule.right !== void 0 ? await resolveCondRef(rule.right) : void 0;
|
|
286858
|
+
async function evalCondRule(rule, stepIndex, stepName) {
|
|
286859
|
+
const left = await resolveCondRef(rule.left, stepIndex, stepName);
|
|
286860
|
+
const right = rule.right !== void 0 ? await resolveCondRef(rule.right, stepIndex, stepName) : void 0;
|
|
285709
286861
|
switch (rule.operator) {
|
|
285710
286862
|
case "equals":
|
|
285711
286863
|
return left === right;
|
|
@@ -285739,12 +286891,12 @@ Output valid JSON only.
|
|
|
285739
286891
|
return false;
|
|
285740
286892
|
}
|
|
285741
286893
|
}
|
|
285742
|
-
async function evalCondition(condition) {
|
|
286894
|
+
async function evalCondition(condition, stepIndex, stepName) {
|
|
285743
286895
|
const rules = condition.rules?.all ?? condition.rules?.any ?? [];
|
|
285744
286896
|
const isAnd = !!condition.rules?.all;
|
|
285745
286897
|
if (rules.length === 0) return { passed: true, reason: "No rules" };
|
|
285746
286898
|
const results = [];
|
|
285747
|
-
for (const rule of rules) results.push({ rule, ok: await evalCondRule(rule) });
|
|
286899
|
+
for (const rule of rules) results.push({ rule, ok: await evalCondRule(rule, stepIndex, stepName) });
|
|
285748
286900
|
const passed2 = isAnd ? results.every((r) => r.ok) : results.some((r) => r.ok);
|
|
285749
286901
|
const failing = results.filter((r) => !r.ok);
|
|
285750
286902
|
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(", ")}`;
|
|
@@ -285754,7 +286906,17 @@ Output valid JSON only.
|
|
|
285754
286906
|
if (aborted) break;
|
|
285755
286907
|
const step = workflow.steps[i];
|
|
285756
286908
|
send("step_start", { index: i, step: { name: step.name, method: step.method, path: step.path } });
|
|
285757
|
-
|
|
286909
|
+
let result;
|
|
286910
|
+
try {
|
|
286911
|
+
result = await executeStep2(step, i);
|
|
286912
|
+
} catch (err) {
|
|
286913
|
+
if (err instanceof HumanInputAbort || err?.name === "HumanInputAbort") {
|
|
286914
|
+
send("aborted", { index: i, reason: err.message });
|
|
286915
|
+
res.end();
|
|
286916
|
+
return;
|
|
286917
|
+
}
|
|
286918
|
+
throw err;
|
|
286919
|
+
}
|
|
285758
286920
|
if (result.passed) passed++;
|
|
285759
286921
|
else failed++;
|
|
285760
286922
|
send("step_result", {
|
|
@@ -285769,7 +286931,7 @@ Output valid JSON only.
|
|
|
285769
286931
|
error: result.error
|
|
285770
286932
|
});
|
|
285771
286933
|
if (step.condition && !aborted) {
|
|
285772
|
-
const condResult = await evalCondition(step.condition);
|
|
286934
|
+
const condResult = await evalCondition(step.condition, i, step.name || `${step.method} ${step.path}`);
|
|
285773
286935
|
send("condition_result", {
|
|
285774
286936
|
index: i,
|
|
285775
286937
|
passed: condResult.passed,
|
|
@@ -285914,29 +287076,34 @@ Output valid JSON only.
|
|
|
285914
287076
|
}
|
|
285915
287077
|
});
|
|
285916
287078
|
try {
|
|
285917
|
-
let dashboardDistPath =
|
|
285918
|
-
|
|
285919
|
-
|
|
287079
|
+
let dashboardDistPath = null;
|
|
287080
|
+
try {
|
|
287081
|
+
const dashboardPackagePath = require.resolve("@jetic/dashboard/package.json");
|
|
287082
|
+
const workspaceDist = import_path.default.join(import_path.default.dirname(dashboardPackagePath), "dist");
|
|
287083
|
+
if (import_fs.default.existsSync(workspaceDist)) dashboardDistPath = workspaceDist;
|
|
287084
|
+
} catch {
|
|
285920
287085
|
}
|
|
285921
|
-
if (!
|
|
285922
|
-
|
|
285923
|
-
|
|
285924
|
-
dashboardDistPath = import_path.default.join(import_path.default.dirname(dashboardPackagePath), "dist");
|
|
285925
|
-
} catch {
|
|
285926
|
-
}
|
|
287086
|
+
if (!dashboardDistPath) {
|
|
287087
|
+
const bundled = import_path.default.join(__dirname, "dashboard");
|
|
287088
|
+
if (import_fs.default.existsSync(bundled)) dashboardDistPath = bundled;
|
|
285927
287089
|
}
|
|
285928
|
-
if (!
|
|
285929
|
-
|
|
287090
|
+
if (!dashboardDistPath) {
|
|
287091
|
+
const sibling = import_path.default.join(__dirname, "..", "dashboard");
|
|
287092
|
+
if (import_fs.default.existsSync(sibling)) dashboardDistPath = sibling;
|
|
287093
|
+
}
|
|
287094
|
+
if (!dashboardDistPath) {
|
|
287095
|
+
console.warn("Dashboard build not found. Build it with: pnpm --filter @jetic/dashboard run build");
|
|
287096
|
+
} else {
|
|
287097
|
+
app.use(import_express.default.static(dashboardDistPath));
|
|
287098
|
+
app.get("*", (_req, res) => {
|
|
287099
|
+
const indexPath = import_path.default.join(dashboardDistPath, "index.html");
|
|
287100
|
+
if (import_fs.default.existsSync(indexPath)) {
|
|
287101
|
+
res.sendFile(indexPath);
|
|
287102
|
+
} else {
|
|
287103
|
+
res.status(404).send("Dashboard not built yet.");
|
|
287104
|
+
}
|
|
287105
|
+
});
|
|
285930
287106
|
}
|
|
285931
|
-
app.use(import_express.default.static(dashboardDistPath));
|
|
285932
|
-
app.get("*", (_req, res) => {
|
|
285933
|
-
const indexPath = import_path.default.join(dashboardDistPath, "index.html");
|
|
285934
|
-
if (import_fs.default.existsSync(indexPath)) {
|
|
285935
|
-
res.sendFile(indexPath);
|
|
285936
|
-
} else {
|
|
285937
|
-
res.status(404).send("Dashboard not built yet.");
|
|
285938
|
-
}
|
|
285939
|
-
});
|
|
285940
287107
|
} catch (e) {
|
|
285941
287108
|
console.error("Could not find dashboard static files. Ensure it is built.", e);
|
|
285942
287109
|
}
|
|
@@ -286107,7 +287274,7 @@ var mcpCommand = new import_commander10.Command("mcp").description("Launch Model
|
|
|
286107
287274
|
|
|
286108
287275
|
// src/index.ts
|
|
286109
287276
|
var program = new import_commander11.Command();
|
|
286110
|
-
program.name("jetic").description("AI-Native API Behavior Testing").version("0.1.
|
|
287277
|
+
program.name("jetic").description("AI-Native API Behavior Testing").version("0.1.9");
|
|
286111
287278
|
program.addCommand(initCommand);
|
|
286112
287279
|
program.addCommand(scanCommand);
|
|
286113
287280
|
program.addCommand(inspectCommand);
|