@oneharness/sdk 0.6.13 → 0.6.15

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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/dist/generated/capabilities.d.ts +1010 -0
  3. package/dist/generated/capabilities.js +1162 -0
  4. package/dist/generated/config-options.d.ts +8 -0
  5. package/dist/generated/config-options.js +2 -0
  6. package/dist/generated/config-report.d.ts +283 -0
  7. package/dist/generated/config-report.js +2 -0
  8. package/dist/generated/detect-options.d.ts +19 -0
  9. package/dist/generated/detect-options.js +2 -0
  10. package/dist/generated/detection.d.ts +6 -0
  11. package/dist/generated/gate-options.d.ts +17 -0
  12. package/dist/generated/gate-options.js +2 -0
  13. package/dist/generated/history-clear-options.d.ts +15 -0
  14. package/dist/generated/history-clear-options.js +2 -0
  15. package/dist/generated/history-clear-report.d.ts +39 -0
  16. package/dist/generated/history-clear-report.js +2 -0
  17. package/dist/generated/history-list-options.d.ts +13 -0
  18. package/dist/generated/history-lookup.d.ts +26 -0
  19. package/dist/generated/history-migrate-options.d.ts +8 -0
  20. package/dist/generated/history-migrate-options.js +2 -0
  21. package/dist/generated/history-migrate-report.d.ts +31 -0
  22. package/dist/generated/history-migrate-report.js +2 -0
  23. package/dist/generated/history-watch-options.d.ts +13 -0
  24. package/dist/generated/init-options.d.ts +11 -0
  25. package/dist/generated/init-options.js +2 -0
  26. package/dist/generated/interrupt-options.d.ts +12 -0
  27. package/dist/generated/interrupt-options.js +2 -0
  28. package/dist/generated/interrupt-response.d.ts +40 -0
  29. package/dist/generated/interrupt-response.js +2 -0
  30. package/dist/generated/mock-options.d.ts +12 -0
  31. package/dist/generated/mock-options.js +2 -0
  32. package/dist/generated/options.d.ts +108 -0
  33. package/dist/generated/registry.d.ts +14 -4
  34. package/dist/generated/schemas.json +3245 -15
  35. package/dist/generated/sync-options.d.ts +14 -0
  36. package/dist/generated/sync-options.js +2 -0
  37. package/dist/generated/sync-report.d.ts +64 -0
  38. package/dist/generated/sync-report.js +2 -0
  39. package/dist/generated/usage-options.d.ts +15 -0
  40. package/dist/generated/usage-options.js +2 -0
  41. package/dist/generated/usage-report.d.ts +241 -0
  42. package/dist/generated/usage-report.js +2 -0
  43. package/dist/generated/zod.d.ts +70 -2
  44. package/dist/generated/zod.js +444 -0
  45. package/dist/index.d.ts +91 -1
  46. package/dist/index.js +271 -102
  47. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -1,26 +1,60 @@
1
+ import type { ConfigOptions } from "./generated/config-options.js";
2
+ import type { ConfigReport } from "./generated/config-report.js";
1
3
  import type { RunReport } from "./generated/contracts.js";
4
+ import type { DetectOptions } from "./generated/detect-options.js";
2
5
  import type { DetectInfo } from "./generated/detection.js";
6
+ import type { GateOptions } from "./generated/gate-options.js";
3
7
  import type { HistoryRecord } from "./generated/history.js";
8
+ import type { HistoryClearOptions } from "./generated/history-clear-options.js";
9
+ import type { HistoryClearReport } from "./generated/history-clear-report.js";
4
10
  import type { HistorySessionSummary } from "./generated/history-list.js";
5
11
  import type { HistoryListOptions } from "./generated/history-list-options.js";
6
12
  import type { HistoryLookup } from "./generated/history-lookup.js";
13
+ import type { HistoryMigrateOptions } from "./generated/history-migrate-options.js";
14
+ import type { HistoryMigrateReport } from "./generated/history-migrate-report.js";
7
15
  import type { HistoryStreamEnvelope } from "./generated/history-stream-envelope.js";
8
16
  import type { HistoryWatchOptions } from "./generated/history-watch-options.js";
17
+ import type { InitOptions } from "./generated/init-options.js";
18
+ import type { InterruptOptions } from "./generated/interrupt-options.js";
19
+ import type { InterruptResponse } from "./generated/interrupt-response.js";
20
+ import type { MockOptions } from "./generated/mock-options.js";
9
21
  import type { RunOptions } from "./generated/options.js";
10
22
  import type { HarnessInfo } from "./generated/registry.js";
11
23
  import type { RunStreamEnvelope } from "./generated/run-stream-envelope.js";
24
+ import type { SyncOptions } from "./generated/sync-options.js";
25
+ import type { SyncReport } from "./generated/sync-report.js";
26
+ import type { UsageOptions } from "./generated/usage-options.js";
27
+ import type { UsageReport } from "./generated/usage-report.js";
28
+ export type { Capability, CapabilityMethod, FlagKind, OptionBinding, StdoutShape, } from "./generated/capabilities.js";
29
+ export { CAPABILITIES } from "./generated/capabilities.js";
30
+ export type { ConfigOptions } from "./generated/config-options.js";
31
+ export type { ConfigReport } from "./generated/config-report.js";
12
32
  export type { ActionEvent, BatchReport, FailureKind, FallbackReport, FallThrough, OutputFormat, RunReport, RunResult, SessionReport, Status, Usage, } from "./generated/contracts.js";
33
+ export type { DetectOptions } from "./generated/detect-options.js";
13
34
  export type { DetectInfo, DetectReport } from "./generated/detection.js";
35
+ export type { GateOptions } from "./generated/gate-options.js";
14
36
  export type { HistoryRecord } from "./generated/history.js";
37
+ export type { HistoryClearOptions } from "./generated/history-clear-options.js";
38
+ export type { HistoryClearDryRun, HistoryClearRemoved, HistoryClearReport, } from "./generated/history-clear-report.js";
15
39
  export type { HistoryLine } from "./generated/history-line.js";
16
40
  export type { HistoryList, HistorySessionSummary, } from "./generated/history-list.js";
17
41
  export type { HistoryListOptions } from "./generated/history-list-options.js";
18
42
  export type { HistoryLookup, HistoryLookupByLast, HistoryLookupBySession, } from "./generated/history-lookup.js";
43
+ export type { HistoryMigrateOptions } from "./generated/history-migrate-options.js";
44
+ export type { HistoryMigrateReport } from "./generated/history-migrate-report.js";
19
45
  export type { HistoryRecords } from "./generated/history-records.js";
20
46
  export type { HistoryStreamEnvelope } from "./generated/history-stream-envelope.js";
21
47
  export type { HistoryWatchOptions } from "./generated/history-watch-options.js";
48
+ export type { InitOptions } from "./generated/init-options.js";
49
+ export type { InterruptOptions } from "./generated/interrupt-options.js";
50
+ export type { InterruptResponse } from "./generated/interrupt-response.js";
51
+ export type { MockOptions } from "./generated/mock-options.js";
22
52
  export type { PermissionMode, RunOptions } from "./generated/options.js";
23
53
  export type { RunStreamEnvelope } from "./generated/run-stream-envelope.js";
54
+ export type { SyncOptions } from "./generated/sync-options.js";
55
+ export type { SyncReport } from "./generated/sync-report.js";
56
+ export type { UsageOptions } from "./generated/usage-options.js";
57
+ export type { UsageReport } from "./generated/usage-report.js";
24
58
  export type Detection = DetectInfo;
25
59
  export type { HarnessInfo, ListReport, ModeInfo, } from "./generated/registry.js";
26
60
  export * from "./generated/zod.js";
@@ -49,12 +83,68 @@ export declare class HistoryNotFoundError extends OneHarnessProcessError {
49
83
  export declare class OneHarness {
50
84
  private readonly options;
51
85
  constructor(options?: OneHarnessOptions);
86
+ /**
87
+ * Validate options, render the capability's argv, invoke, validate stdout.
88
+ *
89
+ * Every JSON-returning method is this call with two schemas, so none of them
90
+ * can drift from the manifest in how it builds a command or in what it
91
+ * promises back.
92
+ */
93
+ private call;
52
94
  run(options: RunOptions): Promise<RunReport>;
53
95
  runMock(harness: string, options: RunOptions, script?: MockHarnessScript): Promise<RunReport>;
54
96
  runStream(options: RunOptions): AsyncGenerator<RunStreamEnvelope>;
55
97
  list(): Promise<HarnessInfo[]>;
56
- detect(harnesses?: readonly string[]): Promise<Detection[]>;
98
+ /**
99
+ * Probe harness binaries.
100
+ *
101
+ * The array overload is the long-standing shape and still means "these
102
+ * harnesses"; the options object is what reaches the rest of the verb's
103
+ * flags (`--all`, `--exclude`, `--bin`, the config layer).
104
+ */
105
+ detect(harnessesOrOptions?: readonly string[] | DetectOptions): Promise<Detection[]>;
106
+ config(options?: ConfigOptions): Promise<ConfigReport>;
107
+ sync(options?: SyncOptions): Promise<SyncReport>;
108
+ usage(options?: UsageOptions): Promise<UsageReport>;
109
+ /**
110
+ * Write a starter `oneharness.toml` and return where it landed.
111
+ *
112
+ * The one verb whose stdout is a human confirmation line rather than a JSON
113
+ * contract — the deliverable is the file — so the return is the resolved
114
+ * path the caller asked for, not a parsed document.
115
+ */
116
+ init(options?: InitOptions): Promise<string>;
117
+ /**
118
+ * Run the pre-tool gate over one harness hook event, as that harness's own
119
+ * runtime would.
120
+ *
121
+ * Returns the harness's native deny verdict, or `null` when the call is
122
+ * allowed through — which the CLI says by printing nothing at all, so an
123
+ * empty answer is the allow rather than a missing one.
124
+ */
125
+ gate(options: GateOptions): Promise<string | null>;
126
+ /** The read-write sibling of {@link gate}, driven by a rules file. */
127
+ mock(options: MockOptions): Promise<string | null>;
128
+ /**
129
+ * Abort the in-flight turn of a `run --control` session, optionally
130
+ * redirecting it with `input`.
131
+ *
132
+ * A refusal is an answer, not a throw: the response says whether the abort
133
+ * was served and, when it was not, why — which is what a supervisor branches
134
+ * on. So a non-zero exit still yields the frame.
135
+ */
136
+ interrupt(options: InterruptOptions): Promise<InterruptResponse>;
57
137
  history(lookup: HistoryLookup): Promise<HistoryRecord[]>;
58
138
  historyList(options?: HistoryListOptions): Promise<HistorySessionSummary[]>;
139
+ /**
140
+ * Remove history sessions.
141
+ *
142
+ * A dry run until `yes` is set, and the two answers are different documents:
143
+ * `dry_run` discriminates them, so a caller reads `removed` only from a run
144
+ * that removed something.
145
+ */
146
+ historyClear(options?: HistoryClearOptions): Promise<HistoryClearReport>;
147
+ /** Rewrite legacy session files to the current record version. */
148
+ historyMigrate(options?: HistoryMigrateOptions): Promise<HistoryMigrateReport>;
59
149
  historyWatch(options?: HistoryWatchOptions): AsyncGenerator<HistoryStreamEnvelope>;
60
150
  }
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createRequire } from "node:module";
3
3
  import { createInterface } from "node:readline";
4
- import { DetectReportSchema, HistoryListOptionsSchema, HistoryListSchema, HistoryLookupSchema, HistoryRecordsSchema, HistoryStreamEnvelopeSchema, HistoryWatchOptionsSchema, ListReportSchema, RunOptionsSchema, RunReportSchema, RunStreamEnvelopeSchema, } from "./generated/zod.js";
4
+ import { CAPABILITIES, } from "./generated/capabilities.js";
5
+ import { ConfigOptionsSchema, ConfigReportSchema, DetectOptionsSchema, DetectReportSchema, GateOptionsSchema, HistoryClearOptionsSchema, HistoryClearReportSchema, HistoryListOptionsSchema, HistoryListSchema, HistoryLookupSchema, HistoryMigrateOptionsSchema, HistoryMigrateReportSchema, HistoryRecordsSchema, HistoryStreamEnvelopeSchema, HistoryWatchOptionsSchema, InitOptionsSchema, InterruptOptionsSchema, InterruptResponseSchema, ListReportSchema, MockOptionsSchema, RunOptionsSchema, RunReportSchema, RunStreamEnvelopeSchema, SyncOptionsSchema, SyncReportSchema, UsageOptionsSchema, UsageReportSchema, } from "./generated/zod.js";
6
+ export { CAPABILITIES } from "./generated/capabilities.js";
5
7
  export * from "./generated/zod.js";
6
8
  /** A non-zero exit from the oneharness subprocess. */
7
9
  export class OneHarnessProcessError extends Error {
@@ -75,6 +77,43 @@ async function invokeWith(options, args, cwd, acceptJsonOnNonzero = false) {
75
77
  });
76
78
  });
77
79
  }
80
+ /**
81
+ * Invoke a capability whose stdout is text rather than a JSON contract, and
82
+ * return it verbatim.
83
+ *
84
+ * `init` prints a confirmation line; `gate` and `mock` answer with a harness's
85
+ * own native verdict, or with nothing when the call is allowed. None of the
86
+ * three has a document to validate, so parsing one would be inventing a shape
87
+ * the CLI does not promise.
88
+ *
89
+ * `stdin` carries the hook event for the two gate verbs, and closing it is what
90
+ * tells them the event is complete.
91
+ */
92
+ async function invokeText(options, args, stdin) {
93
+ const bin = executable(options);
94
+ return await new Promise((resolve, reject) => {
95
+ const child = spawn(bin.command, [...bin.prefix, ...args], {
96
+ env: { ...process.env, ...options.env },
97
+ windowsHide: true,
98
+ });
99
+ let stdout = "";
100
+ let stderr = "";
101
+ child.stdout
102
+ .setEncoding("utf8")
103
+ .on("data", (chunk) => (stdout += chunk));
104
+ child.stderr
105
+ .setEncoding("utf8")
106
+ .on("data", (chunk) => (stderr += chunk));
107
+ child.on("error", reject);
108
+ child.on("close", (code) => {
109
+ if (code !== 0)
110
+ return reject(processError(code, stderr));
111
+ resolve(stdout);
112
+ });
113
+ child.stdin.on("error", reject);
114
+ child.stdin.end(stdin ?? "");
115
+ });
116
+ }
78
117
  function processError(code, stderr) {
79
118
  return new OneHarnessProcessError(`oneharness exited ${code}: ${stderr.trim()}`, code, stderr);
80
119
  }
@@ -147,40 +186,92 @@ function pushMany(args, flag, values) {
147
186
  for (const value of values ?? [])
148
187
  args.push(flag, value);
149
188
  }
150
- function runArguments(input, stream) {
151
- const args = ["run", "--prompt", input.prompt, "--compact"];
152
- pushMany(args, "--harness", input.harnesses);
153
- pushMany(args, "--model", input.models);
154
- if (input.system !== undefined)
155
- args.push("--system", input.system);
156
- if (input.reasoning !== undefined)
157
- args.push("--reasoning", input.reasoning);
158
- if (input.resume !== undefined)
159
- args.push("--resume", input.resume);
160
- if (input.session !== undefined)
161
- args.push("--session", input.session);
162
- if (input.fork)
163
- args.push("--fork");
164
- if (input.mode)
165
- args.push("--mode", input.mode);
166
- if (input.timeoutSeconds !== undefined)
167
- args.push("--timeout", String(input.timeoutSeconds));
168
- if (input.events)
169
- args.push("--events");
170
- if (stream)
171
- args.push("--stream");
172
- if (input.history)
173
- args.push("--history");
174
- if (input.historyName !== undefined)
175
- args.push("--history-name", input.historyName);
176
- if (input.historyDir !== undefined)
177
- args.push("--history-dir", input.historyDir);
178
- for (const [key, value] of Object.entries(input.historyLabels ?? {}))
179
- args.push("--history-label", `${key}=${value}`);
180
- for (const [key, value] of Object.entries(input.env ?? {}))
181
- args.push("--env", `${key}=${value}`);
182
- for (const [key, value] of Object.entries(input.bins ?? {}))
183
- args.push("--bin", `${key}=${value}`);
189
+ /**
190
+ * Does this binding put anything on the argv for `value`?
191
+ *
192
+ * This is the question `unless` asks, and asking it any other way is a bug: the
193
+ * conflict a suppression encodes is clap's, and clap conflicts on a flag being
194
+ * *present*. JavaScript truthiness is not that test — `{all: true, harnesses:
195
+ * []}`, the shape a caller assembling options programmatically produces, sends
196
+ * no `--harness` at all, yet an empty array is truthy, so reading it as truth
197
+ * dropped `--all` and left the call with no selection to make.
198
+ */
199
+ function rendersArgument(binding, value) {
200
+ if (value === undefined || value === null)
201
+ return false;
202
+ switch (binding.kind) {
203
+ case "repeated":
204
+ case "trailing":
205
+ return value.length > 0;
206
+ case "key-value":
207
+ return Object.keys(value).length > 0;
208
+ case "switch":
209
+ return Boolean(value);
210
+ default:
211
+ // A `positional` or `value` binding renders whatever it carries, an
212
+ // empty string included — which is a present flag to clap.
213
+ return true;
214
+ }
215
+ }
216
+ /**
217
+ * Render one capability's argv from its declared bindings.
218
+ *
219
+ * The client lists no flags of its own: `CAPABILITIES` says which option
220
+ * renders which flag and how, so a flag added to the CLI reaches every method
221
+ * as soon as the manifest binds it, and a method cannot quietly omit one. The
222
+ * Rust-side gates hold that manifest to clap and to the option contracts, which
223
+ * is what makes this table trustworthy enough to build calls from.
224
+ */
225
+ function capabilityArguments(method, options) {
226
+ // The options contracts are closed object types with no index signature, so
227
+ // reading a declared binding's key needs this one widening. It is safe
228
+ // because `every_bound_option_is_a_field_of_its_options_contract` proves in
229
+ // Rust that each key named here is a field of the very contract Zod just
230
+ // validated the value against.
231
+ const input = options;
232
+ const capability = CAPABILITIES[method];
233
+ const args = [...capability.argv, ...capability.always];
234
+ const positional = [];
235
+ const trailing = [];
236
+ const bound = new Map(capability.bindings.map((binding) => [binding.option, binding]));
237
+ for (const binding of capability.bindings) {
238
+ // `a_suppressing_option_is_one_the_same_capability_binds` proves in Rust
239
+ // that an `unless` always resolves here, so a missing entry is unreachable
240
+ // rather than a case to render through.
241
+ const suppressor = binding.unless === null ? null : bound.get(binding.unless);
242
+ if (suppressor && rendersArgument(suppressor, input[suppressor.option]))
243
+ continue;
244
+ const value = input[binding.option];
245
+ if (value === undefined || value === null)
246
+ continue;
247
+ switch (binding.kind) {
248
+ case "positional":
249
+ positional.push(String(value));
250
+ break;
251
+ case "value":
252
+ args.push(binding.flag, String(value));
253
+ break;
254
+ case "repeated":
255
+ pushMany(args, binding.flag, value.map(String));
256
+ break;
257
+ case "switch":
258
+ if (value)
259
+ args.push(binding.flag);
260
+ break;
261
+ case "key-value":
262
+ for (const [key, item] of Object.entries(value))
263
+ args.push(binding.flag, `${key}=${String(item)}`);
264
+ break;
265
+ case "trailing":
266
+ trailing.push(...value.map(String));
267
+ break;
268
+ }
269
+ }
270
+ args.push(...positional);
271
+ // After a `--` separator, so a passthrough argument that looks like a flag
272
+ // reaches the harness instead of being read by oneharness.
273
+ if (trailing.length > 0)
274
+ args.push("--", ...trailing);
184
275
  return args;
185
276
  }
186
277
  export class OneHarness {
@@ -188,11 +279,42 @@ export class OneHarness {
188
279
  constructor(options = {}) {
189
280
  this.options = options;
190
281
  }
282
+ /**
283
+ * Validate options, render the capability's argv, invoke, validate stdout.
284
+ *
285
+ * Every JSON-returning method is this call with two schemas, so none of them
286
+ * can drift from the manifest in how it builds a command or in what it
287
+ * promises back.
288
+ */
289
+ async call(method, options, optionsSchema, reportSchema, { acceptJsonOnNonzero = false, history = false,
290
+ // The two labels a caller sees on a rejection. They default to the
291
+ // verb, and are given explicitly where a method's wording predates
292
+ // this shared path — those strings are what a consumer's error
293
+ // handling already reads, so they are contract rather than prose.
294
+ optionsLabel = `invalid oneharness ${CAPABILITIES[method].argv.join(" ")} options`, contractLabel = `invalid oneharness ${CAPABILITIES[method].argv.join(" ")} contract`, } = {}) {
295
+ const input = parseContract(optionsSchema, options, optionsLabel);
296
+ const args = capabilityArguments(method, input);
297
+ const cwd = input.cwd;
298
+ let value;
299
+ try {
300
+ value = await invokeWith(this.options, args, cwd, acceptJsonOnNonzero);
301
+ }
302
+ catch (error) {
303
+ if (history && historyNotFound(error))
304
+ throw asHistoryNotFound(error);
305
+ throw error;
306
+ }
307
+ return parseContract(reportSchema, value, contractLabel);
308
+ }
191
309
  async run(options) {
192
- const input = parseContract(RunOptionsSchema, options, "invalid oneharness run options");
193
- const args = runArguments(input, false);
194
- const value = await invokeWith(this.options, args, input.cwd, true);
195
- return parseContract(RunReportSchema, value, "invalid oneharness run contract");
310
+ // `acceptJsonOnNonzero`, because a harness that fails is data in the
311
+ // report rather than a failure of the call: the CLI still prints a whole
312
+ // contract and exits non-zero to say some result was not `ok`.
313
+ return await this.call("run", options, RunOptionsSchema, RunReportSchema, {
314
+ acceptJsonOnNonzero: true,
315
+ optionsLabel: "invalid oneharness run options",
316
+ contractLabel: "invalid oneharness run contract",
317
+ });
196
318
  }
197
319
  async runMock(harness, options, script = {}) {
198
320
  if (harness.length === 0)
@@ -207,82 +329,129 @@ export class OneHarness {
207
329
  env.MOCK_EXIT = String(script.exitCode);
208
330
  if (script.latencyMs !== undefined)
209
331
  env.MOCK_SLEEP_MS = String(script.latencyMs);
210
- const args = runArguments({ ...input, harnesses: [harness], env }, false);
211
- args.push("--mock-harness", harness);
212
- const value = await invokeWith(this.options, args, input.cwd, true);
213
- return parseContract(RunReportSchema, value, "invalid oneharness run contract");
332
+ return await this.call("run", {
333
+ ...input,
334
+ harnesses: [harness],
335
+ mockHarnesses: [harness],
336
+ env,
337
+ }, RunOptionsSchema, RunReportSchema, {
338
+ acceptJsonOnNonzero: true,
339
+ optionsLabel: "invalid oneharness run options",
340
+ contractLabel: "invalid oneharness run contract",
341
+ });
214
342
  }
215
343
  runStream(options) {
216
344
  const input = parseContract(RunOptionsSchema, options, "invalid oneharness run options");
217
- return invokeStreamWith(this.options, runArguments(input, true), RunStreamEnvelopeSchema, "invalid oneharness run stream contract", input.cwd);
345
+ return invokeStreamWith(this.options, capabilityArguments("runStream", input), RunStreamEnvelopeSchema, "invalid oneharness run stream contract", input.cwd);
218
346
  }
219
347
  async list() {
220
- const value = await invokeWith(this.options, ["list", "--compact"]);
348
+ const value = await invokeWith(this.options, capabilityArguments("list", {}));
221
349
  return parseContract(ListReportSchema, value, "invalid oneharness list contract").harnesses;
222
350
  }
223
- async detect(harnesses = []) {
224
- const args = ["detect", "--compact"];
225
- pushMany(args, "--harness", harnesses);
226
- const value = await invokeWith(this.options, args);
227
- return parseContract(DetectReportSchema, value, "invalid oneharness detect contract").detected;
351
+ /**
352
+ * Probe harness binaries.
353
+ *
354
+ * The array overload is the long-standing shape and still means "these
355
+ * harnesses"; the options object is what reaches the rest of the verb's
356
+ * flags (`--all`, `--exclude`, `--bin`, the config layer).
357
+ */
358
+ async detect(harnessesOrOptions = []) {
359
+ const options = Array.isArray(harnessesOrOptions)
360
+ ? { harnesses: [...harnessesOrOptions] }
361
+ : harnessesOrOptions;
362
+ const report = await this.call("detect", options, DetectOptionsSchema, DetectReportSchema, {
363
+ optionsLabel: "invalid oneharness detect options",
364
+ contractLabel: "invalid oneharness detect contract",
365
+ });
366
+ return report.detected;
367
+ }
368
+ async config(options = {}) {
369
+ return await this.call("config", options, ConfigOptionsSchema, ConfigReportSchema);
370
+ }
371
+ async sync(options = {}) {
372
+ // `--check` exits non-zero when a file would change, which is the answer
373
+ // rather than a failure, and the report says which files those are.
374
+ return await this.call("sync", options, SyncOptionsSchema, SyncReportSchema, { acceptJsonOnNonzero: true });
375
+ }
376
+ async usage(options = {}) {
377
+ return await this.call("usage", options, UsageOptionsSchema, UsageReportSchema);
378
+ }
379
+ /**
380
+ * Write a starter `oneharness.toml` and return where it landed.
381
+ *
382
+ * The one verb whose stdout is a human confirmation line rather than a JSON
383
+ * contract — the deliverable is the file — so the return is the resolved
384
+ * path the caller asked for, not a parsed document.
385
+ */
386
+ async init(options = {}) {
387
+ const input = parseContract(InitOptionsSchema, options, "invalid oneharness init options");
388
+ await invokeText(this.options, capabilityArguments("init", input));
389
+ return input.path ?? "oneharness.toml";
390
+ }
391
+ /**
392
+ * Run the pre-tool gate over one harness hook event, as that harness's own
393
+ * runtime would.
394
+ *
395
+ * Returns the harness's native deny verdict, or `null` when the call is
396
+ * allowed through — which the CLI says by printing nothing at all, so an
397
+ * empty answer is the allow rather than a missing one.
398
+ */
399
+ async gate(options) {
400
+ const input = parseContract(GateOptionsSchema, options, "invalid oneharness gate options");
401
+ const verdict = await invokeText(this.options, capabilityArguments("gate", input), input.event);
402
+ return verdict.trim() === "" ? null : verdict;
403
+ }
404
+ /** The read-write sibling of {@link gate}, driven by a rules file. */
405
+ async mock(options) {
406
+ const input = parseContract(MockOptionsSchema, options, "invalid oneharness mock options");
407
+ const verdict = await invokeText(this.options, capabilityArguments("mock", input), input.event);
408
+ return verdict.trim() === "" ? null : verdict;
409
+ }
410
+ /**
411
+ * Abort the in-flight turn of a `run --control` session, optionally
412
+ * redirecting it with `input`.
413
+ *
414
+ * A refusal is an answer, not a throw: the response says whether the abort
415
+ * was served and, when it was not, why — which is what a supervisor branches
416
+ * on. So a non-zero exit still yields the frame.
417
+ */
418
+ async interrupt(options) {
419
+ return await this.call("interrupt", options, InterruptOptionsSchema, InterruptResponseSchema, { acceptJsonOnNonzero: true });
228
420
  }
229
421
  async history(lookup) {
230
- const input = parseContract(HistoryLookupSchema, lookup, "invalid oneharness history options");
231
- const args = ["history", "show", "--compact"];
232
- // A lookup that selects no session is not a HistoryLookup, so only these
233
- // two cases remain. The variants overlap on `{session, last: true}`, and
234
- // `last: true` keeps its long-standing priority over a name — which is why
235
- // the union tries the last-session variant first, in Rust and in the
236
- // generated Zod alike. Ruling that case out here leaves the variant whose
237
- // session the type guarantees is present.
238
- if (input.last === true)
239
- args.push("--last");
240
- else
241
- args.push(input.session);
242
- if (input.project)
243
- args.push("--project", input.project);
244
- if (input.allProjects)
245
- args.push("--all-projects");
246
- if (input.historyDir)
247
- args.push("--history-dir", input.historyDir);
248
- let value;
249
- try {
250
- value = await invokeWith(this.options, args);
251
- }
252
- catch (error) {
253
- if (historyNotFound(error))
254
- throw asHistoryNotFound(error);
255
- throw error;
256
- }
257
- return parseContract(HistoryRecordsSchema, value, "invalid history contract");
422
+ // The `--last`-suppresses-a-name rule is declared, not re-derived here:
423
+ // the union deliberately accepts `{session, last: true}` and resolves it
424
+ // to "the most recent", and the manifest binds `session` with
425
+ // `unless: "last"` so the builder renders only `--last`. Rust, the
426
+ // generated Zod, and this call therefore agree by construction.
427
+ return await this.call("history", lookup, HistoryLookupSchema, HistoryRecordsSchema, {
428
+ history: true,
429
+ optionsLabel: "invalid oneharness history options",
430
+ contractLabel: "invalid history contract",
431
+ });
258
432
  }
259
433
  async historyList(options = {}) {
260
- const input = parseContract(HistoryListOptionsSchema, options, "invalid oneharness history list options");
261
- const args = ["history", "list", "--compact"];
262
- if (input.project)
263
- args.push("--project", input.project);
264
- if (input.allProjects)
265
- args.push("--all-projects");
266
- if (input.historyDir)
267
- args.push("--history-dir", input.historyDir);
268
- const value = await invokeWith(this.options, args);
269
- return parseContract(HistoryListSchema, value, "invalid history list contract");
434
+ return await this.call("historyList", options, HistoryListOptionsSchema, HistoryListSchema, {
435
+ optionsLabel: "invalid oneharness history list options",
436
+ contractLabel: "invalid history list contract",
437
+ });
438
+ }
439
+ /**
440
+ * Remove history sessions.
441
+ *
442
+ * A dry run until `yes` is set, and the two answers are different documents:
443
+ * `dry_run` discriminates them, so a caller reads `removed` only from a run
444
+ * that removed something.
445
+ */
446
+ async historyClear(options = {}) {
447
+ return await this.call("historyClear", options, HistoryClearOptionsSchema, HistoryClearReportSchema);
448
+ }
449
+ /** Rewrite legacy session files to the current record version. */
450
+ async historyMigrate(options = {}) {
451
+ return await this.call("historyMigrate", options, HistoryMigrateOptionsSchema, HistoryMigrateReportSchema);
270
452
  }
271
453
  historyWatch(options = {}) {
272
454
  const input = parseContract(HistoryWatchOptionsSchema, options, "invalid oneharness history watch options");
273
- const args = ["history", "watch", "--format", "jsonl"];
274
- if (input.after)
275
- args.push("--after", input.after);
276
- for (const [key, value] of Object.entries(input.labels ?? {}))
277
- args.push("--label", `${key}=${value}`);
278
- if (input.project)
279
- args.push("--project", input.project);
280
- if (input.allProjects)
281
- args.push("--all-projects");
282
- if (input.historyDir)
283
- args.push("--history-dir", input.historyDir);
284
- if (input.events)
285
- args.push("--events");
286
- return invokeStreamWith(this.options, args, HistoryStreamEnvelopeSchema, "invalid oneharness history watch contract", undefined, true);
455
+ return invokeStreamWith(this.options, capabilityArguments("historyWatch", input), HistoryStreamEnvelopeSchema, "invalid oneharness history watch contract", undefined, true);
287
456
  }
288
457
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneharness/sdk",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "description": "Typed Node.js SDK for oneharness",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,7 +19,7 @@
19
19
  "node": ">=20"
20
20
  },
21
21
  "dependencies": {
22
- "oneharness-cli": "0.6.13",
22
+ "oneharness-cli": "0.6.15",
23
23
  "zod": "^4.4.3"
24
24
  }
25
25
  }