@oneharness/sdk
v0.21.2
Published
Typed Node.js SDK for oneharness
Readme
@oneharness/sdk
Typed Node.js access to the oneharness engine. The SDK launches its exact-version packaged CLI dependency and validates every response and stream envelope with named Zod schemas generated from the Rust wire types. The corresponding TypeScript declarations come from the same Rust JSON Schema bundle.
import { OneHarness, RunReportSchema, type RunReport } from "@oneharness/sdk";
const oneharness = new OneHarness();
const report = await oneharness.run({ prompt: "Summarize this repository", harnesses: ["codex"], events: true });
const checked: RunReport = RunReportSchema.parse(report);
console.log(checked.results[0]?.text, checked.results[0]?.usage.input_tokens);The client covers every verb the CLI exposes, so nothing needs a hand-built command line: run, runMock, runStream, list, detect, config, sync, init, usage, gate, mock, interrupt, history, historyList, historyWatch, historyClear, historyMigrate, historyReindex, and historyPointers. That coverage is gated, not asserted here. runMock uses the deterministic responder shipped in the CLI; see Testing patterns. Both streaming methods return async iterators:
for await (const envelope of oneharness.runStream({
prompt: "Inspect this repository",
harnesses: ["codex"],
})) {
if (envelope.type === "event" && envelope.event.name === "shell") break;
}
for await (const envelope of oneharness.historyWatch({
labels: { graph: "release" },
after: lastHistoryId,
})) {
console.log(envelope.record.history_id, envelope.record.status);
}Every JSON-returning method passes --format json --compact itself, so the CLI's own default (a human-readable text view) never reaches the SDK. A run over several harnesses is a fallback chain by default — the first candidate that can run does, and the report's fallback block says which; pass runMode: "parallel" to run them all at once.
Breaking or returning from either iterator terminates its oneharness subprocess. Every line is validated before it is yielded; malformed or unknown envelope variants fail the iterator. Additive fields within known output envelopes are accepted and preserved.
null usage fields mean the harness did not report the value; zero remains a real measured zero. String-valued harness/model/event identifiers should be treated as open sets for forward compatibility. history and historyWatch raise the exported HistoryNotFoundError when a session, record, or watch cursor cannot be resolved.
Every contract the CLI reads or prints is exported as a Zod schema named after its generated TypeScript type plus Schema (RunReport → RunReportSchema), generated from the same Rust wire types. Each schema's z.infer type is compile-time checked against its generated TypeScript type.
Output objects accept and preserve unknown fields. That deliberate loose-object behavior lets an older SDK validate a newer additive CLI response without erasing fields before an application can inspect them. Known fields are still validated recursively. The input schemas RunOptionsSchema, HistoryLookupSchema, HistoryListOptionsSchema, and HistoryWatchOptionsSchema are deliberately strict instead: unknown input keys are rejected because this SDK version cannot forward an option it does not understand, which also catches misspellings. Every method validates its input before spawning the CLI.
Nullable CLI fields remain required object keys: the generated response schemas model Rust's serialization contract, so an unavailable value is null, while an omitted guaranteed field is malformed. Optional RunOptions, HistoryLookup, and HistoryListOptions fields may be absent or explicitly undefined; every HistoryListOptions field is optional, so historyList() and historyList({}) both list the default store.
Continuation passes the prior result's native session id with a new user message:
const first = await oneharness.run({ prompt: "Inspect the bug", harnesses: ["codex"] });
const next = await oneharness.run({
prompt: "Now propose the smallest fix",
harnesses: ["codex"],
resume: first.results[0]?.session_id ?? undefined,
});Standardized history records and session summaries use their generated schemas too:
import { HistoryListSchema, HistoryRecordSchema } from "@oneharness/sdk";
const records = await oneharness.history({ last: true });
const sessions = await oneharness.historyList({ allProjects: true });
HistoryRecordSchema.parse(records[0]);
HistoryListSchema.parse(sessions);HistoryLookupSchema states the selector rule itself: a lookup is a union of the only two ways to select a session, so it accepts last: true or a non-empty session and rejects a lookup that selects neither. history({}), history({ last: false }), and history({ session: "" }) fail validation rather than reaching the CLI, and HistoryLookup rejects them at compile time too.
last: true has priority over a name, so history({ session: "old", last: true }) returns the most recent session and history({ session: "old", last: false }) returns old. The two cases overlap deliberately — the union resolves last: true to its last-session variant first — which keeps last an ordinary boolean beside a named session, so a caller can pass one straight through.
