jetic-cli 0.2.2 → 0.2.4

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/dist/index.js CHANGED
@@ -267984,57 +267984,116 @@ var require_workflow_simulator = __commonJS({
267984
267984
  for (let i = 0; i < workflow.steps.length; i++) {
267985
267985
  const step = workflow.steps[i];
267986
267986
  const stepStartTime = Date.now();
267987
+ if (step.condition) {
267988
+ const condResult = await this.evaluateCondition(step.condition);
267989
+ if (!condResult.matched) {
267990
+ stepResults.push({
267991
+ stepIndex: i + 1,
267992
+ name: step.name || `${step.method} ${step.path}`,
267993
+ method: step.method,
267994
+ path: step.path,
267995
+ targetUrl: "",
267996
+ expectedStatus: step.expectStatus ?? 200,
267997
+ actualStatus: 0,
267998
+ durationMs: Date.now() - stepStartTime,
267999
+ passed: true,
268000
+ skipped: true,
268001
+ retryCount: 0,
268002
+ conditionResult: condResult,
268003
+ requestHeaders: {},
268004
+ requestBody: {},
268005
+ responseBody: null,
268006
+ capturedMemory: []
268007
+ });
268008
+ continue;
268009
+ }
268010
+ }
267987
268011
  const { headers: injectedHeaders, body: injectedBody } = await this.resolveInjections(step.inject);
267988
268012
  const resolvedBody = await this.resolveBodyTemplates(step.body || {});
267989
268013
  const finalBody = { ...injectedBody, ...resolvedBody };
267990
268014
  const inputCaptured = await this.captureInputToMemory(step.captureInput, finalBody);
267991
268015
  const resolvedPath = await this.resolvePathParams(step.path, finalBody);
267992
268016
  const fullUrl = `${baseUrl.replace(/\/$/, "")}${resolvedPath}`;
267993
- const expectedStatus = step.expectStatus || 200;
268017
+ const expectedStatus = step.expectStatus ?? 200;
268018
+ const maxAttempts = Math.max(1, step.retry?.times ?? workflow.retry?.times ?? 1);
268019
+ const retryDelayMs = step.retry?.delayMs ?? workflow.retry?.delayMs ?? 0;
268020
+ let attempts = 0;
267994
268021
  let responseStatus = 0;
267995
268022
  let responseBody = null;
267996
268023
  let stepPassed = false;
267997
268024
  let errorMsg;
267998
- try {
267999
- const fetchOptions = {
268000
- method: step.method.toUpperCase(),
268001
- headers: {
268002
- "Content-Type": "application/json",
268003
- ...injectedHeaders
268004
- }
268005
- };
268006
- if (!["GET", "HEAD"].includes(step.method.toUpperCase()) && Object.keys(finalBody).length > 0) {
268007
- fetchOptions.body = JSON.stringify(finalBody);
268008
- }
268009
- let finalUrl = fullUrl;
268010
- if (["GET", "HEAD"].includes(step.method.toUpperCase()) && Object.keys(finalBody).length > 0) {
268011
- const params = new URLSearchParams();
268012
- for (const [k, v] of Object.entries(finalBody)) {
268013
- if (v !== void 0 && v !== null)
268014
- params.set(k, String(v));
268015
- }
268016
- finalUrl = `${fullUrl}?${params.toString()}`;
268017
- }
268018
- const resp = await fetch(finalUrl, fetchOptions);
268019
- responseStatus = resp.status;
268020
- const ct = resp.headers.get("content-type") || "";
268021
- if (ct.includes("application/json")) {
268022
- try {
268023
- responseBody = await resp.json();
268024
- } catch {
268025
- responseBody = null;
268025
+ while (attempts < maxAttempts) {
268026
+ attempts++;
268027
+ if (attempts > 1 && retryDelayMs > 0) {
268028
+ await new Promise((r) => setTimeout(r, retryDelayMs));
268029
+ }
268030
+ responseStatus = 0;
268031
+ responseBody = null;
268032
+ errorMsg = void 0;
268033
+ try {
268034
+ const fetchOptions = {
268035
+ method: step.method.toUpperCase(),
268036
+ headers: { "Content-Type": "application/json", ...injectedHeaders }
268037
+ };
268038
+ if (!["GET", "HEAD"].includes(step.method.toUpperCase()) && Object.keys(finalBody).length > 0) {
268039
+ fetchOptions.body = JSON.stringify(finalBody);
268040
+ }
268041
+ let finalUrl = fullUrl;
268042
+ if (["GET", "HEAD"].includes(step.method.toUpperCase()) && Object.keys(finalBody).length > 0) {
268043
+ const params = new URLSearchParams();
268044
+ for (const [k, v] of Object.entries(finalBody)) {
268045
+ if (v !== void 0 && v !== null)
268046
+ params.set(k, String(v));
268047
+ }
268048
+ finalUrl = `${fullUrl}?${params.toString()}`;
268049
+ }
268050
+ const resp = await fetch(finalUrl, fetchOptions);
268051
+ responseStatus = resp.status;
268052
+ const ct = resp.headers.get("content-type") || "";
268053
+ if (ct.includes("application/json")) {
268054
+ try {
268055
+ responseBody = await resp.json();
268056
+ } catch {
268057
+ responseBody = null;
268058
+ }
268059
+ } else {
268060
+ responseBody = await resp.text();
268026
268061
  }
268027
- } else {
268028
- responseBody = await resp.text();
268062
+ const in2xx = (s) => s >= 200 && s < 300;
268063
+ stepPassed = resp.status === expectedStatus || in2xx(resp.status) && in2xx(expectedStatus) || (step.continueOnStatus || []).includes(resp.status);
268064
+ if (stepPassed)
268065
+ break;
268066
+ } catch (err) {
268067
+ stepPassed = false;
268068
+ errorMsg = err.message || String(err);
268029
268069
  }
268030
- stepPassed = resp.status === expectedStatus || resp.status >= 200 && resp.status < 300 && expectedStatus >= 200 && expectedStatus < 300;
268031
- } catch (err) {
268032
- stepPassed = false;
268033
- errorMsg = err.message || String(err);
268034
268070
  }
268035
268071
  const responseCaptured = stepPassed ? await this.captureToMemory(step.capture, responseBody) : [];
268036
268072
  if (!stepPassed) {
268037
268073
  allPassed = false;
268074
+ const onFailure = step.onFailure ?? "abort";
268075
+ stepResults.push({
268076
+ stepIndex: i + 1,
268077
+ name: step.name || `${step.method} ${step.path}`,
268078
+ method: step.method,
268079
+ path: step.path,
268080
+ targetUrl: fullUrl,
268081
+ expectedStatus,
268082
+ actualStatus: responseStatus,
268083
+ durationMs: Date.now() - stepStartTime,
268084
+ passed: false,
268085
+ skipped: false,
268086
+ retryCount: attempts - 1,
268087
+ failureHandling: onFailure === "continue" ? "continued" : "aborted",
268088
+ requestHeaders: injectedHeaders,
268089
+ requestBody: finalBody,
268090
+ responseBody,
268091
+ capturedMemory: [...inputCaptured],
268092
+ error: errorMsg
268093
+ });
268094
+ if (onFailure === "abort")
268095
+ break;
268096
+ continue;
268038
268097
  }
268039
268098
  stepResults.push({
268040
268099
  stepIndex: i + 1,
@@ -268045,16 +268104,14 @@ var require_workflow_simulator = __commonJS({
268045
268104
  expectedStatus,
268046
268105
  actualStatus: responseStatus,
268047
268106
  durationMs: Date.now() - stepStartTime,
268048
- passed: stepPassed,
268107
+ passed: true,
268108
+ skipped: false,
268109
+ retryCount: attempts - 1,
268049
268110
  requestHeaders: injectedHeaders,
268050
268111
  requestBody: finalBody,
268051
268112
  responseBody,
268052
- capturedMemory: [...inputCaptured, ...responseCaptured],
268053
- error: errorMsg
268113
+ capturedMemory: [...inputCaptured, ...responseCaptured]
268054
268114
  });
268055
- if (!stepPassed) {
268056
- break;
268057
- }
268058
268115
  }
268059
268116
  const totalTimeMs = Date.now() - startTime;
268060
268117
  return {
@@ -268078,6 +268135,32 @@ var require_workflow_simulator = __commonJS({
268078
268135
  results: stepResults
268079
268136
  };
268080
268137
  }
268138
+ // ── Condition evaluator ────────────────────────────────────────────────────
268139
+ // Resolves the `if` template and applies the declared operator. Returns
268140
+ // `matched: true` when the condition passes (step should execute).
268141
+ async evaluateCondition(condition) {
268142
+ const evaluated = await this.resolveTemplateString(condition.if);
268143
+ let matched;
268144
+ if (condition.exists !== void 0) {
268145
+ const hasValue = evaluated !== "" && evaluated !== "null" && evaluated !== "undefined";
268146
+ matched = condition.exists ? hasValue : !hasValue;
268147
+ } else if (condition.equals !== void 0) {
268148
+ matched = evaluated === condition.equals;
268149
+ } else if (condition.notEquals !== void 0) {
268150
+ matched = evaluated !== condition.notEquals;
268151
+ } else if (condition.contains !== void 0) {
268152
+ matched = evaluated.includes(condition.contains);
268153
+ } else if (condition.greaterThan !== void 0) {
268154
+ matched = Number(evaluated) > condition.greaterThan;
268155
+ } else if (condition.lessThan !== void 0) {
268156
+ matched = Number(evaluated) < condition.lessThan;
268157
+ } else if (condition.in !== void 0) {
268158
+ matched = condition.in.includes(evaluated);
268159
+ } else {
268160
+ matched = evaluated !== "" && evaluated !== "false" && evaluated !== "0" && evaluated !== "null" && evaluated !== "undefined";
268161
+ }
268162
+ return { matched, evaluated };
268163
+ }
268081
268164
  async resolveTemplateString(value) {
268082
268165
  const templateRe = /\{\{([^}]+)\}\}/g;
268083
268166
  let match;
@@ -269549,28 +269632,47 @@ var require_workflow_authoring_tools = __commonJS({
269549
269632
  var path7 = __importStar(require("path"));
269550
269633
  var types_1 = require_types();
269551
269634
  var workflow_tools_1 = require_workflow_tools();
269635
+ var stepConditionSchema = zod_1.z.object({
269636
+ if: zod_1.z.string().min(1).describe('Template expression to resolve, e.g. "{{workflow:role}}"'),
269637
+ equals: zod_1.z.string().optional().describe("Resolved value must equal this string"),
269638
+ notEquals: zod_1.z.string().optional().describe("Resolved value must NOT equal this string"),
269639
+ contains: zod_1.z.string().optional().describe("Resolved value must contain this substring"),
269640
+ exists: zod_1.z.boolean().optional().describe("true: value must be non-empty; false: value must be empty"),
269641
+ greaterThan: zod_1.z.number().optional().describe("Numeric greater-than comparison"),
269642
+ lessThan: zod_1.z.number().optional().describe("Numeric less-than comparison"),
269643
+ in: zod_1.z.array(zod_1.z.string()).optional().describe("Resolved value must be one of these strings")
269644
+ }).describe("Skip this step when the condition is not met");
269645
+ var stepRetrySchema = zod_1.z.object({
269646
+ times: zod_1.z.number().int().positive().max(10).describe("Max retry attempts (1\u201310)"),
269647
+ delayMs: zod_1.z.number().int().nonnegative().optional().describe("Delay between retries in ms (default 0)")
269648
+ });
269552
269649
  exports2.workflowStepSchema = zod_1.z.object({
269553
269650
  name: zod_1.z.string().min(1).describe("Step name (must be unique within the workflow)"),
269554
269651
  method: zod_1.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]).describe("HTTP method"),
269555
269652
  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)"),
269556
269653
  description: zod_1.z.string().optional().describe("What this step does"),
269557
- body: zod_1.z.record(zod_1.z.any()).optional().describe("Request payload. Values support {{faker.*}}, {{scope:key}}, {{workflow:key}} and {{human:key}} templates (human pauses for interactive input at runtime)"),
269558
- inject: zod_1.z.record(zod_1.z.string()).optional().describe('Memory/header injection: "header:Name" or "body:field" (or bare header name) -> "scope:key" or "{{template}}" ({{human:key}} supported)'),
269559
- capture: zod_1.z.record(zod_1.z.string()).optional().describe('Save RESPONSE fields to memory: "scope:key" -> "dot.path.in.response"'),
269560
- captureInput: zod_1.z.record(zod_1.z.string()).optional().describe('Save REQUEST body fields to memory BEFORE sending: "scope:key" -> "bodyField"'),
269561
- expectStatus: zod_1.z.number().int().positive().optional().describe("Expected HTTP status (default 200; any 2xx passes when expecting 2xx)")
269654
+ body: zod_1.z.record(zod_1.z.any()).optional().describe("Request payload. Values support {{faker.*}}, {{scope:key}}, {{workflow:key}} and {{human:key}} templates"),
269655
+ inject: zod_1.z.record(zod_1.z.string()).optional().describe('Header/body injection: "header:Name" or "body:field" \u2192 "scope:key" or "{{template}}"'),
269656
+ capture: zod_1.z.record(zod_1.z.string()).optional().describe('Save RESPONSE fields to memory: "scope:key" \u2192 "dot.path.in.response"'),
269657
+ captureInput: zod_1.z.record(zod_1.z.string()).optional().describe('Save REQUEST body fields to memory BEFORE sending: "scope:key" \u2192 "bodyField"'),
269658
+ expectStatus: zod_1.z.number().int().positive().optional().describe("Expected HTTP status (default 200; any 2xx passes when expecting 2xx)"),
269659
+ condition: stepConditionSchema.optional().describe('Skip this step when condition is not met. Use {{workflow:key}} in the "if" field.'),
269660
+ onFailure: zod_1.z.enum(["abort", "continue"]).optional().describe('"abort" (default): stop the workflow on failure. "continue": log failure and proceed (captures skipped).'),
269661
+ continueOnStatus: zod_1.z.array(zod_1.z.number().int()).optional().describe("Additional HTTP status codes treated as pass for this step (e.g. [404])."),
269662
+ retry: stepRetrySchema.optional().describe("Retry on failure \u2014 overrides the workflow-level global retry.")
269562
269663
  // NOTE: .passthrough() is intentional — unknown keys must REACH the validator
269563
- // (MODEL_DIALECT_KEY / UNKNOWN_STEP_KEY diagnostics). Zod's default strip
269664
+ // (MODEL_DIALECT_KEY / UNKNOWN_STEP_KEY diagnostics). Zod’s default strip
269564
269665
  // behavior would silently swallow the exact mistakes we need to report.
269565
269666
  }).passthrough();
269566
269667
  var inlineWorkflowSchema = zod_1.z.object({
269567
269668
  name: zod_1.z.string().min(1).describe("Workflow name (also used to derive the file slug)"),
269568
269669
  description: zod_1.z.string().optional(),
269569
269670
  environment: zod_1.z.string().optional().describe("Target environment name from the model (e.g. local)"),
269671
+ retry: stepRetrySchema.optional().describe("Global retry default applied to all steps (overridden per step)"),
269570
269672
  steps: zod_1.z.array(exports2.workflowStepSchema).min(1).describe("Ordered steps; later steps can use memory captured by earlier ones")
269571
269673
  }).passthrough();
269572
269674
  var TEMPLATE_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
269573
- var KNOWN_WORKFLOW_KEYS = /* @__PURE__ */ new Set(["name", "description", "environment", "steps", "generatedAt"]);
269675
+ var KNOWN_WORKFLOW_KEYS = /* @__PURE__ */ new Set(["name", "description", "environment", "steps", "generatedAt", "retry"]);
269574
269676
  var KNOWN_STEP_KEYS = /* @__PURE__ */ new Set([
269575
269677
  "name",
269576
269678
  "method",
@@ -269581,7 +269683,10 @@ var require_workflow_authoring_tools = __commonJS({
269581
269683
  "captureInput",
269582
269684
  "expectStatus",
269583
269685
  "body",
269584
- "condition"
269686
+ "condition",
269687
+ "onFailure",
269688
+ "continueOnStatus",
269689
+ "retry"
269585
269690
  ]);
269586
269691
  var MODEL_DIALECT_KEYS = /* @__PURE__ */ new Set(["call", "auth", "bind"]);
269587
269692
  function err(list, stepIndex, stepName, code, message, fix) {
@@ -269677,8 +269782,41 @@ var require_workflow_authoring_tools = __commonJS({
269677
269782
  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).");
269678
269783
  }
269679
269784
  }
269680
- if ("condition" in step) {
269681
- 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.");
269785
+ if ("condition" in step && step.condition !== void 0) {
269786
+ const cond = step.condition;
269787
+ if (!cond || typeof cond !== "object" || Array.isArray(cond)) {
269788
+ err(errors, stepNo, sName, "BAD_CONDITION", `Step ${stepNo} "condition" must be an object.`, 'Use { "condition": { "if": "{{workflow:role}}", "equals": "admin" } }.');
269789
+ } else if (!cond.if || typeof cond.if !== "string" || !cond.if.trim()) {
269790
+ err(errors, stepNo, sName, "BAD_CONDITION", `Step ${stepNo} "condition" must have a non-empty "if" field.`, 'Set "if" to a template expression like "{{workflow:role}}".');
269791
+ } else {
269792
+ const operators = ["equals", "notEquals", "contains", "exists", "greaterThan", "lessThan", "in"];
269793
+ const usedOps = operators.filter((op) => op in cond && cond[op] !== void 0);
269794
+ if (usedOps.length === 0) {
269795
+ warn(warnings, stepNo, sName, "CONDITION_NO_OPERATOR", `Step ${stepNo} "condition" has no comparison operator \u2014 uses bare truthy check.`, "Add one of: equals, notEquals, contains, exists, greaterThan, lessThan, in for explicit logic.");
269796
+ }
269797
+ if (!/\{\{[^}]+\}\}/.test(cond.if)) {
269798
+ warn(warnings, stepNo, sName, "CONDITION_BARE_IF", `Step ${stepNo} "condition.if" ("${cond.if}") contains no {{template}} and will be compared literally.`, "Use {{workflow:key}} or {{scope:key}} to reference captured memory values.");
269799
+ }
269800
+ const condRefs = [];
269801
+ collectTemplateRefs(cond.if, condRefs);
269802
+ for (const ref of condRefs) {
269803
+ if (ref.startsWith("faker."))
269804
+ continue;
269805
+ if (!ref.includes(":"))
269806
+ continue;
269807
+ const { scope, key } = splitMemKey(ref);
269808
+ if (!key)
269809
+ continue;
269810
+ if (scope === "human") {
269811
+ humanKeys.add(key);
269812
+ continue;
269813
+ }
269814
+ const full = `${scope}:${key}`;
269815
+ if (!defined.has(full) && !persistedMemory.has(full)) {
269816
+ err(errors, stepNo, sName, "UNDEFINED_MEMORY_REF", `"{{${ref}}}" in condition.if of step ${stepNo} is not captured by any earlier step and is not in memory.json.`, `Capture "${full}" in an earlier step before referencing it in a condition.`);
269817
+ }
269818
+ }
269819
+ }
269682
269820
  }
269683
269821
  if (!sName || !sName.trim()) {
269684
269822
  err(errors, stepNo, void 0, "STEP_MISSING_NAME", `Step ${stepNo} needs a non-empty "name".`, "Give every step a unique descriptive name.");
@@ -269804,6 +269942,40 @@ var require_workflow_authoring_tools = __commonJS({
269804
269942
  }
269805
269943
  }
269806
269944
  }
269945
+ if (step.onFailure !== void 0) {
269946
+ if (!["abort", "continue"].includes(String(step.onFailure))) {
269947
+ err(errors, stepNo, sName, "BAD_ON_FAILURE", `Step ${stepNo} "onFailure" must be "abort" or "continue" (got "${step.onFailure}").`, 'Use "abort" (default, stops the workflow) or "continue" (logs failure, proceeds).');
269948
+ }
269949
+ if (step.onFailure === "continue" && step.capture && Object.keys(step.capture).length > 0) {
269950
+ warn(warnings, stepNo, sName, "CONTINUE_WITH_CAPTURE", `Step ${stepNo} uses onFailure:"continue" but has captures \u2014 captures are only saved when the step passes.`, "This is usually fine for optional captures; ensure later steps handle missing memory gracefully.");
269951
+ }
269952
+ }
269953
+ if (step.continueOnStatus !== void 0) {
269954
+ if (!Array.isArray(step.continueOnStatus)) {
269955
+ err(errors, stepNo, sName, "BAD_CONTINUE_ON_STATUS", `Step ${stepNo} "continueOnStatus" must be an array of HTTP status codes.`, "Use [404] or [404, 409].");
269956
+ } else {
269957
+ for (const code of step.continueOnStatus) {
269958
+ if (typeof code !== "number" || code < 100 || code > 599) {
269959
+ err(errors, stepNo, sName, "BAD_CONTINUE_ON_STATUS", `Step ${stepNo} "continueOnStatus" contains invalid status code: ${code}.`, "HTTP status codes must be integers between 100 and 599.");
269960
+ }
269961
+ }
269962
+ if (step.continueOnStatus.some((c3) => c3 >= 500)) {
269963
+ warn(warnings, stepNo, sName, "CONTINUE_ON_5XX", `Step ${stepNo} "continueOnStatus" includes 5xx server error codes \u2014 accepting server errors as pass is unusual.`, "Verify this is intentional (e.g. for idempotency checks or async polling).");
269964
+ }
269965
+ }
269966
+ }
269967
+ if (step.retry !== void 0) {
269968
+ const r = step.retry;
269969
+ if (!r || typeof r !== "object" || Array.isArray(r)) {
269970
+ err(errors, stepNo, sName, "BAD_RETRY", `Step ${stepNo} "retry" must be an object { times, delayMs? }.`, 'Use { "retry": { "times": 3, "delayMs": 1000 } }.');
269971
+ } else {
269972
+ if (typeof r.times !== "number" || r.times < 1) {
269973
+ err(errors, stepNo, sName, "BAD_RETRY", `Step ${stepNo} "retry.times" must be a positive integer.`, "Set retry.times to 1 or more.");
269974
+ } else if (r.times > 5) {
269975
+ warn(warnings, stepNo, sName, "HIGH_RETRY_COUNT", `Step ${stepNo} "retry.times" is ${r.times} \u2014 this may slow down workflows significantly.`, "Keep retry.times \u2264 5 for most use cases.");
269976
+ }
269977
+ }
269978
+ }
269807
269979
  for (const field of ["capture", "captureInput"]) {
269808
269980
  const cap = step[field];
269809
269981
  if (cap && typeof cap === "object" && !Array.isArray(cap)) {
@@ -270114,9 +270286,11 @@ var require_project_tools = __commonJS({
270114
270286
  };
270115
270287
  })();
270116
270288
  Object.defineProperty(exports2, "__esModule", { value: true });
270117
- exports2.scanProjectSchema = exports2.initProjectSchema = void 0;
270289
+ exports2.clearChangesSchema = exports2.getChangesSchema = exports2.scanProjectSchema = exports2.initProjectSchema = void 0;
270118
270290
  exports2.handleInitProject = handleInitProject;
270119
270291
  exports2.handleScanProject = handleScanProject;
270292
+ exports2.handleGetChanges = handleGetChanges;
270293
+ exports2.handleClearChanges = handleClearChanges;
270120
270294
  var zod_1 = require("zod");
270121
270295
  var fs4 = __importStar(require("fs"));
270122
270296
  var path7 = __importStar(require("path"));
@@ -270278,6 +270452,16 @@ var require_project_tools = __commonJS({
270278
270452
  existing.project = { ...scanned.project, ...existing.project };
270279
270453
  }
270280
270454
  (0, types_1.saveModel)(filePath, existing);
270455
+ const changesPath = path7.join(path7.dirname(filePath), "changes.json");
270456
+ if (fs4.existsSync(changesPath)) {
270457
+ try {
270458
+ const changesData = JSON.parse(fs4.readFileSync(changesPath, "utf-8"));
270459
+ changesData.changes = [];
270460
+ changesData.watchedSince = (/* @__PURE__ */ new Date()).toISOString();
270461
+ fs4.writeFileSync(changesPath, JSON.stringify(changesData, null, 2), "utf-8");
270462
+ } catch {
270463
+ }
270464
+ }
270281
270465
  const total = existing.endpoints.length;
270282
270466
  return {
270283
270467
  success: true,
@@ -270291,6 +270475,64 @@ var require_project_tools = __commonJS({
270291
270475
  next: total === 0 ? "No endpoints yet \u2014 model them manually with jetic_add_endpoint, then jetic_verify_model." : `Next: refine with jetic_add_endpoint / jetic_update_endpoint (AI enrichment is skipped in scans \u2014 add constraints, examples, ownership by hand), then jetic_verify_model until clean. Step 3 (only on request): workflows via jetic_validate_workflow \u2192 jetic_create_workflow \u2192 jetic_simulate_workflow.`
270292
270476
  };
270293
270477
  }
270478
+ exports2.getChangesSchema = zod_1.z.object({
270479
+ projectPath: zod_1.z.string().optional().describe("Path to project root (defaults to JETIC_PROJECT_PATH / cwd)")
270480
+ });
270481
+ function handleGetChanges(args) {
270482
+ const filePath = (0, types_1.resolveModelPath)(args.projectPath);
270483
+ const jeticDir = path7.dirname(filePath);
270484
+ const changesPath = path7.join(jeticDir, "changes.json");
270485
+ if (!fs4.existsSync(changesPath)) {
270486
+ return {
270487
+ hasChanges: false,
270488
+ changes: [],
270489
+ watchedSince: null,
270490
+ message: "No changes.json found. Start `jetic dev` to enable automatic file change tracking."
270491
+ };
270492
+ }
270493
+ let data;
270494
+ try {
270495
+ data = JSON.parse(fs4.readFileSync(changesPath, "utf-8"));
270496
+ } catch {
270497
+ return { hasChanges: false, changes: [], watchedSince: null, message: "changes.json is unreadable or corrupted." };
270498
+ }
270499
+ const changes = Array.isArray(data.changes) ? data.changes : [];
270500
+ return {
270501
+ hasChanges: changes.length > 0,
270502
+ totalChanged: changes.length,
270503
+ watchedSince: data.watchedSince ?? null,
270504
+ projectRoot: data.projectRoot ?? jeticDir,
270505
+ changes,
270506
+ filePaths: changes.map((c3) => c3.filePath),
270507
+ hint: changes.length > 0 ? `Read ONLY these ${changes.length} file(s) to update model.json \u2014 no need to scan the entire codebase. After updating, call jetic_clear_changes.` : "No file changes recorded since last scan. Model is up-to-date."
270508
+ };
270509
+ }
270510
+ exports2.clearChangesSchema = zod_1.z.object({
270511
+ projectPath: zod_1.z.string().optional().describe("Path to project root (defaults to JETIC_PROJECT_PATH / cwd)")
270512
+ });
270513
+ function handleClearChanges(args) {
270514
+ const filePath = (0, types_1.resolveModelPath)(args.projectPath);
270515
+ const jeticDir = path7.dirname(filePath);
270516
+ const changesPath = path7.join(jeticDir, "changes.json");
270517
+ if (!fs4.existsSync(changesPath)) {
270518
+ return { success: true, message: "changes.json did not exist \u2014 nothing to clear." };
270519
+ }
270520
+ try {
270521
+ const data = JSON.parse(fs4.readFileSync(changesPath, "utf-8"));
270522
+ const cleared = Array.isArray(data.changes) ? data.changes.length : 0;
270523
+ data.changes = [];
270524
+ data.watchedSince = (/* @__PURE__ */ new Date()).toISOString();
270525
+ fs4.writeFileSync(changesPath, JSON.stringify(data, null, 2), "utf-8");
270526
+ return {
270527
+ success: true,
270528
+ cleared,
270529
+ watchedSince: data.watchedSince,
270530
+ message: `Cleared ${cleared} change record(s). ChangeScanner will now track future edits from scratch.`
270531
+ };
270532
+ } catch (err) {
270533
+ throw new Error(`Failed to clear changes.json: ${err.message}`);
270534
+ }
270535
+ }
270294
270536
  }
270295
270537
  });
270296
270538
 
@@ -284410,6 +284652,284 @@ var require_session_tools = __commonJS({
284410
284652
  }
284411
284653
  });
284412
284654
 
284655
+ // ../../packages/mcp-server/dist/tools/workflow-designer-tools.js
284656
+ var require_workflow_designer_tools = __commonJS({
284657
+ "../../packages/mcp-server/dist/tools/workflow-designer-tools.js"(exports2) {
284658
+ "use strict";
284659
+ init_cjs_shims();
284660
+ Object.defineProperty(exports2, "__esModule", { value: true });
284661
+ exports2.scaffoldWorkflowSchema = void 0;
284662
+ exports2.handleScaffoldWorkflow = handleScaffoldWorkflow;
284663
+ var zod_1 = require("zod");
284664
+ var types_1 = require_types();
284665
+ function fieldToFakerValue(name, field) {
284666
+ if (field.enum && field.enum.length > 0)
284667
+ return field.enum[0];
284668
+ if (field.format === "email")
284669
+ return "{{faker.internet.email}}";
284670
+ if (field.format === "uuid")
284671
+ return "{{faker.string.uuid}}";
284672
+ if (field.format === "date")
284673
+ return "{{faker.date.past}}";
284674
+ if (field.format === "date-time")
284675
+ return "{{faker.date.recent}}";
284676
+ if (field.format === "uri" || field.format === "url")
284677
+ return "{{faker.internet.url}}";
284678
+ if (field.format === "hostname")
284679
+ return "{{faker.internet.domainName}}";
284680
+ if (field.format === "ipv4")
284681
+ return "{{faker.internet.ip}}";
284682
+ const n = name.toLowerCase().replace(/[_-]/g, "");
284683
+ if (n === "email" || n.endsWith("email"))
284684
+ return "{{faker.internet.email}}";
284685
+ if (n.includes("password") || n.includes("pwd"))
284686
+ return "{{faker.internet.password}}";
284687
+ if (n === "phone" || n.includes("phone") || n.includes("mobile"))
284688
+ return "{{faker.phone.number}}";
284689
+ if (n === "name" || n === "fullname" || n === "displayname")
284690
+ return "{{faker.person.fullName}}";
284691
+ if (n === "firstname")
284692
+ return "{{faker.person.firstName}}";
284693
+ if (n === "lastname")
284694
+ return "{{faker.person.lastName}}";
284695
+ if (n === "username")
284696
+ return "{{faker.internet.username}}";
284697
+ if (n === "title")
284698
+ return "{{faker.lorem.sentence}}";
284699
+ if (n === "description" || n === "bio" || n === "summary")
284700
+ return "{{faker.lorem.paragraph}}";
284701
+ if (n === "url" || n.endsWith("url") || n === "avatar")
284702
+ return "{{faker.internet.url}}";
284703
+ if (n === "company" || n.includes("company") || n.includes("organization") || n.includes("workspace"))
284704
+ return "{{faker.company.name}}";
284705
+ if (n === "city")
284706
+ return "{{faker.location.city}}";
284707
+ if (n === "country")
284708
+ return "{{faker.location.country}}";
284709
+ if (n.includes("address"))
284710
+ return "{{faker.location.streetAddress}}";
284711
+ if (n === "zip" || n === "zipcode" || n.includes("postal"))
284712
+ return "{{faker.location.zipCode}}";
284713
+ if (n === "state" || n === "province")
284714
+ return "{{faker.location.state}}";
284715
+ if (n === "latitude" || n === "lat")
284716
+ return "{{faker.location.latitude}}";
284717
+ if (n === "longitude" || n === "lon" || n === "lng")
284718
+ return "{{faker.location.longitude}}";
284719
+ if (n === "color" || n === "colour")
284720
+ return "{{faker.color.human}}";
284721
+ if (n === "amount" || n === "price")
284722
+ return field.min ?? 1;
284723
+ if (n === "quantity" || n === "count" || n === "limit")
284724
+ return field.min ?? 10;
284725
+ if (n === "slug")
284726
+ return "{{faker.lorem.slug}}";
284727
+ if (n === "token" || n === "secret" || n === "key" || n === "otp" || n === "code" || n === "pin")
284728
+ return null;
284729
+ if (field.type === "string")
284730
+ return "{{faker.lorem.word}}";
284731
+ if (field.type === "number" || field.type === "integer")
284732
+ return field.min ?? 1;
284733
+ if (field.type === "boolean")
284734
+ return true;
284735
+ return null;
284736
+ }
284737
+ function tokenMemoryKey(schemeName, fieldPath) {
284738
+ const last = fieldPath.split(".").pop() || schemeName;
284739
+ return last.charAt(0).toLowerCase() + last.slice(1);
284740
+ }
284741
+ function isReuseCandidate(name) {
284742
+ const n = name.toLowerCase().replace(/[_-]/g, "");
284743
+ return ["email", "password", "username", "phone", "name", "id", "slug"].some((k) => n === k || n.endsWith(k));
284744
+ }
284745
+ exports2.scaffoldWorkflowSchema = zod_1.z.object({
284746
+ projectPath: zod_1.z.string().optional().describe("Path to project root or .jetic folder"),
284747
+ endpoints: zod_1.z.array(zod_1.z.union([
284748
+ zod_1.z.string().describe('Method + path string, e.g. "POST /api/auth/login"'),
284749
+ zod_1.z.object({ method: zod_1.z.string(), path: zod_1.z.string() })
284750
+ ])).min(1).describe("ORDERED list of endpoints to scaffold into steps. Order matters: token-producer endpoints (e.g. login) must come before consumers (e.g. protected routes)."),
284751
+ workflowName: zod_1.z.string().optional().describe("Suggested workflow name"),
284752
+ environment: zod_1.z.string().optional().describe("Target environment name from model")
284753
+ });
284754
+ function handleScaffoldWorkflow(args) {
284755
+ const { model } = (0, types_1.loadModel)(args.projectPath);
284756
+ const requested = args.endpoints.map((ep) => {
284757
+ if (typeof ep === "string") {
284758
+ const parts = ep.trim().split(/\s+/);
284759
+ return { method: (parts[0] || "GET").toUpperCase(), path: parts.slice(1).join(" ") || "/" };
284760
+ }
284761
+ return { method: ep.method.toUpperCase(), path: ep.path };
284762
+ });
284763
+ const securityMemKeys = {};
284764
+ for (const [schemeName, scheme] of Object.entries(model.securitySchemes || {})) {
284765
+ if (scheme.obtainedFrom) {
284766
+ securityMemKeys[schemeName] = {
284767
+ memKey: tokenMemoryKey(schemeName, scheme.obtainedFrom.field),
284768
+ producedByEndpoint: scheme.obtainedFrom.endpoint.toUpperCase().trim(),
284769
+ fieldPath: scheme.obtainedFrom.field
284770
+ };
284771
+ }
284772
+ }
284773
+ const tokenProducers = {};
284774
+ for (const si of Object.values(securityMemKeys)) {
284775
+ if (!tokenProducers[si.producedByEndpoint])
284776
+ tokenProducers[si.producedByEndpoint] = [];
284777
+ tokenProducers[si.producedByEndpoint].push({ memKey: si.memKey, fieldPath: si.fieldPath });
284778
+ }
284779
+ const draftSteps = [];
284780
+ const missingFields = {};
284781
+ const allHumanKeys = /* @__PURE__ */ new Set();
284782
+ const securityChain = [];
284783
+ const producedKeys = /* @__PURE__ */ new Set();
284784
+ for (let idx = 0; idx < requested.length; idx++) {
284785
+ const req = requested[idx];
284786
+ const routeKey = `${req.method} ${req.path}`;
284787
+ const routeKeyNorm = routeKey.toUpperCase().trim();
284788
+ const endpoint = (model.endpoints || []).find((e) => e.method.toUpperCase() === req.method && e.path === req.path);
284789
+ if (!endpoint) {
284790
+ draftSteps.push({
284791
+ name: routeKey,
284792
+ method: req.method,
284793
+ path: req.path,
284794
+ description: `WARNING: "${routeKey}" not found in model.json \u2014 add it with jetic_add_endpoint first.`,
284795
+ expectStatus: 200
284796
+ });
284797
+ missingFields[routeKey] = [`Endpoint "${routeKey}" not found in model.json.`];
284798
+ continue;
284799
+ }
284800
+ const step = {
284801
+ name: endpoint.name || routeKey,
284802
+ method: req.method,
284803
+ path: endpoint.path
284804
+ };
284805
+ if (endpoint.description)
284806
+ step.description = endpoint.description;
284807
+ const missing = [];
284808
+ if (endpoint.requestBody?.fields && Object.keys(endpoint.requestBody.fields).length > 0) {
284809
+ const body = {};
284810
+ for (const [fname, fdef] of Object.entries(endpoint.requestBody.fields)) {
284811
+ let fromMem = null;
284812
+ if (endpoint.consumes) {
284813
+ for (const c3 of endpoint.consumes) {
284814
+ if (c3.usedAs === fname || c3.variable === fname) {
284815
+ const k = `workflow:${c3.variable}`;
284816
+ if (producedKeys.has(k)) {
284817
+ fromMem = k;
284818
+ break;
284819
+ }
284820
+ }
284821
+ }
284822
+ }
284823
+ if (!fromMem) {
284824
+ for (const pk of producedKeys) {
284825
+ const pkName = pk.split(":").pop() || "";
284826
+ if (pkName.toLowerCase() === fname.toLowerCase()) {
284827
+ fromMem = pk;
284828
+ break;
284829
+ }
284830
+ }
284831
+ }
284832
+ if (fromMem) {
284833
+ body[fname] = `{{${fromMem}}}`;
284834
+ continue;
284835
+ }
284836
+ const val = fieldToFakerValue(fname, fdef);
284837
+ if (val !== null) {
284838
+ body[fname] = val;
284839
+ } else {
284840
+ body[fname] = `{{human:${fname}}}`;
284841
+ allHumanKeys.add(fname);
284842
+ missing.push(`"${fname}" (type: ${fdef.type || "unknown"}) \u2014 no auto-mapping. Using {{human:${fname}}} (will pause for interactive input at runtime).`);
284843
+ }
284844
+ }
284845
+ step.body = body;
284846
+ const ci = {};
284847
+ for (const [fname, fdef] of Object.entries(endpoint.requestBody.fields)) {
284848
+ if (fdef.required && isReuseCandidate(fname)) {
284849
+ const k = `workflow:${fname}`;
284850
+ if (!producedKeys.has(k)) {
284851
+ ci[k] = fname;
284852
+ producedKeys.add(k);
284853
+ }
284854
+ }
284855
+ }
284856
+ if (Object.keys(ci).length > 0)
284857
+ step.captureInput = ci;
284858
+ }
284859
+ if (endpoint.security && endpoint.security.length > 0) {
284860
+ const inject = {};
284861
+ for (const sec of endpoint.security) {
284862
+ if (sec.scheme === "none")
284863
+ continue;
284864
+ const si = securityMemKeys[sec.scheme];
284865
+ if (!si) {
284866
+ missing.push(`Security scheme "${sec.scheme}" has no obtainedFrom in model.securitySchemes \u2014 add inject manually.`);
284867
+ continue;
284868
+ }
284869
+ const tokenKey = `workflow:${si.memKey}`;
284870
+ const available = producedKeys.has(tokenKey) || requested.slice(0, idx).some((r) => `${r.method} ${r.path}`.toUpperCase().trim() === si.producedByEndpoint);
284871
+ if (available) {
284872
+ const rawScheme = model.securitySchemes?.[sec.scheme];
284873
+ const prefix = rawScheme?.valuePrefix != null ? rawScheme.valuePrefix : rawScheme?.scheme?.toLowerCase() === "bearer" ? "Bearer " : "";
284874
+ inject["header:Authorization"] = `${prefix}{{${tokenKey}}}`;
284875
+ securityChain.push(`Step ${idx + 1} (${routeKey}) \u2190 inject ${tokenKey} (produced by ${si.producedByEndpoint})`);
284876
+ } else {
284877
+ missing.push(`Security scheme "${sec.scheme}" requires "${tokenKey}" from "${si.producedByEndpoint}" \u2014 place that endpoint earlier in the list.`);
284878
+ }
284879
+ }
284880
+ if (Object.keys(inject).length > 0)
284881
+ step.inject = inject;
284882
+ }
284883
+ const capture = {};
284884
+ if (endpoint.produces) {
284885
+ for (const p of endpoint.produces) {
284886
+ if (p.variable && p.responseField) {
284887
+ const k = `workflow:${p.variable}`;
284888
+ capture[k] = p.responseField;
284889
+ producedKeys.add(k);
284890
+ }
284891
+ }
284892
+ }
284893
+ if (tokenProducers[routeKeyNorm]) {
284894
+ for (const { memKey, fieldPath } of tokenProducers[routeKeyNorm]) {
284895
+ const k = `workflow:${memKey}`;
284896
+ if (!capture[k]) {
284897
+ capture[k] = fieldPath;
284898
+ producedKeys.add(k);
284899
+ securityChain.push(`Step ${idx + 1} (${routeKey}) \u2192 produces ${k} from response.${fieldPath}`);
284900
+ }
284901
+ }
284902
+ }
284903
+ if (Object.keys(capture).length > 0)
284904
+ step.capture = capture;
284905
+ const codes = Object.keys(endpoint.responses || {}).map(Number).filter(Boolean);
284906
+ const successCode = codes.find((c3) => c3 >= 200 && c3 < 300);
284907
+ step.expectStatus = successCode ?? 200;
284908
+ if (missing.length > 0)
284909
+ missingFields[routeKey] = missing;
284910
+ draftSteps.push(step);
284911
+ }
284912
+ const defaultName = args.workflowName || requested.map((r) => r.path.split("/").filter(Boolean).pop() || r.path).join(" \u2192 ") + " workflow";
284913
+ return {
284914
+ workflowName: defaultName,
284915
+ ...args.environment ? { environment: args.environment } : {},
284916
+ draftSteps,
284917
+ stepsCount: draftSteps.length,
284918
+ missingFields: Object.keys(missingFields).length > 0 ? missingFields : void 0,
284919
+ humanKeys: [...allHumanKeys].sort(),
284920
+ securityChain: securityChain.length > 0 ? securityChain : void 0,
284921
+ instructions: [
284922
+ "1. Review draftSteps. Resolve any missingFields entries before proceeding.",
284923
+ "2. humanKeys will pause the workflow at runtime for interactive input \u2014 they are intentional when needed.",
284924
+ '3. Validate: jetic_validate_workflow { definition: { name: "<workflowName>", steps: <draftSteps> } }',
284925
+ '4. Save: jetic_create_workflow { name: "<workflowName>", steps: <draftSteps> }',
284926
+ '5. Run: jetic_simulate_workflow { workflow: "<slug>" }'
284927
+ ].join("\n")
284928
+ };
284929
+ }
284930
+ }
284931
+ });
284932
+
284413
284933
  // ../../packages/mcp-server/dist/server.js
284414
284934
  var require_server3 = __commonJS({
284415
284935
  "../../packages/mcp-server/dist/server.js"(exports2) {
@@ -284471,6 +284991,7 @@ var require_server3 = __commonJS({
284471
284991
  var workflow_authoring_tools_1 = require_workflow_authoring_tools();
284472
284992
  var project_tools_1 = require_project_tools();
284473
284993
  var session_tools_1 = require_session_tools();
284994
+ var workflow_designer_tools_1 = require_workflow_designer_tools();
284474
284995
  exports2.JETIC_MCP_SERVER_NAME = "jetic-mcp-server";
284475
284996
  function getJeticMcpVersion() {
284476
284997
  try {
@@ -284492,7 +285013,7 @@ var require_server3 = __commonJS({
284492
285013
  "\u2501\u2501 PHASE 2 \u2014 SCAN \u2501\u2501",
284493
285014
  "Populate the model with endpoints:",
284494
285015
  " \u2022 Express+TypeScript (tsconfig.json present): call `jetic_scan` (static, keyless, no AI provider needed).",
284495
- " \u2022 Any other stack or scan gaps: read the source code and call `jetic_add_endpoint` for every route (include middleware chains, security, constraints, pagination, produces/consumes).",
285016
+ " \u2022 Any other stack or scan gaps: read source code and call `jetic_add_endpoint` for every route (middleware chains, security, constraints, pagination, produces/consumes).",
284496
285017
  "Gate: DO NOT advance to Phase 3 until endpointsCount > 0.",
284497
285018
  "",
284498
285019
  "\u2501\u2501 PHASE 3 \u2014 VERIFY \u2501\u2501",
@@ -284501,19 +285022,65 @@ var require_server3 = __commonJS({
284501
285022
  " \u2022 Add at least one `tag` per endpoint.",
284502
285023
  " \u2022 Add `description` to every parameter.",
284503
285024
  " \u2022 Add `description` to every response status code.",
284504
- "Use `jetic_update_endpoint` to fill gaps. Call `jetic_verify_model` again after each batch of updates.",
285025
+ "Use `jetic_update_endpoint` to fill gaps. Call `jetic_verify_model` again after each batch.",
284505
285026
  "Gate: DO NOT advance to Phase 4 until jetic_get_session_phase returns phase=4 (modelIsValid AND allFieldsFilled both true).",
284506
285027
  "",
284507
285028
  "\u2501\u2501 PHASE 4 \u2014 SIMULATE \u2501\u2501",
284508
- 'STOP. Ask the user: "What simulation or workflow do you want to build?"',
284509
- "Wait for their explicit answer. Then and only then: author the workflow with `jetic_validate_workflow` (dry run) \u2192 `jetic_create_workflow` \u2192 `jetic_simulate_workflow`.",
284510
- "Chain steps using capture/captureInput (producer) + inject or {{workflow:key}} (consumer). Use {{human:key}} ONLY for values unknowable beforehand (OTP codes, real secrets).",
284511
- "Never start Phase 4 without an explicit user-specified goal.",
285029
+ 'STOP. Ask the user: "What simulation or workflow do you want to build or test?"',
285030
+ "Wait for their explicit answer. Then follow these sub-steps IN ORDER:",
285031
+ "",
285032
+ "STEP 4a \u2014 SCAFFOLD",
285033
+ " Call `jetic_scaffold_workflow` with the ORDERED list of endpoints the user wants to exercise.",
285034
+ " It reads model.json and returns `draftSteps` with:",
285035
+ " \u2022 body derived from requestBody.fields (faker-mapped by type / format / field name)",
285036
+ " \u2022 inject derived from securitySchemes.obtainedFrom (token producer \u2192 consumer chain)",
285037
+ " \u2022 capture derived from endpoint.produces",
285038
+ " \u2022 captureInput for required fields that later steps will reuse",
285039
+ " Review draftSteps. Fix any `missingFields` entries before proceeding.",
285040
+ "",
285041
+ "STEP 4b \u2014 HUMAN INPUT PROTOCOL",
285042
+ " Use {{human:key}} ONLY when the value is truly unknowable before runtime:",
285043
+ " \u2022 One-time secrets: OTP codes, 2FA tokens, CAPTCHA answers",
285044
+ " \u2022 Real credentials that must be typed at execution time",
285045
+ " NEVER use {{human:key}} for things faker can generate (emails, names, UUIDs, passwords).",
285046
+ " The scaffold tool already handles faker mapping \u2014 only override to {{human:key}} when a",
285047
+ " field appears in missingFields AND genuinely requires interactive user input.",
285048
+ "",
285049
+ "STEP 4c \u2014 CONDITIONS (branching / skipping steps)",
285050
+ " Add `condition` to skip a step when a memory value does not match:",
285051
+ ' { "condition": { "if": "{{workflow:userRole}}", "equals": "admin" } }',
285052
+ " Operators: equals, notEquals, contains, exists (bool), greaterThan, lessThan, in (string[]).",
285053
+ " Skipped steps count as passed \u2014 the workflow continues.",
285054
+ ' Pair with onFailure:"continue" for negative/branching flows.',
285055
+ "",
285056
+ "STEP 4d \u2014 FAILURE MODES & RETRY",
285057
+ " onFailure (per step):",
285058
+ ' \u2022 "abort" (default) \u2014 stop the workflow on first failure',
285059
+ ' \u2022 "continue" \u2014 log the failure and proceed (captures are skipped for that step)',
285060
+ " continueOnStatus (per step): extra codes treated as pass, e.g. [404] or [409].",
285061
+ " retry (per step OR workflow-level global default):",
285062
+ ' \u2022 { "times": 3, "delayMs": 1000 } \u2014 retries up to 3\xD7 with 1 s between each attempt',
285063
+ " \u2022 Per-step retry overrides the workflow-level default. Keep times \u2264 5.",
285064
+ "",
285065
+ "STEP 4e \u2014 VALIDATE (dry run, no save)",
285066
+ ' jetic_validate_workflow { "definition": { "name": "...", "steps": [...] } }',
285067
+ " Fix all errors. Warnings are informational.",
285068
+ "",
285069
+ "STEP 4f \u2014 SAVE",
285070
+ ' jetic_create_workflow { "name": "...", "steps": [...] }',
285071
+ "",
285072
+ "STEP 4g \u2014 RUN",
285073
+ ' jetic_simulate_workflow { "workflow": "<slug>" }',
284512
285074
  "",
284513
285075
  "\u2501\u2501 GENERAL RULES \u2501\u2501",
284514
285076
  "No AI-provider setup is needed: never ask for OpenRouter/OpenAI keys and never use `jetic config ai`.",
284515
285077
  "Mutating tools write to disk \u2014 confirm intent with the user before destructive operations (delete/overwrite).",
284516
- "Project resolution: every tool accepts an optional `projectPath`. If omitted, falls back to JETIC_PROJECT_PATH env var, then cwd."
285078
+ "Project resolution: every tool accepts an optional `projectPath`. If omitted, falls back to JETIC_PROJECT_PATH env var, then cwd.",
285079
+ "\u2501\u2501 CHANGE TRACKING (jetic dev) \u2501\u2501",
285080
+ "When `jetic dev` is running it watches source files and logs edits to `.jetic/changes.json`.",
285081
+ "Before rescanning: call `jetic_get_changes` \u2014 if hasChanges is true, read ONLY the listed filePaths.",
285082
+ "Update model.json for those files, then call `jetic_clear_changes` (or jetic_scan, which clears automatically).",
285083
+ ""
284517
285084
  ].join("\n");
284518
285085
  var READ_ONLY = {
284519
285086
  readOnlyHint: true,
@@ -284563,7 +285130,9 @@ var require_server3 = __commonJS({
284563
285130
  };
284564
285131
  register("jetic_get_session_phase", "Get session phase", "ALWAYS CALL THIS FIRST. Returns the current workflow phase (1=init, 2=scan, 3=verify, 4=simulate), gate conditions (modelExists, endpointsCount, modelIsValid, allFieldsFilled), and the exact nextAction the AI must take. Never skip phase gates.", session_tools_1.getSessionPhaseSchema.shape, READ_ONLY, session_tools_1.handleGetSessionPhase);
284565
285132
  register("jetic_init", "Initialize Jetic project", "PHASE 1: Scaffolds .jetic/model.json (+ workflows/, config.json) for repos that have none. Refuses to clobber an existing model (pass overwrite:true to replace). Reports whether auto-scan applies (Express+TypeScript) and what to do next.", project_tools_1.initProjectSchema.shape, WRITES_MODEL, project_tools_1.handleInitProject);
284566
- register("jetic_scan", "Scan Express project", "PHASE 2 (Express+TypeScript only \u2014 requires tsconfig.json): Static AST scan that fills model.json with routes, middleware, and auth heuristics. Keyless \u2014 no AI provider involved. Default merge upserts scanned routes and keeps hand-added endpoints; overwrite replaces all endpoints. Fails with guidance for non-Express stacks (model those manually with jetic_add_endpoint).", project_tools_1.scanProjectSchema.shape, WRITES_MODEL, project_tools_1.handleScanProject);
285133
+ register("jetic_scan", "Scan Express project", "PHASE 2 (Express+TypeScript only \u2014 requires tsconfig.json): Static AST scan that fills model.json with routes, middleware, and auth heuristics. Keyless \u2014 no AI provider involved. Default merge upserts scanned routes and keeps hand-added endpoints; overwrite replaces all endpoints. Fails with guidance for non-Express stacks (model those manually with jetic_add_endpoint). Auto-clears .jetic/changes.json after success.", project_tools_1.scanProjectSchema.shape, WRITES_MODEL, project_tools_1.handleScanProject);
285134
+ register("jetic_get_changes", "Get file change log", "Reads .jetic/changes.json \u2014 the lightweight change log populated by the ChangeScanner running inside `jetic dev`. Returns the list of source files edited since the last scan or jetic_clear_changes. Use this BEFORE rescanning to read only changed files instead of the full codebase.", project_tools_1.getChangesSchema.shape, READ_ONLY, project_tools_1.handleGetChanges);
285135
+ register("jetic_clear_changes", "Clear file change log", "Clears .jetic/changes.json after the AI has finished updating model.json from the changed files. Also called automatically by jetic_scan. Call this when you have finished processing all changes returned by jetic_get_changes.", project_tools_1.clearChangesSchema.shape, WRITES_MODEL, project_tools_1.handleClearChanges);
284567
285136
  register("jetic_read_model", "Read Jetic model", "Reads .jetic/model.json from the workspace and returns the full behavioral model or a metadata summary.", model_tools_1.readModelSchema.shape, READ_ONLY, model_tools_1.handleReadModel);
284568
285137
  register("jetic_list_endpoints", "List endpoints", "Lists API endpoints discovered in .jetic/model.json with optional filters by method, tag, resource, or path substring.", model_tools_1.listEndpointsSchema.shape, READ_ONLY, model_tools_1.handleListEndpoints);
284569
285138
  register("jetic_get_endpoint", "Get endpoint details", "Returns full structural details (parameters, request body, schemas, security, responses) of one endpoint, looked up by ID or by method + path.", model_tools_1.getEndpointSchema.shape, READ_ONLY, model_tools_1.handleGetEndpoint);
@@ -284576,8 +285145,9 @@ var require_server3 = __commonJS({
284576
285145
  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);
284577
285146
  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);
284578
285147
  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);
284579
- register("jetic_validate_workflow", "Validate workflow", "Deep-validates a workflow WITHOUT saving or running it: step structure, endpoint cross-checks against model.json, and memory data-flow analysis (every {{scope:key}} except {{human:key}} must be captured by an earlier step). Accepts a saved workflow reference or an inline definition for pre-save dry runs.", workflow_authoring_tools_1.validateWorkflowSchema.shape, READ_ONLY, workflow_authoring_tools_1.handleValidateWorkflow);
284580
- register("jetic_create_workflow", "Create workflow", "Creates a .jetic/workflows/<slug>.json workflow from ordered steps with capture/inject memory chaining. Refuses to save when validation errors exist (error messages say exactly how to fix); use validateOnly for a dry run. Can also migrate an embedded model.json workflow via fromModelWorkflow.", workflow_authoring_tools_1.createWorkflowSchema.shape, WRITES_MODEL, workflow_authoring_tools_1.handleCreateWorkflow);
285148
+ register("jetic_scaffold_workflow", "Scaffold workflow steps from model", "PHASE 4a \u2014 FIRST STEP when building a workflow. Given an ordered endpoint list, reads model.json and returns ready-made `draftSteps`: body fields faker-mapped by type/format/name, inject derived from securitySchemes.obtainedFrom token-producer chains, capture from endpoint.produces, captureInput for reusable credentials. Returns missingFields (fields needing human input or manual fix) and securityChain analysis. Does NOT save. Review draftSteps, then call jetic_validate_workflow \u2192 jetic_create_workflow.", workflow_designer_tools_1.scaffoldWorkflowSchema.shape, READ_ONLY, workflow_designer_tools_1.handleScaffoldWorkflow);
285149
+ register("jetic_validate_workflow", "Validate workflow (dry run)", "Deep-validates a workflow WITHOUT saving: step structure, endpoint cross-checks against model.json, full memory data-flow analysis. Now also validates condition (including condition.if template refs), onFailure, continueOnStatus, and retry fields. Accepts a saved workflow slug/name or an inline definition.", workflow_authoring_tools_1.validateWorkflowSchema.shape, READ_ONLY, workflow_authoring_tools_1.handleValidateWorkflow);
285150
+ register("jetic_create_workflow", "Create workflow", "Creates a .jetic/workflows/<slug>.json workflow. Supports: condition (skip steps based on memory), onFailure (abort|continue), continueOnStatus (extra pass codes), retry (per-step or global default). Refuses to save on validation errors.", workflow_authoring_tools_1.createWorkflowSchema.shape, WRITES_MODEL, workflow_authoring_tools_1.handleCreateWorkflow);
284581
285151
  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);
284582
285152
  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);
284583
285153
  return server;
@@ -286333,9 +286903,170 @@ var import_path = __toESM(require("path"));
286333
286903
  var import_fs = __toESM(require("fs"));
286334
286904
  var import_core7 = __toESM(require_dist());
286335
286905
  var import_memory3 = __toESM(require_dist4());
286906
+ var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".cache", "__pycache__", ".next", ".nuxt", ".venv"]);
286907
+ var IGNORED_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".mp4", ".webm", ".woff", ".woff2", ".ttf", ".eot", ".map"]);
286908
+ var SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".kt", ".cs", ".rb", ".php", ".json", ".yaml", ".yml", ".toml", ".env", ".graphql", ".gql", ".sql", ".prisma", ".proto"]);
286909
+ var ChangeScanner = class {
286910
+ changesPath;
286911
+ projectRoot;
286912
+ watchers = [];
286913
+ debounceTimers = /* @__PURE__ */ new Map();
286914
+ DEBOUNCE_MS = 300;
286915
+ listeners = /* @__PURE__ */ new Set();
286916
+ constructor(projectRoot, jeticDir) {
286917
+ this.projectRoot = projectRoot;
286918
+ this.changesPath = import_path.default.join(jeticDir, "changes.json");
286919
+ }
286920
+ onChange(cb) {
286921
+ this.listeners.add(cb);
286922
+ return () => this.listeners.delete(cb);
286923
+ }
286924
+ notifyListeners(data) {
286925
+ for (const listener of this.listeners) {
286926
+ try {
286927
+ listener(data);
286928
+ } catch {
286929
+ }
286930
+ }
286931
+ }
286932
+ readChanges() {
286933
+ try {
286934
+ if (import_fs.default.existsSync(this.changesPath)) {
286935
+ return JSON.parse(import_fs.default.readFileSync(this.changesPath, "utf8"));
286936
+ }
286937
+ } catch {
286938
+ }
286939
+ return {
286940
+ version: 1,
286941
+ watchedSince: (/* @__PURE__ */ new Date()).toISOString(),
286942
+ projectRoot: this.projectRoot,
286943
+ changes: []
286944
+ };
286945
+ }
286946
+ saveChanges(data) {
286947
+ import_fs.default.writeFileSync(this.changesPath, JSON.stringify(data, null, 2), "utf8");
286948
+ this.notifyListeners(data);
286949
+ }
286950
+ recordChange(absolutePath, eventType) {
286951
+ const jeticDir = import_path.default.dirname(this.changesPath);
286952
+ if (absolutePath.startsWith(jeticDir)) return;
286953
+ const ext = import_path.default.extname(absolutePath).toLowerCase();
286954
+ if (IGNORED_EXTS.has(ext)) return;
286955
+ if (ext && !SOURCE_EXTS.has(ext)) return;
286956
+ if (!import_fs.default.existsSync(absolutePath)) return;
286957
+ try {
286958
+ if (import_fs.default.statSync(absolutePath).isDirectory()) return;
286959
+ } catch {
286960
+ return;
286961
+ }
286962
+ const relPath = import_path.default.relative(this.projectRoot, absolutePath).replace(/\\/g, "/");
286963
+ const existing = this.debounceTimers.get(absolutePath);
286964
+ if (existing) clearTimeout(existing);
286965
+ this.debounceTimers.set(
286966
+ absolutePath,
286967
+ setTimeout(() => {
286968
+ this.debounceTimers.delete(absolutePath);
286969
+ const data = this.readChanges();
286970
+ const now = (/* @__PURE__ */ new Date()).toISOString();
286971
+ const idx = data.changes.findIndex((c3) => c3.filePath === relPath);
286972
+ const entry = { filePath: relPath, absolutePath, changedAt: now, eventType };
286973
+ if (idx >= 0) data.changes[idx] = entry;
286974
+ else data.changes.push(entry);
286975
+ this.saveChanges(data);
286976
+ }, this.DEBOUNCE_MS)
286977
+ );
286978
+ }
286979
+ watchDir(dir) {
286980
+ try {
286981
+ const watcher = import_fs.default.watch(dir, { recursive: false }, (eventType, filename) => {
286982
+ if (!filename) return;
286983
+ const absPath = import_path.default.join(dir, filename);
286984
+ try {
286985
+ if (import_fs.default.existsSync(absPath) && import_fs.default.statSync(absPath).isDirectory()) {
286986
+ const basename = import_path.default.basename(absPath);
286987
+ if (!IGNORED_DIRS.has(basename)) this.watchDir(absPath);
286988
+ return;
286989
+ }
286990
+ } catch {
286991
+ return;
286992
+ }
286993
+ this.recordChange(absPath, eventType);
286994
+ });
286995
+ this.watchers.push(watcher);
286996
+ } catch {
286997
+ }
286998
+ }
286999
+ /** Recursively enumerate all subdirectories to watch, respecting ignores. */
287000
+ collectDirs(root) {
287001
+ const dirs = [root];
287002
+ try {
287003
+ for (const entry of import_fs.default.readdirSync(root, { withFileTypes: true })) {
287004
+ if (!entry.isDirectory()) continue;
287005
+ if (IGNORED_DIRS.has(entry.name)) continue;
287006
+ if (entry.name.startsWith(".") && entry.name !== ".") continue;
287007
+ dirs.push(...this.collectDirs(import_path.default.join(root, entry.name)));
287008
+ }
287009
+ } catch {
287010
+ }
287011
+ return dirs;
287012
+ }
287013
+ start() {
287014
+ process.stderr.write(`[jetic] ChangeScanner watching ${this.projectRoot}
287015
+ `);
287016
+ if (!import_fs.default.existsSync(this.changesPath)) {
287017
+ this.saveChanges({
287018
+ version: 1,
287019
+ watchedSince: (/* @__PURE__ */ new Date()).toISOString(),
287020
+ projectRoot: this.projectRoot,
287021
+ changes: []
287022
+ });
287023
+ }
287024
+ const dirs = this.collectDirs(this.projectRoot);
287025
+ for (const dir of dirs) this.watchDir(dir);
287026
+ try {
287027
+ if (import_fs.default.existsSync(this.changesPath)) {
287028
+ const fileWatcher = import_fs.default.watch(this.changesPath, () => {
287029
+ const data = this.readChanges();
287030
+ this.notifyListeners(data);
287031
+ });
287032
+ this.watchers.push(fileWatcher);
287033
+ }
287034
+ } catch {
287035
+ }
287036
+ }
287037
+ stop() {
287038
+ for (const w of this.watchers) try {
287039
+ w.close();
287040
+ } catch {
287041
+ }
287042
+ this.watchers = [];
287043
+ }
287044
+ getChanges() {
287045
+ return this.readChanges();
287046
+ }
287047
+ clearChanges() {
287048
+ const data = this.readChanges();
287049
+ data.changes = [];
287050
+ data.watchedSince = (/* @__PURE__ */ new Date()).toISOString();
287051
+ this.saveChanges(data);
287052
+ }
287053
+ };
286336
287054
  var devCommand = new import_commander8.Command("dev").description("Start the Jetic local dashboard").option("-p, --port <number>", "Port to run the dashboard on", "8787").action(async (options) => {
286337
287055
  const port = parseInt(options.port, 10);
286338
287056
  const app = (0, import_express.default)();
287057
+ const projectRoot = process.cwd();
287058
+ const jeticDir = import_path.default.join(projectRoot, ".jetic");
287059
+ if (!import_fs.default.existsSync(jeticDir)) import_fs.default.mkdirSync(jeticDir, { recursive: true });
287060
+ const changeScanner = new ChangeScanner(projectRoot, jeticDir);
287061
+ changeScanner.start();
287062
+ process.on("SIGINT", () => {
287063
+ changeScanner.stop();
287064
+ process.exit(0);
287065
+ });
287066
+ process.on("SIGTERM", () => {
287067
+ changeScanner.stop();
287068
+ process.exit(0);
287069
+ });
286339
287070
  app.use(import_express.default.json());
286340
287071
  app.use((_req, res, next) => {
286341
287072
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -286368,14 +287099,14 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
286368
287099
  }
286369
287100
  });
286370
287101
  app.post("/api/model/endpoint", (req, res) => {
286371
- const jeticDir = import_path.default.join(process.cwd(), ".jetic");
286372
- const modelPath = import_path.default.join(jeticDir, "model.json");
287102
+ const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
287103
+ const modelPath = import_path.default.join(jeticDir2, "model.json");
286373
287104
  try {
286374
287105
  const ep = req.body;
286375
287106
  if (!ep.method || !ep.path) {
286376
287107
  return res.status(400).json({ error: "method and path are required" });
286377
287108
  }
286378
- if (!import_fs.default.existsSync(jeticDir)) import_fs.default.mkdirSync(jeticDir, { recursive: true });
287109
+ if (!import_fs.default.existsSync(jeticDir2)) import_fs.default.mkdirSync(jeticDir2, { recursive: true });
286379
287110
  let model;
286380
287111
  if (import_fs.default.existsSync(modelPath)) {
286381
287112
  model = JSON.parse(import_fs.default.readFileSync(modelPath, "utf8"));
@@ -286414,11 +287145,49 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
286414
287145
  const model = await scanner.scan();
286415
287146
  const modelPath = import_path.default.join(config.jeticDir, "model.json");
286416
287147
  writeJsonSync2(modelPath, model);
287148
+ changeScanner.clearChanges();
286417
287149
  res.json({ ok: true, endpointCount: model.endpoints.length, model });
286418
287150
  } catch (err) {
286419
287151
  res.status(500).json({ error: err.message });
286420
287152
  }
286421
287153
  });
287154
+ app.get("/api/changes", (_req, res) => {
287155
+ try {
287156
+ res.json(changeScanner.getChanges());
287157
+ } catch (err) {
287158
+ res.status(500).json({ error: err.message });
287159
+ }
287160
+ });
287161
+ app.get("/api/changes/stream", (req, res) => {
287162
+ res.setHeader("Content-Type", "text/event-stream");
287163
+ res.setHeader("Cache-Control", "no-cache");
287164
+ res.setHeader("Connection", "keep-alive");
287165
+ if (res.flushHeaders) res.flushHeaders();
287166
+ const initial = changeScanner.getChanges();
287167
+ res.write(`data: ${JSON.stringify(initial)}
287168
+
287169
+ `);
287170
+ const unsubscribe = changeScanner.onChange((updatedData) => {
287171
+ res.write(`data: ${JSON.stringify(updatedData)}
287172
+
287173
+ `);
287174
+ });
287175
+ const heartbeat = setInterval(() => {
287176
+ res.write(": heartbeat\n\n");
287177
+ }, 15e3);
287178
+ req.on("close", () => {
287179
+ unsubscribe();
287180
+ clearInterval(heartbeat);
287181
+ });
287182
+ });
287183
+ app.delete("/api/changes", (_req, res) => {
287184
+ try {
287185
+ changeScanner.clearChanges();
287186
+ res.json({ ok: true, cleared: true });
287187
+ } catch (err) {
287188
+ res.status(500).json({ error: err.message });
287189
+ }
287190
+ });
286422
287191
  function readModel(modelPath) {
286423
287192
  if (!import_fs.default.existsSync(modelPath)) return null;
286424
287193
  return JSON.parse(import_fs.default.readFileSync(modelPath, "utf8"));
@@ -286542,8 +287311,8 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
286542
287311
  }
286543
287312
  });
286544
287313
  app.get("/api/workflows", (_req, res) => {
286545
- const jeticDir = import_path.default.join(process.cwd(), ".jetic");
286546
- const workflowsDir = import_path.default.join(jeticDir, "workflows");
287314
+ const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
287315
+ const workflowsDir = import_path.default.join(jeticDir2, "workflows");
286547
287316
  try {
286548
287317
  const workflows = [];
286549
287318
  if (import_fs.default.existsSync(workflowsDir)) {
@@ -286558,7 +287327,7 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
286558
287327
  }
286559
287328
  }
286560
287329
  }
286561
- const legacyPath = import_path.default.join(jeticDir, "workflow.json");
287330
+ const legacyPath = import_path.default.join(jeticDir2, "workflow.json");
286562
287331
  if (import_fs.default.existsSync(legacyPath)) {
286563
287332
  try {
286564
287333
  const data = JSON.parse(import_fs.default.readFileSync(legacyPath, "utf8"));
@@ -286576,8 +287345,8 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
286576
287345
  app.post("/api/workflows", (req, res) => {
286577
287346
  const { name } = req.body;
286578
287347
  if (!name || !name.trim()) return res.status(400).json({ error: "name is required" });
286579
- const jeticDir = import_path.default.join(process.cwd(), ".jetic");
286580
- const workflowsDir = import_path.default.join(jeticDir, "workflows");
287348
+ const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
287349
+ const workflowsDir = import_path.default.join(jeticDir2, "workflows");
286581
287350
  const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
286582
287351
  const filePath = import_path.default.join(workflowsDir, `${slug}.json`);
286583
287352
  if (import_fs.default.existsSync(filePath)) {
@@ -286598,11 +287367,11 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
286598
287367
  });
286599
287368
  app.put("/api/workflows/:name", (req, res) => {
286600
287369
  const slug = req.params.name;
286601
- const jeticDir = import_path.default.join(process.cwd(), ".jetic");
286602
- const workflowsDir = import_path.default.join(jeticDir, "workflows");
287370
+ const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
287371
+ const workflowsDir = import_path.default.join(jeticDir2, "workflows");
286603
287372
  let filePath = import_path.default.join(workflowsDir, `${slug}.json`);
286604
287373
  if (!import_fs.default.existsSync(filePath)) {
286605
- const legacyPath = import_path.default.join(jeticDir, "workflow.json");
287374
+ const legacyPath = import_path.default.join(jeticDir2, "workflow.json");
286606
287375
  if (slug === "workflow" && import_fs.default.existsSync(legacyPath)) {
286607
287376
  filePath = legacyPath;
286608
287377
  } else {
@@ -286621,8 +287390,8 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
286621
287390
  });
286622
287391
  app.delete("/api/workflows/:name", (req, res) => {
286623
287392
  const slug = req.params.name;
286624
- const jeticDir = import_path.default.join(process.cwd(), ".jetic");
286625
- const workflowsDir = import_path.default.join(jeticDir, "workflows");
287393
+ const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
287394
+ const workflowsDir = import_path.default.join(jeticDir2, "workflows");
286626
287395
  const filePath = import_path.default.join(workflowsDir, `${slug}.json`);
286627
287396
  if (!import_fs.default.existsSync(filePath)) {
286628
287397
  return res.status(404).json({ error: `Workflow "${slug}" not found` });
@@ -286637,12 +287406,12 @@ var devCommand = new import_commander8.Command("dev").description("Start the Jet
286637
287406
  app.post("/api/workflows/generate", async (req, res) => {
286638
287407
  const { goal, name } = req.body;
286639
287408
  const workflowGoal = goal || name || "Full user journey";
286640
- const jeticDir = import_path.default.join(process.cwd(), ".jetic");
287409
+ const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
286641
287410
  const config = (0, import_core7.loadConfig)();
286642
287411
  if (!config.ai) {
286643
287412
  return res.status(400).json({ error: "AI is not configured. Run `jetic config ai` first." });
286644
287413
  }
286645
- const modelPath = import_path.default.join(jeticDir, "model.json");
287414
+ const modelPath = import_path.default.join(jeticDir2, "model.json");
286646
287415
  if (!import_fs.default.existsSync(modelPath)) {
286647
287416
  return res.status(400).json({ error: "No model.json found. Run `jetic scan` first." });
286648
287417
  }
@@ -286764,7 +287533,7 @@ Output valid JSON only.
286764
287533
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
286765
287534
  steps: object.steps
286766
287535
  };
286767
- const workflowsDir = import_path.default.join(jeticDir, "workflows");
287536
+ const workflowsDir = import_path.default.join(jeticDir2, "workflows");
286768
287537
  const slug = object.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "workflow";
286769
287538
  const filePath = import_path.default.join(workflowsDir, `${slug}.json`);
286770
287539
  import_fs.default.mkdirSync(workflowsDir, { recursive: true });
@@ -286800,8 +287569,8 @@ Output valid JSON only.
286800
287569
  if (!file) {
286801
287570
  return res.status(400).json({ error: "file is required" });
286802
287571
  }
286803
- const jeticDir = import_path.default.join(process.cwd(), ".jetic");
286804
- const wfPath = import_path.default.isAbsolute(file) ? file : import_path.default.join(jeticDir, file);
287572
+ const jeticDir2 = import_path.default.join(process.cwd(), ".jetic");
287573
+ const wfPath = import_path.default.isAbsolute(file) ? file : import_path.default.join(jeticDir2, file);
286805
287574
  if (!import_fs.default.existsSync(wfPath)) {
286806
287575
  return res.status(404).json({ error: "workflow file not found" });
286807
287576
  }
@@ -286824,7 +287593,7 @@ Output valid JSON only.
286824
287593
  };
286825
287594
  let baseUrl = req.body?.baseUrl;
286826
287595
  if (!baseUrl) {
286827
- const modelPath = import_path.default.join(jeticDir, "model.json");
287596
+ const modelPath = import_path.default.join(jeticDir2, "model.json");
286828
287597
  if (import_fs.default.existsSync(modelPath)) {
286829
287598
  try {
286830
287599
  const model = JSON.parse(import_fs.default.readFileSync(modelPath, "utf8"));
@@ -287476,7 +288245,7 @@ var mcpCommand = new import_commander10.Command("mcp").description("Launch Model
287476
288245
 
287477
288246
  // src/index.ts
287478
288247
  var program = new import_commander11.Command();
287479
- program.name("jetic").description("AI-Native API Behavior Testing").version("0.2.1");
288248
+ program.name("jetic").description("AI-Native API Behavior Testing").version("0.2.3");
287480
288249
  program.addCommand(initCommand);
287481
288250
  program.addCommand(scanCommand);
287482
288251
  program.addCommand(inspectCommand);