@hasna/workflows 0.2.1 → 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 +25 -0
- package/dist/agent-tools.js +1 -506
- package/dist/cli/index.js +11 -11
- package/dist/handlers.js +115 -15
- package/dist/index.js +116 -17
- package/dist/mcp/index.js +3 -3
- package/dist/sdk.js +3 -3
- package/dist/serve/admin.js +134 -32
- package/dist/serve/index.js +175 -32
- package/dist/service.js +113 -13
- package/hasna.contract.json +1 -1
- package/package.json +2 -2
- package/types/journal-directory.d.ts +4 -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
|
@@ -141,6 +141,31 @@ Use durable storage for the data directory and one deployed executor; ephemeral
|
|
|
141
141
|
container storage is unsuitable. Model lanes require the selected harness's
|
|
142
142
|
native subscription on the executor. They do not inherit paid model API keys.
|
|
143
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
|
+
|
|
144
169
|
When the server sits behind an authenticated gateway, set
|
|
145
170
|
`HASNA_WORKFLOWS_ORIGIN_AUTH` to the gateway's scoped origin identity. It must be
|
|
146
171
|
32 to 256 ASCII letters, digits, underscores or hyphens. All API routes, including
|
package/dist/agent-tools.js
CHANGED
|
@@ -599,513 +599,8 @@ var init_graph = __esm(() => {
|
|
|
599
599
|
NODE_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
600
600
|
});
|
|
601
601
|
|
|
602
|
-
// src/lanes/types.ts
|
|
603
|
-
import { statSync } 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 = statSync(`${entry}/${binary}`);
|
|
609
|
-
if (stat.isFile())
|
|
610
|
-
return true;
|
|
611
|
-
} catch {}
|
|
612
|
-
}
|
|
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;
|
|
622
|
-
}
|
|
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;
|
|
627
|
-
}
|
|
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}`);
|
|
667
|
-
}
|
|
668
|
-
return { ok: false, exitCode: 1, output: "", durationMs: Date.now() - started, error: message };
|
|
669
|
-
}
|
|
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";
|
|
692
|
-
}
|
|
693
|
-
};
|
|
694
|
-
LaneAdapterShapeError = class LaneAdapterShapeError extends Error {
|
|
695
|
-
constructor(message) {
|
|
696
|
-
super(message);
|
|
697
|
-
this.name = "LaneAdapterShapeError";
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
LaneTimeoutError = class LaneTimeoutError extends Error {
|
|
701
|
-
constructor(what, timeoutMs) {
|
|
702
|
-
super(`${what} exceeded the ${timeoutMs}ms budget`);
|
|
703
|
-
this.name = "LaneTimeoutError";
|
|
704
|
-
}
|
|
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
|
-
}
|
|
778
|
-
}
|
|
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 };
|
|
794
|
-
}
|
|
795
|
-
throw err;
|
|
796
|
-
}
|
|
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
|
-
};
|
|
821
|
-
}
|
|
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) };
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
};
|
|
870
|
-
}
|
|
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;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
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
|
-
};
|
|
918
|
-
}
|
|
919
|
-
},
|
|
920
|
-
async run(job) {
|
|
921
|
-
if (!job.prompt) {
|
|
922
|
-
return { ok: false, exitCode: 2, output: "", durationMs: 0, error: "cursor lane requires a prompt" };
|
|
923
|
-
}
|
|
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);
|
|
934
|
-
}
|
|
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 run = await withTimeout(runLocalAgent(sdk, job, (callback) => {
|
|
941
|
-
cancel = callback;
|
|
942
|
-
}), timeoutMs, "cursor lane", () => cancel?.());
|
|
943
|
-
const durationMs = Date.now() - started;
|
|
944
|
-
const status = run.status;
|
|
945
|
-
const ok = status === "finished";
|
|
946
|
-
return { ok, exitCode: ok ? 0 : 1, output: run.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) };
|
|
952
|
-
}
|
|
953
|
-
}
|
|
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 run = await agent.send(job.prompt, { model: job.model });
|
|
968
|
-
if (run && typeof run === "object") {
|
|
969
|
-
const handle = run;
|
|
970
|
-
if (typeof handle.cancel === "function")
|
|
971
|
-
onCancel(() => {
|
|
972
|
-
handle.cancel().catch(() => {});
|
|
973
|
-
});
|
|
974
|
-
if (typeof handle.wait === "function")
|
|
975
|
-
run = await handle.wait();
|
|
976
|
-
}
|
|
977
|
-
if (!run || typeof run !== "object") {
|
|
978
|
-
throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
|
|
979
|
-
}
|
|
980
|
-
const candidate = run;
|
|
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 };
|
|
1002
|
-
}
|
|
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" };
|
|
1018
|
-
}
|
|
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" };
|
|
1029
|
-
}
|
|
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 };
|
|
1045
|
-
}
|
|
1046
|
-
throw err;
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
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
602
|
// src/agent-tools.ts
|
|
1107
603
|
init_graph();
|
|
1108
|
-
init_lanes();
|
|
1109
604
|
|
|
1110
605
|
// src/tool-definitions.ts
|
|
1111
606
|
function textResult(payload) {
|
|
@@ -1199,7 +694,7 @@ async function callWorkflowTool(service, name, args) {
|
|
|
1199
694
|
limit: typeof args.limit === "number" ? args.limit : 100
|
|
1200
695
|
}));
|
|
1201
696
|
case "workflows_lanes_list":
|
|
1202
|
-
return textResult(await laneInventory());
|
|
697
|
+
return textResult(await service.laneInventory());
|
|
1203
698
|
default:
|
|
1204
699
|
throw new Error("Unknown workflows tool");
|
|
1205
700
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -33,7 +33,7 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
33
33
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
34
34
|
var __require = import.meta.require;
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// ../../app-test-sandbox-1/source/node_modules/commander/lib/error.js
|
|
37
37
|
var require_error = __commonJS((exports) => {
|
|
38
38
|
class CommanderError extends Error {
|
|
39
39
|
constructor(exitCode, code, message) {
|
|
@@ -57,7 +57,7 @@ var require_error = __commonJS((exports) => {
|
|
|
57
57
|
exports.InvalidArgumentError = InvalidArgumentError;
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
//
|
|
60
|
+
// ../../app-test-sandbox-1/source/node_modules/commander/lib/argument.js
|
|
61
61
|
var require_argument = __commonJS((exports) => {
|
|
62
62
|
var { InvalidArgumentError } = require_error();
|
|
63
63
|
|
|
@@ -136,7 +136,7 @@ var require_argument = __commonJS((exports) => {
|
|
|
136
136
|
exports.humanReadableArgName = humanReadableArgName;
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
//
|
|
139
|
+
// ../../app-test-sandbox-1/source/node_modules/commander/lib/help.js
|
|
140
140
|
var require_help = __commonJS((exports) => {
|
|
141
141
|
var { humanReadableArgName } = require_argument();
|
|
142
142
|
|
|
@@ -486,7 +486,7 @@ ${itemIndentStr}`);
|
|
|
486
486
|
exports.stripColor = stripColor;
|
|
487
487
|
});
|
|
488
488
|
|
|
489
|
-
//
|
|
489
|
+
// ../../app-test-sandbox-1/source/node_modules/commander/lib/option.js
|
|
490
490
|
var require_option = __commonJS((exports) => {
|
|
491
491
|
var { InvalidArgumentError } = require_error();
|
|
492
492
|
|
|
@@ -664,7 +664,7 @@ var require_option = __commonJS((exports) => {
|
|
|
664
664
|
exports.DualOptions = DualOptions;
|
|
665
665
|
});
|
|
666
666
|
|
|
667
|
-
//
|
|
667
|
+
// ../../app-test-sandbox-1/source/node_modules/commander/lib/suggestSimilar.js
|
|
668
668
|
var require_suggestSimilar = __commonJS((exports) => {
|
|
669
669
|
var maxDistance = 3;
|
|
670
670
|
function editDistance(a, b) {
|
|
@@ -737,7 +737,7 @@ var require_suggestSimilar = __commonJS((exports) => {
|
|
|
737
737
|
exports.suggestSimilar = suggestSimilar;
|
|
738
738
|
});
|
|
739
739
|
|
|
740
|
-
//
|
|
740
|
+
// ../../app-test-sandbox-1/source/node_modules/commander/lib/command.js
|
|
741
741
|
var require_command = __commonJS((exports) => {
|
|
742
742
|
var EventEmitter = __require("events").EventEmitter;
|
|
743
743
|
var childProcess = __require("child_process");
|
|
@@ -2047,7 +2047,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2047
2047
|
exports.useColor = useColor;
|
|
2048
2048
|
});
|
|
2049
2049
|
|
|
2050
|
-
//
|
|
2050
|
+
// ../../app-test-sandbox-1/source/node_modules/commander/index.js
|
|
2051
2051
|
var require_commander = __commonJS((exports) => {
|
|
2052
2052
|
var { Argument } = require_argument();
|
|
2053
2053
|
var { Command } = require_command();
|
|
@@ -2067,7 +2067,7 @@ var require_commander = __commonJS((exports) => {
|
|
|
2067
2067
|
exports.InvalidOptionArgumentError = InvalidArgumentError;
|
|
2068
2068
|
});
|
|
2069
2069
|
|
|
2070
|
-
//
|
|
2070
|
+
// ../../app-test-sandbox-1/source/node_modules/commander/esm.mjs
|
|
2071
2071
|
var import__ = __toESM(require_commander(), 1);
|
|
2072
2072
|
var {
|
|
2073
2073
|
program,
|
|
@@ -2086,7 +2086,7 @@ var {
|
|
|
2086
2086
|
// src/cli/index.ts
|
|
2087
2087
|
import { readFileSync as readFileSync5, readSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2088
2088
|
|
|
2089
|
-
//
|
|
2089
|
+
// ../../app-test-sandbox-1/source/node_modules/@hasna/contracts/dist/client/transport.js
|
|
2090
2090
|
import { isIP } from "net";
|
|
2091
2091
|
import { spawnSync } from "child_process";
|
|
2092
2092
|
import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync2 } from "fs";
|
|
@@ -3575,7 +3575,7 @@ function workflowAgent(env = process.env, explicit) {
|
|
|
3575
3575
|
// package.json
|
|
3576
3576
|
var package_default = {
|
|
3577
3577
|
name: "@hasna/workflows",
|
|
3578
|
-
version: "0.2.
|
|
3578
|
+
version: "0.2.2",
|
|
3579
3579
|
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",
|
|
3580
3580
|
type: "module",
|
|
3581
3581
|
main: "./dist/index.js",
|
|
@@ -3651,7 +3651,7 @@ var package_default = {
|
|
|
3651
3651
|
dependencies: {
|
|
3652
3652
|
"@anthropic-ai/claude-agent-sdk": "0.3.234",
|
|
3653
3653
|
"@cursor/sdk": "^1.0.28",
|
|
3654
|
-
"@hasna/contracts": "1.3.
|
|
3654
|
+
"@hasna/contracts": "1.3.5",
|
|
3655
3655
|
"@openai/codex-sdk": "0.160.0",
|
|
3656
3656
|
commander: "^13.1.0",
|
|
3657
3657
|
pg: "^8.16.3"
|