@hasna/workflows 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/dist/agent-tools.js +36 -500
- package/dist/cli/index.js +23 -12
- package/dist/handlers.js +1226 -1074
- package/dist/index.js +1228 -1076
- package/dist/mcp/index.js +15 -4
- package/dist/sdk.js +4 -4
- package/dist/serve/admin.js +188 -43
- package/dist/serve/index.js +229 -43
- package/dist/service.js +1223 -1072
- package/hasna.contract.json +1 -1
- package/package.json +3 -3
- package/types/graph.d.ts +6 -0
- package/types/journal-directory.d.ts +4 -0
- package/types/lanes/codex.d.ts +0 -6
- package/types/lanes/types.d.ts +4 -0
- package/types/sdk/generated-api.d.ts +2 -0
- package/types/serve/journal-lease.d.ts +7 -0
- package/types/service.d.ts +12 -4
- package/types/types.d.ts +4 -0
package/README.md
CHANGED
|
@@ -87,6 +87,34 @@ run API includes `triggerRun`, `getRun`, `listNodes`, `events`, `cancelRun` and
|
|
|
87
87
|
the graph, context and cycle budget. Reusing a key for a different request returns
|
|
88
88
|
HTTP 409. Run context cannot contain the reserved `__wf` or `steps` fields.
|
|
89
89
|
|
|
90
|
+
A step's optional `cwd` names its working directory on the server executing the
|
|
91
|
+
workflow. It applies to shell commands and model lanes; prefer an absolute path.
|
|
92
|
+
Codex prompts keep their native repository, sandbox and approval defaults. Supply
|
|
93
|
+
`cwd` for work in an existing trusted repository. For a prompt that needs no file
|
|
94
|
+
changes and may run outside a repository, explicitly set `codexMode: "read-only"`:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { createWorkflowsClient, type WorkflowGraph } from "@hasna/workflows/sdk";
|
|
98
|
+
|
|
99
|
+
const workflows = createWorkflowsClient();
|
|
100
|
+
const graph: WorkflowGraph = {
|
|
101
|
+
name: "read-only-answer", version: "1",
|
|
102
|
+
nodes: [
|
|
103
|
+
{ id: "start", type: "start", next: "answer" },
|
|
104
|
+
{ id: "answer", type: "step", lane: "codex", codexMode: "read-only",
|
|
105
|
+
prompt: "Return the requested answer without changing files.", next: "end" },
|
|
106
|
+
{ id: "end", type: "end" },
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
await workflows.triggerRun(graph, {}, { idempotencyKey: "read-only-answer-1" });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This explicit mode uses Codex's read-only sandbox, permits a nonrepository
|
|
113
|
+
directory and refuses approval escalation. It applies only to Codex prompt steps;
|
|
114
|
+
shell commands cannot select it. An empty Codex response fails the step even when
|
|
115
|
+
the native process exits successfully. Memoization distinguishes the working
|
|
116
|
+
directory, lane and execution mode.
|
|
117
|
+
|
|
90
118
|
## MCP
|
|
91
119
|
|
|
92
120
|
Register `workflows-mcp` as a stdio command in Codex, Claude Code, OpenCode, Sumi,
|
|
@@ -113,6 +141,31 @@ Use durable storage for the data directory and one deployed executor; ephemeral
|
|
|
113
141
|
container storage is unsuitable. Model lanes require the selected harness's
|
|
114
142
|
native subscription on the executor. They do not inherit paid model API keys.
|
|
115
143
|
|
|
144
|
+
Set `HASNA_WORKFLOWS_ENABLED_LANES` to a comma-separated subset of
|
|
145
|
+
`claude,codex,cursor,grok` to restrict model execution. An explicitly empty value
|
|
146
|
+
permits command steps only; leaving the variable unset preserves all four lanes.
|
|
147
|
+
Lane inspection reports disabled lanes as `wired: false` with a configuration
|
|
148
|
+
reason. Trigger rejects a graph containing a disabled prompt lane before any
|
|
149
|
+
step runs, and resume leaves such a stored run unchanged. Existing run history
|
|
150
|
+
and event reads remain available. This admission setting does not grant model
|
|
151
|
+
authentication or authorize API billing.
|
|
152
|
+
|
|
153
|
+
For a deployment with a restored data directory, set
|
|
154
|
+
`HASNA_WORKFLOWS_DATA_IDENTITY` to its non-secret storage identity. After verifying
|
|
155
|
+
the restore, the operator provisions `.workflows-data-identity` containing that
|
|
156
|
+
identity followed by a newline. The directory must already contain readable and
|
|
157
|
+
writable `workflows.db` and `sessions/session.wal`. The server refuses missing,
|
|
158
|
+
mismatched or inaccessible state without creating a replacement. Readiness and
|
|
159
|
+
storage operations repeat the check. The server also holds a process-lifetime
|
|
160
|
+
SQLite mutex in `.server-lease.sqlite`, containing no tables, so an overlapping
|
|
161
|
+
replacement cannot open a second server over the same local journal. Keep this
|
|
162
|
+
file in place; never delete it to take a live process's lease. This mode requires
|
|
163
|
+
a local filesystem with reliable SQLite locking, not a network filesystem.
|
|
164
|
+
|
|
165
|
+
Journal append acknowledgements follow file and parent-directory flushes. The
|
|
166
|
+
journal and PostgreSQL are separate stores, not one atomic transaction; capture
|
|
167
|
+
and restore them together while execution is quiescent.
|
|
168
|
+
|
|
116
169
|
When the server sits behind an authenticated gateway, set
|
|
117
170
|
`HASNA_WORKFLOWS_ORIGIN_AUTH` to the gateway's scoped origin identity. It must be
|
|
118
171
|
32 to 256 ASCII letters, digits, underscores or hyphens. All API routes, including
|
package/dist/agent-tools.js
CHANGED
|
@@ -16,502 +16,7 @@ var __export = (target, all) => {
|
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
var __require = import.meta.require;
|
|
18
18
|
|
|
19
|
-
// src/lanes/types.ts
|
|
20
|
-
import { statSync } from "fs";
|
|
21
|
-
function binaryOnPath(binary) {
|
|
22
|
-
const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
|
|
23
|
-
for (const entry of pathEntries) {
|
|
24
|
-
try {
|
|
25
|
-
const stat = statSync(`${entry}/${binary}`);
|
|
26
|
-
if (stat.isFile())
|
|
27
|
-
return true;
|
|
28
|
-
} catch {}
|
|
29
|
-
}
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
function subscriptionEnvironment(overrides = {}) {
|
|
33
|
-
const paid = /^(OPENAI_API_KEY|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|CODEX_API_KEY|CURSOR_API_KEY)$/;
|
|
34
|
-
const sensitive = /(?:API[_-]?KEY|SECRET|PASSWORD|TOKEN|DATABASE_URL|CREDENTIAL)/i;
|
|
35
|
-
const env = {};
|
|
36
|
-
for (const [name, value] of Object.entries(process.env)) {
|
|
37
|
-
if (value !== undefined && !paid.test(name) && (!sensitive.test(name) || name === "CLAUDE_CODE_OAUTH_TOKEN"))
|
|
38
|
-
env[name] = value;
|
|
39
|
-
}
|
|
40
|
-
for (const [name, value] of Object.entries(overrides)) {
|
|
41
|
-
if (paid.test(name) || sensitive.test(name))
|
|
42
|
-
throw new Error(`lane environment field ${name} requires a supported scoped credential consumer`);
|
|
43
|
-
env[name] = value;
|
|
44
|
-
}
|
|
45
|
-
return env;
|
|
46
|
-
}
|
|
47
|
-
function subscriptionSdkEnvironment(overrides = {}) {
|
|
48
|
-
const safe = subscriptionEnvironment(overrides);
|
|
49
|
-
return { ...Object.fromEntries(Object.keys(process.env).map((name) => [name, undefined])), ...safe };
|
|
50
|
-
}
|
|
51
|
-
function withTimeout(promise, timeoutMs, what, cancel) {
|
|
52
|
-
return new Promise((resolve, reject) => {
|
|
53
|
-
const timer = setTimeout(() => {
|
|
54
|
-
cancel?.();
|
|
55
|
-
reject(new LaneTimeoutError(what, timeoutMs));
|
|
56
|
-
}, timeoutMs);
|
|
57
|
-
promise.then((value) => {
|
|
58
|
-
clearTimeout(timer);
|
|
59
|
-
resolve(value);
|
|
60
|
-
}, (err) => {
|
|
61
|
-
clearTimeout(timer);
|
|
62
|
-
reject(err);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
async function spawnCli(cmd, opts) {
|
|
67
|
-
const started = Date.now();
|
|
68
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
|
|
69
|
-
let proc;
|
|
70
|
-
try {
|
|
71
|
-
proc = Bun.spawn({
|
|
72
|
-
cmd,
|
|
73
|
-
cwd: opts.cwd,
|
|
74
|
-
env: subscriptionEnvironment(opts.env),
|
|
75
|
-
stdin: "ignore",
|
|
76
|
-
stdout: "pipe",
|
|
77
|
-
stderr: "pipe",
|
|
78
|
-
timeout: timeoutMs
|
|
79
|
-
});
|
|
80
|
-
} catch (err) {
|
|
81
|
-
const message = String(err instanceof Error ? err.message : err);
|
|
82
|
-
if (message.includes("ENOENT") || /not found/i.test(message)) {
|
|
83
|
-
throw new LaneDependencyMissingError(`lane binary ${JSON.stringify(opts.binaryName)} not found on PATH; install it or provide ${opts.packageHint}`);
|
|
84
|
-
}
|
|
85
|
-
return { ok: false, exitCode: 1, output: "", durationMs: Date.now() - started, error: message };
|
|
86
|
-
}
|
|
87
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
88
|
-
new Response(proc.stdout).text(),
|
|
89
|
-
new Response(proc.stderr).text(),
|
|
90
|
-
proc.exited
|
|
91
|
-
]);
|
|
92
|
-
const output = stderr ? `${stdout}
|
|
93
|
-
${stderr}`.trim() : stdout.trim();
|
|
94
|
-
return { ok: exitCode === 0, exitCode, output, durationMs: Date.now() - started };
|
|
95
|
-
}
|
|
96
|
-
var LANE_KINDS2, LaneDependencyMissingError, LaneAdapterShapeError, DEFAULT_LANE_TIMEOUT_MS = 120000, LaneTimeoutError;
|
|
97
|
-
var init_types = __esm(() => {
|
|
98
|
-
LANE_KINDS2 = ["claude", "codex", "cursor", "grok"];
|
|
99
|
-
LaneDependencyMissingError = class LaneDependencyMissingError extends Error {
|
|
100
|
-
constructor(message) {
|
|
101
|
-
super(message);
|
|
102
|
-
this.name = "LaneDependencyMissingError";
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
LaneAdapterShapeError = class LaneAdapterShapeError extends Error {
|
|
106
|
-
constructor(message) {
|
|
107
|
-
super(message);
|
|
108
|
-
this.name = "LaneAdapterShapeError";
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
LaneTimeoutError = class LaneTimeoutError extends Error {
|
|
112
|
-
constructor(what, timeoutMs) {
|
|
113
|
-
super(`${what} exceeded the ${timeoutMs}ms budget`);
|
|
114
|
-
this.name = "LaneTimeoutError";
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// src/lanes/claude.ts
|
|
120
|
-
function createClaudeAdapter(deps = {}) {
|
|
121
|
-
return {
|
|
122
|
-
kind: "claude",
|
|
123
|
-
async probe() {
|
|
124
|
-
const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
|
|
125
|
-
try {
|
|
126
|
-
await loader();
|
|
127
|
-
return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "sdk" };
|
|
128
|
-
} catch {
|
|
129
|
-
if (binaryOnPath(deps.cliPath ?? "claude")) {
|
|
130
|
-
return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "cli" };
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
kind: "claude",
|
|
134
|
-
sdk: "@anthropic-ai/claude-agent-sdk",
|
|
135
|
-
wired: false,
|
|
136
|
-
reason: "neither @anthropic-ai/claude-agent-sdk nor the claude CLI is available on this machine"
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
async run(job) {
|
|
141
|
-
if (!job.prompt) {
|
|
142
|
-
return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "claude lane requires a prompt" };
|
|
143
|
-
}
|
|
144
|
-
const started = Date.now();
|
|
145
|
-
const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
|
|
146
|
-
const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
|
|
147
|
-
let sdk;
|
|
148
|
-
try {
|
|
149
|
-
sdk = await loader();
|
|
150
|
-
} catch (err) {
|
|
151
|
-
return runClaudeCli(job, deps.cliPath ?? "claude", started, timeoutMs);
|
|
152
|
-
}
|
|
153
|
-
const query = sdk.query;
|
|
154
|
-
if (typeof query !== "function") {
|
|
155
|
-
throw new LaneAdapterShapeError("@anthropic-ai/claude-agent-sdk exports no query() function");
|
|
156
|
-
}
|
|
157
|
-
const prompt = job.prompt;
|
|
158
|
-
const abortController = new AbortController;
|
|
159
|
-
try {
|
|
160
|
-
const { output, isError } = await withTimeout((async () => {
|
|
161
|
-
const generator = await query({
|
|
162
|
-
prompt,
|
|
163
|
-
options: { model: job.model, cwd: job.cwd, maxTurns: 32, abortController, env: subscriptionSdkEnvironment(job.env) }
|
|
164
|
-
});
|
|
165
|
-
let output2 = "";
|
|
166
|
-
let isError2 = false;
|
|
167
|
-
for await (const message of generator) {
|
|
168
|
-
if (message.type === "assistant") {
|
|
169
|
-
const content = message.message?.content;
|
|
170
|
-
if (Array.isArray(content)) {
|
|
171
|
-
for (const block of content) {
|
|
172
|
-
if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
|
|
173
|
-
output2 += block.text;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (message.type === "error")
|
|
179
|
-
isError2 = true;
|
|
180
|
-
}
|
|
181
|
-
return { output: output2, isError: isError2 };
|
|
182
|
-
})(), timeoutMs, "claude lane", () => abortController.abort());
|
|
183
|
-
const durationMs = Date.now() - started;
|
|
184
|
-
return { ok: !isError, exitCode: isError ? 1 : 0, output: output.trim(), durationMs };
|
|
185
|
-
} catch (err) {
|
|
186
|
-
const durationMs = Date.now() - started;
|
|
187
|
-
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
async function runClaudeCli(job, cliPath, started, timeoutMs) {
|
|
193
|
-
try {
|
|
194
|
-
const result = await spawnCli([cliPath, "-p", job.prompt], {
|
|
195
|
-
binaryName: cliPath,
|
|
196
|
-
packageHint: "@anthropic-ai/claude-agent-sdk or the claude CLI",
|
|
197
|
-
timeoutMs,
|
|
198
|
-
env: job.env,
|
|
199
|
-
cwd: job.cwd
|
|
200
|
-
});
|
|
201
|
-
return { ...result, durationMs: Date.now() - started };
|
|
202
|
-
} catch (err) {
|
|
203
|
-
if (err instanceof LaneDependencyMissingError) {
|
|
204
|
-
return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
|
|
205
|
-
}
|
|
206
|
-
throw err;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
var init_claude = __esm(() => {
|
|
210
|
-
init_types();
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// src/lanes/codex.ts
|
|
214
|
-
function createCodexAdapter(deps = {}) {
|
|
215
|
-
return {
|
|
216
|
-
kind: "codex",
|
|
217
|
-
async probe() {
|
|
218
|
-
const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
|
|
219
|
-
try {
|
|
220
|
-
await loader();
|
|
221
|
-
return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "sdk" };
|
|
222
|
-
} catch {
|
|
223
|
-
if (binaryOnPath(deps.cliPath ?? "codex")) {
|
|
224
|
-
return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "cli" };
|
|
225
|
-
}
|
|
226
|
-
return {
|
|
227
|
-
kind: "codex",
|
|
228
|
-
sdk: "@openai/codex-sdk",
|
|
229
|
-
wired: false,
|
|
230
|
-
reason: "neither @openai/codex-sdk nor the codex CLI is available on this machine"
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
async run(job) {
|
|
235
|
-
if (!job.prompt) {
|
|
236
|
-
return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "codex lane requires a prompt" };
|
|
237
|
-
}
|
|
238
|
-
const started = Date.now();
|
|
239
|
-
const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
|
|
240
|
-
const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
|
|
241
|
-
let sdk;
|
|
242
|
-
try {
|
|
243
|
-
sdk = await loader();
|
|
244
|
-
} catch {
|
|
245
|
-
return runCodexCli(job, deps.cliPath ?? "codex", started, timeoutMs);
|
|
246
|
-
}
|
|
247
|
-
const CodexClass = sdk.Codex;
|
|
248
|
-
if (typeof CodexClass !== "function") {
|
|
249
|
-
throw new LaneAdapterShapeError("@openai/codex-sdk exports no Codex class");
|
|
250
|
-
}
|
|
251
|
-
const abortController = new AbortController;
|
|
252
|
-
try {
|
|
253
|
-
const client = await withTimeout(Promise.resolve(new CodexClass({
|
|
254
|
-
config: { model: job.model },
|
|
255
|
-
env: subscriptionEnvironment(job.env)
|
|
256
|
-
})), timeoutMs, "codex lane");
|
|
257
|
-
const thread = client.startThread?.({ model: job.model, workingDirectory: job.cwd });
|
|
258
|
-
if (!thread || typeof thread.run !== "function") {
|
|
259
|
-
throw new LaneAdapterShapeError("@openai/codex-sdk Codex.startThread() returned no run()");
|
|
260
|
-
}
|
|
261
|
-
const turn = await withTimeout(thread.run(job.prompt, { signal: abortController.signal }), timeoutMs, "codex lane", () => abortController.abort());
|
|
262
|
-
const output = turn?.finalResponse ?? "";
|
|
263
|
-
const durationMs = Date.now() - started;
|
|
264
|
-
return { ok: true, exitCode: 0, output, durationMs };
|
|
265
|
-
} catch (err) {
|
|
266
|
-
const durationMs = Date.now() - started;
|
|
267
|
-
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
async function runCodexCli(job, cliPath, started, timeoutMs) {
|
|
273
|
-
try {
|
|
274
|
-
const result = await spawnCli([cliPath, "exec", "--", job.prompt], {
|
|
275
|
-
binaryName: cliPath,
|
|
276
|
-
packageHint: "@openai/codex-sdk or the codex CLI",
|
|
277
|
-
timeoutMs,
|
|
278
|
-
env: job.env,
|
|
279
|
-
cwd: job.cwd
|
|
280
|
-
});
|
|
281
|
-
return { ...result, durationMs: Date.now() - started };
|
|
282
|
-
} catch (err) {
|
|
283
|
-
if (err instanceof LaneDependencyMissingError) {
|
|
284
|
-
return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
|
|
285
|
-
}
|
|
286
|
-
throw err;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
var init_codex = __esm(() => {
|
|
290
|
-
init_types();
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// src/lanes/cursor.ts
|
|
294
|
-
function createCursorAdapter(deps = {}) {
|
|
295
|
-
return {
|
|
296
|
-
kind: "cursor",
|
|
297
|
-
async probe() {
|
|
298
|
-
const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
|
|
299
|
-
try {
|
|
300
|
-
await loader();
|
|
301
|
-
return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "sdk (local mode)" };
|
|
302
|
-
} catch {
|
|
303
|
-
if (binaryOnPath(deps.cliPath ?? "cursor-agent")) {
|
|
304
|
-
return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "cli" };
|
|
305
|
-
}
|
|
306
|
-
return {
|
|
307
|
-
kind: "cursor",
|
|
308
|
-
sdk: "@cursor/sdk",
|
|
309
|
-
wired: false,
|
|
310
|
-
reason: "neither @cursor/sdk nor the cursor-agent CLI is available on this machine"
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
},
|
|
314
|
-
async run(job) {
|
|
315
|
-
if (!job.prompt) {
|
|
316
|
-
return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "cursor lane requires a prompt" };
|
|
317
|
-
}
|
|
318
|
-
if (!deps.sdkLoader && binaryOnPath(deps.cliPath ?? "cursor-agent"))
|
|
319
|
-
return runCursorCli(job, deps.cliPath ?? "cursor-agent", Date.now(), job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS);
|
|
320
|
-
const started = Date.now();
|
|
321
|
-
const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
|
|
322
|
-
const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
|
|
323
|
-
let sdk;
|
|
324
|
-
try {
|
|
325
|
-
sdk = await loader();
|
|
326
|
-
} catch {
|
|
327
|
-
return runCursorCli(job, deps.cliPath ?? "cursor-agent", started, timeoutMs);
|
|
328
|
-
}
|
|
329
|
-
let cancel;
|
|
330
|
-
try {
|
|
331
|
-
subscriptionEnvironment(job.env);
|
|
332
|
-
if (!deps.sdkLoader && process.env.CURSOR_API_KEY)
|
|
333
|
-
throw new Error("cursor lane refuses ambient API billing; use the native subscription CLI");
|
|
334
|
-
const run = await withTimeout(runLocalAgent(sdk, job, (callback) => {
|
|
335
|
-
cancel = callback;
|
|
336
|
-
}), timeoutMs, "cursor lane", () => cancel?.());
|
|
337
|
-
const durationMs = Date.now() - started;
|
|
338
|
-
const status = run.status;
|
|
339
|
-
const ok = status === "finished";
|
|
340
|
-
return { ok, exitCode: ok ? 0 : 1, output: run.result, durationMs };
|
|
341
|
-
} catch (err) {
|
|
342
|
-
if (err instanceof LaneAdapterShapeError)
|
|
343
|
-
throw err;
|
|
344
|
-
const durationMs = Date.now() - started;
|
|
345
|
-
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
async function runLocalAgent(sdk, job, onCancel) {
|
|
351
|
-
const AgentClass = sdk.Agent;
|
|
352
|
-
const CursorClass = sdk.Cursor;
|
|
353
|
-
if (typeof AgentClass !== "function" && typeof CursorClass !== "function") {
|
|
354
|
-
throw new LaneAdapterShapeError("@cursor/sdk exports no Agent/Cursor class");
|
|
355
|
-
}
|
|
356
|
-
const create = AgentClass.create;
|
|
357
|
-
const agent = typeof create === "function" ? await create.call(AgentClass, { local: { cwd: job.cwd, enableAgentRetries: false } }) : typeof AgentClass === "function" ? new AgentClass : undefined;
|
|
358
|
-
if (!agent || typeof agent.send !== "function") {
|
|
359
|
-
throw new LaneAdapterShapeError("@cursor/sdk Agent has no send()");
|
|
360
|
-
}
|
|
361
|
-
let run = await agent.send(job.prompt, { model: job.model });
|
|
362
|
-
if (run && typeof run === "object") {
|
|
363
|
-
const handle = run;
|
|
364
|
-
if (typeof handle.cancel === "function")
|
|
365
|
-
onCancel(() => {
|
|
366
|
-
handle.cancel().catch(() => {});
|
|
367
|
-
});
|
|
368
|
-
if (typeof handle.wait === "function")
|
|
369
|
-
run = await handle.wait();
|
|
370
|
-
}
|
|
371
|
-
if (!run || typeof run !== "object") {
|
|
372
|
-
throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
|
|
373
|
-
}
|
|
374
|
-
const candidate = run;
|
|
375
|
-
if (typeof candidate.status !== "string" || !["finished", "error", "cancelled"].includes(candidate.status)) {
|
|
376
|
-
throw new LaneAdapterShapeError(`@cursor/sdk Agent.send() returned a Run without a terminal status (got ${JSON.stringify(candidate.status)}; expected finished|error|cancelled)`);
|
|
377
|
-
}
|
|
378
|
-
if (typeof candidate.result !== "string") {
|
|
379
|
-
throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned a Run without a string result");
|
|
380
|
-
}
|
|
381
|
-
return { status: candidate.status, result: candidate.result };
|
|
382
|
-
}
|
|
383
|
-
async function runCursorCli(job, cliPath, started, timeoutMs) {
|
|
384
|
-
try {
|
|
385
|
-
const result = await spawnCli([cliPath, "--print", job.prompt, ...job.model ? ["--model", job.model] : []], {
|
|
386
|
-
binaryName: cliPath,
|
|
387
|
-
packageHint: "@cursor/sdk (local mode) or the cursor-agent CLI",
|
|
388
|
-
timeoutMs,
|
|
389
|
-
env: job.env,
|
|
390
|
-
cwd: job.cwd
|
|
391
|
-
});
|
|
392
|
-
return { ...result, durationMs: Date.now() - started };
|
|
393
|
-
} catch (err) {
|
|
394
|
-
if (err instanceof LaneDependencyMissingError) {
|
|
395
|
-
return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
|
|
396
|
-
}
|
|
397
|
-
throw err;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
var init_cursor = __esm(() => {
|
|
401
|
-
init_types();
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
// src/lanes/grok.ts
|
|
405
|
-
function createGrokAdapter(deps = {}) {
|
|
406
|
-
return {
|
|
407
|
-
kind: "grok",
|
|
408
|
-
async probe() {
|
|
409
|
-
const cliPath = deps.cliPath ?? "grok";
|
|
410
|
-
if (binaryOnPath(cliPath)) {
|
|
411
|
-
return { kind: "grok", sdk: "no npm SDK (measured 2026-08-25)", wired: true, via: "cli" };
|
|
412
|
-
}
|
|
413
|
-
return {
|
|
414
|
-
kind: "grok",
|
|
415
|
-
sdk: "no npm SDK (measured 2026-08-25)",
|
|
416
|
-
wired: false,
|
|
417
|
-
reason: `no xAI Grok SDK exists on npm (measured 2026-08-25) and the grok CLI is not on PATH`
|
|
418
|
-
};
|
|
419
|
-
},
|
|
420
|
-
async run(job) {
|
|
421
|
-
if (!job.prompt) {
|
|
422
|
-
return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "grok lane requires a prompt" };
|
|
423
|
-
}
|
|
424
|
-
const started = Date.now();
|
|
425
|
-
const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
|
|
426
|
-
const cliPath = deps.cliPath ?? "grok";
|
|
427
|
-
try {
|
|
428
|
-
const result = await spawnCli([cliPath, job.prompt], {
|
|
429
|
-
binaryName: cliPath,
|
|
430
|
-
packageHint: "the grok CLI (no xAI Grok SDK exists on npm \u2014 measured 2026-08-25)",
|
|
431
|
-
timeoutMs,
|
|
432
|
-
env: job.env,
|
|
433
|
-
cwd: job.cwd
|
|
434
|
-
});
|
|
435
|
-
return { ...result, durationMs: Date.now() - started };
|
|
436
|
-
} catch (err) {
|
|
437
|
-
if (err instanceof LaneDependencyMissingError) {
|
|
438
|
-
return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
|
|
439
|
-
}
|
|
440
|
-
throw err;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
var init_grok = __esm(() => {
|
|
446
|
-
init_types();
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
// src/lanes/index.ts
|
|
450
|
-
var exports_lanes = {};
|
|
451
|
-
__export(exports_lanes, {
|
|
452
|
-
runLaneJob: () => runLaneJob,
|
|
453
|
-
resolveLane: () => resolveLane,
|
|
454
|
-
probeLane: () => probeLane,
|
|
455
|
-
laneInventory: () => laneInventory,
|
|
456
|
-
createGrokAdapter: () => createGrokAdapter,
|
|
457
|
-
createCursorAdapter: () => createCursorAdapter,
|
|
458
|
-
createCodexAdapter: () => createCodexAdapter,
|
|
459
|
-
createClaudeAdapter: () => createClaudeAdapter,
|
|
460
|
-
binaryOnPath: () => binaryOnPath,
|
|
461
|
-
LaneDependencyMissingError: () => LaneDependencyMissingError,
|
|
462
|
-
LaneAdapterShapeError: () => LaneAdapterShapeError,
|
|
463
|
-
LANE_KINDS: () => LANE_KINDS2
|
|
464
|
-
});
|
|
465
|
-
function resolveLane(kind) {
|
|
466
|
-
const adapter = adapters.get(kind);
|
|
467
|
-
if (!adapter)
|
|
468
|
-
throw new Error(`unknown lane ${kind} (expected one of ${LANE_KINDS2.join(", ")})`);
|
|
469
|
-
return adapter;
|
|
470
|
-
}
|
|
471
|
-
async function runLaneJob(job) {
|
|
472
|
-
return resolveLane(job.lane).run(job);
|
|
473
|
-
}
|
|
474
|
-
async function probeLane(kind) {
|
|
475
|
-
return resolveLane(kind).probe();
|
|
476
|
-
}
|
|
477
|
-
async function laneInventory() {
|
|
478
|
-
return Promise.all(LANE_KINDS2.map((kind) => probeLane(kind)));
|
|
479
|
-
}
|
|
480
|
-
var adapters;
|
|
481
|
-
var init_lanes = __esm(() => {
|
|
482
|
-
init_claude();
|
|
483
|
-
init_codex();
|
|
484
|
-
init_cursor();
|
|
485
|
-
init_grok();
|
|
486
|
-
init_types();
|
|
487
|
-
init_types();
|
|
488
|
-
init_claude();
|
|
489
|
-
init_codex();
|
|
490
|
-
init_cursor();
|
|
491
|
-
init_grok();
|
|
492
|
-
adapters = new Map([
|
|
493
|
-
["claude", createClaudeAdapter()],
|
|
494
|
-
["codex", createCodexAdapter()],
|
|
495
|
-
["cursor", createCursorAdapter()],
|
|
496
|
-
["grok", createGrokAdapter()]
|
|
497
|
-
]);
|
|
498
|
-
});
|
|
499
|
-
|
|
500
19
|
// src/expr.ts
|
|
501
|
-
class ExprSyntaxError extends Error {
|
|
502
|
-
constructor(message) {
|
|
503
|
-
super(message);
|
|
504
|
-
this.name = "ExprSyntaxError";
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
class ExprEvalError extends Error {
|
|
509
|
-
constructor(message) {
|
|
510
|
-
super(message);
|
|
511
|
-
this.name = "ExprEvalError";
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
var COMPARISON_OPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
|
|
515
20
|
function tokenize(input) {
|
|
516
21
|
const tokens = [];
|
|
517
22
|
let i = 0;
|
|
@@ -763,11 +268,24 @@ function evaluateExpr(input, context) {
|
|
|
763
268
|
}
|
|
764
269
|
return value;
|
|
765
270
|
}
|
|
271
|
+
var ExprSyntaxError, ExprEvalError, COMPARISON_OPS;
|
|
272
|
+
var init_expr = __esm(() => {
|
|
273
|
+
ExprSyntaxError = class ExprSyntaxError extends Error {
|
|
274
|
+
constructor(message) {
|
|
275
|
+
super(message);
|
|
276
|
+
this.name = "ExprSyntaxError";
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
ExprEvalError = class ExprEvalError extends Error {
|
|
280
|
+
constructor(message) {
|
|
281
|
+
super(message);
|
|
282
|
+
this.name = "ExprEvalError";
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
COMPARISON_OPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
|
|
286
|
+
});
|
|
766
287
|
|
|
767
288
|
// src/graph.ts
|
|
768
|
-
var GRAPH_NODE_TYPES = ["start", "step", "decision", "while", "end"];
|
|
769
|
-
var LANE_KINDS = ["claude", "codex", "cursor", "grok"];
|
|
770
|
-
var NODE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
771
289
|
function issue(issues, path, message) {
|
|
772
290
|
issues.push({ path, message });
|
|
773
291
|
}
|
|
@@ -985,6 +503,16 @@ function validateNode(graph, node, issues) {
|
|
|
985
503
|
if (node.lane !== undefined && !LANE_KINDS.includes(node.lane)) {
|
|
986
504
|
issue(issues, `nodes.${node.id}`, `unknown lane ${JSON.stringify(node.lane)} (expected one of ${LANE_KINDS.join(", ")})`);
|
|
987
505
|
}
|
|
506
|
+
if (node.cwd !== undefined && (typeof node.cwd !== "string" || !node.cwd.trim() || node.cwd.includes("\x00"))) {
|
|
507
|
+
issue(issues, `nodes.${node.id}.cwd`, "cwd must be a non-empty path string without NUL characters on the executor");
|
|
508
|
+
}
|
|
509
|
+
if (node.codexMode !== undefined) {
|
|
510
|
+
if (!CODEX_EXECUTION_MODES.includes(node.codexMode)) {
|
|
511
|
+
issue(issues, `nodes.${node.id}.codexMode`, "codexMode must be repository or read-only");
|
|
512
|
+
} else if (node.lane !== "codex" || !node.prompt || node.command !== undefined) {
|
|
513
|
+
issue(issues, `nodes.${node.id}.codexMode`, "codexMode is supported only for a codex prompt step without command");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
988
516
|
if (node.maxRetries !== undefined && (!Number.isInteger(node.maxRetries) || node.maxRetries < 0)) {
|
|
989
517
|
issue(issues, `nodes.${node.id}`, "maxRetries must be a non-negative integer");
|
|
990
518
|
}
|
|
@@ -1062,9 +590,17 @@ function validateNode(graph, node, issues) {
|
|
|
1062
590
|
break;
|
|
1063
591
|
}
|
|
1064
592
|
}
|
|
593
|
+
var GRAPH_NODE_TYPES, LANE_KINDS, CODEX_EXECUTION_MODES, NODE_ID_RE;
|
|
594
|
+
var init_graph = __esm(() => {
|
|
595
|
+
init_expr();
|
|
596
|
+
GRAPH_NODE_TYPES = ["start", "step", "decision", "while", "end"];
|
|
597
|
+
LANE_KINDS = ["claude", "codex", "cursor", "grok"];
|
|
598
|
+
CODEX_EXECUTION_MODES = ["repository", "read-only"];
|
|
599
|
+
NODE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
600
|
+
});
|
|
1065
601
|
|
|
1066
602
|
// src/agent-tools.ts
|
|
1067
|
-
|
|
603
|
+
init_graph();
|
|
1068
604
|
|
|
1069
605
|
// src/tool-definitions.ts
|
|
1070
606
|
function textResult(payload) {
|
|
@@ -1158,7 +694,7 @@ async function callWorkflowTool(service, name, args) {
|
|
|
1158
694
|
limit: typeof args.limit === "number" ? args.limit : 100
|
|
1159
695
|
}));
|
|
1160
696
|
case "workflows_lanes_list":
|
|
1161
|
-
return textResult(await laneInventory());
|
|
697
|
+
return textResult(await service.laneInventory());
|
|
1162
698
|
default:
|
|
1163
699
|
throw new Error("Unknown workflows tool");
|
|
1164
700
|
}
|