td-barrage 0.1.0

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 ADDED
@@ -0,0 +1,133 @@
1
+ # td-barrage
2
+
3
+ A small TypeScript queue runner for OpenCode tasks. It loads a JSON task file, resolves dependencies, runs ready tasks through the OpenCode SDK, persists state after every transition, and resumes interrupted work on the next invocation.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 20.11 or newer
8
+ - npm
9
+ - OpenCode available when running against a real server
10
+
11
+ ## Install
12
+
13
+ In a target project:
14
+
15
+ ```sh
16
+ npm install --save-dev td-barrage
17
+ ```
18
+
19
+ Or run ad-hoc without installing:
20
+
21
+ ```sh
22
+ npx td-barrage --tasks queue.json
23
+ ```
24
+
25
+ Once installed, the `td-barrage` CLI is on the project `PATH`:
26
+
27
+ ```sh
28
+ npx td-barrage --tasks queue.json
29
+ ```
30
+
31
+ For local development on this package:
32
+
33
+ ```sh
34
+ npm install
35
+ npm run build
36
+ ```
37
+
38
+ ## Task File
39
+
40
+ Task files use a top-level `tasks` array. A bare array is also accepted when loading.
41
+
42
+ ```json
43
+ {
44
+ "tasks": [
45
+ {
46
+ "id": "build",
47
+ "prompt": "Create .queue/results/build.json with {\"status\":\"ok\"}.",
48
+ "success": { "strategy": "result_json", "path": ".queue/results/build.json" }
49
+ },
50
+ {
51
+ "id": "verify",
52
+ "prompt": "Verify the build result and create .queue/results/verify.json with {\"status\":\"ok\"}.",
53
+ "dependsOn": ["build"],
54
+ "timeoutMs": 600000,
55
+ "maxAttempts": 2,
56
+ "success": { "strategy": "result_json", "path": ".queue/results/verify.json" }
57
+ }
58
+ ]
59
+ }
60
+ ```
61
+
62
+ Supported task fields:
63
+
64
+ - `id`: unique task id
65
+ - `prompt`: prompt sent to OpenCode
66
+ - `dependsOn`: dependency ids
67
+ - `status`: `pending`, `running`, `done`, `failed`, or `blocked`
68
+ - `attempts`: persisted attempt counter
69
+ - `maxAttempts`: per-task retry limit
70
+ - `timeoutMs`: per-task prompt timeout
71
+ - `cwd`: per-task working directory
72
+ - `resultPath`: optional metadata field
73
+ - `success`: `default`, `file_exists`, or `result_json`
74
+ - `recoveryNotes`, `error`, `startedAt`, `finishedAt`: runtime metadata
75
+
76
+ ## CLI
77
+
78
+ ```sh
79
+ npm run dev -- --tasks queue.json
80
+ npm run dev -- queue.json --max-attempts 3 --cwd /path/to/work --results-dir .queue/results
81
+ npm run dev -- --tasks queue.json --dry-run
82
+ npm run dev -- --tasks queue.json --json
83
+ ```
84
+
85
+ Exit codes:
86
+
87
+ - `0`: all reachable tasks completed
88
+ - `1`: at least one task failed or became blocked
89
+ - `2`: CLI usage or missing task-file error
90
+
91
+ ## Success Checks
92
+
93
+ `default` succeeds unconditionally.
94
+
95
+ `file_exists` succeeds when the configured path exists relative to the task cwd.
96
+
97
+ `result_json` succeeds when the configured JSON file parses and contains `"status": "ok"`.
98
+
99
+ ## Recovery
100
+
101
+ On startup, any task persisted as `running` is reset to `pending`, annotated in `recoveryNotes`, and retried. The `attempts` counter is preserved, so a task that crashed mid-run still counts that attempt.
102
+
103
+ ## Scripts
104
+
105
+ ```sh
106
+ npm test
107
+ npm run test:watch
108
+ npm run typecheck
109
+ npm run build
110
+ npm run test:integration
111
+ ```
112
+
113
+ Integration tests are excluded by default. Run them with:
114
+
115
+ ```sh
116
+ RUN_INTEGRATION=1 npm run test:integration
117
+ ```
118
+
119
+ Optional integration environment variables:
120
+
121
+ - `OPENCODE_BIN`: OpenCode executable, default `opencode`
122
+ - `OPENCODE_PORT`: server port, default `4096`
123
+
124
+ ## Architecture
125
+
126
+ The orchestrator accepts its dependencies as arguments:
127
+
128
+ - `client`: OpenCode client seam
129
+ - `fs`: filesystem seam
130
+ - `clock`: timer seam for timeout tests
131
+ - `log`: human or JSON event logger
132
+
133
+ This keeps most behavior covered by fast unit tests using a fake client and in-memory filesystem.
@@ -0,0 +1,9 @@
1
+ import type { OpenCodeClient } from "./orchestrator.js";
2
+ export interface ClientOptions {
3
+ baseUrl?: string;
4
+ hostname?: string;
5
+ port?: number;
6
+ timeout?: number;
7
+ config?: unknown;
8
+ }
9
+ export declare function createOpenCodeClient(options?: ClientOptions): Promise<OpenCodeClient>;
package/dist/client.js ADDED
@@ -0,0 +1,67 @@
1
+ export async function createOpenCodeClient(options = {}) {
2
+ const importSdk = new Function("specifier", "return import(specifier)");
3
+ const sdk = (await importSdkWithFallback(importSdk));
4
+ if (typeof sdk.createOpencodeClient === "function") {
5
+ return wrapSdkClient(sdk.createOpencodeClient({ ...options, throwOnError: true }));
6
+ }
7
+ if (typeof sdk.createOpencode === "function") {
8
+ const created = (await sdk.createOpencode(options));
9
+ if (created.client)
10
+ return wrapSdkClient(created.client);
11
+ }
12
+ const Client = (sdk.OpenCode ?? sdk.Opencode ?? sdk.Client ?? sdk.default);
13
+ if (!Client) {
14
+ throw new Error("Unable to locate an OpenCode SDK client export.");
15
+ }
16
+ return new Client(options);
17
+ }
18
+ function wrapSdkClient(client) {
19
+ const sessionDirs = new Map();
20
+ const raw = client;
21
+ return {
22
+ session: {
23
+ create: async (input) => {
24
+ const result = unwrapData(await raw.session.create({ directory: input.cwd, title: "orchestrator task" }));
25
+ const id = result.id;
26
+ if (typeof id !== "string" || id.length === 0) {
27
+ throw new Error("OpenCode SDK session.create did not return a session id.");
28
+ }
29
+ sessionDirs.set(id, input.cwd);
30
+ return { id };
31
+ },
32
+ prompt: async (sessionId, input) => {
33
+ const directory = sessionDirs.get(sessionId) ?? process.cwd();
34
+ const abort = async () => {
35
+ await raw.session.abort?.({ sessionID: sessionId, directory });
36
+ };
37
+ if (input.signal?.aborted)
38
+ await abort();
39
+ input.signal?.addEventListener("abort", abort, { once: true });
40
+ try {
41
+ return await raw.session.prompt({
42
+ sessionID: sessionId,
43
+ directory,
44
+ parts: [{ type: "text", text: input.prompt }],
45
+ }, { signal: input.signal });
46
+ }
47
+ finally {
48
+ input.signal?.removeEventListener("abort", abort);
49
+ }
50
+ },
51
+ },
52
+ };
53
+ }
54
+ function unwrapData(value) {
55
+ if (value && typeof value === "object" && "data" in value) {
56
+ return value.data;
57
+ }
58
+ return value;
59
+ }
60
+ async function importSdkWithFallback(importSdk) {
61
+ try {
62
+ return await importSdk("@opencode-ai/sdk/v2");
63
+ }
64
+ catch {
65
+ return importSdk("@opencode-ai/sdk");
66
+ }
67
+ }
package/dist/deps.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { Task } from "./schema.js";
2
+ export type DependencyState = "ready" | "waiting" | "blocked";
3
+ export declare function dependencyState(task: Task, tasks: Task[]): DependencyState;
4
+ export declare function topologicalOrder(tasks: Task[]): Task[];
package/dist/deps.js ADDED
@@ -0,0 +1,41 @@
1
+ export function dependencyState(task, tasks) {
2
+ const byId = new Map(tasks.map((candidate) => [candidate.id, candidate]));
3
+ for (const depId of task.dependsOn) {
4
+ const dep = byId.get(depId);
5
+ if (!dep)
6
+ return "blocked";
7
+ if (dep.status === "failed" || dep.status === "blocked")
8
+ return "blocked";
9
+ if (dep.status !== "done")
10
+ return "waiting";
11
+ }
12
+ return "ready";
13
+ }
14
+ export function topologicalOrder(tasks) {
15
+ const byId = new Map(tasks.map((task) => [task.id, task]));
16
+ const visiting = new Set();
17
+ const visited = new Set();
18
+ const ordered = [];
19
+ function visit(task, stack) {
20
+ if (visited.has(task.id))
21
+ return;
22
+ if (visiting.has(task.id)) {
23
+ const cycleStart = stack.indexOf(task.id);
24
+ const cycle = [...stack.slice(cycleStart), task.id].join(" -> ");
25
+ throw new Error(`Dependency cycle detected: ${cycle}`);
26
+ }
27
+ visiting.add(task.id);
28
+ for (const depId of task.dependsOn) {
29
+ const dep = byId.get(depId);
30
+ if (!dep)
31
+ throw new Error(`Task "${task.id}" depends on missing task "${depId}"`);
32
+ visit(dep, [...stack, task.id]);
33
+ }
34
+ visiting.delete(task.id);
35
+ visited.add(task.id);
36
+ ordered.push(task);
37
+ }
38
+ for (const task of tasks)
39
+ visit(task, []);
40
+ return ordered;
41
+ }
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import { type OpenCodeClient } from "./orchestrator.js";
4
+ export interface MainDeps {
5
+ fs?: typeof fs;
6
+ client?: OpenCodeClient;
7
+ stdout?: Pick<NodeJS.WritableStream, "write">;
8
+ stderr?: Pick<NodeJS.WritableStream, "write">;
9
+ }
10
+ export declare function main(argv?: string[], deps?: MainDeps): Promise<number>;
package/dist/index.js ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import process from "node:process";
4
+ import { parseArgs } from "node:util";
5
+ import { createOpenCodeClient } from "./client.js";
6
+ import { topologicalOrder } from "./deps.js";
7
+ import { createLogger } from "./log.js";
8
+ import { defaultResultsDir, runOrchestrator } from "./orchestrator.js";
9
+ import { loadTasks } from "./tasks.js";
10
+ export async function main(argv = process.argv.slice(2), deps = {}) {
11
+ const stdout = deps.stdout ?? process.stdout;
12
+ const stderr = deps.stderr ?? process.stderr;
13
+ const parsed = parseArgs({
14
+ args: argv,
15
+ allowPositionals: true,
16
+ options: {
17
+ tasks: { type: "string", short: "t" },
18
+ "max-attempts": { type: "string" },
19
+ cwd: { type: "string" },
20
+ "results-dir": { type: "string" },
21
+ json: { type: "boolean", default: false },
22
+ "dry-run": { type: "boolean", default: false },
23
+ },
24
+ });
25
+ const tasksPath = parsed.values.tasks ?? parsed.positionals[0];
26
+ if (!tasksPath) {
27
+ stderr.write("Missing tasks file. Pass --tasks <path> or a positional path.\n");
28
+ return 2;
29
+ }
30
+ const queueFs = deps.fs ?? fs;
31
+ const cwd = parsed.values.cwd ?? process.cwd();
32
+ const logger = createLogger(Boolean(parsed.values.json), stdout);
33
+ const maxAttempts = parsed.values["max-attempts"] ? Number(parsed.values["max-attempts"]) : undefined;
34
+ const resultsDir = parsed.values["results-dir"] ?? defaultResultsDir(cwd);
35
+ if (maxAttempts !== undefined && (!Number.isInteger(maxAttempts) || maxAttempts < 1)) {
36
+ stderr.write("--max-attempts must be a positive integer.\n");
37
+ return 2;
38
+ }
39
+ try {
40
+ if (parsed.values["dry-run"]) {
41
+ const taskFile = await loadTasks(tasksPath, queueFs);
42
+ const ordered = topologicalOrder(taskFile.tasks);
43
+ logger.event({ type: "dry_run", taskIds: ordered.map((task) => task.id) });
44
+ return 0;
45
+ }
46
+ const client = deps.client ?? (await createOpenCodeClient());
47
+ const summary = await runOrchestrator({
48
+ tasksPath,
49
+ client,
50
+ fs: queueFs,
51
+ log: logger,
52
+ cwd,
53
+ resultsDir,
54
+ maxAttempts,
55
+ });
56
+ return summary.failed > 0 || summary.blocked > 0 ? 1 : 0;
57
+ }
58
+ catch (error) {
59
+ const code = isMissingFile(error) ? 2 : 1;
60
+ stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
61
+ return code;
62
+ }
63
+ }
64
+ function isMissingFile(error) {
65
+ return Boolean(error &&
66
+ typeof error === "object" &&
67
+ (("code" in error && error.code === "ENOENT") ||
68
+ (error instanceof Error && error.message.includes("ENOENT"))));
69
+ }
70
+ if (import.meta.url === `file://${process.argv[1]}`) {
71
+ process.exitCode = await main();
72
+ }
package/dist/log.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ export type LogEvent = {
2
+ type: "task_started";
3
+ taskId: string;
4
+ attempt: number;
5
+ } | {
6
+ type: "task_done";
7
+ taskId: string;
8
+ } | {
9
+ type: "task_failed";
10
+ taskId: string;
11
+ error: string;
12
+ } | {
13
+ type: "task_blocked";
14
+ taskId: string;
15
+ reason: string;
16
+ } | {
17
+ type: "summary";
18
+ summary: unknown;
19
+ } | {
20
+ type: "dry_run";
21
+ taskIds: string[];
22
+ } | {
23
+ type: "server";
24
+ message: string;
25
+ };
26
+ export interface Logger {
27
+ event(event: LogEvent): void;
28
+ }
29
+ export declare function createLogger(json: boolean, stream?: Pick<NodeJS.WritableStream, "write">): Logger;
package/dist/log.js ADDED
@@ -0,0 +1,26 @@
1
+ export function createLogger(json, stream = process.stdout) {
2
+ return {
3
+ event(event) {
4
+ if (json) {
5
+ stream.write(`${JSON.stringify({ time: new Date().toISOString(), ...event })}\n`);
6
+ return;
7
+ }
8
+ stream.write(`${formatHuman(event)}\n`);
9
+ },
10
+ };
11
+ }
12
+ function formatHuman(event) {
13
+ if (event.type === "task_started")
14
+ return `starting ${event.taskId} (attempt ${event.attempt})`;
15
+ if (event.type === "task_done")
16
+ return `done ${event.taskId}`;
17
+ if (event.type === "task_failed")
18
+ return `failed ${event.taskId}: ${event.error}`;
19
+ if (event.type === "task_blocked")
20
+ return `blocked ${event.taskId}: ${event.reason}`;
21
+ if (event.type === "summary")
22
+ return `summary ${JSON.stringify(event.summary)}`;
23
+ if (event.type === "dry_run")
24
+ return event.taskIds.join("\n");
25
+ return event.message;
26
+ }
@@ -0,0 +1,46 @@
1
+ import { type SuccessFs } from "./success.js";
2
+ import { type TaskFs } from "./tasks.js";
3
+ import type { Logger } from "./log.js";
4
+ export declare const DEFAULT_TIMEOUT_MS: number;
5
+ export declare const DEFAULT_MAX_ATTEMPTS = 2;
6
+ export interface OpenCodeClient {
7
+ session: {
8
+ create(input: {
9
+ cwd: string;
10
+ }): Promise<{
11
+ id: string;
12
+ } | string>;
13
+ prompt(sessionId: string, input: {
14
+ prompt: string;
15
+ signal?: AbortSignal;
16
+ }): Promise<unknown>;
17
+ };
18
+ }
19
+ export interface Clock {
20
+ now(): Date;
21
+ setTimeout(callback: () => void, ms: number): ReturnType<typeof setTimeout>;
22
+ clearTimeout(handle: ReturnType<typeof setTimeout>): void;
23
+ }
24
+ export interface OrchestratorFs extends TaskFs, SuccessFs {
25
+ }
26
+ export interface OrchestratorOptions {
27
+ tasksPath: string;
28
+ client: OpenCodeClient;
29
+ fs?: OrchestratorFs;
30
+ clock?: Clock;
31
+ log?: Logger;
32
+ cwd?: string;
33
+ resultsDir?: string;
34
+ maxAttempts?: number;
35
+ timeoutMs?: number;
36
+ }
37
+ export interface Summary {
38
+ total: number;
39
+ pending: number;
40
+ running: number;
41
+ done: number;
42
+ failed: number;
43
+ blocked: number;
44
+ }
45
+ export declare function runOrchestrator(options: OrchestratorOptions): Promise<Summary>;
46
+ export declare function defaultResultsDir(cwd: string): string;
@@ -0,0 +1,119 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { dependencyState } from "./deps.js";
4
+ import { checkSuccess } from "./success.js";
5
+ import { loadTasks, saveTasks } from "./tasks.js";
6
+ export const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
7
+ export const DEFAULT_MAX_ATTEMPTS = 2;
8
+ const defaultClock = {
9
+ now: () => new Date(),
10
+ setTimeout: (callback, ms) => setTimeout(callback, ms),
11
+ clearTimeout: (handle) => clearTimeout(handle),
12
+ };
13
+ export async function runOrchestrator(options) {
14
+ const queueFs = options.fs ?? fs;
15
+ const taskFile = await loadTasks(options.tasksPath, queueFs);
16
+ await saveTasks(options.tasksPath, taskFile, queueFs);
17
+ while (true) {
18
+ let progressed = false;
19
+ for (const task of taskFile.tasks) {
20
+ if (!isRunnable(task, options.maxAttempts))
21
+ continue;
22
+ const state = dependencyState(task, taskFile.tasks);
23
+ if (state === "waiting")
24
+ continue;
25
+ if (state === "blocked") {
26
+ task.status = "blocked";
27
+ task.error = "Dependency failed, blocked, or missing.";
28
+ task.finishedAt = nowIso(options.clock);
29
+ options.log?.event({ type: "task_blocked", taskId: task.id, reason: task.error });
30
+ await saveTasks(options.tasksPath, taskFile, queueFs);
31
+ progressed = true;
32
+ continue;
33
+ }
34
+ await runTask(task, taskFile.tasks, options, queueFs);
35
+ progressed = true;
36
+ }
37
+ if (!progressed)
38
+ break;
39
+ }
40
+ const summary = summarize(taskFile.tasks);
41
+ options.log?.event({ type: "summary", summary });
42
+ return summary;
43
+ }
44
+ async function runTask(task, tasks, options, queueFs) {
45
+ const clock = options.clock ?? defaultClock;
46
+ const cwd = task.cwd ?? options.cwd ?? process.cwd();
47
+ const timeoutMs = task.timeoutMs ?? options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
48
+ const maxAttempts = task.maxAttempts ?? options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
49
+ task.status = "running";
50
+ task.attempts += 1;
51
+ task.startedAt = clock.now().toISOString();
52
+ task.finishedAt = undefined;
53
+ task.error = undefined;
54
+ options.log?.event({ type: "task_started", taskId: task.id, attempt: task.attempts });
55
+ await saveTasks(options.tasksPath, { tasks }, queueFs);
56
+ try {
57
+ const session = await options.client.session.create({ cwd });
58
+ const sessionId = typeof session === "string" ? session : session.id;
59
+ const controller = new AbortController();
60
+ await withTimeout(options.client.session.prompt(sessionId, { prompt: task.prompt, signal: controller.signal }), timeoutMs, clock, controller);
61
+ const success = await checkSuccess(task.success, { fs: queueFs, cwd });
62
+ if (!success)
63
+ throw new Error("Success check failed.");
64
+ task.status = "done";
65
+ task.finishedAt = clock.now().toISOString();
66
+ options.log?.event({ type: "task_done", taskId: task.id });
67
+ }
68
+ catch (error) {
69
+ task.error = error instanceof Error ? error.message : String(error);
70
+ task.status = task.attempts >= maxAttempts ? "failed" : "pending";
71
+ task.finishedAt = clock.now().toISOString();
72
+ options.log?.event({ type: "task_failed", taskId: task.id, error: task.error });
73
+ }
74
+ await saveTasks(options.tasksPath, { tasks }, queueFs);
75
+ }
76
+ function isRunnable(task, globalMaxAttempts = DEFAULT_MAX_ATTEMPTS) {
77
+ if (task.status === "pending")
78
+ return true;
79
+ if (task.status !== "failed")
80
+ return false;
81
+ const maxAttempts = task.maxAttempts ?? globalMaxAttempts;
82
+ return task.attempts < maxAttempts;
83
+ }
84
+ async function withTimeout(promise, timeoutMs, clock, controller) {
85
+ let timeoutHandle;
86
+ const timeout = new Promise((_, reject) => {
87
+ timeoutHandle = clock.setTimeout(() => {
88
+ controller.abort();
89
+ reject(new Error(`Prompt timed out after ${timeoutMs}ms.`));
90
+ }, timeoutMs);
91
+ });
92
+ try {
93
+ return await Promise.race([promise, timeout]);
94
+ }
95
+ finally {
96
+ if (timeoutHandle)
97
+ clock.clearTimeout(timeoutHandle);
98
+ }
99
+ }
100
+ function summarize(tasks) {
101
+ const summary = {
102
+ total: tasks.length,
103
+ pending: 0,
104
+ running: 0,
105
+ done: 0,
106
+ failed: 0,
107
+ blocked: 0,
108
+ };
109
+ for (const task of tasks) {
110
+ summary[task.status] += 1;
111
+ }
112
+ return summary;
113
+ }
114
+ function nowIso(clock = defaultClock) {
115
+ return clock.now().toISOString();
116
+ }
117
+ export function defaultResultsDir(cwd) {
118
+ return path.join(cwd, ".queue", "results");
119
+ }
@@ -0,0 +1,392 @@
1
+ import { z } from "zod";
2
+ export declare const taskStatuses: readonly ["pending", "running", "done", "failed", "blocked"];
3
+ export declare const successCheckSchema: z.ZodDefault<z.ZodDiscriminatedUnion<"strategy", [z.ZodObject<{
4
+ strategy: z.ZodLiteral<"file_exists">;
5
+ path: z.ZodString;
6
+ }, "strip", z.ZodTypeAny, {
7
+ strategy: "file_exists";
8
+ path: string;
9
+ }, {
10
+ strategy: "file_exists";
11
+ path: string;
12
+ }>, z.ZodObject<{
13
+ strategy: z.ZodLiteral<"result_json">;
14
+ path: z.ZodString;
15
+ }, "strip", z.ZodTypeAny, {
16
+ strategy: "result_json";
17
+ path: string;
18
+ }, {
19
+ strategy: "result_json";
20
+ path: string;
21
+ }>, z.ZodObject<{
22
+ strategy: z.ZodLiteral<"default">;
23
+ }, "strip", z.ZodTypeAny, {
24
+ strategy: "default";
25
+ }, {
26
+ strategy: "default";
27
+ }>]>>;
28
+ export declare const taskSchema: z.ZodObject<{
29
+ id: z.ZodString;
30
+ prompt: z.ZodString;
31
+ dependsOn: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
32
+ status: z.ZodDefault<z.ZodEnum<["pending", "running", "done", "failed", "blocked"]>>;
33
+ attempts: z.ZodDefault<z.ZodNumber>;
34
+ maxAttempts: z.ZodOptional<z.ZodNumber>;
35
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
36
+ cwd: z.ZodOptional<z.ZodString>;
37
+ resultPath: z.ZodOptional<z.ZodString>;
38
+ success: z.ZodOptional<z.ZodDefault<z.ZodDiscriminatedUnion<"strategy", [z.ZodObject<{
39
+ strategy: z.ZodLiteral<"file_exists">;
40
+ path: z.ZodString;
41
+ }, "strip", z.ZodTypeAny, {
42
+ strategy: "file_exists";
43
+ path: string;
44
+ }, {
45
+ strategy: "file_exists";
46
+ path: string;
47
+ }>, z.ZodObject<{
48
+ strategy: z.ZodLiteral<"result_json">;
49
+ path: z.ZodString;
50
+ }, "strip", z.ZodTypeAny, {
51
+ strategy: "result_json";
52
+ path: string;
53
+ }, {
54
+ strategy: "result_json";
55
+ path: string;
56
+ }>, z.ZodObject<{
57
+ strategy: z.ZodLiteral<"default">;
58
+ }, "strip", z.ZodTypeAny, {
59
+ strategy: "default";
60
+ }, {
61
+ strategy: "default";
62
+ }>]>>>;
63
+ recoveryNotes: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
64
+ error: z.ZodOptional<z.ZodString>;
65
+ startedAt: z.ZodOptional<z.ZodString>;
66
+ finishedAt: z.ZodOptional<z.ZodString>;
67
+ }, "strip", z.ZodTypeAny, {
68
+ status: "pending" | "running" | "done" | "failed" | "blocked";
69
+ id: string;
70
+ prompt: string;
71
+ dependsOn: string[];
72
+ attempts: number;
73
+ recoveryNotes: string[];
74
+ maxAttempts?: number | undefined;
75
+ timeoutMs?: number | undefined;
76
+ cwd?: string | undefined;
77
+ resultPath?: string | undefined;
78
+ success?: {
79
+ strategy: "file_exists";
80
+ path: string;
81
+ } | {
82
+ strategy: "result_json";
83
+ path: string;
84
+ } | {
85
+ strategy: "default";
86
+ } | undefined;
87
+ error?: string | undefined;
88
+ startedAt?: string | undefined;
89
+ finishedAt?: string | undefined;
90
+ }, {
91
+ id: string;
92
+ prompt: string;
93
+ status?: "pending" | "running" | "done" | "failed" | "blocked" | undefined;
94
+ dependsOn?: string[] | undefined;
95
+ attempts?: number | undefined;
96
+ maxAttempts?: number | undefined;
97
+ timeoutMs?: number | undefined;
98
+ cwd?: string | undefined;
99
+ resultPath?: string | undefined;
100
+ success?: {
101
+ strategy: "file_exists";
102
+ path: string;
103
+ } | {
104
+ strategy: "result_json";
105
+ path: string;
106
+ } | {
107
+ strategy: "default";
108
+ } | undefined;
109
+ recoveryNotes?: string[] | undefined;
110
+ error?: string | undefined;
111
+ startedAt?: string | undefined;
112
+ finishedAt?: string | undefined;
113
+ }>;
114
+ export declare const taskFileSchema: z.ZodUnion<[z.ZodObject<{
115
+ tasks: z.ZodArray<z.ZodObject<{
116
+ id: z.ZodString;
117
+ prompt: z.ZodString;
118
+ dependsOn: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
119
+ status: z.ZodDefault<z.ZodEnum<["pending", "running", "done", "failed", "blocked"]>>;
120
+ attempts: z.ZodDefault<z.ZodNumber>;
121
+ maxAttempts: z.ZodOptional<z.ZodNumber>;
122
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
123
+ cwd: z.ZodOptional<z.ZodString>;
124
+ resultPath: z.ZodOptional<z.ZodString>;
125
+ success: z.ZodOptional<z.ZodDefault<z.ZodDiscriminatedUnion<"strategy", [z.ZodObject<{
126
+ strategy: z.ZodLiteral<"file_exists">;
127
+ path: z.ZodString;
128
+ }, "strip", z.ZodTypeAny, {
129
+ strategy: "file_exists";
130
+ path: string;
131
+ }, {
132
+ strategy: "file_exists";
133
+ path: string;
134
+ }>, z.ZodObject<{
135
+ strategy: z.ZodLiteral<"result_json">;
136
+ path: z.ZodString;
137
+ }, "strip", z.ZodTypeAny, {
138
+ strategy: "result_json";
139
+ path: string;
140
+ }, {
141
+ strategy: "result_json";
142
+ path: string;
143
+ }>, z.ZodObject<{
144
+ strategy: z.ZodLiteral<"default">;
145
+ }, "strip", z.ZodTypeAny, {
146
+ strategy: "default";
147
+ }, {
148
+ strategy: "default";
149
+ }>]>>>;
150
+ recoveryNotes: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
151
+ error: z.ZodOptional<z.ZodString>;
152
+ startedAt: z.ZodOptional<z.ZodString>;
153
+ finishedAt: z.ZodOptional<z.ZodString>;
154
+ }, "strip", z.ZodTypeAny, {
155
+ status: "pending" | "running" | "done" | "failed" | "blocked";
156
+ id: string;
157
+ prompt: string;
158
+ dependsOn: string[];
159
+ attempts: number;
160
+ recoveryNotes: string[];
161
+ maxAttempts?: number | undefined;
162
+ timeoutMs?: number | undefined;
163
+ cwd?: string | undefined;
164
+ resultPath?: string | undefined;
165
+ success?: {
166
+ strategy: "file_exists";
167
+ path: string;
168
+ } | {
169
+ strategy: "result_json";
170
+ path: string;
171
+ } | {
172
+ strategy: "default";
173
+ } | undefined;
174
+ error?: string | undefined;
175
+ startedAt?: string | undefined;
176
+ finishedAt?: string | undefined;
177
+ }, {
178
+ id: string;
179
+ prompt: string;
180
+ status?: "pending" | "running" | "done" | "failed" | "blocked" | undefined;
181
+ dependsOn?: string[] | undefined;
182
+ attempts?: number | undefined;
183
+ maxAttempts?: number | undefined;
184
+ timeoutMs?: number | undefined;
185
+ cwd?: string | undefined;
186
+ resultPath?: string | undefined;
187
+ success?: {
188
+ strategy: "file_exists";
189
+ path: string;
190
+ } | {
191
+ strategy: "result_json";
192
+ path: string;
193
+ } | {
194
+ strategy: "default";
195
+ } | undefined;
196
+ recoveryNotes?: string[] | undefined;
197
+ error?: string | undefined;
198
+ startedAt?: string | undefined;
199
+ finishedAt?: string | undefined;
200
+ }>, "many">;
201
+ }, "strip", z.ZodTypeAny, {
202
+ tasks: {
203
+ status: "pending" | "running" | "done" | "failed" | "blocked";
204
+ id: string;
205
+ prompt: string;
206
+ dependsOn: string[];
207
+ attempts: number;
208
+ recoveryNotes: string[];
209
+ maxAttempts?: number | undefined;
210
+ timeoutMs?: number | undefined;
211
+ cwd?: string | undefined;
212
+ resultPath?: string | undefined;
213
+ success?: {
214
+ strategy: "file_exists";
215
+ path: string;
216
+ } | {
217
+ strategy: "result_json";
218
+ path: string;
219
+ } | {
220
+ strategy: "default";
221
+ } | undefined;
222
+ error?: string | undefined;
223
+ startedAt?: string | undefined;
224
+ finishedAt?: string | undefined;
225
+ }[];
226
+ }, {
227
+ tasks: {
228
+ id: string;
229
+ prompt: string;
230
+ status?: "pending" | "running" | "done" | "failed" | "blocked" | undefined;
231
+ dependsOn?: string[] | undefined;
232
+ attempts?: number | undefined;
233
+ maxAttempts?: number | undefined;
234
+ timeoutMs?: number | undefined;
235
+ cwd?: string | undefined;
236
+ resultPath?: string | undefined;
237
+ success?: {
238
+ strategy: "file_exists";
239
+ path: string;
240
+ } | {
241
+ strategy: "result_json";
242
+ path: string;
243
+ } | {
244
+ strategy: "default";
245
+ } | undefined;
246
+ recoveryNotes?: string[] | undefined;
247
+ error?: string | undefined;
248
+ startedAt?: string | undefined;
249
+ finishedAt?: string | undefined;
250
+ }[];
251
+ }>, z.ZodEffects<z.ZodArray<z.ZodObject<{
252
+ id: z.ZodString;
253
+ prompt: z.ZodString;
254
+ dependsOn: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
255
+ status: z.ZodDefault<z.ZodEnum<["pending", "running", "done", "failed", "blocked"]>>;
256
+ attempts: z.ZodDefault<z.ZodNumber>;
257
+ maxAttempts: z.ZodOptional<z.ZodNumber>;
258
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
259
+ cwd: z.ZodOptional<z.ZodString>;
260
+ resultPath: z.ZodOptional<z.ZodString>;
261
+ success: z.ZodOptional<z.ZodDefault<z.ZodDiscriminatedUnion<"strategy", [z.ZodObject<{
262
+ strategy: z.ZodLiteral<"file_exists">;
263
+ path: z.ZodString;
264
+ }, "strip", z.ZodTypeAny, {
265
+ strategy: "file_exists";
266
+ path: string;
267
+ }, {
268
+ strategy: "file_exists";
269
+ path: string;
270
+ }>, z.ZodObject<{
271
+ strategy: z.ZodLiteral<"result_json">;
272
+ path: z.ZodString;
273
+ }, "strip", z.ZodTypeAny, {
274
+ strategy: "result_json";
275
+ path: string;
276
+ }, {
277
+ strategy: "result_json";
278
+ path: string;
279
+ }>, z.ZodObject<{
280
+ strategy: z.ZodLiteral<"default">;
281
+ }, "strip", z.ZodTypeAny, {
282
+ strategy: "default";
283
+ }, {
284
+ strategy: "default";
285
+ }>]>>>;
286
+ recoveryNotes: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
287
+ error: z.ZodOptional<z.ZodString>;
288
+ startedAt: z.ZodOptional<z.ZodString>;
289
+ finishedAt: z.ZodOptional<z.ZodString>;
290
+ }, "strip", z.ZodTypeAny, {
291
+ status: "pending" | "running" | "done" | "failed" | "blocked";
292
+ id: string;
293
+ prompt: string;
294
+ dependsOn: string[];
295
+ attempts: number;
296
+ recoveryNotes: string[];
297
+ maxAttempts?: number | undefined;
298
+ timeoutMs?: number | undefined;
299
+ cwd?: string | undefined;
300
+ resultPath?: string | undefined;
301
+ success?: {
302
+ strategy: "file_exists";
303
+ path: string;
304
+ } | {
305
+ strategy: "result_json";
306
+ path: string;
307
+ } | {
308
+ strategy: "default";
309
+ } | undefined;
310
+ error?: string | undefined;
311
+ startedAt?: string | undefined;
312
+ finishedAt?: string | undefined;
313
+ }, {
314
+ id: string;
315
+ prompt: string;
316
+ status?: "pending" | "running" | "done" | "failed" | "blocked" | undefined;
317
+ dependsOn?: string[] | undefined;
318
+ attempts?: number | undefined;
319
+ maxAttempts?: number | undefined;
320
+ timeoutMs?: number | undefined;
321
+ cwd?: string | undefined;
322
+ resultPath?: string | undefined;
323
+ success?: {
324
+ strategy: "file_exists";
325
+ path: string;
326
+ } | {
327
+ strategy: "result_json";
328
+ path: string;
329
+ } | {
330
+ strategy: "default";
331
+ } | undefined;
332
+ recoveryNotes?: string[] | undefined;
333
+ error?: string | undefined;
334
+ startedAt?: string | undefined;
335
+ finishedAt?: string | undefined;
336
+ }>, "many">, {
337
+ tasks: {
338
+ status: "pending" | "running" | "done" | "failed" | "blocked";
339
+ id: string;
340
+ prompt: string;
341
+ dependsOn: string[];
342
+ attempts: number;
343
+ recoveryNotes: string[];
344
+ maxAttempts?: number | undefined;
345
+ timeoutMs?: number | undefined;
346
+ cwd?: string | undefined;
347
+ resultPath?: string | undefined;
348
+ success?: {
349
+ strategy: "file_exists";
350
+ path: string;
351
+ } | {
352
+ strategy: "result_json";
353
+ path: string;
354
+ } | {
355
+ strategy: "default";
356
+ } | undefined;
357
+ error?: string | undefined;
358
+ startedAt?: string | undefined;
359
+ finishedAt?: string | undefined;
360
+ }[];
361
+ }, {
362
+ id: string;
363
+ prompt: string;
364
+ status?: "pending" | "running" | "done" | "failed" | "blocked" | undefined;
365
+ dependsOn?: string[] | undefined;
366
+ attempts?: number | undefined;
367
+ maxAttempts?: number | undefined;
368
+ timeoutMs?: number | undefined;
369
+ cwd?: string | undefined;
370
+ resultPath?: string | undefined;
371
+ success?: {
372
+ strategy: "file_exists";
373
+ path: string;
374
+ } | {
375
+ strategy: "result_json";
376
+ path: string;
377
+ } | {
378
+ strategy: "default";
379
+ } | undefined;
380
+ recoveryNotes?: string[] | undefined;
381
+ error?: string | undefined;
382
+ startedAt?: string | undefined;
383
+ finishedAt?: string | undefined;
384
+ }[]>]>;
385
+ export type TaskStatus = (typeof taskStatuses)[number];
386
+ export type SuccessCheck = z.input<typeof successCheckSchema>;
387
+ export type Task = z.infer<typeof taskSchema>;
388
+ export type TaskFile = {
389
+ tasks: Task[];
390
+ };
391
+ export declare function parseTaskFile(value: unknown): TaskFile;
392
+ export declare function validateTaskGraph(tasks: Task[]): void;
package/dist/schema.js ADDED
@@ -0,0 +1,66 @@
1
+ import { z } from "zod";
2
+ export const taskStatuses = ["pending", "running", "done", "failed", "blocked"];
3
+ export const successCheckSchema = z
4
+ .discriminatedUnion("strategy", [
5
+ z.object({
6
+ strategy: z.literal("file_exists"),
7
+ path: z.string().min(1),
8
+ }),
9
+ z.object({
10
+ strategy: z.literal("result_json"),
11
+ path: z.string().min(1),
12
+ }),
13
+ z.object({
14
+ strategy: z.literal("default"),
15
+ }),
16
+ ])
17
+ .default({ strategy: "default" });
18
+ export const taskSchema = z.object({
19
+ id: z.string().min(1),
20
+ prompt: z.string().min(1),
21
+ dependsOn: z.array(z.string().min(1)).default([]),
22
+ status: z.enum(taskStatuses).default("pending"),
23
+ attempts: z.number().int().min(0).default(0),
24
+ maxAttempts: z.number().int().min(1).optional(),
25
+ timeoutMs: z.number().int().positive().optional(),
26
+ cwd: z.string().min(1).optional(),
27
+ resultPath: z.string().min(1).optional(),
28
+ success: successCheckSchema.optional(),
29
+ recoveryNotes: z.array(z.string()).default([]),
30
+ error: z.string().optional(),
31
+ startedAt: z.string().datetime().optional(),
32
+ finishedAt: z.string().datetime().optional(),
33
+ });
34
+ export const taskFileSchema = z.union([
35
+ z.object({
36
+ tasks: z.array(taskSchema),
37
+ }),
38
+ z.array(taskSchema).transform((tasks) => ({ tasks })),
39
+ ]);
40
+ export function parseTaskFile(value) {
41
+ const parsed = taskFileSchema.parse(value);
42
+ validateTaskGraph(parsed.tasks);
43
+ return parsed;
44
+ }
45
+ export function validateTaskGraph(tasks) {
46
+ const seen = new Set();
47
+ const duplicates = new Set();
48
+ for (const task of tasks) {
49
+ if (seen.has(task.id))
50
+ duplicates.add(task.id);
51
+ seen.add(task.id);
52
+ }
53
+ if (duplicates.size > 0) {
54
+ throw new Error(`Duplicate task id: ${Array.from(duplicates).join(", ")}`);
55
+ }
56
+ for (const task of tasks) {
57
+ if (task.dependsOn.includes(task.id)) {
58
+ throw new Error(`Task "${task.id}" cannot depend on itself`);
59
+ }
60
+ for (const dep of task.dependsOn) {
61
+ if (!seen.has(dep)) {
62
+ throw new Error(`Task "${task.id}" depends on missing task "${dep}"`);
63
+ }
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,18 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+ export interface StartServerOptions {
3
+ command?: string;
4
+ args?: string[];
5
+ port: number;
6
+ host?: string;
7
+ timeoutMs?: number;
8
+ pollMs?: number;
9
+ spawnProcess?: typeof spawn;
10
+ portOpen?: (host: string, port: number) => Promise<boolean>;
11
+ sleep?: (ms: number) => Promise<void>;
12
+ now?: () => number;
13
+ }
14
+ export interface StopServerOptions {
15
+ graceMs?: number;
16
+ }
17
+ export declare function startServer(options: StartServerOptions): Promise<ChildProcess>;
18
+ export declare function stopServer(child: Pick<ChildProcess, "kill" | "killed" | "exitCode" | "once">, options?: StopServerOptions): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,60 @@
1
+ import { spawn } from "node:child_process";
2
+ import net from "node:net";
3
+ export async function startServer(options) {
4
+ const command = options.command ?? "opencode";
5
+ const args = options.args ?? ["serve", "--port", String(options.port)];
6
+ const host = options.host ?? "127.0.0.1";
7
+ const timeoutMs = options.timeoutMs ?? 30_000;
8
+ const pollMs = options.pollMs ?? 100;
9
+ const portOpen = options.portOpen ?? isPortOpen;
10
+ const wait = options.sleep ?? sleep;
11
+ const now = options.now ?? Date.now;
12
+ const child = (options.spawnProcess ?? spawn)(command, args, {
13
+ stdio: "ignore",
14
+ detached: false,
15
+ });
16
+ const startedAt = now();
17
+ while (now() - startedAt < timeoutMs) {
18
+ if (await portOpen(host, options.port))
19
+ return child;
20
+ if (child.exitCode !== null)
21
+ throw new Error(`Server exited before port ${options.port} opened.`);
22
+ await wait(pollMs);
23
+ }
24
+ child.kill("SIGTERM");
25
+ throw new Error(`Timed out waiting for ${host}:${options.port}.`);
26
+ }
27
+ export async function stopServer(child, options = {}) {
28
+ const graceMs = options.graceMs ?? 5_000;
29
+ if (child.exitCode !== null)
30
+ return;
31
+ child.kill("SIGTERM");
32
+ await new Promise((resolve) => {
33
+ const timeout = setTimeout(() => {
34
+ if (child.exitCode === null)
35
+ child.kill("SIGKILL");
36
+ resolve();
37
+ }, graceMs);
38
+ child.once("exit", () => {
39
+ clearTimeout(timeout);
40
+ resolve();
41
+ });
42
+ });
43
+ }
44
+ async function isPortOpen(host, port) {
45
+ return new Promise((resolve) => {
46
+ const socket = net.createConnection({ host, port });
47
+ socket.once("connect", () => {
48
+ socket.destroy();
49
+ resolve(true);
50
+ });
51
+ socket.once("error", () => resolve(false));
52
+ socket.setTimeout(250, () => {
53
+ socket.destroy();
54
+ resolve(false);
55
+ });
56
+ });
57
+ }
58
+ function sleep(ms) {
59
+ return new Promise((resolve) => setTimeout(resolve, ms));
60
+ }
@@ -0,0 +1,10 @@
1
+ import type { SuccessCheck } from "./schema.js";
2
+ export interface SuccessFs {
3
+ readFile(path: string, encoding: "utf8"): Promise<string>;
4
+ access(path: string): Promise<void>;
5
+ }
6
+ export interface SuccessOptions {
7
+ fs: SuccessFs;
8
+ cwd: string;
9
+ }
10
+ export declare function checkSuccess(check: SuccessCheck | undefined, options: SuccessOptions): Promise<boolean>;
@@ -0,0 +1,28 @@
1
+ import path from "node:path";
2
+ export async function checkSuccess(check, options) {
3
+ if (!check || check.strategy === "default")
4
+ return true;
5
+ if (check.strategy === "file_exists") {
6
+ try {
7
+ await options.fs.access(resolvePath(options.cwd, check.path));
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ if (check.strategy === "result_json") {
15
+ try {
16
+ const raw = await options.fs.readFile(resolvePath(options.cwd, check.path), "utf8");
17
+ const parsed = JSON.parse(raw);
18
+ return parsed.status === "ok";
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ throw new Error(`Unknown success strategy: ${check.strategy}`);
25
+ }
26
+ function resolvePath(cwd, candidate) {
27
+ return path.isAbsolute(candidate) ? candidate : path.join(cwd, candidate);
28
+ }
@@ -0,0 +1,11 @@
1
+ import { type TaskFile } from "./schema.js";
2
+ export interface TaskFs {
3
+ readFile(path: string, encoding: "utf8"): Promise<string>;
4
+ writeFile(path: string, data: string, encoding: "utf8"): Promise<void>;
5
+ rename(oldPath: string, newPath: string): Promise<void>;
6
+ mkdir(path: string, options: {
7
+ recursive: true;
8
+ }): Promise<unknown>;
9
+ }
10
+ export declare function loadTasks(filePath: string, fs: TaskFs): Promise<TaskFile>;
11
+ export declare function saveTasks(filePath: string, taskFile: TaskFile, fs: TaskFs): Promise<void>;
package/dist/tasks.js ADDED
@@ -0,0 +1,30 @@
1
+ import path from "node:path";
2
+ import { parseTaskFile } from "./schema.js";
3
+ export async function loadTasks(filePath, fs) {
4
+ const raw = await fs.readFile(filePath, "utf8");
5
+ const parsed = parseTaskFile(JSON.parse(raw));
6
+ let recovered = false;
7
+ const tasks = parsed.tasks.map((task) => {
8
+ if (task.status !== "running")
9
+ return task;
10
+ recovered = true;
11
+ return {
12
+ ...task,
13
+ status: "pending",
14
+ recoveryNotes: [
15
+ ...task.recoveryNotes,
16
+ `Recovered running task at ${new Date().toISOString()}; reset to pending.`,
17
+ ],
18
+ startedAt: undefined,
19
+ };
20
+ });
21
+ return recovered ? { tasks } : parsed;
22
+ }
23
+ export async function saveTasks(filePath, taskFile, fs) {
24
+ parseTaskFile(taskFile);
25
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
26
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
27
+ const payload = `${JSON.stringify(taskFile, null, 2)}\n`;
28
+ await fs.writeFile(tempPath, payload, "utf8");
29
+ await fs.rename(tempPath, filePath);
30
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "td-barrage",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript queue runner that loads a JSON task file, resolves dependencies, runs tasks through the OpenCode SDK, and resumes interrupted work.",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20.11"
8
+ },
9
+ "bin": {
10
+ "td-barrage": "dist/index.js"
11
+ },
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/josh-theory/td-barrage.git"
21
+ },
22
+ "license": "MIT",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "start": "node dist/index.js",
29
+ "dev": "tsx src/index.ts",
30
+ "test": "vitest run",
31
+ "test:watch": "vitest",
32
+ "test:integration": "RUN_INTEGRATION=1 vitest run test/integration.test.ts",
33
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
34
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
35
+ },
36
+ "dependencies": {
37
+ "@opencode-ai/sdk": "^1.15.10",
38
+ "zod": "^3.25.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.11.0",
42
+ "tsx": "^4.19.0",
43
+ "typescript": "^5.8.0",
44
+ "vitest": "^3.1.0"
45
+ }
46
+ }