This document defines the JS/TS contract between @oh-my-pi/pi-natives callers and the loaded N-API addon.

Current package shape is direct-to-native: there is no packages/natives/src/<module> TypeScript wrapper layer. The public API is the generated packages/natives/native/index.d.ts declaration file, the ESM loader/export wrapper in packages/natives/native/index.js, and the Rust #[napi] exports in crates/pi-natives/src.

Implementation files

  • packages/natives/native/index.js
  • packages/natives/native/index.d.ts
  • packages/natives/native/loader-state.js
  • packages/natives/scripts/build-native.ts
  • packages/natives/scripts/gen-enums.ts
  • packages/natives/package.json
  • crates/pi-natives/src/lib.rs
  • Rust modules under crates/pi-natives/src/*.rs

Contract model

The contract has three parts:

  1. ESM runtime loader/export wrapper (native/index.js)
    • calls loadNative() from loader-state.js, which require(...)s the .node addon;
    • binds generated classes/functions as explicit named ESM exports;
    • emits enum runtime objects generated by scripts/gen-enums.ts.
  2. Generated TypeScript declarations (native/index.d.ts)
    • generated by napi-rs during scripts/build-native.ts;
    • declares exported functions, classes, object interfaces, and native enums;
    • is the package types entry.
  3. Rust N-API exports (crates/pi-natives/src)
    • #[napi] functions/classes/objects/enums are the source of generated declarations and runtime symbols;
    • snake_case Rust names become camelCase JavaScript names by napi-rs convention.

There is no current NativeBindings declaration-merging lifecycle and no full required-export list in the loader. Install/compiled loads do validate the package-version sentinel export; workspace-dev loads skip that check.

Public export surface organization

packages/natives/package.json exposes the package root only:

{
  "main": "./native/index.js",
  "types": "./native/index.d.ts",
  "exports": {
    ".": {
      "types": "./native/index.d.ts",
      "import": "./native/index.js"
    }
  }
}

Consumers in packages/coding-agent and packages/tui import directly from @oh-my-pi/pi-natives.

JS API ↔ native export mapping (representative)

CategoryPublic JS APIRust sourceReturn style
Grepgrep(options, onMatch?)grep.rsPromise<GrepResult>
Grepsearch(content, options)grep.rsSearchResult
GrephasMatch(content, pattern, ignoreCase?, multiline?)grep.rsboolean
Fuzzy path searchfuzzyFind(options)fd.rsPromise<FuzzyFindResult>
Glob/workspaceglob(options, onMatch?), listWorkspace(options)glob.rs, workspace.rsPromise<...>
Glob cacheinvalidateFsScanCache(path?)fs_cache.rsvoid
AST/block/summaryastGrep(options), astMatch(options), astEdit(options), blockRangeAt(options), enclosingBlockBoundaries(options), summarizeCode(options)ast.rs, block.rs, summary.rsmixed
ShellexecuteShell(options, onChunk?)shell.rsPromise<ShellRunResult>
Shellnew Shell(options?), shell.run(...), shell.abort()shell.rsclass / promises
ShellapplyBashFixups(command)shell.rsBashFixupResult
PTYnew PtySession(), start/write/resize/killpty.rsclass / promises
ProcessProcess.fromPid/fromPath, status/children/killTree/terminate/waitForExitps.rsclass / mixed
KeysparseKey, matchesKey, Kitty/legacy helperskeys.rssync
TextwrapTextWithAnsi, truncateToWidth, sliceWithWidth, extractSegments, visibleWidthtext.rssync
HighlighthighlightCode, supportsLanguage, getSupportedLanguageshighlight.rssync
HTMLhtmlToMarkdown(html, options?)html.rsPromise<string>
SIXELencodeSixelsixel.rssync
SnapcompactrenderSnapcompactPng(text, options)snapcompact.rssync
ClipboardcopyToClipboard, readImageFromClipboardclipboard.rssync / promise
TokenscountTokens(input, encoding?)tokens.rssync
System/isolationdetectMacOSAppearance, MacAppearanceObserver, MacOSPowerAssertion, getWorkProfile, iso* helpersappearance.rs, power.rs, prof.rs, iso.rsmixed

Sync vs async contract differences

The contract preserves Rust/N-API call style:

  • Promise-returning exports for worker-thread or async runtime work (grep, glob, fuzzyFind, astGrep, astMatch, astEdit, htmlToMarkdown, shell/PTY runs, isoStart/isoStop/isoDiff, clipboard image read, workspace scan).
  • Synchronous exports for deterministic in-memory transforms/parsers or direct system calls (search, hasMatch, highlighting, text utilities, token counting, process construction/status, copyToClipboard, encodeSixel, isolation probe/resolve helpers).
  • Constructor exports for stateful runtime objects (Shell, PtySession, Process, macOS observer/power handles).

Changing sync ↔ async for an existing export is a breaking public API change because consumers call these exports directly.

Object and enum typing patterns

Object patterns

#[napi(object)] Rust structs become TS interfaces, for example:

  • GrepResult, SearchResult, GlobResult, FuzzyFindResult
  • ShellRunResult, PtyRunResult, MinimizerResult
  • AstFindResult, AstReplaceResult, BlockRange, SummaryResult
  • System/media/isolation payloads such as ClipboardImage, WorkProfile, ParsedKittyResult, IsoResolveResult

Runtime shape correctness is owned by napi-rs and the Rust implementation.

Enum patterns

Native enums are represented in generated declarations and also emitted as runtime objects by scripts/gen-enums.ts, because napi-rs string enums are TS-only without explicit JS exports. Current enum objects include:

  • AstMatchStrictness
  • Ellipsis
  • Encoding
  • FileType
  • GrepOutputMode
  • IsoBackendKind
  • IsoChangeKind
  • KeyEventType
  • MacOSAppearance
  • ProcessStatus

Error behavior and caveats

  • Addon load failure or unsupported platform throws during package import from native/index.js.
  • The loader rejects install/compiled candidates that lack the package-version sentinel export. It does not verify the full export set after require(...); stale same-version or incomplete binaries surface as native load errors or missing members at use sites.
  • N-API conversion validates basic argument conversion, but TS optional fields do not guarantee semantic validity for untyped callers.
  • Numeric enum declarations do not prevent out-of-range numeric values from untyped callers unless the Rust function rejects them during conversion.
  • Callback exports use napi-rs ThreadsafeFunction shape: (error: Error | null, value) => void. Native code generally emits successful values; hard failures reject/throw through the owning call.

Maintainer checklist for binding changes

When adding/changing an export, update all of:

  1. Rust #[napi] implementation in the owning crates/pi-natives/src/<module>.rs.
  2. crates/pi-natives/src/lib.rs if a new module is added.
  3. Any consumer imports/callsites in packages/coding-agent or packages/tui.
  4. Build output by running the natives build so native/index.d.ts and native/index.js stay in sync.
  5. scripts/gen-enums.ts if enum runtime export patching needs to change.

Do not add a parallel TS wrapper convention unless the package design intentionally moves back to wrappers; current consumers depend on the direct generated API.