@hasna/workflows 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/agent-tools.js +995 -954
- package/dist/cli/index.js +23 -12
- package/dist/handlers.js +830 -778
- package/dist/index.js +831 -778
- package/dist/mcp/index.js +15 -4
- package/dist/sdk.js +4 -4
- package/dist/serve/admin.js +56 -13
- package/dist/serve/index.js +56 -13
- package/dist/service.js +828 -777
- package/package.json +3 -3
- package/types/graph.d.ts +6 -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/dist/service.js
CHANGED
|
@@ -16,603 +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 as statSync2 } 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 = statSync2(`${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 run2 = await withTimeout(runLocalAgent(sdk, job, (callback) => {
|
|
335
|
-
cancel = callback;
|
|
336
|
-
}), timeoutMs, "cursor lane", () => cancel?.());
|
|
337
|
-
const durationMs = Date.now() - started;
|
|
338
|
-
const status = run2.status;
|
|
339
|
-
const ok = status === "finished";
|
|
340
|
-
return { ok, exitCode: ok ? 0 : 1, output: run2.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 run2 = await agent.send(job.prompt, { model: job.model });
|
|
362
|
-
if (run2 && typeof run2 === "object") {
|
|
363
|
-
const handle = run2;
|
|
364
|
-
if (typeof handle.cancel === "function")
|
|
365
|
-
onCancel(() => {
|
|
366
|
-
handle.cancel().catch(() => {});
|
|
367
|
-
});
|
|
368
|
-
if (typeof handle.wait === "function")
|
|
369
|
-
run2 = await handle.wait();
|
|
370
|
-
}
|
|
371
|
-
if (!run2 || typeof run2 !== "object") {
|
|
372
|
-
throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
|
|
373
|
-
}
|
|
374
|
-
const candidate = run2;
|
|
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
|
-
// src/service.ts
|
|
501
|
-
import { createHash as createHash3 } from "crypto";
|
|
502
|
-
// package.json
|
|
503
|
-
var package_default = {
|
|
504
|
-
name: "@hasna/workflows",
|
|
505
|
-
version: "0.2.0",
|
|
506
|
-
description: "Universal graph workflow app \u2014 graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
|
|
507
|
-
type: "module",
|
|
508
|
-
main: "./dist/index.js",
|
|
509
|
-
types: "./types/index.d.ts",
|
|
510
|
-
bin: {
|
|
511
|
-
workflows: "dist/cli/index.js",
|
|
512
|
-
"workflows-mcp": "dist/mcp/index.js",
|
|
513
|
-
"workflows-serve": "dist/serve/index.js"
|
|
514
|
-
},
|
|
515
|
-
exports: {
|
|
516
|
-
".": {
|
|
517
|
-
types: "./types/index.d.ts",
|
|
518
|
-
import: "./dist/index.js"
|
|
519
|
-
},
|
|
520
|
-
"./sdk": {
|
|
521
|
-
types: "./types/sdk.d.ts",
|
|
522
|
-
import: "./dist/sdk.js"
|
|
523
|
-
},
|
|
524
|
-
"./cli": {
|
|
525
|
-
types: "./types/cli/index.d.ts",
|
|
526
|
-
import: "./dist/cli/index.js"
|
|
527
|
-
}
|
|
528
|
-
},
|
|
529
|
-
files: [
|
|
530
|
-
"dist",
|
|
531
|
-
"types",
|
|
532
|
-
"README.md",
|
|
533
|
-
"LICENSE",
|
|
534
|
-
"hasna.contract.json"
|
|
535
|
-
],
|
|
536
|
-
scripts: {
|
|
537
|
-
build: "bun run check:sdk && bun run build:runtime && bun run build:types",
|
|
538
|
-
"generate:sdk": "bun scripts/generate-sdk.ts --write",
|
|
539
|
-
"check:sdk": "bun scripts/generate-sdk.ts",
|
|
540
|
-
"test:artifacts": "bun scripts/artifact-acceptance.ts",
|
|
541
|
-
"build:runtime": "bun build src/cli/index.ts --outdir dist/cli --target bun && bun build src/mcp/index.ts --outdir dist/mcp --target bun && bun build src/serve/index.ts src/serve/admin.ts --outdir dist/serve --target bun --packages external && bun build src/index.ts src/service.ts src/handlers.ts src/agent-tools.ts src/types.ts --root src --outdir dist --target bun --packages external && bun build src/sdk.ts --outdir dist --target node",
|
|
542
|
-
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly --outDir types",
|
|
543
|
-
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
544
|
-
"contract:check": "bun scripts/contracts-cli.mjs repo-conformance .",
|
|
545
|
-
typecheck: "tsc --noEmit",
|
|
546
|
-
test: "bun test",
|
|
547
|
-
"test:pg": 'WORKFLOWS_TEST_DATABASE_URL="${WORKFLOWS_TEST_DATABASE_URL:?point it at a fresh test Postgres database}" WORKFLOWS_REQUIRE_POSTGRES=1 bun test src/db/postgres-store.test.ts src/db/postgres-http.test.ts src/db/postgres-fencing.test.ts',
|
|
548
|
-
prepack: "bun run build && bun run artifact-scan",
|
|
549
|
-
prepublishOnly: "bun run test && bun run build"
|
|
550
|
-
},
|
|
551
|
-
keywords: [
|
|
552
|
-
"workflows",
|
|
553
|
-
"graph",
|
|
554
|
-
"automation",
|
|
555
|
-
"cli",
|
|
556
|
-
"typescript",
|
|
557
|
-
"bun",
|
|
558
|
-
"hasna"
|
|
559
|
-
],
|
|
560
|
-
publishConfig: {
|
|
561
|
-
registry: "https://registry.npmjs.org",
|
|
562
|
-
access: "public"
|
|
563
|
-
},
|
|
564
|
-
repository: {
|
|
565
|
-
type: "git",
|
|
566
|
-
url: "https://github.com/hasna/apps.git",
|
|
567
|
-
directory: "apps/workflows"
|
|
568
|
-
},
|
|
569
|
-
homepage: "https://github.com/hasna/apps/tree/main/apps/workflows",
|
|
570
|
-
bugs: {
|
|
571
|
-
url: "https://github.com/hasna/apps/issues"
|
|
572
|
-
},
|
|
573
|
-
engines: {
|
|
574
|
-
bun: ">=1.0.0"
|
|
575
|
-
},
|
|
576
|
-
author: "Hasna",
|
|
577
|
-
license: "Apache-2.0",
|
|
578
|
-
dependencies: {
|
|
579
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.234",
|
|
580
|
-
"@cursor/sdk": "^1.0.28",
|
|
581
|
-
"@openai/codex-sdk": "0.147.0",
|
|
582
|
-
commander: "^13.1.0",
|
|
583
|
-
"@hasna/contracts": "1.3.3",
|
|
584
|
-
pg: "^8.16.3"
|
|
585
|
-
},
|
|
586
|
-
devDependencies: {
|
|
587
|
-
"@types/bun": "1.3.14",
|
|
588
|
-
"@types/pg": "^8.15.6",
|
|
589
|
-
typescript: "^5.7.3"
|
|
590
|
-
}
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
// src/version.ts
|
|
594
|
-
function packageVersion() {
|
|
595
|
-
return package_default.version;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// src/daemon.ts
|
|
599
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
600
|
-
|
|
601
19
|
// src/expr.ts
|
|
602
|
-
class ExprSyntaxError extends Error {
|
|
603
|
-
constructor(message) {
|
|
604
|
-
super(message);
|
|
605
|
-
this.name = "ExprSyntaxError";
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
class ExprEvalError extends Error {
|
|
610
|
-
constructor(message) {
|
|
611
|
-
super(message);
|
|
612
|
-
this.name = "ExprEvalError";
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
var COMPARISON_OPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
|
|
616
20
|
function tokenize(input) {
|
|
617
21
|
const tokens = [];
|
|
618
22
|
let i = 0;
|
|
@@ -864,11 +268,24 @@ function evaluateExpr(input, context) {
|
|
|
864
268
|
}
|
|
865
269
|
return value;
|
|
866
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
|
+
});
|
|
867
287
|
|
|
868
288
|
// src/graph.ts
|
|
869
|
-
var GRAPH_NODE_TYPES = ["start", "step", "decision", "while", "end"];
|
|
870
|
-
var LANE_KINDS = ["claude", "codex", "cursor", "grok"];
|
|
871
|
-
var NODE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
872
289
|
function issue(issues, path, message) {
|
|
873
290
|
issues.push({ path, message });
|
|
874
291
|
}
|
|
@@ -954,216 +371,841 @@ function findCycles(graph) {
|
|
|
954
371
|
visit(to);
|
|
955
372
|
}
|
|
956
373
|
}
|
|
957
|
-
stack.pop();
|
|
958
|
-
color.set(id, "black");
|
|
959
|
-
}
|
|
960
|
-
for (const node of graph.nodes) {
|
|
961
|
-
if (color.get(node.id) === undefined)
|
|
962
|
-
visit(node.id);
|
|
374
|
+
stack.pop();
|
|
375
|
+
color.set(id, "black");
|
|
376
|
+
}
|
|
377
|
+
for (const node of graph.nodes) {
|
|
378
|
+
if (color.get(node.id) === undefined)
|
|
379
|
+
visit(node.id);
|
|
380
|
+
}
|
|
381
|
+
return cycles;
|
|
382
|
+
}
|
|
383
|
+
function validateGraph(graph) {
|
|
384
|
+
const issues = [];
|
|
385
|
+
if (graph === null || typeof graph !== "object" || Array.isArray(graph)) {
|
|
386
|
+
return { ok: false, issues: [{ path: "graph", message: "graph must be an object" }] };
|
|
387
|
+
}
|
|
388
|
+
if (typeof graph.name !== "string" || graph.name.trim() === "") {
|
|
389
|
+
issue(issues, "name", "graph requires a non-empty name");
|
|
390
|
+
}
|
|
391
|
+
if (typeof graph.version !== "string" || graph.version.trim() === "") {
|
|
392
|
+
issue(issues, "version", "graph requires a version string");
|
|
393
|
+
}
|
|
394
|
+
if (graph.memoWatch !== undefined) {
|
|
395
|
+
if (!Array.isArray(graph.memoWatch) || graph.memoWatch.length === 0) {
|
|
396
|
+
issue(issues, "memoWatch", "memoWatch must be a non-empty array of file paths or globs");
|
|
397
|
+
} else {
|
|
398
|
+
for (const entry of graph.memoWatch) {
|
|
399
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
400
|
+
issue(issues, "memoWatch", "each memoWatch entry must be a non-empty path or glob string");
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!Array.isArray(graph.nodes) || graph.nodes.length === 0) {
|
|
407
|
+
issue(issues, "nodes", "graph requires at least one node");
|
|
408
|
+
return { ok: false, issues };
|
|
409
|
+
}
|
|
410
|
+
for (let index = 0;index < graph.nodes.length; index++) {
|
|
411
|
+
const node = graph.nodes[index];
|
|
412
|
+
if (node === null || typeof node !== "object" || Array.isArray(node)) {
|
|
413
|
+
issue(issues, `nodes.${index}`, "node must be an object");
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (!GRAPH_NODE_TYPES.includes(node.type))
|
|
417
|
+
issue(issues, `nodes.${index}.type`, "type must be start, step, decision, while, or end");
|
|
418
|
+
for (const key of ["next", "then", "else", "prompt", "command"]) {
|
|
419
|
+
const value = node[key];
|
|
420
|
+
if (value !== undefined && (typeof value !== "string" || value.trim() === ""))
|
|
421
|
+
issue(issues, `nodes.${index}.${key}`, `${key} must be a non-empty string`);
|
|
422
|
+
}
|
|
423
|
+
if (node.type === "while" && (!Array.isArray(node.body) || node.body.some((id) => typeof id !== "string")))
|
|
424
|
+
issue(issues, `nodes.${index}.body`, "body must be an array of node ids");
|
|
425
|
+
}
|
|
426
|
+
if (issues.length)
|
|
427
|
+
return { ok: false, issues };
|
|
428
|
+
const starts = graph.nodes.filter((n) => n.type === "start");
|
|
429
|
+
if (starts.length === 0)
|
|
430
|
+
issue(issues, "nodes", "graph requires exactly one start node (found none)");
|
|
431
|
+
if (starts.length > 1)
|
|
432
|
+
issue(issues, "nodes", "graph requires exactly one start node (found multiple)");
|
|
433
|
+
const ends = graph.nodes.filter((n) => n.type === "end");
|
|
434
|
+
if (ends.length === 0)
|
|
435
|
+
issue(issues, "nodes", "graph requires at least one end node");
|
|
436
|
+
const seenIds = new Map;
|
|
437
|
+
for (const node of graph.nodes) {
|
|
438
|
+
if (typeof node.id !== "string" || !NODE_ID_RE.test(node.id)) {
|
|
439
|
+
issue(issues, "nodes", `node id ${JSON.stringify(node.id)} must match ${String(NODE_ID_RE)}`);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (seenIds.has(node.id)) {
|
|
443
|
+
issue(issues, `nodes.${node.id}`, `duplicate node id ${node.id}`);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
seenIds.set(node.id, node);
|
|
447
|
+
if (!GRAPH_NODE_TYPES.includes(node.type)) {
|
|
448
|
+
issue(issues, `nodes.${node.id}`, `unknown node type ${JSON.stringify(node.type)}`);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
validateNode(graph, node, issues);
|
|
452
|
+
}
|
|
453
|
+
for (const node of graph.nodes) {
|
|
454
|
+
const edges = nodeEdges(node);
|
|
455
|
+
for (const { kind, target } of edges) {
|
|
456
|
+
if (!seenIds.has(target)) {
|
|
457
|
+
issue(issues, `nodes.${node.id}`, `${kind} edge targets nonexistent node ${target}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (starts.length === 1) {
|
|
462
|
+
const reachable = new Set(reachableNodes(graph));
|
|
463
|
+
for (const node of graph.nodes) {
|
|
464
|
+
if (!reachable.has(node.id)) {
|
|
465
|
+
issue(issues, `nodes.${node.id}`, `node ${node.id} is unreachable from start`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const cycles = findCycles(graph);
|
|
470
|
+
for (const cycle of cycles) {
|
|
471
|
+
issue(issues, "nodes", `explicit cycle detected: ${cycle.join(" -> ")} (loop only via a while node)`);
|
|
472
|
+
}
|
|
473
|
+
return { ok: issues.length === 0, issues };
|
|
474
|
+
}
|
|
475
|
+
function nodeEdges(node) {
|
|
476
|
+
switch (node.type) {
|
|
477
|
+
case "start":
|
|
478
|
+
case "step":
|
|
479
|
+
return node.next ? [{ kind: "next", target: node.next }] : [];
|
|
480
|
+
case "decision":
|
|
481
|
+
return [
|
|
482
|
+
...node.then ? [{ kind: "then", target: node.then }] : [],
|
|
483
|
+
...node.else ? [{ kind: "else", target: node.else }] : []
|
|
484
|
+
];
|
|
485
|
+
case "while":
|
|
486
|
+
return node.next ? [{ kind: "next", target: node.next }] : [];
|
|
487
|
+
case "end":
|
|
488
|
+
return [];
|
|
489
|
+
default:
|
|
490
|
+
return [];
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function validateNode(graph, node, issues) {
|
|
494
|
+
switch (node.type) {
|
|
495
|
+
case "start":
|
|
496
|
+
if (!node.next)
|
|
497
|
+
issue(issues, `nodes.${node.id}`, "start node requires a next edge");
|
|
498
|
+
break;
|
|
499
|
+
case "step": {
|
|
500
|
+
if (!node.prompt && !node.command) {
|
|
501
|
+
issue(issues, `nodes.${node.id}`, "step node requires prompt or command");
|
|
502
|
+
}
|
|
503
|
+
if (node.lane !== undefined && !LANE_KINDS.includes(node.lane)) {
|
|
504
|
+
issue(issues, `nodes.${node.id}`, `unknown lane ${JSON.stringify(node.lane)} (expected one of ${LANE_KINDS.join(", ")})`);
|
|
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
|
+
}
|
|
516
|
+
if (node.maxRetries !== undefined && (!Number.isInteger(node.maxRetries) || node.maxRetries < 0)) {
|
|
517
|
+
issue(issues, `nodes.${node.id}`, "maxRetries must be a non-negative integer");
|
|
518
|
+
}
|
|
519
|
+
if (node.timeoutMs !== undefined && (!Number.isInteger(node.timeoutMs) || node.timeoutMs < 1)) {
|
|
520
|
+
issue(issues, `nodes.${node.id}`, "timeoutMs must be a positive integer");
|
|
521
|
+
}
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
case "decision": {
|
|
525
|
+
if (typeof node.condition !== "string" || node.condition.trim() === "") {
|
|
526
|
+
issue(issues, `nodes.${node.id}`, "decision node requires a condition expression");
|
|
527
|
+
} else {
|
|
528
|
+
try {
|
|
529
|
+
parseExpr(node.condition);
|
|
530
|
+
} catch (err) {
|
|
531
|
+
if (err instanceof ExprSyntaxError) {
|
|
532
|
+
issue(issues, `nodes.${node.id}.condition`, `invalid condition expression: ${err.message}`);
|
|
533
|
+
} else {
|
|
534
|
+
throw err;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (!node.then && !node.else) {
|
|
539
|
+
issue(issues, `nodes.${node.id}`, "decision node requires then and/or else edge");
|
|
540
|
+
}
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
case "while": {
|
|
544
|
+
if (typeof node.condition !== "string" || node.condition.trim() === "") {
|
|
545
|
+
issue(issues, `nodes.${node.id}`, "while node requires a condition expression");
|
|
546
|
+
} else {
|
|
547
|
+
try {
|
|
548
|
+
parseExpr(node.condition);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
if (err instanceof ExprSyntaxError) {
|
|
551
|
+
issue(issues, `nodes.${node.id}.condition`, `invalid condition expression: ${err.message}`);
|
|
552
|
+
} else {
|
|
553
|
+
throw err;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (node.maxIterations === undefined) {
|
|
558
|
+
issue(issues, `nodes.${node.id}`, "while node requires maxIterations (finite loop bound)");
|
|
559
|
+
} else if (!Number.isInteger(node.maxIterations) || node.maxIterations < 1) {
|
|
560
|
+
issue(issues, `nodes.${node.id}`, "maxIterations must be a positive integer");
|
|
561
|
+
}
|
|
562
|
+
if (!Array.isArray(node.body) || node.body.length === 0) {
|
|
563
|
+
issue(issues, `nodes.${node.id}`, "while node requires a non-empty body");
|
|
564
|
+
} else {
|
|
565
|
+
for (const bodyId of node.body) {
|
|
566
|
+
const bodyNode = findNode(graph, bodyId);
|
|
567
|
+
if (!bodyNode) {
|
|
568
|
+
issue(issues, `nodes.${node.id}`, `while body references nonexistent node ${bodyId}`);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (bodyNode.type === "start" || bodyNode.type === "end" || bodyNode.type === "while") {
|
|
572
|
+
issue(issues, `nodes.${node.id}`, `while body may only contain step/decision nodes in v1 (found ${bodyNode.type})`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const bodySet = new Set(node.body);
|
|
576
|
+
for (const bodyId of node.body) {
|
|
577
|
+
const bodyNode = findNode(graph, bodyId);
|
|
578
|
+
if (!bodyNode)
|
|
579
|
+
continue;
|
|
580
|
+
for (const { kind, target } of nodeEdges(bodyNode)) {
|
|
581
|
+
if (!bodySet.has(target)) {
|
|
582
|
+
issue(issues, `nodes.${node.id}`, `while body node ${bodyId} ${kind}-edge leaves the body (target ${target})`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
case "end":
|
|
590
|
+
break;
|
|
963
591
|
}
|
|
964
|
-
return cycles;
|
|
965
592
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
+
});
|
|
601
|
+
|
|
602
|
+
// src/lanes/types.ts
|
|
603
|
+
import { statSync as statSync2 } from "fs";
|
|
604
|
+
function binaryOnPath(binary) {
|
|
605
|
+
const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
|
|
606
|
+
for (const entry of pathEntries) {
|
|
607
|
+
try {
|
|
608
|
+
const stat = statSync2(`${entry}/${binary}`);
|
|
609
|
+
if (stat.isFile())
|
|
610
|
+
return true;
|
|
611
|
+
} catch {}
|
|
970
612
|
}
|
|
971
|
-
|
|
972
|
-
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
function subscriptionEnvironment(overrides = {}) {
|
|
616
|
+
const paid = /^(OPENAI_API_KEY|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|CODEX_API_KEY|CURSOR_API_KEY)$/;
|
|
617
|
+
const sensitive = /(?:API[_-]?KEY|SECRET|PASSWORD|TOKEN|DATABASE_URL|CREDENTIAL)/i;
|
|
618
|
+
const env = {};
|
|
619
|
+
for (const [name, value] of Object.entries(process.env)) {
|
|
620
|
+
if (value !== undefined && !paid.test(name) && (!sensitive.test(name) || name === "CLAUDE_CODE_OAUTH_TOKEN"))
|
|
621
|
+
env[name] = value;
|
|
973
622
|
}
|
|
974
|
-
|
|
975
|
-
|
|
623
|
+
for (const [name, value] of Object.entries(overrides)) {
|
|
624
|
+
if (paid.test(name) || sensitive.test(name))
|
|
625
|
+
throw new Error(`lane environment field ${name} requires a supported scoped credential consumer`);
|
|
626
|
+
env[name] = value;
|
|
976
627
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
628
|
+
return env;
|
|
629
|
+
}
|
|
630
|
+
function subscriptionSdkEnvironment(overrides = {}) {
|
|
631
|
+
const safe = subscriptionEnvironment(overrides);
|
|
632
|
+
return { ...Object.fromEntries(Object.keys(process.env).map((name) => [name, undefined])), ...safe };
|
|
633
|
+
}
|
|
634
|
+
function withTimeout(promise, timeoutMs, what, cancel) {
|
|
635
|
+
return new Promise((resolve, reject) => {
|
|
636
|
+
const timer = setTimeout(() => {
|
|
637
|
+
cancel?.();
|
|
638
|
+
reject(new LaneTimeoutError(what, timeoutMs));
|
|
639
|
+
}, timeoutMs);
|
|
640
|
+
promise.then((value) => {
|
|
641
|
+
clearTimeout(timer);
|
|
642
|
+
resolve(value);
|
|
643
|
+
}, (err) => {
|
|
644
|
+
clearTimeout(timer);
|
|
645
|
+
reject(err);
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
async function spawnCli(cmd, opts) {
|
|
650
|
+
const started = Date.now();
|
|
651
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
|
|
652
|
+
let proc;
|
|
653
|
+
try {
|
|
654
|
+
proc = Bun.spawn({
|
|
655
|
+
cmd,
|
|
656
|
+
cwd: opts.cwd,
|
|
657
|
+
env: subscriptionEnvironment(opts.env),
|
|
658
|
+
stdin: "ignore",
|
|
659
|
+
stdout: "pipe",
|
|
660
|
+
stderr: "pipe",
|
|
661
|
+
timeout: timeoutMs
|
|
662
|
+
});
|
|
663
|
+
} catch (err) {
|
|
664
|
+
const message = String(err instanceof Error ? err.message : err);
|
|
665
|
+
if (message.includes("ENOENT") || /not found/i.test(message)) {
|
|
666
|
+
throw new LaneDependencyMissingError(`lane binary ${JSON.stringify(opts.binaryName)} not found on PATH; install it or provide ${opts.packageHint}`);
|
|
987
667
|
}
|
|
668
|
+
return { ok: false, exitCode: 1, output: "", durationMs: Date.now() - started, error: message };
|
|
988
669
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
670
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
671
|
+
new Response(proc.stdout).text(),
|
|
672
|
+
new Response(proc.stderr).text(),
|
|
673
|
+
proc.exited
|
|
674
|
+
]);
|
|
675
|
+
const output = opts.stdoutOnly ? stdout.trim() : stderr ? `${stdout}
|
|
676
|
+
${stderr}`.trim() : stdout.trim();
|
|
677
|
+
return {
|
|
678
|
+
ok: exitCode === 0,
|
|
679
|
+
exitCode,
|
|
680
|
+
output,
|
|
681
|
+
durationMs: Date.now() - started,
|
|
682
|
+
...opts.stdoutOnly && exitCode !== 0 && stderr.trim() ? { error: stderr.trim() } : {}
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
var LANE_KINDS2, LaneDependencyMissingError, LaneAdapterShapeError, DEFAULT_LANE_TIMEOUT_MS = 120000, LaneTimeoutError;
|
|
686
|
+
var init_types = __esm(() => {
|
|
687
|
+
LANE_KINDS2 = ["claude", "codex", "cursor", "grok"];
|
|
688
|
+
LaneDependencyMissingError = class LaneDependencyMissingError extends Error {
|
|
689
|
+
constructor(message) {
|
|
690
|
+
super(message);
|
|
691
|
+
this.name = "LaneDependencyMissingError";
|
|
998
692
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
issue(issues, `nodes.${index}.${key}`, `${key} must be a non-empty string`);
|
|
693
|
+
};
|
|
694
|
+
LaneAdapterShapeError = class LaneAdapterShapeError extends Error {
|
|
695
|
+
constructor(message) {
|
|
696
|
+
super(message);
|
|
697
|
+
this.name = "LaneAdapterShapeError";
|
|
1005
698
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const starts = graph.nodes.filter((n) => n.type === "start");
|
|
1012
|
-
if (starts.length === 0)
|
|
1013
|
-
issue(issues, "nodes", "graph requires exactly one start node (found none)");
|
|
1014
|
-
if (starts.length > 1)
|
|
1015
|
-
issue(issues, "nodes", "graph requires exactly one start node (found multiple)");
|
|
1016
|
-
const ends = graph.nodes.filter((n) => n.type === "end");
|
|
1017
|
-
if (ends.length === 0)
|
|
1018
|
-
issue(issues, "nodes", "graph requires at least one end node");
|
|
1019
|
-
const seenIds = new Map;
|
|
1020
|
-
for (const node of graph.nodes) {
|
|
1021
|
-
if (typeof node.id !== "string" || !NODE_ID_RE.test(node.id)) {
|
|
1022
|
-
issue(issues, "nodes", `node id ${JSON.stringify(node.id)} must match ${String(NODE_ID_RE)}`);
|
|
1023
|
-
continue;
|
|
699
|
+
};
|
|
700
|
+
LaneTimeoutError = class LaneTimeoutError extends Error {
|
|
701
|
+
constructor(what, timeoutMs) {
|
|
702
|
+
super(`${what} exceeded the ${timeoutMs}ms budget`);
|
|
703
|
+
this.name = "LaneTimeoutError";
|
|
1024
704
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
705
|
+
};
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// src/lanes/claude.ts
|
|
709
|
+
function createClaudeAdapter(deps = {}) {
|
|
710
|
+
return {
|
|
711
|
+
kind: "claude",
|
|
712
|
+
async probe() {
|
|
713
|
+
const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
|
|
714
|
+
try {
|
|
715
|
+
await loader();
|
|
716
|
+
return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "sdk" };
|
|
717
|
+
} catch {
|
|
718
|
+
if (binaryOnPath(deps.cliPath ?? "claude")) {
|
|
719
|
+
return { kind: "claude", sdk: "@anthropic-ai/claude-agent-sdk", wired: true, via: "cli" };
|
|
720
|
+
}
|
|
721
|
+
return {
|
|
722
|
+
kind: "claude",
|
|
723
|
+
sdk: "@anthropic-ai/claude-agent-sdk",
|
|
724
|
+
wired: false,
|
|
725
|
+
reason: "neither @anthropic-ai/claude-agent-sdk nor the claude CLI is available on this machine"
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
async run(job) {
|
|
730
|
+
if (!job.prompt) {
|
|
731
|
+
return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "claude lane requires a prompt" };
|
|
732
|
+
}
|
|
733
|
+
const started = Date.now();
|
|
734
|
+
const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
|
|
735
|
+
const loader = deps.sdkLoader ?? (async () => await import("@anthropic-ai/claude-agent-sdk"));
|
|
736
|
+
let sdk;
|
|
737
|
+
try {
|
|
738
|
+
sdk = await loader();
|
|
739
|
+
} catch (err) {
|
|
740
|
+
return runClaudeCli(job, deps.cliPath ?? "claude", started, timeoutMs);
|
|
741
|
+
}
|
|
742
|
+
const query = sdk.query;
|
|
743
|
+
if (typeof query !== "function") {
|
|
744
|
+
throw new LaneAdapterShapeError("@anthropic-ai/claude-agent-sdk exports no query() function");
|
|
745
|
+
}
|
|
746
|
+
const prompt = job.prompt;
|
|
747
|
+
const abortController = new AbortController;
|
|
748
|
+
try {
|
|
749
|
+
const { output, isError } = await withTimeout((async () => {
|
|
750
|
+
const generator = await query({
|
|
751
|
+
prompt,
|
|
752
|
+
options: { model: job.model, cwd: job.cwd, maxTurns: 32, abortController, env: subscriptionSdkEnvironment(job.env) }
|
|
753
|
+
});
|
|
754
|
+
let output2 = "";
|
|
755
|
+
let isError2 = false;
|
|
756
|
+
for await (const message of generator) {
|
|
757
|
+
if (message.type === "assistant") {
|
|
758
|
+
const content = message.message?.content;
|
|
759
|
+
if (Array.isArray(content)) {
|
|
760
|
+
for (const block of content) {
|
|
761
|
+
if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
|
|
762
|
+
output2 += block.text;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (message.type === "error")
|
|
768
|
+
isError2 = true;
|
|
769
|
+
}
|
|
770
|
+
return { output: output2, isError: isError2 };
|
|
771
|
+
})(), timeoutMs, "claude lane", () => abortController.abort());
|
|
772
|
+
const durationMs = Date.now() - started;
|
|
773
|
+
return { ok: !isError, exitCode: isError ? 1 : 0, output: output.trim(), durationMs };
|
|
774
|
+
} catch (err) {
|
|
775
|
+
const durationMs = Date.now() - started;
|
|
776
|
+
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
|
|
777
|
+
}
|
|
1028
778
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
async function runClaudeCli(job, cliPath, started, timeoutMs) {
|
|
782
|
+
try {
|
|
783
|
+
const result = await spawnCli([cliPath, "-p", job.prompt], {
|
|
784
|
+
binaryName: cliPath,
|
|
785
|
+
packageHint: "@anthropic-ai/claude-agent-sdk or the claude CLI",
|
|
786
|
+
timeoutMs,
|
|
787
|
+
env: job.env,
|
|
788
|
+
cwd: job.cwd
|
|
789
|
+
});
|
|
790
|
+
return { ...result, durationMs: Date.now() - started };
|
|
791
|
+
} catch (err) {
|
|
792
|
+
if (err instanceof LaneDependencyMissingError) {
|
|
793
|
+
return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
|
|
1033
794
|
}
|
|
1034
|
-
|
|
795
|
+
throw err;
|
|
1035
796
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
797
|
+
}
|
|
798
|
+
var init_claude = __esm(() => {
|
|
799
|
+
init_types();
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// src/lanes/codex.ts
|
|
803
|
+
function createCodexAdapter(deps = {}) {
|
|
804
|
+
return {
|
|
805
|
+
kind: "codex",
|
|
806
|
+
async probe() {
|
|
807
|
+
const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
|
|
808
|
+
try {
|
|
809
|
+
await loader();
|
|
810
|
+
return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "sdk" };
|
|
811
|
+
} catch {
|
|
812
|
+
if (binaryOnPath(deps.cliPath ?? "codex")) {
|
|
813
|
+
return { kind: "codex", sdk: "@openai/codex-sdk", wired: true, via: "cli" };
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
kind: "codex",
|
|
817
|
+
sdk: "@openai/codex-sdk",
|
|
818
|
+
wired: false,
|
|
819
|
+
reason: "neither @openai/codex-sdk nor the codex CLI is available on this machine"
|
|
820
|
+
};
|
|
1041
821
|
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (!
|
|
1048
|
-
|
|
822
|
+
},
|
|
823
|
+
async run(job) {
|
|
824
|
+
if (!job.prompt) {
|
|
825
|
+
return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "codex lane requires a prompt" };
|
|
826
|
+
}
|
|
827
|
+
if (job.codexMode !== undefined && (!CODEX_EXECUTION_MODES.includes(job.codexMode) || job.lane !== "codex" || job.command !== undefined)) {
|
|
828
|
+
return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "codexMode must be repository or read-only on a codex prompt without command", retryable: false };
|
|
829
|
+
}
|
|
830
|
+
const started = Date.now();
|
|
831
|
+
const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
|
|
832
|
+
const loader = deps.sdkLoader ?? (async () => await import("@openai/codex-sdk"));
|
|
833
|
+
let sdk;
|
|
834
|
+
try {
|
|
835
|
+
sdk = await loader();
|
|
836
|
+
} catch {
|
|
837
|
+
return runCodexCli(job, deps.cliPath ?? "codex", started, timeoutMs);
|
|
838
|
+
}
|
|
839
|
+
const CodexClass = sdk.Codex;
|
|
840
|
+
if (typeof CodexClass !== "function") {
|
|
841
|
+
throw new LaneAdapterShapeError("@openai/codex-sdk exports no Codex class");
|
|
842
|
+
}
|
|
843
|
+
const abortController = new AbortController;
|
|
844
|
+
try {
|
|
845
|
+
const client = await withTimeout(Promise.resolve(new CodexClass({
|
|
846
|
+
config: { model: job.model },
|
|
847
|
+
env: subscriptionEnvironment(job.env)
|
|
848
|
+
})), timeoutMs, "codex lane");
|
|
849
|
+
const thread = client.startThread?.({
|
|
850
|
+
model: job.model,
|
|
851
|
+
workingDirectory: job.cwd,
|
|
852
|
+
...job.codexMode === "read-only" ? { sandboxMode: "read-only", skipGitRepoCheck: true, approvalPolicy: "never" } : {}
|
|
853
|
+
});
|
|
854
|
+
if (!thread || typeof thread.run !== "function") {
|
|
855
|
+
throw new LaneAdapterShapeError("@openai/codex-sdk Codex.startThread() returned no run()");
|
|
856
|
+
}
|
|
857
|
+
const turn = await withTimeout(thread.run(job.prompt, { signal: abortController.signal }), timeoutMs, "codex lane", () => abortController.abort());
|
|
858
|
+
const durationMs = Date.now() - started;
|
|
859
|
+
const output = turn?.finalResponse;
|
|
860
|
+
if (typeof output !== "string" || !output.trim()) {
|
|
861
|
+
return missingResponse(durationMs);
|
|
862
|
+
}
|
|
863
|
+
return { ok: true, exitCode: 0, output, durationMs };
|
|
864
|
+
} catch (err) {
|
|
865
|
+
const durationMs = Date.now() - started;
|
|
866
|
+
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
|
|
1049
867
|
}
|
|
1050
868
|
}
|
|
1051
|
-
}
|
|
1052
|
-
const cycles = findCycles(graph);
|
|
1053
|
-
for (const cycle of cycles) {
|
|
1054
|
-
issue(issues, "nodes", `explicit cycle detected: ${cycle.join(" -> ")} (loop only via a while node)`);
|
|
1055
|
-
}
|
|
1056
|
-
return { ok: issues.length === 0, issues };
|
|
869
|
+
};
|
|
1057
870
|
}
|
|
1058
|
-
function
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
return
|
|
871
|
+
async function runCodexCli(job, cliPath, started, timeoutMs) {
|
|
872
|
+
try {
|
|
873
|
+
const controls = job.codexMode === "read-only" ? ["--sandbox", "read-only", "--skip-git-repo-check", "--config", 'approval_policy="never"'] : [];
|
|
874
|
+
const result = await spawnCli([cliPath, "exec", ...controls, "--", job.prompt], {
|
|
875
|
+
binaryName: cliPath,
|
|
876
|
+
packageHint: "@openai/codex-sdk or the codex CLI",
|
|
877
|
+
timeoutMs,
|
|
878
|
+
env: job.env,
|
|
879
|
+
cwd: job.cwd,
|
|
880
|
+
stdoutOnly: true
|
|
881
|
+
});
|
|
882
|
+
const durationMs = Date.now() - started;
|
|
883
|
+
return result.ok && !result.output.trim() ? missingResponse(durationMs) : { ...result, durationMs };
|
|
884
|
+
} catch (err) {
|
|
885
|
+
if (err instanceof LaneDependencyMissingError) {
|
|
886
|
+
return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
|
|
887
|
+
}
|
|
888
|
+
throw err;
|
|
1074
889
|
}
|
|
1075
890
|
}
|
|
1076
|
-
function
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
891
|
+
function missingResponse(durationMs) {
|
|
892
|
+
return { ok: false, exitCode: 1, output: "", durationMs, error: "Codex completed without an agent response; verify the native subscription and model route", retryable: false };
|
|
893
|
+
}
|
|
894
|
+
var init_codex = __esm(() => {
|
|
895
|
+
init_graph();
|
|
896
|
+
init_types();
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
// src/lanes/cursor.ts
|
|
900
|
+
function createCursorAdapter(deps = {}) {
|
|
901
|
+
return {
|
|
902
|
+
kind: "cursor",
|
|
903
|
+
async probe() {
|
|
904
|
+
const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
|
|
905
|
+
try {
|
|
906
|
+
await loader();
|
|
907
|
+
return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "sdk (local mode)" };
|
|
908
|
+
} catch {
|
|
909
|
+
if (binaryOnPath(deps.cliPath ?? "cursor-agent")) {
|
|
910
|
+
return { kind: "cursor", sdk: "@cursor/sdk", wired: true, via: "cli" };
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
kind: "cursor",
|
|
914
|
+
sdk: "@cursor/sdk",
|
|
915
|
+
wired: false,
|
|
916
|
+
reason: "neither @cursor/sdk nor the cursor-agent CLI is available on this machine"
|
|
917
|
+
};
|
|
1085
918
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
919
|
+
},
|
|
920
|
+
async run(job) {
|
|
921
|
+
if (!job.prompt) {
|
|
922
|
+
return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "cursor lane requires a prompt" };
|
|
1088
923
|
}
|
|
1089
|
-
if (
|
|
1090
|
-
|
|
924
|
+
if (!deps.sdkLoader && binaryOnPath(deps.cliPath ?? "cursor-agent"))
|
|
925
|
+
return runCursorCli(job, deps.cliPath ?? "cursor-agent", Date.now(), job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS);
|
|
926
|
+
const started = Date.now();
|
|
927
|
+
const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
|
|
928
|
+
const loader = deps.sdkLoader ?? (async () => await import("@cursor/sdk"));
|
|
929
|
+
let sdk;
|
|
930
|
+
try {
|
|
931
|
+
sdk = await loader();
|
|
932
|
+
} catch {
|
|
933
|
+
return runCursorCli(job, deps.cliPath ?? "cursor-agent", started, timeoutMs);
|
|
1091
934
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
935
|
+
let cancel;
|
|
936
|
+
try {
|
|
937
|
+
subscriptionEnvironment(job.env);
|
|
938
|
+
if (!deps.sdkLoader && process.env.CURSOR_API_KEY)
|
|
939
|
+
throw new Error("cursor lane refuses ambient API billing; use the native subscription CLI");
|
|
940
|
+
const run2 = await withTimeout(runLocalAgent(sdk, job, (callback) => {
|
|
941
|
+
cancel = callback;
|
|
942
|
+
}), timeoutMs, "cursor lane", () => cancel?.());
|
|
943
|
+
const durationMs = Date.now() - started;
|
|
944
|
+
const status = run2.status;
|
|
945
|
+
const ok = status === "finished";
|
|
946
|
+
return { ok, exitCode: ok ? 0 : 1, output: run2.result, durationMs };
|
|
947
|
+
} catch (err) {
|
|
948
|
+
if (err instanceof LaneAdapterShapeError)
|
|
949
|
+
throw err;
|
|
950
|
+
const durationMs = Date.now() - started;
|
|
951
|
+
return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err), retryable: !(err instanceof LaneTimeoutError) };
|
|
1094
952
|
}
|
|
1095
|
-
break;
|
|
1096
953
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
async function runLocalAgent(sdk, job, onCancel) {
|
|
957
|
+
const AgentClass = sdk.Agent;
|
|
958
|
+
const CursorClass = sdk.Cursor;
|
|
959
|
+
if (typeof AgentClass !== "function" && typeof CursorClass !== "function") {
|
|
960
|
+
throw new LaneAdapterShapeError("@cursor/sdk exports no Agent/Cursor class");
|
|
961
|
+
}
|
|
962
|
+
const create = AgentClass.create;
|
|
963
|
+
const agent = typeof create === "function" ? await create.call(AgentClass, { local: { cwd: job.cwd, enableAgentRetries: false } }) : typeof AgentClass === "function" ? new AgentClass : undefined;
|
|
964
|
+
if (!agent || typeof agent.send !== "function") {
|
|
965
|
+
throw new LaneAdapterShapeError("@cursor/sdk Agent has no send()");
|
|
966
|
+
}
|
|
967
|
+
let run2 = await agent.send(job.prompt, { model: job.model });
|
|
968
|
+
if (run2 && typeof run2 === "object") {
|
|
969
|
+
const handle = run2;
|
|
970
|
+
if (typeof handle.cancel === "function")
|
|
971
|
+
onCancel(() => {
|
|
972
|
+
handle.cancel().catch(() => {});
|
|
973
|
+
});
|
|
974
|
+
if (typeof handle.wait === "function")
|
|
975
|
+
run2 = await handle.wait();
|
|
976
|
+
}
|
|
977
|
+
if (!run2 || typeof run2 !== "object") {
|
|
978
|
+
throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
|
|
979
|
+
}
|
|
980
|
+
const candidate = run2;
|
|
981
|
+
if (typeof candidate.status !== "string" || !["finished", "error", "cancelled"].includes(candidate.status)) {
|
|
982
|
+
throw new LaneAdapterShapeError(`@cursor/sdk Agent.send() returned a Run without a terminal status (got ${JSON.stringify(candidate.status)}; expected finished|error|cancelled)`);
|
|
983
|
+
}
|
|
984
|
+
if (typeof candidate.result !== "string") {
|
|
985
|
+
throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned a Run without a string result");
|
|
986
|
+
}
|
|
987
|
+
return { status: candidate.status, result: candidate.result };
|
|
988
|
+
}
|
|
989
|
+
async function runCursorCli(job, cliPath, started, timeoutMs) {
|
|
990
|
+
try {
|
|
991
|
+
const result = await spawnCli([cliPath, "--print", job.prompt, ...job.model ? ["--model", job.model] : []], {
|
|
992
|
+
binaryName: cliPath,
|
|
993
|
+
packageHint: "@cursor/sdk (local mode) or the cursor-agent CLI",
|
|
994
|
+
timeoutMs,
|
|
995
|
+
env: job.env,
|
|
996
|
+
cwd: job.cwd
|
|
997
|
+
});
|
|
998
|
+
return { ...result, durationMs: Date.now() - started };
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
if (err instanceof LaneDependencyMissingError) {
|
|
1001
|
+
return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
|
|
1115
1002
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1003
|
+
throw err;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
var init_cursor = __esm(() => {
|
|
1007
|
+
init_types();
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
// src/lanes/grok.ts
|
|
1011
|
+
function createGrokAdapter(deps = {}) {
|
|
1012
|
+
return {
|
|
1013
|
+
kind: "grok",
|
|
1014
|
+
async probe() {
|
|
1015
|
+
const cliPath = deps.cliPath ?? "grok";
|
|
1016
|
+
if (binaryOnPath(cliPath)) {
|
|
1017
|
+
return { kind: "grok", sdk: "no npm SDK (measured 2026-08-25)", wired: true, via: "cli" };
|
|
1129
1018
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1019
|
+
return {
|
|
1020
|
+
kind: "grok",
|
|
1021
|
+
sdk: "no npm SDK (measured 2026-08-25)",
|
|
1022
|
+
wired: false,
|
|
1023
|
+
reason: `no xAI Grok SDK exists on npm (measured 2026-08-25) and the grok CLI is not on PATH`
|
|
1024
|
+
};
|
|
1025
|
+
},
|
|
1026
|
+
async run(job) {
|
|
1027
|
+
if (!job.prompt) {
|
|
1028
|
+
return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "grok lane requires a prompt" };
|
|
1134
1029
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
const bodyNode = findNode(graph, bodyId);
|
|
1151
|
-
if (!bodyNode)
|
|
1152
|
-
continue;
|
|
1153
|
-
for (const { kind, target } of nodeEdges(bodyNode)) {
|
|
1154
|
-
if (!bodySet.has(target)) {
|
|
1155
|
-
issue(issues, `nodes.${node.id}`, `while body node ${bodyId} ${kind}-edge leaves the body (target ${target})`);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1030
|
+
const started = Date.now();
|
|
1031
|
+
const timeoutMs = job.timeoutMs ?? DEFAULT_LANE_TIMEOUT_MS;
|
|
1032
|
+
const cliPath = deps.cliPath ?? "grok";
|
|
1033
|
+
try {
|
|
1034
|
+
const result = await spawnCli([cliPath, job.prompt], {
|
|
1035
|
+
binaryName: cliPath,
|
|
1036
|
+
packageHint: "the grok CLI (no xAI Grok SDK exists on npm \u2014 measured 2026-08-25)",
|
|
1037
|
+
timeoutMs,
|
|
1038
|
+
env: job.env,
|
|
1039
|
+
cwd: job.cwd
|
|
1040
|
+
});
|
|
1041
|
+
return { ...result, durationMs: Date.now() - started };
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
if (err instanceof LaneDependencyMissingError) {
|
|
1044
|
+
return { ok: false, exitCode: 127, output: "", durationMs: Date.now() - started, error: err.message };
|
|
1158
1045
|
}
|
|
1046
|
+
throw err;
|
|
1159
1047
|
}
|
|
1160
|
-
break;
|
|
1161
1048
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
var init_grok = __esm(() => {
|
|
1052
|
+
init_types();
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
// src/lanes/index.ts
|
|
1056
|
+
var exports_lanes = {};
|
|
1057
|
+
__export(exports_lanes, {
|
|
1058
|
+
runLaneJob: () => runLaneJob,
|
|
1059
|
+
resolveLane: () => resolveLane,
|
|
1060
|
+
probeLane: () => probeLane,
|
|
1061
|
+
laneInventory: () => laneInventory,
|
|
1062
|
+
createGrokAdapter: () => createGrokAdapter,
|
|
1063
|
+
createCursorAdapter: () => createCursorAdapter,
|
|
1064
|
+
createCodexAdapter: () => createCodexAdapter,
|
|
1065
|
+
createClaudeAdapter: () => createClaudeAdapter,
|
|
1066
|
+
binaryOnPath: () => binaryOnPath,
|
|
1067
|
+
LaneDependencyMissingError: () => LaneDependencyMissingError,
|
|
1068
|
+
LaneAdapterShapeError: () => LaneAdapterShapeError,
|
|
1069
|
+
LANE_KINDS: () => LANE_KINDS2
|
|
1070
|
+
});
|
|
1071
|
+
function resolveLane(kind) {
|
|
1072
|
+
const adapter = adapters.get(kind);
|
|
1073
|
+
if (!adapter)
|
|
1074
|
+
throw new Error(`unknown lane ${kind} (expected one of ${LANE_KINDS2.join(", ")})`);
|
|
1075
|
+
return adapter;
|
|
1076
|
+
}
|
|
1077
|
+
async function runLaneJob(job) {
|
|
1078
|
+
return resolveLane(job.lane).run(job);
|
|
1079
|
+
}
|
|
1080
|
+
async function probeLane(kind) {
|
|
1081
|
+
return resolveLane(kind).probe();
|
|
1082
|
+
}
|
|
1083
|
+
async function laneInventory() {
|
|
1084
|
+
return Promise.all(LANE_KINDS2.map((kind) => probeLane(kind)));
|
|
1085
|
+
}
|
|
1086
|
+
var adapters;
|
|
1087
|
+
var init_lanes = __esm(() => {
|
|
1088
|
+
init_claude();
|
|
1089
|
+
init_codex();
|
|
1090
|
+
init_cursor();
|
|
1091
|
+
init_grok();
|
|
1092
|
+
init_types();
|
|
1093
|
+
init_types();
|
|
1094
|
+
init_claude();
|
|
1095
|
+
init_codex();
|
|
1096
|
+
init_cursor();
|
|
1097
|
+
init_grok();
|
|
1098
|
+
adapters = new Map([
|
|
1099
|
+
["claude", createClaudeAdapter()],
|
|
1100
|
+
["codex", createCodexAdapter()],
|
|
1101
|
+
["cursor", createCursorAdapter()],
|
|
1102
|
+
["grok", createGrokAdapter()]
|
|
1103
|
+
]);
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// src/service.ts
|
|
1107
|
+
import { createHash as createHash3 } from "crypto";
|
|
1108
|
+
// package.json
|
|
1109
|
+
var package_default = {
|
|
1110
|
+
name: "@hasna/workflows",
|
|
1111
|
+
version: "0.2.1",
|
|
1112
|
+
description: "Universal graph workflow app \u2014 graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
|
|
1113
|
+
type: "module",
|
|
1114
|
+
main: "./dist/index.js",
|
|
1115
|
+
types: "./types/index.d.ts",
|
|
1116
|
+
bin: {
|
|
1117
|
+
workflows: "dist/cli/index.js",
|
|
1118
|
+
"workflows-mcp": "dist/mcp/index.js",
|
|
1119
|
+
"workflows-serve": "dist/serve/index.js"
|
|
1120
|
+
},
|
|
1121
|
+
exports: {
|
|
1122
|
+
".": {
|
|
1123
|
+
types: "./types/index.d.ts",
|
|
1124
|
+
import: "./dist/index.js"
|
|
1125
|
+
},
|
|
1126
|
+
"./sdk": {
|
|
1127
|
+
types: "./types/sdk.d.ts",
|
|
1128
|
+
import: "./dist/sdk.js"
|
|
1129
|
+
},
|
|
1130
|
+
"./cli": {
|
|
1131
|
+
types: "./types/cli/index.d.ts",
|
|
1132
|
+
import: "./dist/cli/index.js"
|
|
1133
|
+
}
|
|
1134
|
+
},
|
|
1135
|
+
files: [
|
|
1136
|
+
"dist",
|
|
1137
|
+
"types",
|
|
1138
|
+
"README.md",
|
|
1139
|
+
"LICENSE",
|
|
1140
|
+
"hasna.contract.json"
|
|
1141
|
+
],
|
|
1142
|
+
scripts: {
|
|
1143
|
+
build: "bun run check:sdk && bun run build:runtime && bun run build:types",
|
|
1144
|
+
"generate:sdk": "bun scripts/generate-sdk.ts --write",
|
|
1145
|
+
"check:sdk": "bun scripts/generate-sdk.ts",
|
|
1146
|
+
"test:artifacts": "bun scripts/artifact-acceptance.ts",
|
|
1147
|
+
"build:runtime": "bun build src/cli/index.ts --outdir dist/cli --target bun && bun build src/mcp/index.ts --outdir dist/mcp --target bun && bun build src/serve/index.ts src/serve/admin.ts --outdir dist/serve --target bun --packages external && bun build src/index.ts src/service.ts src/handlers.ts src/agent-tools.ts src/types.ts --root src --outdir dist --target bun --packages external && bun build src/sdk.ts --outdir dist --target node",
|
|
1148
|
+
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly --outDir types",
|
|
1149
|
+
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
1150
|
+
"contract:check": "bun scripts/contracts-cli.mjs repo-conformance .",
|
|
1151
|
+
typecheck: "tsc --noEmit",
|
|
1152
|
+
test: "bun test",
|
|
1153
|
+
"test:pg": 'WORKFLOWS_TEST_DATABASE_URL="${WORKFLOWS_TEST_DATABASE_URL:?point it at a fresh test Postgres database}" WORKFLOWS_REQUIRE_POSTGRES=1 bun test src/db/postgres-store.test.ts src/db/postgres-http.test.ts src/db/postgres-fencing.test.ts',
|
|
1154
|
+
prepack: "bun run build && bun run artifact-scan",
|
|
1155
|
+
prepublishOnly: "bun run test && bun run build"
|
|
1156
|
+
},
|
|
1157
|
+
keywords: [
|
|
1158
|
+
"workflows",
|
|
1159
|
+
"graph",
|
|
1160
|
+
"automation",
|
|
1161
|
+
"cli",
|
|
1162
|
+
"typescript",
|
|
1163
|
+
"bun",
|
|
1164
|
+
"hasna"
|
|
1165
|
+
],
|
|
1166
|
+
publishConfig: {
|
|
1167
|
+
registry: "https://registry.npmjs.org",
|
|
1168
|
+
access: "public"
|
|
1169
|
+
},
|
|
1170
|
+
repository: {
|
|
1171
|
+
type: "git",
|
|
1172
|
+
url: "https://github.com/hasna/apps.git",
|
|
1173
|
+
directory: "apps/workflows"
|
|
1174
|
+
},
|
|
1175
|
+
homepage: "https://github.com/hasna/apps/tree/main/apps/workflows",
|
|
1176
|
+
bugs: {
|
|
1177
|
+
url: "https://github.com/hasna/apps/issues"
|
|
1178
|
+
},
|
|
1179
|
+
engines: {
|
|
1180
|
+
bun: ">=1.0.0"
|
|
1181
|
+
},
|
|
1182
|
+
author: "Hasna",
|
|
1183
|
+
license: "Apache-2.0",
|
|
1184
|
+
dependencies: {
|
|
1185
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.234",
|
|
1186
|
+
"@cursor/sdk": "^1.0.28",
|
|
1187
|
+
"@hasna/contracts": "1.3.3",
|
|
1188
|
+
"@openai/codex-sdk": "0.160.0",
|
|
1189
|
+
commander: "^13.1.0",
|
|
1190
|
+
pg: "^8.16.3"
|
|
1191
|
+
},
|
|
1192
|
+
devDependencies: {
|
|
1193
|
+
"@types/bun": "1.3.14",
|
|
1194
|
+
"@types/pg": "^8.15.6",
|
|
1195
|
+
typescript: "^5.7.3"
|
|
1164
1196
|
}
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
// src/version.ts
|
|
1200
|
+
function packageVersion() {
|
|
1201
|
+
return package_default.version;
|
|
1165
1202
|
}
|
|
1166
1203
|
|
|
1204
|
+
// src/daemon.ts
|
|
1205
|
+
init_expr();
|
|
1206
|
+
init_graph();
|
|
1207
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1208
|
+
|
|
1167
1209
|
// src/secrets.ts
|
|
1168
1210
|
class SecretsGateError extends Error {
|
|
1169
1211
|
constructor(message) {
|
|
@@ -2034,7 +2076,14 @@ class WorkflowsDaemon {
|
|
|
2034
2076
|
const isLoopBody = graph.nodes.some((candidate) => candidate.type === "while" && candidate.body.includes(nodeId));
|
|
2035
2077
|
const nodeRow = latest === undefined || isLoopBody && latest.status === "completed" ? await this.store.createRunNode({ runId: run2.id, nodeId, lane: node2.lane ?? "claude" }) : latest;
|
|
2036
2078
|
const memoWatch = node2.memo ? memoWatchFingerprint(graph, this.store.dataDir) : undefined;
|
|
2037
|
-
const memoSource = {
|
|
2079
|
+
const memoSource = {
|
|
2080
|
+
command: node2.command,
|
|
2081
|
+
prompt: node2.prompt,
|
|
2082
|
+
context: stripRunState(context),
|
|
2083
|
+
lane: node2.lane ?? "claude",
|
|
2084
|
+
cwd: node2.cwd,
|
|
2085
|
+
codexMode: node2.lane === "codex" ? node2.codexMode ?? "repository" : undefined
|
|
2086
|
+
};
|
|
2038
2087
|
const memoInput = node2.memo ? memoWatch !== undefined ? { ...memoSource, memoWatch } : memoSource : undefined;
|
|
2039
2088
|
if (memoInput !== undefined) {
|
|
2040
2089
|
const hit = await tryMemoHit(this.store, run2.graphName, nodeId, memoInput);
|
|
@@ -2059,6 +2108,8 @@ class WorkflowsDaemon {
|
|
|
2059
2108
|
lane: node2.lane ?? "claude",
|
|
2060
2109
|
prompt: node2.prompt,
|
|
2061
2110
|
command: node2.command,
|
|
2111
|
+
cwd: node2.cwd,
|
|
2112
|
+
codexMode: node2.codexMode,
|
|
2062
2113
|
timeoutMs: node2.timeoutMs,
|
|
2063
2114
|
env: this.env
|
|
2064
2115
|
};
|