17. TypeScript SDK — `@eigenius/client`
The TypeScript SDK at clients/eigenius-ts/ wraps the orchestrator’s two Connect-RPC services in a single typed Eigen class. It is the same SDK the notebook uses (chapter 14); you can use it from any TypeScript runtime that speaks Connect-RPC — Deno scripts, browser apps, Node.js servers — to drive the kernel programmatically.
Per D22 §5.
17.1. What it covers
The Eigen class has 33 public methods — 30 async plus three synchronous accessors (getEndpoint, getDefaultBranch, useBranch). Async responses are protobuf message objects (camelCased fields).
The table below covers the eleven the notebook MVP drives. The rest are grouped after it; the authoritative list is the class itself.
| Method | Purpose | Underlying RPC |
|---|---|---|
eigen.health() | Liveness check | EigeniusKernel.Health |
eigen.inspect(iri, options?) | Resolve a resource by IRI from the active layer | EigeniusKernel.Inspect |
eigen.query(eigenql, options?) | Run an EigenQL query, get an Eigon-CBOR ResultSet document | EigeniusKernel.Query |
eigen.load(source, options?) | Compile + commit a layer (ESL source or Eigon-JSON document) | EigeniusKernel.Load |
eigen.validateProgram(program, options?) | Run a program’s static checks against the active layer chain; checksPerformed names which ran (no EigenTT type-check — issue #143) | EigeniusKernel.ValidateProgram |
eigen.runProgram(program, input, options?) | Run a program inline against an inline input | EigeniusKernel.RunProgram |
eigen.runProgramByIri(programIri, inputIri, options?) | Run a program already loaded into the chain by IRI against an input also by IRI — the natural “one program × N inputs” shape | EigeniusKernel.RunProgramByIri |
eigen.listInstitutions() | List registered institutions and their declared fiber structure | EigeniusKernel.ListInstitutions |
eigen.layerTopology(options?) | Walk the layer chain; nodes for layers + classes + properties + institutions, edges for parent / is_a / subclass / requires / property-ref | NotebookService.LayerTopology |
eigen.publishNotebook(notebook) | Translate a NotebookJson into Notebook + Cell resources (content-addressed IRIs), then load them | calls Eigen.load internally |
The remaining twenty-two, by area:
| Area | Methods |
|---|---|
| Client state (sync) | getEndpoint, getDefaultBranch, useBranch |
| Traces | reflect |
| Branches | listBranches, getBranch, createBranch, deleteBranch, mergeBranches |
| Merge resolution | previewMerge, prepareMerge, previewCascade, submitResolution |
| Consolidation | estimateConsolidation, consolidateChain |
| Tasks | listTasks, getTaskStatus, cancelTask |
| Tags | createTag, listTags, deleteTag |
| Garbage collection | estimateGc, runGc |
prepareMerge is worth singling out: it is the only surface in the whole platform that returns merge conflict ids, and the CLI has no equivalent subcommand. A caller resolving a merge from the CLI has to obtain the ids through this method, the raw PrepareMerge RPC, or the notebook.
The SDK also exports the underlying request/response types (LoadResponse, QueryResponse, RunProgramResponse, LayerTopologyResponse, TopologyNode, TopologyEdge, NodeKind, EdgeKind, etc.) and notebook-specific types (NotebookJson, CellJson, notebookJsonToResources, resourcesToNotebookJson).
Source: clients/eigenius-ts/src/client.ts (the Eigen class), clients/eigenius-ts/src/notebook.ts (notebook-publish translator), clients/eigenius-ts/mod.ts (public exports).
17.2. Construction
import { Eigen } from "@eigenius/client";
// Browser / single-origin: hits the same origin, expects the// orchestrator's static-file route to be live.const eigen = new Eigen({ endpoint: window.location.origin });
// Deno / Node / explicit endpoint:const eigen = new Eigen({ endpoint: "http://localhost:8080" });EigenOptions:
| Field | Required | Description |
|---|---|---|
endpoint | yes | Orchestrator base URL — e.g. http://localhost:8080 |
fetch | no | Override the global fetch (Deno tests, custom interceptors) |
bearerToken | no | Currently unused; the hook for token auth without an API change later (D22 §8.3) |
defaultBranch | no | Branch used when a call doesn’t pass an explicit branch. Empty / omitted = the server’s default (main). See §17.4 for branch semantics. |
17.3. Five-line examples
Liveness + inspect
const h = await eigen.health();console.log(`kernel v${h.version}, ${h.layerCount} layer(s), ${h.resourceCount} resource(s)`);
const cls = await eigen.inspect("urn:eigenius:core:Class");console.log(cls.found ? `Class CBOR is ${cls.resource.length} bytes` : "not found");Load ESL → query
const { success, layerId, resourceCount, errors } = await eigen.load(` namespace ex = "urn:eigenius:demo:ex"; class ex:Widget { description = "A widget."; requires ex:label; } property ex:label : core:string { description = "Label."; }`);if (!success) throw new Error(errors.map((e) => e.message).join("; "));
const q = await eigen.query(` USING "urn:eigenius:core:Class" MATCH Class(?c) { short_name: ?n } RETURN [] { name: ?n } ORDER BY ?n`);q.document is an Eigon-CBOR document containing the ResultSet + row class + row Property metadata + row resources. Decode it with cbor-x (the same package the notebook uses) — see notebooks/src/runtime/resultDocument.ts for a worked decoder.
Run a program by IRI
// Assumes a previous eigen.load(...) committed both the program// (analyze_patent) and the input (US10452978B2) to the active chain.const result = await eigen.runProgramByIri( "urn:eigenius:demo:patent:analyze_patent", "urn:eigenius:demo:patent:US10452978B2",);console.log(`success=${result.success}, trace=${result.traceIri}`);// result.output is a CBOR-encoded PatentBrief resource;// result.traceIri points at the ProgramTrace if a trace store is configured.The runProgram (inline) variant takes both program and input as bytes with a single content_type — useful when neither is in the chain yet.
Walk the layer chain
import { NodeKind } from "@eigenius/client";
const topo = await eigen.layerTopology({ includeResources: false });const layers = topo.nodes.filter((n) => n.kind === NodeKind.LAYER);const classes = topo.nodes.filter((n) => n.kind === NodeKind.CLASS);console.log(`${layers.length} layers, ${classes.length} classes`);Set includeResources: true to also get one node per Resource instance (heavier; handy for debugging / full-graph rendering).
Publish a notebook
import type { NotebookJson } from "@eigenius/client";
const notebook: NotebookJson = JSON.parse( await Deno.readTextFile("./my-notebook.json"),);const { publish, load } = await eigen.publishNotebook(notebook);console.log(`notebook IRI: ${publish.notebookIri}`);console.log(`${publish.cellIris.length} cell IRI(s) in the published layer ${load.layerId}`);Identical content yields the same notebookIri (content-addressed). See chapter 14 §14.5.
17.4. Branches
Phase 14g made every load / runProgram / runProgramByIri / reflect call branch-aware. The TypeScript client mirrors the gRPC surface: per-call branch overrides on the relevant methods, plus a defaultBranch constructor option and a useBranch(name) mutator for setting the client-wide default after construction. Empty / omitted on a call falls back to the client default; empty / omitted on the constructor falls back to the server default (main).
// Default to a feature branch for the lifetime of this clientconst eigen = new Eigen({ endpoint: "http://localhost:8080", defaultBranch: "feature-x",});
// All commits land on feature-xawait eigen.load(eslSource, { contentType: "application/x-esl" });
// Override for one call — commit to main even though the default is feature-xawait eigen.load(eslSource, { contentType: "application/x-esl", branch: "main",});
// Mutate the default after constructioneigen.useBranch("experiment-42");console.log(eigen.getDefaultBranch()); // "experiment-42"For reads, branch and atLayer are mutually exclusive — pass either to pin the read, leave both empty to use the (default) branch’s current head:
// Pin to a specific layerconst r1 = await eigen.inspect("urn:example:Dog", { atLayer: "abe85ea7..." });
// Pin to a branch's current headconst r2 = await eigen.inspect("urn:example:Dog", { branch: "feature-x" });Branch CRUD methods (require a kernel with a persistent backend):
// Listconst branches = await eigen.listBranches();for (const b of branches) console.log(`${b.name}: ${b.headLayer}`);
// Showconst main = await eigen.getBranch("main");if (main.found) console.log(`main is at ${main.headLayer}`);
// Create — branch off main's headconst created = await eigen.createBranch("feature-x", { fromLayer: main.headLayer });if (!created.success) console.error(created.error);
// Delete (defaults to safe — refuses if a task pin matches the head)const del = await eigen.deleteBranch("feature-x");console.log(del.deleted ? `deleted (was at ${del.previousHead})` : "did not exist");
// Force-delete (skip the task-pin safety check)await eigen.deleteBranch("feature-x", { force: true });See chapter 4 §4.6 for the equivalent CLI surface and D23 §5.4–§5.5 for the design.
17.6. Error handling
Most methods return structured response objects with success: bool and an errors: ValidationError[] array — non-RPC failures (validation, parse, etc.) come back this way and the call doesn’t throw.
Connect-RPC errors (network failures, kernel-side gRPC Status errors that the orchestrator translates) throw ConnectError (from @connectrpc/connect). The orchestrator’s passthrough explicitly re-wraps kernel gRPC errors so the inbound Connect protocol carries the actual message — without this, browser callers see [internal] HTTP 400 with the real message URL-encoded into a grpc-message header. See orchestration/src/notebook/eigenius_kernel_passthrough.ts.
17.7. Layout and dependencies
clients/eigenius-ts/├── deno.jsonc # Deno project config (npm: + jsr: imports)├── package.json # npm package shape (name, exports, deps)├── mod.ts # public API exports├── src/│ ├── client.ts # Eigen class│ └── notebook.ts # NotebookJson <-> Resource[] translator + content-addressed IRIs├── generated/ # buf-generated Connect stubs (do not edit)│ └── eigenius_pb.ts # generated by `npx buf generate`└── examples/ ├── smoke-test.ts # 8 steps; covers the core RPCs, not every method ├── publish-test.ts # publishNotebook end-to-end └── error-test.ts # demonstrates the orchestrator's gRPC→Connect error translationRuntime deps: @bufbuild/protobuf@^2, @connectrpc/connect@^2, @connectrpc/connect-web@^2. The notebook consumes the SDK as a file:../clients/eigenius-ts workspace dep; once the SDK stabilises, npm publication via dnt (Deno-to-Node) will follow.
17.8. Regenerating the proto stubs
The buf pipeline lives at the repo root (buf.yaml + buf.gen.yaml). The SDK’s generated/ is one of two output targets (the other being the orchestrator’s src/gen/). Regenerate after any change to proto/eigenius.proto:
npx --yes @bufbuild/buf generateBuf is pinned at v2; both targets emit protoc-gen-es target=ts, which produces idiomatic Connect-Web TypeScript stubs.
17.9. Smoke test
The smoke test at clients/eigenius-ts/examples/smoke-test.ts exercises the core RPCs against a live stack:
cd clients/eigenius-tsdeno run --allow-net --allow-env examples/smoke-test.ts# expects http://localhost:8080 by default; override with EIGENIUS_ORCHESTRATOR=...Output is an 8-step transcript (TOTAL_STEPS = 8): health → inspect → query → listInstitutions → layerTopology (taxonomy + full) → load → validateProgram → branch CRUD + per-call routing. This is the SDK’s Phase 1 acceptance criterion (D22 §5.7).
17.10. Design references
- D22 — Notebook UX and TypeScript SDK — full SDK + notebook spec, including the layer-topology data shape and the planned post-MVP additions
- D5 — gRPC API specification — the underlying RPC surface (the SDK is a thin wrapper)
- chapter 14 — Notebook — the SDK’s largest consumer
Next: 18. Appendix →