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.jspackages/natives/native/index.d.tspackages/natives/native/loader-state.jspackages/natives/scripts/build-native.tspackages/natives/scripts/gen-enums.tspackages/natives/package.jsoncrates/pi-natives/src/lib.rs- Rust modules under
crates/pi-natives/src/*.rs
Contract model
The contract has three parts:
- ESM runtime loader/export wrapper (
native/index.js)- calls
loadNative()fromloader-state.js, whichrequire(...)s the.nodeaddon; - binds generated classes/functions as explicit named ESM exports;
- emits enum runtime objects generated by
scripts/gen-enums.ts.
- calls
- 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
typesentry.
- generated by napi-rs during
- 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)
| Category | Public JS API | Rust source | Return style |
|---|---|---|---|
| Grep | grep(options, onMatch?) | grep.rs | Promise<GrepResult> |
| Grep | search(content, options) | grep.rs | SearchResult |
| Grep | hasMatch(content, pattern, ignoreCase?, multiline?) | grep.rs | boolean |
| Fuzzy path search | fuzzyFind(options) | fd.rs | Promise<FuzzyFindResult> |
| Glob/workspace | glob(options, onMatch?), listWorkspace(options) | glob.rs, workspace.rs | Promise<...> |
| Glob cache | invalidateFsScanCache(path?) | fs_cache.rs | void |
| AST/block/summary | astGrep(options), astMatch(options), astEdit(options), blockRangeAt(options), enclosingBlockBoundaries(options), summarizeCode(options) | ast.rs, block.rs, summary.rs | mixed |
| Shell | executeShell(options, onChunk?) | shell.rs | Promise<ShellRunResult> |
| Shell | new Shell(options?), shell.run(...), shell.abort() | shell.rs | class / promises |
| Shell | applyBashFixups(command) | shell.rs | BashFixupResult |
| PTY | new PtySession(), start/write/resize/kill | pty.rs | class / promises |
| Process | Process.fromPid/fromPath, status/children/killTree/terminate/waitForExit | ps.rs | class / mixed |
| Keys | parseKey, matchesKey, Kitty/legacy helpers | keys.rs | sync |
| Text | wrapTextWithAnsi, truncateToWidth, sliceWithWidth, extractSegments, visibleWidth | text.rs | sync |
| Highlight | highlightCode, supportsLanguage, getSupportedLanguages | highlight.rs | sync |
| HTML | htmlToMarkdown(html, options?) | html.rs | Promise<string> |
| SIXEL | encodeSixel | sixel.rs | sync |
| Snapcompact | renderSnapcompactPng(text, options) | snapcompact.rs | sync |
| Clipboard | copyToClipboard, readImageFromClipboard | clipboard.rs | sync / promise |
| Tokens | countTokens(input, encoding?) | tokens.rs | sync |
| System/isolation | detectMacOSAppearance, MacAppearanceObserver, MacOSPowerAssertion, getWorkProfile, iso* helpers | appearance.rs, power.rs, prof.rs, iso.rs | mixed |
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,FuzzyFindResultShellRunResult,PtyRunResult,MinimizerResultAstFindResult,AstReplaceResult,BlockRange,SummaryResultSystem/media/isolation payloads such asClipboardImage,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:
AstMatchStrictnessEllipsisEncodingFileTypeGrepOutputModeIsoBackendKindIsoChangeKindKeyEventTypeMacOSAppearanceProcessStatus
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
ThreadsafeFunctionshape:(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:
- Rust
#[napi]implementation in the owningcrates/pi-natives/src/<module>.rs. crates/pi-natives/src/lib.rsif a new module is added.- Any consumer imports/callsites in
packages/coding-agentorpackages/tui. - Build output by running the natives build so
native/index.d.tsandnative/index.jsstay in sync. scripts/gen-enums.tsif 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.