@vornrun/mcp 0.7.1 → 0.7.3
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 +879 -226
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -3065,6 +3065,10 @@ function registerSessionTools(server) {
|
|
|
3065
3065
|
|
|
3066
3066
|
// src/tools/workflows.ts
|
|
3067
3067
|
import crypto4 from "crypto";
|
|
3068
|
+
import { isDeepStrictEqual } from "util";
|
|
3069
|
+
|
|
3070
|
+
// ../shared/src/template-vars.ts
|
|
3071
|
+
var MAX_OUTPUT_LENGTH = 5e4;
|
|
3068
3072
|
|
|
3069
3073
|
// ../shared/src/workflow-graph.ts
|
|
3070
3074
|
var MAX_GATE_ROUNDS = 10;
|
|
@@ -3089,6 +3093,8 @@ function nodesBetween(from, to, edges) {
|
|
|
3089
3093
|
const above = reach(to, predecessors);
|
|
3090
3094
|
return new Set([...below].filter((id) => above.has(id)));
|
|
3091
3095
|
}
|
|
3096
|
+
var LOOP_RESULT_OUTPUT_CHARS = 8e3;
|
|
3097
|
+
var MAX_LOOP_ITERATIONS = 10;
|
|
3092
3098
|
function isSignInWait(state) {
|
|
3093
3099
|
return state.status === "waiting" && state.waitingFor === "signIn";
|
|
3094
3100
|
}
|
|
@@ -3101,9 +3107,391 @@ function buildGraph(edges) {
|
|
|
3101
3107
|
}
|
|
3102
3108
|
return { successors, predecessors };
|
|
3103
3109
|
}
|
|
3110
|
+
function loopBodyOwners(nodes) {
|
|
3111
|
+
const exists = new Set(nodes.map((n) => n.id));
|
|
3112
|
+
const owners = /* @__PURE__ */ new Map();
|
|
3113
|
+
for (const n of nodes) {
|
|
3114
|
+
if (n.type !== "loop") continue;
|
|
3115
|
+
for (const id of n.config.bodyNodeIds ?? []) {
|
|
3116
|
+
if (exists.has(id) && id !== n.id && !owners.has(id)) owners.set(id, n.id);
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
return owners;
|
|
3120
|
+
}
|
|
3121
|
+
function loopBodyGraph(nodes, edges, loop) {
|
|
3122
|
+
const owners = loopBodyOwners(nodes);
|
|
3123
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
3124
|
+
const members = (loop.config.bodyNodeIds ?? []).filter((id) => owners.get(id) === loop.id).map((id) => byId.get(id));
|
|
3125
|
+
const ids = new Set(members.map((m) => m.id));
|
|
3126
|
+
let inner = edges.filter((e) => ids.has(e.source) && ids.has(e.target));
|
|
3127
|
+
if (inner.length === 0 && members.length > 1) {
|
|
3128
|
+
inner = members.slice(1).map((m, i) => ({
|
|
3129
|
+
id: `${members[i].id}->${m.id}:chain`,
|
|
3130
|
+
source: members[i].id,
|
|
3131
|
+
target: m.id
|
|
3132
|
+
}));
|
|
3133
|
+
}
|
|
3134
|
+
const fed = new Set(inner.map((e) => e.target));
|
|
3135
|
+
return { members, edges: inner, entries: members.filter((m) => !fed.has(m.id)).map((m) => m.id) };
|
|
3136
|
+
}
|
|
3137
|
+
function loopStructureError(nodes, edges, loop) {
|
|
3138
|
+
const { members, edges: inner } = loopBodyGraph(nodes, edges, loop);
|
|
3139
|
+
if (members.length === 0) return "Loop has no body steps. Add at least one step for it to repeat.";
|
|
3140
|
+
const ids = new Set(members.map((m) => m.id));
|
|
3141
|
+
for (const m of members) {
|
|
3142
|
+
if (m.type === "approval") {
|
|
3143
|
+
return `Loop body contains an approval gate ("${m.label}"), which is not supported.`;
|
|
3144
|
+
}
|
|
3145
|
+
if (m.type === "loop")
|
|
3146
|
+
return `Loop body contains another loop ("${m.label}"), which is not supported.`;
|
|
3147
|
+
if (m.type === "trigger") return `Loop body contains a trigger ("${m.label}").`;
|
|
3148
|
+
}
|
|
3149
|
+
for (const e of edges) {
|
|
3150
|
+
if (ids.has(e.target) && !ids.has(e.source) && e.source !== loop.id) {
|
|
3151
|
+
const from = nodes.find((n) => n.id === e.source)?.label ?? e.source;
|
|
3152
|
+
return `"${from}" feeds a step inside the loop from outside it. Only the loop starts its steps.`;
|
|
3153
|
+
}
|
|
3154
|
+
if (ids.has(e.source) && !ids.has(e.target) && e.conditionBranch) {
|
|
3155
|
+
return "A condition inside the loop branches to a step outside it. Keep both branches inside the loop.";
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
const indegree = new Map(members.map((m) => [m.id, 0]));
|
|
3159
|
+
for (const e of inner) indegree.set(e.target, (indegree.get(e.target) ?? 0) + 1);
|
|
3160
|
+
const queue = [...indegree].filter(([, d]) => d === 0).map(([id]) => id);
|
|
3161
|
+
let visited = 0;
|
|
3162
|
+
while (queue.length > 0) {
|
|
3163
|
+
const id = queue.shift();
|
|
3164
|
+
visited++;
|
|
3165
|
+
for (const e of inner) {
|
|
3166
|
+
if (e.source !== id) continue;
|
|
3167
|
+
const d = (indegree.get(e.target) ?? 0) - 1;
|
|
3168
|
+
indegree.set(e.target, d);
|
|
3169
|
+
if (d === 0) queue.push(e.target);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
if (visited < members.length) return "The steps inside the loop form a cycle.";
|
|
3173
|
+
return void 0;
|
|
3174
|
+
}
|
|
3104
3175
|
|
|
3105
3176
|
// src/tools/workflows.ts
|
|
3177
|
+
import { z as z6 } from "zod";
|
|
3178
|
+
|
|
3179
|
+
// src/tools/node-config-schemas.ts
|
|
3106
3180
|
import { z as z5 } from "zod";
|
|
3181
|
+
var longText = z5.string().max(2e5, "Text must be 200000 characters or less");
|
|
3182
|
+
var taskStatus = z5.enum(["todo", "in_progress", "in_review", "done", "cancelled"]);
|
|
3183
|
+
var workflowInputDefSchema = z5.object({
|
|
3184
|
+
key: z5.string().regex(
|
|
3185
|
+
/^[A-Za-z_][A-Za-z0-9_]*$/,
|
|
3186
|
+
"key must be a valid identifier \u2014 it becomes {{inputs.<key>}}"
|
|
3187
|
+
).max(100).describe("Identifier the value is read by, as {{inputs.<key>}}"),
|
|
3188
|
+
label: V.shortText.describe("What the run dialog calls this field"),
|
|
3189
|
+
type: z5.enum(["text", "textarea", "number", "select", "boolean", "project", "branch"]).describe("Which control the run dialog shows, and how a supplied value is checked"),
|
|
3190
|
+
required: z5.boolean().optional().describe("A run cannot start without a value"),
|
|
3191
|
+
defaultValue: V.shortText.optional().describe(
|
|
3192
|
+
'Used when the run supplies nothing. A number must be finite, a boolean "true" or "false", a select one of its options'
|
|
3193
|
+
),
|
|
3194
|
+
options: z5.array(
|
|
3195
|
+
z5.object({
|
|
3196
|
+
value: V.shortText.describe("What {{inputs.<key>}} expands to"),
|
|
3197
|
+
label: V.shortText.describe("What the dialog shows")
|
|
3198
|
+
}).loose()
|
|
3199
|
+
).optional().describe("Choices for a select; a select without any is refused"),
|
|
3200
|
+
placeholder: V.shortText.optional().describe("Hint shown in an empty field"),
|
|
3201
|
+
description: V.shortText.optional().describe("Help text shown under the field")
|
|
3202
|
+
}).loose().superRefine((def, ctx) => {
|
|
3203
|
+
const options = def.options ?? [];
|
|
3204
|
+
if (def.type === "select" && options.length === 0) {
|
|
3205
|
+
ctx.addIssue({
|
|
3206
|
+
code: "custom",
|
|
3207
|
+
path: ["options"],
|
|
3208
|
+
message: `select input "${def.key}" declares no options, so the run dialog could offer nothing`
|
|
3209
|
+
});
|
|
3210
|
+
}
|
|
3211
|
+
if (def.defaultValue === void 0) return;
|
|
3212
|
+
if (def.type === "number" && !Number.isFinite(Number(def.defaultValue))) {
|
|
3213
|
+
ctx.addIssue({
|
|
3214
|
+
code: "custom",
|
|
3215
|
+
path: ["defaultValue"],
|
|
3216
|
+
message: `default "${def.defaultValue}" for number input "${def.key}" is not a finite number`
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3219
|
+
if (def.type === "boolean" && !["true", "false"].includes(def.defaultValue)) {
|
|
3220
|
+
ctx.addIssue({
|
|
3221
|
+
code: "custom",
|
|
3222
|
+
path: ["defaultValue"],
|
|
3223
|
+
message: `default "${def.defaultValue}" for boolean input "${def.key}" must be "true" or "false"`
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
if (def.type === "select" && options.length > 0 && !options.some((o) => o.value === def.defaultValue)) {
|
|
3227
|
+
ctx.addIssue({
|
|
3228
|
+
code: "custom",
|
|
3229
|
+
path: ["defaultValue"],
|
|
3230
|
+
message: `default "${def.defaultValue}" for select input "${def.key}" is not one of its options`
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
});
|
|
3234
|
+
var workflowInputsSchema = z5.array(workflowInputDefSchema).superRefine((inputs, ctx) => {
|
|
3235
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3236
|
+
inputs.forEach((def, index) => {
|
|
3237
|
+
if (seen.has(def.key)) {
|
|
3238
|
+
ctx.addIssue({
|
|
3239
|
+
code: "custom",
|
|
3240
|
+
path: [index, "key"],
|
|
3241
|
+
message: `duplicate input key "${def.key}" \u2014 only one value can survive under {{inputs.${def.key}}}`
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
seen.add(def.key);
|
|
3245
|
+
});
|
|
3246
|
+
});
|
|
3247
|
+
var projectFilter = V.name.optional().describe("Only fire for this project, by name. Omit to fire for every project");
|
|
3248
|
+
var triggerConfigSchema = z5.discriminatedUnion("triggerType", [
|
|
3249
|
+
z5.object({
|
|
3250
|
+
triggerType: z5.literal("manual").describe("Runs when someone starts it, or execute_workflow"),
|
|
3251
|
+
contextual: z5.boolean().optional().describe(
|
|
3252
|
+
"Offered from a task card or terminal right-click, inheriting its folder, branch and worktree as {{context.*}}"
|
|
3253
|
+
),
|
|
3254
|
+
inputs: workflowInputsSchema.optional().describe("Parameters asked for before each run, read as {{inputs.<key>}}")
|
|
3255
|
+
}).loose(),
|
|
3256
|
+
z5.object({
|
|
3257
|
+
triggerType: z5.literal("once").describe("Runs once at a set time"),
|
|
3258
|
+
runAt: V.shortText.describe("When to run, as an ISO 8601 date-time")
|
|
3259
|
+
}).loose(),
|
|
3260
|
+
z5.object({
|
|
3261
|
+
triggerType: z5.literal("recurring").describe("Runs on a cron schedule"),
|
|
3262
|
+
cron: V.shortText.describe('Five-field cron expression, e.g. "0 9 * * 1-5"'),
|
|
3263
|
+
timezone: V.shortText.optional().describe('IANA zone the cron is read in, e.g. "Europe/Madrid". Defaults to the machine')
|
|
3264
|
+
}).loose(),
|
|
3265
|
+
z5.object({
|
|
3266
|
+
triggerType: z5.literal("taskCreated").describe("Runs when a task is created; the task is {{task.*}}"),
|
|
3267
|
+
projectFilter
|
|
3268
|
+
}).loose(),
|
|
3269
|
+
z5.object({
|
|
3270
|
+
triggerType: z5.literal("taskStatusChanged").describe(
|
|
3271
|
+
"Runs when a task moves between statuses; the task is {{task.*}}, the move {{trigger.fromStatus}} and {{trigger.toStatus}}"
|
|
3272
|
+
),
|
|
3273
|
+
projectFilter,
|
|
3274
|
+
fromStatus: taskStatus.optional().describe("Only when it leaves this status"),
|
|
3275
|
+
toStatus: taskStatus.optional().describe("Only when it enters this status")
|
|
3276
|
+
}).loose(),
|
|
3277
|
+
z5.object({
|
|
3278
|
+
triggerType: z5.literal("sessionRestored").describe(
|
|
3279
|
+
"Runs when a session comes back after a quit, crash or reboot; {{trigger.restore}} says cold or warm"
|
|
3280
|
+
),
|
|
3281
|
+
projectFilter,
|
|
3282
|
+
restore: z5.enum(["cold", "any"]).optional().describe(
|
|
3283
|
+
"cold (default): only when a process starts again. any: also when a pane reattaches to one still running"
|
|
3284
|
+
),
|
|
3285
|
+
concurrency: z5.enum(["perProject", "unbounded"]).optional().describe(
|
|
3286
|
+
"perProject (default) runs one at a time per project, so restored sessions do not race for one port"
|
|
3287
|
+
)
|
|
3288
|
+
}).loose(),
|
|
3289
|
+
z5.object({
|
|
3290
|
+
triggerType: z5.literal("connectorPoll").describe(
|
|
3291
|
+
"Polls a connection on a cron and runs once per new item; the item is {{connectorItem.*}}"
|
|
3292
|
+
),
|
|
3293
|
+
connectionId: V.id.describe("Connection to poll, from list_connections"),
|
|
3294
|
+
event: V.shortText.describe(
|
|
3295
|
+
"Event type from the connector's manifest, e.g. issueCreated (list_connector_actions shows them)"
|
|
3296
|
+
),
|
|
3297
|
+
cron: V.shortText.describe("How often to poll, as a five-field cron expression"),
|
|
3298
|
+
timezone: V.shortText.optional().describe("IANA zone the cron is read in")
|
|
3299
|
+
}).loose(),
|
|
3300
|
+
z5.object({
|
|
3301
|
+
triggerType: z5.literal("webhook").describe(
|
|
3302
|
+
"Runs when the local webhook route receives a request; its body, headers and query are {{trigger.*}}"
|
|
3303
|
+
),
|
|
3304
|
+
method: z5.enum(["POST", "GET"]).describe("HTTP method the route answers"),
|
|
3305
|
+
token: V.shortText.describe(
|
|
3306
|
+
"Secret path segment; a request without it is refused. Use a long random string"
|
|
3307
|
+
)
|
|
3308
|
+
}).loose()
|
|
3309
|
+
]);
|
|
3310
|
+
var launchAgentNodeConfigSchema = z5.object({
|
|
3311
|
+
agentType: z5.enum(["claude", "copilot", "codex", "opencode", "gemini", "fromTask"]).describe(
|
|
3312
|
+
"Which agent CLI runs. fromTask uses the task's assigned agent, so it needs a task in scope (a task trigger, taskId or taskFromQueue)"
|
|
3313
|
+
),
|
|
3314
|
+
model: V.shortText.optional().describe('Model passed to the CLI as written, e.g. "opus". Needs a concrete agentType'),
|
|
3315
|
+
projectName: z5.string().max(200).describe("Registered project the agent works in, by name (list_projects)"),
|
|
3316
|
+
projectPath: z5.string().max(1e3).describe("That project's absolute path; templates such as {{context.projectPath}} work"),
|
|
3317
|
+
prompt: V.prompt.optional().describe("What the agent is asked to do. Templates expand before it is sent"),
|
|
3318
|
+
headless: z5.boolean().optional().describe(
|
|
3319
|
+
"Run in the background and wait for it to finish, so later steps can read its output. Without it the agent opens in a terminal tab and the run does not wait on its answer"
|
|
3320
|
+
),
|
|
3321
|
+
outputSchema: z5.record(z5.string(), z5.unknown()).optional().describe(
|
|
3322
|
+
"JSON Schema its final answer must match (headless only). Each top-level property becomes {{steps.<slug>.<field>}}; an answer that does not match fails the step"
|
|
3323
|
+
),
|
|
3324
|
+
timeoutMs: z5.number().int().positive().optional().describe(
|
|
3325
|
+
"How long a headless run may take before it is killed and the step fails, in milliseconds. Defaults to the app setting"
|
|
3326
|
+
),
|
|
3327
|
+
worktreeMode: z5.enum(["none", "new", "fromStep", "existing"]).optional().describe(
|
|
3328
|
+
"Where it works: none (the project folder), new (a fresh git worktree), fromStep (the worktree an earlier step made), existing (existingWorktreePath)"
|
|
3329
|
+
),
|
|
3330
|
+
worktreeFromStepSlug: V.shortText.optional().describe(
|
|
3331
|
+
"For worktreeMode fromStep: slug of the earlier agent step whose worktree to reuse"
|
|
3332
|
+
),
|
|
3333
|
+
existingWorktreePath: z5.string().max(1e3).optional().describe("For worktreeMode existing: absolute path of the worktree to work in"),
|
|
3334
|
+
useWorktree: z5.union([z5.boolean(), z5.literal("fromContext")]).optional().describe(
|
|
3335
|
+
"Older switch for a new worktree; prefer worktreeMode. fromContext inherits it from a contextual manual trigger"
|
|
3336
|
+
),
|
|
3337
|
+
branch: V.shortText.optional().describe("Branch to check out or create for the worktree"),
|
|
3338
|
+
args: z5.array(V.shortText).optional().describe("Extra command-line arguments for the agent CLI"),
|
|
3339
|
+
displayName: V.shortText.optional().describe("Name shown on the session tab"),
|
|
3340
|
+
remoteHostId: V.id.optional().describe("Run on this remote host instead of this machine"),
|
|
3341
|
+
promptDelayMs: z5.number().nonnegative().optional().describe("Wait this long after the agent starts before sending the prompt, in milliseconds"),
|
|
3342
|
+
taskId: V.id.optional().describe("Task the session is attached to"),
|
|
3343
|
+
taskFromQueue: z5.boolean().optional().describe("Take the project's next todo task and attach the session to it")
|
|
3344
|
+
}).loose().refine((c) => !c.outputSchema || c.headless === true, {
|
|
3345
|
+
// The engine only parses typed output for headless runs; reject a config that
|
|
3346
|
+
// declares a schema it would silently ignore instead of accepting a lie.
|
|
3347
|
+
message: "outputSchema requires headless: true",
|
|
3348
|
+
path: ["outputSchema"]
|
|
3349
|
+
});
|
|
3350
|
+
var scriptConfigSchema = z5.object({
|
|
3351
|
+
scriptType: z5.enum(["bash", "powershell", "python", "node"]).describe("Interpreter the script runs under"),
|
|
3352
|
+
scriptContent: longText.describe(
|
|
3353
|
+
"The script. Templates expand before it runs, so quote them as data. Its stdout is {{steps.<slug>.output}}; a non-zero exit fails the step"
|
|
3354
|
+
),
|
|
3355
|
+
cwd: z5.string().max(1e3).optional().describe("Directory it runs in. Defaults to projectPath"),
|
|
3356
|
+
projectName: z5.string().max(200).optional().describe("Project the script belongs to, by name"),
|
|
3357
|
+
projectPath: z5.string().max(1e3).optional().describe("That project's absolute path, used as the working directory when cwd is unset"),
|
|
3358
|
+
args: z5.array(V.shortText).optional().describe("Arguments passed to the script"),
|
|
3359
|
+
secretsFrom: V.id.optional().describe(
|
|
3360
|
+
"Connection whose stored secrets become this script's environment variables at run time, so no secret is written into the workflow"
|
|
3361
|
+
),
|
|
3362
|
+
runId: V.id.optional().describe("Set by the engine; leave it out")
|
|
3363
|
+
}).loose();
|
|
3364
|
+
var conditionConfigSchema = z5.object({
|
|
3365
|
+
variable: longText.describe(
|
|
3366
|
+
"The value to test, usually a template such as {{steps.review.approved}}"
|
|
3367
|
+
),
|
|
3368
|
+
operator: z5.enum(["equals", "notEquals", "contains", "notContains", "isEmpty", "isNotEmpty"]).describe("How variable is compared with value. Comparisons are on text"),
|
|
3369
|
+
value: longText.optional().describe("What variable is compared with; ignored by isEmpty and isNotEmpty")
|
|
3370
|
+
}).loose();
|
|
3371
|
+
var approvalConfigSchema = z5.object({
|
|
3372
|
+
message: longText.optional().describe("The question the reviewer is asked. Templates expand when the gate opens"),
|
|
3373
|
+
timeoutMs: z5.number().int().positive().optional().describe("Reject automatically after this long, in milliseconds. Omit to wait for ever"),
|
|
3374
|
+
view: longText.optional().describe(
|
|
3375
|
+
"A review page shown beside the question: HTML, or the path of a .html file, with templates filled in when the gate opens"
|
|
3376
|
+
),
|
|
3377
|
+
edit: longText.optional().describe(
|
|
3378
|
+
"Text the reviewer may rewrite, as a template. Later steps read the result as {{steps.<gate>.text}}. When it expands to a JSON list of records the reviewer sees a table, and the rows they keep are {{steps.<gate>.items}}"
|
|
3379
|
+
),
|
|
3380
|
+
feedback: z5.object({
|
|
3381
|
+
from: V.id.describe(
|
|
3382
|
+
"Id of an earlier step, outside any loop body, that leads to this gate. Send back re-runs everything from it to the gate"
|
|
3383
|
+
),
|
|
3384
|
+
maxRounds: z5.number().int().min(2).max(MAX_GATE_ROUNDS).describe(`How many times the gate may ask, the first included (2 to ${MAX_GATE_ROUNDS})`)
|
|
3385
|
+
}).loose().optional().describe(
|
|
3386
|
+
"Lets the reviewer send the work back with a comment instead of only approving or rejecting; the comment is {{steps.<gate>.feedback}} to the steps that redo it"
|
|
3387
|
+
)
|
|
3388
|
+
}).loose();
|
|
3389
|
+
var createTaskFromItemConfigSchema = z5.object({
|
|
3390
|
+
nodeType: z5.literal("createTaskFromItem").optional().describe("Always createTaskFromItem"),
|
|
3391
|
+
project: z5.string().max(200).describe(
|
|
3392
|
+
"Project the task lands in, by name, or fromConnection to use the connection's execution project"
|
|
3393
|
+
),
|
|
3394
|
+
initialStatus: taskStatus.describe(
|
|
3395
|
+
"Status of a newly created task. Re-syncing an existing one never changes its status"
|
|
3396
|
+
)
|
|
3397
|
+
}).loose();
|
|
3398
|
+
var callConnectorActionConfigSchema = z5.object({
|
|
3399
|
+
nodeType: z5.literal("callConnectorAction").optional().describe("Always callConnectorAction"),
|
|
3400
|
+
connectionId: z5.string().max(100).describe("Connection the action runs against, from list_connections"),
|
|
3401
|
+
action: V.shortText.describe(
|
|
3402
|
+
"Action type from the connector, e.g. commentOnPullRequest; list_connector_actions lists them with their args"
|
|
3403
|
+
),
|
|
3404
|
+
actionLabel: V.shortText.optional().describe("The action's display name, for the canvas"),
|
|
3405
|
+
connectorId: V.shortText.optional().describe("Connector the action belongs to, for a step placed before a connection exists"),
|
|
3406
|
+
args: z5.record(z5.string(), longText).optional().describe(
|
|
3407
|
+
"The action's arguments by name, every value a string template; a list or object a template names arrives as JSON text. list_connector_actions gives each action's args"
|
|
3408
|
+
)
|
|
3409
|
+
}).loose();
|
|
3410
|
+
var httpRequestConfigSchema = z5.object({
|
|
3411
|
+
nodeType: z5.literal("httpRequest").optional().describe("Always httpRequest"),
|
|
3412
|
+
method: z5.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).describe("HTTP method"),
|
|
3413
|
+
url: z5.string().max(2048).describe(
|
|
3414
|
+
"Absolute URL, or a path resolved against the profile's base URL. Templates expand"
|
|
3415
|
+
),
|
|
3416
|
+
headers: z5.record(z5.string(), longText).optional().describe("Request headers. Names are literal; values are templates"),
|
|
3417
|
+
body: longText.optional().describe("Request body as a template; empty sends none"),
|
|
3418
|
+
profileConnectionId: V.id.optional().describe(
|
|
3419
|
+
"An http connection whose base URL and auth are applied server-side, so no credential sits in the workflow"
|
|
3420
|
+
)
|
|
3421
|
+
}).loose();
|
|
3422
|
+
var loopConfigSchema = z5.object({
|
|
3423
|
+
nodeType: z5.literal("loop").optional().describe("Always loop"),
|
|
3424
|
+
mode: z5.enum(["repeat", "forEach"]).optional().describe(
|
|
3425
|
+
`repeat (default) runs the body up to maxIterations times; forEach runs it once for each item of items, with no cap on how many`
|
|
3426
|
+
),
|
|
3427
|
+
items: longText.optional().describe(
|
|
3428
|
+
"For forEach: the list to walk, as a template naming it, e.g. {{steps.gate.items}}. JSON text of a list works too, and so does an object holding exactly one list. Each pass reads its item as {{loop.item}} or {{loop.item.<field>}}"
|
|
3429
|
+
),
|
|
3430
|
+
bodyNodeIds: z5.array(V.id).min(1).describe(
|
|
3431
|
+
"Ids of the steps the loop runs each pass. Wire them with edges: loop -> the entry steps, then between body steps, and from the last body steps to the step after the loop"
|
|
3432
|
+
),
|
|
3433
|
+
maxIterations: z5.number().optional().describe(
|
|
3434
|
+
`For repeat: how many passes at most, a whole number from 1 to ${MAX_LOOP_ITERATIONS}. Required for repeat, ignored for forEach`
|
|
3435
|
+
),
|
|
3436
|
+
until: conditionConfigSchema.optional().describe(
|
|
3437
|
+
"Checked after each pass; the loop stops early once it holds, e.g. {{steps.review.approved}} equals true"
|
|
3438
|
+
)
|
|
3439
|
+
}).loose().superRefine((config, ctx) => {
|
|
3440
|
+
if (config.mode === "forEach") {
|
|
3441
|
+
if (!config.items?.trim()) {
|
|
3442
|
+
ctx.addIssue({
|
|
3443
|
+
code: "custom",
|
|
3444
|
+
path: ["items"],
|
|
3445
|
+
message: "a forEach loop needs items: a template naming the list to walk"
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
const max = config.maxIterations;
|
|
3451
|
+
if (typeof max !== "number" || !Number.isInteger(max) || max < 1 || max > MAX_LOOP_ITERATIONS) {
|
|
3452
|
+
ctx.addIssue({
|
|
3453
|
+
code: "custom",
|
|
3454
|
+
path: ["maxIterations"],
|
|
3455
|
+
message: `a repeat loop needs maxIterations as a whole number from 1 to ${MAX_LOOP_ITERATIONS}`
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
});
|
|
3459
|
+
var edgeSchema = z5.object({
|
|
3460
|
+
id: V.id.describe("Unique within the workflow"),
|
|
3461
|
+
source: V.id.describe("Id of the node that runs first"),
|
|
3462
|
+
target: V.id.describe("Id of the node that runs after it"),
|
|
3463
|
+
// Which branch of a `condition` node this edge represents. Omit for normal
|
|
3464
|
+
// edges; required to wire both outcomes of a condition.
|
|
3465
|
+
conditionBranch: z5.enum(["true", "false"]).optional().describe(
|
|
3466
|
+
"On an edge leaving a condition: which outcome takes it. Wire both. Omit on every other edge"
|
|
3467
|
+
)
|
|
3468
|
+
});
|
|
3469
|
+
var NODE_TYPES = [
|
|
3470
|
+
"trigger",
|
|
3471
|
+
"launchAgent",
|
|
3472
|
+
"script",
|
|
3473
|
+
"condition",
|
|
3474
|
+
"approval",
|
|
3475
|
+
"createTaskFromItem",
|
|
3476
|
+
"callConnectorAction",
|
|
3477
|
+
"httpRequest",
|
|
3478
|
+
"loop"
|
|
3479
|
+
];
|
|
3480
|
+
var configSchemaByType = {
|
|
3481
|
+
trigger: triggerConfigSchema,
|
|
3482
|
+
launchAgent: launchAgentNodeConfigSchema,
|
|
3483
|
+
script: scriptConfigSchema,
|
|
3484
|
+
condition: conditionConfigSchema,
|
|
3485
|
+
approval: approvalConfigSchema,
|
|
3486
|
+
createTaskFromItem: createTaskFromItemConfigSchema,
|
|
3487
|
+
callConnectorAction: callConnectorActionConfigSchema,
|
|
3488
|
+
httpRequest: httpRequestConfigSchema,
|
|
3489
|
+
loop: loopConfigSchema
|
|
3490
|
+
};
|
|
3491
|
+
function nodeConfigIssues(type, config) {
|
|
3492
|
+
const result = configSchemaByType[type].safeParse(config);
|
|
3493
|
+
return result.success ? [] : result.error.issues;
|
|
3494
|
+
}
|
|
3107
3495
|
|
|
3108
3496
|
// ../shared/src/workflow-portability.ts
|
|
3109
3497
|
var PROJECT_PATH_TOKEN = "{{project.path}}";
|
|
@@ -3270,168 +3658,73 @@ function residualAbsolutePaths(portable) {
|
|
|
3270
3658
|
}
|
|
3271
3659
|
|
|
3272
3660
|
// src/tools/workflows.ts
|
|
3273
|
-
var launchAgentConfigSchema =
|
|
3274
|
-
agentType:
|
|
3661
|
+
var launchAgentConfigSchema = z6.object({
|
|
3662
|
+
agentType: z6.enum(["claude", "copilot", "codex", "opencode", "gemini"]),
|
|
3275
3663
|
projectName: V.name,
|
|
3276
3664
|
projectPath: V.absolutePath,
|
|
3277
|
-
args:
|
|
3665
|
+
args: z6.array(V.shortText).optional(),
|
|
3278
3666
|
displayName: V.shortText.optional(),
|
|
3279
3667
|
branch: V.shortText.optional(),
|
|
3280
3668
|
// boolean | 'fromContext' — `'fromContext'` only valid when the workflow's
|
|
3281
3669
|
// manual trigger is contextual (validated at runtime, not here).
|
|
3282
|
-
useWorktree:
|
|
3670
|
+
useWorktree: z6.union([z6.boolean(), z6.literal("fromContext")]).optional(),
|
|
3283
3671
|
remoteHostId: V.id.optional(),
|
|
3284
3672
|
prompt: V.prompt.optional(),
|
|
3285
|
-
promptDelayMs:
|
|
3673
|
+
promptDelayMs: z6.number().optional(),
|
|
3286
3674
|
taskId: V.id.optional(),
|
|
3287
|
-
taskFromQueue:
|
|
3675
|
+
taskFromQueue: z6.boolean().optional(),
|
|
3288
3676
|
// Runs the agent in the background and waits for it to finish (required for
|
|
3289
3677
|
// typed output). Opens a terminal tab when false/omitted.
|
|
3290
|
-
headless:
|
|
3678
|
+
headless: z6.boolean().optional(),
|
|
3291
3679
|
// JSON Schema the agent's final answer must satisfy (headless only). When set,
|
|
3292
3680
|
// the engine parses a matching object from the run and exposes its fields as
|
|
3293
3681
|
// `{{steps.<slug>.<field>}}` for downstream condition nodes.
|
|
3294
|
-
outputSchema:
|
|
3682
|
+
outputSchema: z6.record(z6.string(), z6.unknown()).optional()
|
|
3295
3683
|
}).refine((c) => !c.outputSchema || c.headless === true, {
|
|
3296
3684
|
// The engine only parses typed output for headless runs; reject a config that
|
|
3297
3685
|
// declares a schema it would silently ignore instead of accepting a lie.
|
|
3298
3686
|
message: "outputSchema requires headless: true",
|
|
3299
3687
|
path: ["outputSchema"]
|
|
3300
3688
|
});
|
|
3301
|
-
var
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
label: V.shortText,
|
|
3307
|
-
type: z5.enum(["text", "textarea", "number", "select", "boolean", "project", "branch"]),
|
|
3308
|
-
required: z5.boolean().optional(),
|
|
3309
|
-
defaultValue: V.shortText.optional(),
|
|
3310
|
-
options: z5.array(z5.object({ value: V.shortText, label: V.shortText })).optional(),
|
|
3311
|
-
placeholder: V.shortText.optional(),
|
|
3312
|
-
description: V.shortText.optional()
|
|
3313
|
-
}).superRefine((def, ctx) => {
|
|
3314
|
-
const options = def.options ?? [];
|
|
3315
|
-
if (def.type === "select" && options.length === 0) {
|
|
3316
|
-
ctx.addIssue({
|
|
3317
|
-
code: "custom",
|
|
3318
|
-
path: ["options"],
|
|
3319
|
-
message: `select input "${def.key}" declares no options, so the run dialog could offer nothing`
|
|
3320
|
-
});
|
|
3321
|
-
}
|
|
3322
|
-
if (def.defaultValue === void 0) return;
|
|
3323
|
-
if (def.type === "number" && !Number.isFinite(Number(def.defaultValue))) {
|
|
3324
|
-
ctx.addIssue({
|
|
3325
|
-
code: "custom",
|
|
3326
|
-
path: ["defaultValue"],
|
|
3327
|
-
message: `default "${def.defaultValue}" for number input "${def.key}" is not a finite number`
|
|
3328
|
-
});
|
|
3329
|
-
}
|
|
3330
|
-
if (def.type === "boolean" && !["true", "false"].includes(def.defaultValue)) {
|
|
3331
|
-
ctx.addIssue({
|
|
3332
|
-
code: "custom",
|
|
3333
|
-
path: ["defaultValue"],
|
|
3334
|
-
message: `default "${def.defaultValue}" for boolean input "${def.key}" must be "true" or "false"`
|
|
3335
|
-
});
|
|
3336
|
-
}
|
|
3337
|
-
if (def.type === "select" && options.length > 0 && !options.some((o) => o.value === def.defaultValue)) {
|
|
3338
|
-
ctx.addIssue({
|
|
3339
|
-
code: "custom",
|
|
3340
|
-
path: ["defaultValue"],
|
|
3341
|
-
message: `default "${def.defaultValue}" for select input "${def.key}" is not one of its options`
|
|
3342
|
-
});
|
|
3343
|
-
}
|
|
3344
|
-
});
|
|
3345
|
-
var workflowInputsSchema = z5.array(workflowInputDefSchema).superRefine((inputs, ctx) => {
|
|
3346
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3347
|
-
inputs.forEach((def, index) => {
|
|
3348
|
-
if (seen.has(def.key)) {
|
|
3349
|
-
ctx.addIssue({
|
|
3350
|
-
code: "custom",
|
|
3351
|
-
path: [index, "key"],
|
|
3352
|
-
message: `duplicate input key "${def.key}" \u2014 only one value can survive under {{inputs.${def.key}}}`
|
|
3353
|
-
});
|
|
3354
|
-
}
|
|
3355
|
-
seen.add(def.key);
|
|
3356
|
-
});
|
|
3357
|
-
});
|
|
3358
|
-
var triggerConfigSchema = z5.union([
|
|
3359
|
-
z5.object({
|
|
3360
|
-
triggerType: z5.literal("manual"),
|
|
3361
|
-
contextual: z5.boolean().optional(),
|
|
3362
|
-
inputs: workflowInputsSchema.optional()
|
|
3363
|
-
}),
|
|
3364
|
-
z5.object({ triggerType: z5.literal("once"), runAt: V.shortText }),
|
|
3365
|
-
z5.object({
|
|
3366
|
-
triggerType: z5.literal("recurring"),
|
|
3367
|
-
cron: V.shortText,
|
|
3368
|
-
timezone: V.shortText.optional()
|
|
3369
|
-
}),
|
|
3370
|
-
z5.object({ triggerType: z5.literal("taskCreated"), projectFilter: V.name.optional() }),
|
|
3371
|
-
z5.object({
|
|
3372
|
-
triggerType: z5.literal("taskStatusChanged"),
|
|
3373
|
-
projectFilter: V.name.optional(),
|
|
3374
|
-
fromStatus: z5.enum(["todo", "in_progress", "in_review", "done", "cancelled"]).optional(),
|
|
3375
|
-
toStatus: z5.enum(["todo", "in_progress", "in_review", "done", "cancelled"]).optional()
|
|
3376
|
-
}),
|
|
3377
|
-
z5.object({
|
|
3378
|
-
triggerType: z5.literal("connectorPoll"),
|
|
3379
|
-
connectionId: V.id,
|
|
3380
|
-
event: V.shortText,
|
|
3381
|
-
cron: V.shortText,
|
|
3382
|
-
timezone: V.shortText.optional()
|
|
3383
|
-
}),
|
|
3384
|
-
z5.object({
|
|
3385
|
-
triggerType: z5.literal("webhook"),
|
|
3386
|
-
method: z5.enum(["POST", "GET"]),
|
|
3387
|
-
token: V.shortText
|
|
3388
|
-
})
|
|
3389
|
-
]);
|
|
3390
|
-
var nodeSchema = z5.object({
|
|
3391
|
-
id: V.id,
|
|
3392
|
-
// Full node palette — parity with the editor. `config` is a passthrough so
|
|
3393
|
-
// each type carries its own shape (ConditionConfig, ApprovalConfig, etc.).
|
|
3394
|
-
type: z5.enum([
|
|
3395
|
-
"trigger",
|
|
3396
|
-
"launchAgent",
|
|
3397
|
-
"script",
|
|
3398
|
-
"condition",
|
|
3399
|
-
"approval",
|
|
3400
|
-
"createTaskFromItem",
|
|
3401
|
-
"callConnectorAction",
|
|
3402
|
-
"httpRequest",
|
|
3403
|
-
"loop"
|
|
3404
|
-
]),
|
|
3405
|
-
label: V.shortText,
|
|
3689
|
+
var nodeShapeSchema = z6.object({
|
|
3690
|
+
id: V.id.describe("Unique within the workflow; edges and bodyNodeIds refer to it"),
|
|
3691
|
+
// Full node palette — parity with the editor.
|
|
3692
|
+
type: z6.enum(NODE_TYPES).describe("Node type; describe_workflow_nodes gives the config each one takes"),
|
|
3693
|
+
label: V.shortText.describe("Name shown on the canvas and in run history"),
|
|
3406
3694
|
// Referenced by typed step vars as `{{steps.<slug>.<field>}}`. Set one on any
|
|
3407
3695
|
// node whose output a later node consumes.
|
|
3408
|
-
slug: V.shortText.optional(),
|
|
3409
|
-
config:
|
|
3410
|
-
position:
|
|
3696
|
+
slug: V.shortText.optional().describe("Name later steps read this one by, as {{steps.<slug>.<field>}}"),
|
|
3697
|
+
config: z6.record(z6.string(), z6.unknown()).describe("The type's own settings; describe_workflow_nodes has the schema for each type"),
|
|
3698
|
+
position: z6.object({ x: z6.number(), y: z6.number() }).describe("Where the node sits on the canvas"),
|
|
3411
3699
|
// Omitted means stop. Declared here because the object strips what it does
|
|
3412
3700
|
// not name: without it a workflow authored over MCP could never say a step
|
|
3413
3701
|
// is survivable, and the field would be dropped without complaint.
|
|
3414
|
-
onError:
|
|
3415
|
-
})
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3702
|
+
onError: z6.enum(["stop", "continue"]).optional().describe("stop (default) ends the run when this step fails; continue carries on past it")
|
|
3703
|
+
});
|
|
3704
|
+
function describeConfigIssues(node) {
|
|
3705
|
+
return nodeConfigIssues(node.type, node.config).map(
|
|
3706
|
+
(issue) => `${["config", ...issue.path.map(String)].join(".")}: ${issue.message}`
|
|
3707
|
+
);
|
|
3708
|
+
}
|
|
3709
|
+
function checkNodeConfigs(nodes, stored) {
|
|
3710
|
+
const errors = [];
|
|
3711
|
+
const warnings = [];
|
|
3712
|
+
for (const node of nodes) {
|
|
3713
|
+
const issues = describeConfigIssues(node);
|
|
3714
|
+
if (issues.length === 0) continue;
|
|
3715
|
+
const before = stored.find((n) => n.id === node.id && n.type === node.type);
|
|
3716
|
+
const untouched = before !== void 0 && isDeepStrictEqual(before.config, node.config);
|
|
3717
|
+
const line = `node "${node.label || node.id}" ${issues.join(", ")}`;
|
|
3718
|
+
if (untouched) warnings.push(line);
|
|
3719
|
+
else errors.push(line);
|
|
3424
3720
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
message: `loop "${node.id}" needs maxIterations as a whole number from 1 to ${MAX_LOOP_ITERATIONS}`
|
|
3431
|
-
});
|
|
3721
|
+
return { errors, warnings };
|
|
3722
|
+
}
|
|
3723
|
+
var nodeSchema = nodeShapeSchema.superRefine((node, ctx) => {
|
|
3724
|
+
for (const issue of nodeConfigIssues(node.type, node.config)) {
|
|
3725
|
+
ctx.addIssue({ code: "custom", path: ["config", ...issue.path], message: issue.message });
|
|
3432
3726
|
}
|
|
3433
3727
|
});
|
|
3434
|
-
var MAX_LOOP_ITERATIONS = 10;
|
|
3435
3728
|
function validateLoopBodies(nodes) {
|
|
3436
3729
|
const ids = new Set(nodes.map((n) => n.id));
|
|
3437
3730
|
const errors = [];
|
|
@@ -3449,14 +3742,21 @@ function validateLoopBodies(nodes) {
|
|
|
3449
3742
|
}
|
|
3450
3743
|
return errors;
|
|
3451
3744
|
}
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
}
|
|
3745
|
+
function validateLoopStructures(nodes, edges) {
|
|
3746
|
+
const graph = nodes;
|
|
3747
|
+
const wires = edges;
|
|
3748
|
+
return graph.filter((node) => node.type === "loop").flatMap((loop) => {
|
|
3749
|
+
const error = loopStructureError(graph, wires, loop);
|
|
3750
|
+
return error ? [`loop "${loop.label || loop.id}": ${error}`] : [];
|
|
3751
|
+
});
|
|
3752
|
+
}
|
|
3753
|
+
function validateGraph(nodes, edges) {
|
|
3754
|
+
return [
|
|
3755
|
+
...validateLoopBodies(nodes),
|
|
3756
|
+
...validateLoopStructures(nodes, edges),
|
|
3757
|
+
...validateGateFeedback(nodes, edges)
|
|
3758
|
+
];
|
|
3759
|
+
}
|
|
3460
3760
|
function buildGraphFromFlat(trigger, actions) {
|
|
3461
3761
|
const nodes = [];
|
|
3462
3762
|
const edges = [];
|
|
@@ -3594,6 +3894,7 @@ var ANSWERED = {
|
|
|
3594
3894
|
};
|
|
3595
3895
|
function validateGateFeedback(nodes, edges) {
|
|
3596
3896
|
const errors = [];
|
|
3897
|
+
const owners = loopBodyOwners(nodes);
|
|
3597
3898
|
for (const node of nodes) {
|
|
3598
3899
|
if (node.type !== "approval" || node.config?.feedback === void 0) continue;
|
|
3599
3900
|
const feedback = node.config.feedback;
|
|
@@ -3602,7 +3903,12 @@ function validateGateFeedback(nodes, edges) {
|
|
|
3602
3903
|
const source2 = nodes.find((n) => n.id === from);
|
|
3603
3904
|
if (!source2) errors.push(`gate "${name}" redoes from unknown step "${from}"`);
|
|
3604
3905
|
else if (source2.type === "trigger") errors.push(`gate "${name}" cannot redo from its trigger`);
|
|
3605
|
-
else if (
|
|
3906
|
+
else if (owners.has(from)) {
|
|
3907
|
+
const loop = nodes.find((n) => n.id === owners.get(from));
|
|
3908
|
+
errors.push(
|
|
3909
|
+
`gate "${name}" redoes from "${source2.label || from}", which is inside loop "${loop?.label || owners.get(from)}"; redo from the loop, or a step before it`
|
|
3910
|
+
);
|
|
3911
|
+
} else if (nodesBetween(from, node.id, edges).size === 0) {
|
|
3606
3912
|
errors.push(`gate "${name}" redoes from "${source2.label || from}", which does not lead to it`);
|
|
3607
3913
|
}
|
|
3608
3914
|
const rounds = Number(feedback?.maxRounds);
|
|
@@ -3665,17 +3971,19 @@ function registerWorkflowTools(server) {
|
|
|
3665
3971
|
);
|
|
3666
3972
|
server.tool(
|
|
3667
3973
|
"create_workflow",
|
|
3668
|
-
'Create a
|
|
3974
|
+
'Create a workflow: a graph of steps started by one trigger. Call describe_workflow_nodes first \u2014 it gives the config schema, outputs and rules of every node type, the template namespaces, and a complete example to copy. Pass nodes and edges (every node type is supported), or the convenience format (trigger + actions, a straight line of agents).\n\nNode types: trigger (exactly one, what starts the run), launchAgent (an AI agent; headless with an outputSchema gives typed fields), script (bash/python/node/powershell), condition (branches: wire both outcomes with edge conditionBranch "true" and "false"), approval (a human gate; can show a table to edit and send work back), createTaskFromItem, callConnectorAction (an action of a connected service; list_connector_actions gives its args), httpRequest, and loop.\n\nGive a node a slug and later steps read its output as {{steps.<slug>.<field>}}.\n\nLoops: config.bodyNodeIds lists the steps the loop runs each pass. Wire an edge from the loop to each body entry step, edges between body steps, and from the last body steps to the step after the loop; nothing outside the loop may point into its body, and a condition inside it keeps both branches inside. mode "repeat" runs up to maxIterations (1-10) passes, stopping early when until holds; mode "forEach" runs once per item of items (e.g. {{steps.gate.items}}), and body steps read {{loop.item.<field>}}. A loop body cannot hold an approval, a trigger or another loop.\n\nEach node config is checked against its type, and the graph for loop and gate wiring; a refusal names the node and field.',
|
|
3669
3975
|
{
|
|
3670
3976
|
name: V.title.describe("Workflow name"),
|
|
3671
3977
|
trigger: triggerConfigSchema.optional().describe("Trigger config (convenience mode). Defaults to manual."),
|
|
3672
|
-
actions:
|
|
3673
|
-
nodes:
|
|
3674
|
-
edges:
|
|
3978
|
+
actions: z6.array(launchAgentConfigSchema).optional().describe("Agents to run one after another (convenience mode). Auto-generates the graph."),
|
|
3979
|
+
nodes: z6.array(nodeSchema).optional().describe("Graph nodes; pass with edges. describe_workflow_nodes has each config schema"),
|
|
3980
|
+
edges: z6.array(edgeSchema).optional().describe("Graph edges; pass with nodes. A step runs once every edge into it has run"),
|
|
3675
3981
|
icon: V.shortText.optional().describe("Lucide icon name (default: zap)"),
|
|
3676
3982
|
icon_color: V.hexColor.optional().describe("Hex color (default: #6366f1)"),
|
|
3677
|
-
enabled:
|
|
3678
|
-
|
|
3983
|
+
enabled: z6.boolean().optional().describe(
|
|
3984
|
+
"Whether its trigger fires on its own (default: true). Manual runs work either way"
|
|
3985
|
+
),
|
|
3986
|
+
stagger_delay_ms: z6.number().optional().describe("Delay in ms between actions")
|
|
3679
3987
|
},
|
|
3680
3988
|
async (args) => {
|
|
3681
3989
|
let nodes;
|
|
@@ -3683,11 +3991,10 @@ function registerWorkflowTools(server) {
|
|
|
3683
3991
|
if (args.nodes && args.edges) {
|
|
3684
3992
|
nodes = args.nodes;
|
|
3685
3993
|
edges = args.edges;
|
|
3686
|
-
const
|
|
3687
|
-
|
|
3688
|
-
if (loopErrors.length > 0) {
|
|
3994
|
+
const graphErrors = validateGraph(nodes, edges);
|
|
3995
|
+
if (graphErrors.length > 0) {
|
|
3689
3996
|
return {
|
|
3690
|
-
content: [{ type: "text", text: `Error: ${
|
|
3997
|
+
content: [{ type: "text", text: `Error: ${graphErrors.join("; ")}` }],
|
|
3691
3998
|
isError: true
|
|
3692
3999
|
};
|
|
3693
4000
|
}
|
|
@@ -3715,17 +4022,19 @@ function registerWorkflowTools(server) {
|
|
|
3715
4022
|
);
|
|
3716
4023
|
server.tool(
|
|
3717
4024
|
"update_workflow",
|
|
3718
|
-
"
|
|
4025
|
+
"Change a workflow. Only the fields you pass change; nodes and edges each replace the whole list, so read the workflow first (list_workflows), edit it, and send the full list back. Checked like create_workflow: each node config against its type (describe_workflow_nodes), and the graph for loop and gate wiring. A node whose config you left exactly as stored is let through with a warning even if it breaks a newer rule, so an older workflow stays editable; fix what the warning names when you can.",
|
|
3719
4026
|
{
|
|
3720
4027
|
workflow_id: V.id.optional().describe("Workflow ID (from list_workflows)"),
|
|
3721
4028
|
id: V.id.optional().describe("Deprecated alias for workflow_id"),
|
|
3722
|
-
name: V.title.optional(),
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
4029
|
+
name: V.title.optional().describe("New name"),
|
|
4030
|
+
// The shape only: configs are checked in the handler, where an untouched
|
|
4031
|
+
// stored config can be told apart from a new mistake.
|
|
4032
|
+
nodes: z6.array(nodeShapeSchema).optional().describe("Every node of the workflow, replacing the stored list"),
|
|
4033
|
+
edges: z6.array(edgeSchema).optional().describe("Every edge of the workflow, replacing the stored list"),
|
|
4034
|
+
icon: V.shortText.optional().describe("Lucide icon name"),
|
|
4035
|
+
icon_color: V.hexColor.optional().describe("Hex color"),
|
|
4036
|
+
enabled: z6.boolean().optional().describe("Whether its trigger fires on its own. Manual runs work either way"),
|
|
4037
|
+
stagger_delay_ms: z6.number().optional().describe("Delay in ms between actions")
|
|
3729
4038
|
},
|
|
3730
4039
|
async (args) => {
|
|
3731
4040
|
const resolved = resolveWorkflowId(args);
|
|
@@ -3740,20 +4049,31 @@ function registerWorkflowTools(server) {
|
|
|
3740
4049
|
isError: true
|
|
3741
4050
|
};
|
|
3742
4051
|
}
|
|
3743
|
-
const
|
|
3744
|
-
if (args.name !== void 0) updates.name = args.name;
|
|
4052
|
+
const warnings = [];
|
|
3745
4053
|
if (args.nodes !== void 0) {
|
|
3746
|
-
const
|
|
4054
|
+
const { errors, warnings: legacy } = checkNodeConfigs(args.nodes, workflow.nodes);
|
|
4055
|
+
if (errors.length > 0) {
|
|
4056
|
+
return {
|
|
4057
|
+
content: [{ type: "text", text: `Error: ${errors.join("; ")}` }],
|
|
4058
|
+
isError: true
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
warnings.push(...legacy);
|
|
4062
|
+
}
|
|
4063
|
+
if (args.nodes !== void 0 || args.edges !== void 0) {
|
|
4064
|
+
const nodes = args.nodes ?? workflow.nodes;
|
|
3747
4065
|
const edges = args.edges ?? workflow.edges;
|
|
3748
|
-
const
|
|
3749
|
-
if (
|
|
4066
|
+
const graphErrors = validateGraph(nodes, edges);
|
|
4067
|
+
if (graphErrors.length > 0) {
|
|
3750
4068
|
return {
|
|
3751
|
-
content: [{ type: "text", text: `Error: ${
|
|
4069
|
+
content: [{ type: "text", text: `Error: ${graphErrors.join("; ")}` }],
|
|
3752
4070
|
isError: true
|
|
3753
4071
|
};
|
|
3754
4072
|
}
|
|
3755
|
-
updates.nodes = args.nodes;
|
|
3756
4073
|
}
|
|
4074
|
+
const updates = {};
|
|
4075
|
+
if (args.name !== void 0) updates.name = args.name;
|
|
4076
|
+
if (args.nodes !== void 0) updates.nodes = args.nodes;
|
|
3757
4077
|
if (args.edges !== void 0) updates.edges = args.edges;
|
|
3758
4078
|
if (args.icon !== void 0) updates.icon = args.icon;
|
|
3759
4079
|
if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
|
|
@@ -3761,8 +4081,17 @@ function registerWorkflowTools(server) {
|
|
|
3761
4081
|
if (args.stagger_delay_ms !== void 0) updates.staggerDelayMs = args.stagger_delay_ms;
|
|
3762
4082
|
await dbUpdateWorkflow(resolved.id, updates);
|
|
3763
4083
|
dbSignalChange();
|
|
4084
|
+
const saved = JSON.stringify({ ...workflow, ...updates }, null, 2);
|
|
3764
4085
|
return {
|
|
3765
|
-
content: [
|
|
4086
|
+
content: [
|
|
4087
|
+
{
|
|
4088
|
+
type: "text",
|
|
4089
|
+
text: warnings.length > 0 ? `Warning: saved, but these configs were stored before and break a current rule, so they were kept as they are:
|
|
4090
|
+
- ${warnings.join("\n - ")}
|
|
4091
|
+
|
|
4092
|
+
${saved}` : saved
|
|
4093
|
+
}
|
|
4094
|
+
]
|
|
3766
4095
|
};
|
|
3767
4096
|
}
|
|
3768
4097
|
);
|
|
@@ -3797,7 +4126,7 @@ function registerWorkflowTools(server) {
|
|
|
3797
4126
|
{
|
|
3798
4127
|
workflow_id: V.id.optional().describe("Filter by workflow ID"),
|
|
3799
4128
|
task_id: V.id.optional().describe("Filter by task ID (runs triggered by this task)"),
|
|
3800
|
-
limit:
|
|
4129
|
+
limit: z6.number().int().min(1).max(100).optional().describe("Max results (default: 20)")
|
|
3801
4130
|
},
|
|
3802
4131
|
async (args) => {
|
|
3803
4132
|
if (args.workflow_id && args.task_id) {
|
|
@@ -3877,13 +4206,13 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
|
|
|
3877
4206
|
"Approve, reject, or send back the approval gate a workflow run is parked on, the way the Vorn app does. Requires the Vorn app to be running: the decision is broadcast, and the instance holding the run is what resumes it. Read what is being approved first \u2014 list_workflow_runs names the waiting node and what it asks.",
|
|
3878
4207
|
{
|
|
3879
4208
|
run_id: V.id.describe("Run ID (from list_workflow_runs)"),
|
|
3880
|
-
decision:
|
|
4209
|
+
decision: z6.enum(["approve", "reject", "changes"]).describe(
|
|
3881
4210
|
"approve lets the run go on; reject ends it; changes sends the work back to the step the gate redoes from, with your comment, and the gate asks again"
|
|
3882
4211
|
),
|
|
3883
|
-
comment:
|
|
4212
|
+
comment: z6.string().max(2e4).optional().describe(
|
|
3884
4213
|
"Required for changes: what to change. Optional on approve and reject; kept with the run."
|
|
3885
4214
|
),
|
|
3886
|
-
edited:
|
|
4215
|
+
edited: z6.string().max(2e5).optional().describe(
|
|
3887
4216
|
"Your rewrite of the gate's editable text, when it has one. Kept beside the original, and read by later steps as {{steps.<gate>.text}}. Ignored on reject."
|
|
3888
4217
|
),
|
|
3889
4218
|
node_id: V.id.optional().describe("The waiting node, when a run has more than one gate open")
|
|
@@ -3930,19 +4259,22 @@ The run is stopped by the instance holding it, so confirm with list_workflow_run
|
|
|
3930
4259
|
const gateNode = approvalNode(workflow, target.nodeId);
|
|
3931
4260
|
const asked = run.nodeStates.find((n) => n.nodeId === target.nodeId)?.message ?? askedBy(gateNode);
|
|
3932
4261
|
try {
|
|
3933
|
-
const answer = await rpcCall(
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
4262
|
+
const answer = await rpcCall(
|
|
4263
|
+
"workflow:resolveGate",
|
|
4264
|
+
{
|
|
4265
|
+
runId: args.run_id,
|
|
4266
|
+
nodeId: target.nodeId,
|
|
4267
|
+
decision: args.decision,
|
|
4268
|
+
...args.comment?.trim() && { comment: args.comment.trim() },
|
|
4269
|
+
...args.edited?.trim() && { edited: args.edited.trim() }
|
|
4270
|
+
}
|
|
4271
|
+
);
|
|
3940
4272
|
if (answer?.accepted === false) {
|
|
3941
4273
|
return {
|
|
3942
4274
|
content: [
|
|
3943
4275
|
{
|
|
3944
4276
|
type: "text",
|
|
3945
|
-
text: args.decision === "changes" ? "Error: this gate takes no changes now: it has no step to redo from, or its rounds are used up. Approve or reject it instead." : "Error: the gate did not take that answer. Check list_workflow_runs."
|
|
4277
|
+
text: answer.reason ? `Error: ${answer.reason}` : args.decision === "changes" ? "Error: this gate takes no changes now: it has no step to redo from, or its rounds are used up. Approve or reject it instead." : "Error: the gate did not take that answer. Check list_workflow_runs."
|
|
3946
4278
|
}
|
|
3947
4279
|
],
|
|
3948
4280
|
isError: true
|
|
@@ -3974,7 +4306,7 @@ The decision went out; the instance holding the run acts on it, so a desktop has
|
|
|
3974
4306
|
"Get scheduler info for a workflow: execution log or next scheduled run. Requires the Vorn app to be running.",
|
|
3975
4307
|
{
|
|
3976
4308
|
workflow_id: V.id.optional().describe("Workflow ID (required for next_run, optional for log)"),
|
|
3977
|
-
info:
|
|
4309
|
+
info: z6.enum(["log", "next_run"]).optional().describe("What to retrieve (default: log)")
|
|
3978
4310
|
},
|
|
3979
4311
|
async (args) => {
|
|
3980
4312
|
const info = args.info ?? "log";
|
|
@@ -4012,7 +4344,7 @@ The decision went out; the instance holding the run acts on it, so a desktop has
|
|
|
4012
4344
|
"Run a workflow now, as if triggered manually. Supply values for any parameters the workflow declares (see the trigger node's inputs); declared defaults fill in anything omitted. Requires the Vorn app to be running. Returns as soon as the run is queued \u2014 poll list_workflow_runs for the outcome. Runs of one workflow go side by side; only a run repeating one started in the last ten seconds with the same inputs is refused as a duplicate.",
|
|
4013
4345
|
{
|
|
4014
4346
|
workflow_id: V.id.describe("Workflow ID (from list_workflows)"),
|
|
4015
|
-
inputs:
|
|
4347
|
+
inputs: z6.record(z6.string(), z6.union([z6.string(), z6.number(), z6.boolean()])).optional().describe("Values for the declared parameters, keyed by input key ({{inputs.<key>}})")
|
|
4016
4348
|
},
|
|
4017
4349
|
async (args) => {
|
|
4018
4350
|
const workflow = (await dbListWorkflows()).find((w) => w.id === args.workflow_id);
|
|
@@ -4120,7 +4452,7 @@ Warning: ${unnamed.length} step(s) point at a connection this install could not
|
|
|
4120
4452
|
"import_workflow",
|
|
4121
4453
|
"Import a workflow exported by export_workflow, resolving {{project.path}} and {{project.name}} against a registered project. Recorded connection requirements are rebound when this machine has one unambiguous match, and reported as still to connect otherwise. The id is derived from the bundle and the workflow's slug, so importing the same file again updates it in place instead of creating a duplicate.",
|
|
4122
4454
|
{
|
|
4123
|
-
workflow:
|
|
4455
|
+
workflow: z6.string().max(5e5).describe("The exported workflow JSON"),
|
|
4124
4456
|
project_name: V.name.describe("Registered project to resolve paths against"),
|
|
4125
4457
|
bundle: V.name.optional().describe("Namespace for the derived id (default: the project name)")
|
|
4126
4458
|
},
|
|
@@ -4168,11 +4500,7 @@ Warning: ${unnamed.length} step(s) point at a connection this install could not
|
|
|
4168
4500
|
isError: true
|
|
4169
4501
|
};
|
|
4170
4502
|
}
|
|
4171
|
-
const
|
|
4172
|
-
const loopErrors = [
|
|
4173
|
-
...validateLoopBodies(loose),
|
|
4174
|
-
...validateGateFeedback(loose, parsed.edges)
|
|
4175
|
-
];
|
|
4503
|
+
const loopErrors = validateGraph(parsed.nodes, parsed.edges);
|
|
4176
4504
|
if (loopErrors.length > 0) {
|
|
4177
4505
|
return {
|
|
4178
4506
|
content: [{ type: "text", text: `Error: ${loopErrors.join("; ")}` }],
|
|
@@ -4219,6 +4547,330 @@ Still to connect: ${pending}` : "")
|
|
|
4219
4547
|
);
|
|
4220
4548
|
}
|
|
4221
4549
|
|
|
4550
|
+
// src/tools/describe-nodes.ts
|
|
4551
|
+
import { z as z7 } from "zod";
|
|
4552
|
+
var COMMON_OUTPUTS = [
|
|
4553
|
+
{ field: "status", description: "success or error" },
|
|
4554
|
+
{ field: "error", description: "Why it failed, or empty" }
|
|
4555
|
+
];
|
|
4556
|
+
var DOCS = {
|
|
4557
|
+
trigger: {
|
|
4558
|
+
summary: "What starts a run: by hand, on a schedule, on a task event, when a session is restored, by polling a connection, or on a webhook. config.triggerType picks which.",
|
|
4559
|
+
outputs: [],
|
|
4560
|
+
rules: [
|
|
4561
|
+
"A workflow has exactly one trigger, and no edge points into it.",
|
|
4562
|
+
"A manual trigger may declare inputs; each is read anywhere as {{inputs.<key>}}, and execute_workflow takes their values.",
|
|
4563
|
+
"What the trigger carries is read as {{task.*}}, {{trigger.*}} or {{connectorItem.*}}, depending on its type."
|
|
4564
|
+
]
|
|
4565
|
+
},
|
|
4566
|
+
launchAgent: {
|
|
4567
|
+
summary: "Starts an AI coding agent in a project with a prompt. Headless, it runs in the background and the run waits for its answer; otherwise it opens a terminal tab.",
|
|
4568
|
+
outputs: [
|
|
4569
|
+
{ field: "output", description: "The agent's final answer (headless)" },
|
|
4570
|
+
{
|
|
4571
|
+
field: "<outputSchema property>",
|
|
4572
|
+
description: "Each top-level property of config.outputSchema, parsed from the answer (headless only)"
|
|
4573
|
+
},
|
|
4574
|
+
{ field: "worktreePath", description: "The directory the agent worked in" },
|
|
4575
|
+
...COMMON_OUTPUTS
|
|
4576
|
+
],
|
|
4577
|
+
rules: [
|
|
4578
|
+
"outputSchema requires headless: true, and an answer that does not match it fails the step.",
|
|
4579
|
+
"Read typed fields in a condition, e.g. variable {{steps.review.approved}} equals true, rather than matching words in output.",
|
|
4580
|
+
"agentType fromTask needs a task in scope: a task trigger, taskId or taskFromQueue. model needs a concrete agentType.",
|
|
4581
|
+
"worktreeMode fromStep reuses the worktree of the earlier agent step named by worktreeFromStepSlug."
|
|
4582
|
+
]
|
|
4583
|
+
},
|
|
4584
|
+
script: {
|
|
4585
|
+
summary: "Runs a bash, python, node or powershell script.",
|
|
4586
|
+
outputs: [
|
|
4587
|
+
{ field: "output", description: "What the script printed to stdout" },
|
|
4588
|
+
...COMMON_OUTPUTS
|
|
4589
|
+
],
|
|
4590
|
+
rules: [
|
|
4591
|
+
"Templates expand into the script text before it runs. Keep a value as data rather than code, e.g. inside a quoted heredoc: cat <<'EOF' ... EOF.",
|
|
4592
|
+
"A non-zero exit fails the step.",
|
|
4593
|
+
"secretsFrom names a connection whose secrets become environment variables, so no secret is written into the workflow."
|
|
4594
|
+
]
|
|
4595
|
+
},
|
|
4596
|
+
condition: {
|
|
4597
|
+
summary: "Compares a value, usually a step output, and sends the run down its true or its false branch.",
|
|
4598
|
+
outputs: [{ field: "output", description: '"true" or "false"' }, ...COMMON_OUTPUTS],
|
|
4599
|
+
rules: [
|
|
4600
|
+
'Give it two outgoing edges, one with conditionBranch "true" and one with "false". Steps on the branch not taken are skipped.',
|
|
4601
|
+
'Comparisons are on text: equals true matches the text "true".',
|
|
4602
|
+
"Inside a loop body, both branches must stay inside the body."
|
|
4603
|
+
]
|
|
4604
|
+
},
|
|
4605
|
+
approval: {
|
|
4606
|
+
summary: "Pauses the run until a person approves or rejects, in the app or with resolve_gate. It can show a review page, let the reviewer rewrite some text, and send the work back with a comment.",
|
|
4607
|
+
outputs: [
|
|
4608
|
+
{ field: "text", description: "The editable text as the reviewer left it" },
|
|
4609
|
+
{
|
|
4610
|
+
field: "items",
|
|
4611
|
+
description: "When the editable text is a JSON list of records: the rows the reviewer kept, as a list, ready for a forEach loop"
|
|
4612
|
+
},
|
|
4613
|
+
{ field: "feedback", description: "The reviewer's latest comment" },
|
|
4614
|
+
{ field: "feedbackAll", description: "Every comment, one line per round" },
|
|
4615
|
+
{ field: "round", description: "Which time the gate asked, from 1" },
|
|
4616
|
+
...COMMON_OUTPUTS
|
|
4617
|
+
],
|
|
4618
|
+
rules: [
|
|
4619
|
+
"A gate cannot sit inside a loop body.",
|
|
4620
|
+
"When edit expands to a JSON list of records the reviewer sees a table; a rewrite that is no longer valid JSON is refused.",
|
|
4621
|
+
`feedback.from is the id of an earlier step that leads to this gate, not the trigger and not a step inside a loop. Sending back re-runs every step from it to the gate. maxRounds is 2 to ${MAX_GATE_ROUNDS}.`,
|
|
4622
|
+
"A reject ends the run."
|
|
4623
|
+
]
|
|
4624
|
+
},
|
|
4625
|
+
createTaskFromItem: {
|
|
4626
|
+
summary: "Creates a task from the connector item that started the run, or updates the one it created before.",
|
|
4627
|
+
outputs: [
|
|
4628
|
+
{ field: "output", description: "Imported or Updated, with the item id and title" },
|
|
4629
|
+
...COMMON_OUTPUTS
|
|
4630
|
+
],
|
|
4631
|
+
rules: [
|
|
4632
|
+
"Needs {{connectorItem.*}}: put it after a connectorPoll trigger.",
|
|
4633
|
+
"Re-syncing an item updates its title and description but never its status."
|
|
4634
|
+
]
|
|
4635
|
+
},
|
|
4636
|
+
callConnectorAction: {
|
|
4637
|
+
summary: "Runs one action of a connected service (a comment, an issue, a merge...) with arguments built from templates.",
|
|
4638
|
+
outputs: [
|
|
4639
|
+
{ field: "output", description: "The action result as text" },
|
|
4640
|
+
{
|
|
4641
|
+
field: "<the action's output fields>",
|
|
4642
|
+
description: "Each field the action declares in its outputSchema; list_connector_actions gives them"
|
|
4643
|
+
},
|
|
4644
|
+
...COMMON_OUTPUTS
|
|
4645
|
+
],
|
|
4646
|
+
rules: [
|
|
4647
|
+
"connectionId comes from list_connections; action and its args from list_connector_actions.",
|
|
4648
|
+
"A step waits, rather than fails, when its connection needs signing in again."
|
|
4649
|
+
]
|
|
4650
|
+
},
|
|
4651
|
+
httpRequest: {
|
|
4652
|
+
summary: "Sends one HTTP request, optionally through an http connection that adds auth.",
|
|
4653
|
+
outputs: [
|
|
4654
|
+
{ field: "body", description: "Response body, JSON-parsed when possible" },
|
|
4655
|
+
{ field: "headers", description: "Response headers, by lower-case name" },
|
|
4656
|
+
{
|
|
4657
|
+
field: "output",
|
|
4658
|
+
description: 'HTTP <code>, e.g. "HTTP 404": the way to read the response status'
|
|
4659
|
+
},
|
|
4660
|
+
{
|
|
4661
|
+
field: "status",
|
|
4662
|
+
description: "success or error: the step's own status, which replaces the response code under this name"
|
|
4663
|
+
},
|
|
4664
|
+
{ field: "error", description: "Why it failed, or empty" }
|
|
4665
|
+
],
|
|
4666
|
+
rules: [
|
|
4667
|
+
"Put credentials in an http connection (profileConnectionId), not in headers.",
|
|
4668
|
+
'Any answer succeeds, a 4xx or 5xx included: to act on the code, test {{steps.<slug>.output}} (e.g. contains "HTTP 2"). Only a network error or the 30 second timeout fails the step. Redirects are not followed.'
|
|
4669
|
+
]
|
|
4670
|
+
},
|
|
4671
|
+
loop: {
|
|
4672
|
+
summary: `Runs a group of steps (its body) several times: repeat up to ${MAX_LOOP_ITERATIONS} passes, stopping early when until holds, or forEach once per item of a list.`,
|
|
4673
|
+
outputs: [
|
|
4674
|
+
{ field: "output", description: "How many passes ran" },
|
|
4675
|
+
{ field: "passes", description: "How many passes ran, as a number" },
|
|
4676
|
+
{
|
|
4677
|
+
field: "count",
|
|
4678
|
+
description: "How many passes were planned: maxIterations, or the number of items"
|
|
4679
|
+
},
|
|
4680
|
+
{
|
|
4681
|
+
field: "results",
|
|
4682
|
+
description: "Per pass: {index, item?, status, steps: {<slug>: {output, status, error, ...}}}; each output cut to loopResultOutputChars"
|
|
4683
|
+
},
|
|
4684
|
+
{
|
|
4685
|
+
field: "outputs",
|
|
4686
|
+
description: "Per pass, the output of the last body step that ran (on the branch taken), cut to loopResultOutputChars"
|
|
4687
|
+
},
|
|
4688
|
+
...COMMON_OUTPUTS
|
|
4689
|
+
],
|
|
4690
|
+
rules: [
|
|
4691
|
+
"bodyNodeIds lists every step in the body. Wire the loop to each body entry step, body steps to each other, and the last body steps to the step after the loop.",
|
|
4692
|
+
"Nothing outside the loop may point into its body; only the loop starts its steps.",
|
|
4693
|
+
"A body cannot hold an approval gate, a trigger or another loop, and cannot contain a cycle.",
|
|
4694
|
+
`repeat needs maxIterations, 1 to ${MAX_LOOP_ITERATIONS}. forEach needs items and has no cap.`,
|
|
4695
|
+
"Body steps read {{loop.item}} / {{loop.item.<field>}} (forEach), {{loop.index}} from 0, {{loop.number}} from 1 and {{loop.count}}.",
|
|
4696
|
+
"A body step that fails ends the loop there, unless that step has onError: continue.",
|
|
4697
|
+
"Inside the body, {{steps.<slug>.*}} of a body step is this pass. After the loop, read the passes through {{steps.<loop>.results}} or .outputs."
|
|
4698
|
+
]
|
|
4699
|
+
}
|
|
4700
|
+
};
|
|
4701
|
+
var EXAMPLE = {
|
|
4702
|
+
nodes: [
|
|
4703
|
+
{
|
|
4704
|
+
id: "start",
|
|
4705
|
+
type: "trigger",
|
|
4706
|
+
label: "Start",
|
|
4707
|
+
config: {
|
|
4708
|
+
triggerType: "manual",
|
|
4709
|
+
inputs: [{ key: "pullRequest", label: "Pull request", type: "text", required: true }]
|
|
4710
|
+
},
|
|
4711
|
+
position: { x: 0, y: 0 }
|
|
4712
|
+
},
|
|
4713
|
+
{
|
|
4714
|
+
id: "review",
|
|
4715
|
+
type: "callConnectorAction",
|
|
4716
|
+
label: "Read review findings",
|
|
4717
|
+
slug: "review",
|
|
4718
|
+
config: {
|
|
4719
|
+
nodeType: "callConnectorAction",
|
|
4720
|
+
connectionId: "<connection id from list_connections>",
|
|
4721
|
+
action: "listReviewFindings",
|
|
4722
|
+
args: { pullRequest: "{{inputs.pullRequest}}" }
|
|
4723
|
+
},
|
|
4724
|
+
position: { x: 0, y: 140 }
|
|
4725
|
+
},
|
|
4726
|
+
{
|
|
4727
|
+
id: "gate",
|
|
4728
|
+
type: "approval",
|
|
4729
|
+
label: "Pick findings to post",
|
|
4730
|
+
slug: "gate",
|
|
4731
|
+
config: {
|
|
4732
|
+
message: "Keep the findings worth a comment, and delete the rest.",
|
|
4733
|
+
edit: "{{steps.review.findings}}"
|
|
4734
|
+
},
|
|
4735
|
+
position: { x: 0, y: 280 }
|
|
4736
|
+
},
|
|
4737
|
+
{
|
|
4738
|
+
id: "loop",
|
|
4739
|
+
type: "loop",
|
|
4740
|
+
label: "For each finding",
|
|
4741
|
+
slug: "loop",
|
|
4742
|
+
config: {
|
|
4743
|
+
nodeType: "loop",
|
|
4744
|
+
mode: "forEach",
|
|
4745
|
+
items: "{{steps.gate.items}}",
|
|
4746
|
+
bodyNodeIds: ["severe", "comment", "skip"]
|
|
4747
|
+
},
|
|
4748
|
+
position: { x: 0, y: 420 }
|
|
4749
|
+
},
|
|
4750
|
+
{
|
|
4751
|
+
id: "severe",
|
|
4752
|
+
type: "condition",
|
|
4753
|
+
label: "High severity?",
|
|
4754
|
+
slug: "severe",
|
|
4755
|
+
config: { variable: "{{loop.item.severity}}", operator: "equals", value: "high" },
|
|
4756
|
+
position: { x: 0, y: 560 }
|
|
4757
|
+
},
|
|
4758
|
+
{
|
|
4759
|
+
id: "comment",
|
|
4760
|
+
type: "callConnectorAction",
|
|
4761
|
+
label: "Comment on the line",
|
|
4762
|
+
slug: "comment",
|
|
4763
|
+
config: {
|
|
4764
|
+
nodeType: "callConnectorAction",
|
|
4765
|
+
connectionId: "<connection id from list_connections>",
|
|
4766
|
+
action: "commentOnPullRequest",
|
|
4767
|
+
args: {
|
|
4768
|
+
pullRequest: "{{inputs.pullRequest}}",
|
|
4769
|
+
path: "{{loop.item.path}}",
|
|
4770
|
+
line: "{{loop.item.line}}",
|
|
4771
|
+
body: "{{loop.item.body}}"
|
|
4772
|
+
}
|
|
4773
|
+
},
|
|
4774
|
+
position: { x: -160, y: 700 }
|
|
4775
|
+
},
|
|
4776
|
+
{
|
|
4777
|
+
id: "skip",
|
|
4778
|
+
type: "script",
|
|
4779
|
+
label: "Note the skip",
|
|
4780
|
+
slug: "skip",
|
|
4781
|
+
config: {
|
|
4782
|
+
scriptType: "bash",
|
|
4783
|
+
scriptContent: "cat <<'EOF'\nNot posted: {{loop.item.path}}\nEOF\n"
|
|
4784
|
+
},
|
|
4785
|
+
position: { x: 160, y: 700 }
|
|
4786
|
+
},
|
|
4787
|
+
{
|
|
4788
|
+
id: "summary",
|
|
4789
|
+
type: "script",
|
|
4790
|
+
label: "Summarise",
|
|
4791
|
+
slug: "summary",
|
|
4792
|
+
config: {
|
|
4793
|
+
scriptType: "bash",
|
|
4794
|
+
scriptContent: "cat <<'EOF'\nWhat each finding came to:\n{{steps.loop.outputs}}\nEOF\n"
|
|
4795
|
+
},
|
|
4796
|
+
position: { x: 0, y: 840 }
|
|
4797
|
+
}
|
|
4798
|
+
],
|
|
4799
|
+
edges: [
|
|
4800
|
+
{ id: "e-start-review", source: "start", target: "review" },
|
|
4801
|
+
{ id: "e-review-gate", source: "review", target: "gate" },
|
|
4802
|
+
{ id: "e-gate-loop", source: "gate", target: "loop" },
|
|
4803
|
+
{ id: "e-loop-severe", source: "loop", target: "severe" },
|
|
4804
|
+
{ id: "e-severe-comment", source: "severe", target: "comment", conditionBranch: "true" },
|
|
4805
|
+
{ id: "e-severe-skip", source: "severe", target: "skip", conditionBranch: "false" },
|
|
4806
|
+
{ id: "e-comment-summary", source: "comment", target: "summary" },
|
|
4807
|
+
{ id: "e-skip-summary", source: "skip", target: "summary" }
|
|
4808
|
+
]
|
|
4809
|
+
};
|
|
4810
|
+
function jsonSchema(schema) {
|
|
4811
|
+
return z7.toJSONSchema(schema, { io: "input", unrepresentable: "any" });
|
|
4812
|
+
}
|
|
4813
|
+
var WORKFLOW_NODES_REFERENCE = {
|
|
4814
|
+
nodeTypes: Object.fromEntries(
|
|
4815
|
+
NODE_TYPES.map((type) => [
|
|
4816
|
+
type,
|
|
4817
|
+
{
|
|
4818
|
+
summary: DOCS[type].summary,
|
|
4819
|
+
configSchema: jsonSchema(configSchemaByType[type]),
|
|
4820
|
+
outputs: DOCS[type].outputs,
|
|
4821
|
+
rules: DOCS[type].rules
|
|
4822
|
+
}
|
|
4823
|
+
])
|
|
4824
|
+
),
|
|
4825
|
+
edgeSchema: jsonSchema(edgeSchema),
|
|
4826
|
+
limits: {
|
|
4827
|
+
repeatMaxIterations: MAX_LOOP_ITERATIONS,
|
|
4828
|
+
forEachMaxItems: "uncapped",
|
|
4829
|
+
maxGateRounds: MAX_GATE_ROUNDS,
|
|
4830
|
+
templateTextChars: MAX_OUTPUT_LENGTH,
|
|
4831
|
+
loopResultOutputChars: LOOP_RESULT_OUTPUT_CHARS
|
|
4832
|
+
},
|
|
4833
|
+
templates: {
|
|
4834
|
+
syntax: "{{namespace.path.to.field}} anywhere a config takes text. An unknown namespace is left as written; a known one with nothing there becomes empty. Text longer than templateTextChars keeps its end.",
|
|
4835
|
+
namespaces: {
|
|
4836
|
+
steps: "{{steps.<slug>.<field>}}: the output of an earlier step, by its slug; the fields are listed under each node type",
|
|
4837
|
+
task: "{{task.title}}, .description, .id, .status, .branch, .projectName: the task a task trigger fired for",
|
|
4838
|
+
trigger: "{{trigger.*}}: what the trigger carried: fromStatus/toStatus, restore, or a webhook body, headers and query",
|
|
4839
|
+
connectorItem: "{{connectorItem.title}}, .body, .externalId, .externalUrl: the item a connectorPoll trigger fired for",
|
|
4840
|
+
inputs: "{{inputs.<key>}}: the values a manual run was started with",
|
|
4841
|
+
context: "{{context.cwd}}, .projectPath, .projectName, .branch, .worktreePath: where a contextual manual run was started from",
|
|
4842
|
+
loop: "{{loop.item}}, {{loop.item.<field>}}, {{loop.index}} (from 0), {{loop.number}} (from 1), {{loop.count}}: only inside a loop body"
|
|
4843
|
+
},
|
|
4844
|
+
examples: [
|
|
4845
|
+
"{{steps.review.approved}}",
|
|
4846
|
+
"{{steps.gate.items}}",
|
|
4847
|
+
"{{loop.item.path}}",
|
|
4848
|
+
"{{steps.loop.outputs}}",
|
|
4849
|
+
"{{inputs.pullRequest}}",
|
|
4850
|
+
"{{task.title}}"
|
|
4851
|
+
]
|
|
4852
|
+
},
|
|
4853
|
+
example: EXAMPLE
|
|
4854
|
+
};
|
|
4855
|
+
function registerDescribeNodesTool(server) {
|
|
4856
|
+
server.tool(
|
|
4857
|
+
"describe_workflow_nodes",
|
|
4858
|
+
"The reference for writing a workflow: for each node type its config schema, the outputs later steps read, and the rules create_workflow checks; the edge schema; limits; the template namespaces; and a complete example workflow to adapt. Call it before create_workflow or update_workflow.",
|
|
4859
|
+
{
|
|
4860
|
+
types: z7.array(z7.enum(NODE_TYPES)).optional().describe("Only these node types. Omit for all nine")
|
|
4861
|
+
},
|
|
4862
|
+
async (args) => {
|
|
4863
|
+
const reference = args.types?.length ? {
|
|
4864
|
+
...WORKFLOW_NODES_REFERENCE,
|
|
4865
|
+
nodeTypes: Object.fromEntries(
|
|
4866
|
+
args.types.map((type) => [type, WORKFLOW_NODES_REFERENCE.nodeTypes[type]])
|
|
4867
|
+
)
|
|
4868
|
+
} : WORKFLOW_NODES_REFERENCE;
|
|
4869
|
+
return { content: [{ type: "text", text: JSON.stringify(reference, null, 2) }] };
|
|
4870
|
+
}
|
|
4871
|
+
);
|
|
4872
|
+
}
|
|
4873
|
+
|
|
4222
4874
|
// src/tools/config.ts
|
|
4223
4875
|
function registerConfigTools(server) {
|
|
4224
4876
|
server.tool(
|
|
@@ -4233,7 +4885,7 @@ function registerConfigTools(server) {
|
|
|
4233
4885
|
|
|
4234
4886
|
// src/tools/workspaces.ts
|
|
4235
4887
|
import crypto5 from "crypto";
|
|
4236
|
-
import { z as
|
|
4888
|
+
import { z as z8 } from "zod";
|
|
4237
4889
|
function registerWorkspaceTools(server) {
|
|
4238
4890
|
server.tool("list_workspaces", "List all workspaces", async () => {
|
|
4239
4891
|
const workspaces = await dbListWorkspaces();
|
|
@@ -4270,7 +4922,7 @@ function registerWorkspaceTools(server) {
|
|
|
4270
4922
|
name: V.name.optional().describe("New name"),
|
|
4271
4923
|
icon: V.shortText.optional().describe("Lucide icon name"),
|
|
4272
4924
|
icon_color: V.hexColor.optional().describe("Hex color for icon"),
|
|
4273
|
-
order:
|
|
4925
|
+
order: z8.number().int().min(0).optional().describe("Sort order")
|
|
4274
4926
|
},
|
|
4275
4927
|
async (args) => {
|
|
4276
4928
|
const existing = await dbListWorkspaces();
|
|
@@ -4318,7 +4970,7 @@ function registerWorkspaceTools(server) {
|
|
|
4318
4970
|
}
|
|
4319
4971
|
|
|
4320
4972
|
// src/tools/connectors.ts
|
|
4321
|
-
import { z as
|
|
4973
|
+
import { z as z9 } from "zod";
|
|
4322
4974
|
var PROBE_TIMEOUT_MS = 12e4;
|
|
4323
4975
|
var json = (value) => ({
|
|
4324
4976
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
@@ -4347,8 +4999,8 @@ function registerConnectorTools(server) {
|
|
|
4347
4999
|
"list_connectors",
|
|
4348
5000
|
"List every connector and extension: the ones built into Vorn, the ones installable from a package, and how many connections each already has. A connector polls a service; an extension adds footers and panes to a session card and says what it may touch. Use this before creating a workflow that calls a connector action, or to find an id to install.",
|
|
4349
5001
|
{
|
|
4350
|
-
installable_only:
|
|
4351
|
-
kind:
|
|
5002
|
+
installable_only: z9.boolean().optional().describe("Only connectors that are not set up yet"),
|
|
5003
|
+
kind: z9.enum(["connector", "extension"]).optional().describe("Only one kind: what polls a service, or what shows on a card")
|
|
4352
5004
|
},
|
|
4353
5005
|
async (args) => {
|
|
4354
5006
|
const [builtIns, snapshot, connections, statuses, packs] = await Promise.all([
|
|
@@ -4426,7 +5078,7 @@ function registerConnectorTools(server) {
|
|
|
4426
5078
|
"List configured connector connections, including when each last synced and the error from its last failure. Use this to diagnose a connector that is not producing tasks.",
|
|
4427
5079
|
{
|
|
4428
5080
|
connector_id: V.id.optional().describe("Only connections for this connector"),
|
|
4429
|
-
failing_only:
|
|
5081
|
+
failing_only: z9.boolean().optional().describe("Only connections whose last sync failed")
|
|
4430
5082
|
},
|
|
4431
5083
|
async (args) => {
|
|
4432
5084
|
const connections = await rpcCall("connection:list", {
|
|
@@ -4495,8 +5147,8 @@ function registerConnectorTools(server) {
|
|
|
4495
5147
|
name: V.title.optional().describe("Connection name (defaults to the connector name)"),
|
|
4496
5148
|
project: V.name.optional().describe("Vorn project tasks should be created in"),
|
|
4497
5149
|
trigger: V.shortText.optional().describe("Trigger type to configure (defaults to the first the connector offers)"),
|
|
4498
|
-
env:
|
|
4499
|
-
sync_interval_minutes:
|
|
5150
|
+
env: z9.record(z9.string(), z9.string()).optional().describe("Non-secret environment variables the connector needs"),
|
|
5151
|
+
sync_interval_minutes: z9.number().int().min(1).max(1440).optional()
|
|
4500
5152
|
},
|
|
4501
5153
|
async (args) => {
|
|
4502
5154
|
const { items: catalog } = await rpcCall("connector:catalog");
|
|
@@ -4608,7 +5260,7 @@ function registerConnectorTools(server) {
|
|
|
4608
5260
|
{
|
|
4609
5261
|
connection_id: V.id.describe("Connection ID"),
|
|
4610
5262
|
action: V.shortText.describe("Action name from list_connector_actions"),
|
|
4611
|
-
args:
|
|
5263
|
+
args: z9.record(z9.string(), z9.unknown()).optional().describe("Action arguments")
|
|
4612
5264
|
},
|
|
4613
5265
|
async (args) => {
|
|
4614
5266
|
const result = await rpcCall("connection:executeAction", {
|
|
@@ -4660,7 +5312,7 @@ function plural(count, word) {
|
|
|
4660
5312
|
|
|
4661
5313
|
// src/tools/browser.ts
|
|
4662
5314
|
import crypto6 from "crypto";
|
|
4663
|
-
import { z as
|
|
5315
|
+
import { z as z10 } from "zod";
|
|
4664
5316
|
function sessionId(env = process.env) {
|
|
4665
5317
|
const id = env.VORN_SESSION_ID;
|
|
4666
5318
|
return id && id.length > 0 ? id : null;
|
|
@@ -4718,9 +5370,9 @@ function registerBrowserTools(server) {
|
|
|
4718
5370
|
"read_page",
|
|
4719
5371
|
'Read your session browser pane as an accessibility tree. Interactive elements carry a "ref" you can pass to browser_interact. Prefer this over screenshot: it is far cheaper and gives you actionable handles. Long pages paginate \u2014 pass the returned nextCursor back as `cursor`. When the page is a design artifact it also returns "artifact" (what the file declares) and "artifactValues" (what it is currently showing). Work from artifactValues: a person can turn a control without spending a turn, so the value on screen is routinely not the default in the file.',
|
|
4720
5372
|
{
|
|
4721
|
-
filter:
|
|
5373
|
+
filter: z10.enum(["interactive", "all"]).optional().describe('"interactive" (default) returns only actionable elements; "all" adds text'),
|
|
4722
5374
|
cursor: V.shortText.optional().describe("nextCursor from a previous read_page result"),
|
|
4723
|
-
limit:
|
|
5375
|
+
limit: z10.number().int().min(1).max(200).optional().describe("Max nodes (default 200)")
|
|
4724
5376
|
},
|
|
4725
5377
|
async (args) => withSession(
|
|
4726
5378
|
async (id) => pageResult(
|
|
@@ -4751,7 +5403,7 @@ function registerBrowserTools(server) {
|
|
|
4751
5403
|
server.tool(
|
|
4752
5404
|
"read_console_messages",
|
|
4753
5405
|
"Read console output captured from your session browser pane since it opened.",
|
|
4754
|
-
{ limit:
|
|
5406
|
+
{ limit: z10.number().int().min(1).max(200).optional().describe("Max messages (default 50)") },
|
|
4755
5407
|
async (args) => withSession(
|
|
4756
5408
|
async (id) => pageResult(
|
|
4757
5409
|
await rpcCall("browser:consoleMessages", {
|
|
@@ -4764,7 +5416,7 @@ function registerBrowserTools(server) {
|
|
|
4764
5416
|
server.tool(
|
|
4765
5417
|
"read_network_requests",
|
|
4766
5418
|
"Read network requests captured from your session browser pane since it opened.",
|
|
4767
|
-
{ limit:
|
|
5419
|
+
{ limit: z10.number().int().min(1).max(200).optional().describe("Max requests (default 50)") },
|
|
4768
5420
|
async (args) => withSession(
|
|
4769
5421
|
async (id) => pageResult(
|
|
4770
5422
|
await rpcCall("browser:networkRequests", {
|
|
@@ -4777,7 +5429,7 @@ function registerBrowserTools(server) {
|
|
|
4777
5429
|
server.tool(
|
|
4778
5430
|
"browser_screenshot",
|
|
4779
5431
|
"Capture your session browser pane as a PNG. This is the expensive last resort \u2014 reach for read_page first, and use this only when layout or rendering is the actual question.",
|
|
4780
|
-
{ full_page:
|
|
5432
|
+
{ full_page: z10.boolean().optional().describe("Capture beyond the viewport") },
|
|
4781
5433
|
async (args) => withSession(async (id) => {
|
|
4782
5434
|
const { data } = await rpcCall("browser:screenshot", {
|
|
4783
5435
|
sessionId: id,
|
|
@@ -4799,7 +5451,7 @@ function registerBrowserTools(server) {
|
|
|
4799
5451
|
"Find elements in your session browser pane whose accessible name contains some text. Cheaper than reading the whole page when you already know what you are looking for.",
|
|
4800
5452
|
{
|
|
4801
5453
|
text: V.shortText.describe("Text to match against element names (case-insensitive)"),
|
|
4802
|
-
limit:
|
|
5454
|
+
limit: z10.number().int().min(1).max(50).optional().describe("Max matches (default 20)")
|
|
4803
5455
|
},
|
|
4804
5456
|
async (args) => withSession(
|
|
4805
5457
|
async (id) => pageResult(
|
|
@@ -4815,12 +5467,12 @@ function registerBrowserTools(server) {
|
|
|
4815
5467
|
"browser_interact",
|
|
4816
5468
|
'Act on your session browser pane: click, hover, type, press a key, or scroll. Address the target by "ref" from read_page where possible \u2014 refs survive reflow, coordinates do not. A ref from before a navigation is refused rather than guessed at; re-read the page. A ref target is scrolled into view first, so it does not need to be on screen already. "ok" means the input was dispatched, not that the page did what you expected \u2014 read the state back after anything that matters.',
|
|
4817
5469
|
{
|
|
4818
|
-
action:
|
|
5470
|
+
action: z10.enum(["click", "hover", "type", "key", "scroll"]).describe('What to do. "type" clicks the target first when one is given.'),
|
|
4819
5471
|
ref: V.shortText.optional().describe("Element ref from read_page"),
|
|
4820
|
-
x:
|
|
4821
|
-
y:
|
|
5472
|
+
x: z10.number().optional().describe("Viewport x, when no ref is available"),
|
|
5473
|
+
y: z10.number().optional().describe("Viewport y, when no ref is available"),
|
|
4822
5474
|
text: V.description.optional().describe('Text for "type", or the key name for "key" (e.g. "Enter")'),
|
|
4823
|
-
delta_y:
|
|
5475
|
+
delta_y: z10.number().optional().describe("Scroll amount in pixels (default 400)")
|
|
4824
5476
|
},
|
|
4825
5477
|
async (args) => withSession(async (id) => {
|
|
4826
5478
|
await rpcCall("browser:interact", {
|
|
@@ -4846,9 +5498,9 @@ function registerBrowserTools(server) {
|
|
|
4846
5498
|
"browser_tabs",
|
|
4847
5499
|
'List, add, close, or switch tabs in your session browser pane. "close" and "select" take a zero-based index \u2014 call "list" first to see what those indices name, since a tab that redirected or followed a link is no longer on the page it was opened with. Closing the last remaining tab closes the pane.',
|
|
4848
5500
|
{
|
|
4849
|
-
action:
|
|
5501
|
+
action: z10.enum(["list", "add", "close", "select"]).describe("What to do with tabs"),
|
|
4850
5502
|
url: V.url.optional().describe('URL for "add"'),
|
|
4851
|
-
index:
|
|
5503
|
+
index: z10.number().int().min(0).optional().describe("Zero-based tab index for close/select")
|
|
4852
5504
|
},
|
|
4853
5505
|
async (args) => withSession(async (id) => {
|
|
4854
5506
|
if (args.action === "list") {
|
|
@@ -4881,7 +5533,7 @@ function registerBrowserTools(server) {
|
|
|
4881
5533
|
server.tool(
|
|
4882
5534
|
"browser_history",
|
|
4883
5535
|
"Step back or forward through your session browser pane's own history \u2014 the same thing the pane's back and forward buttons do. Fails when there is no page to move to, rather than quietly staying put.",
|
|
4884
|
-
{ direction:
|
|
5536
|
+
{ direction: z10.enum(["back", "forward"]).describe("Which way to step") },
|
|
4885
5537
|
async (args) => withSession(async (id) => {
|
|
4886
5538
|
const result = await rpcCall("browser:history", {
|
|
4887
5539
|
sessionId: id,
|
|
@@ -4895,7 +5547,7 @@ function registerBrowserTools(server) {
|
|
|
4895
5547
|
}
|
|
4896
5548
|
|
|
4897
5549
|
// src/tools/device.ts
|
|
4898
|
-
import { z as
|
|
5550
|
+
import { z as z11 } from "zod";
|
|
4899
5551
|
var DEVICE_FENCE = "DEVICE CONTENT";
|
|
4900
5552
|
var required = (description) => V.shortText.min(1, "Value must not be empty").describe(description);
|
|
4901
5553
|
async function withSession2(run) {
|
|
@@ -4960,9 +5612,9 @@ function registerDeviceTools(server) {
|
|
|
4960
5612
|
"read_screen",
|
|
4961
5613
|
'Read your claimed simulator as an accessibility tree. Elements carry a "ref" you can pass to device_interact. Prefer this over device_screenshot: it is far cheaper and gives you handles rather than pixels. Coordinates are in POINTS, which is what taps take \u2014 a screenshot is typically 3x larger in pixels. Long screens paginate via nextCursor.',
|
|
4962
5614
|
{
|
|
4963
|
-
filter:
|
|
5615
|
+
filter: z11.enum(["interactive", "all"]).optional().describe('"interactive" (default) returns only actionable elements; "all" adds text'),
|
|
4964
5616
|
cursor: V.shortText.optional().describe("nextCursor from a previous read_screen result"),
|
|
4965
|
-
limit:
|
|
5617
|
+
limit: z11.number().int().min(1).max(200).optional().describe("Max elements (default 200)")
|
|
4966
5618
|
},
|
|
4967
5619
|
async (args) => withSession2(
|
|
4968
5620
|
async (id) => pageResult(
|
|
@@ -4981,7 +5633,7 @@ function registerDeviceTools(server) {
|
|
|
4981
5633
|
"Find elements on your claimed simulator whose label or accessibility identifier contains some text. Searches the whole screen, not just the first page read_screen would return.",
|
|
4982
5634
|
{
|
|
4983
5635
|
text: required("Text to match against labels and identifiers (case-insensitive)"),
|
|
4984
|
-
limit:
|
|
5636
|
+
limit: z11.number().int().min(1).max(50).optional().describe("Max matches (default 20)")
|
|
4985
5637
|
},
|
|
4986
5638
|
async (args) => withSession2(
|
|
4987
5639
|
async (id) => pageResult(
|
|
@@ -4998,17 +5650,17 @@ function registerDeviceTools(server) {
|
|
|
4998
5650
|
"device_interact",
|
|
4999
5651
|
'Act on your claimed simulator: tap, swipe, type, press a hardware button, or long-press. Address the target by "ref" from read_screen where possible. Coordinates are in POINTS. A ref from before an earlier interaction is refused rather than guessed at \u2014 read the screen again. A swipe starting at the very edge of the screen is refused too: iOS claims those as system gestures and swallows them, which looks to you like nothing happened.',
|
|
5000
5652
|
{
|
|
5001
|
-
action:
|
|
5653
|
+
action: z11.enum(["tap", "swipe", "type", "button", "press"]).describe('"press" is a long press; "button" takes a name in `text`'),
|
|
5002
5654
|
ref: required("Element ref from read_screen or device_find").optional(),
|
|
5003
|
-
x:
|
|
5004
|
-
y:
|
|
5005
|
-
to_x:
|
|
5006
|
-
to_y:
|
|
5655
|
+
x: z11.number().optional().describe("Screen x in points, when no ref is available"),
|
|
5656
|
+
y: z11.number().optional().describe("Screen y in points, when no ref is available"),
|
|
5657
|
+
to_x: z11.number().optional().describe("Swipe destination x, in points"),
|
|
5658
|
+
to_y: z11.number().optional().describe("Swipe destination y, in points"),
|
|
5007
5659
|
text: V.description.optional().describe(
|
|
5008
5660
|
'Text for "type", or the button name for "button" (HOME, LOCK, SIRI, SIDE_BUTTON)'
|
|
5009
5661
|
),
|
|
5010
|
-
duration:
|
|
5011
|
-
system_gesture:
|
|
5662
|
+
duration: z11.number().min(0).max(30).optional().describe('Seconds to hold, for "press"'),
|
|
5663
|
+
system_gesture: z11.boolean().optional().describe("Allow a stroke starting in the bezel band, when a system gesture is the intent")
|
|
5012
5664
|
},
|
|
5013
5665
|
async (args) => withSession2(async (id) => {
|
|
5014
5666
|
const r = await rpcCall("device:interact", {
|
|
@@ -5034,7 +5686,7 @@ function registerDeviceTools(server) {
|
|
|
5034
5686
|
"device_screenshot",
|
|
5035
5687
|
"Capture your claimed simulator as a downscaled PNG. The expensive last resort \u2014 reach for read_screen first, and use this only when layout or rendering is the actual question. The result reports the scale factor: divide an image coordinate by it to get the point coordinate device_interact takes.",
|
|
5036
5688
|
{
|
|
5037
|
-
max_edge:
|
|
5689
|
+
max_edge: z11.number().int().min(200).max(2e3).optional().describe("Longest edge of the returned image in pixels (default 1000)")
|
|
5038
5690
|
},
|
|
5039
5691
|
async (args) => withSession2(async (id) => {
|
|
5040
5692
|
const r = await rpcCall("device:screenshot", { sessionId: id, maxEdge: args.max_edge });
|
|
@@ -5089,7 +5741,7 @@ Screen is ${r.screen.width}x${r.screen.height} points. Image pixels are ${r.scal
|
|
|
5089
5741
|
server.tool(
|
|
5090
5742
|
"device_logs",
|
|
5091
5743
|
"Read log output captured from your claimed simulator since it was claimed.",
|
|
5092
|
-
{ limit:
|
|
5744
|
+
{ limit: z11.number().int().min(1).max(500).optional().describe("Max lines (default 100)") },
|
|
5093
5745
|
async (args) => withSession2(
|
|
5094
5746
|
async (id) => pageResult(
|
|
5095
5747
|
await rpcCall("device:logs", { sessionId: id, limit: args.limit }),
|
|
@@ -5122,6 +5774,7 @@ function createMcpServer(version) {
|
|
|
5122
5774
|
registerProjectTools(server);
|
|
5123
5775
|
registerTaskTools(server);
|
|
5124
5776
|
registerWorkflowTools(server);
|
|
5777
|
+
registerDescribeNodesTool(server);
|
|
5125
5778
|
registerWorkspaceTools(server);
|
|
5126
5779
|
return server;
|
|
5127
5780
|
}
|
|
@@ -5135,7 +5788,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
|
|
|
5135
5788
|
console.error = (...args) => _origError("[mcp:error]", ...args);
|
|
5136
5789
|
async function main() {
|
|
5137
5790
|
configManager.init();
|
|
5138
|
-
const version = true ? "0.7.
|
|
5791
|
+
const version = true ? "0.7.3" : createRequire(import.meta.url)("../package.json").version;
|
|
5139
5792
|
const server = createMcpServer(version);
|
|
5140
5793
|
const transport = new StdioServerTransport();
|
|
5141
5794
|
await server.connect(transport);
|