@awebai/oats 0.30.3 → 0.32.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/bin/oats.mjs +216 -79
- package/docs/configuration.md +68 -8
- package/docs/desktop-cli-api.md +233 -27
- package/docs/desktop-instance-start.md +3 -1
- package/docs/desktop.md +36 -0
- package/docs/execution-targets.md +53 -36
- package/docs/implementation.md +59 -1
- package/docs/integrations.md +1 -1
- package/docs/oats-local.schema.json +10 -1
- package/docs/official-catalog.md +1 -1
- package/docs/packages.md +2 -2
- package/docs/plans/0.30-close-out.md +7 -4
- package/docs/release-notes/v0.31.0.md +130 -0
- package/docs/release-notes/v0.32.0.md +137 -0
- package/docs/schedules.md +4 -1
- package/docs/servers.md +94 -25
- package/docs/souls-and-instances.md +2 -2
- package/lib/attachments.mjs +2 -1
- package/lib/automations.mjs +5 -1
- package/lib/core.mjs +241 -238
- package/lib/errors.mjs +17 -0
- package/lib/instance-inspect.mjs +13 -4
- package/lib/instance-resolution.mjs +3 -3
- package/lib/local-inputs.mjs +46 -0
- package/lib/materialize.mjs +3 -2
- package/lib/packages.mjs +26 -6
- package/lib/remote.mjs +792 -36
- package/lib/resolve.mjs +23 -7
- package/lib/schedule.mjs +21 -8
- package/lib/servers.mjs +272 -45
- package/lib/session-input.mjs +9 -23
- package/lib/session-viewer.mjs +0 -5
- package/lib/triggers.mjs +10 -8
- package/lib/workspace.mjs +163 -56
- package/package-catalog.json +1 -1
- package/package.json +1 -1
- package/lib/herdr.mjs +0 -143
package/lib/errors.mjs
CHANGED
|
@@ -5,3 +5,20 @@ export function oatsError(code, message, provenance) {
|
|
|
5
5
|
if (provenance) e.provenance = provenance;
|
|
6
6
|
return e;
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/** Herdr was removed in 0.31.0: every place that meets it refuses with E_HERDR_REMOVED, this stem
|
|
10
|
+
* and what to do. */
|
|
11
|
+
export const HERDR_REMOVED = "Herdr is no longer supported by OATS (removed in 0.31.0); tmux is the only session backend.";
|
|
12
|
+
/** A flag or configuration value that selects Herdr: `what` names the flag (`--backend herdr was
|
|
13
|
+
* given`), or the file and key (`<file> sets spawn.backend: herdr`). */
|
|
14
|
+
export function herdrSettingRemoved(what) {
|
|
15
|
+
return oatsError("E_HERDR_REMOVED", `${HERDR_REMOVED} ${what}. Remove it (or use tmux).`);
|
|
16
|
+
}
|
|
17
|
+
/** A session operation on an instance OATS opened in Herdr. */
|
|
18
|
+
export function herdrInstanceRemoved(name) {
|
|
19
|
+
return oatsError("E_HERDR_REMOVED", `${HERDR_REMOVED} ${name} was opened in Herdr. Retire it (\`oats retire ${name}\`) and spawn a new instance; it opens in tmux.`);
|
|
20
|
+
}
|
|
21
|
+
/** Retiring an instance OATS opened in Herdr while processes still work in its home. */
|
|
22
|
+
export function herdrInstanceBusy(name, pids) {
|
|
23
|
+
return oatsError("E_HERDR_REMOVED", `${HERDR_REMOVED} ${name} was opened in Herdr. A process still works in this home (pid ${pids.join(", ")}); stop its Herdr pane (for example \`herdr --session oats server stop\`), then retire.`);
|
|
24
|
+
}
|
package/lib/instance-inspect.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { spawnSync } from "node:child_process";
|
|
|
14
14
|
import { accessSync, constants as fsConstants, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
15
15
|
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { capabilityManifests, homeLaunchLayers, instanceSoulDir, launchConfigsAt, launchReportFor, manifestOperations, parseYamlNested, servedIdentityOf, teamEnv, upgradeHomeMeta, withConfigFile } from "./core.mjs";
|
|
17
|
+
import { capabilityManifests, sessionDefaults, homeLaunchLayers, instanceSoulDir, launchConfigsAt, launchReportFor, manifestOperations, parseYamlNested, servedIdentityOf, teamEnv, upgradeHomeMeta, withConfigFile } from "./core.mjs";
|
|
18
18
|
import { agentDirOf, discoverOrStandalone, ensureWorkspaceSoul, findSoulEntry, liveTeams, prepareInstance } from "./instance-resolution.mjs";
|
|
19
19
|
import { declaredSettings } from "./capability-contract.mjs";
|
|
20
20
|
import { kernelCompatibility } from "./resolve.mjs";
|
|
@@ -127,7 +127,7 @@ export async function homeTarget(home, meta, { remoteOptions, discover = true, l
|
|
|
127
127
|
try { launchCurrent = launchReportFor({ layers: homeLaunchLayers(realHome, meta), launchConfigs: launchConfigsAt(deployment), contextDir: deployment }); }
|
|
128
128
|
catch { launchCurrent = null; }
|
|
129
129
|
return {
|
|
130
|
-
kind: "instance", home: realHome, meta, deployment, agentsRoot, teams: live.teams, defaultTeam: live.defaultTeam ?? null, teamsSource: live.source, launch, launchCurrent,
|
|
130
|
+
kind: "instance", home: realHome, meta, deployment, agentsRoot, teams: live.teams, defaultTeam: live.defaultTeam ?? null, teamsSource: live.source, launch, launchCurrent, session: newSessionDefaults(deployment),
|
|
131
131
|
recordedDefaultTeam: recordedTeams(meta).defaultTeam, teamModel: model, teamKey,
|
|
132
132
|
subject: { kind: "instance", instance: meta.instance, home, soul: meta.agent },
|
|
133
133
|
soul: { name: meta.agent, repoKey: ws.soul?.repoKey ?? null, commit: ws.soul?.commit ?? null, external, path: null, soulDir, definition, problems: [...problems, ...loadProblems] },
|
|
@@ -183,7 +183,7 @@ export async function soulTarget(contextDir, soul, { remoteOptions } = {}) {
|
|
|
183
183
|
try { launchConfigs = launchConfigsAt(deployment); } catch { launchConfigs = {}; }
|
|
184
184
|
const launch = launchReportFor({ layers: prepared?.launch ?? launchLayers({ definition: soulEntry.definition, entry: soulEntry, key: teamKey, local: found.local }), launchConfigs, contextDir: deployment });
|
|
185
185
|
return {
|
|
186
|
-
kind: "soul", home: null, meta: null, deployment, agentsRoot: join(deployment, "agents"), teams, defaultTeam, teamsSource: "live", teamModel: model, teamKey, launch,
|
|
186
|
+
kind: "soul", home: null, meta: null, deployment, agentsRoot: join(deployment, "agents"), teams, defaultTeam, teamsSource: "live", teamModel: model, teamKey, launch, session: newSessionDefaults(deployment),
|
|
187
187
|
subject: { kind: "soul", soul: soulEntry.name, repoKey: soulEntry.repoKey ?? null, commit: soulEntry.commit ?? null },
|
|
188
188
|
soul: { name: soulEntry.name, repoKey: soulEntry.repoKey ?? null, commit: soulEntry.commit ?? null, external: soulEntry.external === true, path: soulEntry.path ?? null,
|
|
189
189
|
soulDir, definition: soulEntry.definition ?? null, problems: [], ...(soulCopyError ? { copyError: soulCopyError } : {}) },
|
|
@@ -195,6 +195,12 @@ export async function soulTarget(contextDir, soul, { remoteOptions } = {}) {
|
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/** The tmux session a NEW tmux spawn here would open in ({tmuxSession}, 0.31); null when the
|
|
199
|
+
* deployment's oats-local.yaml cannot be read. */
|
|
200
|
+
function newSessionDefaults(deployment) {
|
|
201
|
+
try { return sessionDefaults(deployment); } catch { return null; }
|
|
202
|
+
}
|
|
203
|
+
|
|
198
204
|
/** Is `dir` a v2 context (an oats-local.yaml in reach)? Only a MISSING file means
|
|
199
205
|
* "not a workspace"; an unreadable or invalid one is the caller's error to report,
|
|
200
206
|
* never a silent fallback to the classic chain. */
|
|
@@ -256,8 +262,11 @@ export function inspectDocument(t, { kernel }) {
|
|
|
256
262
|
// Feature launch-preference: a soul's launch here; a home's recorded launch and launchCurrent.
|
|
257
263
|
launch: t.launch ?? null,
|
|
258
264
|
...(t.home ? { launchCurrent: t.launchCurrent ?? null } : {}),
|
|
265
|
+
// What a new tmux spawn here would open in: {tmuxSession} (0.31).
|
|
266
|
+
session: t.session ?? null,
|
|
259
267
|
knowledge: kcap ? { provider: kcap.id, version: kcap.version, operations: kcap.operations.map((o) => ({ name: o.name, kind: o.kind, available: o.available, reason: o.reason })) } : { provider: null, version: null, operations: [] },
|
|
260
|
-
instance: m ? { home: t.home, instance: m.instance, agent: m.agent, harness: m.harness || null, model: m.model ?? null, yolo: m.yolo ?? null,
|
|
268
|
+
instance: m ? { home: t.home, instance: m.instance, agent: m.agent, harness: m.harness || null, model: m.model ?? null, yolo: m.yolo ?? null,
|
|
269
|
+
launchConfig: m.launch?.launchConfig ?? null, launchConfigDefault: m.launch?.launchConfigDefault === true, launched: !!m.launched, createdAt: m.createdAt || null,
|
|
261
270
|
resolution: m.workspace?.resolution ?? null, soulDir: t.soul.soulDir, instructions: { ...readTextCapped(join(t.home, "AGENTS.md")), sources: m.instructions || [] } } : null,
|
|
262
271
|
...(m ? { identity: servedIdentityOf(m) } : {}),
|
|
263
272
|
problems: [...t.soul.problems, ...(t.soul.copyError ? [t.soul.copyError] : []), ...(t.discoveryError ? [t.discoveryError] : []),
|
|
@@ -30,7 +30,7 @@ import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync, symlinkSync,
|
|
|
30
30
|
import { tmpdir } from "node:os";
|
|
31
31
|
import { spawnSync } from "node:child_process";
|
|
32
32
|
import { randomBytes } from "node:crypto";
|
|
33
|
-
import { readLock, LOCK_FILE, SOUL_ALIAS_SYMLINK } from "./packages.mjs";
|
|
33
|
+
import { readLock, readLockIfPresent, LOCK_FILE, SOUL_ALIAS_SYMLINK } from "./packages.mjs";
|
|
34
34
|
import * as defaultRemote from "./remote.mjs";
|
|
35
35
|
import { parseRepoRef } from "./remote.mjs";
|
|
36
36
|
|
|
@@ -438,7 +438,7 @@ export async function prepareInstance(contextDir, soulName, { spawn = {}, remote
|
|
|
438
438
|
const found = localOverride ? { path: null, local: localOverride } : loadLocal(contextDir);
|
|
439
439
|
const local = found.local;
|
|
440
440
|
const deployment = found.path ? dirname(found.path) : resolvePath(contextDir);
|
|
441
|
-
const lock =
|
|
441
|
+
const lock = readLockIfPresent(deployment);
|
|
442
442
|
const discovery = discoveryOverride ?? await discoverOrStandalone(local, { lock, remoteOptions, remote });
|
|
443
443
|
const soulEntry = findSoulEntry(discovery, soulName);
|
|
444
444
|
const disabled = disabledEntry(local, soulEntry);
|
|
@@ -458,7 +458,7 @@ const isAccessFailure = (e) => e?.code === "E_REMOTE_UNREADABLE" && ACCESS_REASO
|
|
|
458
458
|
|
|
459
459
|
/** The deployment's lock, or null when it has none (an unreadable lock is E_LOCK_SCHEMA). */
|
|
460
460
|
export function deploymentLock(deployment) {
|
|
461
|
-
return
|
|
461
|
+
return readLockIfPresent(deployment);
|
|
462
462
|
}
|
|
463
463
|
|
|
464
464
|
/** Decision 10: a standalone view is a MEMBER whose workspace cannot be read — the
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** lib/local-inputs.mjs — the local configuration a command read, for `observation.localRevision`
|
|
2
|
+
* (spec Addendum 4; docs/desktop-cli-api.md "Observation reuse").
|
|
3
|
+
*
|
|
4
|
+
* Every reader of local configuration on the read verbs' paths reports what it parsed:
|
|
5
|
+
* `recordLocalInput(absPath, bytes)` for a file it read, `recordLocalInput(absPath, null)` for one it
|
|
6
|
+
* looked for and found absent (a file appearing changes the answer as much as one changing). The inputs:
|
|
7
|
+
* oats-local.yaml and every walk-up candidate loadLocal found absent (lib/workspace.mjs), oats-lock.json
|
|
8
|
+
* (lib/packages.mjs readLock / readLockIfPresent), an OATS_PACKAGE_CATALOG override (lib/core.mjs; the
|
|
9
|
+
* bundled catalog is the kernel's, not configuration), the automations snapshot (lib/automations.mjs).
|
|
10
|
+
* NOT inputs: instance homes and runtime state (the answer reports them), the parsed and observation caches.
|
|
11
|
+
*
|
|
12
|
+
* Recording is a no-op until the CLI activates it for this process (a command with --max-age), so
|
|
13
|
+
* library callers and tests are unaffected unless they opt in. The first bytes recorded for a path win.
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { realpathSync } from "node:fs";
|
|
17
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
18
|
+
|
|
19
|
+
let inputs = null; // canonical path → sha256(bytes) | "absent", while active
|
|
20
|
+
|
|
21
|
+
/** Start recording for this process (idempotent; clears nothing already recorded). */
|
|
22
|
+
export function activateLocalInputs() { inputs ??= new Map(); }
|
|
23
|
+
|
|
24
|
+
/** A file's canonical path: its realpath when it exists, else its directory's realpath plus its name
|
|
25
|
+
* (else the resolved path), so a symlinked deployment gives one revision. */
|
|
26
|
+
function canonical(absPath) {
|
|
27
|
+
const path = resolve(absPath);
|
|
28
|
+
try { return realpathSync(path); } catch { /* absent */ }
|
|
29
|
+
try { return join(realpathSync(dirname(path)), basename(path)); } catch { return path; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Record one input: the bytes parsed (Buffer | string), or null for a file looked for and absent. */
|
|
33
|
+
export function recordLocalInput(absPath, bytes) {
|
|
34
|
+
if (!inputs) return;
|
|
35
|
+
const key = canonical(absPath);
|
|
36
|
+
if (inputs.has(key)) return;
|
|
37
|
+
inputs.set(key, bytes === null || bytes === undefined ? "absent" : createHash("sha256").update(bytes).digest("hex"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 24 lowercase hex: sha256 over the sorted `path NUL digest|absent` lines of every recorded input (the
|
|
41
|
+
* empty set included). Never a path or content. null when recording is not active. */
|
|
42
|
+
export function localRevision() {
|
|
43
|
+
if (!inputs) return null;
|
|
44
|
+
const lines = [...inputs].map(([path, digest]) => `${path}\0${digest}`).sort();
|
|
45
|
+
return createHash("sha256").update(lines.join("\n")).digest("hex").slice(0, 24);
|
|
46
|
+
}
|
package/lib/materialize.mjs
CHANGED
|
@@ -59,6 +59,7 @@ import { oatsError } from "./errors.mjs";
|
|
|
59
59
|
import * as defaultRemote from "./remote.mjs";
|
|
60
60
|
import { renderInstructionText } from "./instruction-composition.mjs";
|
|
61
61
|
import { DEFAULT_PACKAGE_PATH, validateLock } from "./packages.mjs";
|
|
62
|
+
import { memberRowByKey } from "./workspace.mjs";
|
|
62
63
|
|
|
63
64
|
export const MODULES_DIR = join(".oats", "modules");
|
|
64
65
|
export const SKILLS_DIR = join(".agents", "skills");
|
|
@@ -572,7 +573,7 @@ export function driftOf(instanceJson, discovery, { lock } = {}) {
|
|
|
572
573
|
}
|
|
573
574
|
const repoKey = typeof from.repoKey === "string" ? from.repoKey : null;
|
|
574
575
|
const recorded = { repoKey, commit };
|
|
575
|
-
const member = repoKey ? members
|
|
576
|
+
const member = repoKey ? memberRowByKey(members, repoKey) : null;
|
|
576
577
|
if (!member || !member.confirmed || typeof member.commit !== "string") {
|
|
577
578
|
rows.push({ module: name, from: clone(from), recorded, current: null, status: "missing", reason: member ? (member.reason || "unconfirmed") : "unconfirmed" });
|
|
578
579
|
continue;
|
|
@@ -616,7 +617,7 @@ export function soulDriftOf(instanceJson, discovery) {
|
|
|
616
617
|
return { ...base, current: { commit: now.commit, version: now.version }, status: now.commit === commit ? "current" : "moved" };
|
|
617
618
|
}
|
|
618
619
|
const members = Array.isArray(discovery?.members) ? discovery.members : [];
|
|
619
|
-
const member = members
|
|
620
|
+
const member = memberRowByKey(members, soul.repoKey);
|
|
620
621
|
const standaloneOwn = discovery?.standalone === true && member && member.key === discovery.key && typeof member.commit === "string";
|
|
621
622
|
const base = { name, repoKey: soul.repoKey, commit };
|
|
622
623
|
if (!member || (!member.confirmed && !standaloneOwn) || typeof member.commit !== "string") {
|
package/lib/packages.mjs
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, lstatSync } from "node:fs";
|
|
45
45
|
import { tmpdir } from "node:os";
|
|
46
46
|
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
47
|
+
import { recordLocalInput } from "./local-inputs.mjs";
|
|
47
48
|
import { oatsError as baseOatsError } from "./errors.mjs";
|
|
48
49
|
import * as defaultRemote from "./remote.mjs";
|
|
49
50
|
import { manifestContractProblems } from "./capability-contract.mjs";
|
|
@@ -165,14 +166,23 @@ export function validateLock(lock, { file } = {}) {
|
|
|
165
166
|
* else is validated (E_LOCK_SCHEMA). Returns a fresh object every call. */
|
|
166
167
|
export function readLock(dir) {
|
|
167
168
|
const file = join(resolve(dir), LOCK_FILE);
|
|
168
|
-
if (!existsSync(file)) return emptyLock();
|
|
169
|
+
if (!existsSync(file)) { recordLocalInput(file, null); return emptyLock(); }
|
|
169
170
|
let parsed;
|
|
170
|
-
|
|
171
|
+
const text = readFileSync(file, "utf8");
|
|
172
|
+
recordLocalInput(file, text);
|
|
173
|
+
try { parsed = JSON.parse(text); } catch (e) {
|
|
171
174
|
throw oatsError("E_LOCK_SCHEMA", `oats-lock.json is not valid JSON (${file}): ${e.message}`, { file, path: "/" });
|
|
172
175
|
}
|
|
173
176
|
return validateLock(parsed, { file });
|
|
174
177
|
}
|
|
175
178
|
|
|
179
|
+
/** `<dir>/oats-lock.json` read as readLock does, or null when there is none (recorded as absent). */
|
|
180
|
+
export function readLockIfPresent(dir) {
|
|
181
|
+
const file = join(resolve(dir), LOCK_FILE);
|
|
182
|
+
if (!existsSync(file)) { recordLocalInput(file, null); return null; }
|
|
183
|
+
return readLock(dir);
|
|
184
|
+
}
|
|
185
|
+
|
|
176
186
|
/** Canonical lock text: sorted package ids, sorted capability names, 2-space indent, trailing newline. */
|
|
177
187
|
export function canonicalLock(lock) {
|
|
178
188
|
validateLock(lock);
|
|
@@ -309,8 +319,14 @@ async function readJson(remote, remoteRef, commit, path, what, details) {
|
|
|
309
319
|
|
|
310
320
|
/** Read `oats-package.json` and every capability manifest it lists.
|
|
311
321
|
* → { manifest, capabilities: [{ name, dir, manifest }] } (sorted by name, codepoint order).
|
|
312
|
-
* Two capability dirs declaring the same name → E_PACKAGE_MANIFEST { duplicate }.
|
|
322
|
+
* Two capability dirs declaring the same name → E_PACKAGE_MANIFEST { duplicate }.
|
|
323
|
+
* The answer is a pure function of the bytes at the commit: a remote with the parsed cache
|
|
324
|
+
* (lib/remote.mjs memoAtCommit) keeps it; a refusal is thrown and never kept. */
|
|
313
325
|
export async function readPackageManifests(remote, remoteRef, commit, path, details = {}) {
|
|
326
|
+
const read = () => readPackageManifestsAt(remote, remoteRef, commit, path, details);
|
|
327
|
+
return typeof remote.memoAtCommit === "function" ? remote.memoAtCommit(remoteRef, commit, `package-manifests\0${path}`, read) : read();
|
|
328
|
+
}
|
|
329
|
+
async function readPackageManifestsAt(remote, remoteRef, commit, path, details) {
|
|
314
330
|
const manifestPath = pjoin(path, PACKAGE_MANIFEST);
|
|
315
331
|
const manifest = await readJson(remote, remoteRef, commit, manifestPath, "package manifest", details);
|
|
316
332
|
if (!plainObject(manifest) || typeof manifest.package !== "string" || !Array.isArray(manifest.capabilities)) {
|
|
@@ -412,7 +428,6 @@ function assertDigest(what, value, details) {
|
|
|
412
428
|
return value;
|
|
413
429
|
}
|
|
414
430
|
|
|
415
|
-
/** Bind `remoteOptions` (cacheDir, exec, …) into every call of a contract-§1 remote. */
|
|
416
431
|
/** A remote whose reads at a commit are shared for one command (feature desktop-facts): the souls and
|
|
417
432
|
* capabilities listings resolve many souls over the same package manifests and skill listings, and a read at
|
|
418
433
|
* an immutable commit gives the same answer every time. A failed read is not kept (it is retried). */
|
|
@@ -430,13 +445,18 @@ export function memoizedRemote(remote) {
|
|
|
430
445
|
return memo;
|
|
431
446
|
}
|
|
432
447
|
|
|
448
|
+
/** Bind `remoteOptions` (cacheDir, exec, the command's read `session`, …) into every call of a contract-§1
|
|
449
|
+
* remote. Object references are kept (a spread, never a copy of the values), so one session is shared by
|
|
450
|
+
* every call; `memoAtCommit` (the parsed cache, lib/remote.mjs) and the member prefetch's primitives
|
|
451
|
+
* (`lastObservedCommit`, `peekAtCommit`, `prefetchObservation`) are optional and bound when present. */
|
|
433
452
|
export function bindRemote(remote, remoteOptions) {
|
|
434
453
|
if (!remoteOptions || Object.keys(remoteOptions).length === 0) return remote;
|
|
435
454
|
const bound = { ...remote };
|
|
436
|
-
|
|
455
|
+
const arities = { observeRemote: 1, readRemoteFile: 3, listRemoteTree: 3, fetchRemoteTree: 4, memoAtCommit: 4, lastObservedCommit: 1, peekAtCommit: 3, prefetchObservation: 1, abandonPrefetches: 0 };
|
|
456
|
+
for (const name of Object.keys(arities)) {
|
|
437
457
|
if (typeof remote[name] !== "function") continue;
|
|
438
458
|
bound[name] = (...args) => {
|
|
439
|
-
const arity =
|
|
459
|
+
const arity = arities[name];
|
|
440
460
|
const opts = { ...(args[arity] || {}), ...remoteOptions };
|
|
441
461
|
return remote[name](...args.slice(0, arity), opts);
|
|
442
462
|
};
|