Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70bea92133 | |||
| 6f2cddd695 |
@@ -107,4 +107,27 @@ describe("llmExtract", () => {
|
||||
}
|
||||
expect(result.error.kind).toBe("schema_validation_failed");
|
||||
});
|
||||
|
||||
it("dryRun skips fetch and returns an empty stub value", async () => {
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const schema = z.object({ n: z.number() });
|
||||
const result = await llmExtract({
|
||||
text: "ignored",
|
||||
schema,
|
||||
provider: { baseUrl: "https://example.com", apiKey: "k", model: "m" },
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
logSpy.mockRestore();
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.value).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { spawnSafe } from "../spawn-safe.js";
|
||||
|
||||
@@ -36,4 +36,28 @@ describe("spawnSafe", () => {
|
||||
}
|
||||
expect(result.error.exitCode).toBe(7);
|
||||
});
|
||||
|
||||
it("dryRun skips spawn and returns a zero-exit stub", async () => {
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
const result = await spawnSafe(process.execPath, ["-e", "process.exit(1)"], {
|
||||
cwd: null,
|
||||
env: null,
|
||||
timeoutMs: 10_000,
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
logSpy.mockRestore();
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.value).toEqual({
|
||||
stdout: "[dryRun] skipped",
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,14 +10,27 @@ export type CursorAgentOptions = {
|
||||
cwd: string;
|
||||
env: SpawnEnv | null;
|
||||
timeoutMs: number | null;
|
||||
dryRun: boolean;
|
||||
};
|
||||
|
||||
type CursorAgentOptionsInput = CursorAgentOptions | Omit<CursorAgentOptions, "dryRun">;
|
||||
|
||||
function resolveCursorAgentDryRun(options: CursorAgentOptionsInput): boolean {
|
||||
return "dryRun" in options ? options.dryRun : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes `cursor-agent` with the prompt passed as a single argv slot (`shell: false`).
|
||||
*/
|
||||
export async function cursorAgent(
|
||||
options: CursorAgentOptions,
|
||||
options: CursorAgentOptionsInput,
|
||||
): Promise<Result<string, SpawnError>> {
|
||||
const dryRun = resolveCursorAgentDryRun(options);
|
||||
if (dryRun) {
|
||||
console.log("[dryRun] cursorAgent:", options.prompt, JSON.stringify(options));
|
||||
return ok("[dryRun] skipped");
|
||||
}
|
||||
|
||||
const args: string[] = [
|
||||
"-p",
|
||||
options.prompt,
|
||||
@@ -38,6 +51,7 @@ export async function cursorAgent(
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
timeoutMs: options.timeoutMs,
|
||||
dryRun: false,
|
||||
});
|
||||
|
||||
if (!run.ok) {
|
||||
|
||||
@@ -11,8 +11,15 @@ export type LlmExtractOptions<T> = {
|
||||
text: string;
|
||||
schema: z.ZodType<T>;
|
||||
provider: LlmProvider;
|
||||
dryRun: boolean;
|
||||
};
|
||||
|
||||
type LlmExtractOptionsInput<T> = LlmExtractOptions<T> | Omit<LlmExtractOptions<T>, "dryRun">;
|
||||
|
||||
function resolveLlmExtractDryRun<T>(options: LlmExtractOptionsInput<T>): boolean {
|
||||
return "dryRun" in options ? options.dryRun : false;
|
||||
}
|
||||
|
||||
export type LlmError =
|
||||
| { kind: "http_error"; status: number; body: string }
|
||||
| { kind: "invalid_response_json"; message: string }
|
||||
@@ -90,7 +97,15 @@ function readToolArgumentsJson(parsed: unknown, previewSource: string): Result<s
|
||||
* Calls an OpenAI-compatible chat completions API with `tool_choice` forced to a single function
|
||||
* derived from a Zod v4 schema (`toJSONSchema`). Uses `fetch()` only (no shell).
|
||||
*/
|
||||
export async function llmExtract<T>(options: LlmExtractOptions<T>): Promise<Result<T, LlmError>> {
|
||||
export async function llmExtract<T>(
|
||||
options: LlmExtractOptionsInput<T>,
|
||||
): Promise<Result<T, LlmError>> {
|
||||
const dryRun = resolveLlmExtractDryRun(options);
|
||||
if (dryRun) {
|
||||
console.log("[dryRun] llmExtract:", options.text, JSON.stringify(options.schema));
|
||||
return ok({} as T);
|
||||
}
|
||||
|
||||
const rawJsonSchema = toJSONSchema(options.schema) as Record<string, unknown>;
|
||||
const parameters = stripJsonSchemaMeta(rawJsonSchema);
|
||||
const toolName = readToolName(parameters);
|
||||
|
||||
@@ -30,8 +30,11 @@ export type SpawnSafeOptions = {
|
||||
/** When null, merges {@link nerveCommandEnv} over `process.env`. When set, merges over that default. */
|
||||
env: SpawnEnv | null;
|
||||
timeoutMs: number | null;
|
||||
dryRun: boolean;
|
||||
};
|
||||
|
||||
type SpawnSafeOptionsInput = SpawnSafeOptions | Omit<SpawnSafeOptions, "dryRun">;
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 300_000;
|
||||
|
||||
/**
|
||||
@@ -63,6 +66,10 @@ function resolveTimeout(timeoutMs: number | null): number {
|
||||
return timeoutMs;
|
||||
}
|
||||
|
||||
function resolveDryRun(options: SpawnSafeOptionsInput): boolean {
|
||||
return "dryRun" in options ? options.dryRun : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a process with `shell: false` (argv only), default {@link nerveCommandEnv}, and optional timeout.
|
||||
* Returns `ok` only when the process exits with code 0.
|
||||
@@ -70,8 +77,21 @@ function resolveTimeout(timeoutMs: number | null): number {
|
||||
export function spawnSafe(
|
||||
command: string,
|
||||
args: ReadonlyArray<string>,
|
||||
options: SpawnSafeOptions,
|
||||
options: SpawnSafeOptionsInput,
|
||||
): Promise<Result<SpawnResult, SpawnError>> {
|
||||
const dryRun = resolveDryRun(options);
|
||||
if (dryRun) {
|
||||
console.log("[dryRun] spawnSafe:", command, args, JSON.stringify(options));
|
||||
return Promise.resolve(
|
||||
ok({
|
||||
stdout: "[dryRun] skipped",
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const cwd = options.cwd === null ? process.cwd() : options.cwd;
|
||||
const env = mergeEnv(options.env);
|
||||
|
||||
Reference in New Issue
Block a user